From 2489e563e39596cb960a6c96247f4f63a2458174 Mon Sep 17 00:00:00 2001 From: Jason D Date: Thu, 30 Oct 2025 11:50:14 -0500 Subject: [PATCH] Revert "Revert "Execute button fix issue: #442"" --- .github/workflows/python-app.yml | 2 +- .github/workflows/release.yml | 4 +- CHANGELOG.md | 21 ++ .../GUI5_BrainwaveReading.py | 245 ++++++++++++++++-- .../GUI5_BrainwaveReading.qml | 112 ++++---- VERSION | 2 +- .../prediction_server/server/requirements.txt | 2 +- scripts/auto_version.py | 102 ++++++-- 8 files changed, 395 insertions(+), 95 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 187bc475..991cff4e 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: Python application +name: Application Build on: pull_request: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab539f1b..30cda7a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: if: steps.version.outputs.has_changes != '0' run: | git add VERSION CHANGELOG.md - git commit -m "chore: release v${{ steps.version.outputs.version }} [skip ci]" + git commit -m "chore: v${{ steps.version.outputs.version }} [skip ci]" git push origin main - name: Create Git Tag @@ -65,7 +65,7 @@ jobs: uses: softprops/action-gh-release@v2 with: tag_name: "v${{ steps.version.outputs.version }}" - name: "release v${{ steps.version.outputs.version }}" + name: "v${{ steps.version.outputs.version }}" body_path: RELEASE_NOTES.md draft: false prerelease: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d80dd08..ba0dfb28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + + + +## [1.1.2] - 2025-10-27 + +### Changed +- Fix/brainwave reading (#11) (055584f) + +## [1.1.1] - 2025-10-25 + +### Fixed +- Format indentation fix(indentation) (685700a) + +### Security +- Update high vulnerability (#10)(tensorflow) (ca02989) + +## [1.1.0] - 2025-10-24 + +### Added +- Automate release (#9)(semver) (1ebea7c) + ## [1.0.7] - 2025-10-24 ### Changed diff --git a/GUI5_BrainwaveReading/GUI5_BrainwaveReading.py b/GUI5_BrainwaveReading/GUI5_BrainwaveReading.py index 31833850..dd3c2f08 100644 --- a/GUI5_BrainwaveReading/GUI5_BrainwaveReading.py +++ b/GUI5_BrainwaveReading/GUI5_BrainwaveReading.py @@ -6,59 +6,236 @@ from PySide6.QtCore import QObject, Signal, Slot from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine +from djitellopy import TelloException +# Add parent directory to path to import BrainwavesBackend from GUI5.py +parent_dir = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(parent_dir)) -class BrainwavesBackend(QObject): - # Define signals to update QML components - flightLogUpdated = Signal(list) - predictionsTableUpdated = Signal(list) +from GUI5 import BrainwavesBackend + + +def normalize_bci_label(label): + """ + Normalize BCI prediction labels to drone action commands. + Converts various label formats to standardized lowercase action strings. + + Args: + label (str): The raw BCI prediction label + + Returns: + str: Normalized drone action command in lowercase + """ + if not label: + return "" + + # Convert to lowercase for standardization + label_lower = label.lower().strip() + + # Map label variations to drone actions + label_mapping = { + "move forward": "forward", + "move backward": "backward", + "move left": "left", + "move right": "right", + "move up": "up", + "move down": "down", + "take off": "takeoff", + "landing": "land", + # Direct mappings (already correct) + "forward": "forward", + "backward": "backward", + "left": "left", + "right": "right", + "up": "up", + "down": "down", + "takeoff": "takeoff", + "land": "land", + "turn_left": "turn_left", + "turn_right": "turn_right", + } + + normalized = label_mapping.get(label_lower, "") + if not normalized: + print(f"Warning: Unknown BCI label '{label}' - ignoring") + return normalized - def __init__(self): - super().__init__() - self.flight_log = [] # List to store flight log entries - self.predictions_log = [] # List to store prediction records - self.current_prediction_label = "" + +class BrainwaveReadingBackend(BrainwavesBackend): + """ + Extended BrainwavesBackend specifically for the BrainwaveReading module. + Overrides key methods to integrate BCI label normalization and Tello execution. + """ + + def __init__(self, mock_mode=True, bci_connection=None): + """ + Initialize the BrainwaveReading backend. + + Args: + mock_mode (bool): If True, uses mock predictions for testing. + If False, requires actual BCI connection. + bci_connection: BCI connection object for real brainwave reading. + Required when mock_mode=False. + """ + super().__init__() + self.mock_mode = mock_mode + self.bci_connection = bci_connection + self.mock_predictions = ["forward", "backward", "left", "right", "takeoff", "land"] + self.mock_index = 0 + + if not mock_mode and bci_connection is None: + print("Warning: mock_mode=False but no BCI connection provided. Falling back to mock mode.") + self.mock_mode = True + @Slot() def readMyMind(self): - # Mock function to simulate brainwave reading - self.current_prediction_label = "Move Forward" + """ + Read brainwave data and generate prediction. + Uses mock data if mock_mode=True, otherwise calls actual BCI system. + """ + if self.mock_mode: + # Cycle through mock predictions for testing + self.current_prediction_label = self.mock_predictions[self.mock_index] + self.mock_index = (self.mock_index + 1) % len(self.mock_predictions) + server_name = "Mock Server (Testing)" + else: + # TODO: Implement actual BCI prediction + # Example integration: + # try: + # prediction_response = self.bci_connection.use_brainflow() + # self.current_prediction_label = prediction_response["prediction_label"] + # server_name = "BCI Server" + # except Exception as e: + # self.logMessage.emit(f"BCI prediction failed: {e}") + # return + raise NotImplementedError( + "Real BCI prediction not yet implemented. " + "Set mock_mode=True or provide BCI connection implementation." + ) + + # Normalize the label for drone commands + normalized_label = normalize_bci_label(self.current_prediction_label) + # Update the predictions log self.predictions_log.append( { - "count": "1", - "server": "Prediction Server", + "count": str(len(self.predictions_log) + 1), + "server": server_name, "label": self.current_prediction_label, } ) self.predictionsTableUpdated.emit(self.predictions_log) + + # Log to flight log + mode_indicator = "[MOCK] " if self.mock_mode else "" + self.flight_log.insert(0, f"{mode_indicator}BCI Prediction: {self.current_prediction_label} → {normalized_label}") + self.flightLogUpdated.emit(self.flight_log) @Slot(str) def notWhatIWasThinking(self, manual_action): - # Handle manual action input + """ + Handle manual action input when BCI prediction is incorrect. + Normalizes the manual input and executes it on the drone. + """ + if not manual_action or manual_action.strip() == "": + self.logMessage.emit("No manual action provided") + return + + # Normalize the manual action + normalized_action = normalize_bci_label(manual_action) + + if not normalized_action: + self.logMessage.emit(f"Unknown action '{manual_action}' - no drone command executed") + return + + # Add to predictions log self.predictions_log.append( - {"count": "manual", "server": "manual", "label": manual_action} + { + "count": "manual", + "server": "manual", + "label": manual_action + } ) self.predictionsTableUpdated.emit(self.predictions_log) + + # Execute the manual action on drone + self.getDroneAction(normalized_action) + + # Log the manual override + self.flight_log.insert(0, f"Manual override: {manual_action} → {normalized_action}") + self.flightLogUpdated.emit(self.flight_log) @Slot() def executeAction(self): - # Execute the current prediction - if self.current_prediction_label: - self.flight_log.insert(0, f"Executed: {self.current_prediction_label}") + """ + Execute the current BCI prediction on the Tello drone. + Normalizes the label and sends it to getDroneAction(). + """ + print(f"DEBUG: executeAction() called, current_prediction_label='{self.current_prediction_label}'") + + if not self.current_prediction_label: + msg = "No prediction to execute - click 'Read my mind...' first" + print(f"DEBUG: {msg}") + self.logMessage.emit(msg) + self.flight_log.insert(0, msg) + self.flightLogUpdated.emit(self.flight_log) + return + + # Normalize the BCI label to drone action format + normalized_label = normalize_bci_label(self.current_prediction_label) + print(f"DEBUG: Normalized '{self.current_prediction_label}' → '{normalized_label}'") + + if not normalized_label: + msg = f"Cannot execute - unknown action '{self.current_prediction_label}'" + print(f"DEBUG: {msg}") + self.logMessage.emit(msg) + self.flight_log.insert(0, msg) self.flightLogUpdated.emit(self.flight_log) + return + + print(f"DEBUG: Calling getDroneAction('{normalized_label}')") + # Execute on the drone + self.getDroneAction(normalized_label) + + # Update flight log + log_msg = f"Executed: {self.current_prediction_label} → {normalized_label}" + print(f"DEBUG: {log_msg}") + self.flight_log.insert(0, log_msg) + self.flightLogUpdated.emit(self.flight_log) + self.logMessage.emit(f"Executed action: {normalized_label}") @Slot() def connectDrone(self): - # Mock function to simulate drone connection - self.flight_log.insert(0, "Drone connected.") + """ + Connect to the Tello drone using the actual getDroneAction method. + """ + self.getDroneAction('connect') + self.flight_log.insert(0, "Connecting to drone...") self.flightLogUpdated.emit(self.flight_log) @Slot() def keepDroneAlive(self): - # Mock function to simulate sending keep-alive signal - self.flight_log.insert(0, "Keep alive signal sent.") - self.flightLogUpdated.emit(self.flight_log) + """ + Send keep-alive signal to maintain Tello connection. + Queries battery status to keep the connection active. + """ + if not self.is_connected: + self.logMessage.emit("Drone not connected. Cannot send keep-alive.") + self.flight_log.insert(0, "Keep-alive failed: Not connected") + self.flightLogUpdated.emit(self.flight_log) + return + + try: + # Query battery to keep connection alive + battery = self.tello.query_battery() + self.logMessage.emit(f"Keep-alive sent. Battery: {battery}%") + self.flight_log.insert(0, f"Keep-alive: Battery {battery}%") + self.flightLogUpdated.emit(self.flight_log) + except (AttributeError, ConnectionError, TimeoutError, TelloException) as e: + self.logMessage.emit(f"Keep-alive error: {e}") + self.flight_log.insert(0, f"Keep-alive error: {e}") + self.flightLogUpdated.emit(self.flight_log) if __name__ == "__main__": @@ -68,11 +245,28 @@ def keepDroneAlive(self): app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() + # Determine if we should use mock mode or real BCI + # Check for --mock or --real command line argument + mock_mode = True # Default to mock mode for safety + if "--real" in sys.argv: + mock_mode = False + print("Starting in REAL BCI mode") + # TODO: Initialize actual BCI connection here + # bci_conn = bciConnection(...) + # backend = BrainwaveReadingBackend(mock_mode=False, bci_connection=bci_conn) + elif "--mock" in sys.argv or len(sys.argv) == 1: + print("Starting in MOCK mode (use --real for actual BCI)") + + # Create the backend with full Tello integration + backend = BrainwaveReadingBackend(mock_mode=mock_mode) + engine.rootContext().setContextProperty("backend", backend) + # Load the QML file qml_file = Path(__file__).resolve().parent / "GUI5_BrainwaveReading.qml" # Check if the QML file exists if not qml_file.exists(): + print(f"Error: QML file not found at {qml_file}") sys.exit(-1) # Load the QML file @@ -80,10 +274,7 @@ def keepDroneAlive(self): # Check if the QML engine loaded successfully if not engine.rootObjects(): + print("Error: Failed to load QML file") sys.exit(-1) - # Create and set the backend context - backend = BrainwavesBackend() - engine.rootContext().setContextProperty("backend", backend) - sys.exit(app.exec()) diff --git a/GUI5_BrainwaveReading/GUI5_BrainwaveReading.qml b/GUI5_BrainwaveReading/GUI5_BrainwaveReading.qml index 16de26be..71ca1940 100644 --- a/GUI5_BrainwaveReading/GUI5_BrainwaveReading.qml +++ b/GUI5_BrainwaveReading/GUI5_BrainwaveReading.qml @@ -8,6 +8,29 @@ ApplicationWindow { height: 800 title: "Avatar - Brainwave Reading" + // Connect backend signals to update UI + Connections { + target: backend + + function onFlightLogUpdated(logEntries) { + flightLogView.model.clear() + for (var i = 0; i < logEntries.length; i++) { + flightLogView.model.append({"log": logEntries[i]}) + } + } + + function onPredictionsTableUpdated(predictions) { + predictionsTableView.model.clear() + for (var i = 0; i < predictions.length; i++) { + predictionsTableView.model.append(predictions[i]) + } + } + + function onLogMessage(message) { + consolelog.model.append({"log": message}) + } + } + ColumnLayout { anchors.fill: parent spacing: 10 @@ -228,6 +251,7 @@ ApplicationWindow { } onClicked: backend.keepDroneAlive() } + } // end of GridLayout // Flight Log GroupBox { @@ -247,32 +271,33 @@ ApplicationWindow { Layout.alignment: Qt.AlignHCenter } - // Background Rectangle inside the ListView - Rectangle { - color: "white" // Set only the box area color to white - anchors.fill: parent - - ListView { - id: flightLogView - anchors.fill: parent // Fill the Rectangle background with ListView content - model: ListModel { - - } - delegate: Text { - text: log - color: "black" // Set text color for readability - anchors.horizontalCenter: parent.horizontalCenter + // Background Rectangle inside the ListView + Rectangle { + color: "white" // Set only the box area color to white + anchors.fill: parent + + ListView { + id: flightLogView + anchors.fill: parent // Fill the Rectangle background with ListView content + model: ListModel { + + } + delegate: Text { + text: log + color: "black" // Set text color for readability + anchors.horizontalCenter: parent.horizontalCenter + } } } } - } - + } // end of Flight Log GroupBox // Connect Image with Transparent Button Rectangle { width: 150 height: 150 color: "#1b3a4b" // Dark blue background + Layout.alignment: Qt.AlignHCenter Image { source: "GUI_Pics/connect.png" @@ -296,7 +321,7 @@ ApplicationWindow { onClicked: backend.connectDrone() } } - + } // end of Left Column ColumnLayout // Right Column (Prediction Table and Console Log) ColumnLayout { @@ -362,11 +387,10 @@ ApplicationWindow { } ListView { + id: predictionsTableView Layout.preferredWidth: 700 Layout.preferredHeight: 550 model: ListModel { - ListElement { count: "1"; server: "Prediction A"; label: "Label A" } - ListElement { count: "2"; server: "Prediction B"; label: "Label B" } } delegate: RowLayout { spacing: 50 @@ -398,30 +422,29 @@ ApplicationWindow { Layout.alignment: Qt.AlignHCenter } - // Background Rectangle inside the ListView - Rectangle { - color: "white" // Set only the box area color to white - anchors.fill: parent - - ListView { - id: consolelog - anchors.fill: parent // Fill the Rectangle background with ListView content - model: ListModel { - - } - delegate: Text { - text: log - color: "black" // Set text color for readability - anchors.horizontalCenter: parent.horizontalCenter + // Background Rectangle inside the ListView + Rectangle { + color: "white" // Set only the box area color to white + anchors.fill: parent + + ListView { + id: consolelog + anchors.fill: parent // Fill the Rectangle background with ListView content + model: ListModel { + + } + delegate: Text { + text: log + color: "black" // Set text color for readability + anchors.horizontalCenter: parent.horizontalCenter + } } } } - } - } - } - } - - + } // end of Console Log GroupBox + } // end of Right Column ColumnLayout + } // end of RowLayout + } // end of Brainwave Reading Rectangle // Transfer Data view Rectangle { @@ -588,9 +611,4 @@ ApplicationWindow { } } // end of StackLayout } // end of outer ColumnLayout -} // end of ApplicationWindow - } - } - } - } -} \ No newline at end of file +} // end of ApplicationWindow \ No newline at end of file diff --git a/VERSION b/VERSION index f9cbc01a..8428158d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.7 \ No newline at end of file +1.1.2 \ No newline at end of file diff --git a/prediction-random-forest/tensorflow/prediction_server/server/requirements.txt b/prediction-random-forest/tensorflow/prediction_server/server/requirements.txt index d960a374..7ce4a6c8 100644 --- a/prediction-random-forest/tensorflow/prediction_server/server/requirements.txt +++ b/prediction-random-forest/tensorflow/prediction_server/server/requirements.txt @@ -1,5 +1,5 @@ flask pandas -tensorflow==2.12.0 +tensorflow==2.12.1 numpy tensorflow_decision_forests==1.3.0 \ No newline at end of file diff --git a/scripts/auto_version.py b/scripts/auto_version.py index 1625017b..23c4cfbb 100755 --- a/scripts/auto_version.py +++ b/scripts/auto_version.py @@ -34,32 +34,61 @@ class ConventionalCommit: # Commit type mappings to version bumps TYPE_MAPPING = { 'feat': 'minor', # New features + 'feature': 'minor', # New features (alternative) 'fix': 'patch', # Bug fixes + 'bugfix': 'patch', # Bug fixes (alternative) + 'hotfix': 'patch', # Critical bug fixes 'perf': 'patch', # Performance improvements + 'performance': 'patch', # Performance improvements (alternative) 'docs': 'patch', # Documentation changes + 'documentation': 'patch', # Documentation changes (alternative) 'style': 'patch', # Code style changes 'refactor': 'patch', # Code refactoring + 'refactoring': 'patch', # Code refactoring (alternative) 'test': 'patch', # Test additions/changes + 'tests': 'patch', # Test additions/changes (alternative) 'chore': 'patch', # Maintenance tasks 'ci': 'patch', # CI/CD changes + 'cd': 'patch', # CI/CD changes (alternative) 'build': 'patch', # Build system changes + 'deps': 'patch', # Dependency updates + 'dependencies': 'patch', # Dependency updates (alternative) + 'security': 'patch', # Security fixes + 'sec': 'patch', # Security fixes (alternative) + 'deprecated': 'minor', # Deprecation notices + 'deprecate': 'minor', # Deprecation notices (alternative) + 'removed': 'major', # Removed features + 'remove': 'major', # Removed features (alternative) } # Changelog section mappings CHANGELOG_MAPPING = { 'feat': 'Added', - 'fix': 'Fixed', + 'feature': 'Added', + 'fix': 'Fixed', + 'bugfix': 'Fixed', + 'hotfix': 'Fixed', 'perf': 'Changed', + 'performance': 'Changed', 'docs': 'Changed', + 'documentation': 'Changed', 'style': 'Changed', 'refactor': 'Changed', + 'refactoring': 'Changed', 'test': 'Changed', + 'tests': 'Changed', 'chore': 'Changed', 'ci': 'Changed', + 'cd': 'Changed', 'build': 'Changed', + 'deps': 'Changed', + 'dependencies': 'Changed', 'security': 'Security', + 'sec': 'Security', 'deprecated': 'Deprecated', + 'deprecate': 'Deprecated', 'removed': 'Removed', + 'remove': 'Removed', } def __init__(self, commit_message: str, commit_hash: str = ""): @@ -93,7 +122,8 @@ def parse(self): # Check for breaking changes in body self.body = '\n'.join(lines[1:]).strip() - if 'BREAKING CHANGE' in self.body.upper(): + # Match "BREAKING CHANGE:" / "BREAKING-CHANGE:" (case-insensitive), line-anchored + if re.search(r'(?im)^(?:BREAKING(?:\s|-)?CHANGES?):', self.body): self.breaking_change = True def get_version_bump(self) -> str: @@ -256,21 +286,61 @@ def create_release_notes(self, version: semantic_version.Version, entries: Dict[ if not entries: return + # Map changelog sections to release note categories + release_categories = { + 'Fixed': 'FIXES', + 'Added': 'NEW FEATURES', + 'Changed': 'CHANGES', + 'Deprecated': 'DEPRECATED', + 'Removed': 'REMOVED', + 'Security': 'SECURITY' + } + release_notes = f"# Release v{version}\n\n" + + # Derive repo/branch for links + repo_slug = os.environ.get("GITHUB_REPOSITORY", "") + ref_name = os.environ.get("GITHUB_REF_NAME", "main") + base_url = "" + if repo_slug: + base_url = f"https://github.com/{repo_slug}/blob/{ref_name}" + else: + # Fallback: parse origin remote + try: + origin_url = next((r.url for r in self.repo.remotes if r.name == "origin"), "") + m = re.search(r"github\.com[:/](.*?)(?:\.git)?$", origin_url) + if m: + base_url = f"https://github.com/{m.group(1)}/blob/{ref_name}" + except Exception: + pass - for section, items in entries.items(): - if items: - release_notes += f"## {section}\n\n" - for item in items: - # Remove the leading "- " since GitHub will format it + # Order categories by importance + category_order = ['FIXES', 'NEW FEATURES', 'CHANGES', 'SECURITY', 'DEPRECATED', 'REMOVED'] + + for category in category_order: + # Find matching entries for this category + category_items = [] + for section, items in entries.items(): + if release_categories.get(section) == category and items: + category_items.extend(items) + + if category_items: + release_notes += f"## {category}:\n" + for item in category_items: + # Remove the leading "- " and format consistently clean_item = item[2:] if item.startswith("- ") else item - release_notes += f"- {clean_item}\n" + release_notes += f" - {clean_item}\n" release_notes += "\n" release_notes += "---\n\n" - release_notes += f"**Full Changelog**: [CHANGELOG.md](https://github.com/3C-SCSU/Avatar/blob/main/CHANGELOG.md)\n" - release_notes += f"**Installation**: See [SETUP_GUIDE.md](https://github.com/3C-SCSU/Avatar/blob/main/SETUP_GUIDE.md)\n" - + + if base_url: + release_notes += f"**Full Changelog**: [CHANGELOG.md]({base_url}/CHANGELOG.md)\n" + release_notes += f"**Installation**: See [SETUP_GUIDE.md]({base_url}/SETUP_GUIDE.md)\n" + else: + release_notes += "**Full Changelog**: CHANGELOG.md\n" + release_notes += "**Installation**: See SETUP_GUIDE.md\n" + # Write to release notes file release_notes_file = self.repo_path / "RELEASE_NOTES.md" release_notes_file.write_text(release_notes) @@ -282,14 +352,14 @@ def create_initial_changelog(self, version: semantic_version.Version, entries: D content = """# Changelog -All notable changes to the Avatar BCI project will be documented in this file. + All notable changes to the Avatar BCI project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] + ## [Unreleased] -""" + """ content += f"## [{version}] - {today}\n\n"