diff --git a/DyeingComputer.csproj b/DyeingComputer.csproj index 9845b3f..11a230f 100644 --- a/DyeingComputer.csproj +++ b/DyeingComputer.csproj @@ -137,6 +137,7 @@ + @@ -432,6 +433,10 @@ + + + + diff --git a/Lmage/IconParkExtend.png b/Lmage/IconParkExtend.png new file mode 100644 index 0000000..8ac9738 Binary files /dev/null and b/Lmage/IconParkExtend.png differ diff --git a/Lmage/IconParkExtendR.png b/Lmage/IconParkExtendR.png new file mode 100644 index 0000000..2b104fc Binary files /dev/null and b/Lmage/IconParkExtendR.png differ diff --git a/Lmage/IconParkExtendY.png b/Lmage/IconParkExtendY.png new file mode 100644 index 0000000..fdf90ff Binary files /dev/null and b/Lmage/IconParkExtendY.png differ diff --git a/Lmage/MdiPh.png b/Lmage/MdiPh.png new file mode 100644 index 0000000..f7c03e0 Binary files /dev/null and b/Lmage/MdiPh.png differ diff --git a/MainWindow.xaml b/MainWindow.xaml index bfce87b..153b02f 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -181,11 +181,26 @@ + + + + + + - + MouseLeftButtonUp="Image_MouseLeftButtonUp" VerticalAlignment="Center"/> diff --git a/UserClass/LiquidDosingSystem.cs b/UserClass/LiquidDosingSystem.cs new file mode 100644 index 0000000..5b2ee49 --- /dev/null +++ b/UserClass/LiquidDosingSystem.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DyeingComputer.UserClass +{ + /// + /// 加注曲线类型 + /// + public enum DosingCurveType + { + Linear, // 线性曲线 + Parabolic, // 抛物线曲线(开始快,后来慢) + Exponential // 指数曲线(开始慢,后来快) + } + + /// + /// 液体加注PID控制器 + /// + public class LiquidDosingController + { + private DateTime _startTime; + private DateTime _lastUpdateTime; + private double _lastLevel; + private bool _isDosingActive; + + // PID控制器参数 + private double _proportionalGain; + private double _integralGain; + private double _derivativeGain; + private double _integralTerm; + private double _lastError; + + /// + /// 当前比例阀开度 (0-100%) + /// + public double CurrentValveOpening { get; private set; } + + /// + /// 目标完成时间 (秒) + /// + public double TargetTime { get; set; } + + /// + /// 加注曲线类型 + /// + public DosingCurveType CurveType { get; set; } + + /// + /// 容器最大容量 + /// + public double MaxCapacity { get; set; } + + /// + /// 当前液位 + /// + public double CurrentLevel { get; private set; } + + /// + /// 已加注体积 + /// + public double DosedVolume { get; private set; } + + /// + /// 液体加注事件 + /// + public event Action OnDosingUpdate; + + /// + /// 初始化液体加注控制器 + /// + /// 目标加注时间(秒) + /// 容器最大容量 + /// 加注曲线类型 + /// 比例增益 + /// 积分增益 + /// 微分增益 + public LiquidDosingController(double targetTime, double maxCapacity, + DosingCurveType curveType = DosingCurveType.Linear, + double kP = 1.0, double kI = 0.1, double kD = 0.01) + { + TargetTime = targetTime; + MaxCapacity = maxCapacity; + CurveType = curveType; + + _proportionalGain = kP; + _integralGain = kI; + _derivativeGain = kD; + + _isDosingActive = false; + CurrentValveOpening = 0; + } + + /// + /// 开始加注过程 + /// + /// 初始液位 + public void StartDosing(double initialLevel) + { + if (_isDosingActive) return; + + _lastLevel = initialLevel; + CurrentLevel = initialLevel; + DosedVolume = 0; + _integralTerm = 0; + _lastError = 0; + CurrentValveOpening = 0; + + _startTime = DateTime.Now; + _lastUpdateTime = _startTime; + + _isDosingActive = true; + + OnDosingUpdate?.Invoke(CurrentValveOpening); + } + + /// + /// 停止加注过程 + /// + public void StopDosing() + { + _isDosingActive = false; + CurrentValveOpening = 0; + OnDosingUpdate?.Invoke(0); + } + + /// + /// 更新当前液位并计算新的阀门开度 + /// + /// 当前液位 + /// 新的阀门开度 + public double UpdateLevel(double currentLevel) + { + if (!_isDosingActive) return 0; + + // 获取当前时间 + DateTime currentTime = DateTime.Now; + + // 计算时间间隔 + double timeInterval = (currentTime - _lastUpdateTime).TotalSeconds; + + // 如果时间间隔太小,避免计算过于频繁 + if (timeInterval < 0.1) return CurrentValveOpening; + + // 更新液位 + CurrentLevel = currentLevel; + + // 计算这段时间内的液体减少量 + double volumeDosedInInterval = _lastLevel - currentLevel; + DosedVolume += volumeDosedInInterval; + _lastLevel = currentLevel; + + // 计算已用时间和剩余时间 + double elapsedTime = (currentTime - _startTime).TotalSeconds; + double remainingTime = TargetTime - elapsedTime; + + // 如果已超过目标时间,完全关闭阀门 + if (remainingTime <= 0) + { + StopDosing(); + return 0; + } + + // 计算期望的加注速率(基于所选曲线类型) + double desiredDosingRate = CalculateDesiredDosingRate(elapsedTime, remainingTime); + + // 计算实际加注速率(升/秒) + double actualDosingRate = volumeDosedInInterval / timeInterval; + + // 计算误差(期望速率与实际速率之差) + double error = desiredDosingRate - actualDosingRate; + + // PID计算 + double proportionalTerm = _proportionalGain * error; + + _integralTerm += _integralGain * error * timeInterval; + // 积分项抗饱和 + _integralTerm = Math.Max(Math.Min(_integralTerm, 100), 0); + + double derivativeTerm = _derivativeGain * (error - _lastError) / timeInterval; + _lastError = error; + + // 计算新的阀门开度 + double newValveOpening = CurrentValveOpening + proportionalTerm + _integralTerm + derivativeTerm; + + // 限制阀门开度在0-100%范围内 + newValveOpening = Math.Max(Math.Min(newValveOpening, 100), 0); + + CurrentValveOpening = newValveOpening; + + // 更新最后更新时间 + _lastUpdateTime = currentTime; + + // 触发更新事件 + OnDosingUpdate?.Invoke(CurrentValveOpening); + + // 检查是否已完成加注 + if (CurrentLevel <= 0) + { + StopDosing(); + } + + return CurrentValveOpening; + } + + /// + /// 根据曲线类型计算期望的加注速率 + /// + private double CalculateDesiredDosingRate(double elapsedTime, double remainingTime) + { + double remainingVolume = CurrentLevel; + + // 确保不会除零 + if (remainingTime <= 0) return 0; + + double progress = elapsedTime / TargetTime; + + switch (CurveType) + { + case DosingCurveType.Linear: + // 线性:恒定速率 + return remainingVolume / remainingTime; + + case DosingCurveType.Parabolic: + // 抛物线:开始快,后来慢 + // 使用二次函数减少速率 + double parabolicFactor = 1.0 - Math.Pow(progress, 2); + return (remainingVolume / remainingTime) * parabolicFactor; + + case DosingCurveType.Exponential: + // 指数:开始慢,后来快 + // 使用指数函数增加速率 + double exponentialFactor = Math.Exp(progress) / Math.Exp(1); + return (remainingVolume / remainingTime) * exponentialFactor; + + default: + return remainingVolume / remainingTime; + } + } + + /// + /// 设置PID参数 + /// + public void SetPIDParameters(double kP, double kI, double kD) + { + _proportionalGain = kP; + _integralGain = kI; + _derivativeGain = kD; + + // 重置积分项和误差 + _integralTerm = 0; + _lastError = 0; + } + + /// + /// 检查加注是否正在进行中 + /// + public bool IsDosingActive => _isDosingActive; + } +} diff --git a/ViewModel/MainWindowViewModel.cs b/ViewModel/MainWindowViewModel.cs index f8f13ee..a7e8238 100644 --- a/ViewModel/MainWindowViewModel.cs +++ b/ViewModel/MainWindowViewModel.cs @@ -38,6 +38,7 @@ using static DyeingComputer.UserClass.SqliteHelper; using static DyeingComputer.ViewModel.MainWindowViewModel; using static DyeingComputer.Windows.ViewStep; using static System.Net.WebRequestMethods; +using static System.Security.Cryptography.ECCurve; using static System.Windows.Forms.AxHost; using static System.Windows.Forms.VisualStyles.VisualStyleElement.Button; using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; @@ -838,8 +839,8 @@ namespace DyeingComputer.ViewModel STEP_P1 = Convert.ToDouble(P1); STEP_P2 = Convert.ToDouble(P2); STEP_P3 = Convert.ToDouble(P3); - STEP_P4 = Convert.ToDouble(P4); - STEP_P5 = Convert.ToDouble(P5); + STEP_P5 = 0; + STEP_TIME = 0; SETP_runtime = true; STEP_finish = false; break; @@ -943,6 +944,7 @@ namespace DyeingComputer.ViewModel STEP_P1 = Convert.ToDouble(P1); STEP_P2 = Convert.ToDouble(P2); STEP_P3 = Convert.ToDouble(P3); + STEP_P5 = 0; SETP_runtime = true; STEP_finish = false; break;//药缸加药 @@ -1032,8 +1034,6 @@ namespace DyeingComputer.ViewModel TimeSpan ts = new TimeSpan(00,00,01);//1秒间隔 private int temp_time10s = 10;//开关周期时间 private bool PH_start = false; - private double TANK1_L,TANK2_L,TANK3_L,TANK1_SL,TANK2_SL,TANK3_SL;//实际加料量|每秒加料量 - private double TANK1_Pt, TANK1_Pc, TANK2_Pt, TANK2_Pc, TANK3_Pt, TANK3_Pc;//加料比例开度与模拟 private string TANK1, TANK2, TANK3;//料桶信息 public static string TANK1_DYELOT, TANK2_DYELOT, TANK3_DYELOT;//料单号 private int TANK1_REDYE,TANK1_STEP, TANK2_REDYE, TANK2_STEP, TANK3_REDYE, TANK3_STEP; @@ -1042,6 +1042,8 @@ namespace DyeingComputer.ViewModel int TIME_S; double TO = 0; double T = 0; + int LT=0; + LiquidDosingController controller; void STEP_RUN_master() { WORK_run = WORK_RUN; @@ -1407,11 +1409,91 @@ namespace DyeingComputer.ViewModel // if (errTabler.Rows.Count == 0) Status_Str = Resources.Washing + "(" + Properties.Resources.WaterLevel + ")"; } else { STEP_finish = true; } - break;//批次水位水洗 + break;//批次水位水洗 case "015": + if (STEP_P5 == 0)//入水到水位 + { + if (errTabler.Rows.Count == 0) Status_Str = Resources.Washing + ":" + Resources.AddWater + "1"; + + if (STEP_P1.ToString() == "1") + { + Updata_dtd("3013", true); + } + else if (STEP_P1.ToString() == "2") + { + Updata_dtd("3014", true); + } + else if (STEP_P1.ToString() == "3") + { + Updata_dtd("3015", true); + } + else if (STEP_P1.ToString() == "4") + { + Updata_dtd("3016", true); + } + if (Selet_dtm("1015") >= STEP_P2) //到达水位 + { + ERRinf.ERRinf_d(errTabler, "202");// + STEP_TIME = 0; + STEP_P5 = 1; + } + else + { + if (STEP_TIME > MT44) + { + ERRinf.ERRinf_w(errTabler, Status_Str + "[" + Resources.Abnormal + "]", "202");//写入 + } + else + { + STEP_TIME++; + } + } + } + else if (STEP_P5 == 1)//流量水洗 + { + if (errTabler.Rows.Count == 0) Status_Str = Resources.Washing + "(" + Resources.Overflow + ")"; + + Updata_dtd("3020", true); + if (STEP_P1.ToString() == "1") + { + Updata_dtd("3013", true); + } + else if (STEP_P1.ToString() == "2") + { + Updata_dtd("3014", true); + } + else if (STEP_P1.ToString() == "3") + { + Updata_dtd("3015", true); + } + else if (STEP_P1.ToString() == "4") + { + Updata_dtd("3016", true); + } - if (errTabler.Rows.Count == 0) Status_Str = Resources.Washing + "(" + Resources.Overflow + ")"; - break;//溢流水洗 + if (Selet_dtm("1016") >= STEP_P3) //到达水量 + { + Updata_dtd("3013", false); + Updata_dtd("3014", false); + Updata_dtd("3015", false); + Updata_dtd("3016", false); + ERRinf.ERRinf_d(errTabler, "202");// + if (errTabler.Rows.Count == 0) Status_Str = Resources.AddWater + ":" + Resources.Finish; + STEP_finish = true; + } + else + { + if (STEP_TIME > MT44) + { + ERRinf.ERRinf_w(errTabler, Status_Str + "[" + Resources.Abnormal + "]", "202");//写入 + } + else + { + STEP_TIME++; + } + } + } + break;//溢流水洗 case "017": if (errTabler.Rows.Count == 0) Status_Str = Resources.Washing + "(" + Properties.Resources.Cooling + ")"; break;//降温水洗 @@ -1480,7 +1562,7 @@ namespace DyeingComputer.ViewModel if (errTabler.Rows.Count == 0) Status_Str = Resources.Drainage + ":" + Resources.Drainage + "3"; } - Updata_dtd("3020", true);//开排水泵 + Updata_dtd("3075", true);//开排水泵 if (Selet_dtm("1015") <= STEP_P2) //排水状态完成 { ERRinf.ERRinf_d(errTabler, "203");// @@ -1489,7 +1571,7 @@ namespace DyeingComputer.ViewModel Updata_dtd("3017", false); Updata_dtd("3018", false); Updata_dtd("3019", false); - Updata_dtd("3020", false);//开排水泵 + Updata_dtd("3075", false);//开排水泵 if (errTabler.Rows.Count == 0) Status_Str = Resources.Drainage + ":" + Resources.Finish; STEP_finish = true; } @@ -1851,23 +1933,116 @@ namespace DyeingComputer.ViewModel case "065": if (STEP_P1 == 1) { - if ((TANK1_L - Selet_dtm("1017")) > TANK1_SL) { TANK1_Pc = TANK2_Pc - 3.3; }//计算比例 - else { TANK1_Pc = TANK1_Pc + 3.3; } - if (((TANK1_Pt - TANK1_Pc) > 5) || ((TANK1_Pt - TANK1_Pc) > -5)) TANK1_Pt = TANK1_Pc;//调整比例 + if (STEP_P5 == 0) + { + DosingCurveType Type; + if (STEP_P2 == 1) { Type = DosingCurveType.Parabolic; } + else if (STEP_P2 == 2) { Type = DosingCurveType.Exponential; } + else { Type = DosingCurveType.Linear; } + + controller = new LiquidDosingController( + targetTime: STEP_P3 * 60, + maxCapacity: 1000, + curveType: Type, + kP: 0.8, kI: 0.05, kD: 0.1); + controller.StartDosing(Selet_dtm("1017")); + STEP_P5 = 1; + } + else if (STEP_P5 == 1) + { + int valveOpening = Convert.ToInt16(controller.UpdateLevel(Selet_dtm("1017"))); + Updata_dta("5005", valveOpening); + if (Selet_dtm("1017") >= MS06) + { + LT = MS07; + Updata_dtd("3035",true); + } + else + { + LT--; + if (LT <= 0) + { + Updata_dtd("3035", false); + STEP_finish = true; + } + } + } if (errTabler.Rows.Count == 0) Status_Str = Resources.Tank + " 1 :" + Resources.AddTheMedicine; } //药缸1 else if (STEP_P1 == 2) { - if ((TANK2_L - Selet_dtm("1018")) > TANK2_SL) { TANK2_Pc = TANK2_Pc - 3.3; }//计算比例 - else { TANK2_Pc = TANK2_Pc + 3.3; } - if (((TANK2_Pt - TANK2_Pc) > 5) || ((TANK2_Pt - TANK2_Pc) > -5)) TANK2_Pt = TANK2_Pc;//调整比例 + if (STEP_P5 == 0) + { + DosingCurveType Type; + if (STEP_P2 == 1) { Type = DosingCurveType.Parabolic; } + else if (STEP_P2 == 2) { Type = DosingCurveType.Exponential; } + else { Type = DosingCurveType.Linear; } + + controller = new LiquidDosingController( + targetTime: STEP_P3 * 60, + maxCapacity: 1000, + curveType: Type, + kP: 0.8, kI: 0.05, kD: 0.1); + controller.StartDosing(Selet_dtm("1018")); + STEP_P5 = 1; + } + else if (STEP_P5 == 1) + { + int valveOpening = Convert.ToInt16(controller.UpdateLevel(Selet_dtm("1018"))); + Updata_dta("5006", valveOpening); + if (Selet_dtm("1018") >= MS06) + { + LT = MS07; + Updata_dtd("3045", true); + } + else + { + LT--; + if (LT <= 0) + { + Updata_dtd("3045", false); + STEP_finish = true; + } + } + } if (errTabler.Rows.Count == 0) Status_Str = Resources.Tank + " 2 :" + Resources.AddTheMedicine; }//药缸2 else if (STEP_P1 == 3) { - if ((TANK3_L - Selet_dtm("1019")) > TANK3_SL) { TANK3_Pc = TANK3_Pc - 3.3; }//计算比例 - else { TANK3_Pc = TANK3_Pc + 3.3; } - if (((TANK3_Pt - TANK3_Pc) > 5) || ((TANK3_Pt - TANK3_Pc) > -5)) TANK3_Pt = TANK3_Pc;//调整比例 + if (STEP_P5 == 0) + { + DosingCurveType Type; + if (STEP_P2 == 1) { Type = DosingCurveType.Parabolic; } + else if (STEP_P2 == 2) { Type = DosingCurveType.Exponential; } + else { Type = DosingCurveType.Linear; } + + controller = new LiquidDosingController( + targetTime: STEP_P3 * 60, + maxCapacity: 1000, + curveType: Type, + kP: 0.8, kI: 0.05, kD: 0.1); + controller.StartDosing(Selet_dtm("1019")); + STEP_P5 = 1; + } + else if (STEP_P5 == 1) + { + int valveOpening = Convert.ToInt16(controller.UpdateLevel(Selet_dtm("1019"))); + Updata_dta("5007", valveOpening); + if (Selet_dtm("1019") >= MS06) + { + LT = MS07; + Updata_dtd("3035", true); + } + else + { + LT--; + if (LT <= 0) + { + Updata_dtd("3055", false); + STEP_finish = true; + } + } + } if (errTabler.Rows.Count == 0) Status_Str = Resources.Tank + " 3 :" + Resources.AddTheMedicine; }//药缸3 break;//药缸加药 @@ -3240,18 +3415,18 @@ namespace DyeingComputer.ViewModel if (MT18 >= 11) SYSlog = SYSlog + " | " + "11 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1055"))); if (MT18 >= 12) SYSlog = SYSlog + " | " + "12 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1057"))); SYSlog = SYSlog + "\n" + Resources.Cycletime + "Sec"; - SYSlog = SYSlog + "\n" + "1 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1030"))); - if (MT18 >= 2) SYSlog = SYSlog + " | " + "2 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1031"))); - if (MT18 >= 3) SYSlog = SYSlog + " | " + "3 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1032"))); - if (MT18 >= 4) SYSlog = SYSlog + "\n" + "4 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1033"))); - if (MT18 >= 5) SYSlog = SYSlog + " | " + "5 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1034"))); - if (MT18 >= 6) SYSlog = SYSlog + " | " + "6 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1036"))); - if (MT18 >= 7) SYSlog = SYSlog + "\n" + "7 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1038"))); - if (MT18 >= 8) SYSlog = SYSlog + " | " + "8 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1040"))); - if (MT18 >= 9) SYSlog = SYSlog + " | " + "9 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1052"))); - if (MT18 >= 10) SYSlog = SYSlog + "\n" + "10 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1054"))); - if (MT18 >= 11) SYSlog = SYSlog + " | " + "11 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1056"))); - if (MT18 >= 12) SYSlog = SYSlog + " | " + "12 : " + string.Format("{0:D3}", Convert.ToInt16(Selet_dtm("1058"))); + SYSlog = SYSlog + "\n" + "1 : " + string.Format("{0:D3}", Selet_dta("4017")); + if (MT18 >= 2) SYSlog = SYSlog + " | " + "2 : " + string.Format("{0:D3}", Selet_dta("4018")); + if (MT18 >= 3) SYSlog = SYSlog + " | " + "3 : " + string.Format("{0:D3}", Selet_dta("4019")); + if (MT18 >= 4) SYSlog = SYSlog + "\n" + "4 : " + string.Format("{0:D3}", Selet_dta("4020")); + if (MT18 >= 5) SYSlog = SYSlog + " | " + "5 : " + string.Format("{0:D3}", Selet_dta("4021")); + if (MT18 >= 6) SYSlog = SYSlog + " | " + "6 : " + string.Format("{0:D3}", Selet_dta("4022")); + if (MT18 >= 7) SYSlog = SYSlog + "\n" + "7 : " + string.Format("{0:D3}", Selet_dta("4023")); + if (MT18 >= 8) SYSlog = SYSlog + " | " + "8 : " + string.Format("{0:D3}", Selet_dta("4024")); + if (MT18 >= 9) SYSlog = SYSlog + " | " + "9 : " + string.Format("{0:D3}", Selet_dta("4025")); + if (MT18 >= 10) SYSlog = SYSlog + "\n" + "10 : " + string.Format("{0:D3}", Selet_dta("4026")); + if (MT18 >= 11) SYSlog = SYSlog + " | " + "11 : " + string.Format("{0:D3}", Selet_dta("4027")); + if (MT18 >= 12) SYSlog = SYSlog + " | " + "12 : " + string.Format("{0:D3}", Selet_dta("4028")); }//布轮信息 SYSlog = SYSlog + "\n-------------------------------------------------------"; if (SM01 == 1) @@ -3321,7 +3496,6 @@ namespace DyeingComputer.ViewModel } - } public static DataTable dt_d = new DataTable("DIO"); diff --git a/Windows/ViewStep.xaml.cs b/Windows/ViewStep.xaml.cs index 58790a6..b342535 100644 --- a/Windows/ViewStep.xaml.cs +++ b/Windows/ViewStep.xaml.cs @@ -576,19 +576,17 @@ namespace DyeingComputer.Windows case "015": P1N.Text = Properties.Resources.Headwaters; P2N.Text = Properties.Resources.WaterLevel; - P3N.Text = Properties.Resources.Time; - P4N.Text = Properties.Resources.Flowmeter; - P5N.Text = Properties.Resources.Target + Properties.Resources.Temperature; + P3N.Text = Properties.Resources.Flowmeter; P1.Visibility = Visibility.Visible; P1N.Visibility = Visibility.Visible; P2.Visibility = Visibility.Visible; P2N.Visibility = Visibility.Visible; P3.Visibility = Visibility.Visible; P3N.Visibility = Visibility.Visible; - P4.Visibility = Visibility.Visible; - P4N.Visibility = Visibility.Visible; - P5.Visibility = Visibility.Visible; - P5N.Visibility = Visibility.Visible; + P4.Visibility = Visibility.Collapsed; + P4N.Visibility = Visibility.Collapsed; + P5.Visibility = Visibility.Collapsed; + P5N.Visibility = Visibility.Collapsed; imageP.Source = new BitmapImage(new Uri("/Lmage/ID015.png", UriKind.Relative)); break; case "017":