Skip to content

Commit 19192e4

Browse files
committed
applications: gesture_recognition: track lost samples
Prepend ID to samples send of NUS to keep track of potentially lost samples. Add scripts/check_nus_data.py to verify that received samples are consecutive and trim the added IDs. Jira: NCSDK-37426 Signed-off-by: Szymon Antkowiak <szymon.antkowiak@nordicsemi.no>
1 parent 2c62cd7 commit 19192e4

3 files changed

Lines changed: 121 additions & 3 deletions

File tree

applications/gesture_recognition/README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ You can find raw datasets used for model training on the `training dataset`_ pag
270270
271271
In this mode, you must have an additional development kit running the `Nordic central UART sample`_ to receive the NUS data.
272272

273+
.. note::
274+
When using Bluetooth LE NUS mode, some samples may be lost due to RF noise or increased distance between the device and the central.
275+
To help track lost samples, the application prepends a sequential ID to each sample transmitted over NUS.
276+
Use the :file:`scripts/check_nus_data.py` script to check for dropped samples and strip the ID from the received data.
277+
273278
Building and running
274279
********************
275280

applications/gesture_recognition/src/ble/nus/ble_nus.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,11 @@ int ble_nus_send(const int16_t *input_data)
145145
return -ENOTCONN;
146146
}
147147

148-
len = snprintf(buffer, sizeof(buffer), "%d,%d,%d,%d,%d,%d\r\n",
149-
input_data[0], input_data[1], input_data[2],
150-
input_data[3], input_data[4], input_data[5]);
148+
static uint32_t id;
149+
150+
id++;
151+
len = snprintf(buffer, sizeof(buffer), "%u %d,%d,%d,%d,%d,%d\r\n", id, input_data[0],
152+
input_data[1], input_data[2], input_data[3], input_data[4], input_data[5]);
151153
if ((len <= 0) || (len >= (int)sizeof(buffer))) {
152154
return -EINVAL;
153155
}

scripts/check_nus_data.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2026 Nordic Semiconductor ASA
4+
#
5+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
6+
#
7+
8+
import argparse
9+
from pathlib import Path
10+
11+
12+
def check_consecutive_ids(log_path: Path) -> tuple[list[str], int]:
13+
"""Return (issues, parsed_count) after validating consecutive IDs."""
14+
issues: list[str] = []
15+
parsed_count = 0
16+
prev_id: int | None = None
17+
18+
with log_path.open("r", encoding="utf-8") as f:
19+
for line_no, raw_line in enumerate(f, start=1):
20+
line = raw_line.strip()
21+
22+
if not line:
23+
continue
24+
25+
first_token = line.split(maxsplit=1)[0]
26+
try:
27+
curr_id = int(first_token)
28+
except ValueError:
29+
issues.append(
30+
f"Line {line_no}: could not parse entry ID from first token '{first_token}'"
31+
)
32+
continue
33+
34+
parsed_count += 1
35+
36+
if prev_id is not None:
37+
expected = prev_id + 1
38+
if curr_id != expected:
39+
issues.append(f"Line {line_no}: expected ID {expected}, got {curr_id}")
40+
41+
prev_id = curr_id
42+
43+
return issues, parsed_count
44+
45+
46+
def create_raw_log(log_path: Path) -> tuple[Path, int]:
47+
"""Create a copy with leading ID removed from each parsable line."""
48+
raw_path = log_path.with_name(f"{log_path.stem}_data{log_path.suffix}")
49+
converted_count = 0
50+
51+
with log_path.open("r", encoding="utf-8") as src, raw_path.open("w", encoding="utf-8") as dst:
52+
for raw_line in src:
53+
line = raw_line.rstrip("\n")
54+
parts = line.split(maxsplit=1)
55+
56+
if len(parts) == 2:
57+
try:
58+
int(parts[0])
59+
except ValueError:
60+
dst.write(raw_line)
61+
else:
62+
dst.write(parts[1] + "\n")
63+
converted_count += 1
64+
else:
65+
dst.write(raw_line)
66+
67+
return raw_path, converted_count
68+
69+
70+
def main() -> int:
71+
parser = argparse.ArgumentParser(
72+
description="""
73+
Check NUS serial output for consecutive entry IDs.
74+
Expected format is "<id> <data>".
75+
If no problems were detected, than creates a '_data' file with IDs removed.
76+
""",
77+
allow_abbrev=False,
78+
)
79+
parser.add_argument(
80+
"log_file",
81+
help="Path to serial log file",
82+
)
83+
args = parser.parse_args()
84+
85+
log_path = Path(args.log_file)
86+
if not log_path.is_file():
87+
print(f"Error: file not found: {log_path}")
88+
return 2
89+
90+
issues, parsed_count = check_consecutive_ids(log_path)
91+
92+
if parsed_count == 0:
93+
print("No valid log entries found.")
94+
return 1
95+
96+
if issues:
97+
print(f"Found {len(issues)} issue(s) in {parsed_count} parsed entries:")
98+
for issue in issues:
99+
print(f"- {issue}")
100+
return 1
101+
102+
print(f"OK: {parsed_count} entries have consecutive IDs.")
103+
104+
raw_path, converted_count = create_raw_log(log_path)
105+
print(f"Created raw log: {raw_path} ({converted_count} entries converted)")
106+
107+
return 0
108+
109+
110+
if __name__ == "__main__":
111+
raise SystemExit(main())

0 commit comments

Comments
 (0)