From b801835f4c5de91a6ec7dd9d19c792ba95413e37 Mon Sep 17 00:00:00 2001 From: aidnem <99768676+aidnem@users.noreply.github.com> Date: Thu, 1 May 2025 17:34:08 -0400 Subject: [PATCH 1/3] (#34) Add print_err and print_warn functions --- src/robotvibecoder/cli.py | 44 ++++++++++++++++++++++ src/robotvibecoder/config.py | 17 +++++---- src/robotvibecoder/constants.py | 17 --------- src/robotvibecoder/main.py | 6 +-- src/robotvibecoder/subcommands/generate.py | 17 +++++---- src/robotvibecoder/subcommands/new.py | 7 ++-- src/robotvibecoder/templating.py | 23 +++++------ 7 files changed, 78 insertions(+), 53 deletions(-) create mode 100644 src/robotvibecoder/cli.py diff --git a/src/robotvibecoder/cli.py b/src/robotvibecoder/cli.py new file mode 100644 index 0000000..bdb946e --- /dev/null +++ b/src/robotvibecoder/cli.py @@ -0,0 +1,44 @@ +""" +Utilities for printing warnings and errors and colors +""" + +from dataclasses import dataclass +import sys +from typing import TextIO + + +@dataclass +class Colors: + """ + ANSI Escape Codes for text colors and effects + """ + + fg_red = "\x1b[31m" + fg_green = "\x1b[32m" + fg_cyan = "\x1b[36m" + bold = "\x1b[1m" + reset = "\x1b[0m" + + title_str = f"{fg_cyan}RobotVibeCoder{reset}" + + +def print_err(message: str, file: TextIO = sys.stderr): + """Print an error message + + :param message: The error message. + :type message: str + :param file: File to print the error message to, defaults to sys.stderr + :type file: _type_, optional + """ + print(f"{Colors.fg_red}{Colors.bold}Error{Colors.reset}: {message}", file=file) + + +def print_warning(message: str, file: TextIO = sys.stderr): + """Print a warning message + + :param message: The warning message. + :type message: str + :param file: File to print the warning message to, defaults to sys.stderr + :type file: _type_, optional + """ + print(f"{Colors.fg_red}{Colors.bold}Warning{Colors.reset}: {message}", file=file) diff --git a/src/robotvibecoder/config.py b/src/robotvibecoder/config.py index 342b126..476e1f6 100644 --- a/src/robotvibecoder/config.py +++ b/src/robotvibecoder/config.py @@ -10,6 +10,8 @@ import json import sys +from robotvibecoder.cli import print_err + class MechanismKind(str, Enum): """ @@ -49,14 +51,13 @@ def generate_config_from_data(data: dict) -> MechanismConfig: """ for key in data: if key not in [field.name for field in fields(MechanismConfig)]: - print(f"Error: Config contained unexpected field `{key}`", file=sys.stdout) + print_err(f"Config contained unexpected field `{key}`") sys.exit(1) for field in fields(MechanismConfig): if field.name not in data: - print( - f"Error: Config missing field `{field.name}`", - file=sys.stdout, + print_err( + f"Config missing field `{field.name}`", ) sys.exit(1) @@ -79,10 +80,10 @@ def load_json_config(config_path: str) -> MechanismConfig: with open(config_path, "r", encoding="utf-8") as config_file: data = json.load(config_file) except FileNotFoundError: - print(f"Error: Specified config file {config_path} does not exist.") + print_err(f"Specified config file {config_path} does not exist.") sys.exit(1) except json.JSONDecodeError: - print(f"Error: Invalid JSON format in {config_path}") + print_err(f"Invalid JSON format in {config_path}") sys.exit(1) return generate_config_from_data(data) @@ -94,8 +95,8 @@ def validate_config(config: MechanismConfig) -> None: """ if config.lead_motor not in config.motors: - print( - f"Error in `{config.name}` config: `lead_motor` must be one of the motors listed in `motors`" # pylint: disable=line-too-long + print_err( + f"`{config.name}` config: `lead_motor` must be one of the motors listed in `motors`" # pylint: disable=line-too-long ) print( diff --git a/src/robotvibecoder/constants.py b/src/robotvibecoder/constants.py index b1a217b..d5d7d23 100644 --- a/src/robotvibecoder/constants.py +++ b/src/robotvibecoder/constants.py @@ -1,9 +1,7 @@ """ Constants that will be reused/don't need to live in code, e.g. default config -& color escape codes """ -from dataclasses import dataclass from robotvibecoder.config import MechanismConfig, MechanismKind @@ -16,18 +14,3 @@ "leftMotor", "exampleEncoder", ) - - -@dataclass -class Colors: - """ - ANSI Escape Codes for text colors and effects - """ - - fg_red = "\x1b[31m" - fg_green = "\x1b[32m" - fg_cyan = "\x1b[36m" - bold = "\x1b[1m" - reset = "\x1b[0m" - - title_str = f"{fg_cyan}RobotVibeCoder{reset}" diff --git a/src/robotvibecoder/main.py b/src/robotvibecoder/main.py index 01fce69..0ce9e8e 100644 --- a/src/robotvibecoder/main.py +++ b/src/robotvibecoder/main.py @@ -3,7 +3,7 @@ """ import argparse -from robotvibecoder import constants +from robotvibecoder import cli from robotvibecoder.subcommands.new import new from robotvibecoder.subcommands.generate import generate @@ -58,9 +58,7 @@ def main() -> None: # Call the default function defined by the subcommand args.func(args) - print( - f"{constants.Colors.fg_green}{constants.Colors.bold}Done.{constants.Colors.reset}" - ) + print(f"{cli.Colors.fg_green}{cli.Colors.bold}Done.{cli.Colors.reset}") if __name__ == "__main__": diff --git a/src/robotvibecoder/subcommands/generate.py b/src/robotvibecoder/subcommands/generate.py index 74ee2a8..a4e4aa5 100644 --- a/src/robotvibecoder/subcommands/generate.py +++ b/src/robotvibecoder/subcommands/generate.py @@ -7,7 +7,8 @@ import os import sys -from robotvibecoder import constants +from robotvibecoder import cli +from robotvibecoder.cli import print_err from robotvibecoder.config import ( MechanismConfig, MechanismKind, @@ -29,12 +30,12 @@ def generate(args: Namespace) -> None: config: MechanismConfig = generate_config_from_data(data) else: if args.config is None: - print( - "Error: Config not specified: Either --stdin or --config [file] must be supplied to command." # pylint: disable=line-too-long + print_err( + "Config not specified: Either --stdin or --config [file] must be supplied to command." # pylint: disable=line-too-long ) sys.exit(1) config_path = os.path.join(args.folder, args.config) - print(f"[{constants.Colors.title_str}] Reading config file at {config_path}") + print(f"[{cli.Colors.title_str}] Reading config file at {config_path}") config = load_json_config(config_path) validate_config(config) @@ -54,7 +55,7 @@ def generate(args: Namespace) -> None: if not args.stdin: print( - f"{constants.Colors.fg_red}{constants.Colors.bold}WARNING{constants.Colors.reset}: This will create/overwrite files at the following paths:" # pylint: disable=line-too-long + f"{cli.Colors.fg_red}{cli.Colors.bold}WARNING{cli.Colors.reset}: This will create/overwrite files at the following paths:" # pylint: disable=line-too-long ) for file_template, file_output in template_to_output_map.items(): output_path = os.path.join( @@ -75,12 +76,12 @@ def generate(args: Namespace) -> None: if os.path.exists(output_path) and args.stdin: # stdin mode skips the warning prompt at the start, so files would # be destroyed, necessitating this check - print( - f"Error: File {output_path} already exists. Please move/delete it and retry" + print_err( + f"File {output_path} already exists. Please move/delete it and retry" ) sys.exit(1) - print(f"{constants.Colors.fg_cyan}➜{constants.Colors.reset} {output_path}") + print(f"{cli.Colors.fg_cyan}➜{cli.Colors.reset} {output_path}") template_path = file_template template = env.get_template(template_path) diff --git a/src/robotvibecoder/subcommands/new.py b/src/robotvibecoder/subcommands/new.py index 236e2aa..78f720c 100644 --- a/src/robotvibecoder/subcommands/new.py +++ b/src/robotvibecoder/subcommands/new.py @@ -8,6 +8,7 @@ import sys from robotvibecoder import constants +import robotvibecoder.cli from robotvibecoder.config import MechanismConfig, MechanismKind @@ -76,9 +77,9 @@ def new(args: Namespace) -> None: """ config_path = os.path.join(args.folder, args.outfile) - print(f"[{constants.Colors.title_str}] Creating a new config file") + print(f"[{robotvibecoder.cli.Colors.title_str}] Creating a new config file") print( - f" {constants.Colors.fg_red}{constants.Colors.bold}WARNING{constants.Colors.reset}: This will create/overwrite a file at `{constants.Colors.fg_cyan}{config_path}{constants.Colors.reset}`" # pylint: disable=line-too-long + f" {robotvibecoder.cli.Colors.fg_red}{robotvibecoder.cli.Colors.bold}WARNING{robotvibecoder.cli.Colors.reset}: This will create/overwrite a file at `{robotvibecoder.cli.Colors.fg_cyan}{config_path}{robotvibecoder.cli.Colors.reset}`" # pylint: disable=line-too-long ) try: input(" Press Ctrl+C to cancel or [Enter] to continue") @@ -92,6 +93,6 @@ def new(args: Namespace) -> None: else: config = constants.DEFAULT_CONFIG - print(f"[{constants.Colors.title_str}] Writing config file") + print(f"[{robotvibecoder.cli.Colors.title_str}] Writing config file") with open(config_path, "w+", encoding="utf-8") as outfile: json.dump(config.__dict__, fp=outfile, indent=2) diff --git a/src/robotvibecoder/templating.py b/src/robotvibecoder/templating.py index c1427af..53f6ef8 100644 --- a/src/robotvibecoder/templating.py +++ b/src/robotvibecoder/templating.py @@ -4,7 +4,8 @@ from jinja2 import Environment, PackageLoader, select_autoescape -from robotvibecoder import constants +from robotvibecoder import cli +from robotvibecoder.cli import print_err def article(word: str) -> str: @@ -62,9 +63,9 @@ def hash_can_id(device: str) -> str: if device not in GlobalTemplateState.can_id_map: next_id = GlobalTemplateState.new_id() GlobalTemplateState.can_id_map[device] = next_id - print(f" {constants.Colors.fg_green}➜{constants.Colors.reset} ", end="") + print(f" {cli.Colors.fg_green}➜{cli.Colors.reset} ", end="") print( - f"Mapped device {constants.Colors.fg_cyan}{device}{constants.Colors.reset} to placeholder CAN ID {constants.Colors.fg_cyan}{next_id}{constants.Colors.reset}" # pylint: disable=line-too-long + f"Mapped device {cli.Colors.fg_cyan}{device}{cli.Colors.reset} to placeholder CAN ID {cli.Colors.fg_cyan}{next_id}{cli.Colors.reset}" # pylint: disable=line-too-long ) return str(GlobalTemplateState.can_id_map[device]) @@ -80,8 +81,8 @@ def pos_dimension(kind: str) -> str: return "Distance" # Flywheels won't have a position - print( - f"{constants.Colors.fg_red}Error:{constants.Colors.reset} Invalid kind {kind} passed to pos_dimension." # pylint: disable=line-too-long + print_err( + f"Invalid kind {kind} passed to pos_dimension." # pylint: disable=line-too-long ) print( "This is a robotvibecoder issue, NOT a user error. Please report this on github!" @@ -108,8 +109,8 @@ def pos_unit(kind: str) -> str: if kind == "Elevator": return "Meters" - print( - f"{constants.Colors.fg_red}Error:{constants.Colors.reset} Invalid kind {kind} passed to pos_unit." # pylint: disable=line-too-long + print_err( + f"Invalid kind {kind} passed to pos_unit." # pylint: disable=line-too-long ) print( "This is a robotvibecoder issue, NOT user error. Please report this on github!" @@ -141,9 +142,7 @@ def goal(kind: str) -> str: if kind == "Flywheel": return "Speed" - print( - f"{constants.Colors.fg_red}Error:{constants.Colors.reset} Invalid kind {kind} passed to goal." # pylint: disable=line-too-long - ) + print_err(f"Invalid kind {kind} passed to goal.") # pylint: disable=line-too-long print( "This is a robotvibecoder issue, NOT a user error. Please report this on github!" ) @@ -164,9 +163,7 @@ def goal_dimension(kind: str) -> str: if kind == "Flywheel": return "AngularVelocity" - print( - f"{constants.Colors.fg_red}Error:{constants.Colors.reset} Invalid kind {kind} passed to goal_dimension." # pylint: disable=line-too-long - ) + print_err(f"Invalid kind {kind} passed to goal_dimension.") print( "This is a robotvibecoder issue, NOT a user error. Please report this on github!" ) From 51968d292a120a5c491affceaab68c19b6819463 Mon Sep 17 00:00:00 2001 From: aidnem <99768676+aidnem@users.noreply.github.com> Date: Thu, 1 May 2025 17:36:25 -0400 Subject: [PATCH 2/3] (#34) Fully move to print_warning --- src/robotvibecoder/subcommands/generate.py | 6 +++--- src/robotvibecoder/subcommands/new.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/robotvibecoder/subcommands/generate.py b/src/robotvibecoder/subcommands/generate.py index a4e4aa5..b129355 100644 --- a/src/robotvibecoder/subcommands/generate.py +++ b/src/robotvibecoder/subcommands/generate.py @@ -8,7 +8,7 @@ import sys from robotvibecoder import cli -from robotvibecoder.cli import print_err +from robotvibecoder.cli import print_err, print_warning from robotvibecoder.config import ( MechanismConfig, MechanismKind, @@ -54,8 +54,8 @@ def generate(args: Namespace) -> None: } if not args.stdin: - print( - f"{cli.Colors.fg_red}{cli.Colors.bold}WARNING{cli.Colors.reset}: This will create/overwrite files at the following paths:" # pylint: disable=line-too-long + print_warning( + "This will create/overwrite files at the following paths:" # pylint: disable=line-too-long ) for file_template, file_output in template_to_output_map.items(): output_path = os.path.join( diff --git a/src/robotvibecoder/subcommands/new.py b/src/robotvibecoder/subcommands/new.py index 78f720c..d233482 100644 --- a/src/robotvibecoder/subcommands/new.py +++ b/src/robotvibecoder/subcommands/new.py @@ -78,8 +78,9 @@ def new(args: Namespace) -> None: config_path = os.path.join(args.folder, args.outfile) print(f"[{robotvibecoder.cli.Colors.title_str}] Creating a new config file") - print( - f" {robotvibecoder.cli.Colors.fg_red}{robotvibecoder.cli.Colors.bold}WARNING{robotvibecoder.cli.Colors.reset}: This will create/overwrite a file at `{robotvibecoder.cli.Colors.fg_cyan}{config_path}{robotvibecoder.cli.Colors.reset}`" # pylint: disable=line-too-long + print(" ", end="", file=sys.stderr) # Indent the warning on the line below + robotvibecoder.cli.print_warning( + f"This will create/overwrite a file at `{robotvibecoder.cli.Colors.fg_cyan}{config_path}{robotvibecoder.cli.Colors.reset}`" # pylint: disable=line-too-long ) try: input(" Press Ctrl+C to cancel or [Enter] to continue") From 9fa03490bcd2cb7130941323e779f866a7bf22f5 Mon Sep 17 00:00:00 2001 From: aidnem <99768676+aidnem@users.noreply.github.com> Date: Thu, 1 May 2025 17:38:44 -0400 Subject: [PATCH 3/3] (#34) Detect duplicate motors and throw errors --- examples/ElevatorIO.java | 3 +++ examples/ElevatorIOTalonFX.java | 3 ++- examples/exampleconfig.json | 7 ++----- src/robotvibecoder/config.py | 11 +++++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/ElevatorIO.java b/examples/ElevatorIO.java index 99eee31..71247c7 100644 --- a/examples/ElevatorIO.java +++ b/examples/ElevatorIO.java @@ -81,6 +81,9 @@ public static class ElevatorOutputs { /** The voltage currently applied to the motors */ public MutVoltage elevatorAppliedVolts = Volts.mutable(0.0); + /** The current closed-loop output from Motion Magic */ + public double elevatorClosedLoopOutput = 0.0; + /** Contribution of the p-term to motor output */ public MutVoltage pContrib = Volts.mutable(0.0); diff --git a/examples/ElevatorIOTalonFX.java b/examples/ElevatorIOTalonFX.java index 6cacf23..c2226a6 100644 --- a/examples/ElevatorIOTalonFX.java +++ b/examples/ElevatorIOTalonFX.java @@ -209,7 +209,8 @@ public void applyOutputs(ElevatorOutputs outputs) { "elevator/referenceSlope", leadMotor.getClosedLoopReferenceSlope().getValueAsDouble()); outputs.elevatorAppliedVolts.mut_replace( - Volts.of(leadMotor.getClosedLoopOutput().getValueAsDouble())); + leadMotor.getMotorVoltage().getValue()); + outputs.elevatorClosedLoopOutput = leadMotor.getClosedLoopOutput().getValueAsDouble(); outputs.pContrib.mut_replace( Volts.of(leadMotor.getClosedLoopProportionalOutput().getValueAsDouble())); outputs.iContrib.mut_replace( diff --git a/examples/exampleconfig.json b/examples/exampleconfig.json index c3224be..8d4f337 100644 --- a/examples/exampleconfig.json +++ b/examples/exampleconfig.json @@ -3,10 +3,7 @@ "name": "Elevator", "kind": "Elevator", "canbus": "canivore", - "motors": [ - "leadMotor", - "followerMotor" - ], + "motors": ["leadMotor", "followerMotor"], "lead_motor": "leadMotor", "encoder": "elevatorEncoder" -} \ No newline at end of file +} diff --git a/src/robotvibecoder/config.py b/src/robotvibecoder/config.py index 476e1f6..13e1f06 100644 --- a/src/robotvibecoder/config.py +++ b/src/robotvibecoder/config.py @@ -103,3 +103,14 @@ def validate_config(config: MechanismConfig) -> None: f" Found `{config.lead_motor}` but expected one of {', '.join(['`' + motor + '`' for motor in config.motors])}" # pylint: disable=line-too-long ) sys.exit(1) + + if len(config.motors) != len(set(config.motors)): + motors_seen: set[str] = set() + + for motor in config.motors: + if motor not in motors_seen: + motors_seen.add(motor) + else: + print_err(f"`{config.name}` config: Duplicate motor {motor}") + + sys.exit(1)