1+ #!/usr/bin/env python3
2+ """
3+ Script to run an opmode class derived from the OpMode base class.
4+
5+ Usage:
6+ python run_opmode.py <opmode_file.py>
7+
8+ The opmode file should contain a class that inherits from OpMode.
9+ """
10+
11+ import sys
12+ import time
13+ import importlib .util
14+ import inspect
15+ import argparse
16+ from pathlib import Path
17+
18+ # Add the current directory to Python path to import local modules
19+ sys .path .insert (0 , str (Path (__file__ ).parent ))
20+
21+ from opmode import OpMode
22+ from robot import Robot
23+
24+
25+ def find_opmode_class (module ):
26+ """
27+ Find the first class in the module that inherits from OpMode.
28+
29+ Args:
30+ module: The imported Python module
31+
32+ Returns:
33+ The OpMode-derived class, or None if not found
34+ """
35+ for name , obj in inspect .getmembers (module , inspect .isclass ):
36+ if obj != OpMode and issubclass (obj , OpMode ):
37+ return obj
38+ return None
39+
40+
41+ def load_opmode_from_file (file_path ):
42+ """
43+ Dynamically load an opmode class from a Python file.
44+
45+ Args:
46+ file_path: Path to the Python file containing the opmode class
47+
48+ Returns:
49+ The OpMode-derived class
50+
51+ Raises:
52+ FileNotFoundError: If the file doesn't exist
53+ ImportError: If the file can't be imported
54+ ValueError: If no OpMode-derived class is found
55+ """
56+ file_path = Path (file_path )
57+
58+ if not file_path .exists ():
59+ raise FileNotFoundError (f"OpMode file not found: { file_path } " )
60+
61+ if not file_path .suffix == '.py' :
62+ raise ValueError (f"File must be a Python file (.py): { file_path } " )
63+
64+ # Create module spec and load the module
65+ spec = importlib .util .spec_from_file_location ("opmode_module" , file_path )
66+ if spec is None or spec .loader is None :
67+ raise ImportError (f"Could not load module from { file_path } " )
68+
69+ module = importlib .util .module_from_spec (spec )
70+ spec .loader .exec_module (module )
71+
72+ # Find the OpMode-derived class
73+ opmode_class = find_opmode_class (module )
74+ if opmode_class is None :
75+ raise ValueError (f"No class derived from OpMode found in { file_path } " )
76+
77+ return opmode_class
78+
79+
80+ def run_opmode (opmode_file , duration = None , loop_frequency = 50 ):
81+ """
82+ Run the opmode with the specified parameters.
83+
84+ Args:
85+ opmode_file: Path to the opmode Python file
86+ duration: How long to run in seconds (None for infinite)
87+ loop_frequency: Loops per second (default: 50 Hz)
88+ """
89+ print (f"Loading opmode from: { opmode_file } " )
90+
91+ try :
92+ # Load the opmode class
93+ OpModeClass = load_opmode_from_file (opmode_file )
94+ print (f"Found opmode class: { OpModeClass .__name__ } " )
95+
96+ # Create robot instance
97+ print ("Creating robot instance..." )
98+ robot = Robot ()
99+
100+ # Create opmode instance
101+ print ("Initializing opmode..." )
102+ opmode = OpModeClass (robot )
103+
104+ # Call start method
105+ print ("Starting opmode..." )
106+ opmode .start ()
107+
108+ # Calculate loop timing
109+ loop_period = 1.0 / loop_frequency
110+ print (f"Running main loop at { loop_frequency } Hz..." )
111+
112+ # Main loop
113+ loop_count = 0
114+ start_time = time .time ()
115+
116+ try :
117+ while True :
118+ loop_start = time .time ()
119+
120+ # Call the loop method
121+ opmode .loop ()
122+ loop_count += 1
123+
124+ # Check if we should stop based on duration
125+ if duration is not None and (time .time () - start_time ) >= duration :
126+ break
127+
128+ # Sleep to maintain loop frequency
129+ elapsed = time .time () - loop_start
130+ sleep_time = max (0 , loop_period - elapsed )
131+
132+ if sleep_time > 0 :
133+ time .sleep (sleep_time )
134+ elif elapsed > loop_period * 1.1 : # Warn if loop is running slow
135+ print (f"Warning: Loop { loop_count } took { elapsed :.3f} s "
136+ f"(target: { loop_period :.3f} s)" )
137+
138+ except KeyboardInterrupt :
139+ print ("\n Received interrupt signal..." )
140+
141+ # Call stop method
142+ print ("Stopping opmode..." )
143+ opmode .stop ()
144+
145+ # Print statistics
146+ total_time = time .time () - start_time
147+ actual_frequency = loop_count / total_time if total_time > 0 else 0
148+
149+ print (f"\n OpMode completed:" )
150+ print (f" Total runtime: { total_time :.2f} seconds" )
151+ print (f" Total loops: { loop_count } " )
152+ print (f" Average frequency: { actual_frequency :.1f} Hz" )
153+
154+ except Exception as e :
155+ print (f"Error running opmode: { e } " )
156+ sys .exit (1 )
157+
158+
159+ def main ():
160+ """Main entry point."""
161+ parser = argparse .ArgumentParser (
162+ description = "Run an opmode class derived from OpMode" ,
163+ formatter_class = argparse .RawDescriptionHelpFormatter ,
164+ epilog = """
165+ Examples:
166+ python run_opmode.py my_opmode.py
167+ python run_opmode.py my_opmode.py --duration 30
168+ python run_opmode.py my_opmode.py --frequency 60
169+ """
170+ )
171+
172+ parser .add_argument (
173+ 'opmode_file' ,
174+ help = 'Python file containing the opmode class'
175+ )
176+
177+ parser .add_argument (
178+ '--duration' , '-d' ,
179+ type = float ,
180+ help = 'Duration to run in seconds (default: run until interrupted)'
181+ )
182+
183+ parser .add_argument (
184+ '--frequency' , '-f' ,
185+ type = int ,
186+ default = 50 ,
187+ help = 'Loop frequency in Hz (default: 50)'
188+ )
189+
190+ parser .add_argument (
191+ '--verbose' , '-v' ,
192+ action = 'store_true' ,
193+ help = 'Enable verbose output'
194+ )
195+
196+ args = parser .parse_args ()
197+
198+ if args .frequency <= 0 :
199+ print ("Error: Frequency must be positive" )
200+ sys .exit (1 )
201+
202+ # Run the opmode
203+ run_opmode (
204+ args .opmode_file ,
205+ duration = args .duration ,
206+ loop_frequency = args .frequency
207+ )
208+
209+
210+ if __name__ == '__main__' :
211+ main ()
0 commit comments