add firmware source code (#4)

* add firmware source code
This commit is contained in:
Forairaaaaa
2026-01-08 09:18:20 +08:00
committed by GitHub
parent 4f1373e449
commit 5001b7081b
164 changed files with 81663 additions and 1 deletions
+678
View File
@@ -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(&params, 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, &params, 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
}
+171
View File
@@ -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
+480
View File
@@ -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_