chipfoundry-cli 1.2.8__tar.gz → 1.2.9__tar.gz
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.
- {chipfoundry_cli-1.2.8 → chipfoundry_cli-1.2.9}/PKG-INFO +3 -3
- {chipfoundry_cli-1.2.8 → chipfoundry_cli-1.2.9}/chipfoundry_cli/main.py +705 -114
- {chipfoundry_cli-1.2.8 → chipfoundry_cli-1.2.9}/pyproject.toml +3 -2
- {chipfoundry_cli-1.2.8 → chipfoundry_cli-1.2.9}/LICENSE +0 -0
- {chipfoundry_cli-1.2.8 → chipfoundry_cli-1.2.9}/README.md +0 -0
- {chipfoundry_cli-1.2.8 → chipfoundry_cli-1.2.9}/chipfoundry_cli/__init__.py +0 -0
- {chipfoundry_cli-1.2.8 → chipfoundry_cli-1.2.9}/chipfoundry_cli/utils.py +0 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: chipfoundry-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.9
|
|
4
4
|
Summary: CLI tool to automate ChipFoundry project submission to SFTP server
|
|
5
5
|
Home-page: https://chipfoundry.io
|
|
6
6
|
License: Apache-2.0
|
|
7
7
|
Author: ChipFoundry
|
|
8
8
|
Author-email: marwan.abbas@chipfoundry.io
|
|
9
|
-
Requires-Python: >=3.8.0
|
|
9
|
+
Requires-Python: >=3.8.1,<4.0
|
|
10
10
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -19,6 +18,7 @@ Requires-Dist: click (>=8.0.0,<9)
|
|
|
19
18
|
Requires-Dist: httpx (>=0.24.0,<1.0)
|
|
20
19
|
Requires-Dist: paramiko (>=3.0.0,<4)
|
|
21
20
|
Requires-Dist: rich (>=13,<14)
|
|
21
|
+
Requires-Dist: textual (>=0.40.0,<1)
|
|
22
22
|
Requires-Dist: toml (>=0.10,<1.0)
|
|
23
23
|
Project-URL: Repository, https://github.com/chipfoundry/cf-cli
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
@@ -23,6 +23,13 @@ import sys
|
|
|
23
23
|
import shutil
|
|
24
24
|
import signal
|
|
25
25
|
|
|
26
|
+
# Textual imports for GPIO grid UI
|
|
27
|
+
from textual.app import App, ComposeResult
|
|
28
|
+
from textual.widgets import Button, Static, Footer, Header, Label
|
|
29
|
+
from textual.containers import Grid, Horizontal, Vertical, Container, ScrollableContainer
|
|
30
|
+
from textual.binding import Binding
|
|
31
|
+
from textual.screen import ModalScreen
|
|
32
|
+
|
|
26
33
|
DEFAULT_SSH_KEY = os.path.expanduser('~/.ssh/chipfoundry-key')
|
|
27
34
|
DEFAULT_SFTP_HOST = 'sftp.chipfoundry.io'
|
|
28
35
|
|
|
@@ -398,7 +405,7 @@ def gpio_config(project_root):
|
|
|
398
405
|
|
|
399
406
|
# For openframe, GPIO config is not needed
|
|
400
407
|
if project_type == 'openframe':
|
|
401
|
-
console.print("[red]
|
|
408
|
+
console.print("[red]GPIO configuration is not available for openframe projects.[/red]")
|
|
402
409
|
console.print("[yellow]Openframe projects do not use user_defines.v.[/yellow]")
|
|
403
410
|
raise click.Abort()
|
|
404
411
|
|
|
@@ -407,154 +414,738 @@ def gpio_config(project_root):
|
|
|
407
414
|
# Load existing GPIO configs from project.json or user_defines.v
|
|
408
415
|
existing_configs = get_gpio_config_from_project_json(str(project_json_path))
|
|
409
416
|
if not existing_configs and user_defines_path.exists():
|
|
410
|
-
# Try to parse from user_defines.v
|
|
411
417
|
existing_configs = parse_user_defines_v(str(user_defines_path))
|
|
412
418
|
|
|
413
419
|
# Determine GPIO range based on project type
|
|
414
420
|
if project_type == 'analog': # caravan
|
|
415
|
-
# Caravan: GPIO 5-13 and 25-37 (GPIO 14-24 not available)
|
|
416
|
-
# User sees: 5-13, then 14-26 (which map to 25-37 internally)
|
|
417
421
|
available_gpios = list(range(5, 14)) + list(range(25, 38))
|
|
418
422
|
user_to_real_map = {}
|
|
419
423
|
user_num = 5
|
|
420
424
|
for real_gpio in available_gpios:
|
|
421
425
|
user_to_real_map[user_num] = real_gpio
|
|
422
426
|
user_num += 1
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
console.print("[dim]Note: GPIO 14-24 are not available in Caravan. Numbers 14-26 map to GPIO 25-37.[/dim]\n")
|
|
427
|
+
gpio_label = "Caravan"
|
|
428
|
+
gpio_note = "Note: GPIO 14-24 unavailable. Numbers 14-26 map to GPIO 25-37."
|
|
429
|
+
user_gpio_range = list(range(5, 27))
|
|
427
430
|
else: # digital (caravel)
|
|
428
|
-
# Caravel: GPIO 5-37 all available
|
|
429
431
|
available_gpios = list(range(5, 38))
|
|
430
432
|
user_to_real_map = {gpio: gpio for gpio in available_gpios}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
433
|
+
gpio_label = "Caravel"
|
|
434
|
+
gpio_note = None
|
|
435
|
+
user_gpio_range = list(range(5, 38))
|
|
436
|
+
|
|
437
|
+
total_pins = len(user_to_real_map)
|
|
438
|
+
|
|
439
|
+
# Mode shortcuts - short names that map to full mode keys
|
|
440
|
+
MODE_SHORTCUTS = {
|
|
441
|
+
"out": "user_output", "output": "user_output", "o": "user_output",
|
|
442
|
+
"in": "user_input_nopull", "input": "user_input_nopull", "i": "user_input_nopull",
|
|
443
|
+
"in-pd": "user_input_pulldown", "input-pd": "user_input_pulldown", "pulldown": "user_input_pulldown",
|
|
444
|
+
"in-pu": "user_input_pullup", "input-pu": "user_input_pullup", "pullup": "user_input_pullup",
|
|
445
|
+
"bidir": "user_bidirectional", "bidirectional": "user_bidirectional", "b": "user_bidirectional",
|
|
446
|
+
"analog": "user_analog", "ana": "user_analog", "a": "user_analog",
|
|
447
|
+
"out-mon": "user_output_monitored", "monitored": "user_output_monitored",
|
|
448
|
+
# Management modes
|
|
449
|
+
"mgmt-out": "mgmt_output", "mgmt-in": "mgmt_input_nopull",
|
|
450
|
+
"mgmt-bidir": "mgmt_bidirectional", "mgmt-analog": "mgmt_analog",
|
|
451
|
+
}
|
|
446
452
|
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
def resolve_mode(mode_str):
|
|
454
|
+
"""Resolve a mode string (shortcut or full name) to the mode key."""
|
|
455
|
+
mode_str = mode_str.lower().strip()
|
|
456
|
+
if mode_str in MODE_SHORTCUTS:
|
|
457
|
+
return MODE_SHORTCUTS[mode_str]
|
|
458
|
+
# Check if it's already a valid mode key
|
|
459
|
+
mode_options = [key for key in GPIO_MODES.keys() if key != "invalid"]
|
|
460
|
+
if mode_str in mode_options:
|
|
461
|
+
return mode_str
|
|
462
|
+
# Partial match
|
|
463
|
+
matches = [m for m in mode_options if m.startswith(mode_str)]
|
|
464
|
+
if len(matches) == 1:
|
|
465
|
+
return matches[0]
|
|
466
|
+
return None
|
|
449
467
|
|
|
450
|
-
|
|
468
|
+
def parse_gpio_range(input_str, valid_gpios):
|
|
469
|
+
"""Parse '5-10', '5,7,9', or '5-10,15' into list of GPIO numbers."""
|
|
470
|
+
selected = set()
|
|
471
|
+
parts = input_str.replace(' ', '').split(',')
|
|
472
|
+
for part in parts:
|
|
473
|
+
if '-' in part:
|
|
474
|
+
try:
|
|
475
|
+
start, end = part.split('-', 1)
|
|
476
|
+
for g in range(int(start), int(end) + 1):
|
|
477
|
+
if g in valid_gpios:
|
|
478
|
+
selected.add(g)
|
|
479
|
+
except ValueError:
|
|
480
|
+
pass
|
|
481
|
+
else:
|
|
482
|
+
try:
|
|
483
|
+
g = int(part)
|
|
484
|
+
if g in valid_gpios:
|
|
485
|
+
selected.add(g)
|
|
486
|
+
except ValueError:
|
|
487
|
+
pass
|
|
488
|
+
return sorted(selected)
|
|
489
|
+
|
|
490
|
+
def format_gpio_ranges(gpio_list):
|
|
491
|
+
"""Convert [5,6,7,10,11,15] to '5-7, 10-11, 15'."""
|
|
492
|
+
if not gpio_list:
|
|
493
|
+
return "-"
|
|
494
|
+
gpio_list = sorted(gpio_list)
|
|
495
|
+
ranges = []
|
|
496
|
+
start = gpio_list[0]
|
|
497
|
+
end = start
|
|
498
|
+
for g in gpio_list[1:]:
|
|
499
|
+
if g == end + 1:
|
|
500
|
+
end = g
|
|
501
|
+
else:
|
|
502
|
+
ranges.append(f"{start}-{end}" if start != end else str(start))
|
|
503
|
+
start = end = g
|
|
504
|
+
ranges.append(f"{start}-{end}" if start != end else str(start))
|
|
505
|
+
return ", ".join(ranges)
|
|
451
506
|
|
|
452
|
-
# Helper function to find mode key from mode name or hex value
|
|
453
507
|
def find_mode_key(mode_value):
|
|
454
508
|
"""Find the mode key for a given mode value."""
|
|
455
509
|
if not mode_value:
|
|
456
510
|
return None
|
|
457
|
-
# Direct match
|
|
458
511
|
for key, mode_name in GPIO_MODES.items():
|
|
459
|
-
if mode_name == mode_value:
|
|
460
|
-
|
|
461
|
-
if key != "invalid":
|
|
462
|
-
return key
|
|
463
|
-
# Check if it's a hex value or invalid - return None to indicate invalid
|
|
464
|
-
if mode_value.startswith('0x') or mode_value.startswith("13'h") or 'INVALID' in mode_value:
|
|
465
|
-
return None # Don't show "invalid", just show no default
|
|
512
|
+
if mode_name == mode_value and key != "invalid":
|
|
513
|
+
return key
|
|
466
514
|
return None
|
|
467
515
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
516
|
+
def display_summary(gpio_configs, user_to_real_map):
|
|
517
|
+
"""Display a summary of GPIO configuration grouped by mode."""
|
|
518
|
+
mode_groups = {}
|
|
519
|
+
for user_gpio, real_gpio in user_to_real_map.items():
|
|
520
|
+
mode_value = gpio_configs.get(real_gpio)
|
|
521
|
+
mode_key = find_mode_key(mode_value) if mode_value else None
|
|
522
|
+
if mode_key not in mode_groups:
|
|
523
|
+
mode_groups[mode_key] = []
|
|
524
|
+
mode_groups[mode_key].append(user_gpio)
|
|
525
|
+
|
|
526
|
+
console.print()
|
|
527
|
+
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
528
|
+
table.add_column("Mode", style="cyan", width=24)
|
|
529
|
+
table.add_column("Count", justify="right", width=6)
|
|
530
|
+
table.add_column("GPIOs", style="white")
|
|
531
|
+
|
|
532
|
+
# Sort by count (most common first)
|
|
533
|
+
for mode_key in sorted(mode_groups.keys(), key=lambda k: -len(mode_groups[k])):
|
|
534
|
+
gpios = mode_groups[mode_key]
|
|
535
|
+
display_name = mode_key if mode_key else "[red]unconfigured[/red]"
|
|
536
|
+
style = ""
|
|
537
|
+
if mode_key:
|
|
538
|
+
if "output" in mode_key: style = "[green]"
|
|
539
|
+
elif "input" in mode_key: style = "[cyan]"
|
|
540
|
+
elif "bidirectional" in mode_key: style = "[yellow]"
|
|
541
|
+
elif "analog" in mode_key: style = "[magenta]"
|
|
542
|
+
display_name = f"{style}{mode_key}[/]"
|
|
543
|
+
table.add_row(display_name, str(len(gpios)), format_gpio_ranges(gpios))
|
|
544
|
+
|
|
545
|
+
console.print(table)
|
|
546
|
+
|
|
547
|
+
# Initialize gpio_configs
|
|
548
|
+
gpio_configs = existing_configs.copy() if existing_configs else {}
|
|
549
|
+
|
|
550
|
+
# ========================
|
|
551
|
+
# HEADER
|
|
552
|
+
# ========================
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def get_mode_display(mode_key):
|
|
556
|
+
"""Get display name for a mode."""
|
|
557
|
+
names = {
|
|
558
|
+
"user_output": "user output",
|
|
559
|
+
"user_output_monitored": "user output monitored",
|
|
560
|
+
"user_input_nopull": "user input",
|
|
561
|
+
"user_input_pulldown": "user input pulldown",
|
|
562
|
+
"user_input_pullup": "user input pullup",
|
|
563
|
+
"user_bidirectional": "user bidirectional",
|
|
564
|
+
"user_analog": "user analog",
|
|
565
|
+
"mgmt_output": "mgmt output",
|
|
566
|
+
"mgmt_input_nopull": "mgmt input",
|
|
567
|
+
"mgmt_input_pulldown": "mgmt input pulldown",
|
|
568
|
+
"mgmt_input_pullup": "mgmt input pullup",
|
|
569
|
+
"mgmt_bidirectional": "mgmt bidirectional",
|
|
570
|
+
"mgmt_analog": "mgmt analog",
|
|
571
|
+
}
|
|
572
|
+
return names.get(mode_key, "not set")
|
|
573
|
+
|
|
574
|
+
def get_mode_color(mode_key):
|
|
575
|
+
"""Get color for a mode."""
|
|
576
|
+
if not mode_key:
|
|
577
|
+
return "red"
|
|
578
|
+
elif "output" in mode_key:
|
|
579
|
+
return "green"
|
|
580
|
+
elif "input" in mode_key:
|
|
581
|
+
return "cyan"
|
|
582
|
+
elif "bidirectional" in mode_key:
|
|
583
|
+
return "yellow"
|
|
584
|
+
elif "analog" in mode_key:
|
|
585
|
+
return "magenta"
|
|
586
|
+
return "white"
|
|
587
|
+
|
|
588
|
+
# ========================
|
|
589
|
+
# STEP 2: Textual Grid UI for GPIO Configuration
|
|
590
|
+
# ========================
|
|
591
|
+
|
|
592
|
+
# All available modes for the selector
|
|
593
|
+
ALL_MODES = [
|
|
594
|
+
("user_output", "User Output"),
|
|
595
|
+
("user_output_monitored", "User Output Monitored"),
|
|
596
|
+
("user_input_nopull", "User Input (no pull)"),
|
|
597
|
+
("user_input_pullup", "User Input (pull-up)"),
|
|
598
|
+
("user_input_pulldown", "User Input (pull-down)"),
|
|
599
|
+
("user_bidirectional", "User Bidirectional"),
|
|
600
|
+
("user_analog", "User Analog"),
|
|
601
|
+
("mgmt_output", "Mgmt Output"),
|
|
602
|
+
("mgmt_input_nopull", "Mgmt Input (no pull)"),
|
|
603
|
+
("mgmt_input_pullup", "Mgmt Input (pull-up)"),
|
|
604
|
+
("mgmt_input_pulldown", "Mgmt Input (pull-down)"),
|
|
605
|
+
("mgmt_bidirectional", "Mgmt Bidirectional"),
|
|
606
|
+
("mgmt_analog", "Mgmt Analog"),
|
|
607
|
+
]
|
|
608
|
+
|
|
609
|
+
class NoKeyScrollContainer(ScrollableContainer):
|
|
610
|
+
"""ScrollableContainer that doesn't capture arrow keys."""
|
|
611
|
+
can_focus = False
|
|
612
|
+
BINDINGS = []
|
|
613
|
+
|
|
614
|
+
class GPIOButton(Static):
|
|
615
|
+
"""A widget representing a single GPIO pin."""
|
|
616
|
+
|
|
617
|
+
can_focus = True
|
|
618
|
+
|
|
619
|
+
def __init__(self, gpio_num: int, mode_key: str = None, **kwargs):
|
|
620
|
+
self.gpio_num = gpio_num
|
|
621
|
+
self.mode_key = mode_key
|
|
622
|
+
self.is_selected = False
|
|
623
|
+
# Create initial label - use two lines
|
|
624
|
+
abbrev = get_mode_display(mode_key) if mode_key else "not set"
|
|
625
|
+
super().__init__(f"[b]{gpio_num}[/b]\n{abbrev}", **kwargs)
|
|
626
|
+
self.id = f"gpio_{gpio_num}"
|
|
627
|
+
|
|
628
|
+
def _update_display(self):
|
|
629
|
+
"""Update the display text."""
|
|
630
|
+
abbrev = get_mode_display(self.mode_key) if self.mode_key else "not set"
|
|
631
|
+
self.update(f"[b]{self.gpio_num}[/b]\n{abbrev}")
|
|
632
|
+
|
|
633
|
+
def _update_style(self):
|
|
634
|
+
"""Update widget style based on mode and selection."""
|
|
635
|
+
color = get_mode_color(self.mode_key)
|
|
636
|
+
if self.is_selected:
|
|
637
|
+
self.add_class("selected")
|
|
638
|
+
else:
|
|
639
|
+
self.remove_class("selected")
|
|
640
|
+
self.styles.color = color
|
|
641
|
+
self.styles.border = ("solid", color)
|
|
642
|
+
|
|
643
|
+
def on_mount(self):
|
|
644
|
+
"""Set initial style on mount."""
|
|
645
|
+
self._update_style()
|
|
646
|
+
|
|
647
|
+
def set_mode(self, mode_key: str):
|
|
648
|
+
"""Update the GPIO mode."""
|
|
649
|
+
self.mode_key = mode_key
|
|
650
|
+
self._update_display()
|
|
651
|
+
self._update_style()
|
|
652
|
+
|
|
653
|
+
def toggle_selected(self):
|
|
654
|
+
"""Toggle selection state."""
|
|
655
|
+
self.is_selected = not self.is_selected
|
|
656
|
+
self._update_style()
|
|
657
|
+
|
|
658
|
+
def deselect(self):
|
|
659
|
+
"""Clear selection."""
|
|
660
|
+
self.is_selected = False
|
|
661
|
+
self._update_style()
|
|
662
|
+
|
|
663
|
+
def on_click(self):
|
|
664
|
+
"""Handle click - toggle selection."""
|
|
665
|
+
self.toggle_selected()
|
|
666
|
+
# Update current index to this button
|
|
667
|
+
try:
|
|
668
|
+
self.app.current_index = self.app.gpio_list.index(self.gpio_num)
|
|
669
|
+
self.app._highlight_current()
|
|
670
|
+
except (ValueError, AttributeError):
|
|
671
|
+
pass
|
|
672
|
+
self.app._update_status()
|
|
478
673
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
"""Find mode that matches user input (partial match or full key)."""
|
|
482
|
-
user_input_lower = user_input.lower()
|
|
674
|
+
class ModeSelectScreen(ModalScreen):
|
|
675
|
+
"""Modal screen for selecting GPIO mode."""
|
|
483
676
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
677
|
+
BINDINGS = [
|
|
678
|
+
Binding("escape", "cancel", "Cancel"),
|
|
679
|
+
Binding("up", "move_up", "Up", show=False, priority=True),
|
|
680
|
+
Binding("down", "move_down", "Down", show=False, priority=True),
|
|
681
|
+
Binding("enter", "select_current", "Select", show=False, priority=True),
|
|
682
|
+
]
|
|
487
683
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
elif len(matches) > 1:
|
|
493
|
-
# Multiple matches - return list so we can show them
|
|
494
|
-
return matches
|
|
684
|
+
CSS = """
|
|
685
|
+
ModeSelectScreen {
|
|
686
|
+
align: center middle;
|
|
687
|
+
}
|
|
495
688
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
689
|
+
#mode-dialog {
|
|
690
|
+
width: 60;
|
|
691
|
+
height: auto;
|
|
692
|
+
max-height: 90%;
|
|
693
|
+
padding: 1 2;
|
|
694
|
+
background: $surface;
|
|
695
|
+
border: solid cyan;
|
|
696
|
+
overflow-y: auto;
|
|
697
|
+
}
|
|
501
698
|
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
699
|
+
#mode-title {
|
|
700
|
+
text-align: center;
|
|
701
|
+
text-style: bold;
|
|
702
|
+
margin-bottom: 1;
|
|
703
|
+
color: cyan;
|
|
704
|
+
}
|
|
506
705
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
706
|
+
.section-label {
|
|
707
|
+
text-style: bold;
|
|
708
|
+
margin-top: 1;
|
|
709
|
+
color: white;
|
|
710
|
+
}
|
|
512
711
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
712
|
+
.mode-btn {
|
|
713
|
+
width: 100%;
|
|
714
|
+
margin: 0;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.mode-btn.current-mode {
|
|
718
|
+
border: double white;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.mode-btn-output {
|
|
722
|
+
color: green;
|
|
723
|
+
}
|
|
518
724
|
|
|
519
|
-
|
|
520
|
-
|
|
725
|
+
.mode-btn-input {
|
|
726
|
+
color: cyan;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.mode-btn-bidir {
|
|
730
|
+
color: yellow;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.mode-btn-analog {
|
|
734
|
+
color: magenta;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#cancel-row {
|
|
738
|
+
margin-top: 1;
|
|
739
|
+
align: center middle;
|
|
740
|
+
}
|
|
741
|
+
"""
|
|
742
|
+
|
|
743
|
+
def __init__(self, gpio_nums: list, **kwargs):
|
|
744
|
+
super().__init__(**kwargs)
|
|
745
|
+
self.gpio_nums = gpio_nums
|
|
746
|
+
self.current_idx = 0
|
|
747
|
+
self.mode_buttons = []
|
|
748
|
+
|
|
749
|
+
def compose(self) -> ComposeResult:
|
|
750
|
+
gpio_str = ", ".join(str(g) for g in self.gpio_nums[:5])
|
|
751
|
+
if len(self.gpio_nums) > 5:
|
|
752
|
+
gpio_str += f"... ({len(self.gpio_nums)} total)"
|
|
521
753
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
754
|
+
with Vertical(id="mode-dialog"):
|
|
755
|
+
yield Label(f"Select mode for GPIO: {gpio_str}", id="mode-title")
|
|
756
|
+
yield Label("[dim]Use ↑↓ arrows and Enter, or click[/dim]")
|
|
757
|
+
|
|
758
|
+
# User modes section
|
|
759
|
+
yield Label("── User Modes ──", classes="section-label")
|
|
760
|
+
for mode_key, mode_name in ALL_MODES:
|
|
761
|
+
if mode_key.startswith("user_"):
|
|
762
|
+
if "output" in mode_key:
|
|
763
|
+
btn_class = "mode-btn mode-btn-output"
|
|
764
|
+
elif "input" in mode_key:
|
|
765
|
+
btn_class = "mode-btn mode-btn-input"
|
|
766
|
+
elif "bidirectional" in mode_key:
|
|
767
|
+
btn_class = "mode-btn mode-btn-bidir"
|
|
768
|
+
elif "analog" in mode_key:
|
|
769
|
+
btn_class = "mode-btn mode-btn-analog"
|
|
770
|
+
else:
|
|
771
|
+
btn_class = "mode-btn"
|
|
772
|
+
yield Button(mode_name, id=f"mode_{mode_key}", classes=btn_class)
|
|
773
|
+
|
|
774
|
+
# Management modes section
|
|
775
|
+
yield Label("── Management Modes ──", classes="section-label")
|
|
776
|
+
for mode_key, mode_name in ALL_MODES:
|
|
777
|
+
if mode_key.startswith("mgmt_"):
|
|
778
|
+
if "output" in mode_key:
|
|
779
|
+
btn_class = "mode-btn mode-btn-output"
|
|
780
|
+
elif "input" in mode_key:
|
|
781
|
+
btn_class = "mode-btn mode-btn-input"
|
|
782
|
+
elif "bidirectional" in mode_key:
|
|
783
|
+
btn_class = "mode-btn mode-btn-bidir"
|
|
784
|
+
elif "analog" in mode_key:
|
|
785
|
+
btn_class = "mode-btn mode-btn-analog"
|
|
786
|
+
else:
|
|
787
|
+
btn_class = "mode-btn"
|
|
788
|
+
yield Button(mode_name, id=f"mode_{mode_key}", classes=btn_class)
|
|
789
|
+
|
|
790
|
+
with Horizontal(id="cancel-row"):
|
|
791
|
+
yield Button("Cancel", variant="error", id="cancel-btn")
|
|
792
|
+
|
|
793
|
+
def on_mount(self) -> None:
|
|
794
|
+
"""Initialize on mount."""
|
|
795
|
+
# Collect all mode buttons
|
|
796
|
+
self.mode_buttons = list(self.query(".mode-btn"))
|
|
797
|
+
if self.mode_buttons:
|
|
798
|
+
self._highlight_current()
|
|
799
|
+
|
|
800
|
+
def _highlight_current(self) -> None:
|
|
801
|
+
"""Highlight current button."""
|
|
802
|
+
for i, btn in enumerate(self.mode_buttons):
|
|
803
|
+
if i == self.current_idx:
|
|
804
|
+
btn.add_class("current-mode")
|
|
805
|
+
btn.focus()
|
|
542
806
|
else:
|
|
543
|
-
|
|
807
|
+
btn.remove_class("current-mode")
|
|
808
|
+
|
|
809
|
+
def action_move_up(self) -> None:
|
|
810
|
+
"""Move selection up."""
|
|
811
|
+
if self.current_idx > 0:
|
|
812
|
+
self.current_idx -= 1
|
|
813
|
+
self._highlight_current()
|
|
814
|
+
|
|
815
|
+
def action_move_down(self) -> None:
|
|
816
|
+
"""Move selection down."""
|
|
817
|
+
if self.current_idx < len(self.mode_buttons) - 1:
|
|
818
|
+
self.current_idx += 1
|
|
819
|
+
self._highlight_current()
|
|
820
|
+
|
|
821
|
+
def action_select_current(self) -> None:
|
|
822
|
+
"""Select the current mode."""
|
|
823
|
+
if 0 <= self.current_idx < len(self.mode_buttons):
|
|
824
|
+
btn = self.mode_buttons[self.current_idx]
|
|
825
|
+
mode_key = btn.id[5:] # Remove "mode_" prefix
|
|
826
|
+
self.dismiss(mode_key)
|
|
827
|
+
|
|
828
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
829
|
+
if event.button.id == "cancel-btn":
|
|
830
|
+
self.dismiss(None)
|
|
831
|
+
elif event.button.id.startswith("mode_"):
|
|
832
|
+
mode_key = event.button.id[5:] # Remove "mode_" prefix
|
|
833
|
+
self.dismiss(mode_key)
|
|
834
|
+
|
|
835
|
+
def action_cancel(self):
|
|
836
|
+
self.dismiss(None)
|
|
837
|
+
|
|
838
|
+
class GPIOGridApp(App):
|
|
839
|
+
"""Textual app for GPIO grid configuration."""
|
|
840
|
+
|
|
841
|
+
GRID_COLS = 4 # Number of columns in the grid
|
|
842
|
+
|
|
843
|
+
CSS = """
|
|
844
|
+
Screen {
|
|
845
|
+
align: center middle;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
#main-container {
|
|
849
|
+
width: 100%;
|
|
850
|
+
height: 100%;
|
|
851
|
+
padding: 1;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
#title {
|
|
855
|
+
text-align: center;
|
|
856
|
+
text-style: bold;
|
|
857
|
+
color: cyan;
|
|
858
|
+
margin-bottom: 1;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#grid-scroll {
|
|
862
|
+
height: 1fr;
|
|
863
|
+
width: 100%;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
#gpio-grid {
|
|
867
|
+
grid-size: 4;
|
|
868
|
+
grid-gutter: 1;
|
|
869
|
+
padding: 1;
|
|
870
|
+
height: auto;
|
|
871
|
+
width: 100%;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
GPIOButton {
|
|
875
|
+
width: 1fr;
|
|
876
|
+
min-width: 24;
|
|
877
|
+
height: 4;
|
|
878
|
+
text-align: center;
|
|
879
|
+
border: solid green;
|
|
880
|
+
content-align: center middle;
|
|
881
|
+
padding: 0 1;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
GPIOButton.current {
|
|
885
|
+
border: double cyan;
|
|
886
|
+
background: $primary;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
GPIOButton.selected {
|
|
890
|
+
background: $success;
|
|
891
|
+
color: black;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
#legend {
|
|
895
|
+
text-align: center;
|
|
896
|
+
color: grey;
|
|
897
|
+
margin-top: 1;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
#status {
|
|
901
|
+
text-align: center;
|
|
902
|
+
margin-top: 1;
|
|
903
|
+
color: yellow;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
#mode-dialog {
|
|
907
|
+
width: 70%;
|
|
908
|
+
max-width: 80;
|
|
909
|
+
height: auto;
|
|
910
|
+
padding: 2;
|
|
911
|
+
background: $surface;
|
|
912
|
+
border: solid cyan;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
#mode-title {
|
|
916
|
+
text-align: center;
|
|
917
|
+
text-style: bold;
|
|
918
|
+
margin-bottom: 1;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
#mode-select {
|
|
922
|
+
width: 100%;
|
|
923
|
+
margin-bottom: 1;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
#mode-buttons {
|
|
927
|
+
align: center middle;
|
|
928
|
+
margin-top: 1;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
#mode-buttons Button {
|
|
932
|
+
margin: 0 1;
|
|
933
|
+
}
|
|
934
|
+
"""
|
|
935
|
+
|
|
936
|
+
BINDINGS = [
|
|
937
|
+
Binding("up", "nav_up", "Up", show=False, priority=True),
|
|
938
|
+
Binding("down", "nav_down", "Down", show=False, priority=True),
|
|
939
|
+
Binding("left", "nav_left", "Left", show=False, priority=True),
|
|
940
|
+
Binding("right", "nav_right", "Right", show=False, priority=True),
|
|
941
|
+
Binding("space", "toggle", "Select", priority=True),
|
|
942
|
+
Binding("enter", "open_mode", "Set Mode", priority=True),
|
|
943
|
+
Binding("a", "select_all", "Select All"),
|
|
944
|
+
Binding("n", "select_none", "Clear"),
|
|
945
|
+
Binding("d", "done", "Save & Exit"),
|
|
946
|
+
Binding("q", "quit", "Quit"),
|
|
947
|
+
]
|
|
948
|
+
|
|
949
|
+
def __init__(self, gpio_configs, user_to_real_map, user_gpio_range, gpio_label="", **kwargs):
|
|
950
|
+
super().__init__(**kwargs)
|
|
951
|
+
self.gpio_configs = gpio_configs
|
|
952
|
+
self.user_to_real_map = user_to_real_map
|
|
953
|
+
self.user_gpio_range = user_gpio_range
|
|
954
|
+
self.gpio_label = gpio_label
|
|
955
|
+
self.gpio_buttons = {}
|
|
956
|
+
self.gpio_list = sorted(user_to_real_map.keys())
|
|
957
|
+
self.current_index = 0
|
|
958
|
+
|
|
959
|
+
def compose(self) -> ComposeResult:
|
|
960
|
+
yield Header(show_clock=False)
|
|
961
|
+
|
|
962
|
+
with Vertical(id="main-container"):
|
|
963
|
+
yield Label(f"GPIO Configuration ({self.gpio_label}) - Arrows: navigate, Space: select, Enter: set mode, D: done", id="title")
|
|
964
|
+
|
|
965
|
+
with NoKeyScrollContainer(id="grid-scroll"):
|
|
966
|
+
with Grid(id="gpio-grid"):
|
|
967
|
+
for gpio_num in self.gpio_list:
|
|
968
|
+
real_gpio = self.user_to_real_map[gpio_num]
|
|
969
|
+
mode_value = self.gpio_configs.get(real_gpio)
|
|
970
|
+
mode_key = find_mode_key(mode_value) if mode_value else None
|
|
971
|
+
btn = GPIOButton(gpio_num, mode_key)
|
|
972
|
+
self.gpio_buttons[gpio_num] = btn
|
|
973
|
+
yield btn
|
|
974
|
+
|
|
975
|
+
yield Label("[Space] toggle [Enter] set mode [A] select all [N] select none [D] done", id="legend")
|
|
976
|
+
yield Label("", id="status")
|
|
977
|
+
|
|
978
|
+
yield Footer()
|
|
979
|
+
|
|
980
|
+
def on_mount(self) -> None:
|
|
981
|
+
"""Initialize on mount."""
|
|
982
|
+
if self.gpio_list:
|
|
983
|
+
self.current_index = 0
|
|
984
|
+
self._highlight_current()
|
|
985
|
+
self._update_status()
|
|
986
|
+
|
|
987
|
+
def _highlight_current(self) -> None:
|
|
988
|
+
"""Highlight the GPIO at current_index and scroll into view."""
|
|
989
|
+
# Remove highlight from all
|
|
990
|
+
for btn in self.gpio_buttons.values():
|
|
991
|
+
btn.remove_class("current")
|
|
992
|
+
# Add highlight to current and scroll into view
|
|
993
|
+
if 0 <= self.current_index < len(self.gpio_list):
|
|
994
|
+
gpio_num = self.gpio_list[self.current_index]
|
|
995
|
+
btn = self.gpio_buttons[gpio_num]
|
|
996
|
+
btn.add_class("current")
|
|
997
|
+
btn.scroll_visible()
|
|
998
|
+
|
|
999
|
+
def _is_modal_active(self) -> bool:
|
|
1000
|
+
"""Check if a modal screen is currently active."""
|
|
1001
|
+
return len(self.screen_stack) > 1
|
|
1002
|
+
|
|
1003
|
+
def action_nav_up(self) -> None:
|
|
1004
|
+
"""Move up one row."""
|
|
1005
|
+
if self._is_modal_active():
|
|
1006
|
+
self.screen.action_move_up()
|
|
1007
|
+
return
|
|
1008
|
+
new_idx = self.current_index - self.GRID_COLS
|
|
1009
|
+
if new_idx >= 0:
|
|
1010
|
+
self.current_index = new_idx
|
|
1011
|
+
self._highlight_current()
|
|
1012
|
+
|
|
1013
|
+
def action_nav_down(self) -> None:
|
|
1014
|
+
"""Move down one row."""
|
|
1015
|
+
if self._is_modal_active():
|
|
1016
|
+
self.screen.action_move_down()
|
|
1017
|
+
return
|
|
1018
|
+
new_idx = self.current_index + self.GRID_COLS
|
|
1019
|
+
if new_idx < len(self.gpio_list):
|
|
1020
|
+
self.current_index = new_idx
|
|
1021
|
+
self._highlight_current()
|
|
1022
|
+
|
|
1023
|
+
def action_nav_left(self) -> None:
|
|
1024
|
+
"""Move left one column."""
|
|
1025
|
+
if self._is_modal_active():
|
|
1026
|
+
return # No left/right in modal
|
|
1027
|
+
if self.current_index > 0:
|
|
1028
|
+
self.current_index -= 1
|
|
1029
|
+
self._highlight_current()
|
|
1030
|
+
|
|
1031
|
+
def action_nav_right(self) -> None:
|
|
1032
|
+
"""Move right one column."""
|
|
1033
|
+
if self._is_modal_active():
|
|
1034
|
+
return # No left/right in modal
|
|
1035
|
+
if self.current_index < len(self.gpio_list) - 1:
|
|
1036
|
+
self.current_index += 1
|
|
1037
|
+
self._highlight_current()
|
|
1038
|
+
|
|
1039
|
+
def action_toggle(self) -> None:
|
|
1040
|
+
"""Toggle selection of current GPIO."""
|
|
1041
|
+
if self._is_modal_active():
|
|
1042
|
+
return
|
|
1043
|
+
if 0 <= self.current_index < len(self.gpio_list):
|
|
1044
|
+
gpio_num = self.gpio_list[self.current_index]
|
|
1045
|
+
self.gpio_buttons[gpio_num].toggle_selected()
|
|
1046
|
+
self._update_status()
|
|
1047
|
+
|
|
1048
|
+
def action_open_mode(self) -> None:
|
|
1049
|
+
"""Open mode selector or confirm selection in modal."""
|
|
1050
|
+
if self._is_modal_active():
|
|
1051
|
+
self.screen.action_select_current()
|
|
1052
|
+
return
|
|
1053
|
+
self.action_set_mode()
|
|
1054
|
+
|
|
1055
|
+
def _get_selected_gpios(self) -> list:
|
|
1056
|
+
"""Get list of selected GPIO numbers."""
|
|
1057
|
+
return [num for num, btn in self.gpio_buttons.items() if btn.is_selected]
|
|
1058
|
+
|
|
1059
|
+
def _update_status(self):
|
|
1060
|
+
"""Update the status label."""
|
|
1061
|
+
selected = self._get_selected_gpios()
|
|
1062
|
+
status = self.query_one("#status", Label)
|
|
1063
|
+
if selected:
|
|
1064
|
+
status.update(f"Selected: {', '.join(str(g) for g in sorted(selected))} ({len(selected)} pins)")
|
|
544
1065
|
else:
|
|
545
|
-
|
|
546
|
-
|
|
1066
|
+
status.update("No pins selected - Space to select, Enter to set mode for focused pin")
|
|
1067
|
+
|
|
1068
|
+
def action_toggle_select(self) -> None:
|
|
1069
|
+
"""Toggle selection of focused GPIO."""
|
|
1070
|
+
focused = self.focused
|
|
1071
|
+
if isinstance(focused, GPIOButton):
|
|
1072
|
+
focused.toggle_selected()
|
|
1073
|
+
self._update_status()
|
|
1074
|
+
|
|
1075
|
+
def action_select_all(self) -> None:
|
|
1076
|
+
"""Select all GPIOs."""
|
|
1077
|
+
for btn in self.gpio_buttons.values():
|
|
1078
|
+
btn.is_selected = True
|
|
1079
|
+
btn._update_style()
|
|
1080
|
+
self._update_status()
|
|
1081
|
+
|
|
1082
|
+
def action_select_none(self) -> None:
|
|
1083
|
+
"""Clear all selections."""
|
|
1084
|
+
for btn in self.gpio_buttons.values():
|
|
1085
|
+
btn.deselect()
|
|
1086
|
+
self._update_status()
|
|
1087
|
+
|
|
1088
|
+
def action_set_mode(self) -> None:
|
|
1089
|
+
"""Open mode selection for selected GPIOs."""
|
|
1090
|
+
selected = self._get_selected_gpios()
|
|
1091
|
+
if not selected:
|
|
1092
|
+
# If nothing selected, use the focused one
|
|
1093
|
+
focused = self.focused
|
|
1094
|
+
if isinstance(focused, GPIOButton):
|
|
1095
|
+
selected = [focused.gpio_num]
|
|
1096
|
+
|
|
1097
|
+
if selected:
|
|
1098
|
+
self.push_screen(ModeSelectScreen(selected), self._apply_mode)
|
|
1099
|
+
|
|
1100
|
+
def _apply_mode(self, mode_key: str) -> None:
|
|
1101
|
+
"""Apply the selected mode to selected GPIOs."""
|
|
1102
|
+
if mode_key:
|
|
1103
|
+
selected = self._get_selected_gpios()
|
|
1104
|
+
if not selected:
|
|
1105
|
+
focused = self.focused
|
|
1106
|
+
if isinstance(focused, GPIOButton):
|
|
1107
|
+
selected = [focused.gpio_num]
|
|
547
1108
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1109
|
+
for gpio_num in selected:
|
|
1110
|
+
real_gpio = self.user_to_real_map[gpio_num]
|
|
1111
|
+
self.gpio_configs[real_gpio] = GPIO_MODES[mode_key]
|
|
1112
|
+
self.gpio_buttons[gpio_num].set_mode(mode_key)
|
|
1113
|
+
|
|
1114
|
+
# Clear selection after applying
|
|
1115
|
+
for btn in self.gpio_buttons.values():
|
|
1116
|
+
btn.deselect()
|
|
1117
|
+
self._update_status()
|
|
1118
|
+
|
|
1119
|
+
def action_done(self) -> None:
|
|
1120
|
+
"""Save and exit."""
|
|
1121
|
+
self.exit(result=self.gpio_configs)
|
|
1122
|
+
|
|
1123
|
+
def action_quit(self) -> None:
|
|
1124
|
+
"""Quit without explicit save (but configs are already updated)."""
|
|
1125
|
+
self.exit(result=self.gpio_configs)
|
|
1126
|
+
|
|
1127
|
+
# Run the Textual grid app
|
|
1128
|
+
|
|
1129
|
+
app = GPIOGridApp(gpio_configs, user_to_real_map, user_gpio_range, gpio_label)
|
|
1130
|
+
gpio_configs = app.run()
|
|
1131
|
+
|
|
1132
|
+
# ========================
|
|
1133
|
+
# SUMMARY & SAVE
|
|
1134
|
+
# ========================
|
|
1135
|
+
console.print("\n[bold]Final Configuration:[/bold]")
|
|
1136
|
+
display_summary(gpio_configs, user_to_real_map)
|
|
1137
|
+
|
|
1138
|
+
# Check for unconfigured
|
|
1139
|
+
unconfigured = [g for g in user_to_real_map.keys()
|
|
1140
|
+
if not gpio_configs.get(user_to_real_map[g]) or
|
|
1141
|
+
find_mode_key(gpio_configs.get(user_to_real_map[g])) is None]
|
|
1142
|
+
|
|
1143
|
+
if unconfigured:
|
|
1144
|
+
console.print(f"\n[yellow]Warning: {len(unconfigured)} pins still unconfigured: {format_gpio_ranges(unconfigured)}[/yellow]")
|
|
1145
|
+
confirm = console.input("Save anyway? (y/n): ").strip().lower()
|
|
1146
|
+
if confirm not in ('y', 'yes'):
|
|
1147
|
+
console.print("[yellow]Aborted.[/yellow]")
|
|
1148
|
+
raise click.Abort()
|
|
558
1149
|
|
|
559
1150
|
# Save to project.json
|
|
560
1151
|
save_gpio_config_to_project_json(str(project_json_path), gpio_configs)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "chipfoundry-cli"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.9"
|
|
4
4
|
description = "CLI tool to automate ChipFoundry project submission to SFTP server"
|
|
5
5
|
authors = ["ChipFoundry <marwan.abbas@chipfoundry.io>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -10,12 +10,13 @@ repository = "https://github.com/chipfoundry/cf-cli"
|
|
|
10
10
|
packages = [{ include = "chipfoundry_cli" }]
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
|
-
python = ">=3.8.0"
|
|
13
|
+
python = ">=3.8.1,<4.0"
|
|
14
14
|
click = ">=8.0.0,<9"
|
|
15
15
|
rich = ">=13,<14"
|
|
16
16
|
paramiko = ">=3.0.0,<4"
|
|
17
17
|
toml = ">=0.10,<1.0"
|
|
18
18
|
httpx = ">=0.24.0,<1.0"
|
|
19
|
+
textual = ">=0.40.0,<1"
|
|
19
20
|
|
|
20
21
|
[tool.poetry.dev-dependencies]
|
|
21
22
|
wheel = "*"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|