using CMS_CORE_Library.Models; using Step.Config; using Step.Core; using Step.Model.DTOModels; using Step.Model.DTOModels.AlarmModels; using Step.Model.DTOModels.ToolModels; using Step.NC; using Step.Utils; using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using TeamDev.SDK.MVVM; using static Step.Model.Constants; using static Step.Utils.ExceptionManager; public static class ThreadsFunctions { public static bool reconnectionIsRunning = false; private static long ReadAlarmsTimer = 0, ReadAlarmsTimes = 0; private static long ReadAxesTimer = 0, ReadAxesTimes = 0; private static long ReadPowerOnTimer = 0, ReadPowerOnTimes = 0; private static long ReadProcPPTimer = 0, ReadProcPPTimes = 0; private static long ReadFunctionTimer = 0, ReadFunctionTimes = 0; private static long ReadMaintenanceTimer = 0, ReadMaintenanceTimes = 0; private static long ReadUserSoftKeysTimer = 0, ReadUserSoftKeysTimes = 0; private static long ReadAxesNamesTimer = 0, ReadAxesNamesTimes = 0; private static long ReadHeadsTimer = 0, ReadHeadsTimes = 0; private static long ReadToolDataTimer = 0, ReadToolDataTimes = 0; private static long WatchdogTimer = 0, WatchdogTimes = 0; private static Thread ConnThread; #region Functions public static void ManageWatchdog() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); // Check if client is connected if (ncHandler.numericalControl.NC_IsConnected()) { // Manage watchdog libraryError = ncHandler.ManageWatchdog(); } else RestoreConnection(); sw.Stop(); //Update thread timer WatchdogTimer += sw.ElapsedMilliseconds; WatchdogTimes++; // Wait Thread.Sleep(CalcSleepTime(500, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadAlarms() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); // Check if client is connected if (ncHandler.numericalControl.NC_IsConnected()) { // Get Alarms from NC libraryError = ncHandler.GetNcAlarms(out DTOAlarmsModel alarms); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_ALARMS, null, alarms); } else RestoreConnection(); sw.Stop(); //Update thread timer ReadAlarmsTimer += sw.ElapsedMilliseconds; ReadAlarmsTimes++; // Wait Thread.Sleep(CalcSleepTime(200, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadPowerOnData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); // Check if client is connected if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from NC libraryError = ncHandler.GetPowerOnData(out DTOPowerOnDataModel powerOnData); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_POWER_ON_DATA, null, powerOnData); } else RestoreConnection(); sw.Stop(); // Update thread timer ReadPowerOnTimer += sw.ElapsedMilliseconds; ReadPowerOnTimes++; // Wait Thread.Sleep(CalcSleepTime(400, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadProcessesPPStatus() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from NC libraryError = ncHandler.GetProcessesData(out DTOProcessesDataModel processesPPData); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else { // Get softkey data from config and PLC libraryError = ncHandler.GetNcSoftKeys(out List ncSoftKeys); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else { libraryError = ncHandler.GetM155Data(out List m155Data); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else { // Send processes through signalR MessageServices.Current.Publish(SEND_PROCESSES_DATA, null, processesPPData); // Send ncSoftKeys through signalR MessageServices.Current.Publish(SEND_NC_SOFTKEYS_DATA, null, ncSoftKeys); // Send m155 through signalR MessageServices.Current.Publish(SEND_M155_DATA, null, m155Data); } } } } else RestoreConnection(); sw.Stop(); // Update thread timer ReadProcPPTimer += sw.ElapsedMilliseconds; ReadProcPPTimes++; // Wait Thread.Sleep(CalcSleepTime(200, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadEnabledFunctionality() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from NC libraryError = ncHandler.GetFunctionsMappedWithNC(out List functionsAccessList); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_FUNCTIONALITY_DATA, null, functionsAccessList); } else RestoreConnection(); sw.Stop(); // Update thread timer ReadFunctionTimer += sw.ElapsedMilliseconds; ReadFunctionTimes++; // Wait Thread.Sleep(CalcSleepTime(200, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadExpiredMaintenances() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from database and PLC libraryError = ncHandler.GetExpiredMaintenances(out List expiredMaintenances); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_EXPIRED_MAINTENANCES_DATA, null, expiredMaintenances); } else RestoreConnection(); sw.Stop(); // Update thread timer ReadMaintenanceTimer += sw.ElapsedMilliseconds; ReadMaintenanceTimes++; // Wait Thread.Sleep(30000); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadUserSoftKeysData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get softkey data from config and PLC libraryError = ncHandler.GetUserSoftKeys(out List softKeys); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_USER_SOFTKEYS_DATA, null, softKeys); } else RestoreConnection(); sw.Stop(); // Update thread timer ReadUserSoftKeysTimer += sw.ElapsedMilliseconds; ReadUserSoftKeysTimes++; // Wait Thread.Sleep(CalcSleepTime(200, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadHeadsData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from config and PLC libraryError = ncHandler.GetHeadsData(out List heads); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_HEADS_DATA, null, heads); } else RestoreConnection(); sw.Stop(); //Update thread timer ReadHeadsTimer += sw.ElapsedMilliseconds; ReadHeadsTimes++; // Wait Thread.Sleep(CalcSleepTime(500, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadAxesNamesData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from config and PLC libraryError = ncHandler.ReadAxisData(out List axes); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_AXIS_NAMES_DATA, null, axes); } else RestoreConnection(); sw.Stop(); //Update thread timer ReadAxesNamesTimer += sw.ElapsedMilliseconds; ReadAxesNamesTimes++; // Wait Thread.Sleep(CalcSleepTime(800, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadAxesPositionsData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from NC libraryError = ncHandler.GetAxesPositionsBySelectedProcess(out DTOAxesModel axesPositions); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_AXES, null, axesPositions); } else RestoreConnection(); sw.Stop(); //Update thread timer ReadAxesTimer += sw.ElapsedMilliseconds; ReadAxesTimes++; // Wait Thread.Sleep(CalcSleepTime(100, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadMagazinesStatus() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from config and PLC libraryError = ncHandler.GetMagazineStatus(out DTOMagazineActionModel magazineStatus); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_MAGAZINES_STATUS, null, magazineStatus); } else RestoreConnection(); sw.Stop(); //Update thread timer ReadAxesNamesTimer += sw.ElapsedMilliseconds; ReadAxesNamesTimes++; // Wait Thread.Sleep(CalcSleepTime(1000, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadActiveProgramData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); if (ncHandler.numericalControl.NC_IsConnected()) { // Get Data from config and PLC libraryError = ncHandler.GetActiveProgramInfo(out DTOActiveProgramDataModel active); if (libraryError.IsError()) ManageLibraryError(libraryError); else // Send through signalR MessageServices.Current.Publish(SEND_ACTIVE_PROGRAM_DATA, null, active); } else RestoreConnection(); sw.Stop(); //Update thread timer ReadAxesNamesTimer += sw.ElapsedMilliseconds; ReadAxesNamesTimes++; // Wait Thread.Sleep(CalcSleepTime(400, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void UpdateToolsData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); // Check if client is connected if (ncHandler.numericalControl.NC_IsConnected()) { // Get new data from PLC libraryError = ncHandler.GetUpdatedToolsData(out Dictionary updatedStatus, out Dictionary updatedLives); if (libraryError.IsError()) ManageLibraryError(libraryError); MessageServices.Current.Publish(UPDATE_TOOLS_DATA, null, new DTONewToolDataModel { UpdatedStatus = updatedStatus, UpdatedLives = updatedLives }); } else RestoreConnection(); sw.Stop(); //Update thread timer ReadToolDataTimer += sw.ElapsedMilliseconds; ReadToolDataTimes++; // Wait Thread.Sleep(CalcSleepTime(1000, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadNcMagazineActive() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); // Check if client is connected if (ncHandler.numericalControl.NC_IsConnected()) { // Get new data from PLC libraryError = ncHandler.GetNcMagazineStatus(out Dictionary status); if (libraryError.IsError()) ManageLibraryError(libraryError); MessageServices.Current.Publish(NC_MAGAZINE_IS_ACTIVE, null, status); } else RestoreConnection(); sw.Stop(); // Wait Thread.Sleep(CalcSleepTime(500, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void ReadPartProgramQueueData() { NcHandler ncHandler = new NcHandler(); Stopwatch sw = new Stopwatch(); try { // Try connection CmsError libraryError = ncHandler.Connect(); if (libraryError.errorCode != 0) ManageLibraryError(libraryError); while (true) { sw.Restart(); // Check if client is connected if (ncHandler.numericalControl.NC_IsConnected()) { // Read data libraryError = ncHandler.UpdateQueue(); if (libraryError.IsError()) ManageLibraryError(libraryError); libraryError = ncHandler.GetSelectedProcessQueue(out List queue); if (libraryError.IsError()) ManageLibraryError(libraryError); MessageServices.Current.Publish(SEND_QUEUE_DATA, null, queue); } else RestoreConnection(); sw.Stop(); // Wait Thread.Sleep(CalcSleepTime(500, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ncHandler.Dispose(); } } public static void MonitorPanelPCResources() { float SumRec, SumSen; Stopwatch sw = new Stopwatch(); DTONetworkMonitor Monitor = new DTONetworkMonitor(); int CardId = ServerConfig.ServerStartupConfig.NetworkCardId - 1; PerformanceCounterCategory performanceCounterCategory = new PerformanceCounterCategory("Network Interface"); if(CardId<0 || CardId > performanceCounterCategory.GetInstanceNames().Length ) { Monitor.Name = "No card to monitor"; MessageServices.Current.Publish(NETWORK_USAGE, null, Monitor); return; } Monitor.Name = performanceCounterCategory.GetInstanceNames()[0]; PerformanceCounter PC_Bandwith = new PerformanceCounter("Network Interface", "Current Bandwidth", Monitor.Name); PerformanceCounter PC_RecievedBytes = new PerformanceCounter("Network Interface", "Bytes Received/sec", Monitor.Name); PerformanceCounter PC_SentBytes = new PerformanceCounter("Network Interface", "Bytes Sent/sec", Monitor.Name); Monitor.Bandwidth = PC_Bandwith.NextValue(); try { while (true) { sw.Restart(); SumRec = 0; SumSen = 0; for (int index = 0; index < 50; index++) { SumRec += PC_RecievedBytes.NextValue(); SumSen += PC_SentBytes.NextValue(); } Monitor.Value = Math.Round(((8 * (SumSen + SumRec)) / (Monitor.Bandwidth * 50) * 100), 2) ; MessageServices.Current.Publish(NETWORK_USAGE, null, Monitor); sw.Stop(); // Wait Thread.Sleep(CalcSleepTime(2000, (int)sw.ElapsedMilliseconds)); } } catch (ThreadAbortException) { ExceptionManager.Manage(ERROR_LEVEL.FATAL, "Fatal error in Network Performance monitor Thread"); } } #endregion Functions #region SupportFunctions private static void TryNcConnection() { // Stop all the NC threads ThreadsHandler.Stop(); StatReset(); NcHandler ncHandler = new NcHandler(); // Run loop until NC is connected while (!ncHandler.numericalControl.NC_IsConnected()) { // Try reconnection CmsError cmsError = ncHandler.Connect(); if (cmsError.errorCode == CMS_ERROR_CODES.SIEMENS_ENVIRONMENT_NOT_FOUND || cmsError.errorCode == CMS_ERROR_CODES.SIEMENS_HMI_NOT_RUNNING) ManageLibraryError(cmsError); else if (cmsError.errorCode != CMS_ERROR_CODES.OK) ncHandler.Dispose(); Thread.Sleep(1000); // Send status to UI MessageServices.Current.Publish(SEND_NC_STATUS, null, ncHandler.numericalControl.NC_IsConnected()); } // Start/Restart NC threads ThreadsHandler.StartWorkers(); reconnectionIsRunning = false; } public static void RestoreConnection() { if (reconnectionIsRunning == false) { // Set thread as running state reconnectionIsRunning = true; // Start reconnection thread ConnThread = new Thread(() => TryNcConnection() ); ConnThread.Start(); } } public static void AbortNcConnection() { if (ConnThread != null && ConnThread.IsAlive) ConnThread.Abort(); } public static void ManageLibraryError(CmsError cmsError) { switch (cmsError.errorCode) { case CMS_ERROR_CODES.NC_PROD_ERROR: Manage(ERROR_LEVEL.WARNING, cmsError.localizationKey); break; case CMS_ERROR_CODES.NOT_CONNECTED: RestoreConnection(); // If not connected try reconnection break; case CMS_ERROR_CODES.PROC_NOT_FOUND: break; case CMS_ERROR_CODES.SIEMENS_ENVIRONMENT_NOT_FOUND: Manage(ERROR_LEVEL.FATAL, cmsError.localizationKey); break; case CMS_ERROR_CODES.SIEMENS_HMI_NOT_RUNNING: Manage(ERROR_LEVEL.WARNING, cmsError.localizationKey); RestoreConnection(); // If not connected try reconnection break; } } internal static void StatThread() { while (true) { if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadAlarms") && ReadAlarmsTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadAlarms"] = (ReadAlarmsTimer / ReadAlarmsTimes) + " mS"; ReadAlarmsTimer = 0; ReadAlarmsTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadAxesPositionsData") && ReadAxesTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadAxesPositionsData"] = (ReadAxesTimer / ReadAxesTimes) + " mS"; ReadAxesTimer = 0; ReadAxesTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadPowerOnData") && ReadPowerOnTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadPowerOnData"] = (ReadPowerOnTimer / ReadPowerOnTimes) + " mS"; ReadPowerOnTimer = 0; ReadPowerOnTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadProcessesPPStatus") && ReadProcPPTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadProcessesPPStatus"] = (ReadProcPPTimer / ReadProcPPTimes) + " mS"; ReadProcPPTimer = 0; ReadProcPPTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadEnabledFunctionality") && ReadFunctionTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadEnabledFunctionality"] = (ReadFunctionTimer / ReadFunctionTimes) + " mS"; ReadFunctionTimer = 0; ReadFunctionTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadExpiredMaintenances") && ReadMaintenanceTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadExpiredMaintenances"] = (ReadMaintenanceTimer / ReadMaintenanceTimes) + " mS"; ReadMaintenanceTimer = 0; ReadMaintenanceTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadUserSoftKeysData") && ReadUserSoftKeysTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadUserSoftKeysData"] = (ReadUserSoftKeysTimer / ReadUserSoftKeysTimes) + " mS"; ReadUserSoftKeysTimer = 0; ReadUserSoftKeysTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadHeadsData") && ReadHeadsTimes != 0) { ThreadsHandler.RunningThreadStatus["ReadHeadsData"] = (ReadHeadsTimer / ReadHeadsTimes) + " mS"; ReadHeadsTimer = 0; ReadHeadsTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("ReadAxesNamesData") && ReadAxesNamesTimer != 0) { ThreadsHandler.RunningThreadStatus["ReadAxesNamesData"] = (ReadAxesNamesTimer / ReadAxesNamesTimes) + " mS"; ReadAxesNamesTimer = 0; ReadAxesNamesTimes = 0; } if (ThreadsHandler.RunningThreadStatus.ContainsKey("UpdateToolsData") && ReadAxesNamesTimer != 0) { ThreadsHandler.RunningThreadStatus["UpdateToolsData"] = (ReadToolDataTimer / ReadToolDataTimes) + " mS"; ReadToolDataTimer = 0; ReadToolDataTimes = 0; } MessageServices.Current.Publish(SEND_THREADS_STATUS, null, ThreadsHandler.RunningThreadStatus); Thread.Sleep(2000); } } private static void StatReset() { ReadAlarmsTimer = 0; ReadAlarmsTimes = 0; ReadAxesTimer = 0; ReadAxesTimes = 0; ReadProcPPTimer = 0; ReadProcPPTimes = 0; ReadFunctionTimer = 0; ReadFunctionTimes = 0; ReadMaintenanceTimer = 0; ReadMaintenanceTimes = 0; ReadHeadsTimer = 0; ReadHeadsTimes = 0; ReadAxesNamesTimer = 0; ReadAxesNamesTimes = 0; } private static int CalcSleepTime(int maxSleep, int execTime) { int sleep = 0; // Check if the execution time is greater than the half of the max sleep time if (maxSleep - execTime < maxSleep / 2) { sleep = maxSleep; } else { sleep = maxSleep - execTime; } return sleep; } #endregion SupportFunctions }