Compare commits

..

3 Commits

Author SHA1 Message Date
jh.chun 1d11758162 LED 상태 추가(VesiSeek) 2026-07-10 17:06:45 +09:00
jh.chun a6d1efc089 저장 측정 커맨드 추가(VesiSeek)
- mfq? / mfe? / mqb
2026-07-10 17:06:20 +09:00
jh.chun 5eb6b9cd3c 내부 플래시 파티션 크기 조정 및 신규 파티션 생성
- VesiSeek 연동을 위해 내부 플래시 저장공간 필요
- Primary slot, Secondary slot을 484 KiB에서 320 KiB로 축소
- 신규 파티션 data_storage 생성: 328 KiB
2026-07-10 16:16:20 +09:00
43 changed files with 1780 additions and 1925 deletions
-12
View File
@@ -24,18 +24,6 @@
"${workspaceFolder}\\tools\\flash_preserve_settings.ps1"
],
"problemMatcher": []
},
{
"label": "Flash + Lock (full erase + APPROTECT)",
"type": "shell",
"command": "powershell",
"args": [
"-ExecutionPolicy",
"Bypass",
"-File",
"${workspaceFolder}\\tools\\flash_full_erase_lock.ps1"
],
"problemMatcher": []
}
]
}
+2 -8
View File
@@ -10,9 +10,6 @@ target_include_directories(app PRIVATE
src/command
src/command/handlers
src/ble
src/power
src/dfu
src/nvs
src/drivers/battery
src/drivers/echo_adc
src/drivers/led
@@ -23,20 +20,17 @@ target_include_directories(app PRIVATE
target_sources(app PRIVATE
src/main.c
src/nvs/app_nvs.c
src/app_nvs.c
src/command/parser.c
src/command/ble_cmd_queue.c
src/command/cmd_common.c
src/command/cmd_table.c
src/command/handlers/cmd_device.c
src/command/handlers/cmd_info.c
src/command/handlers/cmd_piezo.c
src/command/handlers/cmd_sensor.c
src/power/power_control.c
src/dfu/dfu_manager.c
src/power_control.c
src/measurement/piezo_measure.c
src/ble/ble_service.c
src/ble/ble_tx_power.c
src/drivers/battery/battery_adc.c
src/drivers/echo_adc/echo_adc.c
src/drivers/led/led_control.c
-12
View File
@@ -1,12 +0,0 @@
source "Kconfig.zephyr"
menu "VesiScan application options"
config VESIS_DEBUG_PRINT
bool "Enable VesiScan debug print output"
default y
help
Enable DBG_PRINTF, DBG_CORE, and DBG_ERR output through printk().
Disable this option to compile out these project debug prints.
endmenu
+1 -2
View File
@@ -1,3 +1,2 @@
CONFIG_BT_CTLR_TX_PWR_PLUS_8=y
CONFIG_BT_CTLR_TX_PWR_PLUS_4=y
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
CONFIG_BT_CTLR_CONN_RSSI=y
+22 -11
View File
@@ -1,35 +1,46 @@
app:
address: 0xc200
region: flash_primary
size: 0x78e00
mcuboot:
address: 0x0
region: flash_primary
size: 0xc000
size: 0xc000 # 48 KiB
mcuboot_pad:
address: 0xc000
region: flash_primary
size: 0x200
size: 0x200 # 512 B
app:
address: 0xc200
region: flash_primary
size: 0x4fe00 # 319.5 KiB = 327,168 B
mcuboot_primary:
address: 0xc000
orig_span: &id001
- mcuboot_pad
- app
region: flash_primary
size: 0x79000
size: 0x50000 # 320 KiB
span: *id001
mcuboot_primary_app:
address: 0xc200
orig_span: &id002
- app
region: flash_primary
size: 0x78e00
size: 0x4fe00 # same as app
span: *id002
mcuboot_secondary:
address: 0x85000
address: 0x5c000
region: flash_primary
size: 0x79000
size: 0x50000 # 320 KiB
data_storage:
address: 0xac000
region: flash_primary
size: 0x52000 # 328 KiB
settings_storage:
address: 0xfe000
region: flash_primary
size: 0x2000
size: 0x2000 # 8 KiB
+8 -7
View File
@@ -5,8 +5,6 @@
CONFIG_GPIO=y
# Debug Message (RTT)
# Build-time switch for project DBG_PRINTF/DBG_CORE/DBG_ERR macros.
CONFIG_VESIS_DEBUG_PRINT=n
# Zephyr logging subsystem enable.
CONFIG_LOG=y
# Print logs immediately when log API is called.
@@ -94,8 +92,6 @@ CONFIG_BT_SMP_SC_ONLY=y
CONFIG_BT_SETTINGS=y
# Maximum number of stored bonded peers.
CONFIG_BT_MAX_PAIRED=20
# Production secure boot: disable flash patching so only signed flash code can run.
CONFIG_DISABLE_FLASH_PATCH=y
# ADC (battery)
# Enable Zephyr ADC driver for battery voltage measurement.
@@ -125,7 +121,7 @@ CONFIG_BOOTLOADER_MCUBOOT=y
# Enable zcbor library used by mcumgr/SMP/DFU CBOR payloads.
CONFIG_ZCBOR=y
# Version string used by MCUboot imgtool signing and versioned DFU zip naming.
CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION="0.0.0+0"
CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION="0.0.2+26"
# Internal flash
# Enable internal flash driver.
@@ -153,7 +149,7 @@ CONFIG_MCUMGR=y
# Enable mcumgr/SMP messages over BLE SMP service.
CONFIG_MCUMGR_TRANSPORT_BT=y
# Require authenticated BLE connection for mcumgr read/write operations.
CONFIG_MCUMGR_TRANSPORT_BT_PERM_RW_AUTHEN=y
CONFIG_MCUMGR_TRANSPORT_BT_PERM_RW_AUTHEN=n
# Reassemble SMP packets fragmented over BLE.
CONFIG_MCUMGR_TRANSPORT_BT_REASSEMBLY=y
# mcumgr transport receive buffer size for DFU image chunks.
@@ -181,4 +177,9 @@ CONFIG_MCUMGR_GRP_OS=y
# Enable image manager for MCUboot image state/verify/confirm operations.
CONFIG_IMG_MANAGER=y
# Write uploaded image data to flash as a stream.
CONFIG_STREAM_FLASH=y
CONFIG_STREAM_FLASH=y
# Netcore HCI IPC
# nRF52840 build does not use network core HCI IPC.
#CONFIG_SB_CONFIG_NETCORE_HCI_IPC=y
#SB_CONFIG_NETCORE_HCI_IPC=y
View File
View File
+233 -197
View File
@@ -1,4 +1,4 @@
/*******************************************************************************
/*******************************************************************************
* @file ble_service.c
* @brief BLE NUS service module
*
@@ -12,6 +12,8 @@
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/hci_vs.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
@@ -23,21 +25,40 @@
#include <errno.h>
#include "ble_service.h"
#include "ble_tx_power.h"
#include "main.h"
#include "debug_print.h"
#include "led_control.h"
LOG_MODULE_REGISTER(ble_svc, LOG_LEVEL_INF);
/* BLE service 상태 */
/* Module variables */
static struct bt_conn *current_conn;
static ble_data_rx_cb_t rx_callback;
static bool dfu_advertising_mode;
static bool advertising_unlimited;
static uint8_t conn_param_update_attempts;
static int64_t last_rx_conn_param_update_ms;
static uint16_t current_notify_mtu = 23U;
#define BLE_TX_POWER_NORMAL_DBM 4
#define BLE_TX_POWER_STRESS_DBM 8
#define BLE_RSSI_MONITOR_ENABLED 0
#define BLE_RSSI_STRESS_DBM (-80)
#define BLE_RSSI_STRESS_COUNT 3
#define BLE_RSSI_MONITOR_INTERVAL_MS 500
#define BLE_TX_SLOW_MS 50
#define BLE_TX_CRITICAL_MS 100
#define BLE_TX_STUCK_MS 200
#define BLE_TX_SLOW_COUNT 3
#define BLE_TX_BUSY_STRESS_RETRY 4
extern int bt_hci_get_conn_handle(const struct bt_conn *conn, uint16_t *conn_handle);
static uint16_t current_conn_handle;
static bool current_conn_handle_valid;
static int8_t current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
static uint8_t low_rssi_count;
static uint8_t slow_tx_count;
static struct k_work_delayable tx_power_monitor_work;
#if IS_ENABLED(CONFIG_BT_SMP)
#define BLE_REQUIRED_SECURITY_LEVEL BT_SECURITY_L4
@@ -56,63 +77,154 @@ static struct k_work adv_restart_work;
static struct k_work_delayable adv_timeout_work;
static struct k_work_delayable conn_param_update_work;
/*
* Store the UATT MTU used by normal GATT/NUS notifications.
* NUS notifications can carry application payload up to UATT MTU - 3 bytes.
*/
static void att_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx)
static int ble_tx_power_write(uint8_t handle_type, uint16_t handle, int8_t dbm,
const char *reason)
{
uint16_t uatt = bt_gatt_get_uatt_mtu(conn);
struct net_buf *buf;
struct net_buf *rsp;
struct bt_hci_cp_vs_write_tx_power_level *cp;
struct bt_hci_rp_vs_write_tx_power_level *rp;
int err;
if (uatt != 0U)
buf = bt_hci_cmd_alloc(K_NO_WAIT);
if (buf == NULL)
{
current_notify_mtu = uatt;
return -ENOBUFS;
}
cp = net_buf_add(buf, sizeof(*cp));
cp->handle_type = handle_type;
cp->handle = sys_cpu_to_le16(handle);
cp->tx_power_level = dbm;
err = bt_hci_cmd_send_sync(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, buf, &rsp);
if (err)
{
DBG_ERR("[BLE] TX power set fail reason=%s err=%d\r\n", reason, err);
return err;
}
rp = (void *)rsp->data;
if (rp->status != 0U)
{
err = -EIO;
DBG_ERR("[BLE] TX power set rejected reason=%s status=0x%02x\r\n",
reason, rp->status);
}
else
{
current_notify_mtu = (tx < rx) ? tx : rx;
current_tx_power_dbm = rp->selected_tx_power;
DBG_CORE("[BLE] TX power %d dBm reason=%s\r\n",
current_tx_power_dbm, reason);
}
DBG_PRINTF("[BLE] ATT MTU updated tx=%u rx=%u uatt=%u used=%u notify_payload=%u\r\n",
tx, rx, uatt, current_notify_mtu,
(current_notify_mtu > 3U) ? (current_notify_mtu - 3U) : 0U);
net_buf_unref(rsp);
return err;
}
/* MTU Exchange 연결 직후 요청 */
static void mtu_exchange_cb(struct bt_conn *conn, uint8_t err, struct bt_gatt_exchange_params *params)
static int ble_set_conn_tx_power(int8_t dbm, const char *reason)
{
uint16_t uatt;
if (!current_conn_handle_valid || current_tx_power_dbm == dbm)
{
return 0;
}
ARG_UNUSED(params);
return ble_tx_power_write(BT_HCI_VS_LL_HANDLE_TYPE_CONN,
current_conn_handle,
dbm,
reason ? reason : "unknown");
}
static void ble_tx_power_stress(const char *reason)
{
(void)ble_set_conn_tx_power(BLE_TX_POWER_STRESS_DBM, reason);
}
static int ble_read_conn_rssi(int8_t *rssi)
{
struct net_buf *buf;
struct net_buf *rsp;
struct bt_hci_cp_read_rssi *cp;
struct bt_hci_rp_read_rssi *rp;
int err;
if (!current_conn_handle_valid || rssi == NULL)
{
return -ENOTCONN;
}
buf = bt_hci_cmd_alloc(K_NO_WAIT);
if (buf == NULL)
{
return -ENOBUFS;
}
cp = net_buf_add(buf, sizeof(*cp));
cp->handle = sys_cpu_to_le16(current_conn_handle);
err = bt_hci_cmd_send_sync(BT_HCI_OP_READ_RSSI, buf, &rsp);
if (err)
{
DBG_ERR("[BLE] MTU exchange failed err=%u\r\n", err);
return err;
}
rp = (void *)rsp->data;
if (rp->status != 0U)
{
err = -EIO;
}
else
{
*rssi = rp->rssi;
}
net_buf_unref(rsp);
return err;
}
static void tx_power_monitor_handler(struct k_work *work)
{
int8_t rssi = 0;
int err;
ARG_UNUSED(work);
if (!current_conn_handle_valid)
{
return;
}
uatt = bt_gatt_get_uatt_mtu(conn);
if (uatt != 0U)
if (!BLE_RSSI_MONITOR_ENABLED)
{
current_notify_mtu = uatt;
return;
}
DBG_PRINTF("[BLE] MTU exchange complete uatt=%u notify_payload=%u\r\n",
uatt,
(uatt > 3U) ? (uatt - 3U) : 0U);
err = ble_read_conn_rssi(&rssi);
if (err)
{
ble_tx_power_stress("rssi-silence");
}
else if (rssi <= BLE_RSSI_STRESS_DBM)
{
if (low_rssi_count < UINT8_MAX)
{
low_rssi_count++;
}
if (low_rssi_count >= BLE_RSSI_STRESS_COUNT)
{
ble_tx_power_stress("rssi-low");
}
}
else
{
low_rssi_count = 0U;
}
k_work_schedule(&tx_power_monitor_work,
K_MSEC(BLE_RSSI_MONITOR_INTERVAL_MS));
}
static struct bt_gatt_exchange_params mtu_exchange_params =
{
.func = mtu_exchange_cb,
};
static struct bt_gatt_cb gatt_callbacks =
{
.att_mtu_updated = att_mtu_updated,
};
/* 로컬 BLE MAC 주소 로그 */
static void ble_log_local_identity(void)
{
bt_addr_le_t addr = {0};
@@ -131,18 +243,15 @@ static void ble_log_local_identity(void)
}
#if IS_ENABLED(CONFIG_BT_SMP)
/* NUS 접근 전 보안 level 확인 */
static bool ble_conn_is_secure(const struct bt_conn *conn)
{
return (conn != NULL) && (bt_conn_get_security(conn) >= BLE_REQUIRED_SECURITY_LEVEL);
}
/* 6자리 ASCII passkey를 숫자로 변환 */
static int ble_passkey_to_uint(const char *passkey, unsigned int *value)
{
unsigned int parsed = 0U;
// 입력 포인터 방어
if ((passkey == NULL) || (value == NULL))
{
return -EINVAL;
@@ -150,7 +259,6 @@ static int ble_passkey_to_uint(const char *passkey, unsigned int *value)
for (size_t i = 0; i < PASSKEY_LENGTH; i++)
{
// 숫자 6자리만 허용
if ((passkey[i] < '0') || (passkey[i] > '9'))
{
return -EINVAL;
@@ -163,7 +271,6 @@ static int ble_passkey_to_uint(const char *passkey, unsigned int *value)
return 0;
}
/* 저장된 passkey 검증, 실패 시 기본값 사용 */
static int ble_security_configure(void)
{
unsigned int passkey;
@@ -187,7 +294,6 @@ static int ble_security_configure(void)
return 0;
}
/* Just Works/passkey pairing 요청 자동 승인 */
static void auth_pairing_confirm(struct bt_conn *conn)
{
int err = bt_conn_auth_pairing_confirm(conn);
@@ -201,7 +307,6 @@ static void auth_pairing_confirm(struct bt_conn *conn)
DBG_CORE("[BLE] Pairing confirmed\r\n");
}
/* pairing 취소 로그 */
static void auth_cancel(struct bt_conn *conn)
{
ARG_UNUSED(conn);
@@ -209,7 +314,6 @@ static void auth_cancel(struct bt_conn *conn)
}
#if IS_ENABLED(CONFIG_BT_APP_PASSKEY)
/* Zephyr pairing callback에 현재 passkey 제공 */
static uint32_t auth_app_passkey(struct bt_conn *conn)
{
unsigned int passkey;
@@ -226,7 +330,6 @@ static uint32_t auth_app_passkey(struct bt_conn *conn)
}
#endif
/* pairing 처리 callback 묶음 */
static struct bt_conn_auth_cb auth_cb =
{
.pairing_confirm = auth_pairing_confirm,
@@ -236,28 +339,24 @@ static struct bt_conn_auth_cb auth_cb =
#endif
};
/* pairing 성공 로그 */
static void auth_pairing_complete(struct bt_conn *conn, bool bonded)
{
ARG_UNUSED(conn);
DBG_CORE("[BLE] Pairing complete bonded=%u\r\n", bonded ? 1U : 0U);
}
/* pairing 실패 시 연결 종료 */
static void auth_pairing_failed(struct bt_conn *conn, enum bt_security_err reason)
{
DBG_ERR("[BLE] Pairing failed reason=%u\r\n", reason);
(void)bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL);
}
/* pairing 결과 callback 묶음 */
static struct bt_conn_auth_info_cb auth_info_cb =
{
.pairing_complete = auth_pairing_complete,
.pairing_failed = auth_pairing_failed,
};
/* BLE 보안 callback 등록, passkey 설정 */
static int ble_security_init(void)
{
int err = bt_conn_auth_cb_register(&auth_cb);
@@ -279,7 +378,6 @@ static int ble_security_init(void)
}
#endif
/* callback context 밖에서 advertising 재시작 */
static void adv_restart_handler(struct k_work *work)
{
ARG_UNUSED(work);
@@ -287,30 +385,26 @@ static void adv_restart_handler(struct k_work *work)
led_set_state(LED_STATE_ADVERTISING);
}
/*
* 연결 해제 후 재광고 정책
* - 앱이 연결을 정상 종료(0x13)한 경우만 10분 타임아웃 후 자동 전원 OFF
* - 그 외(거리 이탈, 링크 타임아웃, 폰 전원 OFF, 페어링/보안 실패 등)는 재연결 대기를 위해 무제한 광고
*/
static bool ble_disconnect_reason_uses_unlimited_adv(uint8_t reason)
{
switch (reason)
{
case BT_HCI_ERR_REMOTE_USER_TERM_CONN: // 0x13 앱이 연결을 정상 종료
return false; // 10분 타임아웃 후 전원 OFF
case BT_HCI_ERR_CONN_TIMEOUT:
case BT_HCI_ERR_LL_RESP_TIMEOUT:
case BT_HCI_ERR_TERM_DUE_TO_MIC_FAIL:
case BT_HCI_ERR_CONN_FAIL_TO_ESTAB:
return true;
default:
return true; // 그 외 전부 → 무제한 광고
default:
return false;
}
}
/* 광고 timeout 시 자동 전원 OFF 판단 */
static void adv_timeout_handler(struct k_work *work)
{
ARG_UNUSED(work);
// 미연결 상태에서만 발동하므로 DFU 광고도 동일하게 10분 후 종료(연결 중이면 발동 안 함)
if (!ble_connection_st && !advertising_unlimited)
if (!ble_connection_st && !advertising_unlimited && !dfu_advertising_mode)
{
DBG_PRINTF("[BLE] Advertising timeout\r\n");
(void)ble_advertising_stop();
@@ -318,14 +412,14 @@ static void adv_timeout_handler(struct k_work *work)
}
}
/* 현재 연결 파라미터가 목표값인지 비교 */
static bool ble_conn_param_is_preferred(uint16_t interval, uint16_t latency,
uint16_t timeout)
{
return (interval == BLE_MIN_CONN_INTERVAL) && (latency == BLE_SLAVE_LATENCY) && (timeout == BLE_CONN_SUP_TIMEOUT);
return (interval == BLE_MIN_CONN_INTERVAL) &&
(latency == BLE_SLAVE_LATENCY) &&
(timeout == BLE_CONN_SUP_TIMEOUT);
}
/* 15ms interval 목표로 connection parameter update 요청 */
static int ble_request_preferred_conn_params(bool count_attempt)
{
struct bt_le_conn_param conn_param =
@@ -335,51 +429,22 @@ static int ble_request_preferred_conn_params(bool count_attempt)
.latency = BLE_SLAVE_LATENCY,
.timeout = BLE_CONN_SUP_TIMEOUT,
};
struct bt_conn_info info;
int err;
// 연결 없으면 요청 불가
if (current_conn == NULL)
{
return -ENOTCONN;
}
// 현재 체결된 파라미터를 조회해서, 이미 목표값이면 재요청/재시도 불필요
err = bt_conn_get_info(current_conn, &info);
if ((err == 0) && (info.type == BT_CONN_TYPE_LE))
{
uint16_t cur_interval = (uint16_t)(info.le.interval_us / 1250U);
uint16_t cur_latency = info.le.latency;
uint16_t cur_timeout = info.le.timeout;
DBG_PRINTF("[BLE] Current params: interval=%u(%u.%01ums) latency=%u timeout=%u(%ums)\r\n",
cur_interval,
(cur_interval * 125U) / 100U,
((cur_interval * 125U) % 100U) / 10U,
cur_latency,
cur_timeout,
cur_timeout * 10U);
if (ble_conn_param_is_preferred(cur_interval, cur_latency, cur_timeout))
{
DBG_PRINTF("[BLE] Already at preferred params, skip request\r\n");
return -EALREADY;
}
}
// 예약 재시도에서만 attempt 증가
if (count_attempt)
{
conn_param_update_attempts++;
}
DBG_PRINTF("[BLE] Request params: interval=%u(%u.%01ums) latency=%u timeout=%u(%ums) attempt=%u\r\n",
DBG_PRINTF("[BLE] Request params: interval=%u latency=%u timeout=%u attempt=%u\r\n",
BLE_MIN_CONN_INTERVAL,
(BLE_MIN_CONN_INTERVAL * 125U) / 100U,
((BLE_MIN_CONN_INTERVAL * 125U) % 100U) / 10U,
BLE_SLAVE_LATENCY,
BLE_CONN_SUP_TIMEOUT,
BLE_CONN_SUP_TIMEOUT * 10U,
conn_param_update_attempts);
err = bt_conn_le_param_update(current_conn, &conn_param);
@@ -392,7 +457,6 @@ static int ble_request_preferred_conn_params(bool count_attempt)
return 0;
}
/* connection parameter 재시도 work */
static void conn_param_update_handler(struct k_work *work)
{
ARG_UNUSED(work);
@@ -402,19 +466,21 @@ static void conn_param_update_handler(struct k_work *work)
return;
}
// central이 계속 거부하면 최대 횟수에서 중지
if (conn_param_update_attempts >= BLE_CONN_PARAM_MAX_ATTEMPTS)
{
return;
}
if (ble_request_preferred_conn_params(true) == 0 && conn_param_update_attempts < BLE_CONN_PARAM_MAX_ATTEMPTS)
if (ble_request_preferred_conn_params(true) == 0 &&
conn_param_update_attempts < BLE_CONN_PARAM_MAX_ATTEMPTS)
{
k_work_schedule(&conn_param_update_work, K_MSEC(BLE_CONN_PARAM_RETRY_INTERVAL_MS));
k_work_schedule(&conn_param_update_work,
K_MSEC(BLE_CONN_PARAM_RETRY_INTERVAL_MS));
}
}
/* Advertising scan response data */
/* Advertising data */
static const struct bt_data sd[] =
{
// 평상시 Web Bluetooth 앱이 찾을 수 있도록 NUS UUID 광고
@@ -427,7 +493,7 @@ static const struct bt_data sd_dfu[] =
BT_DATA_BYTES(BT_DATA_UUID128_SOME, SMP_BT_SVC_UUID_VAL),
};
/* BLE 연결 완료 처리 */
/* Connection callbacks */
static void connected(struct bt_conn *conn, uint8_t err)
{
if (err)
@@ -439,28 +505,24 @@ static void connected(struct bt_conn *conn, uint8_t err)
current_conn = bt_conn_ref(conn);
ble_connection_st = true;
ble_tx_power_on_connected(conn); // TX power/RSSI 상태는 별도 모듈에서 관리
current_conn_handle_valid = (bt_hci_get_conn_handle(conn, &current_conn_handle) == 0);
current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
low_rssi_count = 0U;
slow_tx_count = 0U;
(void)ble_set_conn_tx_power(BLE_TX_POWER_NORMAL_DBM, "connected");
if (BLE_RSSI_MONITOR_ENABLED)
{
k_work_schedule(&tx_power_monitor_work, K_MSEC(BLE_RSSI_MONITOR_INTERVAL_MS));
}
/* 연결 정보 출력 */
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr_str, sizeof(addr_str));
DBG_CORE("[BLE] Peer: %s\r\n", addr_str); // 연결 정보 출력
DBG_CORE("[BLE] Peer: %s\r\n", addr_str);
err = bt_gatt_exchange_mtu(conn, &mtu_exchange_params);
if (err)
{
DBG_ERR("[BLE] MTU exchange request failed err=%d\r\n", err);
}
else
{
DBG_PRINTF("[BLE] MTU exchange requested\r\n");
}
conn_param_update_attempts = 0U;
last_rx_conn_param_update_ms = 0;
// 연결 직후 약간 기다린 뒤 preferred parameter 요청
k_work_schedule(&conn_param_update_work, K_MSEC(BLE_CONN_PARAM_FIRST_DELAY_MS));
#if IS_ENABLED(CONFIG_BT_SMP)
err = bt_conn_set_security(conn, BLE_REQUIRED_SECURITY_LEVEL);
if (err)
@@ -476,13 +538,16 @@ static void connected(struct bt_conn *conn, uint8_t err)
DBG_CORE("[BLE] Connected\r\n");
}
/* BLE 연결 해제 처리 */
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
k_work_cancel_delayable(&conn_param_update_work);
ble_tx_power_on_disconnected();
k_work_cancel_delayable(&tx_power_monitor_work);
conn_param_update_attempts = 0U;
last_rx_conn_param_update_ms = 0;
current_conn_handle_valid = false;
current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
low_rssi_count = 0U;
slow_tx_count = 0U;
if (current_conn)
{
@@ -491,16 +556,7 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
}
ble_connection_st = false;
current_notify_mtu = 23U;
if (dfu_advertising_mode)
{
// DFU 광고는 미연결 10분 타임아웃 적용
advertising_unlimited = false;
}
else
{
advertising_unlimited = ble_disconnect_reason_uses_unlimited_adv(reason);
}
advertising_unlimited = ble_disconnect_reason_uses_unlimited_adv(reason);
DBG_CORE("[BLE] Disconnected reason=0x%02x adv=%s\r\n",
reason,
advertising_unlimited ? "unlimited" : "10min");
@@ -509,7 +565,6 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
k_work_submit(&adv_restart_work);
}
/* central의 connection parameter 요청 허용 */
static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
{
ARG_UNUSED(conn);
@@ -518,33 +573,23 @@ static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
return true;
}
/* connection parameter 변경 결과 확인 */
static void le_param_updated(struct bt_conn *conn, uint16_t interval, uint16_t latency, uint16_t timeout)
static void le_param_updated(struct bt_conn *conn, uint16_t interval,
uint16_t latency, uint16_t timeout)
{
ARG_UNUSED(conn);
DBG_PRINTF("[BLE] Params: interval=%u(%u.%01ums) latency=%u timeout=%u(%ums)\r\n",
interval,
(interval * 125U) / 100U,
((interval * 125U) % 100U) / 10U,
latency,
timeout,
timeout * 10U);
DBG_PRINTF("[BLE] Params: interval=%d latency=%d timeout=%d\r\n",
interval, latency, timeout);
// 목표값 도달 시 재시도 work 중지
if (ble_conn_param_is_preferred(interval, latency, timeout))
{
k_work_cancel_delayable(&conn_param_update_work);
}
else if (conn_param_update_attempts < BLE_CONN_PARAM_MAX_ATTEMPTS)
{
k_work_schedule(&conn_param_update_work, K_MSEC(BLE_CONN_PARAM_RETRY_INTERVAL_MS));
}
}
#if IS_ENABLED(CONFIG_BT_SMP)
/* BLE security level 변경 결과 확인 */
static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
static void security_changed(struct bt_conn *conn, bt_security_t level,
enum bt_security_err err)
{
ARG_UNUSED(conn);
@@ -569,7 +614,7 @@ BT_CONN_CB_DEFINE(conn_callbacks) =
#endif
};
/* NUS RX: 보안 확인 후 app parser로 전달 */
/* NUS callbacks */
static void nus_received(struct bt_conn *conn, const uint8_t *data, uint16_t len)
{
#if IS_ENABLED(CONFIG_BT_SMP)
@@ -597,7 +642,6 @@ static void nus_received(struct bt_conn *conn, const uint8_t *data, uint16_t len
}
}
/* NUS TX 완료 callback */
static void nus_sent(struct bt_conn *conn)
{
ARG_UNUSED(conn);
@@ -612,7 +656,7 @@ static struct bt_nus_cb nus_cb =
.sent = nus_sent,
};
/* BLE stack, security, NUS service 초기화 */
/* Public functions */
int ble_service_init(ble_data_rx_cb_t rx_cb)
{
int err;
@@ -624,8 +668,9 @@ int ble_service_init(ble_data_rx_cb_t rx_cb)
k_work_init(&adv_restart_work, adv_restart_handler);
k_work_init_delayable(&adv_timeout_work, adv_timeout_handler);
k_work_init_delayable(&conn_param_update_work, conn_param_update_handler);
ble_tx_power_init();
// BLE stack 활성화
k_work_init_delayable(&tx_power_monitor_work, tx_power_monitor_handler);
// Enable BLE stack
err = bt_enable(NULL);
if (err)
{
@@ -646,7 +691,6 @@ int ble_service_init(ble_data_rx_cb_t rx_cb)
ble_log_local_identity();
bt_gatt_cb_register(&gatt_callbacks);
#if IS_ENABLED(CONFIG_BT_SMP)
err = ble_security_init();
if (err)
@@ -655,7 +699,7 @@ int ble_service_init(ble_data_rx_cb_t rx_cb)
}
#endif
// NUS service 초기화
// Initialize NUS
err = bt_nus_init(&nus_cb);
if (err)
{
@@ -667,7 +711,6 @@ int ble_service_init(ble_data_rx_cb_t rx_cb)
return 0;
}
/* 현재 모드에 맞는 advertising 시작 */
int ble_advertising_start(void)
{
int err;
@@ -701,10 +744,8 @@ int ble_advertising_start(void)
return err;
}
ble_tx_power_apply_advertising();
k_work_cancel_delayable(&adv_timeout_work);
if (!advertising_unlimited) // DFU 광고 포함, 미연결 10분 시 전원 OFF
if (!advertising_unlimited && !dfu_advertising_mode)
{
k_work_schedule(&adv_timeout_work, K_MSEC(APP_ADV_DURATION * 10));
}
@@ -713,7 +754,6 @@ int ble_advertising_start(void)
return 0;
}
/* advertising 중지, timeout work 취소 */
int ble_advertising_stop(void)
{
int err = bt_le_adv_stop();
@@ -728,7 +768,6 @@ int ble_advertising_stop(void)
return 0;
}
/* DFU 모드 진입, SMP advertising 준비 */
int ble_dfu_advertising_enable(void)
{
if (dfu_advertising_mode)
@@ -756,25 +795,11 @@ int ble_dfu_advertising_enable(void)
return ble_advertising_start();
}
/* DFU 진입용 disconnect */
bool ble_dfu_advertising_is_enabled(void)
{
return dfu_advertising_mode;
}
/* DFU watchdog: 현재 연결 강제 disconnect */
int ble_disconnect_active(void)
{
// 연결 없으면 요청 불가
if (current_conn == NULL)
{
return -ENOTCONN;
}
return bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
/* NUS TX: 직렬화, retry, 완료 대기 */
int ble_data_send(const uint8_t *data, uint16_t len)
{
int err;
@@ -798,9 +823,7 @@ int ble_data_send(const uint8_t *data, uint16_t len)
k_mutex_lock(&nus_tx_lock, K_FOREVER);
// 혹시 남아 있는 완료 신호가 있으면 비우고 이번 전송 전용 상태로 만듦
while (k_sem_take(&nus_tx_done_sem, K_NO_WAIT) == 0)
{
// stale completion 비우기
while (k_sem_take(&nus_tx_done_sem, K_NO_WAIT) == 0) {
}
//DBG_CORE("[NUS TX] send len=%u\r\n", len);
@@ -808,17 +831,19 @@ int ble_data_send(const uint8_t *data, uint16_t len)
for (int retry = 0; ; retry++)
{
err = bt_nus_send(current_conn, data, len);
// -ENOMEM만 짧게 재시도, 그 외 error는 즉시 종료
if (err != -ENOMEM || retry >= 20)
{
if (err != -ENOMEM || retry >= 20) {
break;
}
// -ENOMEM : BLE TX 버퍼가 가득찬 경우: 바로 포기하지 않고 짧게 쉬었다가 다시 시도
/*
* -ENOMEM : BLE TX 버퍼가 가득참
* 바로 포기하지 않고 짧게 쉬었다가 다시 시도
*/
DBG_ERR("[NUS TX] busy, retry=%d\r\n", retry + 1);
// busy 반복 시 TX power boost 판단
ble_tx_power_on_tx_busy_retry(retry + 1);
if ((retry + 1) >= BLE_TX_BUSY_STRESS_RETRY)
{
ble_tx_power_stress("tx-queue-busy");
}
k_msleep(5);
}
@@ -830,12 +855,11 @@ int ble_data_send(const uint8_t *data, uint16_t len)
}
// bt_nus_send() 성공 후 sent 콜백이 올 때까지 기다림 (다음 패킷을 보내도 되는 시점)
err = k_sem_take(&nus_tx_done_sem, K_MSEC(ble_tx_power_tx_stuck_ms()));
err = k_sem_take(&nus_tx_done_sem, K_MSEC(BLE_TX_STUCK_MS));
if (err)
{
// sent callback 지연도 링크 stress로 처리
ble_tx_power_on_tx_complete_timeout();
err = k_sem_take(&nus_tx_done_sem, K_MSEC(1000 - ble_tx_power_tx_stuck_ms()));
ble_tx_power_stress("tx-complete-stuck");
err = k_sem_take(&nus_tx_done_sem, K_MSEC(1000 - BLE_TX_STUCK_MS));
if (err)
{
DBG_ERR("[NUS TX] completion timeout (err %d)\r\n", err);
@@ -846,22 +870,34 @@ int ble_data_send(const uint8_t *data, uint16_t len)
{
int64_t tx_elapsed_ms = k_uptime_get() - tx_start_ms;
// 완료 시간 기반 TX power 복구/boost 판단
ble_tx_power_on_tx_complete(tx_elapsed_ms);
if (tx_elapsed_ms >= BLE_TX_CRITICAL_MS)
{
ble_tx_power_stress("tx-complete-slow100");
slow_tx_count = 0U;
}
else if (tx_elapsed_ms >= BLE_TX_SLOW_MS)
{
if (slow_tx_count < UINT8_MAX)
{
slow_tx_count++;
}
if (slow_tx_count >= BLE_TX_SLOW_COUNT)
{
ble_tx_power_stress("tx-complete-slow50");
}
}
else
{
slow_tx_count = 0U;
}
}
k_mutex_unlock(&nus_tx_lock);
return err;
}
/* BLE 연결 상태 */
bool ble_is_connected(void)
{
return (current_conn != NULL);
}
/* ATT MTU used to calculate notify payload size */
uint16_t ble_current_mtu(void)
{
return current_notify_mtu;
}
-2
View File
@@ -31,10 +31,8 @@ int ble_service_init(ble_data_rx_cb_t rx_cb);
int ble_advertising_start(void);
int ble_advertising_stop(void);
int ble_data_send(const uint8_t *data, uint16_t len);
uint16_t ble_current_mtu(void);
int ble_dfu_advertising_enable(void);
bool ble_dfu_advertising_is_enabled(void);
bool ble_is_connected(void);
int ble_disconnect_active(void);
#endif /* BLE_SERVICE_H__ */
-364
View File
@@ -1,364 +0,0 @@
/*******************************************************************************
* @file ble_tx_power.c
* @brief BLE TX power and RSSI monitor helper
******************************************************************************/
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/hci_vs.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/sys/byteorder.h>
#include <errno.h>
#include "ble_tx_power.h"
#include "debug_print.h"
#define BLE_TX_POWER_NORMAL_DBM 4 // NORMAL: 평상시에는 전류 소모를 낮추기 위해 +4 dBm을 기본값으로 사용
#define BLE_TX_POWER_STRESS_DBM 8 // BOOST: RSSI 또는 TX 지연이 나빠지면 일시적으로 +8 dBm으로 boost
#define BLE_RSSI_MONITOR_ENABLED 1
#define BLE_RSSI_STRESS_DBM (-80)
#define BLE_RSSI_STRESS_COUNT 3
#define BLE_RSSI_MONITOR_INTERVAL_MS 500
#define BLE_TX_SLOW_MS 50
#define BLE_TX_CRITICAL_MS 100
#define BLE_TX_STUCK_MS 200
#define BLE_TX_SLOW_COUNT 3
#define BLE_TX_BUSY_STRESS_RETRY 4
#define BLE_TX_RECOVER_HOLD_MS 10000
extern int bt_hci_get_conn_handle(const struct bt_conn *conn, uint16_t *conn_handle);
/* HCI vendor command는 bt_conn 포인터 대신 controller handle을 사용 */
static uint16_t current_conn_handle;
static bool current_conn_handle_valid;
static int8_t current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
static uint8_t low_rssi_count;
static uint8_t slow_tx_count;
static int64_t last_stress_ms;
static struct k_work_delayable tx_power_monitor_work;
/* Nordic vendor HCI command로 advertising/connection TX power 직접 설정 */
static int ble_tx_power_write(uint8_t handle_type, uint16_t handle, int8_t dbm, const char *reason)
{
struct net_buf *buf;
struct net_buf *rsp;
struct bt_hci_cp_vs_write_tx_power_level *cp;
struct bt_hci_rp_vs_write_tx_power_level *rp;
int err;
buf = bt_hci_cmd_alloc(K_NO_WAIT);
if (buf == NULL)
{
return -ENOBUFS;
}
cp = net_buf_add(buf, sizeof(*cp));
cp->handle_type = handle_type;
cp->handle = sys_cpu_to_le16(handle);
cp->tx_power_level = dbm;
err = bt_hci_cmd_send_sync(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, buf, &rsp);
if (err)
{
DBG_ERR("[BLE] TX power set fail reason=%s err=%d\r\n", reason, err);
return err;
}
rp = (void *)rsp->data;
if (rp->status != 0U)
{
err = -EIO;
DBG_ERR("[BLE] TX power set rejected reason=%s status=0x%02x\r\n", reason, rp->status);
}
else
{
if (handle_type == BT_HCI_VS_LL_HANDLE_TYPE_CONN)
{
current_tx_power_dbm = rp->selected_tx_power;
}
DBG_CORE("[BLE] TX power %d dBm reason=%s\r\n", rp->selected_tx_power, reason);
}
net_buf_unref(rsp);
return err;
}
/* 연결 handle이 유효할 때만 connection TX power 변경 */
static int ble_set_conn_tx_power(int8_t dbm, const char *reason)
{
// 연결 전이거나 이미 목표 출력이면 HCI 명령을 보내지 않음
if (!current_conn_handle_valid || current_tx_power_dbm == dbm)
{
return 0;
}
return ble_tx_power_write(BT_HCI_VS_LL_HANDLE_TYPE_CONN, current_conn_handle, dbm, reason ? reason : "unknown");
}
static void ble_tx_power_stress(const char *reason)
{
// stress 발생 시각을 기록해서 이후 NORMAL 복귀 시점 판단
last_stress_ms = k_uptime_get();
(void)ble_set_conn_tx_power(BLE_TX_POWER_STRESS_DBM, reason);
}
/* 마지막 stress 이후 일정 시간 stress 없는 경우 NORMAL 출력으로 회복 */
static void ble_tx_power_try_recover(void)
{
// 이미 NORMAL이면 불필요한 HCI 명령 피함
if ((current_tx_power_dbm != BLE_TX_POWER_NORMAL_DBM) &&
((k_uptime_get() - last_stress_ms) >= BLE_TX_RECOVER_HOLD_MS))
{
(void)ble_set_conn_tx_power(BLE_TX_POWER_NORMAL_DBM, "link-recovered");
}
}
/* 컨트롤러에 실제 적용된 연결 TX power 확인 */
static int ble_read_conn_tx_power(int8_t *dbm)
{
struct net_buf *buf;
struct net_buf *rsp;
struct bt_hci_cp_vs_read_tx_power_level *cp;
struct bt_hci_rp_vs_read_tx_power_level *rp;
int err;
// 연결 handle 없이는 controller에 연결 TX power 조회 불가
if (!current_conn_handle_valid || dbm == NULL)
{
return -ENOTCONN;
}
buf = bt_hci_cmd_alloc(K_NO_WAIT);
if (buf == NULL)
{
return -ENOBUFS;
}
cp = net_buf_add(buf, sizeof(*cp));
cp->handle_type = BT_HCI_VS_LL_HANDLE_TYPE_CONN;
cp->handle = sys_cpu_to_le16(current_conn_handle);
err = bt_hci_cmd_send_sync(BT_HCI_OP_VS_READ_TX_POWER_LEVEL, buf, &rsp);
if (err)
{
return err;
}
rp = (void *)rsp->data;
if (rp->status != 0U)
{
err = -EIO;
}
else
{
*dbm = rp->tx_power_level;
}
net_buf_unref(rsp);
return err;
}
/* 현재 연결의 RSSI를 읽어 링크 품질 판단에 사용 */
static int ble_read_conn_rssi(int8_t *rssi)
{
struct net_buf *buf;
struct net_buf *rsp;
struct bt_hci_cp_read_rssi *cp;
struct bt_hci_rp_read_rssi *rp;
int err;
// RSSI 조회도 연결 handle이 있어야 가능
if (!current_conn_handle_valid || rssi == NULL)
{
return -ENOTCONN;
}
buf = bt_hci_cmd_alloc(K_NO_WAIT);
if (buf == NULL)
{
return -ENOBUFS;
}
cp = net_buf_add(buf, sizeof(*cp));
cp->handle = sys_cpu_to_le16(current_conn_handle);
err = bt_hci_cmd_send_sync(BT_HCI_OP_READ_RSSI, buf, &rsp);
if (err)
{
return err;
}
rp = (void *)rsp->data;
if (rp->status != 0U)
{
err = -EIO;
}
else
{
*rssi = rp->rssi;
}
net_buf_unref(rsp);
return err;
}
/* 연결 중에는 주기적으로 RSSI를 확인하고, 필요하면 TX power boost */
static void tx_power_monitor_handler(struct k_work *work)
{
int8_t rssi = 0;
int err;
ARG_UNUSED(work);
if (!current_conn_handle_valid)
{
return;
}
if (!BLE_RSSI_MONITOR_ENABLED)
{
return;
}
err = ble_read_conn_rssi(&rssi);
// RSSI를 못 읽는 상황도 링크 품질 저하로 간주
if (err)
{
ble_tx_power_stress("rssi-silence");
}
else if (rssi <= BLE_RSSI_STRESS_DBM)
{
// 순간적인 RSSI 흔들림은 무시하고, 연속 low RSSI일 때만 boost
if (low_rssi_count < UINT8_MAX)
{
low_rssi_count++;
}
if (low_rssi_count >= BLE_RSSI_STRESS_COUNT)
{
ble_tx_power_stress("rssi-low");
}
}
else
{
// RSSI가 회복되면 연속 low RSSI 카운터 초기화
low_rssi_count = 0U;
}
// TX가 없어도 monitor 주기마다 NORMAL 복귀 여부 확인
ble_tx_power_try_recover();
// 연결이 유지되는 동안 monitor work 계속 반복
k_work_schedule(&tx_power_monitor_work, K_MSEC(BLE_RSSI_MONITOR_INTERVAL_MS));
}
/* RSSI monitor용 delayable work 초기화 */
void ble_tx_power_init(void)
{
k_work_init_delayable(&tx_power_monitor_work, tx_power_monitor_handler);
}
/* 연결 직후 TX power 상태를 초기화하고 RSSI monitor를 시작 */
void ble_tx_power_on_connected(struct bt_conn *conn)
{
int8_t cur_txp = 0;
int txp_ret;
// 이후 HCI 명령에서 사용할 controller connection handle 저장
current_conn_handle_valid = (bt_hci_get_conn_handle(conn, &current_conn_handle) == 0);
current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
low_rssi_count = 0U;
slow_tx_count = 0U;
last_stress_ms = k_uptime_get();
if (current_conn_handle_valid)
{
// 새 연결은 항상 NORMAL (4 dBm) 시작
(void)ble_tx_power_write(BT_HCI_VS_LL_HANDLE_TYPE_CONN, current_conn_handle, BLE_TX_POWER_NORMAL_DBM, "connected");
}
txp_ret = ble_read_conn_tx_power(&cur_txp); // 실제 적용값
if (txp_ret == 0)
{
DBG_CORE("[BLE] TX power (current) = %d dBm\r\n", cur_txp);
}
else
{
DBG_CORE("[BLE] TX power read fail ret=%d\r\n", txp_ret);
}
if (BLE_RSSI_MONITOR_ENABLED)
{
// 연결이 유지되는 동안만 RSSI monitor
k_work_schedule(&tx_power_monitor_work, K_MSEC(BLE_RSSI_MONITOR_INTERVAL_MS));
}
}
/* 연결 해제 시 monitor와 링크별 상태 초기화 */
void ble_tx_power_on_disconnected(void)
{
k_work_cancel_delayable(&tx_power_monitor_work);
current_conn_handle_valid = false;
current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
low_rssi_count = 0U;
slow_tx_count = 0U;
last_stress_ms = 0;
}
/* advertising handle의 TX power를 NORMAL 값으로 설정 */
void ble_tx_power_apply_advertising(void)
{
(void)ble_tx_power_write(BT_HCI_VS_LL_HANDLE_TYPE_ADV, 0, BLE_TX_POWER_NORMAL_DBM, "adv");
}
/* ble_service.c가 TX 완료 대기 timeout을 TX power 정책값과 공유 */
uint32_t ble_tx_power_tx_stuck_ms(void)
{
return BLE_TX_STUCK_MS;
}
/* NUS TX queue가 여러 번 busy이면 링크가 불안정한 상황으로 보고 출력 boost */
void ble_tx_power_on_tx_busy_retry(uint8_t retry_count)
{
if (retry_count >= BLE_TX_BUSY_STRESS_RETRY)
{
ble_tx_power_stress("tx-queue-busy");
}
}
/* sent callback이 늦게 오면 TX path가 막힌 것으로 보고 boost */
void ble_tx_power_on_tx_complete_timeout(void)
{
ble_tx_power_stress("tx-complete-stuck");
}
/* TX 완료 시간이 길어지는 것도 링크 품질 저하 신호로 사용 */
void ble_tx_power_on_tx_complete(int64_t elapsed_ms)
{
if (elapsed_ms >= BLE_TX_CRITICAL_MS)
{
// 100ms 이상이면 단발성이라도 즉시 boost
ble_tx_power_stress("tx-complete-slow100");
slow_tx_count = 0U;
}
else if (elapsed_ms >= BLE_TX_SLOW_MS)
{
// 50ms 이상은 연속 발생할 때만 boost
if (slow_tx_count < UINT8_MAX)
{
slow_tx_count++;
}
if (slow_tx_count >= BLE_TX_SLOW_COUNT)
{
ble_tx_power_stress("tx-complete-slow50");
}
}
else
{
// 정상 속도로 전송되면 slow 카운터 지우고 복귀 가능성 확인
slow_tx_count = 0U;
ble_tx_power_try_recover();
}
}
-22
View File
@@ -1,22 +0,0 @@
/*******************************************************************************
* @file ble_tx_power.h
* @brief BLE TX power and RSSI monitor helper
******************************************************************************/
#ifndef BLE_TX_POWER_H__
#define BLE_TX_POWER_H__
#include <stdint.h>
struct bt_conn;
void ble_tx_power_init(void);
void ble_tx_power_on_connected(struct bt_conn *conn);
void ble_tx_power_on_disconnected(void);
void ble_tx_power_apply_advertising(void);
uint32_t ble_tx_power_tx_stuck_ms(void);
void ble_tx_power_on_tx_busy_retry(uint8_t retry_count);
void ble_tx_power_on_tx_complete_timeout(void);
void ble_tx_power_on_tx_complete(int64_t elapsed_ms);
#endif /* BLE_TX_POWER_H__ */
-97
View File
@@ -1,97 +0,0 @@
/*******************************************************************************
* @file ble_cmd_queue.c
* @brief BLE RX command queue
******************************************************************************/
#include <stdint.h>
#include <string.h>
#include <zephyr/kernel.h>
#include "ble_cmd_queue.h"
#include "debug_print.h"
#include "parser.h"
#define BLE_CMD_MAX_LEN 256
#define BLE_CMD_QUEUE_DEPTH 8 // RX command ring buffer depth
#define BLE_CMD_WORKQ_STACK_SZ 8192
#define BLE_CMD_WORKQ_PRIORITY 10
static struct k_work_q ble_cmd_work_q;
K_THREAD_STACK_DEFINE(ble_cmd_workq_stack, BLE_CMD_WORKQ_STACK_SZ);
static struct k_work ble_cmd_work; // BLE command worker
static struct k_spinlock ble_cmd_lock; // RX queue lock
static uint8_t ble_cmd_q_buf[BLE_CMD_QUEUE_DEPTH][BLE_CMD_MAX_LEN]; // command data
static uint16_t ble_cmd_q_len[BLE_CMD_QUEUE_DEPTH]; // command length
static uint8_t ble_cmd_q_head; // next pop slot
static uint8_t ble_cmd_q_tail; // next push slot
static uint8_t ble_cmd_q_count; // queued command count
/* Queue에 쌓인 BLE 명령을 parser로 전달 */
static void ble_cmd_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
uint8_t local_buf[BLE_CMD_MAX_LEN];
uint16_t local_len;
for (;;)
{
k_spinlock_key_t key = k_spin_lock(&ble_cmd_lock);
if (ble_cmd_q_count == 0U)
{
k_spin_unlock(&ble_cmd_lock, key);
break;
}
local_len = ble_cmd_q_len[ble_cmd_q_head];
memcpy(local_buf, ble_cmd_q_buf[ble_cmd_q_head], local_len);
ble_cmd_q_head = (uint8_t)((ble_cmd_q_head + 1U) % BLE_CMD_QUEUE_DEPTH);
ble_cmd_q_count--;
k_spin_unlock(&ble_cmd_lock, key);
if (local_len == 0U)
{
DBG_ERR("[BLE RX] worker: empty\r\n");
continue;
}
ble_cmd_dispatch(local_buf, local_len);
}
}
/* BLE RX 명령 처리 queue 초기화 */
void ble_cmd_queue_init(void)
{
k_work_init(&ble_cmd_work, ble_cmd_work_handler);
k_work_queue_start(&ble_cmd_work_q, ble_cmd_workq_stack, K_THREAD_STACK_SIZEOF(ble_cmd_workq_stack), BLE_CMD_WORKQ_PRIORITY, NULL);
}
/* BLE RX 콜백에서 호출되는 명령 enqueue 함수 */
void ble_cmd_queue_rx_handler(const uint8_t *data, uint16_t len)
{
if (len > BLE_CMD_MAX_LEN)
{
DBG_ERR("[BLE RX] drop: len=%u exceeds %d\r\n", len, BLE_CMD_MAX_LEN);
return;
}
k_spinlock_key_t key = k_spin_lock(&ble_cmd_lock);
if (ble_cmd_q_count >= BLE_CMD_QUEUE_DEPTH)
{
k_spin_unlock(&ble_cmd_lock, key);
DBG_ERR("[BLE RX] drop: queue full\r\n");
return;
}
memcpy(ble_cmd_q_buf[ble_cmd_q_tail], data, len);
ble_cmd_q_len[ble_cmd_q_tail] = len;
ble_cmd_q_tail = (uint8_t)((ble_cmd_q_tail + 1U) % BLE_CMD_QUEUE_DEPTH);
ble_cmd_q_count++;
k_spin_unlock(&ble_cmd_lock, key);
int err = k_work_submit_to_queue(&ble_cmd_work_q, &ble_cmd_work);
if (err < 0)
{
DBG_ERR("[BLE RX] queue submit fail err=%d\r\n", err);
}
}
-13
View File
@@ -1,13 +0,0 @@
/*******************************************************************************
* @file ble_cmd_queue.h
* @brief BLE RX command queue
******************************************************************************/
#ifndef BLE_CMD_QUEUE_H__
#define BLE_CMD_QUEUE_H__
#include <stdint.h>
void ble_cmd_queue_init(void);
void ble_cmd_queue_rx_handler(const uint8_t *data, uint16_t len);
#endif /* BLE_CMD_QUEUE_H__ */
+107 -202
View File
@@ -4,8 +4,8 @@
******************************************************************************/
#include <zephyr/kernel.h>
#include <zephyr/sys/reboot.h>
#include <zephyr/sys/util.h>
#include <string.h>
#include <errno.h>
#include "cmd_common.h"
#include "main.h"
@@ -23,14 +23,6 @@ static uint8_t tx_id_buf[4 + HW_NO_LENGTH + SERIAL_NO_LENGTH + SERIAL_NO_LENGTH
static uint8_t tx_rim_buf[4 + 2 + (IMU_FIFO_RIM_TARGET_SAMPLES * IMU_FIFO_SAMPLE_BYTES) + 2];
static uint8_t g_echo_session;
/* ATT MTU에 따른 패킷 분할 */
#define ATT_NOTIFY_HEADER_SIZE 3U
#define REB_OVERHEAD_SIZE 10U
#define REC_OVERHEAD_SIZE 14U
#define ECHO_SAMPLE_SIZE 2U
#define RIM_OVERHEAD_SIZE 8U
#define RIC_OVERHEAD_SIZE 12U
uint8_t cmd_next_echo_session(void)
{
return g_echo_session++;
@@ -194,27 +186,7 @@ int cmd_send_response_imu(const int16_t accel[3], const int16_t gyro[3])
return ble_data_send(buf, 18);
}
/* ATT MTU에 따라 패킷 분할이 필요한지 계산 */
static uint16_t calc_rec_samples_per_packet(uint16_t notify_payload_max)
{
uint16_t samples_per_pkt = 1U;
// rec:는 고정 오버헤드가 14B, chunk 크기 다시 계산
if (notify_payload_max > REC_OVERHEAD_SIZE)
{
samples_per_pkt = (uint16_t)((notify_payload_max - REC_OVERHEAD_SIZE) / ECHO_SAMPLE_SIZE);
}
// 방어코드
if (samples_per_pkt == 0U)
{
samples_per_pkt = 1U;
}
return samples_per_pkt;
}
static void cmd_send_response_echo_reb(uint8_t session, uint8_t channel, const uint16_t *samples, uint16_t num_samples)
void cmd_send_response_echo(uint8_t session, uint8_t channel, const uint16_t *samples, uint16_t num_samples)
{
// 정적 버퍼 사용 (한 번에 한 명령만 처리)
uint8_t *buf = tx_echo_buf;
@@ -239,87 +211,6 @@ static void cmd_send_response_echo_reb(uint8_t session, uint8_t channel, const u
ble_data_send(buf, payload_len + 2);
}
/*
* rec: packet layout:
* tag (4B): "rec:"
* session (1B): 측정 세션 번호
* channel (1B): piezo 채널 번호
* offset (2B): 전체 샘플 배열에서 시작 인덱스
* total_samples (2B): 전체 샘플 수
* chunk_samples (2B): 현재 패킷에 들어간 샘플 수
* sample data (N): Piezo ADC samples
* crc (2B): tag부터 sample data까지 CRC16
*/
static void cmd_send_response_echo_rec(uint8_t session, uint8_t channel, const uint16_t *samples, uint16_t num_samples, uint16_t samples_per_pkt)
{
uint8_t *buf = tx_echo_buf;
for (uint16_t off = 0U; off < num_samples; off = (uint16_t)(off + samples_per_pkt))
{
uint16_t remain = (uint16_t)(num_samples - off);
uint16_t chunk_samples = MIN(samples_per_pkt, remain);
buf[0] = 'r'; buf[1] = 'e'; buf[2] = 'c'; buf[3] = ':';
buf[4] = session;
buf[5] = channel;
buf[6] = (uint8_t)(off >> 8);
buf[7] = (uint8_t)(off & 0xFF);
buf[8] = (uint8_t)(num_samples >> 8);
buf[9] = (uint8_t)(num_samples & 0xFF);
buf[10] = (uint8_t)(chunk_samples >> 8);
buf[11] = (uint8_t)(chunk_samples & 0xFF);
// 마지막 chunk는 samples_per_pkt보다 작을 수 있음
for (uint16_t i = 0U; i < chunk_samples; i++)
{
buf[12 + i * 2] = (uint8_t)(samples[off + i] >> 8);
buf[13 + i * 2] = (uint8_t)(samples[off + i] & 0xFF);
}
uint16_t payload_len = (uint16_t)(REC_OVERHEAD_SIZE - 2U + (chunk_samples * ECHO_SAMPLE_SIZE));
uint16_t crc = cmd_crc16_compute(buf, payload_len);
DBG_PRINTF("[CMD] rec tx ch=%u off=%u total=%u chunk=%u len=%u\r\n",
channel, off, num_samples, chunk_samples, (uint16_t)(payload_len + 2U));
buf[payload_len] = (uint8_t)(crc & 0xFF);
buf[payload_len + 1U] = (uint8_t)(crc >> 8);
// ble_data_send()는 NUS sent 콜백을 기다리고 다음 chunk 전송
if (ble_data_send(buf, (uint16_t)(payload_len + 2U)) != 0)
{
break;
}
}
}
/* Piezo echo ADC 응답 패킷 */
void cmd_send_response_echo(uint8_t session, uint8_t channel, const uint16_t *samples, uint16_t num_samples)
{
uint16_t notify_payload_max = 0U;
uint16_t mtu = ble_current_mtu();
uint16_t reb_len = (uint16_t)(REB_OVERHEAD_SIZE + (num_samples * ECHO_SAMPLE_SIZE));
// 협상된 ATT MTU에서 최대 payload
if (mtu > ATT_NOTIFY_HEADER_SIZE)
{
notify_payload_max = (uint16_t)(mtu - ATT_NOTIFY_HEADER_SIZE);
}
// 분할이 필요 없는 경우(기존 reb: 패킷이 MTU 안에 들어가는 경우) reb: 응답
if ((notify_payload_max == 0U) || (reb_len <= notify_payload_max))
{
cmd_send_response_echo_reb(session, channel, samples, num_samples);
return;
}
// 기존 reb: 패킷이 현재 MTU 보다 큰 경우 rec:로 전환
uint16_t samples_per_pkt = calc_rec_samples_per_packet(notify_payload_max);
DBG_PRINTF("[CMD] rec split mtu=%u samples=%u chunk=%u\r\n", mtu, num_samples, samples_per_pkt);
cmd_send_response_echo_rec(session, channel, samples, num_samples, samples_per_pkt);
}
void cmd_send_response_bundle(uint16_t batt_mv, const int16_t accel[3], const int16_t gyro[3], int16_t temp_cdeg)
{
uint8_t *buf = tx_bundle_buf;
@@ -349,7 +240,7 @@ void cmd_send_response_bundle(uint16_t batt_mv, const int16_t accel[3], const in
ble_data_send(buf, sizeof(tx_bundle_buf));
}
static void cmd_send_response_rim_legacy(const uint8_t *sample_bytes, uint16_t sample_count)
void cmd_send_response_rim(const uint8_t *sample_bytes, uint16_t sample_count)
{
uint8_t *buf = tx_rim_buf;
uint16_t payload_len;
@@ -371,98 +262,9 @@ static void cmd_send_response_rim_legacy(const uint8_t *sample_bytes, uint16_t s
buf[payload_len + 1U] = (uint8_t)(crc >> 8);
DBG_PRINTF("[MTB] tx rim samples=%u len=%u\r\n", sample_count, (uint16_t)(payload_len + 2U));
ble_data_send(buf, (uint16_t)(payload_len + 2U));
}
static uint16_t calc_ric_samples_per_packet(uint16_t notify_payload_max)
{
if (notify_payload_max < (RIC_OVERHEAD_SIZE + IMU_FIFO_SAMPLE_BYTES))
{
return 0U;
}
return (uint16_t)((notify_payload_max - RIC_OVERHEAD_SIZE) / IMU_FIFO_SAMPLE_BYTES);
}
/*
* ric: packet layout:
* tag (4B): "ric:"
* offset (2B): 전체 샘플 배열에서 시작 인덱스
* total_samples (2B): 전체 IMU FIFO 샘플 수
* chunk_samples (2B): 현재 패킷에 들어간 샘플 수
* sample data (N): IMU FIFO samples
* crc (2B): tag부터 sample data까지 CRC16
*/
static void cmd_send_response_rim_ric(const uint8_t *sample_bytes, uint16_t sample_count, uint16_t samples_per_pkt)
{
uint8_t *buf = tx_rim_buf;
for (uint16_t off = 0U; off < sample_count; off = (uint16_t)(off + samples_per_pkt))
{
uint16_t remain = (uint16_t)(sample_count - off);
uint16_t chunk_samples = MIN(samples_per_pkt, remain);
buf[0] = 'r'; buf[1] = 'i'; buf[2] = 'c'; buf[3] = ':';
buf[4] = (uint8_t)(off >> 8);
buf[5] = (uint8_t)(off & 0xFF);
buf[6] = (uint8_t)(sample_count >> 8);
buf[7] = (uint8_t)(sample_count & 0xFF);
buf[8] = (uint8_t)(chunk_samples >> 8);
buf[9] = (uint8_t)(chunk_samples & 0xFF);
memcpy(&buf[10], &sample_bytes[off * IMU_FIFO_SAMPLE_BYTES], (size_t)chunk_samples * IMU_FIFO_SAMPLE_BYTES);
uint16_t payload_len = (uint16_t)(RIC_OVERHEAD_SIZE - 2U + (chunk_samples * IMU_FIFO_SAMPLE_BYTES));
uint16_t crc = cmd_crc16_compute(buf, payload_len);
DBG_PRINTF("[CMD] ric tx off=%u total=%u chunk=%u len=%u\r\n",
off, sample_count, chunk_samples, (uint16_t)(payload_len + 2U));
buf[payload_len] = (uint8_t)(crc & 0xFF);
buf[payload_len + 1U] = (uint8_t)(crc >> 8);
if (ble_data_send(buf, (uint16_t)(payload_len + 2U)) != 0)
{
break;
}
}
}
void cmd_send_response_rim(const uint8_t *sample_bytes, uint16_t sample_count)
{
uint16_t notify_payload_max = 0U;
uint16_t mtu = ble_current_mtu();
if (sample_count > IMU_FIFO_RIM_TARGET_SAMPLES)
{
sample_count = IMU_FIFO_RIM_TARGET_SAMPLES;
}
uint16_t rim_len = (uint16_t)(RIM_OVERHEAD_SIZE + (sample_count * IMU_FIFO_SAMPLE_BYTES));
if (mtu > ATT_NOTIFY_HEADER_SIZE)
{
notify_payload_max = (uint16_t)(mtu - ATT_NOTIFY_HEADER_SIZE);
}
if ((notify_payload_max == 0U) || (rim_len <= notify_payload_max))
{
cmd_send_response_rim_legacy(sample_bytes, sample_count);
return;
}
uint16_t samples_per_pkt = calc_ric_samples_per_packet(notify_payload_max);
if (samples_per_pkt == 0U)
{
DBG_ERR("[CMD] ric skip mtu=%u payload=%u samples=%u\r\n", mtu, notify_payload_max, sample_count);
return;
}
DBG_PRINTF("[CMD] ric split mtu=%u samples=%u chunk=%u\r\n", mtu, sample_count, samples_per_pkt);
cmd_send_response_rim_ric(sample_bytes, sample_count, samples_per_pkt);
}
void cmd_send_response_piezo_config(const char *tag, uint16_t freq, uint16_t cycles, uint16_t avg, uint16_t delay_us, uint16_t samples)
{
uint8_t *buf = tx_cfg_buf;
@@ -487,4 +289,107 @@ void cmd_send_response_piezo_config(const char *tag, uint16_t freq, uint16_t cyc
buf[15] = (uint8_t)(crc >> 8);
ble_data_send(buf, sizeof(tx_cfg_buf));
}
}
static void cmd_put_u16_be(uint8_t *dst, uint16_t value)
{
dst[0] = (uint8_t)(value >> 8);
dst[1] = (uint8_t)(value & 0xFF);
}
static void cmd_put_u32_be(uint8_t *dst, uint32_t value)
{
dst[0] = (uint8_t)(value >> 24);
dst[1] = (uint8_t)(value >> 16);
dst[2] = (uint8_t)(value >> 8);
dst[3] = (uint8_t)(value & 0xFF);
}
static void cmd_put_crc(uint8_t *buf, uint16_t payload_len)
{
uint16_t crc = cmd_crc16_compute(buf, payload_len);
buf[payload_len] = (uint8_t)(crc & 0xFF);
buf[payload_len + 1U] = (uint8_t)(crc >> 8);
}
int cmd_send_response_rfq(uint32_t tick_start)
{
static uint8_t buf[10];
buf[0] = 'r'; buf[1] = 'f'; buf[2] = 'q'; buf[3] = ':';
cmd_put_u32_be(&buf[4], tick_start);
cmd_put_crc(buf, 8U);
return ble_data_send(buf, sizeof(buf));
}
int cmd_send_response_rfe(uint16_t status, uint8_t session, uint32_t tick_end, uint16_t total_frames)
{
static uint8_t buf[15];
buf[0] = 'r'; buf[1] = 'f'; buf[2] = 'e'; buf[3] = ':';
cmd_put_u16_be(&buf[4], status);
buf[6] = session;
cmd_put_u32_be(&buf[7], tick_end);
cmd_put_u16_be(&buf[11], total_frames);
cmd_put_crc(buf, 13U);
return ble_data_send(buf, sizeof(buf));
}
int cmd_send_response_rqh(const measure_queue_frame_header_t *header)
{
static uint8_t buf[14];
if (header == NULL)
{
return -EINVAL;
}
buf[0] = 'r'; buf[1] = 'q'; buf[2] = 'h'; buf[3] = ':';
buf[4] = header->session;
cmd_put_u16_be(&buf[5], header->frame_idx);
cmd_put_u32_be(&buf[7], header->tick_ms);
buf[11] = header->ch_mask;
cmd_put_crc(buf, 12U);
return ble_data_send(buf, sizeof(buf));
}
int cmd_send_response_rqb(uint16_t frame_idx, uint8_t channel, const uint16_t *samples, uint8_t num_samples)
{
static uint8_t buf[4 + 2 + 1 + 1 + (PIEZO_MEASURE_MAX_SAMPLES * 2) + 2];
uint16_t payload_len;
if ((samples == NULL) || (num_samples > PIEZO_MEASURE_MAX_SAMPLES))
{
return -EINVAL;
}
buf[0] = 'r'; buf[1] = 'q'; buf[2] = 'b'; buf[3] = ':';
cmd_put_u16_be(&buf[4], frame_idx);
buf[6] = channel;
buf[7] = num_samples;
for (uint8_t i = 0; i < num_samples; i++)
{
cmd_put_u16_be(&buf[8U + (uint16_t)i * 2U], samples[i]);
}
payload_len = (uint16_t)(8U + ((uint16_t)num_samples * 2U));
cmd_put_crc(buf, payload_len);
return ble_data_send(buf, (uint16_t)(payload_len + 2U));
}
int cmd_send_response_rqd(uint16_t status, uint16_t sent_frames)
{
static uint8_t buf[10];
buf[0] = 'r'; buf[1] = 'q'; buf[2] = 'd'; buf[3] = ':';
cmd_put_u16_be(&buf[4], status);
cmd_put_u16_be(&buf[6], sent_frames);
cmd_put_crc(buf, 8U);
return ble_data_send(buf, sizeof(buf));
}
+6
View File
@@ -11,6 +11,7 @@
#include "imu_i2c.h"
#include "piezo_measure.h"
#include "measure_queue.h"
void cmd_reboot_after_response(void);
uint16_t cmd_crc16_compute(const uint8_t *p_data, uint32_t size);
@@ -28,5 +29,10 @@ void cmd_send_response_echo(uint8_t session, uint8_t channel, const uint16_t *sa
void cmd_send_response_bundle(uint16_t batt_mv, const int16_t accel[3], const int16_t gyro[3], int16_t temp_cdeg);
void cmd_send_response_rim(const uint8_t *sample_bytes, uint16_t sample_count);
void cmd_send_response_piezo_config(const char *tag, uint16_t freq, uint16_t cycles, uint16_t avg, uint16_t delay_us, uint16_t samples);
int cmd_send_response_rfq(uint32_t tick_start);
int cmd_send_response_rfe(uint16_t status, uint8_t session, uint32_t tick_end, uint16_t total_frames);
int cmd_send_response_rqh(const measure_queue_frame_header_t *header);
int cmd_send_response_rqb(uint16_t frame_idx, uint8_t channel, const uint16_t *samples, uint8_t num_samples);
int cmd_send_response_rqd(uint16_t status, uint16_t sent_frames);
#endif /* CMD_COMMON_H__ */
+3
View File
@@ -28,6 +28,9 @@ const cmd_entry_t cmd_table[] =
{ "mtb?", cmd_mtb },
{ "mcf?", cmd_mcf },
{ "mcs?", cmd_mcs },
{ "mfq?", cmd_mfq },
{ "mfe?", cmd_mfe },
{ "mqb?", cmd_mqb },
{ "mid?", cmd_mid },
{ "mfv?", cmd_mfv },
{ "mwh?", cmd_mwh },
-3
View File
@@ -14,7 +14,6 @@
#include "debug_print.h"
#include "ble_service.h"
#include "cmd_device.h"
#include "led_control.h"
/*
* msq: power off
@@ -82,10 +81,8 @@ int cmd_msr(const uint8_t *data, uint8_t data_len)
}
else
{
led_set_state(LED_STATE_BOND_DELETE); // 삭제 완료 확인 주황 LED ON
bond_data_delete = true;
DBG_PRINTF("[CMD] msr bond data deleted\r\n");
k_msleep(1500);
}
#else
bond_data_delete = true;
+141 -40
View File
@@ -4,6 +4,7 @@
******************************************************************************/
#include <zephyr/sys/util.h>
#include <limits.h>
#include <errno.h>
#include "cmd_common.h"
#include "main.h"
@@ -12,6 +13,7 @@
#include "imu_i2c.h"
#include "piezo.h"
#include "piezo_measure.h"
#include "measure_queue.h"
#include "cmd_piezo.h"
static uint8_t tx_rim_samples[IMU_FIFO_RIM_TARGET_SAMPLES * IMU_FIFO_SAMPLE_BYTES];
@@ -42,6 +44,7 @@ int cmd_mbb(const uint8_t *data, uint8_t data_len)
int16_t gyro[3];
processing = true;
power_button_suspend(true);
DBG_PRINTF("[MBB] cmd start\r\n");
int batt_mv = -1;
@@ -76,6 +79,7 @@ int cmd_mbb(const uint8_t *data, uint8_t data_len)
}
piezo_power_off();
power_button_suspend(false);
DBG_PRINTF("[MBB] done\r\n");
cmd_send_response_u16("raa:", (uint16_t)status); // 최종 상태 raa: 전송
DBG_PRINTF("[CMD] mbb status=0x%04X\r\n", status);
@@ -105,6 +109,7 @@ int cmd_mtb(const uint8_t *data, uint8_t data_len)
bool fifo_started = false;
processing = true;
power_button_suspend(true);
DBG_PRINTF("[MTB] cmd start\r\n");
int imu_ret = imu_fifo_start();
@@ -139,13 +144,7 @@ int cmd_mtb(const uint8_t *data, uint8_t data_len)
{
if (fifo_started)
{
imu_ret = imu_fifo_wait_samples(IMU_FIFO_RIM_TARGET_SAMPLES, 500U);
if (imu_ret != 0)
{
DBG_PRINTF("[MTB] fifo wait ret=%d\r\n", imu_ret);
}
imu_ret = imu_fifo_read_latest(tx_rim_samples, IMU_FIFO_RIM_TARGET_SAMPLES, &rim_count);
int imu_ret = imu_fifo_read_latest(tx_rim_samples, IMU_FIFO_RIM_TARGET_SAMPLES, &rim_count);
if (imu_ret != 0)
{
DBG_PRINTF("[MTB] fifo read fail ret=%d\r\n", imu_ret);
@@ -158,6 +157,7 @@ int cmd_mtb(const uint8_t *data, uint8_t data_len)
DBG_PRINTF("[CMD] mtb status=0x%04X\r\n", status);
piezo_power_off();
power_button_suspend(false);
DBG_PRINTF("[CMD] mtb status=0x%04X rim=%u\r\n", status, rim_count);
processing = false;
@@ -214,47 +214,138 @@ int cmd_mcs(const uint8_t *data, uint8_t data_len)
return 1;
}
// =============================== Test Command ===============================
/* TEST
* mim? : IMU FIFO 15 samples
*/
int cmd_mim(const uint8_t *data, uint8_t data_len)
static uint16_t cmd_queue_status_to_u16(int status)
{
if (status == 0)
{
return ECHO_STATUS_OK;
}
if (status > 0)
{
return (uint16_t)status;
}
return (uint16_t)(0x8000U | ((uint16_t)(-status) & 0x7FFFU));
}
static uint16_t queue_tx_samples[PIEZO_NUM_CHANNELS][PIEZO_MEASURE_MAX_SAMPLES];
/*
* mfq?: start flash queue recording.
* Response: rfq: + tick_start.
*/
int cmd_mfq(const uint8_t *data, uint8_t data_len)
{
ARG_UNUSED(data);
ARG_UNUSED(data_len);
if (processing)
uint32_t tick_start = 0U;
DBG_PRINTF("[MFQ] start\r\n");
int err = measure_queue_start(&tick_start);
if (err)
{
return 1;
DBG_ERR("[MFQ] start failed err=%d\r\n", err);
}
processing = true;
uint16_t rim_count = 0U;
int imu_ret = imu_fifo_start();
if (imu_ret != 0)
{
DBG_PRINTF("[MTB] fifo start fail ret=%d\r\n", imu_ret);
cmd_send_response_rim(tx_rim_samples, 0U);
processing = false;
return 1;
}
k_msleep(320); // IMU FIFO is 50Hz, 15 samples need about 300ms
imu_ret = imu_fifo_read_latest(tx_rim_samples, IMU_FIFO_RIM_TARGET_SAMPLES, &rim_count);
if (imu_ret != 0)
{
DBG_PRINTF("[MIM] fifo read fail ret=%d\r\n", imu_ret);
rim_count = 0U;
}
cmd_send_response_rim(tx_rim_samples, rim_count); // IMU FIFO 15 samples rim: 전송
imu_fifo_stop();
processing = false;
DBG_PRINTF("[MFQ] response rfq tick_start=%u err=%d\r\n", tick_start, err);
cmd_send_response_rfq(tick_start);
return 1;
}
/*
* mfe?: stop flash queue recording.
* Response: rfe: + status/session/tick_end/total_frames.
*/
int cmd_mfe(const uint8_t *data, uint8_t data_len)
{
ARG_UNUSED(data);
ARG_UNUSED(data_len);
uint32_t tick_end = 0U;
uint8_t session = 0U;
DBG_PRINTF("[MFE] stop\r\n");
uint16_t total_frames = 0U;
int status = measure_queue_stop(&tick_end, &session, &total_frames);
DBG_PRINTF("[MFE] response rfe status=0x%04X session=%u tick_end=%u frames=%u\r\n",
cmd_queue_status_to_u16(status), session, tick_end, total_frames);
cmd_send_response_rfe(cmd_queue_status_to_u16(status), session, tick_end, total_frames);
return 1;
}
/*
* mqb?: dump queued frames.
* Response sequence: (rqh + rqb * active channels) * frames, then rqd once.
*/
int cmd_mqb(const uint8_t *data, uint8_t data_len)
{
ARG_UNUSED(data);
ARG_UNUSED(data_len);
uint16_t total_frames = measure_queue_total_frames();
uint16_t num_samples = measure_queue_num_samples();
DBG_PRINTF("[MQB] cmd dump total_frames=%u samples=%u\r\n", total_frames, num_samples);
uint16_t sent_frames = 0U;
uint16_t status = ECHO_STATUS_OK;
if (measure_queue_is_recording())
{
DBG_PRINTF("[MQB] reject: recording busy\r\n");
cmd_send_response_rqd(cmd_queue_status_to_u16(-EBUSY), 0U);
return 1;
}
for (uint16_t frame_idx = 0; frame_idx < total_frames; frame_idx++)
{
measure_queue_frame_header_t header;
int err = measure_queue_read_frame(frame_idx, &header, queue_tx_samples);
if (err)
{
DBG_ERR("[MQB] read frame failed frame=%u err=%d\r\n", frame_idx, err);
status = cmd_queue_status_to_u16(err);
break;
}
err = cmd_send_response_rqh(&header);
if (err)
{
DBG_ERR("[MQB] send rqh failed frame=%u err=%d\r\n", frame_idx, err);
status = cmd_queue_status_to_u16(err);
break;
}
for (uint8_t ch = 0; ch < PIEZO_NUM_CHANNELS; ch++)
{
if ((header.ch_mask & BIT(ch)) == 0U)
{
continue;
}
err = cmd_send_response_rqb(frame_idx, ch, queue_tx_samples[ch], (uint8_t)num_samples);
if (err)
{
DBG_ERR("[MQB] send rqb failed frame=%u ch=%u err=%d\r\n", frame_idx, ch, err);
status = cmd_queue_status_to_u16(err);
break;
}
}
if (status != ECHO_STATUS_OK)
{
break;
}
sent_frames++;
}
DBG_PRINTF("[MQB] response rqd status=0x%04X sent_frames=%u\r\n", status, sent_frames);
cmd_send_response_rqd(status, sent_frames);
return 1;
}
// =============================== Test Command ===============================
/*
* TEST (이전 정렬모드)
* maa?: piezo echo sweep
@@ -272,12 +363,13 @@ int cmd_maa(const uint8_t *data, uint8_t data_len)
uint8_t session = cmd_next_echo_session();
processing = true;
power_button_suspend(true);
int status = piezo_measure_start_session();
if (status == ECHO_STATUS_OK) // Piezo GPIO, power, ADC initialization/wake succeeded
{
DBG_PRINTF("[MAA] piezo sweep start\r\n");
DBG_PRINTF("[MBB] piezo sweep start\r\n");
status = piezo_measure_sweep();
if (status == ECHO_STATUS_OK)
{
@@ -290,6 +382,7 @@ int cmd_maa(const uint8_t *data, uint8_t data_len)
}
piezo_power_off();
power_button_suspend(false);
cmd_send_response_u16("raa:", (uint16_t)status); // 최종 상태 raa: 전송
DBG_PRINTF("[CMD] maa status=0x%04X\r\n", status);
@@ -368,9 +461,11 @@ int cmd_mpc(const uint8_t *data, uint8_t data_len)
return 1;
}
power_button_suspend(true);
if (piezo_init() != 0)
{
power_button_suspend(false);
cmd_send_response_u16("rpc:", 0);
return 1;
}
@@ -380,12 +475,14 @@ int cmd_mpc(const uint8_t *data, uint8_t data_len)
if (piezo_select_channel((uint8_t)(piezo_ch % PIEZO_NUM_CHANNELS)) != 0)
{
piezo_power_off();
power_button_suspend(false);
cmd_send_response_u16("rpc:", 0);
return 1;
}
piezo_burst_sw_freq((uint8_t)freq_option, cycles);
piezo_power_off();
power_button_suspend(false);
cmd_send_response_u16("rpc:", cycles);
return 1;
@@ -427,6 +524,7 @@ int cmd_mec(const uint8_t *data, uint8_t data_len)
}
processing = true;
power_button_suspend(true);
int status = piezo_measure_start_session();
if (status == ECHO_STATUS_OK)
@@ -440,6 +538,7 @@ int cmd_mec(const uint8_t *data, uint8_t data_len)
}
piezo_power_off();
power_button_suspend(false);
cmd_send_response_u16("raa:", (uint16_t)status);
processing = false;
return 1;
@@ -471,6 +570,7 @@ int cmd_mad(const uint8_t *data, uint8_t data_len)
}
processing = true;
power_button_suspend(true);
int status = piezo_measure_start_session();
if (status == ECHO_STATUS_OK)
@@ -484,6 +584,7 @@ int cmd_mad(const uint8_t *data, uint8_t data_len)
}
piezo_power_off();
power_button_suspend(false);
cmd_send_response_u16("raa:", (uint16_t)status);
DBG_PRINTF("[CMD] mad status=0x%04X ch=%u samples=%u avg=%u\r\n",
status,
@@ -493,4 +594,4 @@ int cmd_mad(const uint8_t *data, uint8_t data_len)
processing = false;
return 1;
}
}
+3
View File
@@ -17,6 +17,9 @@ int cmd_mbb(const uint8_t *data, uint8_t data_len);
int cmd_mtb(const uint8_t *data, uint8_t data_len);
int cmd_mcf(const uint8_t *data, uint8_t data_len);
int cmd_mcs(const uint8_t *data, uint8_t data_len);
int cmd_mfq(const uint8_t *data, uint8_t data_len);
int cmd_mfe(const uint8_t *data, uint8_t data_len);
int cmd_mqb(const uint8_t *data, uint8_t data_len);
int cmd_mim(const uint8_t *data, uint8_t data_len);
#endif /* CMD_PIEZO_H__ */
+34 -6
View File
@@ -13,6 +13,8 @@
#include "led_control.h"
#include "cmd_sensor.h"
static uint8_t tx_rim_samples[IMU_FIFO_RIM_TARGET_SAMPLES * IMU_FIFO_SAMPLE_BYTES];
/*
* msn: battery voltage read
*/
@@ -31,7 +33,7 @@ int cmd_msn(const uint8_t *data, uint8_t data_len)
return 1;
}
/*
/* 삭제 예정(mim?으로 대체)
* msp: IMU data direct read
*/
int cmd_msp(const uint8_t *data, uint8_t data_len)
@@ -39,11 +41,6 @@ int cmd_msp(const uint8_t *data, uint8_t data_len)
ARG_UNUSED(data);
ARG_UNUSED(data_len);
if (processing)
{
return 1;
}
int16_t accel[3], gyro[3];
int ret = imu_read(accel, gyro);
@@ -58,6 +55,37 @@ int cmd_msp(const uint8_t *data, uint8_t data_len)
return 1;
}
/*
* mim? : IMU FIFO 15 samples
*/
int cmd_mim(const uint8_t *data, uint8_t data_len)
{
ARG_UNUSED(data);
ARG_UNUSED(data_len);
uint16_t rim_count = 0U;
int imu_ret = imu_fifo_start();
if (imu_ret != 0)
{
DBG_PRINTF("[MTB] fifo start fail ret=%d\r\n", imu_ret);
cmd_send_response_rim(tx_rim_samples, 0U);
return 1;
}
k_msleep(320); // IMU FIFO is 50Hz, 15 samples need about 300ms
imu_ret = imu_fifo_read_latest(tx_rim_samples, IMU_FIFO_RIM_TARGET_SAMPLES, &rim_count);
if (imu_ret != 0)
{
DBG_PRINTF("[MIM] fifo read fail ret=%d\r\n", imu_ret);
rim_count = 0U;
}
cmd_send_response_rim(tx_rim_samples, rim_count); // IMU FIFO 15 samples rim: 전송
imu_fifo_stop();
return 1;
}
/*
* mst: IMU temperature direct read
*/
-6
View File
@@ -14,14 +14,8 @@
*/
#include <zephyr/sys/printk.h>
#if defined(CONFIG_VESIS_DEBUG_PRINT)
#define DBG_PRINTF(...) printk(__VA_ARGS__)
#define DBG_CORE(...) printk(__VA_ARGS__)
#define DBG_ERR(...) printk(__VA_ARGS__)
#else
#define DBG_PRINTF(...) do { } while (0)
#define DBG_CORE(...) printk(__VA_ARGS__)
#define DBG_ERR(...) printk(__VA_ARGS__)
#endif
#endif /* DEBUG_PRINT_H */
-238
View File
@@ -1,238 +0,0 @@
/*******************************************************************************
* @file dfu_manager.c
* @brief DFU state, watchdog, and MCUboot image control
******************************************************************************/
#include <zephyr/kernel.h>
#include <zephyr/dfu/mcuboot.h>
#include <zephyr/mgmt/mcumgr/mgmt/callbacks.h>
#include <hal/nrf_power.h>
#include "main.h"
#include "dfu_manager.h"
#include "debug_print.h"
#include "led_control.h"
#include "ble_service.h"
#define DFU_STALL_TIMEOUT_SEC 30 // DFU upload stall disconnect threshold
#define IMG_MGMT_ID_STATE 0U
#define IMG_MGMT_ID_UPLOAD 1U
static bool dfu_led_active; // DFU upload green LED state
static struct k_work_delayable dfu_watchdog_work; // DFU upload stall watchdog
static void dfu_watchdog_handler(struct k_work *work);
static enum mgmt_cb_return dfu_status_cb(uint32_t event,
enum mgmt_cb_return prev_status,
int32_t *rc,
uint16_t *group,
bool *abort_more,
void *data,
size_t data_size);
static enum mgmt_cb_return smp_cmd_status_cb(uint32_t event,
enum mgmt_cb_return prev_status,
int32_t *rc,
uint16_t *group,
bool *abort_more,
void *data,
size_t data_size);
static struct mgmt_callback dfu_status_callback = {
.callback = dfu_status_cb,
.event_id = MGMT_EVT_OP_IMG_MGMT_DFU_STARTED |
MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK |
MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK_WRITE_COMPLETE |
MGMT_EVT_OP_IMG_MGMT_DFU_PENDING |
MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED,
};
static struct mgmt_callback smp_cmd_status_callback = {
.callback = smp_cmd_status_cb,
.event_id = MGMT_EVT_OP_CMD_DONE,
};
/* DFU watchdog와 mcumgr 콜백 초기화 */
void dfu_manager_init(void)
{
k_work_init_delayable(&dfu_watchdog_work, dfu_watchdog_handler);
mgmt_callback_register(&dfu_status_callback);
mgmt_callback_register(&smp_cmd_status_callback);
}
/* 현재 이미지가 confirm 대기 상태인지 확인 */
bool dfu_is_confirm_pending_boot(void)
{
#if defined(CONFIG_BOOTLOADER_MCUBOOT)
return !boot_is_img_confirmed();
#else
return false;
#endif
}
/* 실행 중 이미지 confirm 처리 */
void dfu_confirm_running_image(void)
{
#if defined(CONFIG_BOOTLOADER_MCUBOOT)
if (!boot_is_img_confirmed())
{
int err = boot_write_img_confirmed();
if (err == 0)
{
DBG_CORE("[DFU] Running image confirmed\r\n");
}
else
{
DBG_ERR("[DFU] Image confirm failed (err %d)\r\n", err);
}
}
else
{
DBG_CORE("[DFU] Running image already confirmed\r\n");
}
#endif
}
/* MCUboot swap 상태 문자열 변환 */
static const char *swap_type_to_str(int swap_type)
{
switch (swap_type)
{
case BOOT_SWAP_TYPE_NONE:
return "none";
case BOOT_SWAP_TYPE_TEST:
return "test";
case BOOT_SWAP_TYPE_PERM:
return "perm";
case BOOT_SWAP_TYPE_REVERT:
return "revert";
case BOOT_SWAP_TYPE_FAIL:
return "fail";
default:
return "unknown";
}
}
/* MCUboot 이미지 상태 로그 출력 */
void dfu_log_mcuboot_state(void)
{
#if defined(CONFIG_BOOTLOADER_MCUBOOT)
struct mcuboot_img_header header;
uint8_t active_slot = boot_fetch_active_slot();
int swap_type = mcuboot_swap_type();
int err = boot_read_bank_header(active_slot, &header, sizeof(header));
DBG_CORE("[DFU] active_slot=%u swap_type=%s(%d) confirmed=%d\r\n",
active_slot,
swap_type_to_str(swap_type),
swap_type,
boot_is_img_confirmed());
if (err == 0)
{
DBG_CORE("[DFU] image version %u.%u.%u+%u size=0x%x\r\n",
header.h.v1.sem_ver.major,
header.h.v1.sem_ver.minor,
header.h.v1.sem_ver.revision,
header.h.v1.sem_ver.build_num,
header.h.v1.image_size);
}
else
{
DBG_ERR("[DFU] image header read failed (slot %u, err %d)\r\n",
active_slot,
err);
}
#endif
}
/* DFU 업로드 무진행 시 BLE 연결 강제 해제 */
static void dfu_watchdog_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (ble_is_connected())
{
DBG_CORE("[DFU] upload stalled (%ds), forcing disconnect\r\n", DFU_STALL_TIMEOUT_SEC);
(void)ble_disconnect_active();
}
}
/* DFU 업로드 상태에 따라 LED와 watchdog 제어 */
static enum mgmt_cb_return dfu_status_cb(uint32_t event, enum mgmt_cb_return prev_status, int32_t *rc, uint16_t *group, bool *abort_more, void *data, size_t data_size)
{
ARG_UNUSED(prev_status);
ARG_UNUSED(rc);
ARG_UNUSED(group);
ARG_UNUSED(abort_more);
ARG_UNUSED(data);
ARG_UNUSED(data_size);
switch (event)
{
case MGMT_EVT_OP_IMG_MGMT_DFU_STARTED:
case MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK:
case MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK_WRITE_COMPLETE:
dfu_led_active = true;
led_ble_solid();
k_work_reschedule(&dfu_watchdog_work, K_SECONDS(DFU_STALL_TIMEOUT_SEC));
break;
case MGMT_EVT_OP_IMG_MGMT_DFU_PENDING:
dfu_led_active = true;
led_ble_solid();
k_work_cancel_delayable(&dfu_watchdog_work);
#if NRF_POWER_HAS_GPREGRET
nrf_power_gpregret_set(NRF_POWER, DFU_RESUME_GPREGRET_REG, DFU_RESUME_MAGIC);
#endif
break;
case MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED:
k_work_cancel_delayable(&dfu_watchdog_work);
if (dfu_led_active)
{
dfu_led_active = false;
led_set_state(LED_STATE_ADVERTISING);
}
break;
default:
break;
}
return MGMT_CB_OK;
}
/* DFU 명령 실패 시 LED와 resume 플래그 복구 */
static enum mgmt_cb_return smp_cmd_status_cb(uint32_t event, enum mgmt_cb_return prev_status, int32_t *rc, uint16_t *group, bool *abort_more, void *data, size_t data_size)
{
ARG_UNUSED(event);
ARG_UNUSED(prev_status);
ARG_UNUSED(rc);
ARG_UNUSED(group);
ARG_UNUSED(abort_more);
if (data == NULL || data_size != sizeof(struct mgmt_evt_op_cmd_arg))
{
return MGMT_CB_OK;
}
const struct mgmt_evt_op_cmd_arg *cmd = data;
if (cmd->group == MGMT_GROUP_ID_IMAGE &&
(cmd->id == IMG_MGMT_ID_UPLOAD || cmd->id == IMG_MGMT_ID_STATE) &&
cmd->err != MGMT_ERR_EOK)
{
DBG_ERR("[DFU] command failed id=%u err=%d\r\n", cmd->id, cmd->err);
dfu_led_active = false;
led_set_state(LED_STATE_ADVERTISING);
#if NRF_POWER_HAS_GPREGRET
nrf_power_gpregret_set(NRF_POWER, DFU_RESUME_GPREGRET_REG, 0U);
#endif
}
return MGMT_CB_OK;
}
-18
View File
@@ -1,18 +0,0 @@
/*******************************************************************************
* @file dfu_manager.h
* @brief DFU state, watchdog, and MCUboot image control
******************************************************************************/
#ifndef DFU_MANAGER_H__
#define DFU_MANAGER_H__
#include <stdbool.h>
#define DFU_RESUME_MAGIC 0xD5U
#define DFU_RESUME_GPREGRET_REG 1U
void dfu_manager_init(void);
bool dfu_is_confirm_pending_boot(void);
void dfu_confirm_running_image(void);
void dfu_log_mcuboot_state(void);
#endif /* DFU_MANAGER_H__ */
+2 -37
View File
@@ -615,43 +615,6 @@ int imu_fifo_start(void)
return 0;
}
int imu_fifo_wait_samples(uint16_t target_samples, uint32_t timeout_ms)
{
uint32_t waited_ms = 0U;
const uint32_t poll_ms = 10U;
if (target_samples == 0U)
{
return 0;
}
if (!fifo_active)
{
return -EALREADY;
}
while (waited_ms <= timeout_ms)
{
uint16_t record_count = 0U;
int ret = imu_fifo_read_count(&record_count);
if (ret)
{
return ret;
}
if (record_count >= target_samples)
{
return 0;
}
k_msleep(poll_ms);
waited_ms += poll_ms;
}
return -ETIMEDOUT;
}
int imu_fifo_read_latest(uint8_t *sample_bytes,
uint16_t max_samples,
uint16_t *out_count)
@@ -687,7 +650,9 @@ int imu_fifo_read_latest(uint8_t *sample_bytes,
}
{
uint16_t raw_count = record_count;
record_count = imu_fifo_compact_records(record_count);
DBG_PRINTF("[IMU FIFO] compact %u -> %u\r\n", raw_count, record_count);
}
if (record_count > max_samples)
{
-7
View File
@@ -65,13 +65,6 @@ int imu_read_temperature_cdeg(int16_t *temp_cdeg);
*/
int imu_fifo_start(void);
/**
* @brief Wait until the running FIFO has at least target_samples records.
*
* @return 0 if the target is reached, -ETIMEDOUT on timeout, or a negative error.
*/
int imu_fifo_wait_samples(uint16_t target_samples, uint32_t timeout_ms);
/**
* @brief 실행 중인 FIFO에서 최신 샘플 읽기
*
+2 -2
View File
@@ -46,7 +46,8 @@ static const led_pattern_t m_patterns[LED_STATE_COUNT] = {
[LED_STATE_ALIGN_SEARCHING] = { 1000, 1000, COLOR_ORANGE, true },
[LED_STATE_ALIGN_COMPLETE] = { 0, 0, COLOR_GREEN, false },
[LED_STATE_ERROR] = { 0, 0, COLOR_ORANGE, true },
[LED_STATE_BOND_DELETE] = { 0, 0, COLOR_ORANGE, false },
[LED_STATE_DATA_STORAGING] = { 0, 0, COLOR_GREEN, false },
[LED_STATE_DATA_ERASING] = { 200, 200, COLOR_GREEN, true },
};
/* Module variables */
@@ -217,7 +218,6 @@ led_state_t led_get_state(void)
return m_current_state;
}
/* DFU 진행 중 초록 LED ON 콜백 */
void led_ble_solid(void)
{
k_timer_stop(&m_led_timer);
+3 -1
View File
@@ -23,7 +23,9 @@ typedef enum
LED_STATE_ALIGN_SEARCHING, // 5: Orange blink 1s/1s
LED_STATE_ALIGN_COMPLETE, // 6: Green ON
LED_STATE_ERROR, // 7: Orange 3Hz x3 / 1s off
LED_STATE_BOND_DELETE, // Bond Delete: Orange on
LED_STATE_DATA_STORAGING, // 8: Green ON
LED_STATE_DATA_ERASING, // 9: Green blink 200ms/200ms
LED_STATE_COUNT
} led_state_t;
+619 -51
View File
@@ -1,31 +1,58 @@
/*******************************************************************************
* @file main.c
* @brief VesiScan-Basic
*
* - 앱 공통 전역 상태 보관
* - 부팅 직후 전원 유지 판단
* - NVS 로드 전 기본값 초기화
* - 각 모듈 초기화 순서 조립
* - main loop 유지
******************************************************************************/
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/printk.h>
#include <zephyr/dfu/mcuboot.h>
#include <zephyr/mgmt/mcumgr/mgmt/callbacks.h>
#include <hal/nrf_power.h>
#include "main.h"
#include "debug_print.h"
#include "power_control.h"
#include "dfu_manager.h"
#include "led_control.h"
#include "ble_service.h"
#include "battery_adc.h"
#include "ble_cmd_queue.h"
#include "parser.h"
#include "imu_i2c.h"
#include "piezo_measure.h"
LOG_MODULE_REGISTER(vesiscan, LOG_LEVEL_INF);
#define BLE_CMD_MAX_LEN 256
#define BLE_CMD_WORKQ_STACK_SZ 8192
#define DFU_RESUME_MAGIC 0xD5U
#define DFU_RESUME_GPREGRET_REG 1U
#define IMG_MGMT_ID_STATE 0U
#define IMG_MGMT_ID_UPLOAD 1U
/*
* BLE 명령 처리 스택
* - piezo sweep
* - ADC capture
* - BLE 응답 패킷 조립
* - 디버깅 여유분 포함
*/
#define BLE_CMD_WORKQ_PRIORITY 10
/* Devicetree GPIO Settings */
#define POWER_HOLD_NODE DT_NODELABEL(pwr_hold) // 전원 래치
#define POWER_BTN_NODE DT_NODELABEL(button_check) // 전원 버튼
/* 전원 버튼 상태머신 (5ms 폴링) */
#define BOOT_THRESHOLD 300 // 5ms x 400 = 2초 -> 초기화 시간 고려 체감상 2초에 맞춤
/* 전원 제어 GPIO */
static const struct gpio_dt_spec power_hold = GPIO_DT_SPEC_GET(POWER_HOLD_NODE, gpios);
static const struct gpio_dt_spec power_btn = GPIO_DT_SPEC_GET(POWER_BTN_NODE, gpios);
/* Timer */
static struct k_timer m_power_on_delay_timer; // 전원 버튼 폴링용 (5ms 싱글샷)
static struct k_timer m_power_off_delay_timer; // 전원 OFF 지연용 (3초 후 전원 차단)
/* 전역 변수 */
volatile bool ble_connection_st = false; // BLE 연결 상태
volatile bool processing = false; // 센서 데이터 처리 중 플래그
bool bond_data_delete = true; // 본딩 데이터 삭제 요청
@@ -36,23 +63,159 @@ char HW_NO[HW_NO_BUF_SIZE]; // 하드웨어 번호
char m_static_passkey[PASSKEY_BUF_SIZE]; // BLE 정적 패스키
uint8_t m_passkey_changed; // 패스키 변경은 출고 후 1회만 허용
static uint16_t cnt_s; // 전원 버튼 폴링 카운터 (5ms 단위)
static bool device_on = false; // 디바이스 전원 상태 (래치 완료 여부)
static bool boot_btn_released = false; // 부팅 후 버튼 놓았는지 여부
static bool power_btn_suspended; // 측정 중 전원 버튼 상태머신 일시 정지
/*
* BLE advertising 제어용 워크 아이템
* - main_s()에서 k_work_submit()으로 예약하여 사용
*/
static bool resume_without_power_button; // 리셋 후 버튼 없이 복귀
static bool dfu_confirm_pending_boot; // DFU test 이미지 확인 전 자동 복귀
static bool dfu_reset_resume_request; // DFU 완료 리셋 후 자동 복귀
static uint32_t boot_reset_reason; // RESETREAS 원본 값
static struct k_work adv_start_work; // advertising 시작 work
static struct k_work adv_stop_work; // advertising 중지 work
static bool dfu_led_active; // DFU 업로드 중 초록 LED ON
static const char *power_off_pending_reason = "none";
/* 부팅 후 전원 유지 판단 및 모듈 초기화 순서 조립 */
static enum mgmt_cb_return dfu_status_cb(uint32_t event,
enum mgmt_cb_return prev_status,
int32_t *rc,
uint16_t *group,
bool *abort_more,
void *data,
size_t data_size);
static enum mgmt_cb_return smp_cmd_status_cb(uint32_t event,
enum mgmt_cb_return prev_status,
int32_t *rc,
uint16_t *group,
bool *abort_more,
void *data,
size_t data_size);
static struct mgmt_callback dfu_status_callback = {
.callback = dfu_status_cb,
.event_id = MGMT_EVT_OP_IMG_MGMT_DFU_STARTED |
MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK |
MGMT_EVT_OP_IMG_MGMT_DFU_PENDING |
MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED,
};
static struct mgmt_callback smp_cmd_status_callback = {
.callback = smp_cmd_status_cb,
.event_id = MGMT_EVT_OP_CMD_DONE,
};
/*
* BLE RX 콜백 안에서 무거운 일을 바로 처리하지 않기 위해 별도 work queue를 둠
* - 명령 하나가 끝날 때까지 다음 명령 drop : 확인 필요
*/
static struct k_work_q ble_cmd_work_q;
K_THREAD_STACK_DEFINE(ble_cmd_workq_stack, BLE_CMD_WORKQ_STACK_SZ);
static struct k_work ble_cmd_work; // BLE 명령 처리 work
static struct k_spinlock ble_cmd_lock; // RX 공유 버퍼 보호
static uint8_t ble_cmd_buf[BLE_CMD_MAX_LEN];// RX 명령 복사 버퍼
static uint16_t ble_cmd_len; // RX 명령 길이
static bool ble_cmd_pending; // 처리 대기 명령 있음
static void ble_cmd_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
// work queue 로컬 복사본
uint8_t local_buf[BLE_CMD_MAX_LEN];
uint16_t local_len = 0U;
// 공유 RX 명령 슬롯 보호: 현재 구조는 한 번에 한 명령만 보관
k_spinlock_key_t key = k_spin_lock(&ble_cmd_lock);
if (ble_cmd_pending)
{
local_len = ble_cmd_len;
memcpy(local_buf, ble_cmd_buf, local_len);
}
k_spin_unlock(&ble_cmd_lock, key);
if (local_len == 0U)
{
DBG_ERR("[BLE RX] worker: empty\r\n");
return;
}
// 일반 스레드 문맥 명령 처리
//DBG_CORE("[BLE RX] worker dispatch len=%u\r\n", local_len);
ble_cmd_dispatch(local_buf, local_len);
// 명령 처리 완료 표시
key = k_spin_lock(&ble_cmd_lock);
ble_cmd_pending = false;
ble_cmd_len = 0U;
k_spin_unlock(&ble_cmd_lock, key);
}
/* 시스템 워크큐에서 BLE advertising 시작 */
static void adv_start_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
ble_advertising_start();
}
/* 시스템 워크큐에서 BLE advertising 중지 */
static void adv_stop_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
ble_advertising_stop();
}
/* BLE RX 콜백 */
static void ble_rx_handler(const uint8_t *data, uint16_t len)
{
if (len > BLE_CMD_MAX_LEN)
{
DBG_ERR("[BLE RX] drop: len=%u exceeds %d\r\n", len, BLE_CMD_MAX_LEN);
return;
}
// 공유 RX 명령 슬롯 보호: 현재 구조는 한 번에 한 명령만 보관
k_spinlock_key_t key = k_spin_lock(&ble_cmd_lock);
if (ble_cmd_pending)
{
k_spin_unlock(&ble_cmd_lock, key);
DBG_ERR("[BLE RX] drop: command busy\r\n");
return;
}
// 명령 버퍼 복사
memcpy(ble_cmd_buf, data, len);
ble_cmd_len = len;
ble_cmd_pending = true;
k_spin_unlock(&ble_cmd_lock, key);
// 명령 처리 work 예약
int err = k_work_submit_to_queue(&ble_cmd_work_q, &ble_cmd_work);
if (err < 0)
{
/* 예약 실패 시 pending 복구 */
key = k_spin_lock(&ble_cmd_lock);
ble_cmd_pending = false;
ble_cmd_len = 0U;
k_spin_unlock(&ble_cmd_lock, key);
DBG_ERR("[BLE RX] queue submit fail err=%d\r\n", err);
}
}
/* 전원 유지 (POWER_HOLD) 제어 */
static void boot_context_detect(void)
{
// 부팅 원인 확인
boot_reset_reason = nrf_power_resetreas_get(NRF_POWER);
// DFU rest (DFU 완료 후 재부팅)
// GPREGRET은 reset 후에도 유지되는 작은 상태 표시용 레지스터
// DFU 완료 리셋인지 확인
#if NRF_POWER_HAS_GPREGRET
dfu_reset_resume_request = (nrf_power_gpregret_get(NRF_POWER, DFU_RESUME_GPREGRET_REG) == DFU_RESUME_MAGIC);
// DFU reset 표시 확인 후 다음 부팅 오판 방지를 위해 정리
if (dfu_reset_resume_request)
{
nrf_power_gpregret_set(NRF_POWER, DFU_RESUME_GPREGRET_REG, 0U);
@@ -61,42 +224,89 @@ static void boot_context_detect(void)
dfu_reset_resume_request = false;
#endif
// soft reset 후 복귀 (전원 버튼 새 부팅 X)
resume_without_power_button = (boot_reset_reason & (NRF_POWER_RESETREAS_SREQ_MASK | NRF_POWER_RESETREAS_DOG_MASK | NRF_POWER_RESETREAS_LOCKUP_MASK | NRF_POWER_RESETREAS_RESETPIN_MASK)) != 0U;
// 소프트 리셋 계열 자동 복귀
resume_without_power_button =
(boot_reset_reason & (NRF_POWER_RESETREAS_SREQ_MASK |
NRF_POWER_RESETREAS_DOG_MASK |
NRF_POWER_RESETREAS_LOCKUP_MASK |
NRF_POWER_RESETREAS_RESETPIN_MASK)) != 0U;
// 다음 부팅 오판 방지를 위해 RESETREAS 플래그 정리
if (boot_reset_reason != 0U)
{
// RESETREAS 플래그 정리
nrf_power_resetreas_clear(NRF_POWER, boot_reset_reason);
}
}
/* NVS 값이 없을 때 사용할 기본값 초기화 (앱 전역 상태 기본값) - NVS 값이 있는 경우 이후 NVS 값으로 덮어씀 */
static void power_hold_init(void)
{
#if defined(CONFIG_BOOTLOADER_MCUBOOT)
// DFU 이미지 확인 전 전원 유지
dfu_confirm_pending_boot = !boot_is_img_confirmed();
#else
dfu_confirm_pending_boot = false;
#endif
// 리셋 복귀/DFU 확인 대기 시 즉시 래치
gpio_pin_configure_dt(&power_hold,
(resume_without_power_button ||
dfu_confirm_pending_boot ||
dfu_reset_resume_request) ? GPIO_OUTPUT_ACTIVE
: GPIO_OUTPUT_INACTIVE);
}
/* 전원 ON/OFF 제어 - P0.08 핀으로 물리적 전원 래치/해제 */
static void power_control_handler(on_off_cont_t device_power_st, const char *reason)
{
if (device_power_st == OFF)
{
DBG_ERR("[PWR] OFF latch release reason=%s pending=%s device_on=%u btn=%d cnt=%u suspended=%u dfu_adv=%u dfu_confirm=%u dfu_resume=%u\r\n",
reason ? reason : "unknown",
power_off_pending_reason,
device_on ? 1U : 0U,
gpio_pin_get_dt(&power_btn),
cnt_s,
power_btn_suspended ? 1U : 0U,
ble_dfu_advertising_is_enabled() ? 1U : 0U,
dfu_confirm_pending_boot ? 1U : 0U,
dfu_reset_resume_request ? 1U : 0U);
gpio_pin_set_dt(&power_hold, 0); // P0.08 LOW → 전원 래치 해제 → 전원 차단
}
else
{
gpio_pin_set_dt(&power_hold, 1); // P0.08 HIGH → 전원 유지
}
}
/* GPIO 초기화 */
static void gpio_init(void)
{
gpio_pin_configure_dt(&power_btn, GPIO_INPUT); // 전원 버튼(P1.08) 입력 설정
DBG_PRINTF("[INIT] HW - GPIO OK (POWER BTN=%d)\r\n", gpio_pin_get_dt(&power_btn));
}
/* 기본 설정값 로드 */
static void load_default_config(void)
{
// 기본 문자열 길이 계산
// 기본 문자열 길이 제한
size_t serial_len = strlen(SERIAL_NUMBER);
size_t hw_len = strlen(HARDWARE_VERSION);
size_t passkey_len = strlen(DEFAULT_PASSKEY);
// 버퍼 오버플로우 방지 - 기본 문자열 길이 제한 (긴 경우 잘라내기)
if (serial_len > SERIAL_NO_LENGTH)
{
serial_len = SERIAL_NO_LENGTH;
}
if (hw_len > HW_NO_LENGTH)
{
hw_len = HW_NO_LENGTH;
}
if (passkey_len > PASSKEY_LENGTH)
{
passkey_len = PASSKEY_LENGTH;
}
// 복사할 때 버퍼를 먼저 0으로 채우고 복사 -> 문자열 종료 NUL 보장
// 기본 Serial Number 복사
memset(SERIAL_NO, 0, sizeof(SERIAL_NO));
memcpy(SERIAL_NO, SERIAL_NUMBER, serial_len);
@@ -108,54 +318,412 @@ static void load_default_config(void)
memset(m_static_passkey, 0, sizeof(m_static_passkey));
memcpy(m_static_passkey, DEFAULT_PASSKEY, passkey_len);
m_passkey_changed = 0U; // 패스키 변경 여부 초기화
// NVS 로드 전 기본값은 변경 가능 상태
m_passkey_changed = 0U;
m_reset_status = 1; // 기본 reset 상태 코드 설정
bond_data_delete = true; // 기본 상태는 bond 삭제 완료 삭제로 초기화
/* 기본 상태값 */
m_reset_status = 1;
bond_data_delete = true;
DBG_CORE("[INIT] Default (S/N=%s)\r\n", SERIAL_NO);
}
/* 전원 OFF 타임아웃 콜백 */
static void t_power_off_timeout_handler(struct k_timer *timer)
{
ARG_UNUSED(timer);
DBG_ERR("[PWR] OFF timeout reason=%s\r\n", power_off_pending_reason);
led_set_state(LED_STATE_OFF);
power_control_handler(OFF, "off-timeout");
}
/* 슬립 모드 / 전원 OFF: LED 표시 후 3초 뒤 전원 차단 */
static void power_off_schedule(const char *reason, led_state_t led_state)
{
power_off_pending_reason = reason ? reason : "unknown";
DBG_ERR("[PWR] OFF scheduled reason = %s\r\n", power_off_pending_reason);
led_set_state(led_state);
k_timer_start(&m_power_off_delay_timer, K_MSEC(POWER_OFF_DELAY), K_NO_WAIT);
}
void sleep_mode_enter_reason(const char *reason)
{
power_off_schedule(reason, LED_STATE_POWER_ON);
}
void sleep_mode_enter(void)
{
sleep_mode_enter_reason("sleep_mode_enter");
}
void device_power_off(void)
{
device_power_off_reason("device_power_off");
}
void device_power_off_reason(const char *reason)
{
power_off_schedule(reason, LED_STATE_POWER_OFF);
}
void device_power_keep_on(void)
{
k_timer_stop(&m_power_off_delay_timer);
device_on = true;
boot_btn_released = (gpio_pin_get_dt(&power_btn) != 1);
cnt_s = 0;
power_control_handler(ON, "keep-on");
}
void power_button_suspend(bool suspend)
{
// 측정 시간이 긴 커맨드 처리 중에만 버튼 폴링 판단 잠시 멈춤
power_btn_suspended = suspend;
cnt_s = 0;
}
/* 전원 버튼 상태머신 (5ms 폴링) */
#define BOOT_THRESHOLD 200 // 5ms x 400 = 2초 -> 초기화 시간 고려 체감상 2초에 맞춤
static void main_s(struct k_timer *timer)
{
ARG_UNUSED(timer);
bool button_pressed = (gpio_pin_get_dt(&power_btn) == 1);
if (power_btn_suspended)
{
k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT);
return;
}
// 부팅 시퀀스 (OFF → ON)
if (!device_on)
{
if (!button_pressed) // 버튼 놓음 → 2초 미만이면 전원 OFF
{
//DBG_PRINTF("[BTN] Short press (%d) -> OFF\r\n", cnt_s);
power_control_handler(OFF, "boot-button-released-before-latch");
cnt_s = 0;
}
else // 버튼 계속 누르고 있음
{
cnt_s++;
if (cnt_s == BOOT_THRESHOLD) // 2초 도달: 래치 + 부팅 완료
{
device_on = true;
cnt_s = 0; // 카운터 리셋: 안 하면 다음 틱에서 ON→OFF 분기가 cnt_s >= 200 조건을 즉시 만족하여 전원 OFF됨
power_control_handler(ON, "button-2s-latch");
led_set_state(LED_STATE_ADVERTISING);
k_work_submit(&adv_start_work);
battery_timer_start();
m_reset_status = 1;
}
}
}
/* 전원 OFF 시퀀스 (ON → OFF) */
else
{
if (!boot_btn_released) // 부팅 시 눌렀던 버튼을 아직 안 놓음 → 대기
{
if (!button_pressed)
{
boot_btn_released = true;
}
}
else if (button_pressed) // 버튼 새로 누르고 있음
{
cnt_s++;
if (cnt_s >= BOOT_THRESHOLD) // 2초 이상 → 전원 OFF
{
battery_timer_stop();
k_work_submit(&adv_stop_work);
device_on = false;
boot_btn_released = false;
cnt_s = 0;
sleep_mode_enter_reason("button-long-press");
return;
}
}
else // 버튼 놓음 → 카운터 리셋
{
if (cnt_s > 0)
{
DBG_PRINTF("[BTN] Short press (%d) -> ignored\r\n", cnt_s);
}
cnt_s = 0;
}
}
k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT);
}
/* 타이머 초기화 / 시작 */
static void timers_init(void)
{
// 전원 상태머신 타이머
k_timer_init(&m_power_on_delay_timer, main_s, NULL);
k_timer_init(&m_power_off_delay_timer, t_power_off_timeout_handler, NULL);
// BLE advertising work
k_work_init(&adv_start_work, adv_start_work_handler);
k_work_init(&adv_stop_work, adv_stop_work_handler);
// BLE 명령 처리 work queue
k_work_init(&ble_cmd_work, ble_cmd_work_handler);
k_work_queue_start(&ble_cmd_work_q, ble_cmd_workq_stack, K_THREAD_STACK_SIZEOF(ble_cmd_workq_stack), BLE_CMD_WORKQ_PRIORITY, NULL);
// 전원 제어 타이머
power_timer_init();
// DFU 업로드 상태를 LED에 반영
mgmt_callback_register(&dfu_status_callback);
// DFU 명령 실패 시 LED 상태 복구
mgmt_callback_register(&smp_cmd_status_callback);
}
/* 전원 버튼 폴링 시작 (5ms 후 main_s 콜백) */
static void timers_start(void)
{
k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT);
}
static enum mgmt_cb_return dfu_status_cb(uint32_t event,
enum mgmt_cb_return prev_status,
int32_t *rc,
uint16_t *group,
bool *abort_more,
void *data,
size_t data_size)
{
ARG_UNUSED(prev_status);
ARG_UNUSED(rc);
ARG_UNUSED(group);
ARG_UNUSED(abort_more);
ARG_UNUSED(data);
ARG_UNUSED(data_size);
switch (event)
{
case MGMT_EVT_OP_IMG_MGMT_DFU_STARTED:
case MGMT_EVT_OP_IMG_MGMT_DFU_CHUNK:
// DFU 업로드 중에는 초록 LED ON
dfu_led_active = true;
led_ble_solid();
break;
case MGMT_EVT_OP_IMG_MGMT_DFU_PENDING:
// 업로드 완료 후 리셋 전까지 초록 LED ON
dfu_led_active = true;
led_ble_solid();
// DFU 리셋 후 버튼 없이 전원 ON
#if NRF_POWER_HAS_GPREGRET
nrf_power_gpregret_set(NRF_POWER, DFU_RESUME_GPREGRET_REG, DFU_RESUME_MAGIC);
#endif
break;
case MGMT_EVT_OP_IMG_MGMT_DFU_STOPPED:
// 실패나 중단 시 일반 광고 LED로 복귀
if (dfu_led_active)
{
dfu_led_active = false;
led_set_state(LED_STATE_ADVERTISING);
}
break;
default:
break;
}
return MGMT_CB_OK;
}
static enum mgmt_cb_return smp_cmd_status_cb(uint32_t event,
enum mgmt_cb_return prev_status,
int32_t *rc,
uint16_t *group,
bool *abort_more,
void *data,
size_t data_size)
{
ARG_UNUSED(event);
ARG_UNUSED(prev_status);
ARG_UNUSED(rc);
ARG_UNUSED(group);
ARG_UNUSED(abort_more);
if (data == NULL || data_size != sizeof(struct mgmt_evt_op_cmd_arg))
{
return MGMT_CB_OK;
}
const struct mgmt_evt_op_cmd_arg *cmd = data;
// 이미지 업로드/test 명령 실패 시 DFU LED 고정 해제
if (cmd->group == MGMT_GROUP_ID_IMAGE &&
(cmd->id == IMG_MGMT_ID_UPLOAD || cmd->id == IMG_MGMT_ID_STATE) &&
cmd->err != MGMT_ERR_EOK)
{
DBG_ERR("[DFU] command failed id=%u err=%d\r\n", cmd->id, cmd->err);
dfu_led_active = false;
led_set_state(LED_STATE_ADVERTISING);
// 실패 후 리셋 복구 표식이 남지 않게 정리
#if NRF_POWER_HAS_GPREGRET
nrf_power_gpregret_set(NRF_POWER, DFU_RESUME_GPREGRET_REG, 0U);
#endif
}
return MGMT_CB_OK;
}
static void resume_device_after_soft_reset(void)
{
// 일반 버튼 부팅이면 skip
if (!resume_without_power_button && !dfu_confirm_pending_boot && !dfu_reset_resume_request)
{
return;
}
// 리셋 복귀나 DFU test 부팅은 버튼 없이도 ON 상태로 복구
device_on = true;
cnt_s = 0;
boot_btn_released = (gpio_pin_get_dt(&power_btn) != 1);
m_reset_status = 1;
// 정상 ON 상태 서비스 재시작
power_control_handler(ON, "resume-after-reset");
led_set_state(LED_STATE_ADVERTISING);
battery_timer_start();
k_work_submit(&adv_start_work);
DBG_CORE("[BOOT] Resume after resetreas=0x%08x\r\n", boot_reset_reason);
}
static void confirm_running_image(void)
{
#if defined(CONFIG_BOOTLOADER_MCUBOOT)
// 기본 초기화와 BLE 준비가 끝난 뒤 MCUboot revert 방지
if (!boot_is_img_confirmed())
{
int err = boot_write_img_confirmed();
if (err == 0)
{
DBG_CORE("[DFU] Running image confirmed\r\n");
}
else
{
DBG_ERR("[DFU] Image confirm failed (err %d)\r\n", err);
}
}
else
{
DBG_CORE("[DFU] Running image already confirmed\r\n");
}
#endif
}
static const char *swap_type_to_str(int swap_type)
{
// MCUboot swap 상태 문자열
switch (swap_type)
{
case BOOT_SWAP_TYPE_NONE:
return "none";
case BOOT_SWAP_TYPE_TEST:
return "test";
case BOOT_SWAP_TYPE_PERM:
return "perm";
case BOOT_SWAP_TYPE_REVERT:
return "revert";
case BOOT_SWAP_TYPE_FAIL:
return "fail";
default:
return "unknown";
}
}
static void log_mcuboot_state(void)
{
#if defined(CONFIG_BOOTLOADER_MCUBOOT)
// 현재 부팅 슬롯/교체 상태
struct mcuboot_img_header header;
uint8_t active_slot = boot_fetch_active_slot();
int swap_type = mcuboot_swap_type();
int err = boot_read_bank_header(active_slot, &header, sizeof(header));
DBG_CORE("[DFU] active_slot=%u swap_type=%s(%d) confirmed=%d\r\n",
active_slot,
swap_type_to_str(swap_type),
swap_type,
boot_is_img_confirmed());
if (err == 0)
{
/* 실행 이미지 버전 */
DBG_CORE("[DFU] image version %u.%u.%u+%u size=0x%x\r\n",
header.h.v1.sem_ver.major,
header.h.v1.sem_ver.minor,
header.h.v1.sem_ver.revision,
header.h.v1.sem_ver.build_num,
header.h.v1.image_size);
}
else
{
DBG_ERR("[DFU] image header read failed (slot %u, err %d)\r\n",
active_slot,
err);
}
#endif
}
/* 메인 함수 */
int main(void)
{
// 리셋/복귀 컨텍스트
boot_context_detect();
#if defined(CONFIG_BOOTLOADER_MCUBOOT)
dfu_confirm_pending_boot = dfu_is_confirm_pending_boot(); // MCUboot test 이미지 상태인지 확인
#else
dfu_confirm_pending_boot = false;
#endif
// 하드웨어 기본 초기화
power_hold_init();
cnt_s = 0;
power_control_latch_init(resume_without_power_button || dfu_confirm_pending_boot || dfu_reset_resume_request);
power_control_reset_state();
// 버튼 부팅 기본 상태
device_on = false;
boot_btn_released = false;
confirm_running_image();
DBG_CORE("\r\n========================================\r\n");
DBG_CORE(" TEST BUILD %s (Zephyr)\r\n", FIRMWARE_VERSION);
DBG_CORE(" BUILD TAG: %s\r\n", CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION);
DBG_CORE(" BUILD TAG: TEST-003 - tx power dynamic control\r\n");
DBG_CORE("========================================\r\n");
// 각 모듈 초기화
power_control_init();
ble_cmd_queue_init();
dfu_manager_init();
dfu_log_mcuboot_state();
DBG_CORE("[INIT] HW Init\r\n");
// 기본 하드웨어/센서 초기화
gpio_init();
timers_init();
log_mcuboot_state();
load_default_config();
led_init();
battery_adc_init();
battery_timer_init();
imu_init();
piezo_config_init();
DBG_CORE("[INIT] HW - gpio/timer/config/led/batt/imu/temp/piezo-cfg OK\r\n");
// BLE Stack/NUS 초기화 (0=성공)
if (ble_service_init(ble_cmd_queue_rx_handler) == 0) // BLE RX 콜백은 command queue로 연결
// BLE 스택 + NUS
DBG_CORE("[INIT] BLE Init\r\n");
if (ble_service_init(ble_rx_handler) == 0)
{
// 버튼 없이 정상 ON 상태로 복구하는 경우
if (resume_without_power_button || dfu_confirm_pending_boot || dfu_reset_resume_request)
{
power_control_resume_after_reset(boot_reset_reason);
}
dfu_confirm_running_image(); // BLE 준비까지 확인한 뒤 DFU test 이미지 확정(confirm)
DBG_CORE("[INIT] BLE - ble/nus OK\r\n");
// 소프트 리셋 후 서비스 복구
resume_device_after_soft_reset();
// BLE 준비까지 확인한 뒤 DFU test 이미지를 확정
confirm_running_image();
}
else
{
@@ -166,10 +734,10 @@ int main(void)
DBG_CORE(" READY [%s]\r\n", SERIAL_NO);
DBG_CORE("========================================\r\n");
power_control_start(); // 전원 버튼 상태머신 시작 (부팅 시 버튼이 눌려있는 상태)
// 전원 버튼 상태머신 시작 (부팅 시 버튼이 눌려있는 상태)
timers_start();
// main loop - idle
// callback, work queue 기반 동작 유지
// 메인 루프 - idle
for (;;)
{
k_msleep(100);
+3 -1
View File
@@ -15,7 +15,7 @@
/*==============================================================================
* Firmware identification : Default values, can be overridden by NVS
*============================================================================*/
#define FIRMWARE_VERSION "VBTFW0200"
#define FIRMWARE_VERSION "TSTFW042"
#define HARDWARE_VERSION "VB0HW0000"
#define SERIAL_NUMBER "VBT260300ZZ"
#define DEFAULT_PASSKEY "123456"
@@ -33,6 +33,7 @@
/*==============================================================================
* Enumerations
*============================================================================*/
typedef enum
{
OFF = 0,
@@ -60,6 +61,7 @@ typedef enum
/*==============================================================================
* Function declarations
*============================================================================*/
void sleep_mode_enter(void);
void sleep_mode_enter_reason(const char *reason);
void device_power_off(void);
+449
View File
@@ -0,0 +1,449 @@
/*******************************************************************************
* @file measure_queue.c
* @brief Flash-backed queued piezo measurement frames
******************************************************************************/
#include <zephyr/kernel.h>
#include <zephyr/storage/flash_map.h>
#include <errno.h>
#include <string.h>
#include "measure_queue.h"
#include "debug_print.h"
#include "main.h"
#include "led_control.h"
#define MEASURE_QUEUE_PERIOD_MS 100U
#define MEASURE_QUEUE_THREAD_STACK_SIZE 2048
#define MEASURE_QUEUE_THREAD_PRIORITY 7
/* data_storage flash partition handle, opened once and reused */
static const struct flash_area *queue_area;
/* Recording runs in its own thread, separate from BLE command handling */
static struct k_thread queue_thread;
static K_THREAD_STACK_DEFINE(queue_thread_stack, MEASURE_QUEUE_THREAD_STACK_SIZE);
/* Lock for shared queue state */
static struct k_mutex queue_lock;
static bool queue_lock_ready;
/* Current recording state */
static bool queue_recording; // true while the worker keeps storing frames
static bool queue_thread_running; // true while the worker thread is alive
static uint8_t queue_session; // measurement session id, incremented on each mfq?
static uint16_t queue_total_frames; // number of frames written to flash
static uint16_t queue_num_samples; // samples value latched at recording start
static uint32_t queue_tick_start; // actual recording start tick
static uint32_t queue_tick_end; // actual recording end tick
static int queue_status; // last stop status: 0=OK, negative=errno
/* Work buffers sized for one max-size frame */
static uint8_t frame_write_buf[4 + (PIEZO_NUM_CHANNELS * PIEZO_MEASURE_MAX_SAMPLES * 2)];
static uint8_t frame_read_buf[4 + (PIEZO_NUM_CHANNELS * PIEZO_MEASURE_MAX_SAMPLES * 2)];
/* Lazy init for the mutex on first use */
static void queue_init_lock_once(void)
{
if (!queue_lock_ready)
{
k_mutex_init(&queue_lock);
queue_lock_ready = true;
}
}
/*
* Byte size of one frame in flash
* Layout: tick_ms 4B + 6 channels * samples * uint16 2B
*/
static uint32_t queue_frame_size(uint16_t num_samples)
{
return 4U + (uint32_t)PIEZO_NUM_CHANNELS * (uint32_t)num_samples * 2U;
}
/* Max frame count that fits in the current data_storage area */
static uint16_t queue_capacity_frames(uint16_t num_samples)
{
if (queue_area == NULL)
{
return 0U;
}
return (uint16_t)(queue_area->fa_size / queue_frame_size(num_samples));
}
/* Flash format uses big endian for tick and samples */
static void put_u16_be(uint8_t *dst, uint16_t value)
{
dst[0] = (uint8_t)(value >> 8);
dst[1] = (uint8_t)(value & 0xFF);
}
static void put_u32_be(uint8_t *dst, uint32_t value)
{
dst[0] = (uint8_t)(value >> 24);
dst[1] = (uint8_t)(value >> 16);
dst[2] = (uint8_t)(value >> 8);
dst[3] = (uint8_t)(value & 0xFF);
}
static uint16_t get_u16_be(const uint8_t *src)
{
return ((uint16_t)src[0] << 8) | (uint16_t)src[1];
}
static uint32_t get_u32_be(const uint8_t *src)
{
return ((uint32_t)src[0] << 24) | ((uint32_t)src[1] << 16) | ((uint32_t)src[2] << 8) | (uint32_t)src[3];
}
/*
* Store the current piezo sweep result as one frame
* Order: tick_ms -> CH0 samples -> CH1 samples -> ... -> CH5 samples
*/
static int queue_write_current_frame(uint16_t frame_idx, uint32_t tick_ms)
{
uint16_t num_samples = queue_num_samples;
uint32_t frame_size = queue_frame_size(num_samples);
uint8_t *p = frame_write_buf;
put_u32_be(p, tick_ms);
p += 4;
for (uint8_t ch = 0; ch < PIEZO_NUM_CHANNELS; ch++)
{
const uint16_t *samples = piezo_measure_channel_buffer(ch);
if (samples == NULL)
{
return -EINVAL;
}
for (uint16_t i = 0; i < num_samples; i++)
{
put_u16_be(p, samples[i]);
p += 2;
}
}
return flash_area_write(queue_area, (off_t)((uint32_t)frame_idx * frame_size), frame_write_buf, frame_size);
}
/*
* Worker thread started by mfq?
* Keeps 100 ms recording separate from BLE command handling
*/
static void queue_record_thread(void *a, void *b, void *c)
{
ARG_UNUSED(a);
ARG_UNUSED(b);
ARG_UNUSED(c);
int status = piezo_measure_start_session();
while (status == ECHO_STATUS_OK)
{
k_mutex_lock(&queue_lock, K_FOREVER);
bool should_record = queue_recording;
uint16_t frame_idx = queue_total_frames;
uint16_t capacity = queue_capacity_frames(queue_num_samples);
k_mutex_unlock(&queue_lock);
if (!should_record)
{
break;
}
if (frame_idx >= capacity)
{
// 저장 공간 full인 경우 측정 및 저장 자동 종료
status = -ENOSPC;
break;
}
/* Frame timestamp shared by all six channels */
uint32_t tick = k_uptime_get_32();
status = piezo_measure_sweep_once();
if (status != ECHO_STATUS_OK)
{
break;
}
int err = queue_write_current_frame(frame_idx, tick);
if (err)
{
status = err;
break;
}
k_mutex_lock(&queue_lock, K_FOREVER);
queue_total_frames++;
k_mutex_unlock(&queue_lock);
/* Keep roughly a 100 ms period if sweep/write finishes early */
uint32_t elapsed = k_uptime_get_32() - tick;
if (elapsed < MEASURE_QUEUE_PERIOD_MS)
{
k_msleep(MEASURE_QUEUE_PERIOD_MS - elapsed);
}
}
/* Common cleanup for manual stop, full storage, and errors */
piezo_power_off();
power_button_suspend(false);
led_set_state(LED_STATE_OFF); // storage stopped
k_mutex_lock(&queue_lock, K_FOREVER);
queue_tick_end = k_uptime_get_32();
queue_status = status;
queue_recording = false;
queue_thread_running = false;
processing = false;
k_mutex_unlock(&queue_lock);
DBG_PRINTF("[MFQ] record thread stop status=%d frames=%u\r\n", status, queue_total_frames);
}
/*
* Start flash-backed recording
* Flow: duplicate check -> open data_storage -> erase -> init state -> start worker
* rfq: is sent by cmd_mfq() after this function returns
*/
int measure_queue_start(uint32_t *tick_start)
{
int err;
queue_init_lock_once();
k_mutex_lock(&queue_lock, K_FOREVER);
if (queue_recording || queue_thread_running)
{
if (tick_start != NULL)
{
*tick_start = queue_tick_start;
}
k_mutex_unlock(&queue_lock);
return -EALREADY;
}
k_mutex_unlock(&queue_lock);
if (queue_area == NULL)
{
// Zephyr flash_map에서 data_storage 파티션 열기
err = flash_area_open(FIXED_PARTITION_ID(data_storage), &queue_area);
if (err)
{
DBG_ERR("[MFQ] data_storage open failed err=%d\r\n", err);
return err;
}
}
const piezo_config_t *cfg = piezo_config_get();
uint32_t frame_size = queue_frame_size(cfg->samples);
uint16_t capacity = queue_capacity_frames(cfg->samples);
uint32_t usable_bytes = (uint32_t)capacity * frame_size;
uint32_t erase_start = k_uptime_get_32();
DBG_PRINTF("[MFQ] data record prepare area_off=0x%08x area_size=%uB samples=%u frame_size=%uB capacity=%u frames usable=%uB\r\n",
(uint32_t)queue_area->fa_off,
(uint32_t)queue_area->fa_size,
cfg->samples,
frame_size,
capacity,
usable_bytes);
DBG_PRINTF("[MFQ] data_storage erase start size=%uB\r\n", (uint32_t)queue_area->fa_size);
led_set_state(LED_STATE_DATA_ERASING); // erasing: green LED blink
// Clear previous recording before starting a new one
err = flash_area_erase(queue_area, 0, queue_area->fa_size);
uint32_t erase_ms = k_uptime_get_32() - erase_start;
if (err)
{
DBG_ERR("[MFQ] data_storage erase failed err=%d elapsed=%ums\r\n", err, erase_ms);
return err;
}
DBG_PRINTF("[MFQ] data_storage erase done elapsed=%ums\r\n", erase_ms);
led_set_state(LED_STATE_DATA_STORAGING); // storaging: green LED on
k_mutex_lock(&queue_lock, K_FOREVER);
queue_session++; // session id
queue_total_frames = 0U; // 새로운 측정 시 프레임 카운트 리셋
queue_num_samples = cfg->samples; // latch samples at recording start
queue_tick_start = k_uptime_get_32();
queue_tick_end = queue_tick_start;
queue_status = ECHO_STATUS_OK;
queue_recording = true;
queue_thread_running = true;
processing = true;
power_button_suspend(true);
if (tick_start != NULL)
{
*tick_start = queue_tick_start;
}
k_mutex_unlock(&queue_lock);
k_thread_create(&queue_thread, queue_thread_stack, K_THREAD_STACK_SIZEOF(queue_thread_stack), queue_record_thread, NULL, NULL, NULL, MEASURE_QUEUE_THREAD_PRIORITY, 0, K_NO_WAIT);
DBG_PRINTF("[MFQ] data record start session=%u tick_start=%u samples=%u frame_size=%uB capacity=%u frames\r\n",
queue_session,
queue_tick_start,
queue_num_samples,
queue_frame_size(queue_num_samples),
queue_capacity_frames(queue_num_samples));
return 0;
}
/* 측정 종료 및 저장 완료 후 상태 확인 */
int measure_queue_stop(uint32_t *tick_end, uint8_t *session, uint16_t *total_frames)
{
queue_init_lock_once();
k_mutex_lock(&queue_lock, K_FOREVER);
bool was_running = queue_thread_running;
queue_recording = false;
k_mutex_unlock(&queue_lock);
if (was_running)
{
// rfe: 응답 시 측정 중인 경우 측정 완료될 때까지 기다림
(void)k_thread_join(&queue_thread, K_FOREVER);
}
k_mutex_lock(&queue_lock, K_FOREVER);
if (tick_end != NULL)
{
*tick_end = queue_tick_end;
}
if (session != NULL)
{
*session = queue_session;
}
if (total_frames != NULL)
{
*total_frames = queue_total_frames;
}
int status = queue_status;
uint8_t stop_session = queue_session;
uint16_t stop_frames = queue_total_frames;
uint16_t num_samples = queue_num_samples;
uint32_t frame_size = queue_frame_size(num_samples);
uint16_t capacity = queue_capacity_frames(num_samples);
uint32_t usable_bytes = (uint32_t)capacity * frame_size;
uint32_t used_bytes = (uint32_t)stop_frames * frame_size;
uint32_t remain_bytes = (used_bytes < usable_bytes) ? (usable_bytes - used_bytes) : 0U;
k_mutex_unlock(&queue_lock);
DBG_PRINTF("[MFE] data record stop status=%d session=%u frames=%u/%u samples=%u frame_size=%uB used=%uB usable=%uB remain=%uB\r\n",
status,
stop_session,
stop_frames,
capacity,
num_samples,
frame_size,
used_bytes,
usable_bytes,
remain_bytes);
return status;
}
bool measure_queue_is_recording(void)
{
queue_init_lock_once();
k_mutex_lock(&queue_lock, K_FOREVER);
bool recording = queue_recording;
k_mutex_unlock(&queue_lock);
return recording;
}
uint8_t measure_queue_session(void)
{
queue_init_lock_once();
k_mutex_lock(&queue_lock, K_FOREVER);
uint8_t session = queue_session;
k_mutex_unlock(&queue_lock);
return session;
}
uint16_t measure_queue_total_frames(void)
{
queue_init_lock_once();
k_mutex_lock(&queue_lock, K_FOREVER);
uint16_t total = queue_total_frames;
k_mutex_unlock(&queue_lock);
return total;
}
uint16_t measure_queue_num_samples(void)
{
queue_init_lock_once();
k_mutex_lock(&queue_lock, K_FOREVER);
uint16_t num_samples = queue_num_samples;
k_mutex_unlock(&queue_lock);
return num_samples;
}
int measure_queue_read_frame(uint16_t frame_idx, measure_queue_frame_header_t *header, uint16_t samples[PIEZO_NUM_CHANNELS][PIEZO_MEASURE_MAX_SAMPLES])
{
if ((header == NULL) || (samples == NULL))
{
return -EINVAL;
}
queue_init_lock_once();
k_mutex_lock(&queue_lock, K_FOREVER);
uint16_t total = queue_total_frames;
uint16_t num_samples = queue_num_samples;
uint8_t session = queue_session;
bool recording = queue_recording;
k_mutex_unlock(&queue_lock);
if (recording)
{
// 아직 저장 측정이 진행 중일 때 mqb? 가 오는 경우 busy 에러
return -EBUSY;
}
if (frame_idx >= total)
{
return -ENOENT;
}
if (queue_area == NULL)
{
int err = flash_area_open(FIXED_PARTITION_ID(data_storage), &queue_area);
if (err)
{
return err;
}
}
uint32_t frame_size = queue_frame_size(num_samples);
int err = flash_area_read(queue_area, (off_t)((uint32_t)frame_idx * frame_size), frame_read_buf, frame_size);
if (err)
{
return err;
}
/* Build header fields for rqh: */
header->session = session;
header->frame_idx = frame_idx;
header->tick_ms = get_u32_be(frame_read_buf);
header->ch_mask = MEASURE_QUEUE_CH_MASK_ALL;
/* Restore channel samples for rqb: */
const uint8_t *p = &frame_read_buf[4];
for (uint8_t ch = 0; ch < PIEZO_NUM_CHANNELS; ch++)
{
for (uint16_t i = 0; i < num_samples; i++)
{
samples[ch][i] = get_u16_be(p);
p += 2;
}
}
return 0;
}
+33
View File
@@ -0,0 +1,33 @@
/*******************************************************************************
* @file measure_queue.h
* @brief Flash-backed queued piezo measurement frames
******************************************************************************/
#ifndef MEASURE_QUEUE_H__
#define MEASURE_QUEUE_H__
#include <stdbool.h>
#include <stdint.h>
#include "piezo_measure.h"
#define MEASURE_QUEUE_CH_MASK_ALL 0x3FU
typedef struct
{
uint8_t session;
uint16_t frame_idx;
uint32_t tick_ms;
uint8_t ch_mask;
} measure_queue_frame_header_t;
int measure_queue_start(uint32_t *tick_start);
int measure_queue_stop(uint32_t *tick_end, uint8_t *session, uint16_t *total_frames);
bool measure_queue_is_recording(void);
uint8_t measure_queue_session(void);
uint16_t measure_queue_total_frames(void);
uint16_t measure_queue_num_samples(void);
int measure_queue_read_frame(uint16_t frame_idx,
measure_queue_frame_header_t *header,
uint16_t samples[PIEZO_NUM_CHANNELS][PIEZO_MEASURE_MAX_SAMPLES]);
#endif /* MEASURE_QUEUE_H__ */
+23 -27
View File
@@ -20,7 +20,7 @@
/* 앱에서 저장/변경 가능한 piezo 측정 기본값 */
#define PIEZO_CFG_FREQ_DEFAULT PIEZO_CFG_FREQ_2_1MHZ
#define PIEZO_CFG_CYCLES_DEFAULT 7
#define PIEZO_CFG_CYCLES_DEFAULT 3
#define PIEZO_CFG_DELAY_DEFAULT 10
#define PIEZO_CFG_SAMPLES_DEFAULT 100
#define PIEZO_CFG_AVG_DEFAULT 3
@@ -39,10 +39,7 @@
#define PIEZO_AVG_INTER_BURST_GAP_US 650
/* real capture 전에 같은 채널에서 버스트+ADC capture를 버리는 횟수(dummy) */
#define PIEZO_DUMMY_CAPTURE_COUNT 1
/* 채널 0 시작 전 안정을 위한 추가 딜레이 */
#define PIEZO_CH0_WARMUP_TO_REAL_SETTLE_US 3000
#define PIEZO_DUMMY_CAPTURE_COUNT 5
/* 6채널 sweep 결과 (각 채널의 평균 완료 후 최종만 저장) */
static uint16_t piezo_channels[PIEZO_NUM_CHANNELS][PIEZO_MEASURE_MAX_SAMPLES];
@@ -184,7 +181,7 @@ int piezo_measure_start_session(void)
return ECHO_STATUS_ADC_INIT;
}
DBG_PRINTF("[SWEEP] piezo session ready\r\n");
//DBG_PRINTF("[SWEEP] piezo session ready\r\n");
return ECHO_STATUS_OK;
}
@@ -192,7 +189,7 @@ int piezo_measure_start_session(void)
* 6채널 전체 sweep
* 각 채널마다 dummy capture를 먼저 버린 뒤, cfg->avg회 real capture를 sample index별로 평균
*/
int piezo_measure_sweep(void)
static int piezo_measure_sweep_with_avg(uint8_t avg_override)
{
const piezo_config_t *cfg = piezo_config_get();
@@ -200,14 +197,15 @@ int piezo_measure_sweep(void)
uint8_t cycles = cfg->cycles;
uint16_t capture_delay_us = cfg->delay_us;
uint16_t samples = cfg->samples;
uint8_t avg = cfg->avg;
uint8_t avg = (avg_override > 0U) ? avg_override : cfg->avg;
if (capture_delay_us < 0)
// 너무 짧은 delay가 들어오면 burst 직후 ADC capture가 겹치므로 최소 delay 보장
if (capture_delay_us < PIEZO_BURST_TO_ADC_DELAY_US)
{
capture_delay_us = 0;
capture_delay_us = PIEZO_BURST_TO_ADC_DELAY_US;
}
DBG_PRINTF("[SWEEP] freq=0x%04X cycles=%u avg=%u delay_us=%u samples=%u\r\n", cfg->freq, cfg->cycles, cfg->avg, cfg->delay_us, cfg->samples);
//DBG_PRINTF("[SWEEP] freq=0x%04X cycles=%u avg=%u delay_us=%u samples=%u\r\n", cfg->freq, cfg->cycles, cfg->avg, cfg->delay_us, cfg->samples);
for (uint8_t ch = 0; ch < PIEZO_NUM_CHANNELS; ch++)
{
@@ -230,11 +228,10 @@ int piezo_measure_sweep(void)
DBG_PRINTF("[ECHO] dummy read fail ch=%d err=%d\r\n", ch, err);
return ECHO_STATUS_CAPTURE;
}
if (ch == 0U) // 채널 0 시작 전에만 dummy burst + ADC capture 수행
{
// Dummy burst + ADC caputre
/*
// Dummy burst - ADC caputre는 수행하지 않음
for (uint8_t dummy = 0; dummy < PIEZO_DUMMY_CAPTURE_COUNT; dummy++)
{
k_busy_wait(PIEZO_AVG_INTER_BURST_GAP_US);
@@ -255,20 +252,10 @@ int piezo_measure_sweep(void)
DBG_PRINTF("[ECHO] dummy capture fail ch=%d dummy=%d err=%d\r\n", ch, dummy, err);
return ECHO_STATUS_CAPTURE;
}
}*/
k_busy_wait(PIEZO_AVG_INTER_BURST_GAP_US);
piezo_burst_sw_freq(freq, cycles);
k_busy_wait(capture_delay_us);
err = echo_adc_capture(echo_capture, samples);
if (err)
{
//DBG_PRINTF("[ECHO] dummy capture fail ch=%d dummy=%d err=%d\r\n", ch, dummy, err);
return ECHO_STATUS_CAPTURE;
}
// 마지막 dummy capture와 첫 real capture 사이에도 같은 회복 시간 + 추가 안정화
k_busy_wait(PIEZO_AVG_INTER_BURST_GAP_US + PIEZO_CH0_WARMUP_TO_REAL_SETTLE_US);
// 마지막 dummy capture와 첫 real capture 사이에도 같은 회복 시간
k_busy_wait(PIEZO_AVG_INTER_BURST_GAP_US + 3000);
}
// 여기부터 실제 평균에 들어갈 capture
@@ -288,7 +275,6 @@ int piezo_measure_sweep(void)
// IRQ 잠금 해제
irq_unlock(key);
if (err)
{
DBG_PRINTF("[ECHO] capture fail ch=%d avg=%d err=%d\r\n", ch, a, err);
@@ -315,6 +301,16 @@ int piezo_measure_sweep(void)
return ECHO_STATUS_OK;
}
int piezo_measure_sweep(void)
{
return piezo_measure_sweep_with_avg(0U);
}
int piezo_measure_sweep_once(void)
{
return piezo_measure_sweep_with_avg(1U);
}
/*
* 테스트용
* 단일 채널 burst + echo capture
+1
View File
@@ -38,6 +38,7 @@ int piezo_config_set(const piezo_config_t *cfg);
int piezo_measure_start_session(void);
int piezo_measure_sweep(void);
int piezo_measure_sweep_once(void);
int piezo_measure_single_capture(uint8_t freq, uint8_t cycles, uint16_t delay_us, uint16_t num_samples, uint16_t averaging, uint8_t channel);
int piezo_measure_adc_only_capture(uint16_t num_samples, uint16_t averaging, uint8_t channel);
-382
View File
@@ -1,382 +0,0 @@
/*******************************************************************************
* @file power_control.c
* @brief Device power sequence control
******************************************************************************/
#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/reboot.h>
#if IS_ENABLED(CONFIG_BT_SMP)
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#endif
#include "main.h"
#include "power_control.h"
#include "debug_print.h"
#include "led_control.h"
#include "ble_service.h"
#include "battery_adc.h"
#define POWER_HOLD_NODE DT_NODELABEL(pwr_hold) // 전원 래치
#define POWER_BTN_NODE DT_NODELABEL(button_check) // 전원 버튼
#define POWER_LOOP_INTERVAL 20 // 전원 시퀀스 타이머 간격(ms)
#define POWER_ON_THRESHOLD 350 // power button hold threshold(off -> on)
#define POWER_OFF_THRESHOLD 500 // power button hold threshold(on -> off)
#define BOND_RESET_THRESHOLD (15000 / POWER_ON_DELAY) // 15s long press bond reset
static const struct gpio_dt_spec power_hold = GPIO_DT_SPEC_GET(POWER_HOLD_NODE, gpios); // 전원 유지 출력
static const struct gpio_dt_spec power_btn = GPIO_DT_SPEC_GET(POWER_BTN_NODE, gpios); // 전원 버튼 입력
static struct k_timer m_power_on_delay_timer; // 전원 버튼 상태머신 타이머
static struct k_timer m_power_off_delay_timer; // 전원 OFF 지연 타이머
static struct k_timer m_power_timer; // 전원 시퀀스 반복 타이머
static struct k_work adv_start_work; // advertising 시작 work
static struct k_work adv_stop_work; // advertising 중지 work
static struct k_work bond_reset_work; // 전원 버튼 15초 롱프레스 본드 삭제 work
static uint16_t cnt_s; // power button hold counter
static bool device_on; // power latch state
static bool boot_btn_released; // 부팅 후 버튼을 놓았는지 여부
static bool power_btn_suspended; // 측정 중 버튼 상태머신 일시 정지
static const char *power_off_pending_reason = "none"; // 예약된 전원 OFF 사유
static bool bond_reset_pending; // 본드 삭제 work 중복 예약 방지
static uint8_t p_order; // 전원 시퀀스 단계
static bool lock_check = false; // power sequence lock flag
/* 시스템 워크에서 BLE advertising 시작 */
static void adv_start_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
ble_advertising_start();
}
/* 시스템 워크에서 BLE advertising 중지 */
static void adv_stop_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
ble_advertising_stop();
}
/* 전원 버튼 15초 롱프레스: BLE 본드 삭제 후 리부팅 */
static void bond_reset_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
DBG_ERR("[BTN] 15s long press -> BLE bond reset\r\n");
battery_timer_stop();
ble_advertising_stop();
#if IS_ENABLED(CONFIG_BT_SMP)
int err = bt_unpair(BT_ID_DEFAULT, NULL);
if (err)
{
DBG_ERR("[BTN] bt_unpair failed err=%d\r\n", err);
}
else
{
bond_data_delete = true;
DBG_PRINTF("[BTN] BLE bond data deleted\r\n");
}
#else
bond_data_delete = true;
DBG_PRINTF("[BTN] BLE bond delete skipped (BT_SMP disabled)\r\n");
#endif
led_set_state(LED_STATE_BOND_DELETE);
k_msleep(1500);
sys_reboot(SYS_REBOOT_COLD);
}
/* 부팅 직후 전원 래치를 유지할지 결정 */
void power_control_latch_init(bool keep_on)
{
gpio_pin_configure_dt(&power_hold, keep_on ? GPIO_OUTPUT_ACTIVE : GPIO_OUTPUT_INACTIVE);
}
/* 전원 버튼 상태머신 초기값 */
void power_control_reset_state(void)
{
cnt_s = 0;
device_on = false;
boot_btn_released = false;
power_btn_suspended = false;
bond_reset_pending = false;
power_off_pending_reason = "none";
}
/* POWER_HOLD 핀으로 실제 전원 래치 ON/OFF 제어 */
static void power_control_handler(on_off_cont_t device_power_st, const char *reason)
{
ARG_UNUSED(reason);
if (device_power_st == OFF)
{
gpio_pin_set_dt(&power_hold, 0);
}
else
{
gpio_pin_set_dt(&power_hold, 1);
}
}
/* 전원 버튼 GPIO 초기화 */
static void power_gpio_init(void)
{
gpio_pin_configure_dt(&power_btn, GPIO_INPUT);
DBG_PRINTF("[INIT] HW - GPIO OK (POWER BTN=%d)\r\n", gpio_pin_get_dt(&power_btn));
}
/* 전원 OFF 타임아웃 콜백 */
static void t_power_off_timeout_handler(struct k_timer *timer)
{
ARG_UNUSED(timer);
DBG_ERR("[PWR] OFF timeout reason=%s\r\n", power_off_pending_reason);
led_set_state(LED_STATE_OFF);
power_control_handler(OFF, "off-timeout");
}
/* sleep/전원 OFF: LED 표시 후 지연 시간 뒤 전원 차단 */
static void power_off_schedule(const char *reason, led_state_t led_state)
{
power_off_pending_reason = reason ? reason : "unknown";
led_set_state(led_state);
k_timer_start(&m_power_off_delay_timer, K_MSEC(POWER_OFF_DELAY), K_NO_WAIT);
}
/* 사유를 포함한 sleep 모드 진입 요청 */
void sleep_mode_enter_reason(const char *reason)
{
power_off_schedule(reason, LED_STATE_POWER_ON);
}
/* 기본 sleep 모드 진입 요청 */
void sleep_mode_enter(void)
{
sleep_mode_enter_reason("sleep_mode_enter");
}
/* 기본 전원 OFF 요청 */
void device_power_off(void)
{
device_power_off_reason("device_power_off");
}
/* 사유를 포함한 전원 OFF 요청 */
void device_power_off_reason(const char *reason)
{
power_off_schedule(reason, LED_STATE_POWER_OFF);
}
/* 외부 명령 처리 후 전원 래치 유지 */
void device_power_keep_on(void)
{
k_timer_stop(&m_power_off_delay_timer);
device_on = true;
boot_btn_released = (gpio_pin_get_dt(&power_btn) != 1);
cnt_s = 0;
power_control_handler(ON, "keep-on");
}
/* 긴 측정 중 전원 버튼 상태머신 일시 정지 */
void power_button_suspend(bool suspend)
{
power_btn_suspended = suspend;
cnt_s = 0;
}
/* 전원 버튼 상태머신 */
static void main_s(struct k_timer *timer)
{
ARG_UNUSED(timer);
// 버튼 입력은 눌림 상태가 1
bool button_pressed = (gpio_pin_get_dt(&power_btn) == 1);
// 측정 중에는 버튼 판단을 멈추고 다음 주기에 다시 확인
if (power_btn_suspended)
{
k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT);
return;
}
// 전원 OFF 상태: 버튼을 충분히 누르면 전원 래치
if (!device_on)
{
if (!button_pressed) // 2초 미만으로 놓으면 전원 유지 실패 처리
{
power_control_handler(OFF, "boot-button-released-before-latch");
cnt_s = 0;
}
else
{
cnt_s++;
if (cnt_s == POWER_ON_THRESHOLD) // 임계 시간 도달 시 전원 ON 확정
{
device_on = true;
power_control_handler(ON, "button-2s-latch");
led_set_state(LED_STATE_ADVERTISING);
k_work_submit(&adv_start_work);
battery_timer_start();
m_reset_status = 1;
}
}
}
else
{
// 전원 ON 상태: 부팅 때 누른 버튼을 먼저 놓아야 새 입력으로 봄
if (!boot_btn_released)
{
if (!button_pressed) // 부팅 때 누른 버튼을 놓으면 이후 새 입력으로 판단
{
boot_btn_released = true;
cnt_s = 0;
}
else
{
cnt_s++;
if ((cnt_s >= BOND_RESET_THRESHOLD) && !bond_reset_pending) // 부팅 버튼 15초 유지 시 본드 삭제
{
bond_reset_pending = true;
k_work_submit(&bond_reset_work);
return;
}
}
}
else if (button_pressed) // ON 상태에서 새로 길게 누르면 전원 OFF
{
cnt_s++;
if (cnt_s >= POWER_OFF_THRESHOLD)
{
battery_timer_stop();
k_work_submit(&adv_stop_work);
device_on = false;
boot_btn_released = false;
cnt_s = 0;
sleep_mode_enter_reason("button-long-press");
return;
}
}
else
{
// 버튼을 놓으면 짧은 입력은 무시하고 카운터를 초기화한다.
if (cnt_s > 0)
{
DBG_PRINTF("[BTN] Short press (%d) -> ignored\r\n", cnt_s);
}
cnt_s = 0;
}
}
k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT);
}
/* 전원 시퀀스 타이머 만료 콜백 */
static void power_loop_expiry(struct k_timer *timer)
{
power_loop(timer);
}
/* 슬립 진입 전 처리 중 플래그 정리 */
int device_sleep_mode(void)
{
k_msleep(2);
DBG_PRINTF("Device_Sleep_Mode OK!\r\n");
k_msleep(10);
processing = false;
return 0;
}
/* 최초 전원 활성화 시퀀스 시작 */
int device_activated(void)
{
p_order = 0;
lock_check = true;
power_timer_start();
return 0;
}
/* 전원 활성화 단계 처리 - 필요 시 다음 타이머 예약 */
void power_loop(struct k_timer *timer)
{
power_timer_stop();
// 센서 초기화는 각 측정 함수에서 필요 시 수행
p_order = 2;
if (p_order < 2)
{
p_order++;
power_timer_start();
}
else
{
DBG_PRINTF("[PWR] Device Activated OK!\r\n");
}
}
/* 소프트 리셋/복귀 후 전원 시퀀스 다시 시작 */
int device_reactivated(void)
{
// 필요 시 복귀 전용 초기화 추가
k_msleep(10);
lock_check = true;
p_order = 0;
power_timer_start();
return 0;
}
/* 전원 시퀀스 타이머 1회 예약 */
void power_timer_start(void)
{
k_timer_start(&m_power_timer, K_MSEC(POWER_LOOP_INTERVAL), K_NO_WAIT);
}
/* 예약된 전원 시퀀스 타이머 중지 */
void power_timer_stop(void)
{
k_timer_stop(&m_power_timer);
}
/* 전원 시퀀스 타이머 콜백 등록 */
void power_timer_init(void)
{
k_timer_init(&m_power_timer, power_loop_expiry, NULL);
}
/* 전원 관련 타이머/work 초기화 */
void power_control_init(void)
{
power_gpio_init();
k_timer_init(&m_power_on_delay_timer, main_s, NULL);
k_timer_init(&m_power_off_delay_timer, t_power_off_timeout_handler, NULL);
k_work_init(&adv_start_work, adv_start_work_handler);
k_work_init(&adv_stop_work, adv_stop_work_handler);
k_work_init(&bond_reset_work, bond_reset_work_handler);
power_timer_init();
}
/* 전원 버튼 상태머신 시작 */
void power_control_start(void)
{
k_timer_start(&m_power_on_delay_timer, K_MSEC(POWER_ON_DELAY), K_NO_WAIT);
}
/* soft reset/DFU reset/test image 부팅 시 버튼 없이 정상 ON 상태로 복구 */
void power_control_resume_after_reset(uint32_t reset_reason)
{
device_on = true;
cnt_s = 0;
boot_btn_released = (gpio_pin_get_dt(&power_btn) != 1);
m_reset_status = 1;
power_control_handler(ON, "resume-after-reset");
led_set_state(LED_STATE_ADVERTISING);
battery_timer_start();
k_work_submit(&adv_start_work);
DBG_CORE("[BOOT] Resume after resetreas=0x%08x\r\n", reset_reason);
}
+83
View File
@@ -0,0 +1,83 @@
/*******************************************************************************
* @file power_control.c
* @brief Device power sequence control
*
* Power-up sequence state machine with k_timer (single-shot 20ms intervals)
******************************************************************************/
#include <zephyr/kernel.h>
#include "main.h"
#include "power_control.h"
#include "debug_print.h"
#define POWER_LOOP_INTERVAL 20 /* ms */
static struct k_timer m_power_timer;
static uint8_t p_order;
static bool lock_check = false;
extern volatile bool processing;
static void power_loop_expiry(struct k_timer *timer)
{
power_loop(timer);
}
int device_sleep_mode(void)
{
k_msleep(2);
DBG_PRINTF("Device_Sleep_Mode OK!\r\n");
k_msleep(10);
processing = false;
return 0;
}
int device_activated(void)
{
p_order = 0;
lock_check = true;
power_timer_start();
return 0;
}
void power_loop(struct k_timer *timer)
{
power_timer_stop();
// Sensor init not needed - imu_read_direct() handles it per measurement
p_order = 2;
if (p_order < 2)
{
p_order++;
power_timer_start();
}
else
{
DBG_PRINTF("[PWR] Device Activated OK!\r\n");
}
}
int device_reactivated(void)
{
// sw_i2c_init_once() will be added in Stage 3
k_msleep(10);
lock_check = true;
p_order = 0;
power_timer_start();
return 0;
}
void power_timer_start(void)
{
k_timer_start(&m_power_timer, K_MSEC(POWER_LOOP_INTERVAL), K_NO_WAIT);
}
void power_timer_stop(void)
{
k_timer_stop(&m_power_timer);
}
void power_timer_init(void)
{
k_timer_init(&m_power_timer, power_loop_expiry, NULL);
}
@@ -5,17 +5,8 @@
#ifndef POWER_CONTROL_H__
#define POWER_CONTROL_H__
#include <stdint.h>
#include <stdbool.h>
#include <zephyr/kernel.h>
#include "main.h"
void power_control_latch_init(bool keep_on);
void power_control_reset_state(void);
void power_control_init(void);
void power_control_start(void);
void power_control_resume_after_reset(uint32_t reset_reason);
int device_sleep_mode(void);
int device_activated(void);
int device_reactivated(void);
-29
View File
@@ -75,32 +75,3 @@ if(EXISTS "${APP_PRJ_CONF}")
)
endif()
endif()
# Production HEX file name based on src/main.h FIRMWARE_VERSION.
set(APP_MAIN_H "${APP_DIR}/src/main.h")
if(EXISTS "${APP_MAIN_H}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${APP_MAIN_H}")
file(READ "${APP_MAIN_H}" APP_MAIN_H_TEXT)
if(APP_MAIN_H_TEXT MATCHES "#define[ \t]+FIRMWARE_VERSION[ \t]+\"([^\"]+)\"")
set(FW_HEX_VERSION "${CMAKE_MATCH_1}")
string(REGEX REPLACE "[^A-Za-z0-9_.+-]" "_" FW_HEX_VERSION "${FW_HEX_VERSION}")
set(FW_HEX_SOURCE "${CMAKE_BINARY_DIR}/merged.hex")
set(FW_HEX_RELEASE_DIR "${APP_DIR}/release")
set(FW_HEX_VERSIONED "${FW_HEX_RELEASE_DIR}/VesiScan-Basic_${FW_HEX_VERSION}.hex")
add_custom_command(
OUTPUT "${FW_HEX_VERSIONED}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${FW_HEX_RELEASE_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${FW_HEX_SOURCE}" "${FW_HEX_VERSIONED}"
DEPENDS "${FW_HEX_SOURCE}"
COMMENT "Generating firmware-versioned HEX ${FW_HEX_VERSIONED}"
VERBATIM
)
add_custom_target(firmware_versioned_hex ALL
DEPENDS "${FW_HEX_VERSIONED}"
)
endif()
endif()
+1 -3
View File
@@ -1,6 +1,4 @@
# nRF52840 build: no network core image.
# Re-enable NETCORE_HCI_IPC only for nRF5340 sysbuild configurations.
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APPLICATION_CONFIG_DIR}/keys/mcuboot_private.pem"
SB_CONFIG_BOOTLOADER_MCUBOOT=y
+1 -1
View File
@@ -1,3 +1,3 @@
CONFIG_BT_CTLR_TX_PWR_PLUS_8=y
CONFIG_BT_CTLR_TX_PWR_PLUS_4=y
CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
-19
View File
@@ -1,20 +1 @@
CONFIG_GPIO_HOGS=y
# Keep MCUboot LOG enabled to satisfy MCUboot default Kconfig choices,
# but disable console/log output backends.
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=0
CONFIG_MCUBOOT_LOG_LEVEL_OFF=y
CONFIG_USE_SEGGER_RTT=y
CONFIG_CONSOLE=n
CONFIG_RTT_CONSOLE=n
CONFIG_LOG_BACKEND_RTT=n
CONFIG_LOG_BACKEND_SHOW_COLOR=n
CONFIG_LOG_BACKEND_UART=n
CONFIG_UART_CONSOLE=n
# security boot
CONFIG_REBOOT=y
CONFIG_DISABLE_FLASH_PATCH=y
-61
View File
@@ -1,61 +0,0 @@
@echo off
setlocal enabledelayedexpansion
REM ==============================================================
REM Build + Flash + APPROTECT lock (PRODUCTION ONLY)
REM - Rebuilds so build\merged.hex is up to date, then flashes.
REM - Do NOT run on development boards (locking needs --recover).
REM ==============================================================
set "REPO=%~dp0.."
set "BUILDDIR=%REPO%\build"
set "HEX=%BUILDDIR%\merged.hex"
REM cmake from the nRF Connect toolchain, fallback to PATH
set "CMAKE=C:\ncs\toolchains\fd21892d0f\opt\bin\cmake.exe"
if not exist "%CMAKE%" set "CMAKE=cmake"
echo [FLASH] FULL ERASE + APPROTECT LOCK mode (production)
echo [FLASH] This wipes application flash, NVS/settings, and BLE bonds.
echo [FLASH] After locking, SWD debug read/write is disabled.
echo [FLASH] To unlock later: nrfjprog --recover (mass-erases the chip).
echo [FLASH] DFU(SMP) over BLE still works after locking.
echo.
REM --- Step 1: build (updates merged.hex) ---
echo [FLASH] Step 1/4: build (update merged.hex)
"%CMAKE%" --build "%BUILDDIR%"
if errorlevel 1 (
echo [FLASH] Build FAILED. Aborting.
exit /b 1
)
if not exist "%HEX%" (
echo [FLASH] merged.hex not found: %HEX%
echo [FLASH] Build the project first.
exit /b 1
)
echo [FLASH] Using: %HEX%
echo.
REM --- Step 2: chip erase + program + verify ---
echo [FLASH] Step 2/4: chip erase + program + verify
nrfjprog --program "%HEX%" --chiperase --verify --reset
if errorlevel 1 (
echo [FLASH] Program FAILED. Aborting before lock.
exit /b 1
)
REM --- Step 3: enable APPROTECT ---
echo [FLASH] Step 3/4: enabling APPROTECT (readback protection)
nrfjprog --rbp ALL
REM --- Step 4: pin reset to apply protection ---
echo [FLASH] Step 4/4: pin reset
nrfjprog --pinreset
echo.
echo [FLASH] Done. POWER-CYCLE the device to fully latch APPROTECT.
echo [FLASH] Verify lock: nrfjprog --memrd 0x0 should now FAIL.
endlocal
pause
-35
View File
@@ -1,35 +0,0 @@
param(
[string]$Hex = "build\merged.hex"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path -Parent $PSScriptRoot
$HexPath = Join-Path $RepoRoot $Hex
if (-not (Test-Path -LiteralPath $HexPath)) {
throw "Hex file not found: $HexPath. Build the project first."
}
Write-Host "[FLASH] FULL ERASE + APPROTECT LOCK mode (production)"
Write-Host "[FLASH] This wipes application flash, NVS/settings, and BLE bonds."
Write-Host "[FLASH] After locking, SWD debug read/write is disabled permanently."
Write-Host "[FLASH] To unlock later you MUST run 'nrfjprog --recover', which mass-erases the chip."
Write-Host "[FLASH] DFU(SMP) over BLE still works after locking."
Write-Host "[FLASH] Programming: $HexPath"
# 1) 전체 지우고 서명된 펌웨어 프로그램
Write-Host "[FLASH] Step 1/3: chip erase + program + verify"
nrfjprog --program $HexPath --chiperase --verify --reset
# 2) UICR.APPROTECT 잠금 (SWD 디버그 포트 차단)
Write-Host "[FLASH] Step 2/3: enabling APPROTECT (readback protection)"
nrfjprog --rbp ALL
# 3) APPROTECT는 파워사이클/핀리셋 후 실제 적용됨
Write-Host "[FLASH] Step 3/3: pin reset to apply protection"
nrfjprog --pinreset
Write-Host "[FLASH] Done. POWER-CYCLE the device to fully latch APPROTECT."
Write-Host "[FLASH] Verify lock: 'nrfjprog --memrd 0x0' should now FAIL."