gudae 2 weeks ago
parent
commit
b050fc1dd1
  1. BIN
      .vs/Housing/DesignTimeBuild/.dtbcache.v2
  2. 4
      Database.ini
  3. 27
      EquipmentSeparated/DMM.ini
  4. 155
      EquipmentSeparated/DMM_Code.md
  5. 37
      EquipmentSeparated/Power Supply.ini
  6. 144
      EquipmentSeparated/Power Supply_Code.md
  7. 2
      Hardware.ini
  8. 3
      Housing.csproj
  9. 8
      Housing.sln
  10. BIN
      Housing_UserManual/Housing_User_Manual_CodeAligned_v1.0.docx
  11. 20
      Login/StartupLoginWindow.xaml.cs
  12. 1
      MainWindow.xaml
  13. 162
      MainWindow.xaml.cs
  14. 29
      Services/BoardTestService.cs
  15. 10
      Services/EquipmentMeasurementService.cs
  16. 46
      Services/HousingAssemblyRepository.cs
  17. 36
      Services/SerialBoardClient.cs
  18. BIN
      Setup (2).zip
  19. BIN
      Setup (3).zip
  20. BIN
      Setup (4).zip
  21. BIN
      Setup (5).zip
  22. BIN
      Setup (6).zip
  23. BIN
      Setup (7).zip
  24. BIN
      Setup (8).zip
  25. BIN
      Setup (9).zip
  26. BIN
      Setup.zip
  27. BIN
      Setup/Release (2).zip
  28. BIN
      Setup/Release.zip
  29. BIN
      Setup/Release/Setup.msi
  30. BIN
      Setup100/Release/Setup100.msi
  31. BIN
      Setup100/Release/setup.exe
  32. 733
      Setup100/Setup100.vdproj
  33. BIN
      housing-result-ng.png
  34. BIN
      housing-result-ok.png
  35. 30
      init.sql
  36. 12
      publish/setup-win-x64/Database.ini
  37. 2
      publish/setup-win-x64/Hardware.ini
  38. 30
      publish/setup-win-x64/init.sql
  39. 12
      tools/ManualScreenCapture/ManualScreenCapture.csproj
  40. 216
      tools/ManualScreenCapture/Program.cs
  41. 1334
      tools/generate_housing_user_manual.py
  42. BIN
      tools/manual-screens/01-login.png
  43. BIN
      tools/manual-screens/02-waiting-barcode.png
  44. BIN
      tools/manual-screens/03-wait-start-signal.png
  45. BIN
      tools/manual-screens/04-board-test.png
  46. BIN
      tools/manual-screens/05-result-ok.png
  47. BIN
      tools/manual-screens/06-result-ng.png
  48. BIN
      tools/manual-screens/07-db-save-error.png

BIN
.vs/Housing/DesignTimeBuild/.dtbcache.v2

Binary file not shown.

4
Database.ini

@ -3,9 +3,9 @@ IP=192.168.10.10,1433
Database=NE1aW_PT_Sensor
DbId=su_user
DbPw=amotech821
Encrypt=True
Encrypt=False
TrustServerCertificate=True
Timeout=5
Timeout=15
[Login]
Procedure=dbo.CheckOperator

27
EquipmentSeparated/DMM.ini

@ -0,0 +1,27 @@
[Equipment]
Timeout=5000
SettleMilliseconds=300
[DMM]
Model=Keysight 34465A
Measurement=Voltage
Connection=LAN
Resource=
IdnMatch=34465A
Host=192.168.10.13
Port=5025
PortName=
BaudRate=115200
Terminator=LF
SetupCommand=
CleanupCommand=
ReadCommand=MEAS:VOLT:DC?
[OriginalHardwareIniKeys]
Connection=VoltageConnection
Resource=VoltageResource
IdnMatch=VoltageIdnMatch
Host=VoltageHost
Port=VoltagePort
SetupCommand=VoltageSetupCommand
ReadCommand=VoltageReadCommand

155
EquipmentSeparated/DMM_Code.md

@ -0,0 +1,155 @@
# DMM Related Code
This file is a reference-only extraction. It is not compiled by the Housing app.
## Source Files
- `Services/EquipmentMeasurementSettings.cs`
- `Services/EquipmentMeasurementService.cs`
- `Services/IScpiClient.cs`
- `Services/TcpScpiClient.cs`
- `Services/SerialScpiClient.cs`
- `Services/VisaScpiClient.cs`
## Hardware.ini Keys
```ini
[Equipment]
Timeout=5000
SettleMilliseconds=300
; 34465A DMM
VoltageConnection=LAN
VoltageResource=
VoltageIdnMatch=34465A
VoltageHost=192.168.10.13
VoltagePort=5025
VoltageSetupCommand=
VoltageReadCommand=MEAS:VOLT:DC?
```
## Settings Load Code
```csharp
Voltage = LoadChannel(
values,
"Voltage",
"MEAS:VOLT:DC?",
"34460,34461,34465,34470,3446,3447");
```
```csharp
private static ScpiChannelSettings LoadChannel(
Dictionary<string, string> values,
string prefix,
string defaultReadCommand,
string defaultIdnMatch)
{
return new ScpiChannelSettings
{
Connection = GetString(values, prefix + "Connection", string.Empty),
ResourceName = GetString(values, prefix + "Resource", string.Empty),
IdnMatch = GetString(values, prefix + "IdnMatch", defaultIdnMatch),
Host = GetString(values, prefix + "Host", string.Empty),
Port = GetInt(values, prefix + "Port", 5025),
PortName = GetString(values, prefix + "PortName", string.Empty),
BaudRate = GetInt(values, prefix + "BaudRate", 115200),
Terminator = GetString(values, prefix + "Terminator", string.Empty),
SetupCommand = GetString(values, prefix + "SetupCommand", string.Empty),
CleanupCommand = GetString(values, prefix + "CleanupCommand", string.Empty),
ReadCommand = GetString(values, prefix + "ReadCommand", defaultReadCommand)
};
}
```
## Voltage Read Flow
```csharp
var voltage = await ReadChannelAsync("VOLTAGE", _settings.Voltage, log);
```
```csharp
private async Task<decimal> ReadChannelAsync(
string label,
ScpiChannelSettings channel,
StringBuilder log,
bool skipSetupCommand = false)
{
if (IsDisabled(channel))
{
log.AppendLine($"> {label} SKIP: disabled in Hardware.ini");
return 0m;
}
if (string.IsNullOrWhiteSpace(channel.ReadCommand))
{
throw new InvalidOperationException($"Hardware.ini [Equipment] {label} ReadCommand 값을 확인하세요.");
}
using var client = await CreateClientAsync(label, channel);
return await ReadChannelWithClientAsync(label, channel, client, log, skipSetupCommand);
}
```
```csharp
private static async Task<decimal> ReadChannelWithClientAsync(
string label,
ScpiChannelSettings channel,
IScpiClient client,
StringBuilder log,
bool skipSetupCommand = false)
{
if (string.IsNullOrWhiteSpace(channel.ReadCommand))
{
throw new InvalidOperationException($"Hardware.ini [Equipment] {label} ReadCommand 값을 확인하세요.");
}
log.AppendLine($"> {label} CONNECT: {DescribeClient(client, channel)}");
if (!skipSetupCommand && !string.IsNullOrWhiteSpace(channel.SetupCommand))
{
log.AppendLine($"> {label} SETUP: {channel.SetupCommand}");
await SendCommandListAsync(client, channel.SetupCommand);
}
log.AppendLine($"> {label} READ: {channel.ReadCommand}");
var response = await client.QueryAsync(channel.ReadCommand);
log.AppendLine(response);
return ExtractFirstDecimal(response, label);
}
```
## Shared SCPI Connection Code
```csharp
private Task<IScpiClient> CreateClientAsync(string label, ScpiChannelSettings channel)
{
var connection = GetConnectionType(channel);
return connection switch
{
"TCP" or "LAN" or "ETHERNET" => CreateTcpClientAsync(label, channel),
"SERIAL" or "COM" => Task.FromResult<IScpiClient>(
new SerialScpiClient(
channel.PortName,
channel.BaudRate,
_settings.Timeout,
channel.GetIdnMatches(),
channel.GetTerminator())),
"VISA" or "USB" => CreateVisaClientAsync(channel),
_ => throw new InvalidOperationException($"{label} 장비 연결 방식은 Visa, Serial, Tcp 중 하나로 설정하세요.")
};
}
```
```csharp
private static decimal ExtractFirstDecimal(string response, string label)
{
var match = Regex.Match(response, @"[-+]?\d+(?:\.\d+)?(?:[Ee][-+]?\d+)?");
if (!match.Success)
{
throw new InvalidOperationException($"{label} 측정값 파싱 실패");
}
return decimal.Parse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture);
}
```

37
EquipmentSeparated/Power Supply.ini

@ -0,0 +1,37 @@
[Equipment]
Timeout=5000
SettleMilliseconds=300
[PowerSupply]
Model=Keysight E36233A
Measurement=Current
PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2)
PreBoardDelayMilliseconds=2000
LogoutCommand=OUTP OFF, (@1)
Connection=LAN
Resource=
IdnMatch=E36233A
Host=192.168.10.12
Port=5025
PortName=
BaudRate=9600
Terminator=LF
SetupCommand=OUTP ON, (@1)
CleanupCommand=OUTP OFF, (@1)
ReadCommand=MEAS:CURR? (@1)
[OriginalHardwareIniKeys]
PreBoardCommand=PreBoardCommand
PreBoardDelayMilliseconds=PreBoardDelayMilliseconds
LogoutCommand=LogoutCommand
Connection=CurrentConnection
Resource=CurrentResource
IdnMatch=CurrentIdnMatch
Host=CurrentHost
Port=CurrentPort
PortName=CurrentPortName
BaudRate=CurrentBaudRate
Terminator=CurrentTerminator
SetupCommand=CurrentSetupCommand
CleanupCommand=CurrentCleanupCommand
ReadCommand=CurrentReadCommand

144
EquipmentSeparated/Power Supply_Code.md

@ -0,0 +1,144 @@
# Power Supply Related Code
This file is a reference-only extraction. It is not compiled by the Housing app.
## Source Files
- `Services/EquipmentMeasurementSettings.cs`
- `Services/EquipmentMeasurementService.cs`
- `Services/IScpiClient.cs`
- `Services/TcpScpiClient.cs`
- `Services/SerialScpiClient.cs`
- `Services/VisaScpiClient.cs`
## Hardware.ini Keys
```ini
[Equipment]
Timeout=5000
SettleMilliseconds=300
PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2)
PreBoardDelayMilliseconds=2000
LogoutCommand=OUTP OFF, (@1)
; E36233A Power Supply
CurrentConnection=LAN
CurrentResource=
CurrentIdnMatch=E36233A
CurrentHost=192.168.10.12
CurrentPort=5025
CurrentPortName=
CurrentBaudRate=9600
CurrentTerminator=LF
CurrentSetupCommand=OUTP ON, (@1)
CurrentCleanupCommand=OUTP OFF, (@1)
CurrentReadCommand=MEAS:CURR? (@1)
```
## Settings Load Code
```csharp
PreBoardCommand = GetString(values, "PreBoardCommand", string.Empty);
PreBoardDelayMilliseconds = GetInt(values, "PreBoardDelayMilliseconds", 0);
LogoutCommand = GetString(values, "LogoutCommand", string.Empty);
Current = LoadChannel(values, "Current", "MEAS:CURR? CH1", "E362");
```
## Pre-Board Output Control
```csharp
public async Task<string> RunPreBoardCommandAsync()
{
var log = new StringBuilder();
if (IsDisabled(_settings.Current) || string.IsNullOrWhiteSpace(_settings.PreBoardCommand))
{
return string.Empty;
}
using var client = await CreateClientAsync("CURRENT", _settings.Current);
log.AppendLine($"> CURRENT PRE-BOARD CONNECT: {DescribeClient(client, _settings.Current)}");
log.AppendLine($"> CURRENT PRE-BOARD SETUP: {_settings.PreBoardCommand}");
await SendCommandListAsync(client, _settings.PreBoardCommand);
return log.ToString();
}
```
## Current Read Flow
```csharp
if (!IsDisabled(_settings.Current) && !string.IsNullOrWhiteSpace(_settings.Current.SetupCommand))
{
currentClient = await CreateClientAsync("CURRENT", _settings.Current);
log.AppendLine($"> CURRENT PRE-READ CONNECT: {DescribeClient(currentClient, _settings.Current)}");
log.AppendLine($"> CURRENT PRE-READ SETUP: {_settings.Current.SetupCommand}");
await SendCommandListAsync(currentClient, _settings.Current.SetupCommand);
}
var current = currentClient is null
? await ReadChannelAsync("CURRENT", _settings.Current, log)
: await ReadChannelWithClientAsync("CURRENT", _settings.Current, currentClient, log, skipSetupCommand: true);
```
## Cleanup And Logout
```csharp
private static async Task SendCleanupCommandAsync(IScpiClient? client, ScpiChannelSettings channel, StringBuilder log)
{
if (client is null || string.IsNullOrWhiteSpace(channel.CleanupCommand))
{
return;
}
try
{
log.AppendLine($"> CURRENT CLEANUP: {channel.CleanupCommand}");
await SendCommandListAsync(client, channel.CleanupCommand);
}
catch (Exception ex)
{
log.AppendLine($"> CURRENT CLEANUP FAILED: {ex.Message}");
}
}
```
```csharp
public async Task<string> RunLogoutCommandAsync()
{
var log = new StringBuilder();
if (IsDisabled(_settings.Current) || string.IsNullOrWhiteSpace(_settings.LogoutCommand))
{
return string.Empty;
}
using var client = await CreateClientAsync("CURRENT", _settings.Current);
log.AppendLine($"> CURRENT LOGOUT CONNECT: {DescribeClient(client, _settings.Current)}");
log.AppendLine($"> CURRENT LOGOUT: {_settings.LogoutCommand}");
await SendCommandListAsync(client, _settings.LogoutCommand);
return log.ToString();
}
```
## Protected Power-Off Command Guard
```csharp
private static async Task SendCommandListAsync(IScpiClient client, string commands)
{
foreach (var command in SplitCommands(commands))
{
if (IsProtectedPowerOffCommand(command))
{
continue;
}
await client.SendAsync(command);
await Task.Delay(100);
}
}
```
```csharp
private static bool IsProtectedPowerOffCommand(string command)
{
return Regex.IsMatch(command, @"^\s*OUTP(?:UT)?\s+OFF\s*,?\s*\(@2\)\s*$", RegexOptions.IgnoreCase);
}
```

2
Hardware.ini

@ -39,7 +39,7 @@ Timeout=5000
SettleMilliseconds=300
PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2)
PreBoardDelayMilliseconds=2000
LogoutCommand=OUTP OFF, (@1);OUTP OFF, (@2)
LogoutCommand=OUTP OFF, (@1)
; 34465A DMM
VoltageConnection=LAN

3
Housing.csproj

@ -12,6 +12,9 @@
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="System.IO.Ports" Version="9.0.10" />
</ItemGroup>
<ItemGroup>
<Compile Remove="tools\ManualScreenCapture\**\*.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\mobidigm-logo.png" />
<Resource Include="Resources\mobidigm-logo.png" />

8
Housing.sln

@ -11,6 +11,8 @@ Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup0", "Setup0\Setup0.vdp
EndProject
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup", "Setup\Setup.vdproj", "{DBE9999D-346E-CA63-6964-F3B7FC1D42B4}"
EndProject
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup100", "Setup100\Setup100.vdproj", "{935631E6-D737-F794-5DC8-B9B7DCD816ED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -51,6 +53,12 @@ Global
{DBE9999D-346E-CA63-6964-F3B7FC1D42B4}.Release|Any CPU.ActiveCfg = Release
{DBE9999D-346E-CA63-6964-F3B7FC1D42B4}.Release|x64.ActiveCfg = Release
{DBE9999D-346E-CA63-6964-F3B7FC1D42B4}.Release|x86.ActiveCfg = Release
{935631E6-D737-F794-5DC8-B9B7DCD816ED}.Debug|Any CPU.ActiveCfg = Debug
{935631E6-D737-F794-5DC8-B9B7DCD816ED}.Debug|x64.ActiveCfg = Debug
{935631E6-D737-F794-5DC8-B9B7DCD816ED}.Debug|x86.ActiveCfg = Debug
{935631E6-D737-F794-5DC8-B9B7DCD816ED}.Release|Any CPU.ActiveCfg = Release
{935631E6-D737-F794-5DC8-B9B7DCD816ED}.Release|x64.ActiveCfg = Release
{935631E6-D737-F794-5DC8-B9B7DCD816ED}.Release|x86.ActiveCfg = Release
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

BIN
Housing_UserManual/Housing_User_Manual_CodeAligned_v1.0.docx

Binary file not shown.

20
Login/StartupLoginWindow.xaml.cs

@ -446,23 +446,17 @@ public partial class StartupLoginWindow : Window
private static string GetLoginOptionsPath()
{
var directory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Housing");
var filePath = Path.Combine(directory, "LoginOptions.json");
if (File.Exists(filePath))
{
return filePath;
}
Directory.CreateDirectory(directory);
var bundledPath = Path.Combine(AppContext.BaseDirectory, "LoginOptions.json");
if (File.Exists(bundledPath))
{
File.Copy(bundledPath, filePath);
return bundledPath;
}
else
var directory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Housing");
var filePath = Path.Combine(directory, "LoginOptions.json");
if (!File.Exists(filePath))
{
LoginSelectionOptions.CreateDefaultFile(filePath);
}

1
MainWindow.xaml

@ -486,6 +486,7 @@
PreviewKeyDown="BarcodeTextBox_PreviewKeyDown"
PreviewTextInput="BarcodeTextBox_PreviewTextInput"
InputMethod.IsInputMethodEnabled="False"
LostKeyboardFocus="BarcodeTextBox_LostKeyboardFocus"
TextChanged="BarcodeTextBox_TextChanged"/>
</Grid>
</Border>

162
MainWindow.xaml.cs

@ -56,6 +56,7 @@ public partial class MainWindow : Window
_scannerInputCommitTimer = new DispatcherTimer { Interval = ScannerInputIdleCommitDelay };
_scannerInputCommitTimer.Tick += ScannerInputCommitTimer_Tick;
CommandManager.AddPreviewExecutedHandler(BarcodeTextBox, BarcodeTextBox_PreviewExecuted);
AttachJudgementCriteriaEnterFocus();
ApplyInspectionSettingsToTextBoxes();
ApplyLoginResult();
@ -87,6 +88,12 @@ public partial class MainWindow : Window
base.OnClosed(e);
}
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
KeepBarcodeFocusIfEmpty();
}
private void BarcodeTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (_suppressBarcodeStatusUpdate)
@ -99,6 +106,7 @@ public partial class MainWindow : Window
IcSnOutputTextBox.Clear();
SetProcessStatus("WAITING BARCODE", "HmiAccentBrush");
ShowResultStatusMessage(BarcodeRequiredMessage);
KeepBarcodeFocusIfEmpty();
return;
}
@ -106,6 +114,16 @@ public partial class MainWindow : Window
ShowResultStatusMessage(BoardReadyMessage);
}
private void BarcodeTextBox_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (IsBarcodeFocusExceptionTarget(e.NewFocus))
{
return;
}
KeepBarcodeFocusIfEmpty();
}
private void BarcodeTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = true;
@ -380,7 +398,7 @@ public partial class MainWindow : Window
var resultCode = inspectionPass ? "OK" : "NG";
SetProcessStatus("DB SAVING", "HmiWarningBrush");
var dbMeasurementSaveError = await SaveMeasurementStatusAsync(resultCode);
var dbMeasurementSaveError = await SaveInspectionResultAsync(measurement, resultCode);
if (!string.IsNullOrWhiteSpace(dbMeasurementSaveError))
{
SetProcessStatus("DB SAVE ERROR", "HmiDangerBrush");
@ -400,8 +418,16 @@ public partial class MainWindow : Window
}
catch (Exception ex)
{
var shutdownError = await TryRunEquipmentLogoutCommandAsync();
SetProcessStatus("ERROR", "HmiDangerBrush");
ShowResultStatusMessage($"보드 테스트 실패: {ex.Message}");
var errorMessage = $"보드 테스트 실패: {ex.Message}";
if (!string.IsNullOrWhiteSpace(shutdownError))
{
errorMessage += $"\r\nPower supply OFF failed: {shutdownError}";
}
ShowResultStatusMessage(errorMessage);
}
finally
{
@ -458,16 +484,26 @@ public partial class MainWindow : Window
}
private async Task RunEquipmentLogoutCommandAsync()
{
var error = await TryRunEquipmentLogoutCommandAsync();
if (!string.IsNullOrWhiteSpace(error))
{
MessageBox.Show(this, $"파워서플라이 logout 전원 OFF 실패: {error}", "Logout", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private async Task<string?> TryRunEquipmentLogoutCommandAsync()
{
try
{
var equipmentSettings = EquipmentMeasurementSettings.Load(GetHardwareIniPath());
var equipmentService = new EquipmentMeasurementService(equipmentSettings);
await equipmentService.RunLogoutCommandAsync();
return null;
}
catch (Exception ex)
{
MessageBox.Show(this, $"파워서플라이 logout 전원 OFF 실패: {ex.Message}", "Logout", MessageBoxButton.OK, MessageBoxImage.Warning);
return ex.Message;
}
}
@ -521,18 +557,108 @@ public partial class MainWindow : Window
AMinTextBox.IsEnabled = isEnabled;
AMaxTextBox.IsEnabled = isEnabled;
LogoutButton.IsEnabled = isEnabled;
if (isEnabled)
{
KeepBarcodeFocusIfEmpty();
}
}
private void AttachJudgementCriteriaEnterFocus()
{
foreach (var textBox in GetJudgementCriteriaTextBoxes())
{
textBox.PreviewKeyDown += JudgementCriteriaTextBox_PreviewKeyDown;
}
}
private void JudgementCriteriaTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter && e.Key != Key.Return)
{
return;
}
e.Handled = true;
FocusBarcodeInput(force: true);
}
private void KeepBarcodeFocusIfEmpty()
{
if (!IsActive ||
!BarcodeTextBox.IsEnabled ||
!BarcodeTextBox.IsVisible ||
IsBarcodeFocusExceptionActive() ||
!string.IsNullOrWhiteSpace(BarcodeTextBox.Text))
{
return;
}
FocusBarcodeInput();
}
private void FocusBarcodeInput()
private void FocusBarcodeInput(bool force = false)
{
Dispatcher.BeginInvoke(new Action(() =>
{
if (!BarcodeTextBox.IsEnabled ||
!BarcodeTextBox.IsVisible ||
(!force && IsBarcodeFocusExceptionActive()))
{
return;
}
BarcodeTextBox.Focus();
Keyboard.Focus(BarcodeTextBox);
BarcodeTextBox.SelectAll();
}), DispatcherPriority.Input);
}
private bool IsBarcodeFocusExceptionTarget(IInputElement? focusTarget)
{
return focusTarget is DependencyObject dependencyObject &&
GetBarcodeFocusExceptionControls()
.Any(control => IsVisualOrLogicalChildOf(dependencyObject, control));
}
private bool IsBarcodeFocusExceptionActive()
{
return GetBarcodeFocusExceptionControls()
.Any(control => control.IsKeyboardFocusWithin);
}
private IEnumerable<Control> GetBarcodeFocusExceptionControls()
{
yield return LogoutButton;
foreach (var textBox in GetJudgementCriteriaTextBoxes())
{
yield return textBox;
}
}
private IEnumerable<TextBox> GetJudgementCriteriaTextBoxes()
{
yield return VMinTextBox;
yield return VMaxTextBox;
yield return AMinTextBox;
yield return AMaxTextBox;
}
private static bool IsVisualOrLogicalChildOf(DependencyObject dependencyObject, DependencyObject parent)
{
for (var current = dependencyObject; current is not null;)
{
if (ReferenceEquals(current, parent))
{
return true;
}
current = VisualTreeHelper.GetParent(current) ?? LogicalTreeHelper.GetParent(current);
}
return false;
}
private void SetProcessStatus(string status, string brushResourceKey)
{
var brush = (Brush)FindResource(brushResourceKey);
@ -696,19 +822,27 @@ public partial class MainWindow : Window
ClearPendingScannerInput();
}
private async Task<string?> SaveMeasurementStatusAsync(string resultCode)
private async Task<string?> SaveInspectionResultAsync(BoardMeasurementResult measurement, string resultCode)
{
var databaseIniPath = GetDatabaseIniPath();
DatabaseSettings? databaseSettings = null;
try
{
var repository = new HousingAssemblyRepository(DatabaseSettings.Load(GetDatabaseIniPath()));
var record = CreateMeasurementRecord(resultCode);
databaseSettings = DatabaseSettings.Load(databaseIniPath);
var repository = new HousingAssemblyRepository(databaseSettings);
var record = CreateInspectionRecord(measurement, resultCode);
await repository.UpsertInspectionAsync(record);
return null;
}
catch (Exception ex)
{
return ex.Message;
var target = databaseSettings is null
? databaseIniPath
: $"{databaseSettings.Ip} / {databaseSettings.Database} (설정: {databaseIniPath})";
return $"{ex.Message}\r\nDB target: {target}";
}
}
@ -732,10 +866,12 @@ public partial class MainWindow : Window
};
}
private HousingAssemblyRecord CreateMeasurementRecord(string resultCode)
private HousingAssemblyRecord CreateInspectionRecord(BoardMeasurementResult measurement, string resultCode)
{
var barcode = BarcodeTextBox.Text.Trim();
var icSn = IcSnOutputTextBox.Text.Trim();
var icSn = string.IsNullOrWhiteSpace(measurement.IcSn)
? IcSnOutputTextBox.Text.Trim()
: measurement.IcSn.Trim();
if (string.IsNullOrWhiteSpace(icSn))
{
@ -756,8 +892,8 @@ public partial class MainWindow : Window
Line = _loginResult.LineNo,
LotNo = _loginResult.LotNo,
JigNo = _loginResult.JigNo,
PtVol1 = _lastMeasuredVoltage ?? ParseDecimal(VOutputTextBox.Text),
PtCurrent1 = ParseDecimal(AOutputTextBox.Text),
PtVol1 = measurement.Voltage,
PtCurrent1 = AmpsToMilliamps(measurement.Current),
Result = resultCode
};
}
@ -844,7 +980,7 @@ public partial class MainWindow : Window
private static string FormatCurrent(decimal value)
{
return AmpsToMilliamps(value).ToString("0.000", CultureInfo.InvariantCulture);
return AmpsToMilliamps(value).ToString("0.00", CultureInfo.InvariantCulture);
}
private static string FormatCurrentLimit(decimal value)

29
Services/BoardTestService.cs

@ -32,6 +32,9 @@ public sealed class BoardTestService
using var boardClient = new SerialBoardClient(_boardSettings);
log.AppendLine($"> BOARD PORT: {boardClient.PortName} @ {_boardSettings.BaudRate}");
var postReadIdCommandSent = false;
try
{
await SendOnlyAndLogAsync(boardClient, "PRE_CONNECT", _boardSettings.PreConnectCommand, log);
var connectResponse = await SendAndLogAsync(boardClient, "CONNECT", _boardSettings.ConnectCommand, _boardSettings.ReadTimeout, true, log);
@ -48,6 +51,7 @@ public sealed class BoardTestService
}
await SendOnlyAndLogAsync(boardClient, "POST_READ_ID", _boardSettings.PostReadIdCommand, log);
postReadIdCommandSent = true;
if (onIcSnReadAsync is not null)
{
@ -77,6 +81,31 @@ public sealed class BoardTestService
RawLog = log.ToString()
};
}
finally
{
if (!postReadIdCommandSent)
{
await SendFinalBoardOffAsync(boardClient, log);
}
}
}
private async Task SendFinalBoardOffAsync(SerialBoardClient boardClient, StringBuilder log)
{
if (string.IsNullOrWhiteSpace(_boardSettings.PostReadIdCommand))
{
return;
}
try
{
await SendOnlyAndLogAsync(boardClient, "FINAL_OFF", _boardSettings.PostReadIdCommand, log);
}
catch (Exception ex)
{
log.AppendLine($"> BOARD FINAL_OFF FAILED: {ex.Message}");
}
}
private static async Task<string> SendAndLogAsync(
SerialBoardClient client,

10
Services/EquipmentMeasurementService.cs

@ -257,6 +257,11 @@ public sealed class EquipmentMeasurementService
{
foreach (var command in SplitCommands(commands))
{
if (IsProtectedPowerOffCommand(command))
{
continue;
}
await client.SendAsync(command);
await Task.Delay(100);
}
@ -270,6 +275,11 @@ public sealed class EquipmentMeasurementService
.Where(command => !string.IsNullOrWhiteSpace(command));
}
private static bool IsProtectedPowerOffCommand(string command)
{
return Regex.IsMatch(command, @"^\s*OUTP(?:UT)?\s+OFF\s*,?\s*\(@2\)\s*$", RegexOptions.IgnoreCase);
}
private static bool IsDisabled(ScpiChannelSettings channel)
{
var connection = channel.Connection.Trim();

46
Services/HousingAssemblyRepository.cs

@ -16,7 +16,7 @@ public sealed class HousingAssemblyRepository
public async Task InsertHeaderAsync(HousingAssemblyRecord record)
{
using var connection = new SqlConnection(CreateConnectionString());
using var command = connection.CreateCommand();
using var command = CreateCommand(connection);
command.CommandText = @"
INSERT INTO dbo.Housing_Assembly
@ -56,7 +56,7 @@ VALUES
AddNullableNVarChar(command, "@Variant_2", 10, record.Variant2);
AddNVarChar(command, "@Operator", 10, record.Operator);
command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate });
AddNVarChar(command, "@Line", 5, record.Line);
AddNVarChar(command, "@Line", 10, record.Line);
AddNVarChar(command, "@Lot_No", 8, record.LotNo);
AddNVarChar(command, "@Jig_No", 10, record.JigNo);
@ -67,7 +67,7 @@ VALUES
public async Task UpsertHeaderAsync(HousingAssemblyRecord record)
{
using var connection = new SqlConnection(CreateConnectionString());
using var command = connection.CreateCommand();
using var command = CreateCommand(connection);
command.CommandText = @"
SET XACT_ABORT ON;
@ -131,7 +131,7 @@ COMMIT TRANSACTION;";
AddNullableNVarChar(command, "@Variant_2", 10, record.Variant2);
AddNVarChar(command, "@Operator", 10, record.Operator);
command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate });
AddNVarChar(command, "@Line", 5, record.Line);
AddNVarChar(command, "@Line", 10, record.Line);
AddNVarChar(command, "@Lot_No", 8, record.LotNo);
AddNVarChar(command, "@Jig_No", 10, record.JigNo);
@ -142,7 +142,7 @@ COMMIT TRANSACTION;";
public async Task UpdateMeasurementAsync(HousingAssemblyRecord record)
{
using var connection = new SqlConnection(CreateConnectionString());
using var command = connection.CreateCommand();
using var command = CreateCommand(connection);
command.CommandText = @"
UPDATE dbo.Housing_Assembly
@ -154,8 +154,8 @@ WHERE
[IC_SN] = @IC_SN;";
AddNVarChar(command, "@IC_SN", 50, record.IcSn);
AddDecimal(command, "@PT_Vol_1", record.PtVol1);
AddDecimal(command, "@PT_Current_1", record.PtCurrent1);
AddDecimal(command, "@PT_Vol_1", record.PtVol1, 4, 2);
AddDecimal(command, "@PT_Current_1", record.PtCurrent1, 4, 2);
AddNVarChar(command, "@Result", 5, record.Result);
await connection.OpenAsync();
@ -174,7 +174,7 @@ WHERE
}
using var connection = new SqlConnection(CreateConnectionString());
using var command = connection.CreateCommand();
using var command = CreateCommand(connection);
command.CommandText = @"
SET XACT_ABORT ON;
@ -247,11 +247,11 @@ COMMIT TRANSACTION;";
AddNullableNVarChar(command, "@Variant_2", 10, record.Variant2);
AddNVarChar(command, "@Operator", 10, record.Operator);
command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate });
AddNVarChar(command, "@Line", 5, record.Line);
AddNVarChar(command, "@Line", 10, record.Line);
AddNVarChar(command, "@Lot_No", 8, record.LotNo);
AddNVarChar(command, "@Jig_No", 10, record.JigNo);
AddDecimal(command, "@PT_Vol_1", record.PtVol1);
AddDecimal(command, "@PT_Current_1", record.PtCurrent1);
AddDecimal(command, "@PT_Vol_1", record.PtVol1, 4, 2);
AddDecimal(command, "@PT_Current_1", record.PtCurrent1, 4, 2);
AddNVarChar(command, "@Result", 5, record.Result);
await connection.OpenAsync();
@ -263,6 +263,17 @@ COMMIT TRANSACTION;";
await UpsertInspectionAsync(record);
}
public async Task WarmUpAsync()
{
using var connection = new SqlConnection(CreateConnectionString());
using var command = CreateCommand(connection);
command.CommandText = "SELECT 1;";
await connection.OpenAsync();
await command.ExecuteScalarAsync();
}
private string CreateConnectionString()
{
if (string.IsNullOrWhiteSpace(_settings.Ip) ||
@ -286,6 +297,13 @@ COMMIT TRANSACTION;";
return builder.ConnectionString;
}
private SqlCommand CreateCommand(SqlConnection connection)
{
var command = connection.CreateCommand();
command.CommandTimeout = Math.Max(1, _settings.Timeout);
return command;
}
private static void AddNVarChar(SqlCommand command, string name, int size, string value)
{
command.Parameters.Add(new SqlParameter(name, SqlDbType.NVarChar, size) { Value = value });
@ -299,12 +317,12 @@ COMMIT TRANSACTION;";
});
}
private static void AddDecimal(SqlCommand command, string name, decimal value)
private static void AddDecimal(SqlCommand command, string name, decimal value, byte precision, byte scale)
{
var parameter = new SqlParameter(name, SqlDbType.Decimal)
{
Precision = 3,
Scale = 2,
Precision = precision,
Scale = scale,
Value = value
};

36
Services/SerialBoardClient.cs

@ -6,6 +6,8 @@ namespace Housing.Services;
public sealed class SerialBoardClient : IDisposable
{
private const int SendOnlyDrainTimeoutMilliseconds = 1000;
private const int SendOnlyDrainIdleMilliseconds = 100;
private readonly BoardHardwareSettings _settings;
private readonly SerialPort _serialPort;
@ -46,6 +48,7 @@ public sealed class SerialBoardClient : IDisposable
Open();
_serialPort.DiscardInBuffer();
_serialPort.Write(command.Trim() + "\r\n");
DrainInputBuffer(SendOnlyDrainTimeoutMilliseconds, SendOnlyDrainIdleMilliseconds);
});
}
@ -201,6 +204,39 @@ public sealed class SerialBoardClient : IDisposable
return response.ToString();
}
private void DrainInputBuffer(int timeoutMilliseconds, int idleMilliseconds)
{
var originalReadTimeout = _serialPort.ReadTimeout;
var startedAt = DateTime.UtcNow;
try
{
_serialPort.ReadTimeout = Math.Max(50, idleMilliseconds);
while ((DateTime.UtcNow - startedAt).TotalMilliseconds < timeoutMilliseconds)
{
try
{
var line = _serialPort.ReadLine();
if (!string.IsNullOrWhiteSpace(_settings.EndToken) &&
line.Contains(_settings.EndToken, StringComparison.OrdinalIgnoreCase))
{
break;
}
}
catch (TimeoutException)
{
break;
}
}
}
finally
{
_serialPort.ReadTimeout = originalReadTimeout;
_serialPort.DiscardInBuffer();
}
}
public void Dispose()
{
_serialPort.Dispose();

BIN
Setup (2).zip

Binary file not shown.

BIN
Setup (3).zip

Binary file not shown.

BIN
Setup (4).zip

Binary file not shown.

BIN
Setup (5).zip

Binary file not shown.

BIN
Setup (6).zip

Binary file not shown.

BIN
Setup (7).zip

Binary file not shown.

BIN
Setup (8).zip

Binary file not shown.

BIN
Setup (9).zip

Binary file not shown.

BIN
Setup/Release (3).zip → Setup.zip

Binary file not shown.

BIN
Setup/Release (2).zip

Binary file not shown.

BIN
Setup/Release.zip

Binary file not shown.

BIN
Setup/Release/Setup.msi

Binary file not shown.

BIN
Setup100/Release/Setup100.msi

Binary file not shown.

BIN
Setup100/Release/setup.exe

Binary file not shown.

733
Setup100/Setup100.vdproj

@ -0,0 +1,733 @@
"DeployProject"
{
"VSVersion" = "3:800"
"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}"
"IsWebType" = "8:FALSE"
"ProjectName" = "8:Setup100"
"LanguageId" = "3:1042"
"CodePage" = "3:949"
"UILanguageId" = "3:1042"
"SccProjectName" = "8:"
"SccLocalPath" = "8:"
"SccAuxPath" = "8:"
"SccProvider" = "8:"
"Hierarchy"
{
"Entry"
{
"MsmKey" = "8:_579C5BF19AF141B38F44C957EADB21FC"
"OwnerKey" = "8:_UNDEFINED"
"MsmSig" = "8:_UNDEFINED"
}
"Entry"
{
"MsmKey" = "8:_8883E9101F6441DEB01A93D9ED8184BB"
"OwnerKey" = "8:_UNDEFINED"
"MsmSig" = "8:_UNDEFINED"
}
}
"Configurations"
{
"Debug"
{
"DisplayName" = "8:Debug"
"IsDebugOnly" = "11:TRUE"
"IsReleaseOnly" = "11:FALSE"
"OutputFilename" = "8:Debug\\Setup100.msi"
"PackageFilesAs" = "3:2"
"PackageFileSize" = "3:-2147483648"
"CabType" = "3:1"
"Compression" = "3:2"
"SignOutput" = "11:FALSE"
"CertificateFile" = "8:"
"PrivateKeyFile" = "8:"
"TimeStampServer" = "8:"
"InstallerBootstrapper" = "3:2"
}
"Release"
{
"DisplayName" = "8:Release"
"IsDebugOnly" = "11:FALSE"
"IsReleaseOnly" = "11:TRUE"
"OutputFilename" = "8:Release\\Setup100.msi"
"PackageFilesAs" = "3:2"
"PackageFileSize" = "3:-2147483648"
"CabType" = "3:1"
"Compression" = "3:2"
"SignOutput" = "11:FALSE"
"CertificateFile" = "8:"
"PrivateKeyFile" = "8:"
"TimeStampServer" = "8:"
"InstallerBootstrapper" = "3:2"
}
}
"Deployable"
{
"CustomAction"
{
}
"DefaultFeature"
{
"Name" = "8:DefaultFeature"
"Title" = "8:"
"Description" = "8:"
}
"ExternalPersistence"
{
"LaunchCondition"
{
"{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_340FFED54D9141C0B594E0BF67568967"
{
"Name" = "8:.NET Core"
"Message" = "8:[VSDNETCOREMSG]"
"AllowLaterVersions" = "11:FALSE"
"InstallUrl" = "8:https://dotnet.microsoft.com/download/dotnet-core/[NetCoreVerMajorDotMinor]"
"IsNETCore" = "11:TRUE"
"Architecture" = "2:0"
"Runtime" = "2:0"
}
}
}
"File"
{
"{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_579C5BF19AF141B38F44C957EADB21FC"
{
"SourcePath" = "8:..\\Resources\\Housing_voltage_current_icon_msi_noborder.ico"
"TargetName" = "8:Housing_voltage_current_icon_msi_noborder.ico"
"Tag" = "8:"
"Folder" = "8:_25EEF778496E404083BDF310C935E38F"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Vital" = "11:TRUE"
"ReadOnly" = "11:FALSE"
"Hidden" = "11:FALSE"
"System" = "11:FALSE"
"Permanent" = "11:FALSE"
"SharedLegacy" = "11:FALSE"
"PackageAs" = "3:1"
"Register" = "3:1"
"Exclude" = "11:FALSE"
"IsDependency" = "11:FALSE"
"IsolateTo" = "8:"
}
}
"FileType"
{
}
"Folder"
{
"{3C67513D-01DD-4637-8A68-80971EB9504F}:_25EEF778496E404083BDF310C935E38F"
{
"DefaultLocation" = "8:[ProgramFiles64Folder][Manufacturer]\\[ProductName]"
"Name" = "8:#1925"
"AlwaysCreate" = "11:FALSE"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Property" = "8:TARGETDIR"
"Folders"
{
}
}
"{1525181F-901A-416C-8A58-119130FE478E}:_66AFF44728114DBBA9BCE34144FAAB41"
{
"Name" = "8:#1916"
"AlwaysCreate" = "11:FALSE"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Property" = "8:DesktopFolder"
"Folders"
{
}
}
"{1525181F-901A-416C-8A58-119130FE478E}:_BEE777F673764227A190AF65ACD21AC2"
{
"Name" = "8:#1919"
"AlwaysCreate" = "11:FALSE"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Property" = "8:ProgramMenuFolder"
"Folders"
{
}
}
}
"LaunchCondition"
{
}
"Locator"
{
}
"MsiBootstrapper"
{
"LangId" = "3:1042"
"RequiresElevation" = "11:FALSE"
}
"Product"
{
"Name" = "8:Microsoft Visual Studio"
"ProductName" = "8:Setup100"
"ProductCode" = "8:{42EE0882-2178-4689-8512-0C0FC32A0125}"
"PackageCode" = "8:{6CF458C9-AF29-44D4-943F-464C3DC7E545}"
"UpgradeCode" = "8:{610D4218-DD05-4EAB-BAE2-1A802D29ADD4}"
"AspNetVersion" = "8:"
"RestartWWWService" = "11:FALSE"
"RemovePreviousVersions" = "11:FALSE"
"DetectNewerInstalledVersion" = "11:TRUE"
"InstallAllUsers" = "11:FALSE"
"ProductVersion" = "8:1.0.0"
"Manufacturer" = "8:temp"
"ARPHELPTELEPHONE" = "8:"
"ARPHELPLINK" = "8:"
"Title" = "8:Setup100"
"Subject" = "8:"
"ARPCONTACT" = "8:temp"
"Keywords" = "8:"
"ARPCOMMENTS" = "8:"
"ARPURLINFOABOUT" = "8:"
"ARPPRODUCTICON" = "8:"
"ARPIconIndex" = "3:0"
"SearchPath" = "8:"
"UseSystemSearchPath" = "11:TRUE"
"TargetPlatform" = "3:1"
"PreBuildEvent" = "8:"
"PostBuildEvent" = "8:"
"RunPostBuildEvent" = "3:0"
}
"Registry"
{
"HKLM"
{
"Keys"
{
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_39EF283C652B4AC69FFE46D72F8A9F1F"
{
"Name" = "8:Software"
"Condition" = "8:"
"AlwaysCreate" = "11:FALSE"
"DeleteAtUninstall" = "11:FALSE"
"Transitive" = "11:FALSE"
"Keys"
{
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_9526C11DBE504CBB8BC54ACE4A67A452"
{
"Name" = "8:[Manufacturer]"
"Condition" = "8:"
"AlwaysCreate" = "11:FALSE"
"DeleteAtUninstall" = "11:FALSE"
"Transitive" = "11:FALSE"
"Keys"
{
}
"Values"
{
}
}
}
"Values"
{
}
}
}
}
"HKCU"
{
"Keys"
{
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_9B0F05EAC02645F99F3EA4153E6985B4"
{
"Name" = "8:Software"
"Condition" = "8:"
"AlwaysCreate" = "11:FALSE"
"DeleteAtUninstall" = "11:FALSE"
"Transitive" = "11:FALSE"
"Keys"
{
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_0EE8870A6A344A6EA9C888D961766653"
{
"Name" = "8:[Manufacturer]"
"Condition" = "8:"
"AlwaysCreate" = "11:FALSE"
"DeleteAtUninstall" = "11:FALSE"
"Transitive" = "11:FALSE"
"Keys"
{
}
"Values"
{
}
}
}
"Values"
{
}
}
}
}
"HKCR"
{
"Keys"
{
}
}
"HKU"
{
"Keys"
{
}
}
"HKPU"
{
"Keys"
{
}
}
}
"Sequences"
{
}
"Shortcut"
{
"{970C0BB2-C7D0-45D7-ABFA-7EC378858BC0}:_0869784F4F8841ACA5963F59D7B16198"
{
"Name" = "8:Housing"
"Arguments" = "8:"
"Description" = "8:"
"ShowCmd" = "3:1"
"IconIndex" = "3:0"
"Transitive" = "11:FALSE"
"Target" = "8:_8883E9101F6441DEB01A93D9ED8184BB"
"Folder" = "8:_66AFF44728114DBBA9BCE34144FAAB41"
"WorkingFolder" = "8:_25EEF778496E404083BDF310C935E38F"
"Icon" = "8:_579C5BF19AF141B38F44C957EADB21FC"
"Feature" = "8:"
}
}
"UserInterface"
{
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_04AB189F79C141D4B656C539A572FAEF"
{
"Name" = "8:#1900"
"Sequence" = "3:2"
"Attributes" = "3:1"
"Dialogs"
{
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_02368588C083450E83F88B39FF14CFA1"
{
"Sequence" = "3:100"
"DisplayName" = "8:환영"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminWelcomeDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
"CopyrightWarning"
{
"Name" = "8:CopyrightWarning"
"DisplayName" = "8:#1002"
"Description" = "8:#1102"
"Type" = "3:3"
"ContextData" = "8:"
"Attributes" = "3:0"
"Setting" = "3:1"
"Value" = "8:#1202"
"DefaultValue" = "8:#1202"
"UsePlugInResources" = "11:TRUE"
}
"Welcome"
{
"Name" = "8:Welcome"
"DisplayName" = "8:#1003"
"Description" = "8:#1103"
"Type" = "3:3"
"ContextData" = "8:"
"Attributes" = "3:0"
"Setting" = "3:1"
"Value" = "8:#1203"
"DefaultValue" = "8:#1203"
"UsePlugInResources" = "11:TRUE"
}
}
}
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_6B2319BBA43F4B0E98499B17431AABC7"
{
"Sequence" = "3:300"
"DisplayName" = "8:설치 확인"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminConfirmDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
}
}
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_8199646E4ADF4CA497CB35DD5D9B4EA3"
{
"Sequence" = "3:200"
"DisplayName" = "8:설치 폴더"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminFolderDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
}
}
}
}
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_48A28929DD8F42FA8754A057491B7190"
{
"Name" = "8:#1902"
"Sequence" = "3:2"
"Attributes" = "3:3"
"Dialogs"
{
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_1958E6FF4D7644948E621DB2C391ABB5"
{
"Sequence" = "3:100"
"DisplayName" = "8:마침"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminFinishedDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
}
}
}
}
"{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_48FD9E8D7F534713B0B26FD39C225534"
{
"UseDynamicProperties" = "11:FALSE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdUserInterface.wim"
}
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_5722F25259EC4038B82754A82CD8314C"
{
"Name" = "8:#1900"
"Sequence" = "3:1"
"Attributes" = "3:1"
"Dialogs"
{
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_7AD188584ACC4527B8ADF78D4B8C1E94"
{
"Sequence" = "3:300"
"DisplayName" = "8:설치 확인"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdConfirmDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
}
}
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_8F418472D442448A82EF5B715330C4AD"
{
"Sequence" = "3:100"
"DisplayName" = "8:환영"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdWelcomeDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
"CopyrightWarning"
{
"Name" = "8:CopyrightWarning"
"DisplayName" = "8:#1002"
"Description" = "8:#1102"
"Type" = "3:3"
"ContextData" = "8:"
"Attributes" = "3:0"
"Setting" = "3:1"
"Value" = "8:#1202"
"DefaultValue" = "8:#1202"
"UsePlugInResources" = "11:TRUE"
}
"Welcome"
{
"Name" = "8:Welcome"
"DisplayName" = "8:#1003"
"Description" = "8:#1103"
"Type" = "3:3"
"ContextData" = "8:"
"Attributes" = "3:0"
"Setting" = "3:1"
"Value" = "8:#1203"
"DefaultValue" = "8:#1203"
"UsePlugInResources" = "11:TRUE"
}
}
}
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_A70CDD4D26FE416E85635A6477E83C99"
{
"Sequence" = "3:200"
"DisplayName" = "8:설치 폴더"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdFolderDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
"InstallAllUsersVisible"
{
"Name" = "8:InstallAllUsersVisible"
"DisplayName" = "8:#1059"
"Description" = "8:#1159"
"Type" = "3:5"
"ContextData" = "8:1;True=1;False=0"
"Attributes" = "3:0"
"Setting" = "3:0"
"Value" = "3:1"
"DefaultValue" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
}
}
}
}
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_61DB10700E264A71B89788C6597725A2"
{
"Name" = "8:#1902"
"Sequence" = "3:1"
"Attributes" = "3:3"
"Dialogs"
{
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_849D9D5D0F5A481A8AEDAE597AE94504"
{
"Sequence" = "3:100"
"DisplayName" = "8:마침"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdFinishedDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
"UpdateText"
{
"Name" = "8:UpdateText"
"DisplayName" = "8:#1058"
"Description" = "8:#1158"
"Type" = "3:15"
"ContextData" = "8:"
"Attributes" = "3:0"
"Setting" = "3:1"
"Value" = "8:#1258"
"DefaultValue" = "8:#1258"
"UsePlugInResources" = "11:TRUE"
}
}
}
}
}
"{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_66D5A4849DA74893B5DE7BC91285BF39"
{
"UseDynamicProperties" = "11:FALSE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdBasicDialogs.wim"
}
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_9ECC9BE8279449D08E3879378D28F5F4"
{
"Name" = "8:#1901"
"Sequence" = "3:1"
"Attributes" = "3:2"
"Dialogs"
{
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_11605F0AA4EF461C9ED97277B3198620"
{
"Sequence" = "3:100"
"DisplayName" = "8:진행률"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdProgressDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
"ShowProgress"
{
"Name" = "8:ShowProgress"
"DisplayName" = "8:#1009"
"Description" = "8:#1109"
"Type" = "3:5"
"ContextData" = "8:1;True=1;False=0"
"Attributes" = "3:0"
"Setting" = "3:0"
"Value" = "3:1"
"DefaultValue" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
}
}
}
}
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_A39EAE152FD34A3D8E60FA00E71EE82E"
{
"Name" = "8:#1901"
"Sequence" = "3:2"
"Attributes" = "3:2"
"Dialogs"
{
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_96B06613F8184F7CB033D968D7ACAB56"
{
"Sequence" = "3:100"
"DisplayName" = "8:진행률"
"UseDynamicProperties" = "11:TRUE"
"IsDependency" = "11:FALSE"
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminProgressDlg.wid"
"Properties"
{
"BannerBitmap"
{
"Name" = "8:BannerBitmap"
"DisplayName" = "8:#1001"
"Description" = "8:#1101"
"Type" = "3:8"
"ContextData" = "8:Bitmap"
"Attributes" = "3:4"
"Setting" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
"ShowProgress"
{
"Name" = "8:ShowProgress"
"DisplayName" = "8:#1009"
"Description" = "8:#1109"
"Type" = "3:5"
"ContextData" = "8:1;True=1;False=0"
"Attributes" = "3:0"
"Setting" = "3:0"
"Value" = "3:1"
"DefaultValue" = "3:1"
"UsePlugInResources" = "11:TRUE"
}
}
}
}
}
}
"MergeModule"
{
}
"ProjectOutput"
{
"{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_8883E9101F6441DEB01A93D9ED8184BB"
{
"SourcePath" = "8:..\\obj\\Release\\net9.0-windows\\apphost.exe"
"TargetName" = "8:"
"Tag" = "8:"
"Folder" = "8:_25EEF778496E404083BDF310C935E38F"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Vital" = "11:TRUE"
"ReadOnly" = "11:FALSE"
"Hidden" = "11:FALSE"
"System" = "11:FALSE"
"Permanent" = "11:FALSE"
"SharedLegacy" = "11:FALSE"
"PackageAs" = "3:1"
"Register" = "3:1"
"Exclude" = "11:FALSE"
"IsDependency" = "11:FALSE"
"IsolateTo" = "8:"
"ProjectOutputGroupRegister" = "3:1"
"OutputConfiguration" = "8:"
"OutputGroupCanonicalName" = "8:PublishItemsOutputGroup"
"OutputProjectGuid" = "8:{7364A567-F115-4ACE-9207-C6FDEEF0A394}"
"ShowKeyOutput" = "11:TRUE"
"ExcludeFilters"
{
}
}
}
}
}

BIN
housing-result-ng.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
housing-result-ok.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

30
init.sql

@ -187,3 +187,33 @@ BEGIN
COMMIT TRANSACTION;
END;
GO
IF OBJECT_ID(N'dbo.Housing_Assembly', N'U') IS NOT NULL
AND COL_LENGTH(N'dbo.Housing_Assembly', N'IC_SN') IS NOT NULL
AND NOT EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Housing_Assembly', N'U')
AND name = N'UX_Housing_Assembly_IC_SN'
)
BEGIN
IF EXISTS
(
SELECT 1
FROM dbo.Housing_Assembly
WHERE [IC_SN] IS NOT NULL
AND [IC_SN] <> N''
GROUP BY [IC_SN]
HAVING COUNT(*) > 1
)
BEGIN
THROW 51001, 'Cannot create UX_Housing_Assembly_IC_SN because duplicate IC_SN values exist.', 1;
END;
CREATE UNIQUE INDEX UX_Housing_Assembly_IC_SN
ON dbo.Housing_Assembly ([IC_SN])
WHERE [IC_SN] IS NOT NULL
AND [IC_SN] <> N'';
END;
GO

12
publish/setup-win-x64/Database.ini

@ -1,11 +1,11 @@
[Database]
IP=127.0.0.1
Database=Housing
DbId=sa
DbPw=your_password
Encrypt=True
IP=192.168.10.10,1433
Database=NE1aW_PT_Sensor
DbId=su_user
DbPw=amotech821
Encrypt=False
TrustServerCertificate=True
Timeout=5
Timeout=15
[Login]
Procedure=dbo.CheckOperator

2
publish/setup-win-x64/Hardware.ini

@ -39,7 +39,7 @@ Timeout=5000
SettleMilliseconds=300
PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2)
PreBoardDelayMilliseconds=2000
LogoutCommand=OUTP OFF, (@1);OUTP OFF, (@2)
LogoutCommand=OUTP OFF, (@1)
; 34465A DMM
VoltageConnection=LAN

30
publish/setup-win-x64/init.sql

@ -187,3 +187,33 @@ BEGIN
COMMIT TRANSACTION;
END;
GO
IF OBJECT_ID(N'dbo.Housing_Assembly', N'U') IS NOT NULL
AND COL_LENGTH(N'dbo.Housing_Assembly', N'IC_SN') IS NOT NULL
AND NOT EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.Housing_Assembly', N'U')
AND name = N'UX_Housing_Assembly_IC_SN'
)
BEGIN
IF EXISTS
(
SELECT 1
FROM dbo.Housing_Assembly
WHERE [IC_SN] IS NOT NULL
AND [IC_SN] <> N''
GROUP BY [IC_SN]
HAVING COUNT(*) > 1
)
BEGIN
THROW 51001, 'Cannot create UX_Housing_Assembly_IC_SN because duplicate IC_SN values exist.', 1;
END;
CREATE UNIQUE INDEX UX_Housing_Assembly_IC_SN
ON dbo.Housing_Assembly ([IC_SN])
WHERE [IC_SN] IS NOT NULL
AND [IC_SN] <> N'';
END;
GO

12
tools/ManualScreenCapture/ManualScreenCapture.csproj

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Housing.csproj" />
</ItemGroup>
</Project>

216
tools/ManualScreenCapture/Program.cs

@ -0,0 +1,216 @@
using System.Reflection;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Housing;
using Housing.Login;
internal static class Program
{
private const int MainWidth = 1200;
private const int MainHeight = 800;
private const string ExampleBarcode = "A251001AM46110901T00001-01";
private const string ExampleIcSn = "025949A8492CFEAC";
[STAThread]
private static void Main()
{
var outputDirectory = Path.GetFullPath(
Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "manual-screens"));
Directory.CreateDirectory(outputDirectory);
var app = new App();
app.InitializeComponent();
CaptureLogin(Path.Combine(outputDirectory, "01-login.png"));
CaptureMainScreens(outputDirectory);
Console.WriteLine(outputDirectory);
}
private static void CaptureLogin(string path)
{
var window = new StartupLoginWindow
{
WindowState = WindowState.Normal,
Width = 760,
Height = 760
};
SelectFirst(window, "MakerComboBox");
SelectFirst(window, "ModelComboBox");
SelectFirst(window, "Variant1ComboBox");
SelectFirst(window, "Variant2ComboBox");
SelectFirst(window, "LineNoComboBox");
SelectFirst(window, "JigNoComboBox");
SetText(window, "OperatorTextBox", "OP1001");
SetPassword(window, "PasswordBox", "manual");
SetText(window, "LotNoTextBox", "LOT26001");
RenderWindowContent(window, path, 760, 760);
window.Close();
}
private static void CaptureMainScreens(string outputDirectory)
{
CaptureMain(Path.Combine(outputDirectory, "02-waiting-barcode.png"), window =>
{
PrepareMain(window);
SetText(window, "BarcodeTextBox", string.Empty);
SetText(window, "IcSnOutputTextBox", string.Empty);
Invoke(window, "SetProcessStatus", "WAITING BARCODE", "HmiAccentBrush");
Invoke(window, "ShowResultStatusMessage", "스캐너로 PCB_Barcode를 스캔해주세요.");
});
CaptureMain(Path.Combine(outputDirectory, "03-wait-start-signal.png"), window =>
{
PrepareMain(window);
SetText(window, "BarcodeTextBox", ExampleBarcode);
Invoke(window, "SetProcessStatus", "WAIT START SIGNAL", "HmiWarningBrush");
Invoke(
window,
"ShowResultStatusMessage",
"장비 시작 신호 대기\nNI-6501 Dev1/port0/line0\n시료를 넣고 Housing 장비 시작 버튼을 눌러주세요.");
});
CaptureMain(Path.Combine(outputDirectory, "04-board-test.png"), window =>
{
PrepareMain(window);
SetText(window, "BarcodeTextBox", ExampleBarcode);
SetText(window, "IcSnOutputTextBox", ExampleIcSn);
Invoke(window, "SetProcessStatus", "BOARD TEST", "HmiWarningBrush");
Invoke(
window,
"ShowResultStatusMessage",
"보드 테스트 시작\n1. 시료 연결 확인\n2. IC_SN 읽기\n3. CAL DEFAULT 조건 설정\n4. 시작 신호 기준 지연 후 계측 장비에서 V/A 읽기");
});
CaptureMain(Path.Combine(outputDirectory, "05-result-ok.png"), window =>
{
PrepareMain(window);
SetText(window, "BarcodeTextBox", ExampleBarcode);
SetText(window, "IcSnOutputTextBox", ExampleIcSn);
SetText(window, "VOutputTextBox", "5.02");
SetText(window, "AOutputTextBox", "118.40");
Invoke(window, "ApplyMeasurementOutputState", Find<TextBox>(window, "VOutputTextBox"), true);
Invoke(window, "ApplyMeasurementOutputState", Find<TextBox>(window, "AOutputTextBox"), true);
Invoke(window, "ShowInspectionResult", true);
Invoke(window, "SetProcessStatus", "COMPLETE OK", "HmiRunBrush");
});
CaptureMain(Path.Combine(outputDirectory, "06-result-ng.png"), window =>
{
PrepareMain(window);
SetText(window, "BarcodeTextBox", ExampleBarcode);
SetText(window, "IcSnOutputTextBox", ExampleIcSn);
SetText(window, "VOutputTextBox", "5.84");
SetText(window, "AOutputTextBox", "122.10");
Invoke(window, "ApplyMeasurementOutputState", Find<TextBox>(window, "VOutputTextBox"), false);
Invoke(window, "ApplyMeasurementOutputState", Find<TextBox>(window, "AOutputTextBox"), true);
Invoke(window, "ShowInspectionResult", false);
Invoke(window, "SetProcessStatus", "COMPLETE NG", "HmiDangerBrush");
});
CaptureMain(Path.Combine(outputDirectory, "07-db-save-error.png"), window =>
{
PrepareMain(window);
SetText(window, "BarcodeTextBox", ExampleBarcode);
SetText(window, "IcSnOutputTextBox", ExampleIcSn);
SetText(window, "VOutputTextBox", "5.01");
SetText(window, "AOutputTextBox", "117.80");
Invoke(window, "ApplyMeasurementOutputState", Find<TextBox>(window, "VOutputTextBox"), true);
Invoke(window, "ApplyMeasurementOutputState", Find<TextBox>(window, "AOutputTextBox"), true);
Invoke(window, "ShowInspectionResult", true);
Invoke(window, "SetProcessStatus", "DB SAVE ERROR", "HmiDangerBrush");
});
}
private static void CaptureMain(string path, Action<MainWindow> arrange)
{
var window = new MainWindow
{
WindowState = WindowState.Normal,
Width = MainWidth,
Height = MainHeight
};
arrange(window);
RenderWindowContent(window, path, MainWidth, MainHeight);
window.Close();
}
private static void PrepareMain(MainWindow window)
{
SetTextBlock(window, "OperatorTextBlock", "OP1001");
Find<Button>(window, "LogoutButton").Visibility = Visibility.Visible;
SetText(window, "VMinTextBox", "4.50");
SetText(window, "VMaxTextBox", "5.50");
SetText(window, "AMinTextBox", "80.00");
SetText(window, "AMaxTextBox", "150.00");
SetText(window, "VOutputTextBox", "0.00");
SetText(window, "AOutputTextBox", "0.00");
}
private static void RenderWindowContent(Window window, string path, int width, int height)
{
if (window.Content is not FrameworkElement content)
{
throw new InvalidOperationException("Window content was not a FrameworkElement.");
}
content.Width = width;
content.Height = height;
content.Measure(new Size(width, height));
content.Arrange(new Rect(0, 0, width, height));
content.UpdateLayout();
var bitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(content);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var stream = File.Create(path);
encoder.Save(stream);
}
private static T Find<T>(FrameworkElement owner, string name) where T : FrameworkElement
{
return owner.FindName(name) as T
?? throw new InvalidOperationException($"{name} was not found.");
}
private static void SetText(FrameworkElement owner, string name, string value)
{
Find<TextBox>(owner, name).Text = value;
}
private static void SetTextBlock(FrameworkElement owner, string name, string value)
{
Find<TextBlock>(owner, name).Text = value;
}
private static void SetPassword(FrameworkElement owner, string name, string value)
{
Find<PasswordBox>(owner, name).Password = value;
}
private static void SelectFirst(FrameworkElement owner, string name)
{
var comboBox = Find<ComboBox>(owner, name);
if (comboBox.Items.Count > 0)
{
comboBox.SelectedIndex = 0;
}
}
private static object? Invoke(object target, string methodName, params object?[] arguments)
{
var method = target.GetType().GetMethod(
methodName,
BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new MissingMethodException(target.GetType().FullName, methodName);
return method.Invoke(target, arguments);
}
}

1334
tools/generate_housing_user_manual.py

File diff suppressed because it is too large

BIN
tools/manual-screens/01-login.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
tools/manual-screens/02-waiting-barcode.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
tools/manual-screens/03-wait-start-signal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
tools/manual-screens/04-board-test.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
tools/manual-screens/05-result-ok.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
tools/manual-screens/06-result-ng.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
tools/manual-screens/07-db-save-error.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Loading…
Cancel
Save