Framework-LED-Matrix 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cli.py ADDED
@@ -0,0 +1,669 @@
1
+ #!/usr/bin/env python3
2
+
3
+ helper = """
4
+ Master Command-Line Interface (CLI) for the LED Matrix project.
5
+
6
+ This script provides a centralized way to run various functions
7
+ from the different modules of the project.
8
+
9
+ USAGE:
10
+ python3 cli.py [COMMAND] [SUBCOMMAND] [OPTIONS]
11
+ ./cli.py [COMMAND] [SUBCOMMAND] [OPTIONS] (if executable)
12
+
13
+ --- EXAMPLES BY COMMAND ---
14
+
15
+ [1] led: Basic hardware commands
16
+ # Get firmware version from modules
17
+ ./cli.py led version
18
+
19
+ # Clear both displays (all LEDs OFF)
20
+ ./cli.py led clear
21
+
22
+ # Fill both displays and hold for 3 seconds
23
+ ./cli.py led fill --hold 3
24
+
25
+ # Start hardware fade/etc animation
26
+ ./cli.py led start-anim
27
+
28
+ # Stop hardware animation
29
+ ./cli.py led stop-anim
30
+
31
+ # Clear display and stop animation
32
+ ./cli.py led reset
33
+
34
+ # Show available serial ports (for debugging)
35
+ ./cli.py led ports
36
+
37
+ [2] text: Render text on the matrix
38
+ # Draw text vertically and hold for 5 seconds
39
+ ./cli.py text vertical "HELLO" --hold 5
40
+
41
+ # Draw text horizontally with a specific font size and offset
42
+ ./cli.py text horizontal "Scrolling" --font-size 8 --x-offset 10
43
+
44
+ [3] anagram: Run anagram-related functions
45
+ # Find and draw all anagrams for the word "post"
46
+ ./cli.py anagram draw post
47
+
48
+ # Do the same, but disable the animation between words
49
+ ./cli.py anagram draw post --no-animate
50
+
51
+ [4] sim: Run complex cellular automata and simulations
52
+ # Run Conway's Game of Life (B3/S23) from a random start
53
+ ./cli.py sim gof
54
+
55
+ # Run Game of Life with a specific pattern (glider)
56
+ ./cli.py sim gof --board glider --steps 300 --delay 0.05
57
+
58
+ # Run a CUSTOM Outer-Totalistic CA (e.g., "HighLife" B3,6/S2,3)
59
+ # NOTE: Rules are now comma-separated.
60
+ ./cli.py sim outer --b-rule "3,6" --s-rule "2,3"
61
+
62
+ # Run a CUSTOM Inner-Totalistic CA (e.t., rule 777)
63
+ # This takes a single number, so double/triple digits are fine.
64
+ ./cli.py sim inner 777 --steps 100
65
+ ./cli.py sim inner 1024 --steps 100
66
+
67
+ # Run the BML traffic simulation
68
+ ./cli.py sim bml --density 0.4 --steps 1000 --delay 0.02
69
+
70
+ # Run the BML simulation LOCALLY (in a Matplotlib window)
71
+ ./cli.py sim bml-local --density 0.4 --steps 1000
72
+
73
+ # Run the HPP Lattice Gas simulation
74
+ ./cli.py sim hpp --density 0.5 --steps 500
75
+
76
+ # Run HPP, but seed the board with math function graphs
77
+ ./cli.py sim hpp-math --graphs 3
78
+
79
+ # Show random greyscale noise for 15 seconds
80
+ ./cli.py sim random-grey --duration 15
81
+
82
+ # Draw anagrams for 3 words, then run GoL on each
83
+ ./cli.py sim anagram-gof --words 3
84
+
85
+ # Show 10 random math graphs, pausing 3 sec each, and hold the last one
86
+ ./cli.py sim math-graphs --num 10 --delay 3 --hold 5
87
+
88
+ [5] math: Run local math visualization tools
89
+ # Plot a predefined function LOCALLY and on the MATRIX
90
+ ./cli.py math plot sin
91
+ """
92
+
93
+ import argparse
94
+ import sys
95
+ import time
96
+ import numpy as np
97
+ from typing import List, Optional
98
+
99
+ # --- Import All Necessary Functions ---
100
+ # We import from the new package structure.
101
+
102
+ try:
103
+ # LED Matrix Core Commands
104
+ from framework_led_matrix.core.led_commands import (
105
+ log,
106
+ reset_modules,
107
+ get_firmware_version,
108
+ clear_graph,
109
+ fill_graph,
110
+ start_animation,
111
+ stop_animation,
112
+ output_ports,
113
+ WIDTH,
114
+ HEIGHT,
115
+ coordinates_to_matrix,
116
+ draw_matrix_on_board
117
+ )
118
+
119
+ # Text Rendering
120
+ from framework_led_matrix.utils.text_rendering import draw_text_vertical, draw_text_horizontal
121
+
122
+ # Anagrams
123
+ from framework_led_matrix.utils.anagrams import draw_anagram_on_matrix
124
+
125
+ # Math Functions
126
+ from framework_led_matrix.core.math_engine import (
127
+ plot_function,
128
+ MATH_OPERATIONS,
129
+ pick_largest_graph,
130
+ REGULAR_OPERATIONS
131
+ )
132
+
133
+ # Simulation Models
134
+ from framework_led_matrix.simulations.BihamMiddletonLevineTrafficModel import run_bml, show_bml_local_animation
135
+ from framework_led_matrix.simulations.HardyPomeauPazzis import run_hpp_simulation, create_hpp_board_np
136
+ from framework_led_matrix.simulations.outer_totalistic import STARTING_STATES_GOF, game_of_life_rules
137
+
138
+ # Runtime Simulation Functions
139
+ from framework_led_matrix.apps.runtime import (
140
+ run_hpp_with_math,
141
+ run_outer_totalistic_simulation,
142
+ game_of_life_totalistic_sim,
143
+ run_bml_simulation,
144
+ run_inner_totalistic_simulation,
145
+ random_greyscale_animation,
146
+ run_anagrams_game_of_life,
147
+ run_draw_anagram_on_matrix,
148
+ run_math_funs_game_of_life,
149
+ show_random_graphs
150
+ )
151
+
152
+ # Background Runner
153
+ from framework_led_matrix.apps.background_runner import run_background_mode
154
+
155
+ except ImportError as e:
156
+ print(f"Error: Failed to import a necessary module: {e}", file=sys.stderr)
157
+ print("Please ensure you are running cli.py from the root directory", file=sys.stderr)
158
+ print("of the graph_functions_on_matrix project.", file=sys.stderr)
159
+ sys.exit(1)
160
+ except Exception as e:
161
+ print(f"An unexpected error occurred during import: {e}", file=sys.stderr)
162
+ sys.exit(1)
163
+
164
+
165
+ # --- Type-Parsing Helper Functions ---
166
+
167
+ def parse_int_list(list_str: str) -> List[int]:
168
+ """Parses a comma-separated list of ints (e.g., '3,6,7') into a list."""
169
+ if not list_str:
170
+ return []
171
+ try:
172
+ # Allow empty strings to return empty lists
173
+ if not list_str.strip():
174
+ return []
175
+ return [int(x.strip()) for x in list_str.split(',')]
176
+ except Exception as e:
177
+ raise argparse.ArgumentTypeError(f"Invalid integer list format: '{list_str}'. Must be comma-separated (e.g., '3,6,7'). Error: {e}")
178
+
179
+
180
+ # --- CLI Definition ---
181
+
182
+ def build_cli():
183
+ """
184
+ Builds the entire argparse CLI structure.
185
+ """
186
+ parser = argparse.ArgumentParser(
187
+ description="Master CLI for LED Matrix controllers and simulations.",
188
+ formatter_class=argparse.RawTextHelpFormatter,
189
+ # Updated main epilog
190
+ epilog="Run a command with -h for more specific help (e.g., ./cli.py sim gof -h)"
191
+ )
192
+
193
+ # --- ADDED: Top-level verbose flag ---
194
+ parser.add_argument(
195
+ '-v', '--verbose',
196
+ action='store_true',
197
+ help='Enable detailed verbose logging for all commands.'
198
+ )
199
+
200
+ subparsers = parser.add_subparsers(dest='command', required=True, help='Main command category')
201
+
202
+ # --- 0. Background Sub-parser ---
203
+ subparsers.add_parser(
204
+ 'background',
205
+ aliases=['bg'],
206
+ help='Run the background runner (screensaver mode).',
207
+ epilog="Example: ./cli.py background"
208
+ )
209
+
210
+ # --- 1. LED Sub-parser ---
211
+ led_parser = subparsers.add_parser(
212
+ 'led',
213
+ help='Basic hardware commands.',
214
+ epilog="Example: ./cli.py led clear"
215
+ )
216
+ led_subparsers = led_parser.add_subparsers(dest='led_command', required=True, help='Specific LED command')
217
+
218
+ led_subparsers.add_parser('version', help='Get firmware version from modules.', epilog="Example: ./cli.py led version")
219
+ led_subparsers.add_parser('clear', help='Clear both displays (all LEDs OFF).', epilog="Example: ./cli.py led clear")
220
+
221
+ # Add 'fill' command with hold argument
222
+ led_fill = led_subparsers.add_parser(
223
+ 'fill',
224
+ help='Fill both displays (all LEDs ON).',
225
+ epilog="Example: ./cli.py led fill --hold 2"
226
+ )
227
+ led_fill.add_argument('--hold', type=float, default=0, help='Seconds to hold the filled screen (default: 0).')
228
+
229
+ led_subparsers.add_parser('start-anim', help='Start hardware animation (e.g., fades).', epilog="Example: ./cli.py led start-anim")
230
+ led_subparsers.add_parser('stop-anim', help='Stop hardware animation.', epilog="Example: ./cli.py led stop-anim")
231
+ led_subparsers.add_parser('reset', help='Clear display and stop animation.', epilog="Example: ./cli.py led reset")
232
+ led_subparsers.add_parser('ports', help='Show available serial ports (for debugging).', epilog="Example: ./cli.py led ports")
233
+
234
+ # --- 2. Text Sub-parser ---
235
+ text_parser = subparsers.add_parser(
236
+ 'text',
237
+ help='Render text on the matrix.',
238
+ epilog="Example: ./cli.py text vertical \"HELLO\" --hold 3"
239
+ )
240
+ text_subparsers = text_parser.add_subparsers(dest='text_command', required=True, help='Specific text command')
241
+
242
+ # 'text vertical' command
243
+ text_vert = text_subparsers.add_parser(
244
+ 'vertical',
245
+ help='Draw text vertically.',
246
+ epilog="Example: ./cli.py text vertical \"MY TEXT\" --font-size 10 --hold 3"
247
+ )
248
+ text_vert.add_argument('text', type=str, help='The text string to render.')
249
+ text_vert.add_argument('--font-size', type=int, default=None, help='Force a specific font size (default: auto).')
250
+ text_vert.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
251
+ text_vert.add_argument('--row-offset', type=int, default=0, help='Row offset from the top (default: 0).')
252
+ text_vert.add_argument('--hold', type=float, default=0, help='Seconds to hold the text on screen after drawing (default: 0).')
253
+
254
+ # 'text horizontal' command
255
+ text_horiz = text_subparsers.add_parser(
256
+ 'horizontal',
257
+ help='Draw text horizontally.',
258
+ epilog="Example: ./cli.py text horizontal \"SCROLL\" --x-offset 5 --hold 3"
259
+ )
260
+ text_horiz.add_argument('text', type=str, help='The text string to render.')
261
+ text_horiz.add_argument('--font-size', type=int, default=None, help='Force a specific font size (default: auto).')
262
+ text_horiz.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
263
+ text_horiz.add_argument('--x-offset', type=int, default=0, help='Horizontal scroll offset (default: 0).')
264
+ text_horiz.add_argument('--y-offset', type=int, default=0, help='Vertical position offset (default: centered).')
265
+ text_horiz.add_argument('--hold', type=float, default=0, help='Seconds to hold the text on screen after drawing (default: 0).')
266
+
267
+ # --- 3. Anagram Sub-parser ---
268
+ anagram_parser = subparsers.add_parser(
269
+ 'anagram',
270
+ help='Run anagram-related functions.',
271
+ epilog="Example: ./cli.py anagram draw post"
272
+ )
273
+ anagram_subparsers = anagram_parser.add_subparsers(dest='anagram_command', required=True, help='Specific anagram command')
274
+
275
+ # 'anagram draw' command
276
+ ana_draw = anagram_subparsers.add_parser(
277
+ 'draw',
278
+ help='Find and draw anagrams for a word.',
279
+ epilog="Example: ./cli.py anagram draw listen"
280
+ )
281
+ ana_draw.add_argument('word', type=str, help='The word to find anagrams for.')
282
+ ana_draw.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
283
+ ana_draw.add_argument('--no-animate', action='store_false', dest='animate', help='Disable animation between words.')
284
+
285
+ # --- 4. Simulation Sub-parser ---
286
+ sim_parser = subparsers.add_parser(
287
+ 'sim',
288
+ help='Run complex simulations.',
289
+ epilog="Example: ./cli.py sim gof --board glider --steps 100"
290
+ )
291
+ sim_subparsers = sim_parser.add_subparsers(dest='sim_command', required=True, help='Specific simulation to run')
292
+
293
+ # 'sim bml' command
294
+ sim_bml = sim_subparsers.add_parser(
295
+ 'bml',
296
+ help='Run BML traffic simulation on LED matrix.',
297
+ epilog="Example: ./cli.py sim bml --density 0.4 --steps 1000 --delay 0.02"
298
+ )
299
+ sim_bml.add_argument('--density', type=float, default=0.35, help='Car density (0.0 to 1.0).')
300
+ sim_bml.add_argument('--steps', type=int, default=500, help='Total half-steps to simulate.')
301
+ sim_bml.add_argument('--delay', type=float, default=0.05, help='Delay in seconds between frames.')
302
+
303
+ # 'sim bml-local' command
304
+ sim_bml_local = sim_subparsers.add_parser(
305
+ 'bml-local',
306
+ help='Run BML sim locally in a Matplotlib window.',
307
+ epilog="Example: ./cli.py sim bml-local --density 0.35 --steps 500"
308
+ )
309
+ sim_bml_local.add_argument('--density', type=float, default=0.35, help='Car density (0.0 to 1.0).')
310
+ sim_bml_local.add_argument('--steps', type=int, default=500, help='Total half-steps to simulate.')
311
+
312
+ # 'sim hpp' command
313
+ sim_hpp = sim_subparsers.add_parser(
314
+ 'hpp',
315
+ help='Run HPP Lattice Gas simulation on LED matrix.',
316
+ epilog="Example: ./cli.py sim hpp --density 0.5 --steps 500"
317
+ )
318
+ sim_hpp.add_argument('--density', type=float, default=0.5, help='Particle density (0.0 to 1.0).')
319
+ sim_hpp.add_argument('--steps', type=int, default=500, help='Simulation steps.')
320
+ sim_hpp.add_argument('--delay', type=float, default=0.05, help='Delay in seconds between frames.')
321
+ sim_hpp.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
322
+
323
+ # 'sim hpp-math' command
324
+ sim_hpp_math = sim_subparsers.add_parser(
325
+ 'hpp-math',
326
+ help='Run HPP sim, seeded with math graphs.',
327
+ epilog="Example: ./cli.py sim hpp-math --graphs 3 --steps 200"
328
+ )
329
+ sim_hpp_math.add_argument('--density', type=float, default=0.3, help='Particle density (0.0 to 1.0).')
330
+ sim_hpp_math.add_argument('--steps', type=int, default=500, help='Simulation steps per graph.')
331
+ sim_hpp_math.add_argument('--delay', type=float, default=0.05, help='Delay in seconds between frames.')
332
+ sim_hpp_math.add_argument('--graphs', type=int, default=5, help='Number of math graphs to run.')
333
+
334
+ # 'sim outer' command
335
+ sim_outer = sim_subparsers.add_parser(
336
+ 'outer',
337
+ help="Run a generic Outer-Totalistic (B/S) CA.",
338
+ epilog="Example (HighLife rule): ./cli.py sim outer --b-rule \"3,6\" --s-rule \"2,3\""
339
+ )
340
+ sim_outer.add_argument('--b-rule', type=parse_int_list, default=[3], help="Birth rule, comma-separated (e.g., '3,6').")
341
+ sim_outer.add_argument('--s-rule', type=parse_int_list, default=[2, 3], help="Survival rule, comma-separated (e.g., '2,3').")
342
+ sim_outer.add_argument('--steps', type=int, default=200, help='Simulation steps.')
343
+ sim_outer.add_argument('--delay', type=float, default=0.1, help='Delay in seconds between frames.')
344
+ sim_outer.add_argument('--oscil-max', type=int, default=20, help='Steps to detect oscillation.')
345
+ sim_outer.add_argument('--still-max', type=int, default=10, help='Steps to detect still life.')
346
+ sim_outer.add_argument('--empty-max', type=int, default=5, help='Steps to detect empty board.')
347
+
348
+ # 'sim gof' command
349
+ sim_gof = sim_subparsers.add_parser(
350
+ 'gof',
351
+ help="Run Conway's Game of Life (B3/S23).",
352
+ epilog="Example: ./cli.py sim gof --board glider --steps 300 --delay 0.05"
353
+ )
354
+ sim_gof.add_argument('--board', type=str, default='random', choices=['random'] + list(STARTING_STATES_GOF.keys()), help='Initial board state (default: random).')
355
+ sim_gof.add_argument('--steps', type=int, default=200, help='Simulation steps.')
356
+ sim_gof.add_argument('--delay', type=float, default=0.1, help='Delay in seconds between frames.')
357
+ sim_gof.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
358
+
359
+ # 'sim inner' command
360
+ sim_inner = sim_subparsers.add_parser(
361
+ 'inner',
362
+ help="Run an Inner-Totalistic (NKS) CA.",
363
+ epilog="Example: ./cli.py sim inner 777 --steps 100"
364
+ )
365
+ sim_inner.add_argument('rule', type=int, help="The NKS-style rule number (e.g., 777). This is a single integer.")
366
+ sim_inner.add_argument('--steps', type=int, default=200, help='Simulation steps.')
367
+ sim_inner.add_argument('--delay', type=float, default=0.1, help='Delay in seconds between frames.')
368
+
369
+ # 'sim random-grey' command
370
+ sim_grey = sim_subparsers.add_parser(
371
+ 'random-grey',
372
+ help='Display random greyscale noise.',
373
+ epilog="Example: ./cli.py sim random-grey --duration 15"
374
+ )
375
+ sim_grey.add_argument('--duration', type=int, default=10, help='Duration in seconds.')
376
+ sim_grey.add_argument('--no-animate', action='store_false', dest='animate', help='Disable hardware animation (for pure noise).')
377
+
378
+ # 'sim anagram-gof' command
379
+ sim_ana_gof = sim_subparsers.add_parser(
380
+ 'anagram-gof',
381
+ help='Draw anagrams, then run GoL on them.',
382
+ epilog="Example: ./cli.py sim anagram-gof --words 3 --steps 50"
383
+ )
384
+ sim_ana_gof.add_argument('--words', type=int, default=4, help='Number of random words to use.')
385
+ sim_ana_gof.add_argument('--steps', type=int, default=100, help='GoL steps per anagram.')
386
+ sim_ana_gof.add_argument('--delay', type=float, default=0.1, help='Delay in seconds between GoL frames.')
387
+ sim_ana_gof.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
388
+
389
+ # 'sim anagram-draw' command
390
+ sim_ana_draw = sim_subparsers.add_parser(
391
+ 'anagram-draw',
392
+ help='Find and draw anagrams for random words.',
393
+ epilog="Example: ./cli.py sim anagram-draw --words 2"
394
+ )
395
+ sim_ana_draw.add_argument('--words', type=int, default=3, help='Number of random words to use.')
396
+ sim_ana_draw.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
397
+
398
+ # 'sim math-gof' command
399
+ sim_math_gof = sim_subparsers.add_parser(
400
+ 'math-gof',
401
+ help='Draw math graphs, then run GoL on them.',
402
+ epilog="Example: ./cli.py sim math-gof --steps 50"
403
+ )
404
+ sim_math_gof.add_argument('--steps', type=int, default=100, help='GoL steps per graph.')
405
+ sim_math_gof.add_argument('--delay', type=float, default=0.1, help='Delay in seconds between GoL frames.')
406
+
407
+ # 'sim math-graphs' command
408
+ sim_math_graphs = sim_subparsers.add_parser(
409
+ 'math-graphs',
410
+ help='Show a sequence of random math graphs.',
411
+ epilog="Example: ./cli.py sim math-graphs --num 5 --delay 3 --hold 5"
412
+ )
413
+ sim_math_graphs.add_argument('--num', type=int, default=5, help='Number of graphs to show.')
414
+ sim_math_graphs.add_argument('--delay', type=float, default=2.0, help='Delay in seconds between graphs.')
415
+ sim_math_graphs.add_argument('--which', type=str, default='both', choices=['left', 'right', 'both'], help='Which module to draw on.')
416
+ sim_math_graphs.add_argument('--hold', type=float, default=0, help='Seconds to hold the *final* graph on screen (default: 0).')
417
+
418
+
419
+ # --- 5. Math Sub-parser (User's modified version) ---
420
+ math_parser = subparsers.add_parser(
421
+ 'math',
422
+ help='Run local math visualization tools.',
423
+ epilog="Example: ./cli.py math plot sin"
424
+ )
425
+ math_subparsers = math_parser.add_subparsers(dest='math_command', required=True, help='Specific math command')
426
+
427
+ # 'math plot' command
428
+ math_plot = math_subparsers.add_parser(
429
+ 'plot',
430
+ help='Plot a predefined function locally and on the matrix.',
431
+ epilog="""
432
+ Examples:
433
+ ./cli.py math plot sin
434
+ ./cli.py math plot tanh
435
+ """
436
+ )
437
+ # Create the list of available function names for the help message
438
+ try:
439
+ func_names = list(REGULAR_OPERATIONS.keys())
440
+ choices_help = f"Picks from this predefined list: {', '.join(func_names)}"
441
+ except NameError:
442
+ # Fallback if REGULAR_OPERATIONS isn't loaded yet (shouldn't happen)
443
+ func_names = ['sin', 'cos', 'tan', 'exp', 'log', 'tanh', 'sqrt', 'arcsin', 'arccos', 'arctanh']
444
+ choices_help = "Picks from a predefined list (e.g., 'sin', 'cos')."
445
+
446
+ math_plot.add_argument(
447
+ 'function_string',
448
+ type=str,
449
+ choices=func_names + [""], # Add "" to handle empty list during build
450
+ help=choices_help
451
+ )
452
+ math_plot.add_argument('--points', type=int, default=500, help='Number of points to plot. Determines resolution (default: 500).')
453
+
454
+ return parser
455
+
456
+ # --- Main Execution ---
457
+
458
+ def main():
459
+ """
460
+ Main function to parse arguments and dispatch commands.
461
+ Includes a try...finally block to reset modules on exit.
462
+ """
463
+ # Note: parser.parse_args() reads directly from sys.argv
464
+ parser = build_cli()
465
+
466
+ # If no commands are given, print main help
467
+ if len(sys.argv) == 1:
468
+ print(helper)
469
+ return
470
+
471
+ args = parser.parse_args()
472
+
473
+ # --- Set verbose flag from new top-level argument ---
474
+ log.verbose = args.verbose
475
+ # --- Removed hardcoded log.verbose = True ---
476
+
477
+ try:
478
+ # --- 0. Background Command Dispatch ---
479
+ if args.command in ['background', 'bg']:
480
+ run_background_mode()
481
+
482
+ # --- 1. LED Command Dispatch ---
483
+ elif args.command == 'led':
484
+ if args.led_command == 'version':
485
+ get_firmware_version()
486
+ elif args.led_command == 'clear':
487
+ clear_graph()
488
+ elif args.led_command == 'fill':
489
+ fill_graph()
490
+ if args.hold > 0:
491
+ log(f"Holding fill for {args.hold} seconds...")
492
+ time.sleep(args.hold)
493
+ elif args.led_command == 'start-anim':
494
+ start_animation()
495
+ elif args.led_command == 'stop-anim':
496
+ stop_animation()
497
+ elif args.led_command == 'reset':
498
+ reset_modules()
499
+ elif args.led_command == 'ports':
500
+ output_ports()
501
+
502
+ # --- 2. Text Command Dispatch ---
503
+ elif args.command == 'text':
504
+ if args.text_command == 'vertical':
505
+ draw_text_vertical(args.text, args.font_size, args.which, args.row_offset)
506
+ if args.hold > 0:
507
+ log(f"Holding text for {args.hold} seconds...")
508
+ time.sleep(args.hold)
509
+ elif args.text_command == 'horizontal':
510
+ draw_text_horizontal(args.text, args.font_size, args.which, args.x_offset, args.y_offset)
511
+ if args.hold > 0:
512
+ log(f"Holding text for {args.hold} seconds...")
513
+ time.sleep(args.hold)
514
+
515
+ # --- 3. Anagram Command Dispatch ---
516
+ elif args.command == 'anagram':
517
+ if args.anagram_command == 'draw':
518
+ draw_anagram_on_matrix(args.word, args.which, args.animate)
519
+ # Note: This function has its own internal delays.
520
+ # We don't add --hold here as it would be ambiguous.
521
+
522
+ # --- 4. Simulation Command Dispatch ---
523
+ elif args.command == 'sim':
524
+ if args.sim_command == 'bml':
525
+ run_bml_simulation(args.density, args.steps, args.delay)
526
+ elif args.sim_command == 'bml-local':
527
+ print(f"Starting local BML simulation (density={args.density}, steps={args.steps})...")
528
+ print("Close the Matplotlib window to exit.")
529
+ show_bml_local_animation(args.density, args.steps)
530
+ elif args.sim_command == 'hpp':
531
+ run_hpp_simulation(None, args.density, args.steps, args.delay, args.which)
532
+ elif args.sim_command == 'hpp-math':
533
+ run_hpp_with_math(args.density, args.steps, args.delay, args.graphs)
534
+ elif args.sim_command == 'outer':
535
+ run_outer_totalistic_simulation(
536
+ initial_state=None,
537
+ b_rule=args.b_rule,
538
+ s_rule=args.s_rule,
539
+ timesteps=args.steps,
540
+ delay_sec=args.delay,
541
+ oscilation_max_steps=args.oscil_max,
542
+ still_board_max_steps=args.still_max,
543
+ empty_board_max_steps=args.empty_max
544
+ )
545
+ elif args.sim_command == 'gof':
546
+ initial_board = None
547
+ if args.board != 'random':
548
+ initial_board = STARTING_STATES_GOF[args.board]
549
+ game_of_life_totalistic_sim(initial_board, args.steps, args.delay, args.which)
550
+ elif args.sim_command == 'inner':
551
+ # 'rule' is already an int thanks to type=int in add_argument
552
+ run_inner_totalistic_simulation(None, args.rule, args.steps, args.delay)
553
+ elif args.sim_command == 'random-grey':
554
+ random_greyscale_animation(args.animate, args.duration)
555
+ elif args.sim_command == 'anagram-gof':
556
+ run_anagrams_game_of_life(args.words, args.steps, args.delay, args.which)
557
+ elif args.sim_command == 'anagram-draw':
558
+ run_draw_anagram_on_matrix(args.words, args.which)
559
+ elif args.sim_command == 'math-gof':
560
+ run_math_funs_game_of_life(args.steps, args.delay)
561
+ elif args.sim_command == 'math-graphs':
562
+ show_random_graphs(args.num, args.delay, args.which)
563
+ if args.hold > 0:
564
+ time.sleep(args.hold)
565
+
566
+ # --- 5. Math Command Dispatch (User's modified version) ---
567
+ elif args.command == 'math':
568
+ if args.math_command == 'plot':
569
+ try:
570
+ op = args.function_string.strip()
571
+ if op in REGULAR_OPERATIONS:
572
+ func = REGULAR_OPERATIONS[op]
573
+ log(f"Plotting predefined function: '{op}'")
574
+ else:
575
+ print(f"Error: '{op}' not found in REGULAR_OPERATIONS.", file=sys.stderr)
576
+ print("Available functions are:", list(REGULAR_OPERATIONS.keys()), file=sys.stderr)
577
+ return
578
+
579
+ log("Generating graph for matrix...")
580
+ graph_matrix = pick_largest_graph(func)
581
+ draw_matrix_on_board(graph_matrix, which='both')
582
+
583
+ log("Generating local plot window...")
584
+ plot_function(func, -40, 40, args.points, f'Plot of {op}', label=op) # type: ignore
585
+
586
+ except Exception as e:
587
+ print(f"Error during math plot: {e}", file=sys.stderr)
588
+ sys.exit(1)
589
+
590
+ except KeyboardInterrupt:
591
+ print("\nCaught KeyboardInterrupt. Exiting and resetting modules.")
592
+ except Exception as e:
593
+ print(f"\nAn unhandled error occurred: {e}", file=sys.stderr)
594
+ import traceback
595
+ traceback.print_exc(file=sys.stderr)
596
+ finally:
597
+ # Check if 'args' exists, as it wouldn't if no args were given
598
+ if 'args' in locals():
599
+ # Don't reset if the command *was* reset
600
+ if not (args.command == 'led' and args.led_command == 'reset'):
601
+ log("Cleaning up... resetting modules.")
602
+ reset_modules()
603
+
604
+ # Always print done
605
+ print("Done.")
606
+
607
+ # --- Built-in Test Suite ---
608
+
609
+ def run_tests():
610
+ """
611
+ Runs a built-in test suite by simulating command-line arguments.
612
+ This is triggered by running: ./cli.py --test
613
+ """
614
+ original_argv = list(sys.argv)
615
+ print("--- STARTING BUILT-IN TEST SUITE ---")
616
+
617
+ # Find a valid function name from REGULAR_OPERATIONS for the test
618
+ # Default to 'sin' if the import failed for some reason
619
+ try:
620
+ math_test_func = list(REGULAR_OPERATIONS.keys())[0]
621
+ except (NameError, IndexError):
622
+ math_test_func = 'sin' # Fallback
623
+
624
+ # Define test commands (as lists of strings, just like sys.argv)
625
+ test_commands = [
626
+ ['cli.py', 'led', 'version'],
627
+ ['cli.py', '-v', 'text', 'vertical', 'TEST', '--hold', '1.5'], # Test verbose flag
628
+ ['cli.py', 'sim', 'gof', '--board', 'blinker', '--steps', '25', '--delay', '0.05'],
629
+ ['cli.py', 'sim', 'outer', '--b-rule', '3,6', '--s-rule', '2,3', '--steps', '25', '--delay', '0.05'],
630
+ ['cli.py', 'sim', 'inner', '777', '--steps', '25', '--delay', '0.05'],
631
+ ['cli.py', 'sim', 'bml-local', '--steps', '50'], # Will pause for user
632
+ # --- FIXED TEST CASE ---
633
+ ['cli.py', 'math', 'plot', math_test_func], # Will pause for user
634
+ ]
635
+
636
+ try:
637
+ for i, cmd in enumerate(test_commands):
638
+ print(f"\n--- TEST {i+1}/{len(test_commands)}: {' '.join(cmd)} ---")
639
+ sys.argv = cmd # Overwrite sys.argv
640
+
641
+ try:
642
+ main() # Run the main function with the new sys.argv
643
+ except SystemExit:
644
+ print(f"Test {i+1} completed with SystemExit (expected for -h or plot).")
645
+ except Exception as e:
646
+ print(f"--- TEST {i+1} FAILED: {e} ---")
647
+
648
+ # Pause briefly for visual tests on the matrix
649
+ # (The --hold test will pause itself)
650
+ if (cmd[1] == 'sim' and 'local' not in cmd[2]):
651
+ print("Pausing 2 seconds for visual check...")
652
+ time.sleep(2)
653
+
654
+ except Exception as e:
655
+ print(f"\n--- TEST SUITE ABORTED: {e} ---")
656
+ finally:
657
+ print("\n--- TEST SUITE FINISHED ---")
658
+ sys.argv = original_argv # Restore original sys.argv
659
+ reset_modules()
660
+ print("Original argv restored. Modules reset.")
661
+
662
+
663
+ if __name__ == "__main__":
664
+ # Check for the special --test flag
665
+ if len(sys.argv) == 2 and sys.argv[1] == '--test':
666
+ run_tests()
667
+ else:
668
+ # Run normal operation
669
+ main()
File without changes
File without changes