chipfoundry-cli 1.2.9__tar.gz → 1.2.10__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: chipfoundry-cli
3
- Version: 1.2.9
3
+ Version: 1.2.10
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 +7,7 @@ from chipfoundry_cli.utils import (
7
7
  open_html_in_browser, download_with_progress, update_repo_files,
8
8
  fetch_versions_from_upstream, parse_user_defines_v, update_user_defines_v,
9
9
  get_gpio_config_from_project_json, save_gpio_config_to_project_json,
10
- GPIO_MODES, GPIO_MODE_DESCRIPTIONS
10
+ GPIO_MODES, GPIO_MODE_DESCRIPTIONS, GPIO_HEX_TO_MODE
11
11
  )
12
12
  import os
13
13
  from pathlib import Path
@@ -527,7 +527,7 @@ def gpio_config(project_root):
527
527
  table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
528
528
  table.add_column("Mode", style="cyan", width=24)
529
529
  table.add_column("Count", justify="right", width=6)
530
- table.add_column("GPIOs", style="white")
530
+ table.add_column("GPIOs")
531
531
 
532
532
  # Sort by count (most common first)
533
533
  for mode_key in sorted(mode_groups.keys(), key=lambda k: -len(mode_groups[k])):
@@ -576,7 +576,7 @@ def gpio_config(project_root):
576
576
  if not mode_key:
577
577
  return "red"
578
578
  elif "output" in mode_key:
579
- return "green"
579
+ return "ansi_bright_blue"
580
580
  elif "input" in mode_key:
581
581
  return "cyan"
582
582
  elif "bidirectional" in mode_key:
@@ -838,7 +838,7 @@ def gpio_config(project_root):
838
838
  class GPIOGridApp(App):
839
839
  """Textual app for GPIO grid configuration."""
840
840
 
841
- GRID_COLS = 4 # Number of columns in the grid
841
+ GRID_COLS = 6 # Number of columns in the grid
842
842
 
843
843
  CSS = """
844
844
  Screen {
@@ -864,21 +864,21 @@ def gpio_config(project_root):
864
864
  }
865
865
 
866
866
  #gpio-grid {
867
- grid-size: 4;
868
- grid-gutter: 1;
869
- padding: 1;
867
+ grid-size: 6;
868
+ grid-gutter: 0;
869
+ padding: 0;
870
870
  height: auto;
871
871
  width: 100%;
872
872
  }
873
873
 
874
874
  GPIOButton {
875
875
  width: 1fr;
876
- min-width: 24;
876
+ min-width: 20;
877
877
  height: 4;
878
878
  text-align: center;
879
- border: solid green;
879
+ border: solid grey;
880
880
  content-align: center middle;
881
- padding: 0 1;
881
+ padding: 0;
882
882
  }
883
883
 
884
884
  GPIOButton.current {
@@ -1089,10 +1089,9 @@ def gpio_config(project_root):
1089
1089
  """Open mode selection for selected GPIOs."""
1090
1090
  selected = self._get_selected_gpios()
1091
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]
1092
+ # If nothing selected, use the current one (where cursor is)
1093
+ if 0 <= self.current_index < len(self.gpio_list):
1094
+ selected = [self.gpio_list[self.current_index]]
1096
1095
 
1097
1096
  if selected:
1098
1097
  self.push_screen(ModeSelectScreen(selected), self._apply_mode)
@@ -1102,9 +1101,9 @@ def gpio_config(project_root):
1102
1101
  if mode_key:
1103
1102
  selected = self._get_selected_gpios()
1104
1103
  if not selected:
1105
- focused = self.focused
1106
- if isinstance(focused, GPIOButton):
1107
- selected = [focused.gpio_num]
1104
+ # Use current one if nothing selected
1105
+ if 0 <= self.current_index < len(self.gpio_list):
1106
+ selected = [self.gpio_list[self.current_index]]
1108
1107
 
1109
1108
  for gpio_num in selected:
1110
1109
  real_gpio = self.user_to_real_map[gpio_num]
@@ -1198,6 +1197,119 @@ def gpio_config(project_root):
1198
1197
  except Exception as e:
1199
1198
  console.print(f"[red]Error updating user_defines.v: {e}[/red]")
1200
1199
 
1200
+
1201
+ @main.command('gpio-status')
1202
+ @click.option('--project-root', required=False, type=click.Path(exists=True, file_okay=False), help='Path to the project directory (defaults to current directory).')
1203
+ def gpio_status(project_root):
1204
+ """Display current GPIO configuration summary."""
1205
+ if not project_root:
1206
+ project_root = os.getcwd()
1207
+
1208
+ project_root = Path(project_root)
1209
+
1210
+ # Load project config
1211
+ project_json_path = project_root / '.cf' / 'project.json'
1212
+ if not project_json_path.exists():
1213
+ console.print("[red]Error: No project configuration found.[/red]")
1214
+ console.print("[dim]Run 'cf gpio-config' first to configure GPIOs.[/dim]")
1215
+ return
1216
+
1217
+ with open(project_json_path, 'r') as f:
1218
+ project_config = json.load(f)
1219
+
1220
+ # GPIO config is stored under project.gpio_config
1221
+ project_data = project_config.get('project', {})
1222
+ gpio_configs = project_data.get('gpio_config', {})
1223
+ if not gpio_configs:
1224
+ console.print("[yellow]No GPIO configuration found in project.json[/yellow]")
1225
+ console.print("[dim]Run 'cf gpio-config' to configure GPIOs.[/dim]")
1226
+ return
1227
+
1228
+ # Detect project type
1229
+ project_type = project_data.get('type')
1230
+ if not project_type:
1231
+ project_type = detect_project_type(project_root)
1232
+
1233
+ # Determine GPIO range based on project type
1234
+ if project_type == 'caravan':
1235
+ user_gpio_range = list(range(0, 32))
1236
+ gpio_label = "Caravan"
1237
+ else:
1238
+ user_gpio_range = list(range(5, 38))
1239
+ gpio_label = "Caravel"
1240
+
1241
+ user_to_real_map = {g: g for g in user_gpio_range}
1242
+
1243
+ # Convert string keys back to int if needed
1244
+ gpio_configs_int = {}
1245
+ for k, v in gpio_configs.items():
1246
+ try:
1247
+ gpio_configs_int[int(k)] = v
1248
+ except (ValueError, TypeError):
1249
+ gpio_configs_int[k] = v
1250
+
1251
+ def find_mode_key(mode_value):
1252
+ """Find the mode key for a given mode value (handles hex values)."""
1253
+ if not mode_value:
1254
+ return None
1255
+ # First, try to convert hex to mode constant name
1256
+ mode_name = GPIO_HEX_TO_MODE.get(mode_value, mode_value)
1257
+ # Then find the short key for that mode constant
1258
+ for key, name in GPIO_MODES.items():
1259
+ if name == mode_name and key != "invalid":
1260
+ return key
1261
+ return None
1262
+
1263
+ def format_gpio_ranges(gpio_list):
1264
+ """Convert [5,6,7,10,11,15] to '5-7, 10-11, 15'."""
1265
+ if not gpio_list:
1266
+ return "-"
1267
+ gpio_list = sorted(gpio_list)
1268
+ ranges = []
1269
+ start = gpio_list[0]
1270
+ end = start
1271
+ for g in gpio_list[1:]:
1272
+ if g == end + 1:
1273
+ end = g
1274
+ else:
1275
+ ranges.append(f"{start}-{end}" if start != end else str(start))
1276
+ start = end = g
1277
+ ranges.append(f"{start}-{end}" if start != end else str(start))
1278
+ return ", ".join(ranges)
1279
+
1280
+ # Group by mode
1281
+ mode_groups = {}
1282
+ for user_gpio, real_gpio in user_to_real_map.items():
1283
+ mode_value = gpio_configs_int.get(real_gpio)
1284
+ mode_key = find_mode_key(mode_value) if mode_value else None
1285
+ if mode_key not in mode_groups:
1286
+ mode_groups[mode_key] = []
1287
+ mode_groups[mode_key].append(user_gpio)
1288
+
1289
+ console.print(f"\n[bold cyan]GPIO Configuration ({gpio_label})[/bold cyan]\n")
1290
+
1291
+ table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
1292
+ table.add_column("Mode", style="cyan", width=24)
1293
+ table.add_column("Count", justify="right", width=6)
1294
+ table.add_column("GPIOs")
1295
+
1296
+ # Sort by count (most common first)
1297
+ for mode_key in sorted(mode_groups.keys(), key=lambda k: -len(mode_groups[k])):
1298
+ gpios = mode_groups[mode_key]
1299
+ display_name = mode_key if mode_key else "[red]unconfigured[/red]"
1300
+ style = ""
1301
+ if mode_key:
1302
+ if "output" in mode_key: style = "[green]"
1303
+ elif "input" in mode_key: style = "[cyan]"
1304
+ elif "bidirectional" in mode_key: style = "[yellow]"
1305
+ elif "analog" in mode_key: style = "[magenta]"
1306
+ display_name = f"{style}{mode_key}[/]"
1307
+ table.add_row(display_name, str(len(gpios)), format_gpio_ranges(gpios))
1308
+
1309
+ console.print(table)
1310
+ console.print()
1311
+
1312
+
1201
1313
  @main.command('push')
1202
1314
  @click.option('--project-root', required=False, type=click.Path(exists=True, file_okay=False), help='Path to the local ChipFoundry project directory (defaults to current directory if .cf/project.json exists).')
1203
1315
  @click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
@@ -2496,10 +2608,12 @@ def setup(project_root, repo_owner, repo_name, branch, pdk, caravel_lite,
2496
2608
  def harden(macro, project_root, list_designs, tag, pdk, use_nix, use_docker, dry_run):
2497
2609
  """Harden a macro using LibreLane (OpenLane 2).
2498
2610
 
2611
+ If no macro is specified, lists all available macros.
2612
+
2499
2613
  Examples:
2500
- cf harden user_proj_example
2614
+ cf harden # List available macros
2615
+ cf harden user_proj_example # Harden a specific macro
2501
2616
  cf harden user_project_wrapper
2502
- cf harden --list
2503
2617
  """
2504
2618
  from datetime import datetime
2505
2619
 
@@ -2512,8 +2626,8 @@ def harden(macro, project_root, list_designs, tag, pdk, use_nix, use_docker, dry
2512
2626
 
2513
2627
  project_root_path = Path(project_root)
2514
2628
 
2515
- # Check if project is initialized (skip check for --list, allow graceful return)
2516
- if not list_designs:
2629
+ # Check if project is initialized (skip check for --list or when no macro specified, allow graceful return)
2630
+ if not list_designs and macro:
2517
2631
  if not check_project_initialized(project_root_path, 'harden', dry_run=dry_run, allow_graceful=True):
2518
2632
  console.print(f"[red]✗[/red] Project not initialized. Please run 'cf init' first.")
2519
2633
  console.print("[yellow]Run 'cf setup' first to install OpenLane[/yellow]")
@@ -2527,7 +2641,11 @@ def harden(macro, project_root, list_designs, tag, pdk, use_nix, use_docker, dry
2527
2641
  console.print("[yellow]Run 'cf setup' first to install OpenLane[/yellow]")
2528
2642
  return
2529
2643
 
2530
- # List designs if requested
2644
+ # If no macro specified, default to listing available macros
2645
+ if not macro:
2646
+ list_designs = True
2647
+
2648
+ # List designs if requested (or if no macro specified)
2531
2649
  if list_designs:
2532
2650
  console.print("[bold cyan]Available macros:[/bold cyan]")
2533
2651
  designs = [d.name for d in openlane_dir.iterdir() if d.is_dir() and ((d / 'config.json').exists() or (d / 'config.yaml').exists() or (d / 'config.tcl').exists())]
@@ -2544,13 +2662,6 @@ def harden(macro, project_root, list_designs, tag, pdk, use_nix, use_docker, dry
2544
2662
  console.print("[yellow]No macros found in openlane/[/yellow]")
2545
2663
  return
2546
2664
 
2547
- # Macro is required if not listing
2548
- if not macro:
2549
- console.print("[red]✗[/red] Error: MACRO argument is required")
2550
- console.print("[yellow]Usage:[/yellow] cf harden <macro>")
2551
- console.print("[yellow] [/yellow] cf harden --list")
2552
- return
2553
-
2554
2665
  # Check if macro exists
2555
2666
  macro_dir = openlane_dir / macro
2556
2667
  if not macro_dir.exists():
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "chipfoundry-cli"
3
- version = "1.2.9"
3
+ version = "1.2.10"
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"
@@ -18,7 +18,7 @@ toml = ">=0.10,<1.0"
18
18
  httpx = ">=0.24.0,<1.0"
19
19
  textual = ">=0.40.0,<1"
20
20
 
21
- [tool.poetry.dev-dependencies]
21
+ [tool.poetry.group.dev.dependencies]
22
22
  wheel = "*"
23
23
  black = ">=24.4.0,<25"
24
24
  flake8 = ">=4"