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:
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.
Reference in New Issue
Block a user