/// <summary> /// Reads through a report subtree and creates a report from it /// </summary> /// <param name="reader"></param> /// <param name="nodeName"></param> /// <param name="deviceName"></param> /// <returns> a RP node </returns> private void CreateReports(NodeBase lnode, IEnumerable <XElement> elements, XNamespace ns) { if (lnode == null || elements == null || lnode.Parent == null || lnode.Parent.Parent == null) { Logger.getLogger().LogError("CreateReports: Something is null"); } // We are at the LN level, up 2 levels is an ied Iec61850Model _dataModel = (lnode.Parent.Parent as NodeIed).Model; foreach (XElement el in elements) { List <NodeRCB> nodeRCBs = new List <NodeRCB>(); XAttribute a = el.Attribute("buffered"); bool buffered = a != null ? (a.Value.ToLower() == "true") : false; string fc = buffered ? "BR" : "RP"; a = el.Attribute("indexed"); bool indexed = a != null ? (a.Value.ToLower() == "true") : true; // default true??? uint maxRptEnabled = 1; XElement xeRptEnabled = el.Element(ns + "RptEnabled"); if (xeRptEnabled != null) { a = xeRptEnabled.Attribute("max"); try { maxRptEnabled = uint.Parse(a.Value); } catch { } } // correction necessary??? if (!indexed) { maxRptEnabled = 1; } if (maxRptEnabled < 1) { maxRptEnabled = 1; } if (maxRptEnabled > 99) { maxRptEnabled = 99; } for (int i = 0; i < maxRptEnabled; i++) { nodeRCBs.Add(new NodeRCB(el.Attribute("name").Value + (indexed ? (i + 1).ToString("D2") : ""))); lnode.AddChildNode(nodeRCBs[i]); nodeRCBs[i].isBuffered = buffered; } // rptID NodeData RptId = new NodeData("RptID"); RptId.SCL_FCDesc = fc; RptId.DataType = scsm_MMS_TypeEnum.visible_string; a = el.Attribute("rptID"); RptId.DataValue = a != null ? a.Value : ""; // datSet NodeData DatSet = new NodeData("DatSet"); DatSet.SCL_FCDesc = fc; DatSet.DataType = scsm_MMS_TypeEnum.visible_string; a = el.Attribute("datSet"); DatSet.DataValue = a != null ? a.Value : null; // null accepted // confRev NodeData ConfRev = new NodeData("ConfRev"); ConfRev.SCL_FCDesc = fc; ConfRev.DataType = scsm_MMS_TypeEnum.unsigned; a = el.Attribute("confRev"); try { ConfRev.DataValue = uint.Parse(a.Value); } catch { ConfRev.DataValue = (uint)1; } // bufTime NodeData BufTm = new NodeData("BufTm"); BufTm.SCL_FCDesc = fc; BufTm.DataType = scsm_MMS_TypeEnum.unsigned; a = el.Attribute("bufTime"); try { BufTm.DataValue = uint.Parse(a.Value); } catch { BufTm.DataValue = (uint)0; } // intgPd NodeData IntgPd = new NodeData("IntgPd"); IntgPd.SCL_FCDesc = fc; IntgPd.DataType = scsm_MMS_TypeEnum.unsigned; a = el.Attribute("intgPd"); try { IntgPd.DataValue = uint.Parse(a.Value); } catch { IntgPd.DataValue = (uint)0; } // <TrgOps dchg="true" qchg="false" dupd="false" period="true" /> NodeData TrgOps = new NodeData("TrgOps"); TrgOps.SCL_FCDesc = fc; TrgOps.DataType = scsm_MMS_TypeEnum.integer; IEC61850.Common.TriggerOptions trgOptions = IEC61850.Common.TriggerOptions.NONE; XElement xeTrgOps = el.Element(ns + "TrgOps"); if (xeTrgOps != null) { a = xeTrgOps.Attribute("dchg"); if ((a != null ? a.Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_CHANGED; } a = xeTrgOps.Attribute("qchg"); if ((a != null ? a.Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.QUALITY_CHANGED; } a = xeTrgOps.Attribute("dupd"); if ((a != null ? a.Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_UPDATE; } a = xeTrgOps.Attribute("period"); if ((a != null ? a.Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.INTEGRITY; } a = xeTrgOps.Attribute("gi"); if ((a != null ? a.Value : "true").ToLower() == "true") // default true { trgOptions |= IEC61850.Common.TriggerOptions.GI; } } TrgOps.DataValue = trgOptions; // <OptFields seqNum="true" timeStamp="true" dataSet="true" reasonCode="true" dataRef="false" entryID="true" configRef="true" bufOvfl="true" /> NodeData OptFlds = new NodeData("OptFlds"); OptFlds.SCL_FCDesc = fc; OptFlds.DataType = scsm_MMS_TypeEnum.integer; IEC61850.Common.ReportOptions rptOptions = IEC61850.Common.ReportOptions.NONE; XElement xeOptFields = el.Element(ns + "OptFields"); if (xeOptFields != null) { a = xeOptFields.Attribute("seqNum"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.SEQ_NUM; } a = xeOptFields.Attribute("timeStamp"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.TIME_STAMP; } a = xeOptFields.Attribute("dataSet"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.DATA_SET; } a = xeOptFields.Attribute("reasonCode"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.REASON_FOR_INCLUSION; } a = xeOptFields.Attribute("dataRef"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.DATA_REFERENCE; } a = xeOptFields.Attribute("entryID"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.ENTRY_ID; } a = xeOptFields.Attribute("configRef"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.CONF_REV; } a = xeOptFields.Attribute("bufOvfl"); if ((a != null ? a.Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.BUFFER_OVERFLOW; } } OptFlds.DataValue = rptOptions; for (int i = 0; i < maxRptEnabled; i++) { nodeRCBs[i].AddChildNode(RptId); nodeRCBs[i].AddChildNode(DatSet); nodeRCBs[i].AddChildNode(ConfRev); nodeRCBs[i].AddChildNode(OptFlds); nodeRCBs[i].AddChildNode(BufTm); nodeRCBs[i].AddChildNode(TrgOps); nodeRCBs[i].AddChildNode(IntgPd); } } }
private static bool FileParseToAttribute(XDocument doc) { if (doc.Root == null) { Log.Log.Write("FileParseToAttribute: doc.Root == null", "Warning "); return(false); } IEnumerable <XElement> xLd = (from x in doc.Descendants() where x.Name.LocalName == "LDevice" select x).ToList(); if (!xLd.Any()) { Log.Log.Write("FileParseToAttribute: LDevice == null", "Warning "); return(false); } foreach (var lditem in xLd) { if (lditem.Attribute("inst") == null) { Log.Log.Write("FileParseToAttribute: LDevice.inst == null", "Warning "); continue; } var ld = lditem.Attribute("inst")?.Value; IEnumerable <XElement> xLn = lditem.Elements().ToList(); if (!xLn.Any()) { Log.Log.Write("FileParseToAttribute: LN == null", "Warning "); return(false); } foreach (var lnitem in xLn) { //if (lnitem.Attribute("type")?.Value.ToUpper() == "EnergocomplektBitArray".ToUpper()) //{ // try // { // var nameBitArray = lnitem.Value.Split(';')[0]; // var addrBitArray = Convert.ToUInt16(lnitem.Value.Split(';')[1]); // //Новый объект source // UpdateDataObj.SourceList.Add(new UpdateDataObj.SourceClassDigital() // { // Addr = addrBitArray, // Count = 1, // NameBitArray = nameBitArray // }); // continue; // } // catch // { // Log.Log.Write("FileParseToAttribute: LN.type == EnergocomplektBitArray", "Warning "); // continue; // } //} //if (lnitem.Attribute("lnClass") == null || lnitem.Attribute("inst") == null) //{ // Log.Log.Write("FileParseToAttribute: LN.lnClass == null or LN.inst == null", "Warning "); // continue; //} string ln = lnitem.Attribute("prefix")?.Value + lnitem.Attribute("lnClass")?.Value + lnitem.Attribute("inst")?.Value; #region DOI IEnumerable <XElement> xDoi = lnitem.Elements().Where(x => x.Name.LocalName.ToUpper() == "DOI".ToUpper()).ToList(); foreach (var doiitem in xDoi) { if (doiitem.Attribute("name") == null) { Log.Log.Write("FileParseToAttribute: DO.name == null", "Warning "); continue; } var doi = doiitem.Attribute("name")?.Value; //Проверяю на собственный формат var type = (from x in doiitem.Descendants() where x.Name.LocalName == "private" select x).ToList(); if (type.Count != 0) //Проверяю на собственный формат { //Отметим объекты которые изменяются try { var tempDo = (from z in (from y in (from x in ServerModel.Model.ListLD where x.NameLD.ToUpper() == ld?.ToUpper() select x).ToList().First().ListLN where y.NameLN.ToUpper() == ln.ToUpper() select y).ToList().First().ListDO where z.NameDO.ToUpper() == doi?.ToUpper() select z).ToList().First(); //tempDo.Type = type.First().Value; } catch { Log.Log.Write("FileParseToAttribute: private DA not found", "Warning "); continue; } } ParseDefultParamBda(doiitem, ld, ln, doi, null); } #endregion #region DataSet IEnumerable <XElement> xDS = lnitem.Elements().Where(x => x.Name.LocalName.ToUpper() == "DataSet".ToUpper()).ToList(); foreach (var dsitem in xDS) { string nameDS = dsitem.Attribute("name").Value; IEnumerable <XElement> xDSElements = dsitem.Elements().Where(x => x.Name.LocalName.ToUpper() == "FCD".ToUpper() || x.Name.LocalName.ToUpper() == "FCDA").ToList(); foreach (var dselement in xDSElements) { string prefix = dselement.Attribute("prefix") != null?dselement.Attribute("prefix").Value : ""; string lnClass = dselement.Attribute("lnClass") != null?dselement.Attribute("lnClass").Value : ""; string lnInst = dselement.Attribute("lnInst") != null?dselement.Attribute("lnInst").Value : ""; string fullName = String.Concat(prefix, lnClass, lnInst); //string ldInst = dselement.Attribute("ldInst") != null ? dselement.Attribute("ldInst").Value : ""; string doName = dselement.Attribute("doName") != null?dselement.Attribute("doName").Value : ""; string daName = dselement.Attribute("daName") != null ? @"$" + dselement.Attribute("daName").Value : ""; string fc = dselement.Attribute("fc") != null?dselement.Attribute("fc").Value : ""; string ldname = lditem.Attribute("inst")?.Value; string memberName = fullName + @"$" + fc + @"$" + doName + daName; //ied + ldname + "/" + string prefix2 = lnitem.Attribute("prefix") != null?lnitem.Attribute("prefix").Value : ""; string lnClass2 = lnitem.Attribute("lnClass") != null?lnitem.Attribute("lnClass").Value : ""; string lnInst2 = lnitem.Attribute("inst") != null?lnitem.Attribute("inst").Value : ""; string lnname = String.Concat(prefix2, lnClass2, lnInst2); if (ServerModel.Model.ListLD.First(x => x.NameLD == ldname).ListLN .First(y => y.NameLN == fullName).ListDO .First(z => z.NameDO == doName) != null) { if (ServerModel.Model.ListLD.First(x => x.NameLD == ldname).ListLN .First(y => y.NameLN == lnname).ListDS .Find(z => z.DSName == nameDS) != null) { ServerModel.Model.ListLD.First(x => x.NameLD == ldname).ListLN .First(y => y.NameLN == lnname).ListDS .First(z => z.DSName == nameDS).DSMemberRef.Add(memberName); } else { ServerModel.Model.ListLD.First(x => x.NameLD == ldname).ListLN .First(y => y.NameLN == lnname).ListDS.Add(new ServerModel.DataSet(nameDS, null)); ServerModel.Model.ListLD.First(x => x.NameLD == ldname).ListLN .First(y => y.NameLN == lnname).ListDS .First(z => z.DSName == nameDS).DSMemberRef.Add(memberName); } } } } #endregion #region ReportControl IEnumerable <XElement> xRCB = lnitem.Elements().Where(x => x.Name.LocalName.ToUpper() == "ReportControl".ToUpper()).ToList(); foreach (XElement itemrcb in xRCB) { string nameRCB = itemrcb.Attribute("name") != null?itemrcb.Attribute("name").Value : ""; string refRCB = itemrcb.Attribute("ref") != null?itemrcb.Attribute("ref").Value : nameRCB; string bufferedRCB = itemrcb.Attribute("buffered") != null && itemrcb.Attribute("buffered").Value.ToLower() == "true" ? "BR" : "RP"; bool indexed = itemrcb.Attribute("indexed") == null || itemrcb.Attribute("indexed").Value.ToLower() == "true"; var rptEnabled = itemrcb.Elements().First(x => x.Name.LocalName.ToUpper() == "RptEnabled".ToUpper()).Attribute("max") != null ? Convert.ToInt32(itemrcb.Elements().First(x => x.Name.LocalName.ToUpper() == "RptEnabled".ToUpper()).Attribute("max").Value): 1; var rptId = itemrcb.Attribute("rptID") != null?itemrcb.Attribute("rptID").Value : ""; var datSet = itemrcb.Attribute("datSet") != null?itemrcb.Attribute("datSet").Value : null; // null accepted var confRev = itemrcb.Attribute("confRev") != null?uint.Parse(itemrcb.Attribute("confRev").Value) : 1; var bufTime = itemrcb.Attribute("bufTime") != null?uint.Parse(itemrcb.Attribute("bufTime").Value) : 0; var intgPd = itemrcb.Attribute("intgPd") != null?uint.Parse(itemrcb.Attribute("intgPd").Value) : 0; // <TrgOps dchg="true" qchg="false" dupd="false" period="true" /> IEC61850.Common.TriggerOptions trgOptions = IEC61850.Common.TriggerOptions.NONE; XElement xTrgOps = itemrcb.Elements().First(x => x.Name.LocalName.ToUpper() == "TrgOps".ToUpper()); if (xTrgOps != null) { if ((xTrgOps.Attribute("dchg") != null ? xTrgOps.Attribute("dchg").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_CHANGED; } if ((xTrgOps.Attribute("qchg") != null ? xTrgOps.Attribute("qchg").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.QUALITY_CHANGED; } if ((xTrgOps.Attribute("dupd") != null ? xTrgOps.Attribute("dupd").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_UPDATE; } if ((xTrgOps.Attribute("period") != null ? xTrgOps.Attribute("period").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.INTEGRITY; } if ((xTrgOps.Attribute("gi") != null ? xTrgOps.Attribute("gi").Value : "true").ToLower() == "true") // default true { trgOptions |= IEC61850.Common.TriggerOptions.GI; } } // <OptFields seqNum="true" timeStamp="true" dataSet="true" reasonCode="true" dataRef="false" entryID="true" configRef="true" bufOvfl="true" /> IEC61850.Common.ReportOptions rptOptions = IEC61850.Common.ReportOptions.NONE; XElement xOptFields = itemrcb.Elements().First(x => x.Name.LocalName.ToUpper() == "OptFields".ToUpper()); if (xOptFields != null) { if ((xOptFields.Attribute("seqNum") != null ? xOptFields.Attribute("seqNum").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.SEQ_NUM; } if ((xOptFields.Attribute("timeStamp") != null ? xOptFields.Attribute("timeStamp").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.TIME_STAMP; } if ((xOptFields.Attribute("dataSet") != null ? xOptFields.Attribute("dataSet").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.DATA_SET; } if ((xOptFields.Attribute("reasonCode") != null ? xOptFields.Attribute("reasonCode").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.REASON_FOR_INCLUSION; } if ((xOptFields.Attribute("dataRef") != null ? xOptFields.Attribute("dataRef").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.DATA_REFERENCE; } if ((xOptFields.Attribute("entryID") != null ? xOptFields.Attribute("entryID").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.ENTRY_ID; } if ((xOptFields.Attribute("configRef") != null ? xOptFields.Attribute("configRef").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.CONF_REV; } if ((xOptFields.Attribute("bufOvfl") != null ? xOptFields.Attribute("bufOvfl").Value : "false").ToLower() == "true") { rptOptions |= IEC61850.Common.ReportOptions.BUFFER_OVERFLOW; } } string ldname = lditem.Attribute("inst")?.Value; string prefix = lnitem.Attribute("prefix") != null?lnitem.Attribute("prefix").Value : ""; string lnClass = lnitem.Attribute("lnClass") != null?lnitem.Attribute("lnClass").Value : ""; string lnInst = lnitem.Attribute("lnInst") != null?lnitem.Attribute("lnInst").Value : ""; string fullName = String.Concat(prefix, lnClass, lnInst); for (int i = 0; i < rptEnabled; i++) { try { if (ServerModel.Model.ListLD.First(x => x.NameLD == ldname).ListLN .First(y => y.NameLN == fullName) != null) { ServerModel.Model.ListLD.First(x => x.NameLD == ldname).ListLN .First(y => y.NameLN == fullName).ListRCB.Add(new ServerModel.RCB(nameRCB + (indexed ? (i + 1).ToString("D2") : ""), refRCB, rptOptions, trgOptions, bufferedRCB, rptId, datSet, confRev, bufTime, intgPd)); } } catch { Log.Log.Write("FileParseToAttribute: ReportControl parse error", "Error "); } } } #endregion #region LogControlBlock IEnumerable <XElement> xLCB = lnitem.Elements().Where(x => x.Name.LocalName.ToUpper() == "LogControl".ToUpper()).ToList(); foreach (var itemlcb in xLCB) { var nameLCB = itemlcb.Attribute("name") != null?itemlcb.Attribute("name").Value : ""; var datSetLCB = itemlcb.Attribute("datSet") != null?itemlcb.Attribute("datSet").Value : null; // null accepted var refLCB = itemlcb.Attribute("ref") != null?itemlcb.Attribute("ref").Value : $"{ld}/{ln}.{nameLCB}"; var logEnaLCB = itemlcb.Attribute("logEna") != null && (itemlcb.Attribute("logEna").Value.ToLower() == "true"); IEC61850.Common.TriggerOptions trgOptions = IEC61850.Common.TriggerOptions.NONE; XElement xTrgOps = itemlcb.Elements().First(x => x.Name.LocalName.ToUpper() == "TrgOps".ToUpper()); if (xTrgOps != null) { if ((xTrgOps.Attribute("dchg") != null ? xTrgOps.Attribute("dchg").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_CHANGED; } if ((xTrgOps.Attribute("qchg") != null ? xTrgOps.Attribute("qchg").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.QUALITY_CHANGED; } if ((xTrgOps.Attribute("dupd") != null ? xTrgOps.Attribute("dupd").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_UPDATE; } if ((xTrgOps.Attribute("period") != null ? xTrgOps.Attribute("period").Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.INTEGRITY; } if ((xTrgOps.Attribute("gi") != null ? xTrgOps.Attribute("gi").Value : "true").ToLower() == "true") // default true { trgOptions |= IEC61850.Common.TriggerOptions.GI; } } var intgPdLCB = itemlcb.Attribute("intPeriod") != null?Convert.ToUInt32(itemlcb.Attribute("intPeriod").Value) : 0; var reasonCodeLCB = itemlcb.Attribute("reasonCode") != null && (itemlcb.Attribute("reasonCode").Value.ToLower() == "true"); try { if (ServerModel.Model.ListLD.First(x => x.NameLD == ld).ListLN.First(y => y.NameLN == $"{ln}") != null) { ServerModel.Model.ListLD.First(x => x.NameLD == ld).ListLN .First(y => y.NameLN == $"{ln}").ListLCB.Add(new ServerModel.LCB(nameLCB, refLCB, logEnaLCB, datSetLCB, trgOptions, intgPdLCB, reasonCodeLCB)); } } catch { Log.Log.Write("FileParseToAttribute: LogControlBlock parse error", "Error "); } } #endregion #region Log #endregion #region SettingGroupControlBlock #endregion } } Log.Log.Write("FileParseToAttribute: File parse success", "Success "); return(true); }
/// <summary> /// Creates a DA node from parsed XML /// </summary> /// <param name="reader"></param> /// <returns></returns> private void CreateDataAttributes(NodeBase root, IEnumerable <XElement> elements, XNamespace ns) { foreach (XElement el in elements) { if (el.Attribute("name") != null) { NodeData data = new NodeData(el.Attribute("name").Value); if (el.Attribute("fc") != null) { data.SCL_FCDesc = el.Attribute("fc").Value; } else if (root is NodeData && !(root is NodeDO)) { data.SCL_FCDesc = (root as NodeData).SCL_FCDesc; } var bType = el.Attribute("bType"); if (bType == null) { XElement en = el.Element(ns + "Val"); if (en != null) { data.DataValue = en.Value; } // Inheritance if (root is NodeData && !(root is NodeDO)) { data.SCL_BType = (root as NodeData).SCL_BType; } } else { data.SCL_BType = bType.Value; if (data.SCL_BType.Equals("Struct") && null != el.Attribute("type")) { data.SCL_Type = el.Attribute("type").Value; } else if (data.SCL_BType.Equals("Enum")) { data.SCL_BType = String.Concat(data.SCL_BType, " (Integer)"); if (null != el.Attribute("type")) { data.SCL_Type = el.Attribute("type").Value; } } } IEC61850.Common.TriggerOptions trgOptions = IEC61850.Common.TriggerOptions.NONE; XAttribute a = el.Attribute("dchg"); if ((a != null ? a.Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_CHANGED; } a = el.Attribute("qchg"); if ((a != null ? a.Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.QUALITY_CHANGED; } a = el.Attribute("dupd"); if ((a != null ? a.Value : "false").ToLower() == "true") { trgOptions |= IEC61850.Common.TriggerOptions.DATA_UPDATE; } data.SCL_TrgOps = (byte)trgOptions; // Inheritance if ((root is NodeData && !(root is NodeDO)) && trgOptions == IEC61850.Common.TriggerOptions.NONE) { data.SCL_TrgOps = (root as NodeData).SCL_TrgOps; } int cnt = 0; if (el.Attribute("count") != null) { int.TryParse(el.Attribute("count").Value, out cnt); } data.SCL_ArraySize = cnt; root.AddChildNode(data); } } }