mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-27 19:12:40 +00:00
@@ -0,0 +1,678 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_mac.h"
|
||||
|
||||
/* BLE */
|
||||
#include "bleprph.h"
|
||||
#include "console/console.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/util/util.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
static uint8_t ext_adv_pattern_1[] = {
|
||||
0x02, 0x01, 0x06, 0x03, 0x03, 0xab, 0xcd, 0x03, 0x03, 0x18, 0x11, 0x11, 0X09, 'n', 'i',
|
||||
'm', 'b', 'l', 'e', '-', 'b', 'l', 'e', 'p', 'r', 'p', 'h', '-', 'e',
|
||||
};
|
||||
#endif
|
||||
|
||||
static const char *tag = "NimBLE_BLE_PRPH";
|
||||
static int bleprph_gap_event(struct ble_gap_event *event, void *arg);
|
||||
#if CONFIG_EXAMPLE_RANDOM_ADDR
|
||||
static uint8_t own_addr_type = BLE_OWN_ADDR_RANDOM;
|
||||
#else
|
||||
static uint8_t own_addr_type;
|
||||
#endif
|
||||
|
||||
#if MYNEWT_VAL(BLE_EATT_CHAN_NUM) > 0
|
||||
static uint16_t cids[MYNEWT_VAL(BLE_EATT_CHAN_NUM)];
|
||||
static uint16_t bearers;
|
||||
#endif
|
||||
|
||||
void ble_store_config_init(void);
|
||||
|
||||
#if NIMBLE_BLE_CONNECT
|
||||
/**
|
||||
* Logs information about a connection to the console.
|
||||
*/
|
||||
static void bleprph_print_conn_desc(struct ble_gap_conn_desc *desc)
|
||||
{
|
||||
MODLOG_DFLT(INFO, "handle=%d our_ota_addr_type=%d our_ota_addr=", desc->conn_handle, desc->our_ota_addr.type);
|
||||
print_addr(desc->our_ota_addr.val);
|
||||
MODLOG_DFLT(INFO, " our_id_addr_type=%d our_id_addr=", desc->our_id_addr.type);
|
||||
print_addr(desc->our_id_addr.val);
|
||||
MODLOG_DFLT(INFO, " peer_ota_addr_type=%d peer_ota_addr=", desc->peer_ota_addr.type);
|
||||
print_addr(desc->peer_ota_addr.val);
|
||||
MODLOG_DFLT(INFO, " peer_id_addr_type=%d peer_id_addr=", desc->peer_id_addr.type);
|
||||
print_addr(desc->peer_id_addr.val);
|
||||
MODLOG_DFLT(INFO,
|
||||
" conn_itvl=%d conn_latency=%d supervision_timeout=%d "
|
||||
"encrypted=%d authenticated=%d bonded=%d\n",
|
||||
desc->conn_itvl, desc->conn_latency, desc->supervision_timeout, desc->sec_state.encrypted,
|
||||
desc->sec_state.authenticated, desc->sec_state.bonded);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
/**
|
||||
* Enables advertising with the following parameters:
|
||||
* o General discoverable mode.
|
||||
* o Undirected connectable mode.
|
||||
*/
|
||||
static void ext_bleprph_advertise(void)
|
||||
{
|
||||
struct ble_gap_ext_adv_params params;
|
||||
struct os_mbuf *data;
|
||||
uint8_t instance = 0;
|
||||
int rc;
|
||||
|
||||
/* First check if any instance is already active */
|
||||
if (ble_gap_ext_adv_active(instance)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* use defaults for non-set params */
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
|
||||
/* enable connectable advertising */
|
||||
params.connectable = 1;
|
||||
|
||||
/* advertise using random addr */
|
||||
params.own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
||||
|
||||
params.primary_phy = BLE_HCI_LE_PHY_1M;
|
||||
params.secondary_phy = BLE_HCI_LE_PHY_2M;
|
||||
// params.tx_power = 127;
|
||||
params.sid = 1;
|
||||
|
||||
params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
|
||||
/* configure instance 0 */
|
||||
rc = ble_gap_ext_adv_configure(instance, ¶ms, NULL, bleprph_gap_event, NULL);
|
||||
assert(rc == 0);
|
||||
|
||||
/* in this case only scan response is allowed */
|
||||
|
||||
/* get mbuf for scan rsp data */
|
||||
data = os_msys_get_pkthdr(sizeof(ext_adv_pattern_1), 0);
|
||||
assert(data);
|
||||
|
||||
/* fill mbuf with scan rsp data */
|
||||
rc = os_mbuf_append(data, ext_adv_pattern_1, sizeof(ext_adv_pattern_1));
|
||||
assert(rc == 0);
|
||||
|
||||
rc = ble_gap_ext_adv_set_data(instance, data);
|
||||
assert(rc == 0);
|
||||
|
||||
/* start advertising */
|
||||
rc = ble_gap_ext_adv_start(instance, 0, 0);
|
||||
assert(rc == 0);
|
||||
}
|
||||
#else
|
||||
|
||||
// /**
|
||||
// * Enables advertising with the following parameters:
|
||||
// * o General discoverable mode.
|
||||
// * o Undirected connectable mode.
|
||||
// */
|
||||
// static void bleprph_advertise(void)
|
||||
// {
|
||||
// struct ble_gap_adv_params adv_params;
|
||||
// struct ble_hs_adv_fields fields;
|
||||
// #if CONFIG_BT_NIMBLE_GAP_SERVICE
|
||||
// const char *name;
|
||||
// #endif
|
||||
// int rc;
|
||||
|
||||
// /**
|
||||
// * Set the advertisement data included in our advertisements:
|
||||
// * o Flags (indicates advertisement type and other general info).
|
||||
// * o Advertising tx power.
|
||||
// * o Device name.
|
||||
// * o 16-bit service UUIDs (alert notifications).
|
||||
// */
|
||||
|
||||
// memset(&fields, 0, sizeof fields);
|
||||
|
||||
// /* Advertise two flags:
|
||||
// * o Discoverability in forthcoming advertisement (general)
|
||||
// * o BLE-only (BR/EDR unsupported).
|
||||
// */
|
||||
// fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
// /* Indicate that the TX power level field should be included; have the
|
||||
// * stack fill this value automatically. This is done by assigning the
|
||||
// * special value BLE_HS_ADV_TX_PWR_LVL_AUTO.
|
||||
// */
|
||||
// fields.tx_pwr_lvl_is_present = 1;
|
||||
// fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
|
||||
// #if CONFIG_BT_NIMBLE_GAP_SERVICE
|
||||
// name = ble_svc_gap_device_name();
|
||||
// fields.name = (uint8_t *)name;
|
||||
// fields.name_len = strlen(name);
|
||||
// fields.name_is_complete = 1;
|
||||
// #endif
|
||||
|
||||
// fields.uuids16 = (ble_uuid16_t[]){BLE_UUID16_INIT(GATT_SVR_SVC_ALERT_UUID)};
|
||||
// fields.num_uuids16 = 1;
|
||||
// fields.uuids16_is_complete = 1;
|
||||
|
||||
// rc = ble_gap_adv_set_fields(&fields);
|
||||
// if (rc != 0) {
|
||||
// MODLOG_DFLT(ERROR, "error setting advertisement data; rc=%d\n", rc);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// /* Begin advertising. */
|
||||
// memset(&adv_params, 0, sizeof adv_params);
|
||||
// adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
// adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
// rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params, bleprph_gap_event,
|
||||
// NULL); if (rc != 0) {
|
||||
// MODLOG_DFLT(ERROR, "error enabling advertisement; rc=%d\n", rc);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
static void bleprph_advertise(void)
|
||||
{
|
||||
struct ble_gap_adv_params adv_params;
|
||||
struct ble_hs_adv_fields fields;
|
||||
struct ble_hs_adv_fields rsp_fields;
|
||||
int rc;
|
||||
|
||||
// 获取MAC地址
|
||||
uint8_t mac[6];
|
||||
esp_read_mac(mac, ESP_MAC_EFUSE_FACTORY);
|
||||
|
||||
// 厂商数据
|
||||
// uint8_t mfg_data[10];
|
||||
uint8_t mfg_data[8];
|
||||
memset(mfg_data, 0, sizeof(mfg_data));
|
||||
|
||||
mfg_data[0] = 0xE5;
|
||||
mfg_data[1] = 0x02;
|
||||
memcpy(&mfg_data[2], mac, 6);
|
||||
// mfg_data[8] = 0x01;
|
||||
// mfg_data[9] = 0x10;
|
||||
|
||||
// ========== 广播包:只放必要信息 ==========
|
||||
memset(&fields, 0, sizeof fields);
|
||||
|
||||
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
// 只放UUID
|
||||
// fields.uuids16 = (ble_uuid16_t[]){BLE_UUID16_INIT(GATT_SVR_SVC_ALERT_UUID)};
|
||||
// fields.num_uuids16 = 1;
|
||||
// fields.uuids16_is_complete = 1;
|
||||
|
||||
ble_uuid128_t stackchan_uuid = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE);
|
||||
fields.uuids128 = &stackchan_uuid;
|
||||
fields.num_uuids128 = 1;
|
||||
fields.uuids128_is_complete = 1;
|
||||
|
||||
rc = ble_gap_adv_set_fields(&fields);
|
||||
if (rc != 0) {
|
||||
MODLOG_DFLT(ERROR, "error setting advertisement data; rc=%d\n", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
// ========== 扫描响应包:放详细信息 ==========
|
||||
memset(&rsp_fields, 0, sizeof rsp_fields);
|
||||
|
||||
#if CONFIG_BT_NIMBLE_GAP_SERVICE
|
||||
const char *name = ble_svc_gap_device_name();
|
||||
rsp_fields.name = (uint8_t *)name;
|
||||
rsp_fields.name_len = strlen(name);
|
||||
rsp_fields.name_is_complete = 1;
|
||||
#endif
|
||||
|
||||
// TX Power放在扫描响应
|
||||
rsp_fields.tx_pwr_lvl_is_present = 1;
|
||||
rsp_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
|
||||
// 厂商数据放在扫描响应
|
||||
rsp_fields.mfg_data = mfg_data;
|
||||
rsp_fields.mfg_data_len = sizeof(mfg_data);
|
||||
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp_fields);
|
||||
if (rc != 0) {
|
||||
MODLOG_DFLT(ERROR, "error setting scan response data; rc=%d\n", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动广播
|
||||
memset(&adv_params, 0, sizeof adv_params);
|
||||
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params, bleprph_gap_event, NULL);
|
||||
if (rc != 0) {
|
||||
MODLOG_DFLT(ERROR, "error enabling advertisement; rc=%d\n", rc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if MYNEWT_VAL(BLE_POWER_CONTROL)
|
||||
static void bleprph_power_control(uint16_t conn_handle)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = ble_gap_read_remote_transmit_power_level(conn_handle, 0x01); // Attempting on LE 1M phy
|
||||
assert(rc == 0);
|
||||
|
||||
rc = ble_gap_set_transmit_power_reporting_enable(conn_handle, 0x1, 0x1);
|
||||
assert(rc == 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The nimble host executes this callback when a GAP event occurs. The
|
||||
* application associates a GAP event callback with each connection that forms.
|
||||
* bleprph uses the same callback for all connections.
|
||||
*
|
||||
* @param event The type of event being signalled.
|
||||
* @param ctxt Various information pertaining to the event.
|
||||
* @param arg Application-specified argument; unused by
|
||||
* bleprph.
|
||||
*
|
||||
* @return 0 if the application successfully handled the
|
||||
* event; nonzero on failure. The semantics
|
||||
* of the return code is specific to the
|
||||
* particular GAP event being signalled.
|
||||
*/
|
||||
static int bleprph_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
#if NIMBLE_BLE_CONNECT
|
||||
struct ble_gap_conn_desc desc;
|
||||
int rc;
|
||||
#endif
|
||||
|
||||
switch (event->type) {
|
||||
#if NIMBLE_BLE_CONNECT
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
/* A new connection was established or a connection attempt failed. */
|
||||
MODLOG_DFLT(INFO, "connection %s; status=%d ", event->connect.status == 0 ? "established" : "failed",
|
||||
event->connect.status);
|
||||
if (event->connect.status == 0) {
|
||||
rc = ble_gap_conn_find(event->connect.conn_handle, &desc);
|
||||
assert(rc == 0);
|
||||
bleprph_print_conn_desc(&desc);
|
||||
stackchan_ble_set_conn_handle(event->connect.conn_handle);
|
||||
}
|
||||
MODLOG_DFLT(INFO, "\n");
|
||||
|
||||
if (event->connect.status != 0) {
|
||||
/* Connection failed; resume advertising. */
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
ext_bleprph_advertise();
|
||||
#else
|
||||
bleprph_advertise();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MYNEWT_VAL(BLE_POWER_CONTROL)
|
||||
bleprph_power_control(event->connect.conn_handle);
|
||||
#endif
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
MODLOG_DFLT(INFO, "disconnect; reason=%d ", event->disconnect.reason);
|
||||
bleprph_print_conn_desc(&event->disconnect.conn);
|
||||
stackchan_ble_set_conn_handle(BLE_HS_CONN_HANDLE_NONE);
|
||||
MODLOG_DFLT(INFO, "\n");
|
||||
|
||||
/* Connection terminated; resume advertising. */
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
ext_bleprph_advertise();
|
||||
#else
|
||||
bleprph_advertise();
|
||||
#endif
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_CONN_UPDATE:
|
||||
/* The central has updated the connection parameters. */
|
||||
MODLOG_DFLT(INFO, "connection updated; status=%d ", event->conn_update.status);
|
||||
rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc);
|
||||
assert(rc == 0);
|
||||
bleprph_print_conn_desc(&desc);
|
||||
MODLOG_DFLT(INFO, "\n");
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
MODLOG_DFLT(INFO, "advertise complete; reason=%d", event->adv_complete.reason);
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
ext_bleprph_advertise();
|
||||
#else
|
||||
bleprph_advertise();
|
||||
#endif
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_ENC_CHANGE:
|
||||
/* Encryption has been enabled or disabled for this connection. */
|
||||
MODLOG_DFLT(INFO, "encryption change event; status=%d ", event->enc_change.status);
|
||||
rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc);
|
||||
assert(rc == 0);
|
||||
bleprph_print_conn_desc(&desc);
|
||||
MODLOG_DFLT(INFO, "\n");
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_NOTIFY_TX:
|
||||
MODLOG_DFLT(INFO,
|
||||
"notify_tx event; conn_handle=%d attr_handle=%d "
|
||||
"status=%d is_indication=%d",
|
||||
event->notify_tx.conn_handle, event->notify_tx.attr_handle, event->notify_tx.status,
|
||||
event->notify_tx.indication);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
MODLOG_DFLT(INFO,
|
||||
"subscribe event; conn_handle=%d attr_handle=%d "
|
||||
"reason=%d prevn=%d curn=%d previ=%d curi=%d\n",
|
||||
event->subscribe.conn_handle, event->subscribe.attr_handle, event->subscribe.reason,
|
||||
event->subscribe.prev_notify, event->subscribe.cur_notify, event->subscribe.prev_indicate,
|
||||
event->subscribe.cur_indicate);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_MTU:
|
||||
MODLOG_DFLT(INFO, "mtu update event; conn_handle=%d cid=%d mtu=%d\n", event->mtu.conn_handle,
|
||||
event->mtu.channel_id, event->mtu.value);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_REPEAT_PAIRING:
|
||||
/* We already have a bond with the peer, but it is attempting to
|
||||
* establish a new secure link. This app sacrifices security for
|
||||
* convenience: just throw away the old bond and accept the new link.
|
||||
*/
|
||||
|
||||
/* Delete the old bond. */
|
||||
rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
||||
assert(rc == 0);
|
||||
ble_store_util_delete_peer(&desc.peer_id_addr);
|
||||
|
||||
/* Return BLE_GAP_REPEAT_PAIRING_RETRY to indicate that the host should
|
||||
* continue with the pairing operation.
|
||||
*/
|
||||
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
||||
|
||||
case BLE_GAP_EVENT_PASSKEY_ACTION:
|
||||
ESP_LOGI(tag, "PASSKEY_ACTION_EVENT started");
|
||||
struct ble_sm_io pkey = {0};
|
||||
int key = 0;
|
||||
|
||||
if (event->passkey.params.action == BLE_SM_IOACT_DISP) {
|
||||
pkey.action = event->passkey.params.action;
|
||||
pkey.passkey = 123456; // This is the passkey to be entered on peer
|
||||
ESP_LOGI(tag, "Enter passkey %" PRIu32 "on the peer side", pkey.passkey);
|
||||
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc);
|
||||
} else if (event->passkey.params.action == BLE_SM_IOACT_NUMCMP) {
|
||||
ESP_LOGI(tag, "Passkey on device's display: %" PRIu32, event->passkey.params.numcmp);
|
||||
ESP_LOGI(tag,
|
||||
"Accept or reject the passkey through console in this "
|
||||
"format -> key Y or key N");
|
||||
pkey.action = event->passkey.params.action;
|
||||
if (scli_receive_key(&key)) {
|
||||
pkey.numcmp_accept = key;
|
||||
} else {
|
||||
pkey.numcmp_accept = 0;
|
||||
ESP_LOGE(tag, "Timeout! Rejecting the key");
|
||||
}
|
||||
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc);
|
||||
} else if (event->passkey.params.action == BLE_SM_IOACT_OOB) {
|
||||
static uint8_t tem_oob[16] = {0};
|
||||
pkey.action = event->passkey.params.action;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
pkey.oob[i] = tem_oob[i];
|
||||
}
|
||||
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc);
|
||||
} else if (event->passkey.params.action == BLE_SM_IOACT_INPUT) {
|
||||
ESP_LOGI(tag, "Enter the passkey through console in this format-> key 123456");
|
||||
pkey.action = event->passkey.params.action;
|
||||
if (scli_receive_key(&key)) {
|
||||
pkey.passkey = key;
|
||||
} else {
|
||||
pkey.passkey = 0;
|
||||
ESP_LOGE(tag, "Timeout! Passing 0 as the key");
|
||||
}
|
||||
rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey);
|
||||
ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_AUTHORIZE:
|
||||
MODLOG_DFLT(INFO, "authorize event: conn_handle=%d attr_handle=%d is_read=%d", event->authorize.conn_handle,
|
||||
event->authorize.attr_handle, event->authorize.is_read);
|
||||
|
||||
/* The default behaviour for the event is to reject authorize request */
|
||||
event->authorize.out_response = BLE_GAP_AUTHORIZE_REJECT;
|
||||
return 0;
|
||||
|
||||
#if MYNEWT_VAL(BLE_POWER_CONTROL)
|
||||
case BLE_GAP_EVENT_TRANSMIT_POWER:
|
||||
MODLOG_DFLT(INFO,
|
||||
"Transmit power event : status=%d conn_handle=%d reason=%d "
|
||||
"phy=%d power_level=%x power_level_flag=%d delta=%d",
|
||||
event->transmit_power.status, event->transmit_power.conn_handle, event->transmit_power.reason,
|
||||
event->transmit_power.phy, event->transmit_power.transmit_power_level,
|
||||
event->transmit_power.transmit_power_level_flag, event->transmit_power.delta);
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_PATHLOSS_THRESHOLD:
|
||||
MODLOG_DFLT(INFO,
|
||||
"Pathloss threshold event : conn_handle=%d current path loss=%d "
|
||||
"zone_entered =%d",
|
||||
event->pathloss_threshold.conn_handle, event->pathloss_threshold.current_path_loss,
|
||||
event->pathloss_threshold.zone_entered);
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
#if MYNEWT_VAL(BLE_EATT_CHAN_NUM) > 0
|
||||
case BLE_GAP_EVENT_EATT:
|
||||
MODLOG_DFLT(INFO, "EATT %s : conn_handle=%d cid=%d", event->eatt.status ? "disconnected" : "connected",
|
||||
event->eatt.conn_handle, event->eatt.cid);
|
||||
if (event->eatt.status) {
|
||||
/* Abort if disconnected */
|
||||
return 0;
|
||||
}
|
||||
cids[bearers] = event->eatt.cid;
|
||||
bearers += 1;
|
||||
if (bearers != MYNEWT_VAL(BLE_EATT_CHAN_NUM)) {
|
||||
/* Wait until all EATT bearers are connected before proceeding */
|
||||
return 0;
|
||||
}
|
||||
/* Set the default bearer to use for further procedures */
|
||||
rc = ble_att_set_default_bearer_using_cid(event->eatt.conn_handle, cids[0]);
|
||||
if (rc != 0) {
|
||||
MODLOG_DFLT(INFO, "Cannot set default EATT bearer, rc = %d\n", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
#if MYNEWT_VAL(BLE_CONN_SUBRATING)
|
||||
case BLE_GAP_EVENT_SUBRATE_CHANGE:
|
||||
MODLOG_DFLT(INFO, "Subrate change event : conn_handle=%d status=%d factor=%d",
|
||||
event->subrate_change.conn_handle, event->subrate_change.status,
|
||||
event->subrate_change.subrate_factor);
|
||||
return 0;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void bleprph_on_reset(int reason)
|
||||
{
|
||||
MODLOG_DFLT(ERROR, "Resetting state; reason=%d\n", reason);
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_RANDOM_ADDR
|
||||
static void ble_app_set_addr(void)
|
||||
{
|
||||
ble_addr_t addr;
|
||||
int rc;
|
||||
|
||||
/* generate new non-resolvable private address */
|
||||
rc = ble_hs_id_gen_rnd(0, &addr);
|
||||
assert(rc == 0);
|
||||
|
||||
/* set generated address */
|
||||
rc = ble_hs_id_set_rnd(addr.val);
|
||||
|
||||
assert(rc == 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void bleprph_on_sync(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
#if CONFIG_EXAMPLE_RANDOM_ADDR
|
||||
/* Generate a non-resolvable private address. */
|
||||
ble_app_set_addr();
|
||||
#endif
|
||||
|
||||
/* Make sure we have proper identity address set (public preferred) */
|
||||
#if CONFIG_EXAMPLE_RANDOM_ADDR
|
||||
rc = ble_hs_util_ensure_addr(1);
|
||||
#else
|
||||
rc = ble_hs_util_ensure_addr(0);
|
||||
#endif
|
||||
assert(rc == 0);
|
||||
|
||||
/* Figure out address to use while advertising (no privacy for now) */
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Printing ADDR */
|
||||
uint8_t addr_val[6] = {0};
|
||||
rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
|
||||
|
||||
MODLOG_DFLT(INFO, "Device Address: ");
|
||||
print_addr(addr_val);
|
||||
MODLOG_DFLT(INFO, "\n");
|
||||
/* Begin advertising. */
|
||||
#if CONFIG_EXAMPLE_EXTENDED_ADV
|
||||
ext_bleprph_advertise();
|
||||
#else
|
||||
bleprph_advertise();
|
||||
#endif
|
||||
}
|
||||
|
||||
void bleprph_host_task(void *param)
|
||||
{
|
||||
ESP_LOGI(tag, "BLE Host Task Started");
|
||||
/* This function will return only when nimble_port_stop() is executed */
|
||||
nimble_port_run();
|
||||
|
||||
nimble_port_freertos_deinit();
|
||||
}
|
||||
|
||||
void ble_prph_init(void)
|
||||
{
|
||||
int rc;
|
||||
esp_err_t ret;
|
||||
|
||||
// /* Initialize NVS — it is used to store PHY calibration data */
|
||||
// ret = nvs_flash_init();
|
||||
// if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
// ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
// ret = nvs_flash_init();
|
||||
// }
|
||||
// ESP_ERROR_CHECK(ret);
|
||||
|
||||
ret = nimble_port_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(tag, "Failed to init nimble %d ", ret);
|
||||
return;
|
||||
}
|
||||
/* Initialize the NimBLE host configuration. */
|
||||
ble_hs_cfg.reset_cb = bleprph_on_reset;
|
||||
ble_hs_cfg.sync_cb = bleprph_on_sync;
|
||||
ble_hs_cfg.gatts_register_cb = gatt_svr_register_cb;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
|
||||
ble_hs_cfg.sm_io_cap = CONFIG_EXAMPLE_IO_TYPE;
|
||||
#ifdef CONFIG_EXAMPLE_BONDING
|
||||
ble_hs_cfg.sm_bonding = 1;
|
||||
/* Enable the appropriate bit masks to make sure the keys
|
||||
* that are needed are exchanged
|
||||
*/
|
||||
ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC;
|
||||
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC;
|
||||
#endif
|
||||
#ifdef CONFIG_EXAMPLE_MITM
|
||||
ble_hs_cfg.sm_mitm = 1;
|
||||
#endif
|
||||
#ifdef CONFIG_EXAMPLE_USE_SC
|
||||
ble_hs_cfg.sm_sc = 1;
|
||||
#else
|
||||
ble_hs_cfg.sm_sc = 0;
|
||||
#endif
|
||||
#ifdef CONFIG_EXAMPLE_RESOLVE_PEER_ADDR
|
||||
/* Stores the IRK */
|
||||
ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ID;
|
||||
ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ID;
|
||||
#endif
|
||||
|
||||
#if MYNEWT_VAL(BLE_GATTS)
|
||||
rc = gatt_svr_init();
|
||||
assert(rc == 0);
|
||||
#endif
|
||||
|
||||
#if CONFIG_BT_NIMBLE_GAP_SERVICE
|
||||
/* Set the default device name. */
|
||||
rc = ble_svc_gap_device_name_set("Stack-Chan");
|
||||
assert(rc == 0);
|
||||
#endif
|
||||
|
||||
/* XXX Need to have template for store */
|
||||
ble_store_config_init();
|
||||
|
||||
nimble_port_freertos_init(bleprph_host_task);
|
||||
|
||||
/* Initialize command line interface to accept input from user */
|
||||
rc = scli_init();
|
||||
if (rc != ESP_OK) {
|
||||
ESP_LOGE(tag, "scli_init() failed");
|
||||
}
|
||||
|
||||
#if MYNEWT_VAL(BLE_EATT_CHAN_NUM) > 0
|
||||
bearers = 0;
|
||||
for (int i = 0; i < MYNEWT_VAL(BLE_EATT_CHAN_NUM); i++) {
|
||||
cids[i] = 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#ifndef H_BLEPRPH_
|
||||
#define H_BLEPRPH_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "nimble/ble.h"
|
||||
#include "modlog/modlog.h"
|
||||
#include "nimble_peripheral_utils/esp_peripheral.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct ble_hs_cfg;
|
||||
struct ble_gatt_register_ctxt;
|
||||
|
||||
/** GATT server. */
|
||||
#define GATT_SVR_SVC_ALERT_UUID 0x1811
|
||||
#define GATT_SVR_CHR_SUP_NEW_ALERT_CAT_UUID 0x2A47
|
||||
#define GATT_SVR_CHR_NEW_ALERT 0x2A46
|
||||
#define GATT_SVR_CHR_SUP_UNR_ALERT_CAT_UUID 0x2A48
|
||||
#define GATT_SVR_CHR_UNR_ALERT_STAT_UUID 0x2A45
|
||||
#define GATT_SVR_CHR_ALERT_NOT_CTRL_PT 0x2A44
|
||||
|
||||
/** Stack-Chan Service UUIDs */
|
||||
// Service UUID: e2e5e5e0-1234-5678-1234-56789abcdef0
|
||||
#define STACKCHAN_SVC_UUID_BASE \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe0, 0xe5, 0xe5, 0xe2
|
||||
|
||||
// Motion Characteristic UUID: e2e5e5e1-1234-5678-1234-56789abcdef0
|
||||
#define STACKCHAN_CHR_MOTION_UUID \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe1, 0xe5, 0xe5, 0xe2
|
||||
|
||||
// Avatar Characteristic UUID: e2e5e5e2-1234-5678-1234-56789abcdef0
|
||||
#define STACKCHAN_CHR_AVATAR_UUID \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe2, 0xe5, 0xe5, 0xe2
|
||||
|
||||
// Config Characteristic UUID: e2e5e5e3-1234-5678-1234-56789abcdef0
|
||||
#define STACKCHAN_CHR_CONFIG_UUID \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe3, 0xe5, 0xe5, 0xe2
|
||||
|
||||
// Animation Characteristic UUID: e2e5e5e4-1234-5678-1234-56789abcdef0
|
||||
#define STACKCHAN_CHR_ANIMATION_UUID \
|
||||
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0xe4, 0xe5, 0xe5, 0xe2
|
||||
|
||||
/** Maximum JSON payload size for Stack-Chan characteristics */
|
||||
#define STACKCHAN_MAX_JSON_LEN 2048
|
||||
|
||||
/**
|
||||
* Stack-Chan callback function types
|
||||
*
|
||||
* @param json_data Pointer to JSON string received
|
||||
* @param len Length of JSON string
|
||||
* @param conn_handle BLE connection handle
|
||||
* @return 0 on success, error code otherwise
|
||||
*/
|
||||
typedef int (*stackchan_ble_motion_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
|
||||
typedef int (*stackchan_ble_avatar_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
|
||||
typedef int (*stackchan_ble_config_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
|
||||
typedef int (*stackchan_ble_animation_callback_t)(const char *json_data, uint16_t len, uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* Battery level callback function type
|
||||
*
|
||||
* @return Battery level (0-100)
|
||||
*/
|
||||
typedef uint8_t (*stackchan_ble_battery_read_callback_t)(void);
|
||||
|
||||
/**
|
||||
* Stack-Chan callback configuration structure
|
||||
*/
|
||||
typedef struct {
|
||||
stackchan_ble_motion_callback_t motion_cb;
|
||||
stackchan_ble_avatar_callback_t avatar_cb;
|
||||
stackchan_ble_config_callback_t config_cb;
|
||||
stackchan_ble_animation_callback_t animation_cb;
|
||||
stackchan_ble_battery_read_callback_t battery_read_cb;
|
||||
} stackchan_ble_callbacks_t;
|
||||
|
||||
/**
|
||||
* Register Stack-Chan service callbacks
|
||||
*
|
||||
* @param callbacks Pointer to callbacks structure
|
||||
*/
|
||||
void stackchan_ble_register_callbacks(const stackchan_ble_callbacks_t *callbacks);
|
||||
|
||||
/**
|
||||
* Send motion data notification to connected client
|
||||
*
|
||||
* @param json_data JSON string to send
|
||||
* @param len Length of JSON string
|
||||
* @return 0 on success, error code otherwise
|
||||
*/
|
||||
int stackchan_ble_notify_motion(const char *json_data, uint16_t len);
|
||||
|
||||
/**
|
||||
* Send avatar data notification to connected client
|
||||
*
|
||||
* @param json_data JSON string to send
|
||||
* @param len Length of JSON string
|
||||
* @return 0 on success, error code otherwise
|
||||
*/
|
||||
int stackchan_ble_notify_avatar(const char *json_data, uint16_t len);
|
||||
|
||||
/**
|
||||
* Send config data notification to connected client
|
||||
*
|
||||
* @param json_data JSON string to send
|
||||
* @param len Length of JSON string
|
||||
* @return 0 on success, error code otherwise
|
||||
*/
|
||||
int stackchan_ble_notify_config(const char *json_data, uint16_t len);
|
||||
|
||||
/**
|
||||
* Send animation data notification to connected client
|
||||
*
|
||||
* @param json_data JSON string to send
|
||||
* @param len Length of JSON string
|
||||
* @return 0 on success, error code otherwise
|
||||
*/
|
||||
int stackchan_ble_notify_animation(const char *json_data, uint16_t len);
|
||||
|
||||
/**
|
||||
* Update battery level and notify if subscribed
|
||||
*
|
||||
* @param level Battery level (0-100)
|
||||
* @return 0 on success, error code otherwise
|
||||
*/
|
||||
int stackchan_ble_update_battery_level(uint8_t level);
|
||||
|
||||
/**
|
||||
* Set BLE connection handle (called internally by GAP event handler)
|
||||
*
|
||||
* @param conn_handle BLE connection handle
|
||||
*/
|
||||
void stackchan_ble_set_conn_handle(uint16_t conn_handle);
|
||||
|
||||
/**
|
||||
* Get current BLE connection status
|
||||
*
|
||||
* @return true if connected, false otherwise
|
||||
*/
|
||||
bool stackchan_ble_is_connected(void);
|
||||
|
||||
void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg);
|
||||
int gatt_svr_init(void);
|
||||
|
||||
void ble_prph_init(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/ble_uuid.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
#include "services/gatt/ble_svc_gatt.h"
|
||||
#include "services/bas/ble_svc_bas.h"
|
||||
#include "bleprph.h"
|
||||
#include "services/ans/ble_svc_ans.h"
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
/*** Maximum number of characteristics with the notify flag ***/
|
||||
#define MAX_NOTIFY 5
|
||||
|
||||
/* Stack-Chan Service */
|
||||
static const ble_uuid128_t stackchan_svc_uuid = BLE_UUID128_INIT(STACKCHAN_SVC_UUID_BASE);
|
||||
|
||||
static const ble_uuid128_t stackchan_chr_motion_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_MOTION_UUID);
|
||||
|
||||
static const ble_uuid128_t stackchan_chr_avatar_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_AVATAR_UUID);
|
||||
|
||||
static const ble_uuid128_t stackchan_chr_config_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_CONFIG_UUID);
|
||||
|
||||
static const ble_uuid128_t stackchan_chr_animation_uuid = BLE_UUID128_INIT(STACKCHAN_CHR_ANIMATION_UUID);
|
||||
|
||||
/* Stack-Chan characteristic data buffers */
|
||||
static char *stackchan_motion_data = NULL;
|
||||
static uint16_t stackchan_motion_len = 0;
|
||||
static uint16_t stackchan_motion_handle;
|
||||
|
||||
static char *stackchan_avatar_data = NULL;
|
||||
static uint16_t stackchan_avatar_len = 0;
|
||||
static uint16_t stackchan_avatar_handle;
|
||||
|
||||
static char *stackchan_config_data = NULL;
|
||||
static uint16_t stackchan_config_len = 0;
|
||||
static uint16_t stackchan_config_handle;
|
||||
|
||||
static char *stackchan_animation_data = NULL;
|
||||
static uint16_t stackchan_animation_len = 0;
|
||||
static uint16_t stackchan_animation_handle;
|
||||
|
||||
/* Battery level */
|
||||
static uint8_t battery_level = 100;
|
||||
static uint16_t battery_level_handle;
|
||||
|
||||
/* Callback storage */
|
||||
static stackchan_ble_callbacks_t g_stackchan_callbacks = {0};
|
||||
|
||||
/* Connection handle for notifications */
|
||||
static uint16_t g_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
|
||||
static int gatt_svc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
|
||||
static int stackchan_svc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg);
|
||||
|
||||
static int battery_svc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
|
||||
|
||||
static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
|
||||
{
|
||||
/*** Stack-Chan Service ***/
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &stackchan_svc_uuid.u,
|
||||
.characteristics =
|
||||
(struct ble_gatt_chr_def[]){{
|
||||
/* Motion Characteristic - Read/Write/Notify */
|
||||
.uuid = &stackchan_chr_motion_uuid.u,
|
||||
.access_cb = stackchan_svc_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &stackchan_motion_handle,
|
||||
},
|
||||
{
|
||||
/* Avatar Characteristic - Read/Write/Notify */
|
||||
.uuid = &stackchan_chr_avatar_uuid.u,
|
||||
.access_cb = stackchan_svc_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &stackchan_avatar_handle,
|
||||
},
|
||||
{
|
||||
/* Config Characteristic - Read/Write/Notify */
|
||||
.uuid = &stackchan_chr_config_uuid.u,
|
||||
.access_cb = stackchan_svc_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &stackchan_config_handle,
|
||||
},
|
||||
{
|
||||
/* Animation Characteristic - Read/Write/Notify */
|
||||
.uuid = &stackchan_chr_animation_uuid.u,
|
||||
.access_cb = stackchan_svc_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &stackchan_animation_handle,
|
||||
},
|
||||
{
|
||||
0, /* No more characteristics */
|
||||
}},
|
||||
},
|
||||
|
||||
{
|
||||
/*** Battery Service (standard 0x180F) ***/
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = BLE_UUID16_DECLARE(0x180F),
|
||||
.characteristics = (struct ble_gatt_chr_def[]){{
|
||||
/* Battery Level Characteristic (standard 0x2A19) */
|
||||
.uuid = BLE_UUID16_DECLARE(0x2A19),
|
||||
.access_cb = battery_svc_access,
|
||||
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &battery_level_handle,
|
||||
},
|
||||
{
|
||||
0, /* No more characteristics */
|
||||
}},
|
||||
},
|
||||
|
||||
{
|
||||
0, /* No more services. */
|
||||
},
|
||||
};
|
||||
|
||||
static int gatt_svr_write(struct os_mbuf *om, uint16_t min_len, uint16_t max_len, void *dst, uint16_t *len)
|
||||
{
|
||||
uint16_t om_len;
|
||||
int rc;
|
||||
|
||||
om_len = OS_MBUF_PKTLEN(om);
|
||||
if (om_len < min_len || om_len > max_len) {
|
||||
MODLOG_DFLT(ERROR, "Invalid attribute value length: %d (expected %d-%d)", om_len, min_len, max_len);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
rc = ble_hs_mbuf_to_flat(om, dst, max_len, len);
|
||||
if (rc != 0) {
|
||||
MODLOG_DFLT(ERROR, "Failed to flatten mbuf: %d", rc);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack-Chan service access callback
|
||||
*/
|
||||
static int stackchan_svc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt,
|
||||
void *arg)
|
||||
{
|
||||
int rc;
|
||||
|
||||
/* Store connection handle for notifications */
|
||||
if (conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
g_conn_handle = conn_handle;
|
||||
}
|
||||
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR:
|
||||
MODLOG_DFLT(INFO, "Stack-Chan characteristic read; conn_handle=%d attr_handle=%d", conn_handle,
|
||||
attr_handle);
|
||||
|
||||
if (attr_handle == stackchan_motion_handle) {
|
||||
rc = os_mbuf_append(ctxt->om, stackchan_motion_data, stackchan_motion_len);
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
} else if (attr_handle == stackchan_avatar_handle) {
|
||||
rc = os_mbuf_append(ctxt->om, stackchan_avatar_data, stackchan_avatar_len);
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
} else if (attr_handle == stackchan_config_handle) {
|
||||
rc = os_mbuf_append(ctxt->om, stackchan_config_data, stackchan_config_len);
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
} else if (attr_handle == stackchan_animation_handle) {
|
||||
rc = os_mbuf_append(ctxt->om, stackchan_animation_data, stackchan_animation_len);
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_GATT_ACCESS_OP_WRITE_CHR:
|
||||
MODLOG_DFLT(INFO, "Stack-Chan characteristic write; conn_handle=%d attr_handle=%d", conn_handle,
|
||||
attr_handle);
|
||||
|
||||
if (attr_handle == stackchan_motion_handle) {
|
||||
rc = gatt_svr_write(ctxt->om, 0, STACKCHAN_MAX_JSON_LEN, stackchan_motion_data, &stackchan_motion_len);
|
||||
if (rc == 0) {
|
||||
stackchan_motion_data[stackchan_motion_len] = '\0';
|
||||
// MODLOG_DFLT(INFO, "Motion data received (%d bytes): %s", stackchan_motion_len,
|
||||
// stackchan_motion_data);
|
||||
|
||||
/* Call user callback if registered */
|
||||
if (g_stackchan_callbacks.motion_cb) {
|
||||
g_stackchan_callbacks.motion_cb(stackchan_motion_data, stackchan_motion_len, conn_handle);
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
} else if (attr_handle == stackchan_avatar_handle) {
|
||||
rc = gatt_svr_write(ctxt->om, 0, STACKCHAN_MAX_JSON_LEN, stackchan_avatar_data, &stackchan_avatar_len);
|
||||
if (rc == 0) {
|
||||
stackchan_avatar_data[stackchan_avatar_len] = '\0';
|
||||
// MODLOG_DFLT(INFO, "Avatar data received (%d bytes): %s", stackchan_avatar_len,
|
||||
// stackchan_avatar_data);
|
||||
|
||||
/* Call user callback if registered */
|
||||
if (g_stackchan_callbacks.avatar_cb) {
|
||||
g_stackchan_callbacks.avatar_cb(stackchan_avatar_data, stackchan_avatar_len, conn_handle);
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
} else if (attr_handle == stackchan_config_handle) {
|
||||
rc = gatt_svr_write(ctxt->om, 0, STACKCHAN_MAX_JSON_LEN, stackchan_config_data, &stackchan_config_len);
|
||||
if (rc == 0) {
|
||||
stackchan_config_data[stackchan_config_len] = '\0';
|
||||
MODLOG_DFLT(INFO, "Config data received (%d bytes): %s", stackchan_config_len,
|
||||
stackchan_config_data);
|
||||
|
||||
/* Call user callback if registered */
|
||||
if (g_stackchan_callbacks.config_cb) {
|
||||
g_stackchan_callbacks.config_cb(stackchan_config_data, stackchan_config_len, conn_handle);
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
} else if (attr_handle == stackchan_animation_handle) {
|
||||
rc = gatt_svr_write(ctxt->om, 0, STACKCHAN_MAX_JSON_LEN, stackchan_animation_data,
|
||||
&stackchan_animation_len);
|
||||
if (rc == 0) {
|
||||
stackchan_animation_data[stackchan_animation_len] = '\0';
|
||||
MODLOG_DFLT(INFO, "Animation data received (%d bytes): %s", stackchan_animation_len,
|
||||
stackchan_animation_data);
|
||||
|
||||
/* Call user callback if registered */
|
||||
if (g_stackchan_callbacks.animation_cb) {
|
||||
g_stackchan_callbacks.animation_cb(stackchan_animation_data, stackchan_animation_len,
|
||||
conn_handle);
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Battery service access callback
|
||||
*/
|
||||
static int battery_svc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
int rc;
|
||||
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_ACCESS_OP_READ_CHR:
|
||||
MODLOG_DFLT(INFO, "Battery level read; conn_handle=%d attr_handle=%d", conn_handle, attr_handle);
|
||||
|
||||
if (attr_handle == battery_level_handle) {
|
||||
/* Call user callback to get current battery level */
|
||||
if (g_stackchan_callbacks.battery_read_cb) {
|
||||
battery_level = g_stackchan_callbacks.battery_read_cb();
|
||||
}
|
||||
|
||||
rc = os_mbuf_append(ctxt->om, &battery_level, sizeof(battery_level));
|
||||
return rc == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Old GATT service access callback (kept for compatibility)
|
||||
*/
|
||||
static int gatt_svc_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
|
||||
{
|
||||
/* This can be removed if not needed */
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
void gatt_svr_register_cb(struct ble_gatt_register_ctxt *ctxt, void *arg)
|
||||
{
|
||||
char buf[BLE_UUID_STR_LEN];
|
||||
|
||||
switch (ctxt->op) {
|
||||
case BLE_GATT_REGISTER_OP_SVC:
|
||||
MODLOG_DFLT(DEBUG, "registered service %s with handle=%d", ble_uuid_to_str(ctxt->svc.svc_def->uuid, buf),
|
||||
ctxt->svc.handle);
|
||||
break;
|
||||
|
||||
case BLE_GATT_REGISTER_OP_CHR:
|
||||
MODLOG_DFLT(DEBUG,
|
||||
"registering characteristic %s with "
|
||||
"def_handle=%d val_handle=%d",
|
||||
ble_uuid_to_str(ctxt->chr.chr_def->uuid, buf), ctxt->chr.def_handle, ctxt->chr.val_handle);
|
||||
break;
|
||||
|
||||
case BLE_GATT_REGISTER_OP_DSC:
|
||||
MODLOG_DFLT(DEBUG, "registering descriptor %s with handle=%d",
|
||||
ble_uuid_to_str(ctxt->dsc.dsc_def->uuid, buf), ctxt->dsc.handle);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int gatt_svr_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
/* Allocate buffers in PSRAM */
|
||||
stackchan_motion_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
|
||||
stackchan_avatar_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
|
||||
stackchan_config_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
|
||||
stackchan_animation_data = (char *)heap_caps_malloc(STACKCHAN_MAX_JSON_LEN, MALLOC_CAP_SPIRAM);
|
||||
|
||||
if (!stackchan_motion_data || !stackchan_avatar_data || !stackchan_config_data || !stackchan_animation_data) {
|
||||
MODLOG_DFLT(ERROR, "Failed to allocate memory for Stack-Chan characteristics\n");
|
||||
return BLE_HS_ENOMEM;
|
||||
}
|
||||
|
||||
ble_svc_gap_init();
|
||||
ble_svc_gatt_init();
|
||||
|
||||
rc = ble_gatts_count_cfg(gatt_svr_svcs);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = ble_gatts_add_svcs(gatt_svr_svcs);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Initialize Stack-Chan data with empty JSON */
|
||||
strcpy(stackchan_motion_data, "{}");
|
||||
stackchan_motion_len = 2;
|
||||
|
||||
strcpy(stackchan_avatar_data, "{}");
|
||||
stackchan_avatar_len = 2;
|
||||
|
||||
strcpy(stackchan_config_data, "{}");
|
||||
stackchan_config_len = 2;
|
||||
|
||||
strcpy(stackchan_animation_data, "{}");
|
||||
stackchan_animation_len = 2;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public API implementations
|
||||
*/
|
||||
|
||||
void stackchan_ble_register_callbacks(const stackchan_ble_callbacks_t *callbacks)
|
||||
{
|
||||
if (callbacks) {
|
||||
g_stackchan_callbacks = *callbacks;
|
||||
MODLOG_DFLT(INFO, "Stack-Chan callbacks registered");
|
||||
}
|
||||
}
|
||||
|
||||
int stackchan_ble_notify_motion(const char *json_data, uint16_t len)
|
||||
{
|
||||
if (!json_data || len == 0 || len >= STACKCHAN_MAX_JSON_LEN) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
memcpy(stackchan_motion_data, json_data, len);
|
||||
stackchan_motion_len = len;
|
||||
stackchan_motion_data[len] = '\0';
|
||||
|
||||
if (g_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gatts_chr_updated(stackchan_motion_handle);
|
||||
MODLOG_DFLT(INFO, "Motion notification sent");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stackchan_ble_notify_avatar(const char *json_data, uint16_t len)
|
||||
{
|
||||
if (!json_data || len == 0 || len >= STACKCHAN_MAX_JSON_LEN) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
memcpy(stackchan_avatar_data, json_data, len);
|
||||
stackchan_avatar_len = len;
|
||||
stackchan_avatar_data[len] = '\0';
|
||||
|
||||
if (g_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gatts_chr_updated(stackchan_avatar_handle);
|
||||
MODLOG_DFLT(INFO, "Avatar notification sent");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stackchan_ble_notify_config(const char *json_data, uint16_t len)
|
||||
{
|
||||
if (!json_data || len == 0 || len >= STACKCHAN_MAX_JSON_LEN) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
memcpy(stackchan_config_data, json_data, len);
|
||||
stackchan_config_len = len;
|
||||
stackchan_config_data[len] = '\0';
|
||||
|
||||
if (g_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gatts_chr_updated(stackchan_config_handle);
|
||||
MODLOG_DFLT(INFO, "Config notification sent");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stackchan_ble_notify_animation(const char *json_data, uint16_t len)
|
||||
{
|
||||
if (!json_data || len == 0 || len >= STACKCHAN_MAX_JSON_LEN) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
memcpy(stackchan_animation_data, json_data, len);
|
||||
stackchan_animation_len = len;
|
||||
stackchan_animation_data[len] = '\0';
|
||||
|
||||
if (g_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gatts_chr_updated(stackchan_animation_handle);
|
||||
MODLOG_DFLT(INFO, "Animation notification sent");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stackchan_ble_update_battery_level(uint8_t level)
|
||||
{
|
||||
if (level > 100) {
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
battery_level = level;
|
||||
|
||||
if (g_conn_handle != BLE_HS_CONN_HANDLE_NONE) {
|
||||
ble_gatts_chr_updated(battery_level_handle);
|
||||
MODLOG_DFLT(INFO, "Battery level updated to %d%%", level);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void stackchan_ble_set_conn_handle(uint16_t conn_handle)
|
||||
{
|
||||
g_conn_handle = conn_handle;
|
||||
MODLOG_DFLT(INFO, "Stack-Chan connection handle updated: %d", conn_handle);
|
||||
}
|
||||
|
||||
bool stackchan_ble_is_connected(void)
|
||||
{
|
||||
return (g_conn_handle != BLE_HS_CONN_HANDLE_NONE);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
|
||||
#ifndef H_ESP_PERIPHERAL_
|
||||
#define H_ESP_PERIPHERAL_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "nimble/ble.h"
|
||||
#include "modlog/modlog.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Console */
|
||||
int scli_init(void);
|
||||
int scli_receive_key(int *key);
|
||||
|
||||
/** Misc. */
|
||||
void print_bytes(const uint8_t *bytes, int len);
|
||||
void print_addr(const void *addr);
|
||||
char *addr_str(const void *addr);
|
||||
void print_mbuf(const struct os_mbuf *om);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
|
||||
#include "esp_peripheral.h"
|
||||
|
||||
/**
|
||||
* Utility function to log an array of bytes.
|
||||
*/
|
||||
void print_bytes(const uint8_t *bytes, int len)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
MODLOG_DFLT(INFO, "%s0x%02x", i != 0 ? ":" : "", bytes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void print_addr(const void *addr)
|
||||
{
|
||||
const uint8_t *u8p;
|
||||
|
||||
u8p = addr;
|
||||
MODLOG_DFLT(INFO, "%02x:%02x:%02x:%02x:%02x:%02x", u8p[5], u8p[4], u8p[3], u8p[2], u8p[1], u8p[0]);
|
||||
}
|
||||
|
||||
char *addr_str(const void *addr)
|
||||
{
|
||||
static char buf[6 * 2 + 5 + 1];
|
||||
const uint8_t *u8p;
|
||||
|
||||
u8p = addr;
|
||||
sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", u8p[5], u8p[4], u8p[3], u8p[2], u8p[1], u8p[0]);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
void print_mbuf(const struct os_mbuf *om)
|
||||
{
|
||||
int colon, i;
|
||||
|
||||
colon = 0;
|
||||
while (om != NULL) {
|
||||
if (colon) {
|
||||
MODLOG_DFLT(DEBUG, ":");
|
||||
} else {
|
||||
colon = 1;
|
||||
}
|
||||
for (i = 0; i < om->om_len; i++) {
|
||||
MODLOG_DFLT(DEBUG, "%s0x%02x", i != 0 ? ":" : "", om->om_data[i]);
|
||||
}
|
||||
om = SLIST_NEXT(om, om_next);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include "esp_log.h"
|
||||
#include <string.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_console.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <driver/uart.h>
|
||||
#include "esp_peripheral.h"
|
||||
|
||||
#define BLE_RX_TIMEOUT (30000 / portTICK_PERIOD_MS)
|
||||
|
||||
static TaskHandle_t cli_task;
|
||||
static QueueHandle_t cli_handle;
|
||||
static int stop;
|
||||
|
||||
static int enter_passkey_handler(int argc, char *argv[])
|
||||
{
|
||||
int key;
|
||||
char pkey[8];
|
||||
int num;
|
||||
|
||||
if (argc != 2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%s", pkey);
|
||||
ESP_LOGI("You entered", "%s %s", argv[0], argv[1]);
|
||||
num = pkey[0];
|
||||
|
||||
if (isalpha(num)) {
|
||||
if ((strcasecmp(pkey, "Y") == 0) || (strcasecmp(pkey, "Yes") == 0)) {
|
||||
key = 1;
|
||||
xQueueSend(cli_handle, &key, 0);
|
||||
} else {
|
||||
key = 0;
|
||||
xQueueSend(cli_handle, &key, 0);
|
||||
}
|
||||
} else {
|
||||
sscanf(pkey, "%d", &key);
|
||||
xQueueSend(cli_handle, &key, 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int scli_receive_key(int *console_key)
|
||||
{
|
||||
return xQueueReceive(cli_handle, console_key, BLE_RX_TIMEOUT);
|
||||
}
|
||||
|
||||
static esp_console_cmd_t cmds[] = {
|
||||
{
|
||||
.command = "key",
|
||||
.help = "",
|
||||
.func = enter_passkey_handler,
|
||||
},
|
||||
};
|
||||
|
||||
static int ble_register_cli(void)
|
||||
{
|
||||
int cmds_num = sizeof(cmds) / sizeof(esp_console_cmd_t);
|
||||
int i;
|
||||
for (i = 0; i < cmds_num; i++) {
|
||||
esp_console_cmd_register(&cmds[i]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void scli_task(void *arg)
|
||||
{
|
||||
int uart_num = (int)arg;
|
||||
uint8_t linebuf[256];
|
||||
int i, cmd_ret;
|
||||
esp_err_t ret;
|
||||
QueueHandle_t uart_queue;
|
||||
uart_event_t event;
|
||||
|
||||
uart_driver_install(uart_num, 256, 0, 8, &uart_queue, 0);
|
||||
/* Initialize the console */
|
||||
esp_console_config_t console_config = {
|
||||
.max_cmdline_args = 8,
|
||||
.max_cmdline_length = 256,
|
||||
};
|
||||
|
||||
esp_console_init(&console_config);
|
||||
|
||||
while (!stop) {
|
||||
i = 0;
|
||||
memset(linebuf, 0, sizeof(linebuf));
|
||||
do {
|
||||
ret = xQueueReceive(uart_queue, (void *)&event, (TickType_t)portMAX_DELAY);
|
||||
if (ret != pdPASS) {
|
||||
if (stop == 1) {
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (event.type == UART_DATA) {
|
||||
while (uart_read_bytes(uart_num, (uint8_t *)&linebuf[i], 1, 0)) {
|
||||
if (linebuf[i] == '\r') {
|
||||
uart_write_bytes(uart_num, "\r\n", 2);
|
||||
} else {
|
||||
uart_write_bytes(uart_num, (char *)&linebuf[i], 1);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} while ((i < 255) && linebuf[i - 1] != '\r');
|
||||
if (stop) {
|
||||
break;
|
||||
}
|
||||
/* Remove the truncating \r\n */
|
||||
linebuf[strlen((char *)linebuf) - 1] = '\0';
|
||||
ret = esp_console_run((char *)linebuf, &cmd_ret);
|
||||
if (ret < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
int scli_init(void)
|
||||
{
|
||||
/* Register CLI "key <value>" to accept input from user during pairing */
|
||||
ble_register_cli();
|
||||
|
||||
xTaskCreate(scli_task, "scli_cli", 4096, (void *)0, 3, &cli_task);
|
||||
if (cli_task == NULL) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
cli_handle = xQueueCreate(1, sizeof(int));
|
||||
if (cli_handle == NULL) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "jpeg_decoder.h"
|
||||
#include <esp_log.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_jpeg_dec.h>
|
||||
#include <cstring>
|
||||
|
||||
static const char* TAG = "JpegDecoder";
|
||||
|
||||
namespace jpeg_dec {
|
||||
|
||||
std::shared_ptr<LvglAllocatedImage> decode_to_lvgl(const uint8_t* jpeg_data, size_t jpeg_len)
|
||||
{
|
||||
if (!jpeg_data || jpeg_len == 0) {
|
||||
ESP_LOGE(TAG, "Invalid input data");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jpeg_dec_config_t config = DEFAULT_JPEG_DEC_CONFIG();
|
||||
config.output_type = JPEG_PIXEL_FORMAT_RGB565_LE;
|
||||
|
||||
jpeg_dec_handle_t jpeg_dec = NULL;
|
||||
if (jpeg_dec_open(&config, &jpeg_dec) != JPEG_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open JPEG decoder");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Ensure decoder is closed when function exits
|
||||
struct JpegDecCloser {
|
||||
jpeg_dec_handle_t handle;
|
||||
~JpegDecCloser()
|
||||
{
|
||||
if (handle) jpeg_dec_close(handle);
|
||||
}
|
||||
} closer{jpeg_dec};
|
||||
|
||||
jpeg_dec_io_t io = {0};
|
||||
io.inbuf = (uint8_t*)jpeg_data;
|
||||
io.inbuf_len = jpeg_len;
|
||||
|
||||
jpeg_dec_header_info_t out_info = {0};
|
||||
if (jpeg_dec_parse_header(jpeg_dec, &io, &out_info) != JPEG_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to parse JPEG header");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int out_size = out_info.width * out_info.height * 2;
|
||||
// Allocate memory for the output image (16-byte aligned required by esp_jpeg_dec)
|
||||
uint8_t* out_buf = (uint8_t*)heap_caps_aligned_alloc(16, out_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (!out_buf) {
|
||||
// Fallback to internal memory if SPIRAM allocation fails
|
||||
out_buf = (uint8_t*)heap_caps_aligned_alloc(16, out_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
}
|
||||
|
||||
if (!out_buf) {
|
||||
ESP_LOGE(TAG, "Failed to allocate output buffer");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
io.outbuf = out_buf;
|
||||
if (jpeg_dec_process(jpeg_dec, &io) != JPEG_ERR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to process JPEG");
|
||||
heap_caps_free(out_buf);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
try {
|
||||
// LvglAllocatedImage takes ownership of out_buf and will free it with heap_caps_free
|
||||
return std::make_shared<LvglAllocatedImage>(out_buf, out_size, out_info.width, out_info.height,
|
||||
out_info.width * 2, LV_COLOR_FORMAT_RGB565);
|
||||
} catch (const std::exception& e) {
|
||||
ESP_LOGE(TAG, "Failed to create LvglAllocatedImage: %s", e.what());
|
||||
heap_caps_free(out_buf);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace jpeg_dec
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <lvgl_image.h>
|
||||
|
||||
namespace jpeg_dec {
|
||||
|
||||
/**
|
||||
* @brief Decodes a JPEG buffer to RGB565 format suitable for LVGL.
|
||||
*
|
||||
* @param jpeg_data Pointer to the JPEG data.
|
||||
* @param jpeg_len Length of the JPEG data.
|
||||
* @return std::shared_ptr<LvglAllocatedImage> The decoded image wrapped in LvglAllocatedImage, or nullptr on failure.
|
||||
*/
|
||||
std::shared_ptr<LvglAllocatedImage> decode_to_lvgl(const uint8_t* jpeg_data, size_t jpeg_len);
|
||||
|
||||
} // namespace jpeg_dec
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
class MotionDetector {
|
||||
public:
|
||||
MotionDetector() = default;
|
||||
|
||||
void update(const float& acc_x, const float& acc_y, const float& acc_z)
|
||||
{
|
||||
uint32_t now = pdTICKS_TO_MS(xTaskGetTickCount());
|
||||
float acc_mag = std::sqrt(std::pow(acc_x, 2) + std::pow(acc_y, 2) + std::pow(acc_z, 2));
|
||||
|
||||
// --- Shake Detection ---
|
||||
// Threshold ~1.5G (14.7 m/s^2)
|
||||
if (std::abs(acc_mag - 9.80665f) > 8.0f) {
|
||||
if (now - _last_shake_peak_time > 200) { // Debounce 200ms
|
||||
if (now - _last_shake_peak_time < 1000) { // Window 1s
|
||||
_shake_count++;
|
||||
} else {
|
||||
_shake_count = 1; // Reset sequence
|
||||
}
|
||||
_last_shake_peak_time = now;
|
||||
|
||||
if (_shake_count >= 3) {
|
||||
_shake_detected = true;
|
||||
_shake_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pick Up Detection ---
|
||||
// Check for stability first
|
||||
float diff = std::abs(acc_x - _prev_acc_x) + std::abs(acc_y - _prev_acc_y) + std::abs(acc_z - _prev_acc_z);
|
||||
|
||||
_prev_acc_x = acc_x;
|
||||
_prev_acc_y = acc_y;
|
||||
_prev_acc_z = acc_z;
|
||||
|
||||
// Threshold for stability (low noise)
|
||||
if (diff < 1.5f) {
|
||||
if (!_is_stable) {
|
||||
_stable_since = now;
|
||||
_is_stable = true;
|
||||
}
|
||||
} else {
|
||||
// If it was stable for > 1s and now moving -> Pick Up
|
||||
if (_is_stable && (now - _stable_since > 1000)) {
|
||||
_pickup_detected = true;
|
||||
}
|
||||
_is_stable = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isShakeDetected()
|
||||
{
|
||||
if (_shake_detected) {
|
||||
_shake_detected = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isPickUpDetected()
|
||||
{
|
||||
if (_pickup_detected) {
|
||||
_pickup_detected = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
int _shake_count = 0;
|
||||
uint32_t _last_shake_peak_time = 0;
|
||||
bool _shake_detected = false;
|
||||
|
||||
bool _pickup_detected = false;
|
||||
bool _is_stable = false;
|
||||
uint32_t _stable_since = 0;
|
||||
float _prev_acc_x = 0;
|
||||
float _prev_acc_y = 0;
|
||||
float _prev_acc_z = 0;
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "reminder.h"
|
||||
#include <esp_log.h>
|
||||
#include <assets/assets.h>
|
||||
#include <hal/hal.h>
|
||||
#include <hal/board/hal_bridge.h>
|
||||
|
||||
static const char* TAG = "ReminderManager";
|
||||
|
||||
ReminderItem::ReminderItem(int duration_s, const std::string& msg) : message_(msg)
|
||||
{
|
||||
target_time_ = std::chrono::steady_clock::now() + std::chrono::seconds(duration_s);
|
||||
}
|
||||
|
||||
bool ReminderItem::IsDue() const
|
||||
{
|
||||
return std::chrono::steady_clock::now() >= target_time_;
|
||||
}
|
||||
|
||||
ReminderManager& ReminderManager::GetInstance()
|
||||
{
|
||||
static ReminderManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
ReminderManager::ReminderManager()
|
||||
{
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
ReminderManager::~ReminderManager()
|
||||
{
|
||||
running_ = false;
|
||||
// 等待任务结束(简单处理,实际可能需要更复杂的同步)
|
||||
if (worker_task_handle_) {
|
||||
// vTaskDelete(worker_task_handle_); // 不建议直接删除,最好让任务自己退出
|
||||
// 这里我们假设任务会检测 running_ 并退出
|
||||
int timeout = 100;
|
||||
while (eTaskGetState(worker_task_handle_) != eDeleted && timeout-- > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
if (mutex_) {
|
||||
vSemaphoreDelete(mutex_);
|
||||
}
|
||||
}
|
||||
|
||||
void ReminderManager::Start()
|
||||
{
|
||||
if (running_) return;
|
||||
running_ = true;
|
||||
xTaskCreate(
|
||||
[](void* arg) {
|
||||
ReminderManager* mgr = (ReminderManager*)arg;
|
||||
mgr->WorkerThread();
|
||||
vTaskDelete(NULL);
|
||||
},
|
||||
"reminder_worker", 4096, this, 5, &worker_task_handle_);
|
||||
ESP_LOGI(TAG, "ReminderManager started");
|
||||
}
|
||||
|
||||
int ReminderManager::CreateReminder(int duration_s, const std::string& message)
|
||||
{
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
auto item = std::make_unique<ReminderItem>(duration_s, message);
|
||||
int id = pool_.create(std::move(item));
|
||||
xSemaphoreGive(mutex_);
|
||||
ESP_LOGI(TAG, "Created reminder ID: %d, Duration: %ds, Msg: %s", id, duration_s, message.c_str());
|
||||
return id;
|
||||
}
|
||||
|
||||
void ReminderManager::StopReminder(int id)
|
||||
{
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
|
||||
// 如果正在响铃的是这个提醒,停止播放
|
||||
if (id == ringing_id_) {
|
||||
if (hal_bridge::is_xiaozhi_mode()) {
|
||||
} else {
|
||||
// audio_player_.Stop();
|
||||
}
|
||||
ringing_id_ = -1;
|
||||
}
|
||||
|
||||
// 标记销毁
|
||||
auto* item = pool_.get(id);
|
||||
if (item) {
|
||||
item->requestDestroy();
|
||||
ESP_LOGI(TAG, "Stopped reminder ID: %d", id);
|
||||
}
|
||||
|
||||
// 立即清理(或者等待 WorkerThread 清理也可以,这里立即清理更及时)
|
||||
pool_.destroy(id);
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void ReminderManager::WorkerThread()
|
||||
{
|
||||
std::vector<std::pair<int, std::string>> triggered_list;
|
||||
while (running_) {
|
||||
triggered_list.clear();
|
||||
|
||||
{
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
|
||||
// 1. 检查所有提醒
|
||||
pool_.forEach([&](ReminderItem* item, int id) {
|
||||
if (!item->IsTriggered() && item->IsDue()) {
|
||||
item->SetTriggered(true);
|
||||
triggered_list.push_back({id, item->GetMessage()});
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 清理已销毁的对象
|
||||
pool_.cleanup();
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
// 3. 处理触发的提醒(在锁外执行,防止死锁)
|
||||
for (const auto& pair : triggered_list) {
|
||||
int id = pair.first;
|
||||
const std::string& msg = pair.second;
|
||||
|
||||
ESP_LOGI(TAG, "Reminder triggered! ID: %d, Msg: %s", id, msg.c_str());
|
||||
|
||||
// 更新响铃 ID
|
||||
{
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
// 再次检查对象是否存在(可能在触发前一瞬间被删除了)
|
||||
if (pool_.get(id) == nullptr) {
|
||||
xSemaphoreGive(mutex_);
|
||||
continue;
|
||||
}
|
||||
ringing_id_ = id;
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
// 播放铃声 (循环)
|
||||
if (!OGG_CAMERA_SHUTTER.empty()) {
|
||||
if (hal_bridge::is_xiaozhi_mode()) {
|
||||
hal_bridge::app_play_sound(OGG_CAMERA_SHUTTER);
|
||||
} else {
|
||||
// audio_player_.Play(reinterpret_cast<const uint8_t*>(OGG_CAMERA_SHUTTER.data()),
|
||||
// OGG_CAMERA_SHUTTER.size(), true);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No ringtone data available");
|
||||
}
|
||||
|
||||
// 发出信号
|
||||
GetHAL().onReminderTriggered.emit(id, msg);
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include "stackchan/utils/object_pool.h"
|
||||
#include "hal/utils/simple_audio_player/simple_audio_player.h"
|
||||
|
||||
class ReminderItem : public stackchan::Poolable {
|
||||
public:
|
||||
ReminderItem(int duration_s, const std::string& msg);
|
||||
|
||||
bool IsDue() const;
|
||||
bool IsTriggered() const
|
||||
{
|
||||
return triggered_;
|
||||
}
|
||||
void SetTriggered(bool t)
|
||||
{
|
||||
triggered_ = t;
|
||||
}
|
||||
const std::string& GetMessage() const
|
||||
{
|
||||
return message_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string message_;
|
||||
std::chrono::steady_clock::time_point target_time_;
|
||||
bool triggered_ = false;
|
||||
};
|
||||
|
||||
class ReminderManager {
|
||||
public:
|
||||
static ReminderManager& GetInstance();
|
||||
|
||||
// 初始化并启动后台线程
|
||||
void Start();
|
||||
|
||||
// 创建一个提醒
|
||||
// duration_s: 多少秒后提醒
|
||||
// message: 提醒内容
|
||||
// 返回: 提醒 ID
|
||||
int CreateReminder(int duration_s, const std::string& message);
|
||||
|
||||
// 停止/关闭提醒
|
||||
// id: 提醒 ID
|
||||
void StopReminder(int id);
|
||||
|
||||
private:
|
||||
ReminderManager();
|
||||
~ReminderManager();
|
||||
|
||||
void WorkerThread();
|
||||
|
||||
SemaphoreHandle_t mutex_ = nullptr;
|
||||
stackchan::ObjectPool<ReminderItem> pool_;
|
||||
// SimpleAudioPlayer audio_player_;
|
||||
|
||||
TaskHandle_t worker_task_handle_ = nullptr;
|
||||
volatile bool running_ = false;
|
||||
int ringing_id_ = -1;
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "simple_audio_player.h"
|
||||
#include <opus_decoder.h>
|
||||
#include <opus_resampler.h>
|
||||
#include <esp_log.h>
|
||||
#include <cstring>
|
||||
#include "board.h"
|
||||
#include "audio_codec.h"
|
||||
|
||||
static const char* TAG = "SimpleAudioPlayer";
|
||||
|
||||
#define OPUS_FRAME_DURATION_MS 60
|
||||
|
||||
SimpleAudioPlayer::SimpleAudioPlayer()
|
||||
{
|
||||
codec_ = Board::GetInstance().GetAudioCodec();
|
||||
codec_->Start();
|
||||
|
||||
// 初始化解码器和重采样器,默认参数,后续会根据 OGG 头调整
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(16000, 1, OPUS_FRAME_DURATION_MS);
|
||||
output_resampler_ = std::make_unique<OpusResampler>();
|
||||
}
|
||||
|
||||
SimpleAudioPlayer::~SimpleAudioPlayer()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
struct PlayerParams {
|
||||
SimpleAudioPlayer* player;
|
||||
const uint8_t* data;
|
||||
size_t size;
|
||||
bool loop;
|
||||
};
|
||||
|
||||
bool SimpleAudioPlayer::Play(const uint8_t* data, size_t size, bool loop)
|
||||
{
|
||||
Stop(); // 停止之前的播放
|
||||
|
||||
is_playing_ = true;
|
||||
stop_requested_ = false;
|
||||
|
||||
PlayerParams* params = new PlayerParams{this, data, size, loop};
|
||||
|
||||
BaseType_t ret = xTaskCreate(
|
||||
[](void* arg) {
|
||||
PlayerParams* p = (PlayerParams*)arg;
|
||||
p->player->PlaybackTask(p->data, p->size, p->loop);
|
||||
delete p;
|
||||
vTaskDelete(NULL);
|
||||
},
|
||||
"simple_player", 4096 * 2, params, 5, &task_handle_);
|
||||
|
||||
if (ret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create playback task");
|
||||
delete params;
|
||||
is_playing_ = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::Stop()
|
||||
{
|
||||
stop_requested_ = true;
|
||||
if (task_handle_) {
|
||||
int timeout = 100;
|
||||
while (is_playing_ && timeout-- > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SimpleAudioPlayer::IsPlaying() const
|
||||
{
|
||||
return is_playing_;
|
||||
}
|
||||
|
||||
void SimpleAudioPlayer::PlaybackTask(const uint8_t* data, size_t size, bool loop)
|
||||
{
|
||||
if (!codec_->output_enabled()) {
|
||||
codec_->EnableOutput(true);
|
||||
}
|
||||
|
||||
const uint8_t* buf = data;
|
||||
|
||||
auto find_page = [&](size_t start) -> size_t {
|
||||
for (size_t i = start; i + 4 <= size; ++i) {
|
||||
if (buf[i] == 'O' && buf[i + 1] == 'g' && buf[i + 2] == 'g' && buf[i + 3] == 'S') return i;
|
||||
}
|
||||
return static_cast<size_t>(-1);
|
||||
};
|
||||
|
||||
do {
|
||||
size_t offset = 0;
|
||||
bool seen_head = false;
|
||||
bool seen_tags = false;
|
||||
int sample_rate = 16000; // 默认值
|
||||
|
||||
// 如果是循环播放,重置解码器状态以避免爆音
|
||||
if (loop) {
|
||||
opus_decoder_->ResetState();
|
||||
}
|
||||
|
||||
while (!stop_requested_) {
|
||||
// 确保输出已启用(防止被 AudioService 的自动省电逻辑关闭)
|
||||
if (!codec_->output_enabled()) {
|
||||
codec_->EnableOutput(true);
|
||||
}
|
||||
|
||||
size_t pos = find_page(offset);
|
||||
if (pos == static_cast<size_t>(-1)) break;
|
||||
offset = pos;
|
||||
if (offset + 27 > size) break;
|
||||
|
||||
const uint8_t* page = buf + offset;
|
||||
uint8_t page_segments = page[26];
|
||||
size_t seg_table_off = offset + 27;
|
||||
if (seg_table_off + page_segments > size) break;
|
||||
|
||||
size_t body_size = 0;
|
||||
for (size_t i = 0; i < page_segments; ++i) body_size += page[27 + i];
|
||||
|
||||
size_t body_off = seg_table_off + page_segments;
|
||||
if (body_off + body_size > size) break;
|
||||
|
||||
// Parse packets using lacing
|
||||
size_t cur = body_off;
|
||||
size_t seg_idx = 0;
|
||||
while (seg_idx < page_segments && !stop_requested_) {
|
||||
size_t pkt_len = 0;
|
||||
size_t pkt_start = cur;
|
||||
bool continued = false;
|
||||
do {
|
||||
uint8_t l = page[27 + seg_idx++];
|
||||
pkt_len += l;
|
||||
cur += l;
|
||||
continued = (l == 255);
|
||||
} while (continued && seg_idx < page_segments);
|
||||
|
||||
if (pkt_len == 0) continue;
|
||||
const uint8_t* pkt_ptr = buf + pkt_start;
|
||||
|
||||
if (!seen_head) {
|
||||
if (pkt_len >= 19 && std::memcmp(pkt_ptr, "OpusHead", 8) == 0) {
|
||||
seen_head = true;
|
||||
if (pkt_len >= 12) {
|
||||
// uint8_t version = pkt_ptr[8];
|
||||
// uint8_t channel_count = pkt_ptr[9];
|
||||
if (pkt_len >= 16) {
|
||||
sample_rate =
|
||||
pkt_ptr[12] | (pkt_ptr[13] << 8) | (pkt_ptr[14] << 16) | (pkt_ptr[15] << 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!seen_tags) {
|
||||
if (pkt_len >= 8 && std::memcmp(pkt_ptr, "OpusTags", 8) == 0) {
|
||||
seen_tags = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Audio packet (Opus)
|
||||
std::vector<uint8_t> payload(pkt_ptr, pkt_ptr + pkt_len);
|
||||
if (!DecodeAndPlay(payload, sample_rate)) {
|
||||
ESP_LOGE(TAG, "Failed to decode and play packet");
|
||||
}
|
||||
}
|
||||
|
||||
offset = body_off + body_size;
|
||||
}
|
||||
} while (loop && !stop_requested_);
|
||||
|
||||
// 播放结束,不关闭 output,以免影响其他音频
|
||||
is_playing_ = false;
|
||||
task_handle_ = nullptr;
|
||||
}
|
||||
|
||||
bool SimpleAudioPlayer::DecodeAndPlay(std::vector<uint8_t>& opus_payload, int sample_rate)
|
||||
{
|
||||
// 检查采样率是否变化
|
||||
if (opus_decoder_->sample_rate() != sample_rate) {
|
||||
opus_decoder_.reset();
|
||||
opus_decoder_ = std::make_unique<OpusDecoderWrapper>(sample_rate, 1, OPUS_FRAME_DURATION_MS);
|
||||
|
||||
if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
|
||||
output_resampler_->Configure(opus_decoder_->sample_rate(), codec_->output_sample_rate());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int16_t> pcm;
|
||||
if (opus_decoder_->Decode(std::move(opus_payload), pcm)) {
|
||||
// Resample if needed
|
||||
if (opus_decoder_->sample_rate() != codec_->output_sample_rate()) {
|
||||
int target_size = output_resampler_->GetOutputSamples(pcm.size());
|
||||
std::vector<int16_t> resampled(target_size);
|
||||
output_resampler_->Process(pcm.data(), pcm.size(), resampled.data());
|
||||
pcm = std::move(resampled);
|
||||
}
|
||||
|
||||
codec_->OutputData(pcm);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
class AudioCodec;
|
||||
class OpusDecoderWrapper;
|
||||
class OpusResampler;
|
||||
|
||||
class SimpleAudioPlayer {
|
||||
public:
|
||||
SimpleAudioPlayer();
|
||||
~SimpleAudioPlayer();
|
||||
|
||||
// 播放 OGG 音频数据
|
||||
// 注意:数据必须在播放期间保持有效(例如存储在 Flash 中的数据)
|
||||
// 不会复制数据
|
||||
// loop: 是否循环播放
|
||||
bool Play(const uint8_t* data, size_t size, bool loop = false);
|
||||
|
||||
// 停止播放
|
||||
void Stop();
|
||||
|
||||
// 是否正在播放
|
||||
bool IsPlaying() const;
|
||||
|
||||
private:
|
||||
void PlaybackTask(const uint8_t* data, size_t size, bool loop);
|
||||
bool DecodeAndPlay(std::vector<uint8_t>& opus_payload, int sample_rate);
|
||||
|
||||
AudioCodec* codec_ = nullptr;
|
||||
std::unique_ptr<OpusDecoderWrapper> opus_decoder_;
|
||||
std::unique_ptr<OpusResampler> output_resampler_;
|
||||
|
||||
TaskHandle_t task_handle_ = nullptr;
|
||||
volatile bool is_playing_ = false;
|
||||
volatile bool stop_requested_ = false;
|
||||
};
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "wifi_station.h"
|
||||
#include <cstring>
|
||||
#include <esp_log.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_system.h>
|
||||
#include "ssid_manager.h"
|
||||
|
||||
#define TAG "StackChanWifiStation"
|
||||
#define WIFI_EVENT_CONNECTED BIT0
|
||||
#define MAX_RECONNECT_COUNT 10
|
||||
|
||||
StackChanWifiStation& StackChanWifiStation::GetInstance()
|
||||
{
|
||||
static StackChanWifiStation instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
StackChanWifiStation::StackChanWifiStation()
|
||||
{
|
||||
event_group_ = xEventGroupCreate();
|
||||
}
|
||||
|
||||
StackChanWifiStation::~StackChanWifiStation()
|
||||
{
|
||||
vEventGroupDelete(event_group_);
|
||||
}
|
||||
|
||||
void StackChanWifiStation::AddAuth(const std::string& ssid, const std::string& password)
|
||||
{
|
||||
// Save to NVS via SsidManager for compatibility
|
||||
SsidManager::GetInstance().AddSsid(ssid, password);
|
||||
|
||||
ssid_ = ssid;
|
||||
wifi_config_t wifi_config;
|
||||
bzero(&wifi_config, sizeof(wifi_config));
|
||||
memcpy(wifi_config.sta.ssid, ssid.c_str(), ssid.length());
|
||||
memcpy(wifi_config.sta.password, password.c_str(), password.length());
|
||||
|
||||
ESP_LOGI(TAG, "Setting WiFi configuration SSID: %s", ssid.c_str());
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
|
||||
if (on_connect_) {
|
||||
on_connect_(ssid_);
|
||||
}
|
||||
|
||||
reconnect_count_ = 0;
|
||||
is_connecting_ = true;
|
||||
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Already connected, disconnecting first...");
|
||||
esp_wifi_disconnect();
|
||||
// The reconnection will be handled by WIFI_EVENT_STA_DISCONNECTED
|
||||
} else {
|
||||
esp_wifi_connect();
|
||||
}
|
||||
}
|
||||
|
||||
void StackChanWifiStation::Stop()
|
||||
{
|
||||
if (instance_any_id_ != nullptr) {
|
||||
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id_);
|
||||
instance_any_id_ = nullptr;
|
||||
}
|
||||
if (instance_got_ip_ != nullptr) {
|
||||
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip_);
|
||||
instance_got_ip_ = nullptr;
|
||||
}
|
||||
|
||||
esp_wifi_stop();
|
||||
esp_wifi_deinit();
|
||||
|
||||
station_netif_ = nullptr;
|
||||
}
|
||||
|
||||
void StackChanWifiStation::OnConnect(std::function<void(const std::string& ssid)> on_connect)
|
||||
{
|
||||
on_connect_ = on_connect;
|
||||
}
|
||||
|
||||
void StackChanWifiStation::OnConnected(std::function<void(const std::string& ssid)> on_connected)
|
||||
{
|
||||
on_connected_ = on_connected;
|
||||
}
|
||||
|
||||
void StackChanWifiStation::OnConnectFailed(std::function<void(const std::string& ssid)> on_connect_failed)
|
||||
{
|
||||
on_connect_failed_ = on_connect_failed;
|
||||
}
|
||||
|
||||
void StackChanWifiStation::Start()
|
||||
{
|
||||
if (is_started_) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_netif_init();
|
||||
// esp_event_loop_create_default(); // Assumed to be created by main app or previous init
|
||||
|
||||
station_netif_ = esp_netif_create_default_wifi_sta();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
cfg.nvs_enable = true; // Enable NVS to store credentials
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID, &StackChanWifiStation::WifiEventHandler, this, &instance_any_id_));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
IP_EVENT, IP_EVENT_STA_GOT_IP, &StackChanWifiStation::IpEventHandler, this, &instance_got_ip_));
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
is_started_ = true;
|
||||
}
|
||||
|
||||
bool StackChanWifiStation::WaitForConnected(int timeout_ms)
|
||||
{
|
||||
auto bits =
|
||||
xEventGroupWaitBits(event_group_, WIFI_EVENT_CONNECTED, pdFALSE, pdFALSE, timeout_ms / portTICK_PERIOD_MS);
|
||||
return (bits & WIFI_EVENT_CONNECTED) != 0;
|
||||
}
|
||||
|
||||
int8_t StackChanWifiStation::GetRssi()
|
||||
{
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
return ap_info.rssi;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t StackChanWifiStation::GetChannel()
|
||||
{
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
return ap_info.primary;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool StackChanWifiStation::IsConnected()
|
||||
{
|
||||
return xEventGroupGetBits(event_group_) & WIFI_EVENT_CONNECTED;
|
||||
}
|
||||
|
||||
void StackChanWifiStation::SetPowerSaveMode(bool enabled)
|
||||
{
|
||||
esp_wifi_set_ps(enabled ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE);
|
||||
}
|
||||
|
||||
void StackChanWifiStation::WifiEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
|
||||
{
|
||||
auto* this_ = static_cast<StackChanWifiStation*>(arg);
|
||||
if (event_id == WIFI_EVENT_STA_START) {
|
||||
// Do not auto connect on start
|
||||
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
xEventGroupClearBits(this_->event_group_, WIFI_EVENT_CONNECTED);
|
||||
|
||||
// Only retry if we are actively trying to connect
|
||||
if (this_->is_connecting_) {
|
||||
if (this_->reconnect_count_ < MAX_RECONNECT_COUNT) {
|
||||
this_->reconnect_count_++;
|
||||
ESP_LOGI(TAG, "Retry to connect to the AP (attempt %d/%d)", this_->reconnect_count_,
|
||||
MAX_RECONNECT_COUNT);
|
||||
esp_wifi_connect();
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Connect to the AP failed");
|
||||
this_->is_connecting_ = false;
|
||||
if (this_->on_connect_failed_) {
|
||||
this_->on_connect_failed_(this_->ssid_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StackChanWifiStation::IpEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
|
||||
{
|
||||
auto* this_ = static_cast<StackChanWifiStation*>(arg);
|
||||
auto* event = static_cast<ip_event_got_ip_t*>(event_data);
|
||||
|
||||
char ip_address[16];
|
||||
esp_ip4addr_ntoa(&event->ip_info.ip, ip_address, sizeof(ip_address));
|
||||
this_->ip_address_ = ip_address;
|
||||
ESP_LOGI(TAG, "Got IP: %s", this_->ip_address_.c_str());
|
||||
|
||||
this_->reconnect_count_ = 0;
|
||||
this_->is_connecting_ = false;
|
||||
xEventGroupSetBits(this_->event_group_, WIFI_EVENT_CONNECTED);
|
||||
|
||||
// Update SSID in case we connected from NVS auto-connect
|
||||
wifi_config_t conf;
|
||||
if (esp_wifi_get_config(WIFI_IF_STA, &conf) == ESP_OK) {
|
||||
this_->ssid_ = (char*)conf.sta.ssid;
|
||||
}
|
||||
|
||||
if (this_->on_connected_) {
|
||||
this_->on_connected_(this_->ssid_);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef _WIFI_STATION_H_
|
||||
#define _WIFI_STATION_H_
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/event_groups.h>
|
||||
#include <esp_event.h>
|
||||
#include <esp_netif.h>
|
||||
#include <esp_wifi_types_generic.h>
|
||||
|
||||
class StackChanWifiStation {
|
||||
public:
|
||||
static StackChanWifiStation& GetInstance();
|
||||
void AddAuth(const std::string& ssid, const std::string& password);
|
||||
void Start();
|
||||
void Stop();
|
||||
bool IsConnected();
|
||||
bool WaitForConnected(int timeout_ms = 10000);
|
||||
int8_t GetRssi();
|
||||
std::string GetSsid() const
|
||||
{
|
||||
return ssid_;
|
||||
}
|
||||
std::string GetIpAddress() const
|
||||
{
|
||||
return ip_address_;
|
||||
}
|
||||
uint8_t GetChannel();
|
||||
void SetPowerSaveMode(bool enabled);
|
||||
|
||||
void OnConnect(std::function<void(const std::string& ssid)> on_connect);
|
||||
void OnConnected(std::function<void(const std::string& ssid)> on_connected);
|
||||
void OnConnectFailed(std::function<void(const std::string& ssid)> on_connect_failed);
|
||||
|
||||
private:
|
||||
StackChanWifiStation();
|
||||
~StackChanWifiStation();
|
||||
StackChanWifiStation(const StackChanWifiStation&) = delete;
|
||||
StackChanWifiStation& operator=(const StackChanWifiStation&) = delete;
|
||||
|
||||
EventGroupHandle_t event_group_;
|
||||
esp_event_handler_instance_t instance_any_id_ = nullptr;
|
||||
esp_event_handler_instance_t instance_got_ip_ = nullptr;
|
||||
esp_netif_t* station_netif_ = nullptr;
|
||||
std::string ssid_;
|
||||
std::string ip_address_;
|
||||
int reconnect_count_ = 0;
|
||||
bool is_started_ = false;
|
||||
bool is_connecting_ = false;
|
||||
std::function<void(const std::string& ssid)> on_connect_;
|
||||
std::function<void(const std::string& ssid)> on_connected_;
|
||||
std::function<void(const std::string& ssid)> on_connect_failed_;
|
||||
|
||||
static void WifiEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
|
||||
static void IpEventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
|
||||
};
|
||||
|
||||
#endif // _WIFI_STATION_H_
|
||||
Reference in New Issue
Block a user