//! Engine-backed programming commands: parse modern options here, translate //! to the legacy ipecmd argument vector, and run the bundled JVM engine. const std = @import("std"); const cli = @import("cli"); const engine = @import("engine.zig"); const style = @import("style.zig"); const mem = std.mem; const process = std.process; const Init = process.Init; const File = Io.File; const Io = std.Io; const Allocator = mem.Allocator; const ArrayList = std.ArrayList; const Region = enum { program, eeprom, id, configuration, boot, auxiliary }; fn regionLetter(r: Region) u8 { return switch (r) { .program => 'P', .eeprom => 'E', .id => 'I', .configuration => 'C', .boot => 'B', .auxiliary => 'A', }; } /// The engine expects prefix-less part numbers and re-adds family prefixes /// itself ("PIC"/"dsPIC"/...); undo whatever prefix the caller included. pub fn stripDevicePrefix(dev: []const u8) []const u8 { const prefixes = [_]struct { name: []const u8, len: usize }{ .{ .name = "dspic3", .len = 5 }, .{ .name = "DSPIC3", .len = 5 }, .{ .name = "rfpic", .len = 5 }, .{ .name = "RFPIC", .len = 5 }, .{ .name = "pic", .len = 3 }, .{ .name = "PIC", .len = 3 }, }; // dsPIC/rfPIC must be tested before PIC; longest first is handled above. for (prefixes) |p| { if (dev.len > p.len and mem.startsWith(u8, dev, p.name)) return dev[p.len..]; } return dev; } fn normalizeTool(raw: []const u8) []const u8 { const map = [_]struct { alias: []const u8, short: []const u8 }{ .{ .alias = "pk5", .short = "PK5" }, .{ .alias = "pickit5", .short = "PK5" }, .{ .alias = "pk4", .short = "PK4" }, .{ .alias = "pickit4", .short = "PK4" }, .{ .alias = "icd4", .short = "ICD4" }, .{ .alias = "icd5", .short = "ICD5" }, .{ .alias = "ice4", .short = "ICE4" }, .{ .alias = "snap", .short = "SNAP" }, .{ .alias = "pkob", .short = "PKOB" }, .{ .alias = "pkob3", .short = "PKOB" }, .{ .alias = "pkob4", .short = "PKOB4" }, .{ .alias = "pkbasic", .short = "PKBASIC" }, .{ .alias = "pickitbasic", .short = "PKBASIC" }, .{ .alias = "nedbg", .short = "NEDBG" }, .{ .alias = "j32", .short = "J32" }, }; var buf: [32]u8 = undefined; if (raw.len < buf.len) { const lower = std.ascii.lowerString(buf[0..raw.len], raw); for (map) |m| { if (mem.eql(u8, lower, m.alias)) return m.short; } } return raw; } const Args = struct { arena: Allocator, legacy: ArrayList([]const u8) = .empty, fn add(self: *Args, comptime fmt: []const u8, args: anytype) !void { const s = try std.fmt.allocPrint(self.arena, fmt, args); try self.legacy.append(self.arena, s); } }; fn die(init: Init, comptime fmt: []const u8, args: anytype) noreturn { var buf: [512]u8 = undefined; var w = File.stderr().writer(init.io, &buf); style.errPrefix(init.io, &w.interface) catch {}; w.interface.print(fmt, args) catch {}; w.interface.flush() catch {}; process.exit(2); } /// Validate NAME,VERSION[,VENDOR] against installed packs; returns the /// canonical triple for the engine-side override property. fn validateDfp( init: Init, spec: []const u8, user_repo: []const u8, system_repo: []const u8, ) ![]const u8 { const allocator = init.arena.allocator(); var it = mem.splitScalar(u8, spec, ','); const name = it.next() orelse ""; const version = it.next() orelse ""; const vendor_in = it.next(); if (name.len == 0 or version.len == 0 or it.next() != null) { die(init, "--dfp expects NAME,VERSION or NAME,VERSION,VENDOR\n", .{}); } const packs_list = try engine.scanInstalledPacks(init.io, allocator, user_repo, system_repo); for (packs_list.items) |p| { if (!mem.eql(u8, p.name, name)) continue; for (p.releases.items) |rel| { if (!rel.installed) continue; if (!mem.eql(u8, rel.version, version)) continue; if (vendor_in) |v| if (!mem.eql(u8, p.vendor, v)) continue; return std.fmt.allocPrint(allocator, "{s},{s},{s}", .{ p.name, rel.version, p.vendor }); } } die(init, "pack not installed: {s} version {s} (see `mplabx packs list`)\n", .{ name, version }); } pub fn run( init: Init, device: cli.Param( "device", []const u8, .required, "Target device (e.g. pic16f1779, dspic33ep128gp502, atmega328p)", ), tool: cli.Param( "tool", ?[]const u8, .optional(null), "Tool family: pk5, pk4, icd4, icd5, snap, ice4, nedbg, pkob4, pkbasic, ...", ), serial: cli.Param( "serial", ?[]const u8, .optional(null), "Select tool by serial number", ), file: cli.Param( "file", ?[]const u8, .optional(null), "Hex file to program/verify/checksum", ), do_program: cli.Param( "program", bool, .optional(false), "Program device from hex file", ), program_regions: cli.Param( "program-regions", ?[]Region, .optional(null), "Memory regions to program (default: all)", ), do_verify: cli.Param( "verify", bool, .optional(false), "Verify device against hex file (implied by --program)", ), verify_regions: cli.Param( "verify-regions", ?[]Region, .optional(null), "Memory regions to verify (default: all)", ), do_erase: cli.Param( "erase", bool, .optional(false), "Erase device", ), blank_check: cli.Param( "blank-check", bool, .optional(false), "Blank check device", ), read_file: cli.Param( "read", ?[]const u8, .optional(null), "Read device into this hex file", ), read_region: cli.Param( "read-region", ?[]const u8, .optional(null), "Dump memory region to screen (e.g. p:600-6ff, c)", ), connect_only: cli.Param( "connect", bool, .optional(false), "Connect only, no operation", ), show_id: cli.Param( "id", bool, .optional(false), "Display device id", ), checksum: cli.Param( "checksum", bool, .optional(false), "Display hex file checksum", ), crc32: cli.Param( "crc32", bool, .optional(false), "Display hex file CRC32", ), cp_checksum: cli.Param( "cp-checksum", bool, .optional(false), "Display code-protect-off checksum", ), power: cli.Param( "power", bool, .optional(false), "Power target from the tool", ), release: cli.Param( "release", bool, .optional(false), "Release target from reset when done", ), vdd_first: cli.Param( "vdd-first", bool, .optional(false), "Power target via VDD first where supported", ), verify_id: cli.Param( "verify-id", bool, .optional(false), "Verify device id before programming", ), no_erase: cli.Param( "no-erase", bool, .optional(false), "Do not erase all before programming", ), no_verify_flag: cli.Param( "no-verify", bool, .optional(false), "Do not verify after programming", ), preserve: cli.Param( "preserve", ?[]const u8, .optional(null), "Preserve program memory range (e.g. 0-1ff)", ), preserve_eeprom: cli.Param( "preserve-eeprom", bool, .optional(false), "Preserve EEPROM on program", ), preserve_userid: cli.Param( "preserve-userid", bool, .optional(false), "Preserve user id memory on program", ), interface: cli.Param( "interface", ?[]const u8, .optional(null), "AVR/SAM interface: jtag swd updi isp tpi pdi hvpp hvsp", ), speed: cli.Param( "speed", ?[]const u8, .optional(null), "AVR/SAM interface clock speed in MHz", ), jtag_wires: cli.Param( "jtag", ?[]const u8, .optional(null), "JTAG wires: 2 or 4", ), dfp: cli.Param( "dfp", ?[]const u8, .optional(null), "Force device pack NAME,VERSION[,VENDOR]", ), ) !void { const allocator = init.arena.allocator(); var do_verify_v = do_verify.value; const has_hex = file.value != null; // --- validation ------------------------------------------------------- const needs_hex = do_program.value or do_verify_v or checksum.value or cp_checksum.value; if (needs_hex and !has_hex) { die(init, "this operation needs a hex file (--file )\n", .{}); } if (do_program.value and !do_verify_v and !no_verify_flag.value) { // Match IDE/IPE behaviour: programming verifies by default. do_verify_v = true; } if (read_region.value) |spec| validateReadRegion(init, spec); if (interface.value) |iface| { const valid = [_][]const u8{ "JTAG", "SWD", "UPDI", "ISP", "TPI", "PDI", "HVPP", "HVSP" }; var buf: [16]u8 = undefined; if (iface.len == 0 or iface.len > buf.len) die(init, "invalid --interface\n", .{}); const upper = std.ascii.upperString(buf[0..iface.len], iface); var ok = false; for (valid) |v| { if (mem.eql(u8, upper, v)) ok = true; } if (!ok) die(init, "invalid interface '{s}'\n", .{iface}); } // --- translation ------------------------------------------------------ var args: Args = .{ .arena = allocator }; if (tool.value) |t| try args.add("-TP{s}", .{normalizeTool(t)}); if (serial.value) |s| try args.add("-TS{s}", .{s}); if (file.value) |f| try args.add("-F{s}", .{f}); if (read_file.value) |f| try args.add("-GF{s}", .{f}); if (read_region.value) |spec| { // Validated form: [PEICBAD] followed by optional ":range" for P/E. var buf: [64]u8 = undefined; const legacy = formatReadRegion(spec, &buf); try args.add("-G{s}", .{legacy}); } if (show_id.value) try args.add("-I", .{}); if (checksum.value) try args.add("-K", .{}); if (crc32.value) try args.add("-OZ", .{}); if (cp_checksum.value) try args.add("-OSC", .{}); if (do_erase.value) try args.add("-E", .{}); if (blank_check.value) try args.add("-C", .{}); if (do_program.value) { if (program_regions.value) |regions| { try args.add("-M{s}", .{try regionLetters(allocator, regions)}); } else try args.add("-M", .{}); } if (do_verify_v) { if (verify_regions.value) |regions| { try args.add("-Y{s}", .{try regionLetters(allocator, regions)}); } else try args.add("-Y", .{}); } if (connect_only.value) try args.add("-OK", .{}); if (power.value) try args.add("-W", .{}); if (release.value) try args.add("-OL", .{}); if (vdd_first.value) try args.add("-OD", .{}); if (verify_id.value) try args.add("-OV", .{}); if (no_erase.value) try args.add("-OH", .{}); if (preserve.value) |r| try args.add("-OP{s}", .{r}); if (preserve_eeprom.value) try args.add("-Z", .{}); if (preserve_userid.value) try args.add("-OID", .{}); if (interface.value) |iface| try args.add("-ORI{s}", .{iface}); if (speed.value) |s| try args.add("-ORS{s}", .{s}); if (jtag_wires.value) |w| try args.add("-OJ{s}", .{w}); try args.add("-P{s}", .{stripDevicePrefix(device.value)}); // --- locate engine & runtime ------------------------------------------ const home = resolveEngineHome(init); var java_path_buf: [512]u8 = undefined; const java = resolveJava(init, &java_path_buf); var cp: ArrayList(u8) = .empty; try buildClasspath(init, allocator, home, &cp); var child_argv: ArrayList([]const u8) = .empty; try child_argv.append(allocator, java); try child_argv.append(allocator, "-Djava.awt.headless=true"); if (dfp.value) |spec| { const user_repo = resolveUserRepo(allocator, init.environ_map); const system_repo = init.environ_map.get("packslib.packsfolder") orelse ""; const triple = try validateDfp(init, spec, user_repo, system_repo); const prop = try std.fmt.allocPrint(allocator, "-Dprogcli.dfp={s}", .{triple}); try child_argv.append(allocator, prop); } try child_argv.append(allocator, "-cp"); try child_argv.append(allocator, cp.items); try child_argv.append(allocator, "com.microchip.mplab.ipecmd.IPECMD"); try child_argv.appendSlice(allocator, args.legacy.items); var child = process.spawn(init.io, .{ .argv = child_argv.items }) catch |err| { die(init, "cannot launch java ({s}): make sure a JRE is available (JAVA_HOME or PATH)\n", .{@errorName(err)}); }; const term = child.wait(init.io) catch |err| { die(init, "waiting for engine failed: {s}\n", .{@errorName(err)}); }; switch (term) { .exited => |code| process.exit(code), .signal => |sig| { var buf: [128]u8 = undefined; var w = File.stderr().writer(init.io, &buf); style.errPrefix(init.io, &w.interface) catch {}; w.interface.print("engine killed by signal {d}\n", .{sig}) catch {}; w.interface.flush() catch {}; process.exit(1); }, else => process.exit(1), } } fn regionLetters(allocator: Allocator, regions: []const Region) ![]const u8 { var out: ArrayList(u8) = .empty; for (regions) |r| try out.append(allocator, regionLetter(r)); return out.items; } fn validateReadRegion(init: Init, spec: []const u8) void { if (spec.len == 0) die(init, "invalid read region (use e.g. p:600-6ff or c)\n", .{}); const types = "PEICBAD"; const t = std.ascii.toUpper(spec[0]); if (mem.findScalar(u8, types, t) == null) { die(init, "invalid read region type '{c}' (valid: p e i c b a d)\n", .{spec[0]}); } const wants_range = t == 'P' or t == 'E'; const rest = spec[1..]; const body = if (rest.len > 0 and (rest[0] == ':' or rest[0] == '=')) rest[1..] else rest; if (wants_range) { // hex-hex with optional whitespace around '-' const trimmed = mem.trim(u8, body, " "); const dash = mem.findScalar(u8, trimmed, '-') orelse die(init, "read region '{c}' requires an address range like 600-6ff\n", .{t}); const lo = mem.trim(u8, trimmed[0..dash], " "); const hi = mem.trim(u8, trimmed[dash + 1 ..], " "); if (lo.len == 0 or hi.len == 0 or !isHex(lo) or !isHex(hi)) { die(init, "bad address range '{s}'\n", .{body}); } } else if (body.len != 0) { die(init, "read region '{c}' does not take a range\n", .{t}); } } /// Build the -G payload from a validated spec: type letter + optional range. fn formatReadRegion(spec: []const u8, buf: *[64]u8) []const u8 { const t = std.ascii.toUpper(spec[0]); buf[0] = t; var n: usize = 1; const rest = spec[1..]; const body = if (rest.len > 0 and (rest[0] == ':' or rest[0] == '=')) rest[1..] else rest; for (body) |c| { if (c == ' ') continue; buf[n] = c; n += 1; } return buf[0..n]; } fn isHex(s: []const u8) bool { for (s) |c| { switch (c) { '0'...'9', 'a'...'f', 'A'...'F' => {}, else => return false, } } return true; } test "formatReadRegion builds -G payloads" { var buf: [64]u8 = undefined; try std.testing.expectEqualStrings("P600-6ff", formatReadRegion("p:600-6ff", &buf)); try std.testing.expectEqualStrings("C", formatReadRegion("c", &buf)); try std.testing.expectEqualStrings("E1ff", formatReadRegion("E=1ff", &buf)); try std.testing.expectEqualStrings("I", formatReadRegion("i", &buf)); // spaces around the dash are tolerated and stripped try std.testing.expectEqualStrings("B600-6FF", formatReadRegion("b 600 - 6FF", &buf)); } test "isHex" { try std.testing.expect(isHex("600")); try std.testing.expect(isHex("0xFF")); try std.testing.expect(isHex("DeAdBeEf")); try std.testing.expect(!isHex("")); try std.testing.expect(!isHex("12g4")); try std.testing.expect(!isHex("-600")); } test "normalizeTool aliases" { try std.testing.expectEqualStrings("PK5", normalizeTool("pk5")); try std.testing.expectEqualStrings("PK5", normalizeTool("PICkit5")); try std.testing.expectEqualStrings("SNAP", normalizeTool("Snap")); try std.testing.expectEqualStrings("NEDBG", normalizeTool("nedbg")); // unknown families pass through untouched try std.testing.expectEqualStrings("custom", normalizeTool("custom")); } fn resolveUserRepo(allocator: Allocator, env: *process.Environ.Map) []const u8 { if (env.get("packslib.userpacksfolder")) |p| return p; if (env.get("MPLABX_PACKS_DIR")) |p| return p; const home = env.get("HOME") orelse return ""; return std.fmt.allocPrint(allocator, "{s}/.mchp_packs", .{home}) catch ""; } /// Engine root containing build/engine.jar (+ lib/, or dev layout with /// build/classes and resources). Resolution order: MPLABX_ENGINE_HOME, then /// ./java_src relative to the working directory (dev layout). fn resolveEngineHome(init: Init) []const u8 { if (init.environ_map.get("MPLABX_ENGINE_HOME")) |h| return h; var path_buf: [std.fs.max_path_bytes]u8 = undefined; const dev_jar = std.fmt.bufPrint(&path_buf, "java_src/build/engine.jar", .{}) catch unreachable; if (std.Io.Dir.cwd().access(init.io, dev_jar, .{})) |_| { return "java_src"; } else |_| {} die(init, \\cannot locate the programming engine \\ set MPLABX_ENGINE_HOME to the directory containing build/engine.jar and lib/ \\ , .{}); } fn resolveJava(init: Init, buf: *[512]u8) []const u8 { if (init.environ_map.get("MPLABX_JAVA")) |j| return j; if (init.environ_map.get("JAVA_HOME")) |jh| { if (std.fmt.bufPrint(buf, "{s}/bin/java", .{jh})) |p| { if (std.Io.Dir.cwd().access(init.io, p, .{})) |_| { return p; } else |_| {} } else |_| {} } return "java"; } fn buildClasspath(init: Init, allocator: Allocator, home: []const u8, out: *ArrayList(u8)) !void { const io = init.io; const cwd = std.Io.Dir.cwd(); // Prefer the packaged jar; fall back to the dev layout. const jar = try std.fmt.allocPrint(allocator, "{s}/build/engine.jar", .{home}); if (cwd.access(io, jar, .{})) |_| { try out.appendSlice(allocator, jar); } else |_| {} const classes = try std.fmt.allocPrint(allocator, "{s}/build/classes", .{home}); const resources = try std.fmt.allocPrint(allocator, "{s}/resources", .{home}); const dev_mode = cwd.access(io, classes, .{}) != error.FileNotFound; if (dev_mode) { if (out.items.len != 0) try out.append(allocator, ':'); try out.appendSlice(allocator, classes); try out.append(allocator, ':'); try out.appendSlice(allocator, resources); } const lib_root = try std.fmt.allocPrint(allocator, "{s}/lib", .{home}); var dir = cwd.openDir(io, lib_root, .{ .iterate = true }) catch { if (out.items.len == 0) { die(init, "no engine jars found under {s}/lib\n", .{home}); } return; }; defer dir.close(io); var walker = try dir.walk(allocator); defer walker.deinit(); while (try walker.next(io)) |entry| { if (entry.kind != .file) continue; if (!mem.endsWith(u8, entry.basename, ".jar")) continue; if (out.items.len != 0) try out.append(allocator, ':'); try out.print(allocator, "{s}/lib/{s}", .{ home, entry.path }); } }