diff --git a/src/AR.Iec61850/FaultRecords/Iec61850FaultRecordInteroperableDownloader.cs b/src/AR.Iec61850/FaultRecords/Iec61850FaultRecordInteroperableDownloader.cs
new file mode 100644
index 0000000..6f61cec
--- /dev/null
+++ b/src/AR.Iec61850/FaultRecords/Iec61850FaultRecordInteroperableDownloader.cs
@@ -0,0 +1,192 @@
+using AR.Iec61850.Mms;
+
+namespace AR.Iec61850.FaultRecords;
+
+///
+/// Downloads every file in a discovered fault-record bundle while preserving the
+/// complete signed Integer32 FRSM returned by the MMS server.
+///
+public static class Iec61850FaultRecordInteroperableDownloader
+{
+ public static async Task DownloadAsync(
+ MmsClientSession session,
+ Iec61850FaultRecordSet record,
+ string destinationRoot,
+ Iec61850FaultRecordDownloadOptions? options = null,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(session);
+ ArgumentNullException.ThrowIfNull(record);
+ ArgumentException.ThrowIfNullOrWhiteSpace(destinationRoot);
+ options ??= new Iec61850FaultRecordDownloadOptions();
+ ValidateOptions(options);
+
+ if (record.Files.Count == 0)
+ return Fail(record, "The selected fault record contains no downloadable file.");
+ if (options.RequireCompleteRecord && !record.IsComplete)
+ return Fail(record, $"The selected fault record is incomplete: {record.Completeness}.");
+ if (!record.HasUnknownSize && record.KnownSizeBytes > options.MaximumTotalBytes)
+ return Fail(record, $"The selected fault record declares {record.KnownSizeBytes} byte(s), exceeding the configured bundle limit of {options.MaximumTotalBytes}.");
+
+ var destinationFullPath = Path.GetFullPath(destinationRoot);
+ Directory.CreateDirectory(destinationFullPath);
+ var temporaryDirectory = Path.Combine(
+ destinationFullPath,
+ $".fault-record-{Guid.NewGuid():N}.partial");
+ Directory.CreateDirectory(temporaryDirectory);
+
+ var downloadedFiles = new List();
+ long totalBytes = 0;
+ long? expectedBytes = record.HasUnknownSize ? null : record.KnownSizeBytes;
+ string? failure = null;
+
+ try
+ {
+ for (var index = 0; index < record.Files.Count; index++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var file = record.Files[index];
+ var safeFileName = Iec61850LocalPath.SanitizeFileName(file.Name);
+ var localPath = Iec61850LocalPath.CombineUnderRoot(temporaryDirectory, safeFileName);
+ var remainingBundleBytes = options.MaximumTotalBytes - totalBytes;
+ if (remainingBundleBytes <= 0)
+ throw new InvalidDataException($"Bundle transfer exceeded the configured limit of {options.MaximumTotalBytes} byte(s).");
+
+ var maximumFileBytes = Math.Min(options.MaximumFileBytes, remainingBundleBytes);
+ var baseBytes = totalBytes;
+ var fileProgress = new InlineProgress(item =>
+ {
+ progress?.Report(new Iec61850FaultRecordDownloadProgress
+ {
+ RecordId = record.RecordId,
+ CurrentFileName = file.Name,
+ CompletedFiles = index,
+ TotalFiles = record.Files.Count,
+ BytesTransferred = baseBytes + item.BytesTransferred,
+ ExpectedBytes = expectedBytes,
+ IsComplete = false
+ });
+ });
+
+ await using (var output = new FileStream(
+ localPath,
+ FileMode.CreateNew,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 64 * 1024,
+ useAsync: true))
+ {
+ var transfer = await session.DownloadFileInteroperableAsync(
+ file.RemotePath,
+ output,
+ new MmsFileTransferOptions
+ {
+ MaximumBytes = maximumFileBytes,
+ MaximumReadOperations = options.MaximumReadOperationsPerFile,
+ RequireDeclaredSizeMatch = options.RequireDeclaredSizeMatch,
+ FlushDestinationOnSuccess = true
+ },
+ fileProgress,
+ cancellationToken).ConfigureAwait(false);
+
+ if (!transfer.IsSuccess)
+ {
+ throw new InvalidDataException(
+ $"{file.RemotePath}: {transfer.Message} " +
+ $"Request={ValueOrDash(session.LastDiscoveryRequestHex)}; " +
+ $"Response={ValueOrDash(session.LastDiscoveryResponseHex)}");
+ }
+
+ totalBytes += transfer.BytesTransferred;
+ downloadedFiles.Add(new Iec61850FaultRecordDownloadedFile
+ {
+ RemotePath = file.RemotePath,
+ LocalPath = localPath,
+ BytesTransferred = transfer.BytesTransferred
+ });
+ }
+
+ progress?.Report(new Iec61850FaultRecordDownloadProgress
+ {
+ RecordId = record.RecordId,
+ CurrentFileName = file.Name,
+ CompletedFiles = index + 1,
+ TotalFiles = record.Files.Count,
+ BytesTransferred = totalBytes,
+ ExpectedBytes = expectedBytes,
+ IsComplete = index + 1 == record.Files.Count
+ });
+ }
+
+ var finalDirectory = Iec61850LocalPath.ResolveAvailableDirectory(
+ destinationFullPath,
+ Iec61850LocalPath.SanitizeFileName(record.BaseName));
+ Directory.Move(temporaryDirectory, finalDirectory);
+
+ var finalFiles = downloadedFiles
+ .Select(file => new Iec61850FaultRecordDownloadedFile
+ {
+ RemotePath = file.RemotePath,
+ LocalPath = Path.Combine(finalDirectory, Path.GetFileName(file.LocalPath)),
+ BytesTransferred = file.BytesTransferred
+ })
+ .ToArray();
+
+ return new Iec61850FaultRecordDownloadResult
+ {
+ IsSuccess = true,
+ RecordId = record.RecordId,
+ DestinationDirectory = finalDirectory,
+ Files = finalFiles,
+ BytesTransferred = totalBytes,
+ Message = $"Downloaded {finalFiles.Length} fault-record file(s), {totalBytes} byte(s), to '{finalDirectory}'."
+ };
+ }
+ catch (OperationCanceledException)
+ {
+ Iec61850LocalPath.TryDeleteDirectory(temporaryDirectory);
+ throw;
+ }
+ catch (Exception ex) when (
+ ex is IOException or
+ InvalidDataException or
+ UnauthorizedAccessException or
+ ArgumentException or
+ InvalidOperationException)
+ {
+ failure = ex.Message;
+ }
+
+ Iec61850LocalPath.TryDeleteDirectory(temporaryDirectory);
+ return new Iec61850FaultRecordDownloadResult
+ {
+ IsSuccess = false,
+ RecordId = record.RecordId,
+ Files = downloadedFiles,
+ BytesTransferred = totalBytes,
+ Message = failure ?? "Fault-record download failed."
+ };
+ }
+
+ private static void ValidateOptions(Iec61850FaultRecordDownloadOptions options)
+ {
+ if (options.MaximumTotalBytes <= 0)
+ throw new ArgumentOutOfRangeException(nameof(options), "MaximumTotalBytes must be greater than zero.");
+ if (options.MaximumFileBytes <= 0)
+ throw new ArgumentOutOfRangeException(nameof(options), "MaximumFileBytes must be greater than zero.");
+ if (options.MaximumReadOperationsPerFile <= 0)
+ throw new ArgumentOutOfRangeException(nameof(options), "MaximumReadOperationsPerFile must be greater than zero.");
+ }
+
+ private static Iec61850FaultRecordDownloadResult Fail(Iec61850FaultRecordSet record, string message)
+ => new()
+ {
+ IsSuccess = false,
+ RecordId = record.RecordId,
+ Message = message
+ };
+
+ private static string ValueOrDash(string? value)
+ => string.IsNullOrWhiteSpace(value) ? "-" : value.Trim();
+}
diff --git a/src/AR.Iec61850/Mms/MmsInteroperableFileTransfer.cs b/src/AR.Iec61850/Mms/MmsInteroperableFileTransfer.cs
new file mode 100644
index 0000000..79e3a81
--- /dev/null
+++ b/src/AR.Iec61850/Mms/MmsInteroperableFileTransfer.cs
@@ -0,0 +1,345 @@
+using AR.Iec61850.Asn1;
+using AR.Iec61850.Diagnostics;
+
+namespace AR.Iec61850.Mms;
+
+///
+/// Compatibility file-transfer path for MMS servers that allocate a negative
+/// FileReadStateMachine identifier. ISO 9506 defines frsmID as Integer32, so the
+/// full signed range is valid and must be echoed unchanged in FileRead/FileClose.
+///
+public static class MmsInteroperableFileOpenResponseDecoder
+{
+ public static MmsFileOpenResult Decode(ReadOnlyMemory presentationPayload, int expectedInvokeId)
+ {
+ var hex = HexDump.ToCompactString(presentationPayload.Span);
+ if (!MmsFileResponseEnvelope.TryDecode(
+ presentationPayload,
+ expectedInvokeId,
+ serviceTagNumber: 72,
+ operationName: "FileOpen",
+ out var service,
+ out var error))
+ {
+ return Fail(error, hex);
+ }
+
+ try
+ {
+ int? stateMachineId = null;
+ uint? fileSize = null;
+ byte[] lastModified = Array.Empty();
+
+ foreach (var field in BerReader.ReadChildren(service.Value))
+ {
+ if (field.Class == BerClass.ContextSpecific && field.TagNumber == 0)
+ {
+ var value = BerReader.ReadSignedInteger(field);
+ if (value is >= int.MinValue and <= int.MaxValue)
+ stateMachineId = (int)value.Value;
+ }
+ else if (field.Class == BerClass.ContextSpecific && field.TagNumber == 1 && field.Constructed)
+ {
+ MmsFileResponseEnvelope.DecodeFileAttributes(field, ref fileSize, ref lastModified);
+ }
+ }
+
+ if (!stateMachineId.HasValue)
+ return Fail("FileOpen response did not contain a valid signed Integer32 file read state machine identifier.", hex);
+
+ return new MmsFileOpenResult
+ {
+ IsSuccess = true,
+ FileReadStateMachineId = stateMachineId.Value,
+ FileSizeBytes = fileSize,
+ LastModifiedRaw = lastModified,
+ Message = fileSize.HasValue
+ ? $"MMS FileOpen succeeded. FRSM={stateMachineId.Value}, declaredSize={fileSize.Value}."
+ : $"MMS FileOpen succeeded. FRSM={stateMachineId.Value}, declared size unavailable.",
+ ResponseHexPreview = hex
+ };
+ }
+ catch (Exception ex) when (ex is BerFormatException or ArgumentException or InvalidOperationException)
+ {
+ return Fail($"FileOpen response decode failed: {ex.GetType().Name}: {ex.Message}", hex);
+ }
+ }
+
+ private static MmsFileOpenResult Fail(string message, string hex)
+ => new()
+ {
+ IsSuccess = false,
+ Message = message,
+ ResponseHexPreview = hex
+ };
+}
+
+public static class MmsInteroperableFileReadRequest
+{
+ public static byte[] Build(int invokeId, int fileReadStateMachineId)
+ => BuildFrsmRequest(invokeId, fileReadStateMachineId, serviceTagNumber: 73);
+
+ internal static byte[] BuildFrsmRequest(int invokeId, int fileReadStateMachineId, int serviceTagNumber)
+ {
+ if (invokeId < 0)
+ throw new ArgumentOutOfRangeException(nameof(invokeId));
+
+ var service = BerWriter.EncodeTlv(
+ BerClass.ContextSpecific,
+ constructed: false,
+ tagNumber: serviceTagNumber,
+ BerWriter.EncodeSignedInteger(fileReadStateMachineId));
+ var confirmedRequest = BerWriter.EncodeTlv(
+ 0xA0,
+ MmsPresentation.Concat(MmsPresentation.Integer(invokeId), service));
+ return MmsPresentation.WrapIsoPresentationPData(confirmedRequest);
+ }
+}
+
+public static class MmsInteroperableFileCloseRequest
+{
+ public static byte[] Build(int invokeId, int fileReadStateMachineId)
+ => MmsInteroperableFileReadRequest.BuildFrsmRequest(invokeId, fileReadStateMachineId, serviceTagNumber: 74);
+}
+
+public sealed partial class MmsClientSession
+{
+ public async Task DownloadFileInteroperableAsync(
+ string remotePath,
+ Stream destination,
+ MmsFileTransferOptions? options = null,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ EnsureMmsReady();
+ ArgumentNullException.ThrowIfNull(destination);
+ if (!destination.CanWrite)
+ throw new ArgumentException("Destination stream must be writable.", nameof(destination));
+
+ options ??= new MmsFileTransferOptions();
+ if (options.MaximumBytes <= 0)
+ throw new ArgumentOutOfRangeException(nameof(options), "MaximumBytes must be greater than zero.");
+ if (options.MaximumReadOperations <= 0)
+ throw new ArgumentOutOfRangeException(nameof(options), "MaximumReadOperations must be greater than zero.");
+
+ var normalizedPath = MmsFileNameEncoding.Normalize(remotePath);
+ int? stateMachineId = null;
+ long bytesTransferred = 0;
+ long? expectedBytes = null;
+ var readOperations = 0;
+ var remoteFileClosed = false;
+ string? failure = null;
+
+ try
+ {
+ var opened = await FileOpenInteroperableAsync(normalizedPath, cancellationToken).ConfigureAwait(false);
+ if (!opened.IsSuccess)
+ {
+ failure = opened.Message;
+ }
+ else
+ {
+ stateMachineId = opened.FileReadStateMachineId;
+ expectedBytes = opened.FileSizeBytes is > 0 ? (long)opened.FileSizeBytes.Value : null;
+ if (expectedBytes > options.MaximumBytes)
+ {
+ failure = $"Remote file declares {expectedBytes.Value} byte(s), exceeding the configured limit of {options.MaximumBytes}.";
+ }
+ else
+ {
+ var moreFollows = true;
+ while (moreFollows && failure == null)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ if (++readOperations > options.MaximumReadOperations)
+ {
+ failure = $"FileRead exceeded the configured operation limit of {options.MaximumReadOperations}.";
+ break;
+ }
+
+ var chunk = await FileReadInteroperableAsync(stateMachineId.Value, cancellationToken).ConfigureAwait(false);
+ if (!chunk.IsSuccess)
+ {
+ failure = chunk.Message;
+ break;
+ }
+
+ if (chunk.Data.Length == 0 && chunk.MoreFollows)
+ {
+ failure = "FileRead returned an empty block while moreFollows remained true.";
+ break;
+ }
+
+ if (bytesTransferred + chunk.Data.LongLength > options.MaximumBytes)
+ {
+ failure = $"File transfer exceeded the configured limit of {options.MaximumBytes} byte(s).";
+ break;
+ }
+
+ if (chunk.Data.Length > 0)
+ {
+ await destination.WriteAsync(chunk.Data.AsMemory(), cancellationToken).ConfigureAwait(false);
+ bytesTransferred += chunk.Data.LongLength;
+ }
+
+ moreFollows = chunk.MoreFollows;
+ progress?.Report(new MmsFileTransferProgress
+ {
+ RemotePath = normalizedPath,
+ BytesTransferred = bytesTransferred,
+ ExpectedBytes = expectedBytes,
+ ReadOperations = readOperations,
+ IsComplete = !moreFollows
+ });
+ }
+
+ if (failure == null &&
+ options.RequireDeclaredSizeMatch &&
+ expectedBytes.HasValue &&
+ expectedBytes.Value != bytesTransferred)
+ {
+ failure = $"Transferred size {bytesTransferred} does not match declared size {expectedBytes.Value}.";
+ }
+
+ if (failure == null && options.FlushDestinationOnSuccess)
+ await destination.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (Exception ex) when (ex is IOException or InvalidDataException or ObjectDisposedException or InvalidOperationException)
+ {
+ failure = $"File transfer failed: {ex.GetType().Name}: {ex.Message}";
+ }
+ finally
+ {
+ if (stateMachineId.HasValue && IsMmsInitiated)
+ {
+ try
+ {
+ using var closeCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(3));
+ var close = await FileCloseInteroperableAsync(stateMachineId.Value, closeCancellation.Token).ConfigureAwait(false);
+ remoteFileClosed = close.IsSuccess;
+ if (!close.IsSuccess && failure == null)
+ failure = close.Message;
+ }
+ catch (OperationCanceledException)
+ {
+ if (failure == null)
+ failure = "FileClose did not complete within the bounded close timeout.";
+ }
+ }
+ }
+
+ var success = failure == null;
+ if (success)
+ {
+ progress?.Report(new MmsFileTransferProgress
+ {
+ RemotePath = normalizedPath,
+ BytesTransferred = bytesTransferred,
+ ExpectedBytes = expectedBytes,
+ ReadOperations = readOperations,
+ IsComplete = true
+ });
+ }
+
+ return new MmsFileTransferResult
+ {
+ IsSuccess = success,
+ RemotePath = normalizedPath,
+ BytesTransferred = bytesTransferred,
+ ExpectedBytes = expectedBytes,
+ ReadOperations = readOperations,
+ RemoteFileClosed = remoteFileClosed,
+ Message = success
+ ? $"Downloaded {bytesTransferred} byte(s) from '{normalizedPath}' in {readOperations} FileRead operation(s); FRSM={stateMachineId}."
+ : $"{failure ?? "MMS file transfer failed."} RemotePath='{normalizedPath}', FRSM={stateMachineId?.ToString() ?? "not-opened"}."
+ };
+ }
+
+ private async Task FileOpenInteroperableAsync(
+ string remotePath,
+ CancellationToken cancellationToken)
+ {
+ var invokeId = NextInvokeId();
+ var request = MmsFileOpenRequest.Build(invokeId, remotePath, initialPosition: 0);
+ LastDiscoveryRequestHex = HexDump.ToCompactString(request);
+
+ try
+ {
+ var response = await SendConfirmedPresentationPayloadAsync(request, invokeId, cancellationToken).ConfigureAwait(false);
+ var result = MmsInteroperableFileOpenResponseDecoder.Decode(response, invokeId);
+ LastDiscoveryResponseHex = result.ResponseHexPreview;
+ LastDiscoveryAttemptSummary = result.Message;
+ return result;
+ }
+ catch (Exception ex) when (ex is IOException or InvalidDataException or ObjectDisposedException or InvalidOperationException)
+ {
+ await MarkProtocolFaultAsync().ConfigureAwait(false);
+ return new MmsFileOpenResult
+ {
+ IsSuccess = false,
+ Message = $"FileOpen transport fault: {ex.GetType().Name}: {ex.Message}",
+ ResponseHexPreview = LastDiscoveryResponseHex
+ };
+ }
+ }
+
+ private async Task FileReadInteroperableAsync(
+ int fileReadStateMachineId,
+ CancellationToken cancellationToken)
+ {
+ var invokeId = NextInvokeId();
+ var request = MmsInteroperableFileReadRequest.Build(invokeId, fileReadStateMachineId);
+ LastDiscoveryRequestHex = HexDump.ToCompactString(request);
+
+ try
+ {
+ var response = await SendConfirmedPresentationPayloadAsync(request, invokeId, cancellationToken).ConfigureAwait(false);
+ var result = MmsFileReadResponseDecoder.Decode(response, invokeId, fileReadStateMachineId);
+ LastDiscoveryResponseHex = result.ResponseHexPreview;
+ LastDiscoveryAttemptSummary = result.Message;
+ return result;
+ }
+ catch (Exception ex) when (ex is IOException or InvalidDataException or ObjectDisposedException or InvalidOperationException)
+ {
+ await MarkProtocolFaultAsync().ConfigureAwait(false);
+ return new MmsFileReadResult
+ {
+ IsSuccess = false,
+ FileReadStateMachineId = fileReadStateMachineId,
+ Message = $"FileRead transport fault: {ex.GetType().Name}: {ex.Message}",
+ ResponseHexPreview = LastDiscoveryResponseHex
+ };
+ }
+ }
+
+ private async Task FileCloseInteroperableAsync(
+ int fileReadStateMachineId,
+ CancellationToken cancellationToken)
+ {
+ var invokeId = NextInvokeId();
+ var request = MmsInteroperableFileCloseRequest.Build(invokeId, fileReadStateMachineId);
+ LastDiscoveryRequestHex = HexDump.ToCompactString(request);
+
+ try
+ {
+ var response = await SendConfirmedPresentationPayloadAsync(request, invokeId, cancellationToken).ConfigureAwait(false);
+ var result = MmsFileCloseResponseDecoder.Decode(response, invokeId, fileReadStateMachineId);
+ LastDiscoveryResponseHex = result.ResponseHexPreview;
+ LastDiscoveryAttemptSummary = result.Message;
+ return result;
+ }
+ catch (Exception ex) when (ex is IOException or InvalidDataException or ObjectDisposedException or InvalidOperationException)
+ {
+ await MarkProtocolFaultAsync().ConfigureAwait(false);
+ return new MmsFileCloseResult
+ {
+ IsSuccess = false,
+ FileReadStateMachineId = fileReadStateMachineId,
+ Message = $"FileClose transport fault: {ex.GetType().Name}: {ex.Message}",
+ ResponseHexPreview = LastDiscoveryResponseHex
+ };
+ }
+ }
+}
diff --git a/tests/AR.Iec61850.Tests/MmsSignedFrsmFileTransferTests.cs b/tests/AR.Iec61850.Tests/MmsSignedFrsmFileTransferTests.cs
new file mode 100644
index 0000000..b45313d
--- /dev/null
+++ b/tests/AR.Iec61850.Tests/MmsSignedFrsmFileTransferTests.cs
@@ -0,0 +1,81 @@
+using AR.Iec61850.Asn1;
+using AR.Iec61850.Mms;
+
+namespace AR.Iec61850.Tests;
+
+public sealed class MmsSignedFrsmFileTransferTests
+{
+ [Fact]
+ public void FileOpenResponse_AcceptsNegativeSignedFrsmIdentifier()
+ {
+ var response = BuildConfirmedResponse(
+ invokeId: 31,
+ serviceTag: 72,
+ constructed: true,
+ serviceValue: BerWriter.EncodeTlv(
+ BerClass.ContextSpecific,
+ constructed: false,
+ tagNumber: 0,
+ BerWriter.EncodeSignedInteger(-17)));
+
+ var result = MmsInteroperableFileOpenResponseDecoder.Decode(response, expectedInvokeId: 31);
+
+ Assert.True(result.IsSuccess, result.Message);
+ Assert.Equal(-17, result.FileReadStateMachineId);
+ }
+
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(-17)]
+ [InlineData(int.MinValue)]
+ [InlineData(23)]
+ public void FileReadRequest_EchoesCompleteSignedInteger32Range(int frsmId)
+ {
+ var request = MmsInteroperableFileReadRequest.Build(32, frsmId);
+ var service = ReadService(request, expectedTag: 73);
+
+ Assert.Equal((long)frsmId, BerReader.ReadSignedInteger(service));
+ }
+
+ [Theory]
+ [InlineData(-1)]
+ [InlineData(-17)]
+ [InlineData(int.MinValue)]
+ [InlineData(23)]
+ public void FileCloseRequest_EchoesCompleteSignedInteger32Range(int frsmId)
+ {
+ var request = MmsInteroperableFileCloseRequest.Build(33, frsmId);
+ var service = ReadService(request, expectedTag: 74);
+
+ Assert.Equal((long)frsmId, BerReader.ReadSignedInteger(service));
+ }
+
+ private static BerTlv ReadService(byte[] request, int expectedTag)
+ {
+ var mms = MmsPresentation.StripPresentationPrefix(request);
+ var offset = 0;
+ Assert.True(BerReader.TryReadTlv(mms, ref offset, out var outer));
+ var children = BerReader.ReadChildren(outer.Value);
+ var service = Assert.Single(children.Skip(1));
+ Assert.Equal(BerClass.ContextSpecific, service.Class);
+ Assert.Equal(expectedTag, service.TagNumber);
+ return service;
+ }
+
+ private static byte[] BuildConfirmedResponse(
+ int invokeId,
+ int serviceTag,
+ bool constructed,
+ byte[] serviceValue)
+ {
+ var service = BerWriter.EncodeTlv(
+ BerClass.ContextSpecific,
+ constructed,
+ serviceTag,
+ serviceValue);
+ var confirmedResponse = BerWriter.EncodeTlv(
+ 0xA1,
+ MmsPresentation.Concat(MmsPresentation.Integer(invokeId), service));
+ return MmsPresentation.WrapIsoPresentationPData(confirmedResponse);
+ }
+}