private async Task PerformWriteAsync(ICollection <IPlcItem> plcItems, CancellationToken cancellationToken) { const PlcItemUsageType usageType = PlcItemUsageType.Write; var underlyingPlc = AgLinkPlc.VerifyConnectivity(this, plcItems, usageType); // Create the mapping. var(mapping, allAgLinkItems) = AgLinkPlc.CreateMappingAndAgLinkItems(plcItems, usageType); // Write to the plc. var writeResult = await Task.Run(() => underlyingPlc.WriteMixEx(allAgLinkItems, allAgLinkItems.Length), cancellationToken); // Verify the total result. //! Ignore the total result and inspect the result of each item individually. var result = AgLinkPlc.ConvertToAgLinkResult(writeResult); //if (result != AgLinkResult.Success) //{ // var errorMessage = AgLinkPlc.ErrorMapping.GetErrorMessageForCode(writeResult); // var items = plcItems.Select(item => (item, "General writing error.")).ToArray(); // throw new WritePlcException(new IPlcItem[0], items, $"Could not write any items to {this:LOG}. AGLink returned error code '{result}' ({errorMessage})."); //} // Verify the result of all items. var(validItems, failedItems) = AgLinkPlc.VerifyPlcItemResults(mapping, allAgLinkItems, false); if (failedItems.Any()) { throw new WritePlcException(validItems, failedItems, $"Some of the items couldn't be written. See the '{nameof(ReadOrWritePlcException.FailedItems)}' property for further information."); } }
private async Task PerformReadAsync(ICollection <IPlcItem> plcItems, CancellationToken cancellationToken) { const PlcItemUsageType usageType = PlcItemUsageType.Read; var underlyingPlc = AgLinkPlc.VerifyConnectivity(this, plcItems, usageType); // Create the mapping. var(mapping, allAgLinkItems) = AgLinkPlc.CreateMappingAndAgLinkItems(plcItems, usageType); // Read from the plc. //! The return value may be zero even if some or all of the items failed. To get the real result the 'Result' property of the AGLink item (AGL4.DATA_RW40.Result) must be checked. var readResult = await Task.Run(() => underlyingPlc.ReadMixEx(allAgLinkItems, allAgLinkItems.Length), cancellationToken); // Verify the total result. //! Ignore the total result and inspect the result of each item individually. var result = AgLinkPlc.ConvertToAgLinkResult(readResult); //if (result != AgLinkResult.Success) //{ // var errorMessage = AgLinkPlc.ErrorMapping.GetErrorMessageForCode(readResult); // var items = plcItems.Select(item => (item, "General reading error.")).ToArray(); // throw new ReadPlcException(new IPlcItem[0], items, $"Could not read any items from {this:LOG}. AGLink returned error code '{result}' ({errorMessage})."); //} // Verify the result of all items and transfer the value. var(validItems, failedItems) = AgLinkPlc.VerifyPlcItemResults(mapping, allAgLinkItems, true); if (failedItems.Any()) { throw new ReadPlcException(validItems, failedItems, $"Some of the items couldn't be read. See the '{nameof(ReadOrWritePlcException.FailedItems)}' property for further information."); } }
/// <summary> /// Asynchronously reads or writes the <paramref name="plcItems"/> to / from the plc. /// </summary> /// <param name="plcItems"> The <see cref="IPlcItem"/>s to write. </param> /// <param name="usageType"> The <see cref="PlcItemUsageType"/> defining whether a read or a write operation is performed. </param> /// <param name="cancellationToken"> A <see cref="CancellationToken"/> for cancelling the operation. </param> protected internal abstract Task PerformReadWriteAsync(ICollection <IPlcItem> plcItems, PlcItemUsageType usageType, CancellationToken cancellationToken);
private async Task <bool> ExecuteReadWriteAsync(ICollection <IPlcItem> plcItems, PlcItemUsageType usageType, CancellationToken cancellationToken) { // Allow only items that have a length greater than zero. This is mostly needed for dynamic items. plcItems = plcItems.Where(item => item.Value.Length > 0).ToList(); if (!plcItems.Any()) { return(true); } var cancellationTokenSource = this.BuildLinkedTokenSource(cancellationToken); cancellationToken = cancellationTokenSource.Token; try { while (true) { try { cancellationToken.ThrowIfCancellationRequested(); lock (_connectionStateChangeLock) { // If no active connection is available, then throw an exception. /*! * This means, that reading/writing items while the plc is not connected, will result in the calling function to indefinitely wait, * because no reconnect will be made when the connection was deliberately not established. */ if (this.ConnectionState == PlcConnectionState.Disconnected) { var itemDescriptions = Plc.GetPlcItemDescription(plcItems); throw new NotConnectedPlcException($"Cannot {usageType.ToString().ToLower()} the plc items ({itemDescriptions}) because {this:LOG} is not connected. All items will be put on hold."); } } await Task.Run(() => this.PerformReadWriteAsync(plcItems, usageType, cancellationToken), cancellationToken); break; } // This handles task cancellation. catch (OperationCanceledException) { if (_disposeToken.IsCancellationRequested) { if (usageType == PlcItemUsageType.Read) { throw new DisposedReadPlcException(plcItems); } else { throw new DisposedWritePlcException(plcItems); } } return(false); } // Handle not connected exceptions by trying to re-connect and halting the items until a connection was established. catch (NotConnectedPlcException ex) { this.Logger.Error(ex.Message); // Create a cancellation token source for this handle callback that is linked to the external one. var sleepCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _waitQueue.Enqueue(sleepCancellationTokenSource); var sleepCancellationToken = sleepCancellationTokenSource.Token; // Check if a reconnection could be useful. lock (_connectionStateChangeLock) { // Don't reconnect if the connection is disconnected to begin with. if (this.ConnectionState != PlcConnectionState.Disconnected) { this.OnInterrupted(executeReconnect: true); } } // Indefinitely wait until the task gets canceled. try { await Task.Delay(Timeout.Infinite, sleepCancellationToken); } catch (OperationCanceledException) { // Throw special dispose exception if the dispose token was canceled. if (_disposeToken.IsCancellationRequested) { if (usageType == PlcItemUsageType.Read) { throw new DisposedReadPlcException(plcItems); } else { throw new DisposedWritePlcException(plcItems); } } // Stop execution if the original (external) token was canceled. else if (cancellationToken.IsCancellationRequested) { return(false); } // In all other cases just keep going. else { /* ignore */ } } this.Logger.Info($"The previously suspended plc items of {this:LOG} will now be handled again."); } // Throw on read or write exceptions. catch (ReadOrWritePlcException) { throw; } } return(true); } finally { cancellationTokenSource.Dispose(); cancellationTokenSource = null; #if DEBUG this.LinkedTokenWasCanceled(); #endif } }
/// <inheritdoc /> protected override async Task PerformReadWriteAsync(ICollection <IPlcItem> plcItems, PlcItemUsageType usageType, CancellationToken cancellationToken) { try { if (usageType == PlcItemUsageType.Read) { await this.PerformReadAsync(plcItems, cancellationToken); } else { await this.PerformWriteAsync(plcItems, cancellationToken); } } catch (NullReferenceException) { throw; } }
private static IEnumerable <AGL4.DATA_RW40> CreateAgLinkItems(IPlcItem plcItem, PlcItemUsageType usageType) { var agLinkItem = new AGL4.DATA_RW40 { Offset = plcItem.Position }; switch (plcItem.Type) { case PlcItemType.Input: agLinkItem.DBNr = 0; agLinkItem.OpArea = AGL4.AREA_IN; break; case PlcItemType.Output: agLinkItem.DBNr = 0; agLinkItem.OpArea = AGL4.AREA_OUT; break; case PlcItemType.Flags: agLinkItem.DBNr = 0; agLinkItem.OpArea = AGL4.AREA_FLAG; break; default: case PlcItemType.Data: agLinkItem.DBNr = plcItem.DataBlock; agLinkItem.OpArea = AGL4.AREA_DATA; break; } if (plcItem.Value.HandlesFullBytes || (usageType == PlcItemUsageType.Read && plcItem.Value.Length > 1)) { agLinkItem.OpType = AGL4.TYP_BYTE; agLinkItem.BitNr = 0; agLinkItem.OpAnz = (ushort)DataHelper.GetByteAmountForBits(plcItem.Value.Length); if (usageType == PlcItemUsageType.Write) { agLinkItem.B = plcItem.Value; } else { agLinkItem.B = new byte[agLinkItem.OpAnz]; } yield return(agLinkItem); } else { agLinkItem.OpType = AGL4.TYP_BIT; agLinkItem.OpAnz = 1; for (byte bitPosition = 0; bitPosition < plcItem.Value.Length; bitPosition++) { var bitAgLinkItem = agLinkItem; // Value type will be copied on assignment. bitAgLinkItem.BitNr = (ushort)(bitPosition + plcItem.BitPosition); if (usageType == PlcItemUsageType.Write) { // Get the relevant bit of this item and set the AGLink byte accordingly. bitAgLinkItem.B = new byte[] { plcItem.Value[bitPosition] ? (byte)1 : (byte)0 }; } else { bitAgLinkItem.B = new byte[1]; } yield return(bitAgLinkItem); } } }
private static (ICollection <ReadPlcItemWrapper> Mapping, AGL4.DATA_RW40[] AllAgLinkItems) CreateMappingAndAgLinkItems(ICollection <IPlcItem> plcItems, PlcItemUsageType usageType) { var mapping = plcItems .Select(plcItem => new ReadPlcItemWrapper(plcItem)) .ToArray() ; // Create all needed AGLink items. //! Directly storing the AGLink items in the ReadPlcItemWrapper instance won't work, as AGL4.DATA_RW40 is a struct that would consequently be copied on assignment. //! Therefore the ReadPlcItemWrapper only contains the start position and the amount of items in the returned AGLink items array. var previousAmount = 0; var allAgLinkItems = mapping .SelectMany ( readPlcItemWrapper => { var agLinkItems = AgLinkPlc.CreateAgLinkItems(readPlcItemWrapper.PlcItem, usageType).ToArray(); readPlcItemWrapper.Start = (previousAmount += agLinkItems.Length) - 1; readPlcItemWrapper.Amount = agLinkItems.Length; return(agLinkItems); } ) .ToArray() ; return(mapping, allAgLinkItems); }
private static IAGLink4 VerifyConnectivity(AgLinkPlc plc, ICollection <IPlcItem> plcItems, PlcItemUsageType usageType) { var underlyingPlc = plc.UnderlyingPlc; if (underlyingPlc is null) { var itemDescriptions = Plc.GetPlcItemDescription(plcItems); throw new NotConnectedPlcException($"Cannot {usageType.ToString().ToLower()} the plc items ({itemDescriptions}) because {plc:LOG} is not connected. All items will be put on hold."); } return(underlyingPlc); }