diff --git a/.codechecker.json b/.codechecker.json
new file mode 100644
index 00000000..4fc6da7e
--- /dev/null
+++ b/.codechecker.json
@@ -0,0 +1,14 @@
+{
+ "analyze": [
+ "--analyzers",
+ "clangsa",
+ "cppcheck",
+ "--disable",
+ "severity:STYLE",
+ "--file",
+ "*/open-earable-v2/*",
+ "--capture-analysis-output",
+ "--report-hash",
+ "context-free-v2"
+ ]
+}
diff --git a/.github/scripts/render_unit_test_comment.py b/.github/scripts/render_unit_test_comment.py
new file mode 100644
index 00000000..cb2ad8a7
--- /dev/null
+++ b/.github/scripts/render_unit_test_comment.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+"""Render Twister results as a concise GitHub pull-request comment for CI."""
+
+from __future__ import annotations
+
+import argparse
+import html
+import json
+import re
+from pathlib import Path
+
+MARKER = ""
+UNITY_RESULT = re.compile(r"^.*:\d+:test_.+:(?:PASS|FAIL)(?::.*)?$")
+IMPORTANT_OUTPUT = re.compile(
+ r"(?:\b(?:ERROR|FAIL|FAILED)\b|PROJECT EXECUTION|Tests? \d+ Failures)",
+ re.IGNORECASE,
+)
+
+
+def load_suites(report_path: Path) -> list[dict]:
+ if not report_path.is_file():
+ return []
+
+ try:
+ report = json.loads(report_path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ return []
+ return report.get("testsuites", [])
+
+
+def status_icon(status: str) -> str:
+ return {
+ "passed": "✅",
+ "failed": "❌",
+ "error": "🚨",
+ "skipped": "⏭️",
+ "filtered": "⏭️",
+ "blocked": "🚫",
+ }.get(status.lower(), "❔")
+
+
+def failure_details(console_path: Path, suites: list[dict]) -> str:
+ console = ""
+ if console_path.is_file():
+ console = console_path.read_text(encoding="utf-8", errors="replace")
+
+ selected = []
+ for line in console.splitlines():
+ stripped = line.strip()
+ if UNITY_RESULT.match(stripped) or IMPORTANT_OUTPUT.search(stripped):
+ selected.append(stripped)
+
+ if not selected:
+ for suite in suites:
+ if suite.get("status", "").lower() not in {"passed", "skipped", "filtered"}:
+ reason = suite.get("reason") or "No detailed failure message was reported."
+ selected.append(f"{suite.get('name', 'unknown suite')}: {reason}")
+
+ if not selected and console:
+ selected = [line for line in console.splitlines() if line.strip()][-40:]
+
+ # Keep comments readable and below GitHub's comment-size limit.
+ return "\n".join(selected[-100:])[-12000:]
+
+
+def render(args: argparse.Namespace) -> str:
+ suites = load_suites(args.report)
+ statuses = [str(suite.get("status", "unknown")).lower() for suite in suites]
+ failed = sum(status not in {"passed", "skipped", "filtered"} for status in statuses)
+ passed = statuses.count("passed")
+ skipped = sum(status in {"skipped", "filtered"} for status in statuses)
+ succeeded = args.outcome == "success" and failed == 0
+
+ heading = "✅ Unit tests passed" if succeeded else "❌ Unit tests failed"
+ lines = [MARKER, f"## {heading}", ""]
+ lines.append(
+ f"**{passed} passed**, **{failed} failed/error**, **{skipped} skipped** "
+ f"— [view workflow run]({args.run_url})"
+ )
+
+ if suites:
+ lines.extend(["", "| Test scenario | Platform | Result |", "|---|---|---|"])
+ for suite in suites:
+ status = str(suite.get("status", "unknown"))
+ lines.append(
+ f"| `{suite.get('name', 'unknown')}` "
+ f"| `{suite.get('platform', 'unknown')}` "
+ f"| {status_icon(status)} {status} |"
+ )
+ elif not succeeded:
+ lines.extend(["", "Twister did not produce a JSON report; this may be an infrastructure failure."])
+
+ if not succeeded:
+ details = failure_details(args.console, suites)
+ if details:
+ lines.extend(
+ [
+ "",
+ "",
+ "Failure details
",
+ "",
+ f"{html.escape(details)}",
+ " ",
+ ]
+ )
+
+ lines.extend(
+ [
+ "",
+ f"[Download the `unit-test-results` artifact]({args.artifact_url}) "
+ "for full Twister reports and logs.",
+ ]
+ )
+ return "\n".join(lines) + "\n"
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--report", type=Path, required=True)
+ parser.add_argument("--console", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--outcome", choices=("success", "failure", "cancelled", "skipped"), required=True)
+ parser.add_argument("--run-url", required=True)
+ parser.add_argument("--artifact-url", required=True)
+ return parser.parse_args()
+
+
+def main() -> None:
+ args = parse_args()
+ args.output.write_text(render(args), encoding="utf-8")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/workflows/build_firmware.yaml b/.github/workflows/build_firmware.yaml
index c2802f06..8a70d08f 100644
--- a/.github/workflows/build_firmware.yaml
+++ b/.github/workflows/build_firmware.yaml
@@ -28,6 +28,7 @@ jobs:
# Clone the repo to a subdirectory, so we can initialize the Zephyr
# workspace in the parent directory.
path: zephyr-workspace/open-earable-v2
+ submodules: recursive
- name: Cache/Install APT Packages
uses: awalsh128/cache-apt-pkgs-action@latest
diff --git a/.github/workflows/codechecker.yaml b/.github/workflows/codechecker.yaml
new file mode 100644
index 00000000..3f605c08
--- /dev/null
+++ b/.github/workflows/codechecker.yaml
@@ -0,0 +1,189 @@
+name: CodeChecker static analysis
+
+on:
+ pull_request: {}
+ workflow_dispatch: {}
+
+permissions:
+ contents: read
+ pull-requests: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+env:
+ CODECHECKER_VERSION: "6.28.2"
+ ZEPHYR_SDK_VERSION: "0.17.0"
+
+jobs:
+ codechecker:
+ name: Advisory analysis
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out application
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ path: zephyr-workspace/open-earable-v2
+ submodules: recursive
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.12"
+
+ - name: Install system dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes \
+ ccache \
+ clang \
+ cppcheck \
+ device-tree-compiler \
+ g++-multilib \
+ gcc-multilib \
+ gperf \
+ libmagic1 \
+ make \
+ ninja-build \
+ wget \
+ xz-utils
+
+ - name: Install Python tools
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install "codechecker==${CODECHECKER_VERSION}" west
+
+ - name: Install Zephyr SDK
+ working-directory: zephyr-workspace
+ run: |
+ wget --quiet \
+ "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${ZEPHYR_SDK_VERSION}/zephyr-sdk-${ZEPHYR_SDK_VERSION}_linux-x86_64_minimal.tar.xz"
+ tar xf "zephyr-sdk-${ZEPHYR_SDK_VERSION}_linux-x86_64_minimal.tar.xz"
+ "${PWD}/zephyr-sdk-${ZEPHYR_SDK_VERSION}/setup.sh" -c -t arm-zephyr-eabi
+
+ - name: Initialize Zephyr workspace
+ working-directory: zephyr-workspace
+ run: |
+ west init -l open-earable-v2
+ west update --narrow -o=--depth=1
+ python -m pip install -r zephyr/scripts/requirements.txt
+
+ - name: Register custom board
+ working-directory: zephyr-workspace
+ run: |
+ mkdir -p zephyr/boards/arm
+ cp -R open-earable-v2/boards/teco/. zephyr/boards/arm/
+
+ - name: Run CodeChecker
+ id: analysis
+ working-directory: zephyr-workspace
+ shell: bash
+ run: |
+ mkdir -p codechecker-output
+ set -o pipefail
+ set +e
+ west build \
+ --no-sysbuild \
+ --board openearable_v2/nrf5340/cpuapp \
+ --pristine=always \
+ open-earable-v2 \
+ -- \
+ -DFILE_SUFFIX=fota \
+ -DZEPHYR_SCA_VARIANT=codechecker \
+ -DCODECHECKER_ANALYZE_JOBS=4 \
+ -DCODECHECKER_CONFIG_FILE="${PWD}/open-earable-v2/.codechecker.json" \
+ -DCODECHECKER_EXPORT=html,json \
+ 2>&1 | tee codechecker-output/analysis.log
+ analysis_exit=${PIPESTATUS[0]}
+ echo "exit_code=${analysis_exit}" >> "${GITHUB_OUTPUT}"
+ echo "CodeChecker/build exit code: ${analysis_exit}" >> codechecker-output/analysis.log
+ if [ "${analysis_exit}" -ne 0 ]; then
+ echo "::error::CodeChecker build failed; see codechecker-output/analysis.log"
+ exit "${analysis_exit}"
+ fi
+ if [ ! -s build/sca/codechecker/codechecker.json ]; then
+ echo "::error::CodeChecker did not produce build/sca/codechecker/codechecker.json"
+ exit 1
+ fi
+ if ! find build/sca/codechecker/codechecker.plist -maxdepth 1 -type f \
+ -name '*.plist' -print -quit | grep -q .; then
+ echo "::error::CodeChecker completed without analyzing any compilation commands"
+ exit 1
+ fi
+
+ - name: Prepare PR annotations
+ if: always()
+ working-directory: zephyr-workspace/open-earable-v2
+ env:
+ CODECHECKER_REPORT: ../build/sca/codechecker/codechecker.json
+ COMMENT_OUTPUT: ../codechecker-output/pr-comment.md
+ GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: |
+ python scripts/ci/codechecker_pr_report.py \
+ --report "${CODECHECKER_REPORT}" \
+ --analysis-dir ../build/sca/codechecker/codechecker.plist \
+ --comment-output "${COMMENT_OUTPUT}" \
+ --base "${PR_BASE_SHA}" \
+ --head "${PR_HEAD_SHA}"
+
+ - name: Collect complete report
+ if: always()
+ working-directory: zephyr-workspace
+ run: |
+ mkdir -p codechecker-output
+ if [ -d build/sca/codechecker ]; then
+ cp -R build/sca/codechecker codechecker-output/report
+ fi
+ if [ -f build/compile_commands.json ]; then
+ cp build/compile_commands.json codechecker-output/compile_commands.json
+ fi
+ {
+ echo "CodeChecker ${CODECHECKER_VERSION}"
+ echo "Analysis exit code: ${{ steps.analysis.outputs.exit_code }}"
+ echo "Commit: ${{ github.sha }}"
+ } > codechecker-output/run-information.txt
+
+ - name: Upload complete CodeChecker report
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: codechecker-report-${{ github.run_number }}
+ path: zephyr-workspace/codechecker-output
+ if-no-files-found: error
+ retention-days: 30
+
+ - name: Publish advisory PR comment
+ if: always() && github.event_name == 'pull_request'
+ continue-on-error: true
+ uses: actions/github-script@v8
+ env:
+ COMMENT_FILE: zephyr-workspace/codechecker-output/pr-comment.md
+ with:
+ script: |
+ const fs = require('fs');
+ const marker = '';
+ const body = fs.readFileSync(process.env.COMMENT_FILE, 'utf8');
+ const { owner, repo } = context.repo;
+ const issue_number = context.issue.number;
+ const comments = await github.paginate(
+ github.rest.issues.listComments,
+ { owner, repo, issue_number, per_page: 100 }
+ );
+ const previous = comments.find(
+ comment => comment.user.type === 'Bot' && comment.body.includes(marker)
+ );
+ if (previous) {
+ await github.rest.issues.updateComment({
+ owner,
+ repo,
+ comment_id: previous.id,
+ body
+ });
+ } else {
+ await github.rest.issues.createComment({ owner, repo, issue_number, body });
+ }
diff --git a/.github/workflows/compiler_warnings.yaml b/.github/workflows/compiler_warnings.yaml
new file mode 100644
index 00000000..7e605d59
--- /dev/null
+++ b/.github/workflows/compiler_warnings.yaml
@@ -0,0 +1,143 @@
+name: Report Compiler Warnings
+
+on:
+ pull_request: {}
+
+permissions:
+ contents: read
+ pull-requests: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ warnings:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ path: zephyr-workspace/open-earable-v2
+ submodules: recursive
+
+ - name: Install build dependencies
+ uses: awalsh128/cache-apt-pkgs-action@latest
+ with:
+ packages: ccache cmake device-tree-compiler dfu-util file gcc gcc-multilib g++-multilib gperf libmagic1 libsdl2-dev make ninja-build python3-dev python3-pip python3-setuptools python3-tk python3-wheel wget xz-utils
+ version: 1.0
+ execute_install_scripts: true
+
+ - name: Set up Zephyr workspace
+ run: |
+ pip3 install west
+ wget -q https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.17.0/zephyr-sdk-0.17.0_linux-x86_64_minimal.tar.xz
+ tar xf zephyr-sdk-0.17.0_linux-x86_64_minimal.tar.xz -C ~/
+ ~/zephyr-sdk-0.17.0/setup.sh -c -t arm-zephyr-eabi
+ cd zephyr-workspace
+ west init -l open-earable-v2
+ west update --narrow -o=--depth=1
+ pip3 install -r zephyr/scripts/requirements.txt
+
+ - name: Register custom board
+ run: |
+ mkdir -p zephyr-workspace/zephyr/boards/arm
+ cp -R zephyr-workspace/open-earable-v2/boards/teco/. zephyr-workspace/zephyr/boards/arm/
+
+ - name: Build with extended warnings
+ id: build
+ working-directory: zephyr-workspace
+ run: |
+ set -o pipefail
+ west build -d build-warnings --board openearable_v2/nrf5340/cpuapp \
+ --pristine=always open-earable-v2 -- \
+ -DFILE_SUFFIX=fota \
+ -DCMAKE_C_COMPILER_LAUNCHER="$PWD/open-earable-v2/tools/ci/extended-warning-compiler-launcher.sh" \
+ -DCMAKE_CXX_COMPILER_LAUNCHER="$PWD/open-earable-v2/tools/ci/extended-warning-compiler-launcher.sh" \
+ 2>&1 | tee warning-build.log
+
+ - name: Prepare warning report
+ if: always()
+ run: |
+ mkdir -p warning-report
+ log=zephyr-workspace/warning-build.log
+ clean="${RUNNER_TEMP:-/tmp}/warning-build.log"
+ all=warning-report/compiler-warnings.txt
+ project=warning-report/project-warnings.txt
+ config=warning-report/configuration-warnings.txt
+ sed -E 's/\x1B\[[0-9;]*[mK]//g' "$log" 2>/dev/null > "$clean"
+ grep -Ei '(^|[[:space:]])warning:' "$clean" > "$all" || true
+ grep '/zephyr-workspace/open-earable-v2/' "$all" \
+ | sed -E 's#^.*/zephyr-workspace/open-earable-v2/##' \
+ | awk '!seen[$0]++' > "$project" || true
+ awk '
+ function flush() {
+ if (capturing && !seen[warning]++) {
+ print warning "\n"
+ }
+ capturing = 0
+ warning = ""
+ }
+ function is_configuration_warning() {
+ return $0 ~ /^[[:space:]]*(---[[:space:]]*)?[Ww][Aa][Rr][Nn][Ii][Nn][Gg]:/ ||
+ $0 ~ /^CMake (Developer )?Warning([[:space:]]|$)/
+ }
+ function is_devicetree_warning() {
+ return $0 ~ /:[[:space:]]+Warning \([^)]*\):/
+ }
+ {
+ if (is_devicetree_warning()) {
+ flush()
+ if (!seen[$0]++) {
+ print $0 "\n"
+ }
+ next
+ }
+ if (capturing && ($0 ~ /^[[:space:]]*$/ || is_configuration_warning())) {
+ flush()
+ }
+ if (!capturing && is_configuration_warning()) {
+ capturing = 1
+ }
+ if (capturing) {
+ warning = warning (warning == "" ? "" : "\n") $0
+ }
+ }
+ END { flush() }
+ ' "$clean" > "$config" || true
+
+ comment=warning-report/pr-comment.md
+ {
+ echo '## Compiler warnings'
+ echo
+ if [ '${{ steps.build.outcome }}' = success ]; then
+ echo 'The extended-warning build completed successfully.'
+ else
+ echo '⚠️ The extended-warning build failed; this report may be incomplete.'
+ fi
+ echo
+ echo '### Application compiler warnings'
+ if [ -s "$project" ]; then
+ echo 'Show warnings
'
+ echo; echo '```text'; head -n 100 "$project"; echo '```'; echo ' '
+ else
+ echo 'None.'
+ fi
+ echo
+ echo "[View this workflow run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)"
+ } | tee "$comment" >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Upload complete warning report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: compiler-warnings
+ path: warning-report
+ if-no-files-found: error
+
+ - name: Comment on pull request
+ if: always()
+ continue-on-error: true
+ uses: thollander/actions-comment-pull-request@v3
+ with:
+ file-path: warning-report/pr-comment.md
+ comment-tag: compiler-warnings
diff --git a/.github/workflows/release_firmware.yaml b/.github/workflows/release_firmware.yaml
index 9f591176..ebe90ad5 100644
--- a/.github/workflows/release_firmware.yaml
+++ b/.github/workflows/release_firmware.yaml
@@ -43,6 +43,24 @@ jobs:
ls openearable_v2_fota
(cd "openearable_v2_fota" && ls && zip -r ../openearable_v2_fota.zip *)
+ - name: Add version to release asset names
+ id: release-assets
+ env:
+ RELEASE_TAG: ${{ inputs.release_tag }}
+ run: |
+ set -euo pipefail
+
+ # v2.2.8 -> 2.2.8
+ version="${RELEASE_TAG#v}"
+ firmware_name="openearable_v2_firmware_${version}.elf"
+ fota_name="openearable_v2_fota_${version}.zip"
+
+ mv openearable_v2_firmware.elf "$firmware_name"
+ mv openearable_v2_fota.zip "$fota_name"
+
+ echo "firmware=$firmware_name" >> "$GITHUB_OUTPUT"
+ echo "fota=$fota_name" >> "$GITHUB_OUTPUT"
+
- name: Create Github Release & Upload Firmware + FOTA
if: startsWith(github.ref, 'refs/tags/${{ inputs.release_tag }}')
uses: softprops/action-gh-release@v2
@@ -52,7 +70,7 @@ jobs:
draft: false
prerelease: false
files: |
- openearable_v2_firmware.elf
- openearable_v2_fota.zip
+ ${{ steps.release-assets.outputs.firmware }}
+ ${{ steps.release-assets.outputs.fota }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/unit_tests.yaml b/.github/workflows/unit_tests.yaml
new file mode 100644
index 00000000..68adca61
--- /dev/null
+++ b/.github/workflows/unit_tests.yaml
@@ -0,0 +1,141 @@
+name: Unit Tests
+
+on:
+ workflow_dispatch:
+ pull_request: {}
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+concurrency:
+ group: unit-tests-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+env:
+ ZEPHYR_TOOLCHAIN_VARIANT: host
+
+jobs:
+ unity-native-sim:
+ name: Unity tests (native_sim 64-bit)
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Check out application
+ uses: actions/checkout@v5
+ with:
+ # Keep the application below the west workspace root.
+ path: zephyr-workspace/open-earable-v2
+
+ - name: Install host dependencies
+ uses: awalsh128/cache-apt-pkgs-action@latest
+ with:
+ packages: git cmake ninja-build gperf device-tree-compiler make gcc g++ ruby python3-dev python3-pip python3-setuptools python3-wheel
+ version: 1.0
+ execute_install_scripts: true
+
+ - name: Cache Python packages
+ uses: actions/cache@v5
+ with:
+ path: ~/.cache/pip
+ key: unit-tests-${{ runner.os }}-pip-${{ hashFiles('zephyr-workspace/open-earable-v2/west.yml') }}
+ restore-keys: |
+ unit-tests-${{ runner.os }}-pip-
+
+ - name: Initialize nRF Connect SDK workspace
+ run: |
+ python3 -m pip install west
+ cd zephyr-workspace
+ west init -l open-earable-v2
+ west update --narrow -o=--depth=1
+ python3 -m pip install -r zephyr/scripts/requirements.txt
+
+ - name: Run Unity tests with Twister
+ id: twister
+ continue-on-error: true
+ run: |
+ set -o pipefail
+ cd zephyr-workspace
+ python3 zephyr/scripts/twister \
+ -T open-earable-v2/tests/unit \
+ -p native_sim/native/64 \
+ --inline-logs \
+ --outdir twister-out \
+ 2>&1 | tee twister-console.log
+
+ - name: Upload Twister reports and logs
+ id: unit_test_artifacts
+ if: always()
+ uses: actions/upload-artifact@v6
+ with:
+ name: unit-test-results
+ path: |
+ zephyr-workspace/twister-console.log
+ zephyr-workspace/twister-out/twister*.xml
+ zephyr-workspace/twister-out/twister.json
+ zephyr-workspace/twister-out/**/handler.log
+ zephyr-workspace/twister-out/**/build.log
+ if-no-files-found: warn
+ retention-days: 14
+
+ - name: Prepare test summary
+ if: always()
+ env:
+ TWISTER_OUTCOME: ${{ steps.twister.outcome }}
+ WORKFLOW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ ARTIFACT_URL: ${{ steps.unit_test_artifacts.outputs.artifact-url }}
+ run: |
+ python3 zephyr-workspace/open-earable-v2/.github/scripts/render_unit_test_comment.py \
+ --report zephyr-workspace/twister-out/twister.json \
+ --console zephyr-workspace/twister-console.log \
+ --output unit-test-comment.md \
+ --outcome "$TWISTER_OUTCOME" \
+ --run-url "$WORKFLOW_RUN_URL" \
+ --artifact-url "$ARTIFACT_URL"
+ cat unit-test-comment.md >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Publish test summary on pull request
+ if: always() && github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ const fs = require('fs');
+ const marker = '';
+ const body = fs.readFileSync('unit-test-comment.md', 'utf8');
+ const comments = await github.paginate(
+ github.rest.issues.listComments,
+ {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ per_page: 100,
+ }
+ );
+ const existing = comments.find(
+ comment => comment.user.type === 'Bot' && comment.body.includes(marker)
+ );
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body,
+ });
+ }
+
+ - name: Fail workflow when tests failed
+ if: steps.twister.outcome == 'failure'
+ run: exit 1
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..5697f77b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "protocol"]
+ path = protocol
+ url = https://github.com/OpenEarable/protocol.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ffa4b8fa..2d73b3c9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -76,6 +76,9 @@ add_subdirectory(src/SD_Card/SDLogger)
add_subdirectory(src/SD_Card/Benchmark)
add_subdirectory(src/SD_Card/SD_Card_Manager)
add_subdirectory(src/time_sync)
+add_subdirectory(protocol/generated/c)
+
+target_link_libraries(open_earable_protocols PRIVATE zephyr_interface)
## Cirrus Logic
if (CONFIG_HW_CODEC_CIRRUS_LOGIC)
@@ -88,3 +91,5 @@ endif()
include(version.cmake)
include_directories(${CMAKE_CURRENT_BINARY_DIR}/include)
+
+target_link_libraries(app PRIVATE open_earable_protocols)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 350fdd3b..28dfd513 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,6 +14,9 @@ This repository contains the OpenEarable 2 firmware for the `openearable_v2/nrf5
The main firmware lives at the repository root and is built through Zephyr and the nRF Connect SDK.
+Unit tests live under `tests/unit`. See [tests/README.md](tests/README.md) for the
+GitHub Actions workflow, optional local execution, and suite structure.
+
- `src/audio`, `src/bluetooth`, `src/modules`: runtime audio, Bluetooth, and application modules.
- `src/SensorManager`, `src/Battery`, `src/SD_Card`, `src/time_sync`: sensor, power, storage, and synchronization subsystems.
- `src/drivers`, `src/Wire`, `src/utils`, `src/buttons`, `src/ParseInfo`: reusable device support and shared utilities.
@@ -145,6 +148,33 @@ This repository expects code to be documented.
- Avoid unrelated formatting churn.
- Keep headers and source files aligned: declarations, ownership, and invariants should be easy to trace.
+### Static Analysis
+
+GitHub Actions runs advisory CodeChecker analysis for pull requests; findings
+and analyzer failures do not block merging. It analyzes the FOTA application
+without sysbuild using Clang Static Analyzer and Cppcheck. Style findings,
+Clang-Tidy, formatting, and automatic fixes are not enabled. The analyzer
+configuration is in [.codechecker.json](.codechecker.json).
+
+Local execution is optional. In an nRF Connect SDK `v3.0.1` Linux or WSL
+workspace, install CodeChecker `6.28.2`, Clang, and Cppcheck, then run:
+
+```bash
+sudo apt-get install clang cppcheck
+python -m pip install "codechecker==6.28.2"
+codechecker_config="$(pwd)/.codechecker.json"
+west build --no-sysbuild --board openearable_v2/nrf5340/cpuapp \
+ --pristine=always . -- -DFILE_SUFFIX=fota \
+ -DZEPHYR_SCA_VARIANT=codechecker \
+ -DCODECHECKER_CONFIG_FILE="${codechecker_config}" \
+ -DCODECHECKER_EXPORT=html,json
+```
+
+Reports are written to `/sca/codechecker/`. See Nordic's
+[CodeChecker documentation for nRF Connect SDK 3.0.1](https://nrfconnectdocs.nordicsemi.com/ncs/3.0.1/zephyr/develop/sca/codechecker.html)
+or the corresponding [upstream Zephyr 4.0 documentation](https://docs.zephyrproject.org/4.0.0/develop/sca/codechecker.html)
+for the available options.
+
## Validation Before Opening A Pull Request
At minimum, contributors should validate that the firmware still builds with the repository's supported configuration.
diff --git a/README.md b/README.md
index 78b3f08a..1c032739 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,16 @@
- In the **nRF Connect** tab, select **"Manage SDK"**.
- Install **SDK version 3.0.1**.
-7. **Open the Firmware Folder in VS Code**
+7. **Clone or Update the Firmware Repository and Open It in VS Code**
+ - For a new checkout, clone the repository together with its submodules:
+ ```bash
+ git clone --recurse-submodules https://github.com/OpenEarable/open-earable-2.git
+ ```
+ - If the repository is already checked out, initialize or update its submodules from the repository root:
+ ```bash
+ git submodule update --init --recursive
+ ```
+ - The `protocol` submodule provides the generated C sources in `protocol/generated/c` that are required during CMake configuration.
- Use `File > Open Folder` or drag-and-drop the firmware directory into VS Code.
- OR in the **APPLICATIONS** section of the nRF Connect tab:
- Select `Open Exisiting Application`.
@@ -195,4 +204,3 @@ If you are using OpenEarable, please cite is as follows:
```
-
diff --git a/VERSION b/VERSION
index 8ba3fd9f..7b19d147 100644
--- a/VERSION
+++ b/VERSION
@@ -1,5 +1,5 @@
VERSION_MAJOR = 2
VERSION_MINOR = 2
-PATCHLEVEL = 7
+PATCHLEVEL = 8
VERSION_TWEAK = 0
EXTRAVERSION =
diff --git a/doc/dmic_gain_reference.rst b/doc/dmic_gain_reference.rst
new file mode 100644
index 00000000..627973a0
--- /dev/null
+++ b/doc/dmic_gain_reference.rst
@@ -0,0 +1,189 @@
+.. _dmic_gain_reference:
+
+DMIC Microphone Gain Control Reference
+#######################################
+
+.. contents::
+ :local:
+ :depth: 2
+
+Overview
+********
+
+The DMIC (Digital Microphone) gain on the ADAU1860 codec can now be adjusted over Bluetooth using the Audio Config Service characteristic ``BT_UUID_DMIC_GAIN`` (UUID: ``1410df99-5f68-4ebb-a7c7-5e0fb9ae7557``).
+
+Hardware Register Mapping
+**************************
+
+The ADAU186x names the digital microphone inputs by channel number. This
+firmware keeps those chip-level names at the driver boundary and maps them to
+board-specific microphone positions in ``hw_codec_adau1860.cpp``:
+
+- ``DMIC_VOL0`` / DMIC channel 0 controls the **external** microphone.
+- ``DMIC_VOL1`` / DMIC channel 1 controls the **internal** microphone.
+
+Based on the ADAU186x datasheet (register DMIC_VOL0 @ address 0x4000C045):
+
+.. list-table::
+ :header-rows: 1
+ :widths: 20 20 60
+
+ * - Register Value
+ - Gain (dB)
+ - Description
+ * - 0x00
+ - +24
+ - Maximum positive gain
+ * - 0x01
+ - +23.625
+ -
+ * - 0x02
+ - +23.25
+ -
+ * - ...
+ - ...
+ - Each step = -0.375 dB
+ * - 0x3F
+ - +0.375
+ -
+ * - **0x40**
+ - **0**
+ - **Reset/Default value**
+ * - 0x41
+ - -0.375
+ -
+ * - ...
+ - ...
+ - Each step = -0.375 dB
+ * - 0xFD
+ - -70.875
+ -
+ * - 0xFE
+ - -71.25
+ -
+ * - 0xFF
+ - ∞
+ - Mute (infinite attenuation)
+
+Gain Calculation
+*****************
+
+For any register value ``reg`` (0x00–0x3F for positive gains):
+
+- **Gain (dB) = 24 - (reg × 0.375)**
+
+For negative gains (0x41–0xFD):
+
+- **Gain (dB) = -(reg - 0x40) × 0.375**
+
+Examples
+========
+
+- ``0x00`` → 24 - (0 × 0.375) = **+24 dB**
+- ``0x10`` → 24 - (16 × 0.375) = **+18 dB**
+- ``0x20`` → 24 - (32 × 0.375) = **+12 dB** (initial value in this firmware)
+- ``0x40`` → **0 dB**
+- ``0x50`` → -(80 - 64) × 0.375 = **-6 dB**
+- ``0x60`` → -(96 - 64) × 0.375 = **-12 dB**
+
+BLE Protocol
+************
+
+Characteristic Details
+======================
+
+- **UUID**: ``0x1410df99-5f68-4ebb-a7c7-5e0fb9ae7557`` (under Audio Config Service)
+- **Properties**: Read, Write
+- **Payload**: 2 bytes
+
+Write Command
+=============
+
+Send 2 bytes ``[external_gain_reg, internal_gain_reg]``:
+
+.. code-block:: text
+
+ Byte 0: External microphone gain register (DMIC_VOL0, 0x00–0xFF)
+ Byte 1: Internal microphone gain register (DMIC_VOL1, 0x00–0xFF)
+
+Read Response
+=============
+
+Returns 2 bytes showing current DMIC gain as ``[external_gain_reg, internal_gain_reg]``.
+
+Usage Examples
+**************
+
+Set both microphones to +12 dB
+===============================
+
+Send: ``[0x20, 0x20]``
+
+Set external to +18 dB, internal to +6 dB
+=========================================
+
+Send: ``[0x10, 0x30]``
+
+(0x10 = 24 - 16×0.375 = +18 dB, 0x30 = 24 - 48×0.375 = +6 dB)
+
+Set to 0 dB (neutral/reset)
+============================
+
+Send: ``[0x40, 0x40]``
+
+Mute both microphones
+=====================
+
+Send: ``[0xFF, 0xFF]``
+
+Set external to -12 dB (96 = 0x60), internal to 0 dB
+=====================================================
+
+Send: ``[0x60, 0x40]``
+
+API Reference (C/C++)
+*********************
+
+Functions
+=========
+
+.. code-block:: c
+
+ // Set DMIC gain for both microphones
+ // gain_external_reg, gain_internal_reg: register values (0x00–0xFF)
+ int hw_codec_mic_gain_set(uint8_t gain_external_reg, uint8_t gain_internal_reg);
+
+ // Get current DMIC gain register value for external/internal microphone
+ uint8_t hw_codec_mic_gain_get_external(void);
+ uint8_t hw_codec_mic_gain_get_internal(void);
+
+Example C Code
+==============
+
+.. code-block:: cpp
+
+ // Set external and internal DMIC to +12 dB
+ hw_codec_mic_gain_set(0x20, 0x20);
+
+ // Read back the values
+ uint8_t external = hw_codec_mic_gain_get_external(); // Should return 0x20
+ uint8_t internal = hw_codec_mic_gain_get_internal(); // Should return 0x20
+
+Implementation Details
+**********************
+
+- **File**: ``src/modules/hw_codec_adau1860.cpp``
+- **Registers Written**: external mic ``DMIC_VOL0`` (0x4000C045), internal mic ``DMIC_VOL1`` (0x4000C046)
+- **BLE Handler**: ``src/bluetooth/gatt_services/audio_config_service.c``
+- **Logging**: INFO level logs show when gain is changed via BLE
+
+Notes
+*****
+
+- Each register value change takes effect immediately on the hardware.
+- Default at startup: 0x20 (+12 dB) for both channels (set in ``ADAU1860::begin()``).
+- The characteristic is read/write; clients can query current gain and set new values.
+- The 0.375 dB step provides fine-grained control with 256 possible values.
+- To verify the physical mapping experimentally, set one microphone to mute
+ (0xFF) and the other to 0 dB (0x40), then speak into the external and
+ internal microphones separately.
diff --git a/include/openearable_common.h b/include/openearable_common.h
index 12c728a5..40a1c38c 100644
--- a/include/openearable_common.h
+++ b/include/openearable_common.h
@@ -115,7 +115,6 @@ struct sensor_config {
uint8_t storageOptions;
} __attribute__((packed));
-
struct battery_settings {
float u_nominal;
float u_term;
diff --git a/prj.conf b/prj.conf
index 5741e028..aae3b8f7 100644
--- a/prj.conf
+++ b/prj.conf
@@ -72,8 +72,7 @@ CONFIG_SD_LOG_LEVEL_OFF=y
# Suppress LOG_INF messages from hci_core
CONFIG_BT_HCI_CORE_LOG_LEVEL_WRN=y
-# Michael: missing somehow
-CONFIG_NRF5340_AUDIO_CS47L63_DRIVER=y
+CONFIG_NRF5340_AUDIO_ADAU1860_DRIVER=y
CONFIG_NRF5340_AUDIO_POWER_MEASUREMENT=n
CONFIG_NRF5340_AUDIO_SD_CARD_MODULE=y
@@ -114,6 +113,10 @@ CONFIG_SFLOAT=y
CONFIG_FPU=y
+CONFIG_CMSIS_DSP=y
+CONFIG_CMSIS_DSP_TRANSFORM=y
+CONFIG_CMSIS_DSP_COMPLEXMATH=y
+
CONFIG_NEWLIB_LIBC=y
CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y
CONFIG_NEWLIB_LIBC_FLOAT_SCANF=y
@@ -137,6 +140,7 @@ CONFIG_BT_USER_PHY_UPDATE=y
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
CONFIG_BT_BUF_ACL_RX_SIZE=502
+CONFIG_BT_BUF_ACL_RX_COUNT_EXTRA=8
CONFIG_BT_BUF_ACL_TX_SIZE=502
CONFIG_BT_L2CAP_TX_MTU=498
CONFIG_BT_CTLR_PHY_2M=y
@@ -197,7 +201,13 @@ CONFIG_FS_FATFS_EXFAT=y # Enable exFAT support
CONFIG_FDSP=y
+# CMSIS-DSP Configuration
+CONFIG_CMSIS_DSP=y
+CONFIG_CMSIS_DSP_TRANSFORM=y
+CONFIG_CMSIS_DSP_COMPLEXMATH=y
+
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096
+CONFIG_AUDIO_RESPONSE_MAX_SAMPLES=32000
#CONFIG_USB_MASS_STORAGE=y
#CONFIG_MASS_STORAGE_STACK_SIZE=1024
diff --git a/prj_fota.conf b/prj_fota.conf
index 0aee2b1b..746c7a26 100644
--- a/prj_fota.conf
+++ b/prj_fota.conf
@@ -85,8 +85,7 @@ CONFIG_FLASH_MAP=y
CONFIG_SPI_NOR=y
CONFIG_SPI_NOR_SFDP_DEVICETREE=y
-# Michael: missing somehow
-CONFIG_NRF5340_AUDIO_CS47L63_DRIVER=y
+CONFIG_NRF5340_AUDIO_ADAU1860_DRIVER=y
CONFIG_NRF5340_AUDIO_POWER_MEASUREMENT=n
CONFIG_NRF5340_AUDIO_SD_CARD_MODULE=y
@@ -136,6 +135,7 @@ CONFIG_BT_USER_PHY_UPDATE=y
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
CONFIG_BT_BUF_ACL_RX_SIZE=502
+CONFIG_BT_BUF_ACL_RX_COUNT_EXTRA=8
CONFIG_BT_BUF_ACL_TX_SIZE=502
CONFIG_BT_L2CAP_TX_MTU=498
CONFIG_BT_CTLR_PHY_2M=y
@@ -262,3 +262,9 @@ CONFIG_MCUMGR_GRP_OS_RESET_HOOK=y
#CONFIG_MCUMGR_GRP_SETTINGS_ACCESS_HOOK=y
CONFIG_SETUP_FUEL_GAUGE=y
+
+CONFIG_CMSIS_DSP=y
+CONFIG_CMSIS_DSP_TRANSFORM=y
+CONFIG_CMSIS_DSP_COMPLEXMATH=y
+
+CONFIG_AUDIO_RESPONSE_MAX_SAMPLES=32000
diff --git a/protocol b/protocol
new file mode 160000
index 00000000..53221c84
--- /dev/null
+++ b/protocol
@@ -0,0 +1 @@
+Subproject commit 53221c847f2c00924ed80e8f0b1b0c90d3a5eba0
diff --git a/scripts/ci/codechecker_pr_report.py b/scripts/ci/codechecker_pr_report.py
new file mode 100644
index 00000000..2c405740
--- /dev/null
+++ b/scripts/ci/codechecker_pr_report.py
@@ -0,0 +1,575 @@
+#!/usr/bin/env python3
+"""Create advisory GitHub output from a CodeChecker JSON report."""
+
+from __future__ import annotations
+
+import argparse
+from collections import Counter
+from html import escape as html_escape
+import json
+import os
+import plistlib
+import re
+import subprocess
+from pathlib import Path
+from typing import Any
+from urllib.parse import quote
+
+
+MARKER = ""
+SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"}
+DIFF_HEADER = re.compile(r"^\+\+\+ b/(.+)$")
+DIFF_RANGE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
+ANALYZER_RESULT = re.compile(
+ r"_(?P[A-Za-z0-9_-]+)_[0-9a-f]+\.plist(?P\.err)?$"
+)
+ANSI_ESCAPE = re.compile(r"(?:\\x1b|\x1b)\[[0-9;]*m")
+DIAGNOSTIC = re.compile(
+ r"(?P[^\r\n]+?):(?P\d+):(?P\d+):\s*"
+ r"(?:fatal\s+)?error:\s*(?P[^\r\n]+)"
+)
+MAX_DETAILS = 100
+CONTEXT_LINES = 1
+MAX_SNIPPET_LINE_LENGTH = 180
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--report", type=Path, required=True)
+ parser.add_argument("--analysis-dir", type=Path)
+ parser.add_argument("--comment-output", type=Path, required=True)
+ parser.add_argument("--base", default="")
+ parser.add_argument("--head", default="HEAD")
+ parser.add_argument("--repo-root", type=Path, default=Path.cwd())
+ return parser.parse_args()
+
+
+def changed_lines(repo_root: Path, base: str, head: str) -> dict[str, list[range]]:
+ if not base:
+ return {}
+
+ command = [
+ "git",
+ "diff",
+ "--unified=0",
+ "--no-color",
+ f"{base}...{head}",
+ "--",
+ "*.c",
+ "*.cc",
+ "*.cpp",
+ "*.cxx",
+ "*.h",
+ "*.hh",
+ "*.hpp",
+ "*.hxx",
+ ]
+ result = subprocess.run(
+ command,
+ cwd=repo_root,
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+
+ current_file: str | None = None
+ changed: dict[str, list[range]] = {}
+ for raw_line in result.stdout.splitlines():
+ header = DIFF_HEADER.match(raw_line)
+ if header:
+ current_file = header.group(1)
+ if Path(current_file).suffix.lower() not in SOURCE_SUFFIXES:
+ current_file = None
+ continue
+
+ hunk = DIFF_RANGE.match(raw_line)
+ if current_file and hunk:
+ start = int(hunk.group(1))
+ count = int(hunk.group(2) or "1")
+ if count:
+ changed.setdefault(current_file, []).append(range(start, start + count))
+
+ return changed
+
+
+def report_path(report: dict[str, Any], repo_root: Path) -> str | None:
+ file_value = report.get("file", {})
+ candidates: list[str] = []
+ if isinstance(file_value, dict):
+ candidates.extend(
+ str(file_value.get(key, "")) for key in ("path", "original_path", "id")
+ )
+ elif file_value:
+ candidates.append(str(file_value))
+
+ repo_root = repo_root.resolve()
+ repo_name = repo_root.name
+ for candidate in candidates:
+ if not candidate:
+ continue
+ normalized = candidate.replace("\\", "/")
+ candidate_path = Path(candidate)
+ if candidate_path.is_absolute():
+ try:
+ return candidate_path.resolve().relative_to(repo_root).as_posix()
+ except ValueError:
+ pass
+
+ if (repo_root / normalized).exists():
+ return normalized
+ parts = Path(normalized).parts
+ for index in range(1, len(parts)):
+ suffix = Path(*parts[index:])
+ if (repo_root / suffix).exists():
+ return suffix.as_posix()
+ marker = f"/{repo_name}/"
+ if marker in normalized:
+ return normalized.rsplit(marker, 1)[1]
+ if normalized.startswith(f"{repo_name}/"):
+ return normalized[len(repo_name) + 1 :]
+
+ return None
+
+
+def load_reports(report_file: Path) -> list[dict[str, Any]]:
+ if not report_file.is_file():
+ return []
+ data = json.loads(report_file.read_text(encoding="utf-8"))
+ if isinstance(data, dict):
+ reports = data.get("reports", [])
+ else:
+ reports = data
+ return [report for report in reports if isinstance(report, dict)]
+
+
+def analyzer_results(analysis_dir: Path | None) -> tuple[Counter[str], Counter[str]]:
+ completed: Counter[str] = Counter()
+ failed: Counter[str] = Counter()
+ if not analysis_dir or not analysis_dir.is_dir():
+ return completed, failed
+
+ for path in analysis_dir.iterdir():
+ match = ANALYZER_RESULT.search(path.name)
+ if not match:
+ continue
+ destination = failed if match.group("failed") else completed
+ destination[match.group("analyzer")] += 1
+ return completed, failed
+
+
+def analyzer_failures(
+ analysis_dir: Path | None, repo_root: Path
+) -> list[dict[str, str | int]]:
+ failures: list[dict[str, str | int]] = []
+ if not analysis_dir or not analysis_dir.is_dir():
+ return failures
+
+ for path in sorted(analysis_dir.glob("*.plist.err")):
+ match = ANALYZER_RESULT.search(path.name)
+ analyzer = match.group("analyzer") if match else "unknown"
+ source = path.name.split(f"_{analyzer}_", 1)[0]
+ line = 1
+ message = "Analyzer invocation failed; see the artifact for diagnostics."
+ try:
+ data = plistlib.loads(path.read_bytes())
+ stderr = ANSI_ESCAPE.sub("", str(data.get("stderr", "")))
+ diagnostic = DIAGNOSTIC.search(stderr)
+ if diagnostic:
+ candidate = report_path(
+ {"file": {"original_path": diagnostic.group("path")}}, repo_root
+ )
+ source = candidate or Path(diagnostic.group("path")).name
+ line = int(diagnostic.group("line"))
+ message = diagnostic.group("message").strip()
+ except (OSError, plistlib.InvalidFileException, ValueError):
+ pass
+ failures.append(
+ {"analyzer": analyzer, "path": source, "line": line, "message": message}
+ )
+ return failures
+
+
+def escape_annotation(value: str) -> str:
+ return (
+ value.replace("%", "%25")
+ .replace("\r", "%0D")
+ .replace("\n", "%0A")
+ .replace(":", "%3A")
+ .replace(",", "%2C")
+ )
+
+
+def markdown_escape(value: str) -> str:
+ return value.replace("|", "\\|").replace("\n", " ").replace("\r", " ")
+
+
+def source_url(path: str, line: int, repository: str, revision: str) -> str | None:
+ if not repository or not revision:
+ return None
+ return f"https://github.com/{repository}/blob/{revision}/{quote(path, safe='/')}#L{line}"
+
+
+def source_location(path: str, line: int, repository: str, revision: str) -> str:
+ label = f"{path}:{line}"
+ url = source_url(path, line, repository, revision)
+ if not url:
+ return f"`{label}`"
+ return f"[`{label}`]({url})"
+
+
+def source_location_html(path: str, line: int, repository: str, revision: str) -> str:
+ label = html_escape(f"{path}:{line}")
+ url = source_url(path, line, repository, revision)
+ if not url:
+ return f"{label}"
+ return f'{label}'
+
+
+def finding_table(
+ reports: list[dict[str, Any]], repository: str, revision: str
+) -> list[str]:
+ lines = [
+ "| Severity | Location | Analyzer/checker | Message |",
+ "| --- | --- | --- | --- |",
+ ]
+ for finding in reports:
+ severity = markdown_escape(str(finding.get("severity") or "UNSPECIFIED"))
+ location = source_location(
+ str(finding["repo_path"]), int(finding["line"]), repository, revision
+ )
+ analyzer = markdown_escape(str(finding.get("analyzer_name") or "unknown"))
+ checker = markdown_escape(str(finding.get("checker_name") or "CodeChecker"))
+ message = markdown_escape(str(finding.get("message") or "Static analysis finding"))
+ lines.append(f"| {severity} | {location} | `{analyzer}/{checker}` | {message} |")
+ return lines
+
+
+def finding_range(
+ finding: dict[str, Any], repo_root: Path
+) -> tuple[int, int, int, int]:
+ line = int(finding["line"])
+ start_col = int(finding.get("column") or 1)
+ end_line = line
+ end_col = start_col
+ for event in reversed(finding.get("bug_path_events") or []):
+ if not isinstance(event, dict):
+ continue
+ event_path = report_path(event, repo_root)
+ event_range = event.get("range") or {}
+ if event_path != finding["repo_path"] or int(event.get("line") or 0) != line:
+ continue
+ start_col = int(event_range.get("start_col") or event.get("column") or start_col)
+ end_line = int(event_range.get("end_line") or line)
+ end_col = int(event_range.get("end_col") or start_col)
+ break
+ return line, start_col, end_line, end_col
+
+
+def source_excerpt(finding: dict[str, Any], repo_root: Path) -> str | None:
+ source = repo_root / str(finding["repo_path"])
+ if not source.is_file():
+ return None
+ try:
+ source_lines = source.read_text(encoding="utf-8", errors="replace").splitlines()
+ except OSError:
+ return None
+
+ line, start_col, end_line, end_col = finding_range(finding, repo_root)
+ if line < 1 or line > len(source_lines):
+ return None
+ first = max(1, line - CONTEXT_LINES)
+ last = min(len(source_lines), max(line, end_line) + CONTEXT_LINES)
+ width = len(str(last))
+ excerpt: list[str] = []
+ for current in range(first, last + 1):
+ prefix = ">" if line <= current <= end_line else " "
+ rendered = source_lines[current - 1].expandtabs(4)
+ if len(rendered) > MAX_SNIPPET_LINE_LENGTH:
+ rendered = f"{rendered[: MAX_SNIPPET_LINE_LENGTH - 3]}..."
+ excerpt.append(f"{prefix} {current:>{width}} | {rendered}")
+ if current == line:
+ original = source_lines[current - 1]
+ expanded_prefix = original[: max(0, start_col - 1)].expandtabs(4)
+ if end_line == line:
+ selected = original[
+ max(0, start_col - 1) : max(start_col, end_col)
+ ].expandtabs(4)
+ marker_width = max(1, len(selected))
+ else:
+ marker_width = max(1, len(rendered) - len(expanded_prefix))
+ excerpt.append(
+ f" {'':>{width}} | {' ' * len(expanded_prefix)}{'^' * marker_width}"
+ )
+ return "\n".join(excerpt)
+
+
+def context_section(
+ reports: list[dict[str, Any]],
+ repo_root: Path,
+ repository: str,
+ revision: str,
+) -> list[str]:
+ details = reports[:MAX_DETAILS]
+ if not details:
+ return []
+
+ lines = [
+ "### Complete-codebase findings",
+ "",
+ "Each finding includes source context. The `>` line and carets identify the "
+ "expression reported by the analyzer.",
+ "",
+ "",
+ f"Show detailed findings with source context ({len(details)}"
+ f" of {len(reports)})
",
+ "",
+ ]
+ for finding in details:
+ severity = html_escape(str(finding.get("severity") or "UNSPECIFIED"))
+ message = html_escape(str(finding.get("message") or "Static analysis finding"))
+ location = source_location_html(
+ str(finding["repo_path"]), int(finding["line"]), repository, revision
+ )
+ excerpt = source_excerpt(finding, repo_root)
+ lines.extend(
+ [
+ "",
+ f"{severity} — {location} — {message}
",
+ "",
+ ]
+ )
+ if excerpt:
+ lines.extend(["```text", excerpt, "```", ""])
+ else:
+ lines.extend(["Source context is unavailable in the workflow checkout.", ""])
+ lines.extend([" ", ""])
+ lines.extend([" ", ""])
+ return lines
+
+
+def main() -> int:
+ args = parse_args()
+ repo_root = args.repo_root.resolve()
+ try:
+ changed = changed_lines(repo_root, args.base, args.head)
+ except subprocess.CalledProcessError as error:
+ print(f"Could not calculate changed lines: {error}")
+ changed = {}
+
+ all_reports = [
+ report
+ for report in load_reports(args.report)
+ if str(report.get("severity", "")).upper() != "STYLE"
+ ]
+ completed_runs, failed_runs = analyzer_results(args.analysis_dir)
+ failures = analyzer_failures(args.analysis_dir, repo_root)
+ analyzer_errors = sum(failed_runs.values())
+ findings: list[dict[str, Any]] = []
+ for report in all_reports:
+ path = report_path(report, repo_root)
+ line = int(report.get("line") or 1)
+ if path not in changed or not any(line in lines for lines in changed[path]):
+ continue
+ finding = dict(report)
+ finding["repo_path"] = path
+ finding["line"] = line
+ findings.append(finding)
+
+ severity_order = {
+ "CRITICAL": 0,
+ "HIGH": 1,
+ "MEDIUM": 2,
+ "LOW": 3,
+ "UNSPECIFIED": 4,
+ }
+ findings.sort(
+ key=lambda item: (
+ severity_order.get(str(item.get("severity", "UNSPECIFIED")).upper(), 5),
+ item["repo_path"],
+ item["line"],
+ )
+ )
+
+ normalized_reports: list[dict[str, Any]] = []
+ for report in all_reports:
+ path = report_path(report, repo_root)
+ if not path:
+ continue
+ normalized = dict(report)
+ normalized["repo_path"] = path
+ normalized["line"] = int(report.get("line") or 1)
+ normalized_reports.append(normalized)
+ normalized_reports.sort(
+ key=lambda item: (
+ severity_order.get(str(item.get("severity", "UNSPECIFIED")).upper(), 5),
+ item["repo_path"],
+ item["line"],
+ )
+ )
+
+ for finding in findings[:50]:
+ severity = str(finding.get("severity") or "UNSPECIFIED").upper()
+ level = "warning" if severity in {"CRITICAL", "HIGH", "MEDIUM"} else "notice"
+ checker = str(finding.get("checker_name") or "CodeChecker")
+ message = str(finding.get("message") or "Static analysis finding")
+ print(
+ f"::{level} file={escape_annotation(finding['repo_path'])},"
+ f"line={finding['line']},title={escape_annotation(checker)}::"
+ f"{escape_annotation(f'[{severity}] {message}')}"
+ )
+
+ run_url = os.environ.get("GITHUB_RUN_URL", "")
+ repository = os.environ.get("GITHUB_REPOSITORY", "")
+ revision = args.head
+ report_available = args.report.is_file()
+ severity_counts = Counter(
+ str(report.get("severity") or "UNSPECIFIED").upper()
+ for report in normalized_reports
+ )
+ analyzer_counts = Counter(
+ str(report.get("analyzer_name") or "unknown") for report in normalized_reports
+ )
+ lines = [
+ MARKER,
+ "## CodeChecker static analysis (advisory)",
+ "",
+ "> [!NOTE]",
+ "> This report is informational. It does not block merging and does not change code.",
+ "",
+ "### At a glance",
+ "",
+ "| Scope | Result |",
+ "| --- | ---: |",
+ f"| Findings on lines changed by this PR | **{len(findings)}** |",
+ f"| Findings in the complete codebase | **{len(normalized_reports)}** |",
+ f"| Analyzer invocations completed | **{sum(completed_runs.values())}** |",
+ f"| Analyzer invocations failed | **{analyzer_errors}** |",
+ "",
+ ]
+ if not report_available:
+ lines.extend(
+ [
+ "CodeChecker did not produce a JSON report. Check the downloadable analysis log "
+ "for build or analyzer errors.",
+ "",
+ ]
+ )
+ elif findings:
+ lines.extend(
+ [
+ "### Findings on changed lines",
+ "",
+ f"Found **{len(findings)}** non-style issue(s) on lines changed by this PR.",
+ "",
+ ]
+ )
+ lines.extend(finding_table(findings[:50], repository, revision))
+ if len(findings) > 50:
+ lines.extend(["", f"Only the first 50 of {len(findings)} findings are shown here."])
+ lines.append("")
+ else:
+ lines.extend(
+ [
+ "### Findings on changed lines",
+ "",
+ "No non-style findings were reported on lines changed by this PR.",
+ "",
+ ]
+ )
+
+ if completed_runs or failed_runs:
+ lines.extend(
+ [
+ "### Analyzer coverage",
+ "",
+ "| Analyzer | Completed | Failed |",
+ "| --- | ---: | ---: |",
+ ]
+ )
+ for analyzer in sorted(completed_runs.keys() | failed_runs.keys()):
+ lines.append(
+ f"| `{analyzer}` | {completed_runs[analyzer]} | {failed_runs[analyzer]} |"
+ )
+ lines.append("")
+
+ if failures:
+ lines.extend(
+ [
+ "",
+ f"{len(failures)} failed analyzer invocation(s)
",
+ "",
+ "These files received reduced analysis coverage. The first compiler diagnostic "
+ "from each invocation is shown below.",
+ "The reason is that the clang frontend was not able to link them because of issues like implicit declaration. Target gcc build may still succeed.",
+ "",
+ "| Analyzer | Source | First diagnostic |",
+ "| --- | --- | --- |",
+ ]
+ )
+ for failure in failures:
+ location = source_location(
+ str(failure["path"]), int(failure["line"]), repository, revision
+ )
+ lines.append(
+ f"| `{markdown_escape(str(failure['analyzer']))}` | {location} | "
+ f"{markdown_escape(str(failure['message']))} |"
+ )
+ lines.extend(["", " ", ""])
+
+ if report_available:
+ severity_summary = ", ".join(
+ f"**{severity_counts.get(severity, 0)} {severity.lower()}**"
+ for severity in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "UNSPECIFIED")
+ if severity_counts.get(severity, 0)
+ ) or "no findings"
+ analyzer_summary = ", ".join(
+ f"`{name}`: {count}" for name, count in sorted(analyzer_counts.items())
+ ) or "none"
+ lines.extend(
+ [
+ "### Complete-codebase summary",
+ "",
+ f"Severity: {severity_summary} ",
+ f"Reported by analyzer: {analyzer_summary}",
+ "",
+ ]
+ )
+ lines.extend(
+ context_section(normalized_reports, repo_root, repository, revision)
+ )
+
+ if run_url:
+ lines.append(
+ "Open the workflow run and download the complete CodeChecker "
+ f"[report]({run_url}) for a detailed view. Check out "
+ "`codechecker.html/index.html`."
+ )
+ lines.append("")
+
+ # Keep successful, finding-free runs deliberately terse. The marker remains so
+ # the workflow can update the same PR comment on subsequent runs.
+ if (
+ report_available
+ and not normalized_reports
+ and not failures
+ and not analyzer_errors
+ ):
+ lines = [
+ MARKER,
+ "## CodeChecker static analysis",
+ "",
+ "✅ No non-style issues found.",
+ ]
+
+ args.comment_output.parent.mkdir(parents=True, exist_ok=True)
+ args.comment_output.write_text("\n".join(lines), encoding="utf-8")
+
+ summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
+ if summary_file:
+ with Path(summary_file).open("a", encoding="utf-8") as summary:
+ summary.write("\n".join(lines[1:]))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/Battery/BQ25120a.cpp b/src/Battery/BQ25120a.cpp
index 71da49d9..b03a3847 100644
--- a/src/Battery/BQ25120a.cpp
+++ b/src/Battery/BQ25120a.cpp
@@ -153,28 +153,28 @@ void BQ25120a::setup(const battery_settings &_battery_settings) {
uint8_t BQ25120a::read_charging_state() {
uint8_t status = 0;
- bool ret = readReg(registers::CTRL, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::CTRL, (uint8_t *) &status, sizeof(status));
return status;
}
uint8_t BQ25120a::read_fault() {
uint8_t status = 0;
- bool ret = readReg(registers::FAULT, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::FAULT, (uint8_t *) &status, sizeof(status));
return status;
}
uint8_t BQ25120a::read_ts_fault() {
uint8_t status = 0;
- bool ret = readReg(registers::TS_FAULT, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::TS_FAULT, (uint8_t *) &status, sizeof(status));
return status;
}
chrg_state BQ25120a::read_charging_control() {
uint8_t status = 0;
- bool ret = readReg(registers::CHARGE_CTRL, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::CHARGE_CTRL, (uint8_t *) &status, sizeof(status));
chrg_state chrg;
@@ -200,13 +200,13 @@ chrg_state BQ25120a::read_charging_control() {
uint8_t BQ25120a::write_charging_control(float mA) {
uint8_t status = 0;
- bool ret = readReg(registers::CHARGE_CTRL, &status, sizeof(status));
+ (void)readReg(registers::CHARGE_CTRL, &status, sizeof(status));
status &= 0x3;
if (mA >= 40) {
if (mA > 300) mA = 300;
- status |= (((uint16_t)((mA - 40) / 10 + EPS)) & 0x1F) << 2;
+ status |= (((uint16_t)((mA - 40.0f) / 10.0f + EPS)) & 0x1F) << 2;
status |= 1 << 7;
} else {
if (mA > 35) mA = 35;
@@ -243,10 +243,8 @@ uint8_t BQ25120a::write_LDO_voltage_control(float volt) {
readReg(registers::LS_LDO_CTRL, &status, sizeof(status));
- //status |= (((uint16_t)((volt - 0.8) * 10)) & 0x1F) << 2;
status &= 1 << 7;
- status |= ((uint8_t)((volt - 0.8f) * 10 + EPS)) << 2;
- //status |= 1 << 7;
+ status |= ((uint8_t)((volt - 0.8f) * 10.0f + EPS)) << 2;
writeReg(registers::LS_LDO_CTRL, &status, sizeof(status));
@@ -255,7 +253,7 @@ uint8_t BQ25120a::write_LDO_voltage_control(float volt) {
float BQ25120a::read_ldo_voltage() {
uint8_t status = 0;
- bool ret = readReg(registers::LS_LDO_CTRL, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::LS_LDO_CTRL, (uint8_t *) &status, sizeof(status));
float voltage = 0.8f + ((status >> 2 & 0x1F)) * 0.1f;
@@ -264,7 +262,7 @@ float BQ25120a::read_ldo_voltage() {
float BQ25120a::read_battery_voltage_control() {
uint8_t status = 0;
- bool ret = readReg(registers::BAT_VOL_CTRL, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::BAT_VOL_CTRL, (uint8_t *) &status, sizeof(status));
float voltage = 3.6f + (status >> 1) * 0.01f;
@@ -279,7 +277,7 @@ uint8_t BQ25120a::write_battery_voltage_control(float volt) {
volt = CLAMP(volt, 3.6f, 4.65f);
- status |= (((uint16_t)((volt - 3.6f) * 100 + EPS)) & 0x7F) << 1;
+ status |= (((uint16_t)((volt - 3.6f) * 100.0f + EPS)) & 0x7F) << 1;
writeReg(registers::BAT_VOL_CTRL, &status, sizeof(status));
@@ -288,12 +286,10 @@ uint8_t BQ25120a::write_battery_voltage_control(float volt) {
chrg_state BQ25120a::read_termination_control() {
uint8_t status = 0;
- bool ret = readReg(registers::TERM_CTRL, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::TERM_CTRL, (uint8_t *) &status, sizeof(status));
struct chrg_state chrg;
- // if (!ret) printk("failed to read\n");
-
chrg.enabled = status & 0x2;
//chrg.high_impedance = status & 0x1;
@@ -305,7 +301,7 @@ chrg_state BQ25120a::read_termination_control() {
if (status & (1 << 7)) {
mAh = 6 + mAh * 1;
} else {
- mAh = 0.5 + mAh * 0.5;
+ mAh = 0.5f + mAh * 0.5f;
}
chrg.mAh = mAh;
@@ -325,7 +321,7 @@ uint8_t BQ25120a::write_termination_control(float mA, bool enable_termination) {
status |= 1 << 7;
} else {
if (mA > 5) mA = 5;
- status |= (((uint16_t)(2 * (mA - 0.5))) & 0x1F) << 2;
+ status |= (((uint16_t)(2.0f * (mA - 0.5f))) & 0x1F) << 2;
}
if (enable_termination) {
@@ -341,24 +337,22 @@ ilim_uvlo BQ25120a::read_uvlo_ilim() {
struct ilim_uvlo param;
uint8_t status = 0;
- bool ret = readReg(registers::ILIM_UVLO, (uint8_t *) &status, sizeof(status));
-
- // if (!ret) printk("failed to read\n");
+ (void)readReg(registers::ILIM_UVLO, (uint8_t *) &status, sizeof(status));
- param.uvlo_v = CLAMP(3.0f- 0.2f * ((status & 0x7) - 2), 2.2, 3.0);
+ param.uvlo_v = CLAMP(3.0f- 0.2f * ((status & 0x7) - 2), 2.2f, 3.0f);
param.lim_mA = 50.f + 50.f * ((status >> 3) & 0x7);
return param;
}
uint8_t BQ25120a::write_uvlo_ilim(ilim_uvlo param) {
- float mA = CLAMP(param.lim_mA, 50, 400);
- float v = CLAMP(param.uvlo_v, 2.2, 3.0);
+ float mA = CLAMP(param.lim_mA, 50.0f, 400.0f);
+ float v = CLAMP(param.uvlo_v, 2.2f, 3.0f);
uint8_t status = 0;
- status |= ((uint16_t)(mA / 50 - 1) & 0x7) << 3;
- status |= ((uint16_t)((3.0 - v) * 5 + 2) & 0x7);
+ status |= ((uint16_t)(mA / 50.0f - 1.0f) & 0x7) << 3;
+ status |= ((uint16_t)((3.0f - v) * 5.0f + 2.0f) & 0x7);
writeReg(registers::ILIM_UVLO, &status, sizeof(status));
@@ -408,9 +402,7 @@ button_state BQ25120a::read_button_state() {
struct button_state btn;
uint8_t status = 0;
- bool ret = readReg(registers::BTN_CTRL, (uint8_t *) &status, sizeof(status));
-
- // if (!ret) printk("failed to read\n");
+ (void)readReg(registers::BTN_CTRL, (uint8_t *) &status, sizeof(status));
btn.wake_1 = status & 0x2;
btn.wake_2 = status & 0x1;
@@ -426,4 +418,4 @@ int BQ25120a::set_power_connect_callback(gpio_callback_handler_t handler) {
int BQ25120a::set_int_callback(gpio_callback_handler_t handler) {
gpio_init_callback(&int_cb_data, handler, int_cb_data.pin_mask | BIT(int_pin.pin));
return gpio_add_callback(int_pin.port, &int_cb_data);
-}
\ No newline at end of file
+}
diff --git a/src/Battery/BQ25120a.h b/src/Battery/BQ25120a.h
index 38eb75f3..79ac3314 100644
--- a/src/Battery/BQ25120a.h
+++ b/src/Battery/BQ25120a.h
@@ -13,7 +13,7 @@
#define BQ25120a_I2C_TIMEOUT_US 66
#define BQ25120a_HIGH_Z_TIMEOUT_US 1000
-#define EPS 1e-3
+#define EPS 1e-3f
struct chrg_state {
float mAh = 0;
@@ -107,4 +107,4 @@ class BQ25120a {
extern BQ25120a battery_controller;
-#endif
\ No newline at end of file
+#endif
diff --git a/src/Battery/BQ27220.cpp b/src/Battery/BQ27220.cpp
index 715f22c1..cf21a764 100644
--- a/src/Battery/BQ27220.cpp
+++ b/src/Battery/BQ27220.cpp
@@ -96,7 +96,7 @@ void BQ27220::writeReg(uint8_t reg, uint8_t *buffer, uint16_t len) {
bat_status BQ27220::battery_status() {
bat_status status;
uint16_t val = 0;
- bool ret = readReg(registers::FLAGS, (uint8_t *) &val, sizeof(val));
+ (void)readReg(registers::FLAGS, (uint8_t *) &val, sizeof(val));
status.DSG = val & 0x1;
status.SYSDWN = val & (1 << 1);
@@ -135,7 +135,7 @@ gauge_status BQ27220::gauging_state() {
float BQ27220::temperature() {
uint16_t temp_K = 0;
- bool ret = readReg(registers::TEMP, (uint8_t *) &temp_K, sizeof(temp_K));
+ (void)readReg(registers::TEMP, (uint8_t *) &temp_K, sizeof(temp_K));
float temp = temp_K / 10.0 - 273.15;
return temp;
@@ -143,7 +143,7 @@ float BQ27220::temperature() {
float BQ27220::voltage() {
uint16_t mV = 0;
- bool ret = readReg(registers::VOLT, (uint8_t *) &mV, sizeof(mV));
+ (void)readReg(registers::VOLT, (uint8_t *) &mV, sizeof(mV));
float v = mV / 1000.0;
return v;
@@ -151,74 +151,74 @@ float BQ27220::voltage() {
float BQ27220::capacity() {
uint16_t mAh = 0;
- bool ret = readReg(registers::FCC, (uint8_t *) &mAh, sizeof(mAh));
+ (void)readReg(registers::FCC, (uint8_t *) &mAh, sizeof(mAh));
return mAh;
}
float BQ27220::time_to_full() {
uint16_t minutes = 0;
- bool ret = readReg(registers::TTF, (uint8_t *) &minutes, sizeof(minutes));
+ (void)readReg(registers::TTF, (uint8_t *) &minutes, sizeof(minutes));
return minutes;
}
float BQ27220::time_to_empty() {
uint16_t minutes = 0;
- bool ret = readReg(registers::TTE, (uint8_t *) &minutes, sizeof(minutes));
+ (void)readReg(registers::TTE, (uint8_t *) &minutes, sizeof(minutes));
return minutes;
}
float BQ27220::state_of_charge() {
uint16_t soc = 0;
- bool ret = readReg(registers::SOC, (uint8_t *) &soc, sizeof(soc));
+ (void)readReg(registers::SOC, (uint8_t *) &soc, sizeof(soc));
return soc;
}
float BQ27220::state_of_health() {
uint16_t soc = 0;
- bool ret = readReg(registers::SOH, (uint8_t *) &soc, sizeof(soc));
+ (void)readReg(registers::SOH, (uint8_t *) &soc, sizeof(soc));
return soc;
}
float BQ27220::current() {
int16_t mA = 0;
- bool ret = readReg(registers::NAC, (uint8_t *) &mA, sizeof(mA));
+ (void)readReg(registers::NAC, (uint8_t *) &mA, sizeof(mA));
return mA;
}
float BQ27220::average_current() {
int16_t mA = 0;
- bool ret = readReg(registers::AI, (uint8_t *) &mA, sizeof(mA));
+ (void)readReg(registers::AI, (uint8_t *) &mA, sizeof(mA));
return mA;
}
float BQ27220::design_cap() {
uint16_t mAh = 0;
- bool ret = readReg(registers::DCAP, (uint8_t *) &mAh, sizeof(mAh));
+ (void)readReg(registers::DCAP, (uint8_t *) &mAh, sizeof(mAh));
return mAh;
}
float BQ27220::remaining_cap() {
uint16_t mAh = 0;
- bool ret = readReg(registers::RM, (uint8_t *) &mAh, sizeof(mAh));
+ (void)readReg(registers::RM, (uint8_t *) &mAh, sizeof(mAh));
return mAh;
}
float BQ27220::charge_current() {
int16_t mA = 0;
- bool ret = readReg(registers::CC, (uint8_t *) &mA, sizeof(mA));
+ (void)readReg(registers::CC, (uint8_t *) &mA, sizeof(mA));
return mA;
}
int BQ27220::cycle_count() {
uint16_t n_cycles = 0;
- bool ret = readReg(registers::CYCT, (uint8_t *) &n_cycles, sizeof(n_cycles));
+ (void)readReg(registers::CYCT, (uint8_t *) &n_cycles, sizeof(n_cycles));
return n_cycles;
}
float BQ27220::standby_current() {
int16_t mA = 0;
- bool ret = readReg(registers::SI, (uint8_t *) &mA, sizeof(mA));
+ (void)readReg(registers::SI, (uint8_t *) &mA, sizeof(mA));
return mA;
}
@@ -226,7 +226,7 @@ float BQ27220::standby_current() {
op_state BQ27220::operation_state() {
op_state state;
uint16_t status = 0;
- bool ret = readReg(registers::OP_STAT, (uint8_t *) &status, sizeof(status));
+ (void)readReg(registers::OP_STAT, (uint8_t *) &status, sizeof(status));
state.CALD = status & 0x01;
state.SEC = (status >> 1) & 0x3;
@@ -302,11 +302,9 @@ void BQ27220::exit_config_update(bool init) {
}
void BQ27220::read_RAM(uint16_t ram_address, uint8_t * data, int len) {
- bool ret;
-
writeReg(0x3E, (uint8_t *) &ram_address, sizeof(ram_address));
k_usleep(BQ27220_RAM_TIMEOUT_US);
- ret = readReg(0x40, data, len);
+ (void)readReg(0x40, data, len);
}
int BQ27220::write_RAM(uint16_t ram_address, uint8_t * data, int len, bool check) {
@@ -314,15 +312,13 @@ int BQ27220::write_RAM(uint16_t ram_address, uint8_t * data, int len, bool check
uint8_t data_len=0;
uint8_t buf[len];
- bool ret;
-
writeReg(0x3E, (uint8_t *) &ram_address, sizeof(ram_address));
k_usleep(BQ27220_RAM_TIMEOUT_US);
- ret = readReg(0x61, (uint8_t *) &data_len, sizeof(data_len));
- ret = readReg(0x40, buf, len);
- ret = readReg(0x60, (uint8_t *) &check_sum, sizeof(check_sum));
+ (void)readReg(0x61, (uint8_t *) &data_len, sizeof(data_len));
+ (void)readReg(0x40, buf, len);
+ (void)readReg(0x60, (uint8_t *) &check_sum, sizeof(check_sum));
uint8_t my_check = (uint8_t)0xFF-check_sum; // - data[0] - data[1];
@@ -367,8 +363,6 @@ int BQ27220::write_RAM(uint16_t ram_address, uint16_t val, bool check) {
}
void BQ27220::setup(const battery_settings &_battery_settings, bool init) {
- int ret;
-
// unseal
write_command(0x0414);
k_msleep(100);
@@ -385,29 +379,29 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) {
//ret = write_RAM(0x9220, 0);
// design and full charge capacity
- ret = write_RAM(0x929F, _battery_settings.capacity);
- ret = write_RAM(0x929D, _battery_settings.capacity); //130
+ (void)write_RAM(0x929F, _battery_settings.capacity);
+ (void)write_RAM(0x929D, _battery_settings.capacity); //130
// near full
- ret = write_RAM(0x926B, 5);
+ (void)write_RAM(0x926B, 5);
- ret = write_RAM(0x91F5, _battery_settings.temp_min * 10);
- ret = write_RAM(0x91F7, _battery_settings.temp_max * 10);
+ (void)write_RAM(0x91F5, _battery_settings.temp_min * 10);
+ (void)write_RAM(0x91F7, _battery_settings.temp_max * 10);
// charge current
- ret = write_RAM(0x91FB, _battery_settings.i_charge);
+ (void)write_RAM(0x91FB, _battery_settings.i_charge);
// charge voltage
- ret = write_RAM(0x91FD, _battery_settings.u_term * 1000);
+ (void)write_RAM(0x91FD, _battery_settings.u_term * 1000);
// taper current
- ret = write_RAM(0x9201, _battery_settings.i_term);
+ (void)write_RAM(0x9201, _battery_settings.i_term);
// experimental: min taper capacity
- ret = write_RAM(0x9203, 4); // standard: 25
+ (void)write_RAM(0x9203, 4); // standard: 25
// deadband
uint8_t val = 1;
- ret = write_RAM(0x91DE, &val, sizeof(uint8_t));
+ (void)write_RAM(0x91DE, &val, sizeof(uint8_t));
// deadband CC (verursacht Probleme, rm zählt zu schnell?)
/*val = 5;
@@ -415,14 +409,14 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) {
*/
// sleep current
- ret = write_RAM(0x9217, 1);
+ (void)write_RAM(0x9217, 1);
// dischage current trd
- ret = write_RAM(0x9228, 2);
+ (void)write_RAM(0x9228, 2);
// charge current trd
- ret = write_RAM(0x922A, 2);
+ (void)write_RAM(0x922A, 2);
// quit current
- ret = write_RAM(0x922C, 1);
+ (void)write_RAM(0x922C, 1);
//dod 0%: 4287
//dod 10%: 4125
@@ -444,29 +438,29 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) {
//dod: 103.25%: 3089
// sysDown set Voltage
- ret = write_RAM(0x9240, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET);
+ (void)write_RAM(0x9240, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET);
// sysDown clear Voltage
- ret = write_RAM(0x9243, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET + CONFIG_BATTERY_SYSDOWN_HYSTERESIS);
+ (void)write_RAM(0x9243, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET + CONFIG_BATTERY_SYSDOWN_HYSTERESIS);
// FD set
- ret = write_RAM(0x9282, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET);
+ (void)write_RAM(0x9282, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET);
// FD clear
- ret = write_RAM(0x9284, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET + CONFIG_BATTERY_FD_HYSTERESIS);
+ (void)write_RAM(0x9284, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET + CONFIG_BATTERY_FD_HYSTERESIS);
// FC Voltage
- ret = write_RAM(0x9288, _battery_settings.u_term * 1000 - CONFIG_BATTERY_FC_VOLTAGE_OFFSET);
+ (void)write_RAM(0x9288, _battery_settings.u_term * 1000 - CONFIG_BATTERY_FC_VOLTAGE_OFFSET);
// Electonic Load in 3µA steps
- ret = write_RAM(0x9269, 6); // 18 µA
+ (void)write_RAM(0x9269, 6); // 18 µA
// EMF
//write_RAM(0x92A7, 36001);
//C0
- ret = write_RAM(0x92A9, 480); //bat1:250
+ (void)write_RAM(0x92A9, 480); //bat1:250
//R0
- ret = write_RAM(0x92AB, 19941); //bat1: 19941 //22542 //new bat: 17340
+ (void)write_RAM(0x92AB, 19941); //bat1: 19941 //22542 //new bat: 17340
//R1
//write_RAM(0x92AF, 3160);
@@ -478,16 +472,16 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) {
// do not use, only on CT makes sense:
// SOC Flag, enable FC voltage detection
uint8_t flags_b = 0x8C;
- ret = write_RAM(0x9281, &flags_b, sizeof(flags_b));
+ (void)write_RAM(0x9281, &flags_b, sizeof(flags_b));
// Overload current
- ret = write_RAM(0x9264, _battery_settings.i_max);
+ (void)write_RAM(0x9264, _battery_settings.i_max);
// CEDV Smoothing Config
uint8_t cedv_conf = 0x0D; //Default: 0x08, Enable SMEXT, SMEN 0x0D
- ret = write_RAM(0x9271, &cedv_conf, sizeof(cedv_conf));
+ (void)write_RAM(0x9271, &cedv_conf, sizeof(cedv_conf));
- ret = write_RAM(0x9272, 3700);
+ (void)write_RAM(0x9272, 3700);
exit_config_update(init);
@@ -498,4 +492,4 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) {
int BQ27220::set_int_callback(gpio_callback_handler_t handler) {
gpio_init_callback(&int_cb_data, handler, int_cb_data.pin_mask | BIT(gpout_pin.pin));
return gpio_add_callback(gpout_pin.port, &int_cb_data);
-}
\ No newline at end of file
+}
diff --git a/src/Battery/PowerManager.cpp b/src/Battery/PowerManager.cpp
index 628739da..77ba0760 100644
--- a/src/Battery/PowerManager.cpp
+++ b/src/Battery/PowerManager.cpp
@@ -2,6 +2,7 @@
#include "macros_common.h"
+#include
#include
#include
#include
@@ -54,15 +55,24 @@ static struct battery_data msg;
//LoadSwitch PowerManager::v1_8_switch(GPIO_DT_SPEC_GET(DT_NODELABEL(load_switch), gpios));
void PowerManager::fuel_gauge_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) {
+ ARG_UNUSED(dev);
+ ARG_UNUSED(cb);
+ ARG_UNUSED(pins);
LOG_DBG("Fuel Gauge GPOUT Interrupt");
k_work_submit(&fuel_gauge_work);
}
void PowerManager::battery_controller_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) {
+ ARG_UNUSED(dev);
+ ARG_UNUSED(cb);
+ ARG_UNUSED(pins);
k_work_submit(&battery_controller_work);
}
void PowerManager::power_good_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) {
+ ARG_UNUSED(dev);
+ ARG_UNUSED(cb);
+ ARG_UNUSED(pins);
bool power_good = battery_controller.power_connected();
k_work_submit(&fuel_gauge_work);
@@ -77,16 +87,19 @@ void PowerManager::power_good_callback(const struct device *dev, struct gpio_cal
}
void PowerManager::power_down_work_handler(struct k_work * work) {
+ ARG_UNUSED(work);
power_manager.power_down();
}
void PowerManager::charge_ctrl_work_handler(struct k_work * work) {
+ ARG_UNUSED(work);
power_manager.charge_task();
// Schedule next execution
k_work_schedule(&charge_ctrl_delayable, power_manager.chrg_interval);
}
void PowerManager::battery_controller_work_handler(struct k_work * work) {
+ ARG_UNUSED(work);
button_state state;
//uint8_t val = gpio_pin_get_dt(&power_manager.error_led);
@@ -106,6 +119,7 @@ void PowerManager::battery_controller_work_handler(struct k_work * work) {
}
void PowerManager::fuel_gauge_work_handler(struct k_work * work) {
+ ARG_UNUSED(work);
int ret;
battery_level_status status;
@@ -168,16 +182,16 @@ void PowerManager::fuel_gauge_work_handler(struct k_work * work) {
msg.charging_state = POWER_CONNECTED;
- LOG_DBG("Voltage: %.3f V", voltage);
- LOG_DBG("Charging current: %.3f mA", current);
- LOG_DBG("Target current: %.3f mA", target_current);
- LOG_DBG("State of charge: %.3f %%", fuel_gauge.state_of_charge());
+ LOG_DBG("Voltage: %.3f V", (double)voltage);
+ LOG_DBG("Charging current: %.3f mA", (double)current);
+ LOG_DBG("Target current: %.3f mA", (double)target_current);
+ LOG_DBG("State of charge: %.3f %%", (double)fuel_gauge.state_of_charge());
// check if target current is met (if not tapering)
- if (current > 0.8 * target_current - 2 * power_manager._battery_settings.i_term) {
+ if (current > 0.8f * target_current - 2.0f * power_manager._battery_settings.i_term) {
msg.charging_state = CHARGING;
}
- else if (voltage > power_manager._battery_settings.u_term - 0.02) {
+ else if (voltage > power_manager._battery_settings.u_term - 0.02f) {
#ifdef CONFIG_BATTERY_ENABLE_TRICKLE_CHARGE
msg.charging_state = TRICKLE_CHARGING;
#else
@@ -196,7 +210,7 @@ void PowerManager::fuel_gauge_work_handler(struct k_work * work) {
uint8_t fault = battery_controller.read_fault();
// Battery fuel gauge status
- bat_status status = fuel_gauge.battery_status();
+ bat_status fault_status = fuel_gauge.battery_status();
voltage = fuel_gauge.voltage();
current = fuel_gauge.current();
@@ -208,10 +222,10 @@ void PowerManager::fuel_gauge_work_handler(struct k_work * work) {
// as long as fault exists
if (fault & (1 << 5)) {
bool power_connected = battery_controller.power_connected();
- if (power_connected && current > 0.5 * power_manager._battery_settings.i_term) {
+ if (power_connected && current > 0.5f * power_manager._battery_settings.i_term) {
msg.charging_state = PRECHARGING;
}
- LOG_WRN("Battery under voltage: %.3f V", voltage);
+ LOG_WRN("Battery under voltage: %.3f V", (double)voltage);
}
// cleared after read
@@ -234,12 +248,12 @@ void PowerManager::fuel_gauge_work_handler(struct k_work * work) {
LOG_DBG("------------------ Battery Info ------------------");
LOG_DBG("Battery Status:");
LOG_DBG(" Present: %d, Full Charge: %d, Full Discharge: %d",
- status.BATTPRES, status.FC, status.FD);
+ fault_status.BATTPRES, fault_status.FC, fault_status.FD);
// Basic measurements
LOG_DBG("Basic Measurements:");
- LOG_DBG(" Voltage: %.3f V", voltage);
- LOG_DBG(" Current: %.3f mA", current);
+ LOG_DBG(" Voltage: %.3f V", (double)voltage);
+ LOG_DBG(" Current: %.3f mA", (double)current);
break;
}
@@ -262,10 +276,12 @@ void PowerManager::fuel_gauge_work_handler(struct k_work * work) {
}
int PowerManager::begin() {
- earable_state oe_state;
+ earable_state oe_state{};
oe_state.charging_state = DISCHARGING;
oe_state.pairing_state = PAIRED;
+ oe_state.sd_state = SD_IDLE;
+ oe_state.led_mode = STATE_INDICATION;
battery_controller.begin();
fuel_gauge.begin();
@@ -404,7 +420,7 @@ int PowerManager::begin() {
// check if fuel gauge has wrong value
float capacity = fuel_gauge.capacity();
- if (abs(capacity - _battery_settings.capacity) > 1e-4) {
+ if (std::fabs(capacity - _battery_settings.capacity) > 1e-4F) {
fuel_gauge.setup(_battery_settings);
set_error_led();
}
@@ -540,7 +556,7 @@ void bt_disconnect_handler(struct bt_conn *conn, void * data) {
if (ret != 0) return;
if (info.state == BT_CONN_STATE_CONNECTED) {
- ret = bt_mgmt_conn_disconnect(conn, *((uint8_t*)data));
+ (void)bt_mgmt_conn_disconnect(conn, *((uint8_t*)data));
}
}
@@ -551,7 +567,7 @@ void PowerManager::reboot() {
uint8_t data = BT_HCI_ERR_REMOTE_USER_TERM_CONN;
bt_conn_foreach(BT_CONN_TYPE_ALL, bt_disconnect_handler, &data);
- ret = bt_le_adv_stop();
+ (void)bt_le_adv_stop();
stop_sensor_manager();
@@ -570,7 +586,7 @@ int PowerManager::power_down(bool fault) {
uint8_t data = BT_HCI_ERR_REMOTE_USER_TERM_CONN;
bt_conn_foreach(BT_CONN_TYPE_ALL, bt_disconnect_handler, &data);
- ret = bt_le_adv_stop();
+ (void)bt_le_adv_stop();
// power disonnected
// prepare interrupts
@@ -611,7 +627,7 @@ int PowerManager::power_down(bool fault) {
}
LOG_PANIC();
- ret = bt_mgmt_stop_watchdog();
+ (void)bt_mgmt_stop_watchdog();
//ERR_CHK(ret);
dac.end();
@@ -628,10 +644,10 @@ int PowerManager::power_down(bool fault) {
return 0;
}
- ret = pm_device_action_run(ls_sd, PM_DEVICE_ACTION_SUSPEND);
- ret = pm_device_action_run(ls_3_3, PM_DEVICE_ACTION_SUSPEND);
- ret = pm_device_action_run(ls_1_8, PM_DEVICE_ACTION_SUSPEND);
- ret = pm_device_action_run(cons, PM_DEVICE_ACTION_SUSPEND);
+ (void)pm_device_action_run(ls_sd, PM_DEVICE_ACTION_SUSPEND);
+ (void)pm_device_action_run(ls_3_3, PM_DEVICE_ACTION_SUSPEND);
+ (void)pm_device_action_run(ls_1_8, PM_DEVICE_ACTION_SUSPEND);
+ (void)pm_device_action_run(cons, PM_DEVICE_ACTION_SUSPEND);
/*const struct device *const i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1));
ret = pm_device_action_run(i2c, PM_DEVICE_ACTION_SUSPEND);
@@ -666,32 +682,13 @@ void PowerManager::charge_task() {
battery_controller.enable_charge();
}
- //if (last_charging_state != charging_state || ) {
- k_work_submit(&fuel_gauge_work);
- //state_inidicator.set_state()
- /*switch (charging_state) {
- case 0:
- LOG_INF("charging state: ready");
- break;
- case 1:
- LOG_INF("charging state: charging");
- break;
- case 2:
- LOG_INF("charging state: done");
- break;
- case 3:
- LOG_WRN("charging state: fault");
-
- //battery_controller.setup(_battery_settings);
-
- break;
- }*/
- //}
+ k_work_submit(&fuel_gauge_work);
last_charging_state = charging_state;
}
int cmd_setup_fuel_gauge(const struct shell *shell, size_t argc, const char **argv) {
+ ARG_UNUSED(shell);
ARG_UNUSED(argc);
ARG_UNUSED(argv);
@@ -715,17 +712,17 @@ static int cmd_battery_info(const struct shell *shell, size_t argc, const char *
// Basic measurements
shell_print(shell, "Basic Measurements:");
- shell_print(shell, " Voltage: %.3f V", fuel_gauge.voltage());
- shell_print(shell, " Temperature: %.1f °C", fuel_gauge.temperature());
+ shell_print(shell, " Voltage: %.3f V", (double)fuel_gauge.voltage());
+ shell_print(shell, " Temperature: %.1f °C", (double)fuel_gauge.temperature());
shell_print(shell, " Current: %.1f mA (avg: %.1f mA)",
- fuel_gauge.current(), fuel_gauge.average_current());
- shell_print(shell, " State of Charge: %.1f%%", fuel_gauge.state_of_charge());
+ (double)fuel_gauge.current(), (double)fuel_gauge.average_current());
+ shell_print(shell, " State of Charge: %.1f%%", (double)fuel_gauge.state_of_charge());
// Capacity info
shell_print(shell, "Capacity Information:");
- shell_print(shell, " Design Capacity: %.1f mAh", fuel_gauge.design_cap());
- shell_print(shell, " Full Charge Capacity: %.1f mAh", fuel_gauge.capacity());
- shell_print(shell, " Remaining Capacity: %.1f mAh", fuel_gauge.remaining_cap());
+ shell_print(shell, " Design Capacity: %.1f mAh", (double)fuel_gauge.design_cap());
+ shell_print(shell, " Full Charge Capacity: %.1f mAh", (double)fuel_gauge.capacity());
+ shell_print(shell, " Remaining Capacity: %.1f mAh", (double)fuel_gauge.remaining_cap());
// Time estimates
float ttf = fuel_gauge.time_to_full();
@@ -744,7 +741,7 @@ static int cmd_battery_info(const struct shell *shell, size_t argc, const char *
struct chrg_state charge_ctrl = battery_controller.read_charging_control();
shell_print(shell, " Charge Control: enabled=%i, current=%.1f mA",
- charge_ctrl.enabled, charge_ctrl.mAh);
+ charge_ctrl.enabled, (double)charge_ctrl.mAh);
battery_controller.enter_high_impedance();
@@ -758,4 +755,4 @@ SHELL_STATIC_SUBCMD_SET_CREATE(battery_cmd,
SHELL_CMD_REGISTER(battery, &battery_cmd, "Power Manager Commands", NULL);
-PowerManager power_manager;
\ No newline at end of file
+PowerManager power_manager;
diff --git a/src/ParseInfo/DefaultSensors.h b/src/ParseInfo/DefaultSensors.h
index 97ce4166..e6daef36 100644
--- a/src/ParseInfo/DefaultSensors.h
+++ b/src/ParseInfo/DefaultSensors.h
@@ -18,8 +18,8 @@
#define MICRO_CHANNEL_COUNT 2
SensorComponent microComponenents[MICRO_CHANNEL_COUNT] = {
+ { .name = "OUTER", .unit = "ADC", .parseType = PARSE_TYPE_UINT16 },
{ .name = "INNER", .unit = "ADC", .parseType = PARSE_TYPE_UINT16 },
- { .name = "Outer", .unit = "ADC", .parseType = PARSE_TYPE_UINT16 },
};
#define MICRO_GROUP_COUNT 1
diff --git a/src/ParseInfo/ParseType.h b/src/ParseInfo/ParseType.h
index 01dc819a..e92361f7 100644
--- a/src/ParseInfo/ParseType.h
+++ b/src/ParseInfo/ParseType.h
@@ -15,7 +15,7 @@ enum ParseType {
PARSE_TYPE_DOUBLE,
};
-const int parseTypeSizes[] = {
+static const int parseTypeSizes[] = {
1, // PARSE_TYPE_INT8
1, // PARSE_TYPE_UINT8
diff --git a/src/ParseInfo/SensorScheme.cpp b/src/ParseInfo/SensorScheme.cpp
index 292dc403..6639af5a 100644
--- a/src/ParseInfo/SensorScheme.cpp
+++ b/src/ParseInfo/SensorScheme.cpp
@@ -54,6 +54,9 @@ static ssize_t write_sensor_request(struct bt_conn *conn,
uint16_t len,
uint16_t offset,
uint8_t flags) {
+ ARG_UNUSED(attr);
+ ARG_UNUSED(offset);
+ ARG_UNUSED(flags);
if (len != sizeof(uint8_t)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
@@ -73,6 +76,7 @@ static ssize_t write_sensor_request(struct bt_conn *conn,
void scheme_ccc_cfg(const struct bt_gatt_attr *attr, uint16_t value) {
+ ARG_UNUSED(attr);
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
}
diff --git a/src/SD_Card/Benchmark/Benchmark.cpp b/src/SD_Card/Benchmark/Benchmark.cpp
index 54a84ed5..48c0750e 100644
--- a/src/SD_Card/Benchmark/Benchmark.cpp
+++ b/src/SD_Card/Benchmark/Benchmark.cpp
@@ -1,7 +1,7 @@
#include "Benchmark.h"
-Benchmark::Benchmark(void (*function)()) {
- this->function = function;
+Benchmark::Benchmark(void (*benchmark_function)()) {
+ this->function = benchmark_function;
}
void Benchmark::start() {
@@ -18,4 +18,4 @@ uint32_t Benchmark::get_time() {
uint32_t Benchmark::get_cycles() {
return this->end_cycles - this->start_cycles;
-}
\ No newline at end of file
+}
diff --git a/src/SD_Card/SDLogger/SDLogger.cpp b/src/SD_Card/SDLogger/SDLogger.cpp
index 335d4b1b..4635c1da 100644
--- a/src/SD_Card/SDLogger/SDLogger.cpp
+++ b/src/SD_Card/SDLogger/SDLogger.cpp
@@ -102,6 +102,7 @@ void sensor_listener_cb(const struct zbus_channel *chan) {
void sd_listener_callback(const struct zbus_channel *chan)
{
+ ARG_UNUSED(chan);
const struct sd_msg * sd_msg_event = (sd_msg *)zbus_chan_const_msg(&sd_card_chan);
if (sdlogger.is_open && sd_msg_event->removed) {
@@ -438,9 +439,9 @@ int SDLogger::write_sensor_data(const void* const* data_blocks, const size_t* le
return 0;
}
-int SDLogger::write_sensor_data(const sensor_data& msg) {
- const size_t data_size = sizeof(sensor_data) - sizeof(msg.data) + msg.size;
- const void* msg_ptr = &msg;
+int SDLogger::write_sensor_data(const sensor_data& sensor_message) {
+ const size_t data_size = sizeof(sensor_data) - sizeof(sensor_message.data) + sensor_message.size;
+ const void* msg_ptr = &sensor_message;
return write_sensor_data(&msg_ptr, &data_size, 1);
}
diff --git a/src/SD_Card/SDLogger/SDLogger.h b/src/SD_Card/SDLogger/SDLogger.h
index 279fdae0..0b037e87 100644
--- a/src/SD_Card/SDLogger/SDLogger.h
+++ b/src/SD_Card/SDLogger/SDLogger.h
@@ -23,11 +23,6 @@ static_assert(BUFFER_SIZE % SD_BLOCK_SIZE == 0, "BUFFER_SIZE must be a multiple
// Singleton pattern
class SDLogger {
-protected:
- // Add static instance pointer for work handler
- //static SDLogger* instance_ptr;
- //friend void sd_work_handler(struct k_work* work);
-
private:
SDCardManager* sd_card = nullptr;
@@ -84,14 +79,23 @@ class SDLogger {
*/
int write_sensor_data(const void* const* data_blocks, const size_t* lengths, size_t block_count);
+ /**
+ * @brief Write a single sensor_data message to the log file
+ * @param msg The sensor_data message to write
+ * @return 0 on success, negative error code on failure
+ */
int write_sensor_data(const sensor_data& msg);
/**
- * @brief End logging and close the current file
- * @return 0 on success, negative error code on failure
- */
+ * @brief End logging and close the current file
+ * @return 0 on success, negative error code on failure
+ */
int end();
+ /**
+ * @brief Check if the logger is currently recording
+ * @return true if active, false otherwise
+ */
bool is_active();
SDLogger(SDLogger const&) = delete;
diff --git a/src/SD_Card/SD_Card_Manager/SD_Card_Manager.cpp b/src/SD_Card/SD_Card_Manager/SD_Card_Manager.cpp
index 9620a2eb..eb2a8ac8 100644
--- a/src/SD_Card/SD_Card_Manager/SD_Card_Manager.cpp
+++ b/src/SD_Card/SD_Card_Manager/SD_Card_Manager.cpp
@@ -35,6 +35,7 @@ bool SDCardManager::sd_inserted() {
}
void SDCardManager::unmount_work_handler(struct k_work *work) {
+ ARG_UNUSED(work);
int ret;
bool _inserted = sdcard_manager.sd_inserted();
@@ -43,6 +44,9 @@ void SDCardManager::unmount_work_handler(struct k_work *work) {
if (!_inserted) {
ret = sdcard_manager.unmount();
+ if (ret != 0) {
+ LOG_ERR("Failed to unmount SD card: %d", ret);
+ }
LOG_INF("SD card unmounted due to card removal.");
ret = zbus_chan_pub(&sd_card_chan, &msg, K_FOREVER);
@@ -55,10 +59,13 @@ void SDCardManager::unmount_work_handler(struct k_work *work) {
K_WORK_DELAYABLE_DEFINE(SDCardManager::unmount_work, SDCardManager::unmount_work_handler);
void SDCardManager::sd_card_state_change_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) {
+ ARG_UNUSED(dev);
+ ARG_UNUSED(cb);
+ ARG_UNUSED(pins);
k_work_reschedule(&sdcard_manager.unmount_work, SD_DEBOUNCE_MS);
}
-SDCardManager::SDCardManager(): path(SD_ROOT_PATH) {
+SDCardManager::SDCardManager(): current_path(SD_ROOT_PATH) {
fs_dir_t_init(&this->dirp);
}
@@ -108,13 +115,11 @@ int SDCardManager::aquire_ls() {
}
int SDCardManager::release_ls() {
- int ret;
-
if (!ls_aquired) return -EALREADY;
- ret = pm_device_runtime_put(ls_1_8);
- ret = pm_device_runtime_put(ls_3_3);
- ret = pm_device_runtime_put(ls_sd);
+ (void)pm_device_runtime_put(ls_1_8);
+ (void)pm_device_runtime_put(ls_3_3);
+ (void)pm_device_runtime_put(ls_sd);
ls_aquired = false;
@@ -125,7 +130,7 @@ void SDCardManager::init() {
int ret;
if (!device_is_ready(sd_state_pin.port)) {
- ret = aquire_ls();
+ (void)aquire_ls();
LOG_ERR("SD state GPIO device not ready\n");
return;
}
@@ -175,7 +180,7 @@ int SDCardManager::mount() {
uint32_t sector_count;
size_t sector_size;
- ret = aquire_ls();
+ (void)aquire_ls();
bool _sd_inserted = sd_inserted();
@@ -239,8 +244,8 @@ int SDCardManager::mount() {
return ret;
}
- LOG_DBG("Root dir: %s", this->path.c_str());
- ret = fs_opendir(&this->dirp, this->path.c_str());
+ LOG_DBG("Root dir: %s", this->current_path.c_str());
+ ret = fs_opendir(&this->dirp, this->current_path.c_str());
k_mutex_unlock(&m_sem_sd_mngr_oper_ongoing);
if (ret) {
release_ls();
@@ -282,7 +287,7 @@ int SDCardManager::cd(std::string path) {
return ret;
}
- std::string abs_path_name = create_path(this->path, path);
+ std::string abs_path_name = create_path(this->current_path, path);
LOG_DBG("abs path name:\t%s", abs_path_name.c_str());
@@ -292,8 +297,8 @@ int SDCardManager::cd(std::string path) {
if (ret) {
LOG_ERR("Open SD card dir failed: %d", ret);
// Try to revert to the previous path if the new one fails
- if (this->path != path) {
- int rret = fs_opendir(&this->dirp, this->path.c_str());
+ if (this->current_path != path) {
+ int rret = fs_opendir(&this->dirp, this->current_path.c_str());
if (rret) {
LOG_ERR("Failed to cd back to previous dir: %d", rret);
}
@@ -302,7 +307,7 @@ int SDCardManager::cd(std::string path) {
return ret;
}
- this->path = abs_path_name;
+ this->current_path = abs_path_name;
k_mutex_unlock(&m_sem_sd_mngr_oper_ongoing);
return 0;
@@ -325,7 +330,7 @@ int SDCardManager::ls(char *buf, size_t *buf_size) {
return -ENODEV;
}
- if (this->path.length() > CONFIG_FS_FATFS_MAX_LFN) {
+ if (this->current_path.length() > CONFIG_FS_FATFS_MAX_LFN) {
LOG_ERR("Path is too long");
k_mutex_unlock(&m_sem_sd_mngr_oper_ongoing);
return -FR_INVALID_NAME;
@@ -348,7 +353,7 @@ int SDCardManager::ls(char *buf, size_t *buf_size) {
&buf[used_buf_size], remaining_buf_size, "[%s]\t%s\n",
entry.type == FS_DIR_ENTRY_DIR ? "DIR " : "FILE", entry.name);
- if (len >= remaining_buf_size) {
+ if (len < 0 || (size_t)len >= remaining_buf_size) {
LOG_ERR("Failed to append to buffer, error: %d", len);
k_mutex_unlock(&m_sem_sd_mngr_oper_ongoing);
return -EINVAL;
@@ -385,7 +390,7 @@ int SDCardManager::mkdir(std::string path) {
return -FR_INVALID_NAME;
}
- std::string abs_path_name = create_path(this->path, path);
+ std::string abs_path_name = create_path(this->current_path, path);
ret = fs_mkdir(abs_path_name.c_str());
if (ret) {
@@ -417,7 +422,7 @@ int SDCardManager::open_file(std::string path, bool write, bool append, bool cre
return -ENAMETOOLONG;
}
- std::string abs_path_name = create_path(this->path, path);
+ std::string abs_path_name = create_path(this->current_path, path);
if (this->tracked_file.is_open) {
LOG_ERR("File is already open");
@@ -446,7 +451,7 @@ int SDCardManager::open_file(std::string path, bool write, bool append, bool cre
}
this->tracked_file.is_open = true;
- this->path = abs_path_name;
+ this->current_path = abs_path_name;
k_mutex_unlock(&m_sem_sd_mngr_oper_ongoing);
return 0;
@@ -478,10 +483,10 @@ int SDCardManager::close_file() {
return ret;
}
- LOG_DBG("File %s closed", this->path.c_str());
- size_t last_slash_pos = this->path.find_last_of("/");
+ LOG_DBG("File %s closed", this->current_path.c_str());
+ size_t last_slash_pos = this->current_path.find_last_of("/");
if (last_slash_pos != std::string::npos) {
- this->path = this->path.substr(0, last_slash_pos);
+ this->current_path = this->current_path.substr(0, last_slash_pos);
}
this->tracked_file.is_open = false;
@@ -639,7 +644,7 @@ int SDCardManager::rm(std::string path) {
return -FR_INVALID_NAME;
}
- std::string abs_path_name = create_path(this->path, path);
+ std::string abs_path_name = create_path(this->current_path, path);
ret = fs_unlink(abs_path_name.c_str());
if (ret) {
@@ -675,4 +680,4 @@ int SDCardManager::sync() {
return 0;
}
-SDCardManager sdcard_manager;
\ No newline at end of file
+SDCardManager sdcard_manager;
diff --git a/src/SD_Card/SD_Card_Manager/SD_Card_Manager.h b/src/SD_Card/SD_Card_Manager/SD_Card_Manager.h
index a83a8534..804cc504 100644
--- a/src/SD_Card/SD_Card_Manager/SD_Card_Manager.h
+++ b/src/SD_Card/SD_Card_Manager/SD_Card_Manager.h
@@ -162,7 +162,7 @@ class SDCardManager {
bool is_mounted() { return this->mounted; }
private:
- std::string path;
+ std::string current_path;
bool mounted = false;
struct fs_dir_t dirp;
@@ -173,14 +173,21 @@ class SDCardManager {
FATFS fat_fs;
struct fs_mount_t mnt_pt = {
+ .node = {},
.type = FS_FATFS,
+ .mnt_point = nullptr,
.fs_data = &fat_fs,
+ .storage_dev = nullptr,
+ .mountp_len = 0,
+ .fs = nullptr,
+ .flags = 0,
};
struct tracked_fs_file_t {
struct fs_file_t filep;
bool is_open;
} tracked_file = {
+ .filep = {},
.is_open = false,
};
@@ -199,4 +206,4 @@ class SDCardManager {
extern SDCardManager sdcard_manager;
-#endif
\ No newline at end of file
+#endif
diff --git a/src/SensorManager/.DS_Store b/src/SensorManager/.DS_Store
deleted file mode 100644
index dd6b9a17..00000000
Binary files a/src/SensorManager/.DS_Store and /dev/null differ
diff --git a/src/SensorManager/BMA580/BMA580_Sensor.cpp b/src/SensorManager/BMA580/BMA580_Sensor.cpp
index 779f69ed..a3e4cc76 100644
--- a/src/SensorManager/BMA580/BMA580_Sensor.cpp
+++ b/src/SensorManager/BMA580/BMA580_Sensor.cpp
@@ -61,14 +61,14 @@ BMA580_dev_inf dev_info = {
BMA5_INTF_RET_TYPE bma5_i2c_read(uint8_t reg_addr, uint8_t *reg_data, uint32_t len, void *intf_ptr)
{
int ret;
- BMA580_dev_inf * dev_info = (BMA580_dev_inf *) intf_ptr;
+ BMA580_dev_inf * device_info = (BMA580_dev_inf *) intf_ptr;
- dev_info->i2c_dev->aquire();
+ device_info->i2c_dev->aquire();
- ret = i2c_burst_read(dev_info->i2c_dev->master, dev_info->addr, reg_addr, reg_data, len);
+ ret = i2c_burst_read(device_info->i2c_dev->master, device_info->addr, reg_addr, reg_data, len);
if (ret) LOG_WRN("I2C read failed: %d\n", ret);
- dev_info->i2c_dev->release();
+ device_info->i2c_dev->release();
return 0;
}
@@ -77,14 +77,14 @@ BMA5_INTF_RET_TYPE bma5_i2c_read(uint8_t reg_addr, uint8_t *reg_data, uint32_t l
*/
BMA5_INTF_RET_TYPE bma5_i2c_write(uint8_t reg_addr, const uint8_t *reg_data, uint32_t len, void *intf_ptr)
{
- BMA580_dev_inf * dev_info = (BMA580_dev_inf *) intf_ptr;
+ BMA580_dev_inf * device_info = (BMA580_dev_inf *) intf_ptr;
- dev_info->i2c_dev->aquire();
+ device_info->i2c_dev->aquire();
- int ret = i2c_burst_write(dev_info->i2c_dev->master, dev_info->addr, reg_addr, reg_data, len);
+ int ret = i2c_burst_write(device_info->i2c_dev->master, device_info->addr, reg_addr, reg_data, len);
if (ret) LOG_WRN("I2C write failed: %d", ret);
- dev_info->i2c_dev->release();
+ device_info->i2c_dev->release();
return 0;
}
@@ -94,6 +94,7 @@ BMA5_INTF_RET_TYPE bma5_i2c_write(uint8_t reg_addr, const uint8_t *reg_data, uin
*/
void bma5_delay_us(uint32_t period, void *intf_ptr)
{
+ ARG_UNUSED(intf_ptr);
k_usleep(period);
}
@@ -160,7 +161,7 @@ int8_t bma5_interface_init(struct bma5_dev *bma5, uint8_t intf, enum bma5_contex
/*!
* @brief This internal API is used to enable accel and interrupt configuration settings.
*/
-int8_t BMA580::get_accel_and_int_settings(struct bma5_dev *dev)
+int8_t BMA580::get_accel_and_int_settings(struct bma5_dev *device)
{
int8_t rslt;
uint8_t n_ints = 1;
@@ -171,10 +172,10 @@ int8_t BMA580::get_accel_and_int_settings(struct bma5_dev *dev)
int_config.int_src = BMA5_INT_1;
/* Get accel configurations */
- rslt = bma5_get_acc_conf_0(&sensor_ctrl, dev);
+ rslt = bma5_get_acc_conf_0(&sensor_ctrl, device);
bma5_check_rslt("bma5_get_acc_conf_0", rslt);
- rslt = bma5_get_acc_conf(&acc_cfg, dev);
+ rslt = bma5_get_acc_conf(&acc_cfg, device);
bma5_check_rslt("bma5_get_acc_conf", rslt);
/* Set accel configurations */
@@ -188,7 +189,7 @@ int8_t BMA580::get_accel_and_int_settings(struct bma5_dev *dev)
acc_cfg.noise_mode = BMA5_NOISE_MODE_LOWER_NOISE;
acc_cfg.acc_drdy_int_auto_clear = BMA5_ACC_DRDY_INT_AUTO_CLEAR_DISABLED;
- rslt = bma5_set_acc_conf(&acc_cfg, dev);
+ rslt = bma5_set_acc_conf(&acc_cfg, device);
bma5_check_rslt("bma5_get_acc_conf", rslt);
/*LOG_DBG("Accel configurations");
@@ -203,7 +204,7 @@ int8_t BMA580::get_accel_and_int_settings(struct bma5_dev *dev)
/* Enable accel */
sensor_ctrl = BMA5_SENSOR_CTRL_ENABLE;
- rslt = bma5_set_acc_conf_0(sensor_ctrl, dev);
+ rslt = bma5_set_acc_conf_0(sensor_ctrl, device);
bma5_check_rslt("bma5_set_acc_conf_0", rslt);
if (rslt == BMA5_OK)
@@ -211,20 +212,20 @@ int8_t BMA580::get_accel_and_int_settings(struct bma5_dev *dev)
LOG_DBG("Accel enabled");
}
- rslt = bma5_get_acc_conf_0(&sensor_ctrl, dev);
+ rslt = bma5_get_acc_conf_0(&sensor_ctrl, device);
bma5_check_rslt("bma5_set_acc_conf_0", rslt);
- rslt = bma5_get_acc_conf(&get_acc_cfg, dev);
+ rslt = bma5_get_acc_conf(&get_acc_cfg, device);
bma5_check_rslt("bma5_get_acc_conf", rslt);
- rslt = bma5_get_int_conf(&int_config, n_ints, dev);
+ rslt = bma5_get_int_conf(&int_config, n_ints, device);
bma5_check_rslt("bma5_get_int_conf", rslt);
int_config.int_conf.int_mode = BMA5_INT1_MODE_LATCHED;
int_config.int_conf.int_od = BMA5_INT1_OD_PUSH_PULL;
int_config.int_conf.int_lvl = BMA5_INT1_LVL_ACTIVE_HIGH;
- rslt = bma5_set_int_conf(&int_config, n_ints, dev);
+ rslt = bma5_set_int_conf(&int_config, n_ints, device);
bma5_check_rslt("bma5_set_int_conf", rslt);
/*LOG_DBG("\nInt Configurations");
@@ -238,24 +239,24 @@ int8_t BMA580::get_accel_and_int_settings(struct bma5_dev *dev)
/*!
* @brief This internal API gets FIFO configurations.
*/
-int8_t BMA580::get_fifo_conf(const struct bma5_fifo_conf *fifo_conf, struct bma5_dev *dev)
+int8_t BMA580::get_fifo_conf(const struct bma5_fifo_conf *fifo_config, struct bma5_dev *device)
{
int8_t rslt;
- struct bma5_fifo_conf read_fifo_conf = { 0 };
+ struct bma5_fifo_conf read_fifo_conf{};
/* Set FIFO configuration.
* NOTE 1: FIFO works only on header mode */
- rslt = bma5_set_fifo_conf(fifo_conf, dev);
+ rslt = bma5_set_fifo_conf(fifo_config, device);
bma5_check_rslt("bma5_set_fifo_conf", rslt);
/* Get FIFO configuration register */
- rslt = bma5_get_fifo_conf(&read_fifo_conf, dev);
+ rslt = bma5_get_fifo_conf(&read_fifo_conf, device);
bma5_check_rslt("bma5_get_fifo_conf", rslt);
return rslt;
}
-int BMA580::init(int odr, int fifo_watermark_level) {
+int BMA580::init(int odr, int watermark_level) {
int8_t rslt;
struct bma580_int_map int_map, get_int_map;
//struct bma5_fifo_conf fifo_conf;
@@ -315,7 +316,7 @@ int BMA580::init(int odr, int fifo_watermark_level) {
/* Update FIFO structure */
fifoframe.data = fifo_data;
- rslt = bma5_set_fifo_wm(fifo_watermark_level, &dev);
+ rslt = bma5_set_fifo_wm(watermark_level, &dev);
bma5_check_rslt("bma5_set_fifo_wm", rslt);
return rslt;
@@ -336,7 +337,7 @@ int BMA580::stop() {
int BMA580::read(bma5_sens_fifo_axes_data_16_bit *fifo_accel_data) {
int8_t rslt = BMA5_OK;
uint8_t n_status = 1;
- struct bma580_int_status_types int_status = { 0 };
+ struct bma580_int_status_types int_status{};
fifoframe.fifo_avail_frames = 0;
@@ -364,4 +365,4 @@ int BMA580::read(bma5_sens_fifo_axes_data_16_bit *fifo_accel_data) {
}
return fifoframe.fifo_avail_frames;
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/BMA580/BMA580_Sensor.h b/src/SensorManager/BMA580/BMA580_Sensor.h
index d8c0574d..1ff93349 100644
--- a/src/SensorManager/BMA580/BMA580_Sensor.h
+++ b/src/SensorManager/BMA580/BMA580_Sensor.h
@@ -164,7 +164,7 @@ class BMA580 {
uint8_t fifo_data[BMA580_FIFO_RAW_DATA_BUFFER_SIZE] = { 0 };
/* Initialize FIFO frame structure */
- struct bma5_fifo_frame fifoframe = { 0 };
+ struct bma5_fifo_frame fifoframe = {};
/* Set FIFO water-mark level */
uint16_t fifo_watermark_level = WATERMARK_LEVEL;
diff --git a/src/SensorManager/BMA580/bma5.c b/src/SensorManager/BMA580/bma5.c
index 62aa8f95..f47a2034 100644
--- a/src/SensorManager/BMA580/bma5.c
+++ b/src/SensorManager/BMA580/bma5.c
@@ -1927,9 +1927,7 @@ static int8_t unpack_accel_sensor_time_16_bit_data(uint8_t frame_header,
if ((((*data_start_index) + BMA5_FIFO_16_BIT_ACCEL_XYZ_DATA) <= fifo->fifo_avail_len) &&
(frame_header != BMA5_FIFO_SENS_TIME_EN_DEDI_FRM))
{
- if (frame_header & BMA5_FIFO_ACC_X_MSK)
- if (frame_header & BMA5_FIFO_ACC_X_MSK)
- {
+ if (frame_header & BMA5_FIFO_ACC_X_MSK) {
/* Accel raw x data */
data_lsb = fifo->data[(*data_start_index)++];
data_msb = fifo->data[(*data_start_index)++];
diff --git a/src/SensorManager/BMP388/Adafruit_BMP3XX.cpp b/src/SensorManager/BMP388/Adafruit_BMP3XX.cpp
index 95748ee2..9a11960f 100644
--- a/src/SensorManager/BMP388/Adafruit_BMP3XX.cpp
+++ b/src/SensorManager/BMP388/Adafruit_BMP3XX.cpp
@@ -112,9 +112,11 @@ bool Adafruit_BMP3XX::_init(void) {
if (rslt != BMP3_OK)
return false;
- rslt = bmp3_init(&the_sensor);
#ifdef BMP3XX_DEBUG
- printk("Init result: %i\n", rslt);
+ const int8_t init_rslt = bmp3_init(&the_sensor);
+ printk("Init result: %i\n", init_rslt);
+#else
+ (void)bmp3_init(&the_sensor);
#endif
rslt = validate_trimming_param(&the_sensor);
@@ -432,7 +434,10 @@ int8_t i2c_write(uint8_t reg_addr, const uint8_t *reg_data, uint32_t len,
return 0;
}
-static void delay_usec(uint32_t us, void *intf_ptr) { k_usleep(us); }
+static void delay_usec(uint32_t us, void *intf_ptr) {
+ ARG_UNUSED(intf_ptr);
+ k_usleep(us);
+}
static int8_t validate_trimming_param(struct bmp3_dev *dev) {
int8_t rslt;
@@ -478,4 +483,4 @@ static int8_t cal_crc(uint8_t seed, uint8_t data) {
}
return (int8_t)seed;
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/BMP388/bmp3.c b/src/SensorManager/BMP388/bmp3.c
index 2b614f3a..1e3b494b 100644
--- a/src/SensorManager/BMP388/bmp3.c
+++ b/src/SensorManager/BMP388/bmp3.c
@@ -1028,17 +1028,17 @@ int8_t bmp3_get_fifo_data(struct bmp3_dev *dev)
/* Get the total no of bytes available in FIFO */
rslt = bmp3_get_fifo_length(&fifo_len, dev);
- /* For sensor time frame */
- if (dev->fifo->settings.time_en == TRUE)
+ if (rslt == BMP3_OK)
{
- fifo_len = fifo_len + 4;
- }
+ /* For sensor time frame */
+ if (dev->fifo->settings.time_en == TRUE)
+ {
+ fifo_len = fifo_len + 4;
+ }
- /* Update the fifo length in the fifo structure */
- dev->fifo->data.byte_count = fifo_len;
+ /* Update the fifo length in the fifo structure */
+ dev->fifo->data.byte_count = fifo_len;
- if (rslt == BMP3_OK)
- {
/* Read the fifo data */
rslt = bmp3_get_regs(BMP3_REG_FIFO_DATA, fifo->data.buffer, fifo_len, dev);
}
@@ -1089,7 +1089,7 @@ int8_t bmp3_extract_fifo_data(struct bmp3_data *data, struct bmp3_dev *dev)
uint8_t header;
uint8_t parsed_frames = 0;
uint8_t t_p_frame;
- struct bmp3_uncomp_data uncomp_data;
+ struct bmp3_uncomp_data uncomp_data = { 0 };
rslt = null_ptr_check(dev);
@@ -2377,18 +2377,18 @@ static double compensate_pressure(const struct bmp3_uncomp_data *uncomp_data, co
double partial_out2;
partial_data1 = quantized_calib_data->par_p6 * quantized_calib_data->t_lin;
- partial_data2 = quantized_calib_data->par_p7 * pow_bmp3(quantized_calib_data->t_lin, 2);
- partial_data3 = quantized_calib_data->par_p8 * pow_bmp3(quantized_calib_data->t_lin, 3);
+ partial_data2 = (double)quantized_calib_data->par_p7 * (double)pow_bmp3(quantized_calib_data->t_lin, 2);
+ partial_data3 = (double)quantized_calib_data->par_p8 * (double)pow_bmp3(quantized_calib_data->t_lin, 3);
partial_out1 = quantized_calib_data->par_p5 + partial_data1 + partial_data2 + partial_data3;
partial_data1 = quantized_calib_data->par_p2 * quantized_calib_data->t_lin;
- partial_data2 = quantized_calib_data->par_p3 * pow_bmp3(quantized_calib_data->t_lin, 2);
- partial_data3 = quantized_calib_data->par_p4 * pow_bmp3(quantized_calib_data->t_lin, 3);
+ partial_data2 = (double)quantized_calib_data->par_p3 * (double)pow_bmp3(quantized_calib_data->t_lin, 2);
+ partial_data3 = (double)quantized_calib_data->par_p4 * (double)pow_bmp3(quantized_calib_data->t_lin, 3);
partial_out2 = uncomp_data->pressure *
(quantized_calib_data->par_p1 + partial_data1 + partial_data2 + partial_data3);
partial_data1 = pow_bmp3((double)uncomp_data->pressure, 2);
partial_data2 = quantized_calib_data->par_p9 + quantized_calib_data->par_p10 * quantized_calib_data->t_lin;
partial_data3 = partial_data1 * partial_data2;
- partial_data4 = partial_data3 + pow_bmp3((double)uncomp_data->pressure, 3) * quantized_calib_data->par_p11;
+ partial_data4 = partial_data3 + (double)pow_bmp3((double)uncomp_data->pressure, 3) * (double)quantized_calib_data->par_p11;
comp_press = partial_out1 + partial_out2 + partial_data4;
return comp_press;
diff --git a/src/SensorManager/BMX160/DFRobot_BMX160.cpp b/src/SensorManager/BMX160/DFRobot_BMX160.cpp
index ace06844..659b38a2 100644
--- a/src/SensorManager/BMX160/DFRobot_BMX160.cpp
+++ b/src/SensorManager/BMX160/DFRobot_BMX160.cpp
@@ -92,15 +92,7 @@ void DFRobot_BMX160::wakeUp(){
bool DFRobot_BMX160::softReset()
{
- int8_t rslt=BMX160_OK;
- if (Obmx160 == NULL){
- rslt = BMX160_E_NULL_PTR;
- }
- rslt = _softReset(Obmx160);
- if (rslt == 0)
- return true;
- else
- return false;
+ return _softReset(Obmx160) == BMX160_OK;
}
int8_t DFRobot_BMX160:: _softReset(sBmx160Dev_t *dev)
diff --git a/src/SensorManager/Baro.cpp b/src/SensorManager/Baro.cpp
index 955b9519..f8340a08 100644
--- a/src/SensorManager/Baro.cpp
+++ b/src/SensorManager/Baro.cpp
@@ -35,6 +35,7 @@ const SampleRateSetting<18> Baro::sample_rates = {
};
void Baro::update_sensor(struct k_work *work) {
+ ARG_UNUSED(work);
int ret;
bmp.performReading();
@@ -51,7 +52,10 @@ void Baro::update_sensor(struct k_work *work) {
msg_baro.data.size = 2 * sizeof(float);
msg_baro.data.time = micros();
- float data[2] = {bmp.temperature, bmp.pressure};
+ float data[2] = {
+ static_cast(bmp.temperature),
+ static_cast(bmp.pressure),
+ };
memcpy(msg_baro.data.data, data, 2 * sizeof(float));
@@ -66,6 +70,7 @@ void Baro::update_sensor(struct k_work *work) {
*/
void Baro::sensor_timer_handler(struct k_timer *dummy)
{
+ ARG_UNUSED(dummy);
k_work_submit_to_queue(&sensor_work_q, &sensor.sensor_work);
};
@@ -93,7 +98,7 @@ bool Baro::init(struct k_msgq * queue) {
void Baro::start(int sample_rate_idx) {
baro_initial_discard = 1;
- k_timeout_t t = K_USEC(1e6 / sample_rates.true_sample_rates[sample_rate_idx]);
+ k_timeout_t t = K_USEC(1000000.0f / sample_rates.true_sample_rates[sample_rate_idx]);
//bmp.set_interrogation_rate(setting.reg_val);
//bmp.start();
@@ -112,4 +117,4 @@ void Baro::stop() {
k_timer_stop(&sensor.sensor_timer);
pm_device_runtime_put(ls_1_8);
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/BoneConduction.cpp b/src/SensorManager/BoneConduction.cpp
index dbbad21c..26d4f648 100644
--- a/src/SensorManager/BoneConduction.cpp
+++ b/src/SensorManager/BoneConduction.cpp
@@ -48,6 +48,7 @@ void BoneConduction::reset() {
}
void BoneConduction::update_sensor(struct k_work *work) {
+ ARG_UNUSED(work);
uint64_t _time_stamp = micros();
BoneConduction::sensor._sample_count += (_time_stamp - BoneConduction::sensor._last_time_stamp) / BoneConduction::sensor.t_sample_us;
@@ -103,18 +104,19 @@ void BoneConduction::update_sensor(struct k_work *work) {
* @brief Submit a k_work on timer expiry.
*/
void BoneConduction::sensor_timer_handler(struct k_timer *dummy) {
+ ARG_UNUSED(dummy);
k_work_submit_to_queue(&sensor_work_q, &sensor.sensor_work);
}
void BoneConduction::start(int sample_rate_idx) {
if (!_active) return;
- t_sample_us = 1e6 / sample_rates.true_sample_rates[sample_rate_idx];
+ t_sample_us = 1000000.0f / sample_rates.true_sample_rates[sample_rate_idx];
k_timeout_t t = K_USEC(t_sample_us);
int word_size = 3 * sizeof(int16_t) + 1;
- _num_samples_buffered = MIN(MAX(1, (int) (CONFIG_SENSOR_LATENCY_MS * 1e3 / t_sample_us)), 512 / word_size - 8); // Buffer size is 512 bytes
+ _num_samples_buffered = MIN(MAX(1, (int) (CONFIG_SENSOR_LATENCY_MS * 1000.0f / t_sample_us)), 512 / word_size - 8); // Buffer size is 512 bytes
bma580.init(sample_rates.reg_vals[sample_rate_idx], _num_samples_buffered * word_size);
bma580.start();
@@ -137,4 +139,4 @@ void BoneConduction::stop() {
bma580.stop();
pm_device_runtime_put(ls_1_8);
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/IMU.cpp b/src/SensorManager/IMU.cpp
index 1f0a59e8..cf7e9e49 100644
--- a/src/SensorManager/IMU.cpp
+++ b/src/SensorManager/IMU.cpp
@@ -25,6 +25,7 @@ const SampleRateSetting<6> IMU::sample_rates = {
};
void IMU::update_sensor(struct k_work *work) {
+ ARG_UNUSED(work);
int ret;
sBmx160SensorData_t magno_data;
@@ -57,6 +58,7 @@ void IMU::update_sensor(struct k_work *work) {
*/
void IMU::sensor_timer_handler(struct k_timer *dummy)
{
+ ARG_UNUSED(dummy);
k_work_submit_to_queue(&sensor_work_q, &sensor.sensor_work);
};
@@ -86,7 +88,7 @@ bool IMU::init(struct k_msgq * queue) {
void IMU::start(int sample_rate_idx) {
if (!_active) return;
- k_timeout_t t = K_USEC(1e6 / sample_rates.true_sample_rates[sample_rate_idx]);
+ k_timeout_t t = K_USEC(1000000.0f / sample_rates.true_sample_rates[sample_rate_idx]);
imu.setAccelODR(sample_rates.reg_vals[sample_rate_idx]);
@@ -107,4 +109,4 @@ void IMU::stop() {
imu.softReset();
pm_device_runtime_put(ls_1_8);
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/MAXM86161/MAXM86161.cpp b/src/SensorManager/MAXM86161/MAXM86161.cpp
index 7a199066..8774d01f 100644
--- a/src/SensorManager/MAXM86161/MAXM86161.cpp
+++ b/src/SensorManager/MAXM86161/MAXM86161.cpp
@@ -146,7 +146,7 @@ int MAXM86161::read(ppg_sample * buffer) {
if (status == 0){
number_of_bytes = num_samples / LED_NUM * LED_NUM * BYTES_PER_CH;
- status = _read_block(REG_FIFO_DATA, number_of_bytes, (uint8_t *) databuffer);
+ (void)_read_block(REG_FIFO_DATA, number_of_bytes, (uint8_t *) databuffer);
for (int i=0; i < num_samples / LED_NUM * LED_NUM; i++) {
int idx = BYTES_PER_CH * i;
@@ -436,7 +436,7 @@ int MAXM86161::_clear_interrupt(void)
int MAXM86161::read_interrupt_state(int &value)
{
int status;
- status = _read_from_reg(REG_IRQ_STATUS2, value);
+ (void)_read_from_reg(REG_IRQ_STATUS2, value);
status = _read_from_reg(REG_IRQ_STATUS1, value);
return status;
}
diff --git a/src/SensorManager/MLX90632/MLX90632.cpp b/src/SensorManager/MLX90632/MLX90632.cpp
index ab98e9de..b794e952 100644
--- a/src/SensorManager/MLX90632/MLX90632.cpp
+++ b/src/SensorManager/MLX90632/MLX90632.cpp
@@ -244,11 +244,9 @@ float MLX90632::getObjectTemp(status& returnError)
double AMB = (sixRAM / 12.0) / VRta * pow(2, 19);
- double sensorTemp = P_O + (AMB - P_R) / P_G + P_T * pow((AMB - P_R), 2);
-
- float S = (float)(lowerRAM + upperRAM) / 2.0;
+ float S = (float)(lowerRAM + upperRAM) / 2.0f;
double VRto = nineRAM + Ka * (sixRAM / 12.0);
- double Sto = (S / 12.0) / VRto * (double)pow(2, 19);
+ double Sto = ((double)S / 12.0) / VRto * pow(2, 19);
double TAdut = (AMB - Eb) / Ea + 25.0;
@@ -270,7 +268,7 @@ float MLX90632::getObjectTemp(status& returnError)
float MLX90632::getObjectTempF()
{
float tempC = getObjectTemp();
- float tempF = tempC * 9.0/5.0 + 32.0;
+ float tempF = tempC * 9.0f / 5.0f + 32.0f;
return(tempF);
}
@@ -324,9 +322,9 @@ float MLX90632::gatherSensorTemp(status &returnError)
double AMB = (sixRAM / 12.0) / VRta * pow(2, 19);
- double sensorTemp = P_O + (AMB - P_R) / P_G + P_T * pow((AMB - P_R), 2);
+ double calculated_sensor_temp = P_O + (AMB - P_R) / P_G + P_T * pow((AMB - P_R), 2);
- return(sensorTemp);
+ return(calculated_sensor_temp);
}
//Returns true if device is busy doing measurement
@@ -348,9 +346,9 @@ bool MLX90632::eepromBusy()
//Returns the cycle_pos from status register. cycle_pos is 0 to 31
uint8_t MLX90632::getCyclePosition()
{
- uint16_t status = getStatus() >> BIT_CYCLE_POS; //Shave off last two bits
- status &= 0x1F; //Get lower 5 bits.
- return (status);
+ uint16_t cycle_status = getStatus() >> BIT_CYCLE_POS; //Shave off last two bits
+ cycle_status &= 0x1F; //Get lower 5 bits.
+ return (cycle_status);
}
//Returns true if new data is available
@@ -447,6 +445,7 @@ uint8_t MLX90632::getMode(status &returnError)
//to something unknown you'll need to use the I2C scanner sketch to detect it again
void MLX90632::writeI2CAddress(uint8_t newAddress)
{
+ ARG_UNUSED(newAddress);
}
@@ -623,4 +622,4 @@ float MLX90632::getSampleRate() {
result = (1 << val1) / 2;
return result;
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/Microphone.cpp b/src/SensorManager/Microphone.cpp
index aaddcdb9..ed1c4f1a 100644
--- a/src/SensorManager/Microphone.cpp
+++ b/src/SensorManager/Microphone.cpp
@@ -56,11 +56,9 @@ bool Microphone::init(struct k_msgq * queue) {
void Microphone::start(int sample_rate_idx) {
//ARG_UNUSED(sample_rate_idx);
- int ret;
-
if (!_active) return;
- LOG_INF("Starting Microphone at %f Hz", sample_rates.sample_rates[sample_rate_idx]);
+ LOG_INF("Starting Microphone at %f Hz", (double)sample_rates.sample_rates[sample_rate_idx]);
record_to_sd(_sd_logging);
audio_datapath_aquire(&fifo_rx);
@@ -85,4 +83,4 @@ void Microphone::stop() {
//audio_datapath_decimator_cleanup();
_running = false;
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/PPG.cpp b/src/SensorManager/PPG.cpp
index 33132b3e..1358fbec 100644
--- a/src/SensorManager/PPG.cpp
+++ b/src/SensorManager/PPG.cpp
@@ -66,6 +66,7 @@ bool PPG::init(struct k_msgq * queue) {
}
void PPG::update_sensor(struct k_work *work) {
+ ARG_UNUSED(work);
int int_status;
int status;
@@ -130,17 +131,18 @@ void PPG::update_sensor(struct k_work *work) {
* @brief Submit a k_work on timer expiry.
*/
void PPG::sensor_timer_handler(struct k_timer *dummy) {
+ ARG_UNUSED(dummy);
k_work_submit_to_queue(&sensor_work_q, &sensor.sensor_work);
}
void PPG::start(int sample_rate_idx) {
if (!_active) return;
- t_sample_us = 1e6 / sample_rates.true_sample_rates[sample_rate_idx];
+ t_sample_us = 1000000.0f / sample_rates.true_sample_rates[sample_rate_idx];
k_timeout_t t = K_USEC(t_sample_us);
- _num_samples_buffered = MIN(MAX(1, (int) (CONFIG_SENSOR_LATENCY_MS * 1e3 / t_sample_us)), FIFO_SIZE / LED_NUM - 2);
+ _num_samples_buffered = MIN(MAX(1, (int) (CONFIG_SENSOR_LATENCY_MS * 1000.0f / t_sample_us)), FIFO_SIZE / LED_NUM - 2);
ppg.set_interrogation_rate(sample_rates.reg_vals[sample_rate_idx]);
ppg.set_watermark(FIFO_SIZE - _num_samples_buffered * LED_NUM);
@@ -165,4 +167,4 @@ void PPG::stop() {
pm_device_runtime_put(ls_1_8);
pm_device_runtime_put(ls_3_3);
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/SensorManager.cpp b/src/SensorManager/SensorManager.cpp
index 1eea6daf..d5d8364c 100644
--- a/src/SensorManager/SensorManager.cpp
+++ b/src/SensorManager/SensorManager.cpp
@@ -71,10 +71,13 @@ static const char sensor_manager_auto_off_token[] = "SensorManager";
static void config_work_handler(struct k_work *work);
void sensor_chan_update(void *p1, void *p2, void *p3) {
+ ARG_UNUSED(p1);
+ ARG_UNUSED(p2);
+ ARG_UNUSED(p3);
int ret;
while (1) {
- ret = k_poll(&sensor_manager_evt, 1, K_FOREVER);
+ (void)k_poll(&sensor_manager_evt, 1, K_FOREVER);
k_msgq_get(&sensor_queue, &msg, K_FOREVER);
@@ -190,6 +193,7 @@ EdgeMlSensor * get_sensor(enum sensor_id id) {
// Worker-Funktion für die Sensor-Konfiguration
static void config_work_handler(struct k_work *work) {
+ ARG_UNUSED(work);
int ret;
struct sensor_config config;
@@ -200,7 +204,7 @@ static void config_work_handler(struct k_work *work) {
float sampleRate = getSampleRateForSensorId(config.sensorId, config.sampleRateIndex);
if (sampleRate <= 0) {
- LOG_ERR("Invalid sample rate %f for sensor %i", sampleRate, config.sensorId);
+ LOG_ERR("Invalid sample rate %f for sensor %i", (double)sampleRate, config.sensorId);
return;
}
@@ -243,8 +247,9 @@ static void config_work_handler(struct k_work *work) {
LOG_INF("Starting SDLogger with recording name prefix: %s", recording_name_prefix);
// Start SDLogger with timestamp-based filename
std::string filename = recording_name_prefix + std::to_string(micros());
- int ret = sdlogger.begin(filename);
- if (ret == 0) state_indicator.set_sd_state(SD_RECORDING);
+ int logger_ret = sdlogger.begin(filename);
+ if (logger_ret == 0) state_indicator.set_sd_state(SD_RECORDING);
+ else LOG_ERR("Failed to start SDLogger, ret: %d", logger_ret);
}
} else if (sd_sensors.find(config.sensorId) != sd_sensors.end()) {
sd_sensors.erase(config.sensorId);
@@ -277,4 +282,4 @@ void config_sensor(struct sensor_config * config) {
//k_work_queue_drain(&sensor_work_q, true);
k_work_submit(&config_work);
//k_work_queue_unplug(&sensor_work_q);
-}
\ No newline at end of file
+}
diff --git a/src/SensorManager/Temp.cpp b/src/SensorManager/Temp.cpp
index 24862dfd..935d4812 100644
--- a/src/SensorManager/Temp.cpp
+++ b/src/SensorManager/Temp.cpp
@@ -49,6 +49,7 @@ bool Temp::init(struct k_msgq * queue) {
}
void Temp::update_sensor(struct k_work *work) {
+ ARG_UNUSED(work);
if (!temp.dataAvailable()) return;
MLX90632::status returnError;
@@ -78,13 +79,14 @@ void Temp::update_sensor(struct k_work *work) {
* @brief Submit a k_work on timer expiry.
*/
void Temp::sensor_timer_handler(struct k_timer *dummy) {
+ ARG_UNUSED(dummy);
k_work_submit_to_queue(&sensor_work_q, &sensor.sensor_work);
}
void Temp::start(int sample_rate_idx) {
if (!_active) return;
- k_timeout_t t = K_USEC(1e6 / sample_rates.true_sample_rates[sample_rate_idx]);
+ k_timeout_t t = K_USEC(1000000.0f / sample_rates.true_sample_rates[sample_rate_idx]);
temp.setSampleRateRegVal(sample_rates.reg_vals[sample_rate_idx]);
temp.continuousMode();
@@ -106,4 +108,4 @@ void Temp::stop() {
pm_device_runtime_put(ls_1_8);
pm_device_runtime_put(ls_3_3);
-}
\ No newline at end of file
+}
diff --git a/src/Wire/TWIM.cpp b/src/Wire/TWIM.cpp
index 057dada0..93959680 100644
--- a/src/Wire/TWIM.cpp
+++ b/src/Wire/TWIM.cpp
@@ -7,8 +7,6 @@ LOG_MODULE_REGISTER(twim, CONFIG_AUDIO_DATAPATH_LOG_LEVEL);
TWIM::TWIM(const struct device * _device) : master(_device) {}
void TWIM::begin() {
- int ret;
-
if (_active) return;
_active = true;
@@ -52,4 +50,4 @@ void TWIM::release() {
TWIM I2C1(DEVICE_DT_GET(DT_NODELABEL(i2c1)));
TWIM I2C2(DEVICE_DT_GET(DT_NODELABEL(i2c2)));
-TWIM I2C3(DEVICE_DT_GET(DT_NODELABEL(i2c3)));
\ No newline at end of file
+TWIM I2C3(DEVICE_DT_GET(DT_NODELABEL(i2c3)));
diff --git a/src/audio/audio_datapath.c b/src/audio/audio_datapath.c
index eac3eb65..ee944999 100644
--- a/src/audio/audio_datapath.c
+++ b/src/audio/audio_datapath.c
@@ -9,6 +9,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -29,7 +31,14 @@
#include "Equalizer.h"
#include "sdlogger_wrapper.h"
#include "decimation_filter.h"
+#include "../SensorManager/SensorManager.h"
#include "arm_math.h"
+#include "hw_codec.h"
+//#include "../drivers/ADAU1860.h"
+
+#include "../SensorManager/SensorManager.h"
+#include "openearable_common.h"
+#include "SensorScheme.h"
#include
LOG_MODULE_REGISTER(audio_datapath, CONFIG_AUDIO_DATAPATH_LOG_LEVEL);
@@ -95,6 +104,10 @@ LOG_MODULE_REGISTER(audio_datapath, CONFIG_AUDIO_DATAPATH_LOG_LEVEL);
/* How often to print under-run warning */
#define UNDERRUN_LOG_INTERVAL_BLKS 5000
+/* Smooth the transition from silence to local buffer playback. */
+#define BUFFER_PLAY_FADE_IN_MS 5U
+#define BUFFER_PLAY_FADE_IN_SAMPLES ((CONFIG_AUDIO_SAMPLE_RATE_HZ * BUFFER_PLAY_FADE_IN_MS) / 1000U)
+
enum drift_comp_state {
DRIFT_STATE_INIT, /* Waiting for data to be received */
DRIFT_STATE_CALIB, /* Calibrate and zero out local delay */
@@ -175,10 +188,8 @@ static struct {
static struct k_msgq * sensor_queue;
-//extern struct audio_data fifo_rx;
-
//K_MSGQ_DEFINE(rx_queue, sizeof(struct audio_data), 16, 4);
-extern struct k_msgq_t encoder_queue;
+extern struct k_msgq encoder_queue;
// Definition eines zbus-Kanals
ZBUS_CHAN_DEFINE(audio_channel, struct audio_data, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0));
@@ -190,17 +201,44 @@ static k_tid_t data_thread_id;
bool _record_to_sd = false;
+// Buffer recording variables
+static bool _record_to_buffer = false;
+static int16_t *_record_buffer = NULL;
+static int _record_num_samples = 0;
+static int _record_current_index = 0;
+static bool _record_left = false;
+static bool _record_right = false;
+static void (*_record_callback)(void) = NULL;
+
int _count = 0;
+static int16_t *buffer_play_data = NULL;
+static uint32_t buffer_play_pos;
+static uint32_t buffer_play_fade_pos;
+static uint32_t buffer_play_num_samples;
+static float buffer_play_amplitude;
+static bool buffer_play_loop;
+static void (*buffer_play_callback)(void) = NULL;
+
extern struct k_poll_signal encoder_sig;
extern struct k_poll_event logger_sig;
-/* Output buffer sized for the largest processed output: decimation factor 2. */
-static int16_t decimated_audio[BLOCK_SIZE_BYTES / sizeof(int16_t) / 2];
+/*
+ * Decimation output for one interleaved stereo audio block.
+ *
+ * The decimator supports factors down to 1, so its worst-case output contains
+ * every input sample. Seal check currently uses factor 3 (48 kHz -> 16 kHz);
+ * sizing this buffer for the old fixed factor 4 overflowed it on every block.
+ */
+static int16_t decimated_audio[BLOCK_SIZE_BYTES / sizeof(int16_t)];
// Funktion für den neuen Thread
static void data_thread(void *arg1, void *arg2, void *arg3)
{
+ ARG_UNUSED(arg1);
+ ARG_UNUSED(arg2);
+ ARG_UNUSED(arg3);
+
//struct audio_data audio_item;
void *tmp_pcm_raw_data[CONFIG_FIFO_FRAME_SPLIT_NUM];
//char pcm_raw_data[FRAME_SIZE_BYTES];
@@ -224,9 +262,7 @@ static void data_thread(void *arg1, void *arg2, void *arg3)
data_fifo_block_free(ctrl_blk.in.fifo, tmp_pcm_raw_data[i]);
- unsigned int logger_signaled;
-
- if (_record_to_sd) {
+ if (_record_to_sd || _record_to_buffer) {
/* Decimate audio data from 48kHz to the desired sampling rate */
int16_t *audio_block = (int16_t *)(audio_item.data + (i * BLOCK_SIZE_BYTES));
uint32_t num_frames = BLOCK_SIZE_BYTES / sizeof(int16_t) / 2; /* stereo frames */
@@ -238,6 +274,48 @@ static void data_thread(void *arg1, void *arg2, void *arg3)
continue;
}
+ // Generic buffer recording
+ if (_record_to_buffer && _record_buffer != NULL) {
+ for(int frame_index = 0; frame_index < decimated_frames; frame_index++) {
+ _record_current_index++;
+
+ // Skip samples during initial drop period
+ if (_record_current_index <= 0) {
+ continue;
+ }
+
+ // Calculate actual buffer index (after initial drop)
+ int buffer_index = _record_current_index - 1;
+
+ if (buffer_index < _record_num_samples) {
+ if (_record_left && _record_right) {
+ // Stereo recording - store both channels
+ if (buffer_index * 2 + 1 < _record_num_samples) {
+ _record_buffer[buffer_index * 2] = decimated_audio[2 * frame_index]; // Left
+ _record_buffer[buffer_index * 2 + 1] = decimated_audio[2 * frame_index + 1]; // Right
+ }
+ } else if (_record_left) {
+ // Left channel only
+ _record_buffer[buffer_index] = decimated_audio[2 * frame_index];
+ } else if (_record_right) {
+ // Right channel only
+ _record_buffer[buffer_index] = decimated_audio[2 * frame_index + 1];
+ }
+
+ }
+
+ // Check if recording is complete
+ if (buffer_index >= _record_num_samples ||
+ (_record_left && _record_right && buffer_index * 2 >= _record_num_samples)) {
+ _record_to_buffer = false;
+ if (_record_callback) {
+ _record_callback();
+ }
+ break;
+ }
+ }
+ }
+
struct sensor_msg audio_msg;
audio_msg.sd = true;
@@ -253,17 +331,17 @@ static void data_thread(void *arg1, void *arg2, void *arg3)
audio_msg.data.size
};
- void *data_ptrs[2] = {
+ const void *data_ptrs[2] = {
&audio_msg.data,
decimated_audio
};
- if (decimated_frames == num_frames) {
+ if ((uint32_t)decimated_frames == num_frames) {
data_ptrs[1] = audio_block;
}
if (decimated_frames > 0) {
- sdlogger_write_data(&data_ptrs, data_size, 2);
+ sdlogger_write_data(data_ptrs, data_size, 2);
}
}
@@ -293,12 +371,41 @@ void record_to_sd(bool active) {
}
void audio_datapath_stop_recording(void) {
+ // Stop buffer recording
+ _record_to_buffer = false;
+ _record_buffer = NULL;
+ _record_callback = NULL;
+
// Stop SD recording
_record_to_sd = false;
LOG_DBG("Audio recording stopped safely");
}
+void record_to_buffer_stop(void) {
+ _record_to_buffer = false;
+ _record_buffer = NULL;
+ _record_callback = NULL;
+ LOG_DBG("Buffer recording stopped");
+}
+
+void record_to_buffer(int16_t *buffer, int num_samples, int initial_drop, bool left, bool right, void (*callback)(void)) {
+ if (buffer == NULL || num_samples <= 0) {
+ LOG_ERR("Invalid buffer recording parameters");
+ return;
+ }
+
+ _record_buffer = buffer;
+ _record_num_samples = num_samples;
+ _record_current_index = -initial_drop;
+ _record_left = left;
+ _record_right = right;
+ _record_callback = callback;
+ _record_to_buffer = true;
+
+ LOG_INF("Started buffer recording: %d samples, initial_drop=%d, left=%d, right=%d", num_samples, initial_drop, left, right);
+}
+
// Funktion, um den neuen Thread zu starten
void start_data_thread(void)
{
@@ -330,6 +437,29 @@ static bool tone_active;
static uint16_t test_tone_buf[CONFIG_AUDIO_SAMPLE_RATE_HZ / 100];
static size_t test_tone_size;
+struct auxiliary_audio_state {
+ bool suspended;
+ bool record_to_sd;
+ bool record_to_buffer;
+ int16_t *record_buffer;
+ int record_num_samples;
+ int record_current_index;
+ bool record_left;
+ bool record_right;
+ void (*record_callback)(void);
+ bool tone_active;
+ uint32_t tone_remaining_ms;
+ int16_t *buffer_data;
+ uint32_t buffer_pos;
+ uint32_t buffer_fade_pos;
+ int buffer_num_samples;
+ float buffer_amplitude;
+ bool buffer_loop;
+ void (*buffer_callback)(void);
+};
+
+static struct auxiliary_audio_state auxiliary_audio_state;
+
/**
* @brief Calculate error between sdu_ref and frame_start_ts_us.
*
@@ -494,8 +624,6 @@ static void audio_datapath_drift_compensation(uint32_t frame_start_ts_us)
static void pres_comp_state_set(enum pres_comp_state new_state)
{
- int ret;
-
if (new_state == ctrl_blk.pres_comp.state) {
return;
}
@@ -505,6 +633,8 @@ static void pres_comp_state_set(enum pres_comp_state new_state)
LOG_INF("Pres comp state: %s", pres_comp_state_names[new_state]);
#if CONFIG_BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP
+ int ret;
+
if (new_state == PRES_STATE_LOCKED) {
ret = led_on(LED_APP_2_GREEN);
} else {
@@ -649,8 +779,20 @@ static void audio_datapath_presentation_compensation(uint32_t recv_frame_ts_us,
static void tone_stop_worker(struct k_work *work)
{
+ ARG_UNUSED(work);
+
tone_active = false;
memset(test_tone_buf, 0, sizeof(test_tone_buf));
+
+ LOG_INF("Tone playback stopped");
+
+ // Call buffer playback callback if set
+ if (buffer_play_callback) {
+ buffer_play_callback();
+ buffer_play_callback = NULL;
+ }
+ buffer_play_data = NULL;
+
LOG_DBG("Tone stopped");
struct sensor_config mic = {ID_MICRO, 0, 0};
@@ -661,11 +803,92 @@ K_WORK_DEFINE(tone_stop_work, tone_stop_worker);
static void tone_stop_timer_handler(struct k_timer *dummy)
{
+ ARG_UNUSED(dummy);
+
k_work_submit(&tone_stop_work);
};
K_TIMER_DEFINE(tone_stop_timer, tone_stop_timer_handler, NULL);
+int audio_datapath_auxiliary_suspend(void)
+{
+ if (auxiliary_audio_state.suspended) {
+ return -EBUSY;
+ }
+
+ auxiliary_audio_state = (struct auxiliary_audio_state) {
+ .suspended = true,
+ .record_to_sd = _record_to_sd,
+ .record_to_buffer = _record_to_buffer,
+ .record_buffer = _record_buffer,
+ .record_num_samples = _record_num_samples,
+ .record_current_index = _record_current_index,
+ .record_left = _record_left,
+ .record_right = _record_right,
+ .record_callback = _record_callback,
+ .tone_active = tone_active,
+ .tone_remaining_ms = k_timer_remaining_get(&tone_stop_timer),
+ .buffer_data = buffer_play_data,
+ .buffer_pos = buffer_play_pos,
+ .buffer_fade_pos = buffer_play_fade_pos,
+ .buffer_num_samples = buffer_play_num_samples,
+ .buffer_amplitude = buffer_play_amplitude,
+ .buffer_loop = buffer_play_loop,
+ .buffer_callback = buffer_play_callback,
+ };
+
+ k_timer_stop(&tone_stop_timer);
+ (void)k_work_cancel(&tone_stop_work);
+ _record_to_sd = false;
+ _record_to_buffer = false;
+ _record_buffer = NULL;
+ _record_callback = NULL;
+ tone_active = false;
+ buffer_play_data = NULL;
+ buffer_play_callback = NULL;
+
+ LOG_INF("Auxiliary audio suspended: tone=%d buffer=%d buffer_recording=%d sd_recording=%d",
+ auxiliary_audio_state.tone_active, auxiliary_audio_state.buffer_data != NULL,
+ auxiliary_audio_state.record_to_buffer, auxiliary_audio_state.record_to_sd);
+ return 0;
+}
+
+int audio_datapath_auxiliary_resume(void)
+{
+ if (!auxiliary_audio_state.suspended) {
+ return -EALREADY;
+ }
+
+ /* A completion queued by measurement playback must not clear restored state. */
+ (void)k_work_cancel(&tone_stop_work);
+ _record_to_sd = auxiliary_audio_state.record_to_sd;
+ _record_to_buffer = auxiliary_audio_state.record_to_buffer;
+ _record_buffer = auxiliary_audio_state.record_buffer;
+ _record_num_samples = auxiliary_audio_state.record_num_samples;
+ _record_current_index = auxiliary_audio_state.record_current_index;
+ _record_left = auxiliary_audio_state.record_left;
+ _record_right = auxiliary_audio_state.record_right;
+ _record_callback = auxiliary_audio_state.record_callback;
+ tone_active = auxiliary_audio_state.tone_active;
+ buffer_play_data = auxiliary_audio_state.buffer_data;
+ buffer_play_pos = auxiliary_audio_state.buffer_pos;
+ buffer_play_fade_pos = auxiliary_audio_state.buffer_fade_pos;
+ buffer_play_num_samples = auxiliary_audio_state.buffer_num_samples;
+ buffer_play_amplitude = auxiliary_audio_state.buffer_amplitude;
+ buffer_play_loop = auxiliary_audio_state.buffer_loop;
+ buffer_play_callback = auxiliary_audio_state.buffer_callback;
+
+ if (tone_active && auxiliary_audio_state.tone_remaining_ms > 0U) {
+ k_timer_start(&tone_stop_timer, K_MSEC(auxiliary_audio_state.tone_remaining_ms),
+ K_NO_WAIT);
+ }
+
+ LOG_INF("Auxiliary audio resumed: tone=%d buffer=%d buffer_recording=%d sd_recording=%d",
+ tone_active, buffer_play_data != NULL, _record_to_buffer, _record_to_sd);
+ memset(&auxiliary_audio_state, 0, sizeof(auxiliary_audio_state));
+ return 0;
+}
+
int audio_datapath_tone_play(uint16_t freq, uint16_t dur_ms, float amplitude)
{
int ret;
@@ -695,25 +918,100 @@ int audio_datapath_tone_play(uint16_t freq, uint16_t dur_ms, float amplitude)
return 0;
}
+int audio_datapath_buffer_play(int16_t *buffer, int num_samples, bool loop, float amplitude, void (*callback)(void))
+{
+ if (buffer_play_data != NULL) {
+ return -EBUSY;
+ }
+
+ if (!IS_ENABLED(CONFIG_AUDIO_TEST_TONE)) {
+ LOG_WRN("Test tone disabled");
+ return -ENOTSUP;
+ }
+
+ if (buffer == NULL || num_samples <= 0) {
+ LOG_ERR("Invalid buffer play parameters");
+ return -EINVAL;
+ }
+
+ buffer_play_data = buffer;
+ buffer_play_pos = 0;
+ buffer_play_fade_pos = 0;
+ buffer_play_num_samples = (uint32_t)num_samples;
+ buffer_play_amplitude = amplitude;
+ buffer_play_loop = loop;
+ buffer_play_callback = callback;
+
+ LOG_DBG("Buffer playback started: %d samples, loop=%d, amplitude=%.2f", num_samples, loop, (double)amplitude);
+ return 0;
+}
+
void audio_datapath_tone_stop(void)
{
k_timer_stop(&tone_stop_timer);
k_work_submit(&tone_stop_work);
}
+void audio_datapath_buffer_stop(void)
+{
+ k_timer_stop(&tone_stop_timer);
+ buffer_play_data = NULL;
+ if (buffer_play_callback) {
+ buffer_play_callback();
+ buffer_play_callback = NULL;
+ }
+ LOG_DBG("Buffer playback stopped");
+}
+
static void tone_mix(uint8_t *tx_buf)
{
int ret;
- int8_t tone_buf_continuous[BLK_MONO_SIZE_OCTETS];
- static uint32_t finite_pos;
- ret = contin_array_create(tone_buf_continuous, BLK_MONO_SIZE_OCTETS, test_tone_buf,
- test_tone_size, &finite_pos);
- ERR_CHK(ret);
+ if (tone_active) {
+ int8_t tone_buf_continuous[BLK_MONO_SIZE_OCTETS];
+ static uint32_t finite_pos;
+
+ ret = contin_array_create(tone_buf_continuous, BLK_MONO_SIZE_OCTETS, test_tone_buf,
+ test_tone_size, &finite_pos);
+ ERR_CHK(ret);
+
+ ret = pcm_mix(tx_buf, BLK_STEREO_SIZE_OCTETS, tone_buf_continuous, BLK_MONO_SIZE_OCTETS,
+ B_MONO_INTO_A_STEREO_L);
+ ERR_CHK(ret);
+ } else if (buffer_play_data != NULL) {
+ int8_t buffer_play_buf[BLK_MONO_SIZE_OCTETS];
+ int samples_per_block = BLK_MONO_SIZE_OCTETS / sizeof(int16_t);
+
+ /* Copy buffer samples to playback buffer with amplitude scaling */
+ for (int i = 0; i < samples_per_block; i++) {
+ float gain = buffer_play_amplitude;
+
+ if (buffer_play_pos >= buffer_play_num_samples) {
+ if (buffer_play_loop) {
+ buffer_play_pos = 0; /* Loop the buffer */
+ } else {
+ /* Stop after one complete playback */
+ k_work_submit(&tone_stop_work);
+ memset(&buffer_play_buf[i * 2], 0, (samples_per_block - i) * 2);
+ break;
+ }
+ }
- ret = pcm_mix(tx_buf, BLK_STEREO_SIZE_OCTETS, tone_buf_continuous, BLK_MONO_SIZE_OCTETS,
- B_MONO_INTO_A_STEREO_L);
- ERR_CHK(ret);
+ if (buffer_play_fade_pos < BUFFER_PLAY_FADE_IN_SAMPLES) {
+ gain *= (float)buffer_play_fade_pos / BUFFER_PLAY_FADE_IN_SAMPLES;
+ buffer_play_fade_pos++;
+ }
+
+ int16_t sample = (int16_t)(buffer_play_data[buffer_play_pos] * gain);
+ buffer_play_buf[i * 2] = sample & 0xFF;
+ buffer_play_buf[i * 2 + 1] = (sample >> 8) & 0xFF;
+ buffer_play_pos++;
+ }
+
+ ret = pcm_mix(tx_buf, BLK_STEREO_SIZE_OCTETS, buffer_play_buf, BLK_MONO_SIZE_OCTETS,
+ B_MONO_INTO_A_STEREO_L);
+ ERR_CHK(ret);
+ }
}
/* Alternate-buffers used when there is no active audio stream.
@@ -774,6 +1072,8 @@ static void alt_buffer_free_both(void)
}
__attribute__((weak)) void bt_mgmt_report_audio_underrun(uint32_t count) {
+ ARG_UNUSED(count);
+
LOG_ERR("Audio underrun reported to bt_mgmt");
}
@@ -793,6 +1093,7 @@ static void audio_datapath_i2s_blk_complete(uint32_t frame_start_ts_us, uint32_t
{
int ret;
static bool underrun_condition;
+ static uint32_t released_tx_reuse_count;
alt_buffer_free(tx_buf_released);
@@ -838,12 +1139,24 @@ static void audio_datapath_i2s_blk_complete(uint32_t frame_start_ts_us, uint32_t
* use alternative buffers
*/
ret = alt_buffer_get((void **)&tx_buf);
- ERR_CHK(ret);
+ if (ret) {
+ released_tx_reuse_count++;
+ if (released_tx_reuse_count == 1U ||
+ (released_tx_reuse_count % UNDERRUN_LOG_INTERVAL_BLKS) == 0U) {
+ LOG_WRN("No alternative I2S TX buffer available; "
+ "reusing released buffer as silence, total: %u",
+ (unsigned int)released_tx_reuse_count);
+ }
+ /* I2S no longer owns this buffer; recycle it as silence
+ * instead of leaving tx_buf NULL.
+ */
+ tx_buf = (uint8_t *)tx_buf_released;
+ }
memset(tx_buf, 0, BLK_STEREO_SIZE_OCTETS);
}
- if (tone_active) {
+ if (tone_active || buffer_play_data != NULL) {
tone_mix(tx_buf);
}
}
@@ -1252,7 +1565,7 @@ int audio_datapath_aquire(struct data_fifo *fifo_rx) {
return ret;
}
-int audio_datapath_release() {
+int audio_datapath_release(void) {
int ret = 0;
_count --;
diff --git a/src/audio/audio_datapath.h b/src/audio/audio_datapath.h
index 6f758fad..211f7be1 100644
--- a/src/audio/audio_datapath.h
+++ b/src/audio/audio_datapath.h
@@ -35,6 +35,40 @@ int audio_datapath_tone_play(uint16_t freq, uint16_t dur_ms, float amplitude);
*/
void audio_datapath_tone_stop(void);
+
+
+
+
+/**
+ * @brief Plays a buffer over I2S TX stream
+ *
+ * @param buffer Pointer to audio buffer to play
+ * @param num_samples Number of samples in buffer
+ * @param loop Whether to loop the buffer continuously
+ * @param amplitude Playback amplitude [0, 1]
+ * @param callback Callback function called when playback completes (optional)
+ *
+ * @return 0 if successful, error otherwise
+ */
+int audio_datapath_buffer_play(int16_t *buffer, int num_samples, bool loop, float amplitude, void (*callback)(void));
+
+/**
+ * @brief Stops buffer playback
+ */
+void audio_datapath_buffer_stop(void);
+
+/**
+ * @brief Records audio data to a buffer
+ *
+ * @param buffer Pointer to buffer where audio will be stored
+ * @param num_samples Number of samples to record
+ * @param initial_drop Number of initial samples to drop before recording
+ * @param left Whether to record left channel
+ * @param right Whether to record right channel
+ * @param callback Callback function called when recording completes (optional)
+ */
+void record_to_buffer(int16_t *buffer, int num_samples, int initial_drop, bool left, bool right, void (*callback)(void));
+
/**
* @brief Stops buffer recording safely
*/
@@ -45,6 +79,24 @@ void record_to_buffer_stop(void);
*/
void audio_datapath_stop_recording(void);
+/**
+ * @brief Suspend local playback and recording without losing their state.
+ *
+ * The datapath must be stopped before this function is called. A matching call
+ * to audio_datapath_auxiliary_resume() restores playback positions, callbacks,
+ * and recording configuration.
+ *
+ * @return 0 on success, or -EBUSY if an auxiliary suspension is already active.
+ */
+int audio_datapath_auxiliary_suspend(void);
+
+/**
+ * @brief Restore local playback and recording after an auxiliary suspension.
+ *
+ * @return 0 on success, or -EALREADY if no suspension is active.
+ */
+int audio_datapath_auxiliary_resume(void);
+
/**
* @brief Set the presentation delay
*
@@ -109,7 +161,7 @@ void record_to_sd(bool active);
void set_sensor_queue(struct k_msgq *queue);
int audio_datapath_aquire(struct data_fifo *fifo_rx);
-int audio_datapath_release();
+int audio_datapath_release(void);
//void set_ring_buffer(struct ring_buf *ring_buf);
diff --git a/src/audio/audio_datapath_decimator.cpp b/src/audio/audio_datapath_decimator.cpp
index 73c9e43d..5650f406 100644
--- a/src/audio/audio_datapath_decimator.cpp
+++ b/src/audio/audio_datapath_decimator.cpp
@@ -10,24 +10,24 @@ LOG_MODULE_DECLARE(audio_datapath);
#ifdef __cplusplus
-/* CascadedDecimator instance for direct C++ usage */
-static CascadedDecimator* g_audio_decimator = nullptr;
+/* Fixed-capacity storage avoids C++ allocation failure in the audio work queue. */
+static CascadedDecimator g_audio_decimator;
+static bool g_audio_decimator_initialized;
/* Mutex to protect access to the decimator during cleanup */
K_MUTEX_DEFINE(decimator_mutex);
-/* Flag to indicate decimator is being used - provides fast-path check */
-static volatile bool g_decimator_in_use = false;
-
extern "C" {
/**
* @brief Reset the audio decimator filter state
*/
void audio_datapath_decimator_reset(void) {
- if (g_audio_decimator) {
- g_audio_decimator->reset();
+ k_mutex_lock(&decimator_mutex, K_FOREVER);
+ if (g_audio_decimator_initialized) {
+ g_audio_decimator.reset();
LOG_DBG("CascadedDecimator state reset");
}
+ k_mutex_unlock(&decimator_mutex);
}
/**
@@ -35,7 +35,10 @@ void audio_datapath_decimator_reset(void) {
* @return Current total decimation factor, or 0 if not initialized
*/
uint8_t audio_datapath_decimator_get_factor(void) {
- return g_audio_decimator ? g_audio_decimator->getTotalFactor() : 0;
+ k_mutex_lock(&decimator_mutex, K_FOREVER);
+ uint8_t factor = g_audio_decimator_initialized ? g_audio_decimator.getTotalFactor() : 0;
+ k_mutex_unlock(&decimator_mutex);
+ return factor;
}
/**
@@ -44,30 +47,27 @@ uint8_t audio_datapath_decimator_get_factor(void) {
* @return 0 on success, negative on error
*/
int audio_datapath_decimator_init(uint8_t factor) {
- /* Lock mutex to ensure no concurrent access during init */
k_mutex_lock(&decimator_mutex, K_FOREVER);
-
- if (g_audio_decimator) {
- delete g_audio_decimator;
- g_audio_decimator = nullptr;
- }
-
- g_audio_decimator = new CascadedDecimator(factor);
- if (!g_audio_decimator) {
- LOG_ERR("Failed to create CascadedDecimator with factor %d", factor);
+
+ g_audio_decimator_initialized = false;
+ g_audio_decimator.cleanup();
+
+ int ret = g_audio_decimator.configure(factor);
+ if (ret != 0) {
+ LOG_ERR("Failed to configure CascadedDecimator: %d", ret);
k_mutex_unlock(&decimator_mutex);
- return -ENOMEM;
+ return ret;
}
-
- int ret = g_audio_decimator->init();
+
+ ret = g_audio_decimator.init();
if (ret != 0) {
LOG_ERR("Failed to initialize CascadedDecimator: %d", ret);
- delete g_audio_decimator;
- g_audio_decimator = nullptr;
+ g_audio_decimator.cleanup();
k_mutex_unlock(&decimator_mutex);
return ret;
}
-
+
+ g_audio_decimator_initialized = true;
LOG_DBG("CascadedDecimator (%dx) initialized successfully", factor);
k_mutex_unlock(&decimator_mutex);
return 0;
@@ -88,16 +88,14 @@ int audio_datapath_decimator_process(const int16_t* input, int16_t* output, uint
return 0;
}
- if (!g_audio_decimator) {
+ if (!g_audio_decimator_initialized) {
k_mutex_unlock(&decimator_mutex);
LOG_WRN("CascadedDecimator not available, returning 0 frames");
return 0;
}
-
- g_decimator_in_use = true;
- int result = g_audio_decimator->process(input, output, num_frames);
- g_decimator_in_use = false;
-
+
+ int result = g_audio_decimator.process(input, output, num_frames);
+
k_mutex_unlock(&decimator_mutex);
return result;
}
@@ -106,23 +104,16 @@ int audio_datapath_decimator_process(const int16_t* input, int16_t* output, uint
* @brief Cleanup the audio decimator
*/
void audio_datapath_decimator_cleanup(void) {
- /* Acquire mutex to ensure no processing is happening */
k_mutex_lock(&decimator_mutex, K_FOREVER);
-
- if (g_audio_decimator) {
- /* Wait briefly if decimator was recently in use (safety margin) */
- if (g_decimator_in_use) {
- LOG_WRN("Decimator still in use, waiting...");
- k_sleep(K_MSEC(5));
- }
-
+
+ if (g_audio_decimator_initialized) {
LOG_DBG("Cleaning up CascadedDecimator");
- delete g_audio_decimator;
- g_audio_decimator = nullptr;
+ g_audio_decimator.cleanup();
+ g_audio_decimator_initialized = false;
}
-
+
k_mutex_unlock(&decimator_mutex);
}
};
-#endif
\ No newline at end of file
+#endif
diff --git a/src/audio/audio_system.c b/src/audio/audio_system.c
index 493ee96f..6ae33b1f 100644
--- a/src/audio/audio_system.c
+++ b/src/audio/audio_system.c
@@ -47,6 +47,10 @@ static K_SEM_DEFINE(sem_encoder_start, 0, 1);
static struct k_thread encoder_thread_data;
static k_tid_t encoder_thread_id;
static atomic_t encoder_started;
+static bool audio_system_suspended;
+static bool audio_system_resume_requested;
+static bool encoder_resume_requested;
+K_MUTEX_DEFINE(audio_system_state_mutex);
struct k_poll_signal encoder_sig;
@@ -118,6 +122,10 @@ static void audio_headset_configure(void)
static void encoder_thread(void *arg1, void *arg2, void *arg3)
{
+ ARG_UNUSED(arg1);
+ ARG_UNUSED(arg2);
+ ARG_UNUSED(arg3);
+
int ret;
uint32_t blocks_alloced_num;
uint32_t blocks_locked_num;
@@ -131,10 +139,11 @@ static void encoder_thread(void *arg1, void *arg2, void *arg3)
static uint8_t *encoded_data;
//static size_t pcm_block_size;
static uint32_t test_tone_finite_pos;
+ static bool encode_failed;
while (1) {
/* Don't start encoding until the stream needing it has started */
- ret = k_poll(&encoder_evt, 1, K_FOREVER);
+ (void)k_poll(&encoder_evt, 1, K_FOREVER);
/* Get PCM data from I2S */
/* Since one audio frame is divided into a number of
@@ -182,8 +191,18 @@ static void encoder_thread(void *arg1, void *arg2, void *arg3)
ret = sw_codec_encode(pcm_raw_data, FRAME_SIZE_BYTES, &encoded_data,
&encoded_data_size);
+ if (ret) {
+ if (!encode_failed) {
+ LOG_WRN("Audio encode failed; dropping frames until recovery: %d", ret);
+ }
+ encode_failed = true;
+ continue;
+ }
- ERR_CHK_MSG(ret, "Encode failed");
+ if (encode_failed) {
+ LOG_INF("Audio encoder recovered");
+ encode_failed = false;
+ }
}
/* Print block usage */
@@ -206,7 +225,7 @@ static void encoder_thread(void *arg1, void *arg2, void *arg3)
}
}
-void audio_system_encoder_start(void)
+static void audio_system_encoder_start_internal(void)
{
if (!sw_codec_cfg.initialized || !sw_codec_cfg.encoder.enabled || !sw_codec_is_initialized()) {
LOG_WRN("Encoder start ignored because codec is not initialized");
@@ -225,7 +244,7 @@ void audio_system_encoder_start(void)
}*/
}
-void audio_system_encoder_stop(void)
+static void audio_system_encoder_stop_internal(void)
{
atomic_clear(&encoder_started);
k_poll_signal_reset(&encoder_sig);
@@ -238,6 +257,28 @@ bool audio_system_encoder_is_started(void)
sw_codec_cfg.encoder.enabled && sw_codec_is_initialized();
}
+void audio_system_encoder_start(void)
+{
+ k_mutex_lock(&audio_system_state_mutex, K_FOREVER);
+ if (audio_system_suspended) {
+ encoder_resume_requested = true;
+ } else {
+ audio_system_encoder_start_internal();
+ }
+ k_mutex_unlock(&audio_system_state_mutex);
+}
+
+void audio_system_encoder_stop(void)
+{
+ k_mutex_lock(&audio_system_state_mutex, K_FOREVER);
+ if (audio_system_suspended) {
+ encoder_resume_requested = false;
+ } else {
+ audio_system_encoder_stop_internal();
+ }
+ k_mutex_unlock(&audio_system_state_mutex);
+}
+
int audio_system_encode_test_tone_set(uint32_t freq)
{
int ret;
@@ -406,9 +447,12 @@ int audio_system_decode(void const *const encoded_data, size_t encoded_data_size
return 0;
}
-/**@brief Initializes the FIFOs, the codec, and starts the I2S
+/**
+ * @brief Initialize the FIFOs and codecs, then start the audio datapath.
+ *
+ * @return 0 on success, or a negative error from the failed subsystem.
*/
-void audio_system_start(void)
+static int audio_system_start_internal(void)
{
int ret;
@@ -418,23 +462,32 @@ void audio_system_start(void)
audio_gateway_configure();
} else {
LOG_ERR("Invalid CONFIG_AUDIO_DEV: %d", CONFIG_AUDIO_DEV);
- ERR_CHK(-EINVAL);
+ return -EINVAL;
}
if (!fifo_tx.initialized) {
ret = data_fifo_init(&fifo_tx);
- ERR_CHK_MSG(ret, "Failed to set up tx FIFO");
+ if (ret) {
+ LOG_ERR("Failed to set up tx FIFO: %d", ret);
+ return ret;
+ }
}
if (!fifo_rx.initialized) {
ret = data_fifo_init(&fifo_rx);
- ERR_CHK_MSG(ret, "Failed to set up rx FIFO");
+ if (ret) {
+ LOG_ERR("Failed to set up rx FIFO: %d", ret);
+ return ret;
+ }
}
LOG_INF("Microphone channel set to %d", sw_codec_cfg.encoder.audio_ch);
ret = sw_codec_init(sw_codec_cfg);
- ERR_CHK_MSG(ret, "Failed to set up codec");
+ if (ret) {
+ LOG_ERR("Failed to set up codec: %d", ret);
+ return ret;
+ }
sw_codec_cfg.initialized = true;
@@ -444,19 +497,37 @@ void audio_system_start(void)
(k_thread_entry_t)encoder_thread, NULL, NULL, NULL,
K_PRIO_PREEMPT(CONFIG_ENCODER_THREAD_PRIO), 0, K_NO_WAIT);
ret = k_thread_name_set(encoder_thread_id, "ENCODER");
- ERR_CHK(ret);
+ if (ret) {
+ LOG_WRN("Failed to name encoder thread: %d", ret);
+ }
}
#if ((CONFIG_AUDIO_SOURCE_USB) && (CONFIG_AUDIO_DEV == GATEWAY))
ret = audio_usb_start(&fifo_tx, &fifo_rx);
- ERR_CHK(ret);
+ if (ret) {
+ LOG_ERR("Failed to start USB audio: %d", ret);
+ goto cleanup_sw_codec;
+ }
#else
ret = audio_datapath_aquire(&fifo_rx);
- ERR_CHK(ret);
+ if (ret) {
+ LOG_ERR("Failed to acquire audio datapath: %d", ret);
+ goto cleanup_sw_codec;
+ }
ret = hw_codec_default_conf_enable();
- ERR_CHK(ret);
+ if (ret) {
+ int release_ret;
+
+ LOG_ERR("Failed to configure hardware codec: %d", ret);
+ release_ret = audio_datapath_release();
+ if (release_ret) {
+ LOG_ERR("Failed to release audio datapath after startup error: %d",
+ release_ret);
+ }
+ goto cleanup_sw_codec;
+ }
/*if (IS_ENABLED(CONFIG_AUDIO_MIC_PDM)) {
ret = pdm_datapath_start(&fifo_rx);
@@ -464,9 +535,22 @@ void audio_system_start(void)
}*/
#endif /* ((CONFIG_AUDIO_SOURCE_USB) && (CONFIG_AUDIO_DEV == GATEWAY))) */
+
+ return 0;
+
+cleanup_sw_codec:
+ {
+ int uninit_ret = sw_codec_uninit(sw_codec_cfg);
+
+ sw_codec_cfg.initialized = false;
+ if (uninit_ret) {
+ LOG_ERR("Failed to uninitialize codec after startup error: %d", uninit_ret);
+ }
+ }
+ return ret;
}
-void audio_system_stop(void)
+static void audio_system_stop_internal(void)
{
int ret;
@@ -476,7 +560,7 @@ void audio_system_stop(void)
}
LOG_DBG("Stopping codec");
- audio_system_encoder_stop();
+ audio_system_encoder_stop_internal();
#if ((CONFIG_AUDIO_DEV == GATEWAY) && CONFIG_AUDIO_SOURCE_USB)
audio_usb_stop();
@@ -501,6 +585,81 @@ void audio_system_stop(void)
data_fifo_empty(&fifo_tx);
}
+int audio_system_start(void)
+{
+ int ret = 0;
+
+ k_mutex_lock(&audio_system_state_mutex, K_FOREVER);
+ if (audio_system_suspended) {
+ audio_system_resume_requested = true;
+ } else {
+ ret = audio_system_start_internal();
+ }
+ k_mutex_unlock(&audio_system_state_mutex);
+ return ret;
+}
+
+void audio_system_stop(void)
+{
+ k_mutex_lock(&audio_system_state_mutex, K_FOREVER);
+ if (audio_system_suspended) {
+ audio_system_resume_requested = false;
+ encoder_resume_requested = false;
+ } else {
+ audio_system_stop_internal();
+ }
+ k_mutex_unlock(&audio_system_state_mutex);
+}
+
+int audio_system_suspend(void)
+{
+ k_mutex_lock(&audio_system_state_mutex, K_FOREVER);
+ if (audio_system_suspended) {
+ k_mutex_unlock(&audio_system_state_mutex);
+ return -EBUSY;
+ }
+
+ audio_system_resume_requested = sw_codec_cfg.initialized;
+ encoder_resume_requested = audio_system_encoder_is_started();
+ audio_system_suspended = true;
+ if (audio_system_resume_requested) {
+ audio_system_stop_internal();
+ }
+ LOG_INF("Audio system suspended: resume=%d encoder=%d", audio_system_resume_requested,
+ encoder_resume_requested);
+ k_mutex_unlock(&audio_system_state_mutex);
+ return 0;
+}
+
+int audio_system_resume(void)
+{
+ int ret = 0;
+ bool resume_requested;
+ bool encoder_requested;
+
+ k_mutex_lock(&audio_system_state_mutex, K_FOREVER);
+ if (!audio_system_suspended) {
+ k_mutex_unlock(&audio_system_state_mutex);
+ return -EALREADY;
+ }
+
+ audio_system_suspended = false;
+ resume_requested = audio_system_resume_requested;
+ encoder_requested = encoder_resume_requested;
+ if (resume_requested) {
+ ret = audio_system_start_internal();
+ if (!ret && encoder_requested) {
+ audio_system_encoder_start_internal();
+ }
+ }
+ LOG_INF("Audio system suspension released: running=%d encoder=%d result=%d",
+ resume_requested && !ret, encoder_requested && !ret, ret);
+ audio_system_resume_requested = false;
+ encoder_resume_requested = false;
+ k_mutex_unlock(&audio_system_state_mutex);
+ return ret;
+}
+
int audio_system_fifo_rx_block_drop(void)
{
int ret;
@@ -554,10 +713,16 @@ int audio_system_init(void)
static int cmd_audio_system_start(const struct shell *shell, size_t argc, const char **argv)
{
+ int ret;
+
ARG_UNUSED(argc);
ARG_UNUSED(argv);
- audio_system_start();
+ ret = audio_system_start();
+ if (ret) {
+ shell_error(shell, "Audio system failed to start: %d", ret);
+ return ret;
+ }
shell_print(shell, "Audio system started");
diff --git a/src/audio/audio_system.h b/src/audio/audio_system.h
index 0c5829e9..c8dc6849 100644
--- a/src/audio/audio_system.h
+++ b/src/audio/audio_system.h
@@ -38,6 +38,24 @@ void audio_system_encoder_stop(void);
*/
bool audio_system_encoder_is_started(void);
+/**
+ * @brief Exclusively suspend the audio system while preserving requested state.
+ *
+ * Start, stop, and encoder requests received during the suspension are deferred
+ * and applied by audio_system_resume().
+ *
+ * @return 0 on success, or -EBUSY if the system is already suspended.
+ */
+int audio_system_suspend(void);
+
+/**
+ * @brief Release an exclusive suspension and apply the latest audio state.
+ *
+ * @return 0 on success, -EALREADY if the system is not suspended, or a
+ * negative startup error when the preserved audio state cannot resume.
+ */
+int audio_system_resume(void);
+
/**
* @brief Toggle a test tone on and off.
*
@@ -87,9 +105,11 @@ int audio_system_config_set(uint32_t encoder_sample_rate_hz, uint32_t encoder_bi
int audio_system_decode(void const *const encoded_data, size_t encoded_data_size, bool bad_frame);
/**
- * @brief Initialize and start both HW and SW audio codec.
+ * @brief Initialize and start both HW and SW audio codecs.
+ *
+ * @return 0 on success, or a negative error returned by the codec or datapath.
*/
-void audio_system_start(void);
+int audio_system_start(void);
/**
* @brief Stop all activities related to audio.
diff --git a/src/audio/decimation_filter.cpp b/src/audio/decimation_filter.cpp
index 7d49f1a0..36af29fa 100644
--- a/src/audio/decimation_filter.cpp
+++ b/src/audio/decimation_filter.cpp
@@ -6,7 +6,6 @@
#include "decimation_filter.h"
#include
-#include
#include
LOG_MODULE_REGISTER(decimator_cpp, CONFIG_AUDIO_DATAPATH_LOG_LEVEL);
@@ -27,10 +26,16 @@ static const float32_t coeff_dec3[2 * 5] = {
};
// Decimator class implementation
-Decimator::Decimator(uint8_t factor)
- : factor_(factor), initialized_(false) {
+Decimator::Decimator()
+ : factor_(0), initialized_(false) {
+ memset(state_, 0, sizeof(state_));
+}
+
+void Decimator::configure(uint8_t factor) {
+ factor_ = factor;
+ initialized_ = false;
+ memset(&biquad_, 0, sizeof(biquad_));
memset(state_, 0, sizeof(state_));
- memset(temp_f32_, 0, sizeof(temp_f32_));
}
int Decimator::init() {
@@ -54,8 +59,10 @@ int Decimator::init() {
return 0;
}
-int Decimator::process(const int16_t* input, int16_t* output, uint32_t num_frames) {
- if (!initialized_ || !input || !output || num_frames == 0 || num_frames > MAX_FRAMES) {
+int Decimator::process(const int16_t* input, int16_t* output, uint32_t num_frames,
+ float32_t* processing_buffer) {
+ if (!initialized_ || !input || !output || !processing_buffer || num_frames == 0 ||
+ num_frames > MAX_FRAMES) {
return -EINVAL;
}
@@ -63,23 +70,23 @@ int Decimator::process(const int16_t* input, int16_t* output, uint32_t num_frame
// Convert int16 to float32
for (uint32_t i = 0; i < num_samples; i++) {
- temp_f32_[i] = static_cast(input[i]);
+ processing_buffer[i] = static_cast(input[i]);
}
-
- // Apply anti-aliasing filter using stereo DF2T
- static float32_t filtered[MAX_FRAMES * 2];
- arm_biquad_cascade_stereo_df2T_f32(&biquad_, temp_f32_, filtered, num_frames);
-
+
+ // CMSIS-DSP stereo DF2T supports identical source and destination buffers.
+ arm_biquad_cascade_stereo_df2T_f32(&biquad_, processing_buffer, processing_buffer,
+ num_frames);
+
// Clip to prevent overflow
- arm_clip_f32(filtered, filtered, -32768.0f, 32767.0f, num_samples);
+ arm_clip_f32(processing_buffer, processing_buffer, -32768.0f, 32767.0f, num_samples);
// Decimate and convert back to int16
uint32_t out_frames = num_frames / factor_;
uint32_t step = factor_ * 2;
for (uint32_t i = 0; i < out_frames; i++) {
- output[i * 2] = static_cast(filtered[i * step]);
- output[i * 2 + 1] = static_cast(filtered[i * step + 1]);
+ output[i * 2] = static_cast(processing_buffer[i * step]);
+ output[i * 2 + 1] = static_cast(processing_buffer[i * step + 1]);
}
return out_frames;
@@ -100,106 +107,94 @@ const float32_t* Decimator::getCoefficients() const {
}
// CascadedDecimator class implementation
-CascadedDecimator::CascadedDecimator(uint8_t total_factor)
- : total_factor_(total_factor), num_stages_(0) {
- memset(stages_, 0, sizeof(stages_));
- memset(temp_buffers_, 0, sizeof(temp_buffers_));
- setupStages();
+CascadedDecimator::CascadedDecimator()
+ : total_factor_(0), num_stages_(0), configured_(false) {
+ memset(intermediate_buffer_, 0, sizeof(intermediate_buffer_));
}
-CascadedDecimator::~CascadedDecimator() {
- cleanupStages();
-}
+int CascadedDecimator::configure(uint8_t total_factor) {
+ uint8_t stage_factors[MAX_STAGES] = {};
-void CascadedDecimator::setupStages() {
- switch (total_factor_) {
+ cleanup();
+ switch (total_factor) {
case 1: // No decimation
num_stages_ = 0;
break;
case 2: // 2x
num_stages_ = 1;
- stages_[0] = new Decimator(2);
+ stage_factors[0] = 2;
break;
case 3: // 3x
num_stages_ = 1;
- stages_[0] = new Decimator(3);
+ stage_factors[0] = 3;
break;
case 4: // 2x -> 2x
num_stages_ = 2;
- stages_[0] = new Decimator(2);
- stages_[1] = new Decimator(2);
+ stage_factors[0] = 2;
+ stage_factors[1] = 2;
break;
-
case 6: // 3x -> 2x
num_stages_ = 2;
- stages_[0] = new Decimator(3);
- stages_[1] = new Decimator(2);
+ stage_factors[0] = 3;
+ stage_factors[1] = 2;
break;
-
case 8: // 2x -> 2x -> 2x
num_stages_ = 3;
- stages_[0] = new Decimator(2);
- stages_[1] = new Decimator(2);
- stages_[2] = new Decimator(2);
+ stage_factors[0] = 2;
+ stage_factors[1] = 2;
+ stage_factors[2] = 2;
break;
-
case 12: // 3x -> 2x -> 2x
num_stages_ = 3;
- stages_[0] = new Decimator(3);
- stages_[1] = new Decimator(2);
- stages_[2] = new Decimator(2);
+ stage_factors[0] = 3;
+ stage_factors[1] = 2;
+ stage_factors[2] = 2;
break;
-
case 16: // 2x -> 2x -> 2x -> 2x
num_stages_ = 4;
- stages_[0] = new Decimator(2);
- stages_[1] = new Decimator(2);
- stages_[2] = new Decimator(2);
- stages_[3] = new Decimator(2);
+ stage_factors[0] = 2;
+ stage_factors[1] = 2;
+ stage_factors[2] = 2;
+ stage_factors[3] = 2;
break;
-
case 24: // 3x -> 2x -> 2x -> 2x
num_stages_ = 4;
- stages_[0] = new Decimator(3);
- stages_[1] = new Decimator(2);
- stages_[2] = new Decimator(2);
- stages_[3] = new Decimator(2);
+ stage_factors[0] = 3;
+ stage_factors[1] = 2;
+ stage_factors[2] = 2;
+ stage_factors[3] = 2;
break;
-
default:
- LOG_ERR("Unsupported total decimation factor: %d", total_factor_);
- num_stages_ = 0;
- return;
+ LOG_ERR("Unsupported total decimation factor: %d", total_factor);
+ return -EINVAL;
}
-
- // Allocate temporary buffers between stages
- for (uint8_t i = 0; i < num_stages_ - 1; i++) {
- temp_buffers_[i] = new int16_t[MAX_FRAMES * 2];
+
+ for (uint8_t i = 0; i < num_stages_; i++) {
+ stages_[i].configure(stage_factors[i]);
}
-
+
+ total_factor_ = total_factor;
+ configured_ = true;
LOG_DBG("CascadedDecimator setup for factor %d with %d stages", total_factor_, num_stages_);
+ return 0;
}
-void CascadedDecimator::cleanupStages() {
- for (uint8_t i = 0; i < num_stages_; i++) {
- delete stages_[i];
- stages_[i] = nullptr;
- }
-
- for (uint8_t i = 0; i < MAX_STAGES - 1; i++) {
- delete[] temp_buffers_[i];
- temp_buffers_[i] = nullptr;
+void CascadedDecimator::cleanup() {
+ for (uint8_t i = 0; i < MAX_STAGES; i++) {
+ stages_[i].configure(0);
}
+ total_factor_ = 0;
+ num_stages_ = 0;
+ configured_ = false;
}
int CascadedDecimator::init() {
+ if (!configured_) {
+ return -EINVAL;
+ }
+
for (uint8_t i = 0; i < num_stages_; i++) {
- if (!stages_[i]) {
- LOG_ERR("Stage %d is null", i);
- return -EINVAL;
- }
-
- int ret = stages_[i]->init();
+ int ret = stages_[i].init();
if (ret != 0) {
LOG_ERR("Failed to initialize stage %d: %d", i, ret);
return ret;
@@ -211,14 +206,17 @@ int CascadedDecimator::init() {
}
int CascadedDecimator::process(const int16_t* input, int16_t* output, uint32_t num_frames) {
+ if (!configured_ || !input || !output || num_frames == 0 || num_frames > MAX_FRAMES) {
+ return -EINVAL;
+ }
+
if (num_stages_ == 0) {
+ if (output != input) {
+ memcpy(output, input, num_frames * 2U * sizeof(int16_t));
+ }
return num_frames;
}
- if (!input || !output || num_frames == 0) {
- return -EINVAL;
- }
-
const int16_t* stage_input = input;
int16_t* stage_output = nullptr;
int frames = num_frames;
@@ -229,12 +227,12 @@ int CascadedDecimator::process(const int16_t* input, int16_t* output, uint32_t n
// Last stage outputs to final output buffer
stage_output = output;
} else {
- // Intermediate stage outputs to temp buffer
- stage_output = temp_buffers_[i];
+ // Alternate storage so adjacent stages never share input and output.
+ stage_output = (i % 2 == 0) ? intermediate_buffer_ : output;
}
// Process this stage
- frames = stages_[i]->process(stage_input, stage_output, frames);
+ frames = stages_[i].process(stage_input, stage_output, frames, processing_buffer_);
if (frames < 0) {
LOG_ERR("Stage %d processing failed: %d", i, frames);
return frames;
@@ -249,8 +247,6 @@ int CascadedDecimator::process(const int16_t* input, int16_t* output, uint32_t n
void CascadedDecimator::reset() {
for (uint8_t i = 0; i < num_stages_; i++) {
- if (stages_[i]) {
- stages_[i]->reset();
- }
+ stages_[i].reset();
}
-}
\ No newline at end of file
+}
diff --git a/src/audio/decimation_filter.h b/src/audio/decimation_filter.h
index 2afd709f..a3f0d5d9 100644
--- a/src/audio/decimation_filter.h
+++ b/src/audio/decimation_filter.h
@@ -10,24 +10,37 @@
#include
#include
#include "arm_math.h"
+#include "audio_i2s.h"
#ifdef __cplusplus
+static_assert(BLOCK_SIZE_BYTES % (sizeof(int16_t) * 2U) == 0,
+ "The audio block must contain complete stereo int16 frames");
+
/**
* @brief Single stage decimation filter class
*/
class Decimator {
public:
+ /** Maximum number of stereo frames supplied by one audio datapath block. */
+ static constexpr uint32_t MAX_FRAMES =
+ BLOCK_SIZE_BYTES / (sizeof(int16_t) * 2U);
+
/**
- * @brief Constructor
- * @param factor Decimation factor (2 or 3)
+ * @brief Construct an unconfigured decimation stage.
*/
- Decimator(uint8_t factor);
+ Decimator();
/**
* @brief Destructor
*/
~Decimator() = default;
+
+ /**
+ * @brief Configure this stage for a decimation factor.
+ * @param factor Decimation factor (2 or 3)
+ */
+ void configure(uint8_t factor);
/**
* @brief Initialize the decimator
@@ -40,9 +53,11 @@ class Decimator {
* @param input Input buffer (interleaved stereo int16)
* @param output Output buffer (interleaved stereo int16)
* @param num_frames Number of input stereo frames
+ * @param processing_buffer In-place float processing buffer for stereo frames
* @return Number of output frames, or negative on error
*/
- int process(const int16_t* input, int16_t* output, uint32_t num_frames);
+ int process(const int16_t* input, int16_t* output, uint32_t num_frames,
+ float32_t* processing_buffer);
/**
* @brief Reset filter state
@@ -56,14 +71,12 @@ class Decimator {
uint8_t getFactor() const { return factor_; }
private:
- static constexpr uint32_t MAX_FRAMES = 512;
static constexpr uint32_t NUM_STAGES = 2;
uint8_t factor_;
bool initialized_;
arm_biquad_cascade_stereo_df2T_instance_f32 biquad_;
float32_t state_[4 * NUM_STAGES];
- float32_t temp_f32_[MAX_FRAMES * 2];
const float32_t* getCoefficients() const;
};
@@ -74,15 +87,26 @@ class Decimator {
class CascadedDecimator {
public:
/**
- * @brief Constructor for predefined cascaded decimation factors
- * @param total_factor Total decimation factor (4, 6, 8, or 12)
+ * @brief Construct an unconfigured cascaded decimator.
*/
- CascadedDecimator(uint8_t total_factor);
+ CascadedDecimator();
/**
* @brief Destructor
*/
- ~CascadedDecimator();
+ ~CascadedDecimator() = default;
+
+ /**
+ * @brief Configure the statically allocated filter stages.
+ * @param total_factor Total decimation factor (1, 2, 3, 4, 6, 8, 12, 16, or 24)
+ * @return 0 on success, or -EINVAL for an unsupported factor.
+ */
+ int configure(uint8_t total_factor);
+
+ /**
+ * @brief Clear configuration and filter state without releasing memory.
+ */
+ void cleanup();
/**
* @brief Initialize all cascaded decimators
@@ -112,15 +136,15 @@ class CascadedDecimator {
private:
static constexpr uint32_t MAX_STAGES = 4;
- static constexpr uint32_t MAX_FRAMES = 512;
+ static constexpr uint32_t MAX_FRAMES = Decimator::MAX_FRAMES;
uint8_t total_factor_;
uint8_t num_stages_;
- Decimator* stages_[MAX_STAGES];
- int16_t* temp_buffers_[MAX_STAGES - 1];
-
- void setupStages();
- void cleanupStages();
+ bool configured_;
+ Decimator stages_[MAX_STAGES];
+ /* Stage output is interleaved stereo, so each frame needs two samples. */
+ int16_t intermediate_buffer_[MAX_FRAMES * 2U];
+ float32_t processing_buffer_[MAX_FRAMES * 2];
};
#endif
#endif /* _DECIMATION_FILTER_H_ */
diff --git a/src/audio/le_audio_rx.c b/src/audio/le_audio_rx.c
index 8c7c9370..9411f230 100644
--- a/src/audio/le_audio_rx.c
+++ b/src/audio/le_audio_rx.c
@@ -138,6 +138,10 @@ void le_audio_rx_data_handler(uint8_t const *const p_data, size_t data_size, boo
*/
static void audio_datapath_thread(void *dummy1, void *dummy2, void *dummy3)
{
+ ARG_UNUSED(dummy1);
+ ARG_UNUSED(dummy2);
+ ARG_UNUSED(dummy3);
+
int ret;
struct ble_iso_data *iso_received = NULL;
size_t iso_received_size;
diff --git a/src/audio/streamctrl.c b/src/audio/streamctrl.c
index 8cd15d97..e3dc6df2 100644
--- a/src/audio/streamctrl.c
+++ b/src/audio/streamctrl.c
@@ -6,6 +6,10 @@
#include "streamctrl.h"
+#include
+#include
+#include
+#include "common/bt_str.h"
#include
#include
@@ -28,6 +32,8 @@
#include "AutoOffManager.h"
#include "BootState.h"
+#include "channel_assignment.h"
+#include "uicr.h"
#include
LOG_MODULE_REGISTER(streamctrl, CONFIG_MAIN_LOG_LEVEL);
@@ -199,7 +205,11 @@ static void le_audio_msg_sub_thread(void)
break;
}
- audio_system_start();
+ ret = audio_system_start();
+ if (ret) {
+ LOG_ERR("Failed to start audio system: %d", ret);
+ break;
+ }
stream_state_set(STATE_STREAMING);
if (msg.dir == BT_AUDIO_DIR_SOURCE) {
audio_system_encoder_start();
@@ -545,11 +555,15 @@ static void write_sirk(uint32_t sirk) {
// Callback-Funktion für gefundene Geräte
static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, struct net_buf_simple *ad)
{
+ ARG_UNUSED(rssi);
+ ARG_UNUSED(type);
+
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
- int ret;
bool is_le_audio_device = false;
+ bool csis_rsi_found = false;
+ bool chip_id_found = false;
uint8_t csis_rsi[6];
uint8_t chip_id[8];
@@ -559,14 +573,14 @@ static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, st
break; // Ungültige Länge
}
- uint8_t type = net_buf_simple_pull_u8(ad);
+ uint8_t data_type = net_buf_simple_pull_u8(ad);
const uint8_t *data = ad->data;
ad->data += len - 1;
ad->len -= len - 1;
// Suchen nach 16-bit Service UUIDs (LE Audio Services)
- if (type == BT_DATA_SVC_DATA16) {
- for (size_t i = 0; i < len - 1; i += 2) {
+ if (data_type == BT_DATA_SVC_DATA16) {
+ for (size_t i = 0; i + 1U < (size_t)(len - 1); i += 2) {
uint16_t uuid = (data[i + 1] << 8) | data[i];
if (uuid == BT_UUID_CAS_VAL) {
is_le_audio_device = true;
@@ -574,13 +588,24 @@ static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, st
}
}
- if (is_le_audio_device && type == BT_DATA_MANUFACTURER_DATA) {
- memset(chip_id, 0, sizeof(chip_id));
- memcpy(chip_id, data, sizeof(chip_id));
+ if (is_le_audio_device && data_type == BT_DATA_MANUFACTURER_DATA) {
+ if (len > 1U && (size_t)(len - 1U) >= sizeof(chip_id)) {
+ memcpy(chip_id, data, sizeof(chip_id));
+ chip_id_found = true;
+ } else {
+ LOG_DBG("Ignoring short manufacturer chip ID (%u bytes)",
+ (unsigned int)(len - 1));
+ }
}
- if (type == BT_DATA_CSIS_RSI) {
- memcpy(csis_rsi, data, sizeof(csis_rsi));
+ /* A valid RSI contains a 3-byte hash followed by a 3-byte random value. */
+ if (data_type == BT_DATA_CSIS_RSI) {
+ if (len > 1U && (size_t)(len - 1U) >= sizeof(csis_rsi)) {
+ memcpy(csis_rsi, data, sizeof(csis_rsi));
+ csis_rsi_found = true;
+ } else {
+ LOG_DBG("Ignoring short CSIS RSI (%u bytes)", (unsigned int)(len - 1));
+ }
}
// channel
@@ -592,10 +617,18 @@ static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, st
if (is_le_audio_device) {
LOG_INF("Found LE-Audio device!");
- uint32_t hash_ref = (csis_rsi[2] << 16) | (csis_rsi[1] << 8) | csis_rsi[0];
+ /* Do not derive or persist a SIRK without a complete peer identity. */
+ if (!chip_id_found) {
+ LOG_DBG("Ignoring LE Audio device without manufacturer chip ID");
+ return;
+ }
+
uint32_t hash;
+ uint32_t peer_device_id;
- uint32_t new_sirk = *((uint32_t *) chip_id) ^ oe_boot_state.device_id;
+ /* Copy from the byte array without alignment or aliasing assumptions. */
+ memcpy(&peer_device_id, chip_id, sizeof(peer_device_id));
+ uint32_t new_sirk = peer_device_id ^ oe_boot_state.device_id;
enum audio_channel channel;
@@ -603,15 +636,23 @@ static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, st
channel_assignment_get(&channel);
if (channel == AUDIO_CH_L) {
- LOG_INF("Device ID 1: %016X", oe_boot_state.device_id);
- LOG_INF("Device ID 2: %016X", *((uint32_t *) chip_id));
- LOG_INF("New Sirk: %016X", new_sirk);
+ LOG_INF("Device ID 1: %016" PRIX64, oe_boot_state.device_id);
+ LOG_INF("Device ID 2: %08" PRIX32, peer_device_id);
+ LOG_INF("New Sirk: %08" PRIX32, new_sirk);
//TODO: check if the device wants to pair (sirk == device_id)
//TODO: check channel
write_sirk(new_sirk);
} else if (channel == AUDIO_CH_R) {
+ /* Only right-channel matching validates the advertised RSI. */
+ if (!csis_rsi_found) {
+ LOG_DBG("Ignoring right-channel LE Audio device without CSIS RSI");
+ return;
+ }
+
+ uint32_t hash_ref =
+ (csis_rsi[2] << 16) | (csis_rsi[1] << 8) | csis_rsi[0];
uint8_t res[BT_CSIP_PADDED_RAND_SIZE];
uint8_t sirk[BT_CSIP_SIRK_SIZE + 1];
@@ -631,6 +672,10 @@ static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type, st
snprintf(sirk, BT_CSIP_SIRK_SIZE, "%08X", new_sirk);
int err = bt_encrypt_le(sirk, res, res);
+ if (err) {
+ LOG_ERR("Failed to calculate CSIS hash: %d", err);
+ return;
+ }
memcpy(out, res, BT_CSIP_CRYPTO_HASH_SIZE);
diff --git a/src/audio/sw_codec_select.c b/src/audio/sw_codec_select.c
index f1564206..8f5de1af 100644
--- a/src/audio/sw_codec_select.c
+++ b/src/audio/sw_codec_select.c
@@ -163,6 +163,15 @@ int sw_codec_encode(void *pcm_data, size_t pcm_size, uint8_t **encoded_data, siz
break;
}
case SW_CODEC_STEREO: {
+ /* Stereo encoding consumes both channel pointers below. Reject mismatched
+ * configurations so the caller can drop the frame safely.
+ */
+ if (m_config.encoder.num_ch != AUDIO_CH_NUM) {
+ LOG_DBG("Rejecting stereo encode with %u configured channels",
+ (unsigned int)m_config.encoder.num_ch);
+ return -EINVAL;
+ }
+
for (int i = 0; i < m_config.encoder.num_ch; ++i) {
ret = sw_codec_sample_rate_convert(
&encoder_converters[i], CONFIG_AUDIO_SAMPLE_RATE_HZ,
@@ -238,13 +247,19 @@ int sw_codec_decode(uint8_t const *const encoded_data, size_t encoded_size, bool
switch (m_config.sw_codec) {
case SW_CODEC_LC3: {
#if (CONFIG_SW_CODEC_LC3)
- char *pcm_in_data_ptrs[m_config.decoder.channel_mode];
+ /* PLC override bypasses sample-rate conversion, so its branches assign these
+ * fixed channel slots directly to zero-filled PCM buffers.
+ */
+ char *pcm_in_data_ptrs[AUDIO_CH_NUM] = {0};
switch (m_config.decoder.channel_mode) {
case SW_CODEC_MONO: {
if (bad_frame && IS_ENABLED(CONFIG_SW_CODEC_OVERRIDE_PLC)) {
memset(decoded_data_mono[AUDIO_CH_L], 0, PCM_NUM_BYTES_MONO);
decoded_data_size = PCM_NUM_BYTES_MONO;
+ pcm_in_data_ptrs[AUDIO_CH_L] = decoded_data_mono[AUDIO_CH_L];
+ pcm_size_mono = decoded_data_size;
+ LOG_DBG("Replacing bad mono frame with silence");
} else {
ret = sw_codec_lc3_dec_run(
encoded_data, encoded_size, LC3_PCM_NUM_BYTES_MONO, 0,
@@ -284,6 +299,10 @@ int sw_codec_decode(uint8_t const *const encoded_data, size_t encoded_size, bool
memset(decoded_data_mono[AUDIO_CH_L], 0, PCM_NUM_BYTES_MONO);
memset(decoded_data_mono[AUDIO_CH_R], 0, PCM_NUM_BYTES_MONO);
decoded_data_size = PCM_NUM_BYTES_MONO;
+ pcm_in_data_ptrs[AUDIO_CH_L] = decoded_data_mono[AUDIO_CH_L];
+ pcm_in_data_ptrs[AUDIO_CH_R] = decoded_data_mono[AUDIO_CH_R];
+ pcm_size_mono = decoded_data_size;
+ LOG_DBG("Replacing bad stereo frame with silence");
} else {
/* Decode left channel */
ret = sw_codec_lc3_dec_run(
@@ -304,7 +323,8 @@ int sw_codec_decode(uint8_t const *const encoded_data, size_t encoded_size, bool
return ret;
}
- for (int i = 0; i < m_config.decoder.channel_mode; ++i) {
+ /* Stereo always initializes both slots consumed by pscm_combine(). */
+ for (int i = 0; i < AUDIO_CH_NUM; ++i) {
ret = sw_codec_sample_rate_convert(
&decoder_converters[i],
m_config.decoder.sample_rate_hz,
diff --git a/src/bluetooth/Kconfig b/src/bluetooth/Kconfig
index 2bb2d8d2..0bafa549 100644
--- a/src/bluetooth/Kconfig
+++ b/src/bluetooth/Kconfig
@@ -8,6 +8,23 @@ rsource "Kconfig.defaults"
menu "Bluetooth"
+config AUDIO_RESPONSE_MAX_SAMPLES
+ int "Maximum uploaded audio response samples"
+ range 1 48000
+ default 32000
+ help
+ Maximum number of signed 16-bit PCM samples retained by the audio
+ response service. Storage for this buffer is reserved at build time.
+
+config AUDIO_RESPONSE_MAX_POINTS
+ int "Maximum audio response measurement points"
+ range 9 60
+ default 60
+ help
+ Maximum number of frequency response points accepted in one audio
+ response config request. The default keeps result notifications within a
+ 247-byte ATT MTU because the encoded result uses 2 + 4 * points bytes.
+
rsource "bt_management/Kconfig"
#----------------------------------------------------------------------------#
diff --git a/src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c b/src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c
index 18e6e2cd..f1b5e3e3 100644
--- a/src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c
+++ b/src/bluetooth/bt_content_control/media/bt_content_ctrl_media.c
@@ -44,7 +44,7 @@ static struct media_ctlr mcc_peer[CONFIG_BT_MAX_CONN];
*/
static int mcc_peer_index_free_get(void)
{
- for (int i = 0; i < ARRAY_SIZE(mcc_peer); i++) {
+ for (size_t i = 0; i < ARRAY_SIZE(mcc_peer); i++) {
if (mcc_peer[i].conn == NULL) {
return i;
}
@@ -162,6 +162,7 @@ static void mcc_send_command_cb(struct bt_conn *conn, int err, const struct mpl_
*/
static void mcc_cmd_notification_cb(struct bt_conn *conn, int err, const struct mpl_cmd_ntf *ntf)
{
+ ARG_UNUSED(conn);
LOG_DBG("mcc_cmd_ntf_cb");
if (err) {
@@ -179,6 +180,7 @@ static void mcc_cmd_notification_cb(struct bt_conn *conn, int err, const struct
*/
static void mcc_read_media_state_cb(struct bt_conn *conn, int err, uint8_t state)
{
+ ARG_UNUSED(conn);
LOG_DBG("mcc_read_media_cb, state: %d", state);
if (err) {
@@ -199,6 +201,7 @@ static void mcc_read_media_state_cb(struct bt_conn *conn, int err, uint8_t state
static void mcs_command_recv_cb(struct media_player *plr, int err,
const struct mpl_cmd_ntf *cmd_ntf)
{
+ ARG_UNUSED(plr);
if (err) {
LOG_ERR("Command failed (%d)", err);
return;
@@ -223,6 +226,7 @@ static void mcs_command_recv_cb(struct media_player *plr, int err,
*/
static void mcs_media_state_cb(struct media_player *plr, int err, uint8_t state)
{
+ ARG_UNUSED(plr);
if (err) {
LOG_ERR("Media state failed (%d)", err);
return;
diff --git a/src/bluetooth/bt_management/advertising/bt_mgmt_adv.c b/src/bluetooth/bt_management/advertising/bt_mgmt_adv.c
index ba1e4922..d43317b4 100644
--- a/src/bluetooth/bt_management/advertising/bt_mgmt_adv.c
+++ b/src/bluetooth/bt_management/advertising/bt_mgmt_adv.c
@@ -9,7 +9,7 @@
#include
#include
#include
-
+
#include "macros_common.h"
#include "zbus_common.h"
@@ -47,8 +47,10 @@
.peer = NULL,
};
- static void bond_find(const struct bt_bond_info *info, void *user_data)
- {
+static void bond_find(const struct bt_bond_info *info, void *user_data)
+{
+ ARG_UNUSED(user_data);
+
int ret;
struct bt_conn *conn;
@@ -83,8 +85,10 @@
}
}
- static void filter_accept_list_add(const struct bt_bond_info *info, void *user_data)
- {
+static void filter_accept_list_add(const struct bt_bond_info *info, void *user_data)
+{
+ ARG_UNUSED(user_data);
+
int ret;
ret = bt_le_filter_accept_list_add(&info->addr);
@@ -227,8 +231,10 @@
return 0;
}
- static void advertising_process(struct k_work *work)
- {
+static void advertising_process(struct k_work *work)
+{
+ ARG_UNUSED(work);
+
int ret;
struct bt_mgmt_msg msg;
uint8_t ext_adv_index;
@@ -470,8 +476,7 @@
return 0;
}
- void bt_mgmt_adv_init(void)
- {
- k_work_init(&adv_work, advertising_process);
- }
-
\ No newline at end of file
+void bt_mgmt_adv_init(void)
+{
+ k_work_init(&adv_work, advertising_process);
+}
diff --git a/src/bluetooth/bt_management/bt_mgmt.c b/src/bluetooth/bt_management/bt_mgmt.c
index da38a9cb..4d0a1818 100644
--- a/src/bluetooth/bt_management/bt_mgmt.c
+++ b/src/bluetooth/bt_management/bt_mgmt.c
@@ -6,6 +6,7 @@
#include "bt_mgmt.h"
+#include
#include "channel_assignment.h"
#include
@@ -19,6 +20,8 @@
#include "macros_common.h"
#include "zbus_common.h"
#include "button_assignments.h"
+#include "../../buttons/button_manager.h"
+#include "uicr.h"
#include "bt_mgmt_ctlr_cfg_internal.h"
#include "bt_mgmt_adv_internal.h"
@@ -67,12 +70,16 @@ static void conn_state_connected_check(struct bt_conn *conn, void *data)
void mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx)
{
+ ARG_UNUSED(conn);
+
LOG_INF("Updated MTU: TX: %d RX: %d bytes", tx, rx);
}
static void le_data_length_updated(struct bt_conn *conn,
struct bt_conn_le_data_len_info *info)
{
+ ARG_UNUSED(conn);
+
LOG_INF("LE data len updated: TX (len: %d time: %d)"
" RX (len: %d time: %d)", info->tx_max_len,
info->tx_max_time, info->rx_max_len, info->rx_max_time);
@@ -83,9 +90,6 @@ static struct bt_le_conn_param *conn_param = BT_LE_CONN_PARAM(CONFIG_BLE_ACL_CON
//callback
static void conn_params_updated(struct bt_conn *conn, uint16_t interval, uint16_t latency, uint16_t timeout)
{
- struct bt_mgmt_msg msg;
- int ret;
-
LOG_INF("Conn params updated: interval %d unit, latency %d, timeout: %d0 ms",interval, latency, timeout);
bt_mgmt_ci_on_conn_param_updated(conn, interval, latency, timeout);
@@ -163,7 +167,7 @@ static void connected_cb(struct bt_conn *conn, uint8_t err)
err = bt_conn_le_param_update(conn, conn_param);
if (err) {
LOG_ERR("Cannot update conneciton parameter (err: %d)", err);
- return err;
+ return;
}
LOG_INF("Connection parameters update requested: interval_min %d interval_max %d latency %d timeout %d",
conn_param->interval_min, conn_param->interval_max,
@@ -319,7 +323,7 @@ static int local_identity_addr_print(void)
bt_id_get(addrs, &num_ids);
- for (int i = 0; i < num_ids; i++) {
+ for (size_t i = 0; i < num_ids; i++) {
(void)bt_addr_le_to_str(&(addrs[i]), addr_str, BT_ADDR_LE_STR_LEN);
LOG_INF("Local identity addr: %s", addr_str);
}
@@ -388,6 +392,9 @@ int bt_mgmt_conn_disconnect(struct bt_conn *conn, uint8_t reason)
int bonded_device_count = 0;
void count_bonds(const struct bt_bond_info *info, void *user_data) {
+ ARG_UNUSED(info);
+ ARG_UNUSED(user_data);
+
bonded_device_count++;
}
@@ -408,7 +415,8 @@ int bt_mgmt_init(void)
bt_gatt_cb_register(&gatt_callbacks);
uint32_t sirk = uicr_sirk_get();
- snprintf(name, CONFIG_BT_DEVICE_NAME_MAX, "%s-%04X", CONFIG_BT_DEVICE_NAME, (sirk != 0xFFFFFFFFU ? sirk : oe_boot_state.device_id) & 0xFFFF);
+ snprintf(name, CONFIG_BT_DEVICE_NAME_MAX, "%s-%04X", CONFIG_BT_DEVICE_NAME,
+ (unsigned int)((sirk != 0xFFFFFFFFU ? sirk : oe_boot_state.device_id) & 0xFFFF));
ret = bt_set_name(name);
if (ret) {
diff --git a/src/bluetooth/bt_management/bt_mgmt_conn_interval.cpp b/src/bluetooth/bt_management/bt_mgmt_conn_interval.cpp
index fa089483..e7fb9836 100644
--- a/src/bluetooth/bt_management/bt_mgmt_conn_interval.cpp
+++ b/src/bluetooth/bt_management/bt_mgmt_conn_interval.cpp
@@ -146,6 +146,6 @@ void BtMgmtConnIntervalStrategy::request_interval(uint16_t min_units, uint16_t m
} else {
LOG_INF("Requested CI [%u..%u] units (%.2f..%.2f ms)",
min_units, max_units,
- min_units * 1.25f, max_units * 1.25f);
+ (double)(min_units * 1.25f), (double)(max_units * 1.25f));
}
-}
\ No newline at end of file
+}
diff --git a/src/bluetooth/bt_management/conn_interval/conn_intvl_exp_backoff_lin_inc.cpp b/src/bluetooth/bt_management/conn_interval/conn_intvl_exp_backoff_lin_inc.cpp
index 79ba1f7f..631335b2 100644
--- a/src/bluetooth/bt_management/conn_interval/conn_intvl_exp_backoff_lin_inc.cpp
+++ b/src/bluetooth/bt_management/conn_interval/conn_intvl_exp_backoff_lin_inc.cpp
@@ -19,10 +19,10 @@ void ConnIntvlExpBackoffLinIncr::init() {
}
void ConnIntvlExpBackoffLinIncr::on_audio_underrun(uint32_t count) {
- uint16_t current_interval_units_ = this->current_interval_units();
+ uint16_t current_interval_units = this->current_interval_units();
- LOG_DBG("Audio underrun reported: %u, current interval: %u units", count, current_interval_units_);
- uint16_t new_interval_units = current_interval_units_ * backoff_factor_;
+ LOG_DBG("Audio underrun reported: %u, current interval: %u units", count, current_interval_units);
+ uint16_t new_interval_units = current_interval_units * backoff_factor_;
if (new_interval_units > max_interval_units_) {
new_interval_units = max_interval_units_;
}
@@ -31,6 +31,7 @@ void ConnIntvlExpBackoffLinIncr::on_audio_underrun(uint32_t count) {
}
void ConnIntvlExpBackoffLinIncr::on_timer_tick(k_timeout_t elapsed) {
+ ARG_UNUSED(elapsed);
bool is_streaming = stream_state_get() == STATE_STREAMING;
if (!is_streaming) {
return;
@@ -41,4 +42,4 @@ void ConnIntvlExpBackoffLinIncr::on_timer_tick(k_timeout_t elapsed) {
new_interval_units = min_interval_units_;
}
this->request_interval(new_interval_units, new_interval_units);
-}
\ No newline at end of file
+}
diff --git a/src/bluetooth/bt_management/conn_interval/conn_intvl_linear.cpp b/src/bluetooth/bt_management/conn_interval/conn_intvl_linear.cpp
index dfc792f9..f1214a78 100644
--- a/src/bluetooth/bt_management/conn_interval/conn_intvl_linear.cpp
+++ b/src/bluetooth/bt_management/conn_interval/conn_intvl_linear.cpp
@@ -16,10 +16,10 @@ void ConnIntvlLinear::init() {
}
void ConnIntvlLinear::on_audio_underrun(uint32_t count) {
- uint16_t current_interval_units_ = this->current_interval_units();
+ uint16_t current_interval_units = this->current_interval_units();
- LOG_DBG("Audio underrun reported: %u, current interval: %u units", count, current_interval_units_);
- uint16_t new_interval_units = current_interval_units_ + inc_step_units_;
+ LOG_DBG("Audio underrun reported: %u, current interval: %u units", count, current_interval_units);
+ uint16_t new_interval_units = current_interval_units + inc_step_units_;
if (new_interval_units > max_interval_units_) {
new_interval_units = max_interval_units_;
}
@@ -28,6 +28,7 @@ void ConnIntvlLinear::on_audio_underrun(uint32_t count) {
}
void ConnIntvlLinear::on_timer_tick(k_timeout_t elapsed) {
+ ARG_UNUSED(elapsed);
bool is_streaming = stream_state_get() == STATE_STREAMING;
if (!is_streaming) {
return;
@@ -38,4 +39,4 @@ void ConnIntvlLinear::on_timer_tick(k_timeout_t elapsed) {
new_interval_units = min_interval_units_;
}
this->request_interval(new_interval_units, new_interval_units);
-}
\ No newline at end of file
+}
diff --git a/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c b/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c
index a7ff6734..8bd09f19 100644
--- a/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c
+++ b/src/bluetooth/bt_management/controller_config/bt_mgmt_ctlr_cfg.c
@@ -42,6 +42,7 @@ K_TIMER_DEFINE(ctlr_poll_timer, ctlr_poll_timer_handler, NULL);
static void work_ctlr_poll_handler(struct k_work *work)
{
+ ARG_UNUSED(work);
int ret;
uint16_t manufacturer = 0;
@@ -54,6 +55,7 @@ static void work_ctlr_poll_handler(struct k_work *work)
static void ctlr_poll_timer_handler(struct k_timer *timer_id)
{
+ ARG_UNUSED(timer_id);
int ret;
ret = k_work_submit_to_queue(&ctrl_poll_work_q, &work_ctlr_poll);
@@ -64,6 +66,8 @@ static void ctlr_poll_timer_handler(struct k_timer *timer_id)
static void wdt_timeout_cb(int channel_id, void *user_data)
{
+ ARG_UNUSED(channel_id);
+ ARG_UNUSED(user_data);
ERR_CHK_MSG(-ETIMEDOUT, "No response from IPC or controller");
}
@@ -139,4 +143,4 @@ int bt_mgmt_stop_watchdog(void)
k_timer_stop(&ctlr_poll_timer);
int ret = task_wdt_delete(wdt_ch_id);
return ret;
-}
\ No newline at end of file
+}
diff --git a/src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c b/src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c
index 8c50cd94..d1560b9a 100644
--- a/src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c
+++ b/src/bluetooth/bt_management/dfu/bt_mgmt_dfu.c
@@ -55,11 +55,14 @@ static void smp_adv(void)
/* These callbacks are to override callback registed in module le_audio_ */
static void dfu_connected_cb(struct bt_conn *conn, uint8_t err)
{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(err);
LOG_INF("SMP connected\n");
}
static void dfu_disconnected_cb(struct bt_conn *conn, uint8_t reason)
{
+ ARG_UNUSED(conn);
LOG_INF("SMP disconnected 0x%02x\n", reason);
}
diff --git a/src/bluetooth/bt_management/scanning/bt_mgmt_scan.c b/src/bluetooth/bt_management/scanning/bt_mgmt_scan.c
index 5466c1b0..94cd2567 100644
--- a/src/bluetooth/bt_management/scanning/bt_mgmt_scan.c
+++ b/src/bluetooth/bt_management/scanning/bt_mgmt_scan.c
@@ -55,9 +55,13 @@ int bt_mgmt_scan_start(uint16_t scan_intvl, uint16_t scan_win, enum bt_mgmt_scan
scan_window = scan_win;
}
- struct bt_le_scan_param *scan_param =
+ struct bt_le_scan_param *scan_param = NULL;
+
+#if defined(CONFIG_BT_CENTRAL) || defined(CONFIG_BT_BAP_BROADCAST_SINK)
+ scan_param =
BT_LE_SCAN_PARAM(NRF5340_AUDIO_GATEWAY_SCAN_TYPE, BT_LE_SCAN_OPT_FILTER_DUPLICATE,
scan_interval, scan_window);
+#endif
if (type == BT_MGMT_SCAN_TYPE_CONN && IS_ENABLED(CONFIG_BT_CENTRAL)) {
ret = bt_mgmt_scan_for_conn_start(scan_param, srch_name);
diff --git a/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c
index 8f1f952f..578abaf6 100644
--- a/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c
+++ b/src/bluetooth/bt_rendering_and_capture/volume/bt_vol_rend.c
@@ -24,6 +24,7 @@ LOG_MODULE_REGISTER(bt_vol_rend, CONFIG_BT_VOL_LOG_LEVEL);
*/
static void vcs_state_rend_cb_handler(struct bt_conn *conn, int err, uint8_t volume, uint8_t mute)
{
+ ARG_UNUSED(conn);
int ret;
static uint8_t prev_mute = 0;
@@ -63,6 +64,7 @@ static void vcs_state_rend_cb_handler(struct bt_conn *conn, int err, uint8_t vol
*/
static void vcs_flags_rend_cb_handler(struct bt_conn *conn, int err, uint8_t flags)
{
+ ARG_UNUSED(conn);
if (err) {
LOG_ERR("VCS flag callback error: %d", err);
} else {
diff --git a/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c b/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c
index 41608e60..81550475 100644
--- a/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c
+++ b/src/bluetooth/bt_stream/bt_le_audio_tx/bt_le_audio_tx.c
@@ -96,10 +96,6 @@ static int iso_stream_send(uint8_t const *const data, size_t size, struct bt_cap
*/
if (atomic_get(&tx_info->iso_tx_pool_alloc) >= HCI_ISO_BUF_PER_CHAN) {
if (!tx_info->hci_wrn_printed) {
- struct bt_iso_chan *iso_chan;
-
- iso_chan = bt_bap_stream_iso_chan_get(&cap_stream->bap_stream);
-
LOG_WRN("HCI ISO TX overrun on stream %p - Single print",
(void *)&cap_stream->bap_stream);
tx_info->hci_wrn_printed = true;
diff --git a/src/bluetooth/bt_stream/unicast/unicast_server.c b/src/bluetooth/bt_stream/unicast/unicast_server.c
index c1aa6384..cd01b88c 100644
--- a/src/bluetooth/bt_stream/unicast/unicast_server.c
+++ b/src/bluetooth/bt_stream/unicast/unicast_server.c
@@ -6,6 +6,8 @@
#include "unicast_server.h"
+ #include
+ #include
#include
#include
#include
@@ -20,7 +22,9 @@
#include "zbus_common.h"
#include "bt_mgmt.h"
#include "bt_le_audio_tx.h"
+ #include "channel_assignment.h"
#include "le_audio.h"
+ #include "uicr.h"
#include "BootState.h"
@@ -98,15 +102,18 @@ static uint8_t device_identifier[] = {
}
/* Callback for locking state change from server side */
- static void csip_lock_changed_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip,
- bool locked)
- {
+static void csip_lock_changed_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip_instance,
+ bool locked)
+{
+ ARG_UNUSED(csip_instance);
LOG_DBG("Client %p %s the lock", (void *)conn, locked ? "locked" : "released");
}
/* Callback for SIRK read request from peer side */
- static uint8_t sirk_read_req_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip)
- {
+static uint8_t sirk_read_req_cb(struct bt_conn *conn, struct bt_csip_set_member_svc_inst *csip_instance)
+{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(csip_instance);
/* Accept the request to read the SIRK, but return encrypted SIRK instead of plaintext */
return BT_CSIP_READ_SIRK_REQ_RSP_ACCEPT_ENC;
}
@@ -177,11 +184,13 @@ static uint8_t device_identifier[] = {
static int lc3_config_cb(struct bt_conn *conn, const struct bt_bap_ep *ep, enum bt_audio_dir dir,
const struct bt_audio_codec_cfg *codec, struct bt_bap_stream **stream,
struct bt_bap_qos_cfg_pref *const pref, struct bt_bap_ascs_rsp *rsp)
- {
+{
int ret;
+ ARG_UNUSED(ep);
+ ARG_UNUSED(rsp);
LOG_DBG("LC3 config callback");
- for (int i = 0; i < ARRAY_SIZE(cap_audio_streams); i++) {
+ for (size_t i = 0; i < ARRAY_SIZE(cap_audio_streams); i++) {
struct bt_cap_stream *cap_audio_stream = &cap_audio_streams[i];
if (!cap_audio_stream->bap_stream.conn) {
@@ -233,7 +242,11 @@ static uint8_t device_identifier[] = {
static int lc3_reconfig_cb(struct bt_bap_stream *stream, enum bt_audio_dir dir,
const struct bt_audio_codec_cfg *codec,
struct bt_bap_qos_cfg_pref *const pref, struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(dir);
+ ARG_UNUSED(codec);
+ ARG_UNUSED(pref);
+ ARG_UNUSED(rsp);
LOG_DBG("ASE Codec Reconfig: stream %p", (void *)stream);
return 0;
@@ -241,7 +254,8 @@ static uint8_t device_identifier[] = {
static int lc3_qos_cb(struct bt_bap_stream *stream, const struct bt_bap_qos_cfg *qos,
struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(rsp);
enum bt_audio_dir dir;
dir = le_audio_stream_dir_get(stream);
@@ -259,27 +273,33 @@ static uint8_t device_identifier[] = {
static int lc3_enable_cb(struct bt_bap_stream *stream, const uint8_t *meta, size_t meta_len,
struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(meta);
+ ARG_UNUSED(rsp);
LOG_DBG("Enable: stream %p meta_len %d", (void *)stream, meta_len);
return 0;
}
static int lc3_start_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(rsp);
LOG_DBG("Start stream %p", (void *)stream);
return 0;
}
static int lc3_metadata_cb(struct bt_bap_stream *stream, const uint8_t *meta, size_t meta_len,
struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(meta);
+ ARG_UNUSED(rsp);
LOG_DBG("Metadata: stream %p meta_len %d", (void *)stream, meta_len);
return 0;
}
static int lc3_disable_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(rsp);
enum bt_audio_dir dir;
dir = le_audio_stream_dir_get(stream);
@@ -296,7 +316,8 @@ static uint8_t device_identifier[] = {
}
static int lc3_stop_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(rsp);
enum bt_audio_dir dir;
dir = le_audio_stream_dir_get(stream);
@@ -313,7 +334,8 @@ static uint8_t device_identifier[] = {
}
static int lc3_release_cb(struct bt_bap_stream *stream, struct bt_bap_ascs_rsp *rsp)
- {
+{
+ ARG_UNUSED(rsp);
enum bt_audio_dir dir;
dir = le_audio_stream_dir_get(stream);
@@ -364,6 +386,7 @@ static uint8_t device_identifier[] = {
#if (CONFIG_BT_AUDIO_TX)
static void stream_sent_cb(struct bt_bap_stream *stream)
{
+ ARG_UNUSED(stream);
/* Unicast server/CIS headset only supports one source stream for now */
struct stream_index idx = {
.lvl1 = 0,
@@ -397,8 +420,9 @@ static uint8_t device_identifier[] = {
}
}
- static void stream_disabled_cb(struct bt_bap_stream *stream)
- {
+static void stream_disabled_cb(struct bt_bap_stream *stream)
+{
+ ARG_UNUSED(stream);
LOG_INF("Stream %p disabled", stream);
}
@@ -441,8 +465,9 @@ static uint8_t device_identifier[] = {
le_audio_event_publish(LE_AUDIO_EVT_NOT_STREAMING, stream->conn, dir);
}
- static void stream_released_cb(struct bt_bap_stream *stream)
- {
+static void stream_released_cb(struct bt_bap_stream *stream)
+{
+ ARG_UNUSED(stream);
/* NOTE: The string below is used by the Nordic CI system */
LOG_INF("Stream %p released", stream);
}
@@ -464,6 +489,7 @@ static uint8_t device_identifier[] = {
int unicast_server_config_get(struct bt_conn *conn, enum bt_audio_dir dir, uint32_t *bitrate,
uint32_t *sampling_rate_hz, uint32_t *pres_delay_us)
{
+ ARG_UNUSED(conn);
int ret;
if (bitrate == NULL && sampling_rate_hz == NULL && pres_delay_us == NULL) {
@@ -630,7 +656,7 @@ static uint8_t device_identifier[] = {
struct le_audio_tx_info tx[CONFIG_BT_ASCS_MAX_ASE_SRC_COUNT];
- for (int i = 0; i < ARRAY_SIZE(cap_tx_streams); i++) {
+ for (size_t i = 0; i < ARRAY_SIZE(cap_tx_streams); i++) {
if (!le_audio_ep_state_check(cap_tx_streams[i]->bap_stream.ep,
BT_BAP_EP_STATE_STREAMING)) {
continue;
@@ -708,7 +734,8 @@ static uint8_t device_identifier[] = {
if (sirk != 0xFFFFFFFF) {
snprintf(sirk_string, 16, "%08X", sirk); //"%016llX"
} else {
- snprintf(sirk_string, 16, "%08X", oe_boot_state.device_id); //"%016llX"
+ snprintf(sirk_string, 16, "%08X",
+ (unsigned int)oe_boot_state.device_id); //"%016llX"
}
// LOG_INF("SIRK as String: %s", sirk_string);
@@ -719,7 +746,7 @@ static uint8_t device_identifier[] = {
// memcpy(csip_param.sirk, CONFIG_BT_SET_IDENTITY_RESOLVING_KEY, BT_CSIP_SIRK_SIZE);
}
- for (int i = 0; i < ARRAY_SIZE(caps); i++) {
+ for (size_t i = 0; i < ARRAY_SIZE(caps); i++) {
ret = bt_pacs_cap_register(caps_dirs[i], &caps[i]);
if (ret) {
LOG_ERR("Capability register failed. Err: %d", ret);
@@ -780,7 +807,7 @@ static uint8_t device_identifier[] = {
return ret;
}
- for (int i = 0; i < ARRAY_SIZE(cap_audio_streams); i++) {
+ for (size_t i = 0; i < ARRAY_SIZE(cap_audio_streams); i++) {
bt_cap_stream_ops_register(&cap_audio_streams[i], &stream_ops);
}
@@ -802,4 +829,3 @@ static uint8_t device_identifier[] = {
return 0;
}
-
\ No newline at end of file
diff --git a/src/bluetooth/gatt_services/CMakeLists.txt b/src/bluetooth/gatt_services/CMakeLists.txt
index 141acb9e..d74ba9b3 100644
--- a/src/bluetooth/gatt_services/CMakeLists.txt
+++ b/src/bluetooth/gatt_services/CMakeLists.txt
@@ -6,4 +6,5 @@ target_sources(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/audio_config_service.c
${CMAKE_CURRENT_SOURCE_DIR}/power_saving_service.c
${CMAKE_CURRENT_SOURCE_DIR}/led_service.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/audio_response_service.c
)
diff --git a/src/bluetooth/gatt_services/audio_config_service.c b/src/bluetooth/gatt_services/audio_config_service.c
index d0a8513a..c8a53e10 100644
--- a/src/bluetooth/gatt_services/audio_config_service.c
+++ b/src/bluetooth/gatt_services/audio_config_service.c
@@ -12,6 +12,10 @@ LOG_MODULE_REGISTER(audio_config_service, CONFIG_BLE_LOG_LEVEL);
static ssize_t write_audio_mode(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(offset);
+ ARG_UNUSED(flags);
if (len != sizeof(uint8_t)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
@@ -21,13 +25,21 @@ static ssize_t write_audio_mode(struct bt_conn *conn, const struct bt_gatt_attr
return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
}
- hw_codec_set_audio_mode((enum audio_mode)mode);
+ int ret = hw_codec_set_audio_mode((enum audio_mode)mode);
+ if (ret) {
+ return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
+ }
+
return len;
}
static ssize_t write_mic_select(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(offset);
+ ARG_UNUSED(flags);
if (len != sizeof(uint8_t)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
@@ -69,7 +81,52 @@ static ssize_t read_audio_channel(struct bt_conn *conn, const struct bt_gatt_att
channel_assignment_get(&channel);
uint8_t channel_u8 = channel;
- return bt_gatt_attr_read(conn, attr, buf, len, offset, &channel_u8, sizeof(channel));
+ return bt_gatt_attr_read(conn, attr, buf, len, offset, &channel_u8, sizeof(channel_u8));
+}
+
+static ssize_t write_dmic_gain(struct bt_conn *conn, const struct bt_gatt_attr *attr,
+ const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
+{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(offset);
+ ARG_UNUSED(flags);
+ // Mic gain is 2 bytes: [outer_reg, inner_reg].
+ // Outer mic maps to DMIC_VOL0; inner mic maps to DMIC_VOL1.
+ // Per ADAU186x DMIC_VOL register (0x4000C045):
+ // 0x00 = +24 dB
+ // 0x01-0x3F = +23.625 to +0.375 dB (0.375 dB steps)
+ // 0x40 = 0 dB
+ // 0x41-0xFD = -0.375 to -70.875 dB (0.375 dB steps)
+ // 0xFE = -71.25 dB
+ // 0xFF = Mute
+ if (len != 2) {
+ return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
+ }
+
+ uint8_t gain_outer = ((uint8_t*)buf)[0];
+ uint8_t gain_inner = ((uint8_t*)buf)[1];
+
+ int ret = hw_codec_mic_gain_set(gain_outer, gain_inner);
+ if (ret) {
+ LOG_ERR("Failed to set mic gain: %d", ret);
+ return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
+ }
+
+ LOG_INF("DMIC gain via BLE: outer=0x%02x inner=0x%02x",
+ gain_outer, gain_inner);
+ return len;
+}
+
+static ssize_t read_dmic_gain(struct bt_conn *conn, const struct bt_gatt_attr *attr,
+ void *buf, uint16_t len, uint16_t offset)
+{
+ uint8_t gains[2] = {
+ hw_codec_mic_gain_get_outer(),
+ hw_codec_mic_gain_get_inner()
+ };
+
+ return bt_gatt_attr_read(conn, attr, buf, len, offset, gains, sizeof(gains));
}
BT_GATT_SERVICE_DEFINE(audio_config_svc,
@@ -86,6 +143,10 @@ BT_GATT_SERVICE_DEFINE(audio_config_svc,
BT_GATT_CHRC_READ,
BT_GATT_PERM_READ,
read_audio_channel, NULL, NULL),
+ BT_GATT_CHARACTERISTIC(BT_UUID_DMIC_GAIN,
+ BT_GATT_CHRC_WRITE | BT_GATT_CHRC_READ,
+ BT_GATT_PERM_WRITE | BT_GATT_PERM_READ,
+ read_dmic_gain, write_dmic_gain, NULL),
);
int init_audio_config_service(void)
diff --git a/src/bluetooth/gatt_services/audio_config_service.h b/src/bluetooth/gatt_services/audio_config_service.h
index 71b69261..40b696be 100644
--- a/src/bluetooth/gatt_services/audio_config_service.h
+++ b/src/bluetooth/gatt_services/audio_config_service.h
@@ -33,6 +33,13 @@
#define BT_UUID_AUDIO_CHANNEL \
BT_UUID_DECLARE_128(BT_UUID_AUDIO_CHANNEL_VAL)
+// DMIC Gain Characteristic UUID
+#define BT_UUID_DMIC_GAIN_VAL \
+ BT_UUID_128_ENCODE(0x1410df99, 0x5f68, 0x4ebb, 0xa7c7, 0x5e0fb9ae7557)
+
+#define BT_UUID_DMIC_GAIN \
+ BT_UUID_DECLARE_128(BT_UUID_DMIC_GAIN_VAL)
+
int init_audio_config_service(void);
#endif /* _AUDIO_CONFIG_SERVICE_H_ */
diff --git a/src/bluetooth/gatt_services/audio_response_service.c b/src/bluetooth/gatt_services/audio_response_service.c
new file mode 100644
index 00000000..921ae3b7
--- /dev/null
+++ b/src/bluetooth/gatt_services/audio_response_service.c
@@ -0,0 +1,919 @@
+#include "audio_response_service.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "audio_datapath.h"
+#include "audio_system.h"
+#include "hw_codec.h"
+
+LOG_MODULE_REGISTER(audio_response_service, CONFIG_LOG_DEFAULT_LEVEL);
+
+#define AUDIO_RESPONSE_CAPTURE_SAMPLES 2048
+#define AUDIO_RESPONSE_CAPTURE_SAMPLE_RATE 4000U
+#define AUDIO_RESPONSE_INITIAL_DROP 128
+#define AUDIO_RESPONSE_DEFAULT_POINTS 9
+#define AUDIO_RESPONSE_TRANSFER_CREDITS 1
+#define AUDIO_RESPONSE_TRANSFER_CREDIT_DELAY K_MSEC(1)
+#define AUDIO_RESPONSE_TRANSFER_TIMEOUT K_SECONDS(30)
+#define AUDIO_RESPONSE_STATUS_ENCODED_SIZE 9
+#define AUDIO_RESPONSE_RESULT_ENCODED_SIZE (2 + (CONFIG_AUDIO_RESPONSE_MAX_POINTS * 4))
+#define AUDIO_RESPONSE_TWO_PI 6.28318530717958647692f
+
+BUILD_ASSERT(CONFIG_AUDIO_RESPONSE_MAX_POINTS >= AUDIO_RESPONSE_DEFAULT_POINTS,
+ "Default audio response points must fit CONFIG_AUDIO_RESPONSE_MAX_POINTS");
+
+enum audio_response_transfer_status {
+ AUDIO_RESPONSE_TRANSFER_READY = 0,
+ AUDIO_RESPONSE_TRANSFER_COMMITTED = 1,
+ AUDIO_RESPONSE_TRANSFER_ABORTED = 2,
+ AUDIO_RESPONSE_TRANSFER_INVALID_STATE = 3,
+ AUDIO_RESPONSE_TRANSFER_INVALID_CHUNK = 4,
+ AUDIO_RESPONSE_TRANSFER_INSUFFICIENT_STORAGE = 5,
+ AUDIO_RESPONSE_TRANSFER_CHECKSUM_MISMATCH = 6,
+ AUDIO_RESPONSE_TRANSFER_TIMED_OUT = 7,
+};
+
+struct audio_response_transfer {
+ uint16_t id;
+ uint32_t total_samples;
+ uint32_t sampling_rate;
+ uint32_t expected_checksum;
+ uint32_t received_samples;
+ int16_t *samples;
+ bool active;
+ bool committed;
+};
+
+struct audio_response_audio_session {
+ bool audio_system_suspended;
+ bool auxiliary_audio_suspended;
+ bool datapath_acquired;
+ bool measurement_codec_enabled;
+ bool measurement_playback_started;
+};
+
+/**
+ * Carries the GATT write metadata and result through the generated union dispatcher.
+ */
+struct transfer_control_dispatch_context {
+ uint16_t write_len;
+ ssize_t result;
+};
+
+static const uint16_t default_frequencies[AUDIO_RESPONSE_DEFAULT_POINTS] = {
+ 40, 60, 90, 135, 203, 304, 456, 683, 1025,
+};
+
+static int16_t captured_samples[AUDIO_RESPONSE_CAPTURE_SAMPLES];
+static int16_t uploaded_samples[CONFIG_AUDIO_RESPONSE_MAX_SAMPLES];
+static uint16_t requested_frequencies[CONFIG_AUDIO_RESPONSE_MAX_POINTS];
+static uint16_t decoded_config_frequencies[UINT8_MAX];
+static uint16_t result_frequencies[CONFIG_AUDIO_RESPONSE_MAX_POINTS];
+static uint16_t result_response[CONFIG_AUDIO_RESPONSE_MAX_POINTS];
+static uint8_t result_payload[AUDIO_RESPONSE_RESULT_ENCODED_SIZE];
+
+static struct audio_response_transfer transfer;
+static struct audio_response_audio_session audio_session;
+static audio_response_config_t pending_config;
+static bool measurement_active;
+static bool transfer_status_notifications_enabled;
+static bool result_notifications_enabled;
+
+static struct k_work measurement_work;
+static struct k_work measurement_complete_work;
+static struct k_work_delayable transfer_ready_work;
+static struct k_work_delayable transfer_timeout_work;
+K_MUTEX_DEFINE(service_mutex);
+
+extern struct data_fifo fifo_rx;
+extern const struct bt_gatt_service_static audio_response_svc;
+
+static int notify_transfer_status(enum audio_response_transfer_status status, uint16_t credits);
+static uint32_t response_frequency_to_bin(uint16_t frequency);
+static bool is_valid_response_frequency(uint16_t frequency);
+static uint16_t response_magnitude_for_bin(const int16_t *samples, size_t sample_count,
+ uint32_t bin);
+static int select_response_points(audio_response_config_t *config);
+static void reset_transfer(void);
+static int suspend_audio_for_measurement(void);
+static void restore_audio_after_measurement(void);
+
+/**
+ * Return a readable transfer status name for logs.
+ */
+static const char *transfer_status_name(enum audio_response_transfer_status status)
+{
+ switch (status) {
+ case AUDIO_RESPONSE_TRANSFER_READY:
+ return "ready";
+ case AUDIO_RESPONSE_TRANSFER_COMMITTED:
+ return "committed";
+ case AUDIO_RESPONSE_TRANSFER_ABORTED:
+ return "aborted";
+ case AUDIO_RESPONSE_TRANSFER_INVALID_STATE:
+ return "invalid_state";
+ case AUDIO_RESPONSE_TRANSFER_INVALID_CHUNK:
+ return "invalid_chunk";
+ case AUDIO_RESPONSE_TRANSFER_INSUFFICIENT_STORAGE:
+ return "insufficient_storage";
+ case AUDIO_RESPONSE_TRANSFER_CHECKSUM_MISMATCH:
+ return "checksum_mismatch";
+ case AUDIO_RESPONSE_TRANSFER_TIMED_OUT:
+ return "timed_out";
+ default:
+ return "unknown";
+ }
+}
+
+/**
+ * Return a readable transfer-control command name for logs.
+ */
+static const char *transfer_control_type_name(audio_response_transfer_control_type_t type)
+{
+ switch (type) {
+ case AUDIO_RESPONSE_TRANSFER_CONTROL_START:
+ return "start";
+ case AUDIO_RESPONSE_TRANSFER_CONTROL_COMMIT:
+ return "commit";
+ case AUDIO_RESPONSE_TRANSFER_CONTROL_ABORT:
+ return "abort";
+ default:
+ return "unknown";
+ }
+}
+
+/**
+ * Return the nearest analysis bin for a requested response frequency.
+ */
+static uint32_t response_frequency_to_bin(uint16_t frequency)
+{
+ return ((uint32_t)frequency * AUDIO_RESPONSE_CAPTURE_SAMPLES +
+ AUDIO_RESPONSE_CAPTURE_SAMPLE_RATE / 2) /
+ AUDIO_RESPONSE_CAPTURE_SAMPLE_RATE;
+}
+
+/**
+ * Validate that a requested response frequency maps to a captured analysis bin.
+ */
+static bool is_valid_response_frequency(uint16_t frequency)
+{
+ return response_frequency_to_bin(frequency) < (AUDIO_RESPONSE_CAPTURE_SAMPLES / 2);
+}
+
+/**
+ * Estimate a single analysis-bin magnitude using the Goertzel recurrence.
+ *
+ * The returned value is normalized into the same unsigned Q15-like range used by
+ * the protocol result. Non-DC bins are scaled by 2 / N so a full-scale sine near
+ * the target bin reports close to INT16_MAX.
+ */
+static uint16_t response_magnitude_for_bin(const int16_t *samples, size_t sample_count,
+ uint32_t bin)
+{
+ const float omega = AUDIO_RESPONSE_TWO_PI * (float)bin / (float)sample_count;
+ const float coefficient = 2.0f * cosf(omega);
+ float previous = 0.0f;
+ float previous2 = 0.0f;
+
+ for (size_t index = 0; index < sample_count; ++index) {
+ const float current = (float)samples[index] + coefficient * previous - previous2;
+
+ previous2 = previous;
+ previous = current;
+ }
+
+ float power = previous2 * previous2 + previous * previous -
+ coefficient * previous * previous2;
+
+ if (power < 0.0f) {
+ power = 0.0f;
+ }
+
+ const float scale = (bin == 0U) ? (1.0f / (float)sample_count) :
+ (2.0f / (float)sample_count);
+ const float magnitude = sqrtf(power) * scale;
+
+ if (magnitude >= (float)INT16_MAX) {
+ return INT16_MAX;
+ }
+
+ return (uint16_t)(magnitude + 0.5f);
+}
+
+/**
+ * Copy the requested response points into service-owned storage.
+ *
+ * A zero-point request selects the firmware default table. Nonzero requests
+ * use the protocol-provided frequency list after validating that every
+ * frequency maps to the captured response range.
+ */
+static int select_response_points(audio_response_config_t *config)
+{
+ const uint16_t *frequencies = config->frequencies;
+ uint8_t points = config->points;
+
+ if (points == 0) {
+ points = ARRAY_SIZE(default_frequencies);
+ frequencies = default_frequencies;
+ LOG_DBG("Using default audio response points: points=%u", points);
+ } else if (points > CONFIG_AUDIO_RESPONSE_MAX_POINTS) {
+ LOG_WRN("Audio response config rejected: points=%u max_points=%u", points,
+ CONFIG_AUDIO_RESPONSE_MAX_POINTS);
+ return -EINVAL;
+ }
+
+ for (size_t index = 0; index < points; ++index) {
+ if (!is_valid_response_frequency(frequencies[index])) {
+ LOG_WRN("Audio response config rejected: frequency=%u index=%u capture_rate=%u",
+ frequencies[index], index, AUDIO_RESPONSE_CAPTURE_SAMPLE_RATE);
+ return -EINVAL;
+ }
+ requested_frequencies[index] = frequencies[index];
+ }
+
+ config->points = points;
+ config->frequencies = requested_frequencies;
+ return 0;
+}
+
+/**
+ * Clear all transfer metadata and make the upload buffer reusable.
+ */
+static void reset_transfer(void)
+{
+ LOG_DBG("Reset transfer state: id=%u active=%d committed=%d received=%u/%u", transfer.id,
+ transfer.active, transfer.committed, transfer.received_samples, transfer.total_samples);
+ k_work_cancel_delayable(&transfer_ready_work);
+ memset(&transfer, 0, sizeof(transfer));
+}
+
+/**
+ * Notify connected clients about the current upload state.
+ */
+static int notify_transfer_status(enum audio_response_transfer_status status, uint16_t credits)
+{
+ audio_response_transfer_status_t message = {
+ .transfer_id = transfer.id,
+ .status = status,
+ .next_sample_offset = transfer.received_samples,
+ .credits = credits,
+ };
+ uint8_t payload[AUDIO_RESPONSE_STATUS_ENCODED_SIZE];
+ size_t payload_size;
+
+ if (!transfer_status_notifications_enabled) {
+ LOG_DBG("Skipping transfer status notification: status=%s id=%u notifications=disabled",
+ transfer_status_name(status), transfer.id);
+ return -EACCES;
+ }
+
+ if (audio_response_transfer_status_encode(&message, payload, sizeof(payload), &payload_size) !=
+ PROTOCOL_OK) {
+ LOG_ERR("Failed to encode transfer status: status=%s id=%u offset=%u credits=%u",
+ transfer_status_name(status), transfer.id, transfer.received_samples, credits);
+ return -EMSGSIZE;
+ }
+
+ int ret = bt_gatt_notify(NULL, &audio_response_svc.attrs[6], payload, payload_size);
+ if (ret != 0) {
+ LOG_WRN("Failed to notify transfer status: status=%s id=%u err=%d",
+ transfer_status_name(status), transfer.id, ret);
+ return ret;
+ }
+
+ LOG_DBG("Notified transfer status: status=%s id=%u offset=%u credits=%u bytes=%u",
+ transfer_status_name(status), transfer.id, transfer.received_samples, credits,
+ payload_size);
+ return 0;
+}
+
+/**
+ * Reject a transfer operation and report the supplied protocol status.
+ */
+static ssize_t reject_transfer(enum audio_response_transfer_status status, ssize_t att_error)
+{
+ LOG_WRN("Rejecting transfer operation: status=%s id=%u active=%d committed=%d received=%u/%u att_error=%zd",
+ transfer_status_name(status), transfer.id, transfer.active, transfer.committed,
+ transfer.received_samples, transfer.total_samples, att_error);
+ (void)notify_transfer_status(status, 0);
+ return att_error;
+}
+
+/**
+ * Start a new audio-buffer upload.
+ */
+static ssize_t start_transfer(const audio_response_transfer_start_t *start, uint16_t write_len)
+{
+ LOG_INF("Transfer start requested: id=%u samples=%u rate=%u checksum=0x%08x",
+ start->transfer_id, start->total_samples, start->sampling_rate, start->checksum);
+
+ if (measurement_active || (transfer.active && !transfer.committed) ||
+ start->total_samples == 0 ||
+ start->sampling_rate != CONFIG_AUDIO_SAMPLE_RATE_HZ) {
+ LOG_WRN("Transfer start rejected: measurement_active=%d active=%d committed=%d samples=%u rate=%u expected_rate=%u",
+ measurement_active, transfer.active, transfer.committed, start->total_samples,
+ start->sampling_rate, CONFIG_AUDIO_SAMPLE_RATE_HZ);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_STATE,
+ BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED));
+ }
+
+ if (start->total_samples > CONFIG_AUDIO_RESPONSE_MAX_SAMPLES) {
+ transfer.id = start->transfer_id;
+ LOG_WRN("Transfer start rejected: samples=%u max_samples=%u", start->total_samples,
+ CONFIG_AUDIO_RESPONSE_MAX_SAMPLES);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INSUFFICIENT_STORAGE,
+ BT_GATT_ERR(BT_ATT_ERR_INSUFFICIENT_RESOURCES));
+ }
+
+ reset_transfer();
+ transfer.id = start->transfer_id;
+ transfer.total_samples = start->total_samples;
+ transfer.sampling_rate = start->sampling_rate;
+ transfer.expected_checksum = start->checksum;
+ transfer.samples = uploaded_samples;
+ transfer.active = true;
+ k_work_reschedule(&transfer_timeout_work, AUDIO_RESPONSE_TRANSFER_TIMEOUT);
+ (void)notify_transfer_status(AUDIO_RESPONSE_TRANSFER_READY, AUDIO_RESPONSE_TRANSFER_CREDITS);
+
+ LOG_INF("Transfer started: id=%u samples=%u timeout_ms=%lld", transfer.id,
+ transfer.total_samples, k_ticks_to_ms_floor64(AUDIO_RESPONSE_TRANSFER_TIMEOUT.ticks));
+ return write_len;
+}
+
+/**
+ * Validate and retain a completely uploaded audio buffer.
+ */
+static ssize_t commit_transfer(const audio_response_transfer_commit_t *commit, uint16_t write_len)
+{
+ LOG_INF("Transfer commit requested: id=%u received=%u/%u", commit->transfer_id,
+ transfer.received_samples, transfer.total_samples);
+
+ if (!transfer.active || transfer.committed || commit->transfer_id != transfer.id ||
+ transfer.received_samples != transfer.total_samples) {
+ LOG_WRN("Transfer commit rejected: requested_id=%u current_id=%u active=%d committed=%d received=%u/%u",
+ commit->transfer_id, transfer.id, transfer.active, transfer.committed,
+ transfer.received_samples, transfer.total_samples);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_STATE,
+ BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED));
+ }
+
+ uint32_t checksum =
+ crc32_ieee((const uint8_t *)transfer.samples, transfer.total_samples * sizeof(int16_t));
+ if (checksum != transfer.expected_checksum) {
+ LOG_WRN("Transfer checksum mismatch: id=%u expected=0x%08x actual=0x%08x",
+ transfer.id, transfer.expected_checksum, checksum);
+ (void)notify_transfer_status(AUDIO_RESPONSE_TRANSFER_CHECKSUM_MISMATCH, 0);
+ reset_transfer();
+ return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
+ }
+
+ transfer.committed = true;
+ k_work_cancel_delayable(&transfer_timeout_work);
+ (void)notify_transfer_status(AUDIO_RESPONSE_TRANSFER_COMMITTED, 0);
+ LOG_INF("Transfer committed: id=%u samples=%u checksum=0x%08x", transfer.id,
+ transfer.total_samples, checksum);
+ return write_len;
+}
+
+/**
+ * Abort the active or committed upload.
+ */
+static ssize_t abort_transfer(const audio_response_transfer_abort_t *abort, uint16_t write_len)
+{
+ LOG_INF("Transfer abort requested: id=%u", abort->transfer_id);
+
+ if ((!transfer.active && !transfer.committed) || abort->transfer_id != transfer.id ||
+ measurement_active) {
+ LOG_WRN("Transfer abort rejected: requested_id=%u current_id=%u active=%d committed=%d measurement_active=%d",
+ abort->transfer_id, transfer.id, transfer.active, transfer.committed,
+ measurement_active);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_STATE,
+ BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED));
+ }
+
+ (void)notify_transfer_status(AUDIO_RESPONSE_TRANSFER_ABORTED, 0);
+ k_work_cancel_delayable(&transfer_timeout_work);
+ LOG_INF("Transfer aborted: id=%u received=%u/%u", transfer.id, transfer.received_samples,
+ transfer.total_samples);
+ reset_transfer();
+ return write_len;
+}
+
+/**
+ * Handle a typed transfer-start command dispatched by the generated protocol API.
+ */
+static protocol_status_t dispatch_transfer_start(void *context,
+ const audio_response_transfer_start_t *start)
+{
+ struct transfer_control_dispatch_context *dispatch_context = context;
+
+ dispatch_context->result = start_transfer(start, dispatch_context->write_len);
+ return PROTOCOL_OK;
+}
+
+/**
+ * Handle a typed transfer-commit command dispatched by the generated protocol API.
+ */
+static protocol_status_t dispatch_transfer_commit(void *context,
+ const audio_response_transfer_commit_t *commit)
+{
+ struct transfer_control_dispatch_context *dispatch_context = context;
+
+ dispatch_context->result = commit_transfer(commit, dispatch_context->write_len);
+ return PROTOCOL_OK;
+}
+
+/**
+ * Handle a typed transfer-abort command dispatched by the generated protocol API.
+ */
+static protocol_status_t dispatch_transfer_abort(void *context,
+ const audio_response_transfer_abort_t *abort)
+{
+ struct transfer_control_dispatch_context *dispatch_context = context;
+
+ dispatch_context->result = abort_transfer(abort, dispatch_context->write_len);
+ return PROTOCOL_OK;
+}
+
+/**
+ * Decode and execute a transfer-control command.
+ */
+static ssize_t write_transfer_control(struct bt_conn *conn, const struct bt_gatt_attr *attr,
+ const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
+{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(flags);
+ static const audio_response_transfer_control_handler_t handlers = {
+ .start = dispatch_transfer_start,
+ .commit = dispatch_transfer_commit,
+ .abort = dispatch_transfer_abort,
+ };
+ audio_response_transfer_control_t control;
+ struct transfer_control_dispatch_context dispatch_context = {
+ .write_len = len,
+ .result = BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED),
+ };
+ size_t bytes_read = 0;
+ protocol_status_t status;
+
+ if (offset != 0) {
+ return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
+ }
+
+ status = audio_response_transfer_control_decode(&control, buf, len, &bytes_read);
+ if (status != PROTOCOL_OK || bytes_read != len) {
+ LOG_ERR("Failed to decode transfer control command: status=%d bytes_read=%u len=%u",
+ status, bytes_read, len);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_STATE,
+ BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN));
+ }
+
+ LOG_DBG("Decoded transfer control command: type=%s len=%u",
+ transfer_control_type_name(control.type), len);
+
+ k_mutex_lock(&service_mutex, K_FOREVER);
+ status = audio_response_transfer_control_dispatch(&control, &handlers, &dispatch_context);
+ k_mutex_unlock(&service_mutex);
+
+ if (status != PROTOCOL_OK) {
+ LOG_ERR("Failed to dispatch transfer control command: type=%s status=%d",
+ transfer_control_type_name(control.type), status);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_STATE,
+ BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED));
+ }
+ LOG_DBG("Handled transfer control command: type=%s result=%zd",
+ transfer_control_type_name(control.type), dispatch_context.result);
+ return dispatch_context.result;
+}
+
+/**
+ * Decode and append one contiguous chunk to the active transfer.
+ */
+static ssize_t write_transfer_data(struct bt_conn *conn, const struct bt_gatt_attr *attr,
+ const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
+{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(flags);
+ protocol_reader_t reader = { .buffer = buf, .size = len, .offset = 0 };
+ uint16_t transfer_id = 0;
+ uint32_t sample_offset = 0;
+ uint16_t sample_count = 0;
+
+ if (offset != 0) {
+ LOG_WRN("Transfer chunk rejected: invalid offset=%u len=%u", offset, len);
+ return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
+ }
+ if (protocol_read_uint16(&reader, &transfer_id) != PROTOCOL_OK ||
+ protocol_read_uint32(&reader, &sample_offset) != PROTOCOL_OK ||
+ protocol_read_uint16(&reader, &sample_count) != PROTOCOL_OK ||
+ reader.size - reader.offset != sample_count * sizeof(int16_t)) {
+ LOG_WRN("Transfer chunk decode failed: len=%u bytes_remaining=%u sample_count=%u",
+ len, reader.size - reader.offset, sample_count);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_CHUNK,
+ BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN));
+ }
+
+ LOG_DBG("Transfer chunk received: id=%u offset=%u samples=%u len=%u", transfer_id,
+ sample_offset, sample_count, len);
+
+ k_mutex_lock(&service_mutex, K_FOREVER);
+ if (!transfer.active || transfer.committed || transfer_id != transfer.id ||
+ sample_offset != transfer.received_samples || sample_count == 0 ||
+ sample_count > transfer.total_samples - transfer.received_samples) {
+ LOG_WRN("Transfer chunk rejected: requested_id=%u current_id=%u offset=%u expected_offset=%u samples=%u remaining=%u active=%d committed=%d",
+ transfer_id, transfer.id, sample_offset, transfer.received_samples,
+ sample_count, transfer.total_samples - transfer.received_samples, transfer.active,
+ transfer.committed);
+ k_mutex_unlock(&service_mutex);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_CHUNK,
+ BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED));
+ }
+
+ for (size_t index = 0; index < sample_count; ++index) {
+ if (protocol_read_int16(&reader, &transfer.samples[transfer.received_samples + index]) !=
+ PROTOCOL_OK) {
+ LOG_WRN("Transfer chunk sample decode failed: id=%u offset=%u sample_index=%u",
+ transfer_id, sample_offset, index);
+ k_mutex_unlock(&service_mutex);
+ return reject_transfer(AUDIO_RESPONSE_TRANSFER_INVALID_CHUNK,
+ BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN));
+ }
+ }
+ transfer.received_samples += sample_count;
+ k_work_reschedule(&transfer_timeout_work, AUDIO_RESPONSE_TRANSFER_TIMEOUT);
+ k_work_reschedule(&transfer_ready_work, AUDIO_RESPONSE_TRANSFER_CREDIT_DELAY);
+ if (transfer.received_samples == transfer.total_samples) {
+ LOG_INF("Transfer upload complete: id=%u samples=%u", transfer.id,
+ transfer.total_samples);
+ } else {
+ LOG_DBG("Transfer chunk accepted: id=%u received=%u/%u", transfer.id,
+ transfer.received_samples, transfer.total_samples);
+ }
+ k_mutex_unlock(&service_mutex);
+ return len;
+}
+
+/**
+ * Decode a measurement request and queue it outside the Bluetooth callback.
+ */
+static ssize_t write_audio_response_config(struct bt_conn *conn, const struct bt_gatt_attr *attr,
+ const void *buf, uint16_t len, uint16_t offset,
+ uint8_t flags)
+{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(flags);
+ audio_response_config_t config;
+ size_t bytes_read = 0;
+ int ret;
+
+ if (offset != 0) {
+ LOG_WRN("Audio response config rejected: invalid offset=%u len=%u", offset, len);
+ return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
+ }
+ config.frequencies = decoded_config_frequencies;
+ if (audio_response_config_decode(&config, buf, len, &bytes_read) != PROTOCOL_OK ||
+ bytes_read != len) {
+ LOG_WRN("Audio response config decode failed: bytes_read=%u len=%u", bytes_read, len);
+ return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
+ }
+ LOG_INF("Audio response config requested: id=%u transfer_id=%u volume=%.2f points=%u",
+ config.id, config.transfer_id, (double)config.volume, config.points);
+ if (!isfinite(config.volume) || config.volume < 0.0f || config.volume > 1.0f) {
+ LOG_WRN("Audio response config rejected: invalid volume=%.3f",
+ (double)config.volume);
+ return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
+ }
+ ret = select_response_points(&config);
+ if (ret != 0) {
+ return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
+ }
+
+ k_mutex_lock(&service_mutex, K_FOREVER);
+ if (!transfer.committed || config.transfer_id != transfer.id || measurement_active) {
+ LOG_WRN("Audio response config rejected: requested_transfer_id=%u current_id=%u committed=%d measurement_active=%d",
+ config.transfer_id, transfer.id, transfer.committed, measurement_active);
+ k_mutex_unlock(&service_mutex);
+ return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
+ }
+ pending_config = config;
+ measurement_active = true;
+ k_mutex_unlock(&service_mutex);
+
+ k_work_submit(&measurement_work);
+ LOG_INF("Audio response measurement queued: id=%u transfer_id=%u points=%u",
+ pending_config.id, pending_config.transfer_id, pending_config.points);
+ return len;
+}
+
+/**
+ * Track whether transfer-status notifications are enabled.
+ */
+static void transfer_status_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
+{
+ ARG_UNUSED(attr);
+ transfer_status_notifications_enabled = value == BT_GATT_CCC_NOTIFY;
+ LOG_DBG("Audio response transfer status notifications %s",
+ transfer_status_notifications_enabled ? "enabled" : "disabled");
+}
+
+/**
+ * Track whether audio-response result notifications are enabled.
+ */
+static void result_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
+{
+ ARG_UNUSED(attr);
+ result_notifications_enabled = value == BT_GATT_CCC_NOTIFY;
+ LOG_DBG("Audio response result notifications %s",
+ result_notifications_enabled ? "enabled" : "disabled");
+}
+
+BT_GATT_SERVICE_DEFINE(audio_response_svc,
+ BT_GATT_PRIMARY_SERVICE(AUDIO_RESPONSE_ZEPHYR_SERVICE_UUID),
+ BT_GATT_CHARACTERISTIC(AUDIO_RESPONSE_ZEPHYR_TRANSFER_CONTROL_CHARACTERISTIC_UUID,
+ AUDIO_RESPONSE_ZEPHYR_TRANSFER_CONTROL_CHARACTERISTIC_PROPERTIES,
+ AUDIO_RESPONSE_ZEPHYR_TRANSFER_CONTROL_CHARACTERISTIC_PERMISSIONS, NULL,
+ write_transfer_control, NULL),
+ BT_GATT_CHARACTERISTIC(AUDIO_RESPONSE_ZEPHYR_TRANSFER_DATA_CHARACTERISTIC_UUID,
+ AUDIO_RESPONSE_ZEPHYR_TRANSFER_DATA_CHARACTERISTIC_PROPERTIES,
+ AUDIO_RESPONSE_ZEPHYR_TRANSFER_DATA_CHARACTERISTIC_PERMISSIONS, NULL,
+ write_transfer_data, NULL),
+ BT_GATT_CHARACTERISTIC(AUDIO_RESPONSE_ZEPHYR_TRANSFER_STATUS_CHARACTERISTIC_UUID,
+ AUDIO_RESPONSE_ZEPHYR_TRANSFER_STATUS_CHARACTERISTIC_PROPERTIES,
+ AUDIO_RESPONSE_ZEPHYR_TRANSFER_STATUS_CHARACTERISTIC_PERMISSIONS, NULL,
+ NULL, NULL),
+ BT_GATT_CCC(transfer_status_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
+ BT_GATT_CHARACTERISTIC(AUDIO_RESPONSE_ZEPHYR_CONFIG_CHARACTERISTIC_UUID,
+ AUDIO_RESPONSE_ZEPHYR_CONFIG_CHARACTERISTIC_PROPERTIES,
+ AUDIO_RESPONSE_ZEPHYR_CONFIG_CHARACTERISTIC_PERMISSIONS, NULL,
+ write_audio_response_config, NULL),
+ BT_GATT_CHARACTERISTIC(AUDIO_RESPONSE_ZEPHYR_RESULT_CHARACTERISTIC_UUID,
+ AUDIO_RESPONSE_ZEPHYR_RESULT_CHARACTERISTIC_PROPERTIES,
+ AUDIO_RESPONSE_ZEPHYR_RESULT_CHARACTERISTIC_PERMISSIONS, NULL, NULL,
+ NULL),
+ BT_GATT_CCC(result_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
+);
+
+/**
+ * Stop shared audio activity and retain enough state to restore it after the
+ * response measurement owns the datapath.
+ */
+static int suspend_audio_for_measurement(void)
+{
+ int ret;
+
+ memset(&audio_session, 0, sizeof(audio_session));
+ ret = audio_system_suspend();
+ if (ret != 0) {
+ LOG_ERR("Failed to suspend audio system: %d", ret);
+ return ret;
+ }
+ audio_session.audio_system_suspended = true;
+
+ ret = audio_datapath_auxiliary_suspend();
+ if (ret != 0) {
+ LOG_ERR("Failed to suspend auxiliary audio activity: %d", ret);
+ (void)audio_system_resume();
+ memset(&audio_session, 0, sizeof(audio_session));
+ return ret;
+ }
+ LOG_DBG("Auxiliary audio activity suspended for measurement");
+
+ audio_session.auxiliary_audio_suspended = true;
+ return 0;
+}
+
+/**
+ * Stop measurement-specific activity and restore the audio state captured by
+ * suspend_audio_for_measurement(). Safe to call after partial setup.
+ */
+static void restore_audio_after_measurement(void)
+{
+ int ret;
+
+ record_to_buffer_stop();
+ if (audio_session.measurement_playback_started) {
+ audio_datapath_buffer_stop();
+ }
+ if (audio_session.measurement_codec_enabled) {
+ ret = hw_codec_stop_audio();
+ if (ret != 0) {
+ LOG_ERR("Failed to stop measurement codec: %d", ret);
+ }
+ }
+ if (audio_session.datapath_acquired) {
+ ret = audio_datapath_release();
+ if (ret != 0) {
+ LOG_ERR("Failed to release measurement datapath: %d", ret);
+ }
+ }
+ if (audio_session.auxiliary_audio_suspended) {
+ ret = audio_datapath_auxiliary_resume();
+ if (ret != 0) {
+ LOG_ERR("Failed to restore auxiliary audio activity: %d", ret);
+ }
+ }
+ if (audio_session.audio_system_suspended) {
+ ret = audio_system_resume();
+ if (ret != 0) {
+ LOG_ERR("Failed to resume audio system: %d", ret);
+ }
+ }
+ memset(&audio_session, 0, sizeof(audio_session));
+}
+
+/**
+ * Run the audio playback and capture setup from the system work queue.
+ */
+static void measurement_work_handler(struct k_work *work)
+{
+ ARG_UNUSED(work);
+ int ret;
+
+ LOG_INF("Starting audio response measurement: id=%u transfer_id=%u samples=%u volume=%.2f points=%u",
+ pending_config.id, pending_config.transfer_id, transfer.total_samples,
+ (double)pending_config.volume, pending_config.points);
+
+ ret = suspend_audio_for_measurement();
+ if (ret != 0) {
+ goto fail;
+ }
+ LOG_DBG("Audio system suspended for audio response measurement: id=%u", pending_config.id);
+
+ if (!fifo_rx.initialized) {
+ LOG_DBG("Initializing RX FIFO for audio response measurement");
+ ret = data_fifo_init(&fifo_rx);
+ if (ret != 0) {
+ LOG_ERR("Failed to initialize RX FIFO: %d", ret);
+ goto fail;
+ }
+ }
+ LOG_DBG("RX FIFO ready for audio response measurement: id=%u", pending_config.id);
+
+ ret = audio_datapath_decimator_init(CONFIG_AUDIO_SAMPLE_RATE_HZ /
+ AUDIO_RESPONSE_CAPTURE_SAMPLE_RATE);
+ if (ret != 0) {
+ LOG_ERR("Failed to initialize audio response decimator: %d", ret);
+ goto fail;
+ }
+ LOG_DBG("Audio response decimator initialized: input_rate=%u output_rate=%u",
+ CONFIG_AUDIO_SAMPLE_RATE_HZ, AUDIO_RESPONSE_CAPTURE_SAMPLE_RATE);
+ ret = audio_datapath_aquire(&fifo_rx);
+ if (ret != 0) {
+ LOG_ERR("Failed to acquire audio datapath: %d", ret);
+ goto fail;
+ }
+ LOG_DBG("Audio datapath acquired: id=%u", pending_config.id);
+ audio_session.datapath_acquired = true;
+ ret = hw_codec_default_conf_enable();
+ if (ret != 0) {
+ LOG_ERR("Failed to enable codec for audio response measurement: %d", ret);
+ goto fail;
+ }
+ audio_session.measurement_codec_enabled = true;
+ LOG_DBG("Codec enabled for audio response measurement: id=%u", pending_config.id);
+ ret = hw_codec_volume_unmute();
+ if (ret != 0) {
+ LOG_ERR("Failed to unmute codec for audio response measurement: %d", ret);
+ goto fail;
+ }
+ ret = audio_datapath_buffer_play(transfer.samples, transfer.total_samples, false,
+ pending_config.volume, NULL);
+ if (ret != 0) {
+ LOG_ERR("Failed to start audio response playback: %d", ret);
+ goto fail;
+ }
+ audio_session.measurement_playback_started = true;
+
+ LOG_INF("Audio response playback started: id=%u capture_samples=%u capture_rate=%u initial_drop=%u",
+ pending_config.id, AUDIO_RESPONSE_CAPTURE_SAMPLES,
+ AUDIO_RESPONSE_CAPTURE_SAMPLE_RATE, AUDIO_RESPONSE_INITIAL_DROP);
+ record_to_buffer(captured_samples, AUDIO_RESPONSE_CAPTURE_SAMPLES,
+ AUDIO_RESPONSE_INITIAL_DROP, false, true, audio_response_capture_complete);
+ return;
+
+fail:
+ LOG_ERR("Failed to start audio response measurement: %d", ret);
+ restore_audio_after_measurement();
+ k_mutex_lock(&service_mutex, K_FOREVER);
+ measurement_active = false;
+ k_mutex_unlock(&service_mutex);
+}
+
+/**
+ * Encode and notify the measured response at the configured target frequencies.
+ */
+static void notify_result(void)
+{
+ audio_response_result_t result = {
+ .id = pending_config.id,
+ .points = pending_config.points,
+ .frequencies = result_frequencies,
+ .response = result_response,
+ };
+ size_t payload_size;
+
+ if (!result_notifications_enabled) {
+ LOG_WRN("Audio response result notifications are not enabled: id=%u",
+ pending_config.id);
+ return;
+ }
+ if (audio_response_result_encode(&result, result_payload, sizeof(result_payload), &payload_size) !=
+ PROTOCOL_OK) {
+ LOG_ERR("Failed to encode audio response result: id=%u points=%u", result.id,
+ result.points);
+ return;
+ }
+
+ int ret = bt_gatt_notify(NULL, &audio_response_svc.attrs[11], result_payload, payload_size);
+ if (ret != 0) {
+ LOG_ERR("Failed to notify audio response result: id=%u err=%d", result.id, ret);
+ return;
+ }
+ LOG_INF("Audio response result notified: id=%u points=%u bytes=%u", result.id,
+ result.points, payload_size);
+}
+
+/**
+ * Compute the captured signal's response at the configured target frequencies.
+ */
+static void measurement_complete_work_handler(struct k_work *work)
+{
+ ARG_UNUSED(work);
+ LOG_INF("Audio response capture complete: id=%u", pending_config.id);
+ restore_audio_after_measurement();
+
+ for (size_t index = 0; index < pending_config.points; ++index) {
+ uint32_t bin = response_frequency_to_bin(pending_config.frequencies[index]);
+
+ result_frequencies[index] = pending_config.frequencies[index];
+ result_response[index] = response_magnitude_for_bin(
+ captured_samples, ARRAY_SIZE(captured_samples), bin);
+ }
+ LOG_DBG("Audio response analysis complete: id=%u bins=%u result_points=%u",
+ pending_config.id,
+ AUDIO_RESPONSE_CAPTURE_SAMPLES / 2, pending_config.points);
+ notify_result();
+
+ k_mutex_lock(&service_mutex, K_FOREVER);
+ measurement_active = false;
+ k_mutex_unlock(&service_mutex);
+ LOG_DBG("Audio response measurement complete: id=%u", pending_config.id);
+}
+
+/**
+ * Grant the next upload credit after the current GATT write has released its
+ * incoming ACL buffer.
+ */
+static void transfer_ready_work_handler(struct k_work *work)
+{
+ ARG_UNUSED(work);
+ k_mutex_lock(&service_mutex, K_FOREVER);
+ if (transfer.active && !transfer.committed) {
+ (void)notify_transfer_status(AUDIO_RESPONSE_TRANSFER_READY,
+ AUDIO_RESPONSE_TRANSFER_CREDITS);
+ }
+ k_mutex_unlock(&service_mutex);
+}
+
+/**
+ * Release an incomplete transfer after the protocol timeout.
+ */
+static void transfer_timeout_work_handler(struct k_work *work)
+{
+ ARG_UNUSED(work);
+ k_mutex_lock(&service_mutex, K_FOREVER);
+ if (transfer.active && !transfer.committed) {
+ LOG_WRN("Transfer timed out: id=%u received=%u/%u", transfer.id,
+ transfer.received_samples, transfer.total_samples);
+ (void)notify_transfer_status(AUDIO_RESPONSE_TRANSFER_TIMED_OUT, 0);
+ reset_transfer();
+ }
+ k_mutex_unlock(&service_mutex);
+}
+
+void audio_response_capture_complete(void)
+{
+ LOG_DBG("Audio response capture completion callback");
+ k_work_submit(&measurement_complete_work);
+}
+
+int init_audio_response_service(void)
+{
+ LOG_INF("Initializing audio response service");
+ k_work_init(&measurement_work, measurement_work_handler);
+ k_work_init(&measurement_complete_work, measurement_complete_work_handler);
+ k_work_init_delayable(&transfer_ready_work, transfer_ready_work_handler);
+ k_work_init_delayable(&transfer_timeout_work, transfer_timeout_work_handler);
+ return 0;
+}
diff --git a/src/bluetooth/gatt_services/audio_response_service.h b/src/bluetooth/gatt_services/audio_response_service.h
new file mode 100644
index 00000000..948f6da7
--- /dev/null
+++ b/src/bluetooth/gatt_services/audio_response_service.h
@@ -0,0 +1,24 @@
+#ifndef _AUDIO_RESPONSE_SERVICE_H_
+#define _AUDIO_RESPONSE_SERVICE_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Initialize the audio response service's asynchronous work items.
+ *
+ * @return 0 on success.
+ */
+int init_audio_response_service(void);
+
+/**
+ * Signal that the microphone capture for an audio response measurement completed.
+ */
+void audio_response_capture_complete(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _AUDIO_RESPONSE_SERVICE_H_ */
diff --git a/src/bluetooth/gatt_services/battery_service.cpp b/src/bluetooth/gatt_services/battery_service.cpp
index 71e0aa5a..cdad8beb 100644
--- a/src/bluetooth/gatt_services/battery_service.cpp
+++ b/src/bluetooth/gatt_services/battery_service.cpp
@@ -75,6 +75,7 @@ struct battery_health_status health_status;
static void battery_ccc_cfg_changed(const struct bt_gatt_attr *attr,
uint16_t value)
{
+ ARG_UNUSED(attr);
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
#ifdef CONFIG_LOG
if (notify_enabled) LOG_INF("subscribe to battery level");
@@ -158,6 +159,7 @@ BT_GATT_CHARACTERISTIC(BT_UUID_BAS_BATTERY_HEALTH_STATUS,
int bt_send_battery_level(struct battery_data * data)
{
+ ARG_UNUSED(data);
if (!notify_enabled) {
LOG_WRN("battery level not subscribed");
return -EACCES;
@@ -166,4 +168,4 @@ int bt_send_battery_level(struct battery_data * data)
LOG_INF("notify battery level change");
return bt_gatt_notify(NULL, &battery_service.attrs[2], &msg.battery_level, sizeof(msg.battery_level));
-}
\ No newline at end of file
+}
diff --git a/src/bluetooth/gatt_services/button_service.c b/src/bluetooth/gatt_services/button_service.c
index 6399cf4f..8addf53b 100644
--- a/src/bluetooth/gatt_services/button_service.c
+++ b/src/bluetooth/gatt_services/button_service.c
@@ -23,6 +23,7 @@ static K_THREAD_STACK_DEFINE(thread_stack, CONFIG_BUTTON_MSG_SUB_STACK_SIZE);
static void button_ccc_cfg_changed(const struct bt_gatt_attr *attr,
uint16_t value)
{
+ ARG_UNUSED(attr);
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
}
@@ -103,4 +104,4 @@ int init_button_service() {
}
return 0;
-}
\ No newline at end of file
+}
diff --git a/src/bluetooth/gatt_services/device_info.c b/src/bluetooth/gatt_services/device_info.c
index 3cbb548a..13c4438b 100644
--- a/src/bluetooth/gatt_services/device_info.c
+++ b/src/bluetooth/gatt_services/device_info.c
@@ -1,4 +1,6 @@
#include "device_info.h"
+#include
+#include
#include
#include
@@ -17,7 +19,8 @@ static ssize_t read_device_identifier(struct bt_conn *conn,
uint16_t len,
uint16_t offset)
{
- snprintf(device_identifier, sizeof(device_identifier), "0x%08X", oe_boot_state.device_id);
+ snprintf(device_identifier, sizeof(device_identifier), "0x%08X",
+ (unsigned int)oe_boot_state.device_id);
return bt_gatt_attr_read(conn, attr, buf, len, offset, device_identifier,
sizeof(device_identifier));
@@ -59,4 +62,4 @@ BT_GATT_CHARACTERISTIC(BT_UUID_FIRMWARE,
BT_GATT_CHRC_READ,
BT_GATT_PERM_READ,
read_firmware, NULL, firmware),
-);
\ No newline at end of file
+);
diff --git a/src/bluetooth/gatt_services/led_service.cpp b/src/bluetooth/gatt_services/led_service.cpp
index 26de1418..f12a82f1 100644
--- a/src/bluetooth/gatt_services/led_service.cpp
+++ b/src/bluetooth/gatt_services/led_service.cpp
@@ -10,6 +10,9 @@ static ssize_t write_led(struct bt_conn *conn,
const void *buf,
uint16_t len, uint16_t offset, uint8_t flags)
{
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(flags);
if (len != 3U) {
LOG_INF("Write led: Incorrect data length");
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
@@ -30,9 +33,9 @@ static ssize_t write_state(struct bt_conn *conn,
const void *buf,
uint16_t len, uint16_t offset, uint8_t flags)
{
- //printk("Attribute write, handle: %u, conn: %p", attr->handle,
- //(void *)conn);
-
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(flags);
if (len != 1U) {
LOG_INF("Write led: Incorrect data length");
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
diff --git a/src/bluetooth/gatt_services/sensor_service.c b/src/bluetooth/gatt_services/sensor_service.c
index 3d821d14..c358ceff 100644
--- a/src/bluetooth/gatt_services/sensor_service.c
+++ b/src/bluetooth/gatt_services/sensor_service.c
@@ -170,12 +170,17 @@ static void connect_evt_handler(const struct zbus_channel *chan)
case BT_MGMT_DISCONNECTED:
reset_sensor_notification_state();
break;
+
+ default:
+ /* Other events do not affect sensor notification state. */
+ break;
}
}
static void sensor_ccc_cfg_changed(const struct bt_gatt_attr *attr,
uint16_t value)
{
+ ARG_UNUSED(attr);
k_spinlock_key_t key = k_spin_lock(¬ify_state_lock);
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
@@ -189,6 +194,7 @@ static void sensor_ccc_cfg_changed(const struct bt_gatt_attr *attr,
static void sensor_config_status_ccc_cfg_changed(const struct bt_gatt_attr *attr,
uint16_t value)
{
+ ARG_UNUSED(attr);
k_spinlock_key_t key = k_spin_lock(¬ify_state_lock);
sensor_config_status_ntfy_enabled = (value == BT_GATT_CCC_NOTIFY);
k_spin_unlock(¬ify_state_lock, key);
@@ -199,6 +205,7 @@ static ssize_t write_config(struct bt_conn *conn,
const void *buf,
uint16_t len, uint16_t offset, uint8_t flags)
{
+ ARG_UNUSED(flags);
LOG_DBG("Attribute write, handle: %u, conn: %p", attr->handle, (void *)conn);
if (len != sizeof(struct sensor_config)) {
@@ -211,12 +218,12 @@ static ssize_t write_config(struct bt_conn *conn,
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
- struct sensor_config * config = (struct sensor_config *)buf;
+struct sensor_config * sensor_configuration = (struct sensor_config *)buf;
- if (config->storageOptions == 0) {
- LOG_INF("Setup sensor ID %i (turned off)", config->sensorId);
+ if (sensor_configuration->storageOptions == 0) {
+ LOG_INF("Setup sensor ID %i (turned off)", sensor_configuration->sensorId);
} else {
- LOG_INF("Setup sensor ID %i with samplerateIndex %i", config->sensorId, config->sampleRateIndex);
+ LOG_INF("Setup sensor ID %i with samplerateIndex %i", sensor_configuration->sensorId, sensor_configuration->sampleRateIndex);
}
//stop_sensor_manager();
@@ -246,6 +253,8 @@ static ssize_t write_sensor_rec_name(struct bt_conn *conn,
const void *buf,
uint16_t len, uint16_t offset, uint8_t flags)
{
+ ARG_UNUSED(offset);
+ ARG_UNUSED(flags);
LOG_DBG("Attribute write, len: %u, handle: %u, conn: %p", len, attr->handle, (void *)conn);
if (len > MAX_SENSOR_REC_NAME_LENGTH - 1) {
LOG_WRN("Write sensor recording name: Data length exceeds maximum allowed length of %i", MAX_SENSOR_REC_NAME_LENGTH - 1);
@@ -405,6 +414,7 @@ static void notification_task(void) {
}
void sensor_queue_listener_cb(const struct zbus_channel *chan) {
+ ARG_UNUSED(chan);
int ret;
const struct sensor_msg * msg;
@@ -447,19 +457,19 @@ int init_sensor_config_status() {
return 0;
}
-int set_sensor_config_status(struct sensor_config config) {
- LOG_DBG("Setting sensor config status for sensorId: %i", config.sensorId);
+int set_sensor_config_status(struct sensor_config sensor_configuration) {
+ LOG_DBG("Setting sensor config status for sensorId: %i", sensor_configuration.sensorId);
ssize_t sensor_config_index = -1;
for (size_t i = 0; i < active_sensor_configs_size; i++) {
- if (active_sensor_configs[i].sensorId == config.sensorId) {
+ if (active_sensor_configs[i].sensorId == sensor_configuration.sensorId) {
sensor_config_index = i;
break;
}
}
if (sensor_config_index >= 0) {
- active_sensor_configs[sensor_config_index] = config;
+ active_sensor_configs[sensor_config_index] = sensor_configuration;
LOG_DBG("Found sensor config");
} else {
LOG_DBG("Sensor config not found, adding new sensor config");
@@ -471,7 +481,7 @@ int set_sensor_config_status(struct sensor_config config) {
return -1;
}
active_sensor_configs = new_active_sensor_configs;
- active_sensor_configs[active_sensor_configs_size - 1] = config;
+ active_sensor_configs[active_sensor_configs_size - 1] = sensor_configuration;
}
if (sensor_config_status_ntfy_enabled) {
diff --git a/src/buttons/Button.cpp b/src/buttons/Button.cpp
index 6e990342..51744664 100644
--- a/src/buttons/Button.cpp
+++ b/src/buttons/Button.cpp
@@ -15,13 +15,14 @@ struct gpio_callback Button::button_cb_data;
void Button::button_isr(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
- Button * button;
-
- if (pins & BIT(BUTTON_EARABLE)) {
- //earable_btn._read_state();
- button = &earable_btn;
+ ARG_UNUSED(dev);
+ ARG_UNUSED(cb);
+ if (!(pins & BIT(BUTTON_EARABLE))) {
+ return;
}
+ Button *button = &earable_btn;
+
/*if (pins & BIT(BUTTON_VOLUME_UP)) {
volume_up_btn._read_state();
}
@@ -114,4 +115,4 @@ button_action Button::getState() const {
Button earable_btn(GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios));
// Button volume_up_btn(GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw1), gpios, {0}));
// Button volume_down_btn(GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw2), gpios, {0}));
-// Button four_btn(GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw3), gpios, {0}));
\ No newline at end of file
+// Button four_btn(GPIO_DT_SPEC_GET_OR(DT_ALIAS(sw3), gpios, {0}));
diff --git a/src/drivers/ADAU1860.cpp b/src/drivers/ADAU1860.cpp
index b75fa521..4b662914 100644
--- a/src/drivers/ADAU1860.cpp
+++ b/src/drivers/ADAU1860.cpp
@@ -2,6 +2,7 @@
#include "zbus_common.h"
#include "openearable_common.h"
#include
+#include
#include
#include
@@ -18,6 +19,7 @@ static struct k_work_delayable ascr_lock_work;
void ADAU1860::check_ascr_lock(struct k_work *work)
{
+ ARG_UNUSED(work);
uint8_t status2;
dac.readReg(registers::STATUS2, &status2, sizeof(status2));
@@ -208,8 +210,9 @@ int ADAU1860::begin() {
uint8_t spt0_route1 = 33; // ASCRO 1
writeReg(registers::SPT0_ROUTE1, &spt0_route1, sizeof(spt0_route1));
- // DMIC_VOL0
uint8_t dmic_vol = 0x20; // 12dB
+ // DMIC channel 0 - outer mic
+ // DMIC channel 1 - inner mic
writeReg(registers::DMIC_VOL0, &dmic_vol, sizeof(dmic_vol));
writeReg(registers::DMIC_VOL1, &dmic_vol, sizeof(dmic_vol));
@@ -343,10 +346,10 @@ int ADAU1860::setup_FDSP() {
uint8_t fdsp_ctrl4 = 2; // framrate source DMIC01
writeReg(registers::FDSP_CTRL4, &fdsp_ctrl4, sizeof(fdsp_ctrl4));
- /*uint8_t fdsp_ctrl4 = 15; // fixed frame rate
+ /* uint8_t fdsp_ctrl4 = 15; // fixed frame rate
writeReg(registers::FDSP_CTRL4, &fdsp_ctrl4, sizeof(fdsp_ctrl4));
- /*uint8_t fdsp_ctrl5 = 0xFF; // fixed frame rate
+ uint8_t fdsp_ctrl5 = 0xFF; // fixed frame rate
writeReg(registers::FDSP_CTRL5, &fdsp_ctrl5, sizeof(fdsp_ctrl5));
uint8_t fdsp_ctrl6 = 0x01; // fixed frame rate
@@ -404,17 +407,17 @@ int ADAU1860::mute(bool active) {
#endif
}
-int ADAU1860::fdsp_safe_load(sl_address address, safe_load_params params, bool update_inactive) {
+int ADAU1860::fdsp_safe_load(sl_address safe_load_address, safe_load_params params, bool update_inactive) {
// TODO: not working
if (update_inactive) {
// write to non active banks
for (int i = 0; i < FDSP_NUM_BANKS; i++) {
if (i == _active_bank) continue;
- writeReg(FDSP_BANK(i, address), (uint8_t *) params, sizeof(safe_load_params));
+ writeReg(FDSP_BANK(i, safe_load_address), (uint8_t *) params, sizeof(safe_load_params));
}
}
- uint8_t _address = address;
+ uint8_t _address = safe_load_address;
writeReg(registers::FDSP_SL_ADDR, &_address, sizeof(_address));
writeReg(registers::FDSP_SL_P0_0, (uint8_t *) params, sizeof(safe_load_params));
@@ -424,16 +427,16 @@ int ADAU1860::fdsp_safe_load(sl_address address, safe_load_params params, bool u
return 0;
}
-int ADAU1860::fdsp_safe_load(sl_address address, int n, uint32_t param, bool update_inactive) {
+int ADAU1860::fdsp_safe_load(sl_address safe_load_address, int n, uint32_t param, bool update_inactive) {
uint32_t params[FDSP_NUM_PARAMS];
for (int i = 0; i < FDSP_NUM_PARAMS; i++) {
- params[i] = fdsp_param_bank_a[i][address];
+ params[i] = fdsp_param_bank_a[i][safe_load_address];
}
params[n] = param;
- fdsp_safe_load(address, params, update_inactive);
+ fdsp_safe_load(safe_load_address, params, update_inactive);
return 0;
}
@@ -495,6 +498,18 @@ uint8_t ADAU1860::fdsp_get_volume() {
return 0xFF-dac_vol;
}
+int ADAU1860::mic_gain_write(uint8_t channel, uint8_t gain) {
+ uint32_t reg = (channel == 0) ? registers::DMIC_VOL0 : registers::DMIC_VOL1;
+ return writeReg(reg, &gain, sizeof(gain));
+}
+
+uint8_t ADAU1860::mic_gain_read(uint8_t channel) {
+ uint8_t val = 0;
+ uint32_t reg = (channel == 0) ? registers::DMIC_VOL0 : registers::DMIC_VOL1;
+ readReg(reg, &val, sizeof(val));
+ return val;
+}
+
int ADAU1860::soft_reset(bool full_reset) {
int ret = 0;
@@ -537,7 +552,7 @@ bool ADAU1860::readReg(uint32_t reg, uint8_t * buffer, uint16_t len) {
}
-void ADAU1860::writeReg(uint32_t reg, uint8_t *buffer, uint16_t len) {
+int ADAU1860::writeReg(uint32_t reg, uint8_t *buffer, uint16_t len) {
int ret;
struct i2c_msg msg[2];
@@ -564,6 +579,8 @@ void ADAU1860::writeReg(uint32_t reg, uint8_t *buffer, uint16_t len) {
}
_i2c->release();
+
+ return ret;
}
#ifdef NOISE_GATE_ACTIVE
@@ -575,11 +592,12 @@ int cmd_dsp_noise_gate(const struct shell *shell, size_t argc, char **argv) {
safe_load_params params;
- params[0] = strtoul(argv[1], NULL, 16) | 0xC80;
- params[1] = strtoul(argv[2], NULL, 16) | 0xD00;
- params[2] = strtoul(argv[3], NULL, 16);
- params[3] = strtoul(argv[4], NULL, 16);
- params[4] = strtoul(argv[5], NULL, 16) | 0x80000000;
+ params[0] = static_cast(strtoul(argv[1], nullptr, 16)) | 0xC80U;
+ params[1] = static_cast(strtoul(argv[2], nullptr, 16)) | 0xD00U;
+ params[2] = static_cast(strtoul(argv[3], nullptr, 16));
+ params[3] = static_cast(strtoul(argv[4], nullptr, 16));
+ params[4] =
+ static_cast(strtoul(argv[5], nullptr, 16)) | 0x80000000U;
shell_print(shell, "Params:");
for (int i = 0; i < FDSP_NUM_PARAMS; i++) {
@@ -598,4 +616,4 @@ SHELL_STATIC_SUBCMD_SET_CREATE(dsp_cmd,
SHELL_SUBCMD_SET_END);
SHELL_CMD_REGISTER(dsp, &dsp_cmd, "Set DSP parameters", NULL);
-#endif
\ No newline at end of file
+#endif
diff --git a/src/drivers/ADAU1860.h b/src/drivers/ADAU1860.h
index 63e3fd98..9614e704 100644
--- a/src/drivers/ADAU1860.h
+++ b/src/drivers/ADAU1860.h
@@ -412,12 +412,16 @@ class ADAU1860 {
uint8_t get_volume();
+ // DMIC gain control
+ int mic_gain_write(uint8_t channel, uint8_t gain);
+ uint8_t mic_gain_read(uint8_t channel);
+
#if CONFIG_FDSP
int fdsp_bank_select(uint8_t bank);
#endif
private:
bool readReg(uint32_t reg, uint8_t * buffer, uint16_t len);
- void writeReg(uint32_t reg, uint8_t * buffer, uint16_t len);
+ int writeReg(uint32_t reg, uint8_t * buffer, uint16_t len);
int setup_EQ();
int setup_FDSP();
@@ -453,4 +457,4 @@ class ADAU1860 {
extern ADAU1860 dac;
-#endif
\ No newline at end of file
+#endif
diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt
index d1f11d41..37d0f0dc 100644
--- a/src/drivers/CMakeLists.txt
+++ b/src/drivers/CMakeLists.txt
@@ -4,7 +4,7 @@
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#
-target_sources_ifdef(CONFIG_NRF5340_AUDIO_CS47L63_DRIVER
+target_sources_ifdef(CONFIG_NRF5340_AUDIO_ADAU1860_DRIVER
app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/ADAU1860.cpp
)
@@ -12,4 +12,4 @@ add_subdirectory(LED_Controller)
zephyr_library_include_directories(
LED_Controller
-)
\ No newline at end of file
+)
diff --git a/src/drivers/Kconfig b/src/drivers/Kconfig
index 97d6acf4..327b92d6 100644
--- a/src/drivers/Kconfig
+++ b/src/drivers/Kconfig
@@ -8,6 +8,7 @@ menu "Drivers"
menuconfig NRF5340_AUDIO_CS47L63_DRIVER
bool "CS47L63 HW codec driver"
+ depends on BOARD_NRF5340_AUDIO_DK_NRF5340_CPUAPP
select HW_CODEC_CIRRUS_LOGIC
help
Include the driver for the Cirrus Logic CS47L63 hardware codec chip
@@ -24,6 +25,31 @@ config CS47L63_STACK_SIZE
int "Stack size for CS47L63"
default 700
+module = CS47L63
+module-str = cs47l63
+source "subsys/logging/Kconfig.template.log_config"
+
+endif # NRF5340_AUDIO_CS47L63_DRIVER
+
+menuconfig NRF5340_AUDIO_ADAU1860_DRIVER
+ bool "ADAU1860 HW codec driver"
+ depends on BOARD_OPENEARABLE_V2_NRF5340_CPUAPP || BOARD_OPENEARABLE_V2_NRF5340_CPUAPP_NS
+ default y if BOARD_OPENEARABLE_V2_NRF5340_CPUAPP || BOARD_OPENEARABLE_V2_NRF5340_CPUAPP_NS
+ help
+ Include the driver for the Analog Devices ADAU1860 hardware codec chip
+
+if NRF5340_AUDIO_ADAU1860_DRIVER
+
+config ADAU1860_THREAD_PRIO
+ int "Priority for ADAU1860 thread"
+ default 5
+ help
+ This is a preemptible thread
+
+config ADAU1860_STACK_SIZE
+ int "Stack size for ADAU1860"
+ default 700
+
choice EQUALIZER
prompt "Choose EQ on DSP or MCU ISO transport"
default EQAULIZER_DSP
@@ -41,9 +67,9 @@ config FDSP
default y
bool "Use FDSP the fast DSP core. This is required for Transparancy mode and ANC."
-module = CS47L63
-module-str = cs47l63
+module = ADAU1860
+module-str = adau1860
source "subsys/logging/Kconfig.template.log_config"
-endif # NRF5340_AUDIO_CS47L63_DRIVER
+endif # NRF5340_AUDIO_ADAU1860_DRIVER
endmenu # Drivers
diff --git a/src/drivers/LED_Controller/KTD2026.cpp b/src/drivers/LED_Controller/KTD2026.cpp
index feb3ead9..15631f7d 100644
--- a/src/drivers/LED_Controller/KTD2026.cpp
+++ b/src/drivers/LED_Controller/KTD2026.cpp
@@ -95,10 +95,17 @@ void KTD2026::begin() {
if (_active) return;
- _active = true;
+ _active = true;
ret = pm_device_runtime_get(ls_1_8);
+ if (ret) {
+ LOG_ERR("Failed to enable 1.8 V LED supply: %d", ret);
+ }
+
ret = pm_device_runtime_get(ls_3_3);
+ if (ret) {
+ LOG_ERR("Failed to enable 3.3 V LED supply: %d", ret);
+ }
_i2c->begin();
@@ -121,7 +128,14 @@ void KTD2026::power_off() {
(void)writeReg(registers::CTRL, &val, sizeof(val));
int ret = pm_device_runtime_put(ls_1_8);
+ if (ret) {
+ LOG_ERR("Failed to disable 1.8 V LED supply: %d", ret);
+ }
+
ret = pm_device_runtime_put(ls_3_3);
+ if (ret) {
+ LOG_ERR("Failed to disable 3.3 V LED supply: %d", ret);
+ }
clearCachedColor();
diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt
index 6780217b..45cf0745 100644
--- a/src/modules/CMakeLists.txt
+++ b/src/modules/CMakeLists.txt
@@ -9,6 +9,8 @@ target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/audio_i2s.c)
target_sources(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/audio_sync_timer.c)
target_sources_ifdef(CONFIG_NRF5340_AUDIO_CS47L63_DRIVER app PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/hw_codec.c)
+target_sources_ifdef(CONFIG_NRF5340_AUDIO_ADAU1860_DRIVER app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/hw_codec_adau1860.cpp)
target_sources_ifdef(CONFIG_NRF5340_AUDIO_POWER_MEASUREMENT app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/power_meas.c)
diff --git a/src/modules/hw_codec.h b/src/modules/hw_codec.h
index c0347432..f25755e8 100644
--- a/src/modules/hw_codec.h
+++ b/src/modules/hw_codec.h
@@ -119,6 +119,22 @@ int hw_codec_set_audio_mode(enum audio_mode mode);
enum audio_mode hw_codec_get_audio_mode();
+/* Microphone (DMIC) gain control.
+ * Board mapping:
+ * ADAU186x DMIC_VOL0 / DMIC channel 0 controls the outer microphone.
+ * ADAU186x DMIC_VOL1 / DMIC channel 1 controls the inner microphone.
+ * Register mapping (per ADAU186x datasheet DMIC_VOL0 @ 0x4000C045):
+ * 0x00 = +24 dB
+ * 0x01-0x3F = +23.625 to +0.375 dB (decrement by 0.375 dB per step)
+ * 0x40 = 0 dB (reset value)
+ * 0x41-0xFD = -0.375 to -70.875 dB (decrement by 0.375 dB per step)
+ * 0xFE = -71.25 dB
+ * 0xFF = Mute
+ */
+int hw_codec_mic_gain_set(uint8_t gain_outer_reg, uint8_t gain_inner_reg);
+uint8_t hw_codec_mic_gain_get_outer(void);
+uint8_t hw_codec_mic_gain_get_inner(void);
+
#ifdef __cplusplus
}
#endif
diff --git a/src/modules/hw_codec_adau1860.cpp b/src/modules/hw_codec_adau1860.cpp
index fd06b2d7..f69668d6 100644
--- a/src/modules/hw_codec_adau1860.cpp
+++ b/src/modules/hw_codec_adau1860.cpp
@@ -26,6 +26,10 @@ LOG_MODULE_REGISTER(hw_codec, CONFIG_MODULE_HW_CODEC_LOG_LEVEL);
#define VOLUME_ADJUST_STEP_DB 3
+/* Allow the DSP mute ramp and I2S clock domain to settle at source changes. */
+#define CODEC_MUTE_SETTLE_MS 100
+#define CODEC_I2S_PREROLL_MS 100
+
ZBUS_SUBSCRIBER_DEFINE(volume_evt_sub, CONFIG_VOLUME_MSG_SUB_QUEUE_SIZE);
static uint32_t prev_volume_reg_val = OUT_VOLUME_DEFAULT;
@@ -38,6 +42,11 @@ K_THREAD_STACK_DEFINE(volume_msg_sub_thread_stack, CONFIG_VOLUME_MSG_SUB_STACK_S
static enum audio_mode audio_mode;
+enum hw_codec_mic {
+ HW_CODEC_MIC_OUTER = 0,
+ HW_CODEC_MIC_INNER = 1,
+};
+
static int settings_set_cb(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
{
if (strcmp(name, "mode") == 0 && len == sizeof(audio_mode)) {
@@ -54,22 +63,46 @@ static int settings_set_cb(const char *name, size_t len, settings_read_cb read_c
SETTINGS_STATIC_HANDLER_DEFINE(audio, "audio", NULL, settings_set_cb, NULL, NULL);
int hw_codec_set_audio_mode(enum audio_mode mode) {
- int ret;
-
- audio_mode = mode;
-
- settings_save_one("audio/mode", &mode, sizeof(mode));
+ int first_error = 0;
+ int ret;
ret = dac.fdsp_bank_select((uint8_t) mode);
+ if (ret) {
+ LOG_ERR("Failed to select DSP bank, ret: %d", ret);
+ first_error = ret;
+ }
+
// TODO: make writing to bank work
k_msleep(200);
ret = hw_codec_volume_adjust(0);
+ if (ret) {
+ LOG_ERR("Failed to adjust codec volume, ret: %d", ret);
+ if (!first_error) {
+ first_error = ret;
+ }
+ }
+
ret = dac.mute(muted);
if (ret) {
LOG_ERR("Failed to set audio mode, ret: %d", ret);
+ if (!first_error) {
+ first_error = ret;
+ }
+ }
+
+ if (first_error) {
+ /* Keep the persisted mode unchanged when the codec could not fully apply it. */
+ return first_error;
+ }
+
+ audio_mode = mode;
+ ret = settings_save_one("audio/mode", &mode, sizeof(mode));
+ if (ret) {
+ LOG_ERR("Failed to persist audio mode, ret: %d", ret);
return ret;
}
- return ret;
+
+ return 0;
}
enum audio_mode hw_codec_get_audio_mode() {
@@ -285,6 +318,11 @@ int hw_codec_default_conf_enable(void)
return ret;
}
+ /* audio_datapath_aquire() starts I2S before enabling the codec. Keep the
+ * output muted while the new clock stream becomes stable.
+ */
+ k_msleep(CODEC_I2S_PREROLL_MS);
+
//ret = dac.setup();
if (!muted) {
ret = dac.mute(false);
@@ -305,9 +343,53 @@ int hw_codec_stop_audio(void)
return ret;
}
+ /* Do not remove the I2S clocks while the DSP is still applying mute. */
+ k_msleep(CODEC_MUTE_SETTLE_MS);
+
+ return 0;
+}
+
+/* Microphone (DMIC) gain control using ADAU186x DMIC_VOL0/1 registers.
+ * Per ADAU186x datasheet register DMIC_VOL0 (addr 0x4000C045):
+ * 0x00 = +24 dB
+ * 0x01-0x3F = +23.625 to +0.375 dB (decrement by 0.375 dB per step)
+ * 0x40 = 0 dB (reset value)
+ * 0x41-0xFD = -0.375 to -70.875 dB (decrement by 0.375 dB per step)
+ * 0xFE = -71.25 dB
+ * 0xFF = Mute
+ */
+int hw_codec_mic_gain_set(uint8_t gain_outer_reg, uint8_t gain_inner_reg)
+{
+ int ret;
+
+ ret = dac.mic_gain_write(HW_CODEC_MIC_OUTER, gain_outer_reg);
+ if (ret) {
+ LOG_ERR("Failed to set outer mic gain: %d", ret);
+ return ret;
+ }
+
+ ret = dac.mic_gain_write(HW_CODEC_MIC_INNER, gain_inner_reg);
+ if (ret) {
+ LOG_ERR("Failed to set inner mic gain: %d", ret);
+ return ret;
+ }
+
+ LOG_INF("DMIC gain set: outer=0x%02x, inner=0x%02x",
+ gain_outer_reg, gain_inner_reg);
return 0;
}
+uint8_t hw_codec_mic_gain_get_outer(void)
+{
+ return dac.mic_gain_read(HW_CODEC_MIC_OUTER);
+}
+
+uint8_t hw_codec_mic_gain_get_inner(void)
+{
+ return dac.mic_gain_read(HW_CODEC_MIC_INNER);
+}
+
+
int hw_codec_soft_reset(void)
{
int ret;
@@ -345,4 +427,4 @@ int hw_codec_init(void)
ERR_CHK(ret);
return 0;
-}
\ No newline at end of file
+}
diff --git a/src/modules/sd_card.c b/src/modules/sd_card.c
index dee80d7e..a147e9a4 100644
--- a/src/modules/sd_card.c
+++ b/src/modules/sd_card.c
@@ -259,7 +259,7 @@ int sd_card_list_files(char const *const path, char *buf, size_t *buf_size, bool
entry.name);
}
- if (len >= remaining_buf_size) {
+ if (len < 0 || (size_t)len >= remaining_buf_size) {
LOG_ERR("Failed to append to buffer, error: %d", len);
return -EINVAL;
}
diff --git a/src/time_sync/time_sync.c b/src/time_sync/time_sync.c
index 4d8a0c16..1b26991f 100644
--- a/src/time_sync/time_sync.c
+++ b/src/time_sync/time_sync.c
@@ -73,6 +73,7 @@ static ssize_t write_rtt_request(
uint16_t offset,
uint8_t flags
) {
+ ARG_UNUSED(flags);
uint64_t rx_time = get_current_time_us();
if (offset != 0) {
@@ -128,6 +129,9 @@ static ssize_t write_time_offset(
uint16_t offset,
uint8_t flags
) {
+ ARG_UNUSED(conn);
+ ARG_UNUSED(attr);
+ ARG_UNUSED(flags);
if (offset != 0) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
@@ -162,15 +166,19 @@ bool time_sync_is_synced(void) {
uint64_t get_current_time_us(void) {
uint64_t base_u = get_time_since_boot_us();
int64_t base_s = (base_u > (uint64_t)INT64_MAX) ? INT64_MAX : (int64_t)base_u;
- int64_t now_s = base_s + time_offset_us;
+ if (time_offset_us > 0 && base_s > INT64_MAX - time_offset_us) {
+ LOG_WRN("Current time overflow, returning UINT64_MAX");
+ return UINT64_MAX;
+ }
+ if (time_offset_us < 0 && base_s < INT64_MIN - time_offset_us) {
+ LOG_WRN("Current time underflow, returning 0");
+ return 0;
+ }
+ int64_t now_s = base_s + time_offset_us;
if (now_s < 0) {
LOG_WRN("Current time underflow, returning 0");
return 0;
}
- if (now_s != base_u + time_offset_us) {
- LOG_WRN("Current time overflow, returning UINT64_MAX");
- return UINT64_MAX;
- }
return (uint64_t)now_s;
}
@@ -180,6 +188,7 @@ uint64_t get_time_since_boot_us(void) {
void rtt_cfg_changed(const struct bt_gatt_attr *attr,
uint16_t value) {
+ ARG_UNUSED(attr);
LOG_DBG("RTT characteristic CCCD changed: %u", value);
notify_rtt_enabled = (value == BT_GATT_CCC_NOTIFY);
}
diff --git a/src/utils/StateIndicator.cpp b/src/utils/StateIndicator.cpp
index 2600637d..89ca40f8 100644
--- a/src/utils/StateIndicator.cpp
+++ b/src/utils/StateIndicator.cpp
@@ -29,6 +29,10 @@ static void connect_evt_handler(const struct zbus_channel *chan)
case BT_MGMT_DISCONNECTED:
state_indicator.set_pairing_state(PAIRED);
break;
+
+ default:
+ /* Other events do not affect the pairing state indication. */
+ break;
}
}
@@ -63,9 +67,9 @@ void StateIndicator::init(struct earable_state state) {
set_state(state);
}
-void StateIndicator::set_custom_color(const RGBColor &color) {
- memcpy(&this->color, color, sizeof(RGBColor));
- if (_state.led_mode == CUSTOM) led_controller.setColor(color);
+void StateIndicator::set_custom_color(const RGBColor &custom_color) {
+ memcpy(&this->color, custom_color, sizeof(RGBColor));
+ if (_state.led_mode == CUSTOM) led_controller.setColor(custom_color);
}
void StateIndicator::set_dfu_active(bool active) {
diff --git a/src/utils/error_handler.c b/src/utils/error_handler.c
index 7e5ae97e..024bf4ab 100644
--- a/src/utils/error_handler.c
+++ b/src/utils/error_handler.c
@@ -23,6 +23,7 @@ static const struct gpio_dt_spec center_led_b = GPIO_DT_SPEC_GET(DT_NODELABEL(rg
void error_handler(unsigned int reason, const struct arch_esf *esf)
{
+ ARG_UNUSED(esf);
#if (CONFIG_DEBUG)
LOG_ERR("Caught system error -- reason %d. Entering infinite loop", reason);
LOG_PANIC();
diff --git a/src/utils/macros/macros_custom.h b/src/utils/macros/macros_custom.h
index 5e102938..fb900645 100644
--- a/src/utils/macros/macros_custom.h
+++ b/src/utils/macros/macros_custom.h
@@ -15,7 +15,7 @@ ZBUS_SUBSCRIBER_DEFINE(name ## _sub, CONFIG_BUTTON_MSG_SUB_QUEUE_SIZE); \
ZBUS_CHAN_DECLARE(name ## _chan); \
static K_THREAD_STACK_DEFINE(thread_stack, CONFIG_BUTTON_MSG_SUB_STACK_SIZE); \
\
-static void write_ ## name ## _gatt(void)\
+static void write_ ## name ## _gatt(void)\
{\
int ret;\
const struct zbus_channel *chan;\
@@ -60,4 +60,4 @@ int init_ ## name ## _service() {\
}
#endif
-#endif
\ No newline at end of file
+#endif
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 00000000..f0fde713
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,109 @@
+# Tests
+
+Unit tests live in `tests/unit`. Zephyr's
+[Twister test runner](https://docs.nordicsemi.com/bundle/ncs-3.0.1/page/zephyr/develop/test/twister.html)
+builds each
+[Unity suite](https://github.com/nrfconnect/sdk-nrf/blob/v3.0.1/doc/nrf/test_and_optimize/test_framework/testing_unity_cmock.rst)
+as a Linux executable using
+[`native_sim/native/64`](https://docs.nordicsemi.com/bundle/ncs-3.0.1/page/zephyr/boards/native/native_sim/doc/index.html),
+so the tests run on a development computer or GitHub Actions runner and do not
+require OpenEarable hardware.
+These links target the nRF Connect SDK version pinned in
+[`west.yml`](../west.yml).
+
+## GitHub Actions
+
+The [`Unit Tests`](../.github/workflows/unit_tests.yaml) workflow is the primary
+way to run the test suite. It runs for every pull request, for pushes to `main`,
+and when started manually from GitHub Actions.
+
+For pull requests, the workflow creates or updates a comment with the result.
+Twister reports and logs are also available as the `unit-test-results` artifact
+for 14 days.
+
+Twister recursively discovers every `testcase.yaml` below `tests/unit`, so a
+valid new suite in that directory is included automatically; the workflow does
+not need to be edited. The suite must allow `native_sim/native/64` to run in
+this workflow.
+
+## Run the tests locally (optional)
+
+Run Twister from the west workspace root: the directory containing `zephyr`,
+`nrf`, and this repository. The commands below assume the repository directory
+is named `open-earable-v2`; adjust the path if it is named differently.
+
+Unity's test-runner generation requires Ruby. On Debian or Ubuntu:
+
+```sh
+sudo apt install ruby
+```
+
+Run all unit-test scenarios:
+
+```sh
+python3 zephyr/scripts/twister \
+ -T open-earable-v2/tests/unit \
+ -p native_sim/native/64 \
+ --inline-logs
+```
+
+Run a single scenario by its name from `testcase.yaml`:
+
+```sh
+python3 zephyr/scripts/twister \
+ -T open-earable-v2/tests/unit \
+ -p native_sim/native/64 \
+ --scenario openearable.unit.sensor_component \
+ --inline-logs
+```
+
+On Windows, run the tests in WSL or a Linux container because `native_sim`
+produces a Linux executable.
+
+## Add a test suite
+
+The existing suites demonstrate two common patterns:
+
+- [`sensor_component`](unit/sensor_component) tests C++ production sources and
+ binary serialization;
+- [`ring_buffer`](unit/ring_buffer) tests a header-only C++ template.
+
+A suite under `tests/unit//` consists of:
+
+- `testcase.yaml`, defining a unique scenario name and allowing
+ `native_sim/native/64`;
+- `prj.conf`, enabling `CONFIG_UNITY` and any configuration required by the
+ production code;
+- `CMakeLists.txt`, passing the test source to `test_runner_generate(...)` and
+ adding the production sources and include directories to the `app` target;
+- test source files whose test functions start with `test_` and use Unity's
+ `TEST_ASSERT_*` macros.
+
+Production sources are not pulled in automatically. List each source and its
+include directories in the suite's `CMakeLists.txt`.
+
+The host build cannot use nRF hardware. Keep hardware-independent behavior
+separate from drivers, or provide test doubles for hardware and Zephyr APIs.
+The CI workflow discovers new suites automatically when they are placed below
+`tests/unit`.
+
+For C++ tests, enable `CONFIG_CPP`. Add `CONFIG_REQUIRES_FULL_LIBCPP` only when
+the tested code requires the full C++ standard library. Keep the
+`test_suiteTearDown` linkage adapter shown in the C++ examples so the generated
+Unity runner can call the nRF Connect SDK's C teardown function.
+
+## Generate coverage
+
+Install `gcovr` in the active Python environment and run Twister with
+[coverage enabled](https://docs.nordicsemi.com/bundle/ncs-3.0.1/page/zephyr/develop/test/coverage.html):
+
+```sh
+python3 -m pip install gcovr
+python3 zephyr/scripts/twister \
+ -T open-earable-v2/tests/unit \
+ -p native_sim/native/64 \
+ --coverage \
+ --coverage-basedir open-earable-v2
+```
+
+Open `twister-out/coverage/index.html` after the run.
diff --git a/tests/unit/ring_buffer/CMakeLists.txt b/tests/unit/ring_buffer/CMakeLists.txt
new file mode 100644
index 00000000..930e4928
--- /dev/null
+++ b/tests/unit/ring_buffer/CMakeLists.txt
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: MIT
+
+cmake_minimum_required(VERSION 3.20.0)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(ring_buffer_tests)
+
+test_runner_generate(src/main.cpp)
+
+target_include_directories(app PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/Wire
+)
+
+target_sources(app PRIVATE
+ src/main.cpp
+)
diff --git a/tests/unit/ring_buffer/prj.conf b/tests/unit/ring_buffer/prj.conf
new file mode 100644
index 00000000..a9136815
--- /dev/null
+++ b/tests/unit/ring_buffer/prj.conf
@@ -0,0 +1,2 @@
+CONFIG_UNITY=y
+CONFIG_CPP=y
diff --git a/tests/unit/ring_buffer/src/main.cpp b/tests/unit/ring_buffer/src/main.cpp
new file mode 100644
index 00000000..e55ffabe
--- /dev/null
+++ b/tests/unit/ring_buffer/src/main.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: MIT
+
+#include
+
+#include "RingBuffer.h"
+
+using arduino::RingBufferN;
+
+void setUp(void)
+{
+}
+
+void test_new_buffer_is_empty(void)
+{
+ RingBufferN<4> buffer;
+
+ TEST_ASSERT_EQUAL_INT(0, buffer.available());
+ TEST_ASSERT_EQUAL_INT(4, buffer.availableForStore());
+ TEST_ASSERT_EQUAL_INT(-1, buffer.peek());
+ TEST_ASSERT_EQUAL_INT(-1, buffer.read_char());
+ TEST_ASSERT_FALSE(buffer.isFull());
+}
+
+void test_reads_values_in_fifo_order_across_wraparound(void)
+{
+ RingBufferN<4> buffer;
+
+ buffer.store_char(1);
+ buffer.store_char(2);
+ buffer.store_char(3);
+ TEST_ASSERT_EQUAL_INT(1, buffer.read_char());
+ TEST_ASSERT_EQUAL_INT(2, buffer.read_char());
+
+ buffer.store_char(4);
+ buffer.store_char(5);
+
+ TEST_ASSERT_EQUAL_INT(3, buffer.read_char());
+ TEST_ASSERT_EQUAL_INT(4, buffer.read_char());
+ TEST_ASSERT_EQUAL_INT(5, buffer.read_char());
+ TEST_ASSERT_EQUAL_INT(-1, buffer.read_char());
+}
+
+void test_full_buffer_rejects_new_values(void)
+{
+ RingBufferN<4> buffer;
+
+ buffer.store_char(1);
+ buffer.store_char(2);
+ buffer.store_char(3);
+ buffer.store_char(4);
+
+ TEST_ASSERT_TRUE(buffer.isFull());
+ TEST_ASSERT_EQUAL_INT(0, buffer.availableForStore());
+
+ buffer.store_char(5);
+
+ TEST_ASSERT_EQUAL_INT(4, buffer.available());
+ TEST_ASSERT_EQUAL_INT(1, buffer.read_char());
+ TEST_ASSERT_EQUAL_INT(2, buffer.read_char());
+ TEST_ASSERT_EQUAL_INT(3, buffer.read_char());
+ TEST_ASSERT_EQUAL_INT(4, buffer.read_char());
+}
+
+void test_peek_does_not_remove_value(void)
+{
+ RingBufferN<4> buffer;
+
+ buffer.store_char(42);
+
+ TEST_ASSERT_EQUAL_INT(42, buffer.peek());
+ TEST_ASSERT_EQUAL_INT(1, buffer.available());
+ TEST_ASSERT_EQUAL_INT(42, buffer.read_char());
+}
+
+void test_clear_discards_values_and_restores_capacity(void)
+{
+ RingBufferN<4> buffer;
+
+ buffer.store_char(1);
+ buffer.store_char(2);
+ buffer.clear();
+
+ TEST_ASSERT_EQUAL_INT(0, buffer.available());
+ TEST_ASSERT_EQUAL_INT(4, buffer.availableForStore());
+ TEST_ASSERT_EQUAL_INT(-1, buffer.read_char());
+}
+
+extern "C" int unity_c_suite_teardown(int failures) asm("test_suiteTearDown");
+
+int test_suiteTearDown(int failures)
+{
+ return unity_c_suite_teardown(failures);
+}
+
+extern int unity_main(void);
+
+int main(void)
+{
+ (void)unity_main();
+ return 0;
+}
diff --git a/tests/unit/ring_buffer/testcase.yaml b/tests/unit/ring_buffer/testcase.yaml
new file mode 100644
index 00000000..82d31a07
--- /dev/null
+++ b/tests/unit/ring_buffer/testcase.yaml
@@ -0,0 +1,9 @@
+tests:
+ openearable.unit.ring_buffer:
+ tags:
+ - unit
+ - wire
+ integration_platforms:
+ - native_sim/native/64
+ platform_allow:
+ - native_sim/native/64
diff --git a/tests/unit/sensor_component/CMakeLists.txt b/tests/unit/sensor_component/CMakeLists.txt
new file mode 100644
index 00000000..001cd348
--- /dev/null
+++ b/tests/unit/sensor_component/CMakeLists.txt
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: MIT
+
+cmake_minimum_required(VERSION 3.20.0)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(sensor_component_tests)
+
+test_runner_generate(src/main.cpp)
+
+target_include_directories(app PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/ParseInfo
+)
+
+target_sources(app PRIVATE
+ src/main.cpp
+ ../../../src/ParseInfo/SensorComponent.cpp
+)
diff --git a/tests/unit/sensor_component/prj.conf b/tests/unit/sensor_component/prj.conf
new file mode 100644
index 00000000..b8c4533b
--- /dev/null
+++ b/tests/unit/sensor_component/prj.conf
@@ -0,0 +1,3 @@
+CONFIG_UNITY=y
+CONFIG_CPP=y
+CONFIG_REQUIRES_FULL_LIBCPP=y
diff --git a/tests/unit/sensor_component/src/main.cpp b/tests/unit/sensor_component/src/main.cpp
new file mode 100644
index 00000000..54106e7f
--- /dev/null
+++ b/tests/unit/sensor_component/src/main.cpp
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: MIT
+
+#include
+
+#include
+#include
+
+#include "SensorComponent.h"
+
+static SensorComponent components[] = {
+ {.name = "x", .unit = "m/s2", .parseType = PARSE_TYPE_FLOAT},
+ {.name = "quality", .unit = "%", .parseType = PARSE_TYPE_UINT8},
+};
+
+static SensorComponentGroup group = {
+ .name = "acceleration",
+ .componentCount = sizeof(components) / sizeof(components[0]),
+ .components = components,
+};
+
+void setUp(void)
+{
+}
+
+extern "C" int unity_c_suite_teardown(int failures) asm("test_suiteTearDown");
+
+int test_suiteTearDown(int failures)
+{
+ return unity_c_suite_teardown(failures);
+}
+
+void test_size_matches_serialized_size(void)
+{
+ char buffer[64] = {0};
+
+ const size_t expected_size = getSensorComponentGroupSize(&group);
+ const ssize_t bytes_written = serializeSensorComponentGroup(&group, buffer, sizeof(buffer));
+
+ TEST_ASSERT_EQUAL_INT64(static_cast(expected_size), bytes_written);
+}
+
+void test_serializes_all_component_fields(void)
+{
+ char buffer[64] = {0};
+ const char *cursor = buffer;
+
+ const ssize_t bytes_written = serializeSensorComponentGroup(&group, buffer, sizeof(buffer));
+
+ TEST_ASSERT_GREATER_THAN_INT64(0, bytes_written);
+
+ for (size_t i = 0; i < group.componentCount; ++i) {
+ const SensorComponent *component = &components[i];
+ uint8_t length;
+
+ TEST_ASSERT_EQUAL_UINT8(component->parseType, static_cast(*cursor++));
+
+ length = static_cast(*cursor++);
+ TEST_ASSERT_EQUAL_UINT8(strlen(group.name), length);
+ TEST_ASSERT_EQUAL_MEMORY(group.name, cursor, length);
+ cursor += length;
+
+ length = static_cast(*cursor++);
+ TEST_ASSERT_EQUAL_UINT8(strlen(component->name), length);
+ TEST_ASSERT_EQUAL_MEMORY(component->name, cursor, length);
+ cursor += length;
+
+ length = static_cast(*cursor++);
+ TEST_ASSERT_EQUAL_UINT8(strlen(component->unit), length);
+ TEST_ASSERT_EQUAL_MEMORY(component->unit, cursor, length);
+ cursor += length;
+ }
+
+ TEST_ASSERT_EQUAL_INT64(bytes_written, cursor - buffer);
+}
+
+void test_accepts_exactly_sized_buffer(void)
+{
+ char buffer[64] = {0};
+ const size_t required_size = getSensorComponentGroupSize(&group);
+
+ const ssize_t bytes_written = serializeSensorComponentGroup(&group, buffer, required_size);
+
+ TEST_ASSERT_EQUAL_INT64(static_cast(required_size), bytes_written);
+}
+
+void test_rejects_buffer_one_byte_too_small(void)
+{
+ char buffer[64];
+ const size_t required_size = getSensorComponentGroupSize(&group);
+
+ memset(buffer, 0x5a, sizeof(buffer));
+ const ssize_t result = serializeSensorComponentGroup(&group, buffer, required_size - 1);
+
+ TEST_ASSERT_EQUAL_INT64(-1, result);
+ for (const char byte : buffer) {
+ TEST_ASSERT_EQUAL_HEX8(0x5a, byte);
+ }
+}
+
+void test_empty_group_has_empty_serialization(void)
+{
+ SensorComponentGroup empty_group = {
+ .name = "empty",
+ .componentCount = 0,
+ .components = nullptr,
+ };
+ char buffer = 0x5a;
+
+ TEST_ASSERT_EQUAL_UINT64(0, getSensorComponentGroupSize(&empty_group));
+ TEST_ASSERT_EQUAL_INT64(0, serializeSensorComponentGroup(&empty_group, &buffer, 0));
+ TEST_ASSERT_EQUAL_HEX8(0x5a, buffer);
+}
+
+/* Zephyr reserves nonzero returns from main, so ignore Unity's return value. */
+extern int unity_main(void);
+
+int main(void)
+{
+ (void)unity_main();
+ return 0;
+}
diff --git a/tests/unit/sensor_component/testcase.yaml b/tests/unit/sensor_component/testcase.yaml
new file mode 100644
index 00000000..9f64fc42
--- /dev/null
+++ b/tests/unit/sensor_component/testcase.yaml
@@ -0,0 +1,9 @@
+tests:
+ openearable.unit.sensor_component:
+ tags:
+ - unit
+ - parse_info
+ integration_platforms:
+ - native_sim/native/64
+ platform_allow:
+ - native_sim/native/64
diff --git a/tools/.DS_Store b/tools/.DS_Store
deleted file mode 100644
index 72588dae..00000000
Binary files a/tools/.DS_Store and /dev/null differ
diff --git a/tools/ci/extended-warning-compiler-launcher.sh b/tools/ci/extended-warning-compiler-launcher.sh
new file mode 100755
index 00000000..07a9bd98
--- /dev/null
+++ b/tools/ci/extended-warning-compiler-launcher.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env sh
+# Apply CI-only diagnostics to this application's sources, not Zephyr/NCS code.
+compiler=$1
+shift
+
+for arg in "$@"; do
+ case "$arg" in
+ */open-earable-v2/src/*) exec "$compiler" -Wextra -Wshadow "$@" ;;
+ esac
+done
+
+exec "$compiler" "$@"
diff --git a/unicast_server/main.cpp b/unicast_server/main.cpp
index 706cab4e..4da7011f 100644
--- a/unicast_server/main.cpp
+++ b/unicast_server/main.cpp
@@ -28,6 +28,7 @@
#include "button_service.h"
#include "sensor_service.h"
#include "led_service.h"
+#include "audio_response_service.h"
#include "SensorScheme.h"
#include "DefaultSensors.h"
@@ -110,6 +111,9 @@ int main(void) {
ret = init_sensor_service();
ERR_CHK(ret);
+ ret = init_audio_response_service();
+ ERR_CHK(ret);
+
bt_mgmt_conn_interval_init(new ConnIntvlLinear(
4, // linear increase step (8ms units)
CONFIG_BLE_ACL_CONN_INTERVAL,