diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md new file mode 100644 index 0000000..7af35a0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature.md @@ -0,0 +1,21 @@ +## Summary + +Provide a brief description of the changes in this pull request. + +## How to Test + +* ... + +## Checklist + +- [ ] My code follows the project's coding standards +- [ ] I have performed a self-review of my own code +- [ ] I have made corresponding changes to the documentation +- [ ] I have added tests that prove my fix is effective or that my feature works + +## Related + +List any related issues or submodule PRs that need to be completed: +* ... + +Closes #123. diff --git a/.github/PULL_REQUEST_TEMPLATE/issue.md b/.github/PULL_REQUEST_TEMPLATE/issue.md deleted file mode 100644 index b1a25cc..0000000 --- a/.github/PULL_REQUEST_TEMPLATE/issue.md +++ /dev/null @@ -1,15 +0,0 @@ -## Summary of PR goals - -Provide a brief description of the purpose of this pull request. - -## How to test? - -Provide instructions for testing this pull request. - -## Submodule PRs and actions prior to closing this - -List any submodule pull requests or actions that need to be completed before closing this pull request. - -## Closes - -Reference the issue(s) this pull request closes, e.g., Closes #123. diff --git a/.gitignore b/.gitignore index e8d0fb6..8ba3c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode build cfg/active.yaml -cfg/build.yaml \ No newline at end of file +cfg/build.yaml +cfg/cli-compose.yaml \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 7d90257..ee48375 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,3 +18,7 @@ path = atlas url = https://github.com/TrySpaceOrg/TrySpaceOrg.github.io.git branch = main +[submodule "comp/eps"] + path = comp/eps + url = https://github.com/TrySpaceOrg/tryspace-comp-eps.git + branch = main diff --git a/Makefile b/Makefile index fb6a367..7926bdd 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Makefile for TrySpace Lab development -.PHONY: build clean clean-cli clean-fsw clean-gsw clean-sim cfg cli container debug fsw gsw help sim start stop uninstall +.PHONY: build clean clean-cache clean-cli clean-fsw clean-gsw clean-sim cfg cli container debug fsw gsw help mold sim start stop uninstall # Build image name export BUILD_IMAGE ?= tryspaceorg/tryspace-lab:0.0.0 @@ -18,17 +18,19 @@ cfg: container clean: $(MAKE) stop - @if docker image inspect $(BUILD_IMAGE):latest >/dev/null 2>&1; then \ + @if docker image inspect $(BUILD_IMAGE) >/dev/null 2>&1; then \ $(MAKE) clean-cli; \ $(MAKE) clean-fsw; \ $(MAKE) clean-gsw; \ $(MAKE) clean-sim; \ - docker volume ls -q --filter "name=gsw-data" | xargs -r docker volume rm \ - docker volume ls -q --filter "name=simulith_ipc" | xargs -r docker volume rm \ + docker volume ls -q --filter "name=gsw-data" | xargs -r docker volume rm; \ + docker volume ls -q --filter "name=simulith_ipc" | xargs -r docker volume rm; \ else \ echo "Docker image $(BUILD_IMAGE) does not exist. Skipping clean subcommands."; \ fi - rm -f $(CFG_DIR)/active.yaml $(CFG_DIR)/build.yaml + +clean-cache: + docker builder prune -f clean-cli: @for dir in $(CURDIR)/comp/*/cli ; do \ @@ -44,7 +46,7 @@ clean-gsw: cd gsw && $(MAKE) clean clean-sim: - @for dir in $(CURDIR)/comp/* ; do \ + @for dir in $(CURDIR)/comp/*/sim ; do \ if [ -d "$$dir" ] && [ -f "$$dir/Makefile" ]; then \ $(MAKE) -C "$$dir" clean; \ fi; \ @@ -53,13 +55,13 @@ clean-sim: cli: cfg $(MAKE) container + $(MAKE) sim @for dir in $(CURDIR)/comp/*/cli ; do \ if [ -f "$$dir/Makefile" ]; then \ $(MAKE) -C "$$dir" runtime; \ fi; \ done - cd $(CURDIR)/simulith && $(MAKE) director && $(MAKE) server - docker compose -f ./cfg/cli-compose.yml up + docker compose -f ./cfg/cli-compose.yaml up container: cfg/Dockerfile.base @command -v docker >/dev/null 2>&1 || { echo "Error: docker is not installed or not in PATH."; exit 1; } @@ -74,6 +76,15 @@ fsw: cfg gsw: cfg cd $(CURDIR)/gsw && $(MAKE) runtime +mold: + @if [ "$(COMP)" = "" ]; then \ + echo "Error: COMP parameter is required"; \ + echo "Usage: make mold COMP="; \ + echo "Example: make mold COMP=my_sensor"; \ + exit 1; \ + fi + python3 $(CFG_DIR)/tryspace-comp-mold.py "$(COMP)" + help: @echo "Usage: make " @echo "Targets:" @@ -81,6 +92,7 @@ help: @echo " cfg - Run orchestrator to configure environment" @echo " cli - Build CLI and start CLI services" @echo " clean - Remove build artifacts and stop services" + @echo " clean-cache - Clean Docker build cache (frees significant disk space)" @echo " clean-cli - Clean CLI components" @echo " clean-fsw - Clean FSW components" @echo " clean-gsw - Clean GSW components" @@ -89,6 +101,7 @@ help: @echo " debug - Start a debug shell in the container" @echo " fsw - Build FSW" @echo " gsw - Build GSW" + @echo " mold - Create new component from demo template (Usage: make mold COMP=)" @echo " sim - Build Simulith and component simulators" @echo " start - Start lab services" @echo " stop - Stop lab and CLI services, clean up Docker images" @@ -98,20 +111,24 @@ sim: cfg @for dir in $(CURDIR)/comp/*/sim ; do \ if [ -d "$$dir" ] && [ -f "$$dir/Makefile" ]; then \ echo "Building component in $$dir"; \ - $(MAKE) -C "$$dir" runtime; \ + $(MAKE) -C "$$dir" build; \ fi; \ done cd $(CURDIR)/simulith && $(MAKE) director && $(MAKE) server start: cfg - docker compose -f ./cfg/lab-compose.yml up + docker compose -f ./cfg/lab-compose.yaml up stop: - docker compose -f ./cfg/cli-compose.yml down - docker compose -f ./cfg/lab-compose.yml down + docker compose -f ./cfg/cli-compose.yaml down --remove-orphans + docker compose -f ./cfg/lab-compose.yaml down --remove-orphans docker images -f "dangling=true" -q | xargs -r docker rmi + @echo "" + @echo "To cleanup Docker build cache, run: make clean-cache" + @echo "To cleanup everything Docker, run: docker system prune -a" -uninstall: clean +uninstall: clean clean-cache + rm -f $(CFG_DIR)/active.yaml $(CFG_DIR)/build.yaml docker ps -a --filter "name=tryspace-" -q | xargs -r docker rm -f docker images "tryspace-*" -q | xargs -r docker rmi docker volume ls -q --filter "name=gsw-data" | xargs -r docker volume rm diff --git a/atlas b/atlas index b2640ae..960e60c 160000 --- a/atlas +++ b/atlas @@ -1 +1 @@ -Subproject commit b2640ae08c899bc5f74d0e654554a7d254abe369 +Subproject commit 960e60cffa4eab8652d1cc6f82bf94199d05d217 diff --git a/cfg/cli-compose.yml b/cfg/cli-compose.j2 similarity index 79% rename from cfg/cli-compose.yml rename to cfg/cli-compose.j2 index 312ad0a..62f9de1 100644 --- a/cfg/cli-compose.yml +++ b/cfg/cli-compose.j2 @@ -1,9 +1,5 @@ -# Start -# docker compose -f cli-compose.yml up -# View specific containers -# docker attach tryspace-comp-demo-cli -# docker attach tryspace-sim -# docker attach tryspace-director +# Jinja2 template for dynamic CLI Compose file +# Usage: Render with orchestrator using active CLI component services: tryspace-sim: image: tryspace-server:latest @@ -40,9 +36,9 @@ services: tmpfs: - /dev/shm:noexec,nosuid,size=100m - tryspace-comp-demo-cli: - image: tryspace-comp-demo-cli:latest - container_name: tryspace-comp-demo-cli + tryspace-comp-{{ cli_component }}-cli: + image: tryspace-comp-{{ cli_component }}-cli:latest + container_name: tryspace-comp-{{ cli_component }}-cli networks: - tryspace-net stdin_open: true diff --git a/cfg/drm/drm.yaml b/cfg/drm/drm.yaml index 40540e8..0ca2482 100644 --- a/cfg/drm/drm.yaml +++ b/cfg/drm/drm.yaml @@ -1,6 +1,7 @@ mission_name: "drm" components: - name: "demo" + - name: "eps" scenarios: - name: "nominal" config_file: "drm/scenarios/drm-nominal.yaml" diff --git a/cfg/lab-compose.yml b/cfg/lab-compose.yaml similarity index 97% rename from cfg/lab-compose.yml rename to cfg/lab-compose.yaml index 444934d..917c325 100644 --- a/cfg/lab-compose.yml +++ b/cfg/lab-compose.yaml @@ -1,5 +1,5 @@ # Start -# docker compose -f lab-compose.yml up +# docker compose -f lab-compose.yaml up # View specific containers # docker attach tryspace-fsw # docker attach tryspace-director diff --git a/cfg/tryspace-comp-mold.py b/cfg/tryspace-comp-mold.py new file mode 100644 index 0000000..621f45d --- /dev/null +++ b/cfg/tryspace-comp-mold.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +TrySpace Component Mold Generator + +Generate a new component by: +- Copy all files from ./comp/demo to ./comp/ +- Change file contents from "demo" to the provided component name +- Rename files that contain "demo" in their names +""" + +import os +import sys +import shutil +import re +import argparse +from pathlib import Path + + +def validate_component_name(name): + """Validate the component name follows naming conventions.""" + if not name: + raise ValueError("Component name cannot be empty") + + # Check for valid characters (alphanumeric and underscore) + if not re.match(r'^[a-zA-Z][a-zA-Z0-9_]*$', name): + raise ValueError("Component name must start with a letter and contain only letters, numbers, and underscores") + + # Convert to lowercase for consistency + return name.lower() + + +def get_name_variants(component_name): + """Generate different case variants of the component name.""" + name_lower = component_name.lower() + name_first = name_lower.capitalize() + name_upper = name_lower.upper() + + return { + 'lower': name_lower, + 'first': name_first, + 'upper': name_upper + } + + +def ignore_patterns(dir, files): + """Define patterns to ignore during copy.""" + ignored = [] + for file in files: + # Ignore git files and directories + if file == '.git' or file == '.gitmodules': + ignored.append(file) + # Ignore build directories + elif file == 'build': + ignored.append(file) + # Ignore device configuration + elif file == 'device_cfg.h': + ignored.append(file) + # Ignore common temporary/cache files + elif file.startswith('.') and file.endswith(('.swp', '.tmp', '.cache')): + ignored.append(file) + return ignored + + +def copy_demo_component(source_dir, target_dir): + """Copy the demo component directory to the new component directory.""" + if target_dir.exists(): + response = input(f"Component '{target_dir.name}' already exists. Overwrite? (y/N): ") + if response.lower() != 'y': + print("Aborting component creation.") + sys.exit(0) + shutil.rmtree(target_dir) + + print(f"Copying demo component from {source_dir} to {target_dir}") + shutil.copytree(source_dir, target_dir, ignore=ignore_patterns) + + print("Skipped git files, build directories, and temporary files during copy") + + +def replace_content_in_files(target_dir, name_variants): + """Replace 'demo' with component name variants in file contents.""" + print("Updating file contents...") + + # Define replacement patterns + replacements = [ + # Component name variants + ('DEMO', name_variants['upper']), + ('Demo', name_variants['first']), + ('demo', name_variants['lower']), + # Change USART device from 5 to 9 in device config + ('/dev/usart_5', '/dev/usart_9'), + ('handle: 5', 'handle: 9'), + # Change message IDs to avoid conflicts (A to C, B to D) + ('0x18FA', '0x18FC'), + ('0x18FB', '0x18FD'), + ('0x08FA', '0x08FC'), + ('0x08FB', '0x08FD') + ] + + # Find all text files (exclude binary files) + text_extensions = {'.c', '.h', '.cmake', '.cli', '.txt', '.md', '.yml', '.yaml', '.xtce', '.scr', '.j2'} + + for file_path in target_dir.rglob('*'): + if file_path.is_file() and (file_path.suffix.lower() in text_extensions or file_path.name in ['CMakeLists.txt', 'Makefile']): + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + modified = False + for old_pattern, new_pattern in replacements: + if old_pattern in content: + content = content.replace(old_pattern, new_pattern) + modified = True + + if modified: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + print(f" Updated: {file_path.relative_to(target_dir)}") + + except (UnicodeDecodeError, PermissionError) as e: + print(f" Skipped: {file_path.relative_to(target_dir)} ({e})") + + +def rename_files_and_dirs(target_dir, name_variants): + """Rename files and directories that contain 'demo' in their names.""" + print("Renaming files and directories...") + + # Define rename patterns (order matters - do DEMO first, then Demo, then demo) + rename_patterns = [ + ('DEMO', name_variants['upper']), + ('Demo', name_variants['first']), + ('demo', name_variants['lower']) + ] + + # Collect all paths that need renaming (files and directories) + paths_to_rename = [] + for item in target_dir.rglob('*'): + for old_pattern, new_pattern in rename_patterns: + if old_pattern in item.name: + new_name = item.name.replace(old_pattern, new_pattern) + new_path = item.parent / new_name + paths_to_rename.append((item, new_path)) + break # Only apply the first matching pattern + + # Sort by depth (deepest first) to avoid path conflicts + paths_to_rename.sort(key=lambda x: len(x[0].parts), reverse=True) + + # Perform renames + for old_path, new_path in paths_to_rename: + if old_path.exists() and old_path != new_path: + print(f" Renaming: {old_path.relative_to(target_dir)} -> {new_path.relative_to(target_dir)}") + old_path.rename(new_path) + + +def main(): + parser = argparse.ArgumentParser(description="Generate a new TrySpace component from the demo template") + parser.add_argument('component_name', help='Name of the new component') + parser.add_argument('--source', default='demo', help='Source component to copy from (default: demo)') + + args = parser.parse_args() + + try: + # Validate and normalize component name + component_name = validate_component_name(args.component_name) + name_variants = get_name_variants(component_name) + + print(f"Creating new component: {component_name}") + print(f" Lower case: {name_variants['lower']}") + print(f" First cap: {name_variants['first']}") + print(f" Upper case: {name_variants['upper']}") + print() + + # Determine paths + script_dir = Path(__file__).parent + workspace_dir = script_dir.parent + comp_dir = workspace_dir / 'comp' + source_dir = comp_dir / args.source + target_dir = comp_dir / component_name + + # Validate source directory exists + if not source_dir.exists(): + raise FileNotFoundError(f"Source component '{args.source}' not found at {source_dir}") + + # Create the new component + copy_demo_component(source_dir, target_dir) + replace_content_in_files(target_dir, name_variants) + rename_files_and_dirs(target_dir, name_variants) + + print() + print(f"Component mold creation complete!") + print(f"New component created at: {target_dir}") + print() + print("Next steps:") + print(f" 1. Review the generated component in ./comp/{component_name}") + print(f" 2. Add the component to your active mission, default is ./cfg/drm/drm.yaml") + print(f" 3. Add the component to the FSW definitions:") + print(f" ./fsw/tryspace_defs/cpu1_cfe_es_startup.scr") + print(f" ./fsw/tryspace_defs/tables/sch_def_msgtbl.c") + print(f" ./fsw/tryspace_defs/tables/sch_def_schtbl.c") + print(f" ./fsw/tryspace_defs/tables/to_lab_sub.c") + print(f" ./fsw/tryspace_defs/targets.cmake") + print(f" 4. Add the component to the GSW definitions:") + print(f" ./gsw/src/main/yamcs/etc/etc/yamcs.nos3.yaml") + print(f" 5. Build like you would normally and confirm new component runs") + print(f" 6. Customize the component for your specific needs") + print() + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/cfg/tryspace-config.yaml b/cfg/tryspace-config.yaml index eea3f2b..fbef272 100644 --- a/cfg/tryspace-config.yaml +++ b/cfg/tryspace-config.yaml @@ -4,3 +4,6 @@ build: config_file: "drm/drm.yaml" components: - name: "demo" + - name: "eps" + cli: + - name: "demo" diff --git a/cfg/tryspace-orchestrator.py b/cfg/tryspace-orchestrator.py index 7f935c4..7cb014f 100644 --- a/cfg/tryspace-orchestrator.py +++ b/cfg/tryspace-orchestrator.py @@ -40,13 +40,14 @@ def main(): mission_cfg = load_yaml(mission_cfg_path) scenarios = mission_cfg.get("scenarios", []) scenario = scenarios[0]["name"] if scenarios else "nominal" - active = {"mission": mission, "scenario": scenario} + active = {"mission": mission, "scenario": scenario, "cli": "demo"} with open(ACTIVE_PATH, "w") as f: yaml.safe_dump(active, f) print(f"[orchestrator] Created {ACTIVE_PATH} with defaults: mission={mission}, scenario={scenario}") mission = active.get("mission") scenario = active.get("scenario", "nominal") + cli_component = active.get("cli", "demo") # Find mission config file mission_entry = next((m for m in global_cfg["build"]["missions"] if m["name"] == mission), None) @@ -125,6 +126,20 @@ def main(): else: print(f"[orchestrator] No config or template found for component '{comp_name}', skipping.") + # Render cli-compose.yaml from Jinja2 template using cli_component + cli_template_path = os.path.abspath(os.path.join(CFG_DIR)) + cli_template_file = "cli-compose.j2" + cli_template_full_path = os.path.join(cli_template_path, cli_template_file) + cli_compose_output_path = os.path.join(CFG_DIR, "cli-compose.yaml") + if os.path.exists(cli_template_full_path): + env = Environment(loader=FileSystemLoader(cli_template_path)) + template = env.get_template(cli_template_file) + output = template.render(cli_component=cli_component) + with open(cli_compose_output_path, "w") as f: + f.write(output) + print(f"[orchestrator] cli-compose.yaml written to {cli_compose_output_path} (cli_component={cli_component})") + else: + print(f"[orchestrator] cli-compose.j2 template not found, skipping cli-compose.yaml generation.") if __name__ == "__main__": main() diff --git a/comp/demo b/comp/demo index ff15044..57d5ea1 160000 --- a/comp/demo +++ b/comp/demo @@ -1 +1 @@ -Subproject commit ff150449448af06b91ae6083be220aa06febf749 +Subproject commit 57d5ea1aeb665bfb1c37a2818e1a6d29e283f4f1 diff --git a/comp/eps b/comp/eps new file mode 160000 index 0000000..2e8f87b --- /dev/null +++ b/comp/eps @@ -0,0 +1 @@ +Subproject commit 2e8f87b3e4332174e2ab2c5635b75e8e508cdb62 diff --git a/fsw b/fsw index f9f1a7d..dd35df9 160000 --- a/fsw +++ b/fsw @@ -1 +1 @@ -Subproject commit f9f1a7dad249bc98bba622d2636362b46a9271ef +Subproject commit dd35df945eb69769e1a7324f765203993260dee2 diff --git a/gsw b/gsw index aef7367..b304b47 160000 --- a/gsw +++ b/gsw @@ -1 +1 @@ -Subproject commit aef73672a950f438602b9f0ba6a232dc35879be2 +Subproject commit b304b47ef13ad554efd95b7e0bf310c191998e40 diff --git a/simulith b/simulith index 7356dbc..a62a722 160000 --- a/simulith +++ b/simulith @@ -1 +1 @@ -Subproject commit 7356dbc338bdf5867150a580abea0d124abf47c7 +Subproject commit a62a722010e49a88b24377de1725f1fe0fbc7a48