Initial commit: 펌웨어 플래시 도구 (WPF) + 기존 PowerShell 키트
- DB720Flasher: WPF .NET 9 GUI 플래셔 - 멀티모델 프로파일(flash.json + 빌트인 DB720/DB402S/DB400S/DB400Q/DB300eM) - 포트 자동감지(JTAG 우선, 유령 포트 자동 회피), esptool v5 동봉 - flash_app.ps1 / flash_full.ps1 / list_ports.ps1: 기존 PowerShell 키트 - profiles/: 모델별 flash.json 템플릿 - firmware/flash.json: DB720 모델 설정 (서명 실펌웨어 .bin 은 repo 제외) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
261
DB720Flasher/Services/EsptoolRunner.cs
Normal file
261
DB720Flasher/Services/EsptoolRunner.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DB720Flasher;
|
||||
|
||||
public sealed class FlashStep
|
||||
{
|
||||
public required string Title { get; init; }
|
||||
public required List<string> Args { get; init; } // 서브커맨드 + 인자 (전역옵션·--after 제외)
|
||||
public long Bytes { get; init; }
|
||||
public bool TouchesBootloader { get; init; }
|
||||
}
|
||||
|
||||
public sealed class FlashRequest
|
||||
{
|
||||
public required string Port { get; init; }
|
||||
public required FlashMode Mode { get; init; }
|
||||
public required string AppPath { get; init; }
|
||||
public required string FirmwareFolder { get; init; }
|
||||
public required FlashProfile Profile { get; init; }
|
||||
public bool Force { get; init; }
|
||||
}
|
||||
|
||||
public sealed class FlashResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public string? FailedStep { get; init; }
|
||||
public int ExitCode { get; init; }
|
||||
public bool BootloaderRejected { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// esptool v5.x CLI(하이픈 문법)로 플래시한다. 칩·baud·스텁·offset·쓰기방식은 프로파일이 결정.
|
||||
/// DB720: no-stub + 영역별 개별 호출 + 마지막 hard-reset(SDM). 클래식: 스텁 + 한 번에.
|
||||
/// </summary>
|
||||
public sealed class EsptoolRunner
|
||||
{
|
||||
readonly string _esptool;
|
||||
public string EsptoolPath => _esptool;
|
||||
|
||||
public event Action<string>? Log;
|
||||
public event Action<double, string>? Progress;
|
||||
|
||||
public EsptoolRunner(string esptoolPath) => _esptool = esptoolPath;
|
||||
|
||||
public static string Locate(string baseDir, string? configured)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured)) return configured!;
|
||||
var bundled = Path.Combine(baseDir, "tools", "esptool", "esptool.exe");
|
||||
if (File.Exists(bundled)) return bundled;
|
||||
return "esptool";
|
||||
}
|
||||
|
||||
static List<string> Common(FlashProfile p, string port, string after)
|
||||
{
|
||||
var c = new List<string>
|
||||
{
|
||||
"--chip", p.Chip, "-p", port, "-b", p.Baud.ToString(CultureInfo.InvariantCulture),
|
||||
"--before", p.Before, "--after", after,
|
||||
"--connect-attempts", p.ConnectAttempts.ToString(CultureInfo.InvariantCulture),
|
||||
};
|
||||
if (p.NoStub) c.Add("--no-stub");
|
||||
return c;
|
||||
}
|
||||
|
||||
static List<string> WriteFlashArgs(FlashProfile p, IEnumerable<(string off, string file)> pairs, bool force)
|
||||
{
|
||||
var a = new List<string> { "write-flash" };
|
||||
if (p.FlashMode is not null) { a.Add("-fm"); a.Add(p.FlashMode); }
|
||||
if (p.FlashSize is not null) { a.Add("-fs"); a.Add(p.FlashSize); }
|
||||
if (p.FlashFreq is not null) { a.Add("-ff"); a.Add(p.FlashFreq); }
|
||||
if (force) a.Add("--force");
|
||||
foreach (var (off, file) in pairs) { a.Add(off); a.Add(file); }
|
||||
return a;
|
||||
}
|
||||
|
||||
static long ParseOff(string s)
|
||||
=> s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
&& long.TryParse(s.AsSpan(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var v) ? v
|
||||
: (long.TryParse(s, out var d) ? d : long.MaxValue);
|
||||
|
||||
public static List<FlashStep> BuildSteps(FlashRequest r)
|
||||
{
|
||||
var p = r.Profile;
|
||||
bool includeBoot = r.Mode == FlashMode.Full;
|
||||
bool supportToo = r.Mode != FlashMode.AppOnly;
|
||||
|
||||
var items = new List<(string off, string file)>();
|
||||
if (supportToo)
|
||||
foreach (var rg in p.SupportRegions)
|
||||
{
|
||||
if (rg.Kind == "bootloader" && !includeBoot) continue;
|
||||
var path = Path.Combine(r.FirmwareFolder, rg.File!);
|
||||
if (File.Exists(path)) items.Add((rg.Offset, path));
|
||||
}
|
||||
items.Add((p.AppOffset, r.AppPath));
|
||||
items = items.OrderBy(x => ParseOff(x.off)).ToList();
|
||||
|
||||
var bootOff = p.Bootloader?.Offset;
|
||||
var steps = new List<FlashStep>();
|
||||
|
||||
if (p.PerRegion)
|
||||
{
|
||||
foreach (var (off, file) in items)
|
||||
steps.Add(new FlashStep
|
||||
{
|
||||
Title = $"{Path.GetFileName(file)} @ {off}",
|
||||
Args = WriteFlashArgs(p, new[] { (off, file) }, r.Force),
|
||||
Bytes = SafeLen(file),
|
||||
TouchesBootloader = bootOff is not null && ParseOff(off) == ParseOff(bootOff),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
steps.Add(new FlashStep
|
||||
{
|
||||
Title = $"전체 {items.Count}개 영역 한 번에 기록",
|
||||
Args = WriteFlashArgs(p, items, r.Force),
|
||||
Bytes = items.Sum(x => SafeLen(x.file)),
|
||||
TouchesBootloader = includeBoot && p.HasBootloader,
|
||||
});
|
||||
}
|
||||
|
||||
if (r.Mode == FlashMode.AppOnly && p.AppOnlyErasesOtadata)
|
||||
{
|
||||
var erase = new List<string> { "erase-region", p.OtadataOffset, p.OtadataSize };
|
||||
if (r.Force) erase.Add("--force");
|
||||
steps.Add(new FlashStep { Title = $"otadata erase @ {p.OtadataOffset}", Args = erase, Bytes = 0 });
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
public async Task<FlashResult> RunAsync(FlashRequest r, CancellationToken ct)
|
||||
{
|
||||
var p = r.Profile;
|
||||
var steps = BuildSteps(r);
|
||||
long total = Math.Max(1, steps.Sum(s => s.Bytes));
|
||||
long done = 0;
|
||||
|
||||
Log?.Invoke($"esptool: {_esptool}");
|
||||
Log?.Invoke($"모델 {p.Name} · 포트 {r.Port} · 모드 {r.Mode}{(r.Force ? " · --force" : "")} · 단계 {steps.Count}개\n");
|
||||
|
||||
for (int i = 0; i < steps.Count; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var step = steps[i];
|
||||
bool last = i == steps.Count - 1;
|
||||
|
||||
var args = Common(p, r.Port, last ? p.After : "no-reset");
|
||||
args.AddRange(step.Args);
|
||||
|
||||
Log?.Invoke($"[{i + 1}/{steps.Count}] {step.Title}");
|
||||
Progress?.Invoke((double)done / total, step.Title);
|
||||
|
||||
long stepBase = done;
|
||||
int rc = await RunOnce(args, pct =>
|
||||
{
|
||||
double cur = (stepBase + step.Bytes * pct) / total;
|
||||
Progress?.Invoke(Math.Clamp(cur, 0, 1), step.Title);
|
||||
}, ct);
|
||||
|
||||
if (rc != 0)
|
||||
return new FlashResult { Success = false, FailedStep = step.Title, ExitCode = rc, BootloaderRejected = step.TouchesBootloader };
|
||||
|
||||
done += step.Bytes;
|
||||
Progress?.Invoke((double)done / total, step.Title);
|
||||
}
|
||||
|
||||
Progress?.Invoke(1.0, "완료");
|
||||
return new FlashResult { Success = true };
|
||||
}
|
||||
|
||||
/// <summary>플래시 전 안전 점검 — chip-id로 포트/케이블 확인.</summary>
|
||||
public async Task<bool> TestConnectionAsync(FlashProfile p, string port, CancellationToken ct)
|
||||
{
|
||||
var args = new List<string>
|
||||
{
|
||||
"--chip", p.Chip, "-p", port, "-b", p.Baud.ToString(CultureInfo.InvariantCulture),
|
||||
"--before", p.Before, "--after", "hard-reset", "--connect-attempts", "8",
|
||||
};
|
||||
if (p.NoStub) args.Add("--no-stub");
|
||||
args.Add("chip-id");
|
||||
|
||||
Log?.Invoke($"[점검] {port} ({p.Chip}) 연결 확인…");
|
||||
int rc = await RunOnce(args, _ => { }, ct);
|
||||
Log?.Invoke(rc == 0 ? "[점검] ✓ 연결 OK" : $"[점검] ✗ 연결 실패 (rc={rc})");
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
static readonly Regex AnsiRe = new(@"\x1B\[[0-9;?]*[ -/]*[@-~]", RegexOptions.Compiled);
|
||||
static readonly Regex PctRe = new(@"(\d{1,3})\s*%", RegexOptions.Compiled);
|
||||
|
||||
async Task<int> RunOnce(List<string> args, Action<double> onPct, CancellationToken ct)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = _esptool,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
};
|
||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||
psi.Environment["PYTHONIOENCODING"] = "utf-8";
|
||||
psi.Environment["PYTHONUNBUFFERED"] = "1";
|
||||
|
||||
using var proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
|
||||
|
||||
void Handle(string? raw)
|
||||
{
|
||||
if (raw is null) return;
|
||||
var line = AnsiRe.Replace(raw, "").TrimEnd();
|
||||
if (line.Length == 0) return;
|
||||
Log?.Invoke(line);
|
||||
var m = PctRe.Match(line);
|
||||
if (m.Success && int.TryParse(m.Groups[1].Value, out var pc))
|
||||
onPct(Math.Clamp(pc / 100.0, 0, 1));
|
||||
}
|
||||
|
||||
proc.OutputDataReceived += (_, e) => Handle(e.Data);
|
||||
proc.ErrorDataReceived += (_, e) => Handle(e.Data);
|
||||
|
||||
try { proc.Start(); }
|
||||
catch (Exception ex) { Log?.Invoke($"✗ esptool 실행 실패: {ex.Message}"); return -999; }
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
|
||||
using (ct.Register(() => { try { if (!proc.HasExited) proc.Kill(true); } catch { } }))
|
||||
await proc.WaitForExitAsync(ct);
|
||||
return proc.ExitCode;
|
||||
}
|
||||
|
||||
static long SafeLen(string p) { try { return new FileInfo(p).Length; } catch { return 0; } }
|
||||
|
||||
/// <summary>한 번 실행하고 출력을 통째로 받는다(버전 확인용).</summary>
|
||||
public static async Task<(int rc, string text)> Capture(string exe, params string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = exe, UseShellExecute = false,
|
||||
RedirectStandardOutput = true, RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8,
|
||||
};
|
||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||
using var p = Process.Start(psi)!;
|
||||
string o = await p.StandardOutput.ReadToEndAsync();
|
||||
string err = await p.StandardError.ReadToEndAsync();
|
||||
await p.WaitForExitAsync();
|
||||
return (p.ExitCode, AnsiRe.Replace((o + err).Trim(), ""));
|
||||
}
|
||||
catch (Exception ex) { return (-999, ex.Message); }
|
||||
}
|
||||
}
|
||||
69
DB720Flasher/Services/FirmwareCatalog.cs
Normal file
69
DB720Flasher/Services/FirmwareCatalog.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.IO;
|
||||
|
||||
namespace DB720Flasher;
|
||||
|
||||
public static class FirmwareCatalog
|
||||
{
|
||||
// 모델 무관하게 보조 파일명은 공통 — 앱 후보 = 이 4개를 뺀 .bin
|
||||
static readonly HashSet<string> Support = new(StringComparer.OrdinalIgnoreCase)
|
||||
{ "bootloader.bin", "partition-table.bin", "ota_data_initial.bin", "www_fs.bin" };
|
||||
|
||||
static string? KnownNote(string fileName) => fileName switch
|
||||
{
|
||||
"DIBD720Te_08C_04R020C.bin" => "320×64 (4단20열, 8색) · 부팅 검증됨",
|
||||
"DIBD720Ue_08B_08R040C.bin" => "640×128 (8단40열, 256색) · 미검증",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>폴더의 앱 후보(.bin 중 보조 4종 제외)를 나열.</summary>
|
||||
public static List<FirmwareItem> LoadApps(string folder)
|
||||
{
|
||||
var apps = new List<FirmwareItem>();
|
||||
if (!Directory.Exists(folder)) return apps;
|
||||
|
||||
foreach (var path in Directory.EnumerateFiles(folder, "*.bin"))
|
||||
{
|
||||
var name = Path.GetFileName(path);
|
||||
if (Support.Contains(name)) continue;
|
||||
apps.Add(new FirmwareItem
|
||||
{
|
||||
FileName = name,
|
||||
FullPath = path,
|
||||
Size = SafeLen(path),
|
||||
Note = KnownNote(name),
|
||||
});
|
||||
}
|
||||
apps.Sort((a, b) => string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase));
|
||||
return apps;
|
||||
}
|
||||
|
||||
/// <summary>선택한 모드+프로파일에 필요한 보조 파일이 폴더에 다 있는지. 없으면 빠진 목록.</summary>
|
||||
public static string? MissingFor(string folder, FlashProfile p, FlashMode mode)
|
||||
{
|
||||
if (mode == FlashMode.AppOnly) return null;
|
||||
bool includeBoot = mode == FlashMode.Full;
|
||||
var miss = new List<string>();
|
||||
foreach (var rg in p.SupportRegions)
|
||||
{
|
||||
if (rg.Kind == "bootloader" && !includeBoot) continue;
|
||||
if (!File.Exists(Path.Combine(folder, rg.File!))) miss.Add(rg.File!);
|
||||
}
|
||||
return miss.Count == 0 ? null : string.Join(", ", miss);
|
||||
}
|
||||
|
||||
/// <summary>exe 위치에서 위로 올라가며 firmware\ (bin 포함) 폴더를 찾는다.</summary>
|
||||
public static string? AutoLocate(string startDir)
|
||||
{
|
||||
var dir = new DirectoryInfo(startDir);
|
||||
for (int i = 0; i < 6 && dir is not null; i++)
|
||||
{
|
||||
var candidate = Path.Combine(dir.FullName, "firmware");
|
||||
if (Directory.Exists(candidate) && Directory.EnumerateFiles(candidate, "*.bin").Any())
|
||||
return candidate;
|
||||
dir = dir.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static long SafeLen(string p) { try { return new FileInfo(p).Length; } catch { return 0; } }
|
||||
}
|
||||
82
DB720Flasher/Services/PortScanner.cs
Normal file
82
DB720Flasher/Services/PortScanner.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.IO.Ports;
|
||||
using System.Management;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DB720Flasher;
|
||||
|
||||
/// <summary>
|
||||
/// WMI(Win32_PnPEntity)로 COM 포트를 전부 열거하고 VID/PID로 종류만 라벨링한다.
|
||||
/// 어떤 포트도 제외하지 않는다 — JTAG로도, UART(CP210x/CH340/FTDI)로도 플래시할 수 있으므로
|
||||
/// 전부 보여주고 사용자가 고른다. JTAG(303A:1001)는 맨 위로 올려 자동선택을 돕는다.
|
||||
/// </summary>
|
||||
public static partial class PortScanner
|
||||
{
|
||||
[GeneratedRegex(@"\(COM(\d+)\)")] private static partial Regex ComRe();
|
||||
[GeneratedRegex(@"VID_([0-9A-Fa-f]{4})")] private static partial Regex VidRe();
|
||||
[GeneratedRegex(@"PID_([0-9A-Fa-f]{4})")] private static partial Regex PidRe();
|
||||
|
||||
public static List<ComPortInfo> Scan()
|
||||
{
|
||||
var list = new List<ComPortInfo>();
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(
|
||||
"SELECT Name, DeviceID FROM Win32_PnPEntity WHERE Name LIKE '%(COM%'");
|
||||
foreach (ManagementBaseObject mo in searcher.Get())
|
||||
{
|
||||
var name = mo["Name"] as string ?? "";
|
||||
var devId = mo["DeviceID"] as string ?? "";
|
||||
var cm = ComRe().Match(name);
|
||||
if (!cm.Success) continue;
|
||||
|
||||
string? vid = VidRe().Match(devId) is { Success: true } v ? v.Groups[1].Value.ToUpperInvariant() : null;
|
||||
string? pid = PidRe().Match(devId) is { Success: true } p ? p.Groups[1].Value.ToUpperInvariant() : null;
|
||||
|
||||
list.Add(new ComPortInfo
|
||||
{
|
||||
PortName = $"COM{cm.Groups[1].Value}",
|
||||
Number = int.TryParse(cm.Groups[1].Value, out var n) ? n : 9999,
|
||||
FriendlyName = name,
|
||||
DeviceId = devId,
|
||||
Vid = vid,
|
||||
Pid = pid,
|
||||
Kind = KindOf(vid, pid),
|
||||
});
|
||||
mo.Dispose();
|
||||
}
|
||||
}
|
||||
catch { /* WMI 실패 시 빈 목록 */ }
|
||||
|
||||
// JTAG는 맨 위에, 그 중 '최신(번호 높은)' 것을 먼저 — 보드 리셋으로 번호가 올라가면
|
||||
// 옛 번호는 유령 포트가 되므로 높은 번호가 살아있을 확률이 높다.
|
||||
var jtag = list.Where(p => p.IsJtag).OrderByDescending(p => p.Number);
|
||||
var rest = list.Where(p => !p.IsJtag).OrderBy(p => p.Number);
|
||||
return jtag.Concat(rest).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 포트를 실제로 열 수 있는지 빠르게 확인(유령 포트 판별).
|
||||
/// DTR/RTS를 건드리지 않아 보드를 리셋하지 않는다.
|
||||
/// </summary>
|
||||
public static bool CanOpen(string portName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sp = new SerialPort(portName) { DtrEnable = false, RtsEnable = false };
|
||||
sp.Open();
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
static string KindOf(string? vid, string? pid) => (vid, pid) switch
|
||||
{
|
||||
("303A", "1001") => "USB-JTAG (내장)",
|
||||
("10C4", _) => "CP210x",
|
||||
("1A86", "7523") => "CH340",
|
||||
("1A86", _) => "CH34x/CH9102",
|
||||
("0403", _) => "FTDI",
|
||||
("067B", _) => "PL2303",
|
||||
_ => "USB 직렬",
|
||||
};
|
||||
}
|
||||
103
DB720Flasher/Services/ProfileStore.cs
Normal file
103
DB720Flasher/Services/ProfileStore.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DB720Flasher;
|
||||
|
||||
/// <summary>빌트인 모델 프로파일 + 폴더 flash.json 로더 + 앱 파일명 자동매칭.</summary>
|
||||
public static class ProfileStore
|
||||
{
|
||||
static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
|
||||
static FwRegion Boot(string off) => new() { File = "bootloader.bin", Offset = off, Kind = "bootloader" };
|
||||
static FwRegion Part() => new() { File = "partition-table.bin", Offset = "0x10000", Kind = "support" };
|
||||
static FwRegion Ota() => new() { File = "ota_data_initial.bin", Offset = "0x11000", Kind = "support" };
|
||||
static FwRegion Www() => new() { File = "www_fs.bin", Offset = "0x420000", Kind = "support" };
|
||||
static FwRegion AppAt(string off = "0x20000") => new() { App = true, Offset = off, Kind = "app" };
|
||||
|
||||
public static List<FlashProfile> BuiltIns() => new()
|
||||
{
|
||||
new FlashProfile
|
||||
{
|
||||
Name = "DB720 (ESP32-P4)", Chip = "esp32p4", Baud = 115200, NoStub = true, PerRegion = true,
|
||||
ConnectAttempts = 30, FlashSize = "16MB", FlashMode = "dio", FlashFreq = "80m",
|
||||
AppOnlyErasesOtadata = true,
|
||||
Regions = { Boot("0x2000"), Part(), Ota(), Www(), AppAt() },
|
||||
FlashPortHint = "303A:1001", ConsolePortHint = "10C4:EA60",
|
||||
DownloadModeHint = "USB-JTAG 케이블 연결. 포트가 안 잡히면 케이블만 다시 꽂으면 됩니다(자동 감지).",
|
||||
Notes = "secure_release · db250513 서명. 영역별 개별 호출(SDM). 부트로더(0x2000)는 이미 구워진 보드면 거부될 수 있음(정상 → 표준/앱만 사용).",
|
||||
},
|
||||
new FlashProfile
|
||||
{
|
||||
Name = "DB402S (ESP32-S3)", Chip = "esp32s3", Baud = 921600,
|
||||
Regions = { Boot("0x0000"), Part(), Ota(), AppAt() },
|
||||
ConsolePortHint = "10C4:EA60",
|
||||
DownloadModeHint = "TTL(COM2) 연결. SW1 누른 상태에서 SW2 눌렀다 떼고 SW1 떼면 다운로드 모드 진입.",
|
||||
Notes = "esp32-s3. 부트로더 offset 0x0000(중요). 스텁 921600 · 한 번에 기록. secure 보드는 부트로더(<0x8000) 거부 → --force 또는 앱만.",
|
||||
},
|
||||
new FlashProfile
|
||||
{
|
||||
Name = "DB400S (ESP32)", Chip = "esp32", Baud = 921600,
|
||||
Regions = { Boot("0x1000"), Part(), Ota(), AppAt() },
|
||||
ConsolePortHint = "10C4:EA60",
|
||||
DownloadModeHint = "TTL(COM2) 연결. SW1 누른 상태에서 SW2 눌렀다 떼고 SW1 떼면 다운로드 모드 진입.",
|
||||
Notes = "esp32 클래식. 부트로더 0x1000. 스텁 921600 · 한 번에. secure 보드는 부트로더(<0x8000) 거부 → --force 또는 앱만.",
|
||||
},
|
||||
new FlashProfile
|
||||
{
|
||||
Name = "DB400Q (ESP32)", Chip = "esp32", Baud = 921600,
|
||||
Regions = { Boot("0x1000"), Part(), Ota(), AppAt() },
|
||||
ConsolePortHint = "10C4:EA60",
|
||||
DownloadModeHint = "TTL(COM2) 연결. SW1 누른 상태에서 SW2 눌렀다 떼고 SW1 떼면 다운로드 모드 진입.",
|
||||
Notes = "esp32 클래식. DB400S와 동일 방식.",
|
||||
},
|
||||
new FlashProfile
|
||||
{
|
||||
Name = "DB300eM (ESP32)", Chip = "esp32", Baud = 921600,
|
||||
Regions = { Boot("0x1000"), Part(), Ota(), AppAt() },
|
||||
ConsolePortHint = "10C4:EA60",
|
||||
DownloadModeHint = "TTL(COM2) 연결. JP1 점퍼핀 단락 후 전원 재인가하면 다운로드 모드 진입.",
|
||||
Notes = "esp32 클래식. JP1 점퍼로 다운로드 진입. web OTA(IP 입력)도 가능.",
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>폴더의 flash.json 을 읽는다(없으면 null).</summary>
|
||||
public static FlashProfile? LoadFromFolder(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = Path.Combine(folder, "flash.json");
|
||||
if (!File.Exists(p)) return null;
|
||||
var prof = JsonSerializer.Deserialize<FlashProfile>(File.ReadAllText(p), JsonOpts);
|
||||
if (prof is not null) prof.FromFile = true;
|
||||
return prof;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>앱 bin 파일명(DIBD720…, DIBD402S… 등)으로 빌트인 프로파일을 추정.</summary>
|
||||
public static FlashProfile? MatchByApp(IEnumerable<string> appFileNames, List<FlashProfile> profiles)
|
||||
{
|
||||
// 더 구체적인 토큰을 먼저 (402S 가 400 보다, 400S 가 400 보다 우선)
|
||||
(string token, string profileTag)[] map =
|
||||
{
|
||||
("402S", "DB402S"), ("400S", "DB400S"), ("400Q", "DB400Q"),
|
||||
("300", "DB300"), ("720", "DB720"),
|
||||
};
|
||||
foreach (var name in appFileNames)
|
||||
{
|
||||
var up = name.ToUpperInvariant();
|
||||
foreach (var (token, tag) in map)
|
||||
if (up.Contains(token))
|
||||
{
|
||||
var hit = profiles.FirstOrDefault(p => p.Name.ToUpperInvariant().Contains(tag));
|
||||
if (hit is not null) return hit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user