microchip-devtools 0.1.0__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.
@@ -0,0 +1,334 @@
1
+ """
2
+ setup_env runner — Environment checker and project installer.
3
+
4
+ Called by each project's thin pymake/setup_env.py wrapper, which passes
5
+ the merged DEFAULTS (COMMON_DEFAULTS | PROJECT_DEFAULTS).
6
+
7
+ Usage (via project wrapper):
8
+ poetry run setup_env [action]
9
+
10
+ Actions:
11
+ check Verify all prerequisites (default)
12
+ install Run `poetry install` to set up the Python environment
13
+ env Show current ENV variable values vs. their defaults
14
+ init-env Create a .env file from defaults
15
+ all Run check → install in sequence
16
+
17
+ Exit codes:
18
+ 0 All checks passed / action completed successfully
19
+ 1 One or more checks failed
20
+ """
21
+
22
+ import argparse
23
+ import os
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ from dotenv import load_dotenv as _dotenv_load
28
+ from rich.panel import Panel
29
+ from rich.prompt import Prompt
30
+ from rich.table import Table
31
+
32
+ from microchip_devtools.setup_env._ui import console
33
+ from microchip_devtools.setup_env.checks import (
34
+ check_boot_hex,
35
+ check_cppcheck,
36
+ check_dfp,
37
+ check_make,
38
+ check_poetry,
39
+ check_programmer,
40
+ check_python,
41
+ check_uncrustify,
42
+ check_xc32,
43
+ )
44
+
45
+ _ENV_DESCRIPTIONS: dict[str, str] = {
46
+ "XC32_PATH": "Path to Microchip XC32 toolchain bin/ directory",
47
+ "DFP_PATH": "Path to the Device Family Pack (DFP) root directory",
48
+ "IPE_CMD": "Path to the MPLAB IPE command-line script (ipecmd.sh)",
49
+ "BOOT_HEX": "Path to the bootloader .hex file used by the flash-with-boot target",
50
+ "PROGRAMMER": "Programmer/debugger tool short name (e.g. PK5, ICD4, SNAP)",
51
+ }
52
+
53
+
54
+ def _load_dotenv(env_file: Path) -> None:
55
+ _dotenv_load(dotenv_path=env_file, override=False)
56
+
57
+
58
+ def _resolve(key: str, defaults: dict[str, str]) -> str:
59
+ return os.path.expandvars(
60
+ os.path.expanduser(os.environ.get(key, defaults.get(key, "")))
61
+ )
62
+
63
+
64
+ def check(defaults: dict[str, str], env_file: Path) -> bool:
65
+ console.print()
66
+ console.rule("Checking prerequisites", style="bold blue")
67
+ console.print()
68
+
69
+ results: list[bool] = [
70
+ check_python(),
71
+ check_poetry(),
72
+ check_make(),
73
+ check_cppcheck(),
74
+ check_uncrustify(),
75
+ ]
76
+
77
+ if "XC32_PATH" in defaults:
78
+ results.append(check_xc32(_resolve("XC32_PATH", defaults), env_file))
79
+ if "DFP_PATH" in defaults:
80
+ results.append(check_dfp(_resolve("DFP_PATH", defaults), env_file))
81
+ if "BOOT_HEX" in defaults:
82
+ results.append(check_boot_hex(_resolve("BOOT_HEX", defaults), env_file))
83
+ if "PROGRAMMER" in defaults:
84
+ results.append(check_programmer(_resolve("PROGRAMMER", defaults)))
85
+
86
+ passed = sum(results)
87
+ total = len(results)
88
+ console.print()
89
+
90
+ if all(results):
91
+ console.print(
92
+ Panel(
93
+ f"[green]✔ All {total} checks passed. The project is ready to build.[/green]\n"
94
+ f" Run [bold]make[/bold] or [bold]poetry run mchp-setup install[/bold] to continue.",
95
+ border_style="green",
96
+ padding=(0, 2),
97
+ )
98
+ )
99
+ else:
100
+ failed = total - passed
101
+ console.print(
102
+ Panel(
103
+ f"[red]✗ {failed} of {total} checks failed.[/red]\n"
104
+ f" Fix the issues above, then run [bold]poetry run mchp-setup check[/bold] again.",
105
+ border_style="red",
106
+ padding=(0, 2),
107
+ )
108
+ )
109
+
110
+ console.print()
111
+ return all(results)
112
+
113
+
114
+ def install(
115
+ defaults: dict[str, str], env_file: Path, skip_checks: bool = False
116
+ ) -> None:
117
+ if not skip_checks:
118
+ ok = check(defaults, env_file)
119
+ if not ok:
120
+ console.print("[red] Skipping install because checks failed.[/red]\n")
121
+ sys.exit(1)
122
+
123
+ console.print()
124
+ console.rule("Installing Python environment", style="bold blue")
125
+ console.print()
126
+ try:
127
+ import subprocess
128
+
129
+ subprocess.run(["poetry", "env", "remove", "python"], check=False)
130
+ subprocess.run(["poetry", "install", "--no-root"], check=True)
131
+ console.print("\n[green] Done. Python environment is ready.[/green]\n")
132
+ except Exception as exc:
133
+ console.print(f"\n[red] `poetry install` failed: {exc}[/red]\n")
134
+ sys.exit(1)
135
+
136
+
137
+ def env(defaults: dict[str, str]) -> None:
138
+ console.print()
139
+ console.rule("Environment variables", style="bold blue")
140
+ console.print()
141
+
142
+ table = Table(
143
+ show_header=True,
144
+ header_style="bold",
145
+ padding=(0, 1),
146
+ show_edge=False,
147
+ )
148
+ table.add_column("Variable", style="bold cyan", no_wrap=True)
149
+ table.add_column("Current value")
150
+ table.add_column("Default", style="dim")
151
+
152
+ for key, default in defaults.items():
153
+ current = os.environ.get(key)
154
+ if current is None:
155
+ value_display = f"[yellow](default) {default}[/yellow]"
156
+ elif current == default:
157
+ value_display = current
158
+ else:
159
+ value_display = f"[green]{current}[/green] [yellow]← overridden[/yellow]"
160
+ table.add_row(key, value_display, default)
161
+
162
+ console.print(table)
163
+ console.print()
164
+
165
+
166
+ def _prompt_env_form(defaults: dict[str, str], env_file: Path) -> None:
167
+ console.print(
168
+ Panel(
169
+ "[bold yellow]ACTION REQUIRED — Configure your environment[/bold yellow]\n\n"
170
+ " Review each variable below.\n"
171
+ " Press [bold]Enter[/bold] to keep the default, or type a new value to override it.\n"
172
+ " Variables you override will be [bold green]uncommented[/bold green] in [cyan].env[/cyan].\n"
173
+ " Variables left at default stay commented out (no effect on the build).",
174
+ border_style="yellow",
175
+ padding=(1, 2),
176
+ title="[bold yellow] .env setup [/bold yellow]",
177
+ )
178
+ )
179
+ console.print()
180
+
181
+ chosen: dict[str, str | None] = {}
182
+ for key, default in defaults.items():
183
+ desc = _ENV_DESCRIPTIONS.get(key, "")
184
+ if desc:
185
+ console.print(f" [dim]{desc}[/dim]")
186
+ value = Prompt.ask(
187
+ f" [bold cyan]{key}[/bold cyan]",
188
+ default=default,
189
+ console=console,
190
+ )
191
+ chosen[key] = value if value != default else None
192
+ console.print()
193
+
194
+ overridden = {k: v for k, v in chosen.items() if v is not None}
195
+
196
+ lines = [
197
+ "# .env — local overrides for environment variables",
198
+ "# Shell-level exports always take priority over values set here.",
199
+ "# Uncomment and edit only the variables you need to override.",
200
+ "",
201
+ ]
202
+ for key, default in defaults.items():
203
+ desc = _ENV_DESCRIPTIONS.get(key, "")
204
+ if desc:
205
+ lines.append(f"# {desc}")
206
+ if key in overridden:
207
+ lines.append(f"{key}={overridden[key]}")
208
+ else:
209
+ lines.append(f"# {key}={default}")
210
+ lines.append("")
211
+
212
+ env_file.write_text("\n".join(lines))
213
+
214
+ if overridden:
215
+ keys_fmt = ", ".join(f"[cyan]{k}[/cyan]" for k in overridden)
216
+ console.print(
217
+ Panel(
218
+ f"[green]✔ .env updated.[/green] Overridden: {keys_fmt}\n\n"
219
+ f" Run [bold]poetry run mchp-setup check[/bold] to verify your setup.",
220
+ border_style="green",
221
+ padding=(0, 2),
222
+ )
223
+ )
224
+ else:
225
+ console.print(
226
+ Panel(
227
+ "[green]✔ .env created.[/green] All variables left at their defaults (commented out).\n\n"
228
+ " Edit [cyan].env[/cyan] manually any time to override a value.\n"
229
+ " Run [bold]poetry run mchp-setup check[/bold] to verify your setup.",
230
+ border_style="green",
231
+ padding=(0, 2),
232
+ )
233
+ )
234
+ console.print()
235
+
236
+
237
+ def init_env(defaults: dict[str, str], env_file: Path, force: bool = False) -> None:
238
+ env_example = env_file.parent / ".env.example"
239
+
240
+ if env_file.exists() and not force:
241
+ console.print(
242
+ "[yellow] .env already exists. Use --force to overwrite it.[/yellow]\n"
243
+ )
244
+ return
245
+
246
+ if env_example.exists():
247
+ import shutil
248
+
249
+ shutil.copy(env_example, env_file)
250
+ console.print(f"[green] Created .env from {env_example.name}.[/green]")
251
+ _prompt_env_form(defaults, env_file)
252
+ return
253
+
254
+ lines = [
255
+ "# .env — local overrides for environment variables",
256
+ "# Shell-level exports always take priority over values set here.",
257
+ "# Uncomment and edit only the variables you need to override.",
258
+ "",
259
+ ]
260
+ for key, default in defaults.items():
261
+ desc = _ENV_DESCRIPTIONS.get(key, "")
262
+ if desc:
263
+ lines.append(f"# {desc}")
264
+ lines.append(f"# {key}={default}")
265
+ lines.append("")
266
+
267
+ env_file.write_text("\n".join(lines))
268
+ console.print(
269
+ f"[green] Created .env with {len(defaults)} commented-out variables.[/green]"
270
+ )
271
+ _prompt_env_form(defaults, env_file)
272
+
273
+
274
+ def main(defaults: dict[str, str] | None = None) -> None:
275
+ """Entry point. Projects pass merged COMMON_DEFAULTS | PROJECT_DEFAULTS."""
276
+ if defaults is None:
277
+ import sys
278
+
279
+ from microchip_devtools.setup_env.defaults import COMMON_DEFAULTS
280
+
281
+ cwd = str(Path.cwd())
282
+ if cwd not in sys.path:
283
+ sys.path.insert(0, cwd)
284
+
285
+ try:
286
+ from pymake import PROJECT_DEFAULTS # type: ignore
287
+
288
+ defaults = COMMON_DEFAULTS | PROJECT_DEFAULTS
289
+ except ImportError:
290
+ defaults = COMMON_DEFAULTS
291
+
292
+ env_file = Path(".env")
293
+ _load_dotenv(env_file)
294
+
295
+ parser = argparse.ArgumentParser(
296
+ prog="setup_env",
297
+ description="Verify and install everything needed to compile the firmware.",
298
+ formatter_class=argparse.RawDescriptionHelpFormatter,
299
+ epilog="""
300
+ actions:
301
+ check Verify all prerequisites (default)
302
+ install Run poetry install
303
+ env Show ENV variable values vs. defaults
304
+ init-env Create a .env file from defaults
305
+ all check → install in sequence
306
+ """,
307
+ )
308
+ parser.add_argument(
309
+ "action",
310
+ nargs="?",
311
+ default="check",
312
+ choices=["check", "install", "env", "init-env", "all"],
313
+ )
314
+ parser.add_argument("--skip-checks", action="store_true")
315
+ parser.add_argument("--force", action="store_true")
316
+
317
+ args = parser.parse_args()
318
+
319
+ assert defaults is not None
320
+
321
+ if args.action == "check":
322
+ ok = check(defaults, env_file)
323
+ sys.exit(0 if ok else 1)
324
+ elif args.action == "install":
325
+ install(defaults, env_file, skip_checks=args.skip_checks)
326
+ elif args.action == "env":
327
+ env(defaults)
328
+ elif args.action == "init-env":
329
+ init_env(defaults, env_file, force=args.force)
330
+ elif args.action == "all":
331
+ ok = check(defaults, env_file)
332
+ if not ok:
333
+ sys.exit(1)
334
+ install(defaults, env_file, skip_checks=True)
File without changes
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ microchip_devtools.xc32.merge_hex — Merge bootloader + app HEX into a single image.
4
+
5
+ Optionally patches:
6
+ - A signature word at a given physical address (for bootloader app-valid checks)
7
+ - DEVCFG0 config bits to enable EJTAG debugging (PIC32MK-specific)
8
+
9
+ Usage:
10
+ mchp-merge-hex boot.hex app.hex out.hex [options]
11
+
12
+ Options:
13
+ --sig-addr ADDR Physical address to write the signature word (hex, e.g. 0x1D0FFFF8)
14
+ --sig-word VALUE Signature word value (hex, default: 0x00000000)
15
+ --ejtag-addr ADDR Physical address of DEVCFG0 config word for EJTAG enable (optional)
16
+
17
+ If --sig-addr is omitted, no signature patch is applied.
18
+ If --ejtag-addr is omitted, no DEVCFG0 patch is applied.
19
+ """
20
+
21
+ import argparse
22
+ import os
23
+ import sys
24
+
25
+ END_RECORD = ":00000001FF"
26
+
27
+ # PIC32 DEVCFG0 bit constants (architecture-level, same for all PIC32 devices):
28
+ # DEBUG[1:0] at bits 1:0 — 0b00 = debugger enabled, 0b11 = disabled
29
+ # JTAGEN at bit 2 — 1 = JTAG enabled, 0 = disabled
30
+ _DEVCFG0_DEBUG_MASK = 0x00000003
31
+ _DEVCFG0_JTAGEN_BIT = 0x00000004
32
+
33
+
34
+ def _ihex_checksum(record_bytes: list[int]) -> int:
35
+ return (~sum(record_bytes) + 1) & 0xFF
36
+
37
+
38
+ def _patch_word(lines: list[str], addr: int, word: int) -> list[str]:
39
+ """Write *word* (little-endian, 4 bytes) at physical *addr* in an Intel HEX line list."""
40
+ word_bytes = [(word >> (8 * i)) & 0xFF for i in range(4)]
41
+
42
+ current_ela = 0
43
+ result: list[str] = []
44
+ patched = False
45
+
46
+ for line in lines:
47
+ stripped = line.strip()
48
+ if not stripped.startswith(':'):
49
+ result.append(line)
50
+ continue
51
+
52
+ byte_count = int(stripped[1:3], 16)
53
+ addr_offset = int(stripped[3:7], 16)
54
+ record_type = int(stripped[7:9], 16)
55
+ data_hex = stripped[9: 9 + byte_count * 2]
56
+
57
+ if record_type == 0x04:
58
+ current_ela = int(data_hex, 16)
59
+ result.append(line)
60
+ continue
61
+
62
+ if record_type != 0x00:
63
+ result.append(line)
64
+ continue
65
+
66
+ base_addr = (current_ela << 16) | addr_offset
67
+ end_addr = base_addr + byte_count
68
+
69
+ if base_addr <= addr < end_addr:
70
+ data = bytearray.fromhex(data_hex)
71
+ off = addr - base_addr
72
+ for i in range(4):
73
+ if off + i < byte_count:
74
+ data[off + i] = word_bytes[i]
75
+
76
+ core = [byte_count,
77
+ (addr_offset >> 8) & 0xFF,
78
+ addr_offset & 0xFF,
79
+ 0x00] + list(data)
80
+ chk = _ihex_checksum(core)
81
+ result.append(f":{byte_count:02X}{addr_offset:04X}00{data.hex().upper()}{chk:02X}")
82
+ patched = True
83
+ else:
84
+ result.append(line)
85
+
86
+ if not patched:
87
+ raise RuntimeError(
88
+ f"[merge_hex] ERROR: address 0x{addr:08X} not found in merged HEX.\n"
89
+ f" Expected a data record covering that address."
90
+ )
91
+
92
+ return result
93
+
94
+
95
+ def _patch_devcfg0_ejtag(lines: list[str], devcfg0_addr: int) -> list[str]:
96
+ """Enable EJTAG in the DEVCFG0 config word at *devcfg0_addr*.
97
+
98
+ Clears DEBUG[1:0] (bits 1:0) and sets JTAGEN (bit 2).
99
+ Without this patch the debugger attach fails with error 0x104 when
100
+ the bootloader was built with JTAGEN=OFF and DEBUG=OFF.
101
+ """
102
+ current_ela = 0
103
+ result: list[str] = []
104
+ patched = False
105
+
106
+ for line in lines:
107
+ stripped = line.strip()
108
+ if not stripped.startswith(':'):
109
+ result.append(line)
110
+ continue
111
+
112
+ byte_count = int(stripped[1:3], 16)
113
+ addr_offset = int(stripped[3:7], 16)
114
+ record_type = int(stripped[7:9], 16)
115
+ data_hex = stripped[9: 9 + byte_count * 2]
116
+
117
+ if record_type == 0x04:
118
+ current_ela = int(data_hex, 16)
119
+ result.append(line)
120
+ continue
121
+
122
+ if record_type != 0x00:
123
+ result.append(line)
124
+ continue
125
+
126
+ base_addr = (current_ela << 16) | addr_offset
127
+ end_addr = base_addr + byte_count
128
+
129
+ if base_addr <= devcfg0_addr < end_addr:
130
+ data = bytearray.fromhex(data_hex)
131
+ off = devcfg0_addr - base_addr
132
+
133
+ word = data[off] | (data[off+1] << 8) | (data[off+2] << 16) | (data[off+3] << 24)
134
+ orig = word
135
+ word &= ~_DEVCFG0_DEBUG_MASK
136
+ word |= _DEVCFG0_JTAGEN_BIT
137
+
138
+ for i in range(4):
139
+ data[off + i] = (word >> (8 * i)) & 0xFF
140
+
141
+ core = [byte_count,
142
+ (addr_offset >> 8) & 0xFF,
143
+ addr_offset & 0xFF,
144
+ 0x00] + list(data)
145
+ chk = _ihex_checksum(core)
146
+ result.append(f":{byte_count:02X}{addr_offset:04X}00{data.hex().upper()}{chk:02X}")
147
+ patched = True
148
+
149
+ print(f"[merge_hex] DEVCFG0 : 0x{orig:08X} → 0x{word:08X} "
150
+ f"(JTAGEN=ON, DEBUG=ON) at phys 0x{devcfg0_addr:08X}")
151
+ else:
152
+ result.append(line)
153
+
154
+ if not patched:
155
+ raise RuntimeError(
156
+ f"[merge_hex] ERROR: DEVCFG0 at 0x{devcfg0_addr:08X} not found.\n"
157
+ f" The bootloader HEX must include config word records."
158
+ )
159
+
160
+ return result
161
+
162
+
163
+ def merge(
164
+ boot_path: str,
165
+ app_path: str,
166
+ out_path: str,
167
+ sig_addr: int | None = None,
168
+ sig_word: int = 0x00000000,
169
+ ejtag_addr: int | None = None,
170
+ ) -> None:
171
+ if not os.path.exists(boot_path):
172
+ sys.exit(f"[merge_hex] ERROR: bootloader HEX not found: {boot_path}")
173
+ if not os.path.exists(app_path):
174
+ sys.exit(f"[merge_hex] ERROR: app HEX not found: {app_path}\n"
175
+ f" Run 'make' first to build the app.")
176
+
177
+ with open(boot_path, "r", encoding="utf-8") as f:
178
+ boot_lines = [l.rstrip("\r\n") for l in f]
179
+
180
+ with open(app_path, "r", encoding="utf-8") as f:
181
+ app_lines = [l.rstrip("\r\n") for l in f]
182
+
183
+ boot_stripped = [l for l in boot_lines if l.upper().strip() != END_RECORD]
184
+ merged = boot_stripped + app_lines
185
+
186
+ if sig_addr is not None:
187
+ merged = _patch_word(merged, sig_addr, sig_word)
188
+ print(f"[merge_hex] signed : 0x{sig_word:08X} written at phys 0x{sig_addr:08X}")
189
+
190
+ if ejtag_addr is not None:
191
+ merged = _patch_devcfg0_ejtag(merged, ejtag_addr)
192
+
193
+ os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
194
+ with open(out_path, "w", newline="\n", encoding="utf-8") as f:
195
+ for line in merged:
196
+ f.write(line + "\n")
197
+
198
+ print(f"[merge_hex] boot : {boot_path} ({len(boot_stripped)} records)")
199
+ print(f"[merge_hex] app : {app_path} ({len(app_lines)} records)")
200
+ print(f"[merge_hex] output : {out_path} ({len(merged)} records total)")
201
+
202
+
203
+ def main() -> None:
204
+ parser = argparse.ArgumentParser(
205
+ description="Merge bootloader + app HEX and optionally patch signature/debug config bits."
206
+ )
207
+ parser.add_argument("boot", help="Bootloader HEX file")
208
+ parser.add_argument("app", help="App HEX file")
209
+ parser.add_argument("out", help="Output merged HEX file")
210
+ parser.add_argument(
211
+ "--sig-addr",
212
+ type=lambda x: int(x, 0),
213
+ default=None,
214
+ metavar="ADDR",
215
+ help="Physical address for signature word patch (e.g. 0x1D0FFFF8). Omit to skip.",
216
+ )
217
+ parser.add_argument(
218
+ "--sig-word",
219
+ type=lambda x: int(x, 0),
220
+ default=0x00000000,
221
+ metavar="VALUE",
222
+ help="Signature word value (default: 0x00000000)",
223
+ )
224
+ parser.add_argument(
225
+ "--ejtag-addr",
226
+ type=lambda x: int(x, 0),
227
+ default=None,
228
+ metavar="ADDR",
229
+ help="Physical address of DEVCFG0 for EJTAG enable (e.g. 0x1FC03FCC). Omit to skip.",
230
+ )
231
+
232
+ args = parser.parse_args()
233
+ merge(args.boot, args.app, args.out,
234
+ sig_addr=args.sig_addr, sig_word=args.sig_word, ejtag_addr=args.ejtag_addr)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()