diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..88a91dd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +*.m text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.md text eol=lf +*.txt text eol=lf +Makefile text eol=lf +Dockerfile text eol=lf diff --git a/.github/workflows/build-builders.yml b/.github/workflows/build-builders.yml index b6f24af..d39f4e8 100644 --- a/.github/workflows/build-builders.yml +++ b/.github/workflows/build-builders.yml @@ -15,17 +15,17 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ./packaging/appimage push: true @@ -37,17 +37,17 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ./packaging/windows push: true @@ -59,17 +59,17 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 + - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: ./packaging/macos push: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..288ec1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [main, pidscope] + pull_request: + branches: [main] + +permissions: {} + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + container: + image: ghcr.io/${{ github.repository_owner }}/pidscope-builder-appimage:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run unit tests + run: octave --no-gui --eval "run('tests/run_tests.m')" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c1bb53..0858099 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,16 +12,34 @@ env: permissions: {} jobs: + # Stage 0: Run tests before building anything + test: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + container: + image: ghcr.io/${{ github.repository_owner }}/pidscope-builder-appimage:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run unit tests + run: octave --no-gui --eval "run('tests/run_tests.m')" + # Stage 1: Build Docker builder images (3 parallel) build-appimage-builder: + needs: test runs-on: ubuntu-latest permissions: contents: read packages: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -33,14 +51,15 @@ jobs: docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-appimage:latest build-windows-builder: + needs: test runs-on: ubuntu-latest permissions: contents: read packages: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -51,24 +70,31 @@ jobs: docker build -t ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-windows:latest ./packaging/windows docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-windows:latest - build-macos-builder: - runs-on: ubuntu-latest + build-macos-blackbox: + needs: test + strategy: + matrix: + include: + - runner: macos-15 + arch_name: arm64 + - runner: macos-15-intel + arch_name: x86_64 + runs-on: ${{ matrix.runner }} permissions: contents: read - packages: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Build blackbox_decode + run: | + git clone --depth 1 https://github.com/betaflight/blackbox-tools.git + cd blackbox-tools + make DEBUG= obj/blackbox_decode + cp obj/blackbox_decode ../blackbox_decode.${{ matrix.arch_name }} + codesign --force --sign - ../blackbox_decode.${{ matrix.arch_name }} - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push builder image - run: | - docker build -t ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-macos:latest ./packaging/macos - docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-macos:latest + name: blackbox-decode-${{ matrix.arch_name }} + path: blackbox_decode.${{ matrix.arch_name }} # Stage 2: Build packages (each depends only on its own builder) build-appimage: @@ -78,9 +104,9 @@ jobs: contents: read packages: read steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -95,7 +121,7 @@ jobs: -v ${{ github.workspace }}/dist:/dist \ ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-appimage:latest - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: appimage path: dist/PIDscope-*-x86_64.AppImage @@ -107,9 +133,9 @@ jobs: contents: read packages: read steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -124,36 +150,64 @@ jobs: -v ${{ github.workspace }}/dist:/dist \ ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-windows:latest - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: windows-zip path: dist/PIDscope-*-windows-x86_64.zip build-macos: - needs: build-macos-builder - runs-on: ubuntu-latest + needs: build-macos-blackbox + runs-on: macos-15 permissions: contents: read - packages: read + env: + INAV_BB_VERSION: "9.0.0" steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + pattern: blackbox-decode-* + merge-multiple: true + + - name: Download INAV blackbox_decode + run: | + brew install zstd + wget -q "https://github.com/iNavFlight/blackbox-tools/releases/download/v${INAV_BB_VERSION}/blackbox-tools-${INAV_BB_VERSION}_macos-aarch64.tar.zst" -O inav-arm64.tar.zst + tar --zstd -xf inav-arm64.tar.zst && cp bin/blackbox_decode blackbox_decode_INAV.arm64 && rm -rf bin share inav-arm64.tar.zst + wget -q "https://github.com/iNavFlight/blackbox-tools/releases/download/v${INAV_BB_VERSION}/blackbox-tools-${INAV_BB_VERSION}_macos-x86_64.tar.zst" -O inav-x86.tar.zst + tar --zstd -xf inav-x86.tar.zst && cp bin/blackbox_decode blackbox_decode_INAV.x86_64 && rm -rf bin share inav-x86.tar.zst - - name: Build macOS ZIP + - name: Package macOS ZIP run: | + VERSION=$(tr -d '[:space:]' < VERSION) + STAGING="/tmp/PIDscope-${VERSION}-macos" + mkdir -p "$STAGING" + cp PIDscope.m "$STAGING/" + cp -r src "$STAGING/" + cp VERSION "$STAGING/" + cp packaging/macos/pidscope.command "$STAGING/" + chmod +x "$STAGING/pidscope.command" + cp blackbox_decode.arm64 blackbox_decode.x86_64 "$STAGING/" + cp blackbox_decode_INAV.arm64 blackbox_decode_INAV.x86_64 "$STAGING/" + chmod +x "$STAGING"/blackbox_decode* + if [ -f packaging/com.pidscope.PIDscope.png ]; then cp packaging/com.pidscope.PIDscope.png "$STAGING/PIDscope.png"; fi + cat > "$STAGING/README-macOS.txt" <<'HEREDOC' + PIDscope - Blackbox Flight Log Analyzer + + SETUP: + 1. brew install octave + 2. xattr -cr ~/Downloads/PIDscope-*-macos + 3. Double-click pidscope.command + (Octave packages are installed automatically on first launch) + + Project: https://buymeacoffee.com/dzikus + License: GPL v3 + HEREDOC mkdir -p dist - docker pull ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-macos:latest - docker run --rm \ - -v ${{ github.workspace }}:/src \ - -v ${{ github.workspace }}/dist:/dist \ - ${{ env.REGISTRY }}/${{ github.repository_owner }}/pidscope-builder-macos:latest + cd /tmp && zip -r -q "$GITHUB_WORKSPACE/dist/PIDscope-${VERSION}-macos-universal.zip" "PIDscope-${VERSION}-macos" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: macos-zip path: dist/PIDscope-*-macos-universal.zip @@ -166,12 +220,12 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: artifacts - name: Create GitHub Release - uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 with: files: | artifacts/appimage/* diff --git a/Makefile b/Makefile index 8de9e1e..522dc9b 100644 --- a/Makefile +++ b/Makefile @@ -1,60 +1,60 @@ -PREFIX ?= /usr/local -INSTALL_DIR = $(PREFIX)/share/pidscope -BIN_DIR = $(PREFIX)/bin -OCTAVE ?= octave - -.PHONY: run install install-deps fetch-blackbox test clean - -run: - $(OCTAVE) --gui --persist --eval "cd('$(CURDIR)'); PIDscope" - -install-deps: - @echo "Installing Octave and required packages..." - @if command -v apt-get >/dev/null 2>&1; then \ - sudo apt-get install -y octave octave-signal octave-statistics octave-control octave-image build-essential; \ - elif command -v dnf >/dev/null 2>&1; then \ - sudo dnf install -y octave octave-signal octave-statistics octave-control octave-image gcc make; \ - elif command -v pacman >/dev/null 2>&1; then \ - sudo pacman -S --noconfirm octave base-devel; \ - else \ - echo "Unknown package manager. Install octave and a C compiler manually."; exit 1; \ - fi - -fetch-blackbox: blackbox_decode blackbox_decode_INAV - -blackbox_decode: - @echo "Building blackbox_decode (betaflight)..." - @rm -rf /tmp/bf-blackbox-tools - git clone --depth 1 https://github.com/betaflight/blackbox-tools.git /tmp/bf-blackbox-tools - make -C /tmp/bf-blackbox-tools obj/blackbox_decode - cp /tmp/bf-blackbox-tools/obj/blackbox_decode . - chmod +x blackbox_decode - rm -rf /tmp/bf-blackbox-tools - @echo "blackbox_decode ready." - -blackbox_decode_INAV: - @echo "Building blackbox_decode_INAV (inav)..." - @rm -rf /tmp/inav-blackbox-tools - git clone --depth 1 https://github.com/iNavFlight/blackbox-tools.git /tmp/inav-blackbox-tools - make -C /tmp/inav-blackbox-tools obj/blackbox_decode - cp /tmp/inav-blackbox-tools/obj/blackbox_decode blackbox_decode_INAV - chmod +x blackbox_decode_INAV - rm -rf /tmp/inav-blackbox-tools - @echo "blackbox_decode_INAV ready." - -install: fetch-blackbox - install -d $(INSTALL_DIR) - install -d $(BIN_DIR) - cp PIDscope.m VERSION $(INSTALL_DIR)/ - cp -r src/ $(INSTALL_DIR)/ - cp blackbox_decode blackbox_decode_INAV $(INSTALL_DIR)/ - @printf '#!/bin/sh\nexec $(OCTAVE) --gui --eval "cd(\\\"$(INSTALL_DIR)\\\"); PIDscope"\n' > $(BIN_DIR)/pidscope - chmod +x $(BIN_DIR)/pidscope - @echo "Installed. Run with: pidscope" - -test: - $(OCTAVE) --no-gui --eval "addpath(genpath('src')); addpath('tests'); run_tests" - -clean: - rm -f blackbox_decode blackbox_decode_INAV - rm -f *.csv *.bbl *.bfl +PREFIX ?= /usr/local +INSTALL_DIR = $(PREFIX)/share/pidscope +BIN_DIR = $(PREFIX)/bin +OCTAVE ?= octave + +.PHONY: run install install-deps fetch-blackbox test clean + +run: + $(OCTAVE) --gui --persist --eval "cd('$(CURDIR)'); PIDscope" + +install-deps: + @echo "Installing Octave and required packages..." + @if command -v apt-get >/dev/null 2>&1; then \ + sudo apt-get install -y octave octave-signal octave-statistics octave-control octave-image build-essential; \ + elif command -v dnf >/dev/null 2>&1; then \ + sudo dnf install -y octave octave-signal octave-statistics octave-control octave-image gcc make; \ + elif command -v pacman >/dev/null 2>&1; then \ + sudo pacman -S --noconfirm octave base-devel; \ + else \ + echo "Unknown package manager. Install octave and a C compiler manually."; exit 1; \ + fi + +fetch-blackbox: blackbox_decode blackbox_decode_INAV + +blackbox_decode: + @echo "Building blackbox_decode (betaflight)..." + @rm -rf /tmp/bf-blackbox-tools + git clone --depth 1 https://github.com/betaflight/blackbox-tools.git /tmp/bf-blackbox-tools + make -C /tmp/bf-blackbox-tools obj/blackbox_decode + cp /tmp/bf-blackbox-tools/obj/blackbox_decode . + chmod +x blackbox_decode + rm -rf /tmp/bf-blackbox-tools + @echo "blackbox_decode ready." + +blackbox_decode_INAV: + @echo "Building blackbox_decode_INAV (inav)..." + @rm -rf /tmp/inav-blackbox-tools + git clone --depth 1 https://github.com/iNavFlight/blackbox-tools.git /tmp/inav-blackbox-tools + make -C /tmp/inav-blackbox-tools obj/blackbox_decode + cp /tmp/inav-blackbox-tools/obj/blackbox_decode blackbox_decode_INAV + chmod +x blackbox_decode_INAV + rm -rf /tmp/inav-blackbox-tools + @echo "blackbox_decode_INAV ready." + +install: fetch-blackbox + install -d $(INSTALL_DIR) + install -d $(BIN_DIR) + cp PIDscope.m VERSION $(INSTALL_DIR)/ + cp -r src/ $(INSTALL_DIR)/ + cp blackbox_decode blackbox_decode_INAV $(INSTALL_DIR)/ + @printf '#!/bin/sh\nexec $(OCTAVE) --gui --eval "cd(\\\"$(INSTALL_DIR)\\\"); PIDscope"\n' > $(BIN_DIR)/pidscope + chmod +x $(BIN_DIR)/pidscope + @echo "Installed. Run with: pidscope" + +test: + $(OCTAVE) --no-gui --eval "addpath(genpath('src')); addpath('tests'); run_tests" + +clean: + rm -f blackbox_decode blackbox_decode_INAV + rm -f *.csv *.bbl *.bfl diff --git a/PIDscope.m b/PIDscope.m index 63c6daa..03d6b29 100644 --- a/PIDscope.m +++ b/PIDscope.m @@ -89,10 +89,15 @@ set(0,'defaultUicontrolFontName', 'Helvetica') % defaultUicontrolFontSize is set after fontsz is calculated (below) -%%%% assign main figure handle and define some UI variables +%%%% assign main figure handle and define some UI variables PSfig = figure(1); +drawnow; % flush Qt event queue so figure is mapped before setting Position set(PSfig, 'InvertHardcopy', 'off'); -bgcolor=[.95 .95 .95]; +th = PStheme(); +bgcolor = th.figBg; +panelBg = th.panelBg; +panelFg = th.panelFg; +panelBorder = th.panelBorder; set(PSfig,'color',bgcolor); wikipage = 'https://buymeacoffee.com/dzikus'; @@ -115,118 +120,123 @@ filepathA=[]; filenameA={}; -hexpand1=[]; -hexpand2=[]; -hexpand3=[]; +hexpand = {[], [], []}; errmsg=[]; plotall_flag=-1; -colorA=[.8 .1 .2]; -colorA2=[.4 .0 .6]; -colorB=[.1 .4 .8]; +colorA = th.btnDash1; +colorB = th.btnDash2; colorC=[1 .2 .2]; colorD=[.1 .7 .2]; -colRun = [0 .5 0]; -saveCol = [.1 .1 .1]; -setUpCol = [.1 .1 .1]; -cautionCol = [0.6 0.3 0]; +colRun = th.btnRun; +saveCol = th.btnSave; +setUpCol = th.textSecondary; +cautionCol = th.btnReset; %use_phsCorrErr=0; flightSpec=0; screensz = get(0,'ScreenSize'); -screensz(3) = round(1.78 * screensz(4)); % force 16:9 - - % Octave Qt bug: setting figure units to 'normalized' permanently breaks uipanel % Calculate pixel position manually instead -figPos = round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)]); -set(PSfig, 'Position', figPos); +set(PSfig, 'Position', round([0 0 screensz(3) screensz(4)])); +try set(PSfig, 'WindowState', 'maximized'); catch, end set(PSfig, 'NumberTitle', 'off'); set(PSfig, 'Name', ['PIDscope (' PsVersion ') - Log Viewer']); +drawnow; pause(0.2); +% Use ACTUAL figure size (accounts for dock/panel/taskbar) +figPos = get(PSfig, 'Position'); +screensz(3) = figPos(3); screensz(4) = figPos(4); -pause(.1)% need to wait for figure to open before extracting screen values - -screensz_multiplier = sqrt(screensz(4)^2) * .011; % based on vertical dimension only, to deal with for ultrawide monitors -prop_max_screen = figPos(4) / screensz(4); -fontsz = (screensz_multiplier*prop_max_screen); +th = PStheme(); +fontsz = th.fontsz; +screensz_multiplier = screensz(4) * .011; % Octave font scaling is done below in layout section markerSz = round(screensz_multiplier * 0.75); -vPos = 0.92; -cpL = .875; % control panel left edge -cpW = .12; % control panel width -rs = 0.025; % row step (vertical spacing between elements) -rh = 0.026; % row height + +% CP dimensions — all derived from pixel sizes, then normalized +cpW_px = 200; rh_px = 22; rs_px = 24; cpM_px = 5; cpTitle_px = 28; +ddh_px = 28; % dropdown height (taller than button) +cbW_px = 40; % checkbox column width +rhs_px = 16; % small text row height +infoH_px = 100; % info table height if isOctave - fontsz = fontsz * 0.85; - rs = 0.034; rh = 0.030; + rh_px = 26; rs_px = 30; ddh_px = 32; rhs_px = 18; end +cpW = cpW_px / screensz(3); +cpL = 1 - cpW - cpM_px/screensz(3); +rh = rh_px / screensz(4); +rs = rs_px / screensz(4); +cpM = cpM_px / screensz(3); % horizontal margin +cpMv = cpM_px / screensz(4); % vertical margin +cpTitleH = cpTitle_px / screensz(4); +cbW = cbW_px / screensz(3); +ddh = ddh_px / screensz(4); +rhs = rhs_px / screensz(4); +tbOff = 40 / screensz(4); % figure toolbar offset +vPos = 1 - tbOff - cpTitleH - cpMv; % top of first row (below toolbar + title bar) set(0,'defaultUicontrolFontSize', fontsz) +set(0,'defaultUicontrolForegroundColor', th.textPrimary) +set(0,'defaultUicontrolBackgroundColor', th.panelBg) row = 1; -posInfo.firmware =[cpL+.003 vPos-rs*row cpW-.006 rh]; row=row+1; -posInfo.fileA=[cpL+.006 vPos-rs*row cpW/2-.006 rh]; -posInfo.clr=[cpL+cpW/2 vPos-rs*row cpW/2-.006 rh]; row=row+1; -posInfo.fnameAText = [cpL+.003 vPos-rs*row cpW-.006 rh]; row=row+1; -posInfo.startEndButton=[cpL+.005 vPos-rs*row cpW/2-.005 rh]; -posInfo.RPYcomboLV = [cpL+cpW/2 vPos-rs*row cpW/2-.003 rh]; row=row+1; -LogStDefault = 2;% default ignore first 2 seconds of logfile -LogNdDefault = 1;% default ignore last 1 second of logfile -posInfo.plotR_LV = [cpL+.005 vPos-rs*row .035 rh]; -posInfo.plotP_LV = [cpL+.04 vPos-rs*row .035 rh]; -posInfo.plotY_LV = [cpL+.075 vPos-rs*row .035 rh]; row=row+1; -posInfo.lineSmooth = [cpL+.003 vPos-rs*row cpW/2-.003 rh]; -posInfo.linewidth = [cpL+cpW/2 vPos-rs*row cpW/2-.003 rh]; row=row+1; -posInfo.spectrogramButton = [cpL+.003 vPos-rs*row cpW-.006 rh]; row=row+1; -posInfo.TuningButton = [cpL+.003 vPos-rs*row cpW-.006 rh]; row=row+1; -posInfo.period2Hz = [cpL+.003 vPos-rs*row cpW/2-.003 rh]; -posInfo.DispInfoButton = [cpL+cpW/2 vPos-rs*row cpW/2-.003 rh]; row=row+1; -posInfo.saveFig = [cpL+.003 vPos-rs*row cpW/2-.003 rh]; -posInfo.saveSettings = [cpL+cpW/2 vPos-rs*row cpW/2-.003 rh]; row=row+1; -%posInfo.wiki = [cpL+.003 vPos-rs*row cpW/2-.003 rh]; -posInfo.PIDtuningService = [cpL+.003 vPos-rs*row cpW-.006 rh]; -cpH = rs*row + 0.04; % control panel height = rows + title margin +posInfo.firmware = [cpL+cpM vPos-rs*row cpW-2*cpM rh]; row=row+1; +posInfo.fileA= [cpL+cpM vPos-rs*row cpW/2-cpM rh]; +posInfo.clr= [cpL+cpW/2 vPos-rs*row cpW/2-cpM rh]; row=row+1; +posInfo.fnameAText = [cpL+cpM vPos-rs*row cpW-2*cpM rh]; row=row+1; +posInfo.startEndButton= [cpL+cpM vPos-rs*row cpW/2-cpM rh]; +posInfo.RPYcomboLV = [cpL+cpW/2 vPos-rs*row cpW/2-cpM rh]; row=row+1; +LogStDefault = 2; +LogNdDefault = 1; +posInfo.plotR_LV = [cpL+cpM vPos-rs*row cbW rh]; +posInfo.plotP_LV = [cpL+cpM+cbW vPos-rs*row cbW rh]; +posInfo.plotY_LV = [cpL+cpM+2*cbW vPos-rs*row cbW rh]; row=row+1; +posInfo.lineSmooth = [cpL+cpM vPos-rs*row cpW/2-cpM rh]; +posInfo.linewidth = [cpL+cpW/2 vPos-rs*row cpW/2-cpM rh]; row=row+1; +posInfo.spectrogramButton = [cpL+cpM vPos-rs*row cpW-2*cpM rh]; row=row+1; +posInfo.TuningButton = [cpL+cpM vPos-rs*row cpW-2*cpM rh]; row=row+1; +posInfo.PIDsliderButton=[cpL+cpM vPos-rs*row cpW-2*cpM rh]; row=row+1; +posInfo.filterSimButton=[cpL+cpM vPos-rs*row cpW/2-cpM rh]; +posInfo.testSignalButton=[cpL+cpW/2 vPos-rs*row cpW/2-cpM rh]; row=row+1; +posInfo.PIDErrorButton = [cpL+cpM vPos-rs*row cpW/2-cpM rh]; +posInfo.FlightStatsButton=[cpL+cpW/2 vPos-rs*row cpW/2-cpM rh]; row=row+1; +posInfo.period2Hz = [cpL+cpM vPos-rs*row cpW/2-cpM rh]; +posInfo.DispInfoButton =[cpL+cpW/2 vPos-rs*row cpW/2-cpM rh]; row=row+1; +posInfo.saveFig = [cpL+cpM vPos-rs*row cpW/2-cpM rh]; +posInfo.saveSettings = [cpL+cpW/2 vPos-rs*row cpW/2-cpM rh]; row=row+1; +posInfo.PIDtuningService = [cpL+cpM vPos-rs*row cpW-2*cpM rh]; +cpH = rs*row + cpTitleH + cpMv; % small padding below last button controlpanel = uipanel('Title','Control Panel','FontSize',fontsz,... - 'BackgroundColor',[.95 .95 .95],... - 'Position',[cpL vPos-cpH+0.02 cpW cpH]); + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... + 'Position',[cpL vPos-cpH+cpTitleH cpW cpH]); -% Position info table just below control panel -cpBottom = vPos - cpH + 0.02; -infoTableH = 0.30; -infoTableY = cpBottom - infoTableH - 0.01; -infoTablePos = [cpL infoTableY cpW infoTableH]; -posInfo.resetMain = [cpL+.003 infoTableY - rh - 0.005 cpW-.006 rh]; fnameMaster = {}; fcnt = 0; -% ColorSet=colormap(jet);%hsv jet gray lines colorcube -% j=[1 8 17 20 23 27 45 50 58 64]; -ColorSet=[.6 .6 .6;..., % gray - Gyro raw - 0 0 0;..., % black - Gyro filt - 0 .7 0;..., % green - Pterm - .8 .65 .1;..., % yellow - I term - .3 .7 .9;..., % light blue - Dterm raw - .1 .2 .8;..., % dark blue -Dterm Filt - .6 .3 .3;..., % brown - Fterm - .8 0 .2;..., % dark red - 1 .2 .9;..., % light purple - .4 0 .9;..., % dark purple - .9 0 0;..., %M1 - 1 .6 0;..., %M2 -0 0 .9;..., %M3 -.1 1 .8;..., %M4 - 0 0 0;..., % throttle - 0 0 0]; % all -j=[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16]; - -k=1; -for i=1:length(j) - eval(['linec.col' int2str(k-1) '=ColorSet(j(i),:);']); - k=k+1; +ColorSet=[th.sigDebug;... % Debug + th.sigGyro;... % Gyro + th.sigPterm;... % Pterm + th.sigIterm;... % Iterm + th.sigDprefilt;... % Dterm prefilt + th.sigDterm;... % Dterm + th.sigFterm;... % Fterm + th.sigSetpoint;... % Setpoint + th.sigPIDsum;... % PIDsum + th.sigPIDerr;... % PIDerr + th.sigMotor{1};... % M1 + th.sigMotor{2};... % M2 + th.sigMotor{3};... % M3 + th.sigMotor{4};... % M4 + th.sigThrottle;... % Throttle + th.textPrimary]; % All +for k=0:15 + linec.(['col' int2str(k)]) = ColorSet(k+1,:); end +linec.colGyroPF = th.sigGyroPrefilt; %%% tooltips TooltipString_files=['Select the .BBL or .BFL file you wish to analyze. ']; @@ -250,24 +260,20 @@ set(guiHandles.fileA, 'ForegroundColor', colRun); guiHandles.clr = uicontrol(PSfig,'string','Reset','fontsize',fontsz,'TooltipString', ['clear all data'], 'units','normalized','Position',[posInfo.clr],... - 'callback',['clear T dataA tta A_lograte epoch1_A epoch2_A SetupInfo rollPIDF pitchPIDF yawPIDF filenameA fnameMaster loaded_firmware debugmode debugIdx fwType fwMajor fwMinor gyro_debug_axis notchData ampmat freq2d2 amp2d2 specMat; ' ... - 'fcnt=0; filenameA={}; fnameMaster={}; Nfiles=0; expandON=0; ' ... - 'try, delete(subplot(''position'',posInfo.linepos1)); delete(subplot(''position'',posInfo.linepos2)); delete(subplot(''position'',posInfo.linepos3)); delete(subplot(''position'',posInfo.linepos4)); catch, end; ' ... - 'figs=findobj(''Type'',''figure''); for fi=1:numel(figs), if figs(fi)~=PSfig, try, close(figs(fi)); catch, end; end; end; ' ... - 'set(guiHandles.FileNum, ''String'', '' ''); try, set(guiHandles.Epoch1_A_Input, ''String'', '' ''); set(guiHandles.Epoch2_A_Input, ''String'', '' ''); catch, end;']); + 'callback','PSresetData;'); set(guiHandles.clr, 'ForegroundColor', cautionCol); guiHandles.startEndButton = uicontrol(PSfig,'style','checkbox', 'string','Trim ','fontsize',fontsz,'TooltipString', [TooltipString_selectButton], 'units','normalized','Position',[posInfo.startEndButton],... 'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA) && get(guiHandles.startEndButton, ''Value''), try, [x y] = ginput(1); epoch1_A(get(guiHandles.FileNum, ''Value'')) = round(x(1)*10)/10; PSplotLogViewer; [x y] = ginput(1); epoch2_A(get(guiHandles.FileNum, ''Value'')) = round(x(1)*10)/10; PSplotLogViewer; catch, end, end'); guiHandles.plotR =uicontrol(PSfig,'Style','checkbox','String','R','fontsize',fontsz,'TooltipString', ['Plot Roll '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotR_LV], 'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisRoll,'Position',[posInfo.plotR_LV], 'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); guiHandles.plotP =uicontrol(PSfig,'Style','checkbox','String','P','fontsize',fontsz,'TooltipString', ['Plot Pitch '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotP_LV], 'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisPitch,'Position',[posInfo.plotP_LV], 'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); guiHandles.plotY =uicontrol(PSfig,'Style','checkbox','String','Y','fontsize',fontsz,'TooltipString', ['Plot Yaw '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotY_LV], 'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisYaw,'Position',[posInfo.plotY_LV], 'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); guiHandles.RPYcomboLV=uicontrol(PSfig,'Style','checkbox','String','Single Panel','fontsize',fontsz,'BackgroundColor',bgcolor,... 'units','normalized','Position',[posInfo.RPYcomboLV],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); @@ -289,8 +295,46 @@ 'callback','PStuneUIcontrol'); set(guiHandles.TuningButton, 'ForegroundColor', colorB); -guiHandles.period2Hz = uicontrol(PSfig,'string','Period','fontsize',fontsz,'TooltipString', ['Calculates peak to peak in Hz similar to the BBE ''Mark'' tool' , newline, 'press button, position mouse over 1st peak, mouse click,' , newline, 'then position over 2nd peak, then mouse click again'], 'units','normalized','Position',[posInfo.period2Hz],... - 'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA) && get(guiHandles.period2Hz, ''Value''), try, [x1 y1] = ginput(1); figure(PSfig); h=plot([x1 x1],[-(maxY*2) maxY],''-r'');set(h,''linewidth'' , get(guiHandles.linewidth, ''Value'')/2); [x2 y2] = ginput(1); h=plot([x2 x2],[-(maxY*2) maxY],''-r''); set(h,''linewidth'' , get(guiHandles.linewidth, ''Value'')/2); plot([x1 x2],[y1 y2],'':k''); x3=[round(x1*1000) round(x2*1000)]; f = 1000/(x3(2)-x3(1)); text(x2, y2, [num2str(x3(2)-x3(1)) ''ms, '' num2str(f) ''Hz''],''FontSize'',fontsz, ''FontWeight'', ''Bold''); catch, end, end'); +guiHandles.PIDsliderButton = uicontrol(PSfig,'string','PID Slider Tool','fontsize',fontsz,... + 'TooltipString','Interactive PID ratio calculator','units','normalized',... + 'Position',[posInfo.PIDsliderButton],... + 'callback',['try,' ... + 'tmpFcnt=get(guiHandles.FileNum,''Value'');tmpFcnt=tmpFcnt(1);' ... + 'PSsliderTool(rollPIDF{tmpFcnt},pitchPIDF{tmpFcnt},yawPIDF{tmpFcnt});' ... + 'clear tmpFcnt;' ... + 'catch,' ... + 'PSsliderTool();' ... + 'end']); +set(guiHandles.PIDsliderButton, 'ForegroundColor', th.btnDash3); + +guiHandles.filterSimButton = uicontrol(PSfig,'string','Filter Simulator','fontsize',fontsz,... + 'TooltipString','Simulate BF filter chain (theoretical response)','units','normalized',... + 'Position',[posInfo.filterSimButton],... + 'callback',['if ~exist(''A_lograte'',''var''),warndlg(''Please select file(s)'');' ... + 'else,tmpFcnt=get(guiHandles.FileNum,''Value'');tmpFcnt=tmpFcnt(1);' ... + 'PSfilterSim([],1000*A_lograte(tmpFcnt),SetupInfo{tmpFcnt});' ... + 'clear tmpFcnt;end']); +set(guiHandles.filterSimButton, 'ForegroundColor', th.btnDash4); + +guiHandles.testSignalButton = uicontrol(PSfig,'string','Test Signal','fontsize',fontsz,... + 'TooltipString','Configure and apply offline filter chain to log data','units','normalized',... + 'Position',[posInfo.testSignalButton],... + 'callback',['if ~exist(''T'',''var''),warndlg(''Please select file(s)'');' ... + 'else,tmpFcnt=get(guiHandles.FileNum,''Value'');tmpFcnt=tmpFcnt(1);' ... + 'PSTestSignalConfig(PSfig,T,Nfiles,A_lograte,SetupInfo,guiHandles,tIND);' ... + 'clear tmpFcnt;end']); +set(guiHandles.testSignalButton, 'ForegroundColor', th.btnDash5); + +guiHandles.PIDErrorButton = uicontrol(PSfig,'string','PID Error','fontsize',fontsz,'TooltipString', ['PID error distribution analysis'],'units','normalized','Position',[posInfo.PIDErrorButton],... + 'callback','PSerrUIcontrol; PSplotPIDerror;'); +set(guiHandles.PIDErrorButton, 'ForegroundColor', th.btnDash6); + +guiHandles.FlightStatsButton = uicontrol(PSfig,'string','Flight Stats','fontsize',fontsz,'TooltipString', ['Flight statistics and stick analysis'],'units','normalized','Position',[posInfo.FlightStatsButton],... + 'callback','PSstatsUIcontrol; PSplotStats;'); +set(guiHandles.FlightStatsButton, 'ForegroundColor', th.btnDash7); + +guiHandles.period2Hz = uicontrol(PSfig,'string','Period','fontsize',fontsz,'TooltipString', ['Click two points on any trace to measure period and frequency.' , newline, 'Red vertical lines + ms/Hz annotation on all axes.'], 'units','normalized','Position',[posInfo.period2Hz],... + 'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA), PSlogViewerPeriod(PSfig); end'); guiHandles.DispInfoButton = uicontrol(PSfig,'string','Setup Info','fontsize',fontsz,'TooltipString', [TooltipString_setup],'units','normalized','Position',[posInfo.DispInfoButton],... 'callback','PSdispSetupInfoUIcontrol;PSdispSetupInfo;'); @@ -313,9 +357,6 @@ set(guiHandles.PIDtuningService, 'ForegroundColor', cautionCol); -guiHandles.resetMain = uicontrol(PSfig,'string','Reset main directory','fontsize',fontsz ,'FontName','arial','FontAngle','normal','TooltipString', ['Donate to the PIDscope project'],'units','normalized','Position',[posInfo.resetMain],... - 'callback','uiwait(helpdlg(resetupStr)), cd(configDir), main_directory = uigetdir(''Navigate to Main folder''); fid = fopen([''mainDir-PS'' PsVersion ''.txt''],''w''); fprintf(fid,''%s\n'',main_directory); fclose(fid); PIDscope'); -set(guiHandles.resetMain, 'ForegroundColor', cautionCol); @@ -330,6 +371,16 @@ || (~exist(fullfile(main_directory, 'blackbox_decode'), 'file') && ~exist(fullfile(main_directory, 'blackbox_decode.exe'), 'file')) main_directory = executableDir; end + +% Store decoder paths globally so PSgetcsv can use absolute paths +if ispc() + setappdata(0, 'PSdecoderPath', fullfile(main_directory, 'blackbox_decode.exe')); + setappdata(0, 'PSdecoderPathINAV', fullfile(main_directory, 'blackbox_decode_INAV.exe')); +else + setappdata(0, 'PSdecoderPath', fullfile(main_directory, 'blackbox_decode')); + setappdata(0, 'PSdecoderPathINAV', fullfile(main_directory, 'blackbox_decode_INAV')); +end + cd(configDir) try @@ -348,14 +399,8 @@ drawnow; pause(0.2); try defaults = readtable('PSdefaults.txt'); - a = char([cellstr([char(defaults.Parameters) num2str(defaults.Values)]); {rdr}; {mdr}; {ldr}]); - t = uitable(PSfig, 'ColumnWidth',{500},'ColumnFormat',{'char'},'Data',[cellstr(a)]); - set(t,'units','normalized','Position',infoTablePos,'FontSize',fontsz*.8, 'ColumnName', ['']) catch - defaults = ' '; - a = char(['Unable to set user defaults '; {rdr}; {mdr}; {ldr}]); - t = uitable(PSfig, 'ColumnWidth',{500},'ColumnFormat',{'char'},'Data',[cellstr(a)]); - set(t,'units','normalized','Position',infoTablePos,'FontSize',fontsz*.8, 'ColumnName', ['']) + defaults = ' '; end @@ -376,3 +421,35 @@ try set(guiHandles.linewidth, 'Value', defaults.Values(find(strcmp(defaults.Para set(PSfig, 'Position', tmpPos); drawnow; end +PSstyleControls(PSfig, th); + +% Register CP elements for resize — keeps fixed pixel sizes when window changes +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', infoH_px); +cpItems = {}; +cpItems{end+1} = struct('h', guiHandles.Firmware, 'type','full', 'row',1, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.fileA, 'type','left', 'row',2, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.clr, 'type','right', 'row',2, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.FileNum, 'type','full', 'row',3, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.startEndButton, 'type','left', 'row',4, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.RPYcomboLV, 'type','right', 'row',4, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.plotR, 'type','cb', 'row',5, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.plotP, 'type','cb', 'row',5, 'col',1, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.plotY, 'type','cb', 'row',5, 'col',2, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.lineSmooth, 'type','left', 'row',6, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.linewidth, 'type','right', 'row',6, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.spectrogramButton, 'type','full', 'row',7, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.TuningButton, 'type','full', 'row',8, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.PIDsliderButton, 'type','full', 'row',9, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.filterSimButton, 'type','left', 'row',10, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.testSignalButton, 'type','right', 'row',10, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.PIDErrorButton, 'type','left', 'row',11, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.FlightStatsButton, 'type','right', 'row',11, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.period2Hz, 'type','left', 'row',12, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.DispInfoButton, 'type','right', 'row',12, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.saveFig, 'type','left', 'row',13, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.saveSettings, 'type','right', 'row',13, 'col',0, 'nrows',0); +cpItems{end+1} = struct('h', guiHandles.PIDtuningService, 'type','full', 'row',14, 'col',0, 'nrows',0); +nrows = max(cellfun(@(x) x.row, cpItems)); +cpItems = [{struct('h', controlpanel, 'type','panel', 'row',0, 'col',0, 'nrows',nrows)}, cpItems]; +PSregisterResize(PSfig, cpPx, cpItems, 'rows'); diff --git a/VERSION b/VERSION index 3768931..9c9684d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -26.03.1 +26.05.0 diff --git a/packaging/macos/Dockerfile b/packaging/macos/Dockerfile index a5eb996..6d2c682 100644 --- a/packaging/macos/Dockerfile +++ b/packaging/macos/Dockerfile @@ -1,6 +1,4 @@ -# PIDscope macOS build environment -# Cross-compiles blackbox_decode as universal binary (arm64 + x86_64) -# using zig cc (no Apple SDK required). +# PIDscope macOS packaging (no compilation, binaries come from GH Actions or /cache) # # docker build -t pidscope-macos packaging/macos/ # docker run --rm -v $(pwd):/src -v $(pwd)/dist:/dist pidscope-macos @@ -9,43 +7,22 @@ FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ - zip wget git make xz-utils llvm \ + zip wget zstd \ && rm -rf /var/lib/apt/lists/* -# Install zig (for macOS cross-compilation without Apple SDK) -RUN wget -q "https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz" \ - -O /tmp/zig.tar.xz && \ - tar -xf /tmp/zig.tar.xz -C /opt && \ - rm /tmp/zig.tar.xz -ENV PATH="/opt/zig-linux-x86_64-0.13.0:${PATH}" +ARG INAV_BB_VERSION=9.0.0 -# Cross-compile blackbox_decode for macOS arm64 + x86_64 -# Betaflight -RUN git clone --depth=1 https://github.com/betaflight/blackbox-tools.git /tmp/bb-bf && \ - cd /tmp/bb-bf && \ - mkdir -p /cache && \ - CC="zig cc -target aarch64-macos" make -j$(nproc) obj/blackbox_decode && \ - zig cc -target aarch64-macos -o /cache/blackbox_decode.arm64 obj/*.o -lc -lm && \ - make clean && \ - CC="zig cc -target x86_64-macos" make -j$(nproc) obj/blackbox_decode && \ - zig cc -target x86_64-macos -o /cache/blackbox_decode.x86_64 obj/*.o -lc -lm && \ - llvm-lipo-14 -create /cache/blackbox_decode.arm64 /cache/blackbox_decode.x86_64 \ - -output /cache/blackbox_decode && \ - rm -f /cache/blackbox_decode.arm64 /cache/blackbox_decode.x86_64 && \ - rm -rf /tmp/bb-bf - -# iNav (LTO_FLAGS= and LDFLAGS= disable -flto which zig cc doesn't support for cross-targets) -RUN git clone --depth=1 https://github.com/iNavFlight/blackbox-tools.git /tmp/bb-inav && \ - cd /tmp/bb-inav && \ - CC="zig cc -target aarch64-macos" make -j$(nproc) LTO_FLAGS= LDFLAGS="-lm" obj/blackbox_decode && \ - cp obj/blackbox_decode /cache/blackbox_decode_INAV.arm64 && \ - make clean && \ - CC="zig cc -target x86_64-macos" make -j$(nproc) LTO_FLAGS= LDFLAGS="-lm" obj/blackbox_decode && \ - cp obj/blackbox_decode /cache/blackbox_decode_INAV.x86_64 && \ - llvm-lipo-14 -create /cache/blackbox_decode_INAV.arm64 /cache/blackbox_decode_INAV.x86_64 \ - -output /cache/blackbox_decode_INAV && \ - rm -f /cache/blackbox_decode_INAV.arm64 /cache/blackbox_decode_INAV.x86_64 && \ - rm -rf /tmp/bb-inav +RUN mkdir -p /cache && \ + wget -q "https://github.com/iNavFlight/blackbox-tools/releases/download/v${INAV_BB_VERSION}/blackbox-tools-${INAV_BB_VERSION}_macos-aarch64.tar.zst" \ + -O /tmp/inav-arm64.tar.zst && \ + tar --zstd -xf /tmp/inav-arm64.tar.zst -C /tmp && \ + cp /tmp/bin/blackbox_decode /cache/blackbox_decode_INAV.arm64 && \ + rm -rf /tmp/bin /tmp/share /tmp/inav-arm64.tar.zst && \ + wget -q "https://github.com/iNavFlight/blackbox-tools/releases/download/v${INAV_BB_VERSION}/blackbox-tools-${INAV_BB_VERSION}_macos-x86_64.tar.zst" \ + -O /tmp/inav-x86.tar.zst && \ + tar --zstd -xf /tmp/inav-x86.tar.zst -C /tmp && \ + cp /tmp/bin/blackbox_decode /cache/blackbox_decode_INAV.x86_64 && \ + rm -rf /tmp/bin /tmp/share /tmp/inav-x86.tar.zst WORKDIR /build COPY build-macos.sh /build/ diff --git a/packaging/macos/build-macos.sh b/packaging/macos/build-macos.sh index e03b48e..3a3abfc 100644 --- a/packaging/macos/build-macos.sh +++ b/packaging/macos/build-macos.sh @@ -27,16 +27,25 @@ echo "Copying PIDscope files..." cp "${SRC_DIR}"/PIDscope.m "${STAGING}/" cp -r "${SRC_DIR}/src" "${STAGING}/" -# 2. Copy blackbox_decode universal binaries +# 2. Copy blackbox_decode binaries +# BF: from GH Actions artifacts (mounted) or /cache; INAV: from /cache (Dockerfile) echo "Copying blackbox_decode binaries..." -cp /cache/blackbox_decode "${STAGING}/" -cp /cache/blackbox_decode_INAV "${STAGING}/" - -# 3. Copy launcher and helper scripts +for bin in blackbox_decode.arm64 blackbox_decode.x86_64 blackbox_decode_INAV.arm64 blackbox_decode_INAV.x86_64; do + if [ -f "/cache/${bin}" ]; then + cp "/cache/${bin}" "${STAGING}/" + elif [ -f "${SRC_DIR}/${bin}" ]; then + cp "${SRC_DIR}/${bin}" "${STAGING}/" + else + echo "WARNING: ${bin} not found, skipping (BF binaries come from GH Actions)" + fi +done + +# 3. Copy launcher cp "${SRC_DIR}/packaging/macos/pidscope.command" "${STAGING}/" -cp "${SRC_DIR}/packaging/macos/fix-quarantine.command" "${STAGING}/" -chmod +x "${STAGING}/pidscope.command" "${STAGING}/fix-quarantine.command" -chmod +x "${STAGING}/blackbox_decode" "${STAGING}/blackbox_decode_INAV" +chmod +x "${STAGING}/pidscope.command" +for bin in "${STAGING}"/blackbox_decode*; do + [ -f "$bin" ] && chmod +x "$bin" +done # 4. Copy icon if [ -f "${SRC_DIR}/packaging/com.pidscope.PIDscope.png" ]; then @@ -58,15 +67,12 @@ FIRST TIME SETUP: brew install octave (If you don't have Homebrew: https://brew.sh) -2. Install required Octave packages (first time only): - octave --eval "pkg install -forge signal statistics control image" - -3. Remove macOS quarantine (required after download): - Double-click "fix-quarantine.command" - (or run in Terminal: xattr -cr /path/to/PIDscope-folder) +2. Remove macOS quarantine - run this in Terminal: + xattr -cr ~/Downloads/PIDscope-*-macos -4. Launch PIDscope: +3. Launch PIDscope: Double-click "pidscope.command" + (Octave packages are installed automatically on first launch) ALTERNATIVE (Terminal): cd /path/to/PIDscope-folder @@ -74,7 +80,7 @@ ALTERNATIVE (Terminal): REQUIREMENTS: - macOS 12+ (Monterey or later) -- GNU Octave 9.x or 10.x (via Homebrew) +- GNU Octave 9.x, 10.x, or 11.x (via Homebrew) - Apple Silicon (M1/M2/M3/M4/M5) or Intel Mac Project: https://buymeacoffee.com/dzikus diff --git a/packaging/macos/fix-quarantine.command b/packaging/macos/fix-quarantine.command index 99b3fd5..38dcdba 100644 --- a/packaging/macos/fix-quarantine.command +++ b/packaging/macos/fix-quarantine.command @@ -1,12 +1,12 @@ #!/bin/bash # Remove macOS quarantine flags from PIDscope files -# Double-click this file in Finder after downloading PIDscope +# Run in Terminal: bash fix-quarantine.command DIR="$(cd "$(dirname "$0")" && pwd)" echo "Removing quarantine flags from PIDscope..." xattr -r -d com.apple.quarantine "$DIR" 2>/dev/null -chmod +x "$DIR/blackbox_decode" "$DIR/blackbox_decode_INAV" "$DIR/pidscope.command" 2>/dev/null +chmod +x "$DIR"/blackbox_decode.* "$DIR"/blackbox_decode_INAV.* "$DIR/pidscope.command" 2>/dev/null echo "" echo "Done! You can now launch PIDscope by double-clicking pidscope.command" diff --git a/packaging/macos/pidscope.command b/packaging/macos/pidscope.command index 29dc735..a5e3bcd 100644 --- a/packaging/macos/pidscope.command +++ b/packaging/macos/pidscope.command @@ -5,6 +5,16 @@ DIR="$(cd "$(dirname "$0")" && pwd)" cd "$DIR" +# Set up arch-specific blackbox_decode symlinks +ARCH=$(uname -m) +if [ "$ARCH" = "arm64" ]; then + ln -sf blackbox_decode.arm64 "$DIR/blackbox_decode" + ln -sf blackbox_decode_INAV.arm64 "$DIR/blackbox_decode_INAV" +else + ln -sf blackbox_decode.x86_64 "$DIR/blackbox_decode" + ln -sf blackbox_decode_INAV.x86_64 "$DIR/blackbox_decode_INAV" +fi + # Find Octave if command -v octave >/dev/null 2>&1; then OCTAVE=octave @@ -15,14 +25,19 @@ elif [ -x "/usr/local/bin/octave" ]; then elif [ -d "/Applications/Octave-9.2.app" ]; then OCTAVE="/Applications/Octave-9.2.app/Contents/Resources/usr/bin/octave" else - osascript -e 'display dialog "GNU Octave not found.\n\nInstall with:\n brew install octave\n\nThen install packages:\n octave --eval \"pkg install -forge signal statistics control image\"" with title "PIDscope" buttons {"OK"} default button "OK" with icon stop' + osascript -e 'display dialog "GNU Octave not found.\n\nInstall with:\n brew install octave\n\nThen install packages:\n octave --eval \"pkg install -forge datatypes signal statistics control image\"" with title "PIDscope" buttons {"OK"} default button "OK" with icon stop' exit 1 fi -# Check if Octave packages are installed (quick check) -if ! "$OCTAVE" --no-gui --eval "pkg load signal" 2>/dev/null; then - osascript -e 'display dialog "Required Octave packages not found.\n\nRun in Terminal:\n octave --eval \"pkg install -forge signal statistics control image\"" with title "PIDscope" buttons {"OK"} default button "OK" with icon caution' - exit 1 +# Check and auto-install required Octave Forge packages +PKGMISSING=$("$OCTAVE" --no-gui --eval "for p={'signal','control','image','statistics'}; if isempty(pkg('list',p{1})), fprintf('MISSING\n'); break; end; end" 2>/dev/null) +if echo "$PKGMISSING" | grep -q MISSING; then + osascript -e 'display notification "Installing required Octave packages (first launch only)..." with title "PIDscope"' + "$OCTAVE" --no-gui --eval "pkg install -forge datatypes signal statistics control image" 2>&1 | tee /tmp/pidscope-pkg-install.log + if [ $? -ne 0 ]; then + osascript -e 'display dialog "Package installation failed.\n\nCheck /tmp/pidscope-pkg-install.log for details.\n\nOr install manually:\n octave --eval \"pkg install -forge datatypes signal statistics control image\"" with title "PIDscope" buttons {"OK"} default button "OK" with icon stop' + exit 1 + fi fi exec "$OCTAVE" --gui --persist --eval "cd('$DIR'); PIDscope" diff --git a/src/compat/PSdatatipSetup.m b/src/compat/PSdatatipSetup.m index 58fb127..23e1654 100644 --- a/src/compat/PSdatatipSetup.m +++ b/src/compat/PSdatatipSetup.m @@ -1,47 +1,47 @@ -function PSdatatipSetup(h) -% PSdatatipSetup - Set up click-to-show-value on axes (Octave) or figure (MATLAB) -% -% In Octave: sets ButtonDownFcn on each axes + HitTest='off' on children -% In MATLAB: uses datacursormode with @PSdatatip callback (original behavior) -% -% Usage: -% PSdatatipSetup(fig) - find all axes in figure and set up each (Octave) -% - set datacursormode (MATLAB) -% PSdatatipSetup(ax) - set up single axes (Octave only) -% -% Call this AFTER plots are created (in plotting functions, not UI setup). - - if ~exist('OCTAVE_VERSION', 'builtin') - % MATLAB: datacursormode on figure (only if figure handle) - if strcmp(get(h, 'Type'), 'figure') - dcm_obj = datacursormode(h); - set(dcm_obj, 'UpdateFcn', @PSdatatip); - end - return - end - - % Octave: axes-level ButtonDownFcn - t = get(h, 'Type'); - if strcmp(t, 'figure') - % Find all axes in figure and set up each - allax = findobj(h, 'Type', 'axes'); - for k = 1:length(allax) - setup_axes(allax(k)); - end - elseif strcmp(t, 'axes') - setup_axes(h); - end -end - - -function setup_axes(ax) - set(ax, 'ButtonDownFcn', @(src, ~) PSdatatip_click(src)); - % Make children pass clicks through to axes ButtonDownFcn - ch = get(ax, 'Children'); - for k = 1:length(ch) - try - set(ch(k), 'HitTest', 'off'); - catch - end - end -end +function PSdatatipSetup(h) +% PSdatatipSetup - Set up click-to-show-value on axes (Octave) or figure (MATLAB) +% +% In Octave: sets ButtonDownFcn on each axes + HitTest='off' on children +% In MATLAB: uses datacursormode with @PSdatatip callback (original behavior) +% +% Usage: +% PSdatatipSetup(fig) - find all axes in figure and set up each (Octave) +% - set datacursormode (MATLAB) +% PSdatatipSetup(ax) - set up single axes (Octave only) +% +% Call this AFTER plots are created (in plotting functions, not UI setup). + + if ~exist('OCTAVE_VERSION', 'builtin') + % MATLAB: datacursormode on figure (only if figure handle) + if strcmp(get(h, 'Type'), 'figure') + dcm_obj = datacursormode(h); + set(dcm_obj, 'UpdateFcn', @PSdatatip); + end + return + end + + % Octave: axes-level ButtonDownFcn + t = get(h, 'Type'); + if strcmp(t, 'figure') + % Find all axes in figure and set up each + allax = findobj(h, 'Type', 'axes'); + for k = 1:length(allax) + setup_axes(allax(k)); + end + elseif strcmp(t, 'axes') + setup_axes(h); + end +end + + +function setup_axes(ax) + set(ax, 'ButtonDownFcn', @(src, ~) PSdatatip_click(src)); + % Make children pass clicks through to axes ButtonDownFcn + ch = get(ax, 'Children'); + for k = 1:length(ch) + try + set(ch(k), 'HitTest', 'off'); + catch + end + end +end diff --git a/src/compat/PSdatatip_click.m b/src/compat/PSdatatip_click.m index f77dacc..0525a1c 100644 --- a/src/compat/PSdatatip_click.m +++ b/src/compat/PSdatatip_click.m @@ -51,7 +51,7 @@ function PSdatatip_click(ax) x = cp(1,1); y = cp(1,2); - % Check if click is within axes limits + xl = get(ax, 'XLim'); yl = get(ax, 'YLim'); if x < xl(1) || x > xl(2) || y < yl(1) || y > yl(2) @@ -77,7 +77,7 @@ function PSdatatip_click(ax) img_obj = ch; break % image takes priority (heatmap background) elseif strcmp(t, 'line') - % Check if line has visible data + xd = get(ch, 'XData'); yd = get(ch, 'YData'); if isempty(xd) || length(xd) < 2, continue; end @@ -108,7 +108,8 @@ function PSdatatip_click(ax) if isempty(obj_type), return; end % Format output text and annotation position - fontsz = 12; + th = PStheme(); + fontsz = th.fontsz; txt = {}; ann_x = x; ann_y = y; @@ -201,7 +202,7 @@ function PSdatatip_click(ax) % Show annotation text(ann_x, ann_y, txt, 'Parent', ax, 'Tag', 'PSdatatip', ... - 'BackgroundColor', [1 1 0.88], 'EdgeColor', [0.3 0.3 0.3], ... + 'BackgroundColor', th.datatipBg, 'EdgeColor', th.gridColor, ... 'FontSize', fontsz, 'FontWeight', 'bold', ... 'VerticalAlignment', 'bottom', 'HorizontalAlignment', 'left', ... 'Margin', 4, 'Clipping', 'on'); diff --git a/src/compat/PSuigetfile.m b/src/compat/PSuigetfile.m index 7f41841..384cd68 100644 --- a/src/compat/PSuigetfile.m +++ b/src/compat/PSuigetfile.m @@ -1,95 +1,95 @@ -function [filenames, filepath] = PSuigetfile(filter_spec, dialog_title, initial_dir, varargin) -% PSuigetfile - File selection dialog with Flatpak zenity fallback -% -% Octave 10.x uigetfile in Flatpak returns the INITIAL directory instead of -% the directory the user navigated to. This wrapper uses zenity --file-selection -% when running inside a Flatpak sandbox (detected via /.flatpak-info). -% -% Usage is identical to uigetfile: -% [filename, filepath] = PSuigetfile(filter, title, startdir, 'MultiSelect','on') - - multi_select = false; - for k = 1:2:numel(varargin) - if strcmpi(varargin{k}, 'MultiSelect') && strcmpi(varargin{k+1}, 'on') - multi_select = true; - end - end - - % Use zenity in Flatpak (/.flatpak-info always exists inside sandbox) - if exist('/.flatpak-info', 'file') - [filenames, filepath] = zenity_dialog(filter_spec, dialog_title, initial_dir, multi_select); - else - if multi_select - [filenames, filepath] = uigetfile(filter_spec, dialog_title, initial_dir, 'MultiSelect', 'on'); - else - [filenames, filepath] = uigetfile(filter_spec, dialog_title, initial_dir); - end - end -end - - -function [filenames, filepath] = zenity_dialog(filter_spec, title, initial_dir, multi_select) - cmd = 'zenity --file-selection'; - - if nargin >= 2 && ~isempty(title) - cmd = [cmd ' --title="' title '"']; - end - - if multi_select - cmd = [cmd ' --multiple --separator="|"']; - end - - if nargin >= 3 && ~isempty(initial_dir) && exist(initial_dir, 'dir') - if initial_dir(end) ~= filesep - initial_dir = [initial_dir filesep]; - end - cmd = [cmd ' --filename="' initial_dir '"']; - end - - % Convert uigetfile filter to zenity format - % uigetfile: {'*.bbl;*.BBL;*.bfl;*.BFL', 'Blackbox Logs'} - % zenity: --file-filter="Blackbox Logs | *.bbl *.BBL *.bfl *.BFL" - if iscell(filter_spec) && size(filter_spec, 2) >= 2 - patterns = strsplit(filter_spec{1,1}, ';'); - filter_name = filter_spec{1,2}; - zf = ['"' filter_name ' |']; - for k = 1:numel(patterns) - zf = [zf ' ' strtrim(patterns{k})]; - end - zf = [zf '"']; - cmd = [cmd ' --file-filter=' zf]; - end - cmd = [cmd ' --file-filter="All files | *"']; - - [status, result] = system(cmd); - result = strtrim(result); - - if status ~= 0 || isempty(result) - filenames = 0; - filepath = 0; - return; - end - - if multi_select - paths = strsplit(result, '|'); - else - paths = {result}; - end - - % Extract directory from first file - [dirpart, ~, ~] = fileparts(paths{1}); - filepath = [dirpart filesep]; - - % Extract just filenames - fnames = cell(1, numel(paths)); - for k = 1:numel(paths) - [~, name, ext] = fileparts(paths{k}); - fnames{k} = [name ext]; - end - - if numel(fnames) == 1 && ~multi_select - filenames = fnames{1}; - else - filenames = fnames; - end -end +function [filenames, filepath] = PSuigetfile(filter_spec, dialog_title, initial_dir, varargin) +% PSuigetfile - File selection dialog with Flatpak zenity fallback +% +% Octave 10.x uigetfile in Flatpak returns the INITIAL directory instead of +% the directory the user navigated to. This wrapper uses zenity --file-selection +% when running inside a Flatpak sandbox (detected via /.flatpak-info). +% +% Usage is identical to uigetfile: +% [filename, filepath] = PSuigetfile(filter, title, startdir, 'MultiSelect','on') + + multi_select = false; + for k = 1:2:numel(varargin) + if strcmpi(varargin{k}, 'MultiSelect') && strcmpi(varargin{k+1}, 'on') + multi_select = true; + end + end + + % Use zenity in Flatpak (/.flatpak-info always exists inside sandbox) + if exist('/.flatpak-info', 'file') + [filenames, filepath] = zenity_dialog(filter_spec, dialog_title, initial_dir, multi_select); + else + if multi_select + [filenames, filepath] = uigetfile(filter_spec, dialog_title, initial_dir, 'MultiSelect', 'on'); + else + [filenames, filepath] = uigetfile(filter_spec, dialog_title, initial_dir); + end + end +end + + +function [filenames, filepath] = zenity_dialog(filter_spec, title, initial_dir, multi_select) + cmd = 'zenity --file-selection'; + + if nargin >= 2 && ~isempty(title) + cmd = [cmd ' --title="' title '"']; + end + + if multi_select + cmd = [cmd ' --multiple --separator="|"']; + end + + if nargin >= 3 && ~isempty(initial_dir) && exist(initial_dir, 'dir') + if initial_dir(end) ~= filesep + initial_dir = [initial_dir filesep]; + end + cmd = [cmd ' --filename="' initial_dir '"']; + end + + % Convert uigetfile filter to zenity format + % uigetfile: {'*.bbl;*.BBL;*.bfl;*.BFL', 'Blackbox Logs'} + % zenity: --file-filter="Blackbox Logs | *.bbl *.BBL *.bfl *.BFL" + if iscell(filter_spec) && size(filter_spec, 2) >= 2 + patterns = strsplit(filter_spec{1,1}, ';'); + filter_name = filter_spec{1,2}; + zf = ['"' filter_name ' |']; + for k = 1:numel(patterns) + zf = [zf ' ' strtrim(patterns{k})]; + end + zf = [zf '"']; + cmd = [cmd ' --file-filter=' zf]; + end + cmd = [cmd ' --file-filter="All files | *"']; + + [status, result] = system(cmd); + result = strtrim(result); + + if status ~= 0 || isempty(result) + filenames = 0; + filepath = 0; + return; + end + + if multi_select + paths = strsplit(result, '|'); + else + paths = {result}; + end + + % Extract directory from first file + [dirpart, ~, ~] = fileparts(paths{1}); + filepath = [dirpart filesep]; + + % Extract just filenames + fnames = cell(1, numel(paths)); + for k = 1:numel(paths) + [~, name, ext] = fileparts(paths{k}); + fnames{k} = [name ext]; + end + + if numel(fnames) == 1 && ~multi_select + filenames = fnames{1}; + else + filenames = fnames; + end +end diff --git a/src/compat/cell2table.m b/src/compat/cell2table.m index 0003c72..192898d 100644 --- a/src/compat/cell2table.m +++ b/src/compat/cell2table.m @@ -1,46 +1,46 @@ -function T = cell2table(C, varargin) -%% CELL2TABLE - Octave-compatible cell array to table conversion -% Drop-in replacement for MATLAB's cell2table() returning a struct. -% -% Usage: -% T = cell2table(C, 'VariableNames', {'col1', 'col2', ...}) -% -% C is a cell array where each column becomes a field in the struct. - - % Parse VariableNames - var_names = {}; - for i = 1:2:length(varargin) - if strcmpi(varargin{i}, 'VariableNames') - var_names = varargin{i+1}; - if ~iscell(var_names) - var_names = cellstr(var_names); - end - var_names = var_names(:)'; % ensure row vector - end - end - - ncols = size(C, 2); - if isempty(var_names) - var_names = arrayfun(@(x) sprintf('Var%d', x), 1:ncols, 'UniformOutput', false); - end - - T = struct(); - for j = 1:ncols - col_data = C(:, j); - % Check if all entries are numeric - all_numeric = true; - for k = 1:length(col_data) - if ~isnumeric(col_data{k}) - all_numeric = false; - break; - end - end - if all_numeric - T.(var_names{j}) = cell2mat(col_data); - else - T.(var_names{j}) = col_data; - end - end - T.Properties = struct('VariableNames', {var_names}); - -end +function T = cell2table(C, varargin) +%% CELL2TABLE - Octave-compatible cell array to table conversion +% Drop-in replacement for MATLAB's cell2table() returning a struct. +% +% Usage: +% T = cell2table(C, 'VariableNames', {'col1', 'col2', ...}) +% +% C is a cell array where each column becomes a field in the struct. + + % Parse VariableNames + var_names = {}; + for i = 1:2:length(varargin) + if strcmpi(varargin{i}, 'VariableNames') + var_names = varargin{i+1}; + if ~iscell(var_names) + var_names = cellstr(var_names); + end + var_names = var_names(:)'; % force row vector + end + end + + ncols = size(C, 2); + if isempty(var_names) + var_names = arrayfun(@(x) sprintf('Var%d', x), 1:ncols, 'UniformOutput', false); + end + + T = struct(); + for j = 1:ncols + col_data = C(:, j); + + all_numeric = true; + for k = 1:length(col_data) + if ~isnumeric(col_data{k}) + all_numeric = false; + break; + end + end + if all_numeric + T.(var_names{j}) = cell2mat(col_data); + else + T.(var_names{j}) = col_data; + end + end + T.Properties = struct('VariableNames', {var_names}); + +end diff --git a/src/compat/contains.m b/src/compat/contains.m index 1bb9c1e..ddae551 100644 --- a/src/compat/contains.m +++ b/src/compat/contains.m @@ -1,43 +1,43 @@ -function tf = contains(str, pat, varargin) -% contains - Octave compatibility shim for MATLAB's contains() -% Copyright (C) 2026 Grzegorz Sterniczuk -% License: GPL v3 (see LICENSE) - - ignorecase = false; - for i = 1:2:numel(varargin) - if strcmpi(varargin{i}, 'IgnoreCase') - ignorecase = varargin{i+1}; - end - end - - if iscell(str) - tf = false(size(str)); - for k = 1:numel(str) - tf(k) = check_contains(str{k}, pat, ignorecase); - end - else - tf = check_contains(str, pat, ignorecase); - end -end - -function tf = check_contains(s, pat, ignorecase) - if iscell(pat) - tf = false; - for k = 1:numel(pat) - if check_one(s, pat{k}, ignorecase) - tf = true; - return; - end - end - else - tf = check_one(s, pat, ignorecase); - end -end - -function tf = check_one(s, p, ignorecase) - if ignorecase - tf = ~isempty(strfind(lower(s), lower(p))); - else - tf = ~isempty(strfind(s, p)); - end -end +function tf = contains(str, pat, varargin) +% contains - Octave compatibility shim for MATLAB's contains() +% Copyright (C) 2026 Grzegorz Sterniczuk +% License: GPL v3 (see LICENSE) + + ignorecase = false; + for i = 1:2:numel(varargin) + if strcmpi(varargin{i}, 'IgnoreCase') + ignorecase = varargin{i+1}; + end + end + + if iscell(str) + tf = false(size(str)); + for k = 1:numel(str) + tf(k) = check_contains(str{k}, pat, ignorecase); + end + else + tf = check_contains(str, pat, ignorecase); + end +end + +function tf = check_contains(s, pat, ignorecase) + if iscell(pat) + tf = false; + for k = 1:numel(pat) + if check_one(s, pat{k}, ignorecase) + tf = true; + return; + end + end + else + tf = check_one(s, pat, ignorecase); + end +end + +function tf = check_one(s, p, ignorecase) + if ignorecase + tf = ~isempty(strfind(lower(s), lower(p))); + else + tf = ~isempty(strfind(s, p)); + end +end diff --git a/src/compat/detectImportOptions.m b/src/compat/detectImportOptions.m deleted file mode 100644 index c16b7a9..0000000 --- a/src/compat/detectImportOptions.m +++ /dev/null @@ -1,40 +0,0 @@ -function opts = detectImportOptions(filename) -%% DETECTIMPORTOPTIONS - Octave-compatible import options detector -% Simplified drop-in for MATLAB's detectImportOptions(). -% Reads the header row of a CSV file and returns options with VariableNames. -% -% Usage: -% opts = detectImportOptions(filename) - - fid = fopen(filename, 'r'); - if fid == -1 - error('detectImportOptions: cannot open file %s', filename); - end - - header_line = fgetl(fid); - fclose(fid); - - if ~ischar(header_line) - opts = struct('VariableNames', {{}}); - return; - end - - col_names = strsplit(strtrim(header_line), ','); - clean_names = cell(size(col_names)); - for i = 1:length(col_names) - s = strtrim(col_names{i}); - s = regexprep(s, '[\[\]\(\) ]+', '_'); - s = regexprep(s, '[^a-zA-Z0-9_]', '_'); - if ~isempty(s) && (s(1) >= '0' && s(1) <= '9') - s = ['x' s]; - end - s = regexprep(s, '_+', '_'); - if isempty(s) - s = sprintf('Var%d', i); - end - clean_names{i} = s; - end - - opts = struct('VariableNames', {clean_names}); - -end diff --git a/src/compat/dkl2rgb.m b/src/compat/dkl2rgb.m index 60b6d2a..38f8ddc 100644 --- a/src/compat/dkl2rgb.m +++ b/src/compat/dkl2rgb.m @@ -1,280 +1,280 @@ -function rgb = dkl2rgb(dkl) -% dkl2rgb - convert DKL cone opponent colors to RGB -% rgb = dkl2rgb(dkl) -% input (dkl) is [luminance; red/green; blue/yellow] -% -% Copyright (C) 2026 Grzegorz Sterniczuk -% License: GPL v3 (see LICENSE) -% Extracted from PScolormap.m for Octave compatibility - -M_dkl2rgb = [ - 1.0000 1.0000 -0.3495 - 1.0000 -0.3203 0.3113 - 1.0000 0.0133 -1.0000 - ]; - - rgb = 0.5 + M_dkl2rgb*(dkl')/2; - rgb = uint8(rgb*255)+1; - - gamma_corr=[ 0 0 0 - 11 5 1 - 16 8 2 - 20 11 3 - 24 15 5 - 27 18 5 - 30 20 6 - 33 23 7 - 35 25 8 - 38 27 9 - 40 29 10 - 42 31 11 - 45 33 13 - 47 35 14 - 49 37 15 - 51 40 16 - 53 41 17 - 54 43 18 - 56 45 20 - 58 46 21 - 60 48 22 - 61 50 23 - 63 51 23 - 65 53 24 - 66 54 25 - 68 56 26 - 69 57 28 - 71 58 29 - 72 60 30 - 74 61 31 - 75 63 32 - 77 65 33 - 78 66 34 - 79 68 36 - 81 69 36 - 82 70 37 - 83 72 38 - 85 73 39 - 86 74 40 - 87 75 41 - 89 77 43 - 90 78 44 - 91 79 45 - 92 80 46 - 94 81 47 - 95 83 47 - 96 84 48 - 97 85 49 - 98 86 51 - 99 87 52 - 101 88 53 - 102 90 54 - 103 92 55 - 104 93 56 - 105 94 57 - 106 95 57 - 107 96 59 - 108 97 60 - 109 98 61 - 110 99 62 - 112 100 63 - 113 101 64 - 114 102 65 - 115 103 67 - 116 104 67 - 117 105 68 - 118 106 69 - 119 107 70 - 120 108 71 - 121 109 72 - 122 110 74 - 123 111 75 - 124 112 76 - 125 113 76 - 127 114 77 - 128 116 78 - 128 117 79 - 130 118 80 - 131 119 82 - 131 120 83 - 132 121 84 - 133 122 84 - 134 123 85 - 135 124 86 - 136 125 87 - 137 126 88 - 138 127 90 - 139 128 91 - 140 129 92 - 141 129 92 - 141 130 93 - 142 131 94 - 143 132 95 - 144 133 96 - 145 134 98 - 146 135 99 - 147 136 100 - 148 137 100 - 148 137 101 - 149 138 102 - 150 139 103 - 151 141 105 - 152 142 106 - 153 143 107 - 154 144 108 - 154 145 108 - 155 145 109 - 156 146 110 - 157 147 111 - 158 148 113 - 158 149 114 - 159 150 115 - 160 150 116 - 161 151 116 - 162 152 117 - 163 153 118 - 163 154 119 - 164 155 121 - 165 155 122 - 166 156 123 - 166 157 123 - 167 158 124 - 168 159 125 - 169 159 126 - 170 160 128 - 170 161 129 - 171 162 130 - 172 163 130 - 173 163 131 - 173 164 132 - 174 165 133 - 175 167 134 - 176 168 136 - 176 168 137 - 177 169 138 - 178 170 138 - 179 171 139 - 179 171 140 - 180 172 141 - 181 173 142 - 182 174 144 - 182 175 145 - 183 175 145 - 184 176 146 - 185 177 147 - 185 178 148 - 186 178 149 - 187 179 150 - 187 180 152 - 188 181 152 - 189 181 153 - 190 182 154 - 190 183 155 - 191 184 156 - 192 184 157 - 192 185 157 - 193 186 159 - 194 186 160 - 194 187 161 - 195 188 162 - 196 189 163 - 197 189 164 - 197 190 164 - 198 192 165 - 199 193 167 - 199 193 168 - 200 194 169 - 201 195 170 - 201 195 171 - 202 196 171 - 203 197 172 - 203 198 173 - 204 198 175 - 205 199 176 - 205 200 177 - 206 200 178 - 207 201 178 - 207 202 179 - 208 202 180 - 209 203 181 - 209 204 183 - 210 205 184 - 211 205 184 - 211 206 185 - 212 207 186 - 213 207 187 - 213 208 188 - 214 209 190 - 215 209 191 - 215 210 191 - 216 211 192 - 217 211 193 - 217 212 194 - 218 213 195 - 218 213 196 - 219 214 196 - 220 215 198 - 220 215 199 - 221 216 200 - 222 218 201 - 222 219 202 - 223 219 203 - 224 220 203 - 224 221 204 - 225 221 206 - 225 222 207 - 226 222 208 - 227 223 209 - 227 224 209 - 228 224 210 - 228 225 211 - 229 226 212 - 230 226 214 - 230 227 215 - 231 228 215 - 232 228 216 - 232 229 217 - 233 230 218 - 233 230 219 - 234 231 221 - 235 232 222 - 235 232 222 - 236 233 223 - 236 234 224 - 237 234 225 - 238 235 226 - 238 235 227 - 239 236 227 - 239 237 229 - 240 237 230 - 241 238 231 - 241 239 232 - 242 239 233 - 242 240 233 - 243 241 234 - 244 241 235 - 244 243 237 - 245 243 238 - 245 244 239 - 246 245 239 - 246 245 240 - 247 246 241 - 248 247 242 - 248 247 244 - 249 248 245 - 249 248 245 - 250 249 246 - 250 250 247 - 251 250 248 - 252 251 249 - 252 251 250 - 253 252 250 - 253 253 252 - 254 253 253 - 254 254 254 - 255 255 255]; - - rgb = [gamma_corr(rgb(1),1); gamma_corr(rgb(2),2); gamma_corr(rgb(3),3)]; - rgb(rgb<0)=0; - rgb(rgb>255)=255; - -end +function rgb = dkl2rgb(dkl) +% dkl2rgb - convert DKL cone opponent colors to RGB +% rgb = dkl2rgb(dkl) +% input (dkl) is [luminance; red/green; blue/yellow] +% +% Copyright (C) 2026 Grzegorz Sterniczuk +% License: GPL v3 (see LICENSE) +% Extracted from PScolormap.m for Octave compatibility + +M_dkl2rgb = [ + 1.0000 1.0000 -0.3495 + 1.0000 -0.3203 0.3113 + 1.0000 0.0133 -1.0000 + ]; + + rgb = 0.5 + M_dkl2rgb*(dkl')/2; + rgb = uint8(rgb*255)+1; + + gamma_corr=[ 0 0 0 + 11 5 1 + 16 8 2 + 20 11 3 + 24 15 5 + 27 18 5 + 30 20 6 + 33 23 7 + 35 25 8 + 38 27 9 + 40 29 10 + 42 31 11 + 45 33 13 + 47 35 14 + 49 37 15 + 51 40 16 + 53 41 17 + 54 43 18 + 56 45 20 + 58 46 21 + 60 48 22 + 61 50 23 + 63 51 23 + 65 53 24 + 66 54 25 + 68 56 26 + 69 57 28 + 71 58 29 + 72 60 30 + 74 61 31 + 75 63 32 + 77 65 33 + 78 66 34 + 79 68 36 + 81 69 36 + 82 70 37 + 83 72 38 + 85 73 39 + 86 74 40 + 87 75 41 + 89 77 43 + 90 78 44 + 91 79 45 + 92 80 46 + 94 81 47 + 95 83 47 + 96 84 48 + 97 85 49 + 98 86 51 + 99 87 52 + 101 88 53 + 102 90 54 + 103 92 55 + 104 93 56 + 105 94 57 + 106 95 57 + 107 96 59 + 108 97 60 + 109 98 61 + 110 99 62 + 112 100 63 + 113 101 64 + 114 102 65 + 115 103 67 + 116 104 67 + 117 105 68 + 118 106 69 + 119 107 70 + 120 108 71 + 121 109 72 + 122 110 74 + 123 111 75 + 124 112 76 + 125 113 76 + 127 114 77 + 128 116 78 + 128 117 79 + 130 118 80 + 131 119 82 + 131 120 83 + 132 121 84 + 133 122 84 + 134 123 85 + 135 124 86 + 136 125 87 + 137 126 88 + 138 127 90 + 139 128 91 + 140 129 92 + 141 129 92 + 141 130 93 + 142 131 94 + 143 132 95 + 144 133 96 + 145 134 98 + 146 135 99 + 147 136 100 + 148 137 100 + 148 137 101 + 149 138 102 + 150 139 103 + 151 141 105 + 152 142 106 + 153 143 107 + 154 144 108 + 154 145 108 + 155 145 109 + 156 146 110 + 157 147 111 + 158 148 113 + 158 149 114 + 159 150 115 + 160 150 116 + 161 151 116 + 162 152 117 + 163 153 118 + 163 154 119 + 164 155 121 + 165 155 122 + 166 156 123 + 166 157 123 + 167 158 124 + 168 159 125 + 169 159 126 + 170 160 128 + 170 161 129 + 171 162 130 + 172 163 130 + 173 163 131 + 173 164 132 + 174 165 133 + 175 167 134 + 176 168 136 + 176 168 137 + 177 169 138 + 178 170 138 + 179 171 139 + 179 171 140 + 180 172 141 + 181 173 142 + 182 174 144 + 182 175 145 + 183 175 145 + 184 176 146 + 185 177 147 + 185 178 148 + 186 178 149 + 187 179 150 + 187 180 152 + 188 181 152 + 189 181 153 + 190 182 154 + 190 183 155 + 191 184 156 + 192 184 157 + 192 185 157 + 193 186 159 + 194 186 160 + 194 187 161 + 195 188 162 + 196 189 163 + 197 189 164 + 197 190 164 + 198 192 165 + 199 193 167 + 199 193 168 + 200 194 169 + 201 195 170 + 201 195 171 + 202 196 171 + 203 197 172 + 203 198 173 + 204 198 175 + 205 199 176 + 205 200 177 + 206 200 178 + 207 201 178 + 207 202 179 + 208 202 180 + 209 203 181 + 209 204 183 + 210 205 184 + 211 205 184 + 211 206 185 + 212 207 186 + 213 207 187 + 213 208 188 + 214 209 190 + 215 209 191 + 215 210 191 + 216 211 192 + 217 211 193 + 217 212 194 + 218 213 195 + 218 213 196 + 219 214 196 + 220 215 198 + 220 215 199 + 221 216 200 + 222 218 201 + 222 219 202 + 223 219 203 + 224 220 203 + 224 221 204 + 225 221 206 + 225 222 207 + 226 222 208 + 227 223 209 + 227 224 209 + 228 224 210 + 228 225 211 + 229 226 212 + 230 226 214 + 230 227 215 + 231 228 215 + 232 228 216 + 232 229 217 + 233 230 218 + 233 230 219 + 234 231 221 + 235 232 222 + 235 232 222 + 236 233 223 + 236 234 224 + 237 234 225 + 238 235 226 + 238 235 227 + 239 236 227 + 239 237 229 + 240 237 230 + 241 238 231 + 241 239 232 + 242 239 233 + 242 240 233 + 243 241 234 + 244 241 235 + 244 243 237 + 245 243 238 + 245 244 239 + 246 245 239 + 246 245 240 + 247 246 241 + 248 247 242 + 248 247 244 + 249 248 245 + 249 248 245 + 250 249 246 + 250 250 247 + 251 250 248 + 252 251 249 + 252 251 250 + 253 252 250 + 253 253 252 + 254 253 253 + 254 254 254 + 255 255 255]; + + rgb = [gamma_corr(rgb(1),1); gamma_corr(rgb(2),2); gamma_corr(rgb(3),3)]; + rgb(rgb<0)=0; + rgb(rgb>255)=255; + +end diff --git a/src/compat/fields.m b/src/compat/fields.m deleted file mode 100644 index ef31e83..0000000 --- a/src/compat/fields.m +++ /dev/null @@ -1,6 +0,0 @@ -function f = fields(s) -% fields - Octave compatibility shim, calls fieldnames -% Copyright (C) 2026 Grzegorz Sterniczuk -% License: GPL v3 (see LICENSE) - f = fieldnames(s); -end diff --git a/src/compat/finddelay.m b/src/compat/finddelay.m index 2c03da0..721d3ed 100644 --- a/src/compat/finddelay.m +++ b/src/compat/finddelay.m @@ -1,25 +1,25 @@ -function d = finddelay(x, y, maxlag) -%% FINDDELAY - Octave-compatible delay estimation via cross-correlation -% Drop-in replacement for MATLAB Signal Processing Toolbox finddelay() -% -% Usage: -% d = finddelay(x, y) - estimate delay between x and y -% d = finddelay(x, y, maxlag) - with maximum lag constraint -% -% Returns positive d when y is a delayed copy of x. -% Uses xcorr from the Octave signal package. - - if nargin < 3 - maxlag = max(length(x), length(y)) - 1; - end - - x = x(:); - y = y(:); - - % Octave's xcorr lag convention is opposite to MATLAB's finddelay: - % xcorr(x,y) peaks at lag=-D when y is delayed by D relative to x - [c, lags] = xcorr(x, y, maxlag); - [~, idx] = max(abs(c)); - d = -lags(idx); - -end +function d = finddelay(x, y, maxlag) +%% FINDDELAY - Octave-compatible delay estimation via cross-correlation +% Drop-in replacement for MATLAB Signal Processing Toolbox finddelay() +% +% Usage: +% d = finddelay(x, y) - estimate delay between x and y +% d = finddelay(x, y, maxlag) - with maximum lag constraint +% +% Returns positive d when y is a delayed copy of x. +% Uses xcorr from the Octave signal package. + + if nargin < 3 + maxlag = max(length(x), length(y)) - 1; + end + + x = x(:); + y = y(:); + + % Octave's xcorr lag convention is opposite to MATLAB's finddelay: + % xcorr(x,y) peaks at lag=-D when y is delayed by D relative to x + [c, lags] = xcorr(x, y, maxlag); + [~, idx] = max(abs(c)); + d = -lags(idx); + +end diff --git a/src/compat/nanmean.m b/src/compat/nanmean.m index 0ec1724..5822783 100644 --- a/src/compat/nanmean.m +++ b/src/compat/nanmean.m @@ -1,24 +1,24 @@ -function m = nanmean(x, dim) -%% NANMEAN - Mean value ignoring NaN entries -% Drop-in replacement for MATLAB/Statistics Toolbox nanmean() -% -% Usage: -% m = nanmean(x) - mean along first non-singleton dimension -% m = nanmean(x, dim) - mean along specified dimension - - if nargin < 2 - if isvector(x) - dim = find(size(x) > 1, 1); - if isempty(dim), dim = 1; end - else - dim = 1; - end - end - - mask = ~isnan(x); - x(~mask) = 0; - n = sum(mask, dim); - n(n == 0) = NaN; - m = sum(x, dim) ./ n; - -end +function m = nanmean(x, dim) +%% NANMEAN - Mean value ignoring NaN entries +% Drop-in replacement for MATLAB/Statistics Toolbox nanmean() +% +% Usage: +% m = nanmean(x) - mean along first non-singleton dimension +% m = nanmean(x, dim) - mean along specified dimension + + if nargin < 2 + if isvector(x) + dim = find(size(x) > 1, 1); + if isempty(dim), dim = 1; end + else + dim = 1; + end + end + + mask = ~isnan(x); + x(~mask) = 0; + n = sum(mask, dim); + n(n == 0) = NaN; + m = sum(x, dim) ./ n; + +end diff --git a/src/compat/nanmedian.m b/src/compat/nanmedian.m index e1d3138..48a1f84 100644 --- a/src/compat/nanmedian.m +++ b/src/compat/nanmedian.m @@ -1,47 +1,47 @@ -function m = nanmedian(x, dim) -%% NANMEDIAN - Median value ignoring NaN entries -% Drop-in replacement for MATLAB/Statistics Toolbox nanmedian() -% -% Usage: -% m = nanmedian(x) - median along first non-singleton dimension -% m = nanmedian(x, dim) - median along specified dimension - - if nargin < 2 - % Vector case - just filter NaN and take median - if isvector(x) - x = x(~isnan(x)); - if isempty(x) - m = NaN; - else - m = median(x); - end - return; - end - dim = 1; - end - - sz = size(x); - % Build output size (collapse dim to 1) - out_sz = sz; - out_sz(dim) = 1; - m = NaN(out_sz); - - % Iterate over all slices along the target dimension - n_slices = prod(sz) / sz(dim); - % Reshape to put target dim first - perm = [dim, 1:dim-1, dim+1:ndims(x)]; - xp = permute(x, perm); - xp = reshape(xp, sz(dim), []); - - mv = NaN(1, size(xp, 2)); - for j = 1:size(xp, 2) - col = xp(:, j); - col = col(~isnan(col)); - if ~isempty(col) - mv(j) = median(col); - end - end - - m = reshape(mv, out_sz); - -end +function m = nanmedian(x, dim) +%% NANMEDIAN - Median value ignoring NaN entries +% Drop-in replacement for MATLAB/Statistics Toolbox nanmedian() +% +% Usage: +% m = nanmedian(x) - median along first non-singleton dimension +% m = nanmedian(x, dim) - median along specified dimension + + if nargin < 2 + % Vector case - just filter NaN and take median + if isvector(x) + x = x(~isnan(x)); + if isempty(x) + m = NaN; + else + m = median(x); + end + return; + end + dim = 1; + end + + sz = size(x); + % Build output size (collapse dim to 1) + out_sz = sz; + out_sz(dim) = 1; + m = NaN(out_sz); + + % Iterate over all slices along the target dimension + n_slices = prod(sz) / sz(dim); + % Reshape to put target dim first + perm = [dim, 1:dim-1, dim+1:ndims(x)]; + xp = permute(x, perm); + xp = reshape(xp, sz(dim), []); + + mv = NaN(1, size(xp, 2)); + for j = 1:size(xp, 2) + col = xp(:, j); + col = col(~isnan(col)); + if ~isempty(col) + mv(j) = median(col); + end + end + + m = reshape(mv, out_sz); + +end diff --git a/src/compat/nanstd.m b/src/compat/nanstd.m new file mode 100644 index 0000000..3417507 --- /dev/null +++ b/src/compat/nanstd.m @@ -0,0 +1,29 @@ +function s = nanstd(x, flag, dim) +%% NANSTD - Standard deviation ignoring NaN entries + + if nargin < 2 || isempty(flag), flag = 0; end + if nargin < 3 + if isvector(x) + dim = find(size(x) > 1, 1); + if isempty(dim), dim = 1; end + else + dim = 1; + end + end + + mask = ~isnan(x); + n = sum(mask, dim); + mu = nanmean(x, dim); + x(~mask) = 0; + x2 = (x - mu).^2; + x2(~mask) = 0; + + if flag == 0 + denom = max(n - 1, 1); + else + denom = max(n, 1); + end + denom(n == 0) = NaN; + s = sqrt(sum(x2, dim) ./ denom); + +end diff --git a/src/compat/readtable.m b/src/compat/readtable.m index 62ddef1..be73bab 100644 --- a/src/compat/readtable.m +++ b/src/compat/readtable.m @@ -1,179 +1,179 @@ -function T = readtable(filename, varargin) -%% READTABLE - Octave-compatible CSV/text file reader -% Drop-in replacement for MATLAB's readtable() returning a struct -% with columns accessible via dot notation (T.colname). -% -% Copyright (C) 2026 Grzegorz Sterniczuk -% License: GPL v3 (see LICENSE) -% -% Usage: -% T = readtable(filename) -% T = readtable(filename, 'HeaderLines', N) -% T = readtable(filename, 'Format', fmt) -% -% Returns a struct where each field is a column vector. -% T.Properties.VariableNames contains the column names. - - % Parse optional arguments - headerlines = 0; - fmt = ''; - for i = 1:2:length(varargin) - key = varargin{i}; - val = varargin{i+1}; - if strcmpi(key, 'HeaderLines') - headerlines = val; - elseif strcmpi(key, 'Format') - fmt = val; - end - end - - fid = fopen(filename, 'r'); - if fid == -1 - error('readtable: cannot open file %s', filename); - end - - % Skip header lines - for i = 1:headerlines - fgetl(fid); - end - - % Read the first non-skipped line as column headers - header_line = fgetl(fid); - if ~ischar(header_line) - fclose(fid); - T = struct(); - T.Properties = struct('VariableNames', {{}}); - return; - end - - % Parse column names from header - col_names = strsplit(strtrim(header_line), ','); - clean_names = sanitize_varnames(col_names); - ncols = length(clean_names); - - % Read first data line to detect column types - first_data_line = fgetl(fid); - if ~ischar(first_data_line) - fclose(fid); - T = struct(); - for j = 1:ncols - T.(clean_names{j}) = []; - end - T.Properties = struct('VariableNames', {clean_names}); - return; - end - - % Detect which columns are numeric vs string - is_numeric = true(1, ncols); - first_fields = strsplit(first_data_line, ','); - for j = 1:min(length(first_fields), ncols) - val = strtrim(first_fields{j}); - num = str2double(val); - % Non-numeric value, or column header contains "flags" -> force string - if (isnan(num) && ~strcmpi(val, 'nan')) || ~isempty(regexpi(col_names{j}, 'flags')) - is_numeric(j) = false; - end - end - - % Build format string based on detected types - fmt_parts = cell(1, ncols); - for j = 1:ncols - if is_numeric(j) - fmt_parts{j} = '%f'; - else - fmt_parts{j} = '%s'; - end - end - fmt_str = strjoin(fmt_parts, ''); - - % Rewind to start of data (skip headerlines + column header line) - frewind(fid); - for i = 1:(headerlines + 1) - fgetl(fid); - end - - % Use textscan directly on file handle - orders of magnitude faster - % than reading lines into cells and joining them - C = textscan(fid, fmt_str, 'Delimiter', ',', 'EmptyValue', NaN); - fclose(fid); - - if length(C) == ncols && ~isempty(C{1}) - T = struct(); - for j = 1:ncols - T.(clean_names{j}) = C{j}; - end - T.Properties = struct('VariableNames', {clean_names}); - return; - end - - % Fallback: reopen and parse line by line (slow, for edge cases) - warning('readtable: textscan returned %d/%d columns, falling back to line-by-line', length(C), ncols); - fid = fopen(filename, 'r'); - for i = 1:(headerlines + 1) - fgetl(fid); - end - - data_lines = {}; - while ~feof(fid) - line = fgetl(fid); - if ischar(line) && ~isempty(strtrim(line)) - data_lines{end+1} = line; - end - end - fclose(fid); - - nrows = length(data_lines); - string_cols = cell(nrows, ncols); - numeric_cols = NaN(nrows, ncols); - - for i = 1:nrows - fields = strsplit(data_lines{i}, ','); - for j = 1:min(length(fields), ncols) - val = strtrim(fields{j}); - num = str2double(val); - if ~isnan(num) || strcmpi(val, 'nan') - numeric_cols(i, j) = num; - else - is_numeric(j) = false; - string_cols{i, j} = val; - end - end - end - - T = struct(); - for j = 1:ncols - if is_numeric(j) - T.(clean_names{j}) = numeric_cols(:, j); - else - T.(clean_names{j}) = string_cols(:, j); - end - end - T.Properties = struct('VariableNames', {clean_names}); - -end - - -function names = sanitize_varnames(raw_names) - % Convert raw CSV header names to valid MATLAB/Octave variable names - % Mimics MATLAB's readtable sanitization: - % 'time (us)' -> 'time_us_' - % 'axisP[0]' -> 'axisP_0_' - names = cell(size(raw_names)); - for i = 1:length(raw_names) - s = strtrim(raw_names{i}); - % Replace brackets, spaces, parentheses with underscores - s = regexprep(s, '[\[\]\(\) ]+', '_'); - % Replace any remaining non-alphanumeric/underscore chars - s = regexprep(s, '[^a-zA-Z0-9_]', '_'); - % Ensure doesn't start with a number - if ~isempty(s) && (s(1) >= '0' && s(1) <= '9') - s = ['x' s]; - end - % Collapse multiple underscores - s = regexprep(s, '_+', '_'); - if isempty(s) - s = sprintf('Var%d', i); - end - names{i} = s; - end -end +function T = readtable(filename, varargin) +%% READTABLE - Octave-compatible CSV/text file reader +% Drop-in replacement for MATLAB's readtable() returning a struct +% with columns accessible via dot notation (T.colname). +% +% Copyright (C) 2026 Grzegorz Sterniczuk +% License: GPL v3 (see LICENSE) +% +% Usage: +% T = readtable(filename) +% T = readtable(filename, 'HeaderLines', N) +% T = readtable(filename, 'Format', fmt) +% +% Returns a struct where each field is a column vector. +% T.Properties.VariableNames contains the column names. + + % Parse optional arguments + headerlines = 0; + fmt = ''; + for i = 1:2:length(varargin) + key = varargin{i}; + val = varargin{i+1}; + if strcmpi(key, 'HeaderLines') + headerlines = val; + elseif strcmpi(key, 'Format') + fmt = val; + end + end + + fid = fopen(filename, 'r'); + if fid == -1 + error('readtable: cannot open file %s', filename); + end + + % Skip header lines + for i = 1:headerlines + fgetl(fid); + end + + % Read the first non-skipped line as column headers + header_line = fgetl(fid); + if ~ischar(header_line) + fclose(fid); + T = struct(); + T.Properties = struct('VariableNames', {{}}); + return; + end + + % Parse column names from header + col_names = strsplit(strtrim(header_line), ','); + clean_names = sanitize_varnames(col_names); + ncols = length(clean_names); + + % Read first data line to detect column types + first_data_line = fgetl(fid); + if ~ischar(first_data_line) + fclose(fid); + T = struct(); + for j = 1:ncols + T.(clean_names{j}) = []; + end + T.Properties = struct('VariableNames', {clean_names}); + return; + end + + % Detect which columns are numeric vs string + is_numeric = true(1, ncols); + first_fields = strsplit(first_data_line, ','); + for j = 1:min(length(first_fields), ncols) + val = strtrim(first_fields{j}); + num = str2double(val); + % Non-numeric value, or column header contains "flags" -> force string + if (isnan(num) && ~strcmpi(val, 'nan')) || ~isempty(regexpi(col_names{j}, 'flags')) + is_numeric(j) = false; + end + end + + % Build format string based on detected types + fmt_parts = cell(1, ncols); + for j = 1:ncols + if is_numeric(j) + fmt_parts{j} = '%f'; + else + fmt_parts{j} = '%s'; + end + end + fmt_str = strjoin(fmt_parts, ''); + + % Rewind to start of data (skip headerlines + column header line) + frewind(fid); + for i = 1:(headerlines + 1) + fgetl(fid); + end + + % Use textscan directly on file handle - orders of magnitude faster + % than reading lines into cells and joining them + C = textscan(fid, fmt_str, 'Delimiter', ',', 'EmptyValue', NaN); + fclose(fid); + + if length(C) == ncols && ~isempty(C{1}) + T = struct(); + for j = 1:ncols + T.(clean_names{j}) = C{j}; + end + T.Properties = struct('VariableNames', {clean_names}); + return; + end + + % Fallback: reopen and parse line by line (slow, for edge cases) + warning('readtable: textscan returned %d/%d columns, falling back to line-by-line', length(C), ncols); + fid = fopen(filename, 'r'); + for i = 1:(headerlines + 1) + fgetl(fid); + end + + data_lines = {}; + while ~feof(fid) + line = fgetl(fid); + if ischar(line) && ~isempty(strtrim(line)) + data_lines{end+1} = line; + end + end + fclose(fid); + + nrows = length(data_lines); + string_cols = cell(nrows, ncols); + numeric_cols = NaN(nrows, ncols); + + for i = 1:nrows + fields = strsplit(data_lines{i}, ','); + for j = 1:min(length(fields), ncols) + val = strtrim(fields{j}); + num = str2double(val); + if ~isnan(num) || strcmpi(val, 'nan') + numeric_cols(i, j) = num; + else + is_numeric(j) = false; + string_cols{i, j} = val; + end + end + end + + T = struct(); + for j = 1:ncols + if is_numeric(j) + T.(clean_names{j}) = numeric_cols(:, j); + else + T.(clean_names{j}) = string_cols(:, j); + end + end + T.Properties = struct('VariableNames', {clean_names}); + +end + + +function names = sanitize_varnames(raw_names) + % Convert raw CSV header names to valid MATLAB/Octave variable names + % Mimics MATLAB's readtable sanitization: + % 'time (us)' -> 'time_us_' + % 'axisP[0]' -> 'axisP_0_' + names = cell(size(raw_names)); + for i = 1:length(raw_names) + s = strtrim(raw_names{i}); + % Replace brackets, spaces, parentheses with underscores + s = regexprep(s, '[\[\]\(\) ]+', '_'); + % Replace any remaining non-alphanumeric/underscore chars + s = regexprep(s, '[^a-zA-Z0-9_]', '_'); + % Force doesn't start with a number + if ~isempty(s) && (s(1) >= '0' && s(1) <= '9') + s = ['x' s]; + end + % Collapse multiple underscores + s = regexprep(s, '_+', '_'); + if isempty(s) + s = sprintf('Var%d', i); + end + names{i} = s; + end +end diff --git a/src/compat/smooth.m b/src/compat/smooth.m index 0e6903a..2be3d55 100644 --- a/src/compat/smooth.m +++ b/src/compat/smooth.m @@ -1,72 +1,72 @@ -function ys = smooth(y, span, method) -%% SMOOTH - Octave-compatible data smoothing function -% Drop-in replacement for MATLAB Curve Fitting Toolbox smooth() -% -% Usage: -% ys = smooth(y) - 5-point moving average -% ys = smooth(y, span) - span-point moving average -% ys = smooth(y, span, method) - with method: 'moving', 'lowess', 'loess' -% -% Methods: -% 'moving' - Simple moving average (default) -% 'lowess' - Locally weighted linear regression (tri-cube weights) -% 'loess' - Locally weighted quadratic regression (tri-cube weights) - - if nargin < 2, span = 5; end - if nargin < 3, method = 'moving'; end - - y = y(:); % ensure column vector - n = length(y); - - % Clamp span - span = min(round(span), n); - span = max(span, 1); - % Ensure span is odd for symmetric window - if mod(span, 2) == 0 - span = span + 1; - end - - switch lower(method) - case 'moving' - ys = smooth_moving(y, n, span); - case 'lowess' - ys = smooth_loess(y, n, span, 1); - case 'loess' - ys = smooth_loess(y, n, span, 2); - otherwise - ys = smooth_moving(y, n, span); - end - -end - - -function ys = smooth_moving(y, n, span) - % Moving average with edge handling (shrinking window at boundaries) - ys = zeros(n, 1); - half = floor(span / 2); - % Use cumulative sum for O(n) moving average - cs = [0; cumsum(y)]; - for i = 1:n - lo = max(1, i - half); - hi = min(n, i + half); - ys(i) = (cs(hi + 1) - cs(lo)) / (hi - lo + 1); - end -end - - -function ys = smooth_loess(y, n, span, degree) - % Local regression approximation using Savitzky-Golay filter - % Much faster than per-point weighted least squares (O(n) vs O(n*span)) - if span <= degree - ys = y; - return; - end - try - ys = sgolayfilt(y, degree, span); - catch - % Fallback: triple-pass moving average (approximates local regression) - ys = smooth_moving(y, n, span); - ys = smooth_moving(ys, n, span); - ys = smooth_moving(ys, n, span); - end -end +function ys = smooth(y, span, method) +%% SMOOTH - Octave-compatible data smoothing function +% Drop-in replacement for MATLAB Curve Fitting Toolbox smooth() +% +% Usage: +% ys = smooth(y) - 5-point moving average +% ys = smooth(y, span) - span-point moving average +% ys = smooth(y, span, method) - with method: 'moving', 'lowess', 'loess' +% +% Methods: +% 'moving' - Simple moving average (default) +% 'lowess' - Locally weighted linear regression (tri-cube weights) +% 'loess' - Locally weighted quadratic regression (tri-cube weights) + + if nargin < 2, span = 5; end + if nargin < 3, method = 'moving'; end + + y = y(:); % force column vector + n = length(y); + + % Clamp span + span = min(round(span), n); + span = max(span, 1); + % Force span is odd for symmetric window + if mod(span, 2) == 0 + span = span + 1; + end + + switch lower(method) + case 'moving' + ys = smooth_moving(y, n, span); + case 'lowess' + ys = smooth_loess(y, n, span, 1); + case 'loess' + ys = smooth_loess(y, n, span, 2); + otherwise + ys = smooth_moving(y, n, span); + end + +end + + +function ys = smooth_moving(y, n, span) + % Moving average with edge handling (shrinking window at boundaries) + ys = zeros(n, 1); + half = floor(span / 2); + % Use cumulative sum for O(n) moving average + cs = [0; cumsum(y)]; + for i = 1:n + lo = max(1, i - half); + hi = min(n, i + half); + ys(i) = (cs(hi + 1) - cs(lo)) / (hi - lo + 1); + end +end + + +function ys = smooth_loess(y, n, span, degree) + % Local regression approximation using Savitzky-Golay filter + % Much faster than per-point weighted least squares (O(n) vs O(n*span)) + if span <= degree + ys = y; + return; + end + try + ys = sgolayfilt(y, degree, span); + catch + % Fallback: triple-pass moving average (approximates local regression) + ys = smooth_moving(y, n, span); + ys = smooth_moving(ys, n, span); + ys = smooth_moving(ys, n, span); + end +end diff --git a/src/compat/writetable.m b/src/compat/writetable.m index 2a78481..91a59c8 100644 --- a/src/compat/writetable.m +++ b/src/compat/writetable.m @@ -1,59 +1,59 @@ -function writetable(T, filename) -%% WRITETABLE - Octave-compatible table-to-file writer -% Drop-in replacement for MATLAB's writetable(). -% -% Usage: -% writetable(T, filename) -% -% Writes the struct-based table T to a text file. -% Appends .txt extension if no extension present. - - % Ensure .txt extension - if isempty(strfind(filename, '.')) - filename = [filename '.txt']; - end - - var_names = T.Properties.VariableNames; - ncols = length(var_names); - - fid = fopen(filename, 'w'); - if fid == -1 - error('writetable: cannot open file %s for writing', filename); - end - - % Write header - fprintf(fid, '%s', var_names{1}); - for j = 2:ncols - fprintf(fid, ',%s', var_names{j}); - end - fprintf(fid, '\n'); - - % Determine number of rows - first_col = T.(var_names{1}); - if iscell(first_col) - nrows = length(first_col); - else - nrows = size(first_col, 1); - end - - % Write data rows - for i = 1:nrows - for j = 1:ncols - col = T.(var_names{j}); - if j > 1 - fprintf(fid, ','); - end - if iscell(col) - fprintf(fid, '%s', col{i}); - elseif isnumeric(col) - fprintf(fid, '%g', col(i)); - else - fprintf(fid, '%s', col(i)); - end - end - fprintf(fid, '\n'); - end - - fclose(fid); - -end +function writetable(T, filename) +%% WRITETABLE - Octave-compatible table-to-file writer +% Drop-in replacement for MATLAB's writetable(). +% +% Usage: +% writetable(T, filename) +% +% Writes the struct-based table T to a text file. +% Appends .txt extension if no extension present. + + % Force .txt extension + if isempty(strfind(filename, '.')) + filename = [filename '.txt']; + end + + var_names = T.Properties.VariableNames; + ncols = length(var_names); + + fid = fopen(filename, 'w'); + if fid == -1 + error('writetable: cannot open file %s for writing', filename); + end + + % Write header + fprintf(fid, '%s', var_names{1}); + for j = 2:ncols + fprintf(fid, ',%s', var_names{j}); + end + fprintf(fid, '\n'); + + % Determine number of rows + first_col = T.(var_names{1}); + if iscell(first_col) + nrows = length(first_col); + else + nrows = size(first_col, 1); + end + + % Write data rows + for i = 1:nrows + for j = 1:ncols + col = T.(var_names{j}); + if j > 1 + fprintf(fid, ','); + end + if iscell(col) + fprintf(fid, '%s', col{i}); + elseif isnumeric(col) + fprintf(fid, '%g', col(i)); + else + fprintf(fid, '%s', col(i)); + end + end + fprintf(fid, '\n'); + end + + fclose(fid); + +end diff --git a/src/core/PSPercent.m b/src/core/PSPercent.m index af58d5c..91db68a 100644 --- a/src/core/PSPercent.m +++ b/src/core/PSPercent.m @@ -1,7 +1,7 @@ -function [P] = PSPercent(X) -% converts raw RCcommand to percent - rcCommandf = abs(X); - P = ((rcCommandf-0) / (500)) * 100; -end - - +function [P] = PSPercent(X) +% converts raw RCcommand to percent + rcCommandf = abs(X); + P = ((rcCommandf-0) / (500)) * 100; +end + + diff --git a/src/core/PSSpec2d.m b/src/core/PSSpec2d.m index 2bd3c74..e3f468d 100644 --- a/src/core/PSSpec2d.m +++ b/src/core/PSSpec2d.m @@ -1,35 +1,35 @@ -function [Fs spec] = PSSpec2d(Y, F, psd) -%% [Fs spec] = PSSpec2d(Y, F, psd) -% computes standard fft on input data Y. F is sample frequency in Hz. - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if psd - % N = length(Y); - % [psdx,Fs] = periodogram(Y,[], N-1, F*1000,'psd'); % power psd - % [spec,Fs] = pspectrum(Y, F*1000, 'FrequencyResolution', 10); - - N = length(Y); - Fs = ((F*1000)*(0:(N/2))/N); - Y = Y.*hann(N)'; - Y = fft(Y); - psdx = abs(Y).^2 / (F*1000*N); % (1/(F*N)) * abs(Y).^2 is exactly same as abs(Y).^2 / (F*N), to - psdx(2:end-1) = 2*psdx(2:end-1); - psdx = psdx(1:N/2+1); -% % scale to dB - spec = 10 * log10(psdx)'; -else - N = length(Y); - Fs = ((F*1000)*(0:(N/2))/N); - Y = Y.*hann(N)'; - Y = fft(Y); - spec = abs(Y) / N; % (1/(F*N)) * abs(Y).^2 is exactly same as abs(Y).^2 / (F*N), to - spec = spec(1:N/2+1)'; -end - -end +function [Fs spec] = PSSpec2d(Y, F, psd) +%% [Fs spec] = PSSpec2d(Y, F, psd) +% computes standard fft on input data Y. F is sample frequency in Hz. + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +N = length(Y); +if N < 4 + Fs = []; spec = []; + return; +end + +if psd + Fs = ((F*1000)*(0:(N/2))/N); + Y = Y.*hann(N)'; + Y = fft(Y); + psdx = abs(Y).^2 / (F*1000*N); % (1/(F*N)) * abs(Y).^2 is exactly same as abs(Y).^2 / (F*N), to + psdx(2:end-1) = 2*psdx(2:end-1); + psdx = psdx(1:N/2+1); +% % scale to dB + spec = 10 * log10(psdx)'; +else + Fs = ((F*1000)*(0:(N/2))/N); + Y = Y.*hann(N)'; + Y = fft(Y); + spec = abs(Y) / N; % (1/(F*N)) * abs(Y).^2 is exactly same as abs(Y).^2 / (F*N), to + spec = spec(1:N/2+1)'; +end + +end diff --git a/src/core/PSarduConvert.m b/src/core/PSarduConvert.m index 77045be..2bb56aa 100644 --- a/src/core/PSarduConvert.m +++ b/src/core/PSarduConvert.m @@ -5,7 +5,17 @@ % parms: struct from PSarduRead (parameter name→value) if ~isfield(data, 'RATE') - error('No RATE messages in log — enable LOG_BITMASK bit 1 (ATTITUDE_FAST)'); + error('No RATE messages in log - enable LOG_BITMASK bit 1 (ATTITUDE_FAST)'); +end + +% Older AP firmware (pre-2017) used TimeMS instead of TimeUS +msgNames = fieldnames(data); +for mi = 1:numel(msgNames) + m = data.(msgNames{mi}); + if isstruct(m) && ~isfield(m, 'TimeUS') && isfield(m, 'TimeMS') + m.TimeUS = double(m.TimeMS) * 1000; + data.(msgNames{mi}) = m; + end end rate = data.RATE; @@ -36,7 +46,7 @@ T_out.setpoint_3_ = zeros(N, 1); end -% PID terms — interpolate to RATE timestamps +% PID terms - interpolate to RATE timestamps axes_map = {'PIDR', '0'; 'PIDP', '1'; 'PIDY', '2'}; for ax = 1:3 msgName = axes_map{ax, 1}; @@ -98,7 +108,7 @@ end end -% debug fields (empty — ArduPilot doesn't use BF debug channels) +% debug fields (empty - ArduPilot doesn't use BF debug channels) for k = 0:3 T_out.(sprintf('debug_%d_', k)) = zeros(N, 1); end @@ -152,7 +162,7 @@ SetupInfo{n,1} = 'd_min'; SetupInfo{n,2} = sprintf('0,0,0'); n = n+1; SetupInfo{n,1} = 'feedforward_weight'; SetupInfo{n,2} = sprintf('%.0f,%.0f,%.0f', rFF*100, pFF*100, yFF*100); n = n+1; -% debug_mode — ArduPilot doesn't have BF debug modes +% debug_mode - ArduPilot doesn't have BF debug modes SetupInfo{n,1} = 'debug_mode'; SetupInfo{n,2} = '0'; n = n+1; % looptime diff --git a/src/core/PSarduRead.m b/src/core/PSarduRead.m index 10cfa9b..ccf6ba1 100644 --- a/src/core/PSarduRead.m +++ b/src/core/PSarduRead.m @@ -109,13 +109,22 @@ % nested function to parse PARM (has string fields, can't fully vectorize) function parse_parms(payloads, nMsg, fstr, ~) layout = field_layout(fstr); + nameIdx = 0; valIdx = 0; + for li = 1:length(layout) + t = layout(li).type; + if nameIdx == 0 && any(t == 'NZna'), nameIdx = li; end + if valIdx == 0 && any(t == 'fe'), valIdx = li; end + end + if nameIdx == 0 || valIdx == 0, return; end for m = 1:nMsg - row = payloads(m, :); - pname = deblank(char(row(layout(2).off : layout(2).off + layout(2).sz - 1))); - pname(pname == 0) = []; - pname = regexprep(pname, '[^a-zA-Z0-9_]', '_'); - if isempty(pname), continue; end - vbytes = row(layout(3).off : layout(3).off + 3); + rawBytes = payloads(m, layout(nameIdx).off : layout(nameIdx).off + layout(nameIdx).sz - 1); + rawBytes = rawBytes(rawBytes ~= 0 & rawBytes < 128); + pname = deblank(char(rawBytes)); + pname(~((pname>='a'&pname<='z')|(pname>='A'&pname<='Z')|(pname>='0'&pname<='9')|pname=='_')) = '_'; + if isempty(pname) || ~(isletter(pname(1)) || pname(1) == '_') + continue; + end + vbytes = payloads(m, layout(valIdx).off : layout(valIdx).off + 3); parms.(pname) = double(typecast(uint8(vbytes), 'single')); end end diff --git a/src/core/PSbfFilters.m b/src/core/PSbfFilters.m index e4f1dfe..89fa2cd 100644 --- a/src/core/PSbfFilters.m +++ b/src/core/PSbfFilters.m @@ -1,55 +1,55 @@ -function [b, a] = PSbfFilters(type, fc, Fs, Q) -%% PSbfFilters - BF-compatible discrete filter coefficients -% type: 'pt1','pt2','pt3','biquad','notch' -% fc: cutoff/center freq (Hz) -% Fs: sample rate (Hz) -% Q: quality factor (notch only, default 3.5 = BF dyn_notch_q/100) - -if nargin < 4, Q = 1/sqrt(2); end -if fc <= 0 || fc >= Fs/2 - b = 1; a = 1; return; -end - -Ts = 1/Fs; - -switch lower(type) - case 'pt1' - RC = 1/(2*pi*fc); - k = Ts/(RC + Ts); - b = [k 0]; a = [1 -(1-k)]; - - case 'pt2' - fc_corr = fc * 1.553773974; - RC = 1/(2*pi*fc_corr); - k = Ts/(RC + Ts); - b1 = [k 0]; a1 = [1 -(1-k)]; - b = conv(b1, b1); a = conv(a1, a1); - - case 'pt3' - fc_corr = fc * 1.961459177; - RC = 1/(2*pi*fc_corr); - k = Ts/(RC + Ts); - b1 = [k 0]; a1 = [1 -(1-k)]; - b = conv(conv(b1, b1), b1); a = conv(conv(a1, a1), a1); - - case 'biquad' - w0 = 2*pi*fc/Fs; - alpha = sin(w0) / (2 * (1/sqrt(2))); - b0 = (1 - cos(w0)) / 2; - b1 = 1 - cos(w0); - b2 = (1 - cos(w0)) / 2; - a0 = 1 + alpha; - b = [b0 b1 b2] / a0; - a = [1 -2*cos(w0)/a0 (1-alpha)/a0]; - - case 'notch' - w0 = 2*pi*fc/Fs; - alpha = sin(w0) / (2*Q); - a0 = 1 + alpha; - b = [1 -2*cos(w0) 1] / a0; - a = [1 -2*cos(w0)/a0 (1-alpha)/a0]; - - otherwise - b = 1; a = 1; -end -end +function [b, a] = PSbfFilters(type, fc, Fs, Q) +%% PSbfFilters - BF-compatible discrete filter coefficients +% type: 'pt1','pt2','pt3','biquad','biquad-bessel','notch' +% fc: cutoff/center freq (Hz) +% Fs: sample rate (Hz) +% Q: quality factor (notch only, default 3.5 = BF dyn_notch_q/100) + +if nargin < 4, Q = 1/sqrt(2); end +if fc <= 0 || fc >= Fs/2 + b = 1; a = 1; return; +end + +Ts = 1/Fs; + +switch lower(type) + case 'pt1' + RC = 1/(2*pi*fc); + k = Ts/(RC + Ts); + b = [k 0]; a = [1 -(1-k)]; + + case 'pt2' + fc_corr = fc * 1.553773974; + RC = 1/(2*pi*fc_corr); + k = Ts/(RC + Ts); + b1 = [k 0]; a1 = [1 -(1-k)]; + b = conv(b1, b1); a = conv(a1, a1); + + case 'pt3' + fc_corr = fc * 1.961459177; + RC = 1/(2*pi*fc_corr); + k = Ts/(RC + Ts); + b1 = [k 0]; a1 = [1 -(1-k)]; + b = conv(conv(b1, b1), b1); a = conv(conv(a1, a1), a1); + + case 'biquad' + w0 = 2*pi*fc/Fs; + alpha = sin(w0) / (2 * (1/sqrt(2))); + b0 = (1 - cos(w0)) / 2; + b1 = 1 - cos(w0); + b2 = (1 - cos(w0)) / 2; + a0 = 1 + alpha; + b = [b0 b1 b2] / a0; + a = [1 -2*cos(w0)/a0 (1-alpha)/a0]; + + case 'notch' + w0 = 2*pi*fc/Fs; + alpha = sin(w0) / (2*Q); + a0 = 1 + alpha; + b = [1 -2*cos(w0) 1] / a0; + a = [1 -2*cos(w0)/a0 (1-alpha)/a0]; + + otherwise + b = 1; a = 1; +end +end diff --git a/src/core/PSestimateRPM.m b/src/core/PSestimateRPM.m index 648a574..84e511c 100644 --- a/src/core/PSestimateRPM.m +++ b/src/core/PSestimateRPM.m @@ -1,75 +1,75 @@ -function [fundFreq, harmonics] = PSestimateRPM(freqAxis, ampMatrix, nHarmonics) -%% PSestimateRPM - estimate motor fundamental frequency from throttle-binned spectra -% freqAxis - 1×M frequency vector in Hz (from PSthrSpec freq(1,:)) -% ampMatrix - 100×M amplitude matrix (throttle bins × freq bins) -% nHarmonics - number of harmonics to return (default 3) -% -% Returns: -% fundFreq - 100×1 estimated fundamental freq per throttle bin (Hz), NaN where unknown -% harmonics - 100×nHarmonics matrix (Hz) - -if nargin < 3, nHarmonics = 3; end - -nBins = size(ampMatrix, 1); -fundFreq = NaN(nBins, 1); - -% motor noise band limits -fLo = 80; -fHi = 500; -fMask = freqAxis >= fLo & freqAxis <= fHi; -fIdx = find(fMask); - -if isempty(fIdx), harmonics = NaN(nBins, nHarmonics); return; end - -for t = 1:nBins - spec = ampMatrix(t, :); - if all(spec == 0), continue; end - - band = spec(fIdx); - noiseFloor = median(band); - threshold = noiseFloor + (max(band) - noiseFloor) * 0.3; - - % find local maxima above threshold - pkIdx = []; - pkVal = []; - for j = 2:length(band)-1 - if band(j) > band(j-1) && band(j) > band(j+1) && band(j) > threshold - pkIdx(end+1) = j; - pkVal(end+1) = band(j); - end - end - if isempty(pkIdx), continue; end - - % sort by amplitude, take top candidate - [~, si] = sort(pkVal, 'descend'); - f0 = freqAxis(fIdx(pkIdx(si(1)))); - - % check for harmonic confirmation: peak near 2*f0 - f2lo = f0 * 1.8; - f2hi = f0 * 2.2; - f2mask = freqAxis >= f2lo & freqAxis <= f2hi; - if any(f2mask) && max(spec(f2mask)) > noiseFloor * 1.5 - fundFreq(t) = f0; - else - % might be a harmonic itself — check if f0/2 has a peak - fhlo = f0 * 0.4; - fhhi = f0 * 0.6; - fhmask = freqAxis >= fhlo & freqAxis <= fhhi & fMask; - if any(fhmask) && max(spec(fhmask)) > noiseFloor * 1.3 - fundFreq(t) = freqAxis(fIdx(pkIdx(si(1)))) / 2; - else - fundFreq(t) = f0; - end - end -end - -% smooth across throttle bins to reduce jitter -validMask = ~isnan(fundFreq); -if sum(validMask) > 5 - fundFreq(validMask) = smooth(fundFreq(validMask), 5, 'moving'); -end - -% generate harmonics -harmonics = fundFreq .* (1:nHarmonics); - -end +function [fundFreq, harmonics] = PSestimateRPM(freqAxis, ampMatrix, nHarmonics) +%% PSestimateRPM - estimate motor fundamental frequency from throttle-binned spectra +% freqAxis - 1×M frequency vector in Hz (from PSthrSpec freq(1,:)) +% ampMatrix - 100×M amplitude matrix (throttle bins × freq bins) +% nHarmonics - number of harmonics to return (default 3) +% +% Returns: +% fundFreq - 100×1 estimated fundamental freq per throttle bin (Hz), NaN where unknown +% harmonics - 100×nHarmonics matrix (Hz) + +if nargin < 3, nHarmonics = 3; end + +nBins = size(ampMatrix, 1); +fundFreq = NaN(nBins, 1); + +% motor noise band limits +fLo = 80; +fHi = 500; +fMask = freqAxis >= fLo & freqAxis <= fHi; +fIdx = find(fMask); + +if isempty(fIdx), harmonics = NaN(nBins, nHarmonics); return; end + +for t = 1:nBins + spec = ampMatrix(t, :); + if all(spec == 0), continue; end + + band = spec(fIdx); + noiseFloor = median(band); + threshold = noiseFloor + (max(band) - noiseFloor) * 0.3; + + % find local maxima above threshold + pkIdx = []; + pkVal = []; + for j = 2:length(band)-1 + if band(j) > band(j-1) && band(j) > band(j+1) && band(j) > threshold + pkIdx(end+1) = j; + pkVal(end+1) = band(j); + end + end + if isempty(pkIdx), continue; end + + % sort by amplitude, take top candidate + [~, si] = sort(pkVal, 'descend'); + f0 = freqAxis(fIdx(pkIdx(si(1)))); + + % check for harmonic confirmation: peak near 2*f0 + f2lo = f0 * 1.8; + f2hi = f0 * 2.2; + f2mask = freqAxis >= f2lo & freqAxis <= f2hi; + if any(f2mask) && max(spec(f2mask)) > noiseFloor * 1.5 + fundFreq(t) = f0; + else + % might be a harmonic itself - check if f0/2 has a peak + fhlo = f0 * 0.4; + fhhi = f0 * 0.6; + fhmask = freqAxis >= fhlo & freqAxis <= fhhi & fMask; + if any(fhmask) && max(spec(fhmask)) > noiseFloor * 1.3 + fundFreq(t) = freqAxis(fIdx(pkIdx(si(1)))) / 2; + else + fundFreq(t) = f0; + end + end +end + +% smooth across throttle bins to reduce jitter +validMask = ~isnan(fundFreq); +if sum(validMask) > 5 + fundFreq(validMask) = smooth(fundFreq(validMask), 5, 'moving'); +end + +% generate harmonics +harmonics = fundFreq .* (1:nHarmonics); + +end diff --git a/src/core/PSgetcsv.m b/src/core/PSgetcsv.m index 8782e3a..cad75e8 100644 --- a/src/core/PSgetcsv.m +++ b/src/core/PSgetcsv.m @@ -1,6 +1,8 @@ -function [filename csvFnames] = PSgetcsv(filename, firmware_flag) -%% [filename csvFnames] = PSgetcsv(filename, firmware_flag) +function [filename csvFnames] = PSgetcsv(filename, firmware_flag, outdir) +%% [filename csvFnames] = PSgetcsv(filename, firmware_flag, outdir) % Converts bbl files to csv using blackbox_decode +% filename: full path to BBL/BFL/TXT/BTFL/JSON/BIN file +% outdir: directory for CSV output (default: same as input file) % ---------------------------------------------------------------------------------- % "THE BEER-WARE LICENSE" (Revision 42): @@ -11,125 +13,108 @@ fnums = 1; +filename_nchars = 17; -a=strfind(filename, ' ');% had to remove white space to run in bb decode -a=fliplr(a); -if ~isempty((find(isspace(filename)))) - filename2=filename(find(~isspace(filename)));% have to get rid of spaces to run blackbox_decode using 'system' function - movefile(filename,filename2);%rename file without spaces - filename=filename2; - clear filename2 -end +mainFname = filename; +[fdir, fname, fext] = fileparts(filename); -filename_nchars = 17; -if size(filename,2)>20 - f=[filename(1:filename_nchars) '...']; - else - f=filename; +if nargin < 3 || isempty(outdir) + outdir = fdir; end - -mainFname=filename; -[fdir, fname, fext] = fileparts(filename); -fbase = fullfile(fdir, fname); + if strcmpi(fext, '.bin') - % ArduPilot DataFlash binary log - handled directly in PSload csvFnames = {filename}; return; elseif strcmpi(fext, '.json') - % QuickSilver JSON blackbox export - parse directly - [headerFile, csvFile] = PSquicJson2csv(filename); + [headerFile, csvFile] = PSquicJson2csv(filename, outdir); filename = headerFile; - files(1).name = csvFile; + [~, csvName, csvExt] = fileparts(csvFile); + files(1).name = [csvName csvExt]; fnums = 1; elseif any(strcmpi(fext, {'.BFL', '.BBL', '.TXT', '.BTFL'})) - if ispc() - bb_decode = 'blackbox_decode.exe'; - bb_decode_inav = 'blackbox_decode_INAV.exe'; + decoder_path = getappdata(0, 'PSdecoderPath'); + decoder_inav = getappdata(0, 'PSdecoderPathINAV'); + + if firmware_flag == 3 && ~isempty(decoder_inav) + % INAV decoder has no --output-dir; copy input to workdir first + tmpSrc = fullfile(outdir, [fname fext]); + copyfile(filename, tmpSrc); + cmd = ['"' decoder_inav '" "' tmpSrc '" 2>&1']; else - bb_decode = './blackbox_decode'; - bb_decode_inav = './blackbox_decode_INAV'; + cmd = ['"' decoder_path '" --output-dir "' outdir '" "' filename '" 2>&1']; end - if firmware_flag == 3 - [status,result]=system([bb_decode_inav ' ' filename ' 2>&1']); - else - [status,result]=system([bb_decode ' ' filename ' 2>&1']); + [status, result] = system(cmd); + if status ~= 0 + warning('blackbox_decode failed (exit %d): %s', status, result); + csvFnames = {}; + return; end - files=dir([fbase '*.csv']); - - % only choose files that don't have .bbl or .bfl extension - clear f2;m=1; - for k=1:size(files,1) - if ~contains(files(k).name,'.bbl','IgnoreCase',true) & ~contains(files(k).name,'.bfl','IgnoreCase',true) - f2(m,:)=files(k); - m=m+1; + + fbase = fullfile(outdir, fname); + files = dir([fbase '*.csv']); + + % filter out files with .bbl or .bfl in name + valid = true(size(files,1), 1); + for k = 1:size(files,1) + if contains(files(k).name, '.bbl', 'IgnoreCase', true) || contains(files(k).name, '.bfl', 'IgnoreCase', true) + valid(k) = false; end end - % report to user, the most common loading error - try - files=f2;clear f2; - catch % report blackbox_decode error to user - set(gcf, 'pointer', 'arrow') - end - - % get rid of all event files and gps.gpx files - fevt=dir([fbase '*.event']); - for k=1:size(fevt,1) - delete([fevt(k).name]); - end - fevt=dir([fbase '*.gps.gpx']); - for k=1:size(fevt,1) - delete([fevt(k).name]); - end - fevt=dir([fbase '*.gps.csv']); - for k=1:size(fevt,1) - delete([fevt(k).name]); + files = files(valid, :); + + if isempty(files) + set(gcf, 'pointer', 'arrow'); + csvFnames = {}; + return; end - - % get list of files after erasing junk + + % clean up junk files in outdir + fevt = dir([fbase '*.event']); + for k = 1:size(fevt,1), delete(fullfile(outdir, fevt(k).name)); end + fevt = dir([fbase '*.gps.gpx']); + for k = 1:size(fevt,1), delete(fullfile(outdir, fevt(k).name)); end + fevt = dir([fbase '*.gps.csv']); + for k = 1:size(fevt,1), delete(fullfile(outdir, fevt(k).name)); end + + % refresh file list after cleanup files = dir([fbase '*.csv']); - - % if more than one file + if size(files,1) > 1 - x=size(files,1); - clear f2; m=1; - for k=1:x - % Check file size instead of parsing entire CSV (much faster for large files) - emptysubfiles = (files(k).bytes < 1000); - - if emptysubfiles - delete(files(k).name) - files(k).name; - else - f2(m,:)=files(k); - m=m+1; - end - end - files=f2;clear f2 - a=strfind(result,'duration'); - logDurStr=''; - for d=1:length(a) - logDurStr{d}=[int2str(d) ') ' result(a(d):a(d)+filename_nchars)]; + % remove empty subfiles (<1KB) + valid = true(size(files,1), 1); + for k = 1:size(files,1) + if files(k).bytes < 1000 + delete(fullfile(outdir, files(k).name)); + valid(k) = false; + end end - - if size(files,1)>0 - x=size(files,1); - if x>1 % if multiple logs exist in BB file - [fnums , tf] = listdlg('ListString',logDurStr, 'ListSize',[250,round(size(logDurStr,2) * 20)], 'Name','Select file(s): ' ); - %%%% delete all unused BB decoded csv files - for k=1:x - if ~ismember(k, fnums), delete(files(k).name); end + files = files(valid, :); + + a = strfind(result, 'duration'); + logDurStr = ''; + for d = 1:length(a) + logDurStr{d} = [int2str(d) ') ' result(a(d):a(d)+filename_nchars)]; + end + + if size(files,1) > 0 + x = size(files,1); + if x > 1 + [fnums, tf] = listdlg('ListString', logDurStr, 'ListSize', [250, round(size(logDurStr,2)*20)], 'Name', 'Select file(s): '); + for k = 1:x + if ~ismember(k, fnums), delete(fullfile(outdir, files(k).name)); end end end else - validData=0; - a=errordlg(['no valid data in ' mainFname]);pause(3);close(a); - end + validData = 0; + a = errordlg(['no valid data in ' mainFname]); pause(3); close(a); + end end end -csvFnames={}; -for k = 1 : length(fnums) - csvFnames{k} = files(fnums(k)).name; + +csvFnames = {}; +for k = 1:length(fnums) + csvFnames{k} = fullfile(outdir, files(fnums(k)).name); end -end \ No newline at end of file +end diff --git a/src/core/PSimport.m b/src/core/PSimport.m index 8dfd550..5fc88d8 100644 --- a/src/core/PSimport.m +++ b/src/core/PSimport.m @@ -1,69 +1,80 @@ -function [DAT newFileName] = PSimport(csvFname, BBLFileName) -%% [DAT newFileName] = PSimport(filename, BBLFileName) -% Imports log data in multiple formats (fw=selected firmware/logfile type). Default is .csv, but if using .bbl or .bfl files, - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -newFileName=csvFname; -T=readtable(csvFname); - -delete(csvFname);% we r done with this-dont wanna leave junk on main directory - -% get header info from .bbl/.bfl file directly -a=strfind(BBLFileName,'.'); -fid = fopen(BBLFileName, 'r'); -c = textscan(fid, '%s', 'Delimiter','\n','CollectOutput',1); %c2 = native2unicode(c) -fclose(fid); - -logStartPoints=[]; -logEndPoints=[]; - -a=strfind(c{1},'Firmware version'); -if isempty(find(cellfun(@(a)~isempty(a)&&a>0,a))) - a=strfind(c{1},'Firmware revision'); - logStartPoints=find(cellfun(@(a)~isempty(a)&&a>0,a)); -else - a=strfind(c{1},'Firmware version'); - logStartPoints=find(cellfun(@(a)~isempty(a)&&a>0,a)); -end - -endStr={'throttle_boost_cutoff'; 'debug_mode' ; 'motor_pwm_rate' ; 'use_unsynced_pwm' ; 'gyro_32khz_hardware_lpf' ; 'gyro_hardware_lpf' ; 'yaw_deadband' ; 'deadband' ; 'pidsum_limit_yaw' ; 'pidsum_limit' ; 'energyCumulative (mAh)'; 'looptime'};% strings not in all revisions, try a few -m=1; a=strfind(c{1},endStr{m}); -while isempty(find(cellfun(@(a)~isempty(a)&&a>0,a))) - a=strfind(c{1},endStr{m}); - m=m+1; -end -logEndPoints = find(cellfun(@(a)~isempty(a)&&a>0,a)); - -relevantLogNum=str2num(csvFname(end-5:end-4)); -s=c{1}(logStartPoints(relevantLogNum):logEndPoints(relevantLogNum)); -n=1; -for m=1:size(s,1) - if size(strsplit(char(s(m)),':'),2)==2 - SetupInfo(n,:)=(strsplit(char(s(m)),':')); - a=char(SetupInfo(n,1)); - if strcmp(a(1:2),'H ') - SetupInfo(n,1)={a(3:end)}; - end - n=n+1; - end -end - - -% -DAT.T=T; -DAT.mainFname=BBLFileName; -DAT.csvFname=csvFname; -DAT.SetupInfo=SetupInfo; -try -DAT.blackbox_decode_results=result; -catch -end - -end - +function [DAT newFileName] = PSimport(csvFname, BBLFileName) +%% [DAT newFileName] = PSimport(filename, BBLFileName) +% Imports log data in multiple formats (fw=selected firmware/logfile type). Default is .csv, but if using .bbl or .bfl files, + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +[~, nfn, nfx] = fileparts(csvFname); +newFileName = [nfn nfx]; +T=readtable(csvFname); + +delete(csvFname);% we r done with this-dont wanna leave junk on main directory + +% get header info from .bbl/.bfl file directly +a=strfind(BBLFileName,'.'); +fid = fopen(BBLFileName, 'r'); +c = textscan(fid, '%s', 'Delimiter','\n','CollectOutput',1); %c2 = native2unicode(c) +fclose(fid); + +logStartPoints=[]; +logEndPoints=[]; + +a=strfind(c{1},'Firmware version'); +if isempty(find(cellfun(@(a)~isempty(a)&&a>0,a))) + a=strfind(c{1},'Firmware revision'); + logStartPoints=find(cellfun(@(a)~isempty(a)&&a>0,a)); +else + a=strfind(c{1},'Firmware version'); + logStartPoints=find(cellfun(@(a)~isempty(a)&&a>0,a)); +end + +endStr={'throttle_boost_cutoff'; 'debug_mode' ; 'motor_pwm_rate' ; 'use_unsynced_pwm' ; 'gyro_32khz_hardware_lpf' ; 'gyro_hardware_lpf' ; 'yaw_deadband' ; 'deadband' ; 'pidsum_limit_yaw' ; 'pidsum_limit' ; 'energyCumulative (mAh)'; 'looptime'}; +logEndPoints = []; +for m = 1:numel(endStr) + a = strfind(c{1}, endStr{m}); + hits = find(cellfun(@(a) ~isempty(a) && a > 0, a)); + if ~isempty(hits), logEndPoints = hits; break; end +end +if isempty(logEndPoints) + % fallback: last "H " line per log section (KISS, etc.) + hLines = find(strncmp(c{1}, 'H ', 2)); + if ~isempty(hLines) + logEndPoints = zeros(size(logStartPoints)); + for k = 1:numel(logStartPoints) + logEndPoints(k) = max(hLines(hLines >= logStartPoints(k))); + end + end +end + +relevantLogNum=str2double(csvFname(end-5:end-4)); +s=c{1}(logStartPoints(relevantLogNum):logEndPoints(relevantLogNum)); +n=1; +for m=1:size(s,1) + if size(strsplit(char(s(m)),':'),2)==2 + SetupInfo(n,:)=(strsplit(char(s(m)),':')); + a=char(SetupInfo(n,1)); + if strcmp(a(1:2),'H ') + SetupInfo(n,1)={a(3:end)}; + end + n=n+1; + end +end + + +% +DAT.T=T; +DAT.mainFname=BBLFileName; +DAT.csvFname=csvFname; +DAT.SetupInfo=SetupInfo; +try +DAT.blackbox_decode_results=result; +catch +end + +end + diff --git a/src/core/PSload.m b/src/core/PSload.m index f254aa5..c93cfd6 100644 --- a/src/core/PSload.m +++ b/src/core/PSload.m @@ -14,7 +14,7 @@ try if ~isempty(filenameA) - %% Detect firmware type change — mixing firmware types causes issues + %% Detect firmware type change - mixing firmware types causes issues current_fw = get(guiHandles.Firmware, 'Value'); if exist('loaded_firmware','var') && loaded_firmware ~= current_fw && exist('fnameMaster','var') && ~isempty(fnameMaster) fw_names = get(guiHandles.Firmware, 'String'); @@ -25,14 +25,25 @@ 'Reset data before loading?'], ... 'Firmware type changed', 'Reset & Load', 'Cancel', 'Reset & Load'); if strcmp(choice, 'Reset & Load') - clear T dataA tta A_lograte epoch1_A epoch2_A SetupInfo rollPIDF pitchPIDF yawPIDF debugmode debugIdx fwType fwMajor fwMinor gyro_debug_axis notchData; - fcnt = 0; fnameMaster = {}; - try, delete(subplot('position',posInfo.linepos1)); catch, end - try, delete(subplot('position',posInfo.linepos2)); catch, end - try, delete(subplot('position',posInfo.linepos3)); catch, end - try, delete(subplot('position',posInfo.linepos4)); catch, end + clear T dataA tta A_lograte epoch1_A epoch2_A SetupInfo rollPIDF pitchPIDF yawPIDF debugmode debugIdx fwType fwMajor fwMinor gyro_debug_axis notchData rpmFilterData ampmat freq2d2 amp2d2 specMat delayDataReady FilterDelayDterm SPGyroDelay Debug01 Debug02 gyro_phase_shift_deg dterm_phase_shift_deg tuneCrtlpanel_init setupInfoWidgets_init; + fcnt = 0; fnameMaster = {}; Nfiles = 0; + try, delete(checkpanel); clear checkpanel; catch, end + % Delete tagged plot axes (never use subplot — creates blank axes) + try delete(findobj(PSfig,'Tag','PSrpy')); catch, end + try delete(findobj(PSfig,'Tag','PSmotor')); catch, end + try delete(findobj(PSfig,'Tag','PScombo')); catch, end + % Delete overlay widgets + ov = getappdata(PSfig, 'PSoverlay'); + if ~isempty(ov) + flds = fieldnames(ov); + for fi=1:numel(flds), try delete(ov.(flds{fi})); catch, end; end + setappdata(PSfig, 'PSoverlay', []); + end + figs=findobj('Type','figure'); for fi=1:numel(figs), if figs(fi)~=PSfig, try, close(figs(fi)); catch, end; end; end + clear PSspecfig PSspecfig2 PSspecfig3 PStunefig PSerrfig PSstatsfig PSdisp errCrtlpanel statsCrtlpanel spec2Crtlpanel specCrtlpanel freqTimeCrtlpanel tuneCrtlpanel fcntSR; set(guiHandles.FileNum, 'String', ' '); try, set(guiHandles.Epoch1_A_Input, 'String', ' '); set(guiHandles.Epoch2_A_Input, 'String', ' '); catch, end + try setappdata(PSfig, 'smoothCacheLV', struct()); catch, end else return; end @@ -40,6 +51,7 @@ loaded_firmware = current_fw; logfile_directory=filepathA; + try setappdata(PSfig, 'rfMotorCount', []); catch, end us2sec=1000000; maxMotorOutput=2000; @@ -64,13 +76,9 @@ try defaults = readtable('PSdefaults.txt'); a = char([cellstr([char(defaults.Parameters) num2str(defaults.Values)]); {rdr}; {mdr}; {ldr}]); - t = uitable(PSfig, 'ColumnWidth',{500},'ColumnFormat',{'char'},'Data',[cellstr(a)]); - set(t,'units','normalized','Position',infoTablePos,'FontSize',fontsz*.8, 'ColumnName', ['']) catch - defaults = ' '; + defaults = ' '; a = char(['Unable to set user defaults '; {rdr}; {mdr}; {ldr}]); - t = uitable(PSfig, 'ColumnWidth',{500},'ColumnFormat',{'char'},'Data',[cellstr(a)]); - set(t,'units','normalized','Position',infoTablePos,'FontSize',fontsz*.8, 'ColumnName', ['']) end fnameMaster = [fnameMaster filenameA]; @@ -79,37 +87,14 @@ n = size(filenameA,2); waitbarFid = waitbar(0,'Please wait...'); - % Work in temp dir (main_directory may be read-only in AppImage) workdir = tempname(); mkdir(workdir); - prev_dir = pwd(); - - % Copy blackbox_decode into workdir so ./blackbox_decode works - if ispc() - decoders = {'blackbox_decode.exe', 'blackbox_decode_INAV.exe'}; - else - decoders = {'blackbox_decode', 'blackbox_decode_INAV'}; - end - for dec = decoders - src = fullfile(main_directory, dec{1}); - if exist(src, 'file') - copyfile(src, workdir); - end - end - - cd(workdir); for ii = 1 : n - source = fullfile(logfile_directory, filenameA{ii}); - try - copyfile(source, workdir); - catch e - warning('PSload: cannot copy %s to workdir: %s', source, e.message); - continue; - end + srcFile = fullfile(logfile_directory, filenameA{ii}); clear subFiles; - [filenameA{ii} subFiles] = PSgetcsv(filenameA{ii}, get(guiHandles.Firmware, 'Value')); + [filenameA{ii} subFiles] = PSgetcsv(srcFile, get(guiHandles.Firmware, 'Value'), workdir); for jj = 1 : size(subFiles,2) @@ -120,13 +105,13 @@ [~, ~, sfext] = fileparts(subFiles{jj}); if strcmpi(sfext, '.bin') - % ArduPilot DataFlash binary — direct parse - binpath = fullfile(logfile_directory, subFiles{jj}); - [ardu_data, ardu_parms] = PSarduRead(binpath); + % ArduPilot DataFlash binary - direct parse + [ardu_data, ardu_parms] = PSarduRead(subFiles{jj}); [T{fcnt}, SetupInfo{fcnt}, A_lograte(fcnt)] = PSarduConvert(ardu_data, ardu_parms); - fnameMaster{fcnt} = subFiles{jj}; + [~, sfname, sfx] = fileparts(subFiles{jj}); + fnameMaster{fcnt} = [sfname sfx]; else - [dataA(fcnt) fnameMaster{fcnt}] = PSimport(subFiles{jj}, char(filenameA{ii})); + [dataA(fcnt) fnameMaster{fcnt}] = PSimport(subFiles{jj}, filenameA{ii}); T{fcnt}=dataA(fcnt).T; A_lograte(fcnt)=round((1000/median(diff(T{fcnt}.time_us_-T{fcnt}.time_us_(1)))) * 10) / 10; SetupInfo{fcnt}=dataA(fcnt).SetupInfo; @@ -138,128 +123,251 @@ epoch2_A(fcnt)=round(((tta{fcnt}(end)/us2sec)-LogNdDefault)*10) / 10; clear a b r p y dm ff - r = (SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'rollPID')),2)); - p = (SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'pitchPID')),2)); - y = (SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'yawPID')),2)); %%%%%%%%%% parse firmware version for per-file debug mode indices %%%%%%%%%% [fwType{fcnt}, fwMajor(fcnt), fwMinor(fcnt)] = PSparseBFversion(SetupInfo{fcnt}); debugIdx{fcnt} = PSdebugModeIndices(fwType{fcnt}, fwMajor(fcnt), fwMinor(fcnt)); + % Auto-switch firmware dropdown if detected type differs + detFwIdx = 0; + ft = fwType{fcnt}; + if strcmpi(ft,'Betaflight') || strcmpi(ft,'Cleanflight'), detFwIdx = 1; + elseif strcmpi(ft,'Emuflight'), detFwIdx = 2; + elseif strcmpi(ft,'INAV'), detFwIdx = 3; + elseif strcmpi(ft,'Rotorflight'), detFwIdx = 6; + elseif strcmpi(ft,'KISS'), detFwIdx = 7; + end + if detFwIdx > 0 && detFwIdx ~= get(guiHandles.Firmware, 'Value') + set(guiHandles.Firmware, 'Value', detFwIdx); + end + if detFwIdx > 0 && detFwIdx ~= loaded_firmware && fcnt > 1 + % stash current file (already imported above) + stash_T_ = T{fcnt}; stash_SI_ = SetupInfo{fcnt}; + stash_LR_ = A_lograte(fcnt); stash_TTA_ = tta{fcnt}; + stash_E1_ = epoch1_A(fcnt); stash_E2_ = epoch2_A(fcnt); + stash_FN_ = fnameMaster{fcnt}; + stash_FwT_ = fwType{fcnt}; stash_FwMaj_ = fwMajor(fcnt); + stash_FwMin_ = fwMinor(fcnt); stash_DbgIdx_ = debugIdx{fcnt}; + + clear T dataA tta A_lograte epoch1_A epoch2_A SetupInfo rollPIDF pitchPIDF yawPIDF debugmode debugIdx fwType fwMajor fwMinor gyro_debug_axis notchData rpmFilterData ampmat freq2d2 amp2d2 specMat delayDataReady FilterDelayDterm SPGyroDelay Debug01 Debug02 gyro_phase_shift_deg dterm_phase_shift_deg tuneCrtlpanel_init setupInfoWidgets_init; + fnameMaster = {}; + try, delete(checkpanel); clear checkpanel; catch, end + try delete(findobj(PSfig,'Tag','PSrpy')); catch, end + try delete(findobj(PSfig,'Tag','PSmotor')); catch, end + try delete(findobj(PSfig,'Tag','PScombo')); catch, end + ov = getappdata(PSfig, 'PSoverlay'); + if ~isempty(ov) + flds = fieldnames(ov); + for fi=1:numel(flds), try delete(ov.(flds{fi})); catch, end; end + setappdata(PSfig, 'PSoverlay', []); + end + figs=findobj('Type','figure'); for fi=1:numel(figs), if figs(fi)~=PSfig, try, close(figs(fi)); catch, end; end; end + clear PSspecfig PSspecfig2 PSspecfig3 PStunefig PSerrfig PSstatsfig PSdisp errCrtlpanel statsCrtlpanel spec2Crtlpanel specCrtlpanel freqTimeCrtlpanel tuneCrtlpanel fcntSR; + set(guiHandles.FileNum, 'String', ' '); + try, set(guiHandles.Epoch1_A_Input, 'String', ' '); set(guiHandles.Epoch2_A_Input, 'String', ' '); catch, end + try setappdata(PSfig, 'smoothCacheLV', struct()); catch, end + try setappdata(PSfig, 'rfMotorCount', []); catch, end + + % restore as file #1 and continue processing + fcnt = 1; Nfiles = 1; + T{1} = stash_T_; SetupInfo{1} = stash_SI_; + A_lograte(1) = stash_LR_; tta{1} = stash_TTA_; + epoch1_A(1) = stash_E1_; epoch2_A(1) = stash_E2_; + fnameMaster{1} = stash_FN_; + fwType{1} = stash_FwT_; fwMajor(1) = stash_FwMaj_; + fwMinor(1) = stash_FwMin_; debugIdx{1} = stash_DbgIdx_; + clear stash_T_ stash_SI_ stash_LR_ stash_TTA_ stash_E1_ stash_E2_ stash_FN_ stash_FwT_ stash_FwMaj_ stash_FwMin_ stash_DbgIdx_; + loaded_firmware = detFwIdx; + end + if detFwIdx > 0 + loaded_firmware = detFwIdx; + end + %%%%%%%%%% collect debug mode info %%%%%%%%%% try - debugmode(fcnt) = str2num(char(SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'debug_mode')),2))); + debugmode(fcnt) = str2double(char(SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'debug_mode')),2))); catch - % BF 2025.12+: GYRO_SCALED removed, use GYRO_FILTERED as default if debugIdx{fcnt}.GYRO_SCALED == -1 debugmode(fcnt) = debugIdx{fcnt}.GYRO_FILTERED; else - debugmode(fcnt) = 6;% default to gyro_scaled + debugmode(fcnt) = 6; end end - %%%%%%%%%% parse gyro_debug_axis (BF 2025.12+, for FFT_FREQ axis) %%%%%%%%%% try - gyro_debug_axis(fcnt) = str2num(char(SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'gyro_debug_axis')),2))); + gyro_debug_axis(fcnt) = str2double(char(SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'gyro_debug_axis')),2))); catch - gyro_debug_axis(fcnt) = 0; % default Roll + gyro_debug_axis(fcnt) = 0; end - dm = {}; - % Try d_max first (BF 2025.12+), fallback to d_min (older) - dm_idx = find(strcmp(SetupInfo{fcnt}(:,1), 'd_max')); - if isempty(dm_idx) - dm_idx = find(strcmp(SetupInfo{fcnt}(:,1), 'd_min')); - end - if ~isempty(dm_idx) && ~isempty(SetupInfo{fcnt}(dm_idx,2)) - dm = SetupInfo{fcnt}(dm_idx, 2); - else - dm = {' , , '}; - end - ff = {}; - if ~isempty(SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'feedforward_weight') | strcmp(SetupInfo{fcnt}(:,1), 'ff_weight')),2)) - ff = (SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'feedforward_weight') | strcmp(SetupInfo{fcnt}(:,1), 'ff_weight')),2)); - else - ff = {' , , '}; + % BF 4.5+: gyroUnfilt → gyroPrefilt; fallback to debug if GYRO_SCALED + for ax_ = 0:2 + uf_ = ['gyroUnfilt_' int2str(ax_) '_']; + pf_ = ['gyroPrefilt_' int2str(ax_) '_']; + if isfield(T{fcnt}, uf_) + T{fcnt}.(pf_) = T{fcnt}.(uf_); + elseif debugmode(fcnt) == debugIdx{fcnt}.GYRO_SCALED && debugIdx{fcnt}.GYRO_SCALED > 0 + df_ = ['debug_' int2str(ax_) '_']; + if isfield(T{fcnt}, df_), T{fcnt}.(pf_) = T{fcnt}.(df_); end + end end - a=strfind(char(dm),','); - b=strfind(char(ff),','); - rollPIDF{fcnt} = [char(r) ',' dm{1}(1:a(1)-1) ',' ff{1}(1:b(1)-1)]; - pitchPIDF{fcnt} = [char(p) ',' dm{1}(a(1)+1:a(2)-1) ',' ff{1}(b(1)+1:b(2)-1)]; - yawPIDF{fcnt} = [char(y) ',' dm{1}(a(2)+1:end) ',' ff{1}(b(2)+1:end)]; + try + r = (SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'rollPID')),2)); + p = (SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'pitchPID')),2)); + y = (SetupInfo{fcnt}(find(strcmp(SetupInfo{fcnt}(:,1), 'yawPID')),2)); + dm_idx = find(strcmp(SetupInfo{fcnt}(:,1), 'd_max')); + if isempty(dm_idx), dm_idx = find(strcmp(SetupInfo{fcnt}(:,1), 'd_min')); end + if ~isempty(dm_idx) && ~isempty(SetupInfo{fcnt}(dm_idx,2)) + dm = SetupInfo{fcnt}(dm_idx, 2); + else + dm = {' , , '}; + end + ff_idx = find(strcmp(SetupInfo{fcnt}(:,1), 'feedforward_weight') | strcmp(SetupInfo{fcnt}(:,1), 'ff_weight')); + if ~isempty(ff_idx) && ~isempty(SetupInfo{fcnt}(ff_idx,2)) + ff = SetupInfo{fcnt}(ff_idx, 2); + else + ff = {' , , '}; + end + a=strfind(char(dm),','); + b=strfind(char(ff),','); + rollPIDF{fcnt} = [char(r) ',' dm{1}(1:a(1)-1) ',' ff{1}(1:b(1)-1)]; + pitchPIDF{fcnt} = [char(p) ',' dm{1}(a(1)+1:a(2)-1) ',' ff{1}(b(1)+1:b(2)-1)]; + yawPIDF{fcnt} = [char(y) ',' dm{1}(a(2)+1:end) ',' ff{1}(b(2)+1:end)]; + catch + rollPIDF{fcnt} = '0,0,0,0,0'; + pitchPIDF{fcnt} = '0,0,0,0,0'; + yawPIDF{fcnt} = '0,0,0,0,0'; + end - if get(guiHandles.Firmware, 'Value') == 3 % INAV + isRF = strcmpi(fwType{fcnt}, 'Rotorflight'); + if strcmpi(fwType{fcnt}, 'INAV') T{fcnt}.setpoint_0_ = T{fcnt}.axisRate_0_; T{fcnt}.setpoint_1_ = T{fcnt}.axisRate_1_; T{fcnt}.setpoint_2_ = T{fcnt}.axisRate_2_; - T{fcnt}.setpoint_3_ = (T{fcnt}.rcData_3_ - 1000); + if isfield(T{fcnt}, 'rcData_3_') + T{fcnt}.setpoint_3_ = (T{fcnt}.rcData_3_ - 1000); + else + T{fcnt}.setpoint_3_ = zeros(length(T{fcnt}.loopIteration), 1); + end + end + if isRF % setpoint_3_ is collective, use rcCommand[4] as throttle + if isfield(T{fcnt}, 'rcCommand_4_') + T{fcnt}.setpoint_3_ = T{fcnt}.rcCommand_4_; + end + end + % KISS/FETTEC: synthesize setpoint from rcCommand if missing + if ~isfield(T{fcnt}, 'setpoint_0_') && isfield(T{fcnt}, 'rcCommand_0_') + for ax = 0:2 + T{fcnt}.(['setpoint_' int2str(ax) '_']) = T{fcnt}.(['rcCommand_' int2str(ax) '_']); + end + T{fcnt}.setpoint_3_ = (T{fcnt}.rcCommand_3_ - 1000) / 10; + elseif ~isfield(T{fcnt}, 'setpoint_0_') + for ax = 0:3 + T{fcnt}.(['setpoint_' int2str(ax) '_']) = zeros(length(T{fcnt}.loopIteration), 1); + end + end + + % gyroUnfilt -> gyroADC fallback (old BF logs lack gyroADC) + for ax = 0:2 + adc = sprintf('gyroADC_%d_', ax); + uf = sprintf('gyroUnfilt_%d_', ax); + if ~isfield(T{fcnt}, adc) && isfield(T{fcnt}, uf) + T{fcnt}.(adc) = T{fcnt}.(uf); + elseif ~isfield(T{fcnt}, adc) + T{fcnt}.(adc) = zeros(length(T{fcnt}.loopIteration), 1); + end end isArduPilot = strcmpi(sfext, '.bin'); + Nsamples = length(T{fcnt}.loopIteration); + isINAV = strcmpi(fwType{fcnt}, 'INAV'); for k = 0 : 3 if ~isArduPilot - try - eval(['T{fcnt}.debug_' int2str(k) '_(1);']) - catch - eval(['T{fcnt}.(''debug_' int2str(k) '_'')' '= zeros(length(T{fcnt}.loopIteration),1);']) ; + dbg_f = ['debug_' int2str(k) '_']; + if ~isfield(T{fcnt}, dbg_f) + T{fcnt}.(dbg_f) = zeros(Nsamples, 1); end - try - eval(['T{fcnt}.axisF_' int2str(k) '_(1);']) - catch - eval(['T{fcnt}.(''axisF_' int2str(k) '_'')' '= zeros(length(T{fcnt}.loopIteration),1);']); + axF_f = ['axisF_' int2str(k) '_']; + if ~isfield(T{fcnt}, axF_f) + T{fcnt}.(axF_f) = zeros(Nsamples, 1); end - if get(guiHandles.Firmware, 'Value') == 3 % INAV - try - eval(['T{fcnt}.motor_' int2str(k) '_ = ((T{fcnt}.motor_' int2str(k) '_ - 1000)) / 10;'])% scale motor sigs to % - catch, end - try - eval(['T{fcnt}.motor_' int2str(k+4) '_ = ((T{fcnt}.motor_' int2str(k+4) '_ - 1000)) / 10;'])% scale motor sigs 4-7 for x8 configuration - catch + mot_f = ['motor_' int2str(k) '_']; + mot8_f = ['motor_' int2str(k+4) '_']; + if isINAV + if isfield(T{fcnt}, mot_f) + T{fcnt}.(mot_f) = (T{fcnt}.(mot_f) - 1000) / 10; + end + if isfield(T{fcnt}, mot8_f) + T{fcnt}.(mot8_f) = (T{fcnt}.(mot8_f) - 1000) / 10; end else - try - eval(['T{fcnt}.motor_' int2str(k) '_ = ((T{fcnt}.motor_' int2str(k) '_) / 2000) * 100;'])% scale motor sigs to % - catch, end - try - eval(['T{fcnt}.motor_' int2str(k+4) '_ = ((T{fcnt}.motor_' int2str(k+4) '_) / 2000) * 100;'])% scale motor sigs 4-7 for x8 configuration - catch + if isfield(T{fcnt}, mot_f) + T{fcnt}.(mot_f) = T{fcnt}.(mot_f) / 2000 * 100; + end + if isfield(T{fcnt}, mot8_f) + T{fcnt}.(mot8_f) = T{fcnt}.(mot8_f) / 2000 * 100; end end end % ~isArduPilot + % Rotorflight: fill empty motor slots with servo data + if isRF && k == 3 + nMotReal = 0; + for mm = 0:3 + if isfield(T{fcnt}, ['motor_' int2str(mm) '_']), nMotReal = mm+1; end + end + si = 0; + for mm = nMotReal:3 + sf = ['servo_' int2str(si) '_']; + mf = ['motor_' int2str(mm) '_']; + if isfield(T{fcnt}, sf) + T{fcnt}.(mf) = (T{fcnt}.(sf) - 1000) / 10; + end + si = si + 1; + end + try setappdata(PSfig, 'rfMotorCount', nMotReal); catch, end + end if k < 3 - if k < 2 % compute prefiltered dterm and scale + ks = int2str(k); + if k < 2 % compute prefiltered dterm try - eval(['T{fcnt}.axisDpf_' int2str(k) '_ = -[0; diff(T{fcnt}.gyroADC_' int2str(k) '_)];']) - clear d1 d2 d3 sclr - eval(['d1 = smooth(T{fcnt}.axisDpf_' int2str(k) '_, 100);']) - eval(['d2 = smooth(T{fcnt}.axisD_' int2str(k) '_, 100);']) - d3 = (d2 ./ d1); + dpf_f = ['axisDpf_' ks '_']; + T{fcnt}.(dpf_f) = -[0; diff(T{fcnt}.(['gyroADC_' ks '_']))]; + d1 = smooth(T{fcnt}.(dpf_f), 100); + d2 = smooth(T{fcnt}.(['axisD_' ks '_']), 100); + d3 = d2 ./ d1; sclr = nanmedian(d3(~isinf(d3) & d3 > 0)); - eval(['T{fcnt}.axisDpf_' int2str(k) '_ = T{fcnt}.axisDpf_' int2str(k) '_ * sclr;']) + T{fcnt}.(dpf_f) = T{fcnt}.(dpf_f) * sclr; catch, end end - eval(['T{fcnt}.(''piderr_' int2str(k) '_'') = T{fcnt}.gyroADC_' int2str(k) '_ - T{fcnt}.setpoint_' int2str(k) '_;']) try - eval(['T{fcnt}.(''pidsum_' int2str(k) '_'') = T{fcnt}.axisP_' int2str(k) '_ + T{fcnt}.axisI_' int2str(k) '_ + T{fcnt}.axisD_' int2str(k) '_ + T{fcnt}.axisF_' int2str(k) '_;']) + T{fcnt}.(['piderr_' ks '_']) = T{fcnt}.(['gyroADC_' ks '_']) - T{fcnt}.(['setpoint_' ks '_']); + catch + T{fcnt}.(['piderr_' ks '_']) = zeros(Nsamples, 1); + end + try + T{fcnt}.(['pidsum_' ks '_']) = T{fcnt}.(['axisP_' ks '_']) + T{fcnt}.(['axisI_' ks '_']) + T{fcnt}.(['axisD_' ks '_']) + T{fcnt}.(['axisF_' ks '_']); catch - eval(['T{fcnt}.(''pidsum_' int2str(k) '_'') = T{fcnt}.axisP_' int2str(k) '_ + T{fcnt}.axisI_' int2str(k) '_ + T{fcnt}.axisF_' int2str(k) '_;']) + try + T{fcnt}.(['pidsum_' ks '_']) = T{fcnt}.(['axisP_' ks '_']) + T{fcnt}.(['axisI_' ks '_']) + T{fcnt}.(['axisF_' ks '_']); + catch + T{fcnt}.(['pidsum_' ks '_']) = zeros(Nsamples, 1); + end end end end end end % Clean up workdir - cd(prev_dir); try if ispc(), system(['rmdir /s /q "' workdir '"']); else system(['rm -rf ' workdir]); end; catch, end end try close(waitbarFid), catch, end + catch ME - try cd(prev_dir); catch, end try if ispc(), system(['rmdir /s /q "' workdir '"']); else system(['rm -rf ' workdir]); end; catch, end try close(waitbarFid); catch, end warning('PSload error: %s', ME.message); diff --git a/src/core/PSparseBFversion.m b/src/core/PSparseBFversion.m index 5a73ce2..393e1f4 100644 --- a/src/core/PSparseBFversion.m +++ b/src/core/PSparseBFversion.m @@ -1,57 +1,62 @@ -function [fwType, fwMajor, fwMinor] = PSparseBFversion(setupInfo) -%% PSparseBFversion - parse firmware type and version from SetupInfo cell array -% [fwType, fwMajor, fwMinor] = PSparseBFversion(setupInfo) -% setupInfo: Nx2 cell array (column 1 = param name, column 2 = value) -% Returns: fwType string ('Betaflight','INAV','Emuflight',...), fwMajor, fwMinor numbers - -fwType = 'Unknown'; -fwMajor = 0; -fwMinor = 0; - -try - % Find 'Firmware version' or 'Firmware revision' row - idx = find(strcmp(setupInfo(:,1), 'Firmware version')); - if isempty(idx) - idx = find(strcmp(setupInfo(:,1), 'Firmware revision')); - end - if isempty(idx) - return; - end - - verStr = strtrim(char(setupInfo(idx(1), 2))); - % Example: " Betaflight / STM32F405 4.5.3 Dec 14 2024 / 11:27:01" - % Example: " Betaflight / STM32F405 2025.12.2 Jan 5 2026 / 09:15:00" - % Example: " INAV / STM32F405 7.1.0 ..." - % Example: " Emuflight / STM32F405 0.4.1 ..." - % Example: "QuickSilver" (from PSquicJson2csv synthetic header) - - % Extract firmware type (first word before ' /' or end of string) - parts = strsplit(verStr, '/'); - fwType = strtrim(parts{1}); - - % Extract version: find pattern X.Y.Z or X.Y anywhere in string - tok = regexp(verStr, '(\d+)\.(\d+)\.(\d+)', 'tokens'); - if ~isempty(tok) - % Could have multiple matches (e.g. board name with numbers) - use last one - % Actually first match after '/' is the version - for t = 1:numel(tok) - maj = str2double(tok{t}{1}); - mnr = str2double(tok{t}{2}); - % Heuristic: version is the token with major >= 2 (BF 2025.x or 3.x/4.x) - % or after the board name - if maj >= 2 - fwMajor = maj; - fwMinor = mnr; - break; - end - end - % If no match with major >= 2, use first token - if fwMajor == 0 && ~isempty(tok) - fwMajor = str2double(tok{1}{1}); - fwMinor = str2double(tok{1}{2}); - end - end -catch -end - -end +function [fwType, fwMajor, fwMinor] = PSparseBFversion(setupInfo) +%% PSparseBFversion - parse firmware type and version from SetupInfo cell array +% [fwType, fwMajor, fwMinor] = PSparseBFversion(setupInfo) +% setupInfo: Nx2 cell array (column 1 = param name, column 2 = value) +% Returns: fwType string ('Betaflight','INAV','Emuflight',...), fwMajor, fwMinor numbers + +fwType = 'Unknown'; +fwMajor = 0; +fwMinor = 0; + +try + % Find 'Firmware version' or 'Firmware revision' row + idx = find(strcmp(setupInfo(:,1), 'Firmware version')); + if isempty(idx) + idx = find(strcmp(setupInfo(:,1), 'Firmware revision')); + end + if isempty(idx) + return; + end + + verStr = strtrim(char(setupInfo(idx(1), 2))); + + % Check for separate 'Firmware type' header (KISS, etc.) + ftIdx = find(strcmp(setupInfo(:,1), 'Firmware type')); + if ~isempty(ftIdx) + fwType = strtrim(char(setupInfo(ftIdx(1), 2))); + else + words = strsplit(strtrim(verStr)); + if ~isempty(words) + fwType = words{1}; + end + end + + % Extract version: find pattern X.Y.Z or X.Y anywhere in string + tok = regexp(verStr, '(\d+)\.(\d+)\.(\d+)', 'tokens'); + if isempty(tok) + tok = regexp(verStr, '(\d+)\.(\d+)', 'tokens'); + end + if ~isempty(tok) + % Could have multiple matches (e.g. board name with numbers) - use last one + % Actually first match after '/' is the version + for t = 1:numel(tok) + maj = str2double(tok{t}{1}); + mnr = str2double(tok{t}{2}); + % Heuristic: version is the token with major >= 2 (BF 2025.x or 3.x/4.x) + % or after the board name + if maj >= 2 + fwMajor = maj; + fwMinor = mnr; + break; + end + end + % If no match with major >= 2, use first token + if fwMajor == 0 && ~isempty(tok) + fwMajor = str2double(tok{1}{1}); + fwMinor = str2double(tok{1}{2}); + end + end +catch +end + +end diff --git a/src/core/PSprocess.m b/src/core/PSprocess.m index 5727d83..8d8c69e 100644 --- a/src/core/PSprocess.m +++ b/src/core/PSprocess.m @@ -1,122 +1,122 @@ -%% PSprocess - script that extracts subset of total data based on highlighted epoch in main fig - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -try - -if ~isempty(filenameA) || ~isempty(filenameB) - -downsampleMultiplier=5;% 5th of the resolution for faster plotting, display only - -set(PSfig, 'pointer', 'watch') - if ~isempty(filenameA) - if isempty(epoch1_A) || isempty(epoch2_A) - epoch1_A=round(tta(1)/us2sec)+2; - epoch2_A=round(tta(end)/us2sec)-2; - guiHandles.Epoch1_A_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch1_A)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch1_A_Input],... - 'callback','@textinput_call; epoch1_A=str2num(get(guiHandles.Epoch1_A_Input, ''String'')); PSprocess;PSplotLogViewer;'); - guiHandles.Epoch2_A_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_A)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_A_Input],... - 'callback','@textinput_call;epoch2_A=str2num(get(guiHandles.Epoch2_A_Input, ''String'')); PSprocess;PSplotLogViewer;'); - end - if (epoch2_A>round(tta(end)/us2sec)) - epoch2_A=round(tta(end)/us2sec); - guiHandles.Epoch2_A_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_A)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_A_Input],... - 'callback','@textinput_call;epoch2_A=str2num(get(guiHandles.Epoch2_A_Input, ''String'')); PSprocess;PSplotLogViewer;'); - end - x=[epoch1_A*us2sec epoch2_A*us2sec]; - x2=tta>tta(find(tta>x(1),1)) & ttax(2),1)); - Time_A=tta(x2,1)/us2sec; - Time_A=Time_A-Time_A(1); - DATtmpA.GyroFilt=DATmainA.GyroFilt(:,x2); - DATtmpA.debug=DATmainA.debug(:,x2); - DATtmpA.RCcommand=DATmainA.RCcommand(:,x2); - DATtmpA.Pterm=DATmainA.Pterm(:,x2); - DATtmpA.Iterm=DATmainA.Iterm(:,x2); - DATtmpA.DtermRaw=DATmainA.DtermRaw(:,x2); - DATtmpA.DtermFilt=DATmainA.DtermFilt(:,x2); - DATtmpA.Fterm=DATmainA.Fterm(:,x2); - DATtmpA.PIDsum=DATmainA.PIDsum(:,x2); - DATtmpA.RCRate=DATmainA.RCRate(:,x2); - DATtmpA.PIDerr=DATmainA.PIDerr(:,x2); - DATtmpA.Motor12=DATmainA.Motor(1:2,x2); - DATtmpA.Motor34=DATmainA.Motor(3:4,x2); - DATtmpA.debug12=DATmainA.debug(1:2,x2); - DATtmpA.debug34=DATmainA.debug(3:4,x2); - - dnsampleFactor=A_lograte*downsampleMultiplier;% 5 times less resolution for faster plotting, display only - DATdnsmplA.tta=downsample(((tta-tta(1))/us2sec), dnsampleFactor)'; - DATdnsmplA.GyroFilt=downsample(DATmainA.GyroFilt', dnsampleFactor)'; - DATdnsmplA.debug=downsample(DATmainA.debug', dnsampleFactor)'; - DATdnsmplA.RCcommand=downsample(DATmainA.RCcommand', dnsampleFactor)'; - DATdnsmplA.Pterm=downsample(DATmainA.Pterm', dnsampleFactor)'; - DATdnsmplA.Iterm=downsample(DATmainA.Iterm', dnsampleFactor)'; - DATdnsmplA.DtermRaw=downsample(DATmainA.DtermRaw', dnsampleFactor)'; - DATdnsmplA.DtermFilt=downsample(DATmainA.DtermFilt', dnsampleFactor)'; - DATdnsmplA.Fterm=downsample(DATmainA.Fterm', dnsampleFactor)'; - DATdnsmplA.RCRate=downsample(DATmainA.RCRate', dnsampleFactor)'; - DATdnsmplA.PIDsum=downsample(DATmainA.PIDsum', dnsampleFactor)'; - DATdnsmplA.PIDerr=downsample(DATmainA.PIDerr', dnsampleFactor)'; - DATdnsmplA.Motor=downsample(DATmainA.Motor', dnsampleFactor)'; - end - - if ~isempty(filenameB) - if isempty(epoch1_B) || isempty(epoch2_B) - epoch1_B=round(ttb(1)/us2sec)+2; - epoch2_B=round(ttb(end)/us2sec)-2; - guiHandles.Epoch1_B_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch1_B)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch1_B_Input],... - 'callback','@textinput_call; epoch1_B=str2num(get(guiHandles.Epoch1_B_Input, ''String''));PSprocess;PSplotLogViewer; '); - guiHandles.Epoch2_B_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_B)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_B_Input],... - 'callback','@textinput_call; epoch2_B=str2num(get(guiHandles.Epoch2_B_Input, ''String''));PSprocess;PSplotLogViewer; '); - end - if (epoch2_B>round(ttb(end)/us2sec)) - epoch2_B=round(ttb(end)/us2sec); - guiHandles.Epoch2_B_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_B)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_B_Input],... - 'callback','@textinput_call; epoch2_B=str2num(get(guiHandles.Epoch2_B_Input, ''String'')); PSprocess;PSplotLogViewer;'); - end - x=[epoch1_B*us2sec epoch2_B*us2sec]; - x2=ttb>ttb(find(ttb>x(1),1)) & ttbx(2),1)); - Time_B=ttb(x2,1)/us2sec; - Time_B=Time_B-Time_B(1); - DATtmpB.GyroFilt=DATmainB.GyroFilt(:,x2); - DATtmpB.debug=DATmainB.debug(:,x2); - DATtmpB.RCcommand=DATmainB.RCcommand(:,x2); - DATtmpB.Pterm=DATmainB.Pterm(:,x2); - DATtmpB.Iterm=DATmainB.Iterm(:,x2); - DATtmpB.DtermRaw=DATmainB.DtermRaw(:,x2); - DATtmpB.DtermFilt=DATmainB.DtermFilt(:,x2); - DATtmpB.Fterm=DATmainB.Fterm(:,x2); - DATtmpB.PIDsum=DATmainB.PIDsum(:,x2); - DATtmpB.RCRate=DATmainB.RCRate(:,x2); - DATtmpB.PIDerr=DATmainB.PIDerr(:,x2); - DATtmpB.Motor12=DATmainB.Motor(1:2,x2); - DATtmpB.Motor34=DATmainB.Motor(3:4,x2); - DATtmpB.debug12=DATmainB.debug(1:2,x2); - DATtmpB.debug34=DATmainB.debug(3:4,x2); - - - dnsampleFactor=B_lograte*downsampleMultiplier;% 5 times less resolution for faster plotting, display only - DATdnsmplB.ttb=downsample(((ttb-ttb(1))/us2sec), dnsampleFactor)'; - DATdnsmplB.GyroFilt=downsample(DATmainB.GyroFilt', dnsampleFactor)'; - DATdnsmplB.debug=downsample(DATmainB.debug', dnsampleFactor)'; - DATdnsmplB.RCcommand=downsample(DATmainB.RCcommand', dnsampleFactor)'; - DATdnsmplB.Pterm=downsample(DATmainB.Pterm', dnsampleFactor)'; - DATdnsmplB.Iterm=downsample(DATmainB.Iterm', dnsampleFactor)'; - DATdnsmplB.DtermRaw=downsample(DATmainB.DtermRaw', dnsampleFactor)'; - DATdnsmplB.DtermFilt=downsample(DATmainB.DtermFilt', dnsampleFactor)'; - DATdnsmplB.Fterm=downsample(DATmainB.Fterm', dnsampleFactor)'; - DATdnsmplB.RCRate=downsample(DATmainB.RCRate', dnsampleFactor)'; - DATdnsmplB.PIDsum=downsample(DATmainB.PIDsum', dnsampleFactor)'; - DATdnsmplB.PIDerr=downsample(DATmainB.PIDerr', dnsampleFactor)'; - DATdnsmplB.Motor=downsample(DATmainB.Motor', dnsampleFactor)'; - end -set(PSfig, 'pointer', 'arrow') -end - -catch ME - errmsg.PSprocess=PSerrorMessages('PSprocess', ME); -end +%% PSprocess - script that extracts subset of total data based on highlighted epoch in main fig + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +try + +if ~isempty(filenameA) || ~isempty(filenameB) + +downsampleMultiplier=5;% 5th of the resolution for faster plotting, display only + +set(PSfig, 'pointer', 'watch') + if ~isempty(filenameA) + if isempty(epoch1_A) || isempty(epoch2_A) + epoch1_A=round(tta(1)/us2sec)+2; + epoch2_A=round(tta(end)/us2sec)-2; + guiHandles.Epoch1_A_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch1_A)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch1_A_Input],... + 'callback','@textinput_call; epoch1_A=str2double(get(guiHandles.Epoch1_A_Input, ''String'')); PSprocess;PSplotLogViewer;'); + guiHandles.Epoch2_A_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_A)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_A_Input],... + 'callback','@textinput_call;epoch2_A=str2double(get(guiHandles.Epoch2_A_Input, ''String'')); PSprocess;PSplotLogViewer;'); + end + if (epoch2_A>round(tta(end)/us2sec)) + epoch2_A=round(tta(end)/us2sec); + guiHandles.Epoch2_A_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_A)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_A_Input],... + 'callback','@textinput_call;epoch2_A=str2double(get(guiHandles.Epoch2_A_Input, ''String'')); PSprocess;PSplotLogViewer;'); + end + x=[epoch1_A*us2sec epoch2_A*us2sec]; + x2=tta>tta(find(tta>x(1),1)) & ttax(2),1)); + Time_A=tta(x2,1)/us2sec; + Time_A=Time_A-Time_A(1); + DATtmpA.GyroFilt=DATmainA.GyroFilt(:,x2); + DATtmpA.debug=DATmainA.debug(:,x2); + DATtmpA.RCcommand=DATmainA.RCcommand(:,x2); + DATtmpA.Pterm=DATmainA.Pterm(:,x2); + DATtmpA.Iterm=DATmainA.Iterm(:,x2); + DATtmpA.DtermRaw=DATmainA.DtermRaw(:,x2); + DATtmpA.DtermFilt=DATmainA.DtermFilt(:,x2); + DATtmpA.Fterm=DATmainA.Fterm(:,x2); + DATtmpA.PIDsum=DATmainA.PIDsum(:,x2); + DATtmpA.RCRate=DATmainA.RCRate(:,x2); + DATtmpA.PIDerr=DATmainA.PIDerr(:,x2); + DATtmpA.Motor12=DATmainA.Motor(1:2,x2); + DATtmpA.Motor34=DATmainA.Motor(3:4,x2); + DATtmpA.debug12=DATmainA.debug(1:2,x2); + DATtmpA.debug34=DATmainA.debug(3:4,x2); + + dnsampleFactor=A_lograte*downsampleMultiplier;% 5 times less resolution for faster plotting, display only + DATdnsmplA.tta=downsample(((tta-tta(1))/us2sec), dnsampleFactor)'; + DATdnsmplA.GyroFilt=downsample(DATmainA.GyroFilt', dnsampleFactor)'; + DATdnsmplA.debug=downsample(DATmainA.debug', dnsampleFactor)'; + DATdnsmplA.RCcommand=downsample(DATmainA.RCcommand', dnsampleFactor)'; + DATdnsmplA.Pterm=downsample(DATmainA.Pterm', dnsampleFactor)'; + DATdnsmplA.Iterm=downsample(DATmainA.Iterm', dnsampleFactor)'; + DATdnsmplA.DtermRaw=downsample(DATmainA.DtermRaw', dnsampleFactor)'; + DATdnsmplA.DtermFilt=downsample(DATmainA.DtermFilt', dnsampleFactor)'; + DATdnsmplA.Fterm=downsample(DATmainA.Fterm', dnsampleFactor)'; + DATdnsmplA.RCRate=downsample(DATmainA.RCRate', dnsampleFactor)'; + DATdnsmplA.PIDsum=downsample(DATmainA.PIDsum', dnsampleFactor)'; + DATdnsmplA.PIDerr=downsample(DATmainA.PIDerr', dnsampleFactor)'; + DATdnsmplA.Motor=downsample(DATmainA.Motor', dnsampleFactor)'; + end + + if ~isempty(filenameB) + if isempty(epoch1_B) || isempty(epoch2_B) + epoch1_B=round(ttb(1)/us2sec)+2; + epoch2_B=round(ttb(end)/us2sec)-2; + guiHandles.Epoch1_B_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch1_B)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch1_B_Input],... + 'callback','@textinput_call; epoch1_B=str2double(get(guiHandles.Epoch1_B_Input, ''String''));PSprocess;PSplotLogViewer; '); + guiHandles.Epoch2_B_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_B)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_B_Input],... + 'callback','@textinput_call; epoch2_B=str2double(get(guiHandles.Epoch2_B_Input, ''String''));PSprocess;PSplotLogViewer; '); + end + if (epoch2_B>round(ttb(end)/us2sec)) + epoch2_B=round(ttb(end)/us2sec); + guiHandles.Epoch2_B_Input = uicontrol(PSfig,'style','edit','string',[int2str(epoch2_B)],'fontsize',fontsz,'units','normalized','Position',[posInfo.Epoch2_B_Input],... + 'callback','@textinput_call; epoch2_B=str2double(get(guiHandles.Epoch2_B_Input, ''String'')); PSprocess;PSplotLogViewer;'); + end + x=[epoch1_B*us2sec epoch2_B*us2sec]; + x2=ttb>ttb(find(ttb>x(1),1)) & ttbx(2),1)); + Time_B=ttb(x2,1)/us2sec; + Time_B=Time_B-Time_B(1); + DATtmpB.GyroFilt=DATmainB.GyroFilt(:,x2); + DATtmpB.debug=DATmainB.debug(:,x2); + DATtmpB.RCcommand=DATmainB.RCcommand(:,x2); + DATtmpB.Pterm=DATmainB.Pterm(:,x2); + DATtmpB.Iterm=DATmainB.Iterm(:,x2); + DATtmpB.DtermRaw=DATmainB.DtermRaw(:,x2); + DATtmpB.DtermFilt=DATmainB.DtermFilt(:,x2); + DATtmpB.Fterm=DATmainB.Fterm(:,x2); + DATtmpB.PIDsum=DATmainB.PIDsum(:,x2); + DATtmpB.RCRate=DATmainB.RCRate(:,x2); + DATtmpB.PIDerr=DATmainB.PIDerr(:,x2); + DATtmpB.Motor12=DATmainB.Motor(1:2,x2); + DATtmpB.Motor34=DATmainB.Motor(3:4,x2); + DATtmpB.debug12=DATmainB.debug(1:2,x2); + DATtmpB.debug34=DATmainB.debug(3:4,x2); + + + dnsampleFactor=B_lograte*downsampleMultiplier;% 5 times less resolution for faster plotting, display only + DATdnsmplB.ttb=downsample(((ttb-ttb(1))/us2sec), dnsampleFactor)'; + DATdnsmplB.GyroFilt=downsample(DATmainB.GyroFilt', dnsampleFactor)'; + DATdnsmplB.debug=downsample(DATmainB.debug', dnsampleFactor)'; + DATdnsmplB.RCcommand=downsample(DATmainB.RCcommand', dnsampleFactor)'; + DATdnsmplB.Pterm=downsample(DATmainB.Pterm', dnsampleFactor)'; + DATdnsmplB.Iterm=downsample(DATmainB.Iterm', dnsampleFactor)'; + DATdnsmplB.DtermRaw=downsample(DATmainB.DtermRaw', dnsampleFactor)'; + DATdnsmplB.DtermFilt=downsample(DATmainB.DtermFilt', dnsampleFactor)'; + DATdnsmplB.Fterm=downsample(DATmainB.Fterm', dnsampleFactor)'; + DATdnsmplB.RCRate=downsample(DATmainB.RCRate', dnsampleFactor)'; + DATdnsmplB.PIDsum=downsample(DATmainB.PIDsum', dnsampleFactor)'; + DATdnsmplB.PIDerr=downsample(DATmainB.PIDerr', dnsampleFactor)'; + DATdnsmplB.Motor=downsample(DATmainB.Motor', dnsampleFactor)'; + end +set(PSfig, 'pointer', 'arrow') +end + +catch ME + errmsg.PSprocess=PSerrorMessages('PSprocess', ME); +end diff --git a/src/core/PSquicJson2csv.m b/src/core/PSquicJson2csv.m index 3649445..80d91e7 100644 --- a/src/core/PSquicJson2csv.m +++ b/src/core/PSquicJson2csv.m @@ -1,161 +1,164 @@ -function [headerFile, csvFile] = PSquicJson2csv(jsonFile) -%% PSquicJson2csv - Convert QuickSilver JSON blackbox export to CSV -% -% Reads JSON exported by BossHobby Configurator ("QUIC download") and -% produces CSV + synthetic header compatible with PSimport/PSload pipeline. -% -% Unit conversions applied: -% gyro/setpoint RPY: rad/s -> deg/s (multiply by 180/pi) -% yaw: negated (QS convention opposite to BF) -% motor: 0-1 range -> 0-2000 (BF scale, PSload does /2000*100) -% PID terms: divided by 1000 (stored as int16*1000) -% throttle setpoint: 0-1 -> 0-1000 (BF scale) -% -% [headerFile, csvFile] = PSquicJson2csv(jsonFile) - - RAD2DEG = 180 / pi; - - % Read and parse JSON - jsonText = fileread(jsonFile); - data = jsondecode(jsonText); - - % Extract metadata - if isfield(data, 'looptime') - looptime_us = data.looptime; - else - looptime_us = 125; % default 8kHz - end - if isfield(data, 'blackbox_rate') - bb_rate = data.blackbox_rate; - else - bb_rate = 1; - end - - % Get field list - if iscell(data.fields) - fields = data.fields; - else - fields = cellstr(data.fields); - end - - % Get entries - entries = data.entries; - if ~iscell(entries) - % If jsondecode made it a matrix (unlikely for mixed types), convert - entries = num2cell(entries, 2); - end - nEntries = numel(entries); - - % Initialize output arrays - loopIter = zeros(nEntries, 1); - time_us = zeros(nEntries, 1); - gyro = zeros(nEntries, 3); % RPY filtered - gyro_raw = zeros(nEntries, 3); % RPY raw - setpt = zeros(nEntries, 4); % RPY + throttle - axisP = zeros(nEntries, 3); - axisI = zeros(nEntries, 3); - axisD = zeros(nEntries, 3); - motors = zeros(nEntries, 4); - rx_cmd = zeros(nEntries, 4); - dbg = zeros(nEntries, 4); - - % Parse entries - each entry is ordered by fields list - for i = 1:nEntries - entry = entries{i}; - if ~iscell(entry) - entry = num2cell(entry); - end - - col = 0; - for f = 1:numel(fields) - col = col + 1; - if col > numel(entry), break; end - val = entry{col}; - if iscell(val), val = cell2mat(val); end - - switch fields{f} - case 'loop' - loopIter(i) = val; - case 'time' - time_us(i) = val; - case 'pid_p_term' - axisP(i, 1:min(3,numel(val))) = val(1:min(3,numel(val))) / 1000; - case 'pid_i_term' - axisI(i, 1:min(3,numel(val))) = val(1:min(3,numel(val))) / 1000; - case 'pid_d_term' - axisD(i, 1:min(3,numel(val))) = val(1:min(3,numel(val))) / 1000; - case 'rx' - rx_cmd(i, 1:min(4,numel(val))) = val(1:min(4,numel(val))) / 1000; - case 'setpoint' - n = min(4, numel(val)); - sp = val(1:n) / 1000; - % RPY: rad/s -> deg/s, negate yaw - if n >= 1, sp(1) = sp(1) * RAD2DEG; end - if n >= 2, sp(2) = sp(2) * RAD2DEG; end - if n >= 3, sp(3) = -sp(3) * RAD2DEG; end % negate yaw - % Throttle: 0-1 -> 0-1000 - if n >= 4, sp(4) = sp(4) * 1000; end - setpt(i, 1:n) = sp; - case 'gyro_filter' - n = min(3, numel(val)); - g = val(1:n) / 1000 * RAD2DEG; - if n >= 3, g(3) = -g(3); end % negate yaw - gyro(i, 1:n) = g; - case 'gyro_raw' - n = min(3, numel(val)); - g = val(1:n) / 1000 * RAD2DEG; - if n >= 3, g(3) = -g(3); end % negate yaw - gyro_raw(i, 1:n) = g; - case 'motor' - n = min(4, numel(val)); - % 0-1 range * 1000 -> 0-2000 (BF scale) - motors(i, 1:n) = val(1:n) / 1000 * 2000; - case 'accel_raw' - % not used by PIDscope, skip - case 'accel_filter' - % not used by PIDscope, skip - case 'cpu_load' - % not used by PIDscope, skip - case 'debug' - n = min(4, numel(val)); - dbg(i, 1:n) = val(1:n); - end - end - end - - % If no gyro_filter but have gyro_raw, use raw as filtered - if all(gyro(:) == 0) && any(gyro_raw(:) ~= 0) - gyro = gyro_raw; - end - - % Build CSV output - [fdir, fname, ~] = fileparts(jsonFile); - csvFile = fullfile(fdir, [fname '.01.csv']); - - % Column names matching blackbox_decode output - header = 'loopIteration,time (us),axisP[0],axisP[1],axisP[2],axisI[0],axisI[1],axisI[2],axisD[0],axisD[1],axisD[2],gyroADC[0],gyroADC[1],gyroADC[2],rcCommand[0],rcCommand[1],rcCommand[2],rcCommand[3],setpoint[0],setpoint[1],setpoint[2],setpoint[3],motor[0],motor[1],motor[2],motor[3],debug[0],debug[1],debug[2],debug[3]'; - - % Build data matrix - M = [loopIter, time_us, axisP, axisI, axisD, gyro, rx_cmd, setpt, motors, dbg]; - - % Write CSV - fid = fopen(csvFile, 'w'); - fprintf(fid, '%s\n', header); - fclose(fid); - dlmwrite(csvFile, M, '-append', 'delimiter', ',', 'precision', '%.6g'); - - % Write synthetic header file (mimics BBL header for PSimport) - headerFile = fullfile(fdir, [fname '.quic_header.txt']); - fid = fopen(headerFile, 'w'); - fprintf(fid, 'H Firmware version:QuickSilver\n'); - fprintf(fid, 'H Firmware revision:QUICKSILVER\n'); - fprintf(fid, 'H looptime:%d\n', round(looptime_us)); - fprintf(fid, 'H rollPID:0, 0, 0\n'); - fprintf(fid, 'H pitchPID:0, 0, 0\n'); - fprintf(fid, 'H yawPID:0, 0, 0\n'); - fprintf(fid, 'H d_min:0, 0, 0\n'); - fprintf(fid, 'H feedforward_weight:0, 0, 0\n'); - fprintf(fid, 'H debug_mode:0\n'); - fclose(fid); - -end +function [headerFile, csvFile] = PSquicJson2csv(jsonFile, outdir) +%% PSquicJson2csv - Convert QuickSilver JSON blackbox export to CSV +% +% Reads JSON exported by BossHobby Configurator ("QUIC download") and +% produces CSV + synthetic header compatible with PSimport/PSload pipeline. +% +% Unit conversions applied: +% gyro/setpoint RPY: rad/s -> deg/s (multiply by 180/pi) +% yaw: negated (QS convention opposite to BF) +% motor: 0-1 range -> 0-2000 (BF scale, PSload does /2000*100) +% PID terms: divided by 1000 (stored as int16*1000) +% throttle setpoint: 0-1 -> 0-1000 (BF scale) +% +% [headerFile, csvFile] = PSquicJson2csv(jsonFile, outdir) + + RAD2DEG = 180 / pi; + + % Read and parse JSON + jsonText = fileread(jsonFile); + data = jsondecode(jsonText); + + % Extract metadata + if isfield(data, 'looptime') + looptime_us = data.looptime; + else + looptime_us = 125; % default 8kHz + end + if isfield(data, 'blackbox_rate') + bb_rate = data.blackbox_rate; + else + bb_rate = 1; + end + + % Get field list + if iscell(data.fields) + fields = data.fields; + else + fields = cellstr(data.fields); + end + + % Get entries + entries = data.entries; + if ~iscell(entries) + % If jsondecode made it a matrix (unlikely for mixed types), convert + entries = num2cell(entries, 2); + end + nEntries = numel(entries); + + + loopIter = zeros(nEntries, 1); + time_us = zeros(nEntries, 1); + gyro = zeros(nEntries, 3); % RPY filtered + gyro_raw = zeros(nEntries, 3); % RPY raw + setpt = zeros(nEntries, 4); % RPY + throttle + axisP = zeros(nEntries, 3); + axisI = zeros(nEntries, 3); + axisD = zeros(nEntries, 3); + motors = zeros(nEntries, 4); + rx_cmd = zeros(nEntries, 4); + dbg = zeros(nEntries, 4); + + % Parse entries - each entry is ordered by fields list + for i = 1:nEntries + entry = entries{i}; + if ~iscell(entry) + entry = num2cell(entry); + end + + col = 0; + for f = 1:numel(fields) + col = col + 1; + if col > numel(entry), break; end + val = entry{col}; + if iscell(val), val = cell2mat(val); end + + switch fields{f} + case 'loop' + loopIter(i) = val; + case 'time' + time_us(i) = val; + case 'pid_p_term' + axisP(i, 1:min(3,numel(val))) = val(1:min(3,numel(val))) / 1000; + case 'pid_i_term' + axisI(i, 1:min(3,numel(val))) = val(1:min(3,numel(val))) / 1000; + case 'pid_d_term' + axisD(i, 1:min(3,numel(val))) = val(1:min(3,numel(val))) / 1000; + case 'rx' + rx_cmd(i, 1:min(4,numel(val))) = val(1:min(4,numel(val))) / 1000; + case 'setpoint' + n = min(4, numel(val)); + sp = val(1:n) / 1000; + % RPY: rad/s -> deg/s, negate yaw + if n >= 1, sp(1) = sp(1) * RAD2DEG; end + if n >= 2, sp(2) = sp(2) * RAD2DEG; end + if n >= 3, sp(3) = -sp(3) * RAD2DEG; end % negate yaw + % Throttle: 0-1 -> 0-1000 + if n >= 4, sp(4) = sp(4) * 1000; end + setpt(i, 1:n) = sp; + case 'gyro_filter' + n = min(3, numel(val)); + g = val(1:n) / 1000 * RAD2DEG; + if n >= 3, g(3) = -g(3); end % negate yaw + gyro(i, 1:n) = g; + case 'gyro_raw' + n = min(3, numel(val)); + g = val(1:n) / 1000 * RAD2DEG; + if n >= 3, g(3) = -g(3); end % negate yaw + gyro_raw(i, 1:n) = g; + case 'motor' + n = min(4, numel(val)); + % 0-1 range * 1000 -> 0-2000 (BF scale) + motors(i, 1:n) = val(1:n) / 1000 * 2000; + case 'accel_raw' + % not used by PIDscope, skip + case 'accel_filter' + % not used by PIDscope, skip + case 'cpu_load' + % not used by PIDscope, skip + case 'debug' + n = min(4, numel(val)); + dbg(i, 1:n) = val(1:n); + end + end + end + + % If no gyro_filter but have gyro_raw, use raw as filtered + if all(gyro(:) == 0) && any(gyro_raw(:) ~= 0) + gyro = gyro_raw; + end + + % Build CSV output + [~, fname, ~] = fileparts(jsonFile); + if nargin < 2 || isempty(outdir) + [outdir, ~, ~] = fileparts(jsonFile); + end + csvFile = fullfile(outdir, [fname '.01.csv']); + + % Column names matching blackbox_decode output + header = 'loopIteration,time (us),axisP[0],axisP[1],axisP[2],axisI[0],axisI[1],axisI[2],axisD[0],axisD[1],axisD[2],gyroADC[0],gyroADC[1],gyroADC[2],rcCommand[0],rcCommand[1],rcCommand[2],rcCommand[3],setpoint[0],setpoint[1],setpoint[2],setpoint[3],motor[0],motor[1],motor[2],motor[3],debug[0],debug[1],debug[2],debug[3]'; + + % Build data matrix + M = [loopIter, time_us, axisP, axisI, axisD, gyro, rx_cmd, setpt, motors, dbg]; + + % Write CSV + fid = fopen(csvFile, 'w'); + fprintf(fid, '%s\n', header); + fclose(fid); + dlmwrite(csvFile, M, '-append', 'delimiter', ',', 'precision', '%.6g'); + + % Write synthetic header file (mimics BBL header for PSimport) + headerFile = fullfile(outdir, [fname '.quic_header.txt']); + fid = fopen(headerFile, 'w'); + fprintf(fid, 'H Firmware version:QuickSilver\n'); + fprintf(fid, 'H Firmware revision:QUICKSILVER\n'); + fprintf(fid, 'H looptime:%d\n', round(looptime_us)); + fprintf(fid, 'H rollPID:0, 0, 0\n'); + fprintf(fid, 'H pitchPID:0, 0, 0\n'); + fprintf(fid, 'H yawPID:0, 0, 0\n'); + fprintf(fid, 'H d_min:0, 0, 0\n'); + fprintf(fid, 'H feedforward_weight:0, 0, 0\n'); + fprintf(fid, 'H debug_mode:0\n'); + fclose(fid); + +end diff --git a/src/core/PSstepcalc.m b/src/core/PSstepcalc.m index ae81545..e51ff02 100644 --- a/src/core/PSstepcalc.m +++ b/src/core/PSstepcalc.m @@ -1,87 +1,82 @@ -function [stepresponse, t] = PSstepcalc(SP, GY, lograte, Ycorrection, smoothFactor) -%% [stepresponse, t] = PSstepcalc(SP, GY, lograte, Ycorrection) -% this function deconvolves the step response function using -% SP = set point (input), GY = filtered gyro (output) -% returns matrix/stack of etimated stepresponse functions, time [t] in ms -% -% -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- -smoothVals = [1 20 40 60]; -GY = smooth(GY, smoothVals(smoothFactor),'lowess'); - -minInput = 20; -segment_length = (lograte*2000); % 2 sec segments -wnd = (lograte*1000) * .5; % 500ms step response function, length will depend on lograte -StepRespDuration_ms = 500; % max dur of step resp in ms for plotting -t = 0 : 1/lograte : StepRespDuration_ms;% time in ms - -fileDurSec = length(SP) / (lograte*1000); -subsampleFactor = 1; -switch subsampleFactor - case fileDurSec <= 20 - subsampleFactor = 10; - case fileDurSec > 20 & fileDurSec <= 60 - subsampleFactor = 7; - case fileDurSec > 60 - subsampleFactor = 3; -end - -segment_vector = 1 : round(segment_length/subsampleFactor) : length(SP); -NSegs = max(find((segment_vector+segment_length) < segment_vector(end))); -if NSegs > 0 - SPseg = []; GYseg = []; - j = 0; - for i = 1 : NSegs - if max(abs(SP(segment_vector(i):segment_vector(i)+segment_length))) >= minInput - j=j+1; - SPseg(j,:) = SP(segment_vector(i):segment_vector(i)+segment_length); - GYseg(j,:) = GY(segment_vector(i):segment_vector(i)+segment_length); - end - end - - padLength = 100;% 2^nextpow2(length(SPseg(i,:))); - clear resp resp2 G H Hcon imp impf a b - j=0; - if ~isempty(SPseg) - for i = 1 : size(SPseg,1) - a = GYseg(i,:).*hann(length(GYseg(i,:)))'; - b = SPseg(i,:).*hann(length(SPseg(i,:)))'; - a = fft([zeros(1,padLength) a zeros(1,padLength)]); - b = fft([zeros(1,padLength) b zeros(1,padLength)]); - G = a / length(a); - H = b / length(b); - Hcon = conj(H); - - imp = real(ifft((G .* Hcon) ./ (H .* Hcon + 0.0001 )))'; % impulse response function - resptmp(i,:) = cumsum(imp);% integrate impulse resp function - - clear a steadyStateWindow steadyStateResp yoffset - steadyStateWindow = find(t > 200 & t < StepRespDuration_ms); - steadyStateResp = resptmp(i, steadyStateWindow); - if Ycorrection - if nanmean(steadyStateResp) < 1 || nanmean(steadyStateResp) > 1 - yoffset = 1 - nanmean(steadyStateResp); - resptmp(i,:) = resptmp(i,:) * (yoffset+1); - end - steadyStateResp = resptmp(i, steadyStateWindow); - else - end - - if min(steadyStateResp) > 0.5 && max(steadyStateResp) < 3 % Quality control - j=j+1; - stepresponse(j,:)=resptmp(i,1:1+wnd); - - end - end - else - end -else -end - - - +function [stepresponse, t] = PSstepcalc(SP, GY, lograte, Ycorrection, smoothFactor, subsampleFactor, minRate, maxRate) +%% [stepresponse, t] = PSstepcalc(SP, GY, lograte, Ycorrection, smoothFactor, subsampleFactor, minRate, maxRate) +% this function deconvolves the step response function using +% SP = set point (input), GY = filtered gyro (output) +% returns matrix/stack of etimated stepresponse functions, time [t] in ms +% +% +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- +smoothVals = [1 20 40 60]; +if smoothFactor > 1 + GY = smooth(GY, smoothVals(smoothFactor),'lowess'); +end + +if nargin < 6 || isempty(subsampleFactor), subsampleFactor = 5; end +if nargin < 7 || isempty(minRate), minRate = 40; end +if nargin < 8 || isempty(maxRate), maxRate = 500; end + +segment_length = (lograte*2000); % 2 sec segments +wnd = (lograte*1000) * .5; % 500ms step response function, length will depend on lograte +StepRespDuration_ms = 500; % max dur of step resp in ms for plotting +t = 0 : 1/lograte : StepRespDuration_ms;% time in ms + +segment_vector = 1 : round(segment_length/subsampleFactor) : length(SP); +tmp = find((segment_vector+segment_length) < segment_vector(end)); +if isempty(tmp), NSegs = 0; else NSegs = max(tmp); end +if NSegs > 0 + SPseg = zeros(NSegs, segment_length+1); GYseg = zeros(NSegs, segment_length+1); + j = 0; + for i = 1 : NSegs + peakRate = max(abs(SP(segment_vector(i):segment_vector(i)+segment_length))); + if peakRate >= minRate && peakRate <= maxRate + j=j+1; + SPseg(j,:) = SP(segment_vector(i):segment_vector(i)+segment_length); + GYseg(j,:) = GY(segment_vector(i):segment_vector(i)+segment_length); + end + end + + nValid = j; + if nValid > 0 + SPseg = SPseg(1:nValid,:); + GYseg = GYseg(1:nValid,:); + + padLength = 100; + segW = size(SPseg, 2); + w = hann(segW)'; + + % vectorized FFT: all segments at once + A = [zeros(nValid, padLength), GYseg .* w, zeros(nValid, padLength)]; + B = [zeros(nValid, padLength), SPseg .* w, zeros(nValid, padLength)]; + fftLen = size(A, 2); + A = fft(A, [], 2) / fftLen; + B = fft(B, [], 2) / fftLen; + Bcon = conj(B); + resptmp = cumsum(real(ifft((A .* Bcon) ./ (B .* Bcon + 0.0001), [], 2)), 2); + + steadyStateWindow = find(t > 200 & t < StepRespDuration_ms); + j = 0; + for i = 1:nValid + steadyStateResp = resptmp(i, steadyStateWindow); + if Ycorrection + ssm = nanmean(steadyStateResp); + if ssm ~= 1 + resptmp(i,:) = resptmp(i,:) * (2 - ssm); + end + steadyStateResp = resptmp(i, steadyStateWindow); + end + if min(steadyStateResp) > 0.5 && max(steadyStateResp) < 3 + j = j + 1; + stepresponse(j,:) = resptmp(i, 1:1+wnd); + end + end + end +else +end + + + diff --git a/src/core/PSthrSpec.m b/src/core/PSthrSpec.m index ebee5b2..0aa0596 100644 --- a/src/core/PSthrSpec.m +++ b/src/core/PSthrSpec.m @@ -1,87 +1,90 @@ -function [freq ampMat] = PSthrSpec(X, Y, F, psd) -%% [freq ampMat] = PSthrSpec(X, Y, F, numspectrograms) -% computes fft as function of throttle(or motor output, optional) and generates throttle/motor x freq -% matrix. X is throttle/motor output data in percent, Y is flight data (gyro, PIDerror, etc), -% RClim sets the limit on the RC data (RPY set-point) used to compute FFT, -% F is sample frequency in Hz of the input flight data. -% The function returns a throttle/motor output x freq matrix/spectrogram [ampMat] of input data X and Y. % - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - - - Tr = 100;% throttle range - multiplier = 0.3; % results in 300ms segments (must be same in datatip for accurate reading) - wnd = 1; % throttle window size -> throttle +/- wnd - segment_length = F * 1000 * multiplier; % 300ms segments - - fileDurSec = length(X) / (F*1000); - if fileDurSec <= 20 - subsampleFactor = 5; - elseif fileDurSec <= 60 - subsampleFactor = 3; - else - subsampleFactor = 1; - end - - if subsampleFactor < 1, subsampleFactor = 1; end - - segment_vector = 1 : segment_length / subsampleFactor : length(Y); - for i = 1 : length(segment_vector) - subsampleFactor - 1 - Tm(i) = nanmean(X(segment_vector(i) : segment_vector(i) + segment_length)); - Yseg(i,:) = Y(segment_vector(i) : segment_vector(i) + segment_length-1); - end - - ampMat = zeros(Tr,(segment_length / 2)); - freq = zeros(Tr,(segment_length / 2)); - - Tm(find(Tm < 0)) = 0; - [Thr_sort Thr_sortInd] = sort(Tm'); - Yseg_sort = Yseg(Thr_sortInd,:);% sorted Y according to X (throttle or motor output) - - for i = 1 : Tr - clear tmp - if ~isempty(find(Thr_sort > i - wnd & Thr_sort <= i + wnd)) - ind = find(Thr_sort > i - wnd & Thr_sort <= i + wnd); - for j = 1 : length(ind) - try - clear Ytmp Ytmp2 Y N Np Fs - Ytmp = Yseg_sort(ind(j),:)'; - N = length(Ytmp); - Fs = (F * 1000) * (1 : (N / 2)) / N; - Ytmp2 = Ytmp .* hann(N);% hann taper on non padded signal - - if psd - Y = fft(Ytmp2); - psdx = abs(Y).^2 / (F * 1000 * N); % (1/(F*N)) * abs(Y).^2 is exactly same as abs(Y).^2 / (F*N), to - psdx(2 : end - 1) = 2 * psdx(2 : end-1); - psdx = psdx(1 : N / 2); - % scale to dB - Y = 10 * log10(psdx) + 40; - else - Y = abs(fft(Ytmp2,N)) / N; - Y = Y(1 : (N / 2)); - end - - tmp(j,:) = Y; - warning off - - catch - end - end - a = nanmean(tmp,1); - ampMat(i,:) = a; - freq(i,:) = Fs; - end - end -% clear tmp -% tmp = ampMat; -% clear ampMat -% if F>2, ampMat=tmp(:,1:floor(size(tmp,2)*(1/F)*2)); end -% if F<=2, ampMat=tmp; end -end - +function [freq ampMat] = PSthrSpec(X, Y, F, psd) +%% [freq ampMat] = PSthrSpec(X, Y, F, numspectrograms) +% computes fft as function of throttle(or motor output, optional) and generates throttle/motor x freq +% matrix. X is throttle/motor output data in percent, Y is flight data (gyro, PIDerror, etc), +% RClim sets the limit on the RC data (RPY set-point) used to compute FFT, +% F is sample frequency in Hz of the input flight data. +% The function returns a throttle/motor output x freq matrix/spectrogram [ampMat] of input data X and Y. % + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + + + Tr = 100;% throttle range + multiplier = 0.3; % results in 300ms segments (must be same in datatip for accurate reading) + wnd = 1; % throttle window size -> throttle +/- wnd + segment_length = F * 1000 * multiplier; % 300ms segments + + fileDurSec = length(X) / (F*1000); + if fileDurSec <= 20 + subsampleFactor = 5; + elseif fileDurSec <= 60 + subsampleFactor = 3; + else + subsampleFactor = 1; + end + + if subsampleFactor < 1, subsampleFactor = 1; end + + segment_vector = 1 : segment_length / subsampleFactor : length(Y); + Tm = []; Yseg = []; + for i = 1 : length(segment_vector) - subsampleFactor - 1 + if segment_vector(i) + segment_length > length(Y), break; end + Tm(i) = nanmean(X(segment_vector(i) : segment_vector(i) + segment_length)); + Yseg(i,:) = Y(segment_vector(i) : segment_vector(i) + segment_length-1); + end + + ampMat = zeros(Tr,(segment_length / 2)); + freq = zeros(Tr,(segment_length / 2)); + + if isempty(Tm), return; end + Tm(find(Tm < 0)) = 0; + [Thr_sort Thr_sortInd] = sort(Tm'); + Yseg_sort = Yseg(Thr_sortInd,:);% sorted Y according to X (throttle or motor output) + + for i = 1 : Tr + clear tmp + ind = find(Thr_sort > i - wnd & Thr_sort <= i + wnd); + if ~isempty(ind) + for j = 1 : length(ind) + try + clear Ytmp Ytmp2 Y N Np Fs + Ytmp = Yseg_sort(ind(j),:)'; + N = length(Ytmp); + Fs = (F * 1000) * (1 : (N / 2)) / N; + Ytmp2 = Ytmp .* hann(N);% hann taper on non padded signal + + if psd + Y = fft(Ytmp2); + psdx = abs(Y).^2 / (F * 1000 * N); % (1/(F*N)) * abs(Y).^2 is exactly same as abs(Y).^2 / (F*N), to + psdx(2 : end - 1) = 2 * psdx(2 : end-1); + psdx = psdx(1 : N / 2); + % scale to dB + Y = 10 * log10(psdx) + 40; + else + Y = abs(fft(Ytmp2,N)) / N; + Y = Y(1 : (N / 2)); + end + + tmp(j,:) = Y; + warning off + + catch + end + end + a = nanmean(tmp,1); + ampMat(i,:) = a; + freq(i,:) = Fs; + end + end +% clear tmp +% tmp = ampMat; +% clear ampMat +% if F>2, ampMat=tmp(:,1:floor(size(tmp,2)*(1/F)*2)); end +% if F<=2, ampMat=tmp; end +end + diff --git a/src/core/PStimeFreqCalc.m b/src/core/PStimeFreqCalc.m index c3c97c3..abd2899 100644 --- a/src/core/PStimeFreqCalc.m +++ b/src/core/PStimeFreqCalc.m @@ -1,44 +1,44 @@ -function [Tm freq specMat] = PStimeFreqCalc(Y, F, smoothFactor, subsampleFactor) -%% [Tm, freq, specMat] = PStimeFreqCalc(Y, F, smoothFactor) -% computes fft as function of time and generates time x freq matrix, -% specMat, a time vector, Tm, and freq vector, Fs. -% Y is flight data (gyro, PIDerror, etc) -% F is sample frequency in kHz of the input flight data. - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - - multiplier = 0.3; % results in 300ms segments (must be same in datatip for accurate reading) - segment_length = F * 1000 * multiplier; % 300ms segments - halfSegment = round(segment_length/2); - fileDurSec = length(Y) / (F*1000); - - stepsz = round(segment_length/subsampleFactor); - - smpls = (1 : stepsz : size(Y,1) - segment_length); - Tm = smpls / (F*1000); - - Yseg=[]; - j = 0; - for i = smpls - j = j + 1; - if i < segment_length - Yseg(j,:) = Y(i : i+(segment_length)-1); - else - Yseg(j,:) = Y(i-(halfSegment) : i+(halfSegment)-1); - end - end - - specMat=[];freq = []; - for i = 1 : size(Yseg,1) - [freq specMat(i,:)] = PSSpec2d(Yseg(i,:), F, 1); - specMat(i,:) = smooth(specMat(i,:), smoothFactor, 'lowess'); - end - specMat = flipud(specMat'); - -end - +function [Tm freq specMat] = PStimeFreqCalc(Y, F, smoothFactor, subsampleFactor) +%% [Tm, freq, specMat] = PStimeFreqCalc(Y, F, smoothFactor) +% computes fft as function of time and generates time x freq matrix, +% specMat, a time vector, Tm, and freq vector, Fs. +% Y is flight data (gyro, PIDerror, etc) +% F is sample frequency in kHz of the input flight data. + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + + multiplier = 0.3; % results in 300ms segments (must be same in datatip for accurate reading) + segment_length = F * 1000 * multiplier; % 300ms segments + halfSegment = round(segment_length/2); + fileDurSec = length(Y) / (F*1000); + + stepsz = round(segment_length/subsampleFactor); + + smpls = (1 : stepsz : size(Y,1) - segment_length); + Tm = smpls / (F*1000); + + Yseg=[]; + j = 0; + for i = smpls + j = j + 1; + if i < segment_length + Yseg(j,:) = Y(i : i+(segment_length)-1); + else + Yseg(j,:) = Y(i-(halfSegment) : i+(halfSegment)-1); + end + end + + specMat=[];freq = []; + for i = 1 : size(Yseg,1) + [freq specMat(i,:)] = PSSpec2d(Yseg(i,:), F, 1); + specMat(i,:) = smooth(specMat(i,:), smoothFactor, 'lowess'); + end + specMat = flipud(specMat'); + +end + diff --git a/src/core/PStpaDetect.m b/src/core/PStpaDetect.m new file mode 100644 index 0000000..b7c52d2 --- /dev/null +++ b/src/core/PStpaDetect.m @@ -0,0 +1,53 @@ +function [tpaFlag, onsetPct, ratioDB] = PStpaDetect(ampMat, freqRow, psd) +%% Detect throttle-correlated noise escalation in 30-80 Hz band +% ampMat - 100xM throttle-binned spectrum (from PSthrSpec) +% freqRow - 1xM frequency vector (Hz) +% psd - true if ampMat values are in dB + + tpaFlag = false; onsetPct = 0; ratioDB = 0; + + if isempty(ampMat) || size(ampMat,2) < 2, return; end + if isempty(freqRow), return; end + + % diagnostic band: 30-80 Hz (D-term 60-80, P-term 30-50) + fIdx = find(freqRow >= 30 & freqRow <= 80); + if isempty(fIdx), return; end + + % throttle regions + loThr = 10:30; % hover + hiThr = 50:90; % cruise to punch-out + loThr = loThr(loThr <= size(ampMat,1)); + hiThr = hiThr(hiThr <= size(ampMat,1)); + if isempty(loThr) || isempty(hiThr), return; end + + bandLo = ampMat(loThr, fIdx); + bandHi = ampMat(hiThr, fIdx); + + Elo = nanmean(bandLo(:)); + Ehi = nanmean(bandHi(:)); + + if psd + ratioDB = Ehi - Elo; + else + if Elo <= 0, return; end + ratioDB = 20 * log10(Ehi / Elo); + end + + tpaFlag = ratioDB > 6; + + if ~tpaFlag, return; end + + % onset: per-bin energy, find first bin exceeding baseline + if psd + thresh = Elo + 3; + else + thresh = Elo * 1.5; + end + for t = 1:size(ampMat,1) + Et = nanmean(ampMat(t, fIdx)); + if Et > thresh + onsetPct = t; + break; + end + end +end diff --git a/src/plot/PSdispSetupInfo.m b/src/plot/PSdispSetupInfo.m index 1aa8da5..6976864 100644 --- a/src/plot/PSdispSetupInfo.m +++ b/src/plot/PSdispSetupInfo.m @@ -1,61 +1,139 @@ -%% PSdispSetupInfo - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if exist('fnameMaster','var') && ~isempty(fnameMaster) - if Nfiles < 2 - str=repmat({':'}, size(dataA(get(guiHandlesInfo.FileNumDispA, 'Value')).SetupInfo,1), 1); % Octave compatible (was: strings) - str2=strcat(dataA(get(guiHandlesInfo.FileNumDispA, 'Value')).SetupInfo(:,1), str); - setupA=strcat(str2, dataA(get(guiHandlesInfo.FileNumDispA, 'Value')).SetupInfo(:,2)); % Octave compatible (was: string()) - else - str=repmat({':'}, size(dataA(get(guiHandlesInfo.FileNumDispA, 'Value')).SetupInfo,1), 1); - str2=strcat(dataA(get(guiHandlesInfo.FileNumDispA, 'Value')).SetupInfo(:,1), str); - setupA=strcat(str2, dataA(get(guiHandlesInfo.FileNumDispA, 'Value')).SetupInfo(:,2)); - - str=repmat({':'}, size(dataA(get(guiHandlesInfo.FileNumDispB, 'Value')).SetupInfo,1), 1); - str2=strcat(dataA(get(guiHandlesInfo.FileNumDispB, 'Value')).SetupInfo(:,1), str); - setupB=strcat(str2, dataA(get(guiHandlesInfo.FileNumDispB, 'Value')).SetupInfo(:,2)); - end - - BGCol = []; - try - for i = 1 : size(setupA,1) - if strcmp(setupA{i}, setupB{i}) - BGCol(i,:) = [1 1 1]; - else - BGCol(i,:) = [1 .7 .7]; - end - end - catch - BGCol=[1 1 1]; - end - u=[]; - u = (sum(BGCol,2)/3) < 1; - - if get(guiHandlesInfo.checkboxDIFF, 'Value') == 1 - t = uitable('ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupA(u)))]); - set(t,'units','normalized','Position',[.02 .05 .45 .9],'FontSize',fontsz, 'ColumnName', [fnameMaster{get(guiHandlesInfo.FileNumDispA, 'Value')}]) - set(t,'BackgroundColor', [1 .7 .7]) - if Nfiles > 1 - t = uitable('ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupB(u)))]); - set(t,'units','normalized','Position',[.52 .05 .45 .9],'FontSize',fontsz, 'ColumnName', fnameMaster{get(guiHandlesInfo.FileNumDispB, 'Value')}) - set(t,'BackgroundColor', [1 .7 .7]) - end - else - t = uitable('ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupA))]); - set(t,'units','normalized','Position',[.02 .05 .45 .9],'FontSize',fontsz, 'ColumnName', [fnameMaster{get(guiHandlesInfo.FileNumDispA, 'Value')}]) - set(t,'BackgroundColor', [BGCol]) - if Nfiles > 1 - t = uitable('ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupB))]); - set(t,'units','normalized','Position',[.52 .05 .45 .9],'FontSize',fontsz, 'ColumnName', fnameMaster{get(guiHandlesInfo.FileNumDispB, 'Value')}) - set(t,'BackgroundColor', [BGCol]) - end - end -end - - \ No newline at end of file +%% PSdispSetupInfo + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + + fA_ = get(guiHandlesInfo.FileNumDispA, 'Value'); + siA = dataA(fA_).SetupInfo; + keysA = strtrim(siA(:,1)); + valsA = strtrim(siA(:,2)); + nA = numel(keysA); + str = repmat({':'}, nA, 1); + setupA = strcat(keysA, str, valsA); + + th = PStheme(); + diffCol = th.diffBg; + BGCol = repmat(th.panelBg, nA, 1); + u = false(nA, 1); + + if Nfiles >= 2 && isfield(guiHandlesInfo, 'FileNumDispB') && ishandle(guiHandlesInfo.FileNumDispB) + fB_ = get(guiHandlesInfo.FileNumDispB, 'Value'); + siB = dataA(fB_).SetupInfo; + keysB = strtrim(siB(:,1)); + valsB = strtrim(siB(:,2)); + nB = numel(keysB); + strB = repmat({':'}, nB, 1); + setupB = strcat(keysB, strB, valsB); + + % renamed parameter aliases (BF version changes) + aliases = { ... + 'gyro_lowpass_type', 'gyro_lpf1_type'; ... + 'gyro_lowpass_hz', 'gyro_lpf1_static_hz'; ... + 'gyro_lowpass2_type', 'gyro_lpf2_type'; ... + 'gyro_lowpass2_hz', 'gyro_lpf2_static_hz'; ... + 'dterm_lowpass_type', 'dterm_lpf1_type'; ... + 'dterm_lowpass_hz', 'dterm_lpf1_static_hz'; ... + 'dterm_lowpass2_type', 'dterm_lpf2_type'; ... + 'dterm_lowpass2_hz', 'dterm_lpf2_static_hz'; ... + 'd_min', 'd_max'; ... + 'feedforward_weight', 'ff_weight'; ... + 'dshot_idle_value', 'motor_idle'; ... + 'gyro_to_use', 'gyro_enabled_bitmask'; ... + }; + + % key-based matching: for each row in A, find matching key in B + for i = 1:nA + kA = keysA{i}; + idxB = find(strcmp(keysB, kA)); + if isempty(idxB) + % try alias lookup + ai = find(strcmp(aliases(:,1), kA)); + if ~isempty(ai) + idxB = find(strcmp(keysB, aliases{ai(1),2})); + else + ai = find(strcmp(aliases(:,2), kA)); + if ~isempty(ai) + idxB = find(strcmp(keysB, aliases{ai(1),1})); + end + end + end + if isempty(idxB) + % param only in A + BGCol(i,:) = diffCol; + u(i) = true; + elseif ~strcmp(valsA{i}, valsB{idxB(1)}) + % same param, different value + BGCol(i,:) = diffCol; + u(i) = true; + end + end + + % mark B rows not in A + BGColB = repmat(th.panelBg, nB, 1); + uB = false(nB, 1); + for i = 1:nB + kB = keysB{i}; + idxA = find(strcmp(keysA, kB)); + if isempty(idxA) + ai = find(strcmp(aliases(:,1), kB)); + if ~isempty(ai) + idxA = find(strcmp(keysA, aliases{ai(1),2})); + else + ai = find(strcmp(aliases(:,2), kB)); + if ~isempty(ai) + idxA = find(strcmp(keysA, aliases{ai(1),1})); + end + end + end + if isempty(idxA) + BGColB(i,:) = diffCol; + uB(i) = true; + elseif ~strcmp(valsB{i}, valsA{idxA(1)}) + BGColB(i,:) = diffCol; + uB(i) = true; + end + end + end + + delete(findobj(PSdisp, 'Type', 'uitable')); + + diffFg = [1.0 .55 .55]; + tbH = 0.88; + if get(guiHandlesInfo.checkboxDIFF, 'Value') == 1 + nDiff = sum(u); + diffBG = repmat(diffCol, max(nDiff,1), 1); + st = uitable(PSdisp,'ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupA(u)))]); + set(st,'units','normalized','Position',[.02 .02 .45 tbH],'FontSize',fontsz, 'ColumnName', [fnameMaster{fA_}]); + try set(st,'BackgroundColor', diffBG); catch, end + try set(st,'ForegroundColor', diffFg); catch, end + try set(st,'RowStriping', 'off'); catch, end + if Nfiles > 1 && exist('setupB','var') + nDiffB = sum(uB); + diffBGB = repmat(diffCol, max(nDiffB,1), 1); + st = uitable(PSdisp,'ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupB(uB)))]); + set(st,'units','normalized','Position',[.52 .02 .45 tbH],'FontSize',fontsz, 'ColumnName', fnameMaster{fB_}); + try set(st,'BackgroundColor', diffBGB); catch, end + try set(st,'ForegroundColor', diffFg); catch, end + try set(st,'RowStriping', 'off'); catch, end + end + else + st = uitable(PSdisp,'ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupA))]); + set(st,'units','normalized','Position',[.02 .02 .45 tbH],'FontSize',fontsz, 'ColumnName', [fnameMaster{fA_}]); + try set(st,'BackgroundColor', BGCol); catch, end + try set(st,'ForegroundColor', th.textPrimary); catch, end + try set(st,'RowStriping', 'off'); catch, end + if Nfiles > 1 && exist('setupB','var') + st = uitable(PSdisp,'ColumnWidth',{columnWidth},'ColumnFormat',{'char'},'Data',[cellstr(char(setupB))]); + set(st,'units','normalized','Position',[.52 .02 .45 tbH],'FontSize',fontsz, 'ColumnName', fnameMaster{fB_}); + try set(st,'BackgroundColor', BGColB); catch, end + try set(st,'ForegroundColor', th.textPrimary); catch, end + try set(st,'RowStriping', 'off'); catch, end + end + end +end diff --git a/src/plot/PSdynSpecPlayer.m b/src/plot/PSdynSpecPlayer.m index 3c866ae..6458a73 100644 --- a/src/plot/PSdynSpecPlayer.m +++ b/src/plot/PSdynSpecPlayer.m @@ -30,13 +30,19 @@ function PSdynSpecPlayer(specMatCell, Tm, F, axisLabels, signalName) end yPad = (yHi - yLo) * 0.05; +thm = PStheme(); +fontsz = thm.fontsz; screensz = get(0, 'ScreenSize'); figW = round(.6 * screensz(3)); figH = round(.85 * screensz(4)); -fig = figure('Name', ['Spectrum Player - ' signalName], 'NumberTitle', 'off', ... - 'Color', [.15 .15 .15], 'Position', [round(.2*screensz(3)) round(.07*screensz(4)) figW figH]); +figName = ['Spectrum Player - ' signalName]; +fig = findobj('Type', 'figure', 'Name', figName); +if ~isempty(fig), close(fig); end +fig = figure('Name', figName, 'NumberTitle', 'off', ... + 'Color', thm.figBg, 'Position', round([0 0 screensz(3) screensz(4)])); +try set(fig, 'WindowState', 'maximized'); catch, end -axColors = {[0 .85 .85], [.85 .85 0], [.85 .3 .85]}; +axColors = {thm.axisRoll, thm.axisPitch, thm.axisYaw}; % layout: nValid spectrum rows + 1 spectrogram row at bottom + transport bar specH = 0.58 / nValid; @@ -52,19 +58,16 @@ function PSdynSpecPlayer(specMatCell, Tm, F, axisLabels, signalName) ax = axes('Parent', fig, 'Units', 'normalized', ... 'Position', [.07 yPos .86 specH - specGap]); specLines{vi} = plot(ax, F, specData{k}(:, 1), 'Color', axColors{k}, 'LineWidth', 1.2); - set(ax, 'XLim', [F(1) F(end)], 'YLim', [yLo-yPad yHi+yPad], ... - 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 13, 'FontWeight', 'bold'); - set(get(ax, 'YLabel'), 'String', 'dB', 'Color', [.8 .8 .8]); + set(ax, 'XLim', [F(1) F(end)], 'YLim', [yLo-yPad yHi+yPad]); + PSstyleAxes(ax, thm); + set(get(ax, 'YLabel'), 'String', 'dB'); if vi == nValid - set(get(ax, 'XLabel'), 'String', 'Frequency (Hz)', 'Color', [.8 .8 .8]); + set(get(ax, 'XLabel'), 'String', 'Frequency (Hz)'); else set(ax, 'XTickLabel', []); end titleHandles{vi} = title(ax, axisLabels{k}); set(titleHandles{vi}, 'Color', axColors{k}); - grid(ax, 'on'); - set(ax, 'GridColor', [.3 .3 .3]); axSpec{vi} = ax; end @@ -72,8 +75,7 @@ function PSdynSpecPlayer(specMatCell, Tm, F, axisLabels, signalName) sgIdx = chIdx(1); axSg = axes('Parent', fig, 'Units', 'normalized', 'Position', [.07 .10 .86 .16]); imagesc(axSg, specMatCell{sgIdx}); -set(axSg, 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 11, 'FontWeight', 'bold'); +PSstyleAxes(axSg, thm); nYt = 4; yTk = linspace(1, nFreq, nYt+1); @@ -84,7 +86,7 @@ function PSdynSpecPlayer(specMatCell, Tm, F, axisLabels, signalName) xTk = linspace(1, nFrames, nXt+1); xLb = arrayfun(@(x) sprintf('%.1f', interp1(1:nFrames, Tm, x)), xTk, 'UniformOutput', false); set(axSg, 'XTick', xTk, 'XTickLabel', xLb); -set(get(axSg, 'XLabel'), 'String', 'Time (s)', 'Color', [.8 .8 .8]); +set(get(axSg, 'XLabel'), 'String', 'Time (s)'); try colormap(axSg, hot); catch, end hold(axSg, 'on'); @@ -95,38 +97,38 @@ function PSdynSpecPlayer(specMatCell, Tm, F, axisLabels, signalName) timeLbl = uicontrol(fig, 'Style', 'text', ... 'String', sprintf('%s t = %.2fs', signalName, Tm(1)), ... 'Units', 'normalized', 'Position', [.07 .27 .40 .025], ... - 'FontSize', 12, 'FontWeight', 'bold', ... - 'BackgroundColor', [.15 .15 .15], 'ForegroundColor', [.9 .9 .9], ... + 'FontSize', fontsz, 'FontWeight', 'bold', ... + 'BackgroundColor', thm.figBg, 'ForegroundColor', thm.textPrimary, ... 'HorizontalAlignment', 'left'); % transport controls btnW = .07; btnH = .04; btnY = .02; uicontrol(fig, 'Style', 'pushbutton', 'String', 'Play', ... 'Units', 'normalized', 'Position', [.07 btnY btnW btnH], ... - 'FontSize', 12, 'FontWeight', 'bold', 'ForegroundColor', [0 .6 0], ... + 'FontSize', fontsz, 'FontWeight', 'bold', 'ForegroundColor', thm.btnPlay, ... 'Callback', @playCallback); uicontrol(fig, 'Style', 'pushbutton', 'String', 'Pause', ... 'Units', 'normalized', 'Position', [.07+btnW+.01 btnY btnW btnH], ... - 'FontSize', 12, 'FontWeight', 'bold', 'ForegroundColor', [.8 .2 0], ... + 'FontSize', fontsz, 'FontWeight', 'bold', 'ForegroundColor', thm.btnPause, ... 'Callback', @pauseCallback); uicontrol(fig, 'Style', 'pushbutton', 'String', 'Stop', ... 'Units', 'normalized', 'Position', [.07+2*(btnW+.01) btnY btnW btnH], ... - 'FontSize', 12, 'FontWeight', 'bold', 'ForegroundColor', [.7 0 0], ... + 'FontSize', fontsz, 'FontWeight', 'bold', 'ForegroundColor', thm.btnStop, ... 'Callback', @stopCallback); uicontrol(fig, 'Style', 'text', 'String', 'Speed:', ... 'Units', 'normalized', 'Position', [.30 btnY+.005 .045 .030], ... - 'FontSize', 11, 'BackgroundColor', [.15 .15 .15], 'ForegroundColor', [.8 .8 .8]); + 'FontSize', fontsz, 'BackgroundColor', thm.figBg, 'ForegroundColor', thm.textPrimary); speedMenu = uicontrol(fig, 'Style', 'popupmenu', ... 'String', {'0.25x', '0.5x', '1x', '2x', '4x'}, 'Value', 3, ... - 'Units', 'normalized', 'Position', [.35 btnY .06 btnH], 'FontSize', 11); + 'Units', 'normalized', 'Position', [.35 btnY .06 btnH], 'FontSize', fontsz); frameLbl = uicontrol(fig, 'Style', 'text', ... 'String', sprintf('1 / %d', nFrames), ... 'Units', 'normalized', 'Position', [.42 btnY+.005 .08 .030], ... - 'FontSize', 10, 'BackgroundColor', [.15 .15 .15], 'ForegroundColor', [.6 .6 .6], ... + 'FontSize', fontsz, 'BackgroundColor', thm.figBg, 'ForegroundColor', thm.textSecondary, ... 'HorizontalAlignment', 'center'); timeSlider = uicontrol(fig, 'Style', 'slider', 'Min', 1, 'Max', nFrames, 'Value', 1, ... diff --git a/src/plot/PSfilterSim.m b/src/plot/PSfilterSim.m index 85179d7..cce7abe 100644 --- a/src/plot/PSfilterSim.m +++ b/src/plot/PSfilterSim.m @@ -1,174 +1,1012 @@ -function PSfilterSim(gyroRaw, Fs, setupInfo) -%% PSfilterSim - simulate BF gyro filter chain on raw gyro data -% gyroRaw - struct with fields r/p/y (column vectors, deg/s) -% Fs - sample rate (Hz) -% setupInfo - cell array {param, value} from header +function PSfilterSim(~, Fs, setupInfo) +%% PSfilterSim - BF filter chain visualization (theoretical response) +% Layout: 2 cols (Lowpass | Notch) x 4 rows (Magnitude, Delay, Phase, Step) +thm = PStheme(); +fontsz = thm.fontsz; screensz = get(0, 'ScreenSize'); +fig = findobj('Type', 'figure', 'Name', 'Filter Simulation'); +if ~isempty(fig), close(fig); end fig = figure('Name', 'Filter Simulation', 'NumberTitle', 'off', ... - 'Color', [.15 .15 .15], ... - 'Position', round([.1*screensz(3) .08*screensz(4) .75*screensz(3) .8*screensz(4)])); + 'Color', thm.figBg, ... + 'Position', round([0 0 screensz(3) screensz(4)])); +try set(fig, 'WindowState', 'maximized'); catch, end -fp = parseFilterParams(setupInfo); +fp = PSparseFilterParams(setupInfo); -axData = {gyroRaw.r, gyroRaw.p, gyroRaw.y}; -axNames = {'Roll', 'Pitch', 'Yaw'}; +% Use gyro loop rate from headers if available (filters run at gyro rate, not logging rate) +if fp.gyro_rate_hz > 0, Fs = fp.gyro_rate_hz; end -axSpec = axes('Parent', fig, 'Units', 'normalized', 'Position', [.06 .54 .62 .40]); -axTime = axes('Parent', fig, 'Units', 'normalized', 'Position', [.06 .10 .62 .36]); +% Layout: 2 cols x 4 rows + gradient bars +plotL = 0.05; +plotR = 0.75; +colGap = 0.055; +colW = (plotR - plotL - colGap) / 2; +colL = [plotL, plotL + colW + colGap]; +titleH = 0.04; +barH = 0.018; +topMargin = 0.01; +botMargin = 0.06; +rowGap = 0.012; +topY = 1 - topMargin - titleH; +barY = topY - barH; +plotTop = barY - rowGap*0.5; +rowH = (plotTop - botMargin - 3*rowGap) / 4; +rowB1 = plotTop - rowH; +rowB2 = rowB1 - rowH - rowGap; +rowB3 = rowB2 - rowH - rowGap; +rowB4 = rowB3 - rowH - rowGap; -cpL = .72; cpW = .27; -uipanel('Parent', fig, 'Title', 'Filter Settings', 'FontSize', 9, 'FontWeight', 'bold', ... - 'BackgroundColor', [.25 .25 .25], 'ForegroundColor', [.9 .9 .9], ... - 'FontSize', 12, 'Position', [cpL .02 cpW .96]); +axLmag = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(1) rowB1 colW rowH]); +axLdelay = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(1) rowB2 colW rowH]); +axLphase = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(1) rowB3 colW rowH]); +axLstep = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(1) rowB4 colW rowH]); -row = .92; rh = .032; gap = .005; -bgc = [.25 .25 .25]; fgc = [.9 .9 .9]; -cb = @(~,~) doUpdate(); +axNmag = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(2) rowB1 colW rowH]); +axNdelay = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(2) rowB2 colW rowH]); +axNphase = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(2) rowB3 colW rowH]); +axNstep = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(2) rowB4 colW rowH]); -% axis selector -mkLabel(fig, 'Axis:', cpL, row, rh, bgc, fgc); -h.axis = uicontrol(fig, 'Style', 'popupmenu', 'String', axNames, 'Value', 1, ... - 'Units', 'normalized', 'Position', [cpL+.06 row .08 rh], 'FontSize', 11, 'Callback', cb); -row = row - rh - gap*3; +% Style all plot axes once (dark theme) +for ax_ = {axLmag, axLdelay, axLphase, axNmag, axNdelay, axNphase} + PSstyleAxes(ax_{1}, thm); + set(ax_{1}, 'XTickLabel', {}); % freq-domain rows: hide X ticks (shared axis) +end +for ax_ = {axLstep, axNstep} + PSstyleAxes(ax_{1}, thm); % row4: keep X tick labels +end + +% Pre-create pooled line objects: 16 per axes, reuse via setLine/hideLines +POOL = 16; +axAll8 = [axLmag axLdelay axLphase axLstep axNmag axNdelay axNphase axNstep]; +lnPool = cell(8, POOL); +for ai = 1:8 + hold(axAll8(ai), 'on'); + for li = 1:POOL + lnPool{ai, li} = line(axAll8(ai), NaN, NaN, 'Visible', 'off', 'HitTest', 'off'); + end +end +% text annotation pools: 8 per axes +TPOOL = 8; +txPool = cell(8, TPOOL); +for ai = 1:8 + for ti = 1:TPOOL + txPool{ai, ti} = text(NaN, NaN, '', 'Parent', axAll8(ai), 'Visible', 'off', ... + 'FontSize', fontsz-1, 'HitTest', 'off'); + end +end +% re-enable grid after hold+line creation (hold can reset grid in Octave) +for ai = 1:8, grid(axAll8(ai), 'on'); end +% axes indices for easy access +AX_LMAG=1; AX_LDLY=2; AX_LPH=3; AX_LSTP=4; +AX_NMAG=5; AX_NDLY=6; AX_NPH=7; AX_NSTP=8; + +% Gradient frequency bars +axBarL = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(1) barY colW barH], 'Tag', 'gradbar'); +axBarN = axes('Parent', fig, 'Units', 'normalized', 'Position', [colL(2) barY colW barH], 'Tag', 'gradbar'); + +titleY = topY; +hTitleL = uicontrol(fig, 'Style', 'text', 'String', 'LOWPASS FILTERS', ... + 'Units', 'normalized', 'Position', [colL(1) titleY colW titleH], ... + 'FontSize', fontsz+1, 'FontWeight', 'bold', ... + 'BackgroundColor', thm.figBg, 'ForegroundColor', thm.textAccent); +hTitleN = uicontrol(fig, 'Style', 'text', 'String', 'NOTCH FILTERS', ... + 'Units', 'normalized', 'Position', [colL(2) titleY colW titleH], ... + 'FontSize', fontsz+1, 'FontWeight', 'bold', ... + 'BackgroundColor', thm.figBg, 'ForegroundColor', thm.secNotch); + +% Control panel +cpL = .76; cpW = .23; +uipanel('Parent', fig, 'Title', 'Filter Controls', 'FontWeight', 'bold', ... + 'BackgroundColor', thm.panelBg, 'ForegroundColor', thm.panelFg, ... + 'HighlightColor', thm.panelBorder, ... + 'FontSize', fontsz, 'Position', [cpL .02 cpW .96]); + +row = .93; rh = .026; gap = .004; +bgc = thm.panelBg; fgc = thm.panelFg; +cb = @(~,~) autoUpdate(); +ibc = thm.inputBg; ifc = thm.inputFg; +lc = thm.textSecondary; -% Gyro LPF1 -mkSection(fig, '--- Gyro LPF1 ---', cpL, row, cpW, rh, bgc, [.5 .9 1]); +% grid: label col = cpL+.01, half-width col = cpL+.115 +x0 = cpL + .01; cW = cpW - .02; halfW = cW / 2; + +% Looprate + #Motors +loopRates = [1000 1100 3200 6400 6664 8000 9000 32000]; +loopLabels = {'1k','1.1k','3.2k','6.4k','6.664k','8k','9k','32k'}; +[~, lrIdx] = min(abs(loopRates - Fs)); +uicontrol(fig, 'Style', 'text', 'String', 'Rate:', ... + 'Units', 'normalized', 'Position', [x0 row .035 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.looprate = uicontrol(fig, 'Style', 'popupmenu', 'String', loopLabels, 'Value', lrIdx, ... + 'Units', 'normalized', 'Position', [x0+.04 row halfW-.04 rh], 'FontSize', fontsz, ... + 'Callback', @(~,~) loopRateChanged()); +uicontrol(fig, 'Style', 'text', 'String', 'Mot:', ... + 'Units', 'normalized', 'Position', [x0+halfW row .035 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.rpm_nmot = uicontrol(fig, 'Style', 'popupmenu', ... + 'String', {'1','2','3','4','5','6','7','8'}, 'Value', 4, ... + 'Units', 'normalized', 'Position', [x0+halfW+.04 row halfW-.04 rh], 'FontSize', fontsz, 'Callback', cb); +row = row - rh - gap*2; + +mkSection(fig, 'Gyro LPF1', cpL, row, cpW, rh, bgc, thm.textAccent, fontsz-1); +row = row - rh - gap; +[h.glpf1_type, h.glpf1_hz, row] = mkTypeHz(fig, x0, row, rh, gap, cW, ibc, ifc, lc, fp.gyro_lpf1_type, fp.gyro_lpf1_hz, cb, fontsz); + +mkSection(fig, 'Gyro LPF2', cpL, row, cpW, rh, bgc, thm.textAccent, fontsz-1); +row = row - rh - gap; +[h.glpf2_type, h.glpf2_hz, row] = mkTypeHz(fig, x0, row, rh, gap, cW, ibc, ifc, lc, fp.gyro_lpf2_type, fp.gyro_lpf2_hz, cb, fontsz); + +mkSection(fig, 'Gyro Notch 1', cpL, row, cpW, rh, bgc, thm.secNotch, fontsz-1); row = row - rh - gap; -[h.glpf1_type, row] = mkType(fig, cpL, row, rh, gap, bgc, fgc, fp.gyro_lpf1_type, cb); -[h.glpf1_hz, h.glpf1_lbl, row] = mkSlider(fig, 'Hz:', cpL, row, rh, gap, bgc, fgc, 0, 1000, fp.gyro_lpf1_hz, cb); -row = row - gap*2; +[h.gn1_hz, h.gn1_cut, row] = mkNotchPair(fig, x0, row, rh, gap, cW, bgc, ibc, ifc, lc, fp.gyro_notch1_hz, fp.gyro_notch1_cut, cb, fontsz); -% Gyro LPF2 -mkSection(fig, '--- Gyro LPF2 ---', cpL, row, cpW, rh, bgc, [.5 .9 1]); +mkSection(fig, 'Gyro Notch 2', cpL, row, cpW, rh, bgc, thm.secNotch, fontsz-1); row = row - rh - gap; -[h.glpf2_type, row] = mkType(fig, cpL, row, rh, gap, bgc, fgc, fp.gyro_lpf2_type, cb); -[h.glpf2_hz, h.glpf2_lbl, row] = mkSlider(fig, 'Hz:', cpL, row, rh, gap, bgc, fgc, 0, 1000, fp.gyro_lpf2_hz, cb); -row = row - gap*2; +[h.gn2_hz, h.gn2_cut, row] = mkNotchPair(fig, x0, row, rh, gap, cW, bgc, ibc, ifc, lc, fp.gyro_notch2_hz, fp.gyro_notch2_cut, cb, fontsz); -% Gyro Notch 1 -mkSection(fig, '--- Gyro Notch 1 ---', cpL, row, cpW, rh, bgc, [1 .8 .4]); +mkSection(fig, 'D-term LPF1', cpL, row, cpW, rh, bgc, thm.secDtermLPF, fontsz-1); row = row - rh - gap; -[h.gn1_hz, h.gn1_hz_lbl, row] = mkSlider(fig, 'Center:', cpL, row, rh, gap, bgc, fgc, 0, 1000, fp.gyro_notch1_hz, cb); -[h.gn1_cut, h.gn1_cut_lbl, row] = mkSlider(fig, 'Cutoff:', cpL, row, rh, gap, bgc, fgc, 0, 800, fp.gyro_notch1_cut, cb); -row = row - gap*2; +[h.dlpf1_type, h.dlpf1_hz, row] = mkTypeHz(fig, x0, row, rh, gap, cW, ibc, ifc, lc, fp.dterm_lpf1_type, fp.dterm_lpf1_hz, cb, fontsz); -% Gyro Notch 2 -mkSection(fig, '--- Gyro Notch 2 ---', cpL, row, cpW, rh, bgc, [1 .8 .4]); +mkSection(fig, 'D-term LPF2', cpL, row, cpW, rh, bgc, thm.secDtermLPF, fontsz-1); row = row - rh - gap; -[h.gn2_hz, h.gn2_hz_lbl, row] = mkSlider(fig, 'Center:', cpL, row, rh, gap, bgc, fgc, 0, 1000, fp.gyro_notch2_hz, cb); -[h.gn2_cut, h.gn2_cut_lbl, row] = mkSlider(fig, 'Cutoff:', cpL, row, rh, gap, bgc, fgc, 0, 800, fp.gyro_notch2_cut, cb); -row = row - gap*2; +[h.dlpf2_type, h.dlpf2_hz, row] = mkTypeHz(fig, x0, row, rh, gap, cW, ibc, ifc, lc, fp.dterm_lpf2_type, fp.dterm_lpf2_hz, cb, fontsz); -% D-term LPF1 -mkSection(fig, '--- D-term LPF1 ---', cpL, row, cpW, rh, bgc, [.5 1 .5]); +mkSection(fig, 'D-term Notch', cpL, row, cpW, rh, bgc, thm.secDtermNotch, fontsz-1); row = row - rh - gap; -[h.dlpf1_type, row] = mkType(fig, cpL, row, rh, gap, bgc, fgc, fp.dterm_lpf1_type, cb); -[h.dlpf1_hz, h.dlpf1_lbl, row] = mkSlider(fig, 'Hz:', cpL, row, rh, gap, bgc, fgc, 0, 500, fp.dterm_lpf1_hz, cb); -row = row - gap*2; +[h.dn_hz, h.dn_cut, row] = mkNotchPair(fig, x0, row, rh, gap, cW, bgc, ibc, ifc, lc, fp.dterm_notch_hz, fp.dterm_notch_cut, cb, fontsz); -% D-term LPF2 -mkSection(fig, '--- D-term LPF2 ---', cpL, row, cpW, rh, bgc, [.5 1 .5]); +% RPM: Hz + #Harm + Q on one line +mkSection(fig, 'RPM Filter Sim', cpL, row, cpW, rh, bgc, thm.btnDash1, fontsz-1); +row = row - rh - gap; +thirdW = cW / 3; +uicontrol(fig, 'Style', 'text', 'String', 'Hz:', ... + 'Units', 'normalized', 'Position', [x0 row .025 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.rpm_base = uicontrol(fig, 'Style', 'edit', 'String', '200', ... + 'Units', 'normalized', 'Position', [x0+.03 row thirdW-.035 rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, 'HorizontalAlignment', 'center', 'Callback', cb); +uicontrol(fig, 'Style', 'text', 'String', '#H:', ... + 'Units', 'normalized', 'Position', [x0+thirdW row .025 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.rpm_nharm = uicontrol(fig, 'Style', 'popupmenu', ... + 'String', {'0','1','2','3','4','5','6','7'}, 'Value', 4, ... + 'Units', 'normalized', 'Position', [x0+thirdW+.03 row thirdW-.035 rh], 'FontSize', fontsz, 'Callback', cb); +uicontrol(fig, 'Style', 'text', 'String', 'Q:', ... + 'Units', 'normalized', 'Position', [x0+thirdW*2 row .025 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.rpm_q = uicontrol(fig, 'Style', 'edit', 'String', '500', ... + 'Units', 'normalized', 'Position', [x0+thirdW*2+.03 row thirdW-.035 rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, 'HorizontalAlignment', 'center', 'Callback', cb); +row = row - rh - gap; + +% Test signal: Lo + Hi + Dur on one line +mkSection(fig, 'Test Signal', cpL, row, cpW, rh, bgc, thm.btnDash5, fontsz-1); +row = row - rh - gap; +uicontrol(fig, 'Style', 'text', 'String', 'Lo:', ... + 'Units', 'normalized', 'Position', [x0 row .025 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.sig_start = uicontrol(fig, 'Style', 'edit', 'String', '0', ... + 'Units', 'normalized', 'Position', [x0+.03 row thirdW-.035 rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, 'HorizontalAlignment', 'center', 'Callback', cb); +uicontrol(fig, 'Style', 'text', 'String', 'Hi:', ... + 'Units', 'normalized', 'Position', [x0+thirdW row .025 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.sig_end = uicontrol(fig, 'Style', 'edit', 'String', '1000', ... + 'Units', 'normalized', 'Position', [x0+thirdW+.03 row thirdW-.035 rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, 'HorizontalAlignment', 'center', 'Callback', cb); +uicontrol(fig, 'Style', 'text', 'String', 'Dur:', ... + 'Units', 'normalized', 'Position', [x0+thirdW*2 row .025 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.sig_dur = uicontrol(fig, 'Style', 'edit', 'String', '1', ... + 'Units', 'normalized', 'Position', [x0+thirdW*2+.03 row thirdW-.035 rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, 'HorizontalAlignment', 'center', 'Callback', cb); row = row - rh - gap; -[h.dlpf2_type, row] = mkType(fig, cpL, row, rh, gap, bgc, fgc, fp.dterm_lpf2_type, cb); -[h.dlpf2_hz, h.dlpf2_lbl, row] = mkSlider(fig, 'Hz:', cpL, row, rh, gap, bgc, fgc, 0, 500, fp.dterm_lpf2_hz, cb); -row = row - gap*2; -% D-term Notch -mkSection(fig, '--- D-term Notch ---', cpL, row, cpW, rh, bgc, [1 .6 .6]); +% Options +mkSection(fig, 'Options', cpL, row, cpW, rh, bgc, thm.btnSave, fontsz-1); row = row - rh - gap; -[h.dn_hz, h.dn_hz_lbl, row] = mkSlider(fig, 'Center:', cpL, row, rh, gap, bgc, fgc, 0, 1000, fp.dterm_notch_hz, cb); -[h.dn_cut, h.dn_cut_lbl, row] = mkSlider(fig, 'Cutoff:', cpL, row, rh, gap, bgc, fgc, 0, 800, fp.dterm_notch_cut, cb); +h.magdB = uicontrol(fig, 'Style', 'checkbox', 'String', 'Magnitude dB', 'Value', 0, ... + 'Units', 'normalized', 'Position', [x0 row halfW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'Callback', cb); +h.logfreq = uicontrol(fig, 'Style', 'checkbox', 'String', 'Log Frequency', 'Value', 0, ... + 'Units', 'normalized', 'Position', [x0+halfW row halfW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'Callback', cb); +row = row - rh - gap; +h.combinelpf = uicontrol(fig, 'Style', 'checkbox', 'String', 'combine lpf', 'Value', 0, ... + 'Units', 'normalized', 'Position', [x0 row halfW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'Callback', cb); +row = row - rh - gap; +ddW_ = cW * 0.48; +h.row4mode = uicontrol(fig, 'Style', 'popupmenu', 'String', {'Step resp.','Impulse resp.','Signal Generator'}, 'Value', 1, ... + 'Units', 'normalized', 'Position', [x0 row ddW_ rh], ... + 'FontSize', fontsz, 'Callback', @(~,~) toggleStepRow()); +h.addnoise = uicontrol(fig, 'Style', 'checkbox', 'String', 'Add noise', 'Value', 0, ... + 'Units', 'normalized', 'Position', [x0+halfW row halfW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'Callback', cb); +row = row - rh - gap; +h.showboth = uicontrol(fig, 'Style', 'checkbox', 'String', 'Show Both', 'Value', 0, ... + 'Units', 'normalized', 'Position', [x0 row halfW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'Callback', cb); +row = row - rh - gap; +h.autoupd = uicontrol(fig, 'Style', 'checkbox', 'String', 'Auto Update', 'Value', 1, ... + 'Units', 'normalized', 'Position', [x0 row halfW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', fgc, ... + 'Callback', @(~,~) toggleAutoUpdate()); +h.updBtn = uicontrol(fig, 'Style', 'pushbutton', 'String', 'Update', ... + 'Units', 'normalized', 'Position', [x0+halfW row halfW rh], ... + 'FontSize', fontsz, 'BackgroundColor', thm.btnBg, 'ForegroundColor', thm.textPrimary, ... + 'Enable', 'off', 'Callback', @(~,~) doUpdate()); +row = row - rh - gap; +h.totalDelay = uicontrol(fig, 'Style', 'text', 'String', '', ... + 'Units', 'normalized', 'Position', [x0 row cW rh], ... + 'FontSize', fontsz, 'FontWeight', 'bold', ... + 'BackgroundColor', bgc, 'ForegroundColor', thm.btnDash1, 'HorizontalAlignment', 'left'); +row = row - rh - gap*2; +h.copyCLI = uicontrol(fig, 'Style', 'pushbutton', 'String', 'Copy CLI', ... + 'Units', 'normalized', 'Position', [x0+halfW*.3 row halfW*1.3 rh+.004], ... + 'FontSize', fontsz, 'FontWeight', 'bold', ... + 'BackgroundColor', thm.btnBg, 'ForegroundColor', thm.textAccent, ... + 'Callback', @(~,~) copyCLI()); doUpdate(); + function autoUpdate() + if get(h.autoupd, 'Value'), doUpdate(); end + end + + function loopRateChanged() + Fs = loopRates(get(h.looprate, 'Value')); + autoUpdate(); + end + + function toggleStepRow() + applyLayout(); + autoUpdate(); + end + + function applyLayout() + sStep = get(h.row4mode, 'Value') > 0; % always show row 4 + sBoth = get(h.showboth, 'Value'); + offscr = [-2 -2 .01 .01]; + + if sStep + nRows = 4; + else + nRows = 3; + end + rH = (plotTop - botMargin - (nRows-1)*rowGap) / nRows; + rB1 = plotTop - rH; + rB2 = rB1 - rH - rowGap; + rB3 = rB2 - rH - rowGap; + if nRows == 4 + rB4 = rB3 - rH - rowGap; + end + + if sBoth + w = plotR - plotL; xL = plotL; + else + w = colW; xL = colL(1); + end + + set(axLmag, 'Position', [xL rB1 w rH]); + set(axLdelay, 'Position', [xL rB2 w rH]); + set(axLphase, 'Position', [xL rB3 w rH]); + if sStep + set(axLstep, 'Position', [xL rB4 w rH], 'Visible', 'on'); + else + set(axLstep, 'Position', offscr, 'Visible', 'off'); + end + + if sBoth + set(axNmag, 'Position', offscr); + set(axNdelay, 'Position', offscr); + set(axNphase, 'Position', offscr); + set(axNstep, 'Position', offscr); + set(hTitleN, 'Visible', 'off'); + set(axBarN, 'Position', offscr); + set(axBarL, 'Position', [plotL barY plotR-plotL barH]); + else + set(axNmag, 'Position', [colL(2) rB1 colW rH]); + set(axNdelay, 'Position', [colL(2) rB2 colW rH]); + set(axNphase, 'Position', [colL(2) rB3 colW rH]); + if sStep + set(axNstep, 'Position', [colL(2) rB4 colW rH], 'Visible', 'on'); + else + set(axNstep, 'Position', offscr, 'Visible', 'off'); + end + set(hTitleN, 'Visible', 'on'); + set(axBarN, 'Position', [colL(2) barY colW barH]); + set(axBarL, 'Position', [colL(1) barY colW barH]); + end + end + + function toggleAutoUpdate() + if get(h.autoupd, 'Value') + set(h.updBtn, 'Enable', 'off'); + doUpdate(); + else + set(h.updBtn, 'Enable', 'on'); + end + end + function doUpdate() if ~ishandle(fig), return; end - ai = get(h.axis, 'Value'); - dat = axData{ai}; + + allFreqAx = [axLmag axLdelay axLphase axNmag axNdelay axNphase]; + + % Suppress "Non-positive limit for logarithmic axis" during redraw + wstate = warning('query', 'all'); + warning('off', 'all'); glpf1t = get(h.glpf1_type, 'Value') - 1; - glpf1f = round(get(h.glpf1_hz, 'Value')); + glpf1f = readEdit(h.glpf1_hz); glpf2t = get(h.glpf2_type, 'Value') - 1; - glpf2f = round(get(h.glpf2_hz, 'Value')); - gn1f = round(get(h.gn1_hz, 'Value')); - gn1c = round(get(h.gn1_cut, 'Value')); - gn2f = round(get(h.gn2_hz, 'Value')); - gn2c = round(get(h.gn2_cut, 'Value')); + glpf2f = readEdit(h.glpf2_hz); + gn1f = readEdit(h.gn1_hz); + gn1c = readEdit(h.gn1_cut); + gn2f = readEdit(h.gn2_hz); + gn2c = readEdit(h.gn2_cut); dlpf1t = get(h.dlpf1_type, 'Value') - 1; - dlpf1f = round(get(h.dlpf1_hz, 'Value')); + dlpf1f = readEdit(h.dlpf1_hz); dlpf2t = get(h.dlpf2_type, 'Value') - 1; - dlpf2f = round(get(h.dlpf2_hz, 'Value')); - dnf = round(get(h.dn_hz, 'Value')); - dnc = round(get(h.dn_cut, 'Value')); - - set(h.glpf1_lbl, 'String', num2str(glpf1f)); - set(h.glpf2_lbl, 'String', num2str(glpf2f)); - set(h.gn1_hz_lbl, 'String', num2str(gn1f)); - set(h.gn1_cut_lbl, 'String', num2str(gn1c)); - set(h.gn2_hz_lbl, 'String', num2str(gn2f)); - set(h.gn2_cut_lbl, 'String', num2str(gn2c)); - set(h.dlpf1_lbl, 'String', num2str(dlpf1f)); - set(h.dlpf2_lbl, 'String', num2str(dlpf2f)); - set(h.dn_hz_lbl, 'String', num2str(dnf)); - set(h.dn_cut_lbl, 'String', num2str(dnc)); - - % gyro filter chain - filt = dat; - filt = applyLPF(filt, glpf1t, glpf1f, Fs); - filt = applyLPF(filt, glpf2t, glpf2f, Fs); - filt = applyNotch(filt, gn1f, gn1c, Fs); - filt = applyNotch(filt, gn2f, gn2c, Fs); - - % D-term - dterm = [0; diff(dat)] * Fs; - dterm = applyLPF(dterm, dlpf1t, dlpf1f, Fs); - dterm = applyLPF(dterm, dlpf2t, dlpf2f, Fs); - dterm = applyNotch(dterm, dnf, dnc, Fs); - - F_kHz = Fs / 1000; - [fO, sO] = PSSpec2d(dat', F_kHz, 1); - [fF, sF] = PSSpec2d(filt', F_kHz, 1); - [~, sD] = PSSpec2d(dterm', F_kHz, 1); - - cla(axSpec); - plot(axSpec, fO, sO, 'Color', [.5 .5 .5], 'LineWidth', 0.8); - hold(axSpec, 'on'); - plot(axSpec, fF, sF, 'c', 'LineWidth', 1.3); - plot(axSpec, fO, sD, 'Color', [.4 .9 .4], 'LineWidth', 0.8); - hold(axSpec, 'off'); - set(axSpec, 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 13, 'FontWeight', 'bold', 'XLim', [0 Fs/2]); - set(get(axSpec, 'XLabel'), 'String', 'Frequency (Hz)', 'Color', [.8 .8 .8]); - set(get(axSpec, 'YLabel'), 'String', 'PSD (dB)', 'Color', [.8 .8 .8]); - th = title(axSpec, [axNames{ai} ' - Spectrum']); - set(th, 'Color', [.9 .9 .9]); - grid(axSpec, 'on'); set(axSpec, 'GridColor', [.3 .3 .3]); - legend(axSpec, {'Raw gyro', 'Filtered gyro', 'D-term (filtered)'}, ... - 'TextColor', [.8 .8 .8], 'Color', [.2 .2 .2], 'EdgeColor', [.4 .4 .4], ... - 'Location', 'northeast', 'FontSize', 11); - - N = min(2000, length(dat)); - t = (0:N-1) / Fs * 1000; - cla(axTime); - plot(axTime, t, dat(1:N), 'Color', [.5 .5 .5], 'LineWidth', 0.6); - hold(axTime, 'on'); - plot(axTime, t, filt(1:N), 'c', 'LineWidth', 1.2); - hold(axTime, 'off'); - set(axTime, 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 13, 'FontWeight', 'bold'); - set(get(axTime, 'XLabel'), 'String', 'Time (ms)', 'Color', [.8 .8 .8]); - set(get(axTime, 'YLabel'), 'String', 'deg/s', 'Color', [.8 .8 .8]); - th2 = title(axTime, [axNames{ai} ' - Time Domain']); - set(th2, 'Color', [.9 .9 .9]); - grid(axTime, 'on'); set(axTime, 'GridColor', [.3 .3 .3]); - legend(axTime, {'Raw', 'Filtered'}, 'TextColor', [.8 .8 .8], ... - 'Color', [.2 .2 .2], 'EdgeColor', [.4 .4 .4], 'Location', 'northeast', 'FontSize', 11); + dlpf2f = readEdit(h.dlpf2_hz); + dnf = readEdit(h.dn_hz); + dnc = readEdit(h.dn_cut); + rpmBase = readEdit(h.rpm_base); + rpmNharm = get(h.rpm_nharm, 'Value') - 1; + rpmQ = readEdit(h.rpm_q); + nMotors = get(h.rpm_nmot, 'Value'); + usedB = get(h.magdB, 'Value'); + useLog = get(h.logfreq, 'Value'); + combineLPF = get(h.combinelpf, 'Value'); + addNoise = get(h.addnoise, 'Value'); + showBoth = get(h.showboth, 'Value'); + row4mode = get(h.row4mode, 'Value'); % 1=step, 2=impulse, 3=signal gen + sigHzLo = readEdit(h.sig_start); + sigHzHi = readEdit(h.sig_end); + sigDur = max(0.1, readEditF(h.sig_dur)); + + % partial update: skip unchanged column + curAll = [glpf1t glpf1f glpf2t glpf2f dlpf1t dlpf1f dlpf2t dlpf2f ... + gn1f gn1c gn2f gn2c dnf dnc rpmBase rpmNharm rpmQ nMotors ... + usedB useLog combineLPF addNoise showBoth row4mode sigHzLo sigHzHi round(sigDur*100)]; + curLPF = curAll(1:8); curNotch = curAll(9:18); + prev = getappdata(fig, 'prevAll'); + doLPF = true; doNotch = true; + if ~isempty(prev) && numel(prev) == numel(curAll) + lpfChanged = ~isequal(curLPF, prev(1:8)); + notchChanged = ~isequal(curNotch, prev(9:18)); + optsChanged = ~isequal(curAll(19:end), prev(19:end)); + if ~optsChanged && ~showBoth + if lpfChanged && ~notchChanged, doNotch = false; end + if notchChanged && ~lpfChanged, doLPF = false; end + end + end + setappdata(fig, 'prevAll', curAll); + + types = {'pt1', 'biquad', 'pt2', 'pt3'}; + Nfft = 4096; + fVec = linspace(0, Fs/2, Nfft)'; + fMax = min(Fs/2, 1000); + fIdx = fVec <= fMax; + if useLog, fMin = 10; fIdx = fVec >= fMin & fVec <= fMax; end + + % LPF frequency responses + H_lpf1 = lpfH(glpf1t, glpf1f, Fs, Nfft, types); + H_lpf2 = lpfH(glpf2t, glpf2f, Fs, Nfft, types); + H_dlpf1 = lpfH(dlpf1t, dlpf1f, Fs, Nfft, types); + H_dlpf2 = lpfH(dlpf2t, dlpf2f, Fs, Nfft, types); + H_gyroLPF = H_lpf1 .* H_lpf2; + H_dtermLPF = H_dlpf1 .* H_dlpf2; + + % Notch frequency responses (static) + H_gn1 = notchH(gn1f, gn1c, Fs, Nfft); + H_gn2 = notchH(gn2f, gn2c, Fs, Nfft); + H_dn = notchH(dnf, dnc, Fs, Nfft); + + % RPM harmonic notches (cascaded nMotors times per harmonic) + rpmCols = {[.9 .2 .2],[.9 .6 .1],[.9 .9 .2],[.3 .8 .3],[.2 .8 .8],[.9 .9 .9],[.8 .3 .8]}; + H_rpm = cell(rpmNharm, 1); + H_rpm1 = cell(rpmNharm, 1); % single-motor for phase plot + H_rpmAll = ones(Nfft, 1); + for ri = 1:rpmNharm + fc_rpm = rpmBase * ri; + if fc_rpm > 0 && fc_rpm < Fs/2 + H_single = notchH_Q(fc_rpm, rpmQ, Fs, Nfft); + H_rpm1{ri} = H_single; + H_rpm{ri} = H_single .^ nMotors; + else + H_rpm1{ri} = ones(Nfft, 1); + H_rpm{ri} = ones(Nfft, 1); + end + H_rpmAll = H_rpmAll .* H_rpm{ri}; + end + H_gyroN = H_gn1 .* H_gn2 .* H_rpmAll; + % single-motor product for phase plot (avoids ±180 wrapping) + H_rpmAll1 = ones(Nfft, 1); + for ri = 1:rpmNharm, H_rpmAll1 = H_rpmAll1 .* H_rpm1{ri}; end + H_gyroN1 = H_gn1 .* H_gn2 .* H_rpmAll1; + H_dtermN = H_dn; + + % Group delay + dw = gradient(2*pi*fVec); + gd_gL = smooth(-gradient(unwrap(angle(H_gyroLPF))) ./ dw * 1000, 21, 'moving'); + gd_dL = smooth(-gradient(unwrap(angle(H_dtermLPF))) ./ dw * 1000, 21, 'moving'); + gd_gN = smooth(-gradient(unwrap(angle(H_gyroN))) ./ dw * 1000, 51, 'moving'); + gd_dN = smooth(-gradient(unwrap(angle(H_dtermN))) ./ dw * 1000, 51, 'moving'); + + delayL = gd_gL(2); + delayN = gd_gN(2); + set(hTitleL, 'String', sprintf('LOWPASS FILTERS | Delay %.5fms', delayL)); + set(hTitleN, 'String', sprintf('NOTCH FILTERS | Delay %.4fms', delayN)); + set(h.totalDelay, 'String', sprintf('Total Delay: %.3fms (LPF %.3f + Notch %.3f)', ... + delayL + delayN, delayL, delayN)); + + % Row 4 input signals + if row4mode == 3 + % Signal Generator: chirp + sigLen = round(Fs * sigDur); + tSigL = (0:sigLen-1)' / Fs; + sigInL = localChirp(tSigL, sigHzLo, sigDur, sigHzHi); + tSigN = tSigL; sigInN = sigInL; + r4labelL = 'Duration (sec)'; r4ylabelL = 'Amplitude'; + r4labelN = 'Duration (sec)'; r4ylabelN = 'Amplitude'; + elseif row4mode == 2 + % Impulse response + lpfStepMs = 4; + stepLenL = round(Fs * lpfStepMs / 1000); + sigInL = zeros(stepLenL, 1); sigInL(1) = 1; + tSigL = (0:stepLenL-1)' / Fs * 1000; + notchStepMs = 100; + stepLenN = round(Fs * notchStepMs / 1000); + sigInN = zeros(stepLenN, 1); sigInN(1) = 1; + tSigN = (0:stepLenN-1)' / Fs * 1000; + r4labelL = 'Time (ms)'; r4ylabelL = 'Impulse Resp.'; + r4labelN = 'Time (ms)'; r4ylabelN = 'Impulse Resp.'; + else + % Step response + lpfStepMs = 4; + stepLenL = round(Fs * lpfStepMs / 1000); + sigInL = ones(stepLenL, 1); + tSigL = (0:stepLenL-1)' / Fs * 1000; + notchStepMs = 100; + stepLenN = round(Fs * notchStepMs / 1000); + sigInN = ones(stepLenN, 1); + tSigN = (0:stepLenN-1)' / Fs * 1000; + r4labelL = 'Time (ms)'; r4ylabelL = 'Step Resp.'; + r4labelN = 'Time (ms)'; r4ylabelN = 'Step Resp.'; + end + + sL_g = applyLPF(applyLPF(sigInL, glpf2t, glpf2f, Fs), glpf1t, glpf1f, Fs); + sL_d = applyLPF(applyLPF(sigInL, dlpf2t, dlpf2f, Fs), dlpf1t, dlpf1f, Fs); + sN_g = applyNotch(applyNotch(sigInN, gn2f, gn2c, Fs), gn1f, gn1c, Fs); + for ri = 1:rpmNharm + fc_rpm = rpmBase * ri; + if fc_rpm > 0 && fc_rpm < Fs/2 + for mi = 1:nMotors + sN_g = applyNotch_Q(sN_g, fc_rpm, rpmQ, Fs); + end + end + end + sN_d = applyNotch(sigInN, dnf, dnc, Fs); + sN_rpm = cell(rpmNharm, 1); + for ri = 1:rpmNharm + fc_rpm = rpmBase * ri; + if fc_rpm > 0 && fc_rpm < Fs/2 + tmp = sigInN; + for mi = 1:nMotors, tmp = applyNotch_Q(tmp, fc_rpm, rpmQ, Fs); end + sN_rpm{ri} = tmp; + else + sN_rpm{ri} = sigInN; + end + end + sN_gn1 = applyNotch(sigInN, gn1f, gn1c, Fs); + sN_gn2 = applyNotch(sigInN, gn2f, gn2c, Fs); + + if addNoise + noiseAmp = 0.03; + noisyL = sigInL + randn(numel(sigInL), 1) * noiseAmp; + nsL_g = applyLPF(applyLPF(noisyL, glpf2t, glpf2f, Fs), glpf1t, glpf1f, Fs); + nsL_d = applyLPF(applyLPF(noisyL, dlpf2t, dlpf2f, Fs), dlpf1t, dlpf1f, Fs); + noisyN = sigInN + randn(numel(sigInN), 1) * noiseAmp; + nsN_g = applyNotch(applyNotch(noisyN, gn2f, gn2c, Fs), gn1f, gn1c, Fs); + for ri = 1:rpmNharm + fc_rpm = rpmBase * ri; + if fc_rpm > 0 && fc_rpm < Fs/2 + for mi = 1:nMotors, nsN_g = applyNotch_Q(nsN_g, fc_rpm, rpmQ, Fs); end + end + end + nsN_d = applyNotch(noisyN, dnf, dnc, Fs); + end + + colG = [0 .85 .85]; colD = [.4 .9 .4]; + colRef = [.45 .45 .45]; + % Notch column: warm colors + colStaticN = [1 .65 .2]; + colCombN = [.95 .85 .2]; + plotFn = @plot; + + %% GRADIENT FREQUENCY BARS + drawGradientBar(axBarL, fMax, [.2 .7 .8; .3 .9 .5], ... + {glpf1f, glpf2f, dlpf1f, dlpf2f}, {colG*0.7, colG, colD*0.7, colD}, thm); + set(axBarL, 'ButtonDownFcn', @(~,~) barClick(axBarL, {h.glpf1_hz, h.glpf2_hz, h.dlpf1_hz, h.dlpf2_hz}, fMax)); + notchFreqs = {}; notchCols = {}; + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); + notchFreqs{end+1} = rpmBase * ri; + notchCols{end+1} = rpmCols{ci}; + end + if gn1f > 0, notchFreqs{end+1} = gn1f; notchCols{end+1} = colStaticN; end + if gn2f > 0, notchFreqs{end+1} = gn2f; notchCols{end+1} = colStaticN*0.85; end + if dnf > 0, notchFreqs{end+1} = dnf; notchCols{end+1} = colD; end + drawGradientBar(axBarN, fMax, [.8 .3 .2; .9 .7 .2], notchFreqs, notchCols, thm); + set(axBarN, 'ButtonDownFcn', @(~,~) barClick(axBarN, {h.gn1_hz, h.gn2_hz, h.dn_hz}, fMax)); + + %% TEST SIGNAL PSD (if sigHzHi > sigHzLo) + if sigHzHi > sigHzLo && sigDur > 0 + Nsig = round(Fs * sigDur); + t_sig = (0:Nsig-1)' / Fs; + sig = localChirp(t_sig, max(sigHzLo,1), sigDur, sigHzHi); + sig_gL = sig; + if glpf1t > 0 && glpf1f > 0, sig_gL = applyLPF(sig_gL, glpf1t, glpf1f, Fs); end + if glpf2t > 0 && glpf2f > 0, sig_gL = applyLPF(sig_gL, glpf2t, glpf2f, Fs); end + sig_gN = sig; + if gn1f > 0, sig_gN = applyNotch(sig_gN, gn1f, gn1c, Fs); end + if gn2f > 0, sig_gN = applyNotch(sig_gN, gn2f, gn2c, Fs); end + for ri = 1:rpmNharm + fc_rpm = rpmBase * ri; + if fc_rpm > 0 && fc_rpm < Fs/2 + for mi = 1:nMotors, sig_gN = applyNotch_Q(sig_gN, fc_rpm, rpmQ, Fs); end + end + end + end + + %% LOWPASS COLUMN + + % Individual filter responses for non-combined mode + gd_g1 = smooth(-gradient(unwrap(angle(H_lpf1))) ./ dw * 1000, 21, 'moving'); + gd_g2 = smooth(-gradient(unwrap(angle(H_lpf2))) ./ dw * 1000, 21, 'moving'); + gd_d1 = smooth(-gradient(unwrap(angle(H_dlpf1))) ./ dw * 1000, 21, 'moving'); + gd_d2 = smooth(-gradient(unwrap(angle(H_dlpf2))) ./ dw * 1000, 21, 'moving'); + ph_g1 = unwrap(angle(H_lpf1)) * 180/pi; + ph_g2 = unwrap(angle(H_lpf2)) * 180/pi; + ph_d1 = unwrap(angle(H_dlpf1)) * 180/pi; + ph_d2 = unwrap(angle(H_dlpf2)) * 180/pi; + sL_g1 = applyLPF(sigInL, glpf1t, glpf1f, Fs); + sL_g2 = applyLPF(sigInL, glpf2t, glpf2f, Fs); + sL_d1 = applyLPF(sigInL, dlpf1t, dlpf1f, Fs); + sL_d2 = applyLPF(sigInL, dlpf2t, dlpf2f, Fs); + + g1on = glpf1t > 0 && glpf1f > 0; + g2on = glpf2t > 0 && glpf2f > 0; + d1on = dlpf1t > 0 && dlpf1f > 0; + d2on = dlpf2t > 0 && dlpf2f > 0; + colNoise = [.9 .25 .25]; + + applyLayout(); + fF = fVec(fIdx); + + if doLPF % --- LPF COLUMN --- + + %% LOWPASS MAGNITUDE (pre-created lines) + li = 1; + if combineLPF + li = setLine(AX_LMAG, li, fF, magY(H_gyroLPF(fIdx), usedB), colG, 1.5); + li = setLine(AX_LMAG, li, fF, magY(H_dtermLPF(fIdx), usedB), colD, 1.2); + else + if g1on, li = setLine(AX_LMAG, li, fF, magY(H_lpf1(fIdx), usedB), colG, 1.2); end + if g2on, li = setLine(AX_LMAG, li, fF, magY(H_lpf2(fIdx), usedB), colG, 1.2, '--'); end + if d1on, li = setLine(AX_LMAG, li, fF, magY(H_dlpf1(fIdx), usedB), colD, 1.0); end + if d2on, li = setLine(AX_LMAG, li, fF, magY(H_dlpf2(fIdx), usedB), colD, 1.0, '--'); end + end + if showBoth + li = setLine(AX_LMAG, li, fF, magY(H_gyroN(fIdx), usedB), colCombN, 1.2); + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); + li = setLine(AX_LMAG, li, fF, magY(H_rpm{ri}(fIdx), usedB), rpmCols{ci}, 0.7); + end + end + fLo = xlimF(useLog, fMax); + refY = 0.707; if usedB, refY = -3; end + li = setLine(AX_LMAG, li, [fLo(1) fMax], [refY refY], thm.refLine3dB, 0.5, ':'); + % cutoff vertical lines (gyro + dterm) + magYL = [0 1.1]; if usedB, magYL = [-60 3]; end + if g1on && glpf1f > 0, li = setLine(AX_LMAG, li, [glpf1f glpf1f], magYL, colG*0.7, 0.5, '--'); end + if g2on && glpf2f > 0, li = setLine(AX_LMAG, li, [glpf2f glpf2f], magYL, colG, 0.5, '--'); end + if d1on && dlpf1f > 0, li = setLine(AX_LMAG, li, [dlpf1f dlpf1f], magYL, colD*0.7, 0.5, '--'); end + if d2on && dlpf2f > 0, li = setLine(AX_LMAG, li, [dlpf2f dlpf2f], magYL, colD, 0.5, '--'); end + hideRest(AX_LMAG, li); + if usedB + set(axLmag, 'XLim', fLo, 'YLim', [-60 3], 'XTickLabel', {}); + set(get(axLmag, 'YLabel'), 'String', 'Magnitude (dB)'); + else + set(axLmag, 'XLim', fLo, 'YLim', [0 1.1], 'XTickLabel', {}); + set(get(axLmag, 'YLabel'), 'String', 'Magnitude (abs)'); + end + % annotations (text + cutoff lines via txPool) + ti = 1; + names_ = {'off', 'pt1', 'biquad', 'pt2', 'pt3'}; + if usedB, ay = -3; ayd = -6; else ay = 1.05; ayd = -0.08; end + if glpf1t > 0 && glpf1f > 0 + ti = setTxt(AX_LMAG, ti, glpf1f+10, ay, sprintf('%s: %dHz', names_{glpf1t+1}, glpf1f), colG*0.7); + ay = ay + ayd; + end + if glpf2t > 0 && glpf2f > 0 + ti = setTxt(AX_LMAG, ti, glpf2f+10, ay, sprintf('%s: %dHz', names_{glpf2t+1}, glpf2f), colG); + ay = ay + ayd; + end + if dlpf1t > 0 && dlpf1f > 0 + ti = setTxt(AX_LMAG, ti, dlpf1f+10, ay, sprintf('D %s: %dHz', names_{dlpf1t+1}, dlpf1f), colD*0.7); + ay = ay + ayd; + end + if dlpf2t > 0 && dlpf2f > 0 + ti = setTxt(AX_LMAG, ti, dlpf2f+10, ay, sprintf('D %s: %dHz', names_{dlpf2t+1}, dlpf2f), colD); + end + hideTxt(AX_LMAG, ti); + + %% LOWPASS DELAY (pre-created lines) + li = 1; + if combineLPF + li = setLine(AX_LDLY, li, fF, gd_gL(fIdx), colG, 1.5); + li = setLine(AX_LDLY, li, fF, gd_dL(fIdx), colD, 1.2); + gdMax = max([max(gd_gL(fIdx)) max(gd_dL(fIdx)) 0.3]) * 1.3; + else + allGdL = [0.3]; + if g1on, li = setLine(AX_LDLY, li, fF, gd_g1(fIdx), colG, 1.2); allGdL(end+1) = max(gd_g1(fIdx)); end + if g2on, li = setLine(AX_LDLY, li, fF, gd_g2(fIdx), colG, 1.2, '--'); allGdL(end+1) = max(gd_g2(fIdx)); end + if d1on, li = setLine(AX_LDLY, li, fF, gd_d1(fIdx), colD, 1.0); allGdL(end+1) = max(gd_d1(fIdx)); end + if d2on, li = setLine(AX_LDLY, li, fF, gd_d2(fIdx), colD, 1.0, '--'); allGdL(end+1) = max(gd_d2(fIdx)); end + gdMax = max(allGdL) * 1.3; + end + if showBoth + li = setLine(AX_LDLY, li, fF, gd_gN(fIdx), colCombN, 1.2); + end + % cutoff lines (gyro + dterm) + if g1on && glpf1f > 0, li = setLine(AX_LDLY, li, [glpf1f glpf1f], [0 gdMax], colG*0.7, 0.5, '--'); end + if g2on && glpf2f > 0, li = setLine(AX_LDLY, li, [glpf2f glpf2f], [0 gdMax], colG, 0.5, '--'); end + if d1on && dlpf1f > 0, li = setLine(AX_LDLY, li, [dlpf1f dlpf1f], [0 gdMax], colD*0.7, 0.5, '--'); end + if d2on && dlpf2f > 0, li = setLine(AX_LDLY, li, [dlpf2f dlpf2f], [0 gdMax], colD, 0.5, '--'); end + hideRest(AX_LDLY, li); + set(axLdelay, 'XLim', xlimF(useLog, fMax), 'XTickLabel', {}); + if isfinite(gdMax) && gdMax > 0, set(axLdelay, 'YLim', [0 gdMax]); end + set(get(axLdelay, 'YLabel'), 'String', 'Filter Delay (ms)'); + % delay annotations + ti = 1; + yTxtL = gdMax * 0.92; yTxtStp = gdMax * 0.17; + if combineLPF + ti = setTxt(AX_LDLY, ti, fMax*0.02, yTxtL, sprintf('gyro lpf: %.5fms', gd_gL(2)), colG); + ti = setTxt(AX_LDLY, ti, fMax*0.02, yTxtL-yTxtStp, sprintf('dterm lpf: %.5fms', gd_dL(2)), colD); + else + if g1on, ti = setTxt(AX_LDLY, ti, fMax*0.02, yTxtL, sprintf('gyro lpf1: %.5fms', gd_g1(2)), colG*0.7); yTxtL=yTxtL-yTxtStp; end + if g2on, ti = setTxt(AX_LDLY, ti, fMax*0.02, yTxtL, sprintf('gyro lpf2: %.5fms', gd_g2(2)), colG); yTxtL=yTxtL-yTxtStp; end + if d1on, ti = setTxt(AX_LDLY, ti, fMax*0.02, yTxtL, sprintf('dterm lpf1: %.5fms', gd_d1(2)), colD*0.7); yTxtL=yTxtL-yTxtStp; end + if d2on, ti = setTxt(AX_LDLY, ti, fMax*0.02, yTxtL, sprintf('dterm lpf2: %.5fms', gd_d2(2)), colD); end + end + hideTxt(AX_LDLY, ti); + + %% LOWPASS PHASE (pre-created lines) + li = 1; + if combineLPF + ph_gL = unwrap(angle(H_gyroLPF)) * 180/pi; + ph_dL = unwrap(angle(H_dtermLPF)) * 180/pi; + li = setLine(AX_LPH, li, fF, ph_gL(fIdx), colG, 1.5); + li = setLine(AX_LPH, li, fF, ph_dL(fIdx), colD, 1.2); + else + if g1on, li = setLine(AX_LPH, li, fF, ph_g1(fIdx), colG, 1.2); end + if g2on, li = setLine(AX_LPH, li, fF, ph_g2(fIdx), colG, 1.2, '--'); end + if d1on, li = setLine(AX_LPH, li, fF, ph_d1(fIdx), colD, 1.0); end + if d2on, li = setLine(AX_LPH, li, fF, ph_d2(fIdx), colD, 1.0, '--'); end + end + if showBoth + ph_gN_ = smooth(angle(H_gyroN1) * 180/pi, 9, 'moving'); + li = setLine(AX_LPH, li, fF, ph_gN_(fIdx), colCombN, 1.2); + end + if g1on && glpf1f > 0, li = setLine(AX_LPH, li, [glpf1f glpf1f], [-200 0], colG*0.7, 0.5, '--'); end + if g2on && glpf2f > 0, li = setLine(AX_LPH, li, [glpf2f glpf2f], [-200 0], colG, 0.5, '--'); end + if d1on && dlpf1f > 0, li = setLine(AX_LPH, li, [dlpf1f dlpf1f], [-200 0], colD*0.7, 0.5, '--'); end + if d2on && dlpf2f > 0, li = setLine(AX_LPH, li, [dlpf2f dlpf2f], [-200 0], colD, 0.5, '--'); end + hideRest(AX_LPH, li); + set(axLphase, 'XLim', xlimF(useLog, fMax), 'XTickLabel', {}); + set(get(axLphase, 'YLabel'), 'String', 'Phase Delay (deg)'); + hideTxt(AX_LPH, 1); + + %% LOWPASS ROW 4 (pre-created lines) + li = 1; + if row4mode == 3 + li = setLine(AX_LSTP, li, tSigL, sigInL, [.5 .5 .5], 0.6); + li = setLine(AX_LSTP, li, tSigL, sL_g, [.95 .2 .2], 1.2); + hideRest(AX_LSTP, li); + set(axLstep, 'XLim', [0 sigDur], 'YLim', [-1.1 1.1]); + else + if row4mode == 1 + li = setLine(AX_LSTP, li, [tSigL(1) tSigL(end)], [1 1], colRef, 0.5, '--'); + end + if combineLPF + li = setLine(AX_LSTP, li, tSigL, sL_g, colG, 1.5); + li = setLine(AX_LSTP, li, tSigL, sL_d, colD, 1.2); + if addNoise + li = setLine(AX_LSTP, li, tSigL, nsL_g, colNoise, 0.8); + li = setLine(AX_LSTP, li, tSigL, nsL_d, colNoise*0.7, 0.7); + end + else + if g1on, li = setLine(AX_LSTP, li, tSigL, sL_g1, colG, 1.2); end + if g2on, li = setLine(AX_LSTP, li, tSigL, sL_g2, colG, 1.2, '--'); end + if d1on, li = setLine(AX_LSTP, li, tSigL, sL_d1, colD, 1.0); end + if d2on, li = setLine(AX_LSTP, li, tSigL, sL_d2, colD, 1.0, '--'); end + if addNoise + li = setLine(AX_LSTP, li, tSigL, nsL_g, colNoise, 0.8); + li = setLine(AX_LSTP, li, tSigL, nsL_d, colNoise*0.7, 0.7); + end + end + hideRest(AX_LSTP, li); + if row4mode == 1 + set(axLstep, 'XLim', [tSigL(1) tSigL(end)], 'YLim', [-0.05 1.15]); + else + set(axLstep, 'XLim', [tSigL(1) tSigL(end)]); + end + end + xlabel(axLstep, r4labelL, 'Color', thm.textPrimary); + set(get(axLstep, 'YLabel'), 'String', r4ylabelL); + hideTxt(AX_LSTP, 1); + end % if doLPF + + %% NOTCH COLUMN (skip when showBoth) + + if doNotch && ~showBoth + gridC = thm.gridColor; + + %% NOTCH MAGNITUDE + li = 1; + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); + li = setLine(AX_NMAG, li, fF, magY(H_rpm{ri}(fIdx), usedB), rpmCols{ci}, 0.9); + end + if gn1f > 0, li = setLine(AX_NMAG, li, fF, magY(H_gn1(fIdx), usedB), colStaticN, 0.8); end + if gn2f > 0, li = setLine(AX_NMAG, li, fF, magY(H_gn2(fIdx), usedB), colStaticN*0.85, 0.8, '--'); end + li = setLine(AX_NMAG, li, fF, magY(H_gyroN(fIdx), usedB), colCombN, 1.5); + if dnf > 0, li = setLine(AX_NMAG, li, fF, magY(H_dtermN(fIdx), usedB), colD, 1.2); end + % notch vertical lines + for ri = 1:rpmNharm, fc=rpmBase*ri; if fc>0, li=setLine(AX_NMAG,li,[fc fc],[0 1.1],gridC,0.5,':'); end; end + if gn1f>0, li=setLine(AX_NMAG,li,[gn1f gn1f],[0 1.1],colStaticN,0.5,'--'); end + if gn2f>0, li=setLine(AX_NMAG,li,[gn2f gn2f],[0 1.1],colStaticN*0.85,0.5,'--'); end + if dnf>0, li=setLine(AX_NMAG,li,[dnf dnf],[0 1.1],colD,0.5,'--'); end + hideRest(AX_NMAG, li); + if usedB + set(axNmag, 'XLim', xlimF(useLog, fMax), 'YLim', [-40 3], 'XTickLabel', {}); + set(get(axNmag, 'YLabel'), 'String', 'Magnitude (dB)'); + else + set(axNmag, 'XLim', xlimF(useLog, fMax), 'YLim', [0 1.1], 'XTickLabel', {}); + set(get(axNmag, 'YLabel'), 'String', 'Magnitude (abs)'); + end + % annotations + ti = 1; + if usedB, ay0=-5; ayd=-5; else ay0=0.85; ayd=-0.07; end + ay = ay0; + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); fc=rpmBase*ri; + ti = setTxt(AX_NMAG, ti, fc+10, ay, sprintf('RPM: %dHz', fc), rpmCols{ci}); ay=ay+ayd; + end + if gn1f>0, ti=setTxt(AX_NMAG,ti,gn1f+10,ay,sprintf('N1: %dHz',gn1f),colStaticN); ay=ay+ayd; end + if gn2f>0, ti=setTxt(AX_NMAG,ti,gn2f+10,ay,sprintf('N2: %dHz',gn2f),colStaticN*0.85); ay=ay+ayd; end + if dnf>0, ti=setTxt(AX_NMAG,ti,dnf+10,ay,sprintf('D: %dHz',dnf),colD); end + hideTxt(AX_NMAG, ti); + + %% NOTCH DELAY + li = 1; + rpmGd1 = cell(rpmNharm, 1); + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); + rpmGd1{ri} = smooth(-gradient(unwrap(angle(H_rpm1{ri}))) ./ dw * 1000, 51, 'moving'); + li = setLine(AX_NDLY, li, fF, rpmGd1{ri}(fIdx), rpmCols{ci}, 0.9); + end + if gn1f > 0 + gd_gn1_v = smooth(-gradient(unwrap(angle(H_gn1))) ./ dw * 1000, 51, 'moving'); + li = setLine(AX_NDLY, li, fF, gd_gn1_v(fIdx), colStaticN, 0.8); + end + if gn2f > 0 + gd_gn2_v = smooth(-gradient(unwrap(angle(H_gn2))) ./ dw * 1000, 51, 'moving'); + li = setLine(AX_NDLY, li, fF, gd_gn2_v(fIdx), colStaticN*0.85, 0.8, '--'); + end + li = setLine(AX_NDLY, li, fF, gd_gN(fIdx), colCombN, 1.5); + if dnf > 0, li = setLine(AX_NDLY, li, fF, gd_dN(fIdx), colD, 1.2); end + % vertical lines + for ri=1:rpmNharm, fc=rpmBase*ri; if fc>0, li=setLine(AX_NDLY,li,[fc fc],[-1 1],gridC,0.5,':'); end; end + if gn1f>0, li=setLine(AX_NDLY,li,[gn1f gn1f],[-1 1],colStaticN,0.5,'--'); end + if gn2f>0, li=setLine(AX_NDLY,li,[gn2f gn2f],[-1 1],colStaticN*0.85,0.5,'--'); end + if dnf>0, li=setLine(AX_NDLY,li,[dnf dnf],[-1 1],colD,0.5,'--'); end + hideRest(AX_NDLY, li); + gdMaxN = max(abs(gd_gN(2)) * 3, 0.5); + if isfinite(gdMaxN), set(axNdelay, 'YLim', [-gdMaxN gdMaxN]); end + set(axNdelay, 'XLim', xlimF(useLog, fMax), 'XTickLabel', {}); + set(get(axNdelay, 'YLabel'), 'String', 'Filter Delay (ms)'); + % annotations + ti = 1; nAn = max(rpmNharm+2, 2); + yTxt = gdMaxN*0.9; yStN = 2*gdMaxN*0.9/nAn; + ti = setTxt(AX_NDLY, ti, fMax*0.3, yTxt, sprintf('combined: %.4fms', gd_gN(2)), colCombN); yTxt=yTxt-yStN; + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); fc=rpmBase*ri; + ti = setTxt(AX_NDLY, ti, fMax*0.3, yTxt, sprintf('RPM %dHz: %.5fms', fc, rpmGd1{ri}(2)), rpmCols{ci}); yTxt=yTxt-yStN; + end + if gn1f>0, ti=setTxt(AX_NDLY,ti,fMax*0.3,yTxt,sprintf('N1 %dHz: %.5fms',gn1f,gd_gn1_v(2)),colStaticN); yTxt=yTxt-yStN; end + if gn2f>0, ti=setTxt(AX_NDLY,ti,fMax*0.3,yTxt,sprintf('N2 %dHz: %.5fms',gn2f,gd_gn2_v(2)),colStaticN*0.85); yTxt=yTxt-yStN; end + if dnf>0, ti=setTxt(AX_NDLY,ti,fMax*0.3,yTxt,sprintf('D %dHz: %.4fms',dnf,gd_dN(2)),colD); end + hideTxt(AX_NDLY, ti); + + %% NOTCH PHASE + li = 1; + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); + ph_ri = smooth(angle(H_rpm1{ri}) * 180/pi, 9, 'moving'); + li = setLine(AX_NPH, li, fF, ph_ri(fIdx), rpmCols{ci}, 0.9); + end + if gn1f > 0 + ph_gn1_ = smooth(angle(H_gn1) * 180/pi, 9, 'moving'); + li = setLine(AX_NPH, li, fF, ph_gn1_(fIdx), colStaticN, 0.8); + end + if gn2f > 0 + ph_gn2_ = smooth(angle(H_gn2) * 180/pi, 9, 'moving'); + li = setLine(AX_NPH, li, fF, ph_gn2_(fIdx), colStaticN*0.85, 0.8, '--'); + end + ph_gN = smooth(angle(H_gyroN1) * 180/pi, 9, 'moving'); + ph_dN_ = smooth(angle(H_dtermN) * 180/pi, 9, 'moving'); + li = setLine(AX_NPH, li, fF, ph_gN(fIdx), colCombN, 1.5); + if dnf > 0, li = setLine(AX_NPH, li, fF, ph_dN_(fIdx), colD, 1.2); end + for ri=1:rpmNharm, fc=rpmBase*ri; if fc>0, li=setLine(AX_NPH,li,[fc fc],[-90 90],gridC,0.5,':'); end; end + if gn1f>0, li=setLine(AX_NPH,li,[gn1f gn1f],[-90 90],colStaticN,0.5,'--'); end + if gn2f>0, li=setLine(AX_NPH,li,[gn2f gn2f],[-90 90],colStaticN*0.85,0.5,'--'); end + if dnf>0, li=setLine(AX_NPH,li,[dnf dnf],[-90 90],colD,0.5,'--'); end + hideRest(AX_NPH, li); + set(axNphase, 'XLim', xlimF(useLog, fMax), 'YLim', [-90 90], 'XTickLabel', {}); + set(get(axNphase, 'YLabel'), 'String', 'Phase Delay (deg)'); + hideTxt(AX_NPH, 1); + + %% NOTCH ROW 4 + li = 1; + if row4mode == 3 + li = setLine(AX_NSTP, li, tSigN, sigInN, [.5 .5 .5], 0.6); + li = setLine(AX_NSTP, li, tSigN, sN_g, [.95 .2 .2], 1.2); + hideRest(AX_NSTP, li); + set(axNstep, 'XLim', [0 sigDur], 'YLim', [-1.1 1.1]); + else + if row4mode == 1, li = setLine(AX_NSTP, li, [tSigN(1) tSigN(end)], [1 1], colRef, 0.5, '--'); end + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); + li = setLine(AX_NSTP, li, tSigN, sN_rpm{ri}, rpmCols{ci}, 0.8); + end + if gn1f > 0, li = setLine(AX_NSTP, li, tSigN, sN_gn1, colStaticN, 0.8); end + if gn2f > 0, li = setLine(AX_NSTP, li, tSigN, sN_gn2, colStaticN*0.85, 0.8, '--'); end + li = setLine(AX_NSTP, li, tSigN, sN_g, colCombN, 1.5); + if dnf > 0, li = setLine(AX_NSTP, li, tSigN, sN_d, colD, 1.2); end + if addNoise + li = setLine(AX_NSTP, li, tSigN, nsN_g, colNoise, 0.8); + if dnf > 0, li = setLine(AX_NSTP, li, tSigN, nsN_d, colNoise*0.7, 0.7); end + end + hideRest(AX_NSTP, li); + if row4mode == 1 + set(axNstep, 'XLim', [tSigN(1) tSigN(end)], 'YLim', [0.8 1.2]); + else + set(axNstep, 'XLim', [tSigN(1) tSigN(end)]); + end + end + xlabel(axNstep, r4labelN, 'Color', thm.textPrimary); + set(get(axNstep, 'YLabel'), 'String', r4ylabelN); + hideTxt(AX_NSTP, 1); + end + + % Set XScale+XLim atomically + xl = xlimF(useLog, fMax); + if useLog, xsc = 'log'; else xsc = 'linear'; end + for axi = 1:numel(allFreqAx) + if ishandle(allFreqAx(axi)), set(allFreqAx(axi), 'XLim', xl, 'XScale', xsc); end + end + + warning(wstate); + end + + function copyCLI() + typeNames = {'PT1', 'BIQUAD', 'PT2', 'PT3'}; + lines = {}; + lines{end+1} = '# PIDscope Filter Sim -> BF CLI'; + lines{end+1} = '# Gyro Lowpass'; + t = cliTypeIdx(h.glpf1_type); + lines{end+1} = sprintf('set gyro_lpf1_type = %s', typeNames{max(t,1)}); + lines{end+1} = sprintf('set gyro_lpf1_static_hz = %d', cliHz(h.glpf1_hz, t)); + t = cliTypeIdx(h.glpf2_type); + lines{end+1} = sprintf('set gyro_lpf2_type = %s', typeNames{max(t,1)}); + lines{end+1} = sprintf('set gyro_lpf2_static_hz = %d', cliHz(h.glpf2_hz, t)); + lines{end+1} = '# Gyro Notch'; + lines{end+1} = sprintf('set gyro_notch1_hz = %d', readEdit(h.gn1_hz)); + lines{end+1} = sprintf('set gyro_notch1_cutoff = %d', readEdit(h.gn1_cut)); + lines{end+1} = sprintf('set gyro_notch2_hz = %d', readEdit(h.gn2_hz)); + lines{end+1} = sprintf('set gyro_notch2_cutoff = %d', readEdit(h.gn2_cut)); + lines{end+1} = '# D-term Lowpass'; + t = cliTypeIdx(h.dlpf1_type); + lines{end+1} = sprintf('set dterm_lpf1_type = %s', typeNames{max(t,1)}); + lines{end+1} = sprintf('set dterm_lpf1_static_hz = %d', cliHz(h.dlpf1_hz, t)); + t = cliTypeIdx(h.dlpf2_type); + lines{end+1} = sprintf('set dterm_lpf2_type = %s', typeNames{max(t,1)}); + lines{end+1} = sprintf('set dterm_lpf2_static_hz = %d', cliHz(h.dlpf2_hz, t)); + lines{end+1} = '# D-term Notch'; + lines{end+1} = sprintf('set dterm_notch_hz = %d', readEdit(h.dn_hz)); + lines{end+1} = sprintf('set dterm_notch_cutoff = %d', readEdit(h.dn_cut)); + lines{end+1} = 'save'; + cliText = strjoin(lines, char(10)); + ok = copyToClipboard(cliText); + if ok + set(h.copyCLI, 'String', 'Copied!'); + else + showCLIDialog(cliText); + end + end + + function t = cliTypeIdx(hPopup) + v = get(hPopup, 'Value'); + t = max(v - 1, 0); + end + + function hz = cliHz(hEdit, typeVal) + if typeVal == 0, hz = 0; return; end + hz = readEdit(hEdit); + end + + function idx = setLine(axi, idx, x, y, col, lw, ls) + % update pre-created line from pool + if nargin < 7, ls = '-'; end + set(lnPool{axi, idx}, 'XData', x, 'YData', y, 'Color', col, ... + 'LineWidth', lw, 'LineStyle', ls, 'Visible', 'on'); + idx = idx + 1; + end + + function hideRest(axi, fromIdx) + for li = fromIdx:POOL + set(lnPool{axi, li}, 'Visible', 'off', 'XData', NaN, 'YData', NaN); + end + end + + function idx = setTxt(axi, idx, x, y, str, col) + set(txPool{axi, idx}, 'Position', [x y 0], 'String', str, 'Color', col, 'Visible', 'on'); + idx = idx + 1; + end + + function hideTxt(axi, fromIdx) + for ti = fromIdx:TPOOL + set(txPool{axi, ti}, 'Visible', 'off', 'String', ''); + end + end + + function barClick(ax, edits, fMax) + if isempty(edits), return; end + cp = get(ax, 'CurrentPoint'); + xClick = max(0, min(fMax, round(cp(1,1)))); + % find nearest edit box by current value + bestDist = inf; bestIdx = 1; + for ei = 1:numel(edits) + ev = str2double(get(edits{ei}, 'String')); + if isnan(ev), ev = 0; end + d = abs(ev - xClick); + if d < bestDist, bestDist = d; bestIdx = ei; end + end + set(edits{bestIdx}, 'String', int2str(xClick)); + autoUpdate(); + end + +end + +%% helpers +function y = magY(H, usedB) + if usedB + y = 20*log10(abs(H) + 1e-12); + else + y = abs(H); end +end +function xl = xlimF(useLog, fMax) + if useLog, xl = [10 fMax]; else xl = [0 fMax]; end end -%% helpers (NOT nested — no closure issues) function y = applyLPF(x, type, hz, Fs) if type == 0 || hz == 0, y = x; return; end types = {'pt1', 'biquad', 'pt2', 'pt3'}; @@ -185,81 +1023,224 @@ function doUpdate() y = filter(b, a, x); end -function fp = parseFilterParams(si) - fp.gyro_lpf1_type = hval(si, 'gyro_lpf1_type', 0); - fp.gyro_lpf1_hz = hval(si, 'gyro_lpf1_static_hz', 0); - fp.gyro_lpf2_type = hval(si, 'gyro_lpf2_type', 0); - fp.gyro_lpf2_hz = hval(si, 'gyro_lpf2_static_hz', 0); - fp.dterm_lpf1_type = hval(si, 'dterm_lpf1_type', 0); - fp.dterm_lpf1_hz = hval(si, 'dterm_lpf1_static_hz', 100); - fp.dterm_lpf2_type = hval(si, 'dterm_lpf2_type', 0); - fp.dterm_lpf2_hz = hval(si, 'dterm_lpf2_static_hz', 0); - fp.dterm_notch_hz = hval(si, 'dterm_notch_hz', 0); - fp.dterm_notch_cut = hval(si, 'dterm_notch_cutoff', 0); - tmp = hstr(si, 'gyro_notch_hz', '0,0'); v = str2num(tmp); - if isempty(v), v = [0 0]; end - fp.gyro_notch1_hz = v(1); - fp.gyro_notch2_hz = 0; if numel(v) > 1, fp.gyro_notch2_hz = v(2); end - tmp = hstr(si, 'gyro_notch_cutoff', '0,0'); v = str2num(tmp); - if isempty(v), v = [0 0]; end - fp.gyro_notch1_cut = v(1); - fp.gyro_notch2_cut = 0; if numel(v) > 1, fp.gyro_notch2_cut = v(2); end +function y = applyNotch_Q(x, center_hz, Q, Fs) + if center_hz == 0, y = x; return; end + [b, a] = PSbfFilters('notch', center_hz, Fs, Q/100); + y = filter(b, a, x); end -function v = hval(si, key, default) - v = default; - for k = 1:size(si, 1) - if strcmp(strtrim(si{k,1}), key) - tmp = str2num(strtrim(si{k,2})); - if ~isempty(tmp), v = tmp(1); end - return; - end +function H = lpfH(type, hz, Fs, Nfft, types) + H = ones(Nfft, 1); + if type == 0 || hz == 0 || type > numel(types), return; end + [b, a] = PSbfFilters(types{type}, hz, Fs); + [H, ~] = freqz(b, a, Nfft, Fs); + H = H(:); +end + +function H = notchH(center_hz, cutoff_hz, Fs, Nfft) + H = ones(Nfft, 1); + if center_hz == 0, return; end + if cutoff_hz <= 0, cutoff_hz = center_hz * 0.7; end + Q = center_hz / (center_hz - cutoff_hz + 1); + [b, a] = PSbfFilters('notch', center_hz, Fs, Q); + [H, ~] = freqz(b, a, Nfft, Fs); + H = H(:); +end + +function H = notchH_Q(center_hz, Q, Fs, Nfft) + H = ones(Nfft, 1); + if center_hz == 0, return; end + [b, a] = PSbfFilters('notch', center_hz, Fs, Q/100); + [H, ~] = freqz(b, a, Nfft, Fs); + H = H(:); +end + +function drawCutoffLines(ax, f1on, f1, f2on, f2, col) + yl = get(ax, 'YLim'); + if f1on && f1 > 0, line(ax, [f1 f1], yl, 'Color', col*0.7, 'LineStyle', '--', 'LineWidth', 0.5, 'HitTest', 'off'); end + if f2on && f2 > 0, line(ax, [f2 f2], yl, 'Color', col, 'LineStyle', '--', 'LineWidth', 0.5, 'HitTest', 'off'); end +end + +function drawNotchLines(ax, rpmBase, rpmNharm, gn1f, gn2f, dnf, gridCol) + yl = get(ax, 'YLim'); + for ri = 1:rpmNharm + fc = rpmBase * ri; + if fc > 0, line(ax, [fc fc], yl, 'Color', gridCol, 'LineStyle', ':', 'LineWidth', 0.5, 'HitTest', 'off'); end end + if gn1f > 0, line(ax, [gn1f gn1f], yl, 'Color', gridCol, 'LineStyle', '--', 'LineWidth', 0.5, 'HitTest', 'off'); end + if gn2f > 0, line(ax, [gn2f gn2f], yl, 'Color', gridCol, 'LineStyle', '--', 'LineWidth', 0.5, 'HitTest', 'off'); end + if dnf > 0, line(ax, [dnf dnf], yl, 'Color', gridCol, 'LineStyle', '--', 'LineWidth', 0.5, 'HitTest', 'off'); end end -function s = hstr(si, key, default) - s = default; - for k = 1:size(si, 1) - if strcmp(strtrim(si{k,1}), key) - s = strtrim(si{k,2}); - return; - end +function annotateLPF(ax, t1, f1, t2, f2, ~, col, fsz, usedB) + names = {'off', 'pt1', 'biquad', 'pt2', 'pt3'}; + if usedB, yLim = [-60 3]; y = -3; yd = -6; + else yLim = [0 1.1]; y = 1.05; yd = -0.08; end + if t1 > 0 && f1 > 0 + text(f1+10, y, sprintf('%s: %dHz', names{t1+1}, f1), 'Parent', ax, ... + 'Color', col*0.7, 'FontSize', fsz-1); + line(ax, [f1 f1], yLim, 'Color', col*0.7, 'LineStyle', '--', 'LineWidth', 0.5); + y = y + yd; + end + if t2 > 0 && f2 > 0 + text(f2+10, y, sprintf('%s: %dHz', names{t2+1}, f2), 'Parent', ax, ... + 'Color', col, 'FontSize', fsz-1); + line(ax, [f2 f2], yLim, 'Color', col, 'LineStyle', '--', 'LineWidth', 0.5); end end -function mkLabel(fig, txt, cpL, row, rh, bgc, fgc) - uicontrol(fig, 'Style', 'text', 'String', txt, ... - 'Units', 'normalized', 'Position', [cpL+.01 row .05 rh], ... - 'FontSize', 11, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'HorizontalAlignment', 'left'); +function annotateNotch(ax, gn1f, gn2f, dnf, rpmBase, rpmNharm, rpmCols, colStaticN, colCombN, colD, fsz, usedB) + if usedB, y0 = -5; yd = -5; yLim = [-40 3]; + else y0 = 0.85; yd = -0.07; yLim = [0 1.1]; end + y = y0; + for ri = 1:rpmNharm + ci = min(ri, numel(rpmCols)); + fc = rpmBase * ri; + text(fc+10, y, sprintf('RPM: %dHz', fc), 'Parent', ax, ... + 'Color', rpmCols{ci}, 'FontSize', fsz-1); + line(ax, [fc fc], yLim, 'Color', rpmCols{ci}, 'LineStyle', '--', 'LineWidth', 0.5); + y = y + yd; + end + if gn1f > 0 + text(gn1f+10, y, sprintf('N1: %dHz', gn1f), 'Parent', ax, ... + 'Color', colStaticN, 'FontSize', fsz-1); + line(ax, [gn1f gn1f], yLim, 'Color', colStaticN, 'LineStyle', '--', 'LineWidth', 0.5); + y = y + yd; + end + if gn2f > 0 + text(gn2f+10, y, sprintf('N2: %dHz', gn2f), 'Parent', ax, ... + 'Color', colStaticN*0.85, 'FontSize', fsz-1); + line(ax, [gn2f gn2f], yLim, 'Color', colStaticN*0.85, 'LineStyle', '--', 'LineWidth', 0.5); + y = y + yd; + end + if dnf > 0 + text(dnf+10, y, sprintf('D: %dHz', dnf), 'Parent', ax, ... + 'Color', colD, 'FontSize', fsz-1); + line(ax, [dnf dnf], yLim, 'Color', colD, 'LineStyle', '--', 'LineWidth', 0.5); + end end -function mkSection(fig, txt, cpL, row, cpW, rh, bgc, col) +% parseFilterParams/hval/hstr moved to src/util/PSparseFilterParams.m + +function mkSection(fig, txt, cpL, row, cpW, rh, bgc, col, fsz) uicontrol(fig, 'Style', 'text', 'String', txt, ... 'Units', 'normalized', 'Position', [cpL+.01 row cpW-.02 rh], ... - 'FontSize', 11, 'FontWeight', 'bold', 'BackgroundColor', bgc, 'ForegroundColor', col); + 'FontSize', fsz, 'FontWeight', 'bold', 'BackgroundColor', bgc, 'ForegroundColor', col); end -function [hType, rowOut] = mkType(fig, cpL, row, rh, gap, bgc, fgc, initVal, cb) - uicontrol(fig, 'Style', 'text', 'String', 'Type:', ... - 'Units', 'normalized', 'Position', [cpL+.01 row .05 rh], ... - 'FontSize', 11, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'HorizontalAlignment', 'left'); +function [hType, hHz, rowOut] = mkTypeHz(fig, x0, row, rh, gap, cW, ibc, ifc, lc, initType, initHz, cb, fsz) + if initHz == 0, ddVal = 1; + else ddVal = min(initType + 2, 5); end + ddW = cW * 0.48; edW = cW * 0.35; lblW = cW * 0.15; + th_ = PStheme(); hType = uicontrol(fig, 'Style', 'popupmenu', ... - 'String', {'OFF', 'PT1', 'Biquad', 'PT2', 'PT3'}, 'Value', initVal + 1, ... - 'Units', 'normalized', 'Position', [cpL+.06 row .10 rh], 'FontSize', 11, 'Callback', cb); + 'String', {'OFF', 'PT1', 'Biquad', 'PT2', 'PT3'}, 'Value', ddVal, ... + 'Units', 'normalized', 'Position', [x0 row ddW rh], 'FontSize', fsz, 'Callback', cb); + hHz = uicontrol(fig, 'Style', 'edit', 'String', num2str(round(initHz)), ... + 'Units', 'normalized', 'Position', [x0+ddW+.005 row edW rh], ... + 'FontSize', fsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, ... + 'HorizontalAlignment', 'center', 'Callback', cb); + uicontrol(fig, 'Style', 'text', 'String', 'Hz', ... + 'Units', 'normalized', 'Position', [x0+ddW+edW+.008 row lblW rh], ... + 'FontSize', fsz-1, 'BackgroundColor', th_.panelBg, ... + 'ForegroundColor', lc, 'HorizontalAlignment', 'left'); rowOut = row - rh - gap; end -function [hSlider, hLbl, rowOut] = mkSlider(fig, label, cpL, row, rh, gap, bgc, fgc, mn, mx, initVal, cb) - initVal = max(mn, min(mx, initVal)); - uicontrol(fig, 'Style', 'text', 'String', label, ... - 'Units', 'normalized', 'Position', [cpL+.01 row .05 rh], ... - 'FontSize', 11, 'BackgroundColor', bgc, 'ForegroundColor', fgc, 'HorizontalAlignment', 'left'); - step1 = 1/max(mx-mn, 1); step10 = 10/max(mx-mn, 1); - hSlider = uicontrol(fig, 'Style', 'slider', 'Min', mn, 'Max', mx, 'Value', initVal, ... - 'Units', 'normalized', 'Position', [cpL+.06 row .13 rh], ... - 'SliderStep', [step1 step10], 'Callback', cb); - hLbl = uicontrol(fig, 'Style', 'text', 'String', num2str(round(initVal)), ... - 'Units', 'normalized', 'Position', [cpL+.20 row .05 rh], ... - 'FontSize', 11, 'BackgroundColor', bgc, 'ForegroundColor', [1 1 .6], 'HorizontalAlignment', 'left'); +function [hCenter, hCutoff, rowOut] = mkNotchPair(fig, x0, row, rh, gap, cW, bgc, ibc, ifc, lc, initCenter, initCutoff, cb, fsz) + halfW = cW / 2; lblW = .03; edW = halfW - lblW - .01; + uicontrol(fig, 'Style', 'text', 'String', 'Ctr:', ... + 'Units', 'normalized', 'Position', [x0 row lblW rh], ... + 'FontSize', fsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); + hCenter = uicontrol(fig, 'Style', 'edit', 'String', num2str(round(initCenter)), ... + 'Units', 'normalized', 'Position', [x0+lblW+.005 row edW rh], ... + 'FontSize', fsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, ... + 'HorizontalAlignment', 'center', 'Callback', cb); + uicontrol(fig, 'Style', 'text', 'String', 'Cut:', ... + 'Units', 'normalized', 'Position', [x0+halfW row lblW rh], ... + 'FontSize', fsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); + hCutoff = uicontrol(fig, 'Style', 'edit', 'String', num2str(round(initCutoff)), ... + 'Units', 'normalized', 'Position', [x0+halfW+lblW+.005 row edW rh], ... + 'FontSize', fsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, ... + 'HorizontalAlignment', 'center', 'Callback', cb); rowOut = row - rh - gap; end + +function v = readEdit(h) + v = round(str2double(get(h, 'String'))); + if isnan(v), v = 0; end +end + +function v = readEditF(h) + v = str2double(get(h, 'String')); + if isnan(v), v = 0; end +end + +function drawGradientBar(ax, fMax, gradCols, markerFreqs, markerCols, thm) + cla(ax); + Ngr = 256; + grad = zeros(1, Ngr, 3); + for ch = 1:3, grad(1,:,ch) = linspace(gradCols(1,ch), gradCols(2,ch), Ngr); end + hi = imagesc(ax, [0 fMax], [0 1], grad); + set(hi, 'HitTest', 'off'); + set(ax, 'YTick', [], 'XTick', [], 'XLim', [0 fMax], 'YLim', [0 1], 'Box', 'on'); + set(ax, 'XColor', thm.axesFg, 'YColor', thm.axesFg); + hold(ax, 'on'); + for k = 1:numel(markerFreqs) + f = markerFreqs{k}; + if f > 0 && f <= fMax + line(ax, [f f], [0 1], 'Color', markerCols{k}, 'LineWidth', 2, 'HitTest', 'off'); + % slider handle: triangle top + bottom + plot(ax, f, 0.95, 'v', 'Color', markerCols{k}, 'MarkerSize', 7, 'MarkerFaceColor', markerCols{k}, 'HitTest', 'off'); + plot(ax, f, 0.05, '^', 'Color', markerCols{k}, 'MarkerSize', 7, 'MarkerFaceColor', markerCols{k}, 'HitTest', 'off'); + end + end + hold(ax, 'off'); +end + +function y = localChirp(t, f0, dur, f1) + % log-sweep chirp + if f0 <= 0, f0 = 1; end + if f1 <= f0, f1 = f0 + 1; end + beta = (f1/f0)^(1/dur); + phase = 2*pi * f0 * (beta.^t - 1) / log(beta); + y = sin(phase); +end + +function ok = copyToClipboard(str) + ok = false; + tmpf = [tempname '.txt']; + fid = fopen(tmpf, 'w'); fprintf(fid, '%s', str); fclose(fid); + if ismac() + [st, ~] = system(sprintf('pbcopy < %s 2>&1', tmpf)); + ok = (st == 0); + elseif ispc() + [st, ~] = system(sprintf('clip < %s 2>&1', tmpf)); + ok = (st == 0); + else + cmds = {'xclip -selection clipboard', 'xsel --clipboard --input', 'wl-copy'}; + for k = 1:numel(cmds) + [st, ~] = system(sprintf('%s < %s 2>&1', cmds{k}, tmpf)); + if st == 0, ok = true; break; end + end + end + delete(tmpf); +end + +function showCLIDialog(cliText) + screensz = get(0, 'ScreenSize'); + dlg = figure('Name', 'BF CLI Commands', 'NumberTitle', 'off', ... + 'Color', [.15 .15 .15], ... + 'Position', round([screensz(3)*.3 screensz(4)*.25 460 380])); + uicontrol(dlg, 'Style', 'text', 'String', 'Select All + Copy:', ... + 'Units', 'normalized', 'Position', [.05 .90 .9 .07], ... + 'FontSize', 11, 'BackgroundColor', [.15 .15 .15], 'ForegroundColor', [.9 .9 .9]); + uicontrol(dlg, 'Style', 'edit', 'Max', 100, 'String', cliText, ... + 'Units', 'normalized', 'Position', [.05 .12 .9 .76], ... + 'HorizontalAlignment', 'left', 'FontName', 'Monospace', 'FontSize', 10, ... + 'BackgroundColor', [.1 .1 .1], 'ForegroundColor', [.9 .9 .9]); + uicontrol(dlg, 'Style', 'pushbutton', 'String', 'Close', ... + 'Units', 'normalized', 'Position', [.35 .02 .3 .08], ... + 'FontSize', 11, 'BackgroundColor', [.3 .3 .3], 'ForegroundColor', [.9 .9 .9], ... + 'Callback', @(~,~) close(dlg)); +end diff --git a/src/plot/PSfreqTime.m b/src/plot/PSfreqTime.m index 13ccb2e..b08c325 100644 --- a/src/plot/PSfreqTime.m +++ b/src/plot/PSfreqTime.m @@ -8,19 +8,11 @@ % ---------------------------------------------------------------------------------- -%% update fonts +th = PStheme(); +%% update fonts set(PSspecfig3, 'pointer', 'watch') figure(PSspecfig3) -PSspecfig3_pos = get(PSspecfig3, 'Position'); -screensz_tmp = get(0,'ScreenSize'); if PSspecfig3_pos(3) > 10, PSspecfig3_pos(3:4) = PSspecfig3_pos(3:4) ./ screensz_tmp(3:4); end -prop_max_screen=(max([PSspecfig3_pos(3) PSspecfig3_pos(4)])); -fontsz=(screensz_multiplier*prop_max_screen); - -f = fields(guiHandlesSpec3); -for i = 1 : size(f,1) - try set(guiHandlesSpec3.(f{i}), 'FontSize', fontsz); catch, end -end specSmoothFactors = [1 5 10 20]; timeSmoothFactors = [1 2 5 10]; @@ -31,37 +23,121 @@ fLim_freqTime = 1000; end -s1={'gyroADC';'debug';'axisD';'axisDpf';'axisP';'piderr';'setpoint';'pidsum'}; +s1={'gyroADC';'gyroPrefilt';'axisD';'axisDpf';'axisP';'piderr';'setpoint';'pidsum'}; datSelectionString=[s1]; axisLabel ={'Roll'; 'Pitch' ; 'Yaw'}; tmpFileVal3 = get(guiHandlesSpec3.FileSelect, 'Value'); tmpSpecVal3 = get(guiHandlesSpec3.SpecList, 'Value'); tmpSmoothVal3 = get(guiHandlesSpec3.smoothFactor_select, 'Value'); tmpSubVal3 = get(guiHandlesSpec3.subsampleFactor_select, 'Value'); + +%% Read RPM overlay controls once (before axis loop) +rpmShowDN = true; rpmMotors = [1 2 3 4]; rpmHarms = [1 2 3]; rpmLw = 1; rpmShowEst = false; +if exist('guiHandlesSpec3','var') + try rpmShowDN = get(guiHandlesSpec3.rpmDynNotch, 'Value'); catch, end + try rpmShowEst = get(guiHandlesSpec3.rpmEstChk, 'Value'); catch, end + try + rpmMotors = []; + nMot_ = 4; try nMot_ = guiHandlesSpec3.nMotors; catch, end + for mi_ = 1:4 + if get(guiHandlesSpec3.(sprintf('rpmMotor%d',mi_)), 'Value') + rpmMotors(end+1) = mi_; + if nMot_ > 4, rpmMotors(end+1) = mi_ + nMot_/2; end + end + end + catch, rpmMotors = [1 2 3 4]; end + try + harmSel = get(guiHandlesSpec3.rpmHarmDd, 'Value'); + harmMap = {[], [1], [2], [3], [1 2], [1 3], [2 3], [1 2 3]}; + rpmHarms = harmMap{harmSel}; + catch, rpmHarms = [1 2 3]; end + try + lwSel = get(guiHandlesSpec3.rpmLwDd, 'Value'); + lwMap = [0.5 1 1.5 2]; + rpmLw = lwMap(lwSel); + catch, rpmLw = 1; end +end + +%% Compute RPM/notch data on-demand (mirrors PSplotSpec2D logic) +if exist('debugmode','var') && exist('debugIdx','var') && exist('T','var') && exist('tIND','var') + if ~exist('notchData','var'), notchData = {}; end + if ~exist('rpmFilterData','var'), rpmFilterData = {}; end + k_ = tmpFileVal3; + if numel(notchData) < k_ || isempty(notchData{k_}) + tmpFFT_ = FFT_FREQ; + if numel(debugIdx) >= k_, tmpFFT_ = debugIdx{k_}.FFT_FREQ; end + if debugmode(k_) == tmpFFT_ + try + if exist('fwMajor','var') && numel(fwMajor) >= k_ && fwMajor(k_) >= 2025 + notchData{k_} = [T{k_}.debug_1_(tIND{k_}), T{k_}.debug_2_(tIND{k_}), T{k_}.debug_3_(tIND{k_})]; + else + notchData{k_} = [T{k_}.debug_0_(tIND{k_}), T{k_}.debug_1_(tIND{k_}), T{k_}.debug_2_(tIND{k_})]; + end + catch, notchData{k_} = []; end + end + end + if numel(rpmFilterData) < k_ || isempty(rpmFilterData{k_}) + tmpRPM_ = 46; + if numel(debugIdx) >= k_, tmpRPM_ = debugIdx{k_}.RPM_FILTER; end + if debugmode(k_) == tmpRPM_ + try rpmFilterData{k_} = [T{k_}.debug_0_(tIND{k_}), T{k_}.debug_1_(tIND{k_}), T{k_}.debug_2_(tIND{k_}), T{k_}.debug_3_(tIND{k_})]; + catch, rpmFilterData{k_} = []; end + end + % eRPM fallback when RPM_FILTER debug mode not active + if (numel(rpmFilterData) < k_ || isempty(rpmFilterData{k_})) && isfield(T{k_}, 'eRPM_0_') + mPoles_ = 14; + try mp_ = find(strcmp(SetupInfo{k_}(:,1), 'motor_poles')); + if ~isempty(mp_), mPoles_ = str2double(SetupInfo{k_}(mp_(1),2)); end + catch, end + if mPoles_ < 2, mPoles_ = 14; end + try + nEm_ = 0; + for mi_ = 0:7 + if isfield(T{k_}, ['eRPM_' int2str(mi_) '_']), nEm_ = mi_+1; end + end + rpmHz_ = zeros(sum(tIND{k_}), nEm_); + for mi_ = 0:nEm_-1 + ef_ = ['eRPM_' int2str(mi_) '_']; + if isfield(T{k_}, ef_) + rpmHz_(:, mi_+1) = T{k_}.(ef_)(tIND{k_}) * 100 / (mPoles_/2) / 60; + end + end + rpmFilterData{k_} = rpmHz_; + catch, rpmFilterData{k_} = []; end + end + end +end + +try delete(findobj(PSspecfig3, 'Tag', 'PScbar')); catch, end for i = 1 : 3 - delete(subplot('position',posInfo.Spec3Pos(i,:))); + stag_ = sprintf('PSfreqTime_%d', i); + h_old = findobj(PSspecfig3, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_old), delete(h_old); end try if ~updateSpec - eval(['dat = T{tmpFileVal3}.' char(datSelectionString(tmpSpecVal3)) '_' int2str(i-1) '_(tIND{tmpFileVal3})'';';]) + fld = [char(datSelectionString(tmpSpecVal3)) '_' int2str(i-1) '_']; + dat = T{tmpFileVal3}.(fld)(tIND{tmpFileVal3})'; [Tm F specMat{i}] = PStimeFreqCalc(dat', A_lograte(tmpFileVal3), specSmoothFactors(tmpSmoothVal3), timeSmoothFactors(tmpSubVal3)); end - - h2=subplot('position',posInfo.Spec3Pos(i,:)); + + h2 = axes('Parent', PSspecfig3, 'Position', posInfo.Spec3Pos(i,:), 'Tag', stag_); h = imagesc(specMat{i}); set(gca,'Clim',[ClimScale3], 'fontsize',fontsz,'fontweight','bold') title(''); - set(get(gca,'Ylabel'), 'String', ['Frequency (Hz) ' axisLabel{i}]); - set(get(gca,'Xlabel'), 'String', 'Time (sec)'); + set(get(gca,'Ylabel'), 'String', ['Frequency (Hz) ' axisLabel{i}], 'Color', th.textPrimary); + if i == 3 + set(get(gca,'Xlabel'), 'String', 'Time (sec)', 'Color', th.textPrimary); + end F2 = F(F<=fLim_freqTime); freqStr = flipud(int2str((0: F2(end) / 5: F2(end))')); timeStr = int2str((0: round(Tm(end)) / 10: round(Tm(end)))'); - + st = find(fliplr(F<=fLim_freqTime),1, 'first'); nd = find(fliplr(F<=fLim_freqTime),1, 'last'); set(gca,'YLim', [st nd]) set(gca,'Ytick', [st : (nd-st) / 5: nd], 'YTickLabel',[freqStr], 'YMinorTick', 'on', 'Xtick', [0 : round( (size(specMat{i},2)-1) / 10) : size(specMat{i},2)],'XTickLabel',[timeStr], 'XMinorTick', 'on', 'TickDir', 'out'); - + try tmpCmapVal = get(guiHandlesSpec3.ColormapSelect, 'Value'); if tmpCmapVal <= 7 @@ -75,50 +151,86 @@ set(PSspecfig3, 'Colormap', cm); catch, end cbar = colorbar('EastOutside'); - set(get(cbar, 'Label'), 'String', 'Power Spectral density (dB)'); - + set(cbar, 'Tag', 'PScbar', 'UserData', 'east'); + set(get(cbar, 'Label'), 'String', 'Power Spectral density (dB)', 'Color', th.textPrimary); + try set(cbar, 'Color', th.axesFg); catch, end + if i == 3 && (strcmp(char(datSelectionString(get(guiHandlesSpec3.SpecList, 'Value'))), 'axisD') || strcmp(char(datSelectionString(get(guiHandlesSpec3.SpecList, 'Value'))), 'axisDpf')) - delete(subplot('position',posInfo.Spec3Pos(i,:))); + h_del = findobj(PSspecfig3, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_del), delete(h_del); end end box off %% Dynamic notch overlay for FFT_FREQ mode - if exist('notchData','var') && exist('debugmode','var') && exist('debugIdx','var') + if rpmShowDN && exist('notchData','var') && exist('debugmode','var') && exist('debugIdx','var') tmpFFTft = FFT_FREQ; if numel(debugIdx) >= tmpFileVal3 tmpFFTft = debugIdx{tmpFileVal3}.FFT_FREQ; end if debugmode(tmpFileVal3) == tmpFFTft && numel(notchData) >= tmpFileVal3 && ~isempty(notchData{tmpFileVal3}) - % Only overlay on the axis matching gyro_debug_axis - tmpGdaFt = 0; - if exist('gyro_debug_axis','var') && numel(gyro_debug_axis) >= tmpFileVal3 - tmpGdaFt = gyro_debug_axis(tmpFileVal3); - end - if (i - 1) == tmpGdaFt - PSplotDynNotchOverlay(gca, notchData{tmpFileVal3}, size(specMat{i}, 2), size(specMat{i}, 1), F(end), 'time'); - end + PSplotDynNotchOverlay(gca, notchData{tmpFileVal3}, size(specMat{i}, 2), size(specMat{i}, 1), F(end), 'time', rpmLw); end end %% RPM filter overlay (motor frequencies + harmonics) - if exist('rpmFilterData','var') && exist('debugmode','var') && exist('debugIdx','var') - tmpRPMft = 46; - if numel(debugIdx) >= tmpFileVal3 - tmpRPMft = debugIdx{tmpFileVal3}.RPM_FILTER; + if ~isempty(rpmHarms) && ~isempty(rpmMotors) && exist('rpmFilterData','var') + if numel(rpmFilterData) >= tmpFileVal3 && ~isempty(rpmFilterData{tmpFileVal3}) + PSplotRPMOverlay(gca, rpmFilterData{tmpFileVal3}, size(specMat{i}, 2), size(specMat{i}, 1), F(end), 'time', 3, rpmMotors, rpmHarms, rpmLw); + end + end + + %% RPM estimator overlay (works on any log — estimates from spectrum peaks) + if rpmShowEst && ~isempty(rpmHarms) && ~isempty(specMat{i}) + ampForEst = flipud(specMat{i})'; + [~, estHarm] = PSestimateRPM(F, ampForEst, 3); + % smooth estimated harmonics (moving average, NaN-safe) + smK = 7; + for sc = 1:size(estHarm, 2) + col = estHarm(:, sc); + sm = col; + for sw = 1:numel(col) + lo = max(1, sw - floor(smK/2)); + hi = min(numel(col), sw + floor(smK/2)); + chunk = col(lo:hi); + chunk = chunk(~isnan(chunk) & chunk > 0); + if numel(chunk) >= 2, sm(sw) = mean(chunk); else sm(sw) = NaN; end + end + estHarm(:, sc) = sm; end - if debugmode(tmpFileVal3) == tmpRPMft && numel(rpmFilterData) >= tmpFileVal3 && ~isempty(rpmFilterData{tmpFileVal3}) - PSplotRPMOverlay(gca, rpmFilterData{tmpFileVal3}, size(specMat{i}, 2), size(specMat{i}, 1), F(end), 'time'); + hz_per_px = F(end) / size(specMat{i}, 1); + hold on; + estCol = [0 .9 .2; .9 .7 0; .9 .2 0]; + estStyle = {'-'; '--'; ':'}; + numWin = size(specMat{i}, 2); + for nh = rpmHarms + if nh > size(estHarm, 2), continue; end + xPts = []; yPts = []; + for w = 1:numWin + if ~isnan(estHarm(w, nh)) && estHarm(w, nh) > 0 && estHarm(w, nh) < F(end) + y_px = size(specMat{i}, 1) - round(estHarm(w, nh) / hz_per_px); + if y_px >= 1 && y_px <= size(specMat{i}, 1) + xPts(end+1) = w; + yPts(end+1) = y_px; + end + end + end + if ~isempty(xPts) + plot(gca, xPts, yPts, estStyle{nh}, 'LineWidth', rpmLw, 'Color', estCol(nh,:), 'HitTest', 'off'); + end end end catch - delete(subplot('position',posInfo.Spec3Pos(i,:))); + h_del = findobj(PSspecfig3, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_del), delete(h_del); end end end updateSpec = 0; -% Set up click-to-show-value datatips + double-click expand on all axes +allax = findobj(PSspecfig3, 'Type', 'axes'); +for axi = 1:numel(allax), PSstyleAxes(allax(axi), th); end PSdatatipSetup(PSspecfig3); +try PSresizeCP(PSspecfig3, []); catch, end set(PSspecfig3, 'pointer', 'arrow') diff --git a/src/plot/PSplotBode.m b/src/plot/PSplotBode.m index 3932c21..cde6543 100644 --- a/src/plot/PSplotBode.m +++ b/src/plot/PSplotBode.m @@ -7,10 +7,16 @@ function PSplotBode(freq, G_track, G_plant, C, stepData, titleStr) % stepData - struct with .t_ms and .step (or [] to skip) % titleStr - plot title suffix +th = PStheme(); +fontsz = th.fontsz; screensz = get(0, 'ScreenSize'); -fig = figure('Name', ['Chirp Analysis - ' titleStr], 'NumberTitle', 'off', ... - 'Color', [.15 .15 .15], ... - 'Position', round([.08*screensz(3) .06*screensz(4) .78*screensz(3) .82*screensz(4)])); +figName = ['Chirp Analysis - ' titleStr]; +fig = findobj('Type', 'figure', 'Name', figName); +if ~isempty(fig), close(fig); end +fig = figure('Name', figName, 'NumberTitle', 'off', ... + 'Color', th.figBg, ... + 'Position', round([0 0 screensz(3) screensz(4)])); +try set(fig, 'WindowState', 'maximized'); catch, end freq = freq(:); fPlot = freq(freq > 0); % skip DC for log plot @@ -18,44 +24,44 @@ function PSplotBode(freq, G_track, G_plant, C, stepData, titleStr) % --- magnitude --- ax1 = axes('Parent', fig, 'Units', 'normalized', 'Position', [.08 .72 .55 .22]); mag_T = 20*log10(abs(G_track(freq > 0))); -semilogx(ax1, fPlot, mag_T, 'Color', [0 .8 1], 'LineWidth', 1.5); +semilogx(ax1, fPlot, mag_T, 'Color', th.bodeMain, 'LineWidth', 1.5); hold(ax1, 'on'); if ~isempty(G_plant) mag_P = 20*log10(abs(G_plant(freq > 0))); - semilogx(ax1, fPlot, mag_P, 'Color', [1 .5 0], 'LineWidth', 1.2); - legend(ax1, {'Tracking (T)', 'Plant (P)'}, 'TextColor', [.8 .8 .8], ... - 'Color', [.2 .2 .2], 'EdgeColor', [.4 .4 .4], 'FontSize', 11, 'Location', 'southwest'); + semilogx(ax1, fPlot, mag_P, 'Color', th.bodeSecondary, 'LineWidth', 1.2); + h_leg = legend(ax1, {'Tracking (T)', 'Plant (P)'}, 'Location', 'southwest'); + try PSstyleLegend(h_leg, th); catch, end end -line(ax1, [fPlot(1) fPlot(end)], [0 0], 'Color', [.5 .5 .5], 'LineStyle', '--'); +line(ax1, [fPlot(1) fPlot(end)], [0 0], 'Color', th.bodeRef, 'LineStyle', '--'); hold(ax1, 'off'); -styleDark(ax1); -set(get(ax1, 'YLabel'), 'String', 'Magnitude (dB)', 'Color', [.8 .8 .8]); -th1 = title(ax1, ['Bode - ' titleStr]); set(th1, 'Color', [.9 .9 .9]); +PSstyleAxes(ax1, th); +set(get(ax1, 'YLabel'), 'String', 'Magnitude (dB)'); +th1 = title(ax1, ['Bode - ' titleStr]); % --- phase --- ax2 = axes('Parent', fig, 'Units', 'normalized', 'Position', [.08 .42 .55 .22]); phase_T = unwrap(angle(G_track(freq > 0))) * 180/pi; -semilogx(ax2, fPlot, phase_T, 'Color', [0 .8 1], 'LineWidth', 1.5); +semilogx(ax2, fPlot, phase_T, 'Color', th.bodeMain, 'LineWidth', 1.5); hold(ax2, 'on'); if ~isempty(G_plant) phase_P = unwrap(angle(G_plant(freq > 0))) * 180/pi; - semilogx(ax2, fPlot, phase_P, 'Color', [1 .5 0], 'LineWidth', 1.2); + semilogx(ax2, fPlot, phase_P, 'Color', th.bodeSecondary, 'LineWidth', 1.2); end -line(ax2, [fPlot(1) fPlot(end)], [-180 -180], 'Color', [.8 .3 .3], 'LineStyle', '--'); +line(ax2, [fPlot(1) fPlot(end)], [-180 -180], 'Color', th.btnDash1, 'LineStyle', '--'); hold(ax2, 'off'); -styleDark(ax2); -set(get(ax2, 'YLabel'), 'String', 'Phase (deg)', 'Color', [.8 .8 .8]); +PSstyleAxes(ax2, th); +set(get(ax2, 'YLabel'), 'String', 'Phase (deg)'); % --- coherence --- ax3 = axes('Parent', fig, 'Units', 'normalized', 'Position', [.08 .08 .55 .26]); -semilogx(ax3, fPlot, C(freq > 0), 'Color', [.3 .9 .3], 'LineWidth', 1.2); +semilogx(ax3, fPlot, C(freq > 0), 'Color', th.bodeCoherence, 'LineWidth', 1.2); hold(ax3, 'on'); -line(ax3, [fPlot(1) fPlot(end)], [.8 .8], 'Color', [.5 .5 .5], 'LineStyle', '--'); +line(ax3, [fPlot(1) fPlot(end)], [.8 .8], 'Color', th.bodeRef, 'LineStyle', '--'); hold(ax3, 'off'); -styleDark(ax3); +PSstyleAxes(ax3, th); set(ax3, 'YLim', [0 1.05]); -set(get(ax3, 'XLabel'), 'String', 'Frequency (Hz)', 'Color', [.8 .8 .8]); -set(get(ax3, 'YLabel'), 'String', 'Coherence', 'Color', [.8 .8 .8]); +set(get(ax3, 'XLabel'), 'String', 'Frequency (Hz)'); +set(get(ax3, 'YLabel'), 'String', 'Coherence'); linkaxes([ax1 ax2 ax3], 'x'); if ~isempty(fPlot) @@ -65,9 +71,9 @@ function PSplotBode(freq, G_track, G_plant, C, stepData, titleStr) % --- step response (right side) --- ax4 = axes('Parent', fig, 'Units', 'normalized', 'Position', [.72 .42 .24 .52]); if ~isempty(stepData) && isfield(stepData, 't_ms') && isfield(stepData, 'step') - plot(ax4, stepData.t_ms, stepData.step, 'Color', [0 .8 1], 'LineWidth', 1.8); + plot(ax4, stepData.t_ms, stepData.step, 'Color', th.bodeMain, 'LineWidth', 1.8); hold(ax4, 'on'); - line(ax4, [0 max(stepData.t_ms)], [1 1], 'Color', [.5 .5 .5], 'LineStyle', '--'); + line(ax4, [0 max(stepData.t_ms)], [1 1], 'Color', th.bodeRef, 'LineStyle', '--'); % overshoot peak = max(stepData.step); if peak > 1.01 @@ -75,16 +81,14 @@ function PSplotBode(freq, G_track, G_plant, C, stepData, titleStr) [~, pk_idx] = max(stepData.step); plot(ax4, stepData.t_ms(pk_idx), peak, 'ro', 'MarkerSize', 8, 'LineWidth', 2); text(stepData.t_ms(pk_idx)+5, peak, sprintf('%.0f%%', os_pct), ... - 'Color', [1 .3 .3], 'FontSize', 12, 'FontWeight', 'bold', 'Parent', ax4); + 'Color', th.btnDash1, 'FontSize', fontsz, 'FontWeight', 'bold', 'Parent', ax4); end hold(ax4, 'off'); end -set(ax4, 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 12, 'FontWeight', 'bold'); -grid(ax4, 'on'); set(ax4, 'GridColor', [.3 .3 .3]); -set(get(ax4, 'XLabel'), 'String', 'Time (ms)', 'Color', [.8 .8 .8]); -set(get(ax4, 'YLabel'), 'String', 'Step Response', 'Color', [.8 .8 .8]); -th4 = title(ax4, 'Step (from FRD)'); set(th4, 'Color', [.9 .9 .9]); +PSstyleAxes(ax4, th); +set(get(ax4, 'XLabel'), 'String', 'Time (ms)'); +set(get(ax4, 'YLabel'), 'String', 'Step Response'); +title(ax4, 'Step (from FRD)'); % --- info panel (bottom right) --- ax5 = axes('Parent', fig, 'Units', 'normalized', 'Position', [.72 .08 .24 .26]); @@ -105,8 +109,8 @@ function PSplotBode(freq, G_track, G_plant, C, stepData, titleStr) infoLines{end+1} = sprintf('Settling (2%%): %.0f ms', stepData.t_ms(settled)); end end -text(0.05, 0.9, infoLines, 'Parent', ax5, 'Color', [.9 .9 .3], ... - 'FontSize', 12, 'FontWeight', 'bold', 'VerticalAlignment', 'top', ... +text(0.05, 0.9, infoLines, 'Parent', ax5, 'Color', th.bodeMain, ... + 'FontSize', fontsz, 'FontWeight', 'bold', 'VerticalAlignment', 'top', ... 'Units', 'normalized'); PSdatatipSetup(fig); @@ -114,12 +118,6 @@ function PSplotBode(freq, G_track, G_plant, C, stepData, titleStr) end -function styleDark(ax) - set(ax, 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 12, 'FontWeight', 'bold'); - grid(ax, 'on'); set(ax, 'GridColor', [.3 .3 .3]); -end - function [gm_dB, pm_deg, wcg, wcp] = margins_from_G(freq, G) % gain margin: gain at -180 deg phase crossing diff --git a/src/plot/PSplotDynNotchOverlay.m b/src/plot/PSplotDynNotchOverlay.m index ea338d7..35f8387 100644 --- a/src/plot/PSplotDynNotchOverlay.m +++ b/src/plot/PSplotDynNotchOverlay.m @@ -1,78 +1,96 @@ -function PSplotDynNotchOverlay(ax, notchMat, xData, imgHeight, freqMax, mode) -%% PSplotDynNotchOverlay - overlay dynamic notch frequencies on spectrogram -% ax - axes handle -% notchMat - Nx3 matrix [notch1_Hz, notch2_Hz, notch3_Hz] (same length as xData) -% xData - Nx1 throttle (%) or time index for each sample -% imgHeight - number of pixel rows in image (size(img,1)) -% freqMax - max frequency in Hz (Nyquist) -% mode - 'throttle' or 'time' - -if isempty(notchMat) || imgHeight < 2 || freqMax <= 0 - return; -end - -hold(ax, 'on'); -colors = [0 1 1; 1 1 1; 1 0 1]; % cyan, white, magenta -numNotches = min(3, size(notchMat, 2)); -hz_per_pixel = freqMax / imgHeight; - -if strcmp(mode, 'throttle') - % Average notch freq per throttle bin (1-100, same as PSthrSpec) - for n = 1:numNotches - xPts = []; - yPts = []; - for bin = 1:100 - inBin = abs(xData - bin) <= 1; - if any(inBin) - vals = notchMat(inBin, n); - vals = vals(vals > 0 & vals < freqMax); - if ~isempty(vals) - avgHz = nanmean(vals); - y_px = imgHeight - round(avgHz / hz_per_pixel); - if y_px >= 1 && y_px <= imgHeight - xPts(end+1) = bin; - yPts(end+1) = y_px; - end - end - end - end - if ~isempty(xPts) - h = plot(ax, xPts, yPts, '.', 'MarkerSize', 4); - set(h, 'Color', colors(n,:), 'HitTest', 'off'); - end - end - -elseif strcmp(mode, 'time') - % xData = number of time windows in spectrogram - % notchMat has full-rate samples, need to resample to numWindows - numWindows = xData; - numSamples = size(notchMat, 1); - if numSamples == 0 || numWindows == 0, return; end - - samplesPerWindow = max(1, round(numSamples / numWindows)); - - for n = 1:numNotches - xPts = []; - yPts = []; - for w = 1:numWindows - lo = max(1, round((w-1) * samplesPerWindow) + 1); - hi = min(numSamples, round(w * samplesPerWindow)); - vals = notchMat(lo:hi, n); - vals = vals(vals > 0 & vals < freqMax); - if ~isempty(vals) - avgHz = nanmean(vals); - y_px = imgHeight - round(avgHz / hz_per_pixel); - if y_px >= 1 && y_px <= imgHeight - xPts(end+1) = w; - yPts(end+1) = y_px; - end - end - end - if ~isempty(xPts) - h = plot(ax, xPts, yPts, '.', 'MarkerSize', 3); - set(h, 'Color', colors(n,:), 'HitTest', 'off'); - end - end -end - -end +function PSplotDynNotchOverlay(ax, notchMat, xData, imgHeight, freqMax, mode, lw) +%% PSplotDynNotchOverlay - overlay dynamic notch frequencies on spectrogram +% ax - axes handle +% notchMat - Nx3 matrix [notch1_Hz, notch2_Hz, notch3_Hz] (same length as xData) +% xData - Nx1 throttle (%) or time index for each sample +% imgHeight - number of pixel rows in image (size(img,1)) +% freqMax - max frequency in Hz (Nyquist) +% mode - 'throttle' or 'time' +% lw - line width (default 1.5) + +if nargin < 7 || isempty(lw), lw = 1.5; end +if isempty(notchMat) || imgHeight < 2 || freqMax <= 0 + return; +end + +hold(ax, 'on'); +colors = [0 1 1; 1 1 1; 1 0 1]; % cyan, white, magenta +numNotches = min(3, size(notchMat, 2)); +hz_per_pixel = freqMax / imgHeight; + +if strcmp(mode, 'throttle') + % Average notch freq per throttle bin (1-100, same as PSthrSpec) + for n = 1:numNotches + xPts = []; + yPts = []; + for bin = 1:100 + inBin = abs(xData - bin) <= 1; + if any(inBin) + vals = notchMat(inBin, n); + vals = vals(vals > 0 & vals < freqMax); + if ~isempty(vals) + avgHz = nanmean(vals); + y_px = imgHeight - round(avgHz / hz_per_pixel); + if y_px >= 1 && y_px <= imgHeight + xPts(end+1) = bin; + yPts(end+1) = y_px; + end + end + end + end + if numel(yPts) >= 5 + try + yPts = round(smooth(yPts(:), 5))'; + catch + kernel = ones(5,1)/5; + yPts = round(conv(yPts(:), kernel, 'same'))'; + end + end + if ~isempty(xPts) + h = plot(ax, xPts, yPts, '-', 'LineWidth', lw); + set(h, 'Color', colors(n,:), 'HitTest', 'off'); + end + end + +elseif strcmp(mode, 'time') + % xData = number of time windows in spectrogram + % notchMat has full-rate samples, need to resample to numWindows + numWindows = xData; + numSamples = size(notchMat, 1); + if numSamples == 0 || numWindows == 0, return; end + + samplesPerWindow = max(1, round(numSamples / numWindows)); + + for n = 1:numNotches + xPts = []; + yPts = []; + for w = 1:numWindows + lo = max(1, round((w-1) * samplesPerWindow) + 1); + hi = min(numSamples, round(w * samplesPerWindow)); + vals = notchMat(lo:hi, n); + vals = vals(vals > 0 & vals < freqMax); + if ~isempty(vals) + avgHz = nanmean(vals); + y_px = imgHeight - round(avgHz / hz_per_pixel); + if y_px >= 1 && y_px <= imgHeight + xPts(end+1) = w; + yPts(end+1) = y_px; + end + end + end + if numel(yPts) >= 5 + try + yPts = round(smooth(yPts(:), 5))'; + catch + kernel = ones(5,1)/5; + yPts = round(conv(yPts(:), kernel, 'same'))'; + end + end + if ~isempty(xPts) + h = plot(ax, xPts, yPts, '-', 'LineWidth', lw); + set(h, 'Color', colors(n,:), 'HitTest', 'off'); + end + end +end + +end diff --git a/src/plot/PSplotLogViewer.m b/src/plot/PSplotLogViewer.m index c0f7333..ef269ff 100644 --- a/src/plot/PSplotLogViewer.m +++ b/src/plot/PSplotLogViewer.m @@ -1,295 +1,430 @@ -%% PSplotLogViewer - script to plot main line graphs - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - - -if exist('fnameMaster','var') && ~isempty(fnameMaster) - - set(PSfig, 'pointer', 'watch') - - global logviewerYscale - logviewerYscale = str2num(get(guiHandles.maxY_input, 'String')); - - figure(PSfig); - - maxY=str2num(get(guiHandles.maxY_input, 'String')); - - alpha_red=.8; - alpha_blue=.8; - - % scale fonts according to size of window and/or screen - PSfig_pos = get(PSfig, 'Position'); - screensz_tmp = get(0,'ScreenSize'); if PSfig_pos(3) > 10, PSfig_pos(3:4) = PSfig_pos(3:4) ./ screensz_tmp(3:4); end - prop_max_screen=(max([PSfig_pos(3) PSfig_pos(4)])); - fontsz=(screensz_multiplier*prop_max_screen); - - f = fields(guiHandles); - for i = 1 : size(f,1) - try set(guiHandles.(f{i}), 'FontSize', fontsz); catch, end - end - set(controlpanel, 'FontSize', fontsz); - - lineSmoothFactors = [1 10 20 40 80]; - - if plotall_flag>=0 - allVal = get(guiHandles.checkbox15, 'Value'); - set(guiHandles.checkbox0, 'Value', allVal); - set(guiHandles.checkbox1, 'Value', allVal); - set(guiHandles.checkbox2, 'Value', allVal); - set(guiHandles.checkbox3, 'Value', allVal); - set(guiHandles.checkbox4, 'Value', allVal); - set(guiHandles.checkbox5, 'Value', allVal); - set(guiHandles.checkbox6, 'Value', allVal); - set(guiHandles.checkbox7, 'Value', allVal); - set(guiHandles.checkbox8, 'Value', allVal); - set(guiHandles.checkbox9, 'Value', allVal); - set(guiHandles.checkbox10, 'Value', allVal); - set(guiHandles.checkbox11, 'Value', allVal); - set(guiHandles.checkbox12, 'Value', allVal); - set(guiHandles.checkbox13, 'Value', allVal); - set(guiHandles.checkbox14, 'Value', allVal); - end - plotall_flag=-1; - - expand_sz=[0.05 0.06 0.815 0.835]; - - - %% where you want full range of data - fileIdx = get(guiHandles.FileNum, 'Value'); - - % Update Debug checkbox label for RC_INTERPOLATION mode (version-aware) - tmpRCidx = RC_INTERPOLATION; % global default - if exist('debugIdx','var') && numel(debugIdx) >= fileIdx - tmpRCidx = debugIdx{fileIdx}.RC_INTERPOLATION; - end - if exist('debugmode','var') && numel(debugmode) >= fileIdx && debugmode(fileIdx) == tmpRCidx - set(guiHandles.checkbox0, 'String', 'SP (raw)'); - else - set(guiHandles.checkbox0, 'String', 'Debug'); - end - - % if start or end > length of file, or start > end - if (epoch1_A(fileIdx) > (tta{fileIdx}(end) / us2sec)) || (epoch2_A(fileIdx) > (tta{fileIdx}(end) / us2sec)) || (epoch1_A(fileIdx) > epoch2_A(fileIdx)) - epoch1_A(fileIdx) = 2; - epoch2_A(fileIdx) = floor(tta{fileIdx}(end) / us2sec) - 1; - end - - y=[epoch1_A(fileIdx)*us2sec epoch2_A(fileIdx)*us2sec];%%% used for fill in unused data range - t1=(tta{fileIdx}(find(tta{fileIdx}>y(1),1))) / us2sec; - t2=(tta{fileIdx}(find(tta{fileIdx}>y(2),1))) / us2sec; - - tIND{fileIdx} = (tta{fileIdx} > (t1*us2sec)) & (tta{fileIdx} < (t2*us2sec)); - -% jRangeSlider = com.jidesoft.swing.RangeSlider(0,200,10,190); % min,max,low,high -% jRangeSlider = javacomponent(jRangeSlider,[50, 80, 500, 80], gcf); -% set(jRangeSlider, 'MajorTickSpacing',50, 'PaintTicks',true, 'PaintLabels',true, 'Background',java.awt.Color.white) -% jRangeSlider.LowValue = 20;, jRangeSlider.HighValue = 180; - - guiHandles.slider = uicontrol(PSfig, 'style','slider','SliderStep',[0.001 0.01],'Visible', 'on', 'units','normalized','position',[0.0826 0.905 0.787 0.02],... - 'min',0,'max',1, 'callback','PSslider1Actions;'); - - - - %% log viewer line plots - %%%%%%%% PLOT %%%%%%% - axLabel={'Roll';'Pitch';'Yaw'}; - lineStyleLV = {'-'; '-'; '-'}; - lineStyle2LV = {'-'; '--'; ':'}; - lineStyle2LVnames = {'solid' ; 'dashed' ; 'dotted'}; - axesOptionsLV = find([get(guiHandles.plotR, 'Value') get(guiHandles.plotP, 'Value') get(guiHandles.plotY, 'Value')]); - - try delete(hexpand1); catch, end - try delete(hexpand2); catch, end - try delete(hexpand3); catch, end - expandON = 0; - - ylabelname=''; - for i = 1 : size(axesOptionsLV,2) - if i == size(axesOptionsLV,2) - ylabelname = [ylabelname axLabel{axesOptionsLV(i)} '-' lineStyle2LVnames{i} ' (deg/s) ']; - else - ylabelname = [ylabelname axLabel{axesOptionsLV(i)} '-' lineStyle2LVnames{i} ' | ']; - end - end - - PSfig; - - try zoomOn = strcmp(get(zoom(PSfig), 'Enable'),'on'); catch, zoomOn = 0; end - if ~zoomOn && ~expandON % - delete(subplot('position' ,fullszPlot)); - delete(subplot('position',posInfo.linepos1)); - delete(subplot('position',posInfo.linepos2)); - delete(subplot('position',posInfo.linepos3)); - delete(subplot('position',posInfo.linepos4)); - end - - for i = 1 : 19 - try - eval(['delete([hch' int2str(i) '])']) - catch - end - end - - - try % datacursormode not available in Octave - dcm_obj = datacursormode(PSfig); - set(dcm_obj,'UpdateFcn',@PSdatatip); - end - - cntLV = 0; - lnstyle = lineStyleLV; - - if exist('fnameMaster','var') && ~isempty(fnameMaster) - for ii = axesOptionsLV - if get(guiHandles.RPYcomboLV, 'Value'), expandON = 0; end - %%%%%%% - if ~get(guiHandles.RPYcomboLV, 'Value') && ~expandON - eval(['LVpanel' int2str(ii) '=subplot(' '''position''' ',posInfo.linepos' int2str(ii) ');']) - LVpanel5 = subplot('position',posInfo.linepos4); - end - if ~get(guiHandles.RPYcomboLV, 'Value') && expandON - try - eval(['subplot(hexpand' int2str(ii) ',' '''position''' ',expand_sz);']) - warning off - catch - end - end - - if eval(['~isempty(hexpand' int2str(ii) ') && ishandle(hexpand' int2str(ii) ') || ~expandON']) - - cntLV = cntLV + 1; - if get(guiHandles.RPYcomboLV, 'Value') - LVpanel4 = subplot('position' ,fullszPlot) - lnstyle = lineStyle2LV; - end - if ~get(guiHandles.RPYcomboLV, 'Value') && expandON == 0 - eval(['LVpanel' int2str(ii) '= subplot(' '''position''' ',posInfo.linepos' int2str(ii) ');']) - lnstyle = lineStyleLV; - end - - xmax=max(tta{fileIdx}/us2sec); - - - h=plot([0 xmax],[-maxY -maxY],'k'); - set(h,'linewidth',.2) - hold on - - set(gca,'ytick',[ -(maxY/2) 0 maxY/2 ],'yticklabel',{num2str(-(maxY/2)) '0' num2str((maxY/2)) ''},'YColor',[.2 .2 .2],'fontweight','bold') - set(gca,'xtick',[round(xmax/10):round(xmax/10):round(xmax)],'XColor',[.2 .2 .2]) - - sFactor = lineSmoothFactors(get(guiHandles.lineSmooth, 'Value')); - fileIdx = get(guiHandles.FileNum, 'Value'); - lwVal = get(guiHandles.linewidth, 'Value')/2; - - if get(guiHandles.checkbox0, 'Value'), hch1=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['debug_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch1,'color', [linec.col0],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox1, 'Value'), hch2=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['gyroADC_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch2,'color', [linec.col1],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox2, 'Value'), hch3=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['axisP_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch3,'color', [linec.col2],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox3, 'Value'), hch4=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['axisI_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch4,'color', [linec.col3],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox4, 'Value') && ii<3, hch5=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['axisDpf_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch5,'color', [linec.col4],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox5, 'Value') && ii<3, hch6=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['axisD_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch6,'color', [linec.col5],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox6, 'Value'), hch7=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['axisF_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch7,'color', [linec.col6],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox7, 'Value'), hch8=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['setpoint_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch8,'color', [linec.col7],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox8, 'Value'), hch9=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['pidsum_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch9,'color', [linec.col8],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - if get(guiHandles.checkbox9, 'Value'), hch10=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.(['piderr_' int2str(ii-1) '_']), sFactor, 'loess'));hold on;set(hch10,'color', [linec.col9],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), end - - - h=fill([0,t1,t1,0],[-maxY,-maxY,maxY,maxY],[.8 .8 .8]); - set(h,'FaceAlpha',0.8,'EdgeColor',[.8 .8 .8]); - h=fill([t2,xmax,xmax,t2],[-maxY,-maxY,maxY,maxY],[.8 .8 .8]); - set(h,'FaceAlpha',0.8,'EdgeColor',[.8 .8 .8]); - - try zoomOn2 = strcmp(get(zoom(PSfig), 'Enable'),'on'); catch, zoomOn2 = 0; end - if zoomOn2 - v = axis; - axis(v) - else - axis([0 xmax -maxY maxY]) - end - - box off - if get(guiHandles.RPYcomboLV, 'Value') - y=ylabel([ylabelname],'fontweight','bold','rotation', 90); - else - y=ylabel([axLabel{ii} ' (deg/s)'],'fontweight','bold','rotation', 90); - end - - - set(y,'Units','normalized', 'position', [-.035 .5 1],'color',[.2 .2 .2]); - y=xlabel('Time (s)','fontweight','bold'); - set(y,'color',[.2 .2 .2]); - set(gca,'fontsize',fontsz,'XMinorGrid','on') - grid on - - % Percent variables - LVpanel5 = subplot('position',posInfo.linepos4); - if get(guiHandles.checkbox10, 'Value'), try hch11=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_0_, sFactor, 'loess'));hold on;set(hch11,'color', [linec.col10],'LineWidth',lwVal), catch, end, end - if get(guiHandles.checkbox11, 'Value'), try hch12=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_1_, sFactor, 'loess'));hold on;set(hch12,'color', [linec.col11],'LineWidth',lwVal), catch, end, end - if get(guiHandles.checkbox12, 'Value'), try hch13=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_2_, sFactor, 'loess'));hold on;set(hch13,'color', [linec.col12],'LineWidth',lwVal), catch, end, end - if get(guiHandles.checkbox13, 'Value'), try hch14=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_3_, sFactor, 'loess'));hold on;set(hch14,'color', [linec.col13],'LineWidth',lwVal), catch, end, end - % motor sigs 4-7 for x8 configuration - if get(guiHandles.checkbox10, 'Value'), try hch15=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_4_, sFactor, 'loess'));hold on;set(hch15,'color', [linec.col10],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end - if get(guiHandles.checkbox11, 'Value'), try hch16=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_5_, sFactor, 'loess'));hold on;set(hch16,'color', [linec.col11],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end - if get(guiHandles.checkbox12, 'Value'), try hch17=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_6_, sFactor, 'loess'));hold on;set(hch17,'color', [linec.col12],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end - if get(guiHandles.checkbox13, 'Value'), try hch18=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.motor_7_, sFactor, 'loess'));hold on;set(hch18,'color', [linec.col13],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end - - if get(guiHandles.checkbox14, 'Value'), hch19=plot(tta{fileIdx}/us2sec, smooth(T{fileIdx}.setpoint_3_/10, sFactor, 'loess'));hold on;set(hch19,'color', [linec.col14],'LineWidth',lwVal), end - - axis([0 xmax 0 100]) - - h=fill([0,t1,t1,0],[0, 0, 100, 100],[.8 .8 .8]); - set(h,'FaceAlpha',0.8,'EdgeColor',[.8 .8 .8]); - h=fill([t2,xmax,xmax,t2],[0, 0, 100, 100],[.8 .8 .8]); - set(h,'FaceAlpha',0.8,'EdgeColor',[.8 .8 .8]); - - y=xlabel('Time (s)','fontweight','bold'); - set(y,'color',[.2 .2 .2]); - y=ylabel({'Throttle | Motor (%)'},'fontweight','bold'); - set(gca,'fontsize',fontsz,'XMinorGrid','on','ylim',[0 100],'ytick',[0 20 40 60 80 100],'fontweight','bold') - set(gca,'xtick',[round(xmax/10):round(xmax/10):round(xmax)],'XColor',[.2 .2 .2]) - grid on - - - - - end - - try - if ii==1 && ~expandON - set(LVpanel1,'color',[1 1 1],'fontsize',fontsz,'tickdir','in','xminortick','on','yminortick','on','position',[posInfo.linepos1]); - end - if ii==2 && ~expandON - set(LVpanel2,'color',[1 1 1],'fontsize',fontsz,'tickdir','in','xminortick','on','yminortick','on','position',[posInfo.linepos2]); - end - if ii==3 && ~expandON - set(LVpanel3,'color',[1 1 1],'fontsize',fontsz,'tickdir','in','xminortick','on','yminortick','on','position',[posInfo.linepos3]); - end - if ~expandON - set(LVpanel5,'color',[1 1 1],'fontsize',fontsz,'tickdir','in','xminortick','on','yminortick','on','position',[posInfo.linepos4]); - end - catch - end - end - end - - % i/o keyboard trim: 'i' sets in-point, 'o' sets out-point - set(PSfig, 'KeyPressFcn', [ ... - 'if exist(''filenameA'',''var'') && ~isempty(filenameA), ' ... - 'kk=get(gcbo,''CurrentCharacter''); fIdx=get(guiHandles.FileNum,''Value''); ' ... - 'if kk==''i'', try, [xt,~]=ginput(1); epoch1_A(fIdx)=round(xt*10)/10; PSplotLogViewer; catch, end; ' ... - 'elseif kk==''o'', try, [xt,~]=ginput(1); epoch2_A(fIdx)=round(xt*10)/10; PSplotLogViewer; catch, end; ' ... - 'end, end']); - - % Set up click-to-show-value datatips on all axes - PSdatatipSetup(PSfig); - - set(PSfig, 'pointer', 'arrow') -else - warndlg('Please select file(s)'); -end - - +%% PSplotLogViewer - script to plot main line graphs + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + + set(PSfig, 'pointer', 'watch') + th = PStheme(); + + global logviewerYscale + logviewerYscale = str2double(get(guiHandles.maxY_input, 'String')); + + set(0, 'CurrentFigure', PSfig); + + maxY=str2double(get(guiHandles.maxY_input, 'String')); + + alpha_red=.8; + alpha_blue=.8; + + + lineSmoothFactors = [1 10 20 40 80]; + + if plotall_flag>=0 + allVal = get(guiHandles.checkbox15, 'Value'); + set(guiHandles.checkbox0, 'Value', allVal); + try set(guiHandles.checkboxGyroPF, 'Value', allVal); catch, end + set(guiHandles.checkbox1, 'Value', allVal); + set(guiHandles.checkbox2, 'Value', allVal); + set(guiHandles.checkbox3, 'Value', allVal); + set(guiHandles.checkbox4, 'Value', allVal); + set(guiHandles.checkbox5, 'Value', allVal); + set(guiHandles.checkbox6, 'Value', allVal); + set(guiHandles.checkbox7, 'Value', allVal); + set(guiHandles.checkbox8, 'Value', allVal); + set(guiHandles.checkbox9, 'Value', allVal); + set(guiHandles.checkbox10, 'Value', allVal); + set(guiHandles.checkbox11, 'Value', allVal); + set(guiHandles.checkbox12, 'Value', allVal); + set(guiHandles.checkbox13, 'Value', allVal); + set(guiHandles.checkbox14, 'Value', allVal); + set(guiHandles.checkboxTS, 'Value', allVal); + for rk_=1:4, try set(guiHandles.(['checkboxRPM' int2str(rk_)]), 'Value', allVal); catch, end, end + end + plotall_flag=-1; + + dynCpL = getappdata(PSfig, 'PScpL'); if isempty(dynCpL), dynCpL = 0.875; end + plotL = 0.095; plotGap = 0.01; + plotW = dynCpL - plotL - plotGap; + expandW = dynCpL - 0.05 - 0.01; + expand_sz=[0.05 0.06 expandW posInfo.slider(2)-0.07]; + % Update linepos widths to match current CP position + posInfo.linepos1(3) = plotW; + posInfo.linepos2(3) = plotW; + posInfo.linepos3(3) = plotW; + posInfo.linepos4(3) = plotW; + fullszPlot(3) = plotW; + + + %% where you want full range of data + fileIdx = get(guiHandles.FileNum, 'Value'); + + % enable/disable RPM checkboxes based on eRPM data + hasERPM_ = exist('T','var') && iscell(T) && numel(T) >= fileIdx && isfield(T{fileIdx}, 'eRPM_0_'); + rpmEn_ = 'off'; if hasERPM_, rpmEn_ = 'on'; end + for rk_=1:4 + try + set(guiHandles.(['checkboxRPM' int2str(rk_)]), 'Enable', rpmEn_); + if ~hasERPM_, set(guiHandles.(['checkboxRPM' int2str(rk_)]), 'Value', 0); end + catch, end + end + + % enable/disable Gyro(pf) checkbox based on gyroPrefilt data + hasPF_ = exist('T','var') && iscell(T) && numel(T) >= fileIdx && isfield(T{fileIdx}, 'gyroPrefilt_0_'); + pfEn_ = 'off'; if hasPF_, pfEn_ = 'on'; end + try set(guiHandles.checkboxGyroPF, 'Enable', pfEn_); + if ~hasPF_, set(guiHandles.checkboxGyroPF, 'Value', 0); end + catch, end + + % Update Debug checkbox label for RC_INTERPOLATION mode (version-aware) + tmpRCidx = RC_INTERPOLATION; % global default + if exist('debugIdx','var') && numel(debugIdx) >= fileIdx + tmpRCidx = debugIdx{fileIdx}.RC_INTERPOLATION; + end + if exist('debugmode','var') && numel(debugmode) >= fileIdx && debugmode(fileIdx) == tmpRCidx + set(guiHandles.checkbox0, 'String', 'SP (raw)'); + else + set(guiHandles.checkbox0, 'String', 'Debug'); + end + + % clamp epochs to valid data range + if ~exist('tta','var') || ~iscell(tta) || numel(tta) < fileIdx + set(PSfig, 'pointer', 'arrow'); return; + end + tStart = tta{fileIdx}(1) / us2sec; + tEnd = tta{fileIdx}(end) / us2sec; + if epoch1_A(fileIdx) >= tEnd || epoch2_A(fileIdx) <= tStart || epoch1_A(fileIdx) >= epoch2_A(fileIdx) + epoch1_A(fileIdx) = tStart; + epoch2_A(fileIdx) = tEnd; + end + + y=[epoch1_A(fileIdx)*us2sec epoch2_A(fileIdx)*us2sec]; + idx1 = find(tta{fileIdx} >= y(1), 1); + idx2 = find(tta{fileIdx} >= y(2), 1); + if isempty(idx1), idx1 = 1; end + if isempty(idx2), idx2 = length(tta{fileIdx}); end + t1 = tta{fileIdx}(idx1) / us2sec; + t2 = tta{fileIdx}(idx2) / us2sec; + + tIND{fileIdx} = (tta{fileIdx} > (t1*us2sec)) & (tta{fileIdx} < (t2*us2sec)); + +% jRangeSlider = com.jidesoft.swing.RangeSlider(0,200,10,190); % min,max,low,high +% jRangeSlider = javacomponent(jRangeSlider,[50, 80, 500, 80], gcf); +% set(jRangeSlider, 'MajorTickSpacing',50, 'PaintTicks',true, 'PaintLabels',true, 'Background',java.awt.Color.white) +% jRangeSlider.LowValue = 20;, jRangeSlider.HighValue = 180; + + sliderPos = posInfo.slider; + sliderPos(3) = dynCpL - sliderPos(1) - 0.005; + guiHandles.slider = uicontrol(PSfig, 'style','slider','SliderStep',[0.001 0.01],'Visible', 'on', 'units','normalized','position',sliderPos,... + 'min',0,'max',1, 'callback','PSslider1Actions;'); + % Update resize data with slider handle + chkBarData = getappdata(PSfig, 'PScheckboxBar'); + if ~isempty(chkBarData), chkBarData.slider = guiHandles.slider; setappdata(PSfig, 'PScheckboxBar', chkBarData); end + + + + %% log viewer line plots + %%%%%%%% PLOT %%%%%%% + axLabel={'Roll';'Pitch';'Yaw'}; + lineStyleLV = {'-'; '-'; '-'}; + lineStyle2LV = {'-'; '--'; ':'}; + lineStyle2LVnames = {'solid' ; 'dashed' ; 'dotted'}; + axesOptionsLV = find([get(guiHandles.plotR, 'Value') get(guiHandles.plotP, 'Value') get(guiHandles.plotY, 'Value')]); + + for ei=1:3, try delete(hexpand{ei}); catch, end, end + expandON = 0; + + ylabelname=''; + for i = 1 : size(axesOptionsLV,2) + if i == size(axesOptionsLV,2) + ylabelname = [ylabelname axLabel{axesOptionsLV(i)} '-' lineStyle2LVnames{i} ' (deg/s) ']; + else + ylabelname = [ylabelname axLabel{axesOptionsLV(i)} '-' lineStyle2LVnames{i} ' | ']; + end + end + + PSfig; + + try zoomOn = strcmp(get(zoom(PSfig), 'Enable'),'on'); catch, zoomOn = 0; end + if ~zoomOn && ~expandON + try delete(findobj(PSfig,'Tag','PSrpy')); catch, end + try delete(findobj(PSfig,'Tag','PSmotor')); catch, end + try delete(findobj(PSfig,'Tag','PScombo')); catch, end + try delete(findobj(PSfig,'Tag','PSeRPMax')); catch, end + end + + try delete(hch1); catch, end, try delete(hch2); catch, end + try delete(hch3); catch, end, try delete(hch4); catch, end + try delete(hch5); catch, end, try delete(hch6); catch, end + try delete(hch7); catch, end, try delete(hch8); catch, end + try delete(hch9); catch, end, try delete(hch10); catch, end + try delete(hch11); catch, end, try delete(hch12); catch, end + try delete(hch13); catch, end, try delete(hch14); catch, end + try delete(hch15); catch, end, try delete(hch16); catch, end + try delete(hch17); catch, end, try delete(hch18); catch, end + try delete(hch19); catch, end + + + try % datacursormode not available in Octave + dcm_obj = datacursormode(PSfig); + set(dcm_obj,'UpdateFcn',@PSdatatip); + end + + cntLV = 0; + lnstyle = lineStyleLV; + LVpanels = {[], [], []}; + + if exist('fnameMaster','var') && ~isempty(fnameMaster) + for ii = axesOptionsLV + if get(guiHandles.RPYcomboLV, 'Value'), expandON = 0; end + lpKey = ['linepos' int2str(ii)]; + if ~get(guiHandles.RPYcomboLV, 'Value') && ~expandON + LVpanels{ii} = axes('Parent', PSfig, 'Position', posInfo.(lpKey), 'Tag', 'PSrpy'); + LVpanel5 = axes('Parent', PSfig, 'Position', posInfo.linepos4, 'Tag', 'PSmotor'); + end + if ~get(guiHandles.RPYcomboLV, 'Value') && expandON + try + set(hexpand{ii}, 'Position', expand_sz); + catch + end + end + + hexp = []; try hexp = hexpand{ii}; catch, end + if (~isempty(hexp) && ishandle(hexp)) || ~expandON + + cntLV = cntLV + 1; + if get(guiHandles.RPYcomboLV, 'Value') + LVpanel4 = findobj(PSfig, 'Type', 'axes', 'Tag', 'PScombo'); + if isempty(LVpanel4), LVpanel4 = axes('Parent', PSfig, 'Position', fullszPlot, 'Tag', 'PScombo'); + else set(PSfig, 'CurrentAxes', LVpanel4); end + lnstyle = lineStyle2LV; + end + if ~get(guiHandles.RPYcomboLV, 'Value') && expandON == 0 + set(PSfig, 'CurrentAxes', LVpanels{ii}); + lnstyle = lineStyleLV; + end + + xmax=max(tta{fileIdx}/us2sec); + + + h=plot([0 xmax],[-maxY -maxY],'Color',th.axesFg); + set(h,'linewidth',.2) + hold on + set(gca,'Color',th.axesBg); + set(gca,'ytick',[ -(maxY/2) 0 maxY/2 ],'yticklabel',{num2str(-(maxY/2)) '0' num2str((maxY/2)) ''},'YColor',th.axesFg,'fontweight','bold') + set(gca,'xtick',[round(xmax/10):round(xmax/10):round(xmax)],'XColor',th.axesFg,'GridColor',th.gridColor) + + sFactor = lineSmoothFactors(get(guiHandles.lineSmooth, 'Value')); + fileIdx = get(guiHandles.FileNum, 'Value'); + lwVal = get(guiHandles.linewidth, 'Value')/2; + + tSec = tta{fileIdx}/us2sec; + if get(guiHandles.checkbox0, 'Value'), try hch1=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['debug_' int2str(ii-1) '_'], sFactor));hold on;set(hch1,'color', [linec.col0],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + try if get(guiHandles.checkboxGyroPF, 'Value'), try hchPF=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['gyroPrefilt_' int2str(ii-1) '_'], sFactor));hold on;set(hchPF,'color', [linec.colGyroPF],'LineWidth',lwVal,'linestyle','--'), catch, end, end, catch, end + if get(guiHandles.checkbox1, 'Value'), try hch2=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['gyroADC_' int2str(ii-1) '_'], sFactor));hold on;set(hch2,'color', [linec.col1],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox2, 'Value'), try hch3=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['axisP_' int2str(ii-1) '_'], sFactor));hold on;set(hch3,'color', [linec.col2],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox3, 'Value'), try hch4=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['axisI_' int2str(ii-1) '_'], sFactor));hold on;set(hch4,'color', [linec.col3],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox4, 'Value') && ii<3, try hch5=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['axisDpf_' int2str(ii-1) '_'], sFactor));hold on;set(hch5,'color', [linec.col4],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox5, 'Value') && ii<3, try hch6=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['axisD_' int2str(ii-1) '_'], sFactor));hold on;set(hch6,'color', [linec.col5],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox6, 'Value'), try hch7=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['axisF_' int2str(ii-1) '_'], sFactor));hold on;set(hch7,'color', [linec.col6],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox7, 'Value'), try hch8=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['setpoint_' int2str(ii-1) '_'], sFactor));hold on;set(hch8,'color', [linec.col7],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox8, 'Value'), try hch9=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['pidsum_' int2str(ii-1) '_'], sFactor));hold on;set(hch9,'color', [linec.col8],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkbox9, 'Value'), try hch10=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['piderr_' int2str(ii-1) '_'], sFactor));hold on;set(hch10,'color', [linec.col9],'LineWidth',lwVal,'linestyle',[lnstyle{cntLV}]), catch, end, end + if get(guiHandles.checkboxTS, 'Value') && isfield(T{fileIdx}, ['testSignal_' int2str(ii-1) '_']), try hchTS=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, ['testSignal_' int2str(ii-1) '_'], sFactor));hold on;set(hchTS,'color', th.sigTestSignal,'LineWidth',lwVal,'linestyle','--'), catch, end, end + + + h=fill([0,t1,t1,0],[-maxY,-maxY,maxY,maxY],th.epochFill); + set(h,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill); + h=fill([t2,xmax,xmax,t2],[-maxY,-maxY,maxY,maxY],th.epochFill); + set(h,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill); + + try zoomOn2 = strcmp(get(zoom(PSfig), 'Enable'),'on'); catch, zoomOn2 = 0; end + if zoomOn2 + v = axis; + axis(v) + else + axis([0 xmax -maxY maxY]) + end + + box off + if get(guiHandles.RPYcomboLV, 'Value') + y=ylabel([ylabelname],'fontweight','bold','rotation', 90); + else + y=ylabel([axLabel{ii} ' (deg/s)'],'fontweight','bold','rotation', 90); + end + + + set(y,'Units','normalized', 'position', [-.035 .5 1],'color',th.textPrimary); + y=xlabel('Time (s)','fontweight','bold'); + set(y,'color',th.textPrimary); + set(gca,'fontsize',fontsz,'XMinorGrid','on') + grid on + + % Percent variables + LVpanel5 = findobj(PSfig, 'Type', 'axes', 'Tag', 'PSmotor'); + if isempty(LVpanel5), LVpanel5 = axes('Parent', PSfig, 'Position', posInfo.linepos4, 'Tag', 'PSmotor'); + else set(PSfig, 'CurrentAxes', LVpanel5); end + if get(guiHandles.checkbox10, 'Value'), try hch11=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_0_', sFactor));hold on;set(hch11,'color', [linec.col10],'LineWidth',lwVal), catch, end, end + if get(guiHandles.checkbox11, 'Value'), try hch12=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_1_', sFactor));hold on;set(hch12,'color', [linec.col11],'LineWidth',lwVal), catch, end, end + if get(guiHandles.checkbox12, 'Value'), try hch13=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_2_', sFactor));hold on;set(hch13,'color', [linec.col12],'LineWidth',lwVal), catch, end, end + if get(guiHandles.checkbox13, 'Value'), try hch14=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_3_', sFactor));hold on;set(hch14,'color', [linec.col13],'LineWidth',lwVal), catch, end, end + % motor sigs 4-7 for x8 configuration + if get(guiHandles.checkbox10, 'Value'), try hch15=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_4_', sFactor));hold on;set(hch15,'color', [linec.col10],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end + if get(guiHandles.checkbox11, 'Value'), try hch16=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_5_', sFactor));hold on;set(hch16,'color', [linec.col11],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end + if get(guiHandles.checkbox12, 'Value'), try hch17=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_6_', sFactor));hold on;set(hch17,'color', [linec.col12],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end + if get(guiHandles.checkbox13, 'Value'), try hch18=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_7_', sFactor));hold on;set(hch18,'color', [linec.col13],'LineWidth',lwVal, 'LineStyle', '--'), catch, end, end + + if get(guiHandles.checkbox14, 'Value'), try hch19=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'setpoint_3_', sFactor, 0.1));hold on;set(hch19,'color', [linec.col14],'LineWidth',lwVal), catch, end, end + + axis([0 xmax 0 100]) + set(gca,'Color',th.axesBg); + h=fill([0,t1,t1,0],[0, 0, 100, 100],th.epochFill); + set(h,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill); + h=fill([t2,xmax,xmax,t2],[0, 0, 100, 100],th.epochFill); + set(h,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill); + + y=xlabel('Time (s)','fontweight','bold'); + set(y,'color',th.textPrimary); + y=ylabel({'Throttle | Motor (%)'},'fontweight','bold'); + set(gca,'fontsize',fontsz,'XMinorGrid','on','ylim',[0 100],'ytick',[0 20 40 60 80 100],'fontweight','bold') + set(gca,'xtick',[round(xmax/10):round(xmax/10):round(xmax)],'XColor',th.axesFg,'YColor',th.axesFg,'GridColor',th.gridColor) + set(y,'color',th.textPrimary); + grid on + + + + + end + + try + if ~expandON && ~isempty(LVpanels{ii}) + set(LVpanels{ii},'color',th.axesBg,'fontsize',fontsz,'tickdir','in','xminortick','on','yminortick','on','position',posInfo.(['linepos' int2str(ii)]),'Tag','PSrpy'); + end + if ~expandON + set(LVpanel5,'color',th.axesBg,'fontsize',fontsz,'tickdir','in','xminortick','on','yminortick','on','position',[posInfo.linepos4],'Tag','PSmotor'); + end + catch + end + end + + % motor-only mode: fill available space when all RPY disabled + if isempty(axesOptionsLV) && ~get(guiHandles.RPYcomboLV, 'Value') && ~expandON + plotTop_m = posInfo.slider(2) - 0.005; + motorFullH = plotTop_m - 0.1 - 0.01; + LVpanel5 = axes('Parent', PSfig, 'Position', [plotL 0.1 plotW motorFullH], 'Tag', 'PSmotor'); + fileIdx = get(guiHandles.FileNum, 'Value'); + sFactor = lineSmoothFactors(get(guiHandles.lineSmooth, 'Value')); + lwVal = get(guiHandles.linewidth, 'Value')/2; + xmax = max(tta{fileIdx}/us2sec); + tSec = tta{fileIdx}/us2sec; + if get(guiHandles.checkbox10, 'Value'), try hch11=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_0_', sFactor));hold on;set(hch11,'color', [linec.col10],'LineWidth',lwVal), catch, end, end + if get(guiHandles.checkbox11, 'Value'), try hch12=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_1_', sFactor));hold on;set(hch12,'color', [linec.col11],'LineWidth',lwVal), catch, end, end + if get(guiHandles.checkbox12, 'Value'), try hch13=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_2_', sFactor));hold on;set(hch13,'color', [linec.col12],'LineWidth',lwVal), catch, end, end + if get(guiHandles.checkbox13, 'Value'), try hch14=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'motor_3_', sFactor));hold on;set(hch14,'color', [linec.col13],'LineWidth',lwVal), catch, end, end + if get(guiHandles.checkbox14, 'Value'), try hch19=plot(tSec, PSsmoothLV(PSfig, T{fileIdx}, fileIdx, 'setpoint_3_', sFactor, 0.1));hold on;set(hch19,'color', [linec.col14],'LineWidth',lwVal), catch, end, end + axis([0 xmax 0 100]) + set(gca,'Color',th.axesBg); + h=fill([0,t1,t1,0],[0, 0, 100, 100],th.epochFill); + set(h,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill); + h=fill([t2,xmax,xmax,t2],[0, 0, 100, 100],th.epochFill); + set(h,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill); + y=xlabel('Time (s)','fontweight','bold'); set(y,'color',th.textPrimary); + y=ylabel({'Throttle | Motor (%)'},'fontweight','bold'); set(y,'color',th.textPrimary); + set(gca,'fontsize',fontsz,'XMinorGrid','on','ylim',[0 100],'ytick',[0 20 40 60 80 100],'fontweight','bold') + set(gca,'xtick',[round(xmax/10):round(xmax/10):round(xmax)],'XColor',th.axesFg,'YColor',th.axesFg,'GridColor',th.gridColor) + grid on + set(LVpanel5,'color',th.axesBg,'fontsize',fontsz,'tickdir','in','xminortick','on','yminortick','on','Tag','PSmotor'); + end + end + + % eRPM overlay on motor subplot (per-motor RPM checkboxes) + try delete(findobj(PSfig,'Tag','PSeRPMax')); catch, end + try + rpmEnabled_ = false(1,4); + for rk_=1:4, try rpmEnabled_(rk_) = get(guiHandles.(['checkboxRPM' int2str(rk_)]), 'Value'); catch, end, end + if any(rpmEnabled_) + motorAx = findobj(PSfig, 'Type', 'axes', 'Tag', 'PSmotor'); + if ~isempty(motorAx) + motorAx = motorAx(1); + fileIdx_ = get(guiHandles.FileNum, 'Value'); + hasERPM = isfield(T{fileIdx_}, 'eRPM_0_'); + if hasERPM + mPoles = 14; + try + mpRow = find(strcmp(SetupInfo{fileIdx_}(:,1), 'motor_poles')); + if ~isempty(mpRow), mPoles = str2double(SetupInfo{fileIdx_}(mpRow(1),2)); end + catch, end + if mPoles < 2, mPoles = 14; end + + mPos = get(motorAx, 'Position'); + mXL = get(motorAx, 'XLim'); + tSec_ = tta{fileIdx_}/us2sec; + sFactor_ = lineSmoothFactors(get(guiHandles.lineSmooth, 'Value')); + lwVal_ = get(guiHandles.linewidth, 'Value')/2; + + rpmAx = axes('Parent', PSfig, 'Position', mPos, ... + 'Color', 'none', 'XLim', mXL, ... + 'YAxisLocation', 'right', 'Box', 'off', ... + 'XTick', [], 'Tag', 'PSeRPMax', 'HitTest', 'off'); + hold(rpmAx, 'on'); + + rpmColors = th.sigRPM; + nEm_ = 0; + for mk = 0:7 + if isfield(T{fileIdx_}, ['eRPM_' int2str(mk) '_']), nEm_ = mk+1; end + end + rpmMax = 0; + for mk = 0:nEm_-1 + ci_ = mod(mk, 4) + 1; + if ~rpmEnabled_(ci_), continue; end + fld = ['eRPM_' int2str(mk) '_']; + if isfield(T{fileIdx_}, fld) + raw = PSsmoothLV(PSfig, T{fileIdx_}, fileIdx_, fld, sFactor_); + hz = raw * 100 / (mPoles/2) / 60; + ls_ = ':'; if mk >= 4, ls_ = '--'; end + plot(rpmAx, tSec_, hz, 'Color', rpmColors{ci_}, 'LineWidth', lwVal_, 'LineStyle', ls_, 'HitTest', 'off'); + rpmMax = max(rpmMax, max(hz)); + end + end + if rpmMax > 0 + rpmCeil = ceil(rpmMax/100)*100; + rpmStep = rpmCeil / 5; + set(rpmAx, 'YLim', [0 rpmCeil], 'YColor', th.textSecondary, ... + 'YTick', 0:rpmStep:rpmCeil, 'fontsize', fontsz, ... + 'XColor', 'none', 'TickDir', 'in'); + yl_ = ylabel(rpmAx, 'eRPM (Hz)', 'fontweight', 'bold'); + set(yl_, 'color', th.textSecondary); + % epoch trim fills on RPM overlay + hf_=fill(rpmAx,[0,t1,t1,0],[0,0,rpmCeil,rpmCeil],th.epochFill); + set(hf_,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill,'HitTest','off'); + hf_=fill(rpmAx,[t2,mXL(2),mXL(2),t2],[0,0,rpmCeil,rpmCeil],th.epochFill); + set(hf_,'FaceAlpha',th.epochAlpha,'EdgeColor',th.epochFill,'HitTest','off'); + end + end + end + end + catch, end + + % i/o keyboard trim: 'i' sets in-point, 'o' sets out-point + set(PSfig, 'KeyPressFcn', [ ... + 'if exist(''filenameA'',''var'') && ~isempty(filenameA), ' ... + 'kk=get(gcbo,''CurrentCharacter''); fIdx=get(guiHandles.FileNum,''Value''); ' ... + 'if kk==''i'', try, [xt,~]=ginput(1); epoch1_A(fIdx)=round(xt*10)/10; PSplotLogViewer; catch, end; ' ... + 'elseif kk==''o'', try, [xt,~]=ginput(1); epoch2_A(fIdx)=round(xt*10)/10; PSplotLogViewer; catch, end; ' ... + 'end, end']); + + PSdatatipSetup(PSfig); + try PSresizeCP(PSfig, []); catch, end + + set(PSfig, 'pointer', 'arrow') +else + warndlg('Please select file(s)'); +end + + diff --git a/src/plot/PSplotMotorNoise.m b/src/plot/PSplotMotorNoise.m index 1193329..00789df 100644 --- a/src/plot/PSplotMotorNoise.m +++ b/src/plot/PSplotMotorNoise.m @@ -5,15 +5,20 @@ function PSplotMotorNoise(T, f, tIND, Fs) % tIND - logical time index mask % Fs - sample rate (Hz) +th = PStheme(); +fontsz = th.fontsz; screensz = get(0, 'ScreenSize'); +fig = findobj('Type', 'figure', 'Name', 'Motor / Prop Noise Analysis'); +if ~isempty(fig), close(fig); end fig = figure('Name', 'Motor / Prop Noise Analysis', 'NumberTitle', 'off', ... - 'Color', [.15 .15 .15], ... - 'Position', round([.08*screensz(3) .06*screensz(4) .78*screensz(3) .82*screensz(4)])); + 'Color', th.figBg, ... + 'Position', round([0 0 screensz(3) screensz(4)])); +try set(fig, 'WindowState', 'maximized'); catch, end F_kHz = Fs / 1000; motorCol = {'motor_0_', 'motor_1_', 'motor_2_', 'motor_3_'}; motorLbl = {'Motor 1', 'Motor 2', 'Motor 3', 'Motor 4'}; -mCol = {[0 .85 .3], [.85 .85 0], [.85 .2 .2], [.3 .5 1]}; +mCol = th.sigMotor; gyroAxLbl = {'Roll', 'Pitch', 'Yaw'}; gyroCol = {'gyroADC_0_', 'gyroADC_1_', 'gyroADC_2_'}; @@ -77,8 +82,8 @@ function PSplotMotorNoise(T, f, tIND, Fs) infoStr = sprintf('Noisiest: %s (%.0f dB) Quietest: %s (%.0f dB) Spread: %.1f dB', ... motorLbl{worst}, rmsAll(worst), motorLbl{best}, rmsAll(best), spread); uicontrol(fig, 'Style', 'text', 'String', infoStr, 'Units', 'normalized', ... - 'Position', [.06 .965 .88 .03], 'FontSize', 13, 'FontWeight', 'bold', ... - 'ForegroundColor', [.9 .9 .3], 'BackgroundColor', [.2 .2 .2], ... + 'Position', [.06 .965 .88 .03], 'FontSize', fontsz, 'FontWeight', 'bold', ... + 'ForegroundColor', th.textAccent, 'BackgroundColor', th.panelBg, ... 'HorizontalAlignment', 'center'); % --- TOP LEFT: All motors overlay PSD --- @@ -89,36 +94,47 @@ function PSplotMotorNoise(T, f, tIND, Fs) end addBandShading(axOvl); hold(axOvl, 'off'); -styleDark(axOvl, fMax); -set(get(axOvl, 'YLabel'), 'String', 'dB', 'Color', [.8 .8 .8]); -th1 = title(axOvl, 'Motor PSD Comparison'); set(th1, 'Color', [.9 .9 .9]); -legend(axOvl, motorLbl(1:nMotors), 'TextColor', [.8 .8 .8], 'Color', [.2 .2 .2], ... - 'EdgeColor', [.4 .4 .4], 'FontSize', 11, 'Location', 'northeast'); +PSstyleAxes(axOvl, th); set(axOvl, 'XLim', [0 fMax]); +set(get(axOvl, 'YLabel'), 'String', 'dB'); +title(axOvl, 'Motor PSD Comparison'); +h_leg = legend(axOvl, motorLbl(1:nMotors), 'Location', 'northeast'); +try PSstyleLegend(h_leg, th); catch, end % --- TOP RIGHT: Gyro PSD --- axGyro = axes('Parent', fig, 'Units', 'normalized', 'Position', [.56 .70 .38 .22]); -gCols = {[1 .4 .4], [.4 1 .4], [.4 .6 1]}; +gCols = {th.axisRoll, th.axisPitch, th.axisYaw}; for g = 1:3 plot(axGyro, gyroFreqV{g}, gyroSpec{g}, 'Color', gCols{g}, 'LineWidth', 1.1); hold(axGyro, 'on'); end addBandShading(axGyro); hold(axGyro, 'off'); -styleDark(axGyro, fMax); -set(get(axGyro, 'YLabel'), 'String', 'dB', 'Color', [.8 .8 .8]); -th2 = title(axGyro, 'Gyro Noise'); set(th2, 'Color', [.9 .9 .9]); -legend(axGyro, gyroAxLbl, 'TextColor', [.8 .8 .8], 'Color', [.2 .2 .2], ... - 'EdgeColor', [.4 .4 .4], 'FontSize', 11, 'Location', 'northeast'); +PSstyleAxes(axGyro, th); set(axGyro, 'XLim', [0 fMax]); +set(get(axGyro, 'YLabel'), 'String', 'dB'); +title(axGyro, 'Gyro Noise'); +h_leg = legend(axGyro, gyroAxLbl, 'Location', 'northeast'); +try PSstyleLegend(h_leg, th); catch, end -% --- MID LEFT: Motor-Gyro coherence --- +% --- MID LEFT: Motor-Gyro coherence (pre-compute all 3 axes) --- axCoh = axes('Parent', fig, 'Units', 'normalized', 'Position', [.06 .39 .42 .25]); dat.axCoh = axCoh; -plotCoherence(axCoh, motorData, gyroData{1}, nMotors, mCol, motorLbl, Fs, fMax, 'Roll'); +cohCache = cell(3, 1); +for gi_ = 1:3 + cc_ = struct(); cc_.Cxy = cell(nMotors, 1); + gdat_ac = gyroData{gi_} - mean(gyroData{gi_}); + for mi_ = 1:nMotors + mdat_ac = motorData{mi_} - mean(motorData{mi_}); + [cc_.Cxy{mi_}, cc_.f] = mscohere_simple(mdat_ac, gdat_ac, Fs, 1024); + end + cohCache{gi_} = cc_; +end +dat.cohCache = cohCache; +plotCohFromCache(axCoh, cohCache{1}, nMotors, mCol, motorLbl, fMax, 'Roll'); -% gyro axis dropdown (right of title, inside plot area) +% gyro axis dropdown uicontrol(fig, 'Style', 'popupmenu', 'String', {'Roll', 'Pitch', 'Yaw'}, ... 'Units', 'normalized', 'Position', [.06 .64 .10 .025], ... - 'FontSize', 12, 'Callback', {@cohAxisCb, fig}); + 'FontSize', fontsz, 'Callback', {@cohAxisCb, fig}); setappdata(fig, 'mndat', dat); % --- MID RIGHT: Noise bar chart per frequency band --- @@ -139,14 +155,12 @@ function PSplotMotorNoise(T, f, tIND, Fs) for m = 1:nMotors set(bh(m), 'FaceColor', mCol{m}); end -set(axBar, 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 12, 'FontWeight', 'bold', 'XTick', 1:size(bands,1), ... - 'XTickLabel', bandLabelsClean); -set(get(axBar, 'YLabel'), 'String', 'Mean PSD (dB)', 'Color', [.8 .8 .8]); -th4 = title(axBar, 'Noise by Frequency Band'); set(th4, 'Color', [.9 .9 .9]); -grid(axBar, 'on'); set(axBar, 'GridColor', [.3 .3 .3]); -legend(axBar, motorLbl(1:nMotors), 'TextColor', [.8 .8 .8], 'Color', [.2 .2 .2], ... - 'EdgeColor', [.4 .4 .4], 'FontSize', 11, 'Location', 'northeast'); +PSstyleAxes(axBar, th); +set(axBar, 'XTick', 1:size(bands,1), 'XTickLabel', bandLabelsClean); +set(get(axBar, 'YLabel'), 'String', 'Mean PSD (dB)'); +title(axBar, 'Noise by Frequency Band'); +h_leg = legend(axBar, motorLbl(1:nMotors), 'Location', 'northeast'); +try PSstyleLegend(h_leg, th); catch, end % --- BOTTOM: Motor output time domain --- axTime = axes('Parent', fig, 'Units', 'normalized', 'Position', [.06 .06 .88 .26]); @@ -156,15 +170,15 @@ function PSplotMotorNoise(T, f, tIND, Fs) plot(axTime, t, motorData{m}, 'Color', mCol{m}, 'LineWidth', 0.5); hold(axTime, 'on'); end -plot(axTime, t, throttle, 'Color', [.9 .9 .9], 'LineWidth', 1.5, 'LineStyle', '--'); +plot(axTime, t, throttle, 'Color', th.sigThrottle, 'LineWidth', 1.5, 'LineStyle', '--'); hold(axTime, 'off'); -styleDark(axTime, max(t)); +PSstyleAxes(axTime, th); set(axTime, 'XLim', [0 max(t)]); set(axTime, 'YLim', [0 100]); -set(get(axTime, 'XLabel'), 'String', 'Time (s)', 'Color', [.8 .8 .8]); -set(get(axTime, 'YLabel'), 'String', 'Motor %', 'Color', [.8 .8 .8]); -th5 = title(axTime, 'Motor Output'); set(th5, 'Color', [.9 .9 .9]); -legend(axTime, [motorLbl(1:nMotors), {'Throttle avg'}], 'TextColor', [.8 .8 .8], ... - 'Color', [.2 .2 .2], 'EdgeColor', [.4 .4 .4], 'FontSize', 11, 'Location', 'northeast'); +set(get(axTime, 'XLabel'), 'String', 'Time (s)'); +set(get(axTime, 'YLabel'), 'String', 'Motor %'); +title(axTime, 'Motor Output'); +h_leg = legend(axTime, [motorLbl(1:nMotors), {'Throttle avg'}], 'Location', 'northeast'); +try PSstyleLegend(h_leg, th); catch, end PSdatatipSetup(fig); @@ -174,37 +188,26 @@ function PSplotMotorNoise(T, f, tIND, Fs) function cohAxisCb(src, ~, fig) dat = getappdata(fig, 'mndat'); gIdx = get(src, 'Value'); - gdat = dat.gyroData{gIdx}; - axLbl = dat.gyroAxLbl{gIdx}; - plotCoherence(dat.axCoh, dat.motorData, gdat, dat.nMotors, ... - dat.mCol{1}, dat.motorLbl{1}, dat.Fs, dat.fMax, axLbl); + plotCohFromCache(dat.axCoh, dat.cohCache{gIdx}, dat.nMotors, ... + dat.mCol{1}, dat.motorLbl{1}, dat.fMax, dat.gyroAxLbl{gIdx}); end -function plotCoherence(ax, motorData, gdat, nMotors, mCol, motorLbl, Fs, fMax, axLabel) +function plotCohFromCache(ax, cc, nMotors, mCol, motorLbl, fMax, axLabel) cla(ax); - gdat_ac = gdat - mean(gdat); + thm = PStheme(); for m = 1:nMotors - mdat = motorData{m} - mean(motorData{m}); - [Cxy, fCoh] = mscohere_simple(mdat, gdat_ac, Fs, 1024); - plot(ax, fCoh, Cxy, 'Color', mCol{m}, 'LineWidth', 1.0); + plot(ax, cc.f, cc.Cxy{m}, 'Color', mCol{m}, 'LineWidth', 1.0); hold(ax, 'on'); end hold(ax, 'off'); - styleDark(ax, fMax); + PSstyleAxes(ax, thm); set(ax, 'XLim', [0 fMax]); set(ax, 'YLim', [0 1.05]); - set(get(ax, 'XLabel'), 'String', 'Hz', 'Color', [.8 .8 .8]); - set(get(ax, 'YLabel'), 'String', 'Coherence', 'Color', [.8 .8 .8]); - th = title(ax, ['Motor-Gyro Coherence (' axLabel ')']); set(th, 'Color', [.9 .9 .9]); - legend(ax, motorLbl(1:nMotors), 'TextColor', [.8 .8 .8], 'Color', [.2 .2 .2], ... - 'EdgeColor', [.4 .4 .4], 'FontSize', 11, 'Location', 'northeast'); -end - - -function styleDark(ax, xMax) - set(ax, 'Color', [.1 .1 .1], 'XColor', [.8 .8 .8], 'YColor', [.8 .8 .8], ... - 'FontSize', 14, 'FontWeight', 'bold', 'XLim', [0 xMax]); - grid(ax, 'on'); set(ax, 'GridColor', [.3 .3 .3]); + set(get(ax, 'XLabel'), 'String', 'Hz'); + set(get(ax, 'YLabel'), 'String', 'Coherence'); + title(ax, ['Motor-Gyro Coherence (' axLabel ')']); + h_leg = legend(ax, motorLbl(1:nMotors), 'Location', 'northeast'); + try PSstyleLegend(h_leg, thm); catch, end end diff --git a/src/plot/PSplotPIDerror.m b/src/plot/PSplotPIDerror.m index 65c6fc6..d3a3936 100644 --- a/src/plot/PSplotPIDerror.m +++ b/src/plot/PSplotPIDerror.m @@ -1,194 +1,206 @@ -%% PSplotPIDerror - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -try - -set(PSerrfig, 'pointer', 'watch') - -if ~isempty(filenameA) || ~isempty(filenameB) - %% update fonts - - PSerrfig_pos = get(PSerrfig, 'Position'); - screensz_tmp = get(0,'ScreenSize'); if PSerrfig_pos(3) > 10, PSerrfig_pos(3:4) = PSerrfig_pos(3:4) ./ screensz_tmp(3:4); end - prop_max_screen=(max([PSerrfig_pos(3) PSerrfig_pos(4)])); - fontsz3=round(screensz_multiplier*prop_max_screen); - - set(guiHandlesPIDerr.refresh, 'FontSize', fontsz3); - set(guiHandlesPIDerr.maxSticktext, 'FontSize', fontsz3); - set(guiHandlesPIDerr.maxStick, 'FontSize', fontsz3); - set(guiHandlesPIDerr.saveFig3, 'FontSize', fontsz3); - - - %% PID error distributions - ylab2={'roll';'pitch';'yaw'}; - figure(PSerrfig); - for p=1:3 - delete(subplot('position',posInfo.PIDerrAnalysis(p,:))) - h1=subplot('position',posInfo.PIDerrAnalysis(p,:)); cla - hold on - - if ~isempty(filenameA) - RCRateALL_Thresh_A=abs(DATtmpA.RCRate(1,:)) < maxDegsec & abs(DATtmpA.RCRate(2,:)) < maxDegsec & abs(DATtmpA.RCRate(3,:)) < maxDegsec & abs(DATtmpA.PIDerr(1,:)) < maxDegsec & abs(DATtmpA.PIDerr(2,:)) < maxDegsec & abs(DATtmpA.PIDerr(3,:)) < maxDegsec; - [yA xA]=hist(DATtmpA.PIDerr(p,RCRateALL_Thresh_A),-1000:1:1000); % wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +try + +if ~exist('fnameMaster','var') || isempty(fnameMaster), return; end + +set(PSerrfig, 'pointer', 'watch'); +th = PStheme(); + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + + + fA = get(guiHandlesPIDerr.FileA, 'Value'); + fB = []; + if Nfiles >= 2 && isfield(guiHandlesPIDerr, 'FileB') && ishandle(guiHandlesPIDerr.FileB) + fB = get(guiHandlesPIDerr.FileB, 'Value'); + if fB == fA, fB = []; end + end + + axPIDerr = {'piderr_0_', 'piderr_1_', 'piderr_2_'}; + axSP = {'setpoint_0_', 'setpoint_1_', 'setpoint_2_'}; + + %% PID error distributions + ylab2 = {'roll'; 'pitch'; 'yaw'}; + figure(PSerrfig); + for p = 1:3 + stag_ = sprintf('PSerr_%d', p); + h_old = findobj(PSerrfig, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_old), delete(h_old); end + h1 = axes('Parent', PSerrfig, 'Position', posInfo.PIDerrAnalysis(p,:), 'Tag', stag_); + hold on; + + piderr_A = T{fA}.(axPIDerr{p})(tIND{fA})'; + mask_A = true(size(piderr_A)); + for q = 1:3 + mask_A = mask_A & abs(T{fA}.(axSP{q})(tIND{fA})') < maxDegsec ... + & abs(T{fA}.(axPIDerr{q})(tIND{fA})') < maxDegsec; + end + + [yA, xA] = hist(piderr_A(mask_A), -1000:1:1000); + yA = yA / max(yA); + h = plot(xA, yA); + set(h, 'color', colorA, 'Linewidth', 2); + if p == 3 + set(h1, 'xtick', -40:10:40, 'ytick', 0:.25:1, 'tickdir', 'out', ... + 'xminortick', 'on', 'yminortick', 'on', 'fontsize', fontsz3); + xlabel('PID error (deg/s)', 'fontweight', 'bold'); + else + set(h1, 'xtick', -40:10:40, 'xticklabel', {}, 'ytick', 0:.25:1, ... + 'tickdir', 'out', 'xminortick', 'on', 'yminortick', 'on', 'fontsize', fontsz3); + end + ylabel('normalized freq', 'fontweight', 'bold'); + h = text(-37, .9, ylab2{p}); + set(h, 'fontsize', fontsz3, 'fontweight', 'bold', 'color', th.textPrimary); + grid on; + axis([-40 40 0 1]); + h = text(10, .9, ['[1]s.d.=' num2str(round(std(piderr_A(mask_A))*10)/10)]); + set(h, 'fontsize', fontsz3, 'color', colorA, 'fontweight', 'bold'); + + if ~isempty(fB) + piderr_B = T{fB}.(axPIDerr{p})(tIND{fB})'; + mask_B = true(size(piderr_B)); + for q = 1:3 + mask_B = mask_B & abs(T{fB}.(axSP{q})(tIND{fB})') < maxDegsec ... + & abs(T{fB}.(axPIDerr{q})(tIND{fB})') < maxDegsec; + end + [yB, xB] = hist(piderr_B(mask_B), -1000:1:1000); + yB = yB / max(yB); + h = plot(xB, yB); + set(h, 'color', colorB, 'Linewidth', 2); + h = text(10, .8, ['[2]s.d.=' num2str(round(std(piderr_B(mask_B))*10)/10)]); + set(h, 'fontsize', fontsz3, 'color', colorB, 'fontweight', 'bold'); + + try + [~, pval] = kstest2(yA, yB); + if pval <= .05, sigflag = '*'; else, sigflag = ''; end + h = text(10, .7, ['p=' num2str(pval) sigflag]); + set(h, 'fontsize', fontsz3, 'fontweight', 'bold', 'color', th.textSecondary); + catch + end + end + + box off; + if p == 1, title('normalized PID error distributions'); end + end + + + %% PID error x stick deflection + if ~updateErr + t = [.1 .2 .3 .4 .5 .6 .7 .8 .9 1]; + + sp_A = zeros(3, sum(tIND{fA})); + err_A = zeros(3, sum(tIND{fA})); + for q = 1:3 + sp_A(q,:) = T{fA}.(axSP{q})(tIND{fA})'; + err_A(q,:) = T{fA}.(axPIDerr{q})(tIND{fA})'; + end + maxSP_A = max(abs(sp_A(:))); + + Perr_a_m = zeros(3, length(t)); + Perr_a_se = zeros(3, length(t)); + for i = 1:length(t) + m = maxSP_A * t(i); + msk = abs(sp_A(1,:)) < m & abs(sp_A(2,:)) < m & abs(sp_A(3,:)) < m ... + & abs(err_A(1,:)) < m & abs(err_A(2,:)) < m & abs(err_A(3,:)) < m; + for j = 1:3 + pe = abs(err_A(j, msk)); + Perr_a_m(j,i) = mean(pe); + Perr_a_se(j,i) = std(pe) / sqrt(numel(pe)); + end + end + + if ~isempty(fB) + sp_B = zeros(3, sum(tIND{fB})); + err_B = zeros(3, sum(tIND{fB})); + for q = 1:3 + sp_B(q,:) = T{fB}.(axSP{q})(tIND{fB})'; + err_B(q,:) = T{fB}.(axPIDerr{q})(tIND{fB})'; + end + maxSP_B = max(abs(sp_B(:))); + + Perr_b_m = zeros(3, length(t)); + Perr_b_se = zeros(3, length(t)); + for i = 1:length(t) + m = maxSP_B * t(i); + msk = abs(sp_B(1,:)) < m & abs(sp_B(2,:)) < m & abs(sp_B(3,:)) < m ... + & abs(err_B(1,:)) < m & abs(err_B(2,:)) < m & abs(err_B(3,:)) < m; + for j = 1:3 + pe = abs(err_B(j, msk)); + Perr_b_m(j,i) = mean(pe); + Perr_b_se(j,i) = std(pe) / sqrt(numel(pe)); + end + end + end + updateErr = 0; + end + + %% plot error x stick + ylab = ['R'; 'P'; 'Y']; + for p = 1:3 + stag_ = sprintf('PSerr_%d', p+3); + h_old = findobj(PSerrfig, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_old), delete(h_old); end + h1 = axes('Parent', PSerrfig, 'Position', posInfo.PIDerrAnalysis(p+3,:), 'Tag', stag_); + posAx = .8:1:9.8; + posBx = 1.2:1:10.2; + + minyA = min(Perr_a_m(p,:)) - .5; if isnan(minyA) || minyA < 0, minyA = 0; end + maxyA = max(Perr_a_m(p,:)) + .5; if isnan(maxyA), maxyA = 1; end + h = errorbar(posAx, Perr_a_m(p,:), Perr_a_se(p,:)); hold on; + set(h, 'color', th.axesFg, 'LineStyle', 'none'); + h = bar(posAx, Perr_a_m(p,:)); + set(h, 'facecolor', colorA, 'BarWidth', .4); + set(h1, 'tickdir', 'out', 'xminortick', 'off', 'yminortick', 'on', 'fontsize', fontsz3); + ylabel(['mean |' ylab(p) ' error| ^o/s'], 'fontweight', 'bold'); + set(h1, 'xtick', 0:2:10, 'xticklabel', {''}, 'ygrid', 'on'); + axis([0 11 minyA maxyA]); + box off; + if p == 3 + set(h1, 'xtick', 0:1:10, 'xticklabel', ... + {'0','','20','','40','','60','','80','','100'}); + xlabel('stick deflection (% of max)', 'fontweight', 'bold'); + else + set(h1, 'xtick', 0:1:10, 'xticklabel', ... + {'','','','','','','','','','',''}); + end + + if ~isempty(fB) + minyB = min(Perr_b_m(p,:)) - .5; if isnan(minyB) || minyB < 0, minyB = 0; end + maxyB = max(Perr_b_m(p,:)) + .5; if isnan(maxyB), maxyB = 1; end + h = errorbar(posBx, Perr_b_m(p,:), Perr_b_se(p,:)); + set(h, 'color', th.axesFg, 'LineStyle', 'none'); + h = bar(posBx, Perr_b_m(p,:)); + set(h, 'facecolor', colorB, 'BarWidth', .4); + axis([0 11 min([minyA minyB]) max([maxyA maxyB])]); + box off; + if p == 3 + set(h1, 'xtick', 0:1:10, 'xticklabel', ... + {'0','','20','','40','','60','','80','','100'}); + xlabel('stick deflection (% of max)', 'fontweight', 'bold'); + else + set(h1, 'xtick', 0:1:10, 'xticklabel', ... + {'','','','','','','','','','',''}); + end + end + + if p == 1, title('mean abs PID error X stick deflection'); end + end + +end + +allax = findobj(PSerrfig, 'Type', 'axes'); +for axi = 1:numel(allax), PSstyleAxes(allax(axi), th); end +try PSresizeCP(PSerrfig, []); catch, end +set(PSerrfig, 'pointer', 'arrow'); + +catch err + msgPSplotPIDerror = PSerrorMessages('PSplotPIDerror', err); +end diff --git a/src/plot/PSplotRPMOverlay.m b/src/plot/PSplotRPMOverlay.m index 17deb3d..4549552 100644 --- a/src/plot/PSplotRPMOverlay.m +++ b/src/plot/PSplotRPMOverlay.m @@ -1,83 +1,99 @@ -function PSplotRPMOverlay(ax, rpmMat, xData, imgHeight, freqMax, mode, nHarmonics) -%% PSplotRPMOverlay - overlay RPM filter motor frequencies on spectrogram -% ax - axes handle -% rpmMat - Nx4 matrix [motor1_Hz, motor2_Hz, motor3_Hz, motor4_Hz] -% xData - throttle vector (Nx1) or number of time windows (scalar) -% imgHeight - number of pixel rows in image -% freqMax - max frequency in Hz -% mode - 'throttle' or 'time' -% nHarmonics - number of harmonics to draw (default 3) - -if nargin < 7, nHarmonics = 3; end -if isempty(rpmMat) || imgHeight < 2 || freqMax <= 0 - return; -end - -hold(ax, 'on'); -motorCol = [0 .9 0; .9 .9 0; .9 0 0; .3 .5 1]; % green yellow red blue -lineStyles = {'-'; '--'; ':'}; -hz_per_pixel = freqMax / imgHeight; -nMotors = min(4, size(rpmMat, 2)); - -if strcmp(mode, 'throttle') - for m = 1:nMotors - for harm = 1:nHarmonics - xPts = []; - yPts = []; - for bin = 1:100 - inBin = abs(xData - bin) <= 1; - if any(inBin) - vals = rpmMat(inBin, m) * harm; - vals = vals(vals > 0 & vals < freqMax); - if ~isempty(vals) - avgHz = nanmean(vals); - y_px = imgHeight - round(avgHz / hz_per_pixel); - if y_px >= 1 && y_px <= imgHeight - xPts(end+1) = bin; - yPts(end+1) = y_px; - end - end - end - end - if ~isempty(xPts) - lstyle = lineStyles{min(harm, numel(lineStyles))}; - h = plot(ax, xPts, yPts, lstyle, 'LineWidth', 1); - set(h, 'Color', motorCol(m,:), 'HitTest', 'off'); - end - end - end - -elseif strcmp(mode, 'time') - numWindows = xData; - numSamples = size(rpmMat, 1); - if numSamples == 0 || numWindows == 0, return; end - samplesPerWindow = max(1, round(numSamples / numWindows)); - - for m = 1:nMotors - for harm = 1:nHarmonics - xPts = []; - yPts = []; - for w = 1:numWindows - lo = max(1, round((w-1) * samplesPerWindow) + 1); - hi = min(numSamples, round(w * samplesPerWindow)); - vals = rpmMat(lo:hi, m) * harm; - vals = vals(vals > 0 & vals < freqMax); - if ~isempty(vals) - avgHz = nanmean(vals); - y_px = imgHeight - round(avgHz / hz_per_pixel); - if y_px >= 1 && y_px <= imgHeight - xPts(end+1) = w; - yPts(end+1) = y_px; - end - end - end - if ~isempty(xPts) - lstyle = lineStyles{min(harm, numel(lineStyles))}; - h = plot(ax, xPts, yPts, lstyle, 'LineWidth', 1); - set(h, 'Color', motorCol(m,:), 'HitTest', 'off'); - end - end - end -end - -end +function PSplotRPMOverlay(ax, rpmMat, xData, imgHeight, freqMax, mode, nHarmonics, motors, harmonics, lw) +%% PSplotRPMOverlay - overlay RPM filter motor frequencies on spectrogram +% ax - axes handle +% rpmMat - Nx4 matrix [motor1_Hz, motor2_Hz, motor3_Hz, motor4_Hz] +% xData - throttle vector (Nx1) or number of time windows (scalar) +% imgHeight - number of pixel rows in image +% freqMax - max frequency in Hz +% mode - 'throttle' or 'time' +% nHarmonics - max harmonics (default 3) +% motors - vector of motor indices to draw, e.g. [1 3] (default all) +% harmonics - vector of harmonic indices to draw, e.g. [1 3] (default 1:nHarmonics) +% lw - line width (default 1) + +if nargin < 7 || isempty(nHarmonics), nHarmonics = 3; end +if nargin < 8 || isempty(motors), motors = 1:size(rpmMat, 2); end +if nargin < 9 || isempty(harmonics), harmonics = 1:nHarmonics; end +if nargin < 10 || isempty(lw), lw = 1; end +if isempty(rpmMat) || imgHeight < 2 || freqMax <= 0 + return; +end + +hold(ax, 'on'); +th = PStheme(); +motorCol = cell2mat(th.sigMotor(:)); +lineStyles = {'-'; '--'; ':'}; +hz_per_pixel = freqMax / imgHeight; + +if strcmp(mode, 'throttle') + for mi = 1:numel(motors) + m = motors(mi); + if m > size(rpmMat, 2), continue; end + ci = mod(m-1, size(motorCol, 1)) + 1; + for hi = 1:numel(harmonics) + harm = harmonics(hi); + xPts = []; + yPts = []; + for bin = 1:100 + inBin = abs(xData - bin) <= 1; + if any(inBin) + vals = rpmMat(inBin, m) * harm; + vals = vals(vals > 0 & vals < freqMax); + if ~isempty(vals) + avgHz = nanmean(vals); + y_px = imgHeight - round(avgHz / hz_per_pixel); + if y_px >= 1 && y_px <= imgHeight + xPts(end+1) = bin; + yPts(end+1) = y_px; + end + end + end + end + if numel(yPts) >= 5, yPts = round(smooth(yPts(:), 5))'; end + if ~isempty(xPts) + lstyle = lineStyles{min(harm, numel(lineStyles))}; + plot(ax, xPts, yPts, lstyle, 'LineWidth', lw, ... + 'Color', motorCol(ci,:), 'HitTest', 'off'); + end + end + end + +elseif strcmp(mode, 'time') + numWindows = xData; + numSamples = size(rpmMat, 1); + if numSamples == 0 || numWindows == 0, return; end + samplesPerWindow = max(1, round(numSamples / numWindows)); + + for mi = 1:numel(motors) + m = motors(mi); + if m > size(rpmMat, 2), continue; end + ci = mod(m-1, size(motorCol, 1)) + 1; + for hi = 1:numel(harmonics) + harm = harmonics(hi); + xPts = []; + yPts = []; + for w = 1:numWindows + lo = max(1, round((w-1) * samplesPerWindow) + 1); + hi2 = min(numSamples, round(w * samplesPerWindow)); + vals = rpmMat(lo:hi2, m) * harm; + vals = vals(vals > 0 & vals < freqMax); + if ~isempty(vals) + avgHz = nanmean(vals); + y_px = imgHeight - round(avgHz / hz_per_pixel); + if y_px >= 1 && y_px <= imgHeight + xPts(end+1) = w; + yPts(end+1) = y_px; + end + end + end + if numel(yPts) >= 5, yPts = round(smooth(yPts(:), 5))'; end + if ~isempty(xPts) + lstyle = lineStyles{min(harm, numel(lineStyles))}; + plot(ax, xPts, yPts, lstyle, 'LineWidth', lw, ... + 'Color', motorCol(ci,:), 'HitTest', 'off'); + end + end + end +end + +end diff --git a/src/plot/PSplotSpec.m b/src/plot/PSplotSpec.m index 824b91a..2f81725 100644 --- a/src/plot/PSplotSpec.m +++ b/src/plot/PSplotSpec.m @@ -1,365 +1,469 @@ -%% PSplotSpec - script that computes and plots spectrograms - - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if exist('fnameMaster','var') && ~isempty(fnameMaster) - -PSspecfig_pos = get(PSspecfig, 'Position'); -screensz_tmp = get(0,'ScreenSize'); if PSspecfig_pos(3) > 10, PSspecfig_pos(3:4) = PSspecfig_pos(3:4) ./ screensz_tmp(3:4); end -prop_max_screen=(max([PSspecfig_pos(3) PSspecfig_pos(4)])); -fontsz=(screensz_multiplier*prop_max_screen); -%% update fonts - -f = fields(guiHandlesSpec); -for i = 1 : size(f,1) - try - h = guiHandlesSpec.(f{i}); - if iscell(h) - for ci = 1:numel(h), set(h{ci}, 'FontSize', fontsz); end - else - set(h, 'FontSize', fontsz); - end - catch - end -end - -guiHandlesSpec.climMax_input = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 1))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 1)=str2num(get(guiHandlesSpec.climMax_input, ''String''));updateSpec=1;PSplotSpec;'); - -guiHandlesSpec.climMax_input2 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 2))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input2],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 2)=str2num(get(guiHandlesSpec.climMax_input2, ''String''));updateSpec=1;PSplotSpec;'); - -guiHandlesSpec.climMax_input3 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 3))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input3],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 3)=str2num(get(guiHandlesSpec.climMax_input3, ''String''));updateSpec=1;PSplotSpec;'); - -guiHandlesSpec.climMax_input4 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 4))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input4],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 4)=str2num(get(guiHandlesSpec.climMax_input4, ''String''));updateSpec=1;PSplotSpec;'); - -%% - -s1={'';'gyroADC';'debug';'piderr';'setpoint';'axisP';'axisD';'axisDpf';'pidsum'}; - -datSelectionString=[s1]; - -clear vars -for i=1:4 - vars(i)=get(guiHandlesSpec.SpecSelect{i}, 'Value'); -end - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% compute fft %%%%%%%%%%%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -if get(guiHandlesSpec.SpecSelect{1}, 'Value')>1 || get(guiHandlesSpec.SpecSelect{2}, 'Value')>1 || get(guiHandlesSpec.SpecSelect{3}, 'Value')>1 || get(guiHandlesSpec.SpecSelect{4}, 'Value')>1 - set(PSspecfig, 'pointer', 'watch') - if updateSpec==0 - clear s dat ampmat amp2d freq a RC smat amp2d freq2d Throt - p=0; - hw = waitbar(0,['please wait... ' ]); - - tmpPSDVal = get(guiHandlesSpec.checkboxPSD, 'Value'); - for k=1:length(vars) - tmpFileSelK = get(guiHandlesSpec.FileSelect{k}, 'Value'); - s=char(datSelectionString(vars(k))); - for a=1:3, - if ( ( ~isempty(strfind(s,'axisD'))) && a==3) || isempty(s) - p=p+1; - smat{p}=[];%string - ampmat{p}=[];%spec matrix - freq{p}=[];% freq matrix - amp2d{p}=[];%spec 2d - freq2d{p}=[];% freq2d - else - p=p+1; - try - eval(['dat{k}(a,:) = T{tmpFileSelK}.' char(datSelectionString(vars(k))) '_' int2str(a-1) '_(tIND{tmpFileSelK});';]) - Throt=T{tmpFileSelK}.setpoint_3_(tIND{tmpFileSelK}) / 10;% throttle - lograte = A_lograte(tmpFileSelK);%in kHz - waitbar(min(1, p/12), hw, ['processing spectrogram... ' int2str(p) ]); - smat{p}=s; - [freq{p} ampmat{p}]=PSthrSpec(Throt, dat{k}(a,:), lograte, tmpPSDVal); % compute matrices - [freq2d{p} amp2d{p}]=PSSpec2d(dat{k}(a,:),lograte, tmpPSDVal); %compute 2d amp spec at same time - catch ME - warning('PSplotSpec compute p=%d: %s', p, ME.message); - smat{p}=[]; ampmat{p}=[]; freq{p}=[]; amp2d{p}=[]; freq2d{p}=[]; - end - end - end - end - close(hw) - end -else - hwarn=warndlg({'Dropdowns set to ''NONE''.'; 'Please select a preset or specific variables to analyze.'}); - pause(3); - try - close(hwarn); - catch - end -end - -if get(guiHandlesSpec.checkbox2d, 'Value')==0 && ~isempty(ampmat) - figure(PSspecfig); - %%%%% plot spec mattrices - c1=[1 1 1 2 2 2 3 3 3 4 4 4]; - c2=[1 2 3 1 2 3 1 2 3 1 2 3]; - baselineY = [0 -40]; - ftr = fspecial('gaussian',[get(guiHandlesSpec.smoothFactor_select, 'Value')*5 get(guiHandlesSpec.smoothFactor_select, 'Value')],4); - for p=1:size(ampmat,2) - try delete(subplot('position',posInfo.SpecPos(p,:))); catch, end - if ~isempty(ampmat{p}) - try - delete(subplot('position',posInfo.SpecPos(p,:))); - h1=subplot('position',posInfo.SpecPos(p,:)); cla - img = flipud((filter2(ftr, ampmat{p} ))') + baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1); - imagesc(img); - - lograte=A_lograte(get(guiHandlesSpec.FileSelect{c1(p)}, 'Value')); - - axLabel={'roll';'pitch';'yaw'}; - - if get(guiHandlesSpec.Sub100HzCheck{c1(p)}, 'Value')==1 - hold on;h=plot([0 100],[size(ampmat{p},2)-round(Flim1/3.33) size(ampmat{p},2)-round(Flim1/3.33)],'y--');set(h,'linewidth',2) - hold on;h=plot([0 100],[size(ampmat{p},2)-round(Flim2/3.33) size(ampmat{p},2)-round(Flim2/3.33)],'y--');set(h,'linewidth',2) - % sub100Hz scaling - xticks=[1 size(ampmat{p},1)/5:size(ampmat{p},1)/5:size(ampmat{p},1)]; - yticks=[(size(ampmat{p},2)-30):6:size(ampmat{p},2)]; - set(h1,'PlotBoxAspectRatioMode','auto','ylim',[size(ampmat{p},2)-30 size(ampmat{p},2)]) - set(h1,'fontsize',fontsz,'CLim',[baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'YTick',yticks,'yticklabel',{'100';'80';'60';'40';'20';'0'},'XTick',xticks,'xticklabel',{'0';'20';'40';'60';'80';'100'},'tickdir','out','xminortick','on','yminortick','on'); - a=[];a2=[];a=filter2(ftr, ampmat{p}) + baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1); - a2 = a(:,(round(Flim1/3.33))+1:(round(Flim2/3.33))); - meanspec=nanmean(a2(:)); - peakspec=max(max(a(:,(round(Flim1/3.33))+1:(round(Flim2/3.33))))); - if get(guiHandlesSpec.ColormapSelect, 'Value')==8 || get(guiHandlesSpec.ColormapSelect, 'Value')==9 - h=text(64,(size(ampmat{p},2)-30)+3,['mean=' num2str(meanspec,3)]); - set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); - h=text(64,(size(ampmat{p},2)-30)+1,['peak=' num2str(peakspec,3)]); - set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); - else - h=text(64,(size(ampmat{p},2)-30)+3,['mean=' num2str(meanspec,3)]); - set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); - h=text(64,(size(ampmat{p},2)-30)+1,['peak=' num2str(peakspec,3)]); - set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); - end - h=text(xticks(1)+1,(size(ampmat{p},2)-30)+1,axLabel{c2(p)}); - set(h,'Color',[1 1 1],'fontsize',fontsz,'fontweight','bold') - else % full scaling - xticks=[1 size(ampmat{p},1)/5:size(ampmat{p},1)/5:size(ampmat{p},1)]; - yticks=[1:(size(ampmat{p},2))/10:size(ampmat{p},2) size(ampmat{p},2)]; - maxHz = max(round(yticks * 3.333)); - ytlbl = {num2str(maxHz), '', num2str(round(maxHz*4/5)), '', num2str(round(maxHz*3/5)), '', num2str(round(maxHz*2/5)), '', num2str(round(maxHz*1/5)), '', '0'}; - set(h1,'fontsize',fontsz,'CLim',[baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'YTick',yticks,'yticklabel',ytlbl,'XTick',xticks,'xticklabel',{'0';'20';'40';'60';'80';'100'},'tickdir','out','xminortick','on','yminortick','on'); - set(h1,'PlotBoxAspectRatioMode','auto','ylim',[1 size(ampmat{p},2)]) - a=[];a2=[];a=filter2(ftr, ampmat{p}) + baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1); - a2 = a(:,(size(ampmat{p},2)/10):size(ampmat{p},2)); - meanspec=nanmean(a2(:)); - peakspec=max(max(a(:,(size(ampmat{p},2)/10):size(ampmat{p},2)))); - if get(guiHandlesSpec.ColormapSelect, 'Value')==8 || get(guiHandlesSpec.ColormapSelect, 'Value')==9 - h=text(64,size(ampmat{p},2)*.04,['mean=' num2str(meanspec,3)]); - set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); - h=text(64,size(ampmat{p},2)*.13,['peak=' num2str(peakspec,3)]); - set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); - else - h=text(64,size(ampmat{p},2)*.04,['mean=' num2str(meanspec,3)]); - set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); - h=text(64,size(ampmat{p},2)*.13,['peak=' num2str(peakspec,3)]); - set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); - end - h=text(xticks(1)+1,size(ampmat{p},2)*.04,axLabel{c2(p)}); - set(h,'Color',[1 1 1],'fontsize',fontsz,'fontweight','bold') - end - - - grid on - ax = gca; - set(ax, 'GridColor', [1 1 1]); - if get(guiHandlesSpec.ColormapSelect, 'Value')==8 || get(guiHandlesSpec.ColormapSelect, 'Value')==9 - set(ax, 'GridColor', [0 0 0]); % black on white background - set(h,'Color',[0 0 0],'fontsize',fontsz,'fontweight','bold') - end - ylabel('Frequency (Hz)','fontweight','bold') - xlabel('% Throttle','fontweight','bold') - - %% Dynamic notch overlay for FFT_FREQ mode - if exist('notchData','var') && exist('debugmode','var') && exist('debugIdx','var') - tmpFileK = get(guiHandlesSpec.FileSelect{c1(p)}, 'Value'); - tmpFFTk = FFT_FREQ; - if numel(debugIdx) >= tmpFileK - tmpFFTk = debugIdx{tmpFileK}.FFT_FREQ; - end - if debugmode(tmpFileK) == tmpFFTk && numel(notchData) >= tmpFileK && ~isempty(notchData{tmpFileK}) - % Only overlay on the axis matching gyro_debug_axis - tmpGdaK = 0; - if exist('gyro_debug_axis','var') && numel(gyro_debug_axis) >= tmpFileK - tmpGdaK = gyro_debug_axis(tmpFileK); - end - if (c2(p) - 1) == tmpGdaK - maxHzOverlay = (A_lograte(tmpFileK) / 2) * 1000; - PSplotDynNotchOverlay(gca, notchData{tmpFileK}, T{tmpFileK}.setpoint_3_(tIND{tmpFileK}) / 10, size(img, 1), maxHzOverlay, 'throttle'); - end - end - end - - %% RPM filter overlay (motor frequencies + harmonics) - if exist('rpmFilterData','var') && exist('debugmode','var') && exist('debugIdx','var') - tmpFileK2 = get(guiHandlesSpec.FileSelect{c1(p)}, 'Value'); - tmpRPMk = 46; - if numel(debugIdx) >= tmpFileK2 - tmpRPMk = debugIdx{tmpFileK2}.RPM_FILTER; - end - if debugmode(tmpFileK2) == tmpRPMk && numel(rpmFilterData) >= tmpFileK2 && ~isempty(rpmFilterData{tmpFileK2}) - maxHzOverlay2 = (A_lograte(tmpFileK2) / 2) * 1000; - PSplotRPMOverlay(gca, rpmFilterData{tmpFileK2}, T{tmpFileK2}.setpoint_3_(tIND{tmpFileK2}) / 10, size(img, 1), maxHzOverlay2, 'throttle'); - end - end - - %% Estimated RPM overlay from spectrum peak detection - if get(guiHandlesSpec.checkboxEstRPM, 'Value') && ~isempty(ampmat{p}) && ~isempty(freq{p}) - % find a throttle row that has freq data (not all zeros) - freqAx = []; - for rr = 1:size(freq{p}, 1) - if any(freq{p}(rr,:) > 0), freqAx = freq{p}(rr,:); break; end - end - if ~isempty(freqAx) - [estFund, estHarm] = PSestimateRPM(freqAx, ampmat{p}, 3); - % convert Hz to image pixel coords (img is flipped: row 1 = max Hz) - maxHzEst = (A_lograte(get(guiHandlesSpec.FileSelect{c1(p)}, 'Value')) / 2) * 1000; - hz_per_px = maxHzEst / size(img, 1); - hold on; - estCol = [0 .9 .2; .9 .7 0; .9 .2 0]; % green, orange, red for 1st/2nd/3rd - estStyle = {'-'; '--'; ':'}; - for nh = 1:3 - xPts = []; yPts = []; - for tb = 1:100 - if ~isnan(estHarm(tb, nh)) && estHarm(tb, nh) > 0 && estHarm(tb, nh) < maxHzEst - y_px = size(img, 1) - round(estHarm(tb, nh) / hz_per_px); - if y_px >= 1 && y_px <= size(img, 1) - xPts(end+1) = tb; - yPts(end+1) = y_px; - end - end - end - if ~isempty(xPts) - h = plot(xPts, yPts, estStyle{nh}, 'LineWidth', 1.5); - set(h, 'Color', estCol(nh,:), 'HitTest', 'off'); - end - end - end % ~isempty(freqAx) - end - - catch ME - warning('PSplotSpec render p=%d: %s', p, ME.message); - end - end - end - - % color bar2 at the top - try - delete(hCbar1);delete(hCbar2);delete(hCbar3);delete(hCbar4) - catch - end - if vars(1)>1 % 1=none - subplot('position',posInfo.SpecPos(1,:)); - hCbar1= colorbar('NorthOutside'); - set(hCbar1,'Position', [posInfo.hCbar1pos]); - end - if vars(2)>1 % 1=none - subplot('position',posInfo.SpecPos(4,:)); - hCbar2= colorbar('NorthOutside'); - set(hCbar2,'Position', [posInfo.hCbar2pos]) - end - if vars(3)>1 % 1=none - subplot('position',posInfo.SpecPos(7,:)); - hCbar3= colorbar('NorthOutside'); - set(hCbar3,'Position', [posInfo.hCbar3pos]) - end - if vars(4)>1 % 1=none - subplot('position',posInfo.SpecPos(10,:)); - hCbar4= colorbar('NorthOutside'); - set(hCbar4,'Position', [posInfo.hCbar4pos]) - end - - % color maps - use set() to avoid stale colorbar listener errors in colormap() - try - tmpCmapVal = get(guiHandlesSpec.ColormapSelect, 'Value'); - if tmpCmapVal <= 7 - tmpCmapStr = get(guiHandlesSpec.ColormapSelect, 'String'); - cm = feval(char(tmpCmapStr(tmpCmapVal)), 64); - elseif tmpCmapVal == 8 - cm = linearREDcmap; - else - cm = linearGREYcmap; - end - set(PSspecfig, 'Colormap', cm); - catch, end - -end - -if get(guiHandlesSpec.checkbox2d, 'Value')==1 && ~isempty(amp2d) - figure(PSspecfig); - try - delete(hCbar1);delete(hCbar2);delete(hCbar3);delete(hCbar4) - catch - end - baselineYlines = [0 -50]; - c1=[1 1 1 2 2 2 3 3 3 4 4 4]; - c2=[1 2 3 1 2 3 1 2 3 1 2 3]; - %%%%% plot 2d amp spec - for p=1:size(amp2d,2) - axLabel={'roll';'pitch';'yaw'}; - - delete(subplot('position',posInfo.SpecPos(p,:))); - if ~isempty(amp2d{p}) - h2=subplot('position',posInfo.SpecPos(p,:)); cla - h=plot(freq2d{p}, smooth(amp2d{p}, log10(size(amp2d{p},1)) * (get(guiHandlesSpec.smoothFactor_select, 'Value')^2), 'lowess'));hold on - set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/2) - set(h2,'fontsize',fontsz,'fontweight','bold') - if get(guiHandlesSpec.specPresets, 'Value') <= 3 - set(h,'Color',[SpecLineCols(c1(p),:,1)]) - end - if get(guiHandlesSpec.specPresets, 'Value') > 4 && get(guiHandlesSpec.specPresets, 'Value') <= 6 - set(h,'Color',[SpecLineCols(c1(p),:,2)]) - end - if get(guiHandlesSpec.specPresets, 'Value') > 6 - set(h,'Color',[SpecLineCols(c1(p),:,3)]) - end - - if get(guiHandlesSpec.Sub100HzCheck{c1(p)}, 'Value')==1 - set(h2,'xtick',[0 20 40 60 80 100], 'yminortick','on') - axis([0 100 baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))]) - h=plot([round(Flim1) round(Flim1)],[baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'k--'); - set(h,'linewidth',1) - h=plot([round(Flim2) round(Flim2)],[baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'k--'); - set(h,'linewidth',1) - else - set(h2,'xtick',[0 : ((A_lograte(get(guiHandlesSpec.FileSelect{k}, 'Value')) / 2) * 1000 / 5) : (A_lograte(get(guiHandlesSpec.FileSelect{k}, 'Value')) / 2) * 1000],'yminortick','on') - axis([0 (A_lograte(get(guiHandlesSpec.FileSelect{k}, 'Value')) / 2) * 1000 baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))]) - end - - xlabel('Frequency (Hz)') - if get(guiHandlesSpec.checkboxPSD, 'Value') - ylabel(['PSD (dB)']) - else - ylabel(['Amplitude']) - end - - - h=text(2,climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))*.95,axLabel{c2(p)}); - set(h,'Color',[.2 .2 .2],'fontsize',fontsz,'fontweight','bold') - - grid on - end - end -end -% Set up click-to-show-value datatips + double-click expand on all axes -PSdatatipSetup(PSspecfig); - -set(PSspecfig, 'pointer', 'arrow') -updateSpec=0; - -end - +%% PSplotSpec - script that computes and plots spectrograms + + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + +th = PStheme(); + +%% Read RPM overlay controls +rpmShowDN = true; rpmMotors = [1 2 3 4]; rpmHarms = [1 2 3]; rpmLw = 1; rpmShowEst = false; +if exist('guiHandlesSpec','var') + try rpmShowDN = get(guiHandlesSpec.rpmDynNotch, 'Value'); catch, end + try + rpmMotors = []; + nMot_ = 4; try nMot_ = guiHandlesSpec.nMotors; catch, end + for mi_ = 1:4 + if get(guiHandlesSpec.(sprintf('rpmMotor%d',mi_)), 'Value') + rpmMotors(end+1) = mi_; + if nMot_ > 4, rpmMotors(end+1) = mi_ + nMot_/2; end + end + end + catch, rpmMotors = [1 2 3 4]; end + try + harmSel = get(guiHandlesSpec.rpmHarmDd, 'Value'); + harmMap = {[], [1], [2], [3], [1 2], [1 3], [2 3], [1 2 3]}; + rpmHarms = harmMap{harmSel}; + catch, rpmHarms = [1 2 3]; end + try + lwSel = get(guiHandlesSpec.rpmLwDd, 'Value'); + lwMap = [0.5 1 1.5 2]; + rpmLw = lwMap(lwSel); + catch, rpmLw = 1; end + try rpmShowEst = get(guiHandlesSpec.rpmEstChk, 'Value'); catch, end +end + +%% Compute RPM/notch data on-demand (mirrors PSplotSpec2D logic) +if exist('debugmode','var') && exist('debugIdx','var') && exist('T','var') && exist('tIND','var') + if ~exist('notchData','var'), notchData = {}; end + if ~exist('rpmFilterData','var'), rpmFilterData = {}; end + for k_ = 1:numel(T) + if numel(notchData) < k_ || isempty(notchData{k_}) + tmpFFT_ = FFT_FREQ; + if numel(debugIdx) >= k_, tmpFFT_ = debugIdx{k_}.FFT_FREQ; end + if debugmode(k_) == tmpFFT_ + try + if exist('fwMajor','var') && numel(fwMajor) >= k_ && fwMajor(k_) >= 2025 + notchData{k_} = [T{k_}.debug_1_(tIND{k_}), T{k_}.debug_2_(tIND{k_}), T{k_}.debug_3_(tIND{k_})]; + else + notchData{k_} = [T{k_}.debug_0_(tIND{k_}), T{k_}.debug_1_(tIND{k_}), T{k_}.debug_2_(tIND{k_})]; + end + catch, notchData{k_} = []; end + end + end + if numel(rpmFilterData) < k_ || isempty(rpmFilterData{k_}) + tmpRPM_ = 46; + if numel(debugIdx) >= k_, tmpRPM_ = debugIdx{k_}.RPM_FILTER; end + if debugmode(k_) == tmpRPM_ + try rpmFilterData{k_} = [T{k_}.debug_0_(tIND{k_}), T{k_}.debug_1_(tIND{k_}), T{k_}.debug_2_(tIND{k_}), T{k_}.debug_3_(tIND{k_})]; + catch, rpmFilterData{k_} = []; end + end + % eRPM fallback when RPM_FILTER debug mode not active + if (numel(rpmFilterData) < k_ || isempty(rpmFilterData{k_})) && isfield(T{k_}, 'eRPM_0_') + mPoles_ = 14; + try mp_ = find(strcmp(SetupInfo{k_}(:,1), 'motor_poles')); + if ~isempty(mp_), mPoles_ = str2double(SetupInfo{k_}(mp_(1),2)); end + catch, end + if mPoles_ < 2, mPoles_ = 14; end + try + nEm_ = 0; + for mi_ = 0:7 + if isfield(T{k_}, ['eRPM_' int2str(mi_) '_']), nEm_ = mi_+1; end + end + rpmHz_ = zeros(sum(tIND{k_}), nEm_); + for mi_ = 0:nEm_-1 + ef_ = ['eRPM_' int2str(mi_) '_']; + if isfield(T{k_}, ef_) + rpmHz_(:, mi_+1) = T{k_}.(ef_)(tIND{k_}) * 100 / (mPoles_/2) / 60; + end + end + rpmFilterData{k_} = rpmHz_; + catch, rpmFilterData{k_} = []; end + end + end + end +end + +psdIdx = get(guiHandlesSpec.checkboxPSD, 'Value') + 1; +set(guiHandlesSpec.climMax_input, 'String', num2str(climScale(psdIdx, 1))); +set(guiHandlesSpec.climMax_input2, 'String', num2str(climScale(psdIdx, 2))); +set(guiHandlesSpec.climMax_input3, 'String', num2str(climScale(psdIdx, 3))); +set(guiHandlesSpec.climMax_input4, 'String', num2str(climScale(psdIdx, 4))); + +%% + +s1={'';'gyroADC';'gyroPrefilt';'piderr';'setpoint';'axisP';'axisD';'axisDpf';'pidsum'}; + +datSelectionString=[s1]; + +clear vars +for i=1:4 + vars(i)=get(guiHandlesSpec.SpecSelect{i}, 'Value'); +end + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%% compute fft %%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +if get(guiHandlesSpec.SpecSelect{1}, 'Value')>1 || get(guiHandlesSpec.SpecSelect{2}, 'Value')>1 || get(guiHandlesSpec.SpecSelect{3}, 'Value')>1 || get(guiHandlesSpec.SpecSelect{4}, 'Value')>1 + set(PSspecfig, 'pointer', 'watch') + if updateSpec==0 + clear s dat ampmat amp2d freq a RC smat amp2d freq2d Throt + p=0; + hw = waitbar(0,['please wait... ' ]); + + tmpPSDVal = get(guiHandlesSpec.checkboxPSD, 'Value'); + for k=1:length(vars) + tmpFileSelK = get(guiHandlesSpec.FileSelect{k}, 'Value'); + s=char(datSelectionString(vars(k))); + for a=1:3, + if ( ( ~isempty(strfind(s,'axisD'))) && a==3) || isempty(s) + p=p+1; + smat{p}=[];%string + ampmat{p}=[];%spec matrix + freq{p}=[];% freq matrix + amp2d{p}=[];%spec 2d + freq2d{p}=[];% freq2d + else + p=p+1; + try + fld = [char(datSelectionString(vars(k))) '_' int2str(a-1) '_']; + dat{k}(a,:) = T{tmpFileSelK}.(fld)(tIND{tmpFileSelK}); + Throt=T{tmpFileSelK}.setpoint_3_(tIND{tmpFileSelK}) / 10;% throttle + lograte = A_lograte(tmpFileSelK);%in kHz + waitbar(min(1, p/12), hw, ['processing spectrogram... ' int2str(p) ]); + smat{p}=s; + [freq{p} ampmat{p}]=PSthrSpec(Throt, dat{k}(a,:), lograte, tmpPSDVal); % compute matrices + [freq2d{p} amp2d{p}]=PSSpec2d(dat{k}(a,:),lograte, tmpPSDVal); %compute 2d amp spec at same time + catch ME + warning('PSplotSpec compute p=%d: %s', p, ME.message); + smat{p}=[]; ampmat{p}=[]; freq{p}=[]; amp2d{p}=[]; freq2d{p}=[]; + end + end + end + end + close(hw) + end +else + warndlg({'Dropdowns set to ''NONE''.'; 'Please select a preset or specific variables to analyze.'}); +end + +if get(guiHandlesSpec.checkbox2d, 'Value')==0 && ~isempty(ampmat) + figure(PSspecfig); + %%%%% plot spec mattrices + c1=[1 1 1 2 2 2 3 3 3 4 4 4]; + c2=[1 2 3 1 2 3 1 2 3 1 2 3]; + baselineY = [0 -40]; + ftr = fspecial('gaussian',[get(guiHandlesSpec.smoothFactor_select, 'Value')*5 get(guiHandlesSpec.smoothFactor_select, 'Value')],4); + for p=1:size(ampmat,2) + stag_ = sprintf('PSspec_%d', p); + h_old = findobj(PSspecfig, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_old), delete(h_old); end + if ~isempty(ampmat{p}) + try + h1 = axes('Parent', PSspecfig, 'Position', posInfo.SpecPos(p,:), 'Tag', stag_); + img = flipud((filter2(ftr, ampmat{p} ))') + baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1); + imagesc(img); + + lograte=A_lograte(get(guiHandlesSpec.FileSelect{c1(p)}, 'Value')); + + axLabel={'roll';'pitch';'yaw'}; + + if get(guiHandlesSpec.Sub100HzCheck{c1(p)}, 'Value')==1 + hold on;h=plot([0 100],[size(ampmat{p},2)-round(Flim1/3.33) size(ampmat{p},2)-round(Flim1/3.33)],'y--');set(h,'linewidth',2) + hold on;h=plot([0 100],[size(ampmat{p},2)-round(Flim2/3.33) size(ampmat{p},2)-round(Flim2/3.33)],'y--');set(h,'linewidth',2) + % sub100Hz scaling + xticks=round([1 size(ampmat{p},1)/5:size(ampmat{p},1)/5:size(ampmat{p},1)]); + yticks=round([(size(ampmat{p},2)-30):6:size(ampmat{p},2)]); + set(h1,'PlotBoxAspectRatioMode','auto','ylim',[size(ampmat{p},2)-30 size(ampmat{p},2)]) + set(h1,'fontsize',fontsz,'CLim',[baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'YTick',yticks,'yticklabel',{'100';'80';'60';'40';'20';'0'},'XTick',xticks,'xticklabel',{'0';'20';'40';'60';'80';'100'},'tickdir','out','xminortick','on','yminortick','on'); + a=[];a2=[];a=filter2(ftr, ampmat{p}) + baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1); + a2 = a(:,(round(Flim1/3.33))+1:(round(Flim2/3.33))); + meanspec=nanmean(a2(:)); + peakspec=max(max(a(:,(round(Flim1/3.33))+1:(round(Flim2/3.33))))); + if get(guiHandlesSpec.ColormapSelect, 'Value')==8 || get(guiHandlesSpec.ColormapSelect, 'Value')==9 + h=text(64,(size(ampmat{p},2)-30)+3,['mean=' num2str(meanspec,3)]); + set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); + h=text(64,(size(ampmat{p},2)-30)+1,['peak=' num2str(peakspec,3)]); + set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); + else + h=text(64,(size(ampmat{p},2)-30)+3,['mean=' num2str(meanspec,3)]); + set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); + h=text(64,(size(ampmat{p},2)-30)+1,['peak=' num2str(peakspec,3)]); + set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); + end + h=text(xticks(1)+1,(size(ampmat{p},2)-30)+1,axLabel{c2(p)}); + set(h,'Color',[1 1 1],'fontsize',fontsz,'fontweight','bold') + % TPA diagnostic annotation + if (c2(p) == 1 || c2(p) == 2) && ~isempty(smat{p}) && any(strcmp(smat{p}, {'gyroADC','gyroPrefilt','piderr'})) + try + freqAx_ = []; + for rr_ = 1:size(freq{p},1) + if any(freq{p}(rr_,:) > 0), freqAx_ = freq{p}(rr_,:); break; end + end + if ~isempty(freqAx_) + [tpaF_, tpaOn_, tpaR_] = PStpaDetect(ampmat{p}, freqAx_, tmpPSDVal); + if tpaF_ + tpaRate_ = 0; + tmpFK_ = get(guiHandlesSpec.FileSelect{c1(p)}, 'Value'); + try idx_ = find(strcmp(SetupInfo{tmpFK_}(:,1), 'tpa_rate')); + if ~isempty(idx_), tpaRate_ = str2double(SetupInfo{tmpFK_}(idx_(1),2)); end + catch, end + if tpaRate_ == 0 + tpaStr_ = sprintf('TPA: +%ddB >%d%%thr', round(tpaR_), tpaOn_); + else + tpaStr_ = sprintf('TPA active, +%ddB', round(tpaR_)); + end + h = text(36, (size(ampmat{p},2)-30)+5, tpaStr_); + set(h, 'Color', [1 .85 0], 'fontsize', fontsz, 'fontweight', 'bold', 'Tag', 'tpaDiag'); + end + end + catch, end + end + else % full scaling + xticks=round([1 size(ampmat{p},1)/5:size(ampmat{p},1)/5:size(ampmat{p},1)]); + yticks=round([1:(size(ampmat{p},2))/10:size(ampmat{p},2) size(ampmat{p},2)]); + maxHz = max(round(yticks * 3.333)); + ytlbl = {num2str(maxHz), '', num2str(round(maxHz*4/5)), '', num2str(round(maxHz*3/5)), '', num2str(round(maxHz*2/5)), '', num2str(round(maxHz*1/5)), '', '0'}; + set(h1,'fontsize',fontsz,'CLim',[baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'YTick',yticks,'yticklabel',ytlbl,'XTick',xticks,'xticklabel',{'0';'20';'40';'60';'80';'100'},'tickdir','out','xminortick','on','yminortick','on'); + set(h1,'PlotBoxAspectRatioMode','auto','ylim',[1 size(ampmat{p},2)]) + a=[];a2=[];a=filter2(ftr, ampmat{p}) + baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1); + a2 = a(:,round(size(ampmat{p},2)/10):size(ampmat{p},2)); + meanspec=nanmean(a2(:)); + peakspec=max(max(a(:,round(size(ampmat{p},2)/10):size(ampmat{p},2)))); + if get(guiHandlesSpec.ColormapSelect, 'Value')==8 || get(guiHandlesSpec.ColormapSelect, 'Value')==9 + h=text(64,size(ampmat{p},2)*.04,['mean=' num2str(meanspec,3)]); + set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); + h=text(64,size(ampmat{p},2)*.13,['peak=' num2str(peakspec,3)]); + set(h,'Color','k','fontsize',fontsz,'fontweight','bold'); + else + h=text(64,size(ampmat{p},2)*.04,['mean=' num2str(meanspec,3)]); + set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); + h=text(64,size(ampmat{p},2)*.13,['peak=' num2str(peakspec,3)]); + set(h,'Color','w','fontsize',fontsz,'fontweight','bold'); + end + h=text(xticks(1)+1,size(ampmat{p},2)*.04,axLabel{c2(p)}); + set(h,'Color',[1 1 1],'fontsize',fontsz,'fontweight','bold') + % TPA diagnostic annotation (full scale) + if (c2(p) == 1 || c2(p) == 2) && ~isempty(smat{p}) && any(strcmp(smat{p}, {'gyroADC','gyroPrefilt','piderr'})) + try + freqAx_ = []; + for rr_ = 1:size(freq{p},1) + if any(freq{p}(rr_,:) > 0), freqAx_ = freq{p}(rr_,:); break; end + end + if ~isempty(freqAx_) + [tpaF_, tpaOn_, tpaR_] = PStpaDetect(ampmat{p}, freqAx_, tmpPSDVal); + if tpaF_ + tpaRate_ = 0; + tmpFK_ = get(guiHandlesSpec.FileSelect{c1(p)}, 'Value'); + try idx_ = find(strcmp(SetupInfo{tmpFK_}(:,1), 'tpa_rate')); + if ~isempty(idx_), tpaRate_ = str2double(SetupInfo{tmpFK_}(idx_(1),2)); end + catch, end + if tpaRate_ == 0 + tpaStr_ = sprintf('TPA: +%ddB >%d%%thr', round(tpaR_), tpaOn_); + else + tpaStr_ = sprintf('TPA active, +%ddB', round(tpaR_)); + end + h = text(36, size(ampmat{p},2)*.22, tpaStr_); + set(h, 'Color', [1 .85 0], 'fontsize', fontsz, 'fontweight', 'bold', 'Tag', 'tpaDiag'); + end + end + catch, end + end + end + + + grid on + ax = gca; + PSstyleAxes(ax, th); + set(ax, 'GridColor', [1 1 1]); + if get(guiHandlesSpec.ColormapSelect, 'Value')==8 || get(guiHandlesSpec.ColormapSelect, 'Value')==9 + set(ax, 'GridColor', [0 0 0]); + set(h,'Color',[0 0 0],'fontsize',fontsz,'fontweight','bold') + end + ylabel('Frequency (Hz)','fontweight','bold','Color',th.textPrimary) + xlabel('% Throttle','fontweight','bold','Color',th.textPrimary) + + %% Dynamic notch overlay for FFT_FREQ mode + if rpmShowDN && exist('notchData','var') && exist('debugmode','var') && exist('debugIdx','var') + tmpFileK = get(guiHandlesSpec.FileSelect{c1(p)}, 'Value'); + tmpFFTk = FFT_FREQ; + if numel(debugIdx) >= tmpFileK + tmpFFTk = debugIdx{tmpFileK}.FFT_FREQ; + end + if debugmode(tmpFileK) == tmpFFTk && numel(notchData) >= tmpFileK && ~isempty(notchData{tmpFileK}) + maxHzOverlay = (A_lograte(tmpFileK) / 2) * 1000; + PSplotDynNotchOverlay(gca, notchData{tmpFileK}, T{tmpFileK}.setpoint_3_(tIND{tmpFileK}) / 10, size(img, 1), maxHzOverlay, 'throttle', rpmLw); + end + end + + %% RPM filter overlay (motor frequencies + harmonics) + if ~isempty(rpmHarms) && ~isempty(rpmMotors) && exist('rpmFilterData','var') + tmpFileK2 = get(guiHandlesSpec.FileSelect{c1(p)}, 'Value'); + if numel(rpmFilterData) >= tmpFileK2 && ~isempty(rpmFilterData{tmpFileK2}) + maxHzOverlay2 = (A_lograte(tmpFileK2) / 2) * 1000; + PSplotRPMOverlay(gca, rpmFilterData{tmpFileK2}, T{tmpFileK2}.setpoint_3_(tIND{tmpFileK2}) / 10, size(img, 1), maxHzOverlay2, 'throttle', 3, rpmMotors, rpmHarms, rpmLw); + end + end + + %% Estimated RPM overlay from spectrum peak detection + if rpmShowEst && ~isempty(rpmHarms) && ~isempty(ampmat{p}) && ~isempty(freq{p}) + % find a throttle row that has freq data (not all zeros) + freqAx = []; + for rr = 1:size(freq{p}, 1) + if any(freq{p}(rr,:) > 0), freqAx = freq{p}(rr,:); break; end + end + if ~isempty(freqAx) + [estFund, estHarm] = PSestimateRPM(freqAx, ampmat{p}, 3); + % smooth estimated harmonics (moving average, NaN-safe) + smK = 7; + for sc = 1:size(estHarm, 2) + col = estHarm(:, sc); + sm = col; + for sw = 1:numel(col) + lo = max(1, sw - floor(smK/2)); + hi = min(numel(col), sw + floor(smK/2)); + chunk = col(lo:hi); + chunk = chunk(~isnan(chunk) & chunk > 0); + if numel(chunk) >= 2, sm(sw) = mean(chunk); else sm(sw) = NaN; end + end + estHarm(:, sc) = sm; + end + maxHzEst = (A_lograte(get(guiHandlesSpec.FileSelect{c1(p)}, 'Value')) / 2) * 1000; + hz_per_px = maxHzEst / size(img, 1); + hold on; + estCol = [0 .9 .2; .9 .7 0; .9 .2 0]; + estStyle = {'-'; '--'; ':'}; + for nh = rpmHarms + if nh > size(estHarm, 2), continue; end + xPts = []; yPts = []; + for tb = 1:100 + if ~isnan(estHarm(tb, nh)) && estHarm(tb, nh) > 0 && estHarm(tb, nh) < maxHzEst + y_px = size(img, 1) - round(estHarm(tb, nh) / hz_per_px); + if y_px >= 1 && y_px <= size(img, 1) + xPts(end+1) = tb; + yPts(end+1) = y_px; + end + end + end + if ~isempty(xPts) + h = plot(xPts, yPts, estStyle{nh}, 'LineWidth', rpmLw); + set(h, 'Color', estCol(nh,:), 'HitTest', 'off'); + end + end + end % ~isempty(freqAx) + end + + catch ME + warning('PSplotSpec render p=%d: %s', p, ME.message); + end + end + end + + % color bar2 at the top + try + delete(findobj(PSspecfig, 'Tag', 'PScbar')) + catch + end + % Standalone colorbar axes — avoids colorbar('NorthOutside') which resizes subplots in Octave + bY = baselineY(get(guiHandlesSpec.checkboxPSD, 'Value')+1); + cbarPosAll = {posInfo.hCbar1pos, posInfo.hCbar2pos, posInfo.hCbar3pos, posInfo.hCbar4pos}; + for ci = 1:4 + if vars(ci) > 1 + cHi = climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, ci); + hCb = axes('Position', cbarPosAll{ci}); + imagesc(hCb, linspace(bY, cHi, 256)); + set(hCb, 'CLim', [bY cHi], 'XTick', [], 'YTick', [], 'Tag', 'PScbar', 'UserData', 'north'); + end + end + + % color maps + try + if ishandle(PSspecfig) + tmpCmapVal = get(guiHandlesSpec.ColormapSelect, 'Value'); + if tmpCmapVal <= 7 + tmpCmapStr = get(guiHandlesSpec.ColormapSelect, 'String'); + cm = feval(char(tmpCmapStr(tmpCmapVal)), 64); + elseif tmpCmapVal == 8 + cm = linearREDcmap; + else + cm = linearGREYcmap; + end + colormap(PSspecfig, cm); + end + catch, end + +end + +if get(guiHandlesSpec.checkbox2d, 'Value')==1 && ~isempty(amp2d) + figure(PSspecfig); + try + delete(findobj(PSspecfig, 'Tag', 'PScbar')) + catch + end + baselineYlines = [0 -50]; + c1=[1 1 1 2 2 2 3 3 3 4 4 4]; + c2=[1 2 3 1 2 3 1 2 3 1 2 3]; + %%%%% plot 2d amp spec + for p=1:size(amp2d,2) + axLabel={'roll';'pitch';'yaw'}; + + stag_ = sprintf('PSspec_%d', p); + h_old = findobj(PSspecfig, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_old), delete(h_old); end + if ~isempty(amp2d{p}) + h2 = axes('Parent', PSspecfig, 'Position', posInfo.SpecPos(p,:), 'Tag', stag_); + h=plot(freq2d{p}, smooth(amp2d{p}, log10(size(amp2d{p},1)) * (get(guiHandlesSpec.smoothFactor_select, 'Value')^2), 'lowess'));hold on + set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/2) + set(h2,'fontsize',fontsz,'fontweight','bold') + if get(guiHandlesSpec.specPresets, 'Value') <= 3 + set(h,'Color',[SpecLineCols(c1(p),:,1)]) + end + if get(guiHandlesSpec.specPresets, 'Value') > 4 && get(guiHandlesSpec.specPresets, 'Value') <= 6 + set(h,'Color',[SpecLineCols(c1(p),:,2)]) + end + if get(guiHandlesSpec.specPresets, 'Value') > 6 + set(h,'Color',[SpecLineCols(c1(p),:,3)]) + end + + if get(guiHandlesSpec.Sub100HzCheck{c1(p)}, 'Value')==1 + set(h2,'xtick',[0 20 40 60 80 100], 'yminortick','on') + axis([0 100 baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))]) + h=plot([round(Flim1) round(Flim1)],[baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'--','Color',th.axesFg); + set(h,'linewidth',1) + h=plot([round(Flim2) round(Flim2)],[baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))],'--','Color',th.axesFg); + set(h,'linewidth',1) + else + set(h2,'xtick',[0 : ((A_lograte(get(guiHandlesSpec.FileSelect{k}, 'Value')) / 2) * 1000 / 5) : (A_lograte(get(guiHandlesSpec.FileSelect{k}, 'Value')) / 2) * 1000],'yminortick','on') + axis([0 (A_lograte(get(guiHandlesSpec.FileSelect{k}, 'Value')) / 2) * 1000 baselineYlines(get(guiHandlesSpec.checkboxPSD, 'Value')+1) climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))]) + end + + xlabel('Frequency (Hz)','Color',th.textPrimary) + if get(guiHandlesSpec.checkboxPSD, 'Value') + ylabel(['PSD (dB)'],'Color',th.textPrimary) + else + ylabel(['Amplitude'],'Color',th.textPrimary) + end + + + h=text(2,climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, c1(p))*.95,axLabel{c2(p)}); + set(h,'Color',th.textPrimary,'fontsize',fontsz,'fontweight','bold') + + grid on + PSstyleAxes(gca, th); + end + end +end +PSdatatipSetup(PSspecfig); +try PSresizeCP(PSspecfig, []); catch, end + +set(PSspecfig, 'pointer', 'arrow') +updateSpec=0; + +end + diff --git a/src/plot/PSplotSpec2D.m b/src/plot/PSplotSpec2D.m index 4edf45a..cd7955a 100644 --- a/src/plot/PSplotSpec2D.m +++ b/src/plot/PSplotSpec2D.m @@ -1,279 +1,775 @@ -%% PSplotSpec2D - script that computes and plots spectrograms - - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if exist('fnameMaster','var') && ~isempty(fnameMaster) -%% update fonts -PSspecfig2_pos = get(PSspecfig2, 'Position'); -screensz_tmp = get(0,'ScreenSize'); if PSspecfig2_pos(3) > 10, PSspecfig2_pos(3:4) = PSspecfig2_pos(3:4) ./ screensz_tmp(3:4); end -prop_max_screen=(max([PSspecfig2_pos(3) PSspecfig2_pos(4)])); -fontsz=(screensz_multiplier*prop_max_screen); - -f = fields(guiHandlesSpec2); -for i = 1 : size(f,1) - try set(guiHandlesSpec2.(f{i}), 'FontSize', fontsz); catch, end -end - -set(spec2Crtlpanel, 'FontSize', fontsz); - -guiHandlesSpec2.climMax1_text = uicontrol(PSspecfig2,'style','text','string','Y min','fontsize',fontsz,'TooltipString',['Y min'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax1_text]); -guiHandlesSpec2.climMax1_input = uicontrol(PSspecfig2,'style','edit','string',[num2str(climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1, 1))],'fontsize',fontsz,'TooltipString',['Y min'],'units','normalized','Position',[posInfo.climMax1_input],... - 'callback','@textinput_call2; climScale1(get(guiHandlesSpec2.checkboxPSD, ''Value'')+1, 1)=str2num(get(guiHandlesSpec2.climMax1_input, ''String''));updateSpec=1;PSplotSpec2D;'); - - guiHandlesSpec2.climMax2_text = uicontrol(PSspecfig2,'style','text','string','Y max','fontsize',fontsz,'TooltipString',['Y max'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax2_text]); -guiHandlesSpec2.climMax2_input = uicontrol(PSspecfig2,'style','edit','string',[num2str(climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1, 1))],'fontsize',fontsz,'TooltipString',['Y max'],'units','normalized','Position',[posInfo.climMax2_input],... - 'callback','@textinput_call2; climScale2(get(guiHandlesSpec2.checkboxPSD, ''Value'')+1, 1)=str2num(get(guiHandlesSpec2.climMax2_input, ''String''));updateSpec=1;PSplotSpec2D;'); - - -%% - -s1={'gyroADC';'debug';'axisD';'axisDpf';'axisP';'piderr';'setpoint';'pidsum'}; - -datSelectionString=[s1]; -axesOptionsSpec = find([get(guiHandlesSpec2.plotR, 'Value') get(guiHandlesSpec2.plotP, 'Value') get(guiHandlesSpec2.plotY, 'Value')]); - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%%%% compute fft %%%%%%%%%%%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -set(PSspecfig2, 'pointer', 'watch') - -clear s dat a RC smat amp2d2 freq2d2 -freq2d2 = {}; -amp2d2 = {}; -p=0; -hw = waitbar(0,['please wait... ' ]); -tmpSpecVal = get(guiHandlesSpec2.SpecList, 'Value'); -tmpFileVal = get(guiHandlesSpec2.FileSelect, 'Value'); -tmpPSDVal = get(guiHandlesSpec2.checkboxPSD, 'Value'); -pTotal = max(1, length(tmpSpecVal) * size(tmpFileVal,2) * length(axesOptionsSpec)); -for k = 1 : length(tmpSpecVal) - s = char(datSelectionString(tmpSpecVal(k))); - for f = 1 : size(tmpFileVal,2) - for a = axesOptionsSpec - if ( ( ~isempty(strfind(s,'axisD'))) && a==3) || isempty(s) - p=p+1; - smat{p}=[];%string - amp2d2{p}=[];%spec 2d - freq2d2{p}=[];% freq2d2 - else - p = p + 1; - clear dat - eval(['dat = T{tmpFileVal(f)}.' char(datSelectionString(tmpSpecVal(k))) '_' int2str(a-1) '_(tIND{tmpFileVal(f)})'';';]) - lograte = A_lograte(tmpFileVal(f));%in kHz - waitbar(min(1, p/pTotal), hw, ['processing spectrogram... ' int2str(p) ]); - smat{p}=s; - eval(['[freq2d2{p}.f' int2str(f) ' amp2d2{p}.f' int2str(f) ' ]=PSSpec2d(dat,lograte, tmpPSDVal);']) %compute 2d amp spec at same time - end - end - end -end -close(hw) - - - - -figure(PSspecfig2); -baselineYlines = [0 -50]; -multilineStyle = {'-' ; ':'; '--'}; -rpyLineStyle = {'-' ; '--'; ':'}; - -delete(subplot('position',posInfo.Spec2Pos(1,:))) -delete(subplot('position',posInfo.Spec2Pos(2,:))) -delete(subplot('position',posInfo.Spec2Pos(3,:))) -delete(subplot('position',posInfo.Spec2Pos(4,:))) -delete(subplot('position',posInfo.Spec2Pos(5,:))) -delete(subplot('position',posInfo.Spec2Pos(6,:))) -%%%%% plot 2d amp spec -axLabel={'Roll';'Pitch';'Yaw'}; - -p = 0; -tmpSpecVal = get(guiHandlesSpec2.SpecList, 'Value'); -tmpFileVal = get(guiHandlesSpec2.FileSelect, 'Value'); -tmpPSDVal = get(guiHandlesSpec2.checkboxPSD, 'Value'); -tmpSmoothVal = get(guiHandlesSpec2.smoothFactor_select, 'Value'); -for k = 1 : length(tmpSpecVal) - s = char(datSelectionString(k)); - for f = 1 : size(tmpFileVal,2) - cnt = 0; - for a = axesOptionsSpec - cnt = cnt + 1; - p = p + 1; - if ~isempty(freq2d2) - if ~isempty(freq2d2{p}) && ~isempty(amp2d2{p}) - - if get(guiHandlesSpec2.RPYcomboSpec, 'Value') == 0 - - h2=subplot('position',posInfo.Spec2Pos(a,:)); - eval(['h=plot(freq2d2{p}.f' int2str(f) ', smooth(amp2d2{p}.f' int2str(f) ', log10(size(amp2d2{p}.f' int2str(f) ',1)) * (tmpSmoothVal^3), ''lowess''));hold on']) - hold on - set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/2,'linestyle',multilineStyle{k}) - set(h2,'fontsize',fontsz) - set(h,'Color',[multiLineCols(f,:)]) - m = (A_lograte(tmpFileVal(f)) * 1000) / 2; - set(h2,'xtick',[0:m/10:m], 'yminortick','on') - axis([0 m climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1) climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)]) - xlabel('Frequency (Hz)','fontweight','bold'); - if get(guiHandlesSpec2.checkboxPSD, 'Value') - ylabel(['Power Spectral Density (dB)'],'fontweight','bold'); - else - ylabel(['Amplitude'],'fontweight','bold'); - end - if a == 1 - title('Full Spectrum','fontweight','bold'); - end - if p < 4 - h=text(2,climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)*.92,axLabel{a}); - set(h,'Color',[.2 .2 .2],'fontsize',fontsz,'fontweight','bold'); - end - grid on - - h2=subplot('position',posInfo.Spec2Pos(a+3,:)); - eval(['h=plot(freq2d2{p}.f' int2str(f) ', smooth(amp2d2{p}.f' int2str(f) ', log10(size(amp2d2{p}.f' int2str(f) ',1)) * (tmpSmoothVal^3), ''lowess''));hold on']) - hold on - set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/2,'linestyle',multilineStyle{k}) - set(h2,'fontsize',fontsz) - set(h,'Color',[multiLineCols(f,:)]) - m = (A_lograte(tmpFileVal(f)) * 1000) / 2; - set(h2,'xtick',[0 20 40 60 80 100],'yminortick','on') - axis([0 100 climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1) climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)]) - xlabel('Frequency (Hz)','fontweight','bold'); - if get(guiHandlesSpec2.checkboxPSD, 'Value') - ylabel(['Power Spectral Density (dB)'],'fontweight','bold'); - else - ylabel(['Amplitude'],'fontweight','bold'); - end - if a == 1 - title('Sub 100Hz','fontweight','bold'); - end - if p < 4 - h=text(1,climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)*.92,axLabel{a}); - set(h,'Color',[.2 .2 .2],'fontsize',fontsz,'fontweight','bold'); - end - - %%%%%%%%%%%%%%%%%%% Plot Latencies %%%%%%%%%%%%%%% - tmpFileSelVals = get(guiHandlesSpec2.FileSelect, 'Value'); - tmpFileIdx = tmpFileSelVals(f); - % Per-file debug mode indices (BF version-aware) - if exist('debugIdx','var') && numel(debugIdx) >= tmpFileIdx - tmpDbgIdx = debugIdx{tmpFileIdx}; - else - tmpDbgIdx = struct('GYRO_SCALED',6,'GYRO_FILTERED',3,'RC_INTERPOLATION',7,'FFT_FREQ',17,'FEEDFORWARD',59); - end - if get(guiHandlesSpec2.Delay, 'Value') == 1 && a == 1 - if debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_SCALED || debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_FILTERED || debugmode(tmpFileIdx) == 1 || debugmode(tmpFileIdx) == 0 - h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Filter: ' Debug01{tmpFileIdx} 'ms | Dterm Filter: ' FilterDelayDterm{tmpFileIdx} 'ms']); - set(h,'Color',[multiLineCols(f,:)],'fontsize',fontsz); - else - h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Filter: ' 'ms | Dterm Filter: ' FilterDelayDterm{tmpFileIdx} 'ms']); - set(h,'Color',[multiLineCols(f,:)],'fontsize',fontsz); - end - end - if get(guiHandlesSpec2.Delay, 'Value') == 2 - h=text(80, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['SP-Gyro: ' int2str(SPGyroDelay(tmpFileIdx, a)) 'ms']); - set(h,'Color',[multiLineCols(f,:)],'fontsize',fontsz); - end - if get(guiHandlesSpec2.Delay, 'Value') == 3 && a == 1 - if debugmode(tmpFileIdx) == tmpDbgIdx.RC_INTERPOLATION || debugmode(tmpFileIdx) == tmpDbgIdx.FEEDFORWARD - h=text(75, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['SP smoothing delay: ' Debug02{tmpFileIdx} 'ms']); - set(h,'Color',[multiLineCols(f,:)],'fontsize',fontsz); - else - h=text(80, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['debug mode not set ']); - set(h,'Color',[multiLineCols(f,:)],'fontsize',fontsz); - end - end - if get(guiHandlesSpec2.Delay, 'Value') == 4 && a == 1 - if debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_SCALED || debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_FILTERED || debugmode(tmpFileIdx) == 1 || debugmode(tmpFileIdx) == 0 - h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Phase: ' num2str(gyro_phase_shift_deg(tmpFileIdx)) 'deg | Dterm Phase: ' num2str(dterm_phase_shift_deg(tmpFileIdx)) 'deg']); - set(h,'Color',[multiLineCols(f,:)],'fontsize',fontsz); - else - h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Phase: ' 'deg | Dterm Phase: ' num2str(dterm_phase_shift_deg(tmpFileIdx)) 'deg']); - set(h,'Color',[multiLineCols(f,:)],'fontsize',fontsz); - end - end - - - else - % combine R P Y - h2=subplot('position',[0.0500 0.1000 0.800 0.840]); - eval(['h=plot(freq2d2{p}.f' int2str(f) ', smooth(amp2d2{p}.f' int2str(f) ', log10(size(amp2d2{p}.f' int2str(f) ',1)) * (tmpSmoothVal^3), ''lowess''));hold on']) - hold on - if k == 1 - set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/1.4,'linestyle',rpyLineStyle{cnt}) - end - if k == 2 - set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/2.6,'linestyle',rpyLineStyle{cnt}) - end - set(h2,'fontsize',fontsz) - set(h,'Color',[multiLineCols(f,:)]) - m = (A_lograte(tmpFileVal(f)) * 1000) / 2; - set(h2,'xtick',[0:m/10:m], 'yminortick','on') - axis([0 m climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1) climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)]) - xlabel('Frequency (Hz)','fontweight','bold'); - if get(guiHandlesSpec2.checkboxPSD, 'Value') - ylabel(['Power Spectral Density (dB)'],'fontweight','bold'); - else - ylabel(['Amplitude'],'fontweight','bold'); - end - if a == 1 - title('Full Spectrum','fontweight','bold'); - end - grid on - - - end - - grid on - - else - end - end - end - end -end - -l=0;legnd={}; -l2=0; -tmpSpecListStr = get(guiHandlesSpec2.SpecList, 'String'); -tmpSpecListVal = get(guiHandlesSpec2.SpecList, 'Value'); -tmpFileSelStr = get(guiHandlesSpec2.FileSelect, 'String'); -tmpFileSelVal = get(guiHandlesSpec2.FileSelect, 'Value'); -for m = 1 : length(tmpSpecListVal) - for n = 1 : length(tmpFileSelVal) - l = l + 1; - clear fstr fltDelayStr - fstr = char(tmpFileSelStr(tmpFileSelVal(n))); - if size(fstr,2) > 12, fstr = fstr(1,1:12); end % only use first 20 characters of file name - if get(guiHandlesSpec2.RPYcomboSpec, 'Value') == 0 - legnd{l} = [char(tmpSpecListStr(tmpSpecListVal(m))) ' | ' fstr]; - else - for a = axesOptionsSpec - l2 = l2 + 1; - legnd{l2} = [axLabel{a} ' | ' char(tmpSpecListStr(tmpSpecListVal(m))) ' | ' fstr ]; - end - end - end -end -if ~isempty(freq2d2) && ~isempty(amp2d2) - if get(guiHandlesSpec2.RPYcomboSpec, 'Value') == 0 - h=legend(legnd); - hPos = get(h, 'Position'); set(h, 'Position', [0.35 0.01 hPos(3:4)]); - else - h=legend(legnd, 'Location','NorthEast') - end -end - - -% Set up click-to-show-value datatips + double-click expand on all axes -PSdatatipSetup(PSspecfig2); - -set(PSspecfig2, 'pointer', 'arrow') -updateSpec=0; -end - - +%% PSplotSpec2D - script that computes and plots spectrograms + + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +if exist('fnameMaster','var') && ~isempty(fnameMaster) +th = PStheme(); + +set(guiHandlesSpec2.climMax1_input, 'String', num2str(climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1, 1))); +set(guiHandlesSpec2.climMax2_input, 'String', num2str(climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1, 1))); + + +%% + +s1={'gyroADC';'gyroPrefilt';'axisD';'axisDpf';'axisP';'piderr';'setpoint';'axisF';'pidsum';'motorAvg'}; +if isfield(T{1}, 'testSignal_0_'), s1{end+1} = 'testSignal'; end + +datSelectionString=[s1]; +axesOptionsSpec = find([get(guiHandlesSpec2.plotR, 'Value') get(guiHandlesSpec2.plotP, 'Value') get(guiHandlesSpec2.plotY, 'Value')]); + +% scale row heights to fill space when fewer than 3 RPY axes +nActiveSpec = numel(axesOptionsSpec); +stdRows = [0.69 0.395 0.1]; stdRowH = 0.25; +if nActiveSpec > 0 && nActiveSpec < 3 && ~get(guiHandlesSpec2.RPYcomboSpec, 'Value') + topY_s = stdRows(1) + stdRowH; botY_s = stdRows(3); gapS = 0.045; + rowH_s = (topY_s - botY_s - (nActiveSpec-1)*gapS) / nActiveSpec; + ci = 0; + for jj = axesOptionsSpec + ci = ci + 1; + yy = topY_s - ci*rowH_s - (ci-1)*gapS; + posInfo.Spec2Pos(jj, 2) = yy; posInfo.Spec2Pos(jj, 4) = rowH_s; + posInfo.Spec2Pos(jj+3, 2) = yy; posInfo.Spec2Pos(jj+3, 4) = rowH_s; + end +else + for jj = 1:3 + posInfo.Spec2Pos(jj, 2) = stdRows(jj); posInfo.Spec2Pos(jj, 4) = stdRowH; + posInfo.Spec2Pos(jj+3, 2) = stdRows(jj); posInfo.Spec2Pos(jj+3, 4) = stdRowH; + end +end + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%% compute fft %%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +set(PSspecfig2, 'pointer', 'watch') + +%%% compute delay/overlay data (deferred from UI open to Run click) +if ~exist('delayDataReady','var') || ~delayDataReady + hw_delay = waitbar(0, 'computing delays...'); + FilterDelayDterm={}; + SPGyroDelay=[]; + Debug01={}; + Debug02={}; + gyro_phase_shift_deg=zeros(Nfiles,1); + dterm_phase_shift_deg=zeros(Nfiles,1); + notchData={}; + rpmFilterData={}; + for k = 1 : Nfiles + waitbar(k/Nfiles, hw_delay, ['computing delays... file ' int2str(k) '/' int2str(Nfiles)]); + Fs=1000/A_lograte(k); + maxlag=round(30000/Fs); + + try + if isfield(T{k}, 'gyroPrefilt_0_') + pg = smooth(T{k}.gyroPrefilt_0_(tIND{k}),50); + else + pg = smooth(T{k}.debug_0_(tIND{k}),50); + end + catch + pg = []; + end + try + g1 = smooth(T{k}.gyroADC_0_(tIND{k}),50); + s1 = smooth(T{k}.setpoint_0_(tIND{k}),50); + g2 = smooth(T{k}.gyroADC_1_(tIND{k}),50); + s2 = smooth(T{k}.setpoint_1_(tIND{k}),50); + g3 = smooth(T{k}.gyroADC_2_(tIND{k}),50); + s3 = smooth(T{k}.setpoint_2_(tIND{k}),50); + + if isempty(pg), pg = zeros(size(g1)); end + + [c,lags] = xcorr(g1,pg,maxlag); + d = lags(find(c==max(c),1)); + d = d * (Fs / 1000); + if d<.1, Debug01{k} = ' '; else Debug01{k} = num2str(d); end + + [c,lags] = xcorr(s1,pg,maxlag); + d = lags(find(c==max(c),1)); + d = d * (Fs / 1000); + if d<.1, Debug02{k} = ' '; else Debug02{k} = num2str(d); end + + [c,lags] = xcorr(g1,s1,maxlag); + d = lags(find(c==max(c),1)); d = d * (Fs / 1000); + if d<.1, SPGyroDelay(k,1) = 0; else, SPGyroDelay(k,1) = d; end + + [c,lags] = xcorr(g2,s2,maxlag); + d = lags(find(c==max(c),1)); d = d * (Fs / 1000); + if d<.1, SPGyroDelay(k,2) = 0; else, SPGyroDelay(k,2) = d; end + + [c,lags] = xcorr(g3,s3,maxlag); + d = lags(find(c==max(c),1)); d = d * (Fs / 1000); + if d<.1, SPGyroDelay(k,3) = 0; else, SPGyroDelay(k,3) = d; end + + try + d1 = smooth(T{k}.axisDpf_0_(tIND{k}),50); + d2 = smooth(T{k}.axisD_0_(tIND{k}),50); + [c,lags] = xcorr(d2,d1,maxlag); + d = lags(find(c==max(c))); + d = d * (Fs / 1000); + if d<.1, FilterDelayDterm{k} = ' '; else FilterDelayDterm{k} = num2str(d); end + catch + FilterDelayDterm{k} = ' '; + end + catch + Debug01{k} = ' '; Debug02{k} = ' '; FilterDelayDterm{k} = ' '; + end + + try + if ~isempty(str2double(Debug01{k})) && ~isnan(str2double(Debug01{k})) && SPGyroDelay(k,1) > 0 + gyro_phase_shift_deg(k,1) = round(PSphaseShiftDeg(str2double(Debug01{k}), 1000/(SPGyroDelay(k,1)))); + end + if ~isempty(str2double(FilterDelayDterm{k})) && ~isnan(str2double(FilterDelayDterm{k})) && SPGyroDelay(k,1) > 0 + dterm_phase_shift_deg(k,1) = round(PSphaseShiftDeg(str2double(FilterDelayDterm{k}), 1000/(SPGyroDelay(k,1)))); + end + catch, end + + % dynamic notch data for FFT_FREQ overlay + tmpFFTidx = FFT_FREQ; + if exist('debugIdx','var') && numel(debugIdx) >= k + tmpFFTidx = debugIdx{k}.FFT_FREQ; + end + if exist('debugmode','var') && numel(debugmode) >= k && debugmode(k) == tmpFFTidx + if exist('fwMajor','var') && numel(fwMajor) >= k && fwMajor(k) >= 2025 + notchData{k} = [T{k}.debug_1_(tIND{k}), T{k}.debug_2_(tIND{k}), T{k}.debug_3_(tIND{k})]; + else + notchData{k} = [T{k}.debug_0_(tIND{k}), T{k}.debug_1_(tIND{k}), T{k}.debug_2_(tIND{k})]; + end + else + notchData{k} = []; + end + + % RPM filter data for motor noise overlay + tmpRPMidx = 46; + if exist('debugIdx','var') && numel(debugIdx) >= k + tmpRPMidx = debugIdx{k}.RPM_FILTER; + end + if exist('debugmode','var') && numel(debugmode) >= k && debugmode(k) == tmpRPMidx + rpmFilterData{k} = [T{k}.debug_0_(tIND{k}), T{k}.debug_1_(tIND{k}), ... + T{k}.debug_2_(tIND{k}), T{k}.debug_3_(tIND{k})]; + else + rpmFilterData{k} = []; + end + end + delayDataReady = true; + try close(hw_delay); catch, end +end + +tmpSpecVal = get(guiHandlesSpec2.SpecList, 'Value'); +tmpFileVal = get(guiHandlesSpec2.FileSelect, 'Value'); +tmpPSDVal = get(guiHandlesSpec2.checkboxPSD, 'Value'); + +% skip PSD recompute when only right-column controls changed +needFFT_ = true; +if exist('prevPsdKey_','var') && exist('freq2d2','var') && ~isempty(freq2d2) && ~updateSpec + if isequal(tmpSpecVal, prevPsdKey_.specVal) && isequal(tmpFileVal, prevPsdKey_.fileVal) && ... + tmpPSDVal == prevPsdKey_.psdVal && isequal(axesOptionsSpec, prevPsdKey_.axes) + needFFT_ = false; + end +end + +if needFFT_ +clear s dat a RC smat amp2d2 freq2d2 +freq2d2 = {}; +amp2d2 = {}; +p=0; +hw_fft = waitbar(0, 'computing FFT...'); +for k = 1 : length(tmpSpecVal) + s = char(datSelectionString(tmpSpecVal(k))); + for f = 1 : size(tmpFileVal,2) + for a = axesOptionsSpec + if ( ( ~isempty(strfind(s,'axisD'))) && a==3) || isempty(s) + p=p+1; + smat{p}=[];%string + amp2d2{p}=[];%spec 2d + freq2d2{p}=[];% freq2d2 + elseif strcmp(s, 'motorAvg') + p = p + 1; + mAvg = zeros(sum(tIND{tmpFileVal(f)}), 1); + nMot = 0; + for mi = 0:3 + mf = ['motor_' int2str(mi) '_']; + if isfield(T{tmpFileVal(f)}, mf) + mAvg = mAvg + T{tmpFileVal(f)}.(mf)(tIND{tmpFileVal(f)}); + nMot = nMot + 1; + end + end + if nMot > 0, mAvg = mAvg / nMot; end + dat = mAvg'; + lograte = A_lograte(tmpFileVal(f)); + smat{p} = s; + [tmpF tmpA] = PSSpec2d(dat, lograte, tmpPSDVal); + if isempty(tmpF) + smat{p}=[]; amp2d2{p}=[]; freq2d2{p}=[]; + else + ff = ['f' int2str(f)]; + freq2d2{p}.(ff) = tmpF; + amp2d2{p}.(ff) = tmpA; + end + elseif ~isfield(T{tmpFileVal(f)}, [s '_' int2str(a-1) '_']) + p=p+1; + smat{p}=[]; amp2d2{p}=[]; freq2d2{p}=[]; + else + p = p + 1; + fld = [s '_' int2str(a-1) '_']; + dat = T{tmpFileVal(f)}.(fld)(tIND{tmpFileVal(f)})'; + lograte = A_lograte(tmpFileVal(f)); + smat{p}=s; + waitbar(min(1, p/(length(tmpSpecVal)*size(tmpFileVal,2)*length(axesOptionsSpec))), hw_fft, ['computing FFT... ' int2str(p)]); + ff = ['f' int2str(f)]; + [tmpF tmpA] = PSSpec2d(dat,lograte, tmpPSDVal); + if isempty(tmpF) + smat{p}=[]; amp2d2{p}=[]; freq2d2{p}=[]; + else + freq2d2{p}.(ff) = tmpF; + amp2d2{p}.(ff) = tmpA; + end + end + end + end +end +try close(hw_fft); catch, end +prevPsdKey_ = struct('specVal', tmpSpecVal, 'fileVal', tmpFileVal, 'psdVal', tmpPSDVal, 'axes', axesOptionsSpec); +end + +figure(PSspecfig2); +baselineYlines = [0 -50]; +multilineStyle = {'-' ; ':'; '--'}; +rpyLineStyle = {'-' ; '--'; ':'}; + +% skip left-column rerender when only right-column controls changed +tmpSmoothVal = get(guiHandlesSpec2.smoothFactor_select, 'Value'); +rightMode_chk_ = 1; try rightMode_chk_ = get(guiHandlesSpec2.rightColMode, 'Value'); catch, end +leftKey_ = struct('specVal', tmpSpecVal, 'fileVal', tmpFileVal, 'psdVal', tmpPSDVal, ... + 'axes', axesOptionsSpec, 'smooth', tmpSmoothVal, 'rightCol', rightMode_chk_); +try leftKey_.clim1 = get(guiHandlesSpec2.climMax1_input, 'String'); + leftKey_.clim2 = get(guiHandlesSpec2.climMax2_input, 'String'); + leftKey_.delay = get(guiHandlesSpec2.Delay, 'Value'); + leftKey_.combo = get(guiHandlesSpec2.RPYcomboSpec, 'Value'); +catch, end +skipLeftRender_ = ~needFFT_ && exist('prevLeftKey_','var') && isequal(leftKey_, prevLeftKey_); +% invalidate if axes were deleted externally +if skipLeftRender_ && isempty(findobj(PSspecfig2, 'Type', 'axes', 'Tag', 'PSspec2_1')), skipLeftRender_ = false; end + +if ~skipLeftRender_ +% cla active panels, delete unchecked ones so they don't linger at old positions +for di_=1:3 + for si_=[di_ di_+3] + h_cla=findobj(PSspecfig2,'Type','axes','Tag',sprintf('PSspec2_%d',si_)); + if ~isempty(h_cla) + if any(axesOptionsSpec == di_), cla(h_cla); hold(h_cla,'off'); + else delete(h_cla); end + end + end +end +h_del=findobj(PSspecfig2,'Type','axes','Tag','PSspec2_combo'); if ~isempty(h_del), delete(h_del); end +h_del=findobj(PSspecfig2,'Type','axes','Tag','legend'); if ~isempty(h_del), delete(h_del); end +%%%%% plot 2d amp spec +axLabel={'Roll';'Pitch';'Yaw'}; + +p = 0; +tmpSpecVal = get(guiHandlesSpec2.SpecList, 'Value'); +tmpFileVal = get(guiHandlesSpec2.FileSelect, 'Value'); +tmpPSDVal = get(guiHandlesSpec2.checkboxPSD, 'Value'); +for k = 1 : length(tmpSpecVal) + s = char(datSelectionString(tmpSpecVal(k))); + for f = 1 : size(tmpFileVal,2) + lineCol = multiLineCols(f,:); + isTS = strcmp(s, 'testSignal'); + if isTS, lineCol = th.sigTestSignal; end + cnt = 0; + for a = axesOptionsSpec + cnt = cnt + 1; + p = p + 1; + if ~isempty(freq2d2) + if ~isempty(freq2d2{p}) && ~isempty(amp2d2{p}) + + if get(guiHandlesSpec2.RPYcomboSpec, 'Value') == 0 + + stag_ = sprintf('PSspec2_%d', a); + h2 = findobj(PSspecfig2, 'Type', 'axes', 'Tag', stag_); + if isempty(h2), h2 = axes('Parent', PSspecfig2, 'Position', posInfo.Spec2Pos(a,:), 'Tag', stag_); + else set(h2, 'Position', posInfo.Spec2Pos(a,:)); set(PSspecfig2, 'CurrentAxes', h2); end + ff = ['f' int2str(f)]; + h=plot(freq2d2{p}.(ff), smooth(amp2d2{p}.(ff), log10(size(amp2d2{p}.(ff),1)) * (tmpSmoothVal^3), 'lowess')); hold on + hold on + lsty = multilineStyle{k}; if isTS, lsty = '-'; end + lw_ = get(guiHandles.linewidth, 'Value')/2; + if k > 1, lw_ = lw_ * 0.6; end + set(h, 'linewidth', lw_,'linestyle',lsty) + set(h2,'fontsize',fontsz) + set(h,'Color',[lineCol]) + m = (A_lograte(tmpFileVal(f)) * 1000) / 2; + set(h2,'xtick',[0:m/10:m], 'yminortick','on') + if ~strcmp(s, 'motorAvg') + axis([0 m climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1) climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)]) + else + set(h2, 'XLim', [0 m]); + end + xlabel('Frequency (Hz)','fontweight','bold','Color',th.textPrimary); + if get(guiHandlesSpec2.checkboxPSD, 'Value') + ylabel(['Power Spectral Density (dB)'],'fontweight','bold','Color',th.textPrimary); + else + ylabel(['Amplitude'],'fontweight','bold','Color',th.textPrimary); + end + if a == 1 + title('Full Spectrum','fontweight','bold','Color',th.textPrimary); + end + if p < 4 + h=text(2,climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)*.92,axLabel{a}); + set(h,'Color',th.textPrimary,'fontsize',fontsz,'fontweight','bold'); + end + grid on + + rightMode_ = 1; + try rightMode_ = get(guiHandlesSpec2.rightColMode, 'Value'); catch, end + + if rightMode_ ~= 2 + stag2_ = sprintf('PSspec2_%d', a+3); + h2 = findobj(PSspecfig2, 'Type', 'axes', 'Tag', stag2_); + if isempty(h2), h2 = axes('Parent', PSspecfig2, 'Position', posInfo.Spec2Pos(a+3,:), 'Tag', stag2_); + else set(h2, 'Position', posInfo.Spec2Pos(a+3,:)); set(PSspecfig2, 'CurrentAxes', h2); end + + if strcmp(s, 'motorAvg') + set(h2, 'Visible', 'on'); + PSstyleAxes(h2, th); + else + % Sub 100Hz PSD + ff = ['f' int2str(f)]; + h=plot(freq2d2{p}.(ff), smooth(amp2d2{p}.(ff), log10(size(amp2d2{p}.(ff),1)) * (tmpSmoothVal^3), 'lowess')); hold on + hold on + lsty = multilineStyle{k}; if isTS, lsty = '-'; end + lw_ = get(guiHandles.linewidth, 'Value')/2; + if k > 1, lw_ = lw_ * 0.6; end + set(h, 'linewidth', lw_,'linestyle',lsty) + set(h2,'fontsize',fontsz) + set(h,'Color',[lineCol]) + m = (A_lograte(tmpFileVal(f)) * 1000) / 2; + set(h2,'xtick',[0 20 40 60 80 100],'yminortick','on') + axis([0 100 climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1) climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)]) + xlabel('Frequency (Hz)','fontweight','bold','Color',th.textPrimary); + if get(guiHandlesSpec2.checkboxPSD, 'Value') + ylabel(['Power Spectral Density (dB)'],'fontweight','bold','Color',th.textPrimary); + else + ylabel(['Amplitude'],'fontweight','bold','Color',th.textPrimary); + end + if a == 1 + title('Sub 100Hz','fontweight','bold','Color',th.textPrimary); + end + if p < 4 + h=text(1,climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)*.92,axLabel{a}); + set(h,'Color',th.textPrimary,'fontsize',fontsz,'fontweight','bold'); + end + end % motorAvg + end % rightMode_ + + %%%%%%%%%%%%%%%%%%% Plot Latencies %%%%%%%%%%%%%%% + tmpFileSelVals = get(guiHandlesSpec2.FileSelect, 'Value'); + tmpFileIdx = tmpFileSelVals(f); + % Per-file debug mode indices (BF version-aware) + if exist('debugIdx','var') && numel(debugIdx) >= tmpFileIdx + tmpDbgIdx = debugIdx{tmpFileIdx}; + else + tmpDbgIdx = struct('GYRO_SCALED',6,'GYRO_FILTERED',3,'RC_INTERPOLATION',7,'FFT_FREQ',17,'FEEDFORWARD',59); + end + if get(guiHandlesSpec2.Delay, 'Value') == 1 && a == 1 + if debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_SCALED || debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_FILTERED || debugmode(tmpFileIdx) == 1 || debugmode(tmpFileIdx) == 0 + h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Filter: ' Debug01{tmpFileIdx} 'ms | Dterm Filter: ' FilterDelayDterm{tmpFileIdx} 'ms']); + set(h,'Color',[lineCol],'fontsize',fontsz); + else + h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Filter: ' 'ms | Dterm Filter: ' FilterDelayDterm{tmpFileIdx} 'ms']); + set(h,'Color',[lineCol],'fontsize',fontsz); + end + end + if get(guiHandlesSpec2.Delay, 'Value') == 2 + h=text(80, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['SP-Gyro: ' int2str(SPGyroDelay(tmpFileIdx, a)) 'ms']); + set(h,'Color',[lineCol],'fontsize',fontsz); + end + if get(guiHandlesSpec2.Delay, 'Value') == 3 && a == 1 + if debugmode(tmpFileIdx) == tmpDbgIdx.RC_INTERPOLATION || debugmode(tmpFileIdx) == tmpDbgIdx.FEEDFORWARD + h=text(75, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['SP smoothing delay: ' Debug02{tmpFileIdx} 'ms']); + set(h,'Color',[lineCol],'fontsize',fontsz); + else + h=text(80, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['debug mode not set ']); + set(h,'Color',[lineCol],'fontsize',fontsz); + end + end + if get(guiHandlesSpec2.Delay, 'Value') == 4 && a == 1 + if debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_SCALED || debugmode(tmpFileIdx) == tmpDbgIdx.GYRO_FILTERED || debugmode(tmpFileIdx) == 1 || debugmode(tmpFileIdx) == 0 + h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Phase: ' num2str(gyro_phase_shift_deg(tmpFileIdx)) 'deg | Dterm Phase: ' num2str(dterm_phase_shift_deg(tmpFileIdx)) 'deg']); + set(h,'Color',[lineCol],'fontsize',fontsz); + else + h=text(65, climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)-(f*4), ['Gyro Phase: ' 'deg | Dterm Phase: ' num2str(dterm_phase_shift_deg(tmpFileIdx)) 'deg']); + set(h,'Color',[lineCol],'fontsize',fontsz); + end + end + + + else + % combine R P Y + h2 = findobj(PSspecfig2, 'Type', 'axes', 'Tag', 'PSspec2_combo'); + if isempty(h2), h2 = axes('Parent', PSspecfig2, 'Position', [0.0500 0.1000 cpL-0.1 0.840], 'Tag', 'PSspec2_combo'); + else set(PSspecfig2, 'CurrentAxes', h2); end + ff = ['f' int2str(f)]; + h=plot(freq2d2{p}.(ff), smooth(amp2d2{p}.(ff), log10(size(amp2d2{p}.(ff),1)) * (tmpSmoothVal^3), 'lowess')); hold on + hold on + if k == 1 + set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/1.4,'linestyle',rpyLineStyle{cnt}) + end + if k == 2 + set(h, 'linewidth', get(guiHandles.linewidth, 'Value')/2.6,'linestyle',rpyLineStyle{cnt}) + end + set(h2,'fontsize',fontsz) + set(h,'Color',[lineCol]) + m = (A_lograte(tmpFileVal(f)) * 1000) / 2; + set(h2,'xtick',[0:m/10:m], 'yminortick','on') + axis([0 m climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1) climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1)]) + xlabel('Frequency (Hz)','fontweight','bold','Color',th.textPrimary); + if get(guiHandlesSpec2.checkboxPSD, 'Value') + ylabel(['Power Spectral Density (dB)'],'fontweight','bold','Color',th.textPrimary); + else + ylabel(['Amplitude'],'fontweight','bold','Color',th.textPrimary); + end + if a == 1 + title('Full Spectrum','fontweight','bold','Color',th.textPrimary); + end + grid on + + + end + + grid on + + else + end + end + end + end +end + +l=0;legnd={}; +l2=0; +tmpSpecListStr = get(guiHandlesSpec2.SpecList, 'String'); +tmpSpecListVal = get(guiHandlesSpec2.SpecList, 'Value'); +tmpFileSelStr = get(guiHandlesSpec2.FileSelect, 'String'); +tmpFileSelVal = get(guiHandlesSpec2.FileSelect, 'Value'); +for m = 1 : length(tmpSpecListVal) + sLeg = char(datSelectionString(tmpSpecListVal(m))); + for n = 1 : length(tmpFileSelVal) + fIdx_ = tmpFileSelVal(n); + if ~isfield(T{fIdx_}, [sLeg '_0_']), continue; end + l = l + 1; + clear fstr fltDelayStr + fstr = char(tmpFileSelStr(tmpFileSelVal(n))); + if size(fstr,2) > 12, fstr = fstr(1,1:12); end + if get(guiHandlesSpec2.RPYcomboSpec, 'Value') == 0 + legnd{l} = [char(tmpSpecListStr(tmpSpecListVal(m))) ' | ' fstr]; + else + for a = axesOptionsSpec + l2 = l2 + 1; + legnd{l2} = [axLabel{a} ' | ' char(tmpSpecListStr(tmpSpecListVal(m))) ' | ' fstr ]; + end + end + end +end +try + if ~isempty(freq2d2) && ~isempty(amp2d2) + if get(guiHandlesSpec2.RPYcomboSpec, 'Value') == 0 + h=legend(legnd); + hPos = get(h, 'Position'); set(h, 'Position', [0.35 0.01 hPos(3:4)]); + else + h=legend(legnd, 'Location','NorthEast'); + end + try PSstyleLegend(h, th); catch, end + end +catch, end +% warn if "Gyro prefilt" selected but no data available +if any(tmpSpecVal == 2) + hasPF_ = false; + for fi_ = 1:size(tmpFileVal,2) + if isfield(T{tmpFileVal(fi_)}, 'gyroPrefilt_0_'), hasPF_ = true; break; end + end + if ~hasPF_ + delete(findobj(PSspecfig2, 'Tag', 'prefiltWarn')); + axW_ = findobj(PSspecfig2, 'Type', 'axes', 'Tag', 'PSspec2_1'); + if ~isempty(axW_) + text(axW_, 0.5, 0.5, {'No pre-filter gyro data.', 'Requires gyroUnfilt (BF 4.5+) or debug\_mode = GYRO\_SCALED.'}, ... + 'Units', 'normalized', 'HorizontalAlignment', 'center', 'FontSize', fontsz+1, ... + 'Color', [.8 .3 .3], 'Tag', 'prefiltWarn'); + end + else + delete(findobj(PSspecfig2, 'Tag', 'prefiltWarn')); + end +else + delete(findobj(PSspecfig2, 'Tag', 'prefiltWarn')); +end +prevLeftKey_ = leftKey_; +else + % right-column only: clear axes 4-6 when Motor Noise will repopulate + if rightMode_chk_ == 2 + for di_=4:6, h_cla=findobj(PSspecfig2,'Type','axes','Tag',sprintf('PSspec2_%d',di_)); if ~isempty(h_cla), cla(h_cla); hold(h_cla,'off'); end; end + end +end % skipLeftRender_ + + +% Motor Noise panels (right column, when rightColMode == 2) +rightMode_final = 1; +try rightMode_final = get(guiHandlesSpec2.rightColMode, 'Value'); catch, end +if rightMode_final == 2 + axLabel_mn = {'Roll','Pitch','Yaw'}; + axesOpt_mn = find([get(guiHandlesSpec2.plotR, 'Value') get(guiHandlesSpec2.plotP, 'Value') get(guiHandlesSpec2.plotY, 'Value')]); + tmpFileVal_mn = get(guiHandlesSpec2.FileSelect, 'Value'); + + % read RPM controls + rpmMotors_mn = [1 2 3 4]; + try + rpmMotors_mn = []; + nMot_mn = 4; try nMot_mn = guiHandlesSpec2.nMotors; catch, end + for mi_ = 1:4 + if get(guiHandlesSpec2.(sprintf('rpmMotor%d', mi_)), 'Value') + rpmMotors_mn(end+1) = mi_; + if nMot_mn > 4, rpmMotors_mn(end+1) = mi_ + nMot_mn/2; end + end + end + catch, rpmMotors_mn = [1 2 3 4]; end + + nHarm_sel = [1 2 3]; + try + harmSel_ = get(guiHandlesSpec2.rpmHarmDd, 'Value'); + harmMap_ = {[1 2 3], [1], [2], [3], [1 2], [1 3], [2 3]}; + nHarm_sel = harmMap_{harmSel_}; + catch, end + + rpmLw_mn = 1.5; + try + lwSel_ = get(guiHandlesSpec2.rpmLwDd, 'Value'); + lwMap_ = [0.5 1 1.5 2]; + rpmLw_mn = lwMap_(lwSel_); + catch, end + + % two-level cache: L1=FFT matrices (file/epoch), L2=interp results (motor sel) + fftKey_ = struct('fileVal', tmpFileVal_mn); + fftKey_.nSamp = zeros(1, numel(tmpFileVal_mn)); + for fi_ = 1:numel(tmpFileVal_mn), fftKey_.nSamp(fi_) = sum(tIND{tmpFileVal_mn(fi_)}); end + fftCache_ = []; + try fftCache_ = getappdata(PSspecfig2, 'mnFftCache'); catch, end + fftHit_ = ~isempty(fftCache_) && isstruct(fftCache_) && isfield(fftCache_, 'key') && ... + isequal(fftCache_.key.fileVal, fftKey_.fileVal) && isequal(fftCache_.key.nSamp, fftKey_.nSamp); + + if ~fftHit_ + % L1: compute RPM Hz (all motors) + PSD matrices per file per axis + fftData_ = cell(numel(tmpFileVal_mn), 1); + nHarm_mn = 3; + for fi_mn = 1:numel(tmpFileVal_mn) + fIdx_mn = tmpFileVal_mn(fi_mn); + fd_ = struct('valid', false); + try + if ~isfield(T{fIdx_mn}, 'eRPM_0_'), fftData_{fi_mn} = fd_; continue; end + nSamp_ = sum(tIND{fIdx_mn}); + mPoles_ = 14; + try mp_ = find(strcmp(SetupInfo{fIdx_mn}(:,1), 'motor_poles')); + if ~isempty(mp_), mPoles_ = str2double(SetupInfo{fIdx_mn}(mp_(1),2)); end + catch, end + if mPoles_ < 2, mPoles_ = 14; end + nEm_ = 0; + for mi_ = 0:7 + if isfield(T{fIdx_mn}, ['eRPM_' int2str(mi_) '_']), nEm_ = mi_+1; end + end + rpmHz_ = zeros(nSamp_, nEm_); + for mi_ = 0:nEm_-1 + ef_ = ['eRPM_' int2str(mi_) '_']; + if isfield(T{fIdx_mn}, ef_) + rpmHz_(:, mi_+1) = T{fIdx_mn}.(ef_)(tIND{fIdx_mn}) * 100 / (mPoles_/2) / 60; + end + end + winLen_ = min(512, floor(nSamp_/4)); + nWin_ = floor(nSamp_ / winLen_); + if nWin_ < 2, nWin_ = 1; winLen_ = nSamp_; end + fd_.rpmHz = rpmHz_(1:nWin_*winLen_, :); + fd_.nEm = nEm_; fd_.nSamp = nSamp_; fd_.lr = A_lograte(fIdx_mn); + fd_.winLen = winLen_; fd_.nWin = nWin_; + hannW_ = hann(winLen_); + Fs_ = fd_.lr * 1000; + halfN_ = floor(winLen_/2) + 1; + fd_.halfN = halfN_; + fd_.df = Fs_ * (1) / winLen_; + fd_.Fs = Fs_; + fd_.psd = cell(1, 3); + fd_.prePsd = cell(1, 3); + fd_.hasPre = false(1, 3); + for ai_ = 1:3 + gyroFld_ = ['gyroADC_' int2str(ai_-1) '_']; + if ~isfield(T{fIdx_mn}, gyroFld_), continue; end + gSig_ = T{fIdx_mn}.(gyroFld_)(tIND{fIdx_mn}); + gSig_ = gSig_(:); + sigMat_ = reshape(gSig_(1:nWin_*winLen_), winLen_, nWin_) .* hannW_; + fftMat_ = fft(sigMat_); + psdMat_ = abs(fftMat_(1:halfN_, :)).^2 / (Fs_ * winLen_); + psdMat_(2:end-1, :) = 2 * psdMat_(2:end-1, :); + fd_.psd{ai_} = 10*log10(psdMat_); + % pre-filter (gyroPrefilt synthesized in PSload) + preFld_ = ['gyroPrefilt_' int2str(ai_-1) '_']; + hp_ = isfield(T{fIdx_mn}, preFld_); + fd_.hasPre(ai_) = hp_; + if hp_ + preSig_ = T{fIdx_mn}.(preFld_)(tIND{fIdx_mn}); + preSig_ = preSig_(:); + preMat_ = reshape(preSig_(1:nWin_*winLen_), winLen_, nWin_) .* hannW_; + prePsd_ = abs(fft(preMat_)(1:halfN_, :)).^2 / (Fs_ * winLen_); + prePsd_(2:end-1, :) = 2 * prePsd_(2:end-1, :); + fd_.prePsd{ai_} = 10*log10(prePsd_); + end + end + fd_.valid = true; + catch + end + fftData_{fi_mn} = fd_; + end + fftS_ = struct(); fftS_.key = fftKey_; fftS_.data = fftData_; + setappdata(PSspecfig2, 'mnFftCache', fftS_); + else + fftData_ = fftCache_.data; + end + + % L2: interp from cached PSD using current motor selection (cheap) + nHarm_mn = 3; + mnData_ = cell(numel(tmpFileVal_mn), 3); + for fi_mn = 1:numel(tmpFileVal_mn) + fd_ = fftData_{fi_mn}; + if ~isstruct(fd_) || ~fd_.valid, continue; end + selCols_ = rpmMotors_mn(rpmMotors_mn <= fd_.nEm); + if isempty(selCols_), selCols_ = 1:min(4, fd_.nEm); end + rpmSel_ = fd_.rpmHz(:, selCols_); + winRpmMean_ = mean(reshape(mean(rpmSel_, 2), fd_.winLen, fd_.nWin), 1)'; + wIdx_ = (1:fd_.nWin)'; + for ai_ = 1:3 + d_ = struct('valid', false, 'hasPre', fd_.hasPre(ai_), 'avgN', [], 'stdN', [], 'avgPre', [], 'stdPre', []); + if isempty(fd_.psd{ai_}), mnData_{fi_mn, ai_} = d_; continue; end + psd_ = fd_.psd{ai_}; + noisePost_ = NaN(fd_.nWin, nHarm_mn); + for hi_ = 1:nHarm_mn + ft_ = winRpmMean_ * hi_; + bin_ = ft_ / fd_.df; + lo_ = floor(bin_) + 1; + frac_ = bin_ - floor(bin_); + ok_ = ft_ > 0 & lo_ >= 1 & lo_ < fd_.halfN; + vi_ = wIdx_(ok_); + if ~isempty(vi_) + iL_ = sub2ind(size(psd_), lo_(ok_), vi_); + iH_ = sub2ind(size(psd_), lo_(ok_)+1, vi_); + noisePost_(vi_, hi_) = psd_(iL_) .* (1-frac_(ok_)) + psd_(iH_) .* frac_(ok_); + end + end + d_.avgN = nanmean(noisePost_, 1); + d_.stdN = nanstd(noisePost_, 0, 1); + if fd_.hasPre(ai_) + pp_ = fd_.prePsd{ai_}; + noisePre_ = NaN(fd_.nWin, nHarm_mn); + for hi_ = 1:nHarm_mn + ft_ = winRpmMean_ * hi_; + bin_ = ft_ / fd_.df; + lo_ = floor(bin_) + 1; + frac_ = bin_ - floor(bin_); + ok_ = ft_ > 0 & lo_ >= 1 & lo_ < fd_.halfN; + vi_ = wIdx_(ok_); + if ~isempty(vi_) + iL_ = sub2ind(size(pp_), lo_(ok_), vi_); + iH_ = sub2ind(size(pp_), lo_(ok_)+1, vi_); + noisePre_(vi_, hi_) = pp_(iL_) .* (1-frac_(ok_)) + pp_(iH_) .* frac_(ok_); + end + end + d_.avgPre = nanmean(noisePre_, 1); + d_.stdPre = nanstd(noisePre_, 0, 1); + end + d_.valid = true; + mnData_{fi_mn, ai_} = d_; + end + end + + % plot from cached data + for ai = axesOpt_mn + stag_mn = sprintf('PSspec2_%d', ai+3); + h_mn = findobj(PSspecfig2, 'Type', 'axes', 'Tag', stag_mn); + if isempty(h_mn), h_mn = axes('Parent', PSspecfig2, 'Position', posInfo.Spec2Pos(ai+3,:), 'Tag', stag_mn); + else cla(h_mn); set(h_mn, 'Position', posInfo.Spec2Pos(ai+3,:)); set(PSspecfig2, 'CurrentAxes', h_mn); title(h_mn, ''); xlabel(h_mn, ''); ylabel(h_mn, ''); end + for fi_mn = 1:numel(tmpFileVal_mn) + fCol_mn = multiLineCols(fi_mn, :); + d_ = mnData_{fi_mn, ai}; + if isempty(d_) || ~isstruct(d_) || ~d_.valid + if fi_mn == 1 + text(0.5, 0.5, 'No RPM data', 'Parent', h_mn, 'Units', 'normalized', ... + 'HorizontalAlignment', 'center', 'Color', th.textSecondary, 'FontSize', fontsz); + end + continue; + end + if d_.hasPre + h_eb = errorbar(h_mn, nHarm_sel, d_.avgPre(nHarm_sel), d_.stdPre(nHarm_sel), 'o:'); + set(h_eb, 'Color', fCol_mn, 'LineWidth', rpmLw_mn, 'MarkerSize', 8); + hold(h_mn, 'on'); + end + h_eb = errorbar(h_mn, nHarm_sel, d_.avgN(nHarm_sel), d_.stdN(nHarm_sel), 'o-'); + set(h_eb, 'Color', fCol_mn, 'LineWidth', rpmLw_mn+0.5, 'MarkerFaceColor', fCol_mn, 'MarkerSize', 8); + hold(h_mn, 'on'); + end + harmLabels_ = {'1st','2nd','3rd'}; + set(h_mn, 'XTick', nHarm_sel, 'XTickLabel', harmLabels_(nHarm_sel)); + % auto-scale Y to data + error bars + yVals_ = []; + yErr_ = []; + for fi2_ = 1:numel(tmpFileVal_mn) + d2_ = mnData_{fi2_, ai}; + if isempty(d2_) || ~isstruct(d2_) || ~d2_.valid, continue; end + yVals_ = [yVals_; d2_.avgN(nHarm_sel)(:)]; + yErr_ = [yErr_; d2_.stdN(nHarm_sel)(:)]; + if d2_.hasPre + yVals_ = [yVals_; d2_.avgPre(nHarm_sel)(:)]; + yErr_ = [yErr_; d2_.stdPre(nHarm_sel)(:)]; + end + end + ok_ = isfinite(yVals_) & isfinite(yErr_); + if any(ok_) + yLo_ = min(yVals_(ok_) - yErr_(ok_)); + yHi_ = max(yVals_(ok_) + yErr_(ok_)); + yPad_ = max(3, (yHi_ - yLo_) * 0.15); + axis(h_mn, [min(nHarm_sel)-0.5 max(nHarm_sel)+0.5 yLo_-yPad_ yHi_+yPad_]); + else + axis(h_mn, [min(nHarm_sel)-0.5 max(nHarm_sel)+0.5 -60 20]); + end + xlabel(h_mn, 'Motor Harmonic', 'fontweight', 'bold', 'Color', th.textPrimary); + ylabel(h_mn, [axLabel_mn{ai} ' | Avg Motor Noise (dB)'], 'fontweight', 'bold', 'Color', th.textPrimary); + if ai == axesOpt_mn(1) + fIdx1_mn = tmpFileVal_mn(1); + try + fwStr = ''; qStr = ''; + fwRow = find(strcmp(SetupInfo{fIdx1_mn}(:,1), 'rpm_filter_weights')); + if ~isempty(fwRow), fwStr = SetupInfo{fIdx1_mn}{fwRow(1),2}; end + qRow = find(strcmp(SetupInfo{fIdx1_mn}(:,1), 'rpm_filter_q')); + if ~isempty(qRow), qStr = ['Q' SetupInfo{fIdx1_mn}{qRow(1),2}]; end + if ~isempty(fwStr) || ~isempty(qStr) + fCol1_mn = multiLineCols(1,:); + text(0.98, 0.95, ['Filter Weights | ' qStr], 'Parent', h_mn, 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'Color', fCol1_mn, 'FontSize', fontsz-1, 'FontWeight', 'bold'); + text(0.98, 0.85, fwStr, 'Parent', h_mn, 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'Color', fCol1_mn, 'FontSize', fontsz-1); + end + catch, end + end + grid(h_mn, 'on'); + PSstyleAxes(h_mn, th); + end +end + +allax = findobj(PSspecfig2, 'Type', 'axes'); +for axi = 1:numel(allax), PSstyleAxes(allax(axi), th); end +PSdatatipSetup(PSspecfig2); +try PSresizeCP(PSspecfig2, []); catch, end + +set(PSspecfig2, 'pointer', 'arrow') +updateSpec=0; +end + + diff --git a/src/plot/PSplotStats.m b/src/plot/PSplotStats.m index 9f430c4..c557307 100644 --- a/src/plot/PSplotStats.m +++ b/src/plot/PSplotStats.m @@ -1,1151 +1,247 @@ -%% PSplotStats - script to plot flight statistics - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if ~isempty(filenameA) || ~isempty(filenameB) - set(PSstatsfig, 'pointer', 'watch') - pause(.05) - %% update fonts - -PSstatsfig_pos = get(PSstatsfig, 'Position'); -screensz_tmp = get(0,'ScreenSize'); if PSstatsfig_pos(3) > 10, PSstatsfig_pos(3:4) = PSstatsfig_pos(3:4) ./ screensz_tmp(3:4); end -prop_max_screen=(max([PSstatsfig_pos(3) PSstatsfig_pos(4)])); -fontsz5=round(screensz_multiplier*prop_max_screen); - -set(guiHandlesStats.saveFig5, 'FontSize', fontsz5); -set(guiHandlesStats.refresh, 'FontSize', fontsz5); -set(guiHandlesStats.degsecStick, 'FontSize', fontsz5); -set(guiHandlesStats.crossAxesStats, 'FontSize', fontsz5); - -set(guiHandlesStats.crossAxesStats_text, 'FontSize', fontsz5); -set(guiHandlesStats.crossAxesStats_input, 'FontSize', fontsz5); -set(guiHandlesStats.crossAxesStats_text2, 'FontSize', fontsz5); -set(guiHandlesStats.crossAxesStats_input2, 'FontSize', fontsz5); - -%% Histograms - -if get(guiHandlesStats.crossAxesStats, 'Value')==1 - if ~isempty(filenameA) - if ~updateStats - - rcRates=dataA.rates(1,:); - rcExpo=dataA.rates(2,:); - Srates=dataA.rates(3,:); - const=200; - if FirmwareCode_A==INAV, const=1000; end - - RateCurveRoll_A=PSrc2deg([0:5:500],dataA.rates(1,1), dataA.rates(2,1), dataA.rates(3,1), const); - RateCurvePitch_A=PSrc2deg([0:5:500],dataA.rates(1,2), dataA.rates(2,2), dataA.rates(3,2), const); - RateCurveYaw_A=PSrc2deg([0:5:500],dataA.rates(1,3), dataA.rates(2,3), dataA.rates(3,3), const); - - Yscale=round((max([max(RateCurveRoll_A) max(RateCurvePitch_A) max(RateCurveYaw_A)])) / 50) * 50; - - if get(guiHandlesStats.degsecStick, 'Value')==1, - RateCurveRoll_A=(diff(RateCurveRoll_A)); - RateCurvePitch_A=(diff(RateCurvePitch_A)); - RateCurveYaw_A=(diff(RateCurveYaw_A)); - - Yscale=round(max([max(RateCurveRoll_A) max(RateCurvePitch_A) max(RateCurveYaw_A)])); - end - - Rpercent_A=PSPercent(DATtmpA.RCcommand(1,:)); - Ppercent_A=PSPercent(DATtmpA.RCcommand(2,:)); - Ypercent_A=PSPercent(DATtmpA.RCcommand(3,:)); - Tpercent_A=DATtmpA.RCRate(4,:); % already computed for throttle - end - - hhist=subplot('position',posInfo.statsPos(1,:)); - cla - h=histogram(Rpercent_A,'Normalization','probability','BinWidth',1); - y=xlabel('% roll','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight','fontweight','bold') - set(h,'FaceColor',[colorA],'FaceAlpha',.9, 'edgecolor',[colorA],'EdgeAlpha',.7) - - hold on - - [ax,h1,h2]=plotyy(0,0,[1:length(RateCurveRoll_A)-1],RateCurveRoll_A(1,2:end)); - set(ax(1),'Ycolor',[colorA]) - set(ax(2),'Xlim',[1 100],'YLim',[0 Yscale] ,'ytick',[0 round(Yscale/2) Yscale], 'Ycolor','k','fontsize',fontsz5) - set(get(ax(2), 'YLabel'), 'String', 'deg/s'); - if get(guiHandlesStats.degsecStick, 'Value')==1, - set(get(ax(2), 'YLabel'), 'String', 'deg/s/stick travel units'); - end - set(h2,'color',[.5 .5 .5],'LineWidth',1.5) - hold(ax(2),'on'); h=plot([20 40 60 80],[RateCurveRoll_A(20) RateCurveRoll_A(40) RateCurveRoll_A(60) RateCurveRoll_A(80)],'ko','Parent', ax(2)); - set(h,'markerfacecolor','k') - - set(hhist,'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(1,:)]); - axis([1 100 0 .1]) - - text(21, RateCurveRoll_A(20),[int2str(RateCurveRoll_A(20)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(41, RateCurveRoll_A(40),[int2str(RateCurveRoll_A(40)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(61, RateCurveRoll_A(60),[int2str(RateCurveRoll_A(60)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(81, RateCurveRoll_A(80),[int2str(RateCurveRoll_A(80)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(2, .095,['rates: ' num2str(rcRates(1))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .085,['expo: ' num2str(rcExpo(1))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .075,['super: ' num2str(Srates(1))],'Parent', ax(1),'fontsize',fontsz5); - axis([1 100 0 .1]) - grid on - - hhist=subplot('position',posInfo.statsPos(2,:)); - cla - h=histogram(Ppercent_A,'Normalization','probability','BinWidth',1); - set(h,'FaceColor',[colorA],'FaceAlpha',.9, 'edgecolor',[colorA],'EdgeAlpha',.7) - y=xlabel('% pitch','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight','fontweight','bold') - hold on - [ax,h1,h2]=plotyy(0,0,[1:length(RateCurvePitch_A)-1],RateCurvePitch_A(1,2:end)); - set(ax(1),'Ycolor',[colorA]) - set(ax(2),'Xlim',[1 100],'YLim',[0 Yscale] ,'ytick',[0 round(Yscale/2) Yscale], 'Ycolor','k','fontsize',fontsz5) - set(get(ax(2), 'YLabel'), 'String', 'deg/s'); - if get(guiHandlesStats.degsecStick, 'Value')==1, - set(get(ax(2), 'YLabel'), 'String', 'deg/s/stick travel units'); - end - set(h2,'color',[.5 .5 .5],'LineWidth',1.5) - hold(ax(2),'on'); h=plot([20 40 60 80],[RateCurvePitch_A(20) RateCurvePitch_A(40) RateCurvePitch_A(60) RateCurvePitch_A(80)],'ko','Parent', ax(2)); - set(h,'markerfacecolor','k') - - set(hhist,'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(2,:)]); - axis([1 100 0 .1]) - - text(21, RateCurvePitch_A(20),[int2str(RateCurvePitch_A(20)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(41, RateCurvePitch_A(40),[int2str(RateCurvePitch_A(40)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(61, RateCurvePitch_A(60),[int2str(RateCurvePitch_A(60)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(81, RateCurvePitch_A(80),[int2str(RateCurvePitch_A(80)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(2, .095,['rates: ' num2str(rcRates(2))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .085,['expo: ' num2str(rcExpo(2))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .075,['super: ' num2str(Srates(2))],'Parent', ax(1),'fontsize',fontsz5); - axis([1 100 0 .1]) - grid on - - hhist=subplot('position',posInfo.statsPos(3,:)); - cla - h=histogram(Ypercent_A,'Normalization','probability','BinWidth',1); - set(h,'FaceColor',[colorA],'FaceAlpha',.9, 'edgecolor',[colorA],'EdgeAlpha',.7) - y=xlabel('% yaw','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight','fontweight','bold') - - hold on - [ax,h1,h2]=plotyy(0,0,[1:length(RateCurveYaw_A)-1],RateCurveYaw_A(1,2:end)); - set(ax(1),'Ycolor',[colorA]) - set(ax(2),'Xlim',[1 100],'YLim',[0 Yscale] ,'ytick',[0 round(Yscale/2) Yscale], 'Ycolor','k','fontsize',fontsz5) - set(get(ax(2), 'YLabel'), 'String', 'deg/s'); - if get(guiHandlesStats.degsecStick, 'Value')==1, - set(get(ax(2), 'YLabel'), 'String', 'deg/s/stick travel units'); - end - set(h2,'color',[.5 .5 .5],'LineWidth',1.5) - hold(ax(2),'on'); h=plot([20 40 60 80],[RateCurveYaw_A(20) RateCurveYaw_A(40) RateCurveYaw_A(60) RateCurveYaw_A(80)],'ko','Parent', ax(2)); - set(h,'markerfacecolor','k') - - set(hhist,'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(3,:)]); - axis([1 100 0 .1]) - - text(21, RateCurveYaw_A(20),[int2str(RateCurveYaw_A(20)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(41, RateCurveYaw_A(40),[int2str(RateCurveYaw_A(40)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(61, RateCurveYaw_A(60),[int2str(RateCurveYaw_A(60)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(81, RateCurveYaw_A(80),[int2str(RateCurveYaw_A(80)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(2, .095,['rates: ' num2str(rcRates(3))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .085,['expo: ' num2str(rcExpo(3))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .075,['super: ' num2str(Srates(3))],'Parent', ax(1),'fontsize',fontsz5); - axis([1 100 0 .1]) - grid on - - hhist=subplot('position',posInfo.statsPos(4,:)); - cla - h=histogram(Tpercent_A,'Normalization','probability','BinWidth',1); - set(h,'FaceColor',[colorA],'FaceAlpha',.9, 'edgecolor',[colorA],'EdgeAlpha',.7); - grid on - y=xlabel('% throttle','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight', 'color',[colorA],'fontweight','bold') - set(hhist,'ycolor',[colorA],'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(4,:)]); - axis([1 100 0 .1]) - end - - if ~isempty(filenameB) - if ~updateStats - rcRates=dataB.rates(1,:); - rcExpo=dataB.rates(2,:); - Srates=dataB.rates(3,:); - const=200; - if FirmwareCode_B==INAV, const=1000; end - - RateCurveRoll_B=PSrc2deg([0:5:500],dataB.rates(1,1), dataB.rates(2,1), dataB.rates(3,1), const); - RateCurvePitch_B=PSrc2deg([0:5:500],dataB.rates(1,2), dataB.rates(2,2), dataB.rates(3,2), const); - RateCurveYaw_B=PSrc2deg([0:5:500],dataB.rates(1,3), dataB.rates(2,3), dataB.rates(3,3), const); - - Yscale=round((max([max(RateCurveRoll_B) max(RateCurvePitch_B) max(RateCurveYaw_B)])) / 50) * 50; - - if get(guiHandlesStats.degsecStick, 'Value')==1, - RateCurveRoll_B=(diff(RateCurveRoll_B)); - RateCurvePitch_B=(diff(RateCurvePitch_B)); - RateCurveYaw_B=(diff(RateCurveYaw_B)); - - Yscale=round(max([max(RateCurveRoll_B) max(RateCurvePitch_B) max(RateCurveYaw_B)])); - end - - Rpercent_B=PSPercent(DATtmpB.RCcommand(1,:)); - Ppercent_B=PSPercent(DATtmpB.RCcommand(2,:)); - Ypercent_B=PSPercent(DATtmpB.RCcommand(3,:)); - Tpercent_B=DATtmpB.RCRate(4,:); % already computed for throttle - end - - hhist=subplot('position',posInfo.statsPos(5,:)); - cla - h=histogram(Rpercent_B,'Normalization','probability','BinWidth',1); - y=xlabel('% roll','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight','fontweight','bold') - set(h,'FaceColor',[colorB],'FaceAlpha',.9, 'edgecolor',[colorB],'EdgeAlpha',.7) - - hold on - - [ax,h1,h2]=plotyy(0,0,[1:length(RateCurveRoll_B)-1],RateCurveRoll_B(1,2:end)); - set(ax(1),'Ycolor',[colorB]) - set(ax(2),'Xlim',[1 100],'YLim',[0 Yscale] ,'ytick',[0 round(Yscale/2) Yscale], 'Ycolor','k','fontsize',fontsz5) - set(get(ax(2), 'YLabel'), 'String', 'deg/s'); - if get(guiHandlesStats.degsecStick, 'Value')==1, - set(get(ax(2), 'YLabel'), 'String', 'deg/s/stick travel units'); - end - set(h2,'color',[.5 .5 .5],'LineWidth',1.5) - hold(ax(2),'on'); h=plot([20 40 60 80],[RateCurveRoll_B(20) RateCurveRoll_B(40) RateCurveRoll_B(60) RateCurveRoll_B(80)],'ko','Parent', ax(2)); - set(h,'markerfacecolor','k') - - set(hhist,'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(5,:)]); - axis([1 100 0 .1]) - - text(21, RateCurveRoll_B(20),[int2str(RateCurveRoll_B(20)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(41, RateCurveRoll_B(40),[int2str(RateCurveRoll_B(40)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(61, RateCurveRoll_B(60),[int2str(RateCurveRoll_B(60)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(81, RateCurveRoll_B(80),[int2str(RateCurveRoll_B(80)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(2, .095,['rates: ' num2str(rcRates(1))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .085,['expo: ' num2str(rcExpo(1))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .075,['super: ' num2str(Srates(1))],'Parent', ax(1),'fontsize',fontsz5); - - axis([1 100 0 .1]) - grid on - - hhist=subplot('position',posInfo.statsPos(6,:)); - cla - h=histogram(Ppercent_B,'Normalization','probability','BinWidth',1); - set(h,'FaceColor',[colorB],'FaceAlpha',.9, 'edgecolor',[colorB],'EdgeAlpha',.7) - y=xlabel('% pitch','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight','fontweight','bold'); - - hold on - [ax,h1,h2]=plotyy(0,0,[1:length(RateCurvePitch_B)-1],RateCurvePitch_B(1,2:end)); - set(ax(1),'Ycolor',[colorB]) - set(ax(2),'Xlim',[1 100],'YLim',[0 Yscale] ,'ytick',[0 round(Yscale/2) Yscale], 'Ycolor','k','fontsize',fontsz5) - set(get(ax(2), 'YLabel'), 'String', 'deg/s'); - if get(guiHandlesStats.degsecStick, 'Value')==1, - set(get(ax(2), 'YLabel'), 'String', 'deg/s/stick travel units'); - end - set(h2,'color',[.5 .5 .5],'LineWidth',1.5) - hold(ax(2),'on'); h=plot([20 40 60 80],[RateCurvePitch_B(20) RateCurvePitch_B(40) RateCurvePitch_B(60) RateCurvePitch_B(80)],'ko','Parent', ax(2)); - set(h,'markerfacecolor','k') - - set(hhist,'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(6,:)]); - axis([1 100 0 .1]) - - text(21, RateCurvePitch_B(20),[int2str(RateCurvePitch_B(20)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(41, RateCurvePitch_B(40),[int2str(RateCurvePitch_B(40)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(61, RateCurvePitch_B(60),[int2str(RateCurvePitch_B(60)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(81, RateCurvePitch_B(80),[int2str(RateCurvePitch_B(80)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - - text(2, .095,['rates: ' num2str(rcRates(2))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .085,['expo: ' num2str(rcExpo(2))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .075,['super: ' num2str(Srates(2))],'Parent', ax(1),'fontsize',fontsz5); - axis([1 100 0 .1]) - grid on - - hhist=subplot('position',posInfo.statsPos(7,:)); - cla - h=histogram(Ypercent_B,'Normalization','probability','BinWidth',1); - set(h,'FaceColor',[colorB],'FaceAlpha',.9, 'edgecolor',[colorB],'EdgeAlpha',.7) - y=xlabel('% yaw','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight','fontweight','bold') - - hold on - [ax,h1,h2]=plotyy(0,0,[1:length(RateCurveYaw_B)-1],RateCurveYaw_B(1,2:end)); - set(ax(1),'Ycolor',[colorB]) - set(ax(2),'Xlim',[1 100],'YLim',[0 Yscale] ,'ytick',[0 round(Yscale/2) Yscale], 'Ycolor','k','fontsize',fontsz5) - set(get(ax(2), 'YLabel'), 'String', 'deg/s'); - if get(guiHandlesStats.degsecStick, 'Value')==1, - set(get(ax(2), 'YLabel'), 'String', 'deg/s/stick travel units'); - end - set(h2,'color',[.5 .5 .5],'LineWidth',1.5) - hold(ax(2),'on'); h=plot([20 40 60 80],[RateCurveYaw_B(20) RateCurveYaw_B(40) RateCurveYaw_B(60) RateCurveYaw_B(80)],'ko','Parent', ax(2)); - set(h,'markerfacecolor','k') - - set(hhist,'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(7,:)]); - axis([1 100 0 .1]) - - text(21, RateCurveYaw_B(20),[int2str(RateCurveYaw_B(20)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(41, RateCurveYaw_B(40),[int2str(RateCurveYaw_B(40)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(61, RateCurveYaw_B(60),[int2str(RateCurveYaw_B(60)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(81, RateCurveYaw_B(80),[int2str(RateCurveYaw_B(80)) 'deg/s'],'Parent', ax(2),'fontsize',fontsz5); - text(2, .095,['rates: ' num2str(rcRates(3))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .085,['expo: ' num2str(rcExpo(3))],'Parent', ax(1),'fontsize',fontsz5); - text(2, .075,['super: ' num2str(Srates(3))],'Parent', ax(1),'fontsize',fontsz5); - axis([1 100 0 .1]) - grid on - - hhist=subplot('position',posInfo.statsPos(8,:)); - cla - h=histogram(Tpercent_B,'Normalization','probability','BinWidth',1); - set(h,'FaceColor',[colorB],'FaceAlpha',.9, 'edgecolor',[colorB],'EdgeAlpha',.7) - grid on - y=xlabel('% throttle','fontweight','bold'); - set(y,'Units','normalized', 'position', [.5 -.1 1],'color',[.2 .2 .2]); - ylabel('% of flight', 'color',[colorB],'fontweight','bold') - set(hhist,'ycolor',[colorB],'tickdir','in','xlim',[1 100],'xtick',[1 20 40 60 80 100],'ylim',[0 .1],'ytick',[0 .05 .1],'xticklabels',{0 20 40 60 80 100},'yticklabels',{0 5 10},'fontsize',fontsz5, 'Position',[posInfo.statsPos(8,:)]); - axis([1 100 0 .1]) - end -end - -%% means/standard deviations - -if get(guiHandlesStats.crossAxesStats, 'Value')==2 - - cols=[0.06 0.3 0.54 0.78]; - rows=[0.76 0.53 0.3 0.08]; - k=0; - for c=1:length(cols) - for r=1:length(rows) - k=k+1; - posInfo.statsPos2(k,:)=[cols(c) rows(r) 0.18 0.16]; - end - end - lineThickness=2; - - if ~isempty(filenameA) - - Rpercent_A=DATtmpA.RCcommand(1,:)/5; - Ppercent_A=DATtmpA.RCcommand(2,:)/5; - Ypercent_A=DATtmpA.RCcommand(3,:)/5; - Tpercent_A=DATtmpA.RCRate(4,:); - - N=length(DATtmpA.GyroFilt(1,:)); - - % gyro - h1=subplot('position',posInfo.statsPos2(1,:)); cla - s1=errorbar([1],mean(abs(DATtmpA.GyroFilt(1,:))), std(abs(DATtmpA.GyroFilt(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpA.GyroFilt(1,:))));hold on - set(s1,'FaceColor',[colorA]);%[ColorSet(11,:)]) - s1=errorbar([2],mean(abs(DATtmpA.GyroFilt(2,:))), std(abs(DATtmpA.GyroFilt(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpA.GyroFilt(2,:)))); - set(s2,'FaceColor',[colorA]);%,[ColorSet(12,:)]) - s1=errorbar([3],mean(abs(DATtmpA.GyroFilt(1,:))), std(abs(DATtmpA.GyroFilt(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpA.GyroFilt(1,:)))); - set(s3,'FaceColor',[colorA]);%,[ColorSet(13,:)]) - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Gyro| [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ymax=ceil(max(mean(abs(DATtmpA.GyroFilt),2))+max((std(abs(DATtmpA.GyroFilt)')'))); - axis([.5 3.5 0 ymax]) - box off - - % RCRate - h1=subplot('position',posInfo.statsPos2(5,:)); cla - s1=errorbar([1],mean(abs(Rpercent_A)), std(abs(Rpercent_A)));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(Rpercent_A)));hold on - set(s1,'FaceColor',[colorA]);% - s1=errorbar([2],mean(abs(Ppercent_A)), std(abs(Ppercent_A)));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(Ppercent_A))); - set(s2,'FaceColor',[colorA]);% - s1=errorbar([3],mean(abs(Ypercent_A)), std(abs(Ypercent_A)));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(Ypercent_A))); - set(s3,'FaceColor',[colorA]);% - s1=errorbar([4],mean(abs(Tpercent_A)), std(abs(Tpercent_A)));hold on - set(s1,'color','k','linewidth',lineThickness) - s4=bar([4],mean(Tpercent_A)); - set(s4,'FaceColor',[colorA]);% - set(gca,'Xtick',[1 2 3 4],'xticklabel',{'R';'P';'Y';'T'},'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('% RPYT [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean % +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - axis([0.5 4.5 0 100]) - box off - - % pterm - h1=subplot('position',posInfo.statsPos2(2,:)); cla - s1=errorbar([1],mean(abs(DATtmpA.Pterm(1,:))), std(abs(DATtmpA.Pterm(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpA.Pterm(1,:))));hold on - set(s1,'FaceColor',[colorA]);% - s1=errorbar([2],mean(abs(DATtmpA.Pterm(2,:))), std(abs(DATtmpA.Pterm(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpA.Pterm(2,:)))); - set(s2,'FaceColor',[colorA]);% - s1=errorbar([3],mean(abs(DATtmpA.Pterm(3,:))), std(abs(DATtmpA.Pterm(3,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpA.Pterm(3,:)))); - set(s3,'FaceColor',[colorA]);% - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Pterm| [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ymax=ceil(max(mean(abs(DATtmpA.Pterm),2))+max((std(abs(DATtmpA.Pterm)')'))); - axis([.5 3.5 0 ymax]) - box off - - % fterm - h1=subplot('position',posInfo.statsPos2(6,:)); cla - s1=errorbar([1],mean(abs(DATtmpA.Fterm(1,:))), std(abs(DATtmpA.Fterm(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpA.Fterm(1,:))));hold on - set(s1,'FaceColor',[colorA]);% - s1=errorbar([2],mean(abs(DATtmpA.Fterm(2,:))), std(abs(DATtmpA.Fterm(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpA.Fterm(2,:)))); - set(s2,'FaceColor',[colorA]);% % - s1=errorbar([3],mean(abs(DATtmpA.Fterm(3,:))), std(abs(DATtmpA.Fterm(3,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpA.Fterm(3,:)))); - set(s3,'FaceColor',[colorA]); - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Fterm| [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ymax=ceil(max(mean(abs(DATtmpA.Fterm),2))+max((std(abs(DATtmpA.Fterm)')'))); - if ymax<1, ymax=10, end - axis([.5 3.5 0 ymax]) - box off - - % Iterm - h1=subplot('position',posInfo.statsPos2(3,:)); cla - s1=errorbar([1],mean(abs(DATtmpA.Iterm(1,:))), std(abs(DATtmpA.Iterm(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpA.Iterm(1,:))));hold on - set(s1,'FaceColor',[colorA]);% - s1=errorbar([2],mean(abs(DATtmpA.Iterm(2,:))), std(abs(DATtmpA.Iterm(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpA.Iterm(2,:)))); - set(s2,'FaceColor',[colorA]);% - s1=errorbar([3],mean(abs(DATtmpA.Iterm(3,:))), std(abs(DATtmpA.Iterm(3,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpA.Iterm(3,:)))); - set(s3,'FaceColor',[colorA]);% - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Iterm| [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ymax=ceil(max(mean(abs(DATtmpA.Iterm),2))+max((std(abs(DATtmpA.Iterm)')'))); - axis([.5 3.5 0 ymax]) - box off - - % dterm - h1=subplot('position',posInfo.statsPos2(7,:)); cla - s1=errorbar([1],mean(abs(DATtmpA.DtermFilt(1,:))), std(abs(DATtmpA.DtermFilt(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpA.DtermFilt(1,:))));hold on - set(s1,'FaceColor',[colorA]);% - s1=errorbar([2],mean(abs(DATtmpA.DtermFilt(2,:))), std(abs(DATtmpA.DtermFilt(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpA.DtermFilt(2,:)))); - set(s2,'FaceColor',[colorA]);% - set(gca,'Xtick',[1 2],'xticklabel',{'R';'P'},'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Dterm| [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ymax=ceil(max(mean(abs(DATtmpA.DtermFilt),2))+max((std(abs(DATtmpA.DtermFilt)')'))); - axis([.5 2.5 0 ymax]) - box off - - h1=subplot('position',posInfo.statsPos2(4,:)); cla - s1=errorbar([1],mean(DATtmpA.Motor12(1,:)), std(DATtmpA.Motor12(1,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(DATtmpA.Motor12(1,:)));hold on - set(s1,'FaceColor',[colorA]); - s1=errorbar([2],mean(DATtmpA.Motor12(2,:)), std(DATtmpA.Motor12(2,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(DATtmpA.Motor12(2,:))); - set(s2,'FaceColor',[colorA]);% - s1=errorbar([3],mean(DATtmpA.Motor34(1,:)), std(DATtmpA.Motor34(1,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(DATtmpA.Motor34(1,:))); - set(s3,'FaceColor',[colorA]);% - s1=errorbar([4],mean(DATtmpA.Motor34(2,:)), std(DATtmpA.Motor34(2,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s4=bar([4],mean(DATtmpA.Motor34(2,:))); - set(s4,'FaceColor',[colorA]);% - set(gca,'Xtick',[1 2 3 4],'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('Motors [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - axis([0.5 4.5 0 100]) - box off - - - h1=subplot('position',posInfo.statsPos2(8,:)); cla - s1=errorbar([1],mean(abs(DATtmpA.debug12(1,:))), std(abs(DATtmpA.debug12(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpA.debug12(1,:))));hold on - set(s1,'FaceColor',[colorA]);% - s1=errorbar([2],mean(abs(DATtmpA.debug12(2,:))), std(abs(DATtmpA.debug12(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpA.debug12(2,:)))); - set(s2,'FaceColor',[colorA]);% - s1=errorbar([3],mean(abs(DATtmpA.debug34(1,:))), std(abs(DATtmpA.debug34(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpA.debug34(1,:)))); - set(s3,'FaceColor',[colorA]);% - s1=errorbar([4],mean(abs(DATtmpA.debug34(2,:))), std(abs(DATtmpA.debug34(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s4=bar([4],mean(abs(DATtmpA.debug34(2,:)))); - set(s4,'FaceColor',[colorA]);% - set(gca,'Xtick',[1 2 3 4],'xcolor',[colorA],'ycolor',[colorA],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Debug| [A]','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorA]); - ymax=ceil(max([max(mean(abs(DATtmpA.debug12),2)) max(mean(abs(DATtmpA.debug34),2))]) + max([max((std(abs(DATtmpA.debug12)')')) max((std(abs(DATtmpA.debug34)')'))])); - axis([.5 4.5 0 ymax]) - box off - end - - - if ~isempty(filenameB) - - Rpercent_B=DATtmpB.RCcommand(1,:)/5; - Ppercent_B=DATtmpB.RCcommand(2,:)/5; - Ypercent_B=DATtmpB.RCcommand(3,:)/5; - Tpercent_B=DATtmpB.RCRate(4,:); - - N=length(DATtmpB.GyroFilt(1,:)); - - % gyro - h1=subplot('position',posInfo.statsPos2(9,:)); cla - s1=errorbar([1],mean(abs(DATtmpB.GyroFilt(1,:))), std(abs(DATtmpB.GyroFilt(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpB.GyroFilt(1,:))));hold on - set(s1,'FaceColor',[colorB]);%[ColorSet(11,:)]) - s1=errorbar([2],mean(abs(DATtmpB.GyroFilt(2,:))), std(abs(DATtmpB.GyroFilt(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpB.GyroFilt(2,:)))); - set(s2,'FaceColor',[colorB]);%,[ColorSet(12,:)]) - s1=errorbar([3],mean(abs(DATtmpB.GyroFilt(1,:))), std(abs(DATtmpB.GyroFilt(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpB.GyroFilt(1,:)))); - set(s3,'FaceColor',[colorB]);%,[ColorSet(13,:)]) - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Gyro| [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ymax=ceil(max(mean(abs(DATtmpB.GyroFilt),2))+max((std(abs(DATtmpB.GyroFilt)')'))); - axis([.5 3.5 0 ymax]) - box off - - % RCRate - h1=subplot('position',posInfo.statsPos2(13,:)); cla - s1=errorbar([1],mean(abs(Rpercent_B)), std(abs(Rpercent_B)));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(Rpercent_B)));hold on - set(s1,'FaceColor',[colorB]);% - s1=errorbar([2],mean(abs(Ppercent_B)), std(abs(Ppercent_B)));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(Ppercent_B))); - set(s2,'FaceColor',[colorB]);% - s1=errorbar([3],mean(abs(Ypercent_B)), std(abs(Ypercent_B)));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(Ypercent_B))); - set(s3,'FaceColor',[colorB]);% - s1=errorbar([4],mean(abs(Tpercent_B)), std(abs(Tpercent_B)));hold on - set(s1,'color','k','linewidth',lineThickness) - s4=bar([4],mean(Tpercent_B)); - set(s4,'FaceColor',[colorB]);% - set(gca,'Xtick',[1 2 3 4],'xticklabel',{'R';'P';'Y';'T'},'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('% RPYT [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean % +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - axis([0.5 4.5 0 100]) - box off - - % pterm - h1=subplot('position',posInfo.statsPos2(10,:)); cla - s1=errorbar([1],mean(abs(DATtmpB.Pterm(1,:))), std(abs(DATtmpB.Pterm(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpB.Pterm(1,:))));hold on - set(s1,'FaceColor',[colorB]);% - s1=errorbar([2],mean(abs(DATtmpB.Pterm(2,:))), std(abs(DATtmpB.Pterm(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpB.Pterm(2,:)))); - set(s2,'FaceColor',[colorB]);% - s1=errorbar([3],mean(abs(DATtmpB.Pterm(3,:))), std(abs(DATtmpB.Pterm(3,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpB.Pterm(3,:)))); - set(s3,'FaceColor',[colorB]);% - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Pterm| [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ymax=ceil(max(mean(abs(DATtmpB.Pterm),2))+max((std(abs(DATtmpB.Pterm)')'))); - axis([.5 3.5 0 ymax]) - box off - - % fterm - h1=subplot('position',posInfo.statsPos2(14,:)); cla - s1=errorbar([1],mean(abs(DATtmpB.Fterm(1,:))), std(abs(DATtmpB.Fterm(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpB.Fterm(1,:))));hold on - set(s1,'FaceColor',[colorB]);% - s1=errorbar([2],mean(abs(DATtmpB.Fterm(2,:))), std(abs(DATtmpB.Fterm(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpB.Fterm(2,:)))); - set(s2,'FaceColor',[colorB]);% % - s1=errorbar([3],mean(abs(DATtmpB.Fterm(3,:))), std(abs(DATtmpB.Fterm(3,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpB.Fterm(3,:)))); - set(s3,'FaceColor',[colorB]); - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Fterm| [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ymax=ceil(max(mean(abs(DATtmpB.Fterm),2))+max((std(abs(DATtmpB.Fterm)')'))); - if ymax<1, ymax=10, end - axis([.5 3.5 0 ymax]) - box off - - % Iterm - h1=subplot('position',posInfo.statsPos2(11,:)); cla - s1=errorbar([1],mean(abs(DATtmpB.Iterm(1,:))), std(abs(DATtmpB.Iterm(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpB.Iterm(1,:))));hold on - set(s1,'FaceColor',[colorB]);% - s1=errorbar([2],mean(abs(DATtmpB.Iterm(2,:))), std(abs(DATtmpB.Iterm(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpB.Iterm(2,:)))); - set(s2,'FaceColor',[colorB]);% - s1=errorbar([3],mean(abs(DATtmpB.Iterm(3,:))), std(abs(DATtmpB.Iterm(3,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpB.Iterm(3,:)))); - set(s3,'FaceColor',[colorB]);% - set(gca,'Xtick',[1 2 3],'xticklabel',{'R';'P';'Y'},'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Iterm| [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ymax=ceil(max(mean(abs(DATtmpB.Iterm),2))+max((std(abs(DATtmpB.Iterm)')'))); - axis([.5 3.5 0 ymax]) - box off - - % dterm - h1=subplot('position',posInfo.statsPos2(15,:)); cla - s1=errorbar([1],mean(abs(DATtmpB.DtermFilt(1,:))), std(abs(DATtmpB.DtermFilt(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpB.DtermFilt(1,:))));hold on - set(s1,'FaceColor',[colorB]);% - s1=errorbar([2],mean(abs(DATtmpB.DtermFilt(2,:))), std(abs(DATtmpB.DtermFilt(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpB.DtermFilt(2,:)))); - set(s2,'FaceColor',[colorB]);% - set(gca,'Xtick',[1 2],'xticklabel',{'R';'P'},'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Dterm| [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ymax=ceil(max(mean(abs(DATtmpB.DtermFilt),2))+max((std(abs(DATtmpB.DtermFilt)')'))); - axis([.5 2.5 0 ymax]) - box off - - h1=subplot('position',posInfo.statsPos2(12,:)); cla - s1=errorbar([1],mean(DATtmpB.Motor12(1,:)), std(DATtmpB.Motor12(1,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(DATtmpB.Motor12(1,:)));hold on - set(s1,'FaceColor',[colorB]); - s1=errorbar([2],mean(DATtmpB.Motor12(2,:)), std(DATtmpB.Motor12(2,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(DATtmpB.Motor12(2,:))); - set(s2,'FaceColor',[colorB]);% - s1=errorbar([3],mean(DATtmpB.Motor34(1,:)), std(DATtmpB.Motor34(1,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(DATtmpB.Motor34(1,:))); - set(s3,'FaceColor',[colorB]);% - s1=errorbar([4],mean(DATtmpB.Motor34(2,:)), std(DATtmpB.Motor34(2,:)));hold on - set(s1,'color','k','linewidth',lineThickness) - s4=bar([4],mean(DATtmpB.Motor34(2,:))); - set(s4,'FaceColor',[colorB]);% - set(gca,'Xtick',[1 2 3 4],'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('Motors [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - axis([0.5 4.5 0 100]) - box off - - - h1=subplot('position',posInfo.statsPos2(16,:)); cla - s1=errorbar([1],mean(abs(DATtmpB.debug12(1,:))), std(abs(DATtmpB.debug12(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s1=bar([1],mean(abs(DATtmpB.debug12(1,:))));hold on - set(s1,'FaceColor',[colorB]);% - s1=errorbar([2],mean(abs(DATtmpB.debug12(2,:))), std(abs(DATtmpB.debug12(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s2=bar([2],mean(abs(DATtmpB.debug12(2,:)))); - set(s2,'FaceColor',[colorB]);% - s1=errorbar([3],mean(abs(DATtmpB.debug34(1,:))), std(abs(DATtmpB.debug34(1,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s3=bar([3],mean(abs(DATtmpB.debug34(1,:)))); - set(s3,'FaceColor',[colorB]);% - s1=errorbar([4],mean(abs(DATtmpB.debug34(2,:))), std(abs(DATtmpB.debug34(2,:))));hold on - set(s1,'color','k','linewidth',lineThickness) - s4=bar([4],mean(abs(DATtmpB.debug34(2,:)))); - set(s4,'FaceColor',[colorB]);% - set(gca,'Xtick',[1 2 3 4],'xcolor',[colorB],'ycolor',[colorB],'YMinorGrid','on') - set(h1,'fontsize',fontsz5); - xlabel('|Debug| [B]','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ylabel('Mean +SD ','fontsize',fontsz5,'fontweight','bold','color',[colorB]); - ymax=ceil(max([max(mean(abs(DATtmpB.debug12),2)) max(mean(abs(DATtmpB.debug34),2))]) + max([max((std(abs(DATtmpB.debug12)')')) max((std(abs(DATtmpB.debug34)')'))])); - axis([.5 4.5 0 ymax]) - box off - end -end - - -%% Mode 1 topography -if get(guiHandlesStats.crossAxesStats, 'Value')==3 - - cols=[0.06 0.52]; - rows=[0.55 0.08]; - k=0; - for c=1:2 - for r=1:2 - k=k+1; - posInfo.statsPos2(k,:)=[cols(c) rows(r) 0.38 0.4]; - end - end - - - lineThickness=2; - - if ~isempty(filenameA) - - Rpercent_A=DATtmpA.RCcommand(1,:)/5; - Ppercent_A=DATtmpA.RCcommand(2,:)/5; - Ypercent_A=DATtmpA.RCcommand(3,:)/5; - Tpercent_A=DATtmpA.RCRate(4,:); - Tacceleration_A=[0 (smooth( diff((Tpercent_A)*10)*A_lograte, 50))']; - col=Tacceleration_A; - - cmap_crossaxisStat=b2r(-1, 1); - - h1=subplot('position',posInfo.statsPos2(1,:)); cla - s1=surface([Ypercent_A;Ypercent_A],[Ppercent_A;Ppercent_A],[Tacceleration_A;Tacceleration_A],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[-100:20:100]) - set(h1,'fontsize',fontsz5); - xlabel('% Yaw [A]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Pitch [A]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 -100 100]) - set(s1, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[-100 100],':k'); - plot([-100 100],[0 0],':k'); - - h2=subplot('position',posInfo.statsPos2(2,:)); cla - s2=surface([Rpercent_A;Rpercent_A],[Tpercent_A;Tpercent_A],[Tacceleration_A;Tacceleration_A],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h2,'fontsize',fontsz5); - xlabel('% Roll [A]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [A]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s2, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - end - - - if ~isempty(filenameB) - - Rpercent_B=DATtmpB.RCcommand(1,:)/5; - Ppercent_B=DATtmpB.RCcommand(2,:)/5; - Ypercent_B=DATtmpB.RCcommand(3,:)/5; - Tpercent_B=DATtmpB.RCRate(4,:); - Tacceleration_B=[0 (smooth( diff((Tpercent_B)*10)*B_lograte, 50))']; - col=Tacceleration_B; - - h3=subplot('position',posInfo.statsPos2(3,:)); cla - s3=surface([Ypercent_B;Ypercent_B],[Ppercent_B;Ppercent_B],[Tacceleration_B;Tacceleration_B],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[-100:20:100]) - set(h3,'fontsize',fontsz5); - xlabel('% Yaw [B]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Pitch [B]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 -100 100]) - set(s3, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[-100 100],':k'); - plot([-100 100],[0 0],':k'); - - h4=subplot('position',posInfo.statsPos2(4,:)); cla - s4=surface([Rpercent_B;Rpercent_B],[Tpercent_B;Tpercent_B],[Tacceleration_B;Tacceleration_B],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h4,'fontsize',fontsz5); - xlabel('% Roll [B]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [B]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s4, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - end - -end - -%% Mode 2 topography -if get(guiHandlesStats.crossAxesStats, 'Value')==4 - - cols=[0.06 0.52]; - rows=[0.55 0.08]; - k=0; - for c=1:2 - for r=1:2 - k=k+1; - posInfo.statsPos2(k,:)=[cols(c) rows(r) 0.38 0.4]; - end - end - - - lineThickness=2; - - if ~isempty(filenameA) - - Rpercent_A=DATtmpA.RCcommand(1,:)/5; - Ppercent_A=DATtmpA.RCcommand(2,:)/5; - Ypercent_A=DATtmpA.RCcommand(3,:)/5; - Tpercent_A=DATtmpA.RCRate(4,:); - Tacceleration_A=[0 (smooth( diff((Tpercent_A)*10)*A_lograte, 50))']; - col=Tacceleration_A; - - cmap_crossaxisStat=b2r(-1, 1); - - h1=subplot('position',posInfo.statsPos2(1,:)); cla - s1=surface([Ypercent_A;Ypercent_A],[Tpercent_A;Tpercent_A],[Tacceleration_A;Tacceleration_A],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h1,'fontsize',fontsz5); - xlabel('% Yaw [A]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [A]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s1, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - - h2=subplot('position',posInfo.statsPos2(2,:)); cla - s2=surface([Rpercent_A;Rpercent_A],[Ppercent_A;Ppercent_A],[Tacceleration_A;Tacceleration_A],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[-100:20:100]) - set(h2,'fontsize',fontsz5); - xlabel('% Roll [A]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Pitch [A]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 -100 100]) - set(s2, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[-100 100],':k'); - plot([-100 100],[0 0],':k'); - end - - - if ~isempty(filenameB) - - Rpercent_B=DATtmpB.RCcommand(1,:)/5; - Ppercent_B=DATtmpB.RCcommand(2,:)/5; - Ypercent_B=DATtmpB.RCcommand(3,:)/5; - Tpercent_B=DATtmpB.RCRate(4,:); - Tacceleration_B=[0 (smooth( diff((Tpercent_B)*10)*B_lograte, 50))']; - col=Tacceleration_B; - - h3=subplot('position',posInfo.statsPos2(3,:)); cla - s3=surface([Ypercent_B;Ypercent_B],[Tpercent_B;Tpercent_B],[Tacceleration_B;Tacceleration_B],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h3,'fontsize',fontsz5); - xlabel('% Yaw [B]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [B]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s3, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - - h4=subplot('position',posInfo.statsPos2(4,:)); cla - s4=surface([Rpercent_B;Rpercent_B],[Ppercent_B;Ppercent_B],[Tacceleration_B;Tacceleration_B],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[-100:20:100]) - set(h4,'fontsize',fontsz5); - xlabel('% Roll [B]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Pitch [B]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 -100 100]) - set(s4, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[-100 100],':k'); - plot([-100 100],[0 0],':k'); - end - -end -%% each against throttle - -if get(guiHandlesStats.crossAxesStats, 'Value')==5 - - cols=[0.06 0.54]; - rows=[0.69 0.385 0.08]; - k=0; - for c=1:2 - for r=1:3 - k=k+1; - posInfo.statsPos2(k,:)=[cols(c) rows(r) 0.39 0.24]; - end - end - - - lineThickness=2; - - if ~isempty(filenameA) - - Rpercent_A=DATtmpA.RCcommand(1,:)/5; - Ppercent_A=DATtmpA.RCcommand(2,:)/5; - Ypercent_A=DATtmpA.RCcommand(3,:)/5; - Tpercent_A=DATtmpA.RCRate(4,:); - Tacceleration_A=[0 (smooth( diff((Tpercent_A)*10)*A_lograte, 50))']; - col=Tacceleration_A; - - cmap_crossaxisStat=b2r(-1, 1); - - h1=subplot('position',posInfo.statsPos2(1,:)); cla - s1=surface([Rpercent_A;Rpercent_A],[Tpercent_A;Tpercent_A],[Tacceleration_A;Tacceleration_A],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h1,'fontsize',fontsz5); - xlabel('% Roll [A]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [A]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s1, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - - h2=subplot('position',posInfo.statsPos2(2,:)); cla - s2=surface([Ppercent_A;Ppercent_A],[Tpercent_A;Tpercent_A],[Tacceleration_A;Tacceleration_A],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h2,'fontsize',fontsz5); - xlabel('% Pitch [A]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [A]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s2, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - - h3=subplot('position',posInfo.statsPos2(3,:)); cla - s3=surface([Ypercent_A;Ypercent_A],[Tpercent_A;Tpercent_A],[Tacceleration_A;Tacceleration_A],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h3,'fontsize',fontsz5); - xlabel('% Yaw [A]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [A]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s3, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - end - - - if ~isempty(filenameB) - - Rpercent_B=DATtmpB.RCcommand(1,:)/5; - Ppercent_B=DATtmpB.RCcommand(2,:)/5; - Ypercent_B=DATtmpB.RCcommand(3,:)/5; - Tpercent_B=DATtmpB.RCRate(4,:); - Tacceleration_B=[0 (smooth( diff((Tpercent_B)*10)*B_lograte, 50))']; - col=Tacceleration_B; - - h4=subplot('position',posInfo.statsPos2(4,:)); cla - s4=surface([Rpercent_B;Rpercent_B],[Tpercent_B;Tpercent_B],[Tacceleration_B;Tacceleration_B],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h4,'fontsize',fontsz5); - xlabel('% Roll [B]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [B]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s4, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - - h5=subplot('position',posInfo.statsPos2(5,:)); cla - s5=surface([Ppercent_B;Ppercent_B],[Tpercent_B;Tpercent_B],[Tacceleration_B;Tacceleration_B],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h5,'fontsize',fontsz5); - xlabel('% Pitch [B]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [B]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s5, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - - h6=subplot('position',posInfo.statsPos2(6,:)); cla - s6=surface([Ypercent_B;Ypercent_B],[Tpercent_B;Tpercent_B],[Tacceleration_B;Tacceleration_B],[col;col],'facecol','no','edgecol','interp','linew',lineThickness); - colormap(cmap_crossaxisStat); - set(gca,'CLim',[-zScale zScale],'xtick',[-100:20:100],'ytick',[0:20:100]) - set(h6,'fontsize',fontsz5); - xlabel('% Yaw [B]','fontsize',fontsz5,'fontweight','bold'); - ylabel('% Throttle [B]','fontsize',fontsz5,'fontweight','bold'); - axis([-100 100 0 100]) - set(s6, 'EdgeAlpha',zTransparency); - hold on; plot([0 0],[0 100],':k'); - plot([-100 100],[50 50],':k'); - end -end - - - - -updateStats=0; -set(PSstatsfig, 'pointer', 'arrow') -end - - -function newmap = b2r(cmin_input,cmax_input) -%BLUEWHITERED Blue, white, and red color map. -% this matlab file is designed to draw anomaly figures. the color of -% the colorbar is from blue to white and then to red, corresponding to -% the anomaly values from negative to zero to positive, respectively. -% The color white always correspondes to value zero. -% -% You should input two values like caxis in matlab, that is the min and -% the max value of color values designed. e.g. colormap(b2r(-3,5)) -% -% the brightness of blue and red will change according to your setting, -% so that the brightness of the color corresponded to the color of his -% opposite number -% e.g. colormap(b2r(-3,6)) is from light blue to deep red -% e.g. colormap(b2r(-3,3)) is from deep blue to deep red -% -% I'd advise you to use colorbar first to make sure the caxis' cmax and cmin. -% Besides, there is also another similar colorbar named 'darkb2r', in which the -% color is darker. -% -% by Cunjie Zhang, 2011-3-14 -% find bugs ====> email : daisy19880411@126.com -% updated: Robert Beckman help to fix the bug when start point is zero, 2015-04-08 -% -% Examples: -% ------------------------------ -% figure -% peaks; -% colormap(b2r(-6,8)), colorbar, title('b2r') -% - - -%% check the input -if nargin ~= 2 ; - disp('input error'); - disp('input two variables, the range of caxis , for example : colormap(b2r(-3,3))'); -end - -if cmin_input >= cmax_input - disp('input error'); - disp('the color range must be from a smaller one to a larger one'); -end - -%% control the figure caxis -lims = get(gca, 'CLim'); % get figure caxis formation -caxis([cmin_input cmax_input]); - -%% color configuration : from blue to to white then to red - -red_top = [1 0 0]; -white_middle= [1 1 1]; -blue_bottom = [0 0 1]; - -%% color interpolation - -color_num = 251; -color_input = [blue_bottom; white_middle; red_top]; -oldsteps = linspace(-1, 1, size(color_input,1)); -newsteps = linspace(-1, 1, color_num); - -%% Category Discussion according to the cmin and cmax input - -% the color data will be remaped to color range from -max(abs(cmin_input),cmax_input) -% to max(abs(cmin_input),cmax_input) , and then squeeze the color data -% in order to make sure the blue and red color selected corresponded -% to their math values - -% for example : -% if b2r(-3,6) ,the color range is from light blue to deep red , so that -% the light blue valued at -3 correspondes to light red valued at 3 - - -%% Category Discussion according to the cmin and cmax input -% first : from negative to positive -% then : from positive to positive -% last : from negative to negative - -for j=1:3 - newmap_all(:,j) = min(max(transpose(interp1(oldsteps, color_input(:,j), newsteps)), 0), 1); -end - -if (cmin_input < 0) && (cmax_input > 0) ; - - - if abs(cmin_input) < cmax_input - - % |--------|---------|--------------------| - % -cmax cmin 0 cmax [cmin,cmax] - - start_point = max(round((cmin_input+cmax_input)/2/cmax_input*color_num),1); - newmap = squeeze(newmap_all(start_point:color_num,:)); - - elseif abs(cmin_input) >= cmax_input - - % |------------------|------|--------------| - % cmin 0 cmax -cmin [cmin,cmax] - - end_point = max(round((cmax_input-cmin_input)/2/abs(cmin_input)*color_num),1); - newmap = squeeze(newmap_all(1:end_point,:)); - end - - -elseif cmin_input >= 0 - - if lims(1) < 0 - disp('caution:') - disp('there are still values smaller than 0, but cmin is larger than 0.') - disp('some area will be in red color while it should be in blue color') - end - - % |-----------------|-------|-------------| - % -cmax 0 cmin cmax [cmin,cmax] - - start_point = max(round((cmin_input+cmax_input)/2/cmax_input*color_num),1); - newmap = squeeze(newmap_all(start_point:color_num,:)); - -elseif cmax_input <= 0 - - if lims(2) > 0 - disp('caution:') - disp('there are still values larger than 0, but cmax is smaller than 0.') - disp('some area will be in blue color while it should be in red color') - end - - % |------------|------|--------------------| - % cmin cmax 0 -cmin [cmin,cmax] - - end_point = max(round((cmax_input-cmin_input)/2/abs(cmin_input)*color_num),1); - newmap = squeeze(newmap_all(1:end_point,:)); -end - - - -end - - +%% PSplotStats - flight statistics (histograms, mean+SD) + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +if ~exist('fnameMaster','var') || isempty(fnameMaster), return; end + +try + +th = PStheme(); +set(PSstatsfig, 'pointer', 'watch'); + +% clean up all old stats axes before replotting +try delete(findobj(PSstatsfig, 'Type', 'axes')); catch, end + +fA = get(guiHandlesStats.FileA, 'Value'); +fB = []; +if Nfiles >= 2 && isfield(guiHandlesStats, 'FileB') && ishandle(guiHandlesStats.FileB) + fB = get(guiHandlesStats.FileB, 'Value'); + if fB == fA, fB = []; end +end + +plotMode = get(guiHandlesStats.crossAxesStats, 'Value'); + +%% Histograms +if plotMode == 1 + + clear posInfo.statsPos + cols = [0.06 0.54]; + rows = [0.69 0.48 0.27 0.06]; + k = 0; + for c = 1:2 + for r = 1:4 + k = k + 1; + posInfo.statsPos(k,:) = [cols(c) rows(r) 0.39 0.18]; + end + end + + axLbl = {'% roll', '% pitch', '% yaw', '% throttle'}; + rcFields = {'rcCommand_0_', 'rcCommand_1_', 'rcCommand_2_'}; + + % File 1 + if ~updateStats + for q = 1:3 + if isfield(T{fA}, rcFields{q}) + Rpct_A{q} = PSPercent(T{fA}.(rcFields{q})(tIND{fA})); + else + Rpct_A{q} = []; + end + end + Tpct_A = T{fA}.setpoint_3_(tIND{fA}) / 10; + end + + for sp = 1:4 + stag_ = sprintf('PSstats_%d', sp); + hhist = axes('Parent', PSstatsfig, 'Position', posInfo.statsPos(sp,:), 'Tag', stag_); + if sp <= 3 + pctData = Rpct_A{sp}; + else + pctData = Tpct_A; + end + if ~isempty(pctData) + [nn, xx] = hist(pctData, 0:1:100); + nn = nn / sum(nn); + hb = bar(xx, nn, 1); + set(hb, 'FaceColor', colorA, 'EdgeColor', colorA); + end + y = xlabel(axLbl{sp}, 'fontweight', 'bold'); + set(y, 'Units', 'normalized', 'position', [.5 -.1 1], 'color', th.textPrimary); + ylabel('% of flight', 'fontweight', 'bold'); + set(hhist, 'tickdir', 'in', 'xlim', [0 100], 'xtick', [0 20 40 60 80 100], ... + 'ylim', [0 .1], 'ytick', [0 .05 .1], ... + 'xticklabel', {'0','20','40','60','80','100'}, ... + 'yticklabel', {'0','5','10'}, 'fontsize', fontsz5); + axis([0 100 0 .1]); + grid on; + end + + % File 2 + if ~isempty(fB) + if ~updateStats + for q = 1:3 + if isfield(T{fB}, rcFields{q}) + Rpct_B{q} = PSPercent(T{fB}.(rcFields{q})(tIND{fB})); + else + Rpct_B{q} = []; + end + end + Tpct_B = T{fB}.setpoint_3_(tIND{fB}) / 10; + end + + for sp = 1:4 + stag_ = sprintf('PSstats_%d', sp+4); + hhist = axes('Parent', PSstatsfig, 'Position', posInfo.statsPos(sp+4,:), 'Tag', stag_); + if sp <= 3 + pctData = Rpct_B{sp}; + else + pctData = Tpct_B; + end + if ~isempty(pctData) + [nn, xx] = hist(pctData, 0:1:100); + nn = nn / sum(nn); + hb = bar(xx, nn, 1); + set(hb, 'FaceColor', colorB, 'EdgeColor', colorB); + end + y = xlabel(axLbl{sp}, 'fontweight', 'bold'); + set(y, 'Units', 'normalized', 'position', [.5 -.1 1], 'color', th.textPrimary); + ylabel('% of flight', 'fontweight', 'bold'); + set(hhist, 'tickdir', 'in', 'xlim', [0 100], 'xtick', [0 20 40 60 80 100], ... + 'ylim', [0 .1], 'ytick', [0 .05 .1], ... + 'xticklabel', {'0','20','40','60','80','100'}, ... + 'yticklabel', {'0','5','10'}, 'fontsize', fontsz5); + axis([0 100 0 .1]); + grid on; + end + end +end + + +%% Mean +/- SD +if plotMode == 2 + + cols = [0.06 0.30 0.54 0.78]; + rows = [0.69 0.48 0.27 0.06]; + k = 0; + for c = 1:length(cols) + for r = 1:length(rows) + k = k + 1; + posInfo.statsPos2(k,:) = [cols(c) rows(r) 0.18 0.16]; + end + end + lineThickness = 2; + + % field groups: {field_prefix, nAxes, xlabel, useAbs} + groups = { + {'gyroADC_', 3, '|Gyro|', true}; + {'axisP_', 3, '|Pterm|', true}; + {'axisI_', 3, '|Iterm|', true}; + {'axisD_', 2, '|Dterm|', true}; + {'setpoint_',3, '% RPYT', true}; + {'axisF_', 3, '|Fterm|', true}; + {'motor_', 4, 'Motors', false}; + {'debug_', 4, '|Debug|', true}; + }; + % subplot indices: col1=[1,2,3,4] col2=[5,6,7,8] col3=[9..] col4=[13..] + % File A: slots 1-8, File B: slots 9-16 + slotA = [1 2 3 7 5 6 4 8]; + slotB = [9 10 11 15 13 14 12 16]; + axLabelsRPY = {'R','P','Y'}; + axLabelsRPYT = {'R','P','Y','T'}; + axLabelsM = {'1','2','3','4'}; + axLabelsD4 = {'0','1','2','3'}; + + for fi = 1:2 + if fi == 1, f = fA; clr = colorA; slots = slotA; tag = '[1]'; + else + if isempty(fB), continue; end + f = fB; clr = colorB; slots = slotB; tag = '[2]'; + end + + for g = 1:length(groups) + grp = groups{g}; + prefix = grp{1}; + nAx = grp{2}; + xlbl = grp{3}; + useAbs = grp{4}; + + stag_ = sprintf('PSstats2_%d', slots(g)); + h1 = axes('Parent', PSstatsfig, 'Position', posInfo.statsPos2(slots(g),:), 'Tag', stag_); + + vals = zeros(nAx, 1); + sds = zeros(nAx, 1); + for q = 1:nAx + fld = [prefix int2str(q-1) '_']; + if ~isfield(T{f}, fld), continue; end + d = T{f}.(fld)(tIND{f}); + if useAbs, d = abs(d); end + vals(q) = mean(d); + sds(q) = std(d); + end + + % special: setpoint group adds throttle as 4th bar + if strcmp(prefix, 'setpoint_') + nAx = 4; + d = T{f}.setpoint_3_(tIND{f}) / 10; + vals(4) = mean(d); + sds(4) = std(d); + end + + for q = 1:nAx + s = errorbar(q, vals(q), sds(q)); hold on; + set(s, 'color', th.axesFg, 'linewidth', lineThickness); + s = bar(q, vals(q)); + set(s, 'FaceColor', clr); + end + + ymx_ = max(vals(1:nAx)+sds(1:nAx))*1.2+1; + if isnan(ymx_) || ymx_ < 1, ymx_ = 10; end + if nAx == 2 + set(gca, 'Xtick', 1:nAx, 'xticklabel', axLabelsRPY(1:2)); + axis([.5 2.5 0 ymx_]); + elseif nAx == 3 + set(gca, 'Xtick', 1:nAx, 'xticklabel', axLabelsRPY); + axis([.5 3.5 0 ymx_]); + elseif nAx == 4 && strcmp(prefix, 'setpoint_') + set(gca, 'Xtick', 1:4, 'xticklabel', axLabelsRPYT); + axis([.5 4.5 0 100]); + elseif nAx == 4 && strcmp(prefix, 'motor_') + set(gca, 'Xtick', 1:4, 'xticklabel', axLabelsM); + axis([.5 4.5 0 100]); + elseif nAx == 4 + set(gca, 'Xtick', 1:4, 'xticklabel', axLabelsD4); + axis([.5 4.5 0 ymx_]); + end + + set(gca, 'xcolor', clr, 'ycolor', clr, 'YMinorGrid', 'on'); + set(h1, 'fontsize', fontsz5); + xlabel([xlbl ' ' tag], 'fontsize', fontsz5, 'fontweight', 'bold', 'color', clr); + ylabel('Mean +SD', 'fontsize', fontsz5, 'fontweight', 'bold', 'color', clr); + box off; + end + end +end + + +%% Topography and Axes x Throttle (modes 3-5) +if plotMode >= 3 + ax_msg = axes('Parent', PSstatsfig, 'Position', [.1 .3 .8 .4]); + set(ax_msg, 'Visible', 'off'); + text(0.5, 0.5, {'Topography and Axes x Throttle modes', 'not yet ported to new data model.'}, ... + 'HorizontalAlignment', 'center', 'FontSize', fontsz, 'FontWeight', 'bold', ... + 'Color', th.textPrimary, 'Parent', ax_msg, 'Units', 'normalized'); +end + + +allax = findobj(PSstatsfig, 'Type', 'axes'); +for axi = 1:numel(allax), PSstyleAxes(allax(axi), th); end +try PSresizeCP(PSstatsfig, []); catch, end +set(PSstatsfig, 'pointer', 'arrow'); + +catch err + msgPSplotStats = PSerrorMessages('PSplotStats', err); +end diff --git a/src/plot/PSrunChirpAnalysis.m b/src/plot/PSrunChirpAnalysis.m index 1b88f54..c260025 100644 --- a/src/plot/PSrunChirpAnalysis.m +++ b/src/plot/PSrunChirpAnalysis.m @@ -13,7 +13,7 @@ function PSrunChirpAnalysis(T, setupInfo, debugIdx, Fs, tIND, axisIdx) % check CHIRP debug mode if ~isfield(T, 'debug_0_') - warndlg('No debug data in log — chirp analysis requires debug_mode = CHIRP'); + warndlg('No debug data in log - chirp analysis requires debug_mode = CHIRP'); return end diff --git a/src/plot/PStuningParams.m b/src/plot/PStuningParams.m index 51fa1ed..681ddeb 100644 --- a/src/plot/PStuningParams.m +++ b/src/plot/PStuningParams.m @@ -1,226 +1,578 @@ -%% PStuningParams - scripts for plotting tune-related parameters - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -PStunefig=figure(4); - -PStunefig_pos = get(PStunefig, 'Position'); -screensz_tmp = get(0,'ScreenSize'); if PStunefig_pos(3) > 10, PStunefig_pos(3:4) = PStunefig_pos(3:4) ./ screensz_tmp(3:4); end -prop_max_screen=(max([PStunefig_pos(3) PStunefig_pos(4)])); -fontsz=(screensz_multiplier*prop_max_screen); - -f = fields(guiHandlesTune); -for i = 1 : size(f,1) - try set(guiHandlesTune.(f{i}), 'FontSize', fontsz); catch, end -end - - -%% step resp computed directly from set point and gyro -ylab={'Roll';'Pitch';'Yaw'}; -ylab2={'roll';'pitch';'yaw'}; - -axesOptions = find([get(guiHandlesTune.plotR, 'Value') get(guiHandlesTune.plotP, 'Value') get(guiHandlesTune.plotY, 'Value')]); -lineStyle = {'-' ; '--' ; ':'}; -lnLabels = {'solid' ; 'dashed'; 'dotted'}; -p = {' P, I, D, Dm, F'; ' P, I, D, Dm, F'; ' P, I, D, cD'}; -pidlabels = p{get(guiHandles.Firmware, 'Value')}; - -%%%%%%%%%%%%% step resp %%%%%%%%%%%%% -figure(PStunefig) - -ymax = str2num(get(guiHandlesTune.maxYStepInput, 'String')); -ypos = [(ymax/3)*2.9 (ymax/3)*1.85 (ymax/3)*.8]; -hwarn=[]; -if ~get(guiHandlesTune.clearPlots, 'Value') - cnt = 0; - set(PStunefig, 'pointer', 'watch') - pause(.05); - - for f = get(guiHandlesTune.fileListWindowStep, 'Value') - fcntSR = fcntSR + 1; - if fcntSR <= 10 - cnt2 = 0; - for p = axesOptions - cnt = cnt + 1; - cnt2 = cnt2 + 1; - try - if ~updateStep - clear H G L - eval(['H = T{f}.setpoint_' int2str(p-1) '_(tIND{f});']) - eval(['G = T{f}.gyroADC_' int2str(p-1) '_(tIND{f});']) - [stepresp_A{p} tA] = PSstepcalc(H, G, A_lograte(f), get(guiHandlesTune.Ycorrection, 'Value'), get(guiHandlesTune.smoothFactor_select, 'Value')); - % xcorrLag(p) = finddelay(H, G) * A_lograte(f); - end - catch - stepresp_A{p}=[]; - end - - if get(guiHandlesTune.RPYcombo, 'Value') == 0 - h1=subplot('position',posInfo.TparamsPos(p,:)); - hold on - - if size(stepresp_A{p},1)>1 - s = []; - s = stepresp_A{p}; - m=nanmean(s); - - h1=plot(tA,m); - set(h1, 'color',[multiLineCols(fcntSR,:)],'linewidth', get(guiHandles.linewidth, 'Value')/1.5); - latencyHalfHeight(p, fcntSR) = (find(m>.5,1) / A_lograte(f)) - 1; - peakresp(p, fcntSR)=max(m(find(tA<150)));%max(m); %%%%%%%%%%%%% CONSTRAIN from 0-150ms %%%%% - peaktime(p, fcntSR)=find(m == max(m(find(tA<150)))) / A_lograte(f); - - eval(['PID=' ylab2{p} 'PIDF{f};']) - if cnt <= 3, h=text(505, ymax, [pidlabels]);set(h,'fontsize',fontsz,'fontweight','bold'); end - h=text(505, ymax-(fcntSR*(ymax*.09)), [int2str(fcntSR) ') ' PID ' (n=' int2str(size(stepresp_A{p},1)) ')']);set(h,'fontsize',fontsz); % | Peak = ' num2str(peakresp(fcntSR)) ', Peak Time = ' num2str(peaktime) 'ms, Latency = ' num2str(latencyHalfHeight(fcntSR)) 'ms']);set(h,'fontsize',fontsz); - set(h, 'Color',[multiLineCols(fcntSR,:)],'fontweight','bold') - set(h,'fontsize',fontsz) - else - peakresp(p, fcntSR) = nan; - peaktime(p, fcntSR) = nan; - latencyHalfHeight(p, fcntSR) = nan; - if cnt <= 3, h=text(505, ymax, [pidlabels]);set(h,'fontsize',fontsz,'fontweight','bold'); end - h=text(505, ymax-(fcntSR*(ymax*.09)), [int2str(fcntSR) ') insufficient data']); - set(h,'Color',[multiLineCols(fcntSR,:)],'fontsize',fontsz, 'fontweight','bold') - end - - set(gca,'fontsize',fontsz,'xminortick','on','yminortick','on','xtick',[0 100 200 300 400 500],'xticklabel',{'0' '100' '200' '300' '400' '500'},'ytick',[0 .25 .5 .75 1 1.25 1.5 1.75 2],'tickdir','out'); - - box off - if cnt <= 3, h=ylabel([ylab{p} ' Response '], 'fontweight','bold'); end - - xlabel('Time (ms)', 'fontweight','bold'); - - if p==1, title('Step Response Functions');end - h=plot([0 500],[1 1],'k--'); - set(h,'linewidth',.5) - axis([0 500 0 ymax]) - grid on - - - h2=subplot('position',posInfo.TparamsPos(p+6,:)); - h=plot(fcntSR, peakresp(p, fcntSR),'sk'); - set(h,'Markersize',markerSz, 'MarkerFaceColor', [multiLineCols(fcntSR,:)]) - set(gca,'fontsize',fontsz, 'ylim',[0.8 ymax],'ytick',[0.8:.1:ymax],'xlim',[0.5 fcntSR+0.5],'xtick',[1:fcntSR]) - ylabel([ylab{p} ' Peak '], 'fontweight','bold'); - xlabel('Test', 'fontweight','bold'); - hold on - grid on - plot([0 10],[1 1],'--k') - - h3=subplot('position',posInfo.TparamsPos(p+9,:)); - h=plot(fcntSR, latencyHalfHeight(p, fcntSR),'sk'); - set(h,'Markersize',markerSz, 'MarkerFaceColor', [multiLineCols(fcntSR,:)]) - - mn = min(latencyHalfHeight(p, :))-rem(min(latencyHalfHeight(p, :)),2); - mx = max(latencyHalfHeight(p, :))+rem(max(latencyHalfHeight(p, :)),2); - - ymaxLat = mx+4; - yminLat = mn-4; - try - set(gca,'fontsize',fontsz,'ylim',[yminLat ymaxLat],'ytick',[yminLat:2:ymaxLat], 'xtick',[1:fcntSR],'xlim',[0.5 fcntSR+0.5]) - catch - end - ylabel([ylab{p} ' Latency (ms) '], 'fontweight','bold'); - xlabel('Test', 'fontweight','bold'); - hold on - grid on - - - end - - - if get(guiHandlesTune.RPYcombo, 'Value') == 1 - h1=subplot('position',[0.0500 0.1 0.72 0.84]) - hold on - - if size(stepresp_A{p},1)>1 - s = []; - s = stepresp_A{p}; - m=nanmean(s); - - h1=plot(tA,m); - set(h1, 'color',[multiLineCols(fcntSR,:)],'linewidth', get(guiHandles.linewidth, 'Value')/1.5, 'linestyle', lineStyle{cnt2}); - latencyHalfHeight(p, fcntSR) = (find(m>.5,1) / A_lograte(f)) - 1; - peakresp(p, fcntSR)=max(m(find(tA<150)));%max(m); %%%%%%%%%%%%% CONSTRAIN from 0-150ms %%%%% - peaktime(p, fcntSR)=find(m == max(m(find(tA<150)))) / A_lograte(f); - - eval(['PID=' ylab2{p} 'PIDF{f};']) - if cnt <= 3 - if size(axesOptions,2) < 2 - h=text(505, ypos(p)+0.04, [ylab{p}]); - else - h=text(505, ypos(p)+0.04, [ylab{p} ' (' lnLabels{cnt2} ')']); - end - - set(h,'fontsize',fontsz,'fontweight','bold'); - h=text(505, ypos(p), [pidlabels]); - set(h,'fontsize',fontsz,'fontweight','bold'); - end - h=text(505, ypos(p)-(fcntSR*.044), [int2str(fcntSR) ') ' PID ' (n=' int2str(size(stepresp_A{p},1)) ')']);set(h,'fontsize',fontsz); % | Peak = ' num2str(peakresp(fcntSR)) ', Peak Time = ' num2str(peaktime) 'ms, Latency = ' num2str(latencyHalfHeight(fcntSR)) 'ms']);set(h,'fontsize',fontsz); - set(h, 'Color',[multiLineCols(fcntSR,:)],'fontweight','bold') - set(h,'fontsize',fontsz) - else - peakresp(p, fcntSR) = nan; - peaktime(p, fcntSR) = nan; - latencyHalfHeight(p, fcntSR) = nan; - if cnt <= 3 - if size(axesOptions,2) < 2 - h=text(505, ypos(p)+0.04, [ylab{p}]); - else - h=text(505, ypos(p)+0.04, [ylab{p} ' (' lnLabels{cnt2} ')']); - end - - set(h,'fontsize',fontsz,'fontweight','bold'); - h=text(505, ypos(p), [pidlabels]); - set(h,'fontsize',fontsz,'fontweight','bold'); - end - h=text(505, ypos(p)-(fcntSR*.044), [int2str(fcntSR) ') insufficient data']); - set(h,'Color',[multiLineCols(fcntSR,:)],'fontsize',fontsz, 'fontweight','bold') - end - - set(gca,'fontsize',fontsz,'xminortick','on','yminortick','on','xtick',[0 100 200 300 400 500],'xticklabel',{'0' '100' '200' '300' '400' '500'},'ytick',[0 .25 .5 .75 1 1.25 1.5 1.75 2],'tickdir','out'); - - box off - if cnt <= 3, h=ylabel(['Response '], 'fontweight','bold'); end - - xlabel('Time (ms)', 'fontweight','bold'); - - title('Step Response Functions'); - h=plot([0 500],[1 1],'k--'); - set(h,'linewidth',.5) - axis([0 500 0 ymax]) - grid on - end - end - - elseif fcntSR == 11 - warndlg('10 files maximum. Click reset.'); - end - end - set(PStunefig, 'pointer', 'arrow') - - updateStep=0; -else - for p = 1 : 3 - delete(subplot('position',posInfo.TparamsPos(p,:))) - delete(subplot('position',posInfo.TparamsPos(p+3,:))) - delete(subplot('position',posInfo.TparamsPos(p+6,:))) - delete(subplot('position',posInfo.TparamsPos(p+9,:))) - peaktime = []; - peakresp = []; - latencyHalfHeight = []; - latencyHalfHeight_std = []; - peakresp_std = []; - end -end - - - - - +%% PStuningParams - scripts for plotting tune-related parameters + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +PStunefig=figure(4); +th = PStheme(); + + + +%% step resp computed directly from set point and gyro +ylab={'Roll';'Pitch';'Yaw'}; +ylab2={'roll';'pitch';'yaw'}; + +% re-enable axes auto-hidden by previous run (only on fresh start) +if ~updateStep && fcntSR == 0 + rpyH_ = {guiHandlesTune.plotR, guiHandlesTune.plotP, guiHandlesTune.plotY}; + for pi_ = 1:3 + if strcmp(get(rpyH_{pi_}, 'Enable'), 'off') + set(rpyH_{pi_}, 'Value', 1, 'Enable', 'on'); + end + end +end + +axesOptions = find([get(guiHandlesTune.plotR, 'Value') get(guiHandlesTune.plotP, 'Value') get(guiHandlesTune.plotY, 'Value')]); + +% scale row heights to fill space when fewer than 3 RPY axes +nActiveTune = numel(axesOptions); +stdRows_t = [0.69 0.395 0.1]; stdRowH_t = 0.245; +if nActiveTune > 0 && nActiveTune < 3 && ~get(guiHandlesTune.RPYcombo, 'Value') + topY_t = stdRows_t(1) + stdRowH_t; botY_t = stdRows_t(3); gapT = 0.05; + rowH_t = (topY_t - botY_t - (nActiveTune-1)*gapT) / nActiveTune; + ci = 0; + for jj = axesOptions + ci = ci + 1; + yy = topY_t - ci*rowH_t - (ci-1)*gapT; + for cc = 0:3 + posInfo.TparamsPos(jj + cc*3, 2) = yy; + posInfo.TparamsPos(jj + cc*3, 4) = rowH_t; + end + end +else + for jj = 1:3 + for cc = 0:3 + posInfo.TparamsPos(jj + cc*3, 2) = stdRows_t(jj); + posInfo.TparamsPos(jj + cc*3, 4) = stdRowH_t; + end + end +end + +lineStyle = {'-' ; '--' ; ':'}; +lnLabels = {'solid' ; 'dashed'; 'dotted'}; +p = {' P, I, D, Dm, F'; ' P, I, D, Dm, F'; ' P, I, D, cD'; ... + ' P, I, D'; ' P, I, D'; ' P, I, D, F'; ' P, I, D'; ' P, I, D'}; +fwIdx = min(get(guiHandles.Firmware, 'Value'), numel(p)); +pidlabels = p{fwIdx}; + +%%%%%%%%%%%%% step resp %%%%%%%%%%%%% +figure(PStunefig); + +ymax = str2double(get(guiHandlesTune.maxYStepInput, 'String')); +ypos = [(ymax/3)*2.9 (ymax/3)*1.85 (ymax/3)*.8]; +hwarn=[]; +showBFsliders = isfield(guiHandlesTune, 'bfSliders') && get(guiHandlesTune.bfSliders, 'Value') == 2; + +if ~get(guiHandlesTune.clearPlots, 'Value') && showBFsliders + % BF Sliders view: read simplified_* headers and draw horizontal bars + set(PStunefig, 'pointer', 'watch'); drawnow; + sliderKeys_ = {'simplified_d_gain','simplified_pi_gain','simplified_feedforward_gain',... + 'simplified_d_max_gain','simplified_i_gain','simplified_pitch_d_gain',... + 'simplified_pitch_pi_gain','simplified_master_multiplier'}; + sliderLabels_ = {'Damping','Tracking','Stick Resp.','Dynamic D.',... + 'Drift-Wobble','Pitch D.','Pitch T.','Master'}; + nSliders_ = numel(sliderKeys_); + selFiles_ = get(guiHandlesTune.fileListWindowStep, 'Value'); + + h_sl = findobj(PStunefig, 'Type', 'axes', 'Tag', 'PSstep_bfsliders'); + if isempty(h_sl) + h_sl = axes('Parent', PStunefig, 'Position', [0.07 0.10 plotR-0.09 0.84], 'Tag', 'PSstep_bfsliders'); + else + set(PStunefig, 'CurrentAxes', h_sl); cla; + end + hold on; + + % horizontal bar lines + for si_ = 1:nSliders_ + plot([0 2], [si_ si_], '-', 'Color', th.textSecondary, 'LineWidth', 1.5); + end + + hasData_ = false; + for fi_ = 1:numel(selFiles_) + f_ = selFiles_(fi_); + if f_ > numel(SetupInfo), continue; end + colIdx_ = min(fi_, size(multiLineCols,1)); + vals_ = ones(1, nSliders_); + anyFound_ = false; + for si_ = 1:nSliders_ + idx_ = find(strcmp(SetupInfo{f_}(:,1), sliderKeys_{si_})); + if ~isempty(idx_) + v_ = str2double(SetupInfo{f_}{idx_(1), 2}); + if ~isnan(v_), vals_(si_) = v_ / 100; anyFound_ = true; end + end + end + if anyFound_ + hasData_ = true; + for si_ = 1:nSliders_ + plot(vals_(si_), si_, 'o', 'MarkerSize', 10, 'MarkerFaceColor', multiLineCols(colIdx_,:), ... + 'MarkerEdgeColor', 'none'); + end + end + end + + if ~hasData_ + text(1, nSliders_/2+0.5, 'No BF slider data in headers', 'fontsize', fontsz+2, ... + 'HorizontalAlignment', 'center', 'Color', th.textSecondary); + end + + set(h_sl, 'YTick', 1:nSliders_, 'YTickLabel', sliderLabels_, 'YDir', 'reverse', ... + 'XLim', [0 2], 'YLim', [0.5 nSliders_+0.5], ... + 'XTick', 0:0.2:2, 'fontsize', fontsz, 'TickDir', 'out'); + xlabel('Slider Position', 'fontweight', 'bold'); + title('Betaflight PID Sliders', 'fontweight', 'bold'); + grid on; box off; + PSstyleAxes(h_sl, th); + + % legend: file names with colors + for fi_ = 1:numel(selFiles_) + colIdx_ = min(fi_, size(multiLineCols,1)); + text(1.85, 0.7 + fi_*0.06, fnameMaster{selFiles_(fi_)}, 'fontsize', max(fontsz-1,6), ... + 'Color', multiLineCols(colIdx_,:), 'Units', 'normalized', ... + 'HorizontalAlignment', 'right', 'fontweight', 'bold'); + end + + % disable irrelevant controls in BF sliders mode + bfOff_ = {'plotR','plotP','plotY','snapManeuver','RPYcombo','rawTraces',... + 'Ycorrection','maxYStepInput','smoothFactor_select','srLatency',... + 'subsample','minRateInput','maxRateInput','minRateTxt','maxYStepTxt',... + 'period','markup'}; + for bi_ = 1:numel(bfOff_) + if isfield(guiHandlesTune, bfOff_{bi_}), set(guiHandlesTune.(bfOff_{bi_}), 'Enable', 'off'); end + end + + try PSresizeCP(PStunefig, []); catch, end + set(PStunefig, 'pointer', 'arrow'); + updateStep = 0; + +elseif ~get(guiHandlesTune.clearPlots, 'Value') + % delete BF sliders axes if switching back to normal view + h_del = findobj(PStunefig, 'Type', 'axes', 'Tag', 'PSstep_bfsliders'); + if ~isempty(h_del), delete(h_del); end + + % re-enable controls when leaving BF sliders mode + bfOff_ = {'plotR','plotP','plotY','snapManeuver','RPYcombo','rawTraces',... + 'Ycorrection','maxYStepInput','smoothFactor_select','srLatency',... + 'subsample','minRateInput','maxRateInput','minRateTxt','maxYStepTxt',... + 'period','markup'}; + for bi_ = 1:numel(bfOff_) + if isfield(guiHandlesTune, bfOff_{bi_}), set(guiHandlesTune.(bfOff_{bi_}), 'Enable', 'on'); end + end + + if ~updateStep && fcntSR == 0 + peakresp = []; peakresp_std = []; peaktime = []; + latencyHalfHeight = []; latencyHalfHeight_std = []; + settlingMin = []; settlingMax = []; + end + cnt = 0; + set(PStunefig, 'pointer', 'watch') + drawnow; + + for f = get(guiHandlesTune.fileListWindowStep, 'Value') + fcntSR = fcntSR + 1; + if fcntSR <= 10 + cnt2 = 0; + for p = axesOptions + cnt = cnt + 1; + cnt2 = cnt2 + 1; + try + if ~updateStep + H = T{f}.(['setpoint_' int2str(p-1) '_'])(tIND{f}); + G = T{f}.(['gyroADC_' int2str(p-1) '_'])(tIND{f}); + subMap_ = [0 1 3 5 7 10]; + subVal_ = subMap_(get(guiHandlesTune.subsample, 'Value')); + if subVal_ == 0 + fileDurSec_ = length(H) / (A_lograte(f)*1000); + if fileDurSec_ <= 20, subVal_ = 10; + elseif fileDurSec_ <= 60, subVal_ = 7; + else subVal_ = 3; end + end + minR_ = str2double(get(guiHandlesTune.minRateInput, 'String')); + maxR_ = str2double(get(guiHandlesTune.maxRateInput, 'String')); + if isnan(minR_), minR_ = 40; end + if isnan(maxR_), maxR_ = 500; end + if get(guiHandlesTune.snapManeuver, 'Value') + minR_ = maxR_; maxR_ = Inf; + end + [stepresp_A{p} tA] = PSstepcalc(H, G, A_lograte(f), get(guiHandlesTune.Ycorrection, 'Value'), get(guiHandlesTune.smoothFactor_select, 'Value'), subVal_, minR_, maxR_); + try xcorrLag_cache(p) = finddelay(H, G) / A_lograte(f); catch, xcorrLag_cache(p) = nan; end + end + catch + stepresp_A{p}=[]; + end + + if get(guiHandlesTune.RPYcombo, 'Value') == 0 + stag_ = sprintf('PSstep_%d', p); + h1 = findobj(PStunefig, 'Type', 'axes', 'Tag', stag_); + if isempty(h1), h1 = axes('Parent', PStunefig, 'Position', posInfo.TparamsPos(p,:), 'Tag', stag_); + else set(PStunefig, 'CurrentAxes', h1); end + hold on + + if size(stepresp_A{p},1)>1 + s = []; + s = stepresp_A{p}; + m=nanmean(s); + sd=nanstd(s); + + col_i = multiLineCols(fcntSR,:); + + % SD shading band + tFlip = [tA fliplr(tA)]; + sdBand = [m+sd fliplr(m-sd)]; + patch(tFlip, sdBand, col_i, 'FaceAlpha', 0.15, 'EdgeColor', 'none', 'Parent', gca); + + % raw segment traces - blend toward bg to simulate low alpha + if isfield(guiHandlesTune, 'rawTraces') && get(guiHandlesTune.rawTraces, 'Value') + col_raw = col_i * 0.12 + th.axesBg * 0.88; + nSeg_ = size(s,1); + tAll_ = repmat([tA NaN], 1, nSeg_); + sAll_ = reshape([s'; NaN(1, nSeg_)], 1, []); + plot(tAll_, sAll_, 'Color', col_raw, 'LineWidth', 0.5); + end + + h1=plot(tA,m); + set(h1, 'color', col_i, 'linewidth', get(guiHandles.linewidth, 'Value')/1.5); + if get(guiHandlesTune.srLatency, 'Value') == 2 && exist('xcorrLag_cache', 'var') + latencyHalfHeight(p, fcntSR) = xcorrLag_cache(p); + else + idx50_ = find(m > .5, 1); + if ~isempty(idx50_) + latencyHalfHeight(p, fcntSR) = (idx50_ / A_lograte(f)) - 1; + else + latencyHalfHeight(p, fcntSR) = nan; + end + end + peakIdx = find(tA < 150); + peakresp(p, fcntSR) = max(m(peakIdx)); + [~, pkI_] = max(m(peakIdx)); + peaktime(p, fcntSR) = pkI_(1) / A_lograte(f); + + % per-segment metrics for error bars (vectorized) + segPeaks = max(s(:, peakIdx), [], 2)'; + above50 = s > 0.5; + [~, firstAbove] = max(above50, [], 2); + segLats = (firstAbove' / A_lograte(f)) - 1; + segLats(~any(above50, 2)') = nan; + peakresp_std(p, fcntSR) = nanstd(segPeaks); + latencyHalfHeight_std(p, fcntSR) = nanstd(segLats); + + % settling metrics (200-500ms window) + settleIdx_ = find(tA > 200 & tA < 500); + if ~isempty(settleIdx_) + settlingMin(p, fcntSR) = min(m(settleIdx_)); + settlingMax(p, fcntSR) = max(m(settleIdx_)); + else + settlingMin(p, fcntSR) = nan; + settlingMax(p, fcntSR) = nan; + end + + pidvar = [ylab2{p} 'PIDF']; + PID = eval([pidvar '{f}']); + else + peakresp(p, fcntSR) = nan; + peaktime(p, fcntSR) = nan; + latencyHalfHeight(p, fcntSR) = nan; + peakresp_std(p, fcntSR) = nan; + latencyHalfHeight_std(p, fcntSR) = nan; + settlingMin(p, fcntSR) = nan; + settlingMax(p, fcntSR) = nan; + PID = ''; + end + + set(gca,'fontsize',fontsz,'xminortick','on','yminortick','on','xtick',[0 100 200 300 400 500],'xticklabel',{'0' '100' '200' '300' '400' '500'},'ytick',[0 .25 .5 .75 1 1.25 1.5 1.75 2],'tickdir','out'); + + box off + if cnt <= 3, h=ylabel([ylab{p} ' Response '], 'fontweight','bold'); end + + xlabel('Time (ms)', 'fontweight','bold'); + + if p==1, title('Step Response Functions');end + h=plot([0 500],[1 1],'--','Color',th.axesFg); + set(h,'linewidth',.5) + axis([0 500 0 ymax]) + grid on + + % Col 2: PID text in dedicated column + stag_ = sprintf('PSstep_%d', p+3); + hTxt = findobj(PStunefig, 'Type', 'axes', 'Tag', stag_); + if isempty(hTxt), hTxt = axes('Parent', PStunefig, 'Position', posInfo.TparamsPos(p+3,:), 'Tag', stag_, 'Visible', 'off', 'XLim', [0 1], 'YLim', [0 1]); + else set(PStunefig, 'CurrentAxes', hTxt); end + hold on + if size(stepresp_A{p},1)>1 + if cnt <= 3, h=text(0.05, 0.97, [pidlabels],'fontsize',fontsz,'fontweight','bold','Color',th.textPrimary); end + yBase_ = 0.88 - (fcntSR-1)*0.11; + h=text(0.05, yBase_, [int2str(fcntSR) ') ' PID ' (n=' int2str(size(stepresp_A{p},1)) ')'],'fontsize',fontsz); + set(h, 'Color',[multiLineCols(fcntSR,:)],'fontweight','bold') + metStr_ = sprintf('Pk=%.2f @%dms L=%dms S=[%.2f %.2f]', ... + peakresp(p,fcntSR), round(peaktime(p,fcntSR)), round(latencyHalfHeight(p,fcntSR)), ... + settlingMin(p,fcntSR), settlingMax(p,fcntSR)); + h=text(0.07, yBase_-0.045, metStr_,'fontsize',max(fontsz-1,6)); + set(h, 'Color',[multiLineCols(fcntSR,:)]) + else + if cnt <= 3, h=text(0.05, 0.97, [pidlabels],'fontsize',fontsz,'fontweight','bold','Color',th.textPrimary); end + yBase_ = 0.88 - (fcntSR-1)*0.11; + h=text(0.05, yBase_, [int2str(fcntSR) ') insufficient data'],'fontsize',fontsz); + set(h,'Color',[multiLineCols(fcntSR,:)],'fontweight','bold') + end + + % Col 3: Peak (skip if no data) + if ~isnan(peakresp(p, fcntSR)) + stag_ = sprintf('PSstep_%d', p+6); + h2 = findobj(PStunefig, 'Type', 'axes', 'Tag', stag_); + if isempty(h2), h2 = axes('Parent', PStunefig, 'Position', posInfo.TparamsPos(p+6,:), 'Tag', stag_); + else set(PStunefig, 'CurrentAxes', h2); end + h=plot(fcntSR, peakresp(p, fcntSR),'sk'); + set(h,'Markersize',markerSz, 'MarkerFaceColor', [multiLineCols(fcntSR,:)]) + if ~isnan(peakresp_std(p, fcntSR)) && peakresp_std(p, fcntSR) > 0 + ey = peakresp_std(p, fcntSR); + yc = peakresp(p, fcntSR); + line([fcntSR fcntSR], [yc-ey yc+ey], 'Color', multiLineCols(fcntSR,:), 'LineWidth', 1.2); + capW = 0.15; + line([fcntSR-capW fcntSR+capW], [yc-ey yc-ey], 'Color', multiLineCols(fcntSR,:), 'LineWidth', 1.2); + line([fcntSR-capW fcntSR+capW], [yc+ey yc+ey], 'Color', multiLineCols(fcntSR,:), 'LineWidth', 1.2); + end + ymxP = ymax; ymnP = 0.8; + for ei = 1:size(peakresp_std, 2) + if ~isnan(peakresp_std(p, ei)) + ymxP = max(ymxP, peakresp(p, ei) + peakresp_std(p, ei)); + ymnP = min(ymnP, peakresp(p, ei) - peakresp_std(p, ei)); + end + end + ymnP = floor(ymnP * 10) / 10; + ymxP = ceil(ymxP * 10) / 10; + set(gca,'fontsize',fontsz, 'ylim',[ymnP ymxP],'ytick',[ymnP:.1:ymxP],'xlim',[0.5 fcntSR+0.5],'xtick',[1:fcntSR]) + if cnt <= 3, title([ylab{p} ' Peak'], 'fontweight','bold'); end + hold on + grid on + plot([0 10],[1 1],'--','Color',th.axesFg) + end + + % Col 4: Latency (skip if no data) + if ~isnan(latencyHalfHeight(p, fcntSR)) + stag_ = sprintf('PSstep_%d', p+9); + h3 = findobj(PStunefig, 'Type', 'axes', 'Tag', stag_); + if isempty(h3), h3 = axes('Parent', PStunefig, 'Position', posInfo.TparamsPos(p+9,:), 'Tag', stag_); + else set(PStunefig, 'CurrentAxes', h3); end + h=plot(fcntSR, latencyHalfHeight(p, fcntSR),'sk'); + set(h,'Markersize',markerSz, 'MarkerFaceColor', [multiLineCols(fcntSR,:)]) + if ~isnan(latencyHalfHeight_std(p, fcntSR)) && latencyHalfHeight_std(p, fcntSR) > 0 + ey = latencyHalfHeight_std(p, fcntSR); + yc = latencyHalfHeight(p, fcntSR); + line([fcntSR fcntSR], [yc-ey yc+ey], 'Color', multiLineCols(fcntSR,:), 'LineWidth', 1.2); + capW = 0.15; + line([fcntSR-capW fcntSR+capW], [yc-ey yc-ey], 'Color', multiLineCols(fcntSR,:), 'LineWidth', 1.2); + line([fcntSR-capW fcntSR+capW], [yc+ey yc+ey], 'Color', multiLineCols(fcntSR,:), 'LineWidth', 1.2); + end + + mn = min(latencyHalfHeight(p, :)); + mx = max(latencyHalfHeight(p, :)); + for ei = 1:size(latencyHalfHeight_std, 2) + if ~isnan(latencyHalfHeight_std(p, ei)) + mn = min(mn, latencyHalfHeight(p, ei) - latencyHalfHeight_std(p, ei)); + mx = max(mx, latencyHalfHeight(p, ei) + latencyHalfHeight_std(p, ei)); + end + end + yminLat = 2*floor((mn-2)/2); + ymaxLat = 2*ceil((mx+2)/2); + latRange_ = ymaxLat - yminLat; + if latRange_ > 40, latStep_ = 10; + elseif latRange_ > 20, latStep_ = 5; + else latStep_ = 2; end + yminLat = latStep_*floor((mn-2)/latStep_); + ymaxLat = latStep_*ceil((mx+2)/latStep_); + try + set(gca,'fontsize',fontsz,'ylim',[yminLat ymaxLat],'ytick',[yminLat:latStep_:ymaxLat], 'xtick',[1:fcntSR],'xlim',[0.5 fcntSR+0.5]) + catch + end + if cnt <= 3, title([ylab{p} ' Latency (ms)'], 'fontweight','bold'); end + hold on + grid on + end + + + end + + + if get(guiHandlesTune.RPYcombo, 'Value') == 1 + h1 = findobj(PStunefig, 'Type', 'axes', 'Tag', 'PSstep_combo'); + if isempty(h1), h1 = axes('Parent', PStunefig, 'Position', [0.0500 0.1 0.72 0.84], 'Tag', 'PSstep_combo'); + else set(PStunefig, 'CurrentAxes', h1); end + hold on + + if size(stepresp_A{p},1)>1 + s = []; + s = stepresp_A{p}; + m=nanmean(s); + sd=nanstd(s); + + col_i = multiLineCols(fcntSR,:); + + % SD shading band + tFlip = [tA fliplr(tA)]; + sdBand = [m+sd fliplr(m-sd)]; + patch(tFlip, sdBand, col_i, 'FaceAlpha', 0.15, 'EdgeColor', 'none', 'Parent', gca); + + if isfield(guiHandlesTune, 'rawTraces') && get(guiHandlesTune.rawTraces, 'Value') + col_raw = col_i * 0.12 + th.axesBg * 0.88; + nSeg_ = size(s,1); + tAll_ = repmat([tA NaN], 1, nSeg_); + sAll_ = reshape([s'; NaN(1, nSeg_)], 1, []); + plot(tAll_, sAll_, 'Color', col_raw, 'LineWidth', 0.5); + end + + h1=plot(tA,m); + set(h1, 'color', col_i, 'linewidth', get(guiHandles.linewidth, 'Value')/1.5, 'linestyle', lineStyle{cnt2}); + if get(guiHandlesTune.srLatency, 'Value') == 2 && exist('xcorrLag_cache', 'var') + latencyHalfHeight(p, fcntSR) = xcorrLag_cache(p); + else + idx50_ = find(m > .5, 1); + if ~isempty(idx50_) + latencyHalfHeight(p, fcntSR) = (idx50_ / A_lograte(f)) - 1; + else + latencyHalfHeight(p, fcntSR) = nan; + end + end + peakIdx_ = find(tA < 150); + peakresp(p, fcntSR) = max(m(peakIdx_)); + [~, pkI_] = max(m(peakIdx_)); + peaktime(p, fcntSR) = pkI_(1) / A_lograte(f); + settleIdx_ = find(tA > 200 & tA < 500); + if ~isempty(settleIdx_) + settlingMin(p, fcntSR) = min(m(settleIdx_)); + settlingMax(p, fcntSR) = max(m(settleIdx_)); + else + settlingMin(p, fcntSR) = nan; + settlingMax(p, fcntSR) = nan; + end + + PID = eval([ylab2{p} 'PIDF{f}']); + if cnt <= 3 + if size(axesOptions,2) < 2 + h=text(505, ypos(p)+0.04, [ylab{p}]); + else + h=text(505, ypos(p)+0.04, [ylab{p} ' (' lnLabels{cnt2} ')']); + end + + set(h,'fontsize',fontsz,'fontweight','bold','Color',th.textPrimary); + h=text(505, ypos(p), [pidlabels]); + set(h,'fontsize',fontsz,'fontweight','bold','Color',th.textPrimary); + end + h=text(505, ypos(p)-(fcntSR*.07), [int2str(fcntSR) ') ' PID ' (n=' int2str(size(stepresp_A{p},1)) ')']); + set(h, 'Color',[multiLineCols(fcntSR,:)],'fontweight','bold','fontsize',fontsz) + metStr_ = sprintf('Pk=%.2f @%dms L=%dms S=[%.2f-%.2f]', ... + peakresp(p,fcntSR), round(peaktime(p,fcntSR)), round(latencyHalfHeight(p,fcntSR)), ... + settlingMin(p,fcntSR), settlingMax(p,fcntSR)); + h=text(510, ypos(p)-(fcntSR*.07)-0.03, metStr_,'fontsize',max(fontsz-1,6)); + set(h, 'Color',[multiLineCols(fcntSR,:)]) + else + peakresp(p, fcntSR) = nan; + peaktime(p, fcntSR) = nan; + latencyHalfHeight(p, fcntSR) = nan; + settlingMin(p, fcntSR) = nan; + settlingMax(p, fcntSR) = nan; + if cnt <= 3 + if size(axesOptions,2) < 2 + h=text(505, ypos(p)+0.04, [ylab{p}]); + else + h=text(505, ypos(p)+0.04, [ylab{p} ' (' lnLabels{cnt2} ')']); + end + + set(h,'fontsize',fontsz,'fontweight','bold','Color',th.textPrimary); + h=text(505, ypos(p), [pidlabels]); + set(h,'fontsize',fontsz,'fontweight','bold','Color',th.textPrimary); + end + h=text(505, ypos(p)-(fcntSR*.044), [int2str(fcntSR) ') insufficient data']); + set(h,'Color',[multiLineCols(fcntSR,:)],'fontsize',fontsz, 'fontweight','bold') + end + + set(gca,'fontsize',fontsz,'xminortick','on','yminortick','on','xtick',[0 100 200 300 400 500],'xticklabel',{'0' '100' '200' '300' '400' '500'},'ytick',[0 .25 .5 .75 1 1.25 1.5 1.75 2],'tickdir','out'); + + box off + if cnt <= 3, h=ylabel(['Response '], 'fontweight','bold'); end + + xlabel('Time (ms)', 'fontweight','bold'); + + title('Step Response Functions'); + h=plot([0 500],[1 1],'--','Color',th.axesFg); + set(h,'linewidth',.5) + axis([0 500 0 ymax]) + grid on + end + end + + elseif fcntSR == 11 + warndlg('10 files maximum. Click reset.'); + end + end + % auto-hide axes where ALL files had insufficient data + if ~updateStep && ~isempty(peakresp) + rpyH_ = {guiHandlesTune.plotR, guiHandlesTune.plotP, guiHandlesTune.plotY}; + needRedraw_ = false; + for pi_ = 1:3 + if get(rpyH_{pi_}, 'Value') == 1 && size(peakresp,1) >= pi_ + if all(isnan(peakresp(pi_, :))) + set(rpyH_{pi_}, 'Value', 0, 'Enable', 'off'); + for off_ = [0 3 6 9] + h_del = findobj(PStunefig, 'Type', 'axes', 'Tag', sprintf('PSstep_%d', pi_+off_)); + if ~isempty(h_del), delete(h_del); end + end + needRedraw_ = true; + end + end + end + if needRedraw_ + axesOptions = find([get(guiHandlesTune.plotR,'Value') get(guiHandlesTune.plotP,'Value') get(guiHandlesTune.plotY,'Value')]); + nActiveTune = numel(axesOptions); + if nActiveTune > 0 && nActiveTune < 3 + topY_t = stdRows_t(1) + stdRowH_t; botY_t = stdRows_t(3); gapT = 0.05; + rowH_t = (topY_t - botY_t - (nActiveTune-1)*gapT) / nActiveTune; + ci = 0; + for jj = axesOptions + ci = ci + 1; + yy = topY_t - ci*rowH_t - (ci-1)*gapT; + for cc = 0:3 + htmp_ = findobj(PStunefig,'Type','axes','Tag',sprintf('PSstep_%d',jj+cc*3)); + if ~isempty(htmp_) + pos_ = get(htmp_(1),'Position'); + pos_(2) = yy; pos_(4) = rowH_t; + set(htmp_(1),'Position',pos_); + end + end + end + end + end + end + + allax = findobj(PStunefig, 'Type', 'axes', 'Visible', 'on'); + for axi = 1:numel(allax), PSstyleAxes(allax(axi), th); end + try PSresizeCP(PStunefig, []); catch, end + set(PStunefig, 'pointer', 'arrow') + + updateStep=0; +else + for p = 1 : 3 + for off_ = [0 3 6 9] + stag_ = sprintf('PSstep_%d', p+off_); + h_del = findobj(PStunefig, 'Type', 'axes', 'Tag', stag_); + if ~isempty(h_del), delete(h_del); end + end + peaktime = []; + peakresp = []; + latencyHalfHeight = []; + latencyHalfHeight_std = []; + peakresp_std = []; + settlingMin = []; + settlingMax = []; + end + h_del = findobj(PStunefig, 'Type', 'axes', 'Tag', 'PSstep_combo'); + if ~isempty(h_del), delete(h_del); end + h_del = findobj(PStunefig, 'Type', 'axes', 'Tag', 'PSstep_bfsliders'); + if ~isempty(h_del), delete(h_del); end +end + + + + diff --git a/src/ui/PSTestSignalConfig.m b/src/ui/PSTestSignalConfig.m new file mode 100644 index 0000000..39e2ae1 --- /dev/null +++ b/src/ui/PSTestSignalConfig.m @@ -0,0 +1,381 @@ +function PSTestSignalConfig(PSfig, T, Nfiles, A_lograte, SetupInfo, guiHandles, tIND) +%% PSTestSignalConfig - configure BF filter chain and apply to log data + +thm = PStheme(); +fontsz = thm.fontsz; +screensz = get(0, 'ScreenSize'); + +fig = findobj('Type', 'figure', 'Name', 'Test Signal Configuration'); +if ~isempty(fig), close(fig); end +fig = figure('Name', 'Test Signal Configuration', 'NumberTitle', 'off', ... + 'Color', thm.figBg, ... + 'Position', round([0 0 screensz(3) screensz(4)])); +try set(fig, 'WindowState', 'maximized'); catch, end + +fileIdx = 1; +try fileIdx = get(guiHandles.FileNum, 'Value'); fileIdx = fileIdx(1); catch, end +Fs = A_lograte(fileIdx) * 1000; + +% parse filter params from log headers +fp = struct(); +if iscell(SetupInfo) && numel(SetupInfo) >= fileIdx + fp = PSparseFilterParams(SetupInfo{fileIdx}); +else + fp.gyro_lpf1_type=0; fp.gyro_lpf1_hz=0; + fp.gyro_lpf2_type=0; fp.gyro_lpf2_hz=0; + fp.gyro_notch1_hz=0; fp.gyro_notch1_cut=0; + fp.gyro_notch2_hz=0; fp.gyro_notch2_cut=0; + fp.dterm_lpf1_type=0; fp.dterm_lpf1_hz=0; + fp.dterm_lpf2_type=0; fp.dterm_lpf2_hz=0; + fp.dterm_notch_hz=0; fp.dterm_notch_cut=0; +end + +% RPY colors: raw=solid, filtered=lighter +axCols = {thm.axisRoll, thm.axisPitch, thm.axisYaw}; +axColsFilt = {thm.axisRollFilt, thm.axisPitchFilt, thm.axisYawFilt}; +axNames = {'Roll','Pitch','Yaw'}; + +% layout: plots left 75%, controls right 25% +cpL = 0.76; cpW = 0.23; +plotL = 0.06; plotR_ = cpL - 0.02; +plotW = plotR_ - plotL; +bgc = thm.panelBg; fgc = thm.textPrimary; +ibc = thm.inputBg; ifc = thm.inputFg; +lc = thm.textSecondary; + +uipanel('Parent', fig, 'Title', '', ... + 'BackgroundColor', thm.panelBg, 'ForegroundColor', thm.panelFg, ... + 'HighlightColor', thm.panelBorder, ... + 'FontSize', fontsz, 'Position', [cpL .02 cpW .96]); + +% axes: top = PSD spectrum (raw vs filtered), bottom = time domain +axSpec = axes('Parent', fig, 'Units', 'normalized', 'Position', [plotL 0.54 plotW 0.42]); +axTime = axes('Parent', fig, 'Units', 'normalized', 'Position', [plotL 0.06 plotW 0.42]); +PSstyleAxes(axSpec, thm); PSstyleAxes(axTime, thm); + +% control panel +x0 = cpL + 0.02; +cW = cpW - 0.04; +halfW = cW / 2; +rh = 0.026; gap = 0.004; +row = 0.94; +cb = @(~,~) updatePreview(); + +% source signal +uicontrol(fig, 'Style', 'text', 'String', 'Source:', ... + 'Units', 'normalized', 'Position', [x0 row .06 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +srcLabels = {'Gyro','P-term','D-term(prefilt)','D-term','Setpoint','PIDsum','PIDerror'}; +h.source = uicontrol(fig, 'Style', 'popupmenu', 'String', srcLabels, 'Value', 1, ... + 'Units', 'normalized', 'Position', [x0+.065 row cW-.065 rh], ... + 'FontSize', fontsz, 'Callback', cb); +row = row - rh - gap*2; + +mkSection('Gyro LPF1', thm.textAccent); +[h.glpf1_type, h.glpf1_hz, row] = mkTypeHz(fp.gyro_lpf1_type, fp.gyro_lpf1_hz); + +mkSection('Gyro LPF2', thm.textAccent); +[h.glpf2_type, h.glpf2_hz, row] = mkTypeHz(fp.gyro_lpf2_type, fp.gyro_lpf2_hz); + +mkSection('Gyro Notch 1', thm.secNotch); +[h.gn1_hz, h.gn1_cut, row] = mkNotchPair(fp.gyro_notch1_hz, fp.gyro_notch1_cut); + +mkSection('Gyro Notch 2', thm.secNotch); +[h.gn2_hz, h.gn2_cut, row] = mkNotchPair(fp.gyro_notch2_hz, fp.gyro_notch2_cut); + +mkSection('D-term LPF1', thm.secDtermLPF); +[h.dlpf1_type, h.dlpf1_hz, row] = mkTypeHz(fp.dterm_lpf1_type, fp.dterm_lpf1_hz); + +mkSection('D-term LPF2', thm.secDtermLPF); +[h.dlpf2_type, h.dlpf2_hz, row] = mkTypeHz(fp.dterm_lpf2_type, fp.dterm_lpf2_hz); + +mkSection('D-term Notch', thm.secDtermNotch); +[h.dn_hz, h.dn_cut, row] = mkNotchPair(fp.dterm_notch_hz, fp.dterm_notch_cut); + +row = row - gap*2; + +% R/P/Y checkboxes +chkW = cW / 3; +h.chkR = uicontrol(fig, 'Style', 'checkbox', 'String', 'R', 'Value', 1, ... + 'Units', 'normalized', 'Position', [x0 row chkW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', thm.axisRoll, ... + 'Callback', cb); +h.chkP = uicontrol(fig, 'Style', 'checkbox', 'String', 'P', 'Value', 0, ... + 'Units', 'normalized', 'Position', [x0+chkW row chkW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', thm.axisPitch, ... + 'Callback', cb); +h.chkY = uicontrol(fig, 'Style', 'checkbox', 'String', 'Y', 'Value', 0, ... + 'Units', 'normalized', 'Position', [x0+2*chkW row chkW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', thm.axisYaw, ... + 'Callback', cb); +row = row - rh - gap; + +% preview window +uicontrol(fig, 'Style', 'text', 'String', 'Win:', ... + 'Units', 'normalized', 'Position', [x0 row .04 rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); +h.winLen = uicontrol(fig, 'Style', 'popupmenu', 'String', {'0.5s','1s','2s','5s','Full'}, 'Value', 2, ... + 'Units', 'normalized', 'Position', [x0+.045 row cW-.045 rh], ... + 'FontSize', fontsz, 'Callback', cb); +row = row - rh - gap*3; + +% Apply button +h.applyBtn = uicontrol(fig, 'Style', 'pushbutton', 'String', 'Apply', ... + 'Units', 'normalized', 'Position', [x0 row cW rh*1.3], ... + 'FontSize', fontsz+1, 'FontWeight', 'bold', ... + 'BackgroundColor', thm.btnRun, 'ForegroundColor', [1 1 1], ... + 'Callback', @(~,~) applyToLogViewer()); +row = row - rh - gap; + +h.status = uicontrol(fig, 'Style', 'text', 'String', '', ... + 'Units', 'normalized', 'Position', [x0 row cW rh], ... + 'FontSize', fontsz-1, 'BackgroundColor', bgc, 'ForegroundColor', thm.btnRun, ... + 'HorizontalAlignment', 'center'); + +updatePreview(); + +%% nested: section header + function mkSection(txt, col) + uicontrol(fig, 'Style', 'text', 'String', txt, ... + 'Units', 'normalized', 'Position', [cpL+.01 row cpW-.02 rh], ... + 'FontSize', fontsz-1, 'FontWeight', 'bold', ... + 'BackgroundColor', bgc, 'ForegroundColor', col); + row = row - rh - gap; + end + +%% nested: type + hz + function [hType, hHz, r] = mkTypeHz(defType, defHz) + if defHz == 0, ddVal = 1; + else ddVal = min(defType + 2, 5); end + ddW = cW * 0.48; edW = cW * 0.35; lblW = cW * 0.15; + hType = uicontrol(fig, 'Style', 'popupmenu', ... + 'String', {'OFF', 'PT1', 'Biquad', 'PT2', 'PT3'}, 'Value', ddVal, ... + 'Units', 'normalized', 'Position', [x0 row ddW rh], 'FontSize', fontsz, 'Callback', cb); + hHz = uicontrol(fig, 'Style', 'edit', 'String', num2str(round(defHz)), ... + 'Units', 'normalized', 'Position', [x0+ddW+.005 row edW rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, ... + 'HorizontalAlignment', 'center', 'Callback', cb); + uicontrol(fig, 'Style', 'text', 'String', 'Hz', ... + 'Units', 'normalized', 'Position', [x0+ddW+edW+.008 row lblW rh], ... + 'FontSize', fontsz-1, 'BackgroundColor', bgc, ... + 'ForegroundColor', lc, 'HorizontalAlignment', 'left'); + r = row - rh - gap; + end + +%% nested: notch pair + function [hHz, hCut, r] = mkNotchPair(defHz, defCut) + lblW = .03; edW = halfW - lblW - .005; + uicontrol(fig, 'Style', 'text', 'String', 'Ctr:', ... + 'Units', 'normalized', 'Position', [x0 row lblW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); + hHz = uicontrol(fig, 'Style', 'edit', 'String', num2str(round(defHz)), ... + 'Units', 'normalized', 'Position', [x0+lblW+.005 row edW rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, ... + 'HorizontalAlignment', 'center', 'Callback', cb); + uicontrol(fig, 'Style', 'text', 'String', 'Cut:', ... + 'Units', 'normalized', 'Position', [x0+halfW row lblW rh], ... + 'FontSize', fontsz, 'BackgroundColor', bgc, 'ForegroundColor', lc, 'HorizontalAlignment', 'right'); + hCut = uicontrol(fig, 'Style', 'edit', 'String', num2str(round(defCut)), ... + 'Units', 'normalized', 'Position', [x0+halfW+lblW+.005 row edW rh], ... + 'FontSize', fontsz, 'BackgroundColor', ibc, 'ForegroundColor', ifc, ... + 'HorizontalAlignment', 'center', 'Callback', cb); + r = row - rh - gap; + end + +%% nested: read edit field + function v = readEdit(hEdit) + v = str2double(get(hEdit, 'String')); + if isnan(v), v = 0; end + end + +%% nested: get source field name + function srcField = getSrcField() + srcIdx = get(h.source, 'Value'); + fields = {'gyroADC_','axisP_','axisDpf_','axisD_','setpoint_','pidsum_','piderr_'}; + srcField = fields{srcIdx}; + end + +%% nested: get full source signal for one axis + function [tFull, rawFull] = getFullData(axNum) + srcField = getSrcField(); + fname = [srcField int2str(axNum) '_']; + if ~isfield(T{fileIdx}, fname) + tFull = []; rawFull = []; return; + end + rawFull = T{fileIdx}.(fname); + N = numel(rawFull); + tFull = (0:N-1)' / Fs; + end + +%% nested: get windowed data slice + function [tSlice, rawSlice, i1, i2] = getPreviewSlice(axNum) + [tFull, rawFull] = getFullData(axNum); + if isempty(tFull), tSlice=[]; rawSlice=[]; i1=1; i2=1; return; end + N = numel(rawFull); + + winIdx = get(h.winLen, 'Value'); + winSecs = [0.5 1 2 5 inf]; + wSec = winSecs(winIdx); + + idx = tIND{fileIdx}; + if isempty(idx), idx = 1:N; end + midSample = round(mean([idx(1) idx(end)])); + if wSec >= tFull(end) + i1 = 1; i2 = N; + else + halfWin = round(wSec/2 * Fs); + i1 = max(1, midSample - halfWin); + i2 = min(N, midSample + halfWin); + end + tSlice = tFull(i1:i2); + rawSlice = rawFull(i1:i2); + end + +%% nested: apply filter chain to a signal vector + function out = applyChain(sig, sFs) + out = applyLPF(sig, get(h.glpf1_type,'Value')-1, readEdit(h.glpf1_hz), sFs); + out = applyLPF(out, get(h.glpf2_type,'Value')-1, readEdit(h.glpf2_hz), sFs); + out = applyNotch(out, readEdit(h.gn1_hz), readEdit(h.gn1_cut), sFs); + out = applyNotch(out, readEdit(h.gn2_hz), readEdit(h.gn2_cut), sFs); + srcIdx = get(h.source, 'Value'); + if srcIdx == 3 || srcIdx == 4 + out = applyLPF(out, get(h.dlpf1_type,'Value')-1, readEdit(h.dlpf1_hz), sFs); + out = applyLPF(out, get(h.dlpf2_type,'Value')-1, readEdit(h.dlpf2_hz), sFs); + out = applyNotch(out, readEdit(h.dn_hz), readEdit(h.dn_cut), sFs); + end + end + +%% nested: which axes are selected + function sel = getSelectedAxes() + sel = find([get(h.chkR,'Value') get(h.chkP,'Value') get(h.chkY,'Value')]); + end + +%% nested: update preview plots + function updatePreview() + if ~ishandle(fig), return; end + sel = getSelectedAxes(); + if isempty(sel), return; end + + fMax = min(Fs/2, 1000); + Nfft = 2^floor(log2(min(4096, numel(T{fileIdx}.loopIteration)))); + + cla(axSpec); hold(axSpec, 'on'); + cla(axTime); hold(axTime, 'on'); + legTxt = {}; + + for si = 1:numel(sel) + axNum = sel(si) - 1; + colRaw = axCols{sel(si)}; + colFilt = axColsFilt{sel(si)}; + + [~, rawFull] = getFullData(axNum); + if isempty(rawFull), continue; end + filtFull = applyChain(rawFull, Fs); + + % PSD + try + [pRaw, fPsd] = pwelch(rawFull, hanning(Nfft), round(Nfft/2), Nfft, Fs); + [pFilt, ~] = pwelch(filtFull, hanning(Nfft), round(Nfft/2), Nfft, Fs); + catch + win = hanning(Nfft); + nSeg = floor(numel(rawFull) / Nfft); + pRaw = zeros(Nfft/2+1, 1); pFilt = pRaw; + for ssi = 1:nSeg + sidx = (ssi-1)*Nfft + (1:Nfft); + segR = rawFull(sidx) .* win; + segF = filtFull(sidx) .* win; + fR = abs(fft(segR)).^2; fF = abs(fft(segF)).^2; + pRaw = pRaw + fR(1:Nfft/2+1); + pFilt = pFilt + fF(1:Nfft/2+1); + end + pRaw = pRaw / max(nSeg,1); + pFilt = pFilt / max(nSeg,1); + fPsd = linspace(0, Fs/2, Nfft/2+1)'; + end + + fIdx_ = fPsd <= fMax & fPsd > 0; + plot(axSpec, fPsd(fIdx_), 10*log10(pRaw(fIdx_)+1e-12), ... + 'Color', colRaw, 'LineWidth', 1.2); + plot(axSpec, fPsd(fIdx_), 10*log10(pFilt(fIdx_)+1e-12), ... + 'Color', colFilt, 'LineWidth', 1.2); + legTxt{end+1} = [axNames{sel(si)} ' raw']; + legTxt{end+1} = [axNames{sel(si)} ' filt']; + + % time domain + [tSlice, rawSlice] = getPreviewSlice(axNum); + if isempty(tSlice), continue; end + filtSlice = applyChain(rawSlice, Fs); + plot(axTime, tSlice, rawSlice, 'Color', colRaw, 'LineWidth', 0.8); + plot(axTime, tSlice, filtSlice, 'Color', colFilt, 'LineWidth', 1.2); + end + + hold(axSpec, 'off'); hold(axTime, 'off'); + PSstyleAxes(axSpec, thm); PSstyleAxes(axTime, thm); + + ttl = strjoin(axNames(sel), '/'); + title(axSpec, [ttl ' - Spectrum'], 'Color', thm.textPrimary, 'FontSize', fontsz); + xlabel(axSpec, 'Frequency (Hz)', 'Color', thm.textSecondary); + ylabel(axSpec, 'PSD (dB)', 'Color', thm.textSecondary); + set(axSpec, 'XLim', [0 fMax]); + legend(axSpec, legTxt, 'TextColor', thm.textPrimary, ... + 'Color', thm.panelBg, 'EdgeColor', thm.legendEdge, 'Location', 'northeast'); + + title(axTime, [ttl ' - Time Domain'], 'Color', thm.textPrimary, 'FontSize', fontsz); + xlabel(axTime, 'Time (s)', 'Color', thm.textSecondary); + ylabel(axTime, 'deg/s', 'Color', thm.textSecondary); + end + +%% nested: apply filter chain to all log data + function applyToLogViewer() + set(h.status, 'String', 'Applying...', 'ForegroundColor', thm.btnDash5); + drawnow; + srcField = getSrcField(); + for f = 1:Nfiles + fFs = A_lograte(f) * 1000; + for p = 0:2 + fname = [srcField int2str(p) '_']; + if isfield(T{f}, fname) + T{f}.(['testSignal_' int2str(p) '_']) = applyChain(T{f}.(fname), fFs); + else + T{f}.(['testSignal_' int2str(p) '_']) = zeros(size(T{f}.loopIteration)); + end + end + end + assignin('base', 'T', T); + set(h.status, 'String', 'Applied! Check Log Viewer & Spectral Analyzer.', 'ForegroundColor', thm.btnRun); + % add Test Signal to SA SpecList if not already there + try + sl = evalin('base', 'guiHandlesSpec2.SpecList'); + if ishandle(sl) + items = get(sl, 'String'); + if ~any(strcmp(items, 'Test Signal')) + items{end+1} = 'Test Signal'; + set(sl, 'String', items); + end + end + catch, end + try + if get(guiHandles.checkboxTS, 'Value') + evalin('base', 'PSplotLogViewer'); + end + catch, end + end + +end + +%% local filter helpers +function y = applyLPF(x, type, hz, Fs) + if type == 0 || hz == 0, y = x; return; end + types = {'pt1', 'biquad', 'pt2', 'pt3'}; + if type > numel(types), y = x; return; end + [b, a] = PSbfFilters(types{type}, hz, Fs); + y = filter(b, a, x); +end + +function y = applyNotch(x, center_hz, cutoff_hz, Fs) + if center_hz == 0, y = x; return; end + if cutoff_hz <= 0, cutoff_hz = center_hz * 0.7; end + Q = center_hz / (center_hz - cutoff_hz + 1); + [b, a] = PSbfFilters('notch', center_hz, Fs, Q); + y = filter(b, a, x); +end diff --git a/src/ui/PScolormap.m b/src/ui/PScolormap.m index aecc745..e2c162a 100644 --- a/src/ui/PScolormap.m +++ b/src/ui/PScolormap.m @@ -1,37 +1,37 @@ -%% PScolormap - creates a few novel, more linear, colormap options -% this is a good place to continue improving colormaps -% -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% Get colormap data without setting any figure's colormap (avoids stale colorbar errors) -try - a=parula(64); -catch - a=viridis(64); -end -viridiscmap=[a(:,1) a(:,2) a(:,3)*.6]; - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% 2) a couple more linear, gamma corrected, cmaps using dkl colorspace -% http://www.scholarpedia.org/article/Color_spaces - -% 64 steps from -1 to 1, DKL color range -i=.01:1/64:1;%half -i2=-1:1/31.9:1;%full - -% DKL 'red' white bg -for j=1:64, - linearREDcmap(j,:)=dkl2rgb([-i2(j) i(j) 0])/255; -end -% DKL 'grey' white bg -for j=1:64, - linearGREYcmap(j,:)=dkl2rgb([-i2(j) 0 0])/255; -end +%% PScolormap - creates a few novel, more linear, colormap options +% this is a good place to continue improving colormaps +% +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Get colormap data without setting any figure's colormap (avoids stale colorbar errors) +try + a=parula(64); +catch + a=viridis(64); +end +viridiscmap=[a(:,1) a(:,2) a(:,3)*.6]; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% 2) a couple more linear, gamma corrected, cmaps using dkl colorspace +% http://www.scholarpedia.org/article/Color_spaces + +% 64 steps from -1 to 1, DKL color range +i=.01:1/64:1;%half +i2=-1:1/31.9:1;%full + +% DKL 'red' white bg +for j=1:64, + linearREDcmap(j,:)=dkl2rgb([-i2(j) i(j) 0])/255; +end +% DKL 'grey' white bg +for j=1:64, + linearGREYcmap(j,:)=dkl2rgb([-i2(j) 0 0])/255; +end diff --git a/src/ui/PSdispSetupInfoUIcontrol.m b/src/ui/PSdispSetupInfoUIcontrol.m index 9adf766..6067cc4 100644 --- a/src/ui/PSdispSetupInfoUIcontrol.m +++ b/src/ui/PSdispSetupInfoUIcontrol.m @@ -1,49 +1,72 @@ -%% PSdispSetupInfo - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - - -if exist('fnameMaster','var') && ~isempty(fnameMaster) - -PSdisp=figure(5); -screensz = get(0,'ScreenSize'); -set(PSdisp, 'Position', round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)])); -set(PSdisp, 'NumberTitle', 'on'); -set(PSdisp, 'Name', ['PIDscope (' PsVersion ') - Setup Info']); -set(PSdisp,'color',bgcolor) - -columnWidth=55*round(screensz_multiplier*prop_max_screen); - -TooltipString_FileNumDispA=['List of files available. Click to view setup info for each']; -posInfo.FileNumDispA=[.22 .95 .1 .04]; -posInfo.FileNumDispB=[.72 .95 .1 .04]; -posInfo.checkboxDIFF=[.04 .96 .1 .04]; - -guiHandlesInfo.FileNumDispA = uicontrol(PSdisp,'Style','popupmenu','string',[fnameMaster],... - 'fontsize',fontsz, 'units','normalized','Position', [posInfo.FileNumDispA],'callback','@selection; PSdispSetupInfo;'); -set(guiHandlesInfo.FileNumDispA, 'Value', 1); -if Nfiles > 1 - guiHandlesInfo.FileNumDispB = uicontrol(PSdisp,'Style','popupmenu','string',[fnameMaster],... - 'fontsize',fontsz, 'units','normalized','Position', [posInfo.FileNumDispB],'callback','@selection; PSdispSetupInfo;'); - set(guiHandlesInfo.FileNumDispB, 'Value', 2); -end - -guiHandlesInfo.checkboxDIFF =uicontrol(PSdisp,'Style','checkbox','String','Show Differences Only','fontsize',fontsz,'TooltipString', [''],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkboxDIFF],'callback', 'PSdispSetupInfo;'); - -else - warndlg('Please select file(s)'); -end - -% functions -function selection(src,event) - val = c.Value; - str = c.String; - str{val}; - % disp(['Selection: ' str{val}]); +%% PSdispSetupInfo + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + +if exist('PSdisp','var') && ishandle(PSdisp) + figure(PSdisp); +else + PSdisp=figure(5); + screensz = get(0,'ScreenSize'); + set(PSdisp, 'Position', round([0 0 screensz(3) screensz(4)])); + try set(PSdisp, 'WindowState', 'maximized'); catch, end + set(PSdisp, 'NumberTitle', 'on'); + set(PSdisp, 'Name', ['PIDscope (' PsVersion ') - Setup Info']); + set(PSdisp,'color',bgcolor); +end + +columnWidth = 55 * fontsz; + +TooltipString_FileNumDispA=['List of files available. Click to view setup info for each']; +topDdW = 160/screensz(3); topCbW = 200/screensz(3); +tbOff = 40/screensz(4); % toolbar offset +topBtnY = 1 - tbOff - rh - cpMv; +posInfo.FileNumDispA=[.22 topBtnY topDdW ddh]; +posInfo.FileNumDispB=[.72 topBtnY topDdW ddh]; +posInfo.checkboxDIFF=[.04 topBtnY topCbW rh]; + +if ~exist('setupInfoWidgets_init','var') || ~ishandle(guiHandlesInfo.FileNumDispA) +guiHandlesInfo.FileNumDispA = uicontrol(PSdisp,'Style','popupmenu','string',[fnameMaster],... + 'fontsize',fontsz, 'units','normalized','Position', [posInfo.FileNumDispA],'callback','@selection; PSdispSetupInfo;'); +set(guiHandlesInfo.FileNumDispA, 'Value', 1); +if Nfiles > 1 + guiHandlesInfo.FileNumDispB = uicontrol(PSdisp,'Style','popupmenu','string',[fnameMaster],... + 'fontsize',fontsz, 'units','normalized','Position', [posInfo.FileNumDispB],'callback','@selection; PSdispSetupInfo;'); + set(guiHandlesInfo.FileNumDispB, 'Value', 2); +end + +guiHandlesInfo.checkboxDIFF =uicontrol(PSdisp,'Style','checkbox','String','Show Differences Only','fontsize',fontsz,'TooltipString', [''],... + 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkboxDIFF],'callback', 'PSdispSetupInfo;'); +setupInfoWidgets_init = true; +end % ishandle widgets + +% Register top bar for fixed-pixel resize +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', 0); +cpI = {}; +cpI{end+1} = struct('h', guiHandlesInfo.checkboxDIFF, 'type','cb', 'row',0, 'col',0, 'hpx',0, 'wpx',200); +cpI{end+1} = struct('h', guiHandlesInfo.FileNumDispA, 'type','dd', 'row',0, 'col',0, 'hpx',0, 'wpx',160); +if Nfiles > 1 && isfield(guiHandlesInfo, 'FileNumDispB') && ishandle(guiHandlesInfo.FileNumDispB) + cpI{end+1} = struct('h', guiHandlesInfo.FileNumDispB, 'type','dd', 'row',0, 'col',0, 'hpx',0, 'wpx',160); +end +PSregisterResize(PSdisp, cpPx, cpI, 'topbar', 0.04); + +else + warndlg('Please select file(s)'); +end +PSstyleControls(PSdisp); + +% functions +function selection(src,event) + val = c.Value; + str = c.String; + str{val}; + % disp(['Selection: ' str{val}]); end \ No newline at end of file diff --git a/src/ui/PSerrUIcontrol.m b/src/ui/PSerrUIcontrol.m index 63046bf..3a1db07 100644 --- a/src/ui/PSerrUIcontrol.m +++ b/src/ui/PSerrUIcontrol.m @@ -1,67 +1,113 @@ -%% PSerrUIcontrol - ui controls for PID error analyses - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - - -if ~isempty(filenameA) || ~isempty(filenameB) - -PSerrfig=figure(3); -set(PSerrfig, 'Position', round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)])); -set(PSerrfig, 'NumberTitle', 'off'); -set(PSerrfig, 'Name', ['PIDscope (' PsVersion ') - PID Error Tool']); -set(PSerrfig, 'InvertHardcopy', 'off'); -set(PSerrfig,'color',bgcolor) - -PSerrfig_pos = get(PSerrfig, 'Position'); -screensz_tmp = get(0,'ScreenSize'); if PSerrfig_pos(3) > 10, PSerrfig_pos(3:4) = PSerrfig_pos(3:4) ./ screensz_tmp(3:4); end -prop_max_screen=(max([PSerrfig_pos(3) PSerrfig_pos(4)])); -fontsz3=round(screensz_multiplier*prop_max_screen); -maxDegsec=100; -updateErr=0; - -TooltipString_degsec=['Sets the maximum rate used in the PID error analysis (distribution plots only).',... - newline , 'E.g., the default means only data in which set point was <= 100deg/s is used.',... - newline , 'This cutoff helps to reduce inclusion of data with inflated PID error as a result of snap maneuvers' ]; - -clear posInfo.PIDerrAnalysis -cols=[0.1 0.55]; -rows=[0.66 0.38 0.1]; -k=0; -for c=1:2 - for r=1:3 - k=k+1; - posInfo.PIDerrAnalysis(k,:)=[cols(c) rows(r) 0.39 0.24]; - end -end - -posInfo.refresh2=[.09 .94 .06 .04]; -posInfo.saveFig3=[.16 .94 .06 .04]; - -posInfo.maxSticktext=[.22 .966 .12 .03]; -posInfo.maxStick=[.24 .94 .06 .03]; - -errCrtlpanel = uipanel('Title','','FontSize',fontsz3,... - 'BackgroundColor',[.95 .95 .95],... - 'Position',[.085 .93 .23 .06]); - -guiHandlesPIDerr.refresh = uicontrol(PSerrfig,'string','Refresh','fontsize',fontsz3,'TooltipString',[TooltipString_refresh],'units','normalized','Position',[posInfo.refresh2],... - 'callback','updateErr=1;PSplotPIDerror;'); -set(guiHandlesPIDerr.refresh, 'BackgroundColor', [1 1 .2]); - -guiHandlesPIDerr.maxSticktext = uicontrol(PSerrfig,'style','text','string','max stick deg/s','fontsize',fontsz3,'TooltipString',[TooltipString_degsec],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.maxSticktext]); -guiHandlesPIDerr.maxStick = uicontrol(PSerrfig,'style','edit','string',[int2str(maxDegsec)],'fontsize',fontsz3,'TooltipString',[TooltipString_degsec],'units','normalized','Position',[posInfo.maxStick],... - 'callback','@textinput_call; maxDegsec=str2num(get(guiHandlesPIDerr.maxStick, ''String'')); updateErr=1;PSplotPIDerror; '); - -guiHandlesPIDerr.saveFig3 = uicontrol(PSerrfig,'string','Save Fig','fontsize',fontsz3,'TooltipString',[TooltipString_saveFig],'units','normalized','Position',[posInfo.saveFig3],... - 'callback','set(guiHandlesPIDerr.saveFig3, ''FontWeight'', ''bold'');PSsaveFig; set(guiHandlesPIDerr.saveFig3, ''FontWeight'', ''normal'');'); -set(guiHandlesPIDerr.saveFig3, 'BackgroundColor', [.8 .8 .8]); - -else - errordlg('Please select file(s) then click ''load+run''', 'Error, no data'); - pause(2); -end +%% PSerrUIcontrol - ui controls for PID error analyses + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + +if exist('PSerrfig','var') && ishandle(PSerrfig) + figure(PSerrfig); +else + PSerrfig=figure(7); + set(PSerrfig, 'Position', round([0 0 screensz(3) screensz(4)])); + try set(PSerrfig, 'WindowState', 'maximized'); catch, end + set(PSerrfig, 'NumberTitle', 'off'); + set(PSerrfig, 'Name', ['PIDscope (' PsVersion ') - PID Error Tool']); + set(PSerrfig, 'InvertHardcopy', 'off'); + set(PSerrfig,'color',bgcolor); +end + +fontsz3 = fontsz; +maxDegsec=100; +updateErr=0; + +TooltipString_degsec=['Sets the maximum rate used in the PID error analysis (distribution plots only).',... + newline , 'E.g., the default means only data in which set point was <= 100deg/s is used.',... + newline , 'This cutoff helps to reduce inclusion of data with inflated PID error as a result of snap maneuvers' ]; + +clear posInfo.PIDerrAnalysis +plotR = cpL - 0.04; plotLe = 0.08; colGapE = 0.02; +colWe = (plotR - plotLe - colGapE) / 2; +cols = [plotLe, plotLe + colWe + colGapE]; +rows=[0.63 0.36 0.09]; +k=0; +for c=1:2 + for r=1:3 + k=k+1; + posInfo.PIDerrAnalysis(k,:)=[cols(c) rows(r) colWe 0.24]; + end +end + +% Top bar layout — pixel-based sizes +topBtnW = 100/screensz(3); topBtnH = rh; topEdtW = 80/screensz(3); +topTxtW = 120/screensz(3); topDdW = 160/screensz(3); topBarL = 0.09; +tbOff = 40/screensz(4); % toolbar offset +topBtnY = 1 - tbOff - rh - cpMv; +topX = topBarL + cpM; +posInfo.refresh2= [topX topBtnY topBtnW topBtnH]; topX=topX+topBtnW+cpM; +posInfo.saveFig3= [topX topBtnY topBtnW topBtnH]; topX=topX+topBtnW+cpM; +posInfo.maxStick= [topX topBtnY topEdtW topBtnH]; topX=topX+topEdtW+cpM; +posInfo.maxSticktext=[topX topBtnY topTxtW topBtnH]; topX=topX+topTxtW+cpM; +posInfo.errFileA= [topX topBtnY topDdW ddh]; topX=topX+topDdW+cpM; +posInfo.errFileB= [topX topBtnY topDdW ddh]; +topPanelW = topX + topDdW + cpM - topBarL; +topPanelH = 1 - tbOff - topBtnY + cpMv; + +if ~exist('errCrtlpanel','var') || ~ishandle(errCrtlpanel) +errCrtlpanel = uipanel('Title','','FontSize',fontsz3,... + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... + 'Position',[topBarL topBtnY-cpMv topPanelW topPanelH]); + +guiHandlesPIDerr.refresh = uicontrol(PSerrfig,'string','Refresh','fontsize',fontsz3,'TooltipString','Refresh plots','units','normalized','Position',[posInfo.refresh2],... + 'callback','updateErr=1;PSplotPIDerror;'); +set(guiHandlesPIDerr.refresh, 'ForegroundColor', colRun); + +guiHandlesPIDerr.maxSticktext = uicontrol(PSerrfig,'style','text','string','max stick deg/s','fontsize',fontsz3,'TooltipString',[TooltipString_degsec],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.maxSticktext]); +guiHandlesPIDerr.maxStick = uicontrol(PSerrfig,'style','edit','string',[int2str(maxDegsec)],'fontsize',fontsz3,'TooltipString',[TooltipString_degsec],'units','normalized','Position',[posInfo.maxStick],... + 'callback','maxDegsec=str2double(get(guiHandlesPIDerr.maxStick, ''String'')); updateErr=1;PSplotPIDerror; '); + +guiHandlesPIDerr.saveFig3 = uicontrol(PSerrfig,'string','Save Fig','fontsize',fontsz3,'TooltipString',[TooltipString_saveFig],'units','normalized','Position',[posInfo.saveFig3],... + 'callback','PSsaveFig;'); +set(guiHandlesPIDerr.saveFig3, 'ForegroundColor', saveCol); + +guiHandlesPIDerr.FileA = uicontrol(PSerrfig,'Style','popupmenu','string',[fnameMaster],... + 'fontsize',fontsz3,'TooltipString','File A (red)','units','normalized','Position',[posInfo.errFileA],... + 'callback','updateErr=0;PSplotPIDerror;'); +set(guiHandlesPIDerr.FileA, 'Value', 1); +if Nfiles > 1 + guiHandlesPIDerr.FileB = uicontrol(PSerrfig,'Style','popupmenu','string',[fnameMaster],... + 'fontsize',fontsz3,'TooltipString','File B (blue)','units','normalized','Position',[posInfo.errFileB],... + 'callback','updateErr=0;PSplotPIDerror;'); + set(guiHandlesPIDerr.FileB, 'Value', min(2, Nfiles)); +end +end % ishandle(errCrtlpanel) + +% Register top bar for fixed-pixel resize +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', 0); +cpI = {}; +cpI{end+1} = struct('h', guiHandlesPIDerr.refresh, 'type','btn', 'row',0, 'col',0, 'hpx',0, 'wpx',100); +cpI{end+1} = struct('h', guiHandlesPIDerr.saveFig3, 'type','btn', 'row',0, 'col',0, 'hpx',0, 'wpx',100); +cpI{end+1} = struct('h', guiHandlesPIDerr.maxStick, 'type','btn', 'row',0, 'col',0, 'hpx',0, 'wpx',80); +cpI{end+1} = struct('h', guiHandlesPIDerr.maxSticktext, 'type','btn', 'row',0, 'col',0, 'hpx',0, 'wpx',120); +cpI{end+1} = struct('h', guiHandlesPIDerr.FileA, 'type','dd', 'row',0, 'col',0, 'hpx',0, 'wpx',160); +if Nfiles > 1 && isfield(guiHandlesPIDerr, 'FileB') && ishandle(guiHandlesPIDerr.FileB) + cpI{end+1} = struct('h', guiHandlesPIDerr.FileB, 'type','dd', 'row',0, 'col',0, 'hpx',0, 'wpx',160); +end +cpI{end+1} = struct('h', errCrtlpanel, 'type','panel', 'row',0, 'col',0, 'hpx',0, 'wpx',0); +setappdata(PSerrfig, 'PSplotGrid', struct('plotL',plotLe, 'colGap',colGapE, ... + 'ncols',2, 'rows',rows, 'rowH',0.24, 'margin',0.04)); +PSregisterResize(PSerrfig, cpPx, cpI, 'topbar', topBarL); + +PSstyleControls(PSerrfig); + +else + errordlg('Please select file(s) then click ''load+run''', 'Error, no data'); + pause(2); +end diff --git a/src/ui/PSfreqTimeUIcontrol.m b/src/ui/PSfreqTimeUIcontrol.m index 939a5fa..5349b38 100644 --- a/src/ui/PSfreqTimeUIcontrol.m +++ b/src/ui/PSfreqTimeUIcontrol.m @@ -21,53 +21,64 @@ %%% clear posInfo.Spec3Pos -cols=[0.09 ]; +plotR = cpL - 0.10; plotL1 = 0.09; +colW1 = plotR - plotL1; +cols=[plotL1]; rows=[0.69 0.395 0.1]; k=0; for c=1 : size(cols,2) for r=1 : size(rows,2) - k=k+1; - posInfo.Spec3Pos(k,:)=[cols(c) rows(r) 0.77 0.255]; + k=k+1; + posInfo.Spec3Pos(k,:)=[cols(c) rows(r) colW1 0.255]; end end -if exist('isOctave','var') && isOctave - posInfo.Spec3Pos(:,3) = 0.74; -end - updateSpec = 0; clear specMat -% Control panel layout (consistent with Log Viewer cpL/cpW) -cpL = .875; cpW = .12; -rh = .030; rs = .034; - -posInfo.fileListWindowSpec= [cpL+.003 .870 cpW-.006 rh]; -posInfo.TermListWindowSpec= [cpL+.003 .836 cpW-.006 rh]; - -posInfo.computeSpec3= [cpL+.006 .802 cpW/2-.006 rh]; -posInfo.resetSpec3= [cpL+cpW/2 .802 cpW/2-.006 rh]; -posInfo.saveFig3= [cpL+.006 .768 cpW/2-.006 rh]; -posInfo.saveSettings3= [cpL+cpW/2 .768 cpW/2-.006 rh]; -posInfo.smooth_select3 = [cpL+.003 .734 cpW-.006 rh]; -posInfo.subsampling_select3= [cpL+.003 .700 cpW-.006 rh]; -posInfo.ColormapSelect2 = [cpL+.003 .666 cpW-.006 rh]; - -posInfo.clim3Max1_text = [cpL+.003 .632 cpW/4 .024]; -posInfo.clim3Max1_input = [cpL+cpW/4 .598 cpW/4 .024]; -posInfo.clim3Max2_text = [cpL+cpW/2 .632 cpW/4 .024]; -posInfo.clim3Max2_input = [cpL+3*cpW/4 .598 cpW/4 .024]; +% Control panel layout — cpL/cpW/rh/rs/ddh/cpM inherited from PIDscope.m (pixel-based) +% yTop tracks where TOP of next element goes; Position Y = yTop - height +gap = rs - rh; fw = cpW-2*cpM; hw = cpW/2-cpM; +tbOff_s3 = 40/screensz(4); +yTop = 1 - tbOff_s3 - cpTitleH - cpMv; +posInfo.fileListWindowSpec= [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.TermListWindowSpec= [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.computeSpec3= [cpL+cpM yTop-rh hw rh]; +posInfo.resetSpec3= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.saveFig3= [cpL+cpM yTop-rh hw rh]; +posInfo.saveSettings3= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.smooth_select3 = [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.subsampling_select3= [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.ColormapSelect2 = [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.clim3Max1_text = [cpL+cpM yTop-rhs cpW/4 rhs]; +posInfo.clim3Max2_text = [cpL+cpW/2 yTop-rhs cpW/4 rhs]; yTop=yTop-rhs-gap; +posInfo.clim3Max1_input = [cpL+cpM yTop-rh cpW/4 rh]; +posInfo.clim3Max2_input = [cpL+cpW/2 yTop-rh cpW/4 rh]; yTop=yTop-rh-gap; ClimScale3 = [-30 10]; - -posInfo.sub100HzfreqTime = [cpL+.003 .564 cpW-.006 .025]; -posInfo.playerBtn3 = [cpL+.003 .530 cpW-.006 rh]; - -PSspecfig3=figure(31); -set(PSspecfig3, 'Position', round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)])); -set(PSspecfig3, 'NumberTitle', 'off'); -set(PSspecfig3, 'Name', ['PIDscope (' PsVersion ') - Frequency x Time Spectrogram']); -set(PSspecfig3, 'InvertHardcopy', 'off'); -set(PSspecfig3,'color',bgcolor); +posInfo.sub100HzfreqTime = [cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +% RPM overlay controls +qw = fw/4; +posInfo.rpmMotor1 = [cpL+cpM yTop-rh qw rh]; +posInfo.rpmMotor2 = [cpL+cpM+qw yTop-rh qw rh]; +posInfo.rpmMotor3 = [cpL+cpM+2*qw yTop-rh qw rh]; +posInfo.rpmMotor4 = [cpL+cpM+3*qw yTop-rh qw rh]; yTop=yTop-rh-gap; +posInfo.rpmHarmDd = [cpL+cpM yTop-ddh hw ddh]; +posInfo.rpmLwDd = [cpL+cpW/2 yTop-ddh hw ddh]; yTop=yTop-ddh-gap; +posInfo.rpmDynNotch = [cpL+cpM yTop-rh hw rh]; +posInfo.rpmEstChk = [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.playerBtn3 = [cpL+cpM yTop-rh fw rh]; + +if exist('PSspecfig3','var') && ishandle(PSspecfig3) + figure(PSspecfig3); +else + PSspecfig3=figure(31); + set(PSspecfig3, 'Position', round([0 0 screensz(3) screensz(4)])); + try set(PSspecfig3, 'WindowState', 'maximized'); catch, end + set(PSspecfig3, 'NumberTitle', 'off'); + set(PSspecfig3, 'Name', ['PIDscope (' PsVersion ') - Frequency x Time Spectrogram']); + set(PSspecfig3, 'InvertHardcopy', 'off'); + set(PSspecfig3,'color',bgcolor); +end try % datacursormode not available in Octave @@ -75,10 +86,12 @@ set(dcm_obj2,'UpdateFcn',@PSdatatip); end +if ~exist('Spec3Crtlpanel','var') || ~ishandle(Spec3Crtlpanel) Spec3Crtlpanel = uipanel('Title','select file ','FontSize',fontsz,... - 'BackgroundColor',[.95 .95 .95],... - 'Position',[cpL .515 cpW .405]); - + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... + 'Position',[cpL yTop-rh-gap cpW vPos-(yTop-rh-gap)+cpTitleH]); + guiHandlesSpec3.computeSpec = uicontrol(PSspecfig3,'string','Run','fontsize',fontsz,'TooltipString', [TooltipString_specRun],'units','normalized','Position',[posInfo.computeSpec3],... 'callback','updateSpec = 0; clear specMat; PSfreqTime;'); set(guiHandlesSpec3.computeSpec, 'ForegroundColor', colRun); @@ -115,15 +128,58 @@ guiHandlesSpec3.climMax1_text = uicontrol(PSspecfig3,'style','text','string','Z min','fontsize',fontsz,'TooltipString',['adjusts the color limits'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.clim3Max1_text]); guiHandlesSpec3.climMax1_input = uicontrol(PSspecfig3,'style','edit','string',[num2str(ClimScale3(1))],'fontsize',fontsz,'TooltipString',['adjusts the color limits'],'units','normalized','Position',[posInfo.clim3Max1_input],... - 'callback','@textinput_call2; ClimScale3(1)=str2num(get(guiHandlesSpec3.climMax1_input, ''String''));updateSpec=1;PSfreqTime;'); + 'callback','@textinput_call2; ClimScale3(1)=str2double(get(guiHandlesSpec3.climMax1_input, ''String''));updateSpec=1;PSfreqTime;'); guiHandlesSpec3.climMax2_text = uicontrol(PSspecfig3,'style','text','string','Z max','fontsize',fontsz,'TooltipString',['adjusts the color limits'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.clim3Max2_text]); guiHandlesSpec3.climMax2_input = uicontrol(PSspecfig3,'style','edit','string',[num2str(ClimScale3(2))],'fontsize',fontsz,'TooltipString',['adjusts the color limits'],'units','normalized','Position',[posInfo.clim3Max2_input],... - 'callback','@textinput_call2; ClimScale3(2)=str2num(get(guiHandlesSpec3.climMax2_input, ''String''));updateSpec=1;PSfreqTime;'); + 'callback','@textinput_call2; ClimScale3(2)=str2double(get(guiHandlesSpec3.climMax2_input, ''String''));updateSpec=1;PSfreqTime;'); - guiHandlesSpec3.sub100HzfreqTime = uicontrol(PSspecfig3,'Style','checkbox','String','sub 100Hz','fontsize',fontsz,'ForegroundColor',[.2 .2 .2],'BackgroundColor',bgcolor,... + guiHandlesSpec3.sub100HzfreqTime = uicontrol(PSspecfig3,'Style','checkbox','String','sub 100Hz','fontsize',fontsz,'ForegroundColor',panelFg,'BackgroundColor',bgcolor,... 'units','normalized','Position',[posInfo.sub100HzfreqTime],'callback','@selection2;updateSpec=1; PSfreqTime;'); +% RPM overlay motor checkboxes - detect hex/octo +motorCols = PStheme().sigMotor; +nMot_ = 4; +if exist('T','var') && ~isempty(T) + for mi_ = 4:7 + if isfield(T{1}, ['motor_' int2str(mi_) '_']), nMot_ = mi_+1; end + end +end +if nMot_ > 4 + motorNames = {sprintf('M1/%d',nMot_/2+1), sprintf('M2/%d',nMot_/2+2), sprintf('M3/%d',nMot_/2+3), sprintf('M4/%d',nMot_/2+4)}; +else + motorNames = {'M1','M2','M3','M4'}; +end +guiHandlesSpec3.nMotors = nMot_; +rpmCb = 'updateSpec=1; PSfreqTime;'; +for mi = 1:4 + fld = sprintf('rpmMotor%d', mi); + guiHandlesSpec3.(fld) = uicontrol(PSspecfig3, 'Style','checkbox', 'String', motorNames{mi}, ... + 'fontsize', fontsz-1, 'Value', 0, ... + 'ForegroundColor', motorCols{mi}, 'BackgroundColor', bgcolor, ... + 'units','normalized', 'Position', posInfo.(fld), 'callback', rpmCb); +end + +guiHandlesSpec3.rpmHarmDd = uicontrol(PSspecfig3, 'Style','popupmenu', ... + 'String', {'RPM off','1st','2nd','3rd','1st & 2nd','1st & 3rd','2nd & 3rd','All harm.'}, ... + 'fontsize', fontsz, 'Value', 2, ... + 'units','normalized', 'Position', posInfo.rpmHarmDd, 'callback', rpmCb); + +guiHandlesSpec3.rpmLwDd = uicontrol(PSspecfig3, 'Style','popupmenu', ... + 'String', {'lw 0.5','lw 1','lw 1.5','lw 2'}, ... + 'fontsize', fontsz, 'Value', 2, ... + 'units','normalized', 'Position', posInfo.rpmLwDd, 'callback', rpmCb); + +guiHandlesSpec3.rpmDynNotch = uicontrol(PSspecfig3, 'Style','checkbox', 'String','Dyn Notch', ... + 'fontsize', fontsz, 'Value', 0, ... + 'ForegroundColor', th.overlayDynNotch, 'BackgroundColor', bgcolor, ... + 'units','normalized', 'Position', posInfo.rpmDynNotch, 'callback', rpmCb); + +guiHandlesSpec3.rpmEstChk = uicontrol(PSspecfig3, 'Style','checkbox', 'String','RPM est.', ... + 'fontsize', fontsz, 'Value', 0, ... + 'ForegroundColor', th.overlayRPM, 'BackgroundColor', bgcolor, ... + 'units','normalized', 'Position', posInfo.rpmEstChk, 'callback', rpmCb); + guiHandlesSpec3.playerBtn = uicontrol(PSspecfig3,'string','Player','fontsize',fontsz,... 'TooltipString','Animated spectrum playback over time','units','normalized',... 'Position',[posInfo.playerBtn3],... @@ -131,7 +187,40 @@ 'tmpSA3={''Gyro'',''Gyro prefilt'',''Dterm'',''Dterm prefilt'',''Pterm'',''PID error'',''Set point'',''PIDsum''};' ... 'PSdynSpecPlayer(specMat,Tm,F,{''Roll'',''Pitch'',''Yaw''},tmpSA3{get(guiHandlesSpec3.SpecList,''Value'')});' ... 'else,warndlg(''Run spectrogram first''),end']); -set(guiHandlesSpec3.playerBtn, 'ForegroundColor', [0 .4 .8]); +set(guiHandlesSpec3.playerBtn, 'ForegroundColor', th.btnPlayer); +end % ishandle(Spec3Crtlpanel) + +% Register CP for fixed-pixel resize +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', 0); +cpI = {}; +cpI{end+1} = struct('h', Spec3Crtlpanel, 'type','panel', 'row',0, 'col',0, 'hpx',0); +cpI{end+1} = struct('h', guiHandlesSpec3.FileSelect, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.SpecList, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.computeSpec, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.resetSpec, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.saveFig3, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.saveSettings3, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.smoothFactor_select, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.subsampleFactor_select, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.ColormapSelect, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.climMax1_text, 'type','text_left', 'row',0, 'col',0, 'hpx',rhs_px); +cpI{end+1} = struct('h', guiHandlesSpec3.climMax2_text, 'type','text_right', 'row',0, 'col',0, 'hpx',rhs_px); +cpI{end+1} = struct('h', guiHandlesSpec3.climMax1_input, 'type','input_left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.climMax2_input, 'type','input_right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.sub100HzfreqTime, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmMotor1, 'type','quarter1', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmMotor2, 'type','quarter2', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmMotor3, 'type','quarter3', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmMotor4, 'type','quarter4', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmHarmDd, 'type','dd_left', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmLwDd, 'type','dd_right', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmDynNotch, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.rpmEstChk, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec3.playerBtn, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +setappdata(PSspecfig3, 'PSplotGrid', struct('plotL',plotL1, 'colGap',0, ... + 'ncols',1, 'rows',rows, 'rowH',0.255, 'margin',0.10)); +PSregisterResize(PSspecfig3, cpPx, cpI, 'seq'); try set(guiHandlesSpec3.SpecList, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqxTime-Preset')))), catch, set(guiHandlesSpec3.SpecList, 'Value', 1), end try set(guiHandlesSpec3.smoothFactor_select, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqxTime-FreqSmoothing')))), catch, set(guiHandlesSpec3.smoothFactor_select, 'Value', 2), end @@ -142,7 +231,7 @@ try set(guiHandlesSpec3.ColormapSelect, 'Value', defaults.Values(find(strcmp(def else warndlg('Please select file(s)'); end - +PSstyleControls(PSspecfig3); % functions function selection2(src,event) @@ -157,7 +246,7 @@ function getList2(hObj,event) function textinput_call2(src,eventdata) str=get(src,'String'); - if isempty(str2num(str)) + if isnan(str2double(str)) set(src,'string','0'); warndlg('Input must be numerical'); end diff --git a/src/ui/PSslider1Actions.m b/src/ui/PSslider1Actions.m index 8f78134..e58d1fb 100644 --- a/src/ui/PSslider1Actions.m +++ b/src/ui/PSslider1Actions.m @@ -7,69 +7,98 @@ % this stuff is worth it, you can buy me a beer in return. -Brian White % ---------------------------------------------------------------------------------- - -try - a1 = axis(LVpanel4); a = [a1(1) a1(2)]; - catch - a = [0 tta{get(guiHandles.FileNum, 'Value')}(end) / us2sec]; - end - adiff = a(2)-a(1); - - x1 = a(1) + (get(guiHandles.slider, 'Value')*adiff) ; - try - delete(hslider1); - delete(hslider2); - delete(hslider3); - delete(hslider5); - catch - end - try - delete(hslider4);, - catch - end - -if ~get(guiHandles.RPYcomboLV, 'Value') - if get(guiHandles.plotR, 'Value'), - LVpanel1=subplot('position',posInfo.linepos1); - hslider1=plot([x1 x1],[-(maxY) maxY],'-k','linewidth',get(guiHandles.linewidth, 'Value')/2); - end - if get(guiHandles.plotP, 'Value'), LVpanel2=subplot('position',posInfo.linepos2); - hslider2=plot([x1 x1],[-(maxY) maxY],'-k','linewidth',get(guiHandles.linewidth, 'Value')/2); - end - if get(guiHandles.plotY, 'Value') - LVpanel3=subplot('position',posInfo.linepos3); - hslider3=plot([x1 x1],[-(maxY) maxY],'-k','linewidth',get(guiHandles.linewidth, 'Value')/2); - end - if get(guiHandles.plotR, 'Value') || get(guiHandles.plotP, 'Value') || get(guiHandles.plotY, 'Value') - LVpanel5=subplot('position',posInfo.linepos4); - hslider5=plot([x1 x1],[0 100],'-k','linewidth',get(guiHandles.linewidth, 'Value')/2); - axis([0 xmax 0 100]), - grid on - end +th = PStheme(); +fileIdx = get(guiHandles.FileNum, 'Value'); + +% Get time range from visible axes (by Tag, not position) +motorAx = findobj(PSfig, 'Type', 'axes', 'Tag', 'PSmotor'); +comboAx = findobj(PSfig, 'Type', 'axes', 'Tag', 'PScombo'); +try + if ~isempty(comboAx), a = xlim(comboAx(1)); + elseif ~isempty(motorAx), a = xlim(motorAx(1)); + else a = [0 tta{fileIdx}(end) / us2sec]; + end +catch + a = [0 tta{fileIdx}(end) / us2sec]; +end +adiff = a(2) - a(1); +x1 = a(1) + get(guiHandles.slider, 'Value') * adiff; + +% Delete old cursor lines +try delete(hslider1); catch, end +try delete(hslider2); catch, end +try delete(hslider3); catch, end +try delete(hslider4); catch, end +try delete(hslider5); catch, end +hslider1=[]; hslider2=[]; hslider3=[]; hslider4=[]; hslider5=[]; + +lwVal = get(guiHandles.linewidth, 'Value') / 2; + +% Draw cursor lines on TAGGED axes (never subplot — avoids position mismatch) +rpyAxes = findobj(PSfig, 'Type', 'axes', 'Tag', 'PSrpy'); +if ~isempty(comboAx) && ishandle(comboAx(1)) + set(PSfig, 'CurrentAxes', comboAx(1)); hold on; + hslider4 = plot([x1 x1], [-maxY maxY], '-', 'color', th.textPrimary, 'linewidth', lwVal); else - LVpanel4=subplot('position' ,[fullszPlot]); - hslider4=plot([x1 x1],[-(maxY) maxY],'-k','linewidth',get(guiHandles.linewidth, 'Value')/2); - LVpanel5=subplot('position',posInfo.linepos4); - hslider5=plot([x1 x1],[0 100],'-k','linewidth',get(guiHandles.linewidth, 'Value')/2); - axis([0 xmax 0 100]) - grid on + if numel(rpyAxes) > 1 + yy = zeros(numel(rpyAxes),1); + for k=1:numel(rpyAxes), p=get(rpyAxes(k),'Position'); yy(k)=p(2); end + [~,si] = sort(yy,'descend'); rpyAxes = rpyAxes(si); + end + for k = 1:min(3, numel(rpyAxes)) + set(PSfig, 'CurrentAxes', rpyAxes(k)); hold on; + h = plot([x1 x1], [-maxY maxY], '-', 'color', th.textPrimary, 'linewidth', lwVal); + if k==1, hslider1=h; elseif k==2, hslider2=h; else hslider3=h; end + end +end +if ~isempty(motorAx) && ishandle(motorAx(1)) + set(PSfig, 'CurrentAxes', motorAx(1)); hold on; + hslider5 = plot([x1 x1], [0 100], '-', 'color', th.textPrimary, 'linewidth', lwVal); end -h=subplot('position',[posInfo.YTstick]); -x2=find(tta{get(guiHandles.FileNum, 'Value')}/us2sec>=x1,1); -plot(-T{get(guiHandles.FileNum, 'Value')}.rcCommand_2_(x2) , (T{get(guiHandles.FileNum, 'Value')}.rcCommand_3_(x2) - 1000)/10,'ko'); -set(h, 'xlim', [-500 500], 'ylim', [0 100], 'xticklabel',['Y'], 'yticklabel',['T'],'xtick',[0], 'ytick',[50], 'xgrid', 'on', 'ygrid', 'on', 'fontweight','bold','FontSize', fontsz); -h=subplot('position',[posInfo.RPstick]); -plot(T{get(guiHandles.FileNum, 'Value')}.rcCommand_0_(x2) , T{get(guiHandles.FileNum, 'Value')}.rcCommand_1_(x2),'ko'); -set(h, 'xlim', [-500 500], 'ylim', [-500 500], 'xticklabel',['R'], 'yticklabel',['P'],'xtick',[0], 'ytick',[0], 'xgrid', 'on', 'ygrid', 'on', 'fontweight','bold','FontSize', fontsz); -subplot('position',[posInfo.YTstick]); h=text(0,110, ['time: ' num2str(tta{(get(guiHandles.FileNum, 'Value'))}(x2) / us2sec) ' sec']); set(h,'FontSize', fontsz); -try h=text(-450,-60, ['M3: ' int2str(T{get(guiHandles.FileNum, 'Value')}.motor_2_(x2)) '%']); set(h,'FontSize', fontsz, 'color', [ColorSet(13,:)]); catch, end -try h=text(-450,-40, ['M4: ' int2str(T{get(guiHandles.FileNum, 'Value')}.motor_3_(x2)) '%']); set(h,'FontSize', fontsz, 'color', [ColorSet(14,:)]); catch, end -subplot('position',[posInfo.RPstick]); -try h=text(-450,-1100, ['M1: ' int2str(T{get(guiHandles.FileNum, 'Value')}.motor_0_(x2)) '%']); set(h,'FontSize', fontsz, 'color', [ColorSet(11,:)]); catch, end -try h=text(-450,-900, ['M2: ' int2str(T{get(guiHandles.FileNum, 'Value')}.motor_1_(x2)) '%']); set(h,'FontSize', fontsz, 'color', [ColorSet(12,:)]); catch, end -subplot('position',[posInfo.YTstick]); -h=text(-450,-80, ['gyro R: ' int2str(T{get(guiHandles.FileNum, 'Value')}.gyroADC_0_(x2)) ' deg/s']); set(h,'FontSize', fontsz, 'color', [ColorSet(2,:)]); -h=text(-450,-100, ['gyro P: ' int2str(T{get(guiHandles.FileNum, 'Value')}.gyroADC_1_(x2)) ' deg/s']); set(h,'FontSize', fontsz, 'color', [ColorSet(2,:)]); -h=text(-450,-120, ['gyro Y: ' int2str(T{get(guiHandles.FileNum, 'Value')}.gyroADC_2_(x2)) ' deg/s']); set(h,'FontSize', fontsz, 'color', [ColorSet(2,:)]); +% Stick overlay — update persistent handles (created by PSviewerUIcontrol) +x2 = find(tta{fileIdx}/us2sec >= x1, 1); +if ~isempty(x2) && isfield(guiHandles, 'stickDotYT') + T_f = T{fileIdx}; + + if isfield(T_f, 'rcCommand_0_') + rfMot = getappdata(PSfig, 'rfMotorCount'); + if ~isempty(rfMot) % Rotorflight: collective is -500..500 + thrPct = (T_f.rcCommand_3_(x2) + 500) / 10; + else + thrPct = (T_f.rcCommand_3_(x2) - 1000) / 10; + end + try set(guiHandles.stickDotYT, 'XData', -T_f.rcCommand_2_(x2), ... + 'YData', thrPct); catch, end + try set(guiHandles.stickDotRP, 'XData', T_f.rcCommand_0_(x2), ... + 'YData', T_f.rcCommand_1_(x2)); catch, end + elseif isfield(T_f, 'setpoint_0_') + try set(guiHandles.stickDotYT, 'XData', -T_f.setpoint_2_(x2), ... + 'YData', T_f.setpoint_3_(x2)); catch, end + try set(guiHandles.stickDotRP, 'XData', T_f.setpoint_0_(x2), ... + 'YData', T_f.setpoint_1_(x2)); catch, end + end + set(guiHandles.overlayTime, 'String', sprintf('time: %.4f sec', tta{fileIdx}(x2)/us2sec)); + try set(guiHandles.overlayM1, 'String', sprintf('M1: %.0f%%', T_f.motor_0_(x2))); catch, end + rfMot = getappdata(PSfig, 'rfMotorCount'); + if ~isempty(rfMot) + si = 1; + if rfMot >= 2 + try set(guiHandles.overlayM2, 'String', sprintf('M2: %.0f%%', T_f.motor_1_(x2))); catch, end + else + try set(guiHandles.overlayM2, 'String', sprintf('S%d: %.0f%%', si, T_f.motor_1_(x2))); catch, end + si = si+1; + end + try set(guiHandles.overlayM3, 'String', sprintf('S%d: %.0f%%', si, T_f.motor_2_(x2))); catch, end + si = si+1; + try set(guiHandles.overlayM4, 'String', sprintf('S%d: %.0f%%', si, T_f.motor_3_(x2))); catch, end + else + try set(guiHandles.overlayM2, 'String', sprintf('M2: %.0f%%', T_f.motor_1_(x2))); catch, end + try set(guiHandles.overlayM3, 'String', sprintf('M3: %.0f%%', T_f.motor_2_(x2))); catch, end + try set(guiHandles.overlayM4, 'String', sprintf('M4: %.0f%%', T_f.motor_3_(x2))); catch, end + end + try set(guiHandles.overlayGR, 'String', sprintf('gyro R: %.0f deg/s', T_f.gyroADC_0_(x2))); catch, end + try set(guiHandles.overlayGP, 'String', sprintf('gyro P: %.0f deg/s', T_f.gyroADC_1_(x2))); catch, end + try set(guiHandles.overlayGY, 'String', sprintf('gyro Y: %.0f deg/s', T_f.gyroADC_2_(x2))); catch, end +end diff --git a/src/ui/PSsliderTool.m b/src/ui/PSsliderTool.m new file mode 100644 index 0000000..c92b0b0 --- /dev/null +++ b/src/ui/PSsliderTool.m @@ -0,0 +1,257 @@ +function PSsliderTool(rollPID_str, pitchPID_str, yawPID_str) +%% PSsliderTool - interactive PID ratio calculator +% Popup window with firmware presets and ratio-based sliders. +% Two math modes: PTB (P-anchor, ratio-based) and BF (simplified_tuning.c style). + +thm = PStheme(); +fontsz = thm.fontsz; + +fig = findobj('Type', 'figure', 'Name', 'PID Slider Tool'); +if ~isempty(fig), close(fig); end + +screensz = get(0, 'ScreenSize'); +figW = round(min(720, screensz(3) * 0.55)); +figH = round(min(520, screensz(4) * 0.55)); +figX = round((screensz(3) - figW) / 2); +figY = round((screensz(4) - figH) / 2); +fig = figure('Name', 'PID Slider Tool', 'NumberTitle', 'off', ... + 'Color', thm.figBg, ... + 'Position', [figX figY figW figH], ... + 'MenuBar', 'none', 'ToolBar', 'none', 'Resize', 'off'); + +% Firmware presets: {name, P, I, D, yawDfactor} +presets = { ... + 'Betaflight', NaN, NaN, NaN, 1.0; + 'INAV', 40, 75, 20, 0.0; + 'FETTEC', 1.00, 0.01, 2.50, 0.08; + 'KISS_ULTRA', 1.5, 0.05, 5.0, 0.04; + 'QuickSilver', 42, 85, 30, 1.0; + 'ArduPilot', 0.135,0.090,0.0036,5.0; +}; + +hasLogPID = nargin >= 3 && ~isempty(rollPID_str); +if hasLogPID + rp = sscanf(strrep(rollPID_str, ' ', ''), '%f,%f,%f'); + yp = sscanf(strrep(yawPID_str, ' ', ''), '%f,%f,%f'); + if numel(rp) >= 3 && numel(yp) >= 3 + ydf = 1.0; + if rp(3) > 0, ydf = yp(3) / rp(3); end + presets(end+1,:) = {'From Log', rp(1), rp(2), rp(3), ydf}; + end +end + +fwNames = presets(:,1); +setappdata(fig, 'presets', presets); + +% Slider names per math mode +slNamesPTB = {'PD Ratio', 'PI Ratio', 'Roll-Pitch Ratio', 'Yaw Ratio', 'Master Multiplier'}; +slNamesBF = {'D Gain', 'PI Gain', 'Roll-Pitch Ratio', 'Yaw Ratio', 'Master Multiplier'}; +setappdata(fig, 'slNamesPTB', slNamesPTB); +setappdata(fig, 'slNamesBF', slNamesBF); + +% --- Controls --- +h = struct(); + +% Row 0: Reset + firmware + math mode +h.resetBtn = uicontrol(fig, 'Style', 'pushbutton', 'String', 'Reset', ... + 'Position', [20 figH-50 70 30], ... + 'FontSize', fontsz, 'FontWeight', 'bold', ... + 'BackgroundColor', thm.btnReset, 'ForegroundColor', [0 0 0], ... + 'Callback', @cb_reset); + +h.fwDd = uicontrol(fig, 'Style', 'popupmenu', 'String', fwNames, ... + 'Position', [100 figH-50 130 28], ... + 'FontSize', fontsz, ... + 'Callback', @cb_fwChanged); + +uicontrol(fig, 'Style', 'text', 'String', 'Math:', ... + 'Position', [240 figH-50 40 22], 'FontSize', fontsz, ... + 'ForegroundColor', thm.textPrimary, 'BackgroundColor', thm.figBg, ... + 'HorizontalAlignment', 'right'); + +h.mathDd = uicontrol(fig, 'Style', 'popupmenu', ... + 'String', {'PTB (ratio)', 'Betaflight'}, ... + 'Position', [285 figH-50 120 28], ... + 'FontSize', fontsz, ... + 'Callback', @cb_mathChanged); + +% Row 1: headers +hdrY = figH - 90; +uicontrol(fig, 'Style', 'text', 'String', 'PID start values', ... + 'Position', [140 hdrY 170 22], 'FontSize', fontsz+1, 'FontWeight', 'bold', ... + 'ForegroundColor', thm.textPrimary, 'BackgroundColor', thm.figBg, ... + 'HorizontalAlignment', 'center'); +uicontrol(fig, 'Style', 'text', 'String', 'PID test values', ... + 'Position', [390 hdrY 300 22], 'FontSize', fontsz+1, 'FontWeight', 'bold', ... + 'ForegroundColor', thm.textPrimary, 'BackgroundColor', thm.figBg, ... + 'HorizontalAlignment', 'center'); + +% Row 2-4: P/I/D edit boxes + test value labels +labels = {'P', 'I', 'D'}; +axNames = {'Roll', 'Pitch', 'Yaw'}; +for k = 1:3 + yy = hdrY - 28*k; + uicontrol(fig, 'Style', 'text', 'String', labels{k}, ... + 'Position', [170 yy 20 22], ... + 'FontSize', fontsz, 'FontWeight', 'bold', ... + 'ForegroundColor', thm.textPrimary, 'BackgroundColor', thm.figBg); + h.startEdit(k) = uicontrol(fig, 'Style', 'edit', 'String', '', ... + 'Position', [195 yy 90 24], 'FontSize', fontsz, ... + 'Callback', @cb_recalc); +end +for k = 1:3 + yy = hdrY - 28*k; + h.testLbl(k) = uicontrol(fig, 'Style', 'text', ... + 'String', [axNames{k} ' PID:'], ... + 'Position', [390 yy 300 22], 'FontSize', fontsz, ... + 'ForegroundColor', thm.textPrimary, 'BackgroundColor', thm.figBg, ... + 'HorizontalAlignment', 'left'); +end + +% Math mode description label +h.mathDesc = uicontrol(fig, 'Style', 'text', 'String', '', ... + 'Position', [420 figH-50 290 22], 'FontSize', fontsz-1, ... + 'ForegroundColor', thm.textSecondary, 'BackgroundColor', thm.figBg, ... + 'HorizontalAlignment', 'left'); + +% Sliders +slY0 = hdrY - 28*3 - 45; +slGap = 55; +slLblW = 200; +slX = 210; +slW = figW - slX - 30; + +for k = 1:5 + yy = slY0 - (k-1)*slGap; + h.slLbl(k) = uicontrol(fig, 'Style', 'text', ... + 'String', sprintf('%s: 1.00', slNamesPTB{k}), ... + 'Position', [5 yy slLblW 22], 'FontSize', fontsz, ... + 'ForegroundColor', thm.textPrimary, 'BackgroundColor', thm.figBg, ... + 'HorizontalAlignment', 'right'); + h.sl(k) = uicontrol(fig, 'Style', 'slider', ... + 'Position', [slX yy slW 20], ... + 'Min', 0.25, 'Max', 2.00, 'Value', 1.00, ... + 'SliderStep', [0.01/1.75, 0.05/1.75], ... + 'Callback', @cb_sliderMoved); +end + +setappdata(fig, 'h', h); + +% Init +if hasLogPID, set(h.fwDd, 'Value', numel(fwNames)); end +cb_mathChanged(h.mathDd, []); +cb_fwChanged(h.fwDd, []); + +% ---- Nested callbacks ---- + function cb_fwChanged(src, ~) + idx = get(src, 'Value'); + pr = getappdata(fig, 'presets'); + for e = 1:3 + v = pr{idx, e+1}; + if isnan(v), set(h.startEdit(e), 'String', ''); + else set(h.startEdit(e), 'String', num2str(v)); end + end + cb_reset([], []); + end + + function cb_mathChanged(~, ~) + mode = get(h.mathDd, 'Value'); + if mode == 1 + sn = getappdata(fig, 'slNamesPTB'); + set(h.mathDesc, 'String', 'P=anchor, ratios adjust I/D'); + else + sn = getappdata(fig, 'slNamesBF'); + set(h.mathDesc, 'String', 'PI Gain scales P+I together'); + end + setappdata(fig, 'slNames', sn); + for s = 1:5 + v = get(h.sl(s), 'Value'); + set(h.slLbl(s), 'String', sprintf('%s: %.2f', sn{s}, v)); + end + recalc(); + end + + function cb_reset(~, ~) + sn = getappdata(fig, 'slNames'); + for s = 1:5 + set(h.sl(s), 'Value', 1.00); + set(h.slLbl(s), 'String', sprintf('%s: 1.00', sn{s})); + end + recalc(); + end + + function cb_sliderMoved(~, ~) + sn = getappdata(fig, 'slNames'); + for s = 1:5 + v = get(h.sl(s), 'Value'); + set(h.slLbl(s), 'String', sprintf('%s: %.2f', sn{s}, v)); + end + recalc(); + end + + function cb_recalc(~, ~), recalc(); end + + function recalc() + P0 = str2double(get(h.startEdit(1), 'String')); + I0 = str2double(get(h.startEdit(2), 'String')); + D0 = str2double(get(h.startEdit(3), 'String')); + if isnan(P0) || isnan(I0) || isnan(D0), return; end + + idx = get(h.fwDd, 'Value'); + pr = getappdata(fig, 'presets'); + ydf = pr{idx, 5}; + yawD0 = D0 * ydf; + + sl1 = get(h.sl(1), 'Value'); % PD Ratio / D Gain + sl2 = get(h.sl(2), 'Value'); % PI Ratio / PI Gain + rpr = get(h.sl(3), 'Value'); % Roll-Pitch Ratio + ywr = get(h.sl(4), 'Value'); % Yaw Ratio + mst = get(h.sl(5), 'Value'); % Master Multiplier + + mode = get(h.mathDd, 'Value'); + + if mode == 1 + % PTB math: P is anchor, PI ratio affects I only, Yaw I excludes PI + rP = P0 * mst; + rI = I0 * sl2 * mst; + rD = D0 * sl1 * mst; + + pP = rP * rpr; + pI = rI * rpr; + pD = rD * rpr; + + yP = rP * ywr; + yI = I0 * ywr * mst; + yD = yawD0 * sl1 * ywr * mst; + else + % BF math: PI Gain scales P AND I together, Yaw I follows PI Gain + rP = P0 * sl2 * mst; + rI = I0 * sl2 * mst; + rD = D0 * sl1 * mst; + + pP = rP * rpr; + pI = rI * rpr; + pD = rD * rpr; + + yP = rP * ywr; + yI = rI * ywr; + yD = yawD0 * sl1 * ywr * mst; + end + + set(h.testLbl(1), 'String', sprintf('Roll PID: %s %s %s', fp(rP), fp(rI), fp(rD))); + set(h.testLbl(2), 'String', sprintf('Pitch PID: %s %s %s', fp(pP), fp(pI), fp(pD))); + set(h.testLbl(3), 'String', sprintf('Yaw PID: %s %s %s', fp(yP), fp(yI), fp(yD))); + end + + function s = fp(v) + if abs(v) < 0.001 + s = sprintf('%.4f', v); + elseif abs(v) < 1 + s = sprintf('%.3f', v); + elseif abs(v - round(v)) < 0.005 + s = sprintf('%.0f', v); + else + s = sprintf('%.2f', v); + end + end +end diff --git a/src/ui/PSspec2DUIcontrol.m b/src/ui/PSspec2DUIcontrol.m index 65434bd..c5a0bb0 100644 --- a/src/ui/PSspec2DUIcontrol.m +++ b/src/ui/PSspec2DUIcontrol.m @@ -13,12 +13,15 @@ TooltipString_specRun=['Run current spectral configuration']; TooltipString_cmap=['Choose from a selection of colormaps']; TooltipString_smooth=['Choose amount of smoothing']; -TooltipString_user=['Choose the variable you wish to plot']; +TooltipString_user=['Choose signal(s) to plot. Select up to 2.' ... + newline, 'Gyro prefilt = pre-filter gyro (dotted line).' ... + newline, 'BF 4.5+: uses gyroUnfilt (logged by default). Older BF: requires debug_mode = GYRO_SCALED.']; TooltipString_sub100=['Zoom data to show sub 100Hz details',... newline, 'Typically used to see propwash or mid-throttle vibration in e.g. Gyro/Pterm/PIDerror']; %%% +th = PStheme(); PScolormap; % define smat=[];%string @@ -28,88 +31,72 @@ clear posInfo.Spec2Pos -cols=[0.05 0.47]; +plotR = cpL - 0.04; plotL2 = 0.05; colGap2 = 0.02; +colW2 = (plotR - plotL2 - colGap2) / 2; +cols = [plotL2, plotL2 + colW2 + colGap2]; rows=[0.69 0.395 0.1]; k=0; for c=1 : size(cols,2) for r=1 : size(rows,2) k=k+1; - posInfo.Spec2Pos(k,:)=[cols(c) rows(r) 0.37 0.25]; + posInfo.Spec2Pos(k,:)=[cols(c) rows(r) colW2 0.25]; end end -vPosSpec2d = .037; -sp2_rh = .026; sp2_ddh = .04; -if exist('isOctave','var') && isOctave - sp2_rh = .030; sp2_ddh = .035; -end - -% Control panel layout (consistent with Log Viewer cpL/cpW) -cpL = .875; cpW = .12; - -posInfo.fileListWindowSpec= [cpL+.003 .7+vPosSpec2d cpW-.006 .20]; -posInfo.TermListWindowSpec= [cpL+.003 .55+vPosSpec2d cpW-.006 .14]; - -posInfo.computeSpec= [cpL+.006 .52+vPosSpec2d cpW/2-.006 sp2_rh]; -posInfo.resetSpec= [cpL+cpW/2 .52+vPosSpec2d cpW/2-.006 sp2_rh]; -posInfo.spectrogramButton2= [cpL+.003 .49+vPosSpec2d cpW-.006 sp2_rh]; -posInfo.spectrogramButton3= [cpL+.003 .46+vPosSpec2d cpW-.006 sp2_rh]; -posInfo.filterSimButton= [cpL+.003 .43+vPosSpec2d cpW-.006 sp2_rh]; -posInfo.motorNoiseButton= [cpL+.003 .40+vPosSpec2d cpW-.006 sp2_rh]; -posInfo.chirpButton= [cpL+.003 .37+vPosSpec2d cpW-.006 sp2_rh]; -posInfo.saveFig2= [cpL+.006 .34+vPosSpec2d cpW/2-.006 sp2_rh]; -posInfo.saveSettings2= [cpL+cpW/2 .34+vPosSpec2d cpW/2-.006 sp2_rh]; - -posInfo.smooth_select = [cpL+.003 .305+vPosSpec2d cpW-.006 sp2_ddh]; -posInfo.Delay = [cpL+.003 .27+vPosSpec2d cpW-.006 sp2_ddh]; - -posInfo.plotRspec = [cpL+.005 .245+vPosSpec2d .035 .025]; -posInfo.plotPspec = [cpL+.04 .245+vPosSpec2d .035 .025]; -posInfo.plotYspec = [cpL+.075 .245+vPosSpec2d .035 .025]; - -posInfo.checkboxPSD = [cpL+.005 .225+vPosSpec2d .04 .02]; -posInfo.RPYcomboSpec = [cpL+cpW/2-.01 .225+vPosSpec2d cpW/2+.004 .02]; - -posInfo.climMax1_text = [cpL+.003 .202+vPosSpec2d cpW/4 .022]; -posInfo.climMax1_input = [cpL+cpW/4 .180+vPosSpec2d cpW/4 .022]; -posInfo.climMax2_text = [cpL+cpW/2 .202+vPosSpec2d cpW/4 .022]; -posInfo.climMax2_input = [cpL+3*cpW/4 .180+vPosSpec2d cpW/4 .022]; - -if exist('isOctave','var') && isOctave - % Octave Qt widgets need more vertical space - vPosSpec2d = .037; - rr = .030; dd = .035; - posInfo.computeSpec= [cpL+.006 .52+vPosSpec2d cpW/2-.006 rr]; - posInfo.resetSpec= [cpL+cpW/2 .52+vPosSpec2d cpW/2-.006 rr]; - posInfo.spectrogramButton2= [cpL+.003 .485+vPosSpec2d cpW-.006 rr]; - posInfo.spectrogramButton3= [cpL+.003 .450+vPosSpec2d cpW-.006 rr]; - posInfo.filterSimButton= [cpL+.003 .415+vPosSpec2d cpW-.006 rr]; - posInfo.motorNoiseButton= [cpL+.003 .380+vPosSpec2d cpW-.006 rr]; - posInfo.chirpButton= [cpL+.003 .345+vPosSpec2d cpW-.006 rr]; - posInfo.saveFig2= [cpL+.006 .310+vPosSpec2d cpW/2-.006 rr]; - posInfo.saveSettings2= [cpL+cpW/2 .310+vPosSpec2d cpW/2-.006 rr]; - posInfo.smooth_select= [cpL+.003 .270+vPosSpec2d cpW-.006 dd]; - posInfo.Delay= [cpL+.003 .233+vPosSpec2d cpW-.006 dd]; - posInfo.plotRspec= [cpL+.005 .208+vPosSpec2d .035 .025]; - posInfo.plotPspec= [cpL+.04 .208+vPosSpec2d .035 .025]; - posInfo.plotYspec= [cpL+.075 .208+vPosSpec2d .035 .025]; - posInfo.checkboxPSD= [cpL+.005 .185+vPosSpec2d .04 .025]; - posInfo.RPYcomboSpec= [cpL+cpW/2-.01 .185+vPosSpec2d cpW/2+.004 .025]; - posInfo.climMax1_text= [cpL+.003 .162+vPosSpec2d cpW/4 .024]; - posInfo.climMax1_input= [cpL+cpW/4 .140+vPosSpec2d cpW/4 .024]; - posInfo.climMax2_text= [cpL+cpW/2 .162+vPosSpec2d cpW/4 .024]; - posInfo.climMax2_input= [cpL+3*cpW/4 .140+vPosSpec2d cpW/4 .024]; -end +% Control panel layout — cpL/cpW/rh/rs/cpM/ddh inherited from PIDscope.m +% yTop tracks where the TOP of the next element goes; Position Y = yTop - height +listH = 5*rs; termH = round(4.3*rs); gap = rs - rh; fw = cpW-2*cpM; hw = cpW/2-cpM; +tbOff_s2 = 40/screensz(4); +yTop = 1 - tbOff_s2 - cpTitleH - cpMv; +posInfo.fileListWindowSpec= [cpL+cpM yTop-listH fw listH]; yTop=yTop-listH-gap; +posInfo.TermListWindowSpec= [cpL+cpM yTop-termH fw termH]; yTop=yTop-termH-gap; + +posInfo.computeSpec= [cpL+cpM yTop-rh hw rh]; +posInfo.resetSpec= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.spectrogramButton2= [cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +posInfo.spectrogramButton3= [cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +posInfo.motorNoiseButton= [cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +posInfo.chirpButton= [cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +posInfo.saveFig2= [cpL+cpM yTop-rh hw rh]; +posInfo.saveSettings2= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; + +posInfo.smooth_select = [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.Delay = [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; + +posInfo.plotRspec = [cpL+cpM yTop-rh cbW rh]; +posInfo.plotPspec = [cpL+cpM+cbW yTop-rh cbW rh]; +posInfo.plotYspec = [cpL+cpM+2*cbW yTop-rh cbW rh]; yTop=yTop-rh-gap; + +posInfo.checkboxPSD = [cpL+cpM yTop-rh cbW rh]; +posInfo.RPYcomboSpec = [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; + +posInfo.climMax1_text = [cpL+cpM yTop-rhs cpW/4 rhs]; +posInfo.climMax2_text = [cpL+cpW/2 yTop-rhs cpW/4 rhs]; yTop=yTop-rhs-gap; +posInfo.climMax1_input = [cpL+cpM yTop-rh cpW/4 rh]; +posInfo.climMax2_input = [cpL+cpW/2 yTop-rh cpW/4 rh]; yTop=yTop-rh-gap; + +qw = fw/4; +posInfo.rpmMotor1 = [cpL+cpM yTop-rh qw rh]; +posInfo.rpmMotor2 = [cpL+cpM+qw yTop-rh qw rh]; +posInfo.rpmMotor3 = [cpL+cpM+2*qw yTop-rh qw rh]; +posInfo.rpmMotor4 = [cpL+cpM+3*qw yTop-rh qw rh]; yTop=yTop-rh-gap; +posInfo.rpmHarmDd = [cpL+cpM yTop-ddh hw ddh]; +posInfo.rpmLwDd = [cpL+cpW/2 yTop-ddh hw ddh]; yTop=yTop-ddh-gap; climScale1=[0 ; -50 ]; climScale2=[0.5 ; 20]; -PSspecfig2=figure(3); -set(PSspecfig2, 'Position', round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)])); -set(PSspecfig2, 'NumberTitle', 'off'); -set(PSspecfig2, 'Name', ['PIDscope (' PsVersion ') - Spectral Analyzer']); -set(PSspecfig2, 'InvertHardcopy', 'off'); -set(PSspecfig2,'color',bgcolor); +if exist('PSspecfig2','var') && ishandle(PSspecfig2) + figure(PSspecfig2); +else + PSspecfig2=figure(3); + set(PSspecfig2, 'Position', round([0 0 screensz(3) screensz(4)])); + try set(PSspecfig2, 'WindowState', 'maximized'); catch, end + set(PSspecfig2, 'NumberTitle', 'off'); + set(PSspecfig2, 'Name', ['PIDscope (' PsVersion ') - Spectral Analyzer']); + set(PSspecfig2, 'InvertHardcopy', 'off'); + set(PSspecfig2,'color',bgcolor); +end try % datacursormode not available in Octave @@ -117,14 +104,15 @@ set(dcm_obj2,'UpdateFcn',@PSdatatip); end -spec2CrtlpanelPos = [cpL .21+vPosSpec2d cpW .71]; -if exist('isOctave','var') && isOctave - spec2CrtlpanelPos = [cpL .16+vPosSpec2d cpW .76]; -end +sp2PanelBot = yTop - rh - gap; +sp2PanelH = vPos - sp2PanelBot + cpTitleH; +spec2CrtlpanelPos = [cpL sp2PanelBot cpW sp2PanelH]; +if ~exist('spec2Crtlpanel','var') || ~ishandle(spec2Crtlpanel) spec2Crtlpanel = uipanel('Title','select files (max 10)','FontSize',fontsz,... - 'BackgroundColor',[.95 .95 .95],... + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... 'Position',spec2CrtlpanelPos); - + guiHandlesSpec2.computeSpec = uicontrol(PSspecfig2,'string','Run','fontsize',fontsz,'TooltipString', [TooltipString_specRun],'units','normalized','Position',[posInfo.computeSpec],... 'callback','PSplotSpec2D;'); set(guiHandlesSpec2.computeSpec, 'ForegroundColor', colRun); @@ -141,10 +129,12 @@ set(guiHandlesSpec2.saveSettings2, 'ForegroundColor', saveCol); % create string list for SpecSelect -sA={'Gyro','Gyro prefilt','Dterm','Dterm prefilt','Pterm','PID error','Set point','PIDsum'}; +sA={'Gyro','Gyro prefilt','Dterm','Dterm prefilt','Pterm','PID error','Set point','Fterm','PIDsum','Motors'}; +if isfield(T{1}, 'testSignal_0_'), sA{end+1} = 'Test Signal'; end guiHandlesSpec2.SpecList = uicontrol(PSspecfig2,'Style','listbox','string',[sA],'max',3,'min',1, 'fontsize',fontsz, 'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.TermListWindowSpec], 'callback', 'if length(get(guiHandlesSpec2.SpecList, ''Value'')) > 2, set(guiHandlesSpec2.SpecList, ''Value'', 1); end;'); -set(guiHandlesSpec2.SpecList, 'Value', [1 2]); +specDef_ = [1]; if isfield(T{1}, 'gyroPrefilt_0_'), specDef_ = [1 2]; end +set(guiHandlesSpec2.SpecList, 'Value', specDef_); guiHandlesSpec2.FileSelect = uicontrol(PSspecfig2,'Style','listbox','string',[fnameMaster],'max', 10, 'min', 1, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fileListWindowSpec], 'callback', 'if length(get(guiHandlesSpec2.FileSelect, ''Value'')) > 10, set(guiHandlesSpec2.FileSelect, ''Value'', 1); end;'); @@ -153,34 +143,19 @@ guiHandlesSpec2.spectrogramButton2 = uicontrol(PSspecfig2,'string','Freq x Throttle','fontsize',fontsz,'TooltipString', ['Opens Freq x Throttle Spectrogram in New Window'], 'units','normalized','Position',[posInfo.spectrogramButton2],... 'callback','PSspecUIcontrol;'); -set(guiHandlesSpec2.spectrogramButton2, 'ForegroundColor', colorA); +set(guiHandlesSpec2.spectrogramButton2, 'ForegroundColor', th.btnDash1); guiHandlesSpec2.spectrogramButton3 = uicontrol(PSspecfig2,'string','Freq x Time','fontsize',fontsz,'TooltipString', ['Opens Freq x Time Spectrogram in New Window'], 'units','normalized','Position',[posInfo.spectrogramButton3],... 'callback','PSfreqTimeUIcontrol;'); - set(guiHandlesSpec2.spectrogramButton3, 'ForegroundColor', colorB); + set(guiHandlesSpec2.spectrogramButton3, 'ForegroundColor', th.btnDash2); -guiHandlesSpec2.filterSimButton = uicontrol(PSspecfig2,'string','Filter Sim','fontsize',fontsz,... - 'TooltipString','Simulate BF filter chain on gyro data','units','normalized',... - 'Position',[posInfo.filterSimButton],... - 'callback',['try,' ... - 'tmpFcnt=get(guiHandlesSpec2.FileSelect,''Value'');tmpFcnt=tmpFcnt(1);' ... - 'tmpGyro.r=T{tmpFcnt}.gyroADC_0_(tIND{tmpFcnt});' ... - 'tmpGyro.p=T{tmpFcnt}.gyroADC_1_(tIND{tmpFcnt});' ... - 'tmpGyro.y=T{tmpFcnt}.gyroADC_2_(tIND{tmpFcnt});' ... - 'PSfilterSim(tmpGyro,1000*A_lograte(tmpFcnt),SetupInfo{tmpFcnt});' ... - 'clear tmpGyro tmpFcnt;' ... - 'catch e,warndlg([''Filter Sim: '' e.message]),end']); -set(guiHandlesSpec2.filterSimButton, 'ForegroundColor', [.8 .5 0]); - -guiHandlesSpec2.motorNoiseButton = uicontrol(PSspecfig2,'string','Motor Noise','fontsize',fontsz,... - 'TooltipString','Per-motor spectral analysis and noise comparison','units','normalized',... - 'Position',[posInfo.motorNoiseButton],... - 'callback',['try,' ... - 'tmpFcnt=get(guiHandlesSpec2.FileSelect,''Value'');tmpFcnt=tmpFcnt(1);' ... - 'PSplotMotorNoise(T{tmpFcnt},tmpFcnt,tIND{tmpFcnt},1000*A_lograte(tmpFcnt));' ... - 'clear tmpFcnt;' ... - 'catch e,warndlg([''Motor Noise: '' e.message]),end']); -set(guiHandlesSpec2.motorNoiseButton, 'ForegroundColor', [.2 .7 .2]); +guiHandlesSpec2.rightColMode = uicontrol(PSspecfig2,'Style','popupmenu','String',{'sub 100Hz','Motor Noise'},... + 'fontsize',fontsz,'TooltipString','Right column: sub 100Hz PSD or Motor Noise per-harmonic',... + 'units','normalized','Position',[posInfo.motorNoiseButton],... + 'callback',['vis_=''off'';if get(guiHandlesSpec2.rightColMode,''Value'')==2,vis_=''on'';end;' ... + 'flds_={''rpmMotor1'',''rpmMotor2'',''rpmMotor3'',''rpmMotor4'',''rpmHarmDd'',''rpmLwDd''};' ... + 'for fi_=1:6,set(guiHandlesSpec2.(flds_{fi_}),''Visible'',vis_);end;' ... + 'try PSresizeCP(PSspecfig2,[]);catch,end;PSplotSpec2D;']); guiHandlesSpec2.chirpButton = uicontrol(PSspecfig2,'string','Chirp Analysis','fontsize',fontsz,... 'TooltipString','Frequency response from chirp log (BF 2025.12+, debug_mode=CHIRP)','units','normalized',... @@ -192,19 +167,19 @@ 'PSrunChirpAnalysis(T{tmpFcnt},SetupInfo{tmpFcnt},debugIdx{tmpFcnt},1000*A_lograte(tmpFcnt),tIND{tmpFcnt},tmpAx);' ... 'clear tmpFcnt tmpRPY tmpAx;' ... 'catch e,warndlg([''Chirp: '' e.message]),end']); -set(guiHandlesSpec2.chirpButton, 'ForegroundColor', [.8 .3 .8]); +set(guiHandlesSpec2.chirpButton, 'ForegroundColor', th.btnChirp); guiHandlesSpec2.Delay = uicontrol(PSspecfig2,'style','popupmenu','string',{'filter delay', 'SP-gyro delay', 'SP smoothing delay', 'phase shift'},'fontsize',fontsz,'TooltipString', ['Select which Delay Display'], 'units','normalized','Position',[posInfo.Delay],... 'callback','PSplotSpec2D;'); guiHandlesSpec2.plotR =uicontrol(PSspecfig2,'Style','checkbox','String','R','fontsize',fontsz,'TooltipString', ['Plot Roll '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotRspec], 'callback', 'delete(findobj(PSspecfig2,''Type'',''axes'')); set(PSspecfig2, ''pointer'', ''arrow'');'); + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisRoll,'Position',[posInfo.plotRspec], 'callback', 'PSplotSpec2D;'); guiHandlesSpec2.plotP =uicontrol(PSspecfig2,'Style','checkbox','String','P','fontsize',fontsz,'TooltipString', ['Plot Pitch '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotPspec], 'callback', 'delete(findobj(PSspecfig2,''Type'',''axes'')); set(PSspecfig2, ''pointer'', ''arrow'');'); + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisPitch,'Position',[posInfo.plotPspec], 'callback', 'PSplotSpec2D;'); guiHandlesSpec2.plotY =uicontrol(PSspecfig2,'Style','checkbox','String','Y','fontsize',fontsz,'TooltipString', ['Plot Yaw '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotYspec], 'callback', 'delete(findobj(PSspecfig2,''Type'',''axes'')); set(PSspecfig2, ''pointer'', ''arrow'');'); + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisYaw,'Position',[posInfo.plotYspec], 'callback', 'PSplotSpec2D;'); guiHandlesSpec2.checkboxPSD =uicontrol(PSspecfig2,'Style','checkbox','String','PSD','fontsize',fontsz,'TooltipString', ['Power Spectral Density'],... 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkboxPSD],'callback', 'PSplotSpec2D;'); @@ -215,14 +190,86 @@ guiHandlesSpec2.climMax1_text = uicontrol(PSspecfig2,'style','text','string','Y min','fontsize',fontsz,'TooltipString',['Y min'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax1_text]); guiHandlesSpec2.climMax1_input = uicontrol(PSspecfig2,'style','edit','string',[num2str(climScale1(get(guiHandlesSpec2.checkboxPSD, 'Value')+1, 1))],'fontsize',fontsz,'TooltipString',['Y min'],'units','normalized','Position',[posInfo.climMax1_input],... - 'callback','@textinput_call2; climScale1(get(guiHandlesSpec2.checkboxPSD, ''Value'')+1, 1)=str2num(get(guiHandlesSpec2.climMax1_input, ''String''));PSplotSpec2D;'); + 'callback','@textinput_call2; climScale1(get(guiHandlesSpec2.checkboxPSD, ''Value'')+1, 1)=str2double(get(guiHandlesSpec2.climMax1_input, ''String''));PSplotSpec2D;'); guiHandlesSpec2.climMax2_text = uicontrol(PSspecfig2,'style','text','string','Y max','fontsize',fontsz,'TooltipString',['Y max'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax2_text]); guiHandlesSpec2.climMax2_input = uicontrol(PSspecfig2,'style','edit','string',[num2str(climScale2(get(guiHandlesSpec2.checkboxPSD, 'Value')+1, 1))],'fontsize',fontsz,'TooltipString',['Y max'],'units','normalized','Position',[posInfo.climMax2_input],... - 'callback','@textinput_call2; climScale2(get(guiHandlesSpec2.checkboxPSD, ''Value'')+1, 1)=str2num(get(guiHandlesSpec2.climMax2_input, ''String''));PSplotSpec2D;'); - + 'callback','@textinput_call2; climScale2(get(guiHandlesSpec2.checkboxPSD, ''Value'')+1, 1)=str2double(get(guiHandlesSpec2.climMax2_input, ''String''));PSplotSpec2D;'); -try set(guiHandlesSpec2.SpecList, 'Value', [defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-term1'))) defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-term2')))]), catch, set(guiHandlesSpec2.SpecList, 'Value', [1 2]), end +motorCols = PStheme().sigMotor; +nMot_ = 4; +if exist('T','var') && ~isempty(T) + for mi_ = 4:7 + if isfield(T{1}, ['motor_' int2str(mi_) '_']), nMot_ = mi_+1; end + end +end +if nMot_ > 4 + motorNames = {sprintf('M1/%d',nMot_/2+1), sprintf('M2/%d',nMot_/2+2), sprintf('M3/%d',nMot_/2+3), sprintf('M4/%d',nMot_/2+4)}; +else + motorNames = {'M1','M2','M3','M4'}; +end +guiHandlesSpec2.nMotors = nMot_; +rpmCb2 = 'PSplotSpec2D;'; +for mi = 1:4 + fld = sprintf('rpmMotor%d', mi); + guiHandlesSpec2.(fld) = uicontrol(PSspecfig2, 'Style','checkbox', 'String', motorNames{mi}, ... + 'fontsize', fontsz-1, 'Value', 1, 'Visible', 'off', ... + 'ForegroundColor', motorCols{mi}, 'BackgroundColor', bgcolor, ... + 'units','normalized', 'Position', posInfo.(fld), 'callback', rpmCb2); +end +guiHandlesSpec2.rpmHarmDd = uicontrol(PSspecfig2, 'Style','popupmenu', ... + 'String', {'All harm.','1st','2nd','3rd','1st & 2nd','1st & 3rd','2nd & 3rd'}, ... + 'fontsize', fontsz, 'Value', 1, 'Visible', 'off', ... + 'units','normalized', 'Position', posInfo.rpmHarmDd, 'callback', rpmCb2); +guiHandlesSpec2.rpmLwDd = uicontrol(PSspecfig2, 'Style','popupmenu', ... + 'String', {'lw 0.5','lw 1','lw 1.5','lw 2'}, ... + 'fontsize', fontsz, 'Value', 3, 'Visible', 'off', ... + 'units','normalized', 'Position', posInfo.rpmLwDd, 'callback', rpmCb2); + +end % ishandle(spec2Crtlpanel) + +% Register CP for fixed-pixel resize +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', 0); +cpI = {}; +cpI{end+1} = struct('h', spec2Crtlpanel, 'type','panel', 'row',0, 'col',0, 'hpx',0); +listH_px = 5*rs_px; termH_px = round(4.3*rs_px); +cpI{end+1} = struct('h', guiHandlesSpec2.FileSelect, 'type','full', 'row',0, 'col',0, 'hpx',listH_px); +cpI{end+1} = struct('h', guiHandlesSpec2.SpecList, 'type','full', 'row',0, 'col',0, 'hpx',termH_px); +cpI{end+1} = struct('h', guiHandlesSpec2.computeSpec, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.resetSpec, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.spectrogramButton2, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.spectrogramButton3, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.rightColMode, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.chirpButton, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.saveFig2, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.saveSettings2, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.smoothFactor_select, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.Delay, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.plotR, 'type','cb', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.plotP, 'type','cb', 'row',0, 'col',1, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.plotY, 'type','cb_end', 'row',0, 'col',2, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.checkboxPSD, 'type','cb', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.RPYcomboSpec, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.climMax1_text, 'type','text_left', 'row',0, 'col',0, 'hpx',rhs_px); +cpI{end+1} = struct('h', guiHandlesSpec2.climMax2_text, 'type','text_right', 'row',0, 'col',0, 'hpx',rhs_px); +cpI{end+1} = struct('h', guiHandlesSpec2.climMax1_input, 'type','input_left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.climMax2_input, 'type','input_right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.rpmMotor1, 'type','quarter1', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.rpmMotor2, 'type','quarter2', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.rpmMotor3, 'type','quarter3', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.rpmMotor4, 'type','quarter4', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.rpmHarmDd, 'type','dd_left', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec2.rpmLwDd, 'type','dd_right', 'row',0, 'col',0, 'hpx',ddh_px); +setappdata(PSspecfig2, 'PSplotGrid', struct('plotL',plotL2, 'colGap',colGap2, ... + 'ncols',2, 'rows',rows, 'rowH',0.25, 'margin',0.04)); +PSregisterResize(PSspecfig2, cpPx, cpI, 'seq'); + +try specSaved_ = [defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-term1'))) defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-term2')))]; + if ~isfield(T{1}, 'gyroPrefilt_0_'), specSaved_(specSaved_ == 2) = []; end + if isempty(specSaved_), specSaved_ = 1; end + set(guiHandlesSpec2.SpecList, 'Value', specSaved_); +catch, set(guiHandlesSpec2.SpecList, 'Value', specDef_), end try set(guiHandlesSpec2.smoothFactor_select, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-smoothing')))), catch, set(guiHandlesSpec2.smoothFactor_select, 'Value', 3), end try set(guiHandlesSpec2.Delay, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-delay')))), catch, set(guiHandlesSpec2.Delay, 'Value', 1), end try set(guiHandlesSpec2.plotR, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-plotR')))), catch, set(guiHandlesSpec2.plotR, 'Value', 1), end @@ -231,117 +278,23 @@ try set(guiHandlesSpec2.plotY, 'Value', defaults.Values(find(strcmp(defaults.Par try set(guiHandlesSpec2.RPYcomboSpec, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'spec2D-SinglePanel')))), catch, set(guiHandlesSpec2.RPYcomboSpec, 'Value', 0), end -FilterDelayDterm={}; -SPGyroDelay=[]; -Debug01={}; -Debug02={}; -gyro_phase_shift_deg=zeros(Nfiles,1); -dterm_phase_shift_deg=zeros(Nfiles,1); -for k = 1 : Nfiles - Fs=1000/A_lograte(k);% yields more consistent results (mode(diff(tta))); - maxlag=round(30000/Fs); %~30ms delay - - - clear d pg g1 g1 s1 g2 s2 g3 s3 - try - pg = smooth(T{k}.debug_0_(tIND{k}),50); - catch - pg = 0; - end - g1 = smooth(T{k}.gyroADC_0_(tIND{k}),50); - s1 = smooth(T{k}.setpoint_0_(tIND{k}),50); - - g2 = smooth(T{k}.gyroADC_1_(tIND{k}),50); - s2 = smooth(T{k}.setpoint_1_(tIND{k}),50); - - g3 = smooth(T{k}.gyroADC_2_(tIND{k}),50); - s3 = smooth(T{k}.setpoint_2_(tIND{k}),50); - - - [c,lags] = xcorr(g1,pg,maxlag); - d = lags(find(c==max(c),1)); - d = d * (Fs / 1000); - if d<.1, Debug01{k} = ' '; else Debug01{k} = num2str(d);end - - [c,lags] = xcorr(s1,pg,maxlag); - d = lags(find(c==max(c),1)); - d = d * (Fs / 1000); - if d<.1, Debug02{k} = ' '; else Debug02{k} = num2str(d);end - - [c,lags] = xcorr(g1,s1,maxlag); - d = lags(find(c==max(c),1)); - d = d * (Fs / 1000); - if d<.1, SPGyroDelay(k,1) = 0; else, SPGyroDelay(k,1) = d; end - - [c,lags] = xcorr(g2,s2,maxlag); - d = lags(find(c==max(c),1)); - d = d * (Fs / 1000); - if d<.1, SPGyroDelay(k,2) = 0; else, SPGyroDelay(k,2) = d; end - - [c,lags] = xcorr(g3,s3,maxlag); - d = lags(find(c==max(c),1)); - d = d * (Fs / 1000); - if d<.1, SPGyroDelay(k,3) = 0; else, SPGyroDelay(k,3) = d; end - - clear d d1 d2 - try - d1 = smooth(T{k}.axisDpf_0_(tIND{k}),50); - d2 = smooth(T{k}.axisD_0_(tIND{k}),50); - [c,lags] = xcorr(d2,d1,maxlag); - d = lags(find(c==max(c))); - d=d * (Fs / 1000); - if d<.1, FilterDelayDterm{k} = ' '; else FilterDelayDterm{k} = num2str(d); end - catch - FilterDelayDterm{k} = ' '; - end - - try - if ~isempty(str2num(Debug01{k})) && SPGyroDelay(k,1) > 0 - [gyro_phase_shift_deg(k,1)] = round(PSphaseShiftDeg(str2num(Debug01{k}), 1000/(SPGyroDelay(k,1)) )); - end - if ~isempty(str2num(FilterDelayDterm{k})) && SPGyroDelay(k,1) > 0 - [dterm_phase_shift_deg(k,1)] = round(PSphaseShiftDeg(str2num(FilterDelayDterm{k}), 1000/(SPGyroDelay(k,1)) )); - end - catch, end - - %%%%%%%%%% extract dynamic notch data for FFT_FREQ overlay %%%%%%%%%% - tmpFFTidx = FFT_FREQ; % global default - if exist('debugIdx','var') && numel(debugIdx) >= k - tmpFFTidx = debugIdx{k}.FFT_FREQ; - end - if exist('debugmode','var') && numel(debugmode) >= k && debugmode(k) == tmpFFTidx - % FFT_FREQ debug field layout depends on BF version - if exist('fwMajor','var') && numel(fwMajor) >= k && fwMajor(k) >= 2025 - % BF 2025.12+: [0]=pre_DN_gyro, [1-3]=notch_Hz - notchData{k} = [T{k}.debug_1_(tIND{k}), T{k}.debug_2_(tIND{k}), T{k}.debug_3_(tIND{k})]; - else - % BF 4.3-4.5: [0-2]=notch_Hz, [3]=pre_DN_gyro - notchData{k} = [T{k}.debug_0_(tIND{k}), T{k}.debug_1_(tIND{k}), T{k}.debug_2_(tIND{k})]; - end - else - notchData{k} = []; - end - - %%%%%%%%%% extract RPM filter data for motor noise overlay %%%%%%%%%% - tmpRPMidx = 46; % default BF 4.x - if exist('debugIdx','var') && numel(debugIdx) >= k - tmpRPMidx = debugIdx{k}.RPM_FILTER; - end - if exist('debugmode','var') && numel(debugmode) >= k && debugmode(k) == tmpRPMidx - % RPM_FILTER: debug[0-3] = motor 1-4 fundamental frequency in Hz - rpmFilterData{k} = [T{k}.debug_0_(tIND{k}), T{k}.debug_1_(tIND{k}), ... - T{k}.debug_2_(tIND{k}), T{k}.debug_3_(tIND{k})]; - else - rpmFilterData{k} = []; - end -end +% Delay/overlay data computed lazily in PSplotSpec2D on "Run" click +if ~exist('FilterDelayDterm','var'), FilterDelayDterm = {}; end +if ~exist('SPGyroDelay','var'), SPGyroDelay = []; end +if ~exist('Debug01','var'), Debug01 = {}; end +if ~exist('Debug02','var'), Debug02 = {}; end +if ~exist('gyro_phase_shift_deg','var'), gyro_phase_shift_deg = zeros(Nfiles,1); end +if ~exist('dterm_phase_shift_deg','var'), dterm_phase_shift_deg = zeros(Nfiles,1); end +if ~exist('notchData','var'), notchData = {}; end +if ~exist('rpmFilterData','var'), rpmFilterData = {}; end +delayDataReady = false; else warndlg('Please select file(s)'); end - +PSstyleControls(PSspecfig2); % functions function selection2(src,event) @@ -356,7 +309,7 @@ function getList2(hObj,event) function textinput_call2(src,eventdata) str=get(src,'String'); - if isempty(str2num(str)) + if isnan(str2double(str)) set(src,'string','0'); warndlg('Input must be numerical'); end diff --git a/src/ui/PSspecUIcontrol.m b/src/ui/PSspecUIcontrol.m index 3571c96..25fdeb6 100644 --- a/src/ui/PSspecUIcontrol.m +++ b/src/ui/PSspecUIcontrol.m @@ -1,294 +1,358 @@ -%% PSspecUIcontrol - ui controls for spectral analyses plots - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if exist('fnameMaster','var') && ~isempty(fnameMaster) - -%%% tooltips -TooltipString_specRun=['Run current spectral configuration',... - newline, 'Warning: Set subsampling dropdown @ or < medium for faster processing.']; -TooltipString_presets=['Choose from a selection of PRESET configurations']; -TooltipString_cmap=['Choose from a selection of colormaps']; -TooltipString_smooth=['Choose amount of smoothing']; -TooltipString_2d=['Show 2 dimensional plots']; -TooltipString_user=['Choose the variable you wish to plot (consider PRESETs dropdown menu above for quick configurations)']; -TooltipString_sub100=['Zoom data to show sub 100Hz details',... - newline, 'Typically used to see propwash or mid-throttle vibration in e.g. Gyro/Pterm/PIDerror']; -TooltipString_phase=['Estimated phase delay based on cross-correlation technique.',... - newline, 'Note: estimate is most reliable with sufficient stick input so as to modulate the gyro and dterm.',... - newline, 'Also requires that betaflight debug_mode is set to ''GYRO_SCALED'' ']; -TooltipString_scale=['Colormap scaling. Note, the default is set such that an optimally filtered gyro ',... - newline, 'should show little to no activity with the exception of a sub 100Hz band across throttle.',... - newline, 'Dterm and motor outputs will typically be noisier, so sometimes scale adjustments ',... - newline, 'are useful to see details. Otherwise, scaling should be the same when making comparisons']; -TooltipString_controlFreqCutoff=['Hz = Freq cutoff bounds for sub100Hz mean/peak analysis window.',... - newline 'Changing this will move the yellow dashed lines representing this range (only in sub100Hz view).']; - - -%%% - -% define -smat=[];%string -ampmat=[];%spec matrix -amp2d=[];%spec 2d -freq=[];% freq - -% only need to call once to compute extra colormaps -try PScolormap; catch, end -SpecLineCols=[]; -SpecLineCols(:,:,1) = [colorA; colorA; colorA; colorA]; -SpecLineCols(:,:,2) = [colorA; colorA; colorB; colorB]; -SpecLineCols(:,:,3) = [colorA; colorB; colorC; colorD]; - - -clear posInfo.SpecPos -cols=[0.04 0.25 0.46 0.67]; -rows=[0.64 0.35 0.06]; -k=0; -for c=1:4 - for r=1:3 - k=k+1; - posInfo.SpecPos(k,:)=[cols(c) rows(r) 0.175 0.24]; - end -end - - -% Control panel layout (consistent with Log Viewer cpL/cpW) -cpL = .875; cpW = .12; - -posInfo.computeSpec= [cpL+.006 .87 cpW/2-.006 .026]; -posInfo.resetSpec= [cpL+cpW/2 .87 cpW/2-.006 .026]; -posInfo.saveFig1= [cpL+.006 .845 cpW/2-.006 .026]; -posInfo.saveSettings1= [cpL+cpW/2 .845 cpW/2-.006 .026]; - -posInfo.specPresets= [cpL+.003 .805 cpW-.006 .035]; -posInfo.ColormapSelect= [cpL+.003 .775 cpW-.006 .035]; -posInfo.smooth_select = [cpL+.003 .745 cpW-.006 .035]; -posInfo.controlFreqCutoff_text =[cpL+.003 .725 cpW-.006 .025]; -posInfo.controlFreq1Cutoff = [cpL+.005 .700 cpW/2-.005 .025]; -posInfo.controlFreq2Cutoff = [cpL+cpW/2 .700 cpW/2-.005 .025]; -posInfo.checkbox2d= [cpL+.005 .675 cpW/2-.005 .025]; -posInfo.checkboxPSD= [cpL+cpW/2 .675 cpW/2-.005 .025]; -posInfo.checkboxEstRPM= [cpL+.005 .650 cpW-.005 .025]; -posInfo.rpmLegend1= [cpL+.005 .632 cpW-.005 .018]; -posInfo.rpmLegend2= [cpL+.005 .616 cpW-.005 .018]; -posInfo.rpmLegend3= [cpL+.005 .600 cpW-.005 .018]; - -if exist('isOctave','var') && isOctave - % Octave Qt widgets need more vertical space - posInfo.computeSpec= [cpL+.006 .87 cpW/2-.006 .030]; - posInfo.resetSpec= [cpL+cpW/2 .87 cpW/2-.006 .030]; - posInfo.saveFig1= [cpL+.006 .838 cpW/2-.006 .030]; - posInfo.saveSettings1= [cpL+cpW/2 .838 cpW/2-.006 .030]; - posInfo.specPresets= [cpL+.003 .795 cpW-.006 .035]; - posInfo.ColormapSelect= [cpL+.003 .758 cpW-.006 .035]; - posInfo.smooth_select= [cpL+.003 .721 cpW-.006 .035]; - posInfo.controlFreqCutoff_text= [cpL+.003 .700 cpW-.006 .024]; - posInfo.controlFreq1Cutoff= [cpL+.005 .675 cpW/2-.005 .024]; - posInfo.controlFreq2Cutoff= [cpL+cpW/2 .675 cpW/2-.005 .024]; - posInfo.checkbox2d= [cpL+.005 .650 cpW/2-.005 .025]; - posInfo.checkboxPSD= [cpL+cpW/2 .650 cpW/2-.005 .025]; - posInfo.checkboxEstRPM= [cpL+.005 .625 cpW-.005 .025]; - posInfo.rpmLegend1= [cpL+.005 .605 cpW-.005 .020]; - posInfo.rpmLegend2= [cpL+.005 .585 cpW-.005 .020]; - posInfo.rpmLegend3= [cpL+.005 .565 cpW-.005 .020]; -end - -posInfo.AphasedelayText1=[.06 .984 .14 .02]; -posInfo.AphasedelayText2=[.27 .984 .14 .02]; -posInfo.AphasedelayText3=[.48 .984 .14 .02]; -posInfo.AphasedelayText4=[.69 .984 .14 .02]; - -posInfo.hCbar1pos=[0.04 0.89 0.175 0.02]; -posInfo.hCbar2pos=[0.25 0.89 0.175 0.02]; -posInfo.hCbar3pos=[0.46 0.89 0.175 0.02]; -posInfo.hCbar4pos=[0.67 0.89 0.175 0.02]; - -ddh = 0.01; % dropdown height -if exist('isOctave','var') && isOctave, ddh = 0.025; end -posInfo.hDropdn1pos=[0.08 0.97 0.095 ddh]; -posInfo.hDropdn2pos=[0.29 0.97 0.095 ddh]; -posInfo.hDropdn3pos=[0.50 0.97 0.095 ddh]; -posInfo.hDropdn4pos=[0.71 0.97 0.095 ddh]; - -posInfo.fDropdn1pos=[0.08 0.942 0.095 ddh]; -posInfo.fDropdn2pos=[0.29 0.942 0.095 ddh]; -posInfo.fDropdn3pos=[0.50 0.942 0.095 ddh]; -posInfo.fDropdn4pos=[0.71 0.942 0.095 ddh]; - -posInfo.Sub100HzCheck1=[0.175 0.942 .06 .025]; -posInfo.Sub100HzCheck2=[.385 .942 .06 .025]; -posInfo.Sub100HzCheck3=[.595 .942 .06 .025]; -posInfo.Sub100HzCheck4=[.805 .942 .06 .025]; - -posInfo.climMax_text = [.01 .913 .025 .024]; -posInfo.climMax_input = [.01 .888 .025 .024]; -posInfo.climMax_text2 = [.22 .913 .025 .024]; -posInfo.climMax_input2 = [.22 .888 .025 .024]; -posInfo.climMax_text3 = [.43 .913 .025 .024]; -posInfo.climMax_input3 = [.43 .888 .025 .024]; -posInfo.climMax_text4 = [.64 .913 .025 .024]; -posInfo.climMax_input4 = [.64 .888 .025 .024]; -climScale=[0.5 0.5 0.5 0.5; 10 10 10 10]; -Flim1=20; % 3.3333Hz steps -Flim2=60; - -PSspecfig=figure(2); -set(PSspecfig, 'Position', round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)])); -set(PSspecfig, 'NumberTitle', 'off'); -set(PSspecfig, 'Name', ['PIDscope (' PsVersion ') - Frequency x Throttle Spectrogram']); -set(PSspecfig, 'InvertHardcopy', 'off'); -set(PSspecfig,'color',bgcolor); - - -try % datacursormode not available in Octave - dcm_obj2 = datacursormode(PSspecfig); - set(dcm_obj2,'UpdateFcn',@PSdatatip); -end - -specCrtlpanelPos = [cpL .59 cpW .33]; -if exist('isOctave','var') && isOctave - specCrtlpanelPos = [cpL .55 cpW .37]; -end -specCrtlpanel = uipanel('Title','Params','FontSize',fontsz,... - 'BackgroundColor',[.95 .95 .95],... - 'Position',specCrtlpanelPos); - -%%% PRESET CONFIGURATIONS - -% guiHandles.FileNum = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster],'TooltipString', [TooltipString_FileNum],... -% 'fontsize',fontsz, 'units','normalized','Position', [posInfo.fnameASpec],'callback','PSplotSpec;'); - -guiHandlesSpec.specPresets = uicontrol(PSspecfig,'Style','popupmenu','string',{'Presets:'; '1. Gyro prefilt | Gyro | Dterm prefilt | Dterm' ; '2. Gyro prefilt | Gyro | Pterm | Dterm' ; '3. Gyro | Dterm | Set point | PID error' ; '4. A|A|B|B Gyro prefilt | Gyro' ; '5. A|A|B|B Dterm prefilt | Dterm' ; '6. A|B|C|D Gyro prefilt ' ;'7. A|B|C|D Gyro '; '8. A|B|C|D Dterm '; '9. A|B|C|D PID error'},... - 'fontsize',fontsz,'TooltipString', [TooltipString_presets], 'units','normalized','Position', [posInfo.specPresets],'callback',... - ['pv=get(guiHandlesSpec.specPresets,''Value''); ',... - 'if pv==1, set(guiHandlesSpec.SpecSelect{1},''Value'',1); set(guiHandlesSpec.SpecSelect{2},''Value'',1); set(guiHandlesSpec.SpecSelect{3},''Value'',1); set(guiHandlesSpec.SpecSelect{4},''Value'',1); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==2, set(guiHandlesSpec.SpecSelect{1},''Value'',3); set(guiHandlesSpec.SpecSelect{2},''Value'',2); set(guiHandlesSpec.SpecSelect{3},''Value'',8); set(guiHandlesSpec.SpecSelect{4},''Value'',7); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==3, set(guiHandlesSpec.SpecSelect{1},''Value'',3); set(guiHandlesSpec.SpecSelect{2},''Value'',2); set(guiHandlesSpec.SpecSelect{3},''Value'',6); set(guiHandlesSpec.SpecSelect{4},''Value'',7); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==4, set(guiHandlesSpec.SpecSelect{1},''Value'',2); set(guiHandlesSpec.SpecSelect{2},''Value'',7); set(guiHandlesSpec.SpecSelect{3},''Value'',5); set(guiHandlesSpec.SpecSelect{4},''Value'',4); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',1); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',1); end;',... - 'if pv==5, set(guiHandlesSpec.SpecSelect{1},''Value'',3); set(guiHandlesSpec.SpecSelect{2},''Value'',2); set(guiHandlesSpec.SpecSelect{3},''Value'',3); set(guiHandlesSpec.SpecSelect{4},''Value'',2); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==6, set(guiHandlesSpec.SpecSelect{1},''Value'',8); set(guiHandlesSpec.SpecSelect{2},''Value'',7); set(guiHandlesSpec.SpecSelect{3},''Value'',8); set(guiHandlesSpec.SpecSelect{4},''Value'',7); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==7, set(guiHandlesSpec.SpecSelect{1},''Value'',3); set(guiHandlesSpec.SpecSelect{2},''Value'',3); set(guiHandlesSpec.SpecSelect{3},''Value'',3); set(guiHandlesSpec.SpecSelect{4},''Value'',3); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==8, set(guiHandlesSpec.SpecSelect{1},''Value'',2); set(guiHandlesSpec.SpecSelect{2},''Value'',2); set(guiHandlesSpec.SpecSelect{3},''Value'',2); set(guiHandlesSpec.SpecSelect{4},''Value'',2); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==9, set(guiHandlesSpec.SpecSelect{1},''Value'',7); set(guiHandlesSpec.SpecSelect{2},''Value'',7); set(guiHandlesSpec.SpecSelect{3},''Value'',7); set(guiHandlesSpec.SpecSelect{4},''Value'',7); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;',... - 'if pv==10, set(guiHandlesSpec.SpecSelect{1},''Value'',4); set(guiHandlesSpec.SpecSelect{2},''Value'',4); set(guiHandlesSpec.SpecSelect{3},''Value'',4); set(guiHandlesSpec.SpecSelect{4},''Value'',4); set(guiHandlesSpec.Sub100HzCheck{1},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{2},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{3},''Value'',0); set(guiHandlesSpec.Sub100HzCheck{4},''Value'',0); end;']); - -guiHandlesSpec.computeSpec = uicontrol(PSspecfig,'string','Run','fontsize',fontsz,'TooltipString', [TooltipString_specRun],'units','normalized','Position',[posInfo.computeSpec],... - 'callback','PSplotSpec;'); -set(guiHandlesSpec.computeSpec, 'ForegroundColor', colRun); - -guiHandlesSpec.resetSpec = uicontrol(PSspecfig,'string','Reset','fontsize',fontsz,'TooltipString', ['Reset Spectral Tool'],'units','normalized','Position',[posInfo.resetSpec],... - 'callback','delete(findobj(PSspecfig,''Type'',''axes'')); set(guiHandlesSpec.specPresets, ''Value'', 1); PSspecUIcontrol; set(PSspecfig, ''pointer'', ''arrow'');'); -set(guiHandlesSpec.resetSpec, 'ForegroundColor', cautionCol); - -guiHandlesSpec.checkbox2d =uicontrol(PSspecfig,'Style','checkbox','String','2D','fontsize',fontsz,'TooltipString', [TooltipString_2d],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkbox2d],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); - -guiHandlesSpec.checkboxPSD =uicontrol(PSspecfig,'Style','checkbox','String','PSD','fontsize',fontsz,'TooltipString', ['Power Spectral Density'],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkboxPSD],'callback', 'PSplotSpec;'); -set(guiHandlesSpec.checkboxPSD, 'Value', 0); - -guiHandlesSpec.checkboxEstRPM =uicontrol(PSspecfig,'Style','checkbox','String','Est. RPM','fontsize',fontsz,'TooltipString', ['Estimate motor RPM from spectrum (2D heatmap only)'],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkboxEstRPM],'callback','updateSpec=1;PSplotSpec;'); -set(guiHandlesSpec.checkboxEstRPM, 'Value', 0); - -uicontrol(PSspecfig,'style','text','string','--- 1st (fund)','fontsize',fontsz-1,'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',[0 .7 .15],'FontWeight','bold','Position',[posInfo.rpmLegend1]); -uicontrol(PSspecfig,'style','text','string','-- 2nd harm','fontsize',fontsz-1,'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',[.9 .7 0],'FontWeight','bold','Position',[posInfo.rpmLegend2]); -uicontrol(PSspecfig,'style','text','string','... 3rd harm','fontsize',fontsz-1,'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',[.9 .2 0],'FontWeight','bold','Position',[posInfo.rpmLegend3]); - -guiHandlesSpec.controlFreqCutoff_text = uicontrol(PSspecfig,'style','text','string','freq lims Hz','fontsize',fontsz,'TooltipString',[TooltipString_controlFreqCutoff],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.controlFreqCutoff_text]); -guiHandlesSpec.controlFreq1Cutoff = uicontrol(PSspecfig,'style','edit','string',[num2str(round(Flim1))],'fontsize',fontsz,'TooltipString',[TooltipString_controlFreqCutoff],'units','normalized','Position',[posInfo.controlFreq1Cutoff],... - 'callback','@textinput_call2; Flim1=round(str2num(get(guiHandlesSpec.controlFreq1Cutoff, ''String'')));updateSpec=1;PSplotSpec;'); -guiHandlesSpec.controlFreq2Cutoff = uicontrol(PSspecfig,'style','edit','string',[num2str(round(Flim2))],'fontsize',fontsz,'TooltipString',[TooltipString_controlFreqCutoff],'units','normalized','Position',[posInfo.controlFreq2Cutoff],... - 'callback','@textinput_call2; Flim2=round(str2num(get(guiHandlesSpec.controlFreq2Cutoff, ''String'')));updateSpec=1;PSplotSpec;'); - -guiHandlesSpec.saveFig1 = uicontrol(PSspecfig,'string','Save Fig','fontsize',fontsz,'TooltipString',[TooltipString_saveFig],'units','normalized','ForegroundColor',[saveCol],'Position',[posInfo.saveFig1],... - 'callback','set(guiHandlesSpec.saveFig1, ''FontWeight'', ''bold'');PSsaveFig;set(guiHandlesSpec.saveFig1, ''FontWeight'', ''normal'');'); - -guiHandlesSpec.saveSettings1 = uicontrol(PSspecfig,'string','Save Settings','fontsize',fontsz, 'TooltipString',['Save current settings to PIDscope defaults' ], 'units','normalized','Position',[posInfo.saveSettings1],... - 'callback','set(guiHandlesSpec.saveSettings1, ''FontWeight'', ''bold'');PSsaveSettings; set(guiHandlesSpec.saveSettings1, ''FontWeight'', ''normal'');'); -set(guiHandlesSpec.saveSettings1, 'ForegroundColor', saveCol); - -guiHandlesSpec.Sub100HzCheck{1} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck1],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); -guiHandlesSpec.Sub100HzCheck{2} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck2],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); -guiHandlesSpec.Sub100HzCheck{3} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck3],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); -guiHandlesSpec.Sub100HzCheck{4} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck4],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); - -% create string list for SpecSelect -sA={'NONE','Gyro','Gyro prefilt','PID error','Set point','Pterm','Dterm','Dterm prefilt','PIDsum'}; - -guiHandlesSpec.SpecSelect{1} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn1pos]); -guiHandlesSpec.SpecSelect{2} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn2pos]); -guiHandlesSpec.SpecSelect{3} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn3pos]); -guiHandlesSpec.SpecSelect{4} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn4pos]); - -guiHandlesSpec.FileSelect{1} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn1pos]); -guiHandlesSpec.FileSelect{2} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn2pos]); -guiHandlesSpec.FileSelect{3} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn3pos]); -guiHandlesSpec.FileSelect{4} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn4pos]); - -guiHandlesSpec.smoothFactor_select = uicontrol(PSspecfig,'style','popupmenu','string',{'smoothing low' 'smoothing low-med' 'smoothing medium' 'smoothing med-high' 'smoothing high'},'fontsize',fontsz,'TooltipString', [TooltipString_smooth], 'units','normalized','Position',[posInfo.smooth_select],... - 'callback','@selection2;updateSpec=1;PSplotSpec;'); - -guiHandlesSpec.climMax_text = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text]); -guiHandlesSpec.climMax_input = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 1))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 1)=str2num(get(guiHandlesSpec.climMax_input, ''String''));updateSpec=1;PSplotSpec;'); - - guiHandlesSpec.climMax_text2 = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text2]); -guiHandlesSpec.climMax_input2 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 2))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input2],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 2)=str2num(get(guiHandlesSpec.climMax_input2, ''String''));updateSpec=1;PSplotSpec;'); - - guiHandlesSpec.climMax_text3 = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text3]); -guiHandlesSpec.climMax_input3 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 3))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input3],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 3)=str2num(get(guiHandlesSpec.climMax_input3, ''String''));updateSpec=1;PSplotSpec;'); - - guiHandlesSpec.climMax_text4 = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text4]); -guiHandlesSpec.climMax_input4 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 4))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input4],... - 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 4)=str2num(get(guiHandlesSpec.climMax_input4, ''String''));updateSpec=1;PSplotSpec;'); - - guiHandlesSpec.ColormapSelect = uicontrol(PSspecfig,'Style','popupmenu','string',{'viridis','jet','hot','cool','gray','bone','copper','linear-RED','linear-GREY'},... - 'fontsize',fontsz,'TooltipString', [TooltipString_cmap], 'units','normalized','Position',[posInfo.ColormapSelect],'callback','@selection2;updateSpec=1; PSplotSpec;'); -set(guiHandlesSpec.ColormapSelect, 'Value', 3);% jet 2 hot 3 viridis 8 - -try set(guiHandlesSpec.SpecSelect{1}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column1')))), catch, set(guiHandlesSpec.SpecSelect{1}, 'Value', 3); end -try set(guiHandlesSpec.SpecSelect{2}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column2')))), catch, set(guiHandlesSpec.SpecSelect{2}, 'Value', 2); end -try set(guiHandlesSpec.SpecSelect{3}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column3')))), catch, set(guiHandlesSpec.SpecSelect{3}, 'Value', 8); end -try set(guiHandlesSpec.SpecSelect{4}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column4')))), catch, set(guiHandlesSpec.SpecSelect{4}, 'Value', 7); end -try set(guiHandlesSpec.specPresets, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Preset')))), catch, set(guiHandlesSpec.specPresets, 'Value', 1); end -try set(guiHandlesSpec.ColormapSelect, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Colormap')))), catch, set(guiHandlesSpec.ColormapSelect, 'Value', 3); end -try set(guiHandlesSpec.smoothFactor_select, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Smoothing')))), catch, set(guiHandlesSpec.smoothFactor_select, 'Value', 3); end - - -else - warndlg('Please select file(s)'); -end - - -% functions -function selection2(src,event) - val = c.Value; - str = c.String; - str{val}; -end - -function getList2(hObj,event) -v=get(hObj,'value') -end - -function textinput_call2(src,eventdata) -str=get(src,'String'); - if isempty(str2num(str)) - set(src,'string','0'); - warndlg('Input must be numerical'); - end -end - - - - - +%% PSspecUIcontrol - ui controls for spectral analyses plots + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + +%%% tooltips +TooltipString_specRun=['Run current spectral configuration',... + newline, 'Warning: Set subsampling dropdown @ or < medium for faster processing.']; +TooltipString_presets=['Choose from a selection of PRESET configurations']; +TooltipString_cmap=['Choose from a selection of colormaps']; +TooltipString_smooth=['Choose amount of smoothing']; +TooltipString_2d=['Show 2 dimensional plots']; +TooltipString_user=['Choose the variable you wish to plot (consider PRESETs dropdown menu above for quick configurations)']; +TooltipString_sub100=['Zoom data to show sub 100Hz details',... + newline, 'Typically used to see propwash or mid-throttle vibration in e.g. Gyro/Pterm/PIDerror']; +TooltipString_phase=['Estimated phase delay based on cross-correlation technique.',... + newline, 'Note: estimate is most reliable with sufficient stick input so as to modulate the gyro and dterm.',... + newline, 'Also requires that betaflight debug_mode is set to ''GYRO_SCALED'' ']; +TooltipString_scale=['Colormap scaling. Note, the default is set such that an optimally filtered gyro ',... + newline, 'should show little to no activity with the exception of a sub 100Hz band across throttle.',... + newline, 'Dterm and motor outputs will typically be noisier, so sometimes scale adjustments ',... + newline, 'are useful to see details. Otherwise, scaling should be the same when making comparisons']; +TooltipString_controlFreqCutoff=['Hz = Freq cutoff bounds for sub100Hz mean/peak analysis window.',... + newline 'Changing this will move the yellow dashed lines representing this range (only in sub100Hz view).']; + + +%%% + +% define +smat=[];%string +ampmat=[];%spec matrix +amp2d=[];%spec 2d +freq=[];% freq + +% only need to call once to compute extra colormaps +try PScolormap; catch, end +SpecLineCols=[]; +SpecLineCols(:,:,1) = [colorA; colorA; colorA; colorA]; +SpecLineCols(:,:,2) = [colorA; colorA; colorB; colorB]; +SpecLineCols(:,:,3) = [colorA; colorB; colorC; colorD]; + + +tbOff_spec = 40/screensz(4); +clear posInfo.SpecPos +plotR = cpL - 0.04; plotL4 = 0.04; colGap4 = 0.01; +colW4 = (plotR - plotL4 - 3*colGap4) / 4; +cols = plotL4 + (0:3)*(colW4 + colGap4); +rows=[0.64-tbOff_spec 0.38-tbOff_spec 0.12-tbOff_spec]; +k=0; +for c=1:4 + for r=1:3 + k=k+1; + posInfo.SpecPos(k,:)=[cols(c) rows(r) colW4 0.21]; + end +end + + +% Control panel layout — cpL/cpW/rh/rs/ddh/cpM inherited from PIDscope.m (pixel-based) +% yTop tracks where TOP of next element goes; Position Y = yTop - height +gap = rs - rh; fw = cpW-2*cpM; hw = cpW/2-cpM; +tbOff_s1 = 40/screensz(4); +yTop = 1 - tbOff_s1 - cpTitleH - cpMv; +posInfo.computeSpec= [cpL+cpM yTop-rh hw rh]; +posInfo.resetSpec= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.saveFig1= [cpL+cpM yTop-rh hw rh]; +posInfo.saveSettings1= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.specPresets= [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.ColormapSelect= [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.smooth_select = [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.controlFreqCutoff_text =[cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +posInfo.controlFreq1Cutoff = [cpL+cpM yTop-rh hw rh]; +posInfo.controlFreq2Cutoff = [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.checkbox2d= [cpL+cpM yTop-rh hw rh]; +posInfo.checkboxPSD= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +qw = fw/4; +posInfo.rpmMotor1 = [cpL+cpM yTop-rh qw rh]; +posInfo.rpmMotor2 = [cpL+cpM+qw yTop-rh qw rh]; +posInfo.rpmMotor3 = [cpL+cpM+2*qw yTop-rh qw rh]; +posInfo.rpmMotor4 = [cpL+cpM+3*qw yTop-rh qw rh]; yTop=yTop-rh-gap; +posInfo.rpmHarmDd = [cpL+cpM yTop-ddh hw ddh]; +posInfo.rpmLwDd = [cpL+cpW/2 yTop-ddh hw ddh]; yTop=yTop-ddh-gap; +posInfo.rpmDynNotch = [cpL+cpM yTop-rh hw rh]; +posInfo.rpmEstChk = [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; + +posInfo.AphasedelayText1=[cols(1)+0.02 .984-tbOff_spec colW4*0.7 .02]; +posInfo.AphasedelayText2=[cols(2)+0.02 .984-tbOff_spec colW4*0.7 .02]; +posInfo.AphasedelayText3=[cols(3)+0.02 .984-tbOff_spec colW4*0.7 .02]; +posInfo.AphasedelayText4=[cols(4)+0.02 .984-tbOff_spec colW4*0.7 .02]; + +posInfo.hCbar1pos=[cols(1) 0.86-tbOff_spec colW4 0.02]; +posInfo.hCbar2pos=[cols(2) 0.86-tbOff_spec colW4 0.02]; +posInfo.hCbar3pos=[cols(3) 0.86-tbOff_spec colW4 0.02]; +posInfo.hCbar4pos=[cols(4) 0.86-tbOff_spec colW4 0.02]; + +ddh_top = 0.01; +if exist('isOctave','var') && isOctave, ddh_top = 0.025; end +ddW_top = min(0.095, colW4*0.5); +cbOff = ddW_top + 0.005; +posInfo.hDropdn1pos=[cols(1)+0.04 0.97-tbOff_spec ddW_top ddh_top]; +posInfo.hDropdn2pos=[cols(2)+0.04 0.97-tbOff_spec ddW_top ddh_top]; +posInfo.hDropdn3pos=[cols(3)+0.04 0.97-tbOff_spec ddW_top ddh_top]; +posInfo.hDropdn4pos=[cols(4)+0.04 0.97-tbOff_spec ddW_top ddh_top]; + +posInfo.fDropdn1pos=[cols(1)+0.04 0.942-tbOff_spec ddW_top ddh_top]; +posInfo.fDropdn2pos=[cols(2)+0.04 0.942-tbOff_spec ddW_top ddh_top]; +posInfo.fDropdn3pos=[cols(3)+0.04 0.942-tbOff_spec ddW_top ddh_top]; +posInfo.fDropdn4pos=[cols(4)+0.04 0.942-tbOff_spec ddW_top ddh_top]; + +posInfo.Sub100HzCheck1=[cols(1)+0.04+cbOff 0.942-tbOff_spec .06 .025]; +posInfo.Sub100HzCheck2=[cols(2)+0.04+cbOff .942-tbOff_spec .06 .025]; +posInfo.Sub100HzCheck3=[cols(3)+0.04+cbOff .942-tbOff_spec .06 .025]; +posInfo.Sub100HzCheck4=[cols(4)+0.04+cbOff .942-tbOff_spec .06 .025]; + +posInfo.climMax_text = [cols(1)-0.03 .913-tbOff_spec .025 .024]; +posInfo.climMax_input = [cols(1)-0.03 .888-tbOff_spec .025 .024]; +posInfo.climMax_text2 = [cols(2)-0.03 .913-tbOff_spec .025 .024]; +posInfo.climMax_input2 = [cols(2)-0.03 .888-tbOff_spec .025 .024]; +posInfo.climMax_text3 = [cols(3)-0.03 .913-tbOff_spec .025 .024]; +posInfo.climMax_input3 = [cols(3)-0.03 .888-tbOff_spec .025 .024]; +posInfo.climMax_text4 = [cols(4)-0.03 .913-tbOff_spec .025 .024]; +posInfo.climMax_input4 = [cols(4)-0.03 .888-tbOff_spec .025 .024]; +climScale=[0.5 0.5 0.5 0.5; 10 10 10 10]; +Flim1=20; % 3.3333Hz steps +Flim2=60; + +if exist('PSspecfig','var') && ishandle(PSspecfig) + figure(PSspecfig); +else + PSspecfig=figure(2); + set(PSspecfig, 'Position', round([0 0 screensz(3) screensz(4)])); + try set(PSspecfig, 'WindowState', 'maximized'); catch, end + set(PSspecfig, 'NumberTitle', 'off'); + set(PSspecfig, 'Name', ['PIDscope (' PsVersion ') - Frequency x Throttle Spectrogram']); + set(PSspecfig, 'InvertHardcopy', 'off'); + set(PSspecfig,'color',bgcolor); +end + + +try % datacursormode not available in Octave + dcm_obj2 = datacursormode(PSspecfig); + set(dcm_obj2,'UpdateFcn',@PSdatatip); +end + +spPanelBot = yTop - rhs - cpMv; +if ~exist('specCrtlpanel','var') || ~ishandle(specCrtlpanel) +specCrtlpanel = uipanel('Title','Params','FontSize',fontsz,... + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... + 'Position',[cpL spPanelBot cpW vPos-spPanelBot+cpTitleH]); + +%%% PRESET CONFIGURATIONS + +% guiHandles.FileNum = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster],'TooltipString', [TooltipString_FileNum],... +% 'fontsize',fontsz, 'units','normalized','Position', [posInfo.fnameASpec],'callback','PSplotSpec;'); + +guiHandlesSpec.specPresets = uicontrol(PSspecfig,'Style','popupmenu','string',{'Presets:'; '1. Gyro prefilt | Gyro | Dterm prefilt | Dterm' ; '2. Gyro prefilt | Gyro | Pterm | Dterm' ; '3. Gyro | Dterm | Set point | PID error' ; '4. A|A|B|B Gyro prefilt | Gyro' ; '5. A|A|B|B Dterm prefilt | Dterm' ; '6. A|B|C|D Gyro prefilt ' ;'7. A|B|C|D Gyro '; '8. A|B|C|D Dterm '; '9. A|B|C|D PID error'},... + 'fontsize',fontsz,'TooltipString', [TooltipString_presets], 'units','normalized','Position', [posInfo.specPresets],... + 'callback','PSapplySpecPreset(get(guiHandlesSpec.specPresets,''Value''), guiHandlesSpec);updateSpec=1;PSplotSpec;'); + +guiHandlesSpec.computeSpec = uicontrol(PSspecfig,'string','Run','fontsize',fontsz,'TooltipString', [TooltipString_specRun],'units','normalized','Position',[posInfo.computeSpec],... + 'callback','PSplotSpec;'); +set(guiHandlesSpec.computeSpec, 'ForegroundColor', colRun); + +guiHandlesSpec.resetSpec = uicontrol(PSspecfig,'string','Reset','fontsize',fontsz,'TooltipString', ['Reset Spectral Tool'],'units','normalized','Position',[posInfo.resetSpec],... + 'callback','delete(findobj(PSspecfig,''Type'',''axes'')); set(guiHandlesSpec.specPresets, ''Value'', 1); PSspecUIcontrol; set(PSspecfig, ''pointer'', ''arrow'');'); +set(guiHandlesSpec.resetSpec, 'ForegroundColor', cautionCol); + +guiHandlesSpec.checkbox2d =uicontrol(PSspecfig,'Style','checkbox','String','2D','fontsize',fontsz,'TooltipString', [TooltipString_2d],... + 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkbox2d],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); + +guiHandlesSpec.checkboxPSD =uicontrol(PSspecfig,'Style','checkbox','String','PSD','fontsize',fontsz,'TooltipString', ['Power Spectral Density'],... + 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.checkboxPSD],'callback', 'PSplotSpec;'); +set(guiHandlesSpec.checkboxPSD, 'Value', 0); + +motorCols = PStheme().sigMotor; +% detect motor count and label pairs for hex/octo +nMot_ = 4; +if exist('T','var') && ~isempty(T) + for mi_ = 4:7 + if isfield(T{1}, ['motor_' int2str(mi_) '_']), nMot_ = mi_+1; end + end +end +if nMot_ > 4 + motorNames = {sprintf('M1/%d',nMot_/2+1), sprintf('M2/%d',nMot_/2+2), sprintf('M3/%d',nMot_/2+3), sprintf('M4/%d',nMot_/2+4)}; +else + motorNames = {'M1','M2','M3','M4'}; +end +guiHandlesSpec.nMotors = nMot_; +rpmCb = 'updateSpec=1;PSplotSpec;'; +for mi = 1:4 + fld = sprintf('rpmMotor%d', mi); + guiHandlesSpec.(fld) = uicontrol(PSspecfig, 'Style','checkbox', 'String', motorNames{mi}, ... + 'fontsize', fontsz-1, 'Value', 0, ... + 'ForegroundColor', motorCols{mi}, 'BackgroundColor', bgcolor, ... + 'units','normalized', 'Position', posInfo.(fld), 'callback', rpmCb); +end + +guiHandlesSpec.rpmHarmDd = uicontrol(PSspecfig, 'Style','popupmenu', ... + 'String', {'RPM off','1st','2nd','3rd','1st & 2nd','1st & 3rd','2nd & 3rd','All harm.'}, ... + 'fontsize', fontsz, 'Value', 2, ... + 'units','normalized', 'Position', posInfo.rpmHarmDd, 'callback', rpmCb); + +guiHandlesSpec.rpmLwDd = uicontrol(PSspecfig, 'Style','popupmenu', ... + 'String', {'lw 0.5','lw 1','lw 1.5','lw 2'}, ... + 'fontsize', fontsz, 'Value', 2, ... + 'units','normalized', 'Position', posInfo.rpmLwDd, 'callback', rpmCb); + +guiHandlesSpec.rpmDynNotch = uicontrol(PSspecfig, 'Style','checkbox', 'String','Dyn Notch', ... + 'fontsize', fontsz, 'Value', 0, ... + 'ForegroundColor', th.overlayDynNotch, 'BackgroundColor', bgcolor, ... + 'units','normalized', 'Position', posInfo.rpmDynNotch, 'callback', rpmCb); + +guiHandlesSpec.rpmEstChk = uicontrol(PSspecfig, 'Style','checkbox', 'String','RPM est.', ... + 'fontsize', fontsz, 'Value', 0, ... + 'ForegroundColor', th.overlayRPM, 'BackgroundColor', bgcolor, ... + 'units','normalized', 'Position', posInfo.rpmEstChk, 'callback', rpmCb); + +guiHandlesSpec.controlFreqCutoff_text = uicontrol(PSspecfig,'style','text','string','freq lims Hz','fontsize',fontsz,'TooltipString',[TooltipString_controlFreqCutoff],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.controlFreqCutoff_text]); +guiHandlesSpec.controlFreq1Cutoff = uicontrol(PSspecfig,'style','edit','string',[num2str(round(Flim1))],'fontsize',fontsz,'TooltipString',[TooltipString_controlFreqCutoff],'units','normalized','Position',[posInfo.controlFreq1Cutoff],... + 'callback','@textinput_call2; Flim1=round(str2double(get(guiHandlesSpec.controlFreq1Cutoff, ''String'')));updateSpec=1;PSplotSpec;'); +guiHandlesSpec.controlFreq2Cutoff = uicontrol(PSspecfig,'style','edit','string',[num2str(round(Flim2))],'fontsize',fontsz,'TooltipString',[TooltipString_controlFreqCutoff],'units','normalized','Position',[posInfo.controlFreq2Cutoff],... + 'callback','@textinput_call2; Flim2=round(str2double(get(guiHandlesSpec.controlFreq2Cutoff, ''String'')));updateSpec=1;PSplotSpec;'); + +guiHandlesSpec.saveFig1 = uicontrol(PSspecfig,'string','Save Fig','fontsize',fontsz,'TooltipString',[TooltipString_saveFig],'units','normalized','ForegroundColor',[saveCol],'Position',[posInfo.saveFig1],... + 'callback','set(guiHandlesSpec.saveFig1, ''FontWeight'', ''bold'');PSsaveFig;set(guiHandlesSpec.saveFig1, ''FontWeight'', ''normal'');'); + +guiHandlesSpec.saveSettings1 = uicontrol(PSspecfig,'string','Save Settings','fontsize',fontsz, 'TooltipString',['Save current settings to PIDscope defaults' ], 'units','normalized','Position',[posInfo.saveSettings1],... + 'callback','set(guiHandlesSpec.saveSettings1, ''FontWeight'', ''bold'');PSsaveSettings; set(guiHandlesSpec.saveSettings1, ''FontWeight'', ''normal'');'); +set(guiHandlesSpec.saveSettings1, 'ForegroundColor', saveCol); + +guiHandlesSpec.Sub100HzCheck{1} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck1],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); +guiHandlesSpec.Sub100HzCheck{2} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck2],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); +guiHandlesSpec.Sub100HzCheck{3} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck3],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); +guiHandlesSpec.Sub100HzCheck{4} =uicontrol(PSspecfig,'Style','checkbox','String','<100Hz','fontsize',fontsz,'TooltipString', [TooltipString_sub100], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Sub100HzCheck4],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), end;updateSpec=1;PSplotSpec;'); + +% create string list for SpecSelect +sA={'NONE','Gyro','Gyro prefilt','PID error','Set point','Pterm','Dterm','Dterm prefilt','PIDsum'}; + +guiHandlesSpec.SpecSelect{1} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn1pos]); +guiHandlesSpec.SpecSelect{2} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn2pos]); +guiHandlesSpec.SpecSelect{3} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn3pos]); +guiHandlesSpec.SpecSelect{4} = uicontrol(PSspecfig,'Style','popupmenu','string',sA, 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.hDropdn4pos]); + +guiHandlesSpec.FileSelect{1} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn1pos]); +guiHandlesSpec.FileSelect{2} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn2pos]); +guiHandlesSpec.FileSelect{3} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn3pos]); +guiHandlesSpec.FileSelect{4} = uicontrol(PSspecfig,'Style','popupmenu','string',[fnameMaster], 'fontsize',fontsz,'TooltipString',[TooltipString_user],'units','normalized','Position', [posInfo.fDropdn4pos]); + +guiHandlesSpec.smoothFactor_select = uicontrol(PSspecfig,'style','popupmenu','string',{'smoothing low' 'smoothing low-med' 'smoothing medium' 'smoothing med-high' 'smoothing high'},'fontsize',fontsz,'TooltipString', [TooltipString_smooth], 'units','normalized','Position',[posInfo.smooth_select],... + 'callback','@selection2;updateSpec=1;PSplotSpec;'); + +guiHandlesSpec.climMax_text = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text]); +guiHandlesSpec.climMax_input = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 1))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input],... + 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 1)=str2double(get(guiHandlesSpec.climMax_input, ''String''));updateSpec=1;PSplotSpec;'); + + guiHandlesSpec.climMax_text2 = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text2]); +guiHandlesSpec.climMax_input2 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 2))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input2],... + 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 2)=str2double(get(guiHandlesSpec.climMax_input2, ''String''));updateSpec=1;PSplotSpec;'); + + guiHandlesSpec.climMax_text3 = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text3]); +guiHandlesSpec.climMax_input3 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 3))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input3],... + 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 3)=str2double(get(guiHandlesSpec.climMax_input3, ''String''));updateSpec=1;PSplotSpec;'); + + guiHandlesSpec.climMax_text4 = uicontrol(PSspecfig,'style','text','string','scale','fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.climMax_text4]); +guiHandlesSpec.climMax_input4 = uicontrol(PSspecfig,'style','edit','string',[num2str(climScale(get(guiHandlesSpec.checkboxPSD, 'Value')+1, 4))],'fontsize',fontsz,'TooltipString',[TooltipString_scale],'units','normalized','Position',[posInfo.climMax_input4],... + 'callback','@textinput_call2; climScale(get(guiHandlesSpec.checkboxPSD, ''Value'')+1, 4)=str2double(get(guiHandlesSpec.climMax_input4, ''String''));updateSpec=1;PSplotSpec;'); + + guiHandlesSpec.ColormapSelect = uicontrol(PSspecfig,'Style','popupmenu','string',{'viridis','jet','hot','cool','gray','bone','copper','linear-RED','linear-GREY'},... + 'fontsize',fontsz,'TooltipString', [TooltipString_cmap], 'units','normalized','Position',[posInfo.ColormapSelect],'callback','@selection2;updateSpec=1; PSplotSpec;'); +set(guiHandlesSpec.ColormapSelect, 'Value', 3);% jet 2 hot 3 viridis 8 +end % ishandle(specCrtlpanel) + +% Register CP for fixed-pixel resize +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', 0); +cpI = {}; +cpI{end+1} = struct('h', specCrtlpanel, 'type','panel', 'row',0, 'col',0, 'hpx',0); +cpI{end+1} = struct('h', guiHandlesSpec.computeSpec, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.resetSpec, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.saveFig1, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.saveSettings1, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.specPresets, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec.ColormapSelect, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec.smoothFactor_select, 'type','dd_full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec.controlFreqCutoff_text, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.controlFreq1Cutoff, 'type','input_left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.controlFreq2Cutoff, 'type','input_right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.checkbox2d, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.checkboxPSD, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmMotor1, 'type','quarter1', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmMotor2, 'type','quarter2', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmMotor3, 'type','quarter3', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmMotor4, 'type','quarter4', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmHarmDd, 'type','dd_left', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmLwDd, 'type','dd_right', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmDynNotch, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesSpec.rpmEstChk, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +setappdata(PSspecfig, 'PSplotGrid', struct('plotL',plotL4, 'colGap',colGap4, ... + 'ncols',4, 'rows',rows, 'rowH',0.21, 'margin',0.04)); +% Per-column top-bar widgets: {handle, col_index, xOffset_from_col_start} +perColItems = {}; +for pci_k = 1:4 + perColItems{end+1} = {guiHandlesSpec.SpecSelect{pci_k}, pci_k, 0.04}; + perColItems{end+1} = {guiHandlesSpec.FileSelect{pci_k}, pci_k, 0.04}; + perColItems{end+1} = {guiHandlesSpec.Sub100HzCheck{pci_k}, pci_k, cbOff+0.04}; +end +climTextH = {guiHandlesSpec.climMax_text, guiHandlesSpec.climMax_text2, guiHandlesSpec.climMax_text3, guiHandlesSpec.climMax_text4}; +climInputH = {guiHandlesSpec.climMax_input, guiHandlesSpec.climMax_input2, guiHandlesSpec.climMax_input3, guiHandlesSpec.climMax_input4}; +for pci_k = 1:4 + perColItems{end+1} = {climTextH{pci_k}, pci_k, -0.03}; + perColItems{end+1} = {climInputH{pci_k}, pci_k, -0.03}; +end +setappdata(PSspecfig, 'PSperColItems', perColItems); +PSregisterResize(PSspecfig, cpPx, cpI, 'seq'); + +try set(guiHandlesSpec.SpecSelect{1}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column1')))), catch, set(guiHandlesSpec.SpecSelect{1}, 'Value', 3); end +try set(guiHandlesSpec.SpecSelect{2}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column2')))), catch, set(guiHandlesSpec.SpecSelect{2}, 'Value', 2); end +try set(guiHandlesSpec.SpecSelect{3}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column3')))), catch, set(guiHandlesSpec.SpecSelect{3}, 'Value', 8); end +try set(guiHandlesSpec.SpecSelect{4}, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Column4')))), catch, set(guiHandlesSpec.SpecSelect{4}, 'Value', 7); end +try set(guiHandlesSpec.specPresets, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Preset')))), catch, set(guiHandlesSpec.specPresets, 'Value', 1); end +try set(guiHandlesSpec.ColormapSelect, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Colormap')))), catch, set(guiHandlesSpec.ColormapSelect, 'Value', 3); end +try set(guiHandlesSpec.smoothFactor_select, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'FreqXthr-Smoothing')))), catch, set(guiHandlesSpec.smoothFactor_select, 'Value', 3); end + + +else + warndlg('Please select file(s)'); +end +PSstyleControls(PSspecfig); + +% functions +function selection2(src,event) + val = c.Value; + str = c.String; + str{val}; +end + +function getList2(hObj,event) +v=get(hObj,'value') +end + +function textinput_call2(src,eventdata) +str=get(src,'String'); + if isnan(str2double(str)) + set(src,'string','0'); + warndlg('Input must be numerical'); + end +end + + + + + diff --git a/src/ui/PSstatsUIcontrol.m b/src/ui/PSstatsUIcontrol.m index b1b209c..c0262ed 100644 --- a/src/ui/PSstatsUIcontrol.m +++ b/src/ui/PSstatsUIcontrol.m @@ -1,123 +1,158 @@ -%% PSplotStats - UI control for flight statistics - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if ~isempty(filenameA) || ~isempty(filenameB) - -PSstatsfig=figure(6); -set(PSstatsfig, 'Position', round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)])); -set(PSstatsfig, 'NumberTitle', 'off'); -set(PSstatsfig, 'Name', ['PIDscope (' PsVersion ') - Flight stats']); -set(PSstatsfig, 'InvertHardcopy', 'off'); -set(PSstatsfig,'color',bgcolor) - -TooltipString_degsecStick=['Plots rate curve (Histograms Figs) in terms of degs per sec per stick-travel units, or how fast one''s rates change across stick travel ']; -TooltipString_crossAxesStats=['Selects from several plotting options, from basic histograms of stick use per flight, to means',... - newline 'and various between-axes representations.',... - newline ' ',... - newline 'Histograms:',... - newline 'Basic descriptive stats of the flight behavior.',... - newline ' ',... - newline 'Mean +/-SD:',... - newline 'Bars represent the mean/average and lines represent the standard deviation (a measure of variability), ||=absolute value or unsigned average.',... - newline 'Note: you can select out portions of the data to be examined if desired, by adjusting the selection window in the log viewer.',... - newline ' ',... - newline 'Topographic Mode1/2 plots:',... - newline 'The plots show the topographic view (like looking down at your radio) of how the sticks were moved during the flight',... - newline 'Line color represents throttle acceleration in red and deceleration in blue, with higher values more saturated.',... - newline 'If throttle is neither accelerating nor decelerating the line color is white.',... - newline 'Note: you can select out portions of the data to be examined if desired, by adjusting the selection window in the log viewer, and refreshing the Flight stats tool.',... - newline ' ',... - newline 'Axes x Throttle plots:',... - newline 'Like the mode plots, line color represents throttle acceleration in red and deceleration in blue, with higher values more saturated.',... - newline 'If throttle is neither accelerating nor decelerating the line color is white.',... - newline 'The ''Mode and axes X throttle topography plots'' are useful for examining patterns in flight behavior,',... - newline 'For example, flight behaviour associated with optimal lap times in a race, or the qualities or asymmetries in one''s flying style ',... - newline 'Note: you can select out portions of the data to be examined if desired, by adjusting the selection window in the log viewer.']; -TooltipString_statScale=['For ''Mode and axes X throttle topography plots'':',... - newline 'Scale: Z-axis (line color) scale, from 1 to infinity, with higher numbers yielding a wider scale,',... - newline 'giving greater distinction between lines, where only the fastest movements (highest acceleration/deceleration) become visible.']; -TooltipString_statAlpha=['For ''Mode and axes X throttle topography plots'':',... - newline 'Alpha: line transparency, from 0 (fully transparent) to 1 (not transparent)']; -updateStats=0; - -zScale=1; -zTransparency=1; - -PSstatsfig_pos = get(PSstatsfig, 'Position'); -screensz_tmp = get(0,'ScreenSize'); if PSstatsfig_pos(3) > 10, PSstatsfig_pos(3:4) = PSstatsfig_pos(3:4) ./ screensz_tmp(3:4); end -prop_max_screen=(max([PSstatsfig_pos(3) PSstatsfig_pos(4)])); -fontsz5=round(screensz_multiplier*prop_max_screen); - -clear posInfo.statsPos -cols=[0.06 0.54]; -rows=[0.75 0.52 0.29 0.06]; -k=0; -for c=1:2 - for r=1:4 - k=k+1; - posInfo.statsPos(k,:)=[cols(c) rows(r) 0.39 0.18]; - end -end - -posInfo.saveFig5=[.065 .945 .06 .04]; -posInfo.refresh3=[.135 .945 .06 .04]; -posInfo.degsecStick=[.20 .945 .09 .04]; -posInfo.crossAxesStats=[.29 .945 .08 .04]; - -posInfo.crossAxesStats_text = [.385 .965 .03 .03]; -posInfo.crossAxesStats_input = [.385 .945 .03 .03]; -posInfo.crossAxesStats_text2 = [.42 .965 .03 .03]; -posInfo.crossAxesStats_input2 = [.42 .945 .03 .03]; - -statsCrtlpanel = uipanel('Title','','FontSize',fontsz5,... - 'BackgroundColor',[.95 .95 .95],... - 'Position',[.06 .935 .40 .06]); - -guiHandlesStats.saveFig5 = uicontrol(PSstatsfig,'string','Save Fig','fontsize',fontsz5,'TooltipString',[TooltipString_saveFig],'units','normalized','Position',[posInfo.saveFig5],... - 'callback','set(guiHandlesStats.saveFig5, ''FontWeight'', ''bold'');PSsaveFig; set(guiHandlesStats.saveFig5, ''FontWeight'', ''normal'');'); -set(guiHandlesStats.saveFig5, 'BackgroundColor', [.8 .8 .8]); - -guiHandlesStats.refresh = uicontrol(PSstatsfig,'string','Refresh','fontsize',fontsz5,'TooltipString',[TooltipString_refresh],'units','normalized','Position',[posInfo.refresh3],... - 'callback','updateStats=1;PSplotStats;'); -set(guiHandlesStats.refresh, 'BackgroundColor', [1 1 .2]); - -guiHandlesStats.degsecStick =uicontrol(PSstatsfig,'Style','checkbox','String','rate of change','fontsize',fontsz5,'TooltipString',[TooltipString_degsecStick],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.degsecStick],'callback','if (~isempty(filenameA) | ~isempty(filenameB)), end; PSplotStats;'); -guiHandlesStats.crossAxesStats =uicontrol(PSstatsfig,'Style','popupmenu','String',{'Histograms'; 'Mean & Standard Deviation'; 'Mode 1 topography'; 'Mode 2 topography'; 'Axes X Throttle'},'fontsize',fontsz5,'TooltipString',[TooltipString_crossAxesStats],... - 'units','normalized','BackgroundColor',[1 1 1 ],'Position',[posInfo.crossAxesStats],'callback','@selection; if (~isempty(filenameA) | ~isempty(filenameB)), end; PSplotStats;'); -%guiHandlesStats.crossAxesStats.Value=0; - -guiHandlesStats.crossAxesStats_text = uicontrol(PSstatsfig,'style','text','string','scale','fontsize',fontsz5,'TooltipString',[TooltipString_statScale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.crossAxesStats_text]); -guiHandlesStats.crossAxesStats_input = uicontrol(PSstatsfig,'style','edit','string',[num2str(zScale)],'fontsize',fontsz5,'TooltipString',[TooltipString_statScale],'units','normalized','Position',[posInfo.crossAxesStats_input],... - 'callback','@textinput_call4; zScale=str2num(get(guiHandlesStats.crossAxesStats_input, ''String''));updateStats=1;PSplotStats;'); - -guiHandlesStats.crossAxesStats_text2 = uicontrol(PSstatsfig,'style','text','string','alpha','fontsize',fontsz5,'TooltipString',[TooltipString_statAlpha],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.crossAxesStats_text2]); -guiHandlesStats.crossAxesStats_input2 = uicontrol(PSstatsfig,'style','edit','string',[num2str(zTransparency)],'fontsize',fontsz5,'TooltipString',[TooltipString_statAlpha],'units','normalized','Position',[posInfo.crossAxesStats_input2],... - 'callback','@textinput_call4; zTransparency=str2num(get(guiHandlesStats.crossAxesStats_input2, ''String'')); if (zTransparency>1), zTransparency=1; end; if (zTransparency<0), zTransparency=0; end; updateStats=1;PSplotStats;'); - -else - errordlg('Please select file(s) then click ''load+run''', 'Error, no data'); - pause(2); -end - - -function textinput_call4(src,eventdata) -str=get(src,'String'); - if isempty(str2num(str)) - set(src,'string','0'); - warndlg('Input must be numerical'); - end -end - -function selection(src,event) - val = c.Value; - str = c.String; - str{val}; - % disp(['Selection: ' str{val}]); +%% PSplotStats - UI control for flight statistics + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + +if exist('PSstatsfig','var') && ishandle(PSstatsfig) + figure(PSstatsfig); +else + PSstatsfig=figure(6); + set(PSstatsfig, 'Position', round([0 0 screensz(3) screensz(4)])); + try set(PSstatsfig, 'WindowState', 'maximized'); catch, end + set(PSstatsfig, 'NumberTitle', 'off'); + set(PSstatsfig, 'Name', ['PIDscope (' PsVersion ') - Flight stats']); + set(PSstatsfig, 'InvertHardcopy', 'off'); + set(PSstatsfig,'color',bgcolor); +end + +TooltipString_degsecStick=['Plots rate curve (Histograms Figs) in terms of degs per sec per stick-travel units, or how fast one''s rates change across stick travel ']; +TooltipString_crossAxesStats=['Selects from several plotting options, from basic histograms of stick use per flight, to means',... + newline 'and various between-axes representations.',... + newline ' ',... + newline 'Histograms:',... + newline 'Basic descriptive stats of the flight behavior.',... + newline ' ',... + newline 'Mean +/-SD:',... + newline 'Bars represent the mean/average and lines represent the standard deviation (a measure of variability), ||=absolute value or unsigned average.',... + newline 'Note: you can select out portions of the data to be examined if desired, by adjusting the selection window in the log viewer.',... + newline ' ',... + newline 'Topographic Mode1/2 plots:',... + newline 'The plots show the topographic view (like looking down at your radio) of how the sticks were moved during the flight',... + newline 'Line color represents throttle acceleration in red and deceleration in blue, with higher values more saturated.',... + newline 'If throttle is neither accelerating nor decelerating the line color is white.',... + newline 'Note: you can select out portions of the data to be examined if desired, by adjusting the selection window in the log viewer, and refreshing the Flight stats tool.',... + newline ' ',... + newline 'Axes x Throttle plots:',... + newline 'Like the mode plots, line color represents throttle acceleration in red and deceleration in blue, with higher values more saturated.',... + newline 'If throttle is neither accelerating nor decelerating the line color is white.',... + newline 'The ''Mode and axes X throttle topography plots'' are useful for examining patterns in flight behavior,',... + newline 'For example, flight behaviour associated with optimal lap times in a race, or the qualities or asymmetries in one''s flying style ',... + newline 'Note: you can select out portions of the data to be examined if desired, by adjusting the selection window in the log viewer.']; +TooltipString_statScale=['For ''Mode and axes X throttle topography plots'':',... + newline 'Scale: Z-axis (line color) scale, from 1 to infinity, with higher numbers yielding a wider scale,',... + newline 'giving greater distinction between lines, where only the fastest movements (highest acceleration/deceleration) become visible.']; +TooltipString_statAlpha=['For ''Mode and axes X throttle topography plots'':',... + newline 'Alpha: line transparency, from 0 (fully transparent) to 1 (not transparent)']; +updateStats=0; + +zScale=1; +zTransparency=1; + +fontsz5 = fontsz; + +clear posInfo.statsPos +plotR = cpL - 0.04; plotLs = 0.06; colGapS = 0.02; +colWs = (plotR - plotLs - colGapS) / 2; +cols = [plotLs, plotLs + colWs + colGapS]; +rows=[0.69 0.48 0.27 0.06]; +k=0; +for c=1:2 + for r=1:4 + k=k+1; + posInfo.statsPos(k,:)=[cols(c) rows(r) colWs 0.18]; + end +end + +% Top bar layout — pixel-based sizes +topBtnW = 100/screensz(3); topBtnH = rh; topCbW = 150/screensz(3); +topDdW = 140/screensz(3); topEdtW = 50/screensz(3); topTxtW = 50/screensz(3); +topBarL = 0.065; +tbOff = 40/screensz(4); % toolbar offset +topLblY = 1 - tbOff - rhs - cpMv; topBtnY = topLblY - rhs - cpMv; +topX = topBarL + cpM; +posInfo.saveFig5= [topX topBtnY topBtnW topBtnH]; topX=topX+topBtnW+cpM; +posInfo.refresh3= [topX topBtnY topBtnW topBtnH]; topX=topX+topBtnW+cpM; +posInfo.degsecStick= [topX topBtnY topCbW topBtnH]; topX=topX+topCbW+cpM; +posInfo.crossAxesStats=[topX topBtnY topDdW topBtnH]; topX=topX+topDdW+cpM; +posInfo.crossAxesStats_text = [topX topLblY topTxtW rhs]; +posInfo.crossAxesStats_input = [topX topBtnY topEdtW topBtnH]; topX=topX+topEdtW+cpM; +posInfo.crossAxesStats_text2 = [topX topLblY topTxtW rhs]; +posInfo.crossAxesStats_input2 = [topX topBtnY topEdtW topBtnH]; topX=topX+topEdtW+cpM; +topDdW2 = 160/screensz(3); +posInfo.statsFileA = [topX topBtnY topDdW2 ddh]; topX=topX+topDdW2+cpM; +posInfo.statsFileB = [topX topBtnY topDdW2 ddh]; +topPanelW = topX + topDdW2 + cpM - topBarL; + +if ~exist('statsCrtlpanel','var') || ~ishandle(statsCrtlpanel) +statsCrtlpanel = uipanel('Title','','FontSize',fontsz5,... + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... + 'Position',[topBarL topBtnY-cpMv topPanelW 1-tbOff-topBtnY+cpMv]); + +guiHandlesStats.saveFig5 = uicontrol(PSstatsfig,'string','Save Fig','fontsize',fontsz5,'TooltipString',[TooltipString_saveFig],'units','normalized','Position',[posInfo.saveFig5],... + 'callback','PSsaveFig;'); +set(guiHandlesStats.saveFig5, 'ForegroundColor', saveCol); + +guiHandlesStats.refresh = uicontrol(PSstatsfig,'string','Refresh','fontsize',fontsz5,'TooltipString','Refresh plots','units','normalized','Position',[posInfo.refresh3],... + 'callback','updateStats=1;PSplotStats;'); +set(guiHandlesStats.refresh, 'ForegroundColor', colRun); + +guiHandlesStats.degsecStick =uicontrol(PSstatsfig,'Style','checkbox','String','rate of change','fontsize',fontsz5,'TooltipString',[TooltipString_degsecStick],... + 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.degsecStick],'callback','PSplotStats;'); +guiHandlesStats.crossAxesStats =uicontrol(PSstatsfig,'Style','popupmenu','String',{'Histograms'; 'Mean & Standard Deviation'; 'Mode 1 topography'; 'Mode 2 topography'; 'Axes X Throttle'},'fontsize',fontsz5,'TooltipString',[TooltipString_crossAxesStats],... + 'units','normalized','Position',[posInfo.crossAxesStats],'callback','PSplotStats;'); +%guiHandlesStats.crossAxesStats.Value=0; + +guiHandlesStats.crossAxesStats_text = uicontrol(PSstatsfig,'style','text','string','scale','fontsize',fontsz5,'TooltipString',[TooltipString_statScale],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.crossAxesStats_text]); +guiHandlesStats.crossAxesStats_input = uicontrol(PSstatsfig,'style','edit','string',[num2str(zScale)],'fontsize',fontsz5,'TooltipString',[TooltipString_statScale],'units','normalized','Position',[posInfo.crossAxesStats_input],... + 'callback','zScale=str2double(get(guiHandlesStats.crossAxesStats_input, ''String''));updateStats=1;PSplotStats;'); + +guiHandlesStats.crossAxesStats_text2 = uicontrol(PSstatsfig,'style','text','string','alpha','fontsize',fontsz5,'TooltipString',[TooltipString_statAlpha],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.crossAxesStats_text2]); +guiHandlesStats.crossAxesStats_input2 = uicontrol(PSstatsfig,'style','edit','string',[num2str(zTransparency)],'fontsize',fontsz5,'TooltipString',[TooltipString_statAlpha],'units','normalized','Position',[posInfo.crossAxesStats_input2],... + 'callback','zTransparency=str2double(get(guiHandlesStats.crossAxesStats_input2, ''String'')); if (zTransparency>1), zTransparency=1; end; if (zTransparency<0), zTransparency=0; end; updateStats=1;PSplotStats;'); + +guiHandlesStats.FileA = uicontrol(PSstatsfig,'Style','popupmenu','string',[fnameMaster],... + 'fontsize',fontsz5,'TooltipString','File A (red)','units','normalized','Position',[posInfo.statsFileA],... + 'callback','updateStats=0;PSplotStats;'); +set(guiHandlesStats.FileA, 'Value', 1); +if Nfiles > 1 + guiHandlesStats.FileB = uicontrol(PSstatsfig,'Style','popupmenu','string',[fnameMaster],... + 'fontsize',fontsz5,'TooltipString','File B (blue)','units','normalized','Position',[posInfo.statsFileB],... + 'callback','updateStats=0;PSplotStats;'); + set(guiHandlesStats.FileB, 'Value', min(2, Nfiles)); +end +end % ishandle(statsCrtlpanel) + +% Register top bar for fixed-pixel resize +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', 0); +cpI = {}; +cpI{end+1} = struct('h', guiHandlesStats.saveFig5, 'type','btn', 'row',0, 'col',0, 'hpx',0, 'wpx',100); +cpI{end+1} = struct('h', guiHandlesStats.refresh, 'type','btn', 'row',0, 'col',0, 'hpx',0, 'wpx',100); +cpI{end+1} = struct('h', guiHandlesStats.degsecStick, 'type','cb', 'row',0, 'col',0, 'hpx',0, 'wpx',150); +cpI{end+1} = struct('h', guiHandlesStats.crossAxesStats, 'type','dd', 'row',0, 'col',0, 'hpx',0, 'wpx',140); +cpI{end+1} = struct('h', guiHandlesStats.crossAxesStats_text, 'type','lbl', 'row',0, 'col',0, 'hpx',0, 'wpx',50); +cpI{end+1} = struct('h', guiHandlesStats.crossAxesStats_input, 'type','input', 'row',0, 'col',0, 'hpx',0, 'wpx',50); +cpI{end+1} = struct('h', guiHandlesStats.crossAxesStats_text2, 'type','lbl', 'row',0, 'col',0, 'hpx',0, 'wpx',50); +cpI{end+1} = struct('h', guiHandlesStats.crossAxesStats_input2, 'type','input', 'row',0, 'col',0, 'hpx',0, 'wpx',50); +cpI{end+1} = struct('h', guiHandlesStats.FileA, 'type','dd', 'row',0, 'col',0, 'hpx',0, 'wpx',160); +if Nfiles > 1 && isfield(guiHandlesStats, 'FileB') && ishandle(guiHandlesStats.FileB) + cpI{end+1} = struct('h', guiHandlesStats.FileB, 'type','dd', 'row',0, 'col',0, 'hpx',0, 'wpx',160); +end +cpI{end+1} = struct('h', statsCrtlpanel, 'type','panel', 'row',0, 'col',0, 'hpx',0, 'wpx',0); +setappdata(PSstatsfig, 'PSplotGrid', struct('plotL',plotLs, 'colGap',colGapS, ... + 'ncols',2, 'rows',rows, 'rowH',0.18, 'margin',0.04)); +PSregisterResize(PSstatsfig, cpPx, cpI, 'topbar', topBarL); + +PSstyleControls(PSstatsfig); + +else + errordlg('Please select file(s) then click ''load+run''', 'Error, no data'); + pause(2); end \ No newline at end of file diff --git a/src/ui/PStuneUIcontrol.m b/src/ui/PStuneUIcontrol.m index 4d0bafb..8f4f43d 100644 --- a/src/ui/PStuneUIcontrol.m +++ b/src/ui/PStuneUIcontrol.m @@ -1,146 +1,245 @@ -%% PStuneUIcontrol - ui controls for tuning-specific parameters - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -if exist('fnameMaster','var') && ~isempty(fnameMaster) - -PStunefig=figure(4); -set(PStunefig, 'Position', round([.1*screensz(3) .1*screensz(4) .75*screensz(3) .8*screensz(4)])); -set(PStunefig, 'NumberTitle', 'on'); -set(PStunefig, 'Name', ['PIDscope (' PsVersion ') - Step Response Tool']); -set(PStunefig, 'InvertHardcopy', 'off'); -set(PStunefig,'color',bgcolor) - -updateStep=0; - -TooltipString_steprun=['Runs step response analysis.',... - newline, 'Warning: Set subsampling dropdown @ or < medium for faster processing.']; -TooltipString_minRate=['Input the minimum rate of rotation for calculating the step response (lower bound must be > 0 but lower than upper bound).',... - newline, 'Really low values may yield more noisy contributions to the data, whereas higher values limit the total data used.',... - newline, 'The default of 40deg/s should be sufficient in most cases, but if N is low, try setting this to lower']; -TooltipString_maxRate=['Input the maximum rate of rotation for for calculating the step response (upper bound must be greater than lower bound).',... - newline, 'This also marks the lower bound for step resp plots associated with the ''snap maneuvers'' selection.',... - newline, 'The default of 500deg/s is sufficient in most cases']; -TooltipString_FastStepResp=['Plots the step response associated with snap maneuvers, whose lower cutoff is defined by upper deg/s dropdown.',... - newline, 'Note: this requires that the log contains maneuvers > the selected upper deg/s, else the plot is left blank']; -TooltipString_fileListWindowStep=['List of files available. Click to select which files to run']; -TooltipString_clearPlot=['Clears lines from all subplots']; - -fcntSR = 0; - -clear posInfo.TparamsPos -cols=[0.05 0.45 0.58 0.73]; -rows=[0.69 0.395 0.1]; -k=0; -for c=1 : size(cols,2) - for r=1 : size(rows,2) - k=k+1; - if c == 1 - posInfo.TparamsPos(k,:)=[cols(c) rows(r) 0.4 0.245]; - else - posInfo.TparamsPos(k,:)=[cols(c) rows(r) 0.11 0.245]; - end - end -end - -% Control panel layout (consistent with Log Viewer cpL/cpW) -cpL = .875; cpW = .12; -rh = .030; rs = .034; - -posInfo.fileListWindowStep= [cpL+.003 .660 cpW-.006 .24]; -posInfo.run4= [cpL+.006 .625 cpW/2-.006 rh]; -posInfo.clearPlots= [cpL+cpW/2 .625 cpW/2-.006 rh]; -posInfo.saveFig4= [cpL+.006 .591 cpW/2-.006 rh]; -posInfo.saveSettings4= [cpL+cpW/2 .591 cpW/2-.006 rh]; -posInfo.smooth_tuning= [cpL+.003 .557 cpW-.006 rh]; -posInfo.plotR= [cpL+.005 .523 .035 .025]; -posInfo.plotP= [cpL+.04 .523 .035 .025]; -posInfo.plotY= [cpL+.075 .523 .035 .025]; -posInfo.RPYcombo= [cpL+.005 .489 cpW-.01 .025]; -posInfo.Ycorrection= [cpL+.005 .455 cpW-.01 .025]; -posInfo.maxYStepInput= [cpL+.005 .421 cpW/3 .025]; -posInfo.maxYStepTxt= [cpL+cpW/3+.005 .421 cpW/2 .025]; - -guiHandlesTune.tuneCrtlpanel = uipanel('Title','select files (max 10)','FontSize',fontsz,... - 'BackgroundColor',[.95 .95 .95],... - 'Position',[cpL .41 cpW .51]); - -guiHandlesTune.run4 = uicontrol(PStunefig,'string','Run','fontsize',fontsz,'TooltipString',[TooltipString_steprun],'units','normalized','Position',[posInfo.run4],... - 'callback','PStuningParams;'); -set(guiHandlesTune.run4, 'ForegroundColor', colRun); - -guiHandlesTune.fileListWindowStep = uicontrol(PStunefig,'Style','listbox','string',[fnameMaster],'max',10,'min',1,... - 'fontsize',fontsz,'TooltipString', [TooltipString_fileListWindowStep],'units','normalized','Position', [posInfo.fileListWindowStep],'callback','@selection2;'); -set(guiHandlesTune.fileListWindowStep, 'Value', 1); - -guiHandlesTune.saveFig4 = uicontrol(PStunefig,'string','Save Fig','fontsize',fontsz,'TooltipString',[TooltipString_saveFig],'units','normalized','Position',[posInfo.saveFig4],... - 'callback','set(guiHandlesTune.saveFig4, ''FontWeight'', ''bold'');PSsaveFig; set(guiHandlesTune.saveFig4, ''FontWeight'', ''normal'');'); -set(guiHandlesTune.saveFig4, 'ForegroundColor', saveCol); - -guiHandlesTune.saveSettings = uicontrol(PStunefig,'string','Save Settings','fontsize',fontsz, 'TooltipString',['Save current settings to PIDscope defaults' ], 'units','normalized','Position',[posInfo.saveSettings4],... - 'callback','set(guiHandlesTune.saveSettings, ''FontWeight'', ''bold'');PSsaveSettings; set(guiHandlesTune.saveSettings, ''FontWeight'', ''normal'');'); -set(guiHandlesTune.saveSettings, 'ForegroundColor', saveCol); - -guiHandlesTune.plotR =uicontrol(PStunefig,'Style','checkbox','String','R','fontsize',fontsz,'TooltipString', ['Plot Roll '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotR],'callback', 'set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); set(PStunefig, ''pointer'', ''arrow'');'); - -guiHandlesTune.plotP =uicontrol(PStunefig,'Style','checkbox','String','P','fontsize',fontsz,'TooltipString', ['Plot Pitch '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotP],'callback', 'set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); set(PStunefig, ''pointer'', ''arrow'');'); -set(guiHandlesTune.plotP, 'Value', 1); - -guiHandlesTune.plotY =uicontrol(PStunefig,'Style','checkbox','String','Y','fontsize',fontsz,'TooltipString', ['Plot Yaw '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.plotY],'callback', 'set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); set(PStunefig, ''pointer'', ''arrow'');'); -set(guiHandlesTune.plotY, 'Value', 0); - -guiHandlesTune.clearPlots = uicontrol(PStunefig,'string','Reset','fontsize',fontsz,'TooltipString',[TooltipString_clearPlot],'units','normalized','Position',[posInfo.clearPlots],... - 'callback','set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); set(PStunefig, ''pointer'', ''arrow'');'); -set(guiHandlesTune.clearPlots, 'ForegroundColor', cautionCol); - -guiHandlesTune.Ycorrection =uicontrol(PStunefig,'Style','checkbox','String','Y correction','fontsize',fontsz,'TooltipString', ['Y axis offset correction '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Ycorrection],'callback', 'set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); set(PStunefig, ''pointer'', ''arrow''); PStuningParams;'); -set(guiHandlesTune.Ycorrection, 'Value', 0); - -guiHandlesTune.RPYcombo =uicontrol(PStunefig,'Style','checkbox','String','Single Panel','fontsize',fontsz,'TooltipString', ['Plot RPY in same panel '],... - 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.RPYcombo],'callback', 'set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); set(PStunefig, ''pointer'', ''arrow''); PStuningParams;'); -set(guiHandlesTune.RPYcombo, 'Value', 0); - -guiHandlesTune.maxYStepTxt = uicontrol(PStunefig,'style','text','string','Y max ','fontsize',fontsz,'TooltipString', ['Y scale max'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.maxYStepTxt]); -guiHandlesTune.maxYStepInput = uicontrol(PStunefig,'style','edit','string','1.75','fontsize',fontsz,'TooltipString', ['Y scale max'],'units','normalized','Position',[posInfo.maxYStepInput],... - 'callback','@textinput_call3; set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0;PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal'') ;PStuningParams; '); - -guiHandlesTune.smoothFactor_select = uicontrol(PStunefig,'style','popupmenu','string',{'smoothing off' 'smoothing low' 'smoothing medium' 'smoothing high'},'fontsize',fontsz,'TooltipString', ['Smooth the gyro when step response traces are too noisy'], 'units','normalized','Position',[posInfo.smooth_tuning],... - 'callback','@selection2;'); -set(guiHandlesTune.smoothFactor_select, 'Value', 1); - -try set(guiHandlesTune.plotR, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'StepResp-plotR')))), catch, set(guiHandlesTune.plotR, 'Value', 1), end -try set(guiHandlesTune.plotP, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'StepResp-plotP')))), catch, set(guiHandlesTune.plotP, 'Value', 1), end -try set(guiHandlesTune.plotY, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'StepResp-plotY')))), catch, set(guiHandlesTune.plotY, 'Value', 1), end -try set(guiHandlesTune.RPYcombo, 'Value', defaults.Values(find(strcmp(defaults.Parameters, 'StepResp-SinglePanel')))), catch, set(guiHandlesTune.RPYcombo, 'Value', 0), end -try set(guiHandlesTune.maxYStepInput, 'String', num2str(defaults.Values(find(strcmp(defaults.Parameters, 'StepResp-Ymax'))))), catch, end - -else - warndlg('Please select file(s)'); -end - -% functions -function textinput_call3(src,eventdata) -str=get(src,'String'); - if isempty(str2num(str)) - set(src,'string','0'); - warndlg('Input must be numerical'); - end -end - -% functions -function selection2(src,event) - val = c.Value; - str = c.String; - str{val}; - % disp(['Selection: ' str{val}]); -end - +%% PStuneUIcontrol - ui controls for tuning-specific parameters + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +if exist('fnameMaster','var') && ~isempty(fnameMaster) + +th = PStheme(); +if exist('PStunefig','var') && ishandle(PStunefig) + figure(PStunefig); +else + PStunefig=figure(4); + set(PStunefig, 'Position', round([0 0 screensz(3) screensz(4)])); + try set(PStunefig, 'WindowState', 'maximized'); catch, end + set(PStunefig, 'NumberTitle', 'on'); + set(PStunefig, 'Name', ['PIDscope (' PsVersion ') - Step Response Tool']); + set(PStunefig, 'InvertHardcopy', 'off'); + set(PStunefig,'color',bgcolor); +end + +updateStep=0; + +TooltipString_steprun=['Runs step response analysis.',... + newline, 'Warning: Set subsampling dropdown @ or < medium for faster processing.']; +TooltipString_minRate=['Input the minimum rate of rotation for calculating the step response (lower bound must be > 0 but lower than upper bound).',... + newline, 'Really low values may yield more noisy contributions to the data, whereas higher values limit the total data used.',... + newline, 'The default of 40deg/s should be sufficient in most cases, but if N is low, try setting this to lower']; +TooltipString_maxRate=['Input the maximum rate of rotation for for calculating the step response (upper bound must be greater than lower bound).',... + newline, 'This also marks the lower bound for step resp plots associated with the ''snap maneuvers'' selection.',... + newline, 'The default of 500deg/s is sufficient in most cases']; +TooltipString_FastStepResp=['Plots the step response associated with snap maneuvers, whose lower cutoff is defined by upper deg/s dropdown.',... + newline, 'Note: this requires that the log contains maneuvers > the selected upper deg/s, else the plot is left blank']; +TooltipString_fileListWindowStep=['List of files available. Click to select which files to run']; +TooltipString_clearPlot=['Clears lines from all subplots']; + +if ~exist('fcntSR','var'), fcntSR = 0; end + +clear posInfo.TparamsPos +plotR = cpL - 0.02; plotLt = 0.07; colGapT = 0.015; +% Col 1 = step response, Col 2 = PID text, Col 3 = peak, Col 4 = latency +totalW = plotR - plotLt; +colFracs = [0.42, 0.12, 0.23, 0.23]; +usableW = totalW - 3*colGapT; +wCols = usableW * colFracs / sum(colFracs); +cols = zeros(1,4); +cols(1) = plotLt; +for ci = 2:4, cols(ci) = cols(ci-1) + wCols(ci-1) + colGapT; end +rows=[0.69 0.395 0.1]; +k=0; +for c=1:4 + for r=1:3 + k=k+1; + posInfo.TparamsPos(k,:)=[cols(c) rows(r) wCols(c) 0.245]; + end +end + +% Control panel layout — cpL/cpW/rh/rs/ddh/cpM/cbW inherited from PIDscope.m (pixel-based) +% yTop tracks where TOP of next element goes; Position Y = yTop - height +listH_step = 8*rs; gap = rs - rh; fw = cpW-2*cpM; hw = cpW/2-cpM; +tbOff_s4 = 40/screensz(4); +yTop = 1 - tbOff_s4 - cpTitleH - cpMv; +posInfo.fileListWindowStep= [cpL+cpM yTop-listH_step fw listH_step]; yTop=yTop-listH_step-gap; +posInfo.run4= [cpL+cpM yTop-rh hw rh]; +posInfo.clearPlots= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.saveFig4= [cpL+cpM yTop-rh hw rh]; +posInfo.saveSettings4= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.period= [cpL+cpM yTop-rh hw rh]; +posInfo.markup= [cpL+cpW/2 yTop-rh hw rh]; yTop=yTop-rh-gap; +posInfo.smooth_tuning= [cpL+cpM yTop-ddh fw/2-gap ddh]; +posInfo.srLatency= [cpL+cpM+fw/2 yTop-ddh fw/2 ddh]; yTop=yTop-ddh-gap; +posInfo.subsample= [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +w3_ = (fw - 2*gap) / 3; +posInfo.minRateTxt= [cpL+cpM yTop-rhs w3_ rhs]; +posInfo.minRateInput= [cpL+cpM+w3_+gap yTop-rh w3_ rh]; +posInfo.maxRateInput= [cpL+cpM+2*(w3_+gap) yTop-rh w3_ rh]; yTop=yTop-rh-gap; +posInfo.snapManeuver= [cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +posInfo.plotR= [cpL+cpM yTop-rh cbW rh]; +posInfo.plotP= [cpL+cpM+cbW yTop-rh cbW rh]; +posInfo.plotY= [cpL+cpM+2*cbW yTop-rh cbW rh]; yTop=yTop-rh-gap; +posInfo.RPYcombo= [cpL+cpM yTop-rh fw/2-gap rh]; +posInfo.rawTraces= [cpL+cpM+fw/2 yTop-rh fw/2 rh]; yTop=yTop-rh-gap; +posInfo.Ycorrection= [cpL+cpM yTop-rh fw rh]; yTop=yTop-rh-gap; +posInfo.bfSliders= [cpL+cpM yTop-ddh fw ddh]; yTop=yTop-ddh-gap; +posInfo.maxYStepInput= [cpL+cpM yTop-rh cpW/3 rh]; +posInfo.maxYStepTxt= [cpL+cpW/3+cpM yTop-rhs cpW/2 rhs]; + +if ~exist('tuneCrtlpanel_init','var') || ~ishandle(guiHandlesTune.tuneCrtlpanel) +guiHandlesTune.tuneCrtlpanel = uipanel('Title','select files (max 10)','FontSize',fontsz,... + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... + 'Position',[cpL yTop-rh-gap cpW vPos-(yTop-rh-gap)+cpTitleH+cpMv]); + +guiHandlesTune.run4 = uicontrol(PStunefig,'string','Run','fontsize',fontsz,'TooltipString',[TooltipString_steprun],'units','normalized','Position',[posInfo.run4],... + 'callback','updateStep = 0; PStuningParams;'); +set(guiHandlesTune.run4, 'ForegroundColor', colRun); + +guiHandlesTune.fileListWindowStep = uicontrol(PStunefig,'Style','listbox','string',[fnameMaster],'max',10,'min',1,... + 'fontsize',fontsz,'TooltipString', [TooltipString_fileListWindowStep],'units','normalized','Position', [posInfo.fileListWindowStep]); +set(guiHandlesTune.fileListWindowStep, 'Value', 1); + +guiHandlesTune.saveFig4 = uicontrol(PStunefig,'string','Save Fig','fontsize',fontsz,'TooltipString',[TooltipString_saveFig],'units','normalized','Position',[posInfo.saveFig4],... + 'callback','set(guiHandlesTune.saveFig4, ''FontWeight'', ''bold'');PSsaveFig; set(guiHandlesTune.saveFig4, ''FontWeight'', ''normal'');'); +set(guiHandlesTune.saveFig4, 'ForegroundColor', saveCol); + +guiHandlesTune.saveSettings = uicontrol(PStunefig,'string','Save Settings','fontsize',fontsz, 'TooltipString',['Save current settings to PIDscope defaults' ], 'units','normalized','Position',[posInfo.saveSettings4],... + 'callback','set(guiHandlesTune.saveSettings, ''FontWeight'', ''bold'');PSsaveSettings; set(guiHandlesTune.saveSettings, ''FontWeight'', ''normal'');'); +set(guiHandlesTune.saveSettings, 'ForegroundColor', saveCol); + +guiHandlesTune.period = uicontrol(PStunefig,'string','Period','fontsize',fontsz,'TooltipString', 'Click two points to measure period + frequency', 'units','normalized','Position',[posInfo.period],... + 'callback','PSstepPeriod(PStunefig);'); +guiHandlesTune.markup = uicontrol(PStunefig,'string','Markup','fontsize',fontsz,'TooltipString', 'Clear period markers', 'units','normalized','Position',[posInfo.markup],... + 'callback','delete(findobj(PStunefig, ''Tag'', ''PSperiod''));'); + +guiHandlesTune.smoothFactor_select = uicontrol(PStunefig,'style','popupmenu','string',{'smoothin...' 'smooth low' 'smooth med' 'smooth high'},'fontsize',fontsz,'TooltipString', ['Smooth the gyro when step response traces are too noisy'], 'units','normalized','Position',[posInfo.smooth_tuning],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 0; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.smoothFactor_select, 'Value', 1); +guiHandlesTune.srLatency = uicontrol(PStunefig,'style','popupmenu','string',{'SR Latency' 'Xcorr Latency'},'fontsize',fontsz,'TooltipString', 'Latency measurement method', 'units','normalized','Position',[posInfo.srLatency],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); + +guiHandlesTune.subsample = uicontrol(PStunefig,'style','popupmenu','string',{'sub auto' 'sub low (fastest)' 'sub med-low' 'sub medium' 'sub med-high' 'sub high (slowest)'},... + 'fontsize',fontsz,'TooltipString', [TooltipString_steprun], 'units','normalized','Position',[posInfo.subsample],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 0; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.subsample, 'Value', 1); + +guiHandlesTune.minRateTxt = uicontrol(PStunefig,'style','text','string','deg/s','fontsize',fontsz,... + 'TooltipString', [TooltipString_minRate], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.minRateTxt]); +guiHandlesTune.minRateInput = uicontrol(PStunefig,'style','edit','string','40','fontsize',fontsz,... + 'TooltipString', [TooltipString_minRate], 'units','normalized','Position',[posInfo.minRateInput],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 0; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +guiHandlesTune.maxRateInput = uicontrol(PStunefig,'style','edit','string','500','fontsize',fontsz,... + 'TooltipString', [TooltipString_maxRate], 'units','normalized','Position',[posInfo.maxRateInput],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 0; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); + +guiHandlesTune.snapManeuver = uicontrol(PStunefig,'Style','checkbox','String','Snap maneuvers','fontsize',fontsz,... + 'TooltipString', [TooltipString_FastStepResp], 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.snapManeuver],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 0; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.snapManeuver, 'Value', 0); + +guiHandlesTune.plotR =uicontrol(PStunefig,'Style','checkbox','String','R','fontsize',fontsz,'TooltipString', ['Plot Roll '],... + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisRoll,'Position',[posInfo.plotR],'callback', 'delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.plotR, 'Value', 1); + +guiHandlesTune.plotP =uicontrol(PStunefig,'Style','checkbox','String','P','fontsize',fontsz,'TooltipString', ['Plot Pitch '],... + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisPitch,'Position',[posInfo.plotP],'callback', 'delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.plotP, 'Value', 1); + +guiHandlesTune.plotY =uicontrol(PStunefig,'Style','checkbox','String','Y','fontsize',fontsz,'TooltipString', ['Plot Yaw '],... + 'units','normalized','BackgroundColor',bgcolor,'ForegroundColor',th.axisYaw,'Position',[posInfo.plotY],'callback', 'delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.plotY, 'Value', 1); + +guiHandlesTune.clearPlots = uicontrol(PStunefig,'string','Reset','fontsize',fontsz,'TooltipString',[TooltipString_clearPlot],'units','normalized','Position',[posInfo.clearPlots],... + 'callback','set(guiHandlesTune.clearPlots, ''Value'', 1); set(guiHandlesTune.clearPlots, ''FontWeight'', ''bold''); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.clearPlots, 'ForegroundColor', cautionCol); + +guiHandlesTune.Ycorrection =uicontrol(PStunefig,'Style','checkbox','String','Y correction','fontsize',fontsz,'TooltipString', ['Y axis offset correction '],... + 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.Ycorrection],'callback', 'set(guiHandlesTune.clearPlots, ''Value'', 1); fcntSR = 0; PStuningParams; set(guiHandlesTune.clearPlots, ''Value'', 0); set(guiHandlesTune.clearPlots, ''FontWeight'', ''normal''); fcntSR = 0; updateStep = 0; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.Ycorrection, 'Value', 0); + +guiHandlesTune.RPYcombo =uicontrol(PStunefig,'Style','checkbox','String','Single Panel','fontsize',fontsz,'TooltipString', ['Plot RPY in same panel '],... + 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.RPYcombo],'callback', 'delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.RPYcombo, 'Value', 0); + +guiHandlesTune.rawTraces =uicontrol(PStunefig,'Style','checkbox','String','Raw','fontsize',fontsz,'TooltipString', ['Show individual segment traces'],... + 'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.rawTraces],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +set(guiHandlesTune.rawTraces, 'Value', 0); + +guiHandlesTune.bfSliders =uicontrol(PStunefig,'Style','popupmenu','String',{'Peak-Latency' 'BF sliders'},'fontsize',fontsz,'TooltipString', ['Switch between Peak/Latency scatter and BF slider positions'],... + 'units','normalized','Position',[posInfo.bfSliders],... + 'callback','delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); + +guiHandlesTune.maxYStepTxt = uicontrol(PStunefig,'style','text','string','Y max ','fontsize',fontsz,'TooltipString', ['Y scale max'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.maxYStepTxt]); +guiHandlesTune.maxYStepInput = uicontrol(PStunefig,'style','edit','string','1.75','fontsize',fontsz,'TooltipString', ['Y scale max'],'units','normalized','Position',[posInfo.maxYStepInput],... + 'callback','@textinput_call3; delete(findobj(PStunefig,''Type'',''axes'')); fcntSR = 0; updateStep = 1; PStuningParams; set(PStunefig, ''pointer'', ''arrow'');'); +tuneCrtlpanel_init = true; +end % ishandle(tuneCrtlpanel) + +% Register CP for fixed-pixel resize +cpPx = struct('cpW', cpW_px, 'cpM', cpM_px, 'rh', rh_px, 'rs', rs_px, ... + 'ddh', ddh_px, 'cbW', cbW_px, 'rhs', rhs_px, 'cpTitle', cpTitle_px, 'infoH', 0); +cpI = {}; +listH_step_px = 8*rs_px; +cpI{end+1} = struct('h', guiHandlesTune.tuneCrtlpanel, 'type','panel', 'row',0, 'col',0, 'hpx',0); +cpI{end+1} = struct('h', guiHandlesTune.fileListWindowStep, 'type','full', 'row',0, 'col',0, 'hpx',listH_step_px); +cpI{end+1} = struct('h', guiHandlesTune.run4, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.clearPlots, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.saveFig4, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.saveSettings, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.period, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.markup, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.smoothFactor_select, 'type','left', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesTune.srLatency, 'type','right', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesTune.subsample, 'type','full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesTune.minRateTxt, 'type','cb', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.minRateInput, 'type','cb', 'row',0, 'col',1, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.maxRateInput, 'type','cb_end', 'row',0, 'col',2, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.snapManeuver, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.plotR, 'type','cb', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.plotP, 'type','cb', 'row',0, 'col',1, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.plotY, 'type','cb_end', 'row',0, 'col',2, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.RPYcombo, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.rawTraces, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.Ycorrection, 'type','full', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.bfSliders, 'type','full', 'row',0, 'col',0, 'hpx',ddh_px); +cpI{end+1} = struct('h', guiHandlesTune.maxYStepInput, 'type','left', 'row',0, 'col',0, 'hpx',rh_px); +cpI{end+1} = struct('h', guiHandlesTune.maxYStepTxt, 'type','right', 'row',0, 'col',0, 'hpx',rh_px); +setappdata(PStunefig, 'PSplotGrid', struct('plotL',plotLt, 'colGap',colGapT, ... + 'ncols',4, 'rows',rows, 'rowH',0.245, 'margin',0.02, 'colWidthFracs',colFracs)); +PSregisterResize(PStunefig, cpPx, cpI, 'seq'); + +try idx_=find(strcmp(defaults.Parameters,'StepResp-plotR')); if ~isempty(idx_), set(guiHandlesTune.plotR,'Value',defaults.Values(idx_)); end, catch, end +try idx_=find(strcmp(defaults.Parameters,'StepResp-plotP')); if ~isempty(idx_), set(guiHandlesTune.plotP,'Value',defaults.Values(idx_)); end, catch, end +try idx_=find(strcmp(defaults.Parameters,'StepResp-plotY')); if ~isempty(idx_), set(guiHandlesTune.plotY,'Value',defaults.Values(idx_)); end, catch, end +try idx_=find(strcmp(defaults.Parameters,'StepResp-SinglePanel')); if ~isempty(idx_), set(guiHandlesTune.RPYcombo,'Value',defaults.Values(idx_)); end, catch, end +try idx_=find(strcmp(defaults.Parameters,'StepResp-Ymax')); if ~isempty(idx_), set(guiHandlesTune.maxYStepInput,'String',num2str(defaults.Values(idx_))); end, catch, end +try idx_=find(strcmp(defaults.Parameters,'StepResp-Subsample')); if ~isempty(idx_), set(guiHandlesTune.subsample,'Value',defaults.Values(idx_)); end, catch, end +try idx_=find(strcmp(defaults.Parameters,'StepResp-MinRate')); if ~isempty(idx_), set(guiHandlesTune.minRateInput,'String',num2str(defaults.Values(idx_))); end, catch, end +try idx_=find(strcmp(defaults.Parameters,'StepResp-MaxRate')); if ~isempty(idx_), set(guiHandlesTune.maxRateInput,'String',num2str(defaults.Values(idx_))); end, catch, end + +else + warndlg('Please select file(s)'); +end +PSstyleControls(PStunefig); + +% functions +function textinput_call3(src,eventdata) +str=get(src,'String'); + if isnan(str2double(str)) + set(src,'string','0'); + warndlg('Input must be numerical'); + end +end + +% functions +function selection2(src,event) + val = c.Value; + str = c.String; + str{val}; + % disp(['Selection: ' str{val}]); +end + diff --git a/src/ui/PSviewerUIcontrol.m b/src/ui/PSviewerUIcontrol.m index 82beb4e..8384672 100644 --- a/src/ui/PSviewerUIcontrol.m +++ b/src/ui/PSviewerUIcontrol.m @@ -1,130 +1,296 @@ -%% PSviewerUIcontrol - - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - - -posInfo.checkbox0=[.1 .965 .1 .025]; -posInfo.checkbox1=[.1 .94 .1 .025]; -posInfo.checkbox2=[.18 .965 .1 .025]; -posInfo.checkbox3=[.18 .94 .1 .025]; -posInfo.checkbox4=[.26 .965 .1 .025]; -posInfo.checkbox5=[.26 .94 .1 .025]; -posInfo.checkbox6=[.34 .965 .1 .025]; -posInfo.checkbox7=[.34 .94 .1 .025]; -posInfo.checkbox8=[.42 .965 .1 .025]; -posInfo.checkbox9=[.42 .94 .1 .025]; -posInfo.checkbox13=[.50 .965 .06 .025];%m4 -posInfo.checkbox12=[.50 .94 .06 .025];%m3 -posInfo.checkbox11=[.58 .965 .06 .025];%m2 -posInfo.checkbox10=[.58 .94 .06 .025]; %m1 -posInfo.checkbox14=[.66 .965 .06 .025]; -posInfo.checkbox15=[.66 .94 .06 .025]; - -posInfo.maxYtext = [.70 .965 .04 .025]; -posInfo.maxYinput = [.735 .965 .025 .025]; - -posInfo.nCols_text = [.70 .94 .04 .025]; -posInfo.nCols_input = [.735 .94 .025 .025]; - -posInfo.YTstick = [cpL+.005 vPos-0.39 .05 .085]; -posInfo.RPstick = [cpL+cpW/2 vPos-0.39 .05 .085]; - -posInfo.linepos1=[0.095 0.685 0.77 0.21]; -posInfo.linepos2=[0.095 0.47 0.77 0.21]; -posInfo.linepos3=[0.095 0.255 0.77 0.21]; - -posInfo.linepos4=[0.095 0.1 0.77 0.11];% - -fullszPlot = [0.095 0.255 0.77 0.63]; - - -checkpanel = uipanel('Title','','FontSize',fontsz,... - 'BackgroundColor',[.95 .95 .95],... - 'Position',[.096 .932 .68 .065]); - -guiHandles.checkbox0=uicontrol(PSfig,'Style','checkbox','String','Debug','fontsize',fontsz,'ForegroundColor',[linec.col0],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox0],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox1=uicontrol(PSfig,'Style','checkbox','String','Gyro','fontsize',fontsz,'ForegroundColor',[linec.col1],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox1],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox2=uicontrol(PSfig,'Style','checkbox','String','P-term','fontsize',fontsz,'ForegroundColor',[linec.col2],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox2],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox3=uicontrol(PSfig,'Style','checkbox','String','I-term','fontsize',fontsz,'ForegroundColor',[linec.col3],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox3],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox4=uicontrol(PSfig,'Style','checkbox','String','D-term (prefilt)','fontsize',fontsz,'ForegroundColor',[linec.col4],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox4],'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA), PSplotLogViewer; end'); -guiHandles.checkbox5=uicontrol(PSfig,'Style','checkbox','String','D-term','fontsize',fontsz,'ForegroundColor',[linec.col5],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox5],'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA), PSplotLogViewer; end'); -guiHandles.checkbox6=uicontrol(PSfig,'Style','checkbox','String','F-term','fontsize',fontsz,'ForegroundColor',[linec.col6],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox6],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox7=uicontrol(PSfig,'Style','checkbox','String','Set point','fontsize',fontsz,'ForegroundColor',[linec.col7],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox7],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox8=uicontrol(PSfig,'Style','checkbox','String','PID sum','fontsize',fontsz,'ForegroundColor',[linec.col8],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox8],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox9=uicontrol(PSfig,'Style','checkbox','String','PID error','fontsize',fontsz,'ForegroundColor',[linec.col9],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox9],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox10=uicontrol(PSfig,'Style','checkbox','String','Motor 1','fontsize',fontsz,'ForegroundColor',[linec.col10],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox10],'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA), PSplotLogViewer; end'); -guiHandles.checkbox11=uicontrol(PSfig,'Style','checkbox','String','Motor 2','fontsize',fontsz,'ForegroundColor',[linec.col11],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox11],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox12=uicontrol(PSfig,'Style','checkbox','String','Motor 3','fontsize',fontsz,'ForegroundColor',[linec.col12],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox12],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox13=uicontrol(PSfig,'Style','checkbox','String','Motor 4','fontsize',fontsz,'ForegroundColor',[linec.col13],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox13],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); -guiHandles.checkbox14=uicontrol(PSfig,'Style','checkbox','String','Throttle','fontsize',fontsz,'ForegroundColor',[linec.col14],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox14],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); - -set(guiHandles.checkbox1, 'Value', 1); -set(guiHandles.checkbox7, 'Value', 1); -set(guiHandles.checkbox10, 'Value', 1); -set(guiHandles.checkbox11, 'Value', 1); -set(guiHandles.checkbox12, 'Value', 1); -set(guiHandles.checkbox13, 'Value', 1); -set(guiHandles.checkbox14, 'Value', 1); - -guiHandles.checkbox15=uicontrol(PSfig,'Style','checkbox','String','All','fontsize',fontsz,'TooltipString', ['Plot or clear all lines '],'ForegroundColor',[linec.col15],'BackgroundColor',bgcolor,... - 'units','normalized','Position',[posInfo.checkbox15],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), plotall_flag=get(guiHandles.checkbox15, ''Value''); PSplotLogViewer; end'); - -TooltipString_FileNum=['Select the file you wish to plot in the logviewer. ']; -guiHandles.FileNum = uicontrol(PSfig,'Style','popupmenu','string',[fnameMaster],'TooltipString', [TooltipString_FileNum],... - 'fontsize',fontsz, 'units','normalized','Position', [posInfo.fnameAText],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), try set(zoom, ''Enable'',''off''); catch, end, expandON=0; PSplotLogViewer; if exist(''filenameA'',''var'') && ~isempty(filenameA) && get(guiHandles.startEndButton, ''Value''), try, [x y] = ginput(1); epoch1_A(get(guiHandles.FileNum, ''Value'')) = round(x(1)*10)/10; PSplotLogViewer; [x y] = ginput(1); epoch2_A(get(guiHandles.FileNum, ''Value'')) = round(x(1)*10)/10; PSplotLogViewer; catch, end, end, end'); - - -fileIdx = get(guiHandles.FileNum, 'Value'); -if exist('tta','var') && iscell(tta) && numel(tta) >= fileIdx - if numel(epoch1_A) < fileIdx || numel(epoch2_A) < fileIdx - epoch1_A(fileIdx)=tta{fileIdx}(1)/us2sec; - epoch2_A(fileIdx)=tta{fileIdx}(end)/us2sec; - end -end - -% set IND for data subset. Updated in logviewer. -for f = 1 : Nfiles - tIND{f} = tta{f} > (epoch1_A(f)*us2sec) & tta{f} < (epoch2_A(f)*us2sec); -end - -maxY_textToolTip = ['+/- Scaling factor for the Y axis in degs/s']; -guiHandles.maxY_text = uicontrol(PSfig,'style','text','string','y scale','fontsize',fontsz,'TooltipString', [maxY_textToolTip],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.maxYtext]); -guiHandles.maxY_input = uicontrol(PSfig,'style','edit','string',int2str(maxY),'fontsize',fontsz,'TooltipString', [maxY_textToolTip],'units','normalized','Position',[posInfo.maxYinput],... - 'callback','PSplotLogViewer; '); - -guiHandles.nCols_text = uicontrol(PSfig,'style','text','string','N colors','fontsize',fontsz,'TooltipString', ['sets the number of colors for other tools (allowable range 1 - 20)'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.nCols_text]); -guiHandles.nCols_input = uicontrol(PSfig,'style','edit','string',int2str(nLineCols),'fontsize',fontsz,'TooltipString', ['sets the number of colors for other tools (allowable range 1 - 20)'],'units','normalized','Position',[posInfo.nCols_input],... - 'callback','if str2num(get(guiHandles.nCols_input, ''String'')) > 20, set(guiHandles.nCols_input, ''String'', ''20''); end; if str2num(get(guiHandles.nCols_input, ''String'')) < 1, set(guiHandles.nCols_input, ''String'', ''1''); end; multiLineCols=PSlinecmap(str2num(get(guiHandles.nCols_input, ''String''))); '); - -subplot('position',[posInfo.YTstick]); -set(gca, 'xlim', [-500 500], 'ylim', [0 100], 'xticklabel',[], 'yticklabel',[],'xtick',[0], 'ytick',[50], 'xgrid', 'on', 'ygrid', 'on'); -box on -subplot('position',[posInfo.RPstick]) -set(gca, 'xlim', [-500 500], 'ylim', [0 100], 'xticklabel',[], 'yticklabel',[],'xtick',[0], 'ytick',[50], 'xgrid', 'on', 'ygrid', 'on'); -box on - -try set(guiHandles.maxY_input, 'String', num2str(defaults.Values(find(strcmp(defaults.Parameters, 'LogViewer-Ymax'))))), catch, end -try set(guiHandles.nCols_input, 'String', num2str(defaults.Values(find(strcmp(defaults.Parameters, 'LogViewer-Ncolors'))))), catch, end - - +%% PSviewerUIcontrol + + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + + +% Checkbox bar — pixel sizes (constant across resizes) +chkW_px = 130; chkMotW_px = 100; chkEdtW_px = 45; chkTxtW_px = 65; +figPos = get(PSfig, 'Position'); figW = figPos(3); figH = figPos(4); +chkW = chkW_px/figW; chkH = rh; chkMotW = chkMotW_px/figW; +chkEdtW = chkEdtW_px/figW; chkTxtW = chkTxtW_px/figW; +tbOff = 40/figH; +chkRow1 = 1 - tbOff; chkRow2 = chkRow1 - rs; +chkX = 0.10; +posInfo.checkboxGyroPF=[chkX chkRow1 chkW chkH]; +posInfo.checkbox1=[chkX chkRow2 chkW chkH]; chkX=chkX+chkW; +posInfo.checkbox2=[chkX chkRow1 chkW chkH]; +posInfo.checkbox3=[chkX chkRow2 chkW chkH]; chkX=chkX+chkW; +posInfo.checkbox4=[chkX chkRow1 chkW chkH]; +posInfo.checkbox5=[chkX chkRow2 chkW chkH]; chkX=chkX+chkW; +posInfo.checkbox6=[chkX chkRow1 chkW chkH]; +posInfo.checkbox7=[chkX chkRow2 chkW chkH]; chkX=chkX+chkW; +posInfo.checkbox8=[chkX chkRow1 chkW chkH]; +posInfo.checkbox9=[chkX chkRow2 chkW chkH]; chkX=chkX+chkW; +posInfo.checkbox13=[chkX chkRow1 chkMotW chkH]; +posInfo.checkbox12=[chkX chkRow2 chkMotW chkH]; chkX=chkX+chkMotW; +posInfo.checkbox11=[chkX chkRow1 chkMotW chkH]; +posInfo.checkbox10=[chkX chkRow2 chkMotW chkH]; chkX=chkX+chkMotW; +posInfo.checkbox14=[chkX chkRow1 chkMotW chkH]; +posInfo.checkboxTS=[chkX chkRow2 chkMotW chkH]; chkX=chkX+chkMotW; +posInfo.checkbox15=[chkX chkRow1 chkMotW chkH]; +posInfo.checkbox0=[chkX chkRow2 chkMotW chkH]; chkX=chkX+chkMotW; +chkRpmW_px = 80; chkRpmW = chkRpmW_px/figW; +posInfo.checkboxRPM4=[chkX chkRow1 chkRpmW chkH]; +posInfo.checkboxRPM3=[chkX chkRow2 chkRpmW chkH]; chkX=chkX+chkRpmW; +posInfo.checkboxRPM2=[chkX chkRow1 chkRpmW chkH]; +posInfo.checkboxRPM1=[chkX chkRow2 chkRpmW chkH]; chkX=chkX+chkRpmW; + +posInfo.maxYtext = [chkX chkRow1 chkTxtW chkH]; +posInfo.maxYinput = [chkX+chkTxtW chkRow1 chkEdtW chkH]; +posInfo.nCols_text = [chkX chkRow2 chkTxtW chkH]; +posInfo.nCols_input = [chkX+chkTxtW chkRow2 chkEdtW chkH]; + +% Plot positions — right edge stops at CP left edge +plotL = 0.095; plotGap = 0.01; +dynCpL = getappdata(PSfig, 'PScpL'); if isempty(dynCpL), dynCpL = cpL; end +plotW = dynCpL - plotL - plotGap; + +% Stick overlay — directly below Control Panel +stickGap = cpM; +stickW = (cpW - stickGap) / 2; stickH = stickW * 1.3; +try cpBot = get(controlpanel,'Position'); cpBot = cpBot(2); catch, cpBot = vPos - rs*14 - cpMv; end +stickY = max(0.01, cpBot - stickH - cpMv); +posInfo.YTstick = [cpL stickY stickW stickH]; +posInfo.RPstick = [cpL+stickW+stickGap stickY stickW stickH]; +sliderW = dynCpL - 0.0826 - 0.005; +posInfo.slider = [0.0826 chkRow2-2*cpMv-0.02 sliderW 0.02]; +plotTop = posInfo.slider(2) - 0.005; +gapV = 0.005; +linepos4H = 0.11; +plotH = (plotTop - 0.1 - linepos4H - 4*gapV) / 3; +posInfo.linepos1=[plotL plotTop-plotH plotW plotH]; +posInfo.linepos2=[plotL plotTop-2*plotH-gapV plotW plotH]; +posInfo.linepos3=[plotL plotTop-3*plotH-2*gapV plotW plotH]; +posInfo.linepos4=[plotL 0.1 plotW linepos4H]; + +fullszPlot = [plotL posInfo.linepos3(2) plotW plotTop-posInfo.linepos3(2)]; + + +if ~exist('checkpanel','var') || ~ishandle(checkpanel) +chkPanelW = chkX + chkTxtW + chkEdtW + cpM - 0.096; +checkpanel = uipanel('Title','','FontSize',fontsz,... + 'BackgroundColor',panelBg,'ForegroundColor',panelFg,... + 'HighlightColor',panelBorder,... + 'Position',[0.096 chkRow2-cpMv chkPanelW chkRow1+rh+cpMv-chkRow2+cpMv]); + +guiHandles.checkboxGyroPF=uicontrol(PSfig,'Style','checkbox','String','Gyro(pf)','fontsize',fontsz,'ForegroundColor',[linec.colGyroPF],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkboxGyroPF],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox0=uicontrol(PSfig,'Style','checkbox','String','Debug','fontsize',fontsz,'ForegroundColor',[linec.col0],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox0],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox1=uicontrol(PSfig,'Style','checkbox','String','Gyro','fontsize',fontsz,'ForegroundColor',[linec.col1],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox1],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox2=uicontrol(PSfig,'Style','checkbox','String','P-term','fontsize',fontsz,'ForegroundColor',[linec.col2],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox2],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox3=uicontrol(PSfig,'Style','checkbox','String','I-term','fontsize',fontsz,'ForegroundColor',[linec.col3],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox3],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox4=uicontrol(PSfig,'Style','checkbox','String','D-term (prefilt)','fontsize',fontsz,'ForegroundColor',[linec.col4],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox4],'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA), PSplotLogViewer; end'); +guiHandles.checkbox5=uicontrol(PSfig,'Style','checkbox','String','D-term','fontsize',fontsz,'ForegroundColor',[linec.col5],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox5],'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA), PSplotLogViewer; end'); +guiHandles.checkbox6=uicontrol(PSfig,'Style','checkbox','String','F-term','fontsize',fontsz,'ForegroundColor',[linec.col6],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox6],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox7=uicontrol(PSfig,'Style','checkbox','String','Set point','fontsize',fontsz,'ForegroundColor',[linec.col7],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox7],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox8=uicontrol(PSfig,'Style','checkbox','String','PID sum','fontsize',fontsz,'ForegroundColor',[linec.col8],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox8],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox9=uicontrol(PSfig,'Style','checkbox','String','PID error','fontsize',fontsz,'ForegroundColor',[linec.col9],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox9],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox10=uicontrol(PSfig,'Style','checkbox','String','Motor 1','fontsize',fontsz,'ForegroundColor',[linec.col10],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox10],'callback','if exist(''filenameA'',''var'') && ~isempty(filenameA), PSplotLogViewer; end'); +m2lbl='Motor 2'; m3lbl='Motor 3'; m4lbl='Motor 4'; +rfMot = getappdata(PSfig, 'rfMotorCount'); +if ~isempty(rfMot) + si = 1; + if rfMot < 2, m2lbl = ['Servo ' int2str(si)]; si=si+1; end + if rfMot < 3, m3lbl = ['Servo ' int2str(si)]; si=si+1; end + if rfMot < 4, m4lbl = ['Servo ' int2str(si)]; end +end +guiHandles.checkbox11=uicontrol(PSfig,'Style','checkbox','String',m2lbl,'fontsize',fontsz,'ForegroundColor',[linec.col11],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox11],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox12=uicontrol(PSfig,'Style','checkbox','String',m3lbl,'fontsize',fontsz,'ForegroundColor',[linec.col12],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox12],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox13=uicontrol(PSfig,'Style','checkbox','String',m4lbl,'fontsize',fontsz,'ForegroundColor',[linec.col13],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox13],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkbox14=uicontrol(PSfig,'Style','checkbox','String','Throttle','fontsize',fontsz,'ForegroundColor',[linec.col14],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox14],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +guiHandles.checkboxTS=uicontrol(PSfig,'Style','checkbox','String','Test Signal','fontsize',fontsz,'ForegroundColor',th.btnDash5,'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkboxTS],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); + +set(guiHandles.checkbox1, 'Value', 1); +set(guiHandles.checkbox7, 'Value', 1); +set(guiHandles.checkbox10, 'Value', 1); +set(guiHandles.checkbox11, 'Value', 1); +set(guiHandles.checkbox12, 'Value', 1); +set(guiHandles.checkbox13, 'Value', 1); +set(guiHandles.checkbox14, 'Value', 1); + +guiHandles.checkbox15=uicontrol(PSfig,'Style','checkbox','String','All','fontsize',fontsz,'TooltipString', ['Plot or clear all lines '],'ForegroundColor',[linec.col15],'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.checkbox15],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), plotall_flag=get(guiHandles.checkbox15, ''Value''); PSplotLogViewer; end'); +rpmColors_ = th.sigRPM; +nEmLV_ = 0; +if exist('T','var') && ~isempty(T) + for mi_ = 4:7, if isfield(T{1}, ['eRPM_' int2str(mi_) '_']), nEmLV_ = mi_+1; end, end +end +if nEmLV_ > 4 + rpmLabels_ = {sprintf('RPM 1/%d',nEmLV_/2+1), sprintf('RPM 2/%d',nEmLV_/2+2), sprintf('RPM 3/%d',nEmLV_/2+3), sprintf('RPM 4/%d',nEmLV_/2+4)}; +else + rpmLabels_ = {'RPM 1', 'RPM 2', 'RPM 3', 'RPM 4'}; +end +rpmTip_ = 'Motor eRPM trace (Hz) on motor subplot'; +for rk_ = 1:4 + guiHandles.(['checkboxRPM' int2str(rk_)]) = uicontrol(PSfig,'Style','checkbox','String',rpmLabels_{rk_},'fontsize',fontsz,'TooltipString',rpmTip_,'ForegroundColor',rpmColors_{rk_},'BackgroundColor',bgcolor,... + 'units','normalized','Position',[posInfo.(['checkboxRPM' int2str(rk_)])],'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), PSplotLogViewer; end'); +end + +TooltipString_FileNum=['Select the file you wish to plot in the logviewer. ']; +set(guiHandles.FileNum, 'string', fnameMaster, 'TooltipString', TooltipString_FileNum,... + 'callback','if exist(''fnameMaster'',''var'') && ~isempty(fnameMaster), try set(zoom, ''Enable'',''off''); catch, end, expandON=0; PSplotLogViewer; if exist(''filenameA'',''var'') && ~isempty(filenameA) && get(guiHandles.startEndButton, ''Value''), try, [x y] = ginput(1); epoch1_A(get(guiHandles.FileNum, ''Value'')) = round(x(1)*10)/10; PSplotLogViewer; [x y] = ginput(1); epoch2_A(get(guiHandles.FileNum, ''Value'')) = round(x(1)*10)/10; PSplotLogViewer; catch, end, end, end'); +maxY_textToolTip = ['+/- Scaling factor for the Y axis in degs/s']; +guiHandles.maxY_text = uicontrol(PSfig,'style','text','string','y scale','fontsize',fontsz,'TooltipString', [maxY_textToolTip],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.maxYtext]); +guiHandles.maxY_input = uicontrol(PSfig,'style','edit','string',int2str(maxY),'fontsize',fontsz,'TooltipString', [maxY_textToolTip],'units','normalized','Position',[posInfo.maxYinput],... + 'callback','PSplotLogViewer; '); + +guiHandles.nCols_text = uicontrol(PSfig,'style','text','string','N colors','fontsize',fontsz,'TooltipString', ['sets the number of colors for other tools (allowable range 1 - 20)'],'units','normalized','BackgroundColor',bgcolor,'Position',[posInfo.nCols_text]); +guiHandles.nCols_input = uicontrol(PSfig,'style','edit','string',int2str(nLineCols),'fontsize',fontsz,'TooltipString', ['sets the number of colors for other tools (allowable range 1 - 20)'],'units','normalized','Position',[posInfo.nCols_input],... + 'callback','if str2double(get(guiHandles.nCols_input, ''String'')) > 20, set(guiHandles.nCols_input, ''String'', ''20''); end; if str2double(get(guiHandles.nCols_input, ''String'')) < 1, set(guiHandles.nCols_input, ''String'', ''1''); end; multiLineCols=PSlinecmap(str2double(get(guiHandles.nCols_input, ''String''))); '); + +% Register checkbox bar for pixel-based resize +chkBarItems = {}; +chkBarItems{end+1} = struct('h', guiHandles.checkboxGyroPF, 'wpx', chkW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox1, 'wpx', chkW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox2, 'wpx', chkW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox3, 'wpx', chkW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox4, 'wpx', chkW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox5, 'wpx', chkW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox6, 'wpx', chkW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox7, 'wpx', chkW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox8, 'wpx', chkW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox9, 'wpx', chkW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox13, 'wpx', chkMotW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox12, 'wpx', chkMotW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox11, 'wpx', chkMotW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox10, 'wpx', chkMotW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox14, 'wpx', chkMotW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkboxTS, 'wpx', chkMotW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkbox15, 'wpx', chkMotW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkbox0, 'wpx', chkMotW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkboxRPM4, 'wpx', chkRpmW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkboxRPM3, 'wpx', chkRpmW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.checkboxRPM2, 'wpx', chkRpmW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.checkboxRPM1, 'wpx', chkRpmW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.maxY_text, 'wpx', chkTxtW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.nCols_text, 'wpx', chkTxtW_px, 'row', 2, 'advance', true); +chkBarItems{end+1} = struct('h', guiHandles.maxY_input, 'wpx', chkEdtW_px, 'row', 1, 'advance', false); +chkBarItems{end+1} = struct('h', guiHandles.nCols_input, 'wpx', chkEdtW_px, 'row', 2, 'advance', true); +chkBarData = struct('x0', 0.10, 'items', {chkBarItems}, 'panel', checkpanel); +if exist('guiHandles','var') && isfield(guiHandles, 'slider') && ishandle(guiHandles.slider) + chkBarData.slider = guiHandles.slider; +end +setappdata(PSfig, 'PScheckboxBar', chkBarData); + +% Stick overlay — persistent axes + uicontrol labels +th_ = PStheme(); +bCol = th_.axesFg; +guiHandles.stickAxYT = axes('Parent', PSfig, 'Position', posInfo.YTstick, ... + 'xlim',[-500 500], 'ylim',[0 100], ... + 'xtick',[], 'ytick',[], 'Color',th_.axesBg, ... + 'XColor',th_.axesBg, 'YColor',th_.axesBg, ... + 'TickLength',[0 0], 'Box','off'); +line([0 0],[-500 600],'Parent',guiHandles.stickAxYT,'Color',th_.gridColor,'HitTest','off'); +line([-500 500],[50 50],'Parent',guiHandles.stickAxYT,'Color',th_.gridColor,'HitTest','off'); +line([-500 500 500 -500 -500],[0 0 100 100 0],'Parent',guiHandles.stickAxYT, ... + 'Color',bCol,'LineWidth',1,'HitTest','off'); +text(-470, 90, 'T/Y', 'Parent',guiHandles.stickAxYT, 'Color',th_.textSecondary, ... + 'FontSize',max(6,fontsz-2), 'FontWeight','bold', 'HitTest','off'); +guiHandles.stickDotYT = line(NaN,NaN, 'Parent',guiHandles.stickAxYT, ... + 'Marker','o', 'Color',th_.textPrimary, 'MarkerFaceColor',th_.textPrimary, ... + 'MarkerSize',6, 'LineStyle','none', 'HitTest','off'); + +guiHandles.stickAxRP = axes('Parent', PSfig, 'Position', posInfo.RPstick, ... + 'xlim',[-500 500], 'ylim',[-500 500], ... + 'xtick',[], 'ytick',[], 'Color',th_.axesBg, ... + 'XColor',th_.axesBg, 'YColor',th_.axesBg, ... + 'TickLength',[0 0], 'Box','off'); +line([0 0],[-500 500],'Parent',guiHandles.stickAxRP,'Color',th_.gridColor,'HitTest','off'); +line([-500 500],[0 0],'Parent',guiHandles.stickAxRP,'Color',th_.gridColor,'HitTest','off'); +line([-500 500 500 -500 -500],[-500 -500 500 500 -500],'Parent',guiHandles.stickAxRP, ... + 'Color',bCol,'LineWidth',1,'HitTest','off'); +text(-470, 420, 'R/P', 'Parent',guiHandles.stickAxRP, 'Color',th_.textSecondary, ... + 'FontSize',max(6,fontsz-2), 'FontWeight','bold', 'HitTest','off'); +guiHandles.stickDotRP = line(NaN,NaN, 'Parent',guiHandles.stickAxRP, ... + 'Marker','o', 'Color',th_.textPrimary, 'MarkerFaceColor',th_.textPrimary, ... + 'MarkerSize',6, 'LineStyle','none', 'HitTest','off'); + +% Text labels below sticks (uicontrol — exact pixel positioning, no overlap) +oFsz = max(6, fontsz-1); +oH = rhs; halfW = cpW/2; fullW = cpW; +oY = stickY - oH - 2*cpMv; +oBg = th_.figBg; +guiHandles.overlayTime = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',th_.textPrimary,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL oY fullW oH]); +oY = oY - oH; +guiHandles.overlayM4 = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',linec.col13,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL oY halfW oH]); +guiHandles.overlayM1 = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',linec.col10,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL+halfW oY halfW oH]); +oY = oY - oH; +guiHandles.overlayM3 = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',linec.col12,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL oY halfW oH]); +guiHandles.overlayM2 = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',linec.col11,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL+halfW oY halfW oH]); +oY = oY - oH; +guiHandles.overlayGR = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',th_.axisRollFilt,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL oY fullW oH]); +oY = oY - oH; +guiHandles.overlayGP = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',th_.axisPitchFilt,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL oY fullW oH]); +oY = oY - oH; +guiHandles.overlayGY = uicontrol(PSfig,'Style','text','String','','FontSize',oFsz, ... + 'ForegroundColor',th_.axisYawFilt,'BackgroundColor',oBg,'HorizontalAlignment','left', ... + 'units','normalized','Position',[cpL oY fullW oH]); +% Store overlay handles for PSresizeCP +setappdata(PSfig, 'PSoverlay', struct( ... + 'axYT',guiHandles.stickAxYT, 'axRP',guiHandles.stickAxRP, ... + 'time',guiHandles.overlayTime, ... + 'M4',guiHandles.overlayM4, 'M1',guiHandles.overlayM1, ... + 'M3',guiHandles.overlayM3, 'M2',guiHandles.overlayM2, ... + 'GR',guiHandles.overlayGR, 'GP',guiHandles.overlayGP, 'GY',guiHandles.overlayGY)); +end % ishandle(checkpanel) + +% always update file list (fnameMaster may have grown since last call) +set(guiHandles.FileNum, 'string', fnameMaster); + + +fileIdx = get(guiHandles.FileNum, 'Value'); +if exist('tta','var') && iscell(tta) && numel(tta) >= fileIdx + if numel(epoch1_A) < fileIdx || numel(epoch2_A) < fileIdx + epoch1_A(fileIdx)=tta{fileIdx}(1)/us2sec; + epoch2_A(fileIdx)=tta{fileIdx}(end)/us2sec; + end +end + +% set IND for data subset. Updated in logviewer. +if exist('tta','var') && iscell(tta) + for f = 1 : min(Nfiles, numel(tta)) + tIND{f} = tta{f} > (epoch1_A(f)*us2sec) & tta{f} < (epoch2_A(f)*us2sec); + end +end + +try set(guiHandles.maxY_input, 'String', num2str(defaults.Values(find(strcmp(defaults.Parameters, 'LogViewer-Ymax'))))), catch, end +try set(guiHandles.nCols_input, 'String', num2str(defaults.Values(find(strcmp(defaults.Parameters, 'LogViewer-Ncolors'))))), catch, end +PSstyleControls(PSfig); + + diff --git a/src/util/PSapplySpecPreset.m b/src/util/PSapplySpecPreset.m new file mode 100644 index 0000000..8063bd0 --- /dev/null +++ b/src/util/PSapplySpecPreset.m @@ -0,0 +1,20 @@ +function PSapplySpecPreset(pv, guiHandlesSpec) +% Apply Freq x Throttle preset to SpecSelect and Sub100Hz checkboxes +if pv < 2, return; end +switch pv + case 2, vals = [3 2 8 7]; sub = [0 0 0 0]; + case 3, vals = [3 2 6 7]; sub = [0 0 0 0]; + case 4, vals = [2 7 5 4]; sub = [0 0 1 1]; + case 5, vals = [3 2 3 2]; sub = [0 0 0 0]; + case 6, vals = [8 7 8 7]; sub = [0 0 0 0]; + case 7, vals = [3 3 3 3]; sub = [0 0 0 0]; + case 8, vals = [2 2 2 2]; sub = [0 0 0 0]; + case 9, vals = [7 7 7 7]; sub = [0 0 0 0]; + case 10, vals = [4 4 4 4]; sub = [0 0 0 0]; + otherwise, return; +end +for k = 1:4 + set(guiHandlesSpec.SpecSelect{k}, 'Value', vals(k)); + set(guiHandlesSpec.Sub100HzCheck{k}, 'Value', sub(k)); +end +end diff --git a/src/util/PSerrorMessages.m b/src/util/PSerrorMessages.m index 9519630..6348baf 100644 --- a/src/util/PSerrorMessages.m +++ b/src/util/PSerrorMessages.m @@ -1,20 +1,20 @@ -function [errorMessage] = PSerrorMessages(message_string, err) -%% Displays Matlab error messages in a popup so users can report specific errors for development and debugging purposes -% - B. White -% -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- -% - -errorMessage = sprintf(['Error in ' message_string ' \n\n Message:\n%s'], err.message); -% Display pop up message and wait for user to click OK -% Print to command window. -fprintf(1, '%s\n', errorMessage); -uiwait(warndlg(errorMessage)); -%close % in a previous version, the program would close following an error, but this was problematic -end - +function [errorMessage] = PSerrorMessages(message_string, err) +%% Displays Matlab error messages in a popup so users can report specific errors for development and debugging purposes +% - B. White +% +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- +% + +errorMessage = sprintf(['Error in ' message_string ' \n\n Message:\n%s'], err.message); +% Display pop up message and wait for user to click OK +% Print to command window. +fprintf(1, '%s\n', errorMessage); +uiwait(warndlg(errorMessage)); +%close % in a previous version, the program would close following an error, but this was problematic +end + diff --git a/src/util/PSlogViewerPeriod.m b/src/util/PSlogViewerPeriod.m new file mode 100644 index 0000000..423800e --- /dev/null +++ b/src/util/PSlogViewerPeriod.m @@ -0,0 +1,63 @@ +function PSlogViewerPeriod(fig) +%% PSlogViewerPeriod - click two points on log viewer trace, show period + frequency + +th = PStheme(); + +% clear previous markers +delete(findobj(fig, 'Tag', 'PSperiodLV')); + +% find all visible LV axes (PSrpy, PSmotor, PScombo) +allAx = findobj(fig, 'Type', 'axes', 'Visible', 'on'); +lvAx = []; +for ai = 1:numel(allAx) + t = get(allAx(ai), 'Tag'); + if any(strcmp(t, {'PSrpy', 'PSmotor', 'PScombo'})) + lvAx(end+1) = allAx(ai); + end +end +if isempty(lvAx), return; end + +set(fig, 'pointer', 'crosshair'); + +% first click +try ginput(1); catch, set(fig,'pointer','arrow'); return; end +figPt = get(fig, 'CurrentPoint'); +ax = []; +for ai = 1:numel(lvAx) + p = getpixelposition(lvAx(ai)); + if figPt(1) >= p(1) && figPt(1) <= p(1)+p(3) && figPt(2) >= p(2) && figPt(2) <= p(2)+p(4) + ax = lvAx(ai); break; + end +end +if isempty(ax), set(fig,'pointer','arrow'); return; end + +xl = get(ax, 'XLim'); +p = getpixelposition(ax); +x1 = xl(1) + (figPt(1) - p(1)) / p(3) * (xl(2) - xl(1)); + +% second click +try ginput(1); catch, set(fig,'pointer','arrow'); return; end +figPt = get(fig, 'CurrentPoint'); +p = getpixelposition(ax); +x2 = xl(1) + (figPt(1) - p(1)) / p(3) * (xl(2) - xl(1)); + +set(fig, 'pointer', 'arrow'); + +x = sort([x1 x2]); +dt_sec = x(2) - x(1); +if dt_sec <= 0, return; end +dt_ms = dt_sec * 1000; +freq = 1 / dt_sec; + +% draw on all LV axes +for ai = 1:numel(lvAx) + yl_i = get(lvAx(ai), 'YLim'); + line(lvAx(ai), [x(1) x(1)], yl_i, 'Color', th.periodMarker, 'LineWidth', 1.5, 'Tag', 'PSperiodLV'); + line(lvAx(ai), [x(2) x(2)], yl_i, 'Color', th.periodMarker, 'LineWidth', 1.5, 'Tag', 'PSperiodLV'); +end +yl = get(ax, 'YLim'); +text(mean(x), yl(2)*0.93, sprintf('%.1fms, %.2fHz', dt_ms, freq), ... + 'Parent', ax, 'Color', th.textPrimary, 'FontSize', th.fontsz, ... + 'HorizontalAlignment', 'center', 'FontWeight', 'bold', 'Tag', 'PSperiodLV'); + +end diff --git a/src/util/PSparseFilterParams.m b/src/util/PSparseFilterParams.m new file mode 100644 index 0000000..507bd49 --- /dev/null +++ b/src/util/PSparseFilterParams.m @@ -0,0 +1,54 @@ +function fp = PSparseFilterParams(si) +%% PSparseFilterParams - extract BF filter settings from setupInfo headers +% BF enum: 0=PT1, 1=BIQUAD, 2=PT2, 3=PT3 +% Filter disabled when hz == 0 (not by type value) +% Header key names differ: BF 4.3+ uses gyro_lpf1_*, BF 4.2 uses gyro_lowpass_* + +fp.gyro_lpf1_type = hval(si, 'gyro_lpf1_type', hval(si, 'gyro_lowpass_type', 0)); +fp.gyro_lpf1_hz = hval(si, 'gyro_lpf1_static_hz', hval(si, 'gyro_lowpass_hz', hval(si, 'gyro_lowpass_hz_roll', 0))); +fp.gyro_lpf2_type = hval(si, 'gyro_lpf2_type', hval(si, 'gyro_lowpass2_type', 0)); +fp.gyro_lpf2_hz = hval(si, 'gyro_lpf2_static_hz', hval(si, 'gyro_lowpass2_hz', hval(si, 'gyro_lowpass2_hz_roll', 0))); +fp.dterm_lpf1_type = hval(si, 'dterm_lpf1_type', hval(si, 'dterm_lowpass_type', 0)); +fp.dterm_lpf1_hz = hval(si, 'dterm_lpf1_static_hz', hval(si, 'dterm_lowpass_hz', hval(si, 'dterm_lowpass_hz_roll', 0))); +fp.dterm_lpf2_type = hval(si, 'dterm_lpf2_type', hval(si, 'dterm_lowpass2_type', 0)); +fp.dterm_lpf2_hz = hval(si, 'dterm_lpf2_static_hz', hval(si, 'dterm_lowpass2_hz', hval(si, 'dterm_lowpass2_hz_roll', 0))); +fp.dterm_notch_hz = hval(si, 'dterm_notch_hz', 0); +fp.dterm_notch_cut = hval(si, 'dterm_notch_cutoff', 0); +tmp = hstr(si, 'gyro_notch_hz', '0,0'); v = str2double(strsplit(tmp, ',')); +if any(isnan(v)), v = [0 0]; end +fp.gyro_notch1_hz = v(1); +fp.gyro_notch2_hz = 0; if numel(v) > 1, fp.gyro_notch2_hz = v(2); end +tmp = hstr(si, 'gyro_notch_cutoff', '0,0'); v = str2double(strsplit(tmp, ',')); +if any(isnan(v)), v = [0 0]; end +fp.gyro_notch1_cut = v(1); +fp.gyro_notch2_cut = 0; if numel(v) > 1, fp.gyro_notch2_cut = v(2); end + +% Gyro loop rate from headers (filters run at gyro rate, not logging rate) +lt = hval(si, 'looptime', 0); +if lt > 0 + fp.gyro_rate_hz = round(1e6 / lt); +else + fp.gyro_rate_hz = 0; +end +end + +function v = hval(si, key, default) + v = default; + for k = 1:size(si, 1) + if strcmp(strtrim(si{k,1}), key) + tmp = str2double(strtrim(si{k,2})); + if ~isnan(tmp), v = tmp; end + return; + end + end +end + +function s = hstr(si, key, default) + s = default; + for k = 1:size(si, 1) + if strcmp(strtrim(si{k,1}), key) + s = strtrim(si{k,2}); + return; + end + end +end diff --git a/src/util/PSregisterResize.m b/src/util/PSregisterResize.m new file mode 100644 index 0000000..58f9d20 --- /dev/null +++ b/src/util/PSregisterResize.m @@ -0,0 +1,14 @@ +function PSregisterResize(fig, cpPx, cpItems, mode, topBarL) +%% PSregisterResize - register CP items for PSresizeCP callback +cpd = struct('px', cpPx, 'items', {cpItems}, 'mode', mode); +if nargin >= 5 + cpd.topBarL = topBarL; +end +setappdata(fig, 'PScp', cpd); +try set(fig, 'SizeChangedFcn', @PSresizeCP); catch + try set(fig, 'ResizeFcn', @PSresizeCP); catch, end +end +% Ensure figure is rendered at final size before computing layout +drawnow; +PSresizeCP(fig, []); +end diff --git a/src/util/PSresetData.m b/src/util/PSresetData.m new file mode 100644 index 0000000..3616a71 --- /dev/null +++ b/src/util/PSresetData.m @@ -0,0 +1,46 @@ +%% PSresetData - clear all loaded data and reset UI state +% Called from Reset button and firmware-change dialog + +clear T dataA tta A_lograte epoch1_A epoch2_A SetupInfo; +clear rollPIDF pitchPIDF yawPIDF filenameA fnameMaster loaded_firmware; +clear debugmode debugIdx fwType fwMajor fwMinor gyro_debug_axis; +clear notchData rpmFilterData ampmat freq2d2 amp2d2 specMat; +clear delayDataReady FilterDelayDterm SPGyroDelay Debug01 Debug02; +clear gyro_phase_shift_deg dterm_phase_shift_deg; +clear tuneCrtlpanel_init setupInfoWidgets_init; + +fcnt = 0; filenameA = {}; fnameMaster = {}; Nfiles = 0; expandON = 0; +try setappdata(PSfig, 'smoothCacheLV', struct()); catch, end +try setappdata(PSfig, 'rfMotorCount', []); catch, end + +try, delete(checkpanel); clear checkpanel; catch, end +try delete(findobj(PSfig,'Tag','PSrpy')); catch, end +try delete(findobj(PSfig,'Tag','PSmotor')); catch, end +try delete(findobj(PSfig,'Tag','PScombo')); catch, end +% Delete overlay widgets +ov = getappdata(PSfig, 'PSoverlay'); +if ~isempty(ov) + flds = fieldnames(ov); + for fi=1:numel(flds), try delete(ov.(flds{fi})); catch, end; end + setappdata(PSfig, 'PSoverlay', []); +end + +% close all secondary figures +figs = findobj('Type', 'figure'); +for fi = 1:numel(figs) + if figs(fi) ~= PSfig + try, close(figs(fi)); catch, end + end +end + +% clear secondary figure and panel handles +clear PSspecfig PSspecfig2 PSspecfig3 PStunefig PSerrfig PSstatsfig PSdisp; +clear errCrtlpanel statsCrtlpanel spec2Crtlpanel specCrtlpanel; +clear freqTimeCrtlpanel tuneCrtlpanel fcntSR; + +% reset UI +set(guiHandles.FileNum, 'String', ' '); +try + set(guiHandles.Epoch1_A_Input, 'String', ' '); + set(guiHandles.Epoch2_A_Input, 'String', ' '); +catch, end diff --git a/src/util/PSresizeCP.m b/src/util/PSresizeCP.m new file mode 100644 index 0000000..0eedf77 --- /dev/null +++ b/src/util/PSresizeCP.m @@ -0,0 +1,354 @@ +function PSresizeCP(fig, ~) +%% PSresizeCP - resize callback: keeps CP elements at fixed pixel sizes +cpd = getappdata(fig, 'PScp'); +if isempty(cpd), return; end + +figPos = get(fig, 'Position'); +figW = figPos(3); figH = figPos(4); +if figW < 300 || figH < 200, return; end + +px = cpd.px; +cpW = px.cpW / figW; +cpL = 1 - cpW - px.cpM / figW; +setappdata(fig, 'PScpL', cpL); +cpM = px.cpM / figW; +cpMv = px.cpM / figH; +rh = px.rh / figH; +rs = px.rs / figH; +ddh = px.ddh / figH; +cbW = px.cbW / figW; +rhs = px.rhs / figH; +cpTitleH = px.cpTitle / figH; +vPos = 1 - cpTitleH - cpMv; +hw = cpW/2 - cpM; +fw = cpW - 2*cpM; +gap = rs - rh; + +mode = 'rows'; +if isfield(cpd, 'mode'), mode = cpd.mode; end + +if strcmp(mode, 'seq') + % Sequential layout: items placed top-to-bottom, each with its own height + tbOff = 40 / figH; % toolbar offset + vPos = 1 - tbOff - cpTitleH - cpMv; + yTop = vPos; + panelH = 0; panelBot = 0; + for i = 1:numel(cpd.items) + it = cpd.items{i}; + if ~ishandle(it.h), continue; end + if ~strcmp(it.type, 'panel') && strcmp(get(it.h, 'Visible'), 'off'), continue; end + try + hpx = it.hpx; + h = hpx / figH; + switch it.type + case 'full', pos = [cpL+cpM yTop-h fw h]; yTop = yTop-h-gap; + case 'left', pos = [cpL+cpM yTop-h hw h]; + case 'right', pos = [cpL+cpW/2 yTop-h hw h]; yTop = yTop-h-gap; + case 'cb', pos = [cpL+cpM+it.col*cbW yTop-h cbW h]; + case 'cb_end', pos = [cpL+cpM+it.col*cbW yTop-h cbW h]; yTop = yTop-h-gap; + case 'dd_full', pos = [cpL+cpM yTop-h fw h]; yTop = yTop-h-gap; + case 'dd_left', pos = [cpL+cpM yTop-h hw h]; + case 'dd_right',pos = [cpL+cpW/2 yTop-h hw h]; yTop = yTop-h-gap; + case 'text_left', pos = [cpL+cpM yTop-h cpW/4 h]; + case 'text_right', pos = [cpL+cpW/2 yTop-h cpW/4 h]; yTop = yTop-h-gap; + case 'input_left', pos = [cpL+cpM yTop-h cpW/4 h]; + case 'input_right',pos = [cpL+cpW/2 yTop-h cpW/4 h]; yTop = yTop-h-gap; + case 'quarter1', pos = [cpL+cpM yTop-h fw/4 h]; + case 'quarter2', pos = [cpL+cpM+fw/4 yTop-h fw/4 h]; + case 'quarter3', pos = [cpL+cpM+fw/2 yTop-h fw/4 h]; + case 'quarter4', pos = [cpL+cpM+3*fw/4 yTop-h fw/4 h]; yTop = yTop-h-gap; + case 'panel' + continue; + otherwise, continue; + end + set(it.h, 'Position', pos); + catch + end + end + % Panel sized to wrap all items + for i = 1:numel(cpd.items) + it = cpd.items{i}; + if strcmp(it.type, 'panel') && ishandle(it.h) + panelBot = yTop - gap; + panelH = vPos - panelBot + cpTitleH; + set(it.h, 'Position', [cpL panelBot cpW panelH]); + end + end + +elseif strcmp(mode, 'topbar') + % Horizontal top-bar layout (PID Error, Flight Stats, Setup Info) + topBtnH = rh; + tbOff = 40 / figH; % toolbar offset + % Check if any items use 'lbl' type — if not, no label row above buttons + hasLbl = false; + for i = 1:numel(cpd.items), if strcmp(cpd.items{i}.type,'lbl'), hasLbl = true; break; end; end + if hasLbl + topLblY = 1 - tbOff - rhs - cpMv; + topBtnY = topLblY - rhs - cpMv; + else + topBtnY = 1 - tbOff - rh - cpMv; + topLblY = topBtnY; + end + topX = cpd.topBarL + cpM; + for i = 1:numel(cpd.items) + it = cpd.items{i}; + if ~ishandle(it.h), continue; end + try + w = it.wpx / figW; + switch it.type + case 'btn', pos = [topX topBtnY w topBtnH]; topX = topX+w+cpM; + case 'lbl', pos = [topX topLblY w rhs]; + case 'input', pos = [topX topBtnY w topBtnH]; topX = topX+w+cpM; + case 'cb', pos = [topX topBtnY w topBtnH]; topX = topX+w+cpM; + case 'dd', pos = [topX topBtnY w topBtnH]; topX = topX+w+cpM; + case 'panel' + panelW = topX - cpd.topBarL; + topPanelH = 1 - tbOff - topBtnY + cpMv; + pos = [cpd.topBarL topBtnY-cpMv panelW topPanelH]; + otherwise, continue; + end + set(it.h, 'Position', pos); + catch + end + end + +else + % Row-based layout (PIDscope.m main CP) + % Always offset for checkbox bar + slider (present on main window) + tbOff = 40 / figH; + chkRow2 = (1 - tbOff) - rs; + sliderBottom = chkRow2 - 2*cpMv - 0.02; + vPos = sliderBottom - 0.005 - cpTitleH - cpMv; + for i = 1:numel(cpd.items) + it = cpd.items{i}; + if ~ishandle(it.h), continue; end + try + row = it.row; + switch it.type + case 'full' + pos = [cpL+cpM vPos-rs*row fw rh]; + case 'left' + pos = [cpL+cpM vPos-rs*row hw rh]; + case 'right' + pos = [cpL+cpW/2 vPos-rs*row hw rh]; + case 'cb' + pos = [cpL+cpM+it.col*cbW vPos-rs*row cbW rh]; + case 'dd_left' + pos = [cpL+cpM vPos-rs*row hw rh]; + case 'dd_right' + pos = [cpL+cpW/2 vPos-rs*row hw rh]; + case 'panel' + cpH = rs * it.nrows + cpTitleH + cpMv; + pos = [cpL vPos-cpH+cpTitleH cpW cpH]; + case 'infotable' + cpH = rs * it.nrows + cpTitleH + cpMv; + cpBot = vPos - cpH + cpTitleH; + infoH = px.infoH / figH; + pos = [cpL cpBot-infoH-cpMv cpW infoH]; + case 'below_info' + cpH = rs * it.nrows + cpTitleH + cpMv; + cpBot = vPos - cpH + cpTitleH; + infoH = px.infoH / figH; + infoY = cpBot - infoH - cpMv; + pos = [cpL+cpM infoY-rh-cpMv fw rh]; + otherwise + continue; + end + set(it.h, 'Position', pos); + catch + end + end + +% Reposition stick overlay below CP panel +ov = getappdata(fig, 'PSoverlay'); +if ~isempty(ov) + stickGap = cpM; + stickW = (cpW - stickGap) / 2; stickH = stickW * 1.3; + cpPanel = findobj(fig, 'Type', 'uipanel', 'Title', 'Control Panel'); + if ~isempty(cpPanel) + pp = get(cpPanel(1), 'Position'); cpBot = pp(2); + else + cpBot = vPos - rs*14 - cpMv; + end + stickY = max(0.01, cpBot - stickH - cpMv); + oH = rhs; + try set(ov.axYT, 'Position', [cpL stickY stickW stickH]); catch, end + try set(ov.axRP, 'Position', [cpL+stickW+stickGap stickY stickW stickH]); catch, end + oY = max(0.001, stickY - oH - 2*cpMv); halfW = cpW/2; fullW = cpW; + try set(ov.time, 'Position', [cpL oY fullW oH]); catch, end + oY = oY - oH; + try set(ov.M4, 'Position', [cpL oY halfW oH]); catch, end + try set(ov.M1, 'Position', [cpL+halfW oY halfW oH]); catch, end + oY = oY - oH; + try set(ov.M3, 'Position', [cpL oY halfW oH]); catch, end + try set(ov.M2, 'Position', [cpL+halfW oY halfW oH]); catch, end + oY = oY - oH; + try set(ov.GR, 'Position', [cpL oY fullW oH]); catch, end + oY = oY - oH; + try set(ov.GP, 'Position', [cpL oY fullW oH]); catch, end + oY = oY - oH; + try set(ov.GY, 'Position', [cpL oY fullW oH]); catch, end +end + +% Reposition Log Viewer checkbox bar (fixed pixel sizes) +chkBar = getappdata(fig, 'PScheckboxBar'); +if ~isempty(chkBar) + tbOff = 40 / figH; + chkH = rh; + chkRow1 = 1 - tbOff; + chkRow2 = chkRow1 - rs; + chkX = chkBar.x0; + for i = 1:numel(chkBar.items) + it = chkBar.items{i}; + if ~ishandle(it.h), continue; end + w = it.wpx / figW; + row = chkRow1; if it.row == 2, row = chkRow2; end + set(it.h, 'Position', [chkX row w chkH]); + if it.advance, chkX = chkX + w; end + end + % Reposition panel background + if isfield(chkBar, 'panel') && ishandle(chkBar.panel) + panelW = chkX - chkBar.x0 + cpM; + set(chkBar.panel, 'Position', [chkBar.x0 chkRow2-cpMv panelW chkRow1+chkH+cpMv-chkRow2+cpMv]); + end + % Reposition slider + sliderY = chkRow2 - 2*cpMv - 0.02; + if isfield(chkBar, 'slider') && ishandle(chkBar.slider) + sliderW = cpL - 0.0826 - 0.005; + set(chkBar.slider, 'Position', [0.0826 sliderY sliderW 0.02]); + end + + % Reposition Log Viewer plot axes + plotL = 0.095; plotGap = 0.01; + gapV = 0.005; linepos4H = 0.11; + plotTop = sliderY - 0.005; + plotW = cpL - plotL - plotGap; + + motorAx = findobj(fig, 'Tag', 'PSmotor'); + rpyAxes = findobj(fig, 'Tag', 'PSrpy'); + comboAx = findobj(fig, 'Tag', 'PScombo'); + upperAxes = []; + for k = 1:numel(rpyAxes), if ishandle(rpyAxes(k)), upperAxes(end+1) = rpyAxes(k); end; end + for k = 1:numel(comboAx), if ishandle(comboAx(k)), upperAxes(end+1) = comboAx(k); end; end + + nUpper = numel(upperAxes); + if ~isempty(motorAx) && ishandle(motorAx(1)) + if nUpper == 0 + set(motorAx(1), 'Position', [plotL 0.1 plotW plotTop - 0.1 - gapV]); + else + set(motorAx(1), 'Position', [plotL 0.1 plotW linepos4H]); + end + end + if nUpper > 0 + upperBot = 0.1 + linepos4H + gapV; + upperH = (plotTop - upperBot - max(0,nUpper-1)*gapV) / max(1,nUpper); + if nUpper > 1 + yVals = zeros(nUpper, 1); + for k = 1:nUpper, p = get(upperAxes(k), 'Position'); yVals(k) = p(2); end + [~, si] = sort(yVals, 'descend'); + upperAxes = upperAxes(si); + end + for k = 1:nUpper + y = plotTop - k*upperH - (k-1)*gapV; + set(upperAxes(k), 'Position', [plotL y plotW upperH]); + end + end +end + +end % if/elseif/else mode + +% Recompute subplot grid positions based on current cpL +grid = getappdata(fig, 'PSplotGrid'); +if ~isempty(grid) && isfield(grid, 'ncols') + plotR = cpL - grid.margin; + totalW = plotR - grid.plotL; + if totalW > 0.1 + if isfield(grid, 'colWidthFracs') && numel(grid.colWidthFracs) == grid.ncols + usable = totalW - (grid.ncols-1)*grid.colGap; + colWidths = usable * grid.colWidthFracs / sum(grid.colWidthFracs); + elseif isfield(grid, 'bigColFrac') && ~isempty(grid.bigColFrac) + wBig = totalW * grid.bigColFrac; + wSmall = (totalW - wBig - (grid.ncols-1)*grid.colGap) / max(1, grid.ncols-1); + colWidths = [wBig, repmat(wSmall, 1, grid.ncols-1)]; + else + colW_new = (totalW - (grid.ncols-1)*grid.colGap) / grid.ncols; + colWidths = repmat(colW_new, 1, grid.ncols); + end + newCols = zeros(1, grid.ncols); + newCols(1) = grid.plotL; + for c = 2:grid.ncols + newCols(c) = newCols(c-1) + colWidths(c-1) + grid.colGap; + end + + allAx = findobj(fig, 'Type', 'axes', 'Tag', 'PSgrid'); + validAx = []; validPos = []; + for i = 1:numel(allAx) + try + axP = get(allAx(i), 'Position'); + if axP(3) >= 0.05 && axP(4) >= 0.04 + validAx(end+1) = allAx(i); + validPos(end+1,:) = axP; + end + catch, end + end + + for ri = 1:numel(grid.rows) + rowIdx = []; + rowX = []; + for j = 1:numel(validAx) + [~, best_ri] = min(abs(grid.rows - validPos(j,2))); + if best_ri == ri && abs(grid.rows(ri) - validPos(j,2)) < 0.15 + rowIdx(end+1) = j; + rowX(end+1) = validPos(j,1); + end + end + if numel(rowIdx) < 1 || numel(rowIdx) > grid.ncols, continue; end + [~, si] = sort(rowX); + for k = 1:numel(si) + ci = min(k, grid.ncols); + j = rowIdx(si(k)); + set(validAx(j), 'Position', [newCols(ci) grid.rows(ri) colWidths(ci) grid.rowH]); + end + end + + % Store grid results for plot files + setappdata(fig, 'PSgridCols', newCols); + setappdata(fig, 'PSgridWidths', colWidths); + + % Reposition tagged colorbars + cbars = findobj(fig, 'Tag', 'PScbar'); + for cbi = 1:numel(cbars) + try + cpos = get(cbars(cbi), 'Position'); + ud = get(cbars(cbi), 'UserData'); + if ischar(ud) && strcmp(ud, 'north') + [~, cci] = min(abs(newCols - cpos(1))); + if cci <= numel(colWidths) + set(cbars(cbi), 'Position', [newCols(cci) cpos(2) colWidths(cci) cpos(4)]); + end + elseif ischar(ud) && strcmp(ud, 'east') + cbarX = plotR + 0.005; + cbarW = max(0.02, grid.margin * 0.35); + set(cbars(cbi), 'Position', [cbarX cpos(2) cbarW cpos(4)]); + end + catch, end + end + + % Reposition per-column top-bar widgets (Freq x Throttle) + pci = getappdata(fig, 'PSperColItems'); + if ~isempty(pci) + for pii = 1:numel(pci) + try + ph = pci{pii}{1}; pci_col = pci{pii}{2}; pci_xOff = pci{pii}{3}; + if pci_col <= numel(newCols) && ishandle(ph) + ppos = get(ph, 'Position'); + ppos(1) = newCols(pci_col) + pci_xOff; + set(ph, 'Position', ppos); + end + catch, end + end + end + end +end + +end diff --git a/src/util/PSsaveFig.m b/src/util/PSsaveFig.m index c880db1..f6b7ab8 100644 --- a/src/util/PSsaveFig.m +++ b/src/util/PSsaveFig.m @@ -1,57 +1,57 @@ -%% PSsaveFig - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -%% create saveDirectory -if exist('fnameMaster','var') && ~isempty(fnameMaster) - saveDirectory='PS_FIGS'; - saveDirectory = [saveDirectory '_' currentDate]; - - % Try logfile_directory first, fall back to configDir (Flatpak: home is read-only) - saveBase = ''; - try - cd(logfile_directory); - if ~isfolder(saveDirectory), mkdir(saveDirectory); end - % Verify directory is writable (Flatpak mounts home read-only) - testf = fullfile(saveDirectory, '.writetest'); - fid = fopen(testf, 'w'); fclose(fid); delete(testf); - saveBase = logfile_directory; - catch - try - cd(configDir); - if ~isfolder(saveDirectory), mkdir(saveDirectory); end - saveBase = configDir; - catch - end - end - - if isempty(saveBase) - warndlg('Cannot create save directory (file system may be read-only)'); - return; - end - -%% -set(gcf, 'pointer', 'watch') -cd(saveBase) -cd(saveDirectory) -FigDoesNotExist=1; -n=0; -while FigDoesNotExist, - n=n+1; - FigDoesNotExist=isfile([saveDirectory '-' int2str(n) '.png']); -end -figname=[saveDirectory '-' int2str(n)]; -saveas(gcf, [figname '.png'] ); -try, print(figname,'-dpng','-r200'); catch, end - -set(gcf, 'pointer', 'arrow') -cd(configDir) - -else - warndlg('Please select file(s)'); +%% PSsaveFig + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +%% create saveDirectory +if exist('fnameMaster','var') && ~isempty(fnameMaster) + saveDirectory='PS_FIGS'; + saveDirectory = [saveDirectory '_' currentDate]; + + % Try logfile_directory first, fall back to configDir (Flatpak: home is read-only) + saveBase = ''; + try + cd(logfile_directory); + if ~isfolder(saveDirectory), mkdir(saveDirectory); end + % Verify directory is writable (Flatpak mounts home read-only) + testf = fullfile(saveDirectory, '.writetest'); + fid = fopen(testf, 'w'); fclose(fid); delete(testf); + saveBase = logfile_directory; + catch + try + cd(configDir); + if ~isfolder(saveDirectory), mkdir(saveDirectory); end + saveBase = configDir; + catch + end + end + + if isempty(saveBase) + warndlg('Cannot create save directory (file system may be read-only)'); + return; + end + +%% +set(gcf, 'pointer', 'watch') +cd(saveBase) +cd(saveDirectory) +FigDoesNotExist=1; +n=0; +while FigDoesNotExist, + n=n+1; + FigDoesNotExist=isfile([saveDirectory '-' int2str(n) '.png']); +end +figname=[saveDirectory '-' int2str(n)]; +saveas(gcf, [figname '.png'] ); +try, print(figname,'-dpng','-r200'); catch, end + +set(gcf, 'pointer', 'arrow') +cd(configDir) + +else + warndlg('Please select file(s)'); end \ No newline at end of file diff --git a/src/util/PSsaveSettings.m b/src/util/PSsaveSettings.m index 604483c..185c1fb 100644 --- a/src/util/PSsaveSettings.m +++ b/src/util/PSsaveSettings.m @@ -1,146 +1,146 @@ -%% PSsaveSettings - -% ---------------------------------------------------------------------------------- -% "THE BEER-WARE LICENSE" (Revision 42): -% wrote this file. As long as you retain this notice you -% can do whatever you want with this stuff. If we meet some day, and you think -% this stuff is worth it, you can buy me a beer in return. -Brian White -% ---------------------------------------------------------------------------------- - -%% create saveDirectory -if exist('fnameMaster','var') && ~isempty(fnameMaster) - - -%% -set(gcf, 'pointer', 'watch') -try - cd(configDir) - clear defaults - if ~isfile('PSdefaults.txt') - clear var - var(1,:) = [{'firmware' 1}]; - var(2,:) = [{'LogViewer-SinglePanel' 0}]; - var(3,:) = [{'LogViewer-plotR' 1}]; - var(4,:) = [{'LogViewer-plotP' 1}]; - var(5,:) = [{'LogViewer-plotY' 1}]; - var(6,:) = [{'LogViewer-lineSmooth' 1}]; - var(7,:) = [{'LogViewer-lineWidth' 3}]; - var(8,:) = [{'LogViewer-Ymax' 500}]; - var(9,:) = [{'LogViewer-Ncolors' 8}]; - var(10,:) = [{'spec2D-term1' 1}]; - var(11,:) = [{'spec2D-term2' 2}]; - var(12,:) = [{'spec2D-smoothing' 3}]; - var(13,:) = [{'spec2D-delay' 1}]; - var(14,:) = [{'spec2D-plotR' 1}]; - var(15,:) = [{'spec2D-plotP' 1}]; - var(16,:) = [{'spec2D-plotY' 1}]; - var(17,:) = [{'spec2D-SinglePanel' 0}]; - var(18,:) = [{'FreqXthr-Column1' 3}]; - var(19,:) = [{'FreqXthr-Column2' 2}]; - var(20,:) = [{'FreqXthr-Column3' 8}]; - var(21,:) = [{'FreqXthr-Column4' 7}]; - var(22,:) = [{'FreqXthr-Preset' 1}]; - var(23,:) = [{'FreqXthr-Colormap' 3}]; - var(24,:) = [{'FreqXthr-Smoothing' 3}]; - var(25,:) = [{'FreqxTime-Preset' 2}]; - var(26,:) = [{'FreqxTime-FreqSmoothing' 2}]; - var(27,:) = [{'FreqxTime-TimeSmoothing' 2}]; - var(28,:) = [{'FreqxTime-Colormap' 3}]; - var(29,:) = [{'StepResp-plotR' 1}]; - var(30,:) = [{'StepResp-plotP' 1}]; - var(31,:) = [{'StepResp-plotY' 1}]; - var(32,:) = [{'StepResp-SinglePanel' 0}]; - var(33,:) = [{'StepResp-Ymax' 1.75}]; - - defaults = cell2table(var, 'VariableNames',{'Parameters' ; 'Values'}); - else - defaults = readtable('PSdefaults.txt'); - end -catch -end - -try - defaults.Values(1) = get(guiHandles.Firmware, 'Value'); - defaults.Values(2) = get(guiHandles.RPYcomboLV, 'Value'); - defaults.Values(3) = get(guiHandles.plotR, 'Value'); - defaults.Values(4) = get(guiHandles.plotP, 'Value'); - defaults.Values(5) = get(guiHandles.plotY, 'Value'); - defaults.Values(6) = get(guiHandles.lineSmooth, 'Value'); - defaults.Values(7) = get(guiHandles.linewidth, 'Value'); - defaults.Values(8) = str2num(get(guiHandles.maxY_input, 'String')); - defaults.Values(9) = str2num(get(guiHandles.nCols_input, 'String')); -catch -end -try - tmpSpecListVal = get(guiHandlesSpec2.SpecList, 'Value'); - defaults.Values(10) = tmpSpecListVal(1); - defaults.Values(11) = tmpSpecListVal(2); - defaults.Values(12) = get(guiHandlesSpec2.smoothFactor_select, 'Value'); - defaults.Values(13) = get(guiHandlesSpec2.Delay, 'Value'); - defaults.Values(14) = get(guiHandlesSpec2.plotR, 'Value'); - defaults.Values(15) = get(guiHandlesSpec2.plotP, 'Value'); - defaults.Values(16) = get(guiHandlesSpec2.plotY, 'Value'); - defaults.Values(17) = get(guiHandlesSpec2.RPYcomboSpec, 'Value'); -catch -end -try - defaults.Values(18) = get(guiHandlesSpec.SpecSelect{1}, 'Value'); - defaults.Values(19) = get(guiHandlesSpec.SpecSelect{2}, 'Value'); - defaults.Values(20) = get(guiHandlesSpec.SpecSelect{3}, 'Value'); - defaults.Values(21) = get(guiHandlesSpec.SpecSelect{4}, 'Value'); - defaults.Values(22) = get(guiHandlesSpec.specPresets, 'Value'); - defaults.Values(23) = get(guiHandlesSpec.ColormapSelect, 'Value'); - defaults.Values(24) = get(guiHandlesSpec.smoothFactor_select, 'Value'); -catch -end - -try - defaults.Values(25) = get(guiHandlesSpec3.SpecList, 'Value'); - defaults.Values(26) = get(guiHandlesSpec3.smoothFactor_select, 'Value'); - defaults.Values(27) = get(guiHandlesSpec3.subsampleFactor_select, 'Value'); - defaults.Values(28) = get(guiHandlesSpec3.ColormapSelect, 'Value'); -catch -end -try - defaults.Values(29) = get(guiHandlesTune.plotR, 'Value'); - defaults.Values(30) = get(guiHandlesTune.plotP, 'Value'); - defaults.Values(31) = get(guiHandlesTune.plotY, 'Value'); - defaults.Values(32) = get(guiHandlesTune.RPYcombo, 'Value'); - defaults.Values(33) = str2num(get(guiHandlesTune.maxYStepInput, 'String')); -catch -end - -try - writetable(defaults, 'PSdefaults') -catch -end - -try - fid = fopen('logfileDir.txt','r'); - logfile_directory = fscanf(fid, '%c'); - fclose(fid); -catch - logfile_directory = [pwd '/']; -end - -ldr = ['logfileDirectory: ' logfile_directory ]; - -try - defaults = readtable('PSdefaults.txt'); - a = char([cellstr([char(defaults.Parameters) num2str(defaults.Values)]); {rdr}; {mdr}; {ldr}]); - t = uitable(PSfig,'ColumnWidth',{500},'ColumnFormat',{'char'},'Data',[cellstr(a)]); - set(t,'units','normalized','Position',infoTablePos,'FontSize',fontsz*.8, 'ColumnName', ['']) -catch - defaults = ' '; - a = char(['Unable to set user defaults '; {rdr}; {mdr}; {ldr}]); - t = uitable(PSfig,'ColumnWidth',{500},'ColumnFormat',{'char'},'Data',[cellstr(a)]); - set(t,'units','normalized','Position',infoTablePos,'FontSize',fontsz*.8, 'ColumnName', ['']) -end - -clear var - -set(gcf, 'pointer', 'arrow') - -else - warndlg('Please select file(s)'); +%% PSsaveSettings + +% ---------------------------------------------------------------------------------- +% "THE BEER-WARE LICENSE" (Revision 42): +% wrote this file. As long as you retain this notice you +% can do whatever you want with this stuff. If we meet some day, and you think +% this stuff is worth it, you can buy me a beer in return. -Brian White +% ---------------------------------------------------------------------------------- + +%% create saveDirectory +if exist('fnameMaster','var') && ~isempty(fnameMaster) + + +%% +set(gcf, 'pointer', 'watch') +try + cd(configDir) + clear defaults + if ~isfile('PSdefaults.txt') + clear var + var(1,:) = [{'firmware' 1}]; + var(2,:) = [{'LogViewer-SinglePanel' 0}]; + var(3,:) = [{'LogViewer-plotR' 1}]; + var(4,:) = [{'LogViewer-plotP' 1}]; + var(5,:) = [{'LogViewer-plotY' 1}]; + var(6,:) = [{'LogViewer-lineSmooth' 1}]; + var(7,:) = [{'LogViewer-lineWidth' 3}]; + var(8,:) = [{'LogViewer-Ymax' 500}]; + var(9,:) = [{'LogViewer-Ncolors' 8}]; + var(10,:) = [{'spec2D-term1' 1}]; + var(11,:) = [{'spec2D-term2' 2}]; + var(12,:) = [{'spec2D-smoothing' 3}]; + var(13,:) = [{'spec2D-delay' 1}]; + var(14,:) = [{'spec2D-plotR' 1}]; + var(15,:) = [{'spec2D-plotP' 1}]; + var(16,:) = [{'spec2D-plotY' 1}]; + var(17,:) = [{'spec2D-SinglePanel' 0}]; + var(18,:) = [{'FreqXthr-Column1' 3}]; + var(19,:) = [{'FreqXthr-Column2' 2}]; + var(20,:) = [{'FreqXthr-Column3' 8}]; + var(21,:) = [{'FreqXthr-Column4' 7}]; + var(22,:) = [{'FreqXthr-Preset' 1}]; + var(23,:) = [{'FreqXthr-Colormap' 3}]; + var(24,:) = [{'FreqXthr-Smoothing' 3}]; + var(25,:) = [{'FreqxTime-Preset' 2}]; + var(26,:) = [{'FreqxTime-FreqSmoothing' 2}]; + var(27,:) = [{'FreqxTime-TimeSmoothing' 2}]; + var(28,:) = [{'FreqxTime-Colormap' 3}]; + var(29,:) = [{'StepResp-plotR' 1}]; + var(30,:) = [{'StepResp-plotP' 1}]; + var(31,:) = [{'StepResp-plotY' 1}]; + var(32,:) = [{'StepResp-SinglePanel' 0}]; + var(33,:) = [{'StepResp-Ymax' 1.75}]; + var(34,:) = [{'StepResp-Subsample' 1}]; + var(35,:) = [{'StepResp-MinRate' 40}]; + var(36,:) = [{'StepResp-MaxRate' 500}]; + + defaults = cell2table(var, 'VariableNames',{'Parameters' ; 'Values'}); + else + defaults = readtable('PSdefaults.txt'); + end +catch +end + +try + defaults.Values(1) = get(guiHandles.Firmware, 'Value'); + defaults.Values(2) = get(guiHandles.RPYcomboLV, 'Value'); + defaults.Values(3) = get(guiHandles.plotR, 'Value'); + defaults.Values(4) = get(guiHandles.plotP, 'Value'); + defaults.Values(5) = get(guiHandles.plotY, 'Value'); + defaults.Values(6) = get(guiHandles.lineSmooth, 'Value'); + defaults.Values(7) = get(guiHandles.linewidth, 'Value'); + defaults.Values(8) = str2double(get(guiHandles.maxY_input, 'String')); + defaults.Values(9) = str2double(get(guiHandles.nCols_input, 'String')); +catch +end +try + tmpSpecListVal = get(guiHandlesSpec2.SpecList, 'Value'); + defaults.Values(10) = tmpSpecListVal(1); + defaults.Values(11) = tmpSpecListVal(2); + defaults.Values(12) = get(guiHandlesSpec2.smoothFactor_select, 'Value'); + defaults.Values(13) = get(guiHandlesSpec2.Delay, 'Value'); + defaults.Values(14) = get(guiHandlesSpec2.plotR, 'Value'); + defaults.Values(15) = get(guiHandlesSpec2.plotP, 'Value'); + defaults.Values(16) = get(guiHandlesSpec2.plotY, 'Value'); + defaults.Values(17) = get(guiHandlesSpec2.RPYcomboSpec, 'Value'); +catch +end +try + defaults.Values(18) = get(guiHandlesSpec.SpecSelect{1}, 'Value'); + defaults.Values(19) = get(guiHandlesSpec.SpecSelect{2}, 'Value'); + defaults.Values(20) = get(guiHandlesSpec.SpecSelect{3}, 'Value'); + defaults.Values(21) = get(guiHandlesSpec.SpecSelect{4}, 'Value'); + defaults.Values(22) = get(guiHandlesSpec.specPresets, 'Value'); + defaults.Values(23) = get(guiHandlesSpec.ColormapSelect, 'Value'); + defaults.Values(24) = get(guiHandlesSpec.smoothFactor_select, 'Value'); +catch +end + +try + defaults.Values(25) = get(guiHandlesSpec3.SpecList, 'Value'); + defaults.Values(26) = get(guiHandlesSpec3.smoothFactor_select, 'Value'); + defaults.Values(27) = get(guiHandlesSpec3.subsampleFactor_select, 'Value'); + defaults.Values(28) = get(guiHandlesSpec3.ColormapSelect, 'Value'); +catch +end +try + defaults.Values(29) = get(guiHandlesTune.plotR, 'Value'); + defaults.Values(30) = get(guiHandlesTune.plotP, 'Value'); + defaults.Values(31) = get(guiHandlesTune.plotY, 'Value'); + defaults.Values(32) = get(guiHandlesTune.RPYcombo, 'Value'); + defaults.Values(33) = str2double(get(guiHandlesTune.maxYStepInput, 'String')); + defaults.Values(34) = get(guiHandlesTune.subsample, 'Value'); + defaults.Values(35) = str2double(get(guiHandlesTune.minRateInput, 'String')); + defaults.Values(36) = str2double(get(guiHandlesTune.maxRateInput, 'String')); +catch +end + +try + writetable(defaults, 'PSdefaults') +catch +end + +try + fid = fopen('logfileDir.txt','r'); + logfile_directory = fscanf(fid, '%c'); + fclose(fid); +catch + logfile_directory = [pwd '/']; +end + +ldr = ['logfileDirectory: ' logfile_directory ]; + +try + defaults = readtable('PSdefaults.txt'); +catch + defaults = ' '; +end + +clear var + +set(gcf, 'pointer', 'arrow') + +else + warndlg('Please select file(s)'); end \ No newline at end of file diff --git a/src/util/PSsmoothLV.m b/src/util/PSsmoothLV.m new file mode 100644 index 0000000..101fb43 --- /dev/null +++ b/src/util/PSsmoothLV.m @@ -0,0 +1,26 @@ +function y = PSsmoothLV(fig, Tfile, fileIdx, fieldName, sFactor, scale) +%% PSsmoothLV - cached smooth() for Log Viewer performance +% Avoids recomputing loess on 100k+ samples when user only toggles checkboxes + +if nargin < 6, scale = 1; end + +sc = getappdata(fig, 'smoothCacheLV'); +if isempty(sc) || ~isfield(sc, 'fIdx') || sc.fIdx ~= fileIdx + sc = struct('fIdx', fileIdx); +end + +if ~isfield(Tfile, fieldName), y = []; return; end + +cacheKey = [fieldName '_s' int2str(sFactor)]; +if isfield(sc, cacheKey) + y = sc.(cacheKey); + if numel(y) == numel(Tfile.(fieldName)), return; end + sc = struct('fIdx', fileIdx); % stale cache - different data length +end + +raw = Tfile.(fieldName); +if scale ~= 1, raw = raw * scale; end +y = smooth(raw, sFactor, 'loess'); +sc.(cacheKey) = y; +setappdata(fig, 'smoothCacheLV', sc); +end diff --git a/src/util/PSstepPeriod.m b/src/util/PSstepPeriod.m new file mode 100644 index 0000000..ddec44d --- /dev/null +++ b/src/util/PSstepPeriod.m @@ -0,0 +1,54 @@ +function PSstepPeriod(fig) +%% PSstepPeriod - click two points on step response trace, show period + frequency + +th = PStheme(); + +% clear previous period markers +delete(findobj(fig, 'Tag', 'PSperiod')); + +allAx = findobj(fig, 'Type', 'axes', 'Visible', 'on'); +if isempty(allAx), return; end + +% first click - determines target axes +try ginput(1); catch, return; end +figPt = get(fig, 'CurrentPoint'); +ax = []; +for ai = 1:numel(allAx) + p = getpixelposition(allAx(ai)); + if figPt(1) >= p(1) && figPt(1) <= p(1)+p(3) && figPt(2) >= p(2) && figPt(2) <= p(2)+p(4) + ax = allAx(ai); break; + end +end +if isempty(ax), return; end + +xl = get(ax, 'XLim'); +x1 = xl(1) + (figPt(1) - p(1)) / p(3) * (xl(2) - xl(1)); + +% second click +try ginput(1); catch, return; end +figPt = get(fig, 'CurrentPoint'); +p = getpixelposition(ax); +x2 = xl(1) + (figPt(1) - p(1)) / p(3) * (xl(2) - xl(1)); + +x = sort([x1 x2]); +dt = x(2) - x(1); +freq = 1000 / dt; + +% draw on all step trace axes (XLim > 100ms excludes scatter plots) +stepAxes = []; +for ai = 1:numel(allAx) + xl_chk = get(allAx(ai), 'XLim'); + if xl_chk(2) > 100, stepAxes(end+1) = allAx(ai); end +end +if isempty(stepAxes), stepAxes = ax; end +for ai = 1:numel(stepAxes) + yl_i = get(stepAxes(ai), 'YLim'); + line(stepAxes(ai), [x(1) x(1)], yl_i, 'Color', th.periodMarker, 'LineWidth', 1.5, 'Tag', 'PSperiod'); + line(stepAxes(ai), [x(2) x(2)], yl_i, 'Color', th.periodMarker, 'LineWidth', 1.5, 'Tag', 'PSperiod'); +end +yl = get(ax, 'YLim'); +text(mean(x), yl(2)*0.93, sprintf('%.0fms, %.4fHz', dt, freq), ... + 'Parent', ax, 'Color', th.textPrimary, 'FontSize', th.fontsz, ... + 'HorizontalAlignment', 'center', 'FontWeight', 'bold', 'Tag', 'PSperiod'); + +end diff --git a/src/util/PSstyleAxes.m b/src/util/PSstyleAxes.m new file mode 100644 index 0000000..5b3c25b --- /dev/null +++ b/src/util/PSstyleAxes.m @@ -0,0 +1,13 @@ +function PSstyleAxes(ax, th) +%% PSstyleAxes - apply dark theme to axes +if nargin < 2, th = PStheme(); end +if ~ishandle(ax), return; end +% skip colorbars - findobj('Type','axes') returns them in Octave +try tag = get(ax, 'Tag'); if strcmpi(tag, 'colorbar') || strcmpi(tag, 'Colorbar'), return; end; catch, end +set(ax, 'Color', th.axesBg, 'XColor', th.axesFg, 'YColor', th.axesFg, ... + 'GridColor', th.gridColor, 'FontWeight', 'bold'); +try set(get(ax, 'Title'), 'Color', th.textPrimary); catch, end +try set(get(ax, 'XLabel'), 'Color', th.textPrimary); catch, end +try set(get(ax, 'YLabel'), 'Color', th.textPrimary); catch, end +try grid(ax, 'on'); catch, end +end diff --git a/src/util/PSstyleControls.m b/src/util/PSstyleControls.m new file mode 100644 index 0000000..d59695a --- /dev/null +++ b/src/util/PSstyleControls.m @@ -0,0 +1,26 @@ +function PSstyleControls(fig, th) +%% PSstyleControls - apply dark theme to all uicontrols on figure +if nargin < 2, th = PStheme(); end +controls = findobj(fig, 'Type', 'uicontrol'); +for i = 1:numel(controls) + style = get(controls(i), 'Style'); + if strcmp(style, 'pushbutton') + set(controls(i), 'BackgroundColor', th.btnBg); + continue; + end + if strcmp(style, 'checkbox') || strcmp(style, 'radiobutton') + try set(controls(i), 'BackgroundColor', th.figBg); catch, end + end + fg = get(controls(i), 'ForegroundColor'); + % fix controls still at default black [0 0 0] + if all(abs(fg) < 0.01) + set(controls(i), 'ForegroundColor', th.textPrimary); + end + if strcmp(style, 'edit') + set(controls(i), 'BackgroundColor', th.inputBg, 'ForegroundColor', th.inputFg); + end + if strcmp(style, 'popupmenu') || strcmp(style, 'listbox') + try set(controls(i), 'BackgroundColor', th.inputBg, 'ForegroundColor', th.inputFg); catch, end + end +end +end diff --git a/src/util/PSstyleFig.m b/src/util/PSstyleFig.m new file mode 100644 index 0000000..f737080 --- /dev/null +++ b/src/util/PSstyleFig.m @@ -0,0 +1,6 @@ +function PSstyleFig(fig, titleStr) +%% PSstyleFig - apply dark theme to figure +th = PStheme(); +set(fig, 'Color', th.figBg, 'InvertHardcopy', 'off'); +if nargin >= 2, set(fig, 'Name', titleStr, 'NumberTitle', 'off'); end +end diff --git a/src/util/PSstyleLegend.m b/src/util/PSstyleLegend.m new file mode 100644 index 0000000..6a85b85 --- /dev/null +++ b/src/util/PSstyleLegend.m @@ -0,0 +1,5 @@ +function PSstyleLegend(lg, th) +%% PSstyleLegend - apply dark theme to legend +if nargin < 2, th = PStheme(); end +set(lg, 'TextColor', th.legendFg, 'Color', th.legendBg, 'EdgeColor', th.legendEdge); +end diff --git a/src/util/PStheme.m b/src/util/PStheme.m new file mode 100644 index 0000000..0c84d42 --- /dev/null +++ b/src/util/PStheme.m @@ -0,0 +1,114 @@ +function th = PStheme() +%% PStheme - central UI theme for PIDscope (dark) + +% figure & panel +th.figBg = [.18 .18 .20]; +th.panelBg = [.22 .22 .24]; +th.panelFg = [.90 .90 .90]; +th.panelBorder = [.35 .35 .38]; + +% axes +th.axesBg = [.10 .10 .12]; +th.axesFg = [.75 .75 .75]; +th.gridColor = [.28 .28 .30]; + +% text +th.textPrimary = [.90 .90 .90]; +th.textSecondary = [.65 .65 .65]; +th.textAccent = [.40 .80 1.0]; + +% legend +th.legendBg = [.16 .16 .18]; +th.legendFg = [.80 .80 .80]; +th.legendEdge = [.35 .35 .38]; + +% epoch shading (trim regions) +th.epochFill = [.30 .30 .32]; +th.epochAlpha = 0.7; + +% buttons +th.btnBg = [.30 .30 .32]; +th.btnRun = [.15 .85 .25]; +th.btnReset = [.95 .55 .15]; +th.btnSave = [.70 .70 .70]; +th.btnDash1 = [.95 .30 .30]; % Spectral Analyzer - vivid red +th.btnDash2 = [.30 .50 .95]; % Step Response - vivid blue +th.btnDash3 = [.20 .80 .90]; % PID Slider Tool - vivid cyan +th.btnDash4 = [.95 .60 .15]; % Filter Sim - vivid amber +th.btnDash5 = [.90 .75 .20]; % Test Signal - vivid gold +th.btnDash6 = [.90 .30 .60]; % PID Error - vivid magenta +th.btnDash7 = [.25 .75 .65]; % Flight Stats - vivid teal +th.btnMotNoise = [.20 .85 .30]; % Motor Noise - vivid green +th.btnChirp = [.70 .40 .95]; % Chirp Analysis - vivid purple +th.btnLink = [.95 .60 .15]; % Support PIDscope - amber +th.btnPlayer = [.40 .75 1.0]; % Player button - bright sky blue + +% overlay indicator colors (checkboxes in CP) +th.overlayDynNotch = [0 .8 .8]; % Dyn Notch - cyan +th.overlayRPM = [.6 .9 .6]; % RPM est - light green + +% font size - single source of truth +screensz = get(0, 'ScreenSize'); +th.fontsz = round(screensz(4) * .011); +% Octave Qt renders fonts bigger than MATLAB +if exist('OCTAVE_VERSION', 'builtin') + th.fontsz = round(th.fontsz * 0.85); +end + +% period marker (Step Response) +th.periodMarker = [.95 .20 .20]; + +% diff highlight (Setup Info) +th.diffBg = [.30 .30 .32]; + +% checkbox / input bg +th.checkBg = [.18 .18 .20]; +th.inputBg = [.14 .14 .16]; +th.inputFg = [.90 .90 .90]; + +% datatip tooltip +th.datatipBg = [1.0 1.0 .88]; % pale yellow + +% RPY axis colors (FPV standard: Roll=red, Pitch=green, Yaw=blue) +th.axisRoll = [.95 .35 .15]; % red-orange +th.axisRollFilt = [1.0 .45 .45]; % light red (distinct from M2 orange) +th.axisPitch = [.20 .85 .25]; % green +th.axisPitchFilt = [.55 1.0 .55]; % lighter lime +th.axisYaw = [.30 .45 .95]; % blue +th.axisYawFilt = [.60 .75 1.0]; % lighter sky blue + +% filter section headers +th.secNotch = [1.0 .70 .30]; % amber/orange +th.secDtermLPF = [.40 1.0 .40]; % green +th.secDtermNotch = [.40 .90 .40]; % green (matches D-term plot curves) +th.refLine3dB = [.60 .60 .20]; % -3dB / 0.707 reference + +% Bode / chirp analysis +th.bodeMain = [0 .80 1.0]; % tracking TF - bright cyan +th.bodeSecondary = [1.0 .50 0]; % plant TF - orange +th.bodeCoherence = [.30 .90 .30]; % coherence - green +th.bodeRef = [.50 .50 .50]; % reference lines (0dB, -180, unity) + +% transport buttons (Player) +th.btnPlay = [0 .60 0]; % play - dark green +th.btnPause = [.80 .20 0]; % pause - dark red-orange +th.btnStop = [.70 0 0]; % stop - crimson + +% signal colors (bright for dark bg) +th.sigDebug = [.50 .50 .50]; +th.sigGyro = [.85 .85 .85]; +th.sigGyroPrefilt = [.65 .65 .65]; +th.sigPterm = [.20 .85 .20]; +th.sigIterm = [.90 .75 .20]; +th.sigDprefilt = [.45 .80 .95]; +th.sigDterm = [.30 .40 .95]; +th.sigFterm = [.75 .45 .45]; +th.sigSetpoint = [.90 .25 .35]; +th.sigPIDsum = [1.0 .35 .90]; +th.sigPIDerr = [.55 .20 .95]; +th.sigMotor = {[.95 .20 .20], [.95 .65 .10], [.20 .85 .25], [.20 .45 .95]}; +th.sigRPM = {[1.0 .50 .50], [1.0 .80 .40], [.50 1.0 .55], [.50 .65 1.0]}; +th.sigThrottle = [.85 .85 .85]; +th.sigTestSignal = [1.0 1.0 1.0]; % white (matches Log Viewer overlay) + +end diff --git a/src/util/b2r.m b/src/util/b2r.m index 6376ea4..c9d2958 100644 --- a/src/util/b2r.m +++ b/src/util/b2r.m @@ -1,135 +1,135 @@ -function newmap = b2r(cmin_input,cmax_input) -%BLUEWHITERED Blue, white, and red color map. -% this matlab file is designed to draw anomaly figures. the color of -% the colorbar is from blue to white and then to red, corresponding to -% the anomaly values from negative to zero to positive, respectively. -% The color white always correspondes to value zero. -% -% You should input two values like caxis in matlab, that is the min and -% the max value of color values designed. e.g. colormap(b2r(-3,5)) -% -% the brightness of blue and red will change according to your setting, -% so that the brightness of the color corresponded to the color of his -% opposite number -% e.g. colormap(b2r(-3,6)) is from light blue to deep red -% e.g. colormap(b2r(-3,3)) is from deep blue to deep red -% -% I'd advise you to use colorbar first to make sure the caxis' cmax and cmin. -% Besides, there is also another similar colorbar named 'darkb2r', in which the -% color is darker. -% -% by Cunjie Zhang, 2011-3-14 -% find bugs ====> email : daisy19880411@126.com -% updated: Robert Beckman help to fix the bug when start point is zero, 2015-04-08 -% -% Examples: -% ------------------------------ -% figure -% peaks; -% colormap(b2r(-6,8)), colorbar, title('b2r') -% - - -%% check the input -if nargin ~= 2 ; - disp('input error'); - disp('input two variables, the range of caxis , for example : colormap(b2r(-3,3))'); -end - -if cmin_input >= cmax_input - disp('input error'); - disp('the color range must be from a smaller one to a larger one'); -end - -%% control the figure caxis -lims = get(gca, 'CLim'); % get figure caxis formation -caxis([cmin_input cmax_input]); - -%% color configuration : from blue to to white then to red - -red_top = [1 0 0]; -white_middle= [1 1 1]; -blue_bottom = [0 0 1]; - -%% color interpolation - -color_num = 251; -color_input = [blue_bottom; white_middle; red_top]; -oldsteps = linspace(-1, 1, size(color_input,1)); -newsteps = linspace(-1, 1, color_num); - -%% Category Discussion according to the cmin and cmax input - -% the color data will be remaped to color range from -max(abs(cmin_input),cmax_input) -% to max(abs(cmin_input),cmax_input) , and then squeeze the color data -% in order to make sure the blue and red color selected corresponded -% to their math values - -% for example : -% if b2r(-3,6) ,the color range is from light blue to deep red , so that -% the light blue valued at -3 correspondes to light red valued at 3 - - -%% Category Discussion according to the cmin and cmax input -% first : from negative to positive -% then : from positive to positive -% last : from negative to negative - -for j=1:3 - newmap_all(:,j) = min(max(transpose(interp1(oldsteps, color_input(:,j), newsteps)), 0), 1); -end - -if (cmin_input < 0) && (cmax_input > 0) ; - - - if abs(cmin_input) < cmax_input - - % |--------|---------|--------------------| - % -cmax cmin 0 cmax [cmin,cmax] - - start_point = max(round((cmin_input+cmax_input)/2/cmax_input*color_num),1); - newmap = squeeze(newmap_all(start_point:color_num,:)); - - elseif abs(cmin_input) >= cmax_input - - % |------------------|------|--------------| - % cmin 0 cmax -cmin [cmin,cmax] - - end_point = max(round((cmax_input-cmin_input)/2/abs(cmin_input)*color_num),1); - newmap = squeeze(newmap_all(1:end_point,:)); - end - - -elseif cmin_input >= 0 - - if lims(1) < 0 - disp('caution:') - disp('there are still values smaller than 0, but cmin is larger than 0.') - disp('some area will be in red color while it should be in blue color') - end - - % |-----------------|-------|-------------| - % -cmax 0 cmin cmax [cmin,cmax] - - start_point = max(round((cmin_input+cmax_input)/2/cmax_input*color_num),1); - newmap = squeeze(newmap_all(start_point:color_num,:)); - -elseif cmax_input <= 0 - - if lims(2) > 0 - disp('caution:') - disp('there are still values larger than 0, but cmax is smaller than 0.') - disp('some area will be in blue color while it should be in red color') - end - - % |------------|------|--------------------| - % cmin cmax 0 -cmin [cmin,cmax] - - end_point = max(round((cmax_input-cmin_input)/2/abs(cmin_input)*color_num),1); - newmap = squeeze(newmap_all(1:end_point,:)); -end - - - - - +function newmap = b2r(cmin_input,cmax_input) +%BLUEWHITERED Blue, white, and red color map. +% this matlab file is designed to draw anomaly figures. the color of +% the colorbar is from blue to white and then to red, corresponding to +% the anomaly values from negative to zero to positive, respectively. +% The color white always correspondes to value zero. +% +% You should input two values like caxis in matlab, that is the min and +% the max value of color values designed. e.g. colormap(b2r(-3,5)) +% +% the brightness of blue and red will change according to your setting, +% so that the brightness of the color corresponded to the color of his +% opposite number +% e.g. colormap(b2r(-3,6)) is from light blue to deep red +% e.g. colormap(b2r(-3,3)) is from deep blue to deep red +% +% I'd advise you to use colorbar first to make sure the caxis' cmax and cmin. +% Besides, there is also another similar colorbar named 'darkb2r', in which the +% color is darker. +% +% by Cunjie Zhang, 2011-3-14 +% find bugs ====> email : daisy19880411@126.com +% updated: Robert Beckman help to fix the bug when start point is zero, 2015-04-08 +% +% Examples: +% ------------------------------ +% figure +% peaks; +% colormap(b2r(-6,8)), colorbar, title('b2r') +% + + +%% check the input +if nargin ~= 2 ; + disp('input error'); + disp('input two variables, the range of caxis , for example : colormap(b2r(-3,3))'); +end + +if cmin_input >= cmax_input + disp('input error'); + disp('the color range must be from a smaller one to a larger one'); +end + +%% control the figure caxis +lims = get(gca, 'CLim'); % get figure caxis formation +caxis([cmin_input cmax_input]); + +%% color configuration : from blue to to white then to red + +red_top = [1 0 0]; +white_middle= [1 1 1]; +blue_bottom = [0 0 1]; + +%% color interpolation + +color_num = 251; +color_input = [blue_bottom; white_middle; red_top]; +oldsteps = linspace(-1, 1, size(color_input,1)); +newsteps = linspace(-1, 1, color_num); + +%% Category Discussion according to the cmin and cmax input + +% the color data will be remaped to color range from -max(abs(cmin_input),cmax_input) +% to max(abs(cmin_input),cmax_input) , and then squeeze the color data +% in order to make sure the blue and red color selected corresponded +% to their math values + +% for example : +% if b2r(-3,6) ,the color range is from light blue to deep red , so that +% the light blue valued at -3 correspondes to light red valued at 3 + + +%% Category Discussion according to the cmin and cmax input +% first : from negative to positive +% then : from positive to positive +% last : from negative to negative + +for j=1:3 + newmap_all(:,j) = min(max(transpose(interp1(oldsteps, color_input(:,j), newsteps)), 0), 1); +end + +if (cmin_input < 0) && (cmax_input > 0) ; + + + if abs(cmin_input) < cmax_input + + % |--------|---------|--------------------| + % -cmax cmin 0 cmax [cmin,cmax] + + start_point = max(round((cmin_input+cmax_input)/2/cmax_input*color_num),1); + newmap = squeeze(newmap_all(start_point:color_num,:)); + + elseif abs(cmin_input) >= cmax_input + + % |------------------|------|--------------| + % cmin 0 cmax -cmin [cmin,cmax] + + end_point = max(round((cmax_input-cmin_input)/2/abs(cmin_input)*color_num),1); + newmap = squeeze(newmap_all(1:end_point,:)); + end + + +elseif cmin_input >= 0 + + if lims(1) < 0 + disp('caution:') + disp('there are still values smaller than 0, but cmin is larger than 0.') + disp('some area will be in red color while it should be in blue color') + end + + % |-----------------|-------|-------------| + % -cmax 0 cmin cmax [cmin,cmax] + + start_point = max(round((cmin_input+cmax_input)/2/cmax_input*color_num),1); + newmap = squeeze(newmap_all(start_point:color_num,:)); + +elseif cmax_input <= 0 + + if lims(2) > 0 + disp('caution:') + disp('there are still values larger than 0, but cmax is smaller than 0.') + disp('some area will be in blue color while it should be in red color') + end + + % |------------|------|--------------------| + % cmin cmax 0 -cmin [cmin,cmax] + + end_point = max(round((cmax_input-cmin_input)/2/abs(cmin_input)*color_num),1); + newmap = squeeze(newmap_all(1:end_point,:)); +end + + + + + diff --git a/tests/run_tests.m b/tests/run_tests.m index b34f9b6..0254a07 100644 --- a/tests/run_tests.m +++ b/tests/run_tests.m @@ -1,69 +1,70 @@ -% run_tests.m - PIDscope test runner for GNU Octave -% Usage: octave --no-gui --eval "run('tests/run_tests.m')" - -fprintf('\n=== PIDscope Test Suite ===\n\n'); - -% Setup paths -project_root = fileparts(fileparts(mfilename('fullpath'))); -addpath(project_root); -addpath(genpath(fullfile(project_root, 'src'))); -addpath(fullfile(project_root, 'tests')); - -% Load required packages -if exist('OCTAVE_VERSION', 'builtin') - pkg load signal; - pkg load statistics; -end - -% Collect test files -test_dir = fullfile(project_root, 'tests'); -test_files = dir(fullfile(test_dir, 'test_*.m')); - -total_pass = 0; -total_fail = 0; -total_skip = 0; -failed_files = {}; - -for i = 1:length(test_files) - fname = test_files(i).name; - fpath = fullfile(test_dir, fname); - fprintf('Running %s ... ', fname); - try - % Octave test() returns [n, nmax, nxfail, nskip] - % n = passed, nmax = total tests - [n, nmax, nxfail, nskip] = test(fpath, 'quiet'); - nfail = nmax - n - nxfail; - total_pass = total_pass + n; - total_fail = total_fail + nfail; - total_skip = total_skip + nskip; - if nfail > 0 - fprintf('FAIL (%d/%d passed, %d failed)\n', n, nmax, nfail); - failed_files{end+1} = fname; - elseif nmax == 0 - fprintf('no tests\n'); - else - fprintf('OK (%d passed', n); - if nskip > 0, fprintf(', %d skipped', nskip); end - fprintf(')\n'); - end - catch e - fprintf('ERROR: %s\n', e.message); - total_fail = total_fail + 1; - failed_files{end+1} = fname; - end -end - -fprintf('\n=== Results ===\n'); -fprintf('Passed: %d\n', total_pass); -fprintf('Failed: %d\n', total_fail); -fprintf('Skipped: %d\n', total_skip); - -if ~isempty(failed_files) - fprintf('\nFailed test files:\n'); - for i = 1:length(failed_files) - fprintf(' - %s\n', failed_files{i}); - end - fprintf('\nSOME TESTS FAILED\n'); -else - fprintf('\nALL TESTS PASSED\n'); -end +% run_tests.m - PIDscope test runner for GNU Octave +% Usage: octave --no-gui --eval "run('tests/run_tests.m')" + +fprintf('\n=== PIDscope Test Suite ===\n\n'); + +% Setup paths +project_root = fileparts(fileparts(mfilename('fullpath'))); +addpath(project_root); +addpath(genpath(fullfile(project_root, 'src'))); +addpath(fullfile(project_root, 'tests')); + +% Load required packages +if exist('OCTAVE_VERSION', 'builtin') + pkg load signal; + pkg load statistics; +end + +% Collect test files +test_dir = fullfile(project_root, 'tests'); +test_files = dir(fullfile(test_dir, 'test_*.m')); + +total_pass = 0; +total_fail = 0; +total_skip = 0; +failed_files = {}; + +for i = 1:length(test_files) + fname = test_files(i).name; + fpath = fullfile(test_dir, fname); + fprintf('Running %s ... ', fname); + try + % Octave test() returns [n, nmax, nxfail, nskip] + % n = passed, nmax = total tests + [n, nmax, nxfail, nskip] = test(fpath, 'quiet'); + nfail = nmax - n - nxfail; + total_pass = total_pass + n; + total_fail = total_fail + nfail; + total_skip = total_skip + nskip; + if nfail > 0 + fprintf('FAIL (%d/%d passed, %d failed)\n', n, nmax, nfail); + failed_files{end+1} = fname; + elseif nmax == 0 + fprintf('no tests\n'); + else + fprintf('OK (%d passed', n); + if nskip > 0, fprintf(', %d skipped', nskip); end + fprintf(')\n'); + end + catch e + fprintf('ERROR: %s\n', e.message); + total_fail = total_fail + 1; + failed_files{end+1} = fname; + end +end + +fprintf('\n=== Results ===\n'); +fprintf('Passed: %d\n', total_pass); +fprintf('Failed: %d\n', total_fail); +fprintf('Skipped: %d\n', total_skip); + +if ~isempty(failed_files) + fprintf('\nFailed test files:\n'); + for i = 1:length(failed_files) + fprintf(' - %s\n', failed_files{i}); + end + fprintf('\nSOME TESTS FAILED\n'); + exit(1); +else + fprintf('\nALL TESTS PASSED\n'); +end diff --git a/tests/test_PSSpec2d.m b/tests/test_PSSpec2d.m index 051bdd6..38258f0 100644 --- a/tests/test_PSSpec2d.m +++ b/tests/test_PSSpec2d.m @@ -1,42 +1,42 @@ -% test_PSSpec2d.m - Tests for FFT computation engine -% NOTE: PSSpec2d expects Y as ROW vector, F in kHz - -%!test -%! % Pure 100Hz sine at 4kHz -> peak at 100Hz -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! t = (0 : 1/Fs : 0.3 - 1/Fs); % row vector -%! y = sin(2 * pi * 100 * t); -%! [freq, spec] = PSSpec2d(y, Fs_khz, 0); -%! [~, peak_idx] = max(spec); -%! peak_freq = freq(peak_idx); -%! assert(abs(peak_freq - 100) < 10); - -%!test -%! % Output has N/2+1 frequencies (DC to Nyquist) -%! Fs_khz = 4; -%! N = 1200; -%! y = randn(1, N); -%! [freq, spec] = PSSpec2d(y, Fs_khz, 0); -%! assert(length(freq), N/2 + 1); -%! assert(length(spec), N/2 + 1); - -%!test -%! % PSD mode produces values in dB range -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! t = (0 : 1/Fs : 0.3 - 1/Fs); -%! y = sin(2 * pi * 200 * t); -%! [~, spec_psd] = PSSpec2d(y, Fs_khz, 1); -%! % PSD in dB should have negative values for most bins -%! assert(any(spec_psd < 0)); - -%!test -%! % Frequency vector spans 0 to Nyquist -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = 1200; -%! y = randn(1, N); -%! [freq, ~] = PSSpec2d(y, Fs_khz, 0); -%! assert(freq(1), 0, 1e-10); % DC at 0 -%! assert(abs(freq(end) - Fs/2) < Fs/N); % last bin near Nyquist +% test_PSSpec2d.m - Tests for FFT computation engine +% NOTE: PSSpec2d expects Y as ROW vector, F in kHz + +%!test +%! % Pure 100Hz sine at 4kHz -> peak at 100Hz +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! t = (0 : 1/Fs : 0.3 - 1/Fs); % row vector +%! y = sin(2 * pi * 100 * t); +%! [freq, spec] = PSSpec2d(y, Fs_khz, 0); +%! [~, peak_idx] = max(spec); +%! peak_freq = freq(peak_idx); +%! assert(abs(peak_freq - 100) < 10); + +%!test +%! % Output has N/2+1 frequencies (DC to Nyquist) +%! Fs_khz = 4; +%! N = 1200; +%! y = randn(1, N); +%! [freq, spec] = PSSpec2d(y, Fs_khz, 0); +%! assert(length(freq), N/2 + 1); +%! assert(length(spec), N/2 + 1); + +%!test +%! % PSD mode produces values in dB range +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! t = (0 : 1/Fs : 0.3 - 1/Fs); +%! y = sin(2 * pi * 200 * t); +%! [~, spec_psd] = PSSpec2d(y, Fs_khz, 1); +%! % PSD in dB should have negative values for most bins +%! assert(any(spec_psd < 0)); + +%!test +%! % Frequency vector spans 0 to Nyquist +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = 1200; +%! y = randn(1, N); +%! [freq, ~] = PSSpec2d(y, Fs_khz, 0); +%! assert(freq(1), 0, 1e-10); % DC at 0 +%! assert(abs(freq(end) - Fs/2) < Fs/N); % last bin near Nyquist diff --git a/tests/test_PSbfFilters.m b/tests/test_PSbfFilters.m index a894595..bc9f2ae 100644 --- a/tests/test_PSbfFilters.m +++ b/tests/test_PSbfFilters.m @@ -1,59 +1,59 @@ -% test_PSbfFilters.m - tests for BF-compatible filter implementations - -%!test -%! % PT1 lowpass attenuates above cutoff -%! Fs = 4000; fc = 200; -%! [b, a] = PSbfFilters('pt1', fc, Fs); -%! t = (0:4000-1)'/Fs; -%! sig_lo = sin(2*pi*50*t); % 50 Hz — should pass -%! sig_hi = sin(2*pi*800*t); % 800 Hz — should be attenuated -%! out_lo = filter(b, a, sig_lo); -%! out_hi = filter(b, a, sig_hi); -%! rms_lo = sqrt(mean(out_lo(2000:end).^2)); -%! rms_hi = sqrt(mean(out_hi(2000:end).^2)); -%! assert(rms_lo > 0.5); % passes through -%! assert(rms_hi < 0.2); % attenuated - -%!test -%! % PT2 steeper rolloff than PT1 -%! Fs = 4000; fc = 200; -%! [b1, a1] = PSbfFilters('pt1', fc, Fs); -%! [b2, a2] = PSbfFilters('pt2', fc, Fs); -%! t = (0:4000-1)'/Fs; -%! sig = sin(2*pi*400*t); -%! out1 = filter(b1, a1, sig); -%! out2 = filter(b2, a2, sig); -%! rms1 = sqrt(mean(out1(2000:end).^2)); -%! rms2 = sqrt(mean(out2(2000:end).^2)); -%! assert(rms2 < rms1); % PT2 attenuates more at 2x cutoff - -%!test -%! % Notch filter removes target frequency -%! Fs = 4000; fc = 300; Q = 5; -%! [b, a] = PSbfFilters('notch', fc, Fs, Q); -%! t = (0:8000-1)'/Fs; -%! sig = sin(2*pi*300*t) + sin(2*pi*100*t); -%! out = filter(b, a, sig); -%! % 300 Hz should be removed, 100 Hz should remain -%! N = length(out); f = (0:N/2)*Fs/N; -%! Y = abs(fft(out .* hann(N))); -%! Y = Y(1:N/2+1); -%! [~, i100] = min(abs(f - 100)); -%! [~, i300] = min(abs(f - 300)); -%! assert(Y(i100) > Y(i300) * 5); % 100 Hz much stronger than 300 Hz - -%!test -%! % Edge case: fc=0 returns passthrough -%! [b, a] = PSbfFilters('pt1', 0, 4000); -%! assert(b, 1); -%! assert(a, 1); - -%!test -%! % Biquad is second-order lowpass -%! Fs = 4000; fc = 200; -%! [b, a] = PSbfFilters('biquad', fc, Fs); -%! assert(length(b), 3); -%! assert(length(a), 3); -%! % DC gain should be ~1 -%! dc_gain = sum(b) / sum(a); -%! assert(abs(dc_gain - 1) < 0.01); +% test_PSbfFilters.m - tests for BF-compatible filter implementations + +%!test +%! % PT1 lowpass attenuates above cutoff +%! Fs = 4000; fc = 200; +%! [b, a] = PSbfFilters('pt1', fc, Fs); +%! t = (0:4000-1)'/Fs; +%! sig_lo = sin(2*pi*50*t); % 50 Hz — should pass +%! sig_hi = sin(2*pi*800*t); % 800 Hz — should be attenuated +%! out_lo = filter(b, a, sig_lo); +%! out_hi = filter(b, a, sig_hi); +%! rms_lo = sqrt(mean(out_lo(2000:end).^2)); +%! rms_hi = sqrt(mean(out_hi(2000:end).^2)); +%! assert(rms_lo > 0.5); % passes through +%! assert(rms_hi < 0.2); % attenuated + +%!test +%! % PT2 steeper rolloff than PT1 +%! Fs = 4000; fc = 200; +%! [b1, a1] = PSbfFilters('pt1', fc, Fs); +%! [b2, a2] = PSbfFilters('pt2', fc, Fs); +%! t = (0:4000-1)'/Fs; +%! sig = sin(2*pi*400*t); +%! out1 = filter(b1, a1, sig); +%! out2 = filter(b2, a2, sig); +%! rms1 = sqrt(mean(out1(2000:end).^2)); +%! rms2 = sqrt(mean(out2(2000:end).^2)); +%! assert(rms2 < rms1); % PT2 attenuates more at 2x cutoff + +%!test +%! % Notch filter removes target frequency +%! Fs = 4000; fc = 300; Q = 5; +%! [b, a] = PSbfFilters('notch', fc, Fs, Q); +%! t = (0:8000-1)'/Fs; +%! sig = sin(2*pi*300*t) + sin(2*pi*100*t); +%! out = filter(b, a, sig); +%! % 300 Hz should be removed, 100 Hz should remain +%! N = length(out); f = (0:N/2)*Fs/N; +%! Y = abs(fft(out .* hann(N))); +%! Y = Y(1:N/2+1); +%! [~, i100] = min(abs(f - 100)); +%! [~, i300] = min(abs(f - 300)); +%! assert(Y(i100) > Y(i300) * 5); % 100 Hz much stronger than 300 Hz + +%!test +%! % Edge case: fc=0 returns passthrough +%! [b, a] = PSbfFilters('pt1', 0, 4000); +%! assert(b, 1); +%! assert(a, 1); + +%!test +%! % Biquad is second-order lowpass +%! Fs = 4000; fc = 200; +%! [b, a] = PSbfFilters('biquad', fc, Fs); +%! assert(length(b), 3); +%! assert(length(a), 3); +%! % DC gain should be ~1 +%! dc_gain = sum(b) / sum(a); +%! assert(abs(dc_gain - 1) < 0.01); diff --git a/tests/test_PSdebugModeIndices.m b/tests/test_PSdebugModeIndices.m index 864a632..8bbc3ce 100644 --- a/tests/test_PSdebugModeIndices.m +++ b/tests/test_PSdebugModeIndices.m @@ -1,34 +1,34 @@ -% test_PSdebugModeIndices.m - tests for PSdebugModeIndices - -%!test -%! % BF 4.5 (old indices) -%! idx = PSdebugModeIndices('Betaflight', 4, 5); -%! assert(idx.GYRO_SCALED, 6); -%! assert(idx.GYRO_FILTERED, 3); -%! assert(idx.RC_INTERPOLATION, 7); -%! assert(idx.FFT_FREQ, 17); -%! assert(idx.RPM_FILTER, 46); -%! assert(idx.FEEDFORWARD, 59); - -%!test -%! % BF 2025.12 (new indices, GYRO_SCALED removed) -%! idx = PSdebugModeIndices('Betaflight', 2025, 12); -%! assert(idx.GYRO_SCALED, -1); -%! assert(idx.GYRO_FILTERED, 3); -%! assert(idx.RC_INTERPOLATION, 6); -%! assert(idx.FFT_FREQ, 16); -%! assert(idx.RPM_FILTER, 45); -%! assert(idx.FEEDFORWARD, 58); - -%!test -%! % INAV (uses default old indices) -%! idx = PSdebugModeIndices('INAV', 7, 1); -%! assert(idx.GYRO_SCALED, 6); -%! assert(idx.RC_INTERPOLATION, 7); -%! assert(idx.FFT_FREQ, 17); - -%!test -%! % Emuflight (uses default old indices) -%! idx = PSdebugModeIndices('Emuflight', 0, 4); -%! assert(idx.GYRO_SCALED, 6); -%! assert(idx.FFT_FREQ, 17); +% test_PSdebugModeIndices.m - tests for PSdebugModeIndices + +%!test +%! % BF 4.5 (old indices) +%! idx = PSdebugModeIndices('Betaflight', 4, 5); +%! assert(idx.GYRO_SCALED, 6); +%! assert(idx.GYRO_FILTERED, 3); +%! assert(idx.RC_INTERPOLATION, 7); +%! assert(idx.FFT_FREQ, 17); +%! assert(idx.RPM_FILTER, 46); +%! assert(idx.FEEDFORWARD, 59); + +%!test +%! % BF 2025.12 (new indices, GYRO_SCALED removed) +%! idx = PSdebugModeIndices('Betaflight', 2025, 12); +%! assert(idx.GYRO_SCALED, -1); +%! assert(idx.GYRO_FILTERED, 3); +%! assert(idx.RC_INTERPOLATION, 6); +%! assert(idx.FFT_FREQ, 16); +%! assert(idx.RPM_FILTER, 45); +%! assert(idx.FEEDFORWARD, 58); + +%!test +%! % INAV (uses default old indices) +%! idx = PSdebugModeIndices('INAV', 7, 1); +%! assert(idx.GYRO_SCALED, 6); +%! assert(idx.RC_INTERPOLATION, 7); +%! assert(idx.FFT_FREQ, 17); + +%!test +%! % Emuflight (uses default old indices) +%! idx = PSdebugModeIndices('Emuflight', 0, 4); +%! assert(idx.GYRO_SCALED, 6); +%! assert(idx.FFT_FREQ, 17); diff --git a/tests/test_PSestimateRPM.m b/tests/test_PSestimateRPM.m index d7f6656..86a0e9e 100644 --- a/tests/test_PSestimateRPM.m +++ b/tests/test_PSestimateRPM.m @@ -1,35 +1,35 @@ -% test_PSestimateRPM.m - tests for PSestimateRPM - -%!test -%! % Synthetic motor noise at 200 Hz fundamental -%! freqAxis = 1:500; -%! ampMatrix = zeros(100, 500); -%! for t = 30:80 -%! f0 = 150 + t; % fundamental scales with throttle -%! ampMatrix(t, round(f0)) = 10; % fundamental -%! ampMatrix(t, round(f0*2)) = 5; % 2nd harmonic -%! end -%! [fund, harm] = PSestimateRPM(freqAxis, ampMatrix, 3); -%! % check mid-throttle detection -%! assert(~isnan(fund(50))); -%! assert(abs(fund(50) - 200) < 20); % should be near 200 Hz at throttle=50 - -%!test -%! % Empty matrix returns NaN -%! freqAxis = 1:100; -%! ampMatrix = zeros(100, 100); -%! [fund, harm] = PSestimateRPM(freqAxis, ampMatrix, 2); -%! assert(all(isnan(fund))); -%! assert(size(harm, 2), 2); - -%!test -%! % Harmonics are multiples of fundamental -%! freqAxis = 1:500; -%! ampMatrix = zeros(100, 500); -%! ampMatrix(50, 180) = 10; -%! ampMatrix(50, 360) = 5; -%! [fund, harm] = PSestimateRPM(freqAxis, ampMatrix, 3); -%! assert(~isnan(fund(50))); -%! assert(size(harm), [100 3]); -%! % 2nd harmonic should be ~2x fundamental -%! assert(abs(harm(50,2) / harm(50,1) - 2) < 0.1); +% test_PSestimateRPM.m - tests for PSestimateRPM + +%!test +%! % Synthetic motor noise at 200 Hz fundamental +%! freqAxis = 1:500; +%! ampMatrix = zeros(100, 500); +%! for t = 30:80 +%! f0 = 150 + t; % fundamental scales with throttle +%! ampMatrix(t, round(f0)) = 10; % fundamental +%! ampMatrix(t, round(f0*2)) = 5; % 2nd harmonic +%! end +%! [fund, harm] = PSestimateRPM(freqAxis, ampMatrix, 3); +%! % check mid-throttle detection +%! assert(~isnan(fund(50))); +%! assert(abs(fund(50) - 200) < 20); % should be near 200 Hz at throttle=50 + +%!test +%! % Empty matrix returns NaN +%! freqAxis = 1:100; +%! ampMatrix = zeros(100, 100); +%! [fund, harm] = PSestimateRPM(freqAxis, ampMatrix, 2); +%! assert(all(isnan(fund))); +%! assert(size(harm, 2), 2); + +%!test +%! % Harmonics are multiples of fundamental +%! freqAxis = 1:500; +%! ampMatrix = zeros(100, 500); +%! ampMatrix(50, 180) = 10; +%! ampMatrix(50, 360) = 5; +%! [fund, harm] = PSestimateRPM(freqAxis, ampMatrix, 3); +%! assert(~isnan(fund(50))); +%! assert(size(harm), [100 3]); +%! % 2nd harmonic should be ~2x fundamental +%! assert(abs(harm(50,2) / harm(50,1) - 2) < 0.1); diff --git a/tests/test_PSfilterSim.m b/tests/test_PSfilterSim.m new file mode 100644 index 0000000..2726227 --- /dev/null +++ b/tests/test_PSfilterSim.m @@ -0,0 +1,191 @@ +% test_PSfilterSim.m - verify filter response calculations used in PSfilterSim + +%!test +%! % PT1 at cutoff: forward Euler shifts -3dB point, expect -3 to -4 dB +%! Fs = 4000; fc = 200; +%! [b, a] = PSbfFilters('pt1', fc, Fs); +%! [H, f] = freqz(b, a, 4096, Fs); +%! [~, ifc] = min(abs(f - fc)); +%! mag_at_fc = 20*log10(abs(H(ifc))); +%! assert(mag_at_fc, -3, 1.0); + +%!test +%! % PT2 at cutoff: BF corrects fc so -3dB lands near nominal fc +%! Fs = 4000; fc = 200; +%! [b, a] = PSbfFilters('pt2', fc, Fs); +%! [H, f] = freqz(b, a, 4096, Fs); +%! [~, ifc] = min(abs(f - fc)); +%! mag_at_fc = 20*log10(abs(H(ifc))); +%! assert(mag_at_fc, -3, 1.5); + +%!test +%! % PT3 at cutoff: BF corrects fc so -3dB lands near nominal fc +%! Fs = 4000; fc = 200; +%! [b, a] = PSbfFilters('pt3', fc, Fs); +%! [H, f] = freqz(b, a, 4096, Fs); +%! [~, ifc] = min(abs(f - fc)); +%! mag_at_fc = 20*log10(abs(H(ifc))); +%! assert(mag_at_fc, -3, 2.0); + +%!test +%! % DC gain of all lowpass types should be 0 dB +%! Fs = 4000; fc = 200; +%! for t = {'pt1', 'pt2', 'pt3', 'biquad'} +%! [b, a] = PSbfFilters(t{1}, fc, Fs); +%! [H, ~] = freqz(b, a, 4096, Fs); +%! dc_dB = 20*log10(abs(H(1))); +%! assert(abs(dc_dB) < 0.01); +%! end + +%!test +%! % Notch filter: near-zero magnitude at center, unity elsewhere +%! Fs = 4000; fc = 300; Q = 5; +%! [b, a] = PSbfFilters('notch', fc, Fs, Q); +%! [H, f] = freqz(b, a, 4096, Fs); +%! [~, ifc] = min(abs(f - fc)); +%! mag_notch = 20*log10(abs(H(ifc))); +%! mag_dc = 20*log10(abs(H(1))); +%! assert(mag_notch < -20); +%! assert(abs(mag_dc) < 0.01); + +%!test +%! % Cascaded H multiplication matches sequential filtering +%! Fs = 4000; +%! [b1, a1] = PSbfFilters('pt1', 200, Fs); +%! [b2, a2] = PSbfFilters('pt2', 300, Fs); +%! Nfft = 2048; +%! [H1, ~] = freqz(b1, a1, Nfft, Fs); +%! [H2, ~] = freqz(b2, a2, Nfft, Fs); +%! H_cascade = H1(:) .* H2(:); +%! imp = [1; zeros(Nfft*2-1, 1)]; +%! out = filter(b1, a1, imp); +%! out = filter(b2, a2, out); +%! H_seq = fft(out, Nfft*2); +%! H_seq = H_seq(1:Nfft); +%! mag_cascade = 20*log10(abs(H_cascade)); +%! mag_seq = 20*log10(abs(H_seq)); +%! assert(max(abs(mag_cascade - mag_seq)) < 0.5); + +%!test +%! % Group delay of PT1 at DC should be ~1/(2*pi*fc) seconds +%! Fs = 8000; fc = 200; +%! [b, a] = PSbfFilters('pt1', fc, Fs); +%! [H, f] = freqz(b, a, 4096, Fs); +%! f = f(:); +%! dw = gradient(2*pi*f); +%! gd_s = -gradient(unwrap(angle(H(:)))) ./ dw; +%! gd_ms = gd_s * 1000; +%! gd_dc = gd_ms(2); % skip f=0 edge artifact +%! expected_ms = 1/(2*pi*fc) * 1000; +%! assert(gd_dc, expected_ms, 0.15); + +%!test +%! % Step response of PT1 should reach ~63.2% at t=RC +%! Fs = 8000; fc = 200; +%! [b, a] = PSbfFilters('pt1', fc, Fs); +%! stepIn = ones(round(Fs * 0.050), 1); +%! stepOut = filter(b, a, stepIn); +%! RC_samples = round(1/(2*pi*fc) * Fs); +%! val_at_RC = stepOut(RC_samples); +%! assert(val_at_RC, 0.632, 0.05); + +%!test +%! % Step response of lowpass should settle to 1.0 +%! Fs = 4000; fc = 200; +%! [b, a] = PSbfFilters('pt2', fc, Fs); +%! stepIn = ones(round(Fs * 0.1), 1); +%! stepOut = filter(b, a, stepIn); +%! assert(stepOut(end), 1.0, 0.01); + +%!test +%! % PT1 phase is 0 at DC and negative in passband +%! Fs = 4000; fc = 200; +%! [b, a] = PSbfFilters('pt1', fc, Fs); +%! [H, f] = freqz(b, a, 512, Fs); +%! ph = angle(H) * 180/pi; +%! assert(abs(ph(1)) < 0.1); % 0 at DC +%! [~, ifc] = min(abs(f - fc)); +%! assert(ph(ifc) < 0); % negative at cutoff + +%!test +%! % PT1 phase at cutoff should be -45 degrees +%! Fs = 8000; fc = 200; +%! [b, a] = PSbfFilters('pt1', fc, Fs); +%! [H, f] = freqz(b, a, 4096, Fs); +%! [~, ifc] = min(abs(f - fc)); +%! ph_deg = angle(H(ifc)) * 180/pi; +%! assert(ph_deg, -45, 3); + +%!test +%! % Emuflight per-axis keys: dterm_lowpass_hz_roll, gyro_lowpass_hz_roll +%! si = { +%! 'Firmware revision', 'EmuFlight 0.4.1 HELIOSPRING'; +%! 'gyro_lowpass_type', '0'; +%! 'gyro_lowpass_hz_roll', '200'; +%! 'gyro_lowpass_hz_pitch', '200'; +%! 'gyro_lowpass_hz_yaw', '200'; +%! 'gyro_lowpass2_type', '0'; +%! 'gyro_lowpass2_hz_roll', '150'; +%! 'gyro_lowpass2_hz_pitch', '150'; +%! 'gyro_lowpass2_hz_yaw', '150'; +%! 'dterm_lowpass_hz_roll', '110'; +%! 'dterm_lowpass_hz_pitch', '110'; +%! 'dterm_lowpass_hz_yaw', '110'; +%! 'dterm_lowpass2_hz_roll', '185'; +%! 'dterm_lowpass2_hz_pitch', '185'; +%! 'dterm_lowpass2_hz_yaw', '185'; +%! 'dterm_notch_hz', '0'; +%! 'dterm_notch_cutoff', '0'; +%! 'gyro_notch_hz', '0,0'; +%! 'gyro_notch_cutoff', '0,0'; +%! }; +%! fp = PSparseFilterParams(si); +%! assert(fp.gyro_lpf1_hz, 200); +%! assert(fp.gyro_lpf2_hz, 150); +%! assert(fp.dterm_lpf1_hz, 110); +%! assert(fp.dterm_lpf2_hz, 185); + +%!test +%! % PSparseFilterParams must return gyro loop rate from headers +%! % looptime:125 = 125us = 8kHz gyro rate +%! si = { +%! 'looptime', '125'; +%! 'gyro_sync_denom', '1'; +%! 'pid_process_denom', '1'; +%! 'gyro_lowpass_type', '0'; +%! 'gyro_lowpass_hz', '300'; +%! 'dterm_lowpass_hz', '100'; +%! 'gyro_notch_hz', '0,0'; +%! 'gyro_notch_cutoff', '0,0'; +%! 'dterm_notch_hz', '0'; +%! 'dterm_notch_cutoff', '0'; +%! }; +%! fp = PSparseFilterParams(si); +%! assert(fp.gyro_rate_hz, 8000, 'looptime:125 should give 8kHz gyro rate'); + +%!test +%! % looptime:250 with gyro_sync_denom:2 = 500us gyro, 1000us pid -> gyro 4kHz +%! si = { +%! 'looptime', '250'; +%! 'gyro_sync_denom', '2'; +%! 'pid_process_denom', '2'; +%! 'gyro_lowpass_hz', '300'; +%! 'gyro_notch_hz', '0,0'; +%! 'gyro_notch_cutoff', '0,0'; +%! 'dterm_notch_hz', '0'; +%! 'dterm_notch_cutoff', '0'; +%! }; +%! fp = PSparseFilterParams(si); +%! assert(fp.gyro_rate_hz, 4000, 'looptime 250us / gyro_sync_denom 2 = 4kHz'); + +%!test +%! % No looptime header -> gyro_rate_hz should be 0 (use log rate as fallback) +%! si = { +%! 'gyro_lowpass_hz', '300'; +%! 'gyro_notch_hz', '0,0'; +%! 'gyro_notch_cutoff', '0,0'; +%! 'dterm_notch_hz', '0'; +%! 'dterm_notch_cutoff', '0'; +%! }; +%! fp = PSparseFilterParams(si); +%! assert(fp.gyro_rate_hz, 0, 'no looptime header -> gyro_rate_hz = 0'); diff --git a/tests/test_PSparseBFversion.m b/tests/test_PSparseBFversion.m index ba2d6ad..39719c1 100644 --- a/tests/test_PSparseBFversion.m +++ b/tests/test_PSparseBFversion.m @@ -1,47 +1,55 @@ -% test_PSparseBFversion.m - tests for PSparseBFversion - -%!test -%! % BF 4.5.3 -%! si = {'Firmware version', ' Betaflight / STM32F405 4.5.3 Dec 14 2024 / 11:27:01'}; -%! [t, maj, mnr] = PSparseBFversion(si); -%! assert(strcmp(t, 'Betaflight')); -%! assert(maj, 4); -%! assert(mnr, 5); - -%!test -%! % BF 2025.12.2 (CalVer) -%! si = {'Firmware version', ' Betaflight / STM32H743 2025.12.2 Jan 5 2026 / 09:15:00'}; -%! [t, maj, mnr] = PSparseBFversion(si); -%! assert(strcmp(t, 'Betaflight')); -%! assert(maj, 2025); -%! assert(mnr, 12); - -%!test -%! % INAV 7.1.0 -%! si = {'Firmware version', ' INAV / STM32F405 7.1.0 Aug 2024'}; -%! [t, maj, mnr] = PSparseBFversion(si); -%! assert(strcmp(t, 'INAV')); -%! assert(maj, 7); -%! assert(mnr, 1); - -%!test -%! % Emuflight 0.4.1 -%! si = {'Firmware version', ' Emuflight / STM32F411 0.4.1 Mar 2023'}; -%! [t, maj, mnr] = PSparseBFversion(si); -%! assert(strcmp(t, 'Emuflight')); - -%!test -%! % Malformed / missing version row -%! si = {'other_param', 'some_value'}; -%! [t, maj, mnr] = PSparseBFversion(si); -%! assert(strcmp(t, 'Unknown')); -%! assert(maj, 0); -%! assert(mnr, 0); - -%!test -%! % Firmware revision (alternate key) -%! si = {'Firmware revision', ' Betaflight / STM32F7X2 4.4.0 Jun 2024'}; -%! [t, maj, mnr] = PSparseBFversion(si); -%! assert(strcmp(t, 'Betaflight')); -%! assert(maj, 4); -%! assert(mnr, 4); +% test_PSparseBFversion.m - tests for PSparseBFversion + +%!test +%! % BF 4.5.3 +%! si = {'Firmware version', ' Betaflight / STM32F405 4.5.3 Dec 14 2024 / 11:27:01'}; +%! [t, maj, mnr] = PSparseBFversion(si); +%! assert(strcmp(t, 'Betaflight')); +%! assert(maj, 4); +%! assert(mnr, 5); + +%!test +%! % BF 2025.12.2 (CalVer) +%! si = {'Firmware version', ' Betaflight / STM32H743 2025.12.2 Jan 5 2026 / 09:15:00'}; +%! [t, maj, mnr] = PSparseBFversion(si); +%! assert(strcmp(t, 'Betaflight')); +%! assert(maj, 2025); +%! assert(mnr, 12); + +%!test +%! % INAV 7.1.0 +%! si = {'Firmware version', ' INAV / STM32F405 7.1.0 Aug 2024'}; +%! [t, maj, mnr] = PSparseBFversion(si); +%! assert(strcmp(t, 'INAV')); +%! assert(maj, 7); +%! assert(mnr, 1); + +%!test +%! % Emuflight 0.4.1 +%! si = {'Firmware version', ' Emuflight / STM32F411 0.4.1 Mar 2023'}; +%! [t, maj, mnr] = PSparseBFversion(si); +%! assert(strcmp(t, 'Emuflight')); + +%!test +%! % Malformed / missing version row +%! si = {'other_param', 'some_value'}; +%! [t, maj, mnr] = PSparseBFversion(si); +%! assert(strcmp(t, 'Unknown')); +%! assert(maj, 0); +%! assert(mnr, 0); + +%!test +%! % Firmware revision (alternate key) +%! si = {'Firmware revision', ' Betaflight / STM32F7X2 4.4.0 Jun 2024'}; +%! [t, maj, mnr] = PSparseBFversion(si); +%! assert(strcmp(t, 'Betaflight')); +%! assert(maj, 4); +%! assert(mnr, 4); + +%!test +%! % BF 2025.12 new header format (no slash, from real BBL) +%! si = {'Firmware revision', 'Betaflight 2025.12.0-beta (080b2f5da) STM32F405'}; +%! [t, maj, mnr] = PSparseBFversion(si); +%! assert(strcmp(t, 'Betaflight'), true, 'fwType should be Betaflight'); +%! assert(maj, 2025); +%! assert(mnr, 12); diff --git a/tests/test_PSquicJson2csv.m b/tests/test_PSquicJson2csv.m index 6ee2880..fe6b096 100644 --- a/tests/test_PSquicJson2csv.m +++ b/tests/test_PSquicJson2csv.m @@ -1,104 +1,104 @@ -%% Tests for PSquicJson2csv - QuickSilver JSON blackbox parser - -%!test -%! % Create a minimal QuickSilver JSON file -%! jsonFile = [tempname() '.json']; -%! RAD2DEG = 180 / pi; -%! fid = fopen(jsonFile, 'w'); -%! fprintf(fid, '{\n'); -%! fprintf(fid, ' "blackbox_rate": 1,\n'); -%! fprintf(fid, ' "looptime": 125.0,\n'); -%! fprintf(fid, ' "fields": ["loop", "time", "gyro_filter", "setpoint", "motor"],\n'); -%! fprintf(fid, ' "entries": [\n'); -%! % Entry 1: loop=0, time=1000, gyro=[1000,0,0] (1 rad/s), setpoint=[500,0,0,800], motor=[500,500,500,500] -%! fprintf(fid, ' [0, 1000, [1000, 0, 0], [500, 0, 0, 800], [500, 500, 500, 500]],\n'); -%! % Entry 2: loop=1, time=2000 -%! fprintf(fid, ' [1, 2000, [2000, 1000, -500], [1000, 500, -250, 900], [600, 600, 600, 600]]\n'); -%! fprintf(fid, ' ]\n'); -%! fprintf(fid, '}\n'); -%! fclose(fid); -%! -%! [headerFile, csvFile] = PSquicJson2csv(jsonFile); -%! -%! % Check files were created -%! assert(exist(csvFile, 'file') == 2); -%! assert(exist(headerFile, 'file') == 2); -%! -%! % Check CSV has correct number of rows (header + 2 data rows) -%! fid = fopen(csvFile, 'r'); -%! lines = {}; -%! while ~feof(fid) -%! l = fgetl(fid); -%! if ischar(l) && ~isempty(strtrim(l)), lines{end+1} = l; end -%! end -%! fclose(fid); -%! assert(numel(lines) >= 3, 'Expected at least 3 lines (header + 2 data)'); -%! -%! % Check header file has required fields -%! hdr = fileread(headerFile); -%! assert(~isempty(strfind(hdr, 'Firmware version'))); -%! assert(~isempty(strfind(hdr, 'debug_mode'))); -%! assert(~isempty(strfind(hdr, 'rollPID'))); -%! -%! % Read CSV with readtable and check values -%! addpath(genpath(fullfile(fileparts(fileparts(mfilename('fullpath'))), 'src'))); -%! T = readtable(csvFile); -%! -%! % Check loop counter -%! assert(T.loopIteration(1), 0); -%! assert(T.loopIteration(2), 1); -%! -%! % Check time -%! assert(T.time_us_(1), 1000); -%! -%! % Check gyro conversion: 1000 / 1000 * RAD2DEG = 57.296 deg/s -%! assert(abs(T.gyroADC_0_(1) - RAD2DEG) < 0.1, 'Gyro should be converted to deg/s'); -%! -%! % Check motor conversion: 500 / 1000 * 2000 = 1000 -%! assert(T.motor_0_(1), 1000); -%! -%! % Cleanup -%! delete(jsonFile); -%! delete(csvFile); -%! delete(headerFile); - -%!test -%! % Test with PID terms -%! jsonFile = [tempname() '.json']; -%! fid = fopen(jsonFile, 'w'); -%! fprintf(fid, '{"blackbox_rate":1,"looptime":250,"fields":["loop","time","pid_p_term","pid_i_term","pid_d_term"],"entries":[[0,100,[300,-150,50],[100,200,50],[80,40,0]]]}'); -%! fclose(fid); -%! -%! [headerFile, csvFile] = PSquicJson2csv(jsonFile); -%! addpath(genpath(fullfile(fileparts(fileparts(mfilename('fullpath'))), 'src'))); -%! T = readtable(csvFile); -%! -%! % PID P: 300/1000 = 0.3 -%! assert(abs(T.axisP_0_(1) - 0.3) < 0.001); -%! assert(abs(T.axisP_1_(1) - (-0.15)) < 0.001); -%! -%! delete(jsonFile); delete(csvFile); delete(headerFile); - -%!test -%! % Test yaw negation on setpoint and gyro -%! jsonFile = [tempname() '.json']; -%! RAD2DEG = 180 / pi; -%! fid = fopen(jsonFile, 'w'); -%! fprintf(fid, '{"blackbox_rate":1,"looptime":125,"fields":["loop","time","setpoint","gyro_filter"],"entries":[[0,100,[0,0,1000,500],[0,0,1000]]]}'); -%! fclose(fid); -%! -%! [headerFile, csvFile] = PSquicJson2csv(jsonFile); -%! addpath(genpath(fullfile(fileparts(fileparts(mfilename('fullpath'))), 'src'))); -%! T = readtable(csvFile); -%! -%! % Yaw setpoint: 1000/1000 * RAD2DEG = 57.296, but NEGATED -> -57.296 -%! assert(T.setpoint_2_(1) < 0, 'Yaw setpoint should be negated'); -%! assert(abs(T.setpoint_2_(1) + RAD2DEG) < 0.1); -%! -%! % Yaw gyro: same negation -%! assert(T.gyroADC_2_(1) < 0, 'Yaw gyro should be negated'); -%! -%! % Throttle setpoint: 500/1000 * 1000 = 500 -%! assert(T.setpoint_3_(1), 500); -%! -%! delete(jsonFile); delete(csvFile); delete(headerFile); +%% Tests for PSquicJson2csv - QuickSilver JSON blackbox parser + +%!test +%! % Create a minimal QuickSilver JSON file +%! jsonFile = [tempname() '.json']; +%! RAD2DEG = 180 / pi; +%! fid = fopen(jsonFile, 'w'); +%! fprintf(fid, '{\n'); +%! fprintf(fid, ' "blackbox_rate": 1,\n'); +%! fprintf(fid, ' "looptime": 125.0,\n'); +%! fprintf(fid, ' "fields": ["loop", "time", "gyro_filter", "setpoint", "motor"],\n'); +%! fprintf(fid, ' "entries": [\n'); +%! % Entry 1: loop=0, time=1000, gyro=[1000,0,0] (1 rad/s), setpoint=[500,0,0,800], motor=[500,500,500,500] +%! fprintf(fid, ' [0, 1000, [1000, 0, 0], [500, 0, 0, 800], [500, 500, 500, 500]],\n'); +%! % Entry 2: loop=1, time=2000 +%! fprintf(fid, ' [1, 2000, [2000, 1000, -500], [1000, 500, -250, 900], [600, 600, 600, 600]]\n'); +%! fprintf(fid, ' ]\n'); +%! fprintf(fid, '}\n'); +%! fclose(fid); +%! +%! [headerFile, csvFile] = PSquicJson2csv(jsonFile); +%! +%! % Check files were created +%! assert(exist(csvFile, 'file') == 2); +%! assert(exist(headerFile, 'file') == 2); +%! +%! % Check CSV has correct number of rows (header + 2 data rows) +%! fid = fopen(csvFile, 'r'); +%! lines = {}; +%! while ~feof(fid) +%! l = fgetl(fid); +%! if ischar(l) && ~isempty(strtrim(l)), lines{end+1} = l; end +%! end +%! fclose(fid); +%! assert(numel(lines) >= 3, 'Expected at least 3 lines (header + 2 data)'); +%! +%! % Check header file has required fields +%! hdr = fileread(headerFile); +%! assert(~isempty(strfind(hdr, 'Firmware version'))); +%! assert(~isempty(strfind(hdr, 'debug_mode'))); +%! assert(~isempty(strfind(hdr, 'rollPID'))); +%! +%! % Read CSV with readtable and check values +%! addpath(genpath(fullfile(fileparts(fileparts(mfilename('fullpath'))), 'src'))); +%! T = readtable(csvFile); +%! +%! % Check loop counter +%! assert(T.loopIteration(1), 0); +%! assert(T.loopIteration(2), 1); +%! +%! % Check time +%! assert(T.time_us_(1), 1000); +%! +%! % Check gyro conversion: 1000 / 1000 * RAD2DEG = 57.296 deg/s +%! assert(abs(T.gyroADC_0_(1) - RAD2DEG) < 0.1, 'Gyro should be converted to deg/s'); +%! +%! % Check motor conversion: 500 / 1000 * 2000 = 1000 +%! assert(T.motor_0_(1), 1000); +%! +%! % Cleanup +%! delete(jsonFile); +%! delete(csvFile); +%! delete(headerFile); + +%!test +%! % Test with PID terms +%! jsonFile = [tempname() '.json']; +%! fid = fopen(jsonFile, 'w'); +%! fprintf(fid, '{"blackbox_rate":1,"looptime":250,"fields":["loop","time","pid_p_term","pid_i_term","pid_d_term"],"entries":[[0,100,[300,-150,50],[100,200,50],[80,40,0]]]}'); +%! fclose(fid); +%! +%! [headerFile, csvFile] = PSquicJson2csv(jsonFile); +%! addpath(genpath(fullfile(fileparts(fileparts(mfilename('fullpath'))), 'src'))); +%! T = readtable(csvFile); +%! +%! % PID P: 300/1000 = 0.3 +%! assert(abs(T.axisP_0_(1) - 0.3) < 0.001); +%! assert(abs(T.axisP_1_(1) - (-0.15)) < 0.001); +%! +%! delete(jsonFile); delete(csvFile); delete(headerFile); + +%!test +%! % Test yaw negation on setpoint and gyro +%! jsonFile = [tempname() '.json']; +%! RAD2DEG = 180 / pi; +%! fid = fopen(jsonFile, 'w'); +%! fprintf(fid, '{"blackbox_rate":1,"looptime":125,"fields":["loop","time","setpoint","gyro_filter"],"entries":[[0,100,[0,0,1000,500],[0,0,1000]]]}'); +%! fclose(fid); +%! +%! [headerFile, csvFile] = PSquicJson2csv(jsonFile); +%! addpath(genpath(fullfile(fileparts(fileparts(mfilename('fullpath'))), 'src'))); +%! T = readtable(csvFile); +%! +%! % Yaw setpoint: 1000/1000 * RAD2DEG = 57.296, but NEGATED -> -57.296 +%! assert(T.setpoint_2_(1) < 0, 'Yaw setpoint should be negated'); +%! assert(abs(T.setpoint_2_(1) + RAD2DEG) < 0.1); +%! +%! % Yaw gyro: same negation +%! assert(T.gyroADC_2_(1) < 0, 'Yaw gyro should be negated'); +%! +%! % Throttle setpoint: 500/1000 * 1000 = 500 +%! assert(T.setpoint_3_(1), 500); +%! +%! delete(jsonFile); delete(csvFile); delete(headerFile); diff --git a/tests/test_PSsliderToolMath.m b/tests/test_PSsliderToolMath.m new file mode 100644 index 0000000..b3a1d89 --- /dev/null +++ b/tests/test_PSsliderToolMath.m @@ -0,0 +1,100 @@ +% test_PSsliderToolMath.m — verify PTB and BF slider math formulas + +%!function [rP,rI,rD, pP,pI,pD, yP,yI,yD] = ptb_calc(P0,I0,D0,yawD0, pd,pir,rpr,ywr,mst) +%! rP = P0 * mst; +%! rI = I0 * pir * mst; +%! rD = D0 * pd * mst; +%! pP = rP * rpr; +%! pI = rI * rpr; +%! pD = rD * rpr; +%! yP = P0 * mst * ywr; +%! yI = I0 * mst * ywr; +%! yD = yawD0 * pd * mst * ywr; +%!endfunction + +%!function [rP,rI,rD, pP,pI,pD, yP,yI,yD] = bf_calc(P0,I0,D0,yawD0, dg,pig,rpr,ywr,mst) +%! rP = P0 * pig * mst; +%! rI = I0 * pig * mst; +%! rD = D0 * dg * mst; +%! pP = rP * rpr; +%! pI = rI * rpr; +%! pD = rD * rpr; +%! yP = rP * ywr; +%! yI = rI * ywr; +%! yD = yawD0 * dg * mst * ywr; +%!endfunction + +%!test +%! % PTB: PD=0.75, all others default — only D changes +%! [rP,rI,rD,pP,pI,pD,yP,yI,yD] = ptb_calc(4,0.05,15,2, 0.75,1.00,1.00,1.00,1.00); +%! assert(rP, 4, 0.01); +%! assert(rI, 0.050, 0.001); +%! assert(rD, 11.25, 0.01); +%! assert(pP, 4, 0.01); +%! assert(pI, 0.050, 0.001); +%! assert(pD, 11.25, 0.01); +%! assert(yP, 4, 0.01); +%! assert(yI, 0.050, 0.001); +%! assert(yD, 1.50, 0.01); + +%!test +%! % PTB: PD=1.35, PI=0.90 — Yaw I = 0.050 (PI ratio excluded from Yaw) +%! [rP,rI,rD,pP,pI,pD,yP,yI,yD] = ptb_calc(4,0.05,15,2, 1.35,0.90,1.00,1.00,1.00); +%! assert(rP, 4, 0.01); +%! assert(rI, 0.045, 0.001); +%! assert(rD, 20.25, 0.01); +%! assert(yI, 0.050, 0.001); +%! assert(yD, 2.70, 0.01); + +%!test +%! % PTB: PD=1.35, PI=1.20, RP=1.40 — Pitch P round(5.6)=6 +%! [rP,rI,rD,pP,pI,pD,yP,yI,yD] = ptb_calc(4,0.05,15,2, 1.35,1.20,1.40,1.00,1.00); +%! assert(rP, 4, 0.01); +%! assert(rI, 0.060, 0.001); +%! assert(round(pP), 6); +%! assert(pI, 0.084, 0.001); +%! assert(pD, 28.35, 0.01); +%! assert(yI, 0.050, 0.001); + +%!test +%! % PTB: all sliders non-default — P rounding: 5.4→5, 7.02→7, 7.29→7 +%! [rP,rI,rD,pP,pI,pD,yP,yI,yD] = ptb_calc(4,0.05,15,2, 1.35,1.20,1.30,1.35,1.35); +%! assert(round(rP), 5); +%! assert(rI, 0.081, 0.001); +%! assert(rD, 27.34, 0.02); +%! assert(round(pP), 7); +%! assert(pI, 0.105, 0.002); +%! assert(pD, 35.54, 0.02); +%! assert(round(yP), 7); +%! assert(yI, 0.091, 0.001); +%! assert(yD, 4.92, 0.02); + +%!test +%! % PTB: PD=1.35, PI=1.20, RP=1.30, Yaw=1.05, Master=1.45 +%! [rP,rI,rD,pP,pI,pD,yP,yI,yD] = ptb_calc(4,0.05,15,2, 1.35,1.20,1.30,1.05,1.45); +%! assert(round(rP), 6); +%! assert(rI, 0.087, 0.001); +%! assert(rD, 29.36, 0.02); +%! assert(round(pP), 8); +%! assert(pI, 0.113, 0.002); +%! assert(pD, 38.17, 0.02); +%! assert(round(yP), 6); +%! assert(yI, 0.076, 0.001); +%! assert(yD, 4.11, 0.02); + +%!test +%! % BF: PI Gain=0.90 scales P AND I together, Yaw I follows PI Gain +%! [rP,rI,rD,pP,pI,pD,yP,yI,yD] = bf_calc(4,0.05,15,2, 1.35,0.90,1.00,1.00,1.00); +%! assert(rP, 3.6, 0.01); +%! assert(rI, 0.045, 0.001); +%! assert(rD, 20.25, 0.01); +%! assert(yI, 0.045, 0.001); + +%!test +%! % BF: full combination — PD=1.35, PI=1.20, RP=1.30, Yaw=1.05, Master=1.45 +%! [rP,rI,rD,pP,pI,pD,yP,yI,yD] = bf_calc(4,0.05,15,2, 1.35,1.20,1.30,1.05,1.45); +%! assert(rP, 6.96, 0.01); +%! assert(rI, 0.087, 0.001); +%! assert(rD, 29.3625, 0.01); +%! assert(pP, 9.048, 0.01); +%! assert(yI, 0.09135, 0.001); diff --git a/tests/test_PSstepcalc.m b/tests/test_PSstepcalc.m index 0370e64..e5f4657 100644 --- a/tests/test_PSstepcalc.m +++ b/tests/test_PSstepcalc.m @@ -1,54 +1,122 @@ -% test_PSstepcalc.m - Tests for step response deconvolution -% NOTE: PSstepcalc expects SP,GY as column vectors, lograte in kHz - -%!test -%! % Smoke test: function runs without error -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 5; % 5 seconds -%! sp = zeros(N, 1); -%! gy = zeros(N, 1); -%! % Create step inputs above 20 deg/s threshold -%! for k = 1:3 -%! st = round(k * Fs + 100); -%! en = min(st + round(0.4*Fs), N); -%! sp(st:en) = 300; -%! gy(st:en) = 285; -%! end -%! [stepresponse, t] = PSstepcalc(sp, gy, Fs_khz, 0, 1); -%! assert(~isempty(t)); - -%!test -%! % Time vector starts at 0 and ends at 500ms -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 5; -%! sp = zeros(N, 1); -%! gy = zeros(N, 1); -%! for k = 1:3 -%! st = round(k * Fs + 100); -%! en = min(st + round(0.4*Fs), N); -%! sp(st:en) = 300; -%! gy(st:en) = 280; -%! end -%! [~, t] = PSstepcalc(sp, gy, Fs_khz, 0, 1); -%! assert(t(1), 0, 1e-10); -%! assert(t(end), 500, 1); - -%!test -%! % Step response values are finite when valid segments exist -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 5; -%! sp = zeros(N, 1); -%! gy = zeros(N, 1); -%! for k = 1:3 -%! st = round(k * Fs + 100); -%! en = min(st + round(0.4*Fs), N); -%! sp(st:en) = 400; -%! gy(st:en) = 380; -%! end -%! [stepresponse, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1); -%! if ~isempty(stepresponse) -%! assert(all(isfinite(stepresponse(:)))); -%! end +% test_PSstepcalc.m - Tests for step response deconvolution +% NOTE: PSstepcalc expects SP,GY as column vectors, lograte in kHz + +%!test +%! % Smoke test: function runs without error +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 5; % 5 seconds +%! sp = zeros(N, 1); +%! gy = zeros(N, 1); +%! % Create step inputs above 20 deg/s threshold +%! for k = 1:3 +%! st = round(k * Fs + 100); +%! en = min(st + round(0.4*Fs), N); +%! sp(st:en) = 300; +%! gy(st:en) = 285; +%! end +%! [stepresponse, t] = PSstepcalc(sp, gy, Fs_khz, 0, 1); +%! assert(~isempty(t)); + +%!test +%! % Time vector starts at 0 and ends at 500ms +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 5; +%! sp = zeros(N, 1); +%! gy = zeros(N, 1); +%! for k = 1:3 +%! st = round(k * Fs + 100); +%! en = min(st + round(0.4*Fs), N); +%! sp(st:en) = 300; +%! gy(st:en) = 280; +%! end +%! [~, t] = PSstepcalc(sp, gy, Fs_khz, 0, 1); +%! assert(t(1), 0, 1e-10); +%! assert(t(end), 500, 1); + +%!test +%! % Step response values are finite when valid segments exist +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 5; +%! sp = zeros(N, 1); +%! gy = zeros(N, 1); +%! for k = 1:3 +%! st = round(k * Fs + 100); +%! en = min(st + round(0.4*Fs), N); +%! sp(st:en) = 400; +%! gy(st:en) = 380; +%! end +%! [stepresponse, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1); +%! if ~isempty(stepresponse) +%! assert(all(isfinite(stepresponse(:)))); +%! end + +%!test +%! % Higher subsampleFactor yields more segments (more overlap) +%! Fs_khz = 4; Fs = Fs_khz * 1000; N = Fs * 10; +%! sp = zeros(N, 1); gy = zeros(N, 1); +%! for k = 1:8 +%! st = round(k * Fs * 1.1 + 100); en = min(st + round(0.3*Fs), N); +%! sp(st:en) = 200; gy(st:en) = 190; +%! end +%! [s_low, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1, 1, 40, 500); +%! [s_high, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1, 10, 40, 500); +%! n_low = 0; n_high = 0; +%! if exist('s_low','var') && ~isempty(s_low), n_low = size(s_low, 1); end +%! if exist('s_high','var') && ~isempty(s_high), n_high = size(s_high, 1); end +%! assert(n_high >= n_low, 'high subsample should yield >= segments than low'); + +%!test +%! % minRate filters out low-amplitude segments +%! Fs_khz = 4; Fs = Fs_khz * 1000; N = Fs * 10; +%! sp = zeros(N, 1); gy = zeros(N, 1); +%! % low-amplitude segments (30 deg/s) +%! for k = 1:4 +%! st = round(k * Fs * 1.1 + 100); en = min(st + round(0.3*Fs), N); +%! sp(st:en) = 30; gy(st:en) = 28; +%! end +%! % high-amplitude segments (200 deg/s) +%! for k = 5:8 +%! st = round(k * Fs * 1.1 + 100); en = min(st + round(0.3*Fs), N); +%! sp(st:en) = 200; gy(st:en) = 190; +%! end +%! [s_low, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1, 5, 10, 500); +%! [s_high, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1, 5, 100, 500); +%! n_low = 0; n_high = 0; +%! if ~isempty(s_low), n_low = size(s_low, 1); end +%! if ~isempty(s_high), n_high = size(s_high, 1); end +%! assert(n_low >= n_high, 'lower minRate should include more segments'); + +%!test +%! % maxRate excludes extreme maneuvers +%! Fs_khz = 4; Fs = Fs_khz * 1000; N = Fs * 10; +%! sp = zeros(N, 1); gy = zeros(N, 1); +%! % moderate segments (200 deg/s) +%! for k = 1:4 +%! st = round(k * Fs * 1.1 + 100); en = min(st + round(0.3*Fs), N); +%! sp(st:en) = 200; gy(st:en) = 190; +%! end +%! % extreme segments (800 deg/s) +%! for k = 5:8 +%! st = round(k * Fs * 1.1 + 100); en = min(st + round(0.3*Fs), N); +%! sp(st:en) = 800; gy(st:en) = 750; +%! end +%! [s_limited, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1, 5, 40, 500); +%! [s_all, ~] = PSstepcalc(sp, gy, Fs_khz, 0, 1, 5, 40, Inf); +%! n_limited = 0; n_all = 0; +%! if ~isempty(s_limited), n_limited = size(s_limited, 1); end +%! if ~isempty(s_all), n_all = size(s_all, 1); end +%! assert(n_all >= n_limited, 'unlimited maxRate should include more segments'); + +%!test +%! % Backward compat: old 5-arg call still works +%! Fs_khz = 4; Fs = Fs_khz * 1000; N = Fs * 5; +%! sp = zeros(N, 1); gy = zeros(N, 1); +%! for k = 1:3 +%! st = round(k * Fs + 100); en = min(st + round(0.4*Fs), N); +%! sp(st:en) = 300; gy(st:en) = 285; +%! end +%! [stepresponse, t] = PSstepcalc(sp, gy, Fs_khz, 0, 1); +%! assert(~isempty(t), 'old 5-arg signature must still work'); diff --git a/tests/test_PSthrSpec.m b/tests/test_PSthrSpec.m index a0c11f7..3e28c28 100644 --- a/tests/test_PSthrSpec.m +++ b/tests/test_PSthrSpec.m @@ -1,36 +1,36 @@ -% test_PSthrSpec.m - Tests for throttle x frequency spectrogram -% NOTE: PSthrSpec expects X,Y as column vectors, F in kHz - -%!test -%! % Smoke test: function runs on synthetic data -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! duration = 5; -%! N = Fs * duration; -%! throttle = ones(N, 1) * 50; -%! signal = sin(2 * pi * 150 * (0:N-1)' / Fs); -%! [freq, ampMat] = PSthrSpec(throttle, signal, Fs_khz, 0); -%! assert(~isempty(ampMat)); -%! assert(~isempty(freq)); - -%!test -%! % Output matrix is 100 rows (throttle bins) -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 5; -%! throttle = ones(N, 1) * 50; -%! signal = randn(N, 1); -%! [~, ampMat] = PSthrSpec(throttle, signal, Fs_khz, 0); -%! assert(size(ampMat, 1), 100); - -%!test -%! % Energy concentrated at correct throttle bin -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 10; -%! throttle = ones(N, 1) * 50; -%! signal = sin(2 * pi * 100 * (0:N-1)' / Fs); -%! [~, ampMat] = PSthrSpec(throttle, signal, Fs_khz, 0); -%! energy_at_50 = sum(ampMat(50, :)); -%! energy_at_10 = sum(ampMat(10, :)); -%! assert(energy_at_50 > energy_at_10); +% test_PSthrSpec.m - Tests for throttle x frequency spectrogram +% NOTE: PSthrSpec expects X,Y as column vectors, F in kHz + +%!test +%! % Smoke test: function runs on synthetic data +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! duration = 5; +%! N = Fs * duration; +%! throttle = ones(N, 1) * 50; +%! signal = sin(2 * pi * 150 * (0:N-1)' / Fs); +%! [freq, ampMat] = PSthrSpec(throttle, signal, Fs_khz, 0); +%! assert(~isempty(ampMat)); +%! assert(~isempty(freq)); + +%!test +%! % Output matrix is 100 rows (throttle bins) +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 5; +%! throttle = ones(N, 1) * 50; +%! signal = randn(N, 1); +%! [~, ampMat] = PSthrSpec(throttle, signal, Fs_khz, 0); +%! assert(size(ampMat, 1), 100); + +%!test +%! % Energy concentrated at correct throttle bin +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 10; +%! throttle = ones(N, 1) * 50; +%! signal = sin(2 * pi * 100 * (0:N-1)' / Fs); +%! [~, ampMat] = PSthrSpec(throttle, signal, Fs_khz, 0); +%! energy_at_50 = sum(ampMat(50, :)); +%! energy_at_10 = sum(ampMat(10, :)); +%! assert(energy_at_50 > energy_at_10); diff --git a/tests/test_PStimeFreqCalc.m b/tests/test_PStimeFreqCalc.m index 72d2854..ad6f212 100644 --- a/tests/test_PStimeFreqCalc.m +++ b/tests/test_PStimeFreqCalc.m @@ -1,45 +1,45 @@ -% test_PStimeFreqCalc.m - Tests for time x frequency computation -% NOTE: PStimeFreqCalc expects Y as column vector, F in kHz - -%!test -%! % Smoke test: function runs on synthetic data -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 5; -%! signal = sin(2 * pi * 200 * (0:N-1)' / Fs); -%! [Tm, freq, specMat] = PStimeFreqCalc(signal, Fs_khz, 1, 1); -%! assert(~isempty(specMat)); -%! assert(~isempty(Tm)); -%! assert(~isempty(freq)); - -%!test -%! % Time vector spans signal duration -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! duration = 5; -%! N = Fs * duration; -%! signal = randn(N, 1); -%! [Tm, ~, ~] = PStimeFreqCalc(signal, Fs_khz, 1, 1); -%! assert(Tm(1) >= 0); -%! assert(Tm(end) <= duration + 1); - -%!test -%! % Frequency vector reaches toward Nyquist -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 5; -%! signal = randn(N, 1); -%! [~, freq, ~] = PStimeFreqCalc(signal, Fs_khz, 1, 1); -%! assert(max(freq) <= Fs/2 + 100); -%! assert(max(freq) > 100); - -%!test -%! % specMat is 2D with reasonable dimensions -%! Fs_khz = 4; -%! Fs = Fs_khz * 1000; -%! N = Fs * 5; -%! signal = randn(N, 1); -%! [~, ~, specMat] = PStimeFreqCalc(signal, Fs_khz, 1, 1); -%! assert(ndims(specMat), 2); -%! assert(size(specMat, 1) > 0); -%! assert(size(specMat, 2) > 0); +% test_PStimeFreqCalc.m - Tests for time x frequency computation +% NOTE: PStimeFreqCalc expects Y as column vector, F in kHz + +%!test +%! % Smoke test: function runs on synthetic data +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 5; +%! signal = sin(2 * pi * 200 * (0:N-1)' / Fs); +%! [Tm, freq, specMat] = PStimeFreqCalc(signal, Fs_khz, 1, 1); +%! assert(~isempty(specMat)); +%! assert(~isempty(Tm)); +%! assert(~isempty(freq)); + +%!test +%! % Time vector spans signal duration +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! duration = 5; +%! N = Fs * duration; +%! signal = randn(N, 1); +%! [Tm, ~, ~] = PStimeFreqCalc(signal, Fs_khz, 1, 1); +%! assert(Tm(1) >= 0); +%! assert(Tm(end) <= duration + 1); + +%!test +%! % Frequency vector reaches toward Nyquist +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 5; +%! signal = randn(N, 1); +%! [~, freq, ~] = PStimeFreqCalc(signal, Fs_khz, 1, 1); +%! assert(max(freq) <= Fs/2 + 100); +%! assert(max(freq) > 100); + +%!test +%! % specMat is 2D with reasonable dimensions +%! Fs_khz = 4; +%! Fs = Fs_khz * 1000; +%! N = Fs * 5; +%! signal = randn(N, 1); +%! [~, ~, specMat] = PStimeFreqCalc(signal, Fs_khz, 1, 1); +%! assert(ndims(specMat), 2); +%! assert(size(specMat, 1) > 0); +%! assert(size(specMat, 2) > 0); diff --git a/tests/test_compat.m b/tests/test_compat.m index b84519c..c91730c 100644 --- a/tests/test_compat.m +++ b/tests/test_compat.m @@ -1,60 +1,60 @@ -% test_compat.m - Tests for compat/ shim functions - -%!test -%! % smooth() - moving average with span 5 -%! y = [1 2 3 4 5 6 7 8 9 10]'; -%! ys = smooth(y, 5); -%! assert(length(ys), 10); -%! assert(ys(3), 3, 1e-10); % middle of window [1,2,3,4,5] = 3 -%! assert(ys(5), 5, 1e-10); % middle of window [3,4,5,6,7] = 5 - -%!test -%! % smooth() - moving average preserves length -%! y = randn(100, 1); -%! ys = smooth(y, 11); -%! assert(length(ys), 100); - -%!test -%! % smooth() - lowess method runs without error -%! y = sin(linspace(0, 4*pi, 100))' + 0.1*randn(100, 1); -%! ys = smooth(y, 21, 'lowess'); -%! assert(length(ys), 100); - -%!test -%! % nanmean() - basic mean ignoring NaN -%! x = [1 2 NaN 4 5]; -%! assert(nanmean(x), 3, 1e-10); - -%!test -%! % nanmean() - column-wise with dim=1 -%! x = [1 2; NaN 4; 3 6]; -%! m = nanmean(x, 1); -%! assert(m(1), 2, 1e-10); -%! assert(m(2), 4, 1e-10); - -%!test -%! % nanmedian() - basic median ignoring NaN -%! x = [1 NaN 3 4 5]; -%! assert(nanmedian(x), 3.5, 1e-10); - -%!test -%! % finddelay() - detect known delay -%! x = [zeros(1,10) ones(1,90)]'; -%! y = [zeros(1,15) ones(1,85)]'; % delayed by 5 samples -%! d = finddelay(x, y, 20); -%! assert(abs(d), 5, 2); % allow +-2 sample tolerance - -%!test -%! % contains() - string matching -%! assert(contains('hello world', 'world')); -%! assert(!contains('hello world', 'foo')); - -%!test -%! % contains() - cell array -%! c = {'alpha', 'beta', 'gamma'}; -%! result = contains(c, 'bet'); -%! assert(result, [false true false]); - -%!test -%! % contains() - case insensitive -%! assert(contains('Hello', 'hello', 'IgnoreCase', true)); +% test_compat.m - Tests for compat/ shim functions + +%!test +%! % smooth() - moving average with span 5 +%! y = [1 2 3 4 5 6 7 8 9 10]'; +%! ys = smooth(y, 5); +%! assert(length(ys), 10); +%! assert(ys(3), 3, 1e-10); % middle of window [1,2,3,4,5] = 3 +%! assert(ys(5), 5, 1e-10); % middle of window [3,4,5,6,7] = 5 + +%!test +%! % smooth() - moving average preserves length +%! y = randn(100, 1); +%! ys = smooth(y, 11); +%! assert(length(ys), 100); + +%!test +%! % smooth() - lowess method runs without error +%! y = sin(linspace(0, 4*pi, 100))' + 0.1*randn(100, 1); +%! ys = smooth(y, 21, 'lowess'); +%! assert(length(ys), 100); + +%!test +%! % nanmean() - basic mean ignoring NaN +%! x = [1 2 NaN 4 5]; +%! assert(nanmean(x), 3, 1e-10); + +%!test +%! % nanmean() - column-wise with dim=1 +%! x = [1 2; NaN 4; 3 6]; +%! m = nanmean(x, 1); +%! assert(m(1), 2, 1e-10); +%! assert(m(2), 4, 1e-10); + +%!test +%! % nanmedian() - basic median ignoring NaN +%! x = [1 NaN 3 4 5]; +%! assert(nanmedian(x), 3.5, 1e-10); + +%!test +%! % finddelay() - detect known delay +%! x = [zeros(1,10) ones(1,90)]'; +%! y = [zeros(1,15) ones(1,85)]'; % delayed by 5 samples +%! d = finddelay(x, y, 20); +%! assert(abs(d), 5, 2); % allow +-2 sample tolerance + +%!test +%! % contains() - string matching +%! assert(contains('hello world', 'world')); +%! assert(!contains('hello world', 'foo')); + +%!test +%! % contains() - cell array +%! c = {'alpha', 'beta', 'gamma'}; +%! result = contains(c, 'bet'); +%! assert(result, [false true false]); + +%!test +%! % contains() - case insensitive +%! assert(contains('Hello', 'hello', 'IgnoreCase', true)); diff --git a/tests/test_helpers.m b/tests/test_helpers.m index b579357..2607efc 100644 --- a/tests/test_helpers.m +++ b/tests/test_helpers.m @@ -1,48 +1,48 @@ -function out = test_helpers() - % Test helpers - mock data generators for PIDscope tests - % Usage: h = test_helpers(); y = h.mock_sine(100, 2, 4); - out.mock_sine = @mock_sine; - out.mock_step = @mock_step; - out.mock_throttle = @mock_throttle; -end - -function y = mock_sine(freq_hz, duration_s, sample_rate_khz) - % Generate pure sine wave - % freq_hz - frequency in Hz - % duration_s - duration in seconds - % sample_rate_khz - sample rate in kHz (matching PIDscope convention) - Fs = sample_rate_khz * 1000; - t = (0 : 1/Fs : duration_s - 1/Fs)'; - y = sin(2 * pi * freq_hz * t); -end - -function [sp, gy] = mock_step(delay_samples, duration_s, sample_rate_khz) - % Generate step input (setpoint) and delayed step response (gyro) - % delay_samples - response delay in samples - % duration_s - total duration in seconds - % sample_rate_khz - sample rate in kHz - Fs = sample_rate_khz * 1000; - N = round(Fs * duration_s); - sp = zeros(N, 1); - gy = zeros(N, 1); - % Step at 25% of signal - step_start = round(N * 0.25); - sp(step_start:end) = 500; % 500 deg/s step (above 20 deg/s threshold) - % Delayed response with first-order dynamics - resp_start = step_start + delay_samples; - if resp_start <= N - tau = round(Fs * 0.02); % 20ms time constant - t_resp = (0 : N - resp_start)'; - gy(resp_start:end) = 500 * (1 - exp(-t_resp / tau)); - end -end - -function x = mock_throttle(level, duration_s, sample_rate_khz) - % Generate constant throttle signal - % level - throttle percentage (0-100) - % duration_s - duration in seconds - % sample_rate_khz - sample rate in kHz - Fs = sample_rate_khz * 1000; - N = round(Fs * duration_s); - x = ones(N, 1) * level; -end +function out = test_helpers() + % Test helpers - mock data generators for PIDscope tests + % Usage: h = test_helpers(); y = h.mock_sine(100, 2, 4); + out.mock_sine = @mock_sine; + out.mock_step = @mock_step; + out.mock_throttle = @mock_throttle; +end + +function y = mock_sine(freq_hz, duration_s, sample_rate_khz) + % Generate pure sine wave + % freq_hz - frequency in Hz + % duration_s - duration in seconds + % sample_rate_khz - sample rate in kHz (matching PIDscope convention) + Fs = sample_rate_khz * 1000; + t = (0 : 1/Fs : duration_s - 1/Fs)'; + y = sin(2 * pi * freq_hz * t); +end + +function [sp, gy] = mock_step(delay_samples, duration_s, sample_rate_khz) + % Generate step input (setpoint) and delayed step response (gyro) + % delay_samples - response delay in samples + % duration_s - total duration in seconds + % sample_rate_khz - sample rate in kHz + Fs = sample_rate_khz * 1000; + N = round(Fs * duration_s); + sp = zeros(N, 1); + gy = zeros(N, 1); + % Step at 25% of signal + step_start = round(N * 0.25); + sp(step_start:end) = 500; % 500 deg/s step (above 20 deg/s threshold) + % Delayed response with first-order dynamics + resp_start = step_start + delay_samples; + if resp_start <= N + tau = round(Fs * 0.02); % 20ms time constant + t_resp = (0 : N - resp_start)'; + gy(resp_start:end) = 500 * (1 - exp(-t_resp / tau)); + end +end + +function x = mock_throttle(level, duration_s, sample_rate_khz) + % Generate constant throttle signal + % level - throttle percentage (0-100) + % duration_s - duration in seconds + % sample_rate_khz - sample rate in kHz + Fs = sample_rate_khz * 1000; + N = round(Fs * duration_s); + x = ones(N, 1) * level; +end