diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 991cff4e..187bc475 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: Application Build +name: Python application on: pull_request: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30cda7a2..ab539f1b 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: v${{ steps.version.outputs.version }} [skip ci]" + git commit -m "chore: release 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: "v${{ steps.version.outputs.version }}" + name: "release v${{ steps.version.outputs.version }}" body_path: RELEASE_NOTES.md draft: false prerelease: false diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0dfb28..6d80dd08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,27 +14,6 @@ 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 dd3c2f08..31833850 100644 --- a/GUI5_BrainwaveReading/GUI5_BrainwaveReading.py +++ b/GUI5_BrainwaveReading/GUI5_BrainwaveReading.py @@ -6,236 +6,59 @@ 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)) -from GUI5 import BrainwavesBackend +class BrainwavesBackend(QObject): + # Define signals to update QML components + flightLogUpdated = Signal(list) + predictionsTableUpdated = Signal(list) - -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 - - - -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. - """ + def __init__(self): 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 - + self.flight_log = [] # List to store flight log entries + self.predictions_log = [] # List to store prediction records + self.current_prediction_label = "" + @Slot() def readMyMind(self): - """ - 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) - + # Mock function to simulate brainwave reading + self.current_prediction_label = "Move Forward" # Update the predictions log self.predictions_log.append( { - "count": str(len(self.predictions_log) + 1), - "server": server_name, + "count": "1", + "server": "Prediction Server", "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 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 + # Handle manual action input 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 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) + # Execute the current prediction + if self.current_prediction_label: + self.flight_log.insert(0, f"Executed: {self.current_prediction_label}") 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): - """ - Connect to the Tello drone using the actual getDroneAction method. - """ - self.getDroneAction('connect') - self.flight_log.insert(0, "Connecting to drone...") + # Mock function to simulate drone connection + self.flight_log.insert(0, "Drone connected.") self.flightLogUpdated.emit(self.flight_log) @Slot() def keepDroneAlive(self): - """ - 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) + # Mock function to simulate sending keep-alive signal + self.flight_log.insert(0, "Keep alive signal sent.") + self.flightLogUpdated.emit(self.flight_log) if __name__ == "__main__": @@ -245,28 +68,11 @@ 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 @@ -274,7 +80,10 @@ 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 71ca1940..16de26be 100644 --- a/GUI5_BrainwaveReading/GUI5_BrainwaveReading.qml +++ b/GUI5_BrainwaveReading/GUI5_BrainwaveReading.qml @@ -8,29 +8,6 @@ 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 @@ -251,7 +228,6 @@ ApplicationWindow { } onClicked: backend.keepDroneAlive() } - } // end of GridLayout // Flight Log GroupBox { @@ -271,33 +247,32 @@ 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" @@ -321,7 +296,7 @@ ApplicationWindow { onClicked: backend.connectDrone() } } - } // end of Left Column ColumnLayout + // Right Column (Prediction Table and Console Log) ColumnLayout { @@ -387,10 +362,11 @@ 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 @@ -422,29 +398,30 @@ 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 { @@ -611,4 +588,9 @@ 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 8428158d..f9cbc01a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.2 \ No newline at end of file +1.0.7 \ 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 7ce4a6c8..d960a374 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.1 +tensorflow==2.12.0 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 23c4cfbb..1625017b 100755 --- a/scripts/auto_version.py +++ b/scripts/auto_version.py @@ -34,61 +34,32 @@ 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', - 'feature': 'Added', - 'fix': 'Fixed', - 'bugfix': 'Fixed', - 'hotfix': 'Fixed', + 'fix': '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 = ""): @@ -122,8 +93,7 @@ def parse(self): # Check for breaking changes in body self.body = '\n'.join(lines[1:]).strip() - # Match "BREAKING CHANGE:" / "BREAKING-CHANGE:" (case-insensitive), line-anchored - if re.search(r'(?im)^(?:BREAKING(?:\s|-)?CHANGES?):', self.body): + if 'BREAKING CHANGE' in self.body.upper(): self.breaking_change = True def get_version_bump(self) -> str: @@ -286,61 +256,21 @@ 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 - # 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 + 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 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" - - 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" - + 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" + # Write to release notes file release_notes_file = self.repo_path / "RELEASE_NOTES.md" release_notes_file.write_text(release_notes) @@ -352,14 +282,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"