Browse Source

최종

main
gudae 2 months ago
parent
commit
f8080b1ee2
  1. BIN
      .vs/Housing/DesignTimeBuild/.dtbcache.v2
  2. 8
      Database.ini
  3. 14
      Hardware.ini
  4. 8
      Housing.csproj
  5. 28
      Housing.csproj.Backup.tmp
  6. 7
      Housing.sln
  7. 11
      Login/StartupLoginWindow.xaml.cs
  8. 20
      MainWindow.xaml
  9. 161
      MainWindow.xaml.cs
  10. 1
      Models/HousingAssemblyRecord.cs
  11. BIN
      Resources/Housing_voltage_current_icon.ico
  12. 3
      Resources/mobidigm 1.svg
  13. BIN
      Resources/mobidigm-logo.png
  14. 69
      Services/BarcodeScannerSettings.cs
  15. 2
      Services/BoardHardwareSettings.cs
  16. 2
      Services/BoardTestService.cs
  17. 7
      Services/HousingAssemblyRepository.cs
  18. 2
      Services/InspectionSettingsStore.cs
  19. 21
      Services/LoginAccountRepository.cs
  20. 40
      Services/LoginSettings.cs
  21. 426
      Services/SerialBarcodeScanner.cs
  22. BIN
      SetupProject/Release/SetupProject.tmp
  23. 44829
      SetupProject/SetupProject.vdproj
  24. 112
      init.sql
  25. BIN
      publish/win-x64/D3DCompiler_47_cor3.dll
  26. 12
      publish/win-x64/Database.ini
  27. 78
      publish/win-x64/Hardware.ini
  28. BIN
      publish/win-x64/Housing.exe
  29. BIN
      publish/win-x64/Housing.pdb
  30. 5
      publish/win-x64/InspectionSettings.ini
  31. BIN
      publish/win-x64/Microsoft.Data.SqlClient.SNI.dll
  32. BIN
      publish/win-x64/PenImc_cor3.dll
  33. BIN
      publish/win-x64/PresentationNative_cor3.dll
  34. 112
      publish/win-x64/init.sql
  35. BIN
      publish/win-x64/vcruntime140_cor3.dll
  36. BIN
      publish/win-x64/wpfgfx_cor3.dll

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

Binary file not shown.

8
Database.ini

@ -8,9 +8,5 @@ TrustServerCertificate=True
Timeout=5 Timeout=5
[Login] [Login]
; 로그인 승인에 사용할 DB 테이블/컬럼 Procedure=dbo.CheckOperator
Table=dbo.Housing_Assembly OfflinePreview=true
IdColumn=Operator
PasswordColumn=Password
; DB 연결이 안 되는 상태에서 화면만 확인할 때 true로 둡니다. 운영 시 false로 바꾸세요.
OfflinePreview=false

14
Hardware.ini

@ -5,11 +5,21 @@ ReadTimeout=5000
PreConnectCommand=x00o PreConnectCommand=x00o
ConnectCommand=x00c_001101:owt28006727ea97c7801 ConnectCommand=x00c_001101:owt28006727ea97c7801
ReadIdCommand=x00c_001101:ow2800326003e ReadIdCommand=x00c_001101:ow2800326003e
PostReadIdCommand=x00o
CalDefaultCommand=x00c_001001:owt28006727ea97c7801 CalDefaultCommand=x00c_001001:owt28006727ea97c7801
EndToken=<end> EndToken=<end>
DtrEnable=false DtrEnable=false
RtsEnable=false RtsEnable=false
[BarcodeScanner]
Enabled=true
PortName=Auto
BaudRate=9600
ReadTimeout=500
IdleCommitMilliseconds=250
DtrEnable=true
RtsEnable=true
[StartSignal] [StartSignal]
; USB-6501 ; USB-6501
Enabled=true Enabled=true
@ -35,7 +45,7 @@ LogoutCommand=OUTP OFF, (@1);OUTP OFF, (@2)
VoltageConnection=LAN VoltageConnection=LAN
VoltageResource= VoltageResource=
VoltageIdnMatch=34465A VoltageIdnMatch=34465A
VoltageHost=192.168.0.3 VoltageHost=192.168.200.3
VoltagePort=5025 VoltagePort=5025
VoltageSetupCommand= VoltageSetupCommand=
VoltageReadCommand=MEAS:VOLT:DC? VoltageReadCommand=MEAS:VOLT:DC?
@ -45,7 +55,7 @@ CurrentConnection=LAN
CurrentResource= CurrentResource=
CurrentIdnMatch=E36233A CurrentIdnMatch=E36233A
; Change this to the E36233A LAN IP if the instrument uses a different address. ; Change this to the E36233A LAN IP if the instrument uses a different address.
CurrentHost=192.168.0.4 CurrentHost=192.168.200.111
CurrentPort=5025 CurrentPort=5025
CurrentPortName= CurrentPortName=
CurrentBaudRate=9600 CurrentBaudRate=9600

8
Housing.csproj

@ -6,11 +6,16 @@
<RootNamespace>Housing</RootNamespace> <RootNamespace>Housing</RootNamespace>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>Resources\Housing_voltage_current_icon.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="System.IO.Ports" Version="9.0.10" /> <PackageReference Include="System.IO.Ports" Version="9.0.10" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="Resources\mobidigm-logo.png" />
<Resource Include="Resources\mobidigm-logo.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="Database.ini"> <None Update="Database.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -21,5 +26,8 @@
<None Update="InspectionSettings.ini"> <None Update="InspectionSettings.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="init.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

28
Housing.csproj.Backup.tmp

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<RootNamespace>Housing</RootNamespace>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="System.IO.Ports" Version="9.0.10" />
</ItemGroup>
<ItemGroup>
<None Update="Database.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Hardware.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="InspectionSettings.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="init.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

7
Housing.sln

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 18
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 18.3.11520.95
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Housing", "Housing.csproj", "{7364A567-F115-4ACE-9207-C6FDEEF0A394}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Housing", "Housing.csproj", "{7364A567-F115-4ACE-9207-C6FDEEF0A394}"
EndProject EndProject
@ -31,4 +31,7 @@ Global
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3209F145-2010-45EC-8A1A-998F1D3CAE2F}
EndGlobalSection
EndGlobal EndGlobal

11
Login/StartupLoginWindow.xaml.cs

@ -15,7 +15,6 @@ public sealed class StartupLoginWindowResult
public string Variant1 { get; set; } = string.Empty; public string Variant1 { get; set; } = string.Empty;
public string Variant2 { get; set; } = string.Empty; public string Variant2 { get; set; } = string.Empty;
public string Operator { get; set; } = string.Empty; public string Operator { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string LineNo { get; set; } = string.Empty; public string LineNo { get; set; } = string.Empty;
public string LotNo { get; set; } = string.Empty; public string LotNo { get; set; } = string.Empty;
public string JigNo { get; set; } = string.Empty; public string JigNo { get; set; } = string.Empty;
@ -45,7 +44,7 @@ public partial class StartupLoginWindow : Window
{ {
CloseHistoryPopup(); CloseHistoryPopup();
if (!TryReadRequiredFields(out var loginResult)) if (!TryReadRequiredFields(out var loginResult, out var password))
{ {
return; return;
} }
@ -71,7 +70,7 @@ public partial class StartupLoginWindow : Window
DatabaseSettings.Load(GetDatabaseIniPath()), DatabaseSettings.Load(GetDatabaseIniPath()),
loginSettings); loginSettings);
if (!await repository.ExistsAsync(loginResult.Operator, loginResult.Password)) if (!await repository.ExistsAsync(loginResult.Operator, password))
{ {
MessageBox.Show(this, "DB에 등록된 ID/PW가 아닙니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show(this, "DB에 등록된 ID/PW가 아닙니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error);
PasswordBox.Clear(); PasswordBox.Clear();
@ -99,16 +98,17 @@ public partial class StartupLoginWindow : Window
} }
} }
private bool TryReadRequiredFields(out StartupLoginWindowResult result) private bool TryReadRequiredFields(out StartupLoginWindowResult result, out string password)
{ {
result = new StartupLoginWindowResult(); result = new StartupLoginWindowResult();
password = string.Empty;
if (!TryReadTextBox(MakerTextBox, "Maker", out var maker) || if (!TryReadTextBox(MakerTextBox, "Maker", out var maker) ||
!TryReadTextBox(ModelTextBox, "Model", out var model) || !TryReadTextBox(ModelTextBox, "Model", out var model) ||
!TryReadTextBox(Variant1TextBox, "Variant 1", out var variant1) || !TryReadTextBox(Variant1TextBox, "Variant 1", out var variant1) ||
!TryReadTextBox(Variant2TextBox, "Variant 2", out var variant2) || !TryReadTextBox(Variant2TextBox, "Variant 2", out var variant2) ||
!TryReadTextBox(OperatorTextBox, "ID", out var loginId) || !TryReadTextBox(OperatorTextBox, "ID", out var loginId) ||
!TryReadPasswordBox(PasswordBox, "PW", out var password) || !TryReadPasswordBox(PasswordBox, "PW", out password) ||
!TryReadTextBox(LineNoTextBox, "Line", out var lineNo) || !TryReadTextBox(LineNoTextBox, "Line", out var lineNo) ||
!TryReadTextBox(LotNoTextBox, "Lot No", out var lotNo) || !TryReadTextBox(LotNoTextBox, "Lot No", out var lotNo) ||
!TryReadTextBox(JigNoTextBox, "Jig No", out var jigNo)) !TryReadTextBox(JigNoTextBox, "Jig No", out var jigNo))
@ -123,7 +123,6 @@ public partial class StartupLoginWindow : Window
Variant1 = variant1, Variant1 = variant1,
Variant2 = variant2, Variant2 = variant2,
Operator = loginId, Operator = loginId,
Password = password,
LineNo = lineNo, LineNo = lineNo,
LotNo = lotNo, LotNo = lotNo,
JigNo = jigNo JigNo = jigNo

20
MainWindow.xaml

@ -295,17 +295,7 @@
BorderBrush="#2E4658" BorderBrush="#2E4658"
BorderThickness="1" BorderThickness="1"
Padding="12,7" Padding="12,7"
Margin="0,0,10,0"> Margin="0,0,14,0">
<StackPanel Orientation="Horizontal">
<Ellipse Width="12" Height="12" Fill="{StaticResource HmiRunBrush}" Stroke="#8AF0C0" StrokeThickness="1" Margin="0,0,8,0"/>
<TextBlock Text="RUN READY" Foreground="{StaticResource HmiRunBrush}" FontSize="13" FontWeight="Bold"/>
</StackPanel>
</Border>
<Border Background="#0B161F"
BorderBrush="#2E4658"
BorderThickness="1"
Padding="12,7">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="LOGIN ID" <TextBlock Text="LOGIN ID"
Foreground="{StaticResource HmiMutedTextBrush}" Foreground="{StaticResource HmiMutedTextBrush}"
@ -327,6 +317,12 @@
Click="LogoutButton_Click"/> Click="LogoutButton_Click"/>
</StackPanel> </StackPanel>
</Border> </Border>
<Image Source="Resources/mobidigm-logo.png"
Width="150"
Height="37"
Stretch="Uniform"
RenderOptions.BitmapScalingMode="HighQuality"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
@ -498,7 +494,7 @@
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="PT_CURRENT_1 / A OUTPUT" Style="{StaticResource LabelStyle}"/> <TextBlock Text="PT_CURRENT_1 / mA OUTPUT" Style="{StaticResource LabelStyle}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBox x:Name="AMinTextBox" Style="{StaticResource RangeBoxStyle}"/> <TextBox x:Name="AMinTextBox" Style="{StaticResource RangeBoxStyle}"/>
<TextBlock Text="~" <TextBlock Text="~"

161
MainWindow.xaml.cs

@ -19,11 +19,14 @@ public partial class MainWindow : Window
private const string BarcodeInvalidMessage = "스캔 형식이 올바르지 않습니다. PCBA S/N 바코드를 스캔해주세요."; private const string BarcodeInvalidMessage = "스캔 형식이 올바르지 않습니다. PCBA S/N 바코드를 스캔해주세요.";
private const string DefaultOutputValue = "0.00"; private const string DefaultOutputValue = "0.00";
private const int ScannerInputMinimumLength = 4; private const int ScannerInputMinimumLength = 4;
private const decimal MilliampsPerAmp = 1000m;
private static readonly TimeSpan ScannerInputMaxGap = TimeSpan.FromMilliseconds(80); private static readonly TimeSpan ScannerInputMaxGap = TimeSpan.FromMilliseconds(80);
private static readonly TimeSpan ScannerInputIdleCommitDelay = TimeSpan.FromMilliseconds(250); private static readonly TimeSpan ScannerInputIdleCommitDelay = TimeSpan.FromMilliseconds(250);
private readonly StringBuilder _scannerInputBuffer = new(); private readonly StringBuilder _scannerInputBuffer = new();
private readonly DispatcherTimer _scannerInputCommitTimer; private readonly DispatcherTimer _scannerInputCommitTimer;
private SerialBarcodeScanner? _barcodeScanner;
private decimal? _lastMeasuredVoltage;
private DateTime _lastScannerInputAt = DateTime.MinValue; private DateTime _lastScannerInputAt = DateTime.MinValue;
private StartupLoginWindowResult _loginResult = new(); private StartupLoginWindowResult _loginResult = new();
private InspectionJudgementSettings _inspectionSettings; private InspectionJudgementSettings _inspectionSettings;
@ -60,6 +63,15 @@ public partial class MainWindow : Window
} }
} }
protected override void OnClosed(EventArgs e)
{
_barcodeScanner?.Dispose();
_scannerInputCommitTimer.Stop();
CommandManager.RemovePreviewExecutedHandler(BarcodeTextBox, BarcodeTextBox_PreviewExecuted);
base.OnClosed(e);
}
private void BarcodeTextBox_TextChanged(object sender, TextChangedEventArgs e) private void BarcodeTextBox_TextChanged(object sender, TextChangedEventArgs e)
{ {
if (string.IsNullOrWhiteSpace(BarcodeTextBox.Text)) if (string.IsNullOrWhiteSpace(BarcodeTextBox.Text))
@ -129,6 +141,12 @@ public partial class MainWindow : Window
var scannerText = _scannerInputBuffer.ToString().Trim(); var scannerText = _scannerInputBuffer.ToString().Trim();
_scannerInputBuffer.Clear(); _scannerInputBuffer.Clear();
await ProcessScannerTextAsync(scannerText, runBoardSequence, showInvalidInput: true);
}
private async Task<bool> ProcessScannerTextAsync(string scannerText, bool runBoardSequence, bool showInvalidInput)
{
scannerText = scannerText.Trim();
var hasPendingScannerInput = scannerText.Length > 0; var hasPendingScannerInput = scannerText.Length > 0;
var acceptedScannerInput = TryExtractPcbBarcode(scannerText, out var scannedBarcode); var acceptedScannerInput = TryExtractPcbBarcode(scannerText, out var scannedBarcode);
@ -137,7 +155,7 @@ public partial class MainWindow : Window
BarcodeTextBox.Text = scannedBarcode; BarcodeTextBox.Text = scannedBarcode;
BarcodeTextBox.CaretIndex = BarcodeTextBox.Text.Length; BarcodeTextBox.CaretIndex = BarcodeTextBox.Text.Length;
} }
else if (hasPendingScannerInput && scannerText.Length >= ScannerInputMinimumLength) else if (showInvalidInput && hasPendingScannerInput && scannerText.Length >= ScannerInputMinimumLength)
{ {
BarcodeTextBox.Clear(); BarcodeTextBox.Clear();
ResultOutputTextBox.Text = BarcodeInvalidMessage; ResultOutputTextBox.Text = BarcodeInvalidMessage;
@ -151,10 +169,63 @@ public partial class MainWindow : Window
await RunBoardSequenceAsync(); await RunBoardSequenceAsync();
} }
return; return acceptedScannerInput;
} }
FocusBarcodeInput(); FocusBarcodeInput();
return acceptedScannerInput;
}
private void StartBarcodeScanner()
{
if (_barcodeScanner is not null)
{
return;
}
SerialBarcodeScanner? barcodeScanner = null;
try
{
var hardwareIniPath = GetHardwareIniPath();
var settings = BarcodeScannerSettings.Load(hardwareIniPath);
if (!settings.Enabled)
{
return;
}
barcodeScanner = new SerialBarcodeScanner(settings, GetReservedBarcodeScannerPortNames(hardwareIniPath));
barcodeScanner.ScanReceived += BarcodeScanner_ScanReceived;
barcodeScanner.Start();
_barcodeScanner = barcodeScanner;
}
catch (Exception ex)
{
barcodeScanner?.Dispose();
_barcodeScanner?.Dispose();
_barcodeScanner = null;
ResultOutputTextBox.Text = $"{BarcodeRequiredMessage}\r\nBarcode scanner COM: {ex.Message}";
}
}
private void BarcodeScanner_ScanReceived(object? sender, BarcodeScanReceivedEventArgs e)
{
Dispatcher.BeginInvoke(new Action(async () =>
{
try
{
var hasBarcodeFormat = TryExtractPcbBarcode(e.Text, out _);
if (hasBarcodeFormat)
{
_barcodeScanner?.ConfirmPort(e.PortName);
}
await ProcessScannerTextAsync(e.Text, runBoardSequence: true, showInvalidInput: e.IsConfirmedPort);
}
catch (Exception ex)
{
ResultOutputTextBox.Text = $"Barcode scanner input failed: {ex.Message}";
}
}), DispatcherPriority.Input);
} }
private static bool TryExtractPcbBarcode(string scannerText, out string pcbBarcode) private static bool TryExtractPcbBarcode(string scannerText, out string pcbBarcode)
@ -175,6 +246,13 @@ public partial class MainWindow : Window
return true; return true;
} }
var plainSerial = scannerText.Trim();
if (IsPlainPcbBarcode(plainSerial))
{
pcbBarcode = plainSerial;
return true;
}
var fields = scannerText var fields = scannerText
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(part => part.Split(':', 2, StringSplitOptions.TrimEntries)) .Select(part => part.Split(':', 2, StringSplitOptions.TrimEntries))
@ -193,6 +271,12 @@ public partial class MainWindow : Window
return pcbBarcode.Length <= 50; return pcbBarcode.Length <= 50;
} }
private static bool IsPlainPcbBarcode(string value)
{
return Regex.IsMatch(value, @"^[A-Za-z0-9][A-Za-z0-9-]{3,49}$") &&
Regex.IsMatch(value, @"\d");
}
private void ClearPendingScannerInput() private void ClearPendingScannerInput()
{ {
_scannerInputCommitTimer.Stop(); _scannerInputCommitTimer.Stop();
@ -253,8 +337,9 @@ public partial class MainWindow : Window
measurementNotBeforeUtc); measurementNotBeforeUtc);
IcSnOutputTextBox.Text = measurement.IcSn; IcSnOutputTextBox.Text = measurement.IcSn;
_lastMeasuredVoltage = measurement.Voltage;
VOutputTextBox.Text = FormatDecimal(measurement.Voltage); VOutputTextBox.Text = FormatDecimal(measurement.Voltage);
AOutputTextBox.Text = FormatDecimal(measurement.Current); AOutputTextBox.Text = FormatCurrent(measurement.Current);
var resultCode = _inspectionSettings.IsPass(measurement.Voltage, measurement.Current) ? "OK" : "NG"; var resultCode = _inspectionSettings.IsPass(measurement.Voltage, measurement.Current) ? "OK" : "NG";
ResultOutputTextBox.Text = resultCode; ResultOutputTextBox.Text = resultCode;
@ -346,6 +431,7 @@ public partial class MainWindow : Window
_loginResult = loginWindow.Result; _loginResult = loginWindow.Result;
ApplyLoginResult(); ApplyLoginResult();
ResetScreen(); ResetScreen();
StartBarcodeScanner();
return true; return true;
} }
@ -363,8 +449,9 @@ public partial class MainWindow : Window
{ {
BarcodeTextBox.Clear(); BarcodeTextBox.Clear();
IcSnOutputTextBox.Clear(); IcSnOutputTextBox.Clear();
_lastMeasuredVoltage = null;
VOutputTextBox.Text = DefaultOutputValue; VOutputTextBox.Text = DefaultOutputValue;
AOutputTextBox.Text = DefaultOutputValue; AOutputTextBox.Text = FormatCurrent(0m);
ResultOutputTextBox.Text = BarcodeRequiredMessage; ResultOutputTextBox.Text = BarcodeRequiredMessage;
FocusBarcodeInput(); FocusBarcodeInput();
} }
@ -450,7 +537,6 @@ public partial class MainWindow : Window
Variant1 = _loginResult.Variant1, Variant1 = _loginResult.Variant1,
Variant2 = _loginResult.Variant2, Variant2 = _loginResult.Variant2,
Operator = _loginResult.Operator, Operator = _loginResult.Operator,
Password = _loginResult.Password,
ProductionDate = DateTime.Now, ProductionDate = DateTime.Now,
Line = _loginResult.LineNo, Line = _loginResult.LineNo,
LotNo = _loginResult.LotNo, LotNo = _loginResult.LotNo,
@ -473,7 +559,7 @@ public partial class MainWindow : Window
{ {
IcSn = icSn, IcSn = icSn,
PcbBarcode = barcode, PcbBarcode = barcode,
PtVol1 = ParseDecimal(VOutputTextBox.Text), PtVol1 = _lastMeasuredVoltage ?? ParseDecimal(VOutputTextBox.Text),
PtCurrent1 = ParseDecimal(AOutputTextBox.Text), PtCurrent1 = ParseDecimal(AOutputTextBox.Text),
Result = resultCode, Result = resultCode,
Spare1 = null, Spare1 = null,
@ -487,20 +573,22 @@ public partial class MainWindow : Window
{ {
VMinTextBox.Text = FormatDecimal(_inspectionSettings.VMin); VMinTextBox.Text = FormatDecimal(_inspectionSettings.VMin);
VMaxTextBox.Text = FormatDecimal(_inspectionSettings.VMax); VMaxTextBox.Text = FormatDecimal(_inspectionSettings.VMax);
AMinTextBox.Text = FormatDecimal(_inspectionSettings.AMin); AMinTextBox.Text = FormatCurrentLimit(_inspectionSettings.AMin);
AMaxTextBox.Text = FormatDecimal(_inspectionSettings.AMax); AMaxTextBox.Text = FormatCurrentLimit(_inspectionSettings.AMax);
} }
private bool TryApplyInspectionSettingsFromTextBoxes() private bool TryApplyInspectionSettingsFromTextBoxes()
{ {
if (!TryReadRangeValue(VMinTextBox, "V 최소값", out var vMin) || if (!TryReadRangeValue(VMinTextBox, "V 최소값", out var vMin) ||
!TryReadRangeValue(VMaxTextBox, "V 최대값", out var vMax) || !TryReadRangeValue(VMaxTextBox, "V 최대값", out var vMax) ||
!TryReadRangeValue(AMinTextBox, "A 최소값", out var aMin) || !TryReadRangeValue(AMinTextBox, "mA 최소값", out var aMinMilliamp) ||
!TryReadRangeValue(AMaxTextBox, "A 최대값", out var aMax)) !TryReadRangeValue(AMaxTextBox, "mA 최대값", out var aMaxMilliamp))
{ {
return false; return false;
} }
var aMin = MilliampsToAmps(aMinMilliamp);
var aMax = MilliampsToAmps(aMaxMilliamp);
if (vMin > vMax) if (vMin > vMax)
{ {
MessageBox.Show(this, "V 최소값은 최대값보다 클 수 없습니다.", "판정 범위", MessageBoxButton.OK, MessageBoxImage.Warning); MessageBox.Show(this, "V 최소값은 최대값보다 클 수 없습니다.", "판정 범위", MessageBoxButton.OK, MessageBoxImage.Warning);
@ -555,6 +643,59 @@ public partial class MainWindow : Window
return value.ToString("0.00", CultureInfo.InvariantCulture); return value.ToString("0.00", CultureInfo.InvariantCulture);
} }
private static string FormatCurrent(decimal value)
{
return AmpsToMilliamps(value).ToString("0.000", CultureInfo.InvariantCulture);
}
private static string FormatCurrentLimit(decimal value)
{
return AmpsToMilliamps(value).ToString("0.00", CultureInfo.InvariantCulture);
}
private static decimal AmpsToMilliamps(decimal amps)
{
return amps * MilliampsPerAmp;
}
private static decimal MilliampsToAmps(decimal milliamps)
{
return milliamps / MilliampsPerAmp;
}
private static IReadOnlyCollection<string> GetReservedBarcodeScannerPortNames(string hardwareIniPath)
{
var reservedPortNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var boardSettings = BoardHardwareSettings.Load(hardwareIniPath);
AddReservedPortName(reservedPortNames, boardSettings.PortName);
var equipmentSettings = EquipmentMeasurementSettings.Load(hardwareIniPath);
AddReservedScpiChannelPortName(reservedPortNames, equipmentSettings.Voltage);
AddReservedScpiChannelPortName(reservedPortNames, equipmentSettings.Current);
return reservedPortNames;
}
private static void AddReservedScpiChannelPortName(ISet<string> reservedPortNames, ScpiChannelSettings channel)
{
if (string.Equals(channel.Connection, "Serial", StringComparison.OrdinalIgnoreCase) ||
string.Equals(channel.Connection, "COM", StringComparison.OrdinalIgnoreCase))
{
AddReservedPortName(reservedPortNames, channel.PortName);
}
}
private static void AddReservedPortName(ISet<string> reservedPortNames, string portName)
{
if (BarcodeScannerSettings.IsAutoPort(portName))
{
return;
}
reservedPortNames.Add(portName.Trim());
}
private static string GetDatabaseIniPath() private static string GetDatabaseIniPath()
{ {
return Path.Combine(AppContext.BaseDirectory, "Database.ini"); return Path.Combine(AppContext.BaseDirectory, "Database.ini");

1
Models/HousingAssemblyRecord.cs

@ -11,7 +11,6 @@ public sealed class HousingAssemblyRecord
public string Variant1 { get; set; } = string.Empty; public string Variant1 { get; set; } = string.Empty;
public string Variant2 { get; set; } = string.Empty; public string Variant2 { get; set; } = string.Empty;
public string Operator { get; set; } = string.Empty; public string Operator { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public DateTime ProductionDate { get; set; } = DateTime.Now; public DateTime ProductionDate { get; set; } = DateTime.Now;
public string Line { get; set; } = string.Empty; public string Line { get; set; } = string.Empty;
public string LotNo { get; set; } = string.Empty; public string LotNo { get; set; } = string.Empty;

BIN
Resources/Housing_voltage_current_icon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

3
Resources/mobidigm 1.svg

@ -0,0 +1,3 @@
<svg width="216" height="53" viewBox="0 0 216 53" fill="none" xmlns="http://www.w3.org/2000/svg">
<image width="216" height="53" href="mobidigm-logo.png" preserveAspectRatio="none"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

BIN
Resources/mobidigm-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

69
Services/BarcodeScannerSettings.cs

@ -0,0 +1,69 @@
using System.IO;
namespace Housing.Services;
public sealed class BarcodeScannerSettings
{
public bool Enabled { get; set; }
public string PortName { get; set; } = "Auto";
public int BaudRate { get; set; } = 9600;
public int ReadTimeout { get; set; } = 500;
public int IdleCommitMilliseconds { get; set; } = 250;
public bool DtrEnable { get; set; } = true;
public bool RtsEnable { get; set; } = true;
public static BarcodeScannerSettings Load(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException("Hardware.ini file was not found.", filePath);
}
var values = IniFile.LoadSection(filePath, "BarcodeScanner");
return new BarcodeScannerSettings
{
Enabled = GetBool(values, "Enabled", false),
PortName = GetString(values, "PortName", "Auto"),
BaudRate = GetInt(values, "BaudRate", 9600),
ReadTimeout = GetInt(values, "ReadTimeout", 500),
IdleCommitMilliseconds = GetInt(values, "IdleCommitMilliseconds", 250),
DtrEnable = GetBool(values, "DtrEnable", true),
RtsEnable = GetBool(values, "RtsEnable", true)
};
}
public static bool IsAutoPort(string portName)
{
return string.IsNullOrWhiteSpace(portName) ||
string.Equals(portName.Trim(), "Auto", StringComparison.OrdinalIgnoreCase);
}
private static string GetString(Dictionary<string, string> values, string key, string defaultValue)
{
return values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)
? value.Trim()
: defaultValue;
}
private static int GetInt(Dictionary<string, string> values, string key, int defaultValue)
{
return values.TryGetValue(key, out var value) && int.TryParse(value, out var result)
? result
: defaultValue;
}
private static bool GetBool(Dictionary<string, string> values, string key, bool defaultValue)
{
if (!values.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))
{
return defaultValue;
}
return value.Trim().ToUpperInvariant() switch
{
"1" or "TRUE" or "YES" or "ON" => true,
"0" or "FALSE" or "NO" or "OFF" => false,
_ => defaultValue
};
}
}

2
Services/BoardHardwareSettings.cs

@ -10,6 +10,7 @@ public sealed class BoardHardwareSettings
public string PreConnectCommand { get; set; } = "x00o"; public string PreConnectCommand { get; set; } = "x00o";
public string ConnectCommand { get; set; } = "x00c_001101:owt28006727ea97c7801"; public string ConnectCommand { get; set; } = "x00c_001101:owt28006727ea97c7801";
public string ReadIdCommand { get; set; } = "x00c_001101:ow2800326003e"; public string ReadIdCommand { get; set; } = "x00c_001101:ow2800326003e";
public string PostReadIdCommand { get; set; } = "x00o";
public string CalDefaultCommand { get; set; } = "x00c_001001:owt28006727ea97c7801"; public string CalDefaultCommand { get; set; } = "x00c_001001:owt28006727ea97c7801";
public string EndToken { get; set; } = "<end>"; public string EndToken { get; set; } = "<end>";
public bool DtrEnable { get; set; } public bool DtrEnable { get; set; }
@ -31,6 +32,7 @@ public sealed class BoardHardwareSettings
PreConnectCommand = GetString(values, "PreConnectCommand", "x00o"), PreConnectCommand = GetString(values, "PreConnectCommand", "x00o"),
ConnectCommand = GetString(values, "ConnectCommand", "x00c_001101:owt28006727ea97c7801"), ConnectCommand = GetString(values, "ConnectCommand", "x00c_001101:owt28006727ea97c7801"),
ReadIdCommand = GetString(values, "ReadIdCommand", "x00c_001101:ow2800326003e"), ReadIdCommand = GetString(values, "ReadIdCommand", "x00c_001101:ow2800326003e"),
PostReadIdCommand = GetString(values, "PostReadIdCommand", "x00o"),
CalDefaultCommand = GetString(values, "CalDefaultCommand", "x00c_001001:owt28006727ea97c7801"), CalDefaultCommand = GetString(values, "CalDefaultCommand", "x00c_001001:owt28006727ea97c7801"),
EndToken = GetString(values, "EndToken", "<end>"), EndToken = GetString(values, "EndToken", "<end>"),
DtrEnable = GetBool(values, "DtrEnable", false), DtrEnable = GetBool(values, "DtrEnable", false),

2
Services/BoardTestService.cs

@ -47,6 +47,8 @@ public sealed class BoardTestService
throw new InvalidOperationException("IC_SN 읽기 실패"); throw new InvalidOperationException("IC_SN 읽기 실패");
} }
await SendOnlyAndLogAsync(boardClient, "POST_READ_ID", _boardSettings.PostReadIdCommand, log);
if (onIcSnReadAsync is not null) if (onIcSnReadAsync is not null)
{ {
await onIcSnReadAsync(icSn); await onIcSnReadAsync(icSn);

7
Services/HousingAssemblyRepository.cs

@ -28,7 +28,6 @@ INSERT INTO dbo.Housing_Assembly
[Variant_1], [Variant_1],
[Variant_2], [Variant_2],
[Operator], [Operator],
[Password],
[Production_Date], [Production_Date],
[Line], [Line],
[Lot_No], [Lot_No],
@ -43,7 +42,6 @@ VALUES
@Variant_1, @Variant_1,
@Variant_2, @Variant_2,
@Operator, @Operator,
@Password,
@Production_Date, @Production_Date,
@Line, @Line,
@Lot_No, @Lot_No,
@ -57,7 +55,6 @@ VALUES
AddNVarChar(command, "@Variant_1", 10, record.Variant1); AddNVarChar(command, "@Variant_1", 10, record.Variant1);
AddNVarChar(command, "@Variant_2", 10, record.Variant2); AddNVarChar(command, "@Variant_2", 10, record.Variant2);
AddNVarChar(command, "@Operator", 10, record.Operator); AddNVarChar(command, "@Operator", 10, record.Operator);
AddNVarChar(command, "@Password", 10, record.Password);
command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate }); command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate });
AddNVarChar(command, "@Line", 5, record.Line); AddNVarChar(command, "@Line", 5, record.Line);
AddNVarChar(command, "@Lot_No", 8, record.LotNo); AddNVarChar(command, "@Lot_No", 8, record.LotNo);
@ -85,7 +82,6 @@ SET
[Variant_1] = @Variant_1, [Variant_1] = @Variant_1,
[Variant_2] = @Variant_2, [Variant_2] = @Variant_2,
[Operator] = @Operator, [Operator] = @Operator,
[Password] = @Password,
[Production_Date] = @Production_Date, [Production_Date] = @Production_Date,
[Line] = @Line, [Line] = @Line,
[Lot_No] = @Lot_No, [Lot_No] = @Lot_No,
@ -104,7 +100,6 @@ BEGIN
[Variant_1], [Variant_1],
[Variant_2], [Variant_2],
[Operator], [Operator],
[Password],
[Production_Date], [Production_Date],
[Line], [Line],
[Lot_No], [Lot_No],
@ -119,7 +114,6 @@ BEGIN
@Variant_1, @Variant_1,
@Variant_2, @Variant_2,
@Operator, @Operator,
@Password,
@Production_Date, @Production_Date,
@Line, @Line,
@Lot_No, @Lot_No,
@ -136,7 +130,6 @@ COMMIT TRANSACTION;";
AddNVarChar(command, "@Variant_1", 10, record.Variant1); AddNVarChar(command, "@Variant_1", 10, record.Variant1);
AddNVarChar(command, "@Variant_2", 10, record.Variant2); AddNVarChar(command, "@Variant_2", 10, record.Variant2);
AddNVarChar(command, "@Operator", 10, record.Operator); AddNVarChar(command, "@Operator", 10, record.Operator);
AddNVarChar(command, "@Password", 10, record.Password);
command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate }); command.Parameters.Add(new SqlParameter("@Production_Date", SqlDbType.DateTime2) { Value = record.ProductionDate });
AddNVarChar(command, "@Line", 5, record.Line); AddNVarChar(command, "@Line", 5, record.Line);
AddNVarChar(command, "@Lot_No", 8, record.LotNo); AddNVarChar(command, "@Lot_No", 8, record.LotNo);

2
Services/InspectionSettingsStore.cs

@ -53,6 +53,6 @@ public static class InspectionSettingsStore
private static string Format(decimal value) private static string Format(decimal value)
{ {
return value.ToString("0.00", CultureInfo.InvariantCulture); return value.ToString("0.######", CultureInfo.InvariantCulture);
} }
} }

21
Services/LoginAccountRepository.cs

@ -19,18 +19,21 @@ public sealed class LoginAccountRepository
using var connection = new SqlConnection(CreateConnectionString()); using var connection = new SqlConnection(CreateConnectionString());
using var command = connection.CreateCommand(); using var command = connection.CreateCommand();
command.CommandText = $@" command.CommandType = CommandType.StoredProcedure;
SELECT TOP (1) 1 command.CommandText = _loginSettings.GetProcedureName();
FROM {_loginSettings.GetQuotedTableName()} command.Parameters.Add(new SqlParameter("@Operator", SqlDbType.NVarChar, 10) { Value = loginId });
WHERE {_loginSettings.GetQuotedIdColumn()} = @LoginId
AND {_loginSettings.GetQuotedPasswordColumn()} = @Password;";
command.Parameters.Add(new SqlParameter("@LoginId", SqlDbType.NVarChar, 50) { Value = loginId });
command.Parameters.Add(new SqlParameter("@Password", SqlDbType.NVarChar, 50) { Value = password }); command.Parameters.Add(new SqlParameter("@Password", SqlDbType.NVarChar, 50) { Value = password });
await connection.OpenAsync(); await connection.OpenAsync();
var result = await command.ExecuteScalarAsync(); var result = await command.ExecuteScalarAsync();
return result is not null;
return result switch
{
bool isValid => isValid,
byte value => value == 1,
int value => value == 1,
_ => result is not null && Convert.ToInt32(result) == 1
};
} }
private string CreateConnectionString() private string CreateConnectionString()
@ -39,7 +42,7 @@ WHERE {_loginSettings.GetQuotedIdColumn()} = @LoginId
string.IsNullOrWhiteSpace(_databaseSettings.Database) || string.IsNullOrWhiteSpace(_databaseSettings.Database) ||
string.IsNullOrWhiteSpace(_databaseSettings.DbId)) string.IsNullOrWhiteSpace(_databaseSettings.DbId))
{ {
throw new InvalidOperationException("Database.ini의 IP, Database, DbId 값을 확인하세요."); throw new InvalidOperationException("Check Database.ini IP, Database, and DbId values.");
} }
var builder = new SqlConnectionStringBuilder var builder = new SqlConnectionStringBuilder

40
Services/LoginSettings.cs

@ -5,53 +5,40 @@ namespace Housing.Services;
public sealed class LoginSettings public sealed class LoginSettings
{ {
public string Table { get; set; } = "dbo.Housing_Assembly"; private const string DefaultProcedure = "dbo.CheckOperator";
public string IdColumn { get; set; } = "Operator";
public string PasswordColumn { get; set; } = "Password"; public string Procedure { get; set; } = DefaultProcedure;
public bool OfflinePreview { get; set; } public bool OfflinePreview { get; set; }
public static LoginSettings Load(string filePath) public static LoginSettings Load(string filePath)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
{ {
throw new FileNotFoundException("Database.ini 파일을 찾을 수 없습니다.", filePath); throw new FileNotFoundException("Database.ini file was not found.", filePath);
} }
var values = IniFile.LoadSection(filePath, "Login"); var values = IniFile.LoadSection(filePath, "Login");
return new LoginSettings return new LoginSettings
{ {
Table = GetString(values, "Table", "dbo.Housing_Assembly"), Procedure = GetString(values, "Procedure", DefaultProcedure),
IdColumn = GetString(values, "IdColumn", "Operator"),
PasswordColumn = GetString(values, "PasswordColumn", "Password"),
OfflinePreview = GetBool(values, "OfflinePreview", false) OfflinePreview = GetBool(values, "OfflinePreview", false)
}; };
} }
public string GetQuotedTableName() public string GetProcedureName()
{ {
var parts = Table var parts = Procedure
.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(QuoteIdentifier)
.ToArray(); .ToArray();
if (parts.Length is 0 or > 3) if (parts.Length is 0 or > 3 || parts.Any(part => !IsSqlIdentifier(part)))
{ {
throw new InvalidOperationException("Database.ini [Login] Table 값을 확인하세요."); throw new InvalidOperationException("Check Database.ini [Login] Procedure value.");
} }
return string.Join(".", parts); return string.Join(".", parts);
} }
public string GetQuotedIdColumn()
{
return QuoteIdentifier(IdColumn);
}
public string GetQuotedPasswordColumn()
{
return QuoteIdentifier(PasswordColumn);
}
private static string GetString(Dictionary<string, string> values, string key, string defaultValue) private static string GetString(Dictionary<string, string> values, string key, string defaultValue)
{ {
return values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) return values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)
@ -74,13 +61,8 @@ public sealed class LoginSettings
}; };
} }
private static string QuoteIdentifier(string identifier) private static bool IsSqlIdentifier(string value)
{ {
if (Regex.IsMatch(identifier, @"^[A-Za-z_][A-Za-z0-9_]*$")) return Regex.IsMatch(value, @"^[A-Za-z_][A-Za-z0-9_]*$");
{
return $"[{identifier}]";
}
throw new InvalidOperationException($"DB 식별자 값이 올바르지 않습니다: {identifier}");
} }
} }

426
Services/SerialBarcodeScanner.cs

@ -0,0 +1,426 @@
using System.IO.Ports;
using System.Text;
using System.Text.RegularExpressions;
namespace Housing.Services;
public sealed class BarcodeScanReceivedEventArgs : EventArgs
{
public BarcodeScanReceivedEventArgs(string portName, string text, bool isConfirmedPort)
{
PortName = portName;
Text = text;
IsConfirmedPort = isConfirmedPort;
}
public string PortName { get; }
public string Text { get; }
public bool IsConfirmedPort { get; }
}
public sealed class SerialBarcodeScanner : IDisposable
{
private readonly BarcodeScannerSettings _settings;
private readonly HashSet<string> _excludedPortNames;
private readonly List<PortSession> _sessions = new();
private readonly object _sync = new();
private bool _disposed;
private string? _confirmedPortName;
public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable<string> excludedPortNames)
{
_settings = settings;
_excludedPortNames = excludedPortNames
.Where(portName => !string.IsNullOrWhiteSpace(portName))
.Select(NormalizePortName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
}
public event EventHandler<BarcodeScanReceivedEventArgs>? ScanReceived;
public string? ConfirmedPortName
{
get
{
lock (_sync)
{
return _confirmedPortName;
}
}
}
public void Start()
{
ThrowIfDisposed();
var candidatePortNames = GetCandidatePortNames();
if (candidatePortNames.Length == 0)
{
throw new InvalidOperationException("No available COM ports for the barcode scanner.");
}
var openErrors = new List<string>();
foreach (var portName in candidatePortNames)
{
PortSession? session = null;
try
{
var port = CreateSerialPort(portName);
session = new PortSession(port, CommitBufferedScan);
port.DataReceived += SerialPort_DataReceived;
lock (_sync)
{
_sessions.Add(session);
}
port.Open();
port.DiscardInBuffer();
port.DiscardOutBuffer();
}
catch (Exception ex)
{
if (session is not null)
{
lock (_sync)
{
_sessions.Remove(session);
}
CloseSession(session);
}
openErrors.Add($"{portName}: {ex.Message}");
}
}
int sessionCount;
lock (_sync)
{
sessionCount = _sessions.Count;
}
if (sessionCount == 0)
{
var detail = openErrors.Count == 0 ? string.Empty : $" ({string.Join("; ", openErrors)})";
throw new InvalidOperationException($"Could not open a barcode scanner COM port.{detail}");
}
if (!BarcodeScannerSettings.IsAutoPort(_settings.PortName) || sessionCount == 1)
{
lock (_sync)
{
_confirmedPortName = _sessions[0].Port.PortName;
}
}
}
public void ConfirmPort(string portName)
{
if (string.IsNullOrWhiteSpace(portName))
{
return;
}
List<PortSession> sessionsToClose;
lock (_sync)
{
if (_disposed)
{
return;
}
var normalizedPortName = NormalizePortName(portName);
if (!string.IsNullOrWhiteSpace(_confirmedPortName) &&
!string.Equals(_confirmedPortName, normalizedPortName, StringComparison.OrdinalIgnoreCase))
{
return;
}
var confirmedSession = _sessions.FirstOrDefault(session =>
string.Equals(session.Port.PortName, normalizedPortName, StringComparison.OrdinalIgnoreCase));
if (confirmedSession is null)
{
return;
}
_confirmedPortName = confirmedSession.Port.PortName;
sessionsToClose = _sessions
.Where(session => !ReferenceEquals(session, confirmedSession))
.ToList();
_sessions.RemoveAll(session => !ReferenceEquals(session, confirmedSession));
}
foreach (var session in sessionsToClose)
{
CloseSession(session);
}
}
private SerialPort CreateSerialPort(string portName)
{
return new SerialPort(portName, _settings.BaudRate)
{
Encoding = Encoding.ASCII,
NewLine = "\r\n",
ReadTimeout = _settings.ReadTimeout,
WriteTimeout = _settings.ReadTimeout,
DtrEnable = _settings.DtrEnable,
RtsEnable = _settings.RtsEnable
};
}
private string[] GetCandidatePortNames()
{
if (!BarcodeScannerSettings.IsAutoPort(_settings.PortName))
{
return new[] { NormalizePortName(_settings.PortName) };
}
return SerialPort.GetPortNames()
.Select(NormalizePortName)
.Where(portName => !_excludedPortNames.Contains(portName))
.OrderBy(GetPortNumber)
.ThenBy(portName => portName, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (sender is not SerialPort port)
{
return;
}
PortSession? session;
lock (_sync)
{
if (_disposed)
{
return;
}
session = _sessions.FirstOrDefault(item => ReferenceEquals(item.Port, port));
}
if (session is null)
{
return;
}
string text;
try
{
text = port.ReadExisting();
}
catch
{
return;
}
if (string.IsNullOrEmpty(text))
{
return;
}
var scans = AppendReceivedText(session, text);
foreach (var scan in scans)
{
PublishScan(session, scan);
}
}
private List<string> AppendReceivedText(PortSession session, string text)
{
lock (session.Sync)
{
session.Buffer.Append(text);
var scans = DrainCompleteScans(session.Buffer);
if (session.Buffer.Length > 0)
{
var delay = Math.Max(50, _settings.IdleCommitMilliseconds);
session.CommitTimer.Change(delay, Timeout.Infinite);
}
else
{
session.CommitTimer.Change(Timeout.Infinite, Timeout.Infinite);
}
return scans;
}
}
private void CommitBufferedScan(PortSession session)
{
string scan;
lock (session.Sync)
{
scan = NormalizeScanText(session.Buffer.ToString());
session.Buffer.Clear();
session.CommitTimer.Change(Timeout.Infinite, Timeout.Infinite);
}
PublishScan(session, scan);
}
private void PublishScan(PortSession session, string scan)
{
scan = NormalizeScanText(scan);
if (string.IsNullOrWhiteSpace(scan))
{
return;
}
bool isConfirmedPort;
lock (_sync)
{
if (_disposed || !_sessions.Contains(session))
{
return;
}
isConfirmedPort = string.Equals(
_confirmedPortName,
session.Port.PortName,
StringComparison.OrdinalIgnoreCase);
}
ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scan, isConfirmedPort));
}
private static List<string> DrainCompleteScans(StringBuilder buffer)
{
var scans = new List<string>();
var text = buffer.ToString();
var start = 0;
for (var index = 0; index < text.Length; index++)
{
if (text[index] != '\r' && text[index] != '\n')
{
continue;
}
var line = NormalizeScanText(text[start..index]);
if (line.Length > 0)
{
scans.Add(line);
}
if (text[index] == '\r' && index + 1 < text.Length && text[index + 1] == '\n')
{
index++;
}
start = index + 1;
}
if (start > 0)
{
buffer.Clear();
if (start < text.Length)
{
buffer.Append(text[start..]);
}
}
return scans;
}
private static string NormalizeScanText(string text)
{
if (string.IsNullOrEmpty(text))
{
return string.Empty;
}
var builder = new StringBuilder(text.Length);
foreach (var character in text.Trim())
{
if (!char.IsControl(character))
{
builder.Append(character);
}
}
return builder.ToString().Trim();
}
private static string NormalizePortName(string portName)
{
return portName.Trim().ToUpperInvariant();
}
private static int GetPortNumber(string portName)
{
var match = Regex.Match(portName, @"\d+");
return match.Success && int.TryParse(match.Value, out var number)
? number
: int.MaxValue;
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
private void CloseSession(PortSession session)
{
try
{
session.Port.DataReceived -= SerialPort_DataReceived;
}
catch
{
// Best effort during shutdown.
}
session.Dispose();
}
public void Dispose()
{
List<PortSession> sessionsToClose;
lock (_sync)
{
if (_disposed)
{
return;
}
_disposed = true;
sessionsToClose = _sessions.ToList();
_sessions.Clear();
}
foreach (var session in sessionsToClose)
{
CloseSession(session);
}
}
private sealed class PortSession : IDisposable
{
private readonly Action<PortSession> _commitBufferedScan;
public PortSession(SerialPort port, Action<PortSession> commitBufferedScan)
{
Port = port;
_commitBufferedScan = commitBufferedScan;
CommitTimer = new Timer(_ => _commitBufferedScan(this), null, Timeout.Infinite, Timeout.Infinite);
}
public SerialPort Port { get; }
public StringBuilder Buffer { get; } = new();
public object Sync { get; } = new();
public Timer CommitTimer { get; }
public void Dispose()
{
CommitTimer.Dispose();
Port.Dispose();
}
}
}

BIN
SetupProject/Release/SetupProject.tmp

Binary file not shown.

44829
SetupProject/SetupProject.vdproj

File diff suppressed because it is too large

112
init.sql

@ -0,0 +1,112 @@
-- Run this script against the Housing database before running the app.
-- It creates the login table and login procedures.
IF OBJECT_ID(N'dbo.Operator_Account', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Operator_Account
(
[Operator] NVARCHAR(10) NOT NULL
CONSTRAINT PK_Operator_Account PRIMARY KEY,
[Password] NVARCHAR(50) NOT NULL,
[CreatedAt] DATETIME2(0) NOT NULL
CONSTRAINT DF_Operator_Account_CreatedAt DEFAULT (SYSDATETIME()),
[UpdatedAt] DATETIME2(0) NULL
);
END;
GO
IF COL_LENGTH(N'dbo.Operator_Account', N'IsActive') IS NOT NULL
BEGIN
DECLARE @DefaultConstraintName SYSNAME;
SELECT @DefaultConstraintName = dc.name
FROM sys.default_constraints AS dc
INNER JOIN sys.columns AS c
ON c.default_object_id = dc.object_id
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.Operator_Account', N'U')
AND c.name = N'IsActive';
IF @DefaultConstraintName IS NOT NULL
BEGIN
EXEC(N'ALTER TABLE dbo.Operator_Account DROP CONSTRAINT [' + @DefaultConstraintName + N']');
END;
ALTER TABLE dbo.Operator_Account DROP COLUMN [IsActive];
END;
GO
IF EXISTS (SELECT 1 FROM dbo.Operator_Account WHERE [Operator] = N'su')
BEGIN
UPDATE dbo.Operator_Account
SET
[Password] = N'su',
[UpdatedAt] = SYSDATETIME()
WHERE [Operator] = N'su';
END
ELSE
BEGIN
INSERT INTO dbo.Operator_Account
(
[Operator],
[Password]
)
VALUES
(
N'su',
N'su'
);
END;
GO
CREATE OR ALTER PROCEDURE dbo.CheckOperator
@Operator NVARCHAR(10),
@Password NVARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
SELECT CAST(
CASE
WHEN EXISTS
(
SELECT 1
FROM dbo.Operator_Account
WHERE [Operator] = @Operator
AND [Password] = @Password
)
THEN 1
ELSE 0
END AS BIT) AS IsValid;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SaveOperator
@Operator NVARCHAR(10),
@Password NVARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT 1 FROM dbo.Operator_Account WHERE [Operator] = @Operator)
BEGIN
UPDATE dbo.Operator_Account
SET
[Password] = @Password,
[UpdatedAt] = SYSDATETIME()
WHERE [Operator] = @Operator;
END
ELSE
BEGIN
INSERT INTO dbo.Operator_Account
(
[Operator],
[Password]
)
VALUES
(
@Operator,
@Password
);
END;
END;
GO

BIN
publish/win-x64/D3DCompiler_47_cor3.dll

Binary file not shown.

12
publish/win-x64/Database.ini

@ -0,0 +1,12 @@
[Database]
IP=127.0.0.1
Database=Housing
DbId=sa
DbPw=your_password
Encrypt=True
TrustServerCertificate=True
Timeout=5
[Login]
Procedure=dbo.CheckOperator
OfflinePreview=true

78
publish/win-x64/Hardware.ini

@ -0,0 +1,78 @@
[Board]
PortName=COM5
BaudRate=115200
ReadTimeout=5000
PreConnectCommand=x00o
ConnectCommand=x00c_001101:owt28006727ea97c7801
ReadIdCommand=x00c_001101:ow2800326003e
PostReadIdCommand=x00o
CalDefaultCommand=x00c_001001:owt28006727ea97c7801
EndToken=<end>
DtrEnable=false
RtsEnable=false
[BarcodeScanner]
Enabled=true
PortName=Auto
BaudRate=9600
ReadTimeout=500
IdleCommitMilliseconds=250
DtrEnable=true
RtsEnable=true
[StartSignal]
; USB-6501
Enabled=true
Connection=Ni6501
PhysicalChannel=Dev1/port0/line0
ActiveState=High
PollIntervalMilliseconds=50
TimeoutMilliseconds=30000
PostSignalDelayMilliseconds=2000
RequireInactiveBeforeStart=true
[Equipment]
; V 출력: Keysight 34465A DMM 값
; A 출력: Keysight E36233A Power Supply CH1 Current 값
; USB 장비는 Keysight Connection Expert 또는 NI MAX에서 보이는 VISA 주소를 Resource
Timeout=5000
SettleMilliseconds=300
PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2)
PreBoardDelayMilliseconds=2000
LogoutCommand=OUTP OFF, (@1);OUTP OFF, (@2)
; 34465A DMM
VoltageConnection=LAN
VoltageResource=
VoltageIdnMatch=34465A
VoltageHost=192.168.200.3
VoltagePort=5025
VoltageSetupCommand=
VoltageReadCommand=MEAS:VOLT:DC?
; E36233A Power Supply
CurrentConnection=LAN
CurrentResource=
CurrentIdnMatch=E36233A
; Change this to the E36233A LAN IP if the instrument uses a different address.
CurrentHost=192.168.200.111
CurrentPort=5025
CurrentPortName=
CurrentBaudRate=9600
CurrentTerminator=LF
CurrentSetupCommand=OUTP ON, (@1)
CurrentCleanupCommand=OUTP OFF, (@1)
CurrentReadCommand=MEAS:CURR? (@1)
; COM 포트로 잡히는 장비 예시:
; VoltageConnection=Serial
; VoltagePortName=Auto
; VoltageBaudRate=115200
; LAN 장비 예시:
; VoltageConnection=Tcp
; VoltageHost=192.168.0.10
; VoltagePort=5025
; E36233A 파워서플라이에서 CH1 전압까지 읽고 싶을 때 예시:
; VoltageReadCommand=MEAS:VOLT? CH1

BIN
publish/win-x64/Housing.exe

Binary file not shown.

BIN
publish/win-x64/Housing.pdb

Binary file not shown.

5
publish/win-x64/InspectionSettings.ini

@ -0,0 +1,5 @@
[Judgement]
VMin=0.00
VMax=9.99
AMin=0.00
AMax=9.99

BIN
publish/win-x64/Microsoft.Data.SqlClient.SNI.dll

Binary file not shown.

BIN
publish/win-x64/PenImc_cor3.dll

Binary file not shown.

BIN
publish/win-x64/PresentationNative_cor3.dll

Binary file not shown.

112
publish/win-x64/init.sql

@ -0,0 +1,112 @@
-- Run this script against the Housing database before running the app.
-- It creates the login table and login procedures.
IF OBJECT_ID(N'dbo.Operator_Account', N'U') IS NULL
BEGIN
CREATE TABLE dbo.Operator_Account
(
[Operator] NVARCHAR(10) NOT NULL
CONSTRAINT PK_Operator_Account PRIMARY KEY,
[Password] NVARCHAR(50) NOT NULL,
[CreatedAt] DATETIME2(0) NOT NULL
CONSTRAINT DF_Operator_Account_CreatedAt DEFAULT (SYSDATETIME()),
[UpdatedAt] DATETIME2(0) NULL
);
END;
GO
IF COL_LENGTH(N'dbo.Operator_Account', N'IsActive') IS NOT NULL
BEGIN
DECLARE @DefaultConstraintName SYSNAME;
SELECT @DefaultConstraintName = dc.name
FROM sys.default_constraints AS dc
INNER JOIN sys.columns AS c
ON c.default_object_id = dc.object_id
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.Operator_Account', N'U')
AND c.name = N'IsActive';
IF @DefaultConstraintName IS NOT NULL
BEGIN
EXEC(N'ALTER TABLE dbo.Operator_Account DROP CONSTRAINT [' + @DefaultConstraintName + N']');
END;
ALTER TABLE dbo.Operator_Account DROP COLUMN [IsActive];
END;
GO
IF EXISTS (SELECT 1 FROM dbo.Operator_Account WHERE [Operator] = N'su')
BEGIN
UPDATE dbo.Operator_Account
SET
[Password] = N'su',
[UpdatedAt] = SYSDATETIME()
WHERE [Operator] = N'su';
END
ELSE
BEGIN
INSERT INTO dbo.Operator_Account
(
[Operator],
[Password]
)
VALUES
(
N'su',
N'su'
);
END;
GO
CREATE OR ALTER PROCEDURE dbo.CheckOperator
@Operator NVARCHAR(10),
@Password NVARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
SELECT CAST(
CASE
WHEN EXISTS
(
SELECT 1
FROM dbo.Operator_Account
WHERE [Operator] = @Operator
AND [Password] = @Password
)
THEN 1
ELSE 0
END AS BIT) AS IsValid;
END;
GO
CREATE OR ALTER PROCEDURE dbo.SaveOperator
@Operator NVARCHAR(10),
@Password NVARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT 1 FROM dbo.Operator_Account WHERE [Operator] = @Operator)
BEGIN
UPDATE dbo.Operator_Account
SET
[Password] = @Password,
[UpdatedAt] = SYSDATETIME()
WHERE [Operator] = @Operator;
END
ELSE
BEGIN
INSERT INTO dbo.Operator_Account
(
[Operator],
[Password]
)
VALUES
(
@Operator,
@Password
);
END;
END;
GO

BIN
publish/win-x64/vcruntime140_cor3.dll

Binary file not shown.

BIN
publish/win-x64/wpfgfx_cor3.dll

Binary file not shown.
Loading…
Cancel
Save