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:
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 텍스트는 자동 정규화(repo엔 LF, 체크아웃 시 OS에 맞게)
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# 바이너리 — 줄바꿈 변환 금지
|
||||||
|
*.exe binary
|
||||||
|
*.bin binary
|
||||||
|
*.png binary
|
||||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# ===== .NET 빌드 출력 =====
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
|
||||||
|
# ===== 배포본 (repo 말고 Gitea Releases로) =====
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# ===== 서명 실펌웨어 — repo에 넣지 않음(보안·분리). flash.json 은 포함 =====
|
||||||
|
firmware/*.bin
|
||||||
|
|
||||||
|
# ===== 임시 캡처/찌꺼기 =====
|
||||||
|
*_shot.png
|
||||||
|
_distcheck.png
|
||||||
12
DB720Flasher/App.xaml
Normal file
12
DB720Flasher/App.xaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<Application x:Class="DB720Flasher.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Themes/Controls.xaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
7
DB720Flasher/App.xaml.cs
Normal file
7
DB720Flasher/App.xaml.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace DB720Flasher;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
26
DB720Flasher/DB720Flasher.csproj
Normal file
26
DB720Flasher/DB720Flasher.csproj
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<RootNamespace>DB720Flasher</RootNamespace>
|
||||||
|
<AssemblyName>DB720Flasher</AssemblyName>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Management" Version="9.0.0" />
|
||||||
|
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- 동봉 자산: firmware\ 와 tools\esptool\ 를 빌드 출력으로 복사 -->
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="tools\**\*" CopyToOutputDirectory="PreserveNewest" LinkBase="tools\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
179
DB720Flasher/MainWindow.xaml
Normal file
179
DB720Flasher/MainWindow.xaml
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<Window x:Class="DB720Flasher.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="펌웨어 플래시 도구" Height="660" Width="960"
|
||||||
|
MinHeight="560" MinWidth="860"
|
||||||
|
Background="{StaticResource Bg}" FontFamily="Segoe UI"
|
||||||
|
TextOptions.TextFormattingMode="Display">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Surface}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="Padding" Value="16" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="Hint" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextSubtle}" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="FieldLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextSubtle}" />
|
||||||
|
<Setter Property="FontSize" Value="11" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="Margin" Value="2,0,0,4" />
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<Border Grid.Row="0" Background="{StaticResource SurfaceAlt}" Padding="18,12">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="펌웨어 플래시 도구" Foreground="{StaticResource Text}"
|
||||||
|
FontSize="18" FontWeight="Bold" VerticalAlignment="Center" />
|
||||||
|
<TextBlock x:Name="txtEsptool" Text="esptool 확인 중…" Foreground="{StaticResource TextSubtle}"
|
||||||
|
FontSize="11" VerticalAlignment="Center" HorizontalAlignment="Right"
|
||||||
|
TextTrimming="CharacterEllipsis" MaxWidth="460" />
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 본문: 좌(설정) / 우(실행+로그) -->
|
||||||
|
<Grid Grid.Row="1" Margin="16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="350" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- ===== 좌: 설정 ===== -->
|
||||||
|
<Border Grid.Column="0" Style="{StaticResource Card}">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}" Text="펌웨어 폴더" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox x:Name="txtFwFolder" Grid.Column="0" IsReadOnly="True" Text="(탐색 중…)" />
|
||||||
|
<Button x:Name="btnBrowse" Grid.Column="1" Content="폴더…" Margin="6,0,0,0" Click="OnBrowseClick" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}" Text="앱" Margin="2,12,0,4" />
|
||||||
|
<ComboBox x:Name="cboApps" />
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}" Text="모델" Margin="2,12,0,4" />
|
||||||
|
<ComboBox x:Name="cboModel" />
|
||||||
|
<TextBlock x:Name="txtProfile" Style="{StaticResource Hint}" FontSize="11" Margin="2,4,0,0" />
|
||||||
|
|
||||||
|
<Grid Margin="0,12,0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}" Text="포트" VerticalAlignment="Bottom" Margin="2,0,0,0" />
|
||||||
|
<Button x:Name="btnRefresh" Grid.Column="1" Content="↻ 새로고침" Click="OnRefreshClick"
|
||||||
|
MinHeight="24" Padding="8,0" FontSize="11" />
|
||||||
|
</Grid>
|
||||||
|
<ComboBox x:Name="cboPorts" />
|
||||||
|
<TextBlock x:Name="txtPortStatus" Margin="2,6,0,0" FontSize="12" Foreground="{StaticResource Warn}"
|
||||||
|
TextWrapping="Wrap" Text="포트 검색 중…" />
|
||||||
|
<TextBlock x:Name="txtConsoleWarn" Style="{StaticResource Hint}" Margin="2,4,0,0"
|
||||||
|
Foreground="{StaticResource Warn}" Visibility="Collapsed" />
|
||||||
|
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}" Text="플래시 모드" Margin="2,14,0,4" />
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<RadioButton x:Name="rbAppOnly" GroupName="mode" Content="앱만" IsChecked="True"
|
||||||
|
Checked="OnModeChanged" Margin="0,0,16,0" />
|
||||||
|
<RadioButton x:Name="rbStandard" GroupName="mode" Content="표준" Checked="OnModeChanged" Margin="0,0,16,0" />
|
||||||
|
<RadioButton x:Name="rbFull" GroupName="mode" Content="전체" Checked="OnModeChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Style="{StaticResource Hint}" FontSize="11" Margin="2,6,0,0"
|
||||||
|
Text="앱만=부트로더 안 건드림(안전) · 표준=부트로더 빼고 전부 · 전체=부트로더까지(eFuse 위험)" />
|
||||||
|
<CheckBox x:Name="chkForce" Margin="0,10,0,0" FontSize="12"
|
||||||
|
Content="--force (secure 보드 부트로더 거부 시)" />
|
||||||
|
|
||||||
|
<!-- eFuse 경고(전체 선택 시) — 컴팩트, 전체 설명은 툴팁 -->
|
||||||
|
<Border x:Name="pnlEfuse" Visibility="Collapsed" Margin="0,10,0,0"
|
||||||
|
Background="{StaticResource DangerDim}" BorderBrush="{StaticResource Danger}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="10">
|
||||||
|
<Border.ToolTip>
|
||||||
|
<ToolTip MaxWidth="360">
|
||||||
|
<TextBlock TextWrapping="Wrap" Text="전체 플래시는 부트로더를 다시 씁니다. secure boot eFuse가 아직 안 구워진 보드는 첫 부팅에 영구로 구워집니다(되돌릴 수 없음). 이미 구워진 보드는 부트로더(0x8000 아래) 쓰기를 거부하는데(정상) — 그땐 '표준'이나 '앱만'으로. 부트로더를 꼭 다시 써야 하면 --force가 필요할 수 있습니다." />
|
||||||
|
</ToolTip>
|
||||||
|
</Border.ToolTip>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Foreground="{StaticResource Danger}" FontWeight="Bold" FontSize="12" TextWrapping="Wrap"
|
||||||
|
Text="⚠ 부트로더 재작성 · eFuse 영구 굽힘 위험(비가역)" />
|
||||||
|
<CheckBox x:Name="chkEfuseAck" Margin="0,8,0,0" FontSize="12" Foreground="{StaticResource Danger}"
|
||||||
|
Checked="OnModeChanged" Unchecked="OnModeChanged"
|
||||||
|
Content="위험을 이해했습니다(첫 굽기/복구)" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- ===== 우: 실행 + 로그 ===== -->
|
||||||
|
<Border Grid.Column="1" Style="{StaticResource Card}" Margin="12,0,0,0">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 액션 행 -->
|
||||||
|
<Grid Grid.Row="0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button x:Name="btnFlash" Grid.Column="0" Style="{StaticResource PrimaryButton}"
|
||||||
|
Height="46" FontSize="15" Content="플래시 시작" Click="OnFlashClick" />
|
||||||
|
<Button x:Name="btnTest" Grid.Column="1" Content="포트 점검" Margin="8,0,0,0" Height="46" Click="OnTestClick" />
|
||||||
|
<Button x:Name="btnStop" Grid.Column="2" Content="중지" Margin="8,0,0,0" Height="46" Width="80"
|
||||||
|
IsEnabled="False" Click="OnStopClick" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 진행바 -->
|
||||||
|
<ProgressBar x:Name="barProgress" Grid.Row="1" Height="10" Margin="0,14,0,0" Minimum="0" Maximum="1" />
|
||||||
|
<Grid Grid.Row="2" Margin="0,6,0,0">
|
||||||
|
<TextBlock x:Name="txtStep" Style="{StaticResource Hint}" HorizontalAlignment="Left" />
|
||||||
|
<TextBlock x:Name="txtPercent" Style="{StaticResource Hint}" HorizontalAlignment="Right" Text="0%" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- 다운로드 모드 안내 strip -->
|
||||||
|
<Border Grid.Row="3" Margin="0,12,0,0" Background="{StaticResource SurfaceAlt}" CornerRadius="6" Padding="10,7">
|
||||||
|
<TextBlock x:Name="txtDlMode" Style="{StaticResource Hint}" Foreground="{StaticResource Text}" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 로그(채움) -->
|
||||||
|
<TextBox x:Name="txtLog" Grid.Row="4" Margin="0,12,0,0" IsReadOnly="True"
|
||||||
|
FontFamily="Consolas" FontSize="12" Background="#FF15161A"
|
||||||
|
Foreground="#FFCFD3D8" BorderBrush="{StaticResource Border}"
|
||||||
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||||
|
TextWrapping="NoWrap" VerticalContentAlignment="Top" />
|
||||||
|
|
||||||
|
<!-- 결과 배너 -->
|
||||||
|
<Border x:Name="pnlResult" Grid.Row="5" Visibility="Collapsed" Margin="0,12,0,0"
|
||||||
|
CornerRadius="8" Padding="14" BorderThickness="1">
|
||||||
|
<TextBlock x:Name="txtResult" TextWrapping="Wrap" FontSize="13" />
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
476
DB720Flasher/MainWindow.xaml.cs
Normal file
476
DB720Flasher/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace DB720Flasher;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
string _baseDir = "";
|
||||||
|
string _esptoolPath = "esptool";
|
||||||
|
string? _fwFolder;
|
||||||
|
List<FirmwareItem> _apps = new();
|
||||||
|
List<FlashProfile> _builtins = new();
|
||||||
|
List<FlashProfile> _models = new();
|
||||||
|
FlashProfile? _profile;
|
||||||
|
List<ComPortInfo> _ports = new();
|
||||||
|
CancellationTokenSource? _cts;
|
||||||
|
bool _busy, _ready, _refreshing;
|
||||||
|
readonly DispatcherTimer _debounce;
|
||||||
|
readonly DispatcherTimer _watch;
|
||||||
|
string[] _portSnapshot = Array.Empty<string>();
|
||||||
|
string? _shotPath;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_debounce = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(450) };
|
||||||
|
_debounce.Tick += async (_, __) => { _debounce.Stop(); if (!_busy) await RefreshPortsAsync(); };
|
||||||
|
|
||||||
|
_watch = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||||
|
_watch.Tick += OnWatchTick;
|
||||||
|
|
||||||
|
var args = Environment.GetCommandLineArgs();
|
||||||
|
int si = Array.IndexOf(args, "--shot");
|
||||||
|
if (si >= 0 && si + 1 < args.Length) _shotPath = args[si + 1];
|
||||||
|
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
async void OnLoaded(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_baseDir = AppContext.BaseDirectory;
|
||||||
|
_esptoolPath = EsptoolRunner.Locate(_baseDir, null);
|
||||||
|
_builtins = ProfileStore.BuiltIns();
|
||||||
|
|
||||||
|
cboModel.SelectionChanged += (_, __) => OnModelChanged();
|
||||||
|
cboApps.SelectionChanged += (_, __) => UpdateFlashEnabled();
|
||||||
|
cboPorts.SelectionChanged += (_, __) => { UpdatePortHint(); UpdateConsoleWarn(); UpdateFlashEnabled(); };
|
||||||
|
|
||||||
|
var fwFolder = FirmwareCatalog.AutoLocate(_baseDir) ?? FirmwareCatalog.AutoLocate(Directory.GetCurrentDirectory());
|
||||||
|
if (fwFolder is not null) LoadFirmware(fwFolder);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
txtFwFolder.Text = "(firmware 폴더를 찾지 못함 — '폴더…' 로 지정)";
|
||||||
|
cboModel.ItemsSource = _builtins;
|
||||||
|
cboModel.SelectedItem = _builtins.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ready = true;
|
||||||
|
OnModeChanged(this, new RoutedEventArgs());
|
||||||
|
await RefreshPortsAsync();
|
||||||
|
_ = ProbeEsptoolAsync();
|
||||||
|
|
||||||
|
_portSnapshot = CurrentPortNames();
|
||||||
|
_watch.Start();
|
||||||
|
|
||||||
|
if (_shotPath is not null) await CaptureAndExitAsync(_shotPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSourceInitialized(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnSourceInitialized(e);
|
||||||
|
(PresentationSource.FromVisual(this) as HwndSource)?.AddHook(WndProc);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int WM_DEVICECHANGE = 0x0219;
|
||||||
|
IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
if (msg == WM_DEVICECHANGE) { _debounce.Stop(); _debounce.Start(); }
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
async void OnWatchTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var now = CurrentPortNames();
|
||||||
|
if (now.SequenceEqual(_portSnapshot)) return;
|
||||||
|
_portSnapshot = now;
|
||||||
|
if (!_busy) await RefreshPortsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
static string[] CurrentPortNames()
|
||||||
|
{
|
||||||
|
try { var n = System.IO.Ports.SerialPort.GetPortNames(); Array.Sort(n); return n; }
|
||||||
|
catch { return Array.Empty<string>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 펌웨어 폴더 + 모델 =====
|
||||||
|
void LoadFirmware(string folder)
|
||||||
|
{
|
||||||
|
_fwFolder = folder;
|
||||||
|
txtFwFolder.Text = folder;
|
||||||
|
_apps = FirmwareCatalog.LoadApps(folder);
|
||||||
|
cboApps.ItemsSource = _apps;
|
||||||
|
cboApps.SelectedItem = _apps.FirstOrDefault();
|
||||||
|
|
||||||
|
var models = new List<FlashProfile>(_builtins);
|
||||||
|
FlashProfile? pick;
|
||||||
|
var fileProf = ProfileStore.LoadFromFolder(folder);
|
||||||
|
if (fileProf is not null)
|
||||||
|
{
|
||||||
|
fileProf.Name = $"{fileProf.Name} (폴더 flash.json)";
|
||||||
|
models.Insert(0, fileProf);
|
||||||
|
pick = fileProf;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pick = ProfileStore.MatchByApp(_apps.Select(a => a.FileName), _builtins);
|
||||||
|
}
|
||||||
|
_models = models;
|
||||||
|
cboModel.ItemsSource = _models;
|
||||||
|
cboModel.SelectedItem = pick ?? _models.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnModelChanged()
|
||||||
|
{
|
||||||
|
_profile = cboModel.SelectedItem as FlashProfile ?? _builtins.FirstOrDefault();
|
||||||
|
if (_profile is null) return;
|
||||||
|
txtProfile.Text = "설정: " + _profile.Summary + (_profile.FromFile ? " · 폴더 flash.json" : " · 빌트인");
|
||||||
|
txtDlMode.Text = _profile.DownloadModeHint ?? "(안내 없음)";
|
||||||
|
SelectPreferredPort((cboPorts.SelectedItem as ComPortInfo)?.PortName);
|
||||||
|
UpdatePortHint();
|
||||||
|
UpdateConsoleWarn();
|
||||||
|
UpdateFlashEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnBrowseClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new OpenFolderDialog { Title = "펌웨어 폴더 선택" };
|
||||||
|
if (_fwFolder is not null && Directory.Exists(_fwFolder)) dlg.InitialDirectory = _fwFolder;
|
||||||
|
if (dlg.ShowDialog(this) == true) LoadFirmware(dlg.FolderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 포트 =====
|
||||||
|
async Task RefreshPortsAsync()
|
||||||
|
{
|
||||||
|
if (_refreshing) return;
|
||||||
|
_refreshing = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? prev = (cboPorts.SelectedItem as ComPortInfo)?.PortName;
|
||||||
|
List<ComPortInfo> ports;
|
||||||
|
try { ports = await Task.Run(PortScanner.Scan); } catch { ports = new(); }
|
||||||
|
_ports = ports;
|
||||||
|
cboPorts.ItemsSource = ports; // 전부 표시(제외 없음)
|
||||||
|
SelectPreferredPort(prev);
|
||||||
|
UpdatePortHint();
|
||||||
|
UpdateConsoleWarn();
|
||||||
|
UpdateFlashEnabled();
|
||||||
|
}
|
||||||
|
finally { _refreshing = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectPreferredPort(string? prev)
|
||||||
|
{
|
||||||
|
var list = (cboPorts.ItemsSource as IEnumerable<ComPortInfo>)?.ToList() ?? new();
|
||||||
|
// JTAG/권장 포트 중 가장 높은 번호(재열거된 최신)를 우선 선택 — 유령(옛 번호) 회피
|
||||||
|
var pref = list
|
||||||
|
.Where(p => p.IsJtag || (_profile is not null && p.Matches(_profile.FlashPortHint)))
|
||||||
|
.OrderByDescending(p => p.Number)
|
||||||
|
.FirstOrDefault();
|
||||||
|
cboPorts.SelectedItem = pref
|
||||||
|
?? list.FirstOrDefault(p => p.PortName == prev)
|
||||||
|
?? list.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>선택 포트가 유령이면(열기 실패) 살아있는 JTAG/권장 포트로 자동 전환해 돌려준다.</summary>
|
||||||
|
async Task<ComPortInfo?> ResolveLivePortAsync(ComPortInfo sel)
|
||||||
|
{
|
||||||
|
if (await Task.Run(() => PortScanner.CanOpen(sel.PortName))) return sel;
|
||||||
|
|
||||||
|
AppendLog($"[포트] {sel.PortName} 를 열 수 없음 — 보드 리셋으로 번호가 바뀐 듯. 살아있는 포트 탐색…");
|
||||||
|
var ports = await Task.Run(PortScanner.Scan);
|
||||||
|
foreach (var p in ports
|
||||||
|
.Where(p => p.IsJtag || (_profile is not null && p.Matches(_profile.FlashPortHint)))
|
||||||
|
.OrderByDescending(p => p.Number))
|
||||||
|
{
|
||||||
|
if (string.Equals(p.PortName, sel.PortName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
if (await Task.Run(() => PortScanner.CanOpen(p.PortName)))
|
||||||
|
{
|
||||||
|
AppendLog($"[포트] → {p.PortName} 로 자동 전환");
|
||||||
|
_ports = ports;
|
||||||
|
cboPorts.ItemsSource = ports;
|
||||||
|
cboPorts.SelectedItem = ports.FirstOrDefault(x => x.PortName == p.PortName);
|
||||||
|
return cboPorts.SelectedItem as ComPortInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdatePortHint()
|
||||||
|
{
|
||||||
|
var sel = cboPorts.SelectedItem as ComPortInfo;
|
||||||
|
var jtags = _ports.Where(p => p.IsJtag).ToList();
|
||||||
|
string multi = jtags.Count > 1
|
||||||
|
? $" · JTAG {jtags.Count}개(리셋으로 번호 바뀜) — 안 되면 자동으로 다른 번호 시도"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (sel is null)
|
||||||
|
{
|
||||||
|
txtPortStatus.Foreground = GetBrush("Danger");
|
||||||
|
txtPortStatus.Text = "포트 없음 — 케이블을 연결하세요 (자동 감지 대기 중)";
|
||||||
|
}
|
||||||
|
else if (sel.IsJtag)
|
||||||
|
{
|
||||||
|
txtPortStatus.Foreground = GetBrush("Success");
|
||||||
|
txtPortStatus.Text = $"✓ {sel.PortName} 선택됨 — USB-JTAG (303A:1001) 자동 감지{multi}";
|
||||||
|
}
|
||||||
|
else if (_profile is not null && sel.Matches(_profile.FlashPortHint))
|
||||||
|
{
|
||||||
|
txtPortStatus.Foreground = GetBrush("Success");
|
||||||
|
txtPortStatus.Text = $"✓ {sel.PortName} 선택됨 — {sel.Kind} (모델 권장 포트){multi}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var liveJtag = jtags.OrderByDescending(p => p.Number).FirstOrDefault();
|
||||||
|
txtPortStatus.Foreground = GetBrush("Warn");
|
||||||
|
txtPortStatus.Text = $"{sel.PortName} 선택됨 — {sel.Kind}. 모델에 맞는 포트인지 확인하세요"
|
||||||
|
+ (liveJtag is not null ? $" (JTAG {liveJtag.PortName}도 있음)" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateConsoleWarn()
|
||||||
|
{
|
||||||
|
var sel = cboPorts.SelectedItem as ComPortInfo;
|
||||||
|
// FlashPortHint 가 있는(=JTAG로 굽는) 모델에서만, 콘솔 포트를 고르면 경고. 막지는 않음.
|
||||||
|
if (_profile?.FlashPortHint is not null && _profile.ConsolePortHint is not null
|
||||||
|
&& sel is not null && sel.Matches(_profile.ConsolePortHint))
|
||||||
|
{
|
||||||
|
txtConsoleWarn.Visibility = Visibility.Visible;
|
||||||
|
txtConsoleWarn.Text = $"※ {sel.PortName}({sel.Kind})는 이 모델에선 디버그 콘솔일 수 있습니다. 플래시는 보통 JTAG로 합니다 — 의도한 거면 그대로 진행하세요.";
|
||||||
|
}
|
||||||
|
else txtConsoleWarn.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async void OnRefreshClick(object sender, RoutedEventArgs e) => await RefreshPortsAsync();
|
||||||
|
|
||||||
|
// ===== 모드 =====
|
||||||
|
FlashMode CurrentMode() =>
|
||||||
|
rbFull.IsChecked == true ? FlashMode.Full
|
||||||
|
: rbStandard.IsChecked == true ? FlashMode.Standard
|
||||||
|
: FlashMode.AppOnly;
|
||||||
|
|
||||||
|
void OnModeChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_ready) return;
|
||||||
|
pnlEfuse.Visibility = CurrentMode() == FlashMode.Full ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
UpdateFlashEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateFlashEnabled()
|
||||||
|
{
|
||||||
|
if (!_ready) return;
|
||||||
|
bool ok = !_busy
|
||||||
|
&& cboPorts.SelectedItem is ComPortInfo
|
||||||
|
&& cboApps.SelectedItem is FirmwareItem
|
||||||
|
&& _profile is not null
|
||||||
|
&& _fwFolder is not null
|
||||||
|
&& FirmwareCatalog.MissingFor(_fwFolder, _profile, CurrentMode()) is null;
|
||||||
|
if (CurrentMode() == FlashMode.Full && chkEfuseAck.IsChecked != true) ok = false;
|
||||||
|
btnFlash.IsEnabled = ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 실행 =====
|
||||||
|
async void OnFlashClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_profile is null || _fwFolder is null) return;
|
||||||
|
if (cboPorts.SelectedItem is not ComPortInfo port) { Info("포트를 선택하세요."); return; }
|
||||||
|
if (cboApps.SelectedItem is not FirmwareItem app) { Info("올릴 앱을 선택하세요."); return; }
|
||||||
|
var mode = CurrentMode();
|
||||||
|
if (FirmwareCatalog.MissingFor(_fwFolder, _profile, mode) is string miss) { Info($"이 모드에 필요한 파일이 없습니다: {miss}"); return; }
|
||||||
|
|
||||||
|
// 로그/진행 초기화 후 '살아있는' 포트 확인 — 유령 COM(번호 바뀜) 자동 회피
|
||||||
|
HideResult();
|
||||||
|
txtLog.Clear();
|
||||||
|
barProgress.Value = 0; txtPercent.Text = "0%"; txtStep.Text = "";
|
||||||
|
|
||||||
|
var live = await ResolveLivePortAsync(port);
|
||||||
|
if (live is null)
|
||||||
|
{
|
||||||
|
ShowResultBox(false, "포트를 열 수 없습니다. 보드 리셋으로 COM 번호가 바뀌었거나(유령 포트) 다른 프로그램이 포트를 잡고 있을 수 있습니다 — USB 케이블을 다시 꽂고 '↻ 새로고침' 후 다시 시도하세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
port = live;
|
||||||
|
|
||||||
|
string modeName = mode switch { FlashMode.AppOnly => "앱만", FlashMode.Standard => "표준", _ => "전체" };
|
||||||
|
string msg = $"모델: {_profile.Name}\n포트: {port.PortName} ({port.Kind})\n앱: {app.FileName}\n모드: {modeName} 플래시{(chkForce.IsChecked == true ? " (--force)" : "")}";
|
||||||
|
if (mode == FlashMode.Full)
|
||||||
|
msg += "\n\n★ 전체 플래시는 부트로더를 다시 씁니다. eFuse 미굽힘 보드는 첫 부팅에 영구로 구워집니다(비가역).";
|
||||||
|
if (MessageBox.Show(this, msg + "\n\n진행할까요?", "플래시 확인",
|
||||||
|
MessageBoxButton.YesNo,
|
||||||
|
mode == FlashMode.Full ? MessageBoxImage.Warning : MessageBoxImage.Question) != MessageBoxResult.Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var req = new FlashRequest
|
||||||
|
{
|
||||||
|
Port = port.PortName,
|
||||||
|
Mode = mode,
|
||||||
|
AppPath = app.FullPath,
|
||||||
|
FirmwareFolder = _fwFolder,
|
||||||
|
Profile = _profile,
|
||||||
|
Force = chkForce.IsChecked == true,
|
||||||
|
};
|
||||||
|
|
||||||
|
SetBusy(true);
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var runner = MakeRunner();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await runner.RunAsync(req, _cts.Token);
|
||||||
|
ShowResult(res);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
AppendLog("\n■ 중지됨");
|
||||||
|
ShowResultBox(false, "중지됨 — 플래시가 중간에 멈췄습니다. 보드 상태가 불완전할 수 있으니 다시 하세요.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog("\n✗ 오류: " + ex.Message);
|
||||||
|
ShowResultBox(false, "오류: " + ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_cts?.Dispose(); _cts = null;
|
||||||
|
SetBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async void OnTestClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_profile is null) { Info("모델을 선택하세요."); return; }
|
||||||
|
if (cboPorts.SelectedItem is not ComPortInfo port) { Info("점검할 포트를 선택하세요."); return; }
|
||||||
|
HideResult();
|
||||||
|
txtLog.Clear();
|
||||||
|
var live = await ResolveLivePortAsync(port);
|
||||||
|
if (live is null) { ShowResultBox(false, "포트를 열 수 없습니다 — USB 케이블을 다시 꽂고 '↻ 새로고침' 후 다시 시도하세요."); return; }
|
||||||
|
port = live;
|
||||||
|
SetBusy(true);
|
||||||
|
txtStep.Text = "연결 점검";
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var runner = MakeRunner();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool ok = await runner.TestConnectionAsync(_profile, port.PortName, _cts.Token);
|
||||||
|
ShowResultBox(ok, ok
|
||||||
|
? $"✓ {port.PortName} 연결 OK — 플래시 준비됨."
|
||||||
|
: $"✗ {port.PortName} 연결 실패 — 케이블/포트·다운로드 모드 진입을 확인하세요. 다른 프로그램이 포트를 잡고 있을 수 있습니다.");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { AppendLog("\n■ 중지됨"); }
|
||||||
|
catch (Exception ex) { ShowResultBox(false, "오류: " + ex.Message); }
|
||||||
|
finally { _cts?.Dispose(); _cts = null; SetBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnStopClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
AppendLog("\n■ 중지 요청…");
|
||||||
|
_cts?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
EsptoolRunner MakeRunner()
|
||||||
|
{
|
||||||
|
var runner = new EsptoolRunner(_esptoolPath);
|
||||||
|
runner.Log += s => Dispatcher.BeginInvoke(() => AppendLog(s));
|
||||||
|
runner.Progress += (p, t) => Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
barProgress.Value = p;
|
||||||
|
txtPercent.Text = $"{p * 100:0}%";
|
||||||
|
txtStep.Text = t;
|
||||||
|
});
|
||||||
|
return runner;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 상태 표시 =====
|
||||||
|
void SetBusy(bool busy)
|
||||||
|
{
|
||||||
|
_busy = busy;
|
||||||
|
foreach (Control c in new Control[]
|
||||||
|
{ cboModel, cboApps, cboPorts, btnRefresh, btnTest, btnBrowse, rbAppOnly, rbStandard, rbFull, chkForce, chkEfuseAck })
|
||||||
|
c.IsEnabled = !busy;
|
||||||
|
btnStop.IsEnabled = busy;
|
||||||
|
if (busy) btnFlash.IsEnabled = false; else UpdateFlashEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowResult(FlashResult res)
|
||||||
|
{
|
||||||
|
if (res.Success)
|
||||||
|
{
|
||||||
|
string tip = _profile is { NoStub: true }
|
||||||
|
? "\n• USB-JTAG 케이블을 뽑고 전원을 재인가하면 확실히 정상 부팅합니다."
|
||||||
|
: "";
|
||||||
|
ShowResultBox(true, "✓ 플래시 완료. 보드 전원을 재인가(재부팅)하세요." + tip);
|
||||||
|
}
|
||||||
|
else if (res.BootloaderRejected)
|
||||||
|
{
|
||||||
|
ShowResultBox(false,
|
||||||
|
$"부트로더 쓰기를 거부했습니다 (rc={res.ExitCode}).\n" +
|
||||||
|
"이미 secure boot가 구워진 보드면 '정상'입니다 — 모드를 '표준'이나 '앱만'으로 바꿔 다시 하세요(부트로더 건너뜀). 꼭 부트로더를 써야 하면 '--force'를 켜세요.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShowResultBox(false,
|
||||||
|
$"단계 실패: {res.FailedStep} (rc={res.ExitCode}).\n" +
|
||||||
|
"로그를 확인하세요. 포트·케이블, 다운로드 모드 진입, 모델 선택을 점검하고 재시도하세요.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShowResultBox(bool ok, string text)
|
||||||
|
{
|
||||||
|
pnlResult.Visibility = Visibility.Visible;
|
||||||
|
pnlResult.BorderBrush = GetBrush(ok ? "Success" : "Danger");
|
||||||
|
pnlResult.Background = GetBrush(ok ? "SuccessDim" : "DangerDim");
|
||||||
|
txtResult.Foreground = GetBrush("Text");
|
||||||
|
txtResult.Text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HideResult() => pnlResult.Visibility = Visibility.Collapsed;
|
||||||
|
void AppendLog(string s) { txtLog.AppendText(s + "\n"); txtLog.ScrollToEnd(); }
|
||||||
|
|
||||||
|
async Task ProbeEsptoolAsync()
|
||||||
|
{
|
||||||
|
var (rc, text) = await EsptoolRunner.Capture(_esptoolPath, "version");
|
||||||
|
string tag = _esptoolPath.Contains(@"\tools\esptool\", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "동봉" : (Path.IsPathRooted(_esptoolPath) ? "설치본" : "PATH");
|
||||||
|
if (rc == 0)
|
||||||
|
{
|
||||||
|
var ver = text.Split('\n')[0].Trim();
|
||||||
|
txtEsptool.Text = $"{ver} · {tag} · {_esptoolPath}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
txtEsptool.Foreground = GetBrush("Warn");
|
||||||
|
txtEsptool.Text = $"⚠ esptool 실행 불가 ({_esptoolPath}) — tools\\esptool\\ 에 동봉하거나 설치가 필요합니다.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 디버그용 화면 캡처(--shot 경로) =====
|
||||||
|
async Task CaptureAndExitAsync(string path)
|
||||||
|
{
|
||||||
|
await Task.Delay(1100);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UpdateLayout();
|
||||||
|
int w = Math.Max(1, (int)Math.Ceiling(ActualWidth));
|
||||||
|
int h = Math.Max(1, (int)Math.Ceiling(ActualHeight));
|
||||||
|
var rtb = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Pbgra32);
|
||||||
|
rtb.Render(this);
|
||||||
|
var enc = new PngBitmapEncoder();
|
||||||
|
enc.Frames.Add(BitmapFrame.Create(rtb));
|
||||||
|
using var fs = File.Create(path);
|
||||||
|
enc.Save(fs);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
Application.Current.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Info(string m) => MessageBox.Show(this, m, "확인", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
Brush GetBrush(string key) => (Brush)FindResource(key);
|
||||||
|
}
|
||||||
40
DB720Flasher/Models.cs
Normal file
40
DB720Flasher/Models.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
namespace DB720Flasher;
|
||||||
|
|
||||||
|
public sealed class ComPortInfo
|
||||||
|
{
|
||||||
|
public required string PortName { get; init; } // "COM4"
|
||||||
|
public required string FriendlyName { get; init; } // Win32 PnP 이름
|
||||||
|
public required string DeviceId { get; init; } // USB\VID_303A&PID_1001\...
|
||||||
|
public int Number { get; init; }
|
||||||
|
public string? Vid { get; init; }
|
||||||
|
public string? Pid { get; init; }
|
||||||
|
public string Kind { get; init; } = "USB 직렬"; // "USB-JTAG (내장)", "CP210x", "CH340", "FTDI" …
|
||||||
|
|
||||||
|
public bool IsJtag => Vid == "303A" && Pid == "1001";
|
||||||
|
public string VidPid => Vid is null || Pid is null ? "" : $"{Vid}:{Pid}";
|
||||||
|
public bool Matches(string? hint)
|
||||||
|
=> !string.IsNullOrEmpty(hint) && string.Equals(VidPid, hint, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public string Display => $"{PortName} · {Kind}";
|
||||||
|
public override string ToString() => Display;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>플래시 모드.</summary>
|
||||||
|
public enum FlashMode
|
||||||
|
{
|
||||||
|
AppOnly, // 앱만 (+ 프로파일이 지정하면 otadata erase) — 권장
|
||||||
|
Standard, // 부트로더 제외 전부 (파티션+ota_data+앱(+www))
|
||||||
|
Full, // 부트로더까지 전부 (eFuse 위험)
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FirmwareItem
|
||||||
|
{
|
||||||
|
public required string FileName { get; init; }
|
||||||
|
public required string FullPath { get; init; }
|
||||||
|
public long Size { get; init; }
|
||||||
|
public string? Note { get; init; }
|
||||||
|
|
||||||
|
public string SizeText => Size >= 1_048_576 ? $"{Size / 1048576.0:0.0}MB" : $"{Size / 1024.0:0}KB";
|
||||||
|
public string Display => Note is null ? $"{FileName} ({SizeText})" : $"{FileName} · {Note}";
|
||||||
|
public override string ToString() => Display;
|
||||||
|
}
|
||||||
51
DB720Flasher/Profile.cs
Normal file
51
DB720Flasher/Profile.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
namespace DB720Flasher;
|
||||||
|
|
||||||
|
/// <summary>flash.json 의 한 영역 — 어떤 파일을 flash 어느 주소에 쓰는지.</summary>
|
||||||
|
public sealed class FwRegion
|
||||||
|
{
|
||||||
|
public string? File { get; set; } // 보조 파일명 (app=true 면 비움)
|
||||||
|
public bool App { get; set; } // 앱 영역 표시
|
||||||
|
public string Offset { get; set; } = "0x0";
|
||||||
|
public string Kind { get; set; } = "support"; // bootloader | support | app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 모델 프로파일 — 칩·offset·플래시 방식을 한 곳에 담는다.
|
||||||
|
/// firmware 폴더의 flash.json 으로 덮어쓰거나, 빌트인을 그대로 쓴다.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FlashProfile
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "Custom";
|
||||||
|
public string Chip { get; set; } = "esp32"; // esp32 | esp32s3 | esp32p4 | ...
|
||||||
|
public int Baud { get; set; } = 921600;
|
||||||
|
public bool NoStub { get; set; } // DB720(SDM)=true
|
||||||
|
public bool PerRegion { get; set; } // DB720=true(영역별 개별 호출), 클래식=false(한 번에)
|
||||||
|
public int ConnectAttempts { get; set; } = 10;
|
||||||
|
public string? FlashSize { get; set; } // DB720="16MB"; null=자동(스텁)
|
||||||
|
public string? FlashMode { get; set; } // DB720="dio"
|
||||||
|
public string? FlashFreq { get; set; } // DB720="80m"
|
||||||
|
public string Before { get; set; } = "default-reset";
|
||||||
|
public string After { get; set; } = "hard-reset";
|
||||||
|
public bool AppOnlyErasesOtadata { get; set; } // DB720=true
|
||||||
|
public string OtadataOffset { get; set; } = "0x11000";
|
||||||
|
public string OtadataSize { get; set; } = "0x2000";
|
||||||
|
public List<FwRegion> Regions { get; set; } = new();
|
||||||
|
public string? FlashPortHint { get; set; } // "303A:1001" — 자동선택 우선
|
||||||
|
public string? ConsolePortHint { get; set; } // "10C4:EA60" — 경고만(막지 않음)
|
||||||
|
public string? DownloadModeHint { get; set; } // 다운로드 모드 진입 안내
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
public bool FromFile { get; set; } // 폴더 flash.json 출처면 true(런타임)
|
||||||
|
|
||||||
|
public FwRegion? AppRegion => Regions.FirstOrDefault(r => r.App);
|
||||||
|
public FwRegion? Bootloader => Regions.FirstOrDefault(r => r.Kind == "bootloader");
|
||||||
|
public bool HasBootloader => Bootloader?.File is not null;
|
||||||
|
public IEnumerable<FwRegion> SupportRegions => Regions.Where(r => !r.App && r.File is not null);
|
||||||
|
public string AppOffset => AppRegion?.Offset ?? "0x20000";
|
||||||
|
|
||||||
|
public string Summary =>
|
||||||
|
$"{Chip} · {(NoStub ? "no-stub" : "stub")} · {Baud}" +
|
||||||
|
(FlashPortHint is not null ? $" · 포트 {FlashPortHint}" : "");
|
||||||
|
|
||||||
|
public override string ToString() => Name;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
370
DB720Flasher/Themes/Controls.xaml
Normal file
370
DB720Flasher/Themes/Controls.xaml
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||||
|
|
||||||
|
<!-- ===== 디자인 토큰 (전역 규칙: 컨트롤 높이 통일, 인라인 Padding 금지) ===== -->
|
||||||
|
<sys:Double x:Key="Control.Height">34</sys:Double>
|
||||||
|
<CornerRadius x:Key="Control.Radius">6</CornerRadius>
|
||||||
|
|
||||||
|
<!-- 색 (다크) -->
|
||||||
|
<SolidColorBrush x:Key="Bg" Color="#FF1E1F22" />
|
||||||
|
<SolidColorBrush x:Key="Surface" Color="#FF2A2C30" />
|
||||||
|
<SolidColorBrush x:Key="SurfaceAlt" Color="#FF34373C" />
|
||||||
|
<SolidColorBrush x:Key="Border" Color="#FF44474D" />
|
||||||
|
<SolidColorBrush x:Key="Text" Color="#FFECECEC" />
|
||||||
|
<SolidColorBrush x:Key="TextSubtle" Color="#FF9AA0A6" />
|
||||||
|
<SolidColorBrush x:Key="Accent" Color="#FF3B82F6" />
|
||||||
|
<SolidColorBrush x:Key="AccentHover" Color="#FF60A5FA" />
|
||||||
|
<SolidColorBrush x:Key="Success" Color="#FF34D399" />
|
||||||
|
<SolidColorBrush x:Key="Warn" Color="#FFF59E0B" />
|
||||||
|
<SolidColorBrush x:Key="Danger" Color="#FFEF4444" />
|
||||||
|
<SolidColorBrush x:Key="DangerDim" Color="#33EF4444" />
|
||||||
|
<SolidColorBrush x:Key="SuccessDim" Color="#2234D399" />
|
||||||
|
<SolidColorBrush x:Key="PopupBg" Color="#FF313338" />
|
||||||
|
|
||||||
|
<!-- ===== Button: 전역 스타일만, 인라인 Padding 금지 ===== -->
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="MinHeight" Value="{StaticResource Control.Height}" />
|
||||||
|
<Setter Property="Padding" Value="14,0" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource SurfaceAlt}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="b" Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{StaticResource Control.Radius}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Margin="{TemplateBinding Padding}" TextElement.Foreground="{TemplateBinding Foreground}" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="b" Property="Background" Value="{StaticResource Border}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.45" />
|
||||||
|
<Setter Property="Cursor" Value="Arrow" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Accent}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="b" Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="{StaticResource Control.Radius}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Margin="{TemplateBinding Padding}" TextElement.Foreground="White" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="b" Property="Background" Value="{StaticResource AccentHover}" />
|
||||||
|
<Setter TargetName="b" Property="BorderBrush" Value="{StaticResource AccentHover}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4" />
|
||||||
|
<Setter Property="Cursor" Value="Arrow" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Danger}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Danger}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== 공용 다크 ScrollBar ===== -->
|
||||||
|
<Style TargetType="Thumb" x:Key="ScrollThumb">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Border CornerRadius="4" Background="{StaticResource Border}" Margin="2" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="RepeatButton" x:Key="ScrollHidden">
|
||||||
|
<Setter Property="Opacity" Value="0" />
|
||||||
|
<Setter Property="Focusable" Value="False" />
|
||||||
|
<Setter Property="IsTabStop" Value="False" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RepeatButton"><Border Background="Transparent" /></ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="ScrollBar">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Width" Value="11" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Border Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||||
|
<Track.IncreaseRepeatButton>
|
||||||
|
<RepeatButton Style="{StaticResource ScrollHidden}" Command="ScrollBar.PageDownCommand" />
|
||||||
|
</Track.IncreaseRepeatButton>
|
||||||
|
<Track.DecreaseRepeatButton>
|
||||||
|
<RepeatButton Style="{StaticResource ScrollHidden}" Command="ScrollBar.PageUpCommand" />
|
||||||
|
</Track.DecreaseRepeatButton>
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb Style="{StaticResource ScrollThumb}" />
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="Orientation" Value="Horizontal">
|
||||||
|
<Setter Property="Width" Value="Auto" />
|
||||||
|
<Setter Property="Height" Value="11" />
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== ComboBox (완전 다크: 토글·화살표·팝업·항목) ===== -->
|
||||||
|
<Style x:Key="ComboToggle" TargetType="ToggleButton">
|
||||||
|
<Setter Property="Focusable" Value="False" />
|
||||||
|
<Setter Property="ClickMode" Value="Press" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Border x:Name="b" Background="{StaticResource Surface}" BorderBrush="{StaticResource Border}"
|
||||||
|
BorderThickness="1" CornerRadius="{StaticResource Control.Radius}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="28" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Path Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Data="M0,0 L4,4 L8,0 Z" Fill="{StaticResource TextSubtle}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="b" Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ComboBox">
|
||||||
|
<Setter Property="MinHeight" Value="{StaticResource Control.Height}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBox">
|
||||||
|
<Grid>
|
||||||
|
<ToggleButton x:Name="Toggle" Style="{StaticResource ComboToggle}"
|
||||||
|
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
|
||||||
|
<ContentPresenter x:Name="ContentSite" IsHitTestVisible="False"
|
||||||
|
Content="{TemplateBinding SelectionBoxItem}"
|
||||||
|
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||||
|
Margin="11,0,32,0" VerticalAlignment="Center" HorizontalAlignment="Left"
|
||||||
|
TextElement.Foreground="{StaticResource Text}" />
|
||||||
|
<Popup x:Name="Popup" Placement="Bottom" IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||||
|
AllowsTransparency="True" Focusable="False" PopupAnimation="Slide">
|
||||||
|
<Border Background="{StaticResource PopupBg}" BorderBrush="{StaticResource Border}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Margin="0,4,0,4"
|
||||||
|
MinWidth="{Binding ActualWidth, ElementName=Toggle}"
|
||||||
|
MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained" Margin="0,3" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ComboBoxItem">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Padding" Value="11,8" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ComboBoxItem">
|
||||||
|
<Border x:Name="b" Background="Transparent" Padding="{TemplateBinding Padding}"
|
||||||
|
CornerRadius="4" Margin="4,1">
|
||||||
|
<ContentPresenter TextElement.Foreground="{StaticResource Text}" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="b" Property="Background" Value="{StaticResource Border}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="b" Property="Background" Value="{StaticResource Accent}" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== TextBox (다크) ===== -->
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="MinHeight" Value="{StaticResource Control.Height}" />
|
||||||
|
<Setter Property="Padding" Value="10,6" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource Surface}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
|
<Setter Property="CaretBrush" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="SelectionBrush" Value="{StaticResource Accent}" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="{StaticResource Control.Radius}">
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Focusable="False" Margin="{TemplateBinding Padding}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== CheckBox (다크) ===== -->
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border x:Name="box" Width="18" Height="18" CornerRadius="4" BorderThickness="1.5"
|
||||||
|
BorderBrush="{StaticResource Border}" Background="{StaticResource Surface}"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Path x:Name="chk" Data="M0,4 L3.5,7.5 L9,1" Stroke="White" StrokeThickness="2"
|
||||||
|
StrokeEndLineCap="Round" StrokeStartLineCap="Round"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed" />
|
||||||
|
</Border>
|
||||||
|
<ContentPresenter Grid.Column="1" Margin="9,0,0,0" VerticalAlignment="Center"
|
||||||
|
TextElement.Foreground="{TemplateBinding Foreground}" />
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="box" Property="Background" Value="{StaticResource Accent}" />
|
||||||
|
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
<Setter TargetName="chk" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== RadioButton (다크) ===== -->
|
||||||
|
<Style TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="Margin" Value="0,5" />
|
||||||
|
<Setter Property="Cursor" Value="Hand" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border x:Name="box" Width="18" Height="18" CornerRadius="9" BorderThickness="1.5"
|
||||||
|
BorderBrush="{StaticResource Border}" Background="{StaticResource Surface}"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<Ellipse x:Name="dot" Width="8" Height="8" Fill="White" Visibility="Collapsed" />
|
||||||
|
</Border>
|
||||||
|
<ContentPresenter Grid.Column="1" Margin="9,0,0,0" VerticalAlignment="Center"
|
||||||
|
TextElement.Foreground="{TemplateBinding Foreground}" />
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="box" Property="Background" Value="{StaticResource Accent}" />
|
||||||
|
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
<Setter TargetName="dot" Property="Visibility" Value="Visible" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource Accent}" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== ProgressBar (다크) ===== -->
|
||||||
|
<Style TargetType="ProgressBar">
|
||||||
|
<Setter Property="Background" Value="{StaticResource Bg}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Accent}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ProgressBar">
|
||||||
|
<Border Background="{TemplateBinding Background}" CornerRadius="5" ClipToBounds="True">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="PART_Track" />
|
||||||
|
<Border x:Name="PART_Indicator" Background="{TemplateBinding Foreground}"
|
||||||
|
HorizontalAlignment="Left" CornerRadius="5" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Label">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextSubtle}" />
|
||||||
|
<Setter Property="FontSize" Value="12" />
|
||||||
|
<Setter Property="Padding" Value="0,0,0,2" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ToolTip 다크 -->
|
||||||
|
<Style TargetType="ToolTip">
|
||||||
|
<Setter Property="Background" Value="{StaticResource PopupBg}" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
19
DB720Flasher/app.manifest
Normal file
19
DB720Flasher/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="DB720Flasher.app" />
|
||||||
|
|
||||||
|
<!-- DPI 인식: 고해상도에서 또렷하게 -->
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Windows 10/11 호환 선언 -->
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
339
DB720Flasher/tools/esptool/LICENSE
Normal file
339
DB720Flasher/tools/esptool/LICENSE
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
BIN
DB720Flasher/tools/esptool/esptool.exe
Normal file
BIN
DB720Flasher/tools/esptool/esptool.exe
Normal file
Binary file not shown.
225
README.md
Normal file
225
README.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# DB720 펌웨어 적재 키트 (PowerShell)
|
||||||
|
|
||||||
|
ESP32-P4 기반 DB720 보드에 **secure_release 서명 펌웨어**를 PowerShell로 적재하는 독립 키트입니다.
|
||||||
|
이 폴더 하나만 있으면 빌드 환경 없이도 적재할 수 있습니다(esptool만 있으면 됨).
|
||||||
|
|
||||||
|
- 펌웨어: `DIBD720Ue_08B_08R040C` (8단40열 = 640×128, 256색/08B) — db250513 키로 V2 서명한 secure_release 빌드
|
||||||
|
- 빌드일: 2026-06-22 (버전 3.7.2)
|
||||||
|
|
||||||
|
> **DB720은 secure_release 단일입니다.** 적재·빌드·OTA에 쓰는 펌웨어는 **db250513 키로 서명한 바이너리뿐**입니다.
|
||||||
|
> 서명이 없는 일반 빌드(예: V3.7.1 같은 공개 릴리즈 바이너리)는 secure_release 부트로더가 **서명 검증에서 거부**해 부팅되지 않습니다.
|
||||||
|
> 이 키트의 `firmware\` 폴더에는 **db250513 서명본만** 두세요(비서명 bin 금지).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 두 경우 — 어느 스크립트를 쓰나
|
||||||
|
|
||||||
|
| 보드 상태 | 스크립트 | 무엇을 쓰나 | 위험 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **일상적 앱 교체** (서명 부트로더가 이미 정상) | `flash_app.ps1` | 부트로더 **제외** 4종(파티션/ota_data/앱/www) | 없음 — 부트로더 안 건드려 **brick 불가** |
|
||||||
|
| **부트로더가 비었을 때** (flash 통째 지워짐 / 0x2000 빔 / 부팅 안 됨) | `flash_full.ps1` | 부트로더+파티션+ota_data+앱+www **5종 전부** | ⚠ 부트로더(0x2000)에 손댐 — 아래 §1 경고 |
|
||||||
|
|
||||||
|
> **일상 적재는 `flash_app.ps1`(경우 B)만 쓰세요.** 가능하면 `-AppOnly`로 앱만 올립니다.
|
||||||
|
> `flash_full.ps1`(경우 A)은 **부트로더가 실제로 비어 부팅이 안 될 때만** 쓰는 복구 전용입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. ★★★ full flash 경고 (경우 A 전용) ★★★
|
||||||
|
|
||||||
|
`flash_full.ps1`은 부트로더(0x2000)까지 다시 씁니다.
|
||||||
|
|
||||||
|
- **이미 secure_release로 구워진 보드** (예: 회사 `.230`) → 부트로더가 이미 영구 고정돼 있으므로 같은 서명 부트로더를 다시 쓸 이유가 없습니다. **일상 적재에 경우 A를 쓰지 마세요** — `flash_app.ps1`이면 충분합니다.
|
||||||
|
- **eFuse가 아직 안 구워진 보드** → secure_release 서명본을 full flash하면 첫 부팅에 secure boot **eFuse가 영구로 구워집니다**(되돌릴 수 없음).
|
||||||
|
- **eFuse는 비가역입니다.** 한 번 구워지면 그 보드는 영원히 이 서명 체계에만 묶입니다(DB400 V1에서 `ABS_DONE_0` 잠김으로 보드가 brick된 실증이 있습니다).
|
||||||
|
- **db250513 키를 분실하면 그 보드는 OTA로 더 이상 펌웨어를 못 올립니다(영구).** 키 백업은 필수입니다.
|
||||||
|
|
||||||
|
`flash_app.ps1`(경우 B)은 부트로더를 안 건드리므로 이 위험이 **없습니다**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 사전 준비
|
||||||
|
|
||||||
|
1. **USB-JTAG 케이블**로 PC ↔ DB720 연결 (보드의 USB-JTAG 포트).
|
||||||
|
2. **esptool**이 있는 파이썬. 두 스크립트는 기본으로 ESP-IDF 파이썬을 씁니다:
|
||||||
|
`C:\Users\jyheo\.espressif\python_env\idf5.5_py3.13_env\Scripts\python.exe`
|
||||||
|
- 환경이 다르면 `flash_full.ps1` / `flash_app.ps1` 상단의 `$Python` 한 줄만 고치세요.
|
||||||
|
- 또는 `pip install esptool` 한 파이썬이 PATH에 있으면 자동 폴백됩니다.
|
||||||
|
- esptool은 **v4.11 이상**이어야 합니다(낮으면 적재 중 멈춤).
|
||||||
|
3. **COM 포트 확인** — 적재 포트(USB-JTAG)는 번호가 바뀔 수 있습니다:
|
||||||
|
```powershell
|
||||||
|
.\list_ports.ps1
|
||||||
|
```
|
||||||
|
- `USB 직렬 장치(COMx)` 또는 VID `303A:1001` = **적재 포트** (`-Port COMx`)
|
||||||
|
- `Silicon Labs CP210x (COMx)` = **COM2 자리**(디버그·문구 전송, 적재 중 열지 말 것)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 적재 방법
|
||||||
|
|
||||||
|
PowerShell을 이 폴더에서 열고:
|
||||||
|
|
||||||
|
### 경우 B — 서명 부트로더 있음 (일상 적재, 권장)
|
||||||
|
```powershell
|
||||||
|
# 앱만 최소로(앱 0x20000 + otadata erase) — 권장
|
||||||
|
.\flash_app.ps1 -Port COM4 -AppOnly
|
||||||
|
|
||||||
|
# 표준(파티션+ota_data+앱+www, 부트로더 제외)
|
||||||
|
.\flash_app.ps1 -Port COM4
|
||||||
|
```
|
||||||
|
|
||||||
|
### 경우 A — 부트로더 비었음 (전체 복구 전용, §1 경고 확인)
|
||||||
|
```powershell
|
||||||
|
.\flash_full.ps1 -Port COM4
|
||||||
|
```
|
||||||
|
|
||||||
|
> 실행 정책 때문에 막히면(이 PC에 처음): `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` 한 번.
|
||||||
|
> `-Port`를 생략하면 기본 `COM4`로 시도합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3a. 앱이 여러 개일 때 — 골라서 올리기
|
||||||
|
|
||||||
|
`firmware\` 폴더에 앱 `.bin`이 여러 개 있을 수 있습니다. 예를 들어 지금은 두 개가 들어 있습니다.
|
||||||
|
|
||||||
|
| 앱 파일 | 화면 크기 | 상태 |
|
||||||
|
|---|---|---|
|
||||||
|
| `DIBD720Te_08C_04R020C.bin` | 320×64 (4단20열, 8색) | 부팅 검증됨 — 안전, 일상·복구용 권장 |
|
||||||
|
| `DIBD720Ue_08B_08R040C.bin` | 640×128 (8단40열, 256색) | 부팅 미검증 — 테스트용(SRAM 한도 의심) |
|
||||||
|
|
||||||
|
> 부트로더·파티션·ota_data·www_fs는 앱이 아니라 보조 파일이라 선택 대상에서 빠집니다. 앱 후보는 그 4개를 뺀 나머지 `.bin`입니다.
|
||||||
|
|
||||||
|
### 1단계 — 어떤 앱이 있는지 본다
|
||||||
|
|
||||||
|
앱을 지정하지 않고 그냥 실행하면, 스크립트가 **임의로 고르지 않고** 앱 목록을 보여준 뒤 멈춥니다.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\flash_app.ps1 -Port COM4
|
||||||
|
```
|
||||||
|
출력 예:
|
||||||
|
```
|
||||||
|
여러 앱이 있습니다 — -App 으로 하나를 지정하세요:
|
||||||
|
DIBD720Te_08C_04R020C.bin
|
||||||
|
DIBD720Ue_08B_08R040C.bin
|
||||||
|
예: .\flash_app.ps1 -Port COM4 -App DIBD720Te_08C_04R020C.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2단계 — 올릴 앱을 `-App`으로 지정한다
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 320×64(검증된 것)을 앱만 최소로 올리기 — 권장
|
||||||
|
.\flash_app.ps1 -Port COM4 -App DIBD720Te_08C_04R020C.bin -AppOnly
|
||||||
|
|
||||||
|
# 640×128(테스트)을 표준으로 올리기
|
||||||
|
.\flash_app.ps1 -Port COM4 -App DIBD720Ue_08B_08R040C.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-App` 값은 `firmware\` 안의 **파일명만** 적습니다(경로 없이).
|
||||||
|
- 앱이 **하나뿐이면** `-App`을 생략해도 그것이 자동 선택됩니다(여러 개일 때만 지정 필수).
|
||||||
|
- `flash_full.ps1`(경우 A)도 똑같이 `-App`으로 고릅니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3b. 전체 굽기 (full flash) — 5영역 전부
|
||||||
|
|
||||||
|
부트로더(0x2000)까지 포함해 **5개 영역을 전부** 쓰는 절차입니다. flash가 통째로 비었거나 부트로더가 깨져 부팅이 안 될 때 보드를 처음부터 살립니다.
|
||||||
|
|
||||||
|
> ⚠ **먼저 §1 경고를 읽으세요.** 전체 굽기는 부트로더를 건드리므로, eFuse가 안 구워진 보드에서는 **첫 부팅에 secure boot eFuse가 영구로 구워집니다**(되돌릴 수 없음). 일상 적재에는 쓰지 말고 §3(앱 적재)을 쓰세요. 전체 굽기는 **부트로더가 실제로 비어 부팅이 안 될 때만** 씁니다.
|
||||||
|
|
||||||
|
### 무엇을 쓰나 (영역 5개)
|
||||||
|
|
||||||
|
| offset | 파일 | 크기 | 서명 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0x2000` | `firmware\bootloader.bin` | 45KB | db250513 서명 |
|
||||||
|
| `0x10000` | `firmware\partition-table.bin` | 3KB | 비서명(정상) |
|
||||||
|
| `0x11000` | `firmware\ota_data_initial.bin` | 8KB | 비서명(정상) |
|
||||||
|
| `0x20000` | 선택한 앱 (`-App`) | 1.2~1.7MB | db250513 서명 |
|
||||||
|
| `0x420000` | `firmware\www_fs.bin` | 512KB | 비서명(정상) |
|
||||||
|
|
||||||
|
### 방법 1 — 스크립트 (권장)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 앱이 하나면 그대로
|
||||||
|
.\flash_full.ps1 -Port COM4
|
||||||
|
|
||||||
|
# 앱이 여러 개면 -App 으로 지정
|
||||||
|
.\flash_full.ps1 -Port COM4 -App DIBD720Te_08C_04R020C.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
스크립트가 5영역을 **하나씩 개별 호출**로 쓴 뒤(SDM 다중영역 한 번에 쓰면 중간 실패) hard_reset 합니다.
|
||||||
|
|
||||||
|
### 방법 2 — 수동 esptool (스크립트가 안 될 때, 한 줄씩)
|
||||||
|
|
||||||
|
`COM4`는 적재 포트로 바꾸고(`.\list_ports.ps1`), `--flash_size 16MB`는 **반드시** 붙입니다(이게 빠지면 SDM에서 4MB로 오인해 `0x420000`(www_fs)을 못 써 부팅 루프가 납니다 — 2026-06-22 실증).
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$P = "C:\Users\jyheo\.espressif\python_env\idf5.5_py3.13_env\Scripts\python.exe"
|
||||||
|
$C = "--chip","esp32p4","-p","COM4","-b","115200","--before","default_reset","--after","no_reset","--connect-attempts","30","--no-stub"
|
||||||
|
$W = "write_flash","--flash_mode","dio","--flash_size","16MB","--flash_freq","80m"
|
||||||
|
& $P -m esptool @C @W 0x2000 firmware\bootloader.bin
|
||||||
|
& $P -m esptool @C @W 0x10000 firmware\partition-table.bin
|
||||||
|
& $P -m esptool @C @W 0x11000 firmware\ota_data_initial.bin
|
||||||
|
& $P -m esptool @C @W 0x20000 firmware\DIBD720Te_08C_04R020C.bin # 올릴 앱
|
||||||
|
& $P -m esptool @C @W 0x420000 firmware\www_fs.bin
|
||||||
|
# SDM 탈출(정상 부팅)
|
||||||
|
& $P -c "from esptool.targets.esp32p4 import ESP32P4ROM; r=ESP32P4ROM('COM4'); r.connect('default_reset'); r.hard_reset()"
|
||||||
|
```
|
||||||
|
|
||||||
|
> ★ **secure 보드는 부트로더(0x2000)·전체 erase write를 거부할 수 있습니다** — `0105 format invalid` 같은 거부는 부트로더가 이미 같은 키로 구워져 있다는 뜻(정상)이라, 그 경우 0x2000은 건너뛰고 나머지 4영역만 쓰면 됩니다(= §3 앱 적재로 충분).
|
||||||
|
> ★ esptool/USB 리셋은 이 보드에서 다운로드 모드로 떨어지는 경향이 있어, **최종 부팅은 USB-JTAG 케이블을 뽑고 전원 사이클**해야 확실합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 적재 메커니즘 (왜 이렇게 하나)
|
||||||
|
|
||||||
|
- **eco2 secure download mode(SDM) 레시피**: `--no-stub` + `115200` baud + `--before default_reset --after no_reset`.
|
||||||
|
secure 보드는 esptool stub를 못 올리므로 `--no-stub`, 다운로드 모드 안정 위해 115200을 씁니다.
|
||||||
|
- **영역은 하나씩 개별 esptool 호출로** 씁니다. SDM에서 여러 영역을 한 번에 쓰면 중간에 "Failed to enter Flash download mode"로 실패합니다(2026-06-22 실증 — www_fs가 빈 채 남아 부팅 루프).
|
||||||
|
- **부트로더(0x2000)는 경우 A에서만** 씁니다. secure 보드는 부트로더/전체 erase를 거부하는 게 정상입니다.
|
||||||
|
- 적재 후 `--after no_reset`로 남은 다운로드 모드를 **hard_reset**(esptool ROM 객체)으로 빠져나와 정상 부팅합니다.
|
||||||
|
- 오프셋 정본: 부트로더 `0x2000` / 파티션 `0x10000` / ota_data `0x11000` / 앱 `0x20000` / www_fs `0x420000`. flash: dio / keep / 80m.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 적재 후 확인
|
||||||
|
|
||||||
|
다음 중 하나로 새 펌웨어가 실제로 올라가 부팅됐는지 확인합니다.
|
||||||
|
|
||||||
|
- **COM2(디버그 콘솔, CP210x)에서 `![0081!]`** 을 115200bps로 보내 버전 배너가 오면 정상(적재 포트 COM4 아님).
|
||||||
|
- **부트로그**에 `RSA-PSS verified`(secure_release 서명 검증 통과)와 정상 부팅 메시지가 보이면 정상.
|
||||||
|
- **LAN으로 받은 빌드 시각**을 키트의 빌드일(이 README 상단)과 대조 — 날짜가 맞으면 새 펌웨어가 적용된 것입니다.
|
||||||
|
|
||||||
|
> 적재 포트(COM4/USB-JTAG)는 **측정·표출 중 열지 마세요** — DTR/RTS가 보드를 리셋해 화면이 날아갑니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 안 될 때
|
||||||
|
|
||||||
|
| 증상 | 조치 |
|
||||||
|
|---|---|
|
||||||
|
| 포트 연결 실패 / `could not open port` | `.\list_ports.ps1`로 실제 USB-JTAG COM 번호 확인 후 `-Port COMx`. 다른 프로그램이 그 포트를 잡고 있으면 닫기 |
|
||||||
|
| `~0.72MB`에서 멈춤(hang) | esptool 버전이 낮음(<4.11). `pip install -U esptool` (이 키트는 v4.11+ 전제) |
|
||||||
|
| 적재는 됐는데 부팅 안 됨(검정/무응답) | 먼저 `flash_app.ps1 -AppOnly`로 otadata를 지워 ota_0으로 폴백 시도. 그래도 안 되고 부트로더가 빈 것 같으면 경우 A `flash_full.ps1` |
|
||||||
|
| 부트로그에 서명 검증 실패 | 비서명(서명 없는) bin을 올렸을 때 발생. `firmware\`에 **db250513 서명본만** 있는지 확인(V3.7.1 같은 일반 릴리즈 bin은 secure_release에서 부팅 거부) |
|
||||||
|
| `secure download` / 권한 오류 반복 | USB 케이블 재연결 후 재시도(스크립트가 connect 30회 재시도) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 포함 파일
|
||||||
|
|
||||||
|
```
|
||||||
|
DB720_Flash\
|
||||||
|
├─ README.md ← 이 문서
|
||||||
|
├─ list_ports.ps1 ← COM 포트 확인
|
||||||
|
├─ flash_app.ps1 ← 경우 B: 서명 부트로더 있을 때(brick-safe, 권장)
|
||||||
|
├─ flash_full.ps1 ← 경우 A: 부트로더 비었을 때(full, §1 경고)
|
||||||
|
└─ firmware\
|
||||||
|
├─ bootloader.bin (0x2000, 45KB) ← 경우 A에서만 사용 (db250513 서명)
|
||||||
|
├─ partition-table.bin (0x10000, 3KB) ← 비서명(설계상 정상)
|
||||||
|
├─ ota_data_initial.bin (0x11000, 8KB) ← 비서명(설계상 정상)
|
||||||
|
├─ DIBD720Te_08C_04R020C.bin (0x20000, 1.2MB) ← 앱① 320×64, 8색 (db250513 서명, 부팅 검증됨·권장)
|
||||||
|
├─ DIBD720Ue_08B_08R040C.bin (0x20000, 1.7MB) ← 앱② 640×128, 256색 (db250513 서명, 테스트용·미검증)
|
||||||
|
└─ www_fs.bin (0x420000, 512KB) ← 비서명(설계상 정상)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 앱이 둘 다 `0x20000`(ota_0)에 올라가는 같은 자리라, **한 번에 하나만** 적재됩니다. 어느 것을 올릴지는 §3a의 `-App`으로 고릅니다.
|
||||||
|
> 서명 대상(앱·부트로더)은 모두 db250513 서명본이고, 파티션·ota_data·www_fs는 secure boot v2가 서명하지 않는 정상 파일입니다.
|
||||||
27
firmware/flash.json
Normal file
27
firmware/flash.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "DB720 (ESP32-P4)",
|
||||||
|
"chip": "esp32p4",
|
||||||
|
"baud": 115200,
|
||||||
|
"noStub": true,
|
||||||
|
"perRegion": true,
|
||||||
|
"connectAttempts": 30,
|
||||||
|
"flashSize": "16MB",
|
||||||
|
"flashMode": "dio",
|
||||||
|
"flashFreq": "80m",
|
||||||
|
"before": "default-reset",
|
||||||
|
"after": "hard-reset",
|
||||||
|
"appOnlyErasesOtadata": true,
|
||||||
|
"otadataOffset": "0x11000",
|
||||||
|
"otadataSize": "0x2000",
|
||||||
|
"regions": [
|
||||||
|
{ "file": "bootloader.bin", "offset": "0x2000", "kind": "bootloader" },
|
||||||
|
{ "file": "partition-table.bin", "offset": "0x10000", "kind": "support" },
|
||||||
|
{ "file": "ota_data_initial.bin", "offset": "0x11000", "kind": "support" },
|
||||||
|
{ "file": "www_fs.bin", "offset": "0x420000", "kind": "support" },
|
||||||
|
{ "app": true, "offset": "0x20000" }
|
||||||
|
],
|
||||||
|
"flashPortHint": "303A:1001",
|
||||||
|
"consolePortHint": "10C4:EA60",
|
||||||
|
"downloadModeHint": "USB-JTAG 케이블 연결. 포트가 안 잡히면 케이블만 다시 꽂으면 됩니다(자동 감지).",
|
||||||
|
"notes": "secure_release · db250513 서명. 영역별 개별 호출(SDM). 부트로더(0x2000)는 이미 구워진 보드면 거부될 수 있음(정상 → 표준/앱만)."
|
||||||
|
}
|
||||||
83
flash_app.ps1
Normal file
83
flash_app.ps1
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# =====================================================================
|
||||||
|
# DB720 (ESP32-P4) 앱 적재 — 경우 B: 서명된 부트로더가 이미 있을 때 (brick-safe)
|
||||||
|
# =====================================================================
|
||||||
|
# 언제 쓰나
|
||||||
|
# - 부트로더(0x2000)는 정상이고 펌웨어(앱)만 새로 올릴 때. 일상적 적재는 이쪽.
|
||||||
|
# - 부트로더(0x2000)를 건드리지 않으므로 eFuse 재굽힘·brick 위험이 없다 (가장 안전).
|
||||||
|
#
|
||||||
|
# 무엇을 쓰나 (부트로더 0x2000 제외)
|
||||||
|
# - 기본: 파티션 0x10000 / ota_data 0x11000 / 앱 0x20000 / www_fs 0x420000
|
||||||
|
# - -AppOnly: 앱 0x20000 + otadata erase 만 (최소 적재)
|
||||||
|
#
|
||||||
|
# ★ 앱이 여러 개일 때 — firmware\ 안의 앱 .bin 을 -App 으로 지정한다.
|
||||||
|
# 지정 안 하면: 앱이 1개면 자동 선택 / 여러 개면 목록을 보여주고 멈춘다(임의 선택 안 함).
|
||||||
|
#
|
||||||
|
# ★ 적재 방식 — 영역을 "하나씩" 개별 esptool 호출로 쓴다.
|
||||||
|
# eco2 보안 다운로드 모드(SDM)에서 여러 영역을 한 번에 쓰면 중간에
|
||||||
|
# "Failed to enter Flash download mode"로 실패한다(2026-06-22 실증 — www_fs가 빈 채 남아 부팅 루프).
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Port = "COM4", # USB-JTAG 포트 (.\list_ports.ps1 로 확인)
|
||||||
|
[string]$App = "", # 앱 bin 파일명 (firmware\ 안). 비우면 자동 선택
|
||||||
|
[switch]$AppOnly # 켜면 앱(0x20000)만 + otadata erase
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$fw = Join-Path $here "firmware"
|
||||||
|
|
||||||
|
$Python = "C:\Users\jyheo\.espressif\python_env\idf5.5_py3.13_env\Scripts\python.exe"
|
||||||
|
if (-not (Test-Path $Python)) { $Python = "python" }
|
||||||
|
|
||||||
|
# eco2 SDM 안정 레시피: --no-stub + 115200 + --after no_reset(이후 별도 hard_reset)
|
||||||
|
$baud = "115200"
|
||||||
|
$common = @("--chip","esp32p4","-p",$Port,"-b",$baud,
|
||||||
|
"--before","default_reset","--after","no_reset","--connect-attempts","30","--no-stub")
|
||||||
|
|
||||||
|
# ---- 앱 선택 (여러 개면 -App 으로 지정) ----------------------------------
|
||||||
|
$support = @("bootloader.bin","partition-table.bin","ota_data_initial.bin","www_fs.bin")
|
||||||
|
$apps = @(Get-ChildItem (Join-Path $fw "*.bin") -ErrorAction SilentlyContinue | Where-Object { $support -notcontains $_.Name })
|
||||||
|
if ($App) {
|
||||||
|
$appPath = Join-Path $fw $App
|
||||||
|
if (-not (Test-Path $appPath)) { Write-Host "✗ 앱 없음: $appPath" -ForegroundColor Red; exit 1 }
|
||||||
|
} elseif ($apps.Count -eq 1) {
|
||||||
|
$appPath = $apps[0].FullName
|
||||||
|
} elseif ($apps.Count -eq 0) {
|
||||||
|
Write-Host "✗ firmware\ 에 앱 bin 이 없습니다." -ForegroundColor Red; exit 1
|
||||||
|
} else {
|
||||||
|
Write-Host "여러 앱이 있습니다 — -App 으로 하나를 지정하세요:" -ForegroundColor Yellow
|
||||||
|
$apps | ForEach-Object { Write-Host " $($_.Name)" }
|
||||||
|
Write-Host "예: .\flash_app.ps1 -Port $Port -App $($apps[0].Name)" -ForegroundColor Gray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 영역별 개별 적재 함수 (SDM 다중영역 실패 회피) ----------------------
|
||||||
|
function Flash-Region([string]$off, [string]$file) {
|
||||||
|
Write-Host "[flash] $off $(Split-Path -Leaf $file)" -ForegroundColor Cyan
|
||||||
|
& $Python -m esptool @common write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m $off $file
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "✗ $off write 실패 (rc=$LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "============================================================" -ForegroundColor Yellow
|
||||||
|
Write-Host " DB720 APP FLASH (경우 B — 서명 부트로더 있음, brick-safe)" -ForegroundColor Yellow
|
||||||
|
Write-Host " Port=$Port App=$(Split-Path -Leaf $appPath) AppOnly=$AppOnly" -ForegroundColor Yellow
|
||||||
|
Write-Host " 부트로더(0x2000) 안 건드림 = eFuse 재굽힘/brick 없음" -ForegroundColor Green
|
||||||
|
Write-Host "============================================================" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
if ($AppOnly) {
|
||||||
|
Flash-Region "0x20000" $appPath
|
||||||
|
Write-Host "[otadata] erase 0x11000 0x2000 (ota_0 부팅)" -ForegroundColor Cyan
|
||||||
|
& $Python -m esptool @common erase_region "0x11000" "0x2000"
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "✗ otadata erase 실패 (rc=$LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||||
|
} else {
|
||||||
|
Flash-Region "0x10000" (Join-Path $fw "partition-table.bin")
|
||||||
|
Flash-Region "0x11000" (Join-Path $fw "ota_data_initial.bin")
|
||||||
|
Flash-Region "0x20000" $appPath
|
||||||
|
Flash-Region "0x420000" (Join-Path $fw "www_fs.bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "[reset] hard_reset (SDM 탈출)" -ForegroundColor Cyan
|
||||||
|
& $Python -c "from esptool.targets.esp32p4 import ESP32P4ROM; r=ESP32P4ROM('$Port'); r.connect('default_reset'); r.hard_reset(); print('hard_reset OK')"
|
||||||
|
|
||||||
|
Write-Host "✓ APP FLASH 완료 — 부팅 확인은 COM2(디버그 콘솔)에서 ![0081!] 응답/배너로." -ForegroundColor Green
|
||||||
76
flash_full.ps1
Normal file
76
flash_full.ps1
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# =====================================================================
|
||||||
|
# DB720 (ESP32-P4) 전체 적재 — 경우 A: 부트로더가 비었을 때 (full flash)
|
||||||
|
# =====================================================================
|
||||||
|
# 언제 쓰나
|
||||||
|
# - flash가 통째로 지워졌거나(erase_flash 후) 부트로더(0x2000)가 비어 부팅이 안 될 때.
|
||||||
|
# - 서명 부트로더까지 다시 써서 보드를 처음부터 살린다(서명 5종 전부).
|
||||||
|
#
|
||||||
|
# ★★★ eFuse 경고 (비가역) ★★★
|
||||||
|
# 이 빌드는 secure_RELEASE(실 eFuse 전제) 서명본이다.
|
||||||
|
# - 이미 secure boot eFuse가 구워진 보드(.230 등) → 같은 서명 부트로더를 다시 쓰는 것이라 안전(복구).
|
||||||
|
# - eFuse가 아직 안 구워진 보드 → 이걸 full flash하면 첫 부팅에 eFuse가 "영구로" 구워진다(비가역 brick 위험).
|
||||||
|
# => eFuse 미굽힘 보드의 최초 프로비저닝은 secure_DEV(가상 eFuse) 빌드로 해야 한다. 여기선 쓰지 말 것.
|
||||||
|
#
|
||||||
|
# ★ 앱이 여러 개일 때 — firmware\ 안의 앱 .bin 을 -App 으로 지정한다(미지정+여러개면 목록 후 멈춤).
|
||||||
|
# ★ 적재 방식 — 영역을 "하나씩" 개별 esptool 호출로 쓴다(SDM 다중영역 한 번에 쓰면 중간 실패, 2026-06-22 실증).
|
||||||
|
#
|
||||||
|
# 연결: USB-JTAG(보드의 COM4 자리). COM 번호 바뀌면 .\list_ports.ps1 로 확인.
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Port = "COM4",
|
||||||
|
[string]$App = "" # 앱 bin 파일명 (firmware\ 안). 비우면 자동 선택
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$fw = Join-Path $here "firmware"
|
||||||
|
|
||||||
|
$Python = "C:\Users\jyheo\.espressif\python_env\idf5.5_py3.13_env\Scripts\python.exe"
|
||||||
|
if (-not (Test-Path $Python)) { $Python = "python" }
|
||||||
|
|
||||||
|
$baud = "115200"
|
||||||
|
$common = @("--chip","esp32p4","-p",$Port,"-b",$baud,
|
||||||
|
"--before","default_reset","--after","no_reset","--connect-attempts","30","--no-stub")
|
||||||
|
|
||||||
|
# ---- 앱 선택 ------------------------------------------------------------
|
||||||
|
$support = @("bootloader.bin","partition-table.bin","ota_data_initial.bin","www_fs.bin")
|
||||||
|
$apps = @(Get-ChildItem (Join-Path $fw "*.bin") -ErrorAction SilentlyContinue | Where-Object { $support -notcontains $_.Name })
|
||||||
|
if ($App) {
|
||||||
|
$appPath = Join-Path $fw $App
|
||||||
|
if (-not (Test-Path $appPath)) { Write-Host "✗ 앱 없음: $appPath" -ForegroundColor Red; exit 1 }
|
||||||
|
} elseif ($apps.Count -eq 1) {
|
||||||
|
$appPath = $apps[0].FullName
|
||||||
|
} elseif ($apps.Count -eq 0) {
|
||||||
|
Write-Host "✗ firmware\ 에 앱 bin 이 없습니다." -ForegroundColor Red; exit 1
|
||||||
|
} else {
|
||||||
|
Write-Host "여러 앱이 있습니다 — -App 으로 하나를 지정하세요:" -ForegroundColor Yellow
|
||||||
|
$apps | ForEach-Object { Write-Host " $($_.Name)" }
|
||||||
|
Write-Host "예: .\flash_full.ps1 -Port $Port -App $($apps[0].Name)" -ForegroundColor Gray
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- 영역별 개별 적재 함수 ----------------------------------------------
|
||||||
|
function Flash-Region([string]$off, [string]$file) {
|
||||||
|
Write-Host "[flash] $off $(Split-Path -Leaf $file)" -ForegroundColor Cyan
|
||||||
|
& $Python -m esptool @common write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m $off $file
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Host "✗ $off write 실패 (rc=$LASTEXITCODE)" -ForegroundColor Red; exit 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "============================================================" -ForegroundColor Yellow
|
||||||
|
Write-Host " DB720 FULL FLASH (경우 A — 부트로더 비었을 때)" -ForegroundColor Yellow
|
||||||
|
Write-Host " Port=$Port App=$(Split-Path -Leaf $appPath) Python=$Python" -ForegroundColor Yellow
|
||||||
|
Write-Host " ★ eFuse 미굽힘 보드면 첫 부팅에 영구 굽힘 — 위 경고 확인" -ForegroundColor Red
|
||||||
|
Write-Host "============================================================" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# 서명 5종 — 각 영역 개별 호출 (부트로더 0x2000 포함)
|
||||||
|
Flash-Region "0x2000" (Join-Path $fw "bootloader.bin")
|
||||||
|
Flash-Region "0x10000" (Join-Path $fw "partition-table.bin")
|
||||||
|
Flash-Region "0x11000" (Join-Path $fw "ota_data_initial.bin")
|
||||||
|
Flash-Region "0x20000" $appPath
|
||||||
|
Flash-Region "0x420000" (Join-Path $fw "www_fs.bin")
|
||||||
|
|
||||||
|
Write-Host "[reset] hard_reset (SDM 탈출)" -ForegroundColor Cyan
|
||||||
|
& $Python -c "from esptool.targets.esp32p4 import ESP32P4ROM; r=ESP32P4ROM('$Port'); r.connect('default_reset'); r.hard_reset(); print('hard_reset OK')"
|
||||||
|
|
||||||
|
Write-Host "✓ FULL FLASH 완료 — 부팅 확인은 COM2(디버그 콘솔)에서 ![0081!] 응답/배너로." -ForegroundColor Green
|
||||||
10
list_ports.ps1
Normal file
10
list_ports.ps1
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# DB720 COM 포트 확인 — USB-JTAG(303A:1001) = 적재 포트, CP210x(10C4:EA60) = COM2 디버그
|
||||||
|
# 적재 포트는 COM 번호가 매번 바뀔 수 있으니 적재 전 이걸로 확인한다.
|
||||||
|
Get-CimInstance Win32_PnPEntity |
|
||||||
|
Where-Object { $_.Name -match 'COM\d' } |
|
||||||
|
Select-Object Name, @{N='DeviceID';E={$_.DeviceID}} |
|
||||||
|
Format-Table -AutoSize -Wrap
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "→ USB-JTAG (VID_303A&PID_1001) 또는 'USB 직렬 장치(COMx)' = 적재 포트(-Port COMx)" -ForegroundColor Cyan
|
||||||
|
Write-Host "→ Silicon Labs CP210x (COMx) = COM2 자리(디버그·문구 전송, 적재 중 열지 말 것)" -ForegroundColor Cyan
|
||||||
20
profiles/DB400S.flash.json
Normal file
20
profiles/DB400S.flash.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"_comment": "DB400S(또는 DB400Q) 펌웨어 폴더에 'flash.json'으로 복사해서 쓰세요. DB400Q면 name만 바꾸면 됩니다.",
|
||||||
|
"name": "DB400S (ESP32)",
|
||||||
|
"chip": "esp32",
|
||||||
|
"baud": 921600,
|
||||||
|
"noStub": false,
|
||||||
|
"perRegion": false,
|
||||||
|
"before": "default-reset",
|
||||||
|
"after": "hard-reset",
|
||||||
|
"appOnlyErasesOtadata": false,
|
||||||
|
"regions": [
|
||||||
|
{ "file": "bootloader.bin", "offset": "0x1000", "kind": "bootloader" },
|
||||||
|
{ "file": "partition-table.bin", "offset": "0x10000", "kind": "support" },
|
||||||
|
{ "file": "ota_data_initial.bin", "offset": "0x11000", "kind": "support" },
|
||||||
|
{ "app": true, "offset": "0x20000" }
|
||||||
|
],
|
||||||
|
"consolePortHint": "10C4:EA60",
|
||||||
|
"downloadModeHint": "TTL(COM2) 연결. SW1 누른 상태에서 SW2 눌렀다 떼고 SW1 떼면 다운로드 모드 진입.",
|
||||||
|
"notes": "esp32 클래식. 부트로더 0x1000. 스텁 921600 · 한 번에. secure 보드는 부트로더(0x8000 아래) 거부 → --force 또는 앱만."
|
||||||
|
}
|
||||||
20
profiles/DB402S.flash.json
Normal file
20
profiles/DB402S.flash.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"_comment": "DB402S 펌웨어 폴더에 'flash.json'으로 복사해서 쓰세요(이 _comment 줄은 지워도 됨). 그러면 앱이 폴더만 보고 이 설정대로 플래시합니다.",
|
||||||
|
"name": "DB402S (ESP32-S3)",
|
||||||
|
"chip": "esp32s3",
|
||||||
|
"baud": 921600,
|
||||||
|
"noStub": false,
|
||||||
|
"perRegion": false,
|
||||||
|
"before": "default-reset",
|
||||||
|
"after": "hard-reset",
|
||||||
|
"appOnlyErasesOtadata": false,
|
||||||
|
"regions": [
|
||||||
|
{ "file": "bootloader.bin", "offset": "0x0000", "kind": "bootloader" },
|
||||||
|
{ "file": "partition-table.bin", "offset": "0x10000", "kind": "support" },
|
||||||
|
{ "file": "ota_data_initial.bin", "offset": "0x11000", "kind": "support" },
|
||||||
|
{ "app": true, "offset": "0x20000" }
|
||||||
|
],
|
||||||
|
"consolePortHint": "10C4:EA60",
|
||||||
|
"downloadModeHint": "TTL(COM2) 연결. SW1 누른 상태에서 SW2 눌렀다 떼고 SW1 떼면 다운로드 모드 진입.",
|
||||||
|
"notes": "esp32-s3. 부트로더 offset 0x0000(중요). 스텁 921600 · 한 번에 기록. secure 보드는 부트로더(0x8000 아래) 거부 → --force 또는 앱만."
|
||||||
|
}
|
||||||
28
profiles/DB720.flash.json
Normal file
28
profiles/DB720.flash.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"_comment": "DB720 펌웨어 폴더에 'flash.json'으로 복사해서 쓰세요. (이 키트의 firmware\\ 폴더엔 이미 들어 있습니다 — 여기 건 참고/백업용 템플릿입니다.)",
|
||||||
|
"name": "DB720 (ESP32-P4)",
|
||||||
|
"chip": "esp32p4",
|
||||||
|
"baud": 115200,
|
||||||
|
"noStub": true,
|
||||||
|
"perRegion": true,
|
||||||
|
"connectAttempts": 30,
|
||||||
|
"flashSize": "16MB",
|
||||||
|
"flashMode": "dio",
|
||||||
|
"flashFreq": "80m",
|
||||||
|
"before": "default-reset",
|
||||||
|
"after": "hard-reset",
|
||||||
|
"appOnlyErasesOtadata": true,
|
||||||
|
"otadataOffset": "0x11000",
|
||||||
|
"otadataSize": "0x2000",
|
||||||
|
"regions": [
|
||||||
|
{ "file": "bootloader.bin", "offset": "0x2000", "kind": "bootloader" },
|
||||||
|
{ "file": "partition-table.bin", "offset": "0x10000", "kind": "support" },
|
||||||
|
{ "file": "ota_data_initial.bin", "offset": "0x11000", "kind": "support" },
|
||||||
|
{ "file": "www_fs.bin", "offset": "0x420000", "kind": "support" },
|
||||||
|
{ "app": true, "offset": "0x20000" }
|
||||||
|
],
|
||||||
|
"flashPortHint": "303A:1001",
|
||||||
|
"consolePortHint": "10C4:EA60",
|
||||||
|
"downloadModeHint": "USB-JTAG 케이블 연결. 포트가 안 잡히면 케이블만 다시 꽂으면 됩니다(자동 감지).",
|
||||||
|
"notes": "secure_release · db250513 서명. 영역별 개별 호출(SDM). 부트로더(0x2000)는 이미 구워진 보드면 거부될 수 있음(정상 → 표준/앱만)."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user