using SA_LTT.Base;
using SA_LTT.Module.RTC5;
using System;
using System.Collections.Generic;
using System.IO;
namespace SA_LTT.Module
{
public enum ScanMode
{
JUMP,
MARK,
}
///
/// Scanner 가공 좌표.
///
public struct Coord
{
ScanMode _scanMode;
int _x;
int _y;
public ScanMode ScanMode
{
get
{
return _scanMode;
}
set
{
_scanMode = value;
}
}
///
/// -8388608 ~ 8388607
///
public int X
{
get
{
return _x;
}
set
{
if (value > 8388607)
{
_x = 8388607;
}
else if (value < -8388608)
{
_x = -8388608;
}
else
{
_x = value;
}
}
}
///
/// -8388608 ~ 8388607
///
public int Y
{
get
{
return _y;
}
set
{
if (value > 8388607)
{
_y = 8388607;
}
else if (value < -8388608)
{
_y = -8388608;
}
else
{
_y = value;
}
}
}
public Coord(ScanMode scanMode, int x, int y)
{
_scanMode = ScanMode.JUMP;
_x = 0;
_y = 0;
ScanMode = scanMode;
X = x;
Y = y;
}
}
public delegate void ScanListAdded(Coord coord);
public delegate void ScanListCleared();
public class Scanner : XmlManager
{
private Equipment _equipment;
private object _lock = new object();
public event ScanListAdded ScanListAdded;
public event ScanListCleared ScanListCleared;
readonly string _fileName = "Scanner.xml";
readonly string _filePath = @"Setting\";
#region Field
private string _correctionFilePath;
private bool _isInitialized;
private bool _isBusy;
private uint _isPosition;
private int _encoder0;
private int _encoder1;
private int _encoderCountPerMmX;
private int _encoderCountPerMmY;
private double _fieldOfView;
private uint _halfPeriod;
private uint _pulseLength1;
private uint _timeBase;
private uint _jumpDelay;
private uint _markDelay;
private uint _polygonDelay;
private int _laserOnDelay;
private uint _laserOffDelay;
private double _xGain;
private double _yGain;
private bool _isLaserOn;
private List scanList = new List();
#endregion
#region Property
//보정 파일 위치
public string CorrectionFilePath
{
get
{
return _correctionFilePath;
}
set
{
_correctionFilePath = value;
}
}
///
/// RTC5 Board 마지막 초기화 결과
///
public bool IsInitialized
{
get
{
return _isInitialized;
}
set
{
_isInitialized = value;
}
}
///
/// RTC5 Board Status.
///
public bool IsBusy
{
get
{
return _isBusy;
}
set
{
_isBusy = value;
}
}
///
/// Scanner에 연결된 Encoder 0 값
///
public int Encoder0
{
get
{
return _encoder0;
}
set
{
_encoder0 = value;
}
}
///
/// Scanner에 연결된 Encoder 1 값
///
public int Encoder1
{
get
{
return _encoder1;
}
set
{
_encoder1 = value;
}
}
///
/// X축 mm당 Encoder Count
///
public int EncoderCountPerMmX
{
get
{
return _encoderCountPerMmX;
}
set
{
_encoderCountPerMmX = value;
}
}
///
/// Y축 mm당 Encoder Count
///
public int EncoderCountPerMmY
{
get
{
return _encoderCountPerMmY;
}
set
{
_encoderCountPerMmY = value;
}
}
///
/// 1mm당 bits 수.
///
public double BitsPerMm
{
get
{
if (FOV == 0)
{
return 0;
}
else
{
return Math.Pow(2, 20 - 1) / (FOV / 2);
}
}
}
///
/// Scanner의 field of view
///
public double FOV
{
get
{
return _fieldOfView;
}
set
{
_fieldOfView = value;
}
}
///
/// Scanner Kfactor : FOV에 따라 정해짐
///
public double KFactor
{
get
{
if (FOV == 0)
{
return 1.0;
}
else
{
return Math.Pow(2, 20) / (FOV / 2);
}
}
}
///
/// Half of the laser signal period
/// 1bit equals 1/64 us. Allowed range [0 ~ 2^(32 -1)]
///
public uint HalfPeriod
{
get
{
return _halfPeriod;
}
set
{
if (value > 2147483648)
{
_halfPeriod = 2147483648;
}
else
{
_halfPeriod = value;
}
}
}
///
/// Pulse widths of signals Laser1
/// 1bit equals 1/64 us. Allowed range [0 ~ 2^(32 -1)]
///
public uint PulseLength1
{
get
{
return _pulseLength1;
}
set
{
if (value > 2147483648)
{
_pulseLength1 = 2147483648;
}
else
{
_pulseLength1 = value;
}
}
}
///
/// Pulse widths of signals Laser2
/// The value of PulseLength2 is ignored and is no longer used
///
public uint PulseLength2
{
get
{
return _pulseLength1;
}
}
///
/// time base; 0 corresponds to 1 microsecond
/// In RTC5 mode, the value is ignored and the clock frequency is fixed
/// at 64 MHz. 1 bit equals 1/64 µs
///
public uint TimeBase
{
get
{
return _timeBase;
}
set
{
_timeBase = value;
}
}
///
/// Jump delay in 10 microseconds
///
public uint JumpDelay
{
get
{
return _jumpDelay;
}
set
{
_jumpDelay = value;
}
}
///
/// Mark delay in 10 microseconds
///
public uint MarkDelay
{
get
{
return _markDelay;
}
set
{
_markDelay = value;
}
}
///
/// Polygon delay in 10 microseconds
///
public uint PolygonDelay
{
get
{
return _polygonDelay;
}
set
{
_polygonDelay = value;
}
}
///
/// Laser on delay in microseconds
///
public int LaserOnDelay
{
get
{
return _laserOnDelay;
}
set
{
_laserOnDelay = value;
}
}
///
/// Laser off delay in microseconds
///
public uint LaserOffDelay
{
get
{
return _laserOffDelay;
}
set
{
_laserOffDelay = value;
}
}
public uint IsPosition
{
get
{
return _isPosition;
}
set
{
_isPosition = value;
}
}
public double XGain
{
get
{
return _xGain;
}
set
{
if (value < 0)
_xGain = 1;
_xGain = value;
}
}
public double YGain
{
get
{
return _yGain;
}
set
{
if (value < 0)
_yGain = 1;
_yGain = value;
}
}
public bool IsLaserOn
{
get
{
return _isLaserOn;
}
set
{
_isLaserOn = value;
}
}
#endregion
public Scanner(Equipment equipment)
{
_equipment = equipment;
TimeBase = 0;
Refresh();
}
public Scanner()
{
}
public void Refresh()
{
DirectoryInfo directoryInfo = new DirectoryInfo(Equipment.settingFilePath + _filePath);
if (directoryInfo.Exists == false)
{
directoryInfo.Create();
}
FileInfo fileInfo = new FileInfo(directoryInfo.FullName + _fileName);
if (fileInfo.Exists == false)
{
fileInfo.Create().Close();
SaveFile(fileInfo.FullName, this);
}
else
{
Scanner scanner = new Scanner();
scanner = ReadFile(fileInfo.FullName);
Copy(scanner);
}
}
///
/// Error 발생했는지 확인.
///
///
private bool ErrorExist()
{
uint errorCode = RTC5Wrap.get_error();
if (errorCode == 0) return false;
byte[] errorBytes = BitConverter.GetBytes(errorCode);
System.Collections.BitArray errorBits = new System.Collections.BitArray(errorBytes);
string str = string.Empty;
if (errorBits[0] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0800_RTC5_NO_BOARD_FOUND);
if (errorBits[1] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0801_RTC5_ACCESS_DENIED);
if (errorBits[2] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0802_RTC5_COMMAND_NOT_FORWARDED);
if (errorBits[3] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0803_RTC5_NO_RESPONSE_FROM_BOARD);
if (errorBits[4] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0804_RTC5_INVALID_PARAMETER);
if (errorBits[5] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0805_RTC5_LIST_PROCESSING_IS_NOT_ACTIVE);
if (errorBits[6] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0806_RTC5_LIST_COMMAND_REJECTED);
if (errorBits[7] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0807_RTC5_LIST_COMMAND_HAS_BEEN_COVERTED);
if (errorBits[8] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0808_RTC5_VERSION_ERROR_RTC5_DLL_VERSION);
if (errorBits[9] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0809_RTC5_VERIFY_ERROR_RTC5_DLL_VERSION);
if (errorBits[10] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0810_RTC5_DSP_VERSION_ERROR);
if (errorBits[11] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0811_RTC5_DLL_INTERNAL_WINDOWS_MEMORY_REQUEST_FAIL);
if (errorBits[12] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0812_RTC5_EEPROM_READ_OR_WRITE_ERROR);
if (errorBits[16] == true)
_equipment.alarmManager.Occur(AlarmCode.AL_0813_RTC5_ERROR_READING_PCI_CONFIGURATION_REGISTER);
RTC5Wrap.reset_error(errorCode);
return true;
}
///
/// Load Program 후 반환된 코드로 Error 발생 확인.
///
///
///
private bool LoadProgramFileErrorExist(uint errorCode)
{
switch (errorCode)
{
case 0:
return false;
case 1:
_equipment.alarmManager.Occur(AlarmCode.AL_0814_RTC5_RESET_ERROR);
return true;
case 2:
_equipment.alarmManager.Occur(AlarmCode.AL_0815_RTC5_UNRESET_ERROR);
return true;
case 3:
_equipment.alarmManager.Occur(AlarmCode.AL_0816_RTC5_FILE_ERROR);
return true;
case 4:
_equipment.alarmManager.Occur(AlarmCode.AL_0817_RTC5_FORMAT_ERROR);
return true;
case 5:
_equipment.alarmManager.Occur(AlarmCode.AL_0818_RTC5_SYSTEM_ERROR);
return true;
case 6:
_equipment.alarmManager.Occur(AlarmCode.AL_0819_RTC5_ACCESS_ERROR);
return true;
case 7:
_equipment.alarmManager.Occur(AlarmCode.AL_0820_RTC5_VERSION_ERROR);
return true;
case 8:
_equipment.alarmManager.Occur(AlarmCode.AL_0821_RTC5_SYSTEM_DRIVER_NOT_FOUND);
return true;
case 9:
_equipment.alarmManager.Occur(AlarmCode.AL_0822_RTC5_DRIVER_CALL_ERROR);
return true;
case 10:
_equipment.alarmManager.Occur(AlarmCode.AL_0823_RTC5_CONFIGURATION_ERROR);
return true;
case 11:
_equipment.alarmManager.Occur(AlarmCode.AL_0824_RTC5_FPGA_FIRMWARE_ERROR);
return true;
case 12:
_equipment.alarmManager.Occur(AlarmCode.AL_0825_RTC5_PCI_DOWNLOAD_ERROR);
return true;
case 13:
_equipment.alarmManager.Occur(AlarmCode.AL_0826_RTC5_BUSY_ERROR);
return true;
case 14:
_equipment.alarmManager.Occur(AlarmCode.AL_0827_RTC5_DSP_MEMORY_ERROR);
return true;
case 15:
_equipment.alarmManager.Occur(AlarmCode.AL_0828_RTC5_VERIFY_ERROR);
return true;
case 16:
_equipment.alarmManager.Occur(AlarmCode.AL_0829_RTC5_PCI_ERROR);
return true;
default:
return true;
}
}
///
/// 보정파일 읽어올때 반환된 코드로 Error 발생 확인.
///
///
///
private bool LoadCorrectionFileErrorExist(uint errorCode)
{
switch (errorCode)
{
case 0:
return false;
case 1:
_equipment.alarmManager.Occur(AlarmCode.AL_0830_RTC5_FILE_ERROR);
return true;
case 2:
_equipment.alarmManager.Occur(AlarmCode.AL_0831_RTC5_MEMORY_ERROR);
return true;
case 3:
_equipment.alarmManager.Occur(AlarmCode.AL_0832_RTC5_FILE_OPEN_ERROR);
return true;
case 4:
_equipment.alarmManager.Occur(AlarmCode.AL_0833_RTC5_DSP_MEMORY_ERROR);
return true;
case 5:
_equipment.alarmManager.Occur(AlarmCode.AL_0834_RTC5_PCI_DOWNLOAD_ERROR);
return true;
case 8:
_equipment.alarmManager.Occur(AlarmCode.AL_0835_RTC5_SYSTEM_DRIVER_NOT_FOUND);
return true;
case 10:
_equipment.alarmManager.Occur(AlarmCode.AL_0836_RTC5_PARAMETER_ERROR);
return true;
case 11:
_equipment.alarmManager.Occur(AlarmCode.AL_0837_RTC5_ACCESS_ERROR);
return true;
case 12:
_equipment.alarmManager.Occur(AlarmCode.AL_0838_RTC5_WARNING_3D_CORRECTION_TABLE);
return true;
case 13:
_equipment.alarmManager.Occur(AlarmCode.AL_0839_RTC5_BUSY_ERROR);
return true;
case 14:
_equipment.alarmManager.Occur(AlarmCode.AL_0840_RTC5_PCI_UPLOAD_ERROR);
return true;
case 15:
_equipment.alarmManager.Occur(AlarmCode.AL_0841_RTC5_VERIFY_ERROR);
return true;
default:
return true;
}
}
///
/// RTC 5 DLL 초기화.
///
///
private bool InitializeRTC5Dll()
{
// ====== RTC 5 Dll 초기화 ======
RTC5Wrap.init_rtc5_dll();
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
///
/// RTC5 RBF, OUT, DAT 파일 로드
///
///
private bool LoadProgramFiles()
{
uint errorCode = RTC5Wrap.load_program_file(null);
if (LoadProgramFileErrorExist(errorCode))
{
return false;
}
else
{
return true;
}
}
///
/// 보정파일 로드
///
/// 보정파일 위치
///
private bool LoadCorrectionFiles(string filePath)
{
uint errorCode = RTC5Wrap.load_correction_file(CorrectionFilePath,
1, // table; #1 is used by default
2); // 2D correction files and 3D correction files are sotred as a 2D correction table.
if (LoadCorrectionFileErrorExist(errorCode))
{
return false;
}
else
{
return true;
}
}
///
/// RTC5 보드 초기화. [기본 위치 : 실행파일 위치]
///
/// 보정 파일 위치
///
public bool Initialize(string correctionFilePath = null)
{
lock (_lock)
{
if (InitializeRTC5Dll() == false)
{
IsInitialized = false;
return false;
}
if (LoadProgramFiles() == false)
{
IsInitialized = false;
return false;
}
if (LoadCorrectionFiles(correctionFilePath) == false)
{
IsInitialized = false;
return false;
}
}
RTC5Wrap.home_position(0, 0);
if(SetLaserDelays() && SetLaserTiming() && SetScannerDelay())
{
IsInitialized = true;
}
else
{
IsInitialized = false;
}
RTC5Wrap.set_laser_control(1);
return true;
}
///
/// List 동작 중 확인.
///
///
public bool GetBusy()
{
try
{
if (IsInitialized)
{
lock (_lock)
{
uint ubusy, position;
RTC5Wrap.get_status(out ubusy, out position);
IsBusy = ubusy != 0;
IsPosition = position;
}
}
else
{
IsBusy = false;
}
return IsBusy;
}
catch(Exception e)
{
EquipmentLogManager.Instance.WriteExceptionLog(e.StackTrace);
return false;
}
}
public bool GetLaserOn()
{
try
{
if (IsInitialized)
{
lock (_lock)
{
IsLaserOn = RTC5Wrap.get_value((ushort)0) == (short)1;
}
}
else
{
IsLaserOn = false;
}
return IsLaserOn;
}
catch(Exception e)
{
EquipmentLogManager.Instance.WriteExceptionLog(e.StackTrace);
return false;
}
}
///
/// Laser timing 설정 [Laser Half of signal period, Laser Pulse widths, Time Base]
///
///
public bool SetLaserTiming()
{
RTC5Wrap.set_start_list(1);
RTC5Wrap.set_laser_timing(HalfPeriod, PulseLength1, PulseLength2, TimeBase);
RTC5Wrap.set_end_of_list();
RTC5Wrap.execute_list(1);
//CtlFrequency(1000, 0.085f);
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
///
/// Scanner delay 설정. [jump delay, mark delay, polygon delay]
///
///
public bool SetScannerDelay()
{
RTC5Wrap.set_start_list(1);
RTC5Wrap.set_scanner_delays(JumpDelay, MarkDelay, PolygonDelay);
RTC5Wrap.set_end_of_list();
RTC5Wrap.execute_list(1);
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
/// 주파수와 펄스폭 설정
/// 주파수 (Hz)
/// 펄스폭 (usec)
///
public virtual bool CtlFrequency(float frequency, float pulseWidth)
{
if (IsBusy)
{
return false;
}
if ((double)frequency <= 0.0 || (double)pulseWidth <= 0.0)
{
return false;
}
double num1 = Math.Round(1.0 / (double)frequency * 1000000.0 / 2.0, 3);
double num2 = Math.Round((double)pulseWidth, 3);
RTC5Wrap.set_start_list(1);
RTC5Wrap.set_laser_timing((uint)Math.Floor(num1 * 64.0), (uint)Math.Floor(num2 * 64.0), (uint)Math.Floor(num2 * 64.0), 0U);
RTC5Wrap.set_end_of_list();
RTC5Wrap.execute_list(1);
return true;
}
///
/// Laser delay 설정 [Laser on delay, Laser off delay]
///
///
public bool SetLaserDelays()
{
RTC5Wrap.set_start_list(1);
RTC5Wrap.set_laser_delays(LaserOnDelay, LaserOffDelay);
RTC5Wrap.set_end_of_list();
RTC5Wrap.execute_list(1);
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
public void LaserOn()
{
RTC5Wrap.laser_signal_on();
}
public void LaserOff()
{
RTC5Wrap.laser_signal_off();
}
///
/// Set jump speed, mark speed
///
/// mm/s
/// mm/s
///
public bool SetJumpMarkSpeed(double jumpSpeed, double markSpeed)
{
if (jumpSpeed == 0)
jumpSpeed = 10;
if (markSpeed == 0)
markSpeed = 10;
double jump = (BitsPerMm / 1000) * jumpSpeed;
double mark = (BitsPerMm / 1000) * markSpeed;
RTC5Wrap.set_start_list(1);
RTC5Wrap.set_jump_speed(jump);
RTC5Wrap.set_mark_speed(mark);
RTC5Wrap.set_end_of_list();
RTC5Wrap.execute_list(1);
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
///
/// RCT5 보드에 연결되어 있는 Encoder 값 읽기.
///
///
public bool GetEncoder()
{
int encoder0, encoder1;
RTC5Wrap.get_encoder(out encoder0, out encoder1);
Encoder0 = encoder0;
Encoder1 = encoder1;
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
///
/// Scan list Data 반환
///
///
public Coord[] GetScanList()
{
return scanList.ToArray();
}
///
/// Scan list 초기화
///
///
public bool ClearScanList()
{
scanList.Clear();
ScanListCleared.Invoke();
return true;
}
///
/// Scan list에 가공 정보 추가
///
///
///
public bool AddScanList(Coord coord)
{
scanList.Add(coord);
ScanListAdded?.Invoke(coord);
return true;
}
///
///
///
///
/// mm
/// mm
///
public bool AddScanList(ScanMode scanMode, double x, double y)
{
Coord coord = new Coord();
coord.ScanMode = scanMode;
coord.X = (int)Math.Floor(x * BitsPerMm);
coord.Y = (int)Math.Floor(y * BitsPerMm);
scanList.Add(coord);
ScanListAdded?.Invoke(coord);
return true;
}
///
/// 가공 시작
///
///
public bool ExcuteScanList(bool onTheFly = false)
{
RTC5Wrap.set_start_list(1);
if (onTheFly)
{
double ScaleX;
double ScaleY;
if (EncoderCountPerMmX == 0)
{
ScaleX = 0;
}
else
{
ScaleX = BitsPerMm / EncoderCountPerMmX;
RTC5Wrap.set_fly_x(ScaleX);
}
if (EncoderCountPerMmY == 0)
{
ScaleY = 0;
}
else
{
ScaleY = BitsPerMm / EncoderCountPerMmY;
RTC5Wrap.set_fly_y(ScaleY);
}
RTC5Wrap.set_rot_center(0, 0);
}
foreach (Coord coord in scanList)
{
int x = (int)(coord.X * XGain);
int y = (int)(coord.Y * YGain);
if (coord.ScanMode == ScanMode.JUMP)
{
//RTC5Wrap.jump_abs(coord.X, coord.Y);
RTC5Wrap.jump_abs(x, y);
}
else
{
//RTC5Wrap.mark_abs(coord.X, coord.Y);
RTC5Wrap.mark_abs(x, y);
}
}
if (onTheFly)
{
RTC5Wrap.fly_return(0, 0);
}
else
{
RTC5Wrap.jump_abs(0, 0);
}
RTC5Wrap.set_end_of_list();
RTC5Wrap.execute_list(1);
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
public bool Stop()
{
RTC5Wrap.stop_execution();
if (ErrorExist())
{
return false;
}
else
{
return true;
}
}
public bool Save()
{
DirectoryInfo directoryInfo = new DirectoryInfo(Equipment.settingFilePath + _filePath);
if (directoryInfo.Exists == false)
{
directoryInfo.Create();
}
FileInfo fileInfo = new FileInfo(directoryInfo.FullName + _fileName);
if (fileInfo.Exists == false)
{
fileInfo.Create().Close();
}
SetScannerDelay();
SetLaserDelays();
SetLaserTiming();
return TrySaveFile(fileInfo.FullName, this);
}
public void AddScanField(int linecount = 5, double fieldSize = 15)
{
double intervalLength = 0;
if (linecount % 2 == 0) // Line count가 짝수 일 때
{
intervalLength = fieldSize / linecount;
//X축 그리기.
for (int i = linecount / 2; i > 0; i--)
{
AddScanList(ScanMode.JUMP, -(fieldSize / 2 + 1), intervalLength * i);
AddScanList(ScanMode.MARK, (fieldSize / 2 + 1), intervalLength * i);
}
for (int i = -(linecount / 2); i < 0; i++)
{
AddScanList(ScanMode.JUMP, -(fieldSize / 2 + 1), intervalLength * i);
AddScanList(ScanMode.MARK, (fieldSize / 2 + 1), intervalLength * i);
}
//Y축 그리기
for (int i = linecount / 2; i > 0; i--)
{
AddScanList(ScanMode.JUMP, intervalLength * i, -(fieldSize / 2 + 1));
AddScanList(ScanMode.MARK, intervalLength * i, (fieldSize / 2 + 1));
}
for (int i = -(linecount / 2); i < 0; i++)
{
AddScanList(ScanMode.JUMP, intervalLength * i, -(fieldSize / 2 + 1));
AddScanList(ScanMode.MARK, intervalLength * i, (fieldSize / 2 + 1));
}
}
else // Line count가 홀수 일 때
{
intervalLength = fieldSize / (linecount - 1);
//X축 그리기.
for (int i = -(linecount - 1) / 2; i <= (linecount - 1) / 2; i++)
{
AddScanList(ScanMode.JUMP, -(fieldSize / 2 + 1), intervalLength * i);
AddScanList(ScanMode.MARK, (fieldSize / 2 + 1), intervalLength * i);
}
//Y축 그리기
for (int i = -(linecount - 1) / 2; i <= (linecount - 1) / 2; i++)
{
AddScanList(ScanMode.JUMP, intervalLength * i, -(fieldSize / 2 + 1));
AddScanList(ScanMode.MARK, intervalLength * i, (fieldSize / 2 + 1));
}
}
}
}
}