changed ExtendedChar to ExtendedByte,

changed CharacterDataViewModel to ByteDataViewModel,
added Unit Tests for ExtendedByte Conversion,
refactored CommunicationDataViewModel,
changed from reading chars to reading bytes in SerialProtocol,
fixed Clear functionality of MultiFormatDataView
master
Jonas Arnold 3 years ago
parent 39e6420a58
commit df8fdd4a29
  1. 87
      MultiTerm.Core.Tests/ExtendedByteTests.cs
  2. 24
      MultiTerm.Core.Tests/MultiTerm.Protocols.Tests.csproj
  3. 1
      MultiTerm.Core.Tests/Usings.cs
  4. 53
      MultiTerm.Core/ViewModel/ByteDataViewModel.cs
  5. 46
      MultiTerm.Core/ViewModel/CharacterDataViewModel.cs
  6. 208
      MultiTerm.Core/ViewModel/CommunicationDataViewModel.cs
  7. 5
      MultiTerm.Core/ViewModel/ICommunicationDataViewModel.cs
  8. 4
      MultiTerm.Core/ViewModel/IDataViewModel.cs
  9. 8
      MultiTerm.Core/ViewModel/TerminalViewModel.cs
  10. 131
      MultiTerm.Protocols/Model/ExtendedByte.cs
  11. 156
      MultiTerm.Protocols/Model/ExtendedChar.cs
  12. 6
      MultiTerm.Protocols/ReceivedDataEventArgs.cs
  13. 6
      MultiTerm.Protocols/SentDataEventArgs.cs
  14. 8
      MultiTerm.Protocols/Serial/SerialProtocol.cs
  15. 19
      MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.cs
  16. 3
      MultiTerm.Wpf.CustomControl/MultiFormatDataView/MultiFormatDataView.xaml
  17. 6
      MultiTerm.Wpf/View/SendReceiveView.xaml
  18. 6
      MultiTerm.sln

@ -0,0 +1,87 @@
using FluentAssertions;
using MultiTerm.Protocols.Model;
namespace MultiTerm.Protocols.Tests;
[TestFixture(Author = "JonasArnold")]
public class ExtendedByteTests
{
// testname: thema_aktion_expectedout
// ablauf: arrange, act, assert
[SetUp]
public void Setup()
{
}
[Test]
[TestCase((byte)40 , "(")]
[TestCase((byte)10, "\n")]
[TestCase((byte)13, "\r")]
[TestCase((byte)32, " ")]
[TestCase((byte)125, "}")]
[TestCase((byte)128, "?")]
public void ToString_ByteConverted_ResultsInAsciiEncodedString(byte dataByte, string expectedOutcome)
{
// Arrange
ExtendedByte extdByte = new(dataByte);
// Act
var toStringResult = extdByte.ToString();
// Assert
toStringResult.Should().BeEquivalentTo(expectedOutcome);
}
[Test]
[TestCase((byte)40, "(")]
[TestCase((byte)10, "\u240A")]
[TestCase((byte)13, "\u240D")]
[TestCase((byte)32, " ")]
[TestCase((byte)125, "}")]
[TestCase((byte)128, "?")]
public void ToCharacterString_ByteConverted_ResultsInDisplayableCharacterString(byte dataByte, string expectedOutcome)
{
// Arrange
ExtendedByte extdByte = new(dataByte);
// Act
var toStringResult = extdByte.ToCharacterString();
// Assert
toStringResult.Should().BeEquivalentTo(expectedOutcome);
}
[Test]
[TestCase((byte)30, "00011110")]
[TestCase((byte)10, "00001010")]
[TestCase((byte)240, "11110000")]
public void ToBinaryString_ByteConverted_ResultsInCorrectlyFormattedBinaryString(byte dataByte, string expectedOutcome)
{
// Arrange
ExtendedByte extdByte = new(dataByte);
// Act
var toStringResult = extdByte.ToBinaryString();
// Assert
toStringResult.Should().BeEquivalentTo(expectedOutcome);
}
[Test]
[TestCase((byte)2, "02")]
[TestCase((byte)10, "0A")]
[TestCase((byte)127, "7F")]
[TestCase((byte)240, "F0")]
public void ToHexString_ByteConverted_ResultsInCorrectlyFormattedHexString(byte dataByte, string expectedOutcome)
{
// Arrange
ExtendedByte extdByte = new(dataByte);
// Act
var toStringResult = extdByte.ToHexString();
// Assert
toStringResult.Should().BeEquivalentTo(expectedOutcome);
}
}

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.5.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MultiTerm.Protocols\MultiTerm.Protocols.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1 @@
global using NUnit.Framework;

@ -0,0 +1,53 @@
using CommunityToolkit.Mvvm.ComponentModel;
using MultiTerm.Protocols.Model;
using System.Text;
namespace MultiTerm.Core.ViewModel;
public partial class ByteDataViewModel : ObservableObject, IDataViewModel
{
/// <summary>
/// Object of data model.
/// </summary>
private readonly ExtendedByte data;
#region IDataViewModel Implementation
[ObservableProperty]
private int lineIdentifier;
[ObservableProperty]
private TimeOnly time;
[ObservableProperty]
private string displayStringChar = String.Empty;
[ObservableProperty]
private string displayStringHex = String.Empty;
[ObservableProperty]
private string displayStringBin = String.Empty;
#endregion
/// <summary>
/// Allows access to the low level data byte.
/// </summary>
public byte Byte => this.data.Byte;
/// <summary>
/// Instanciates new <see cref="ByteDataViewModel"/>.
/// Initializes internal variables with data according to <paramref name="extendedByte"/>.
/// </summary>
/// <param name="extendedByte">data</param>
/// <param name="lineIdentifier">identifies line in which this byte is located</param>
public ByteDataViewModel(ExtendedByte extendedByte, int lineIdentifier)
{
this.data = extendedByte;
this.lineIdentifier = lineIdentifier;
this.DisplayStringChar = extendedByte.ToCharacterString();
this.DisplayStringHex = extendedByte.ToHexString();
this.DisplayStringBin = extendedByte.ToBinaryString();
this.Time = extendedByte.Time;
}
}

@ -1,46 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using MultiTerm.Protocols.Model;
namespace MultiTerm.Core.ViewModel;
public partial class CharacterDataViewModel : ObservableObject, IDataViewModel
{
/// <summary>
/// Object of data model.
/// </summary>
private readonly ExtendedChar character;
#region IDataViewModel Implementation
[ObservableProperty]
private int lineIdentifier;
[ObservableProperty]
private TimeOnly time;
[ObservableProperty]
private string displayString = String.Empty;
[ObservableProperty]
private string displayStringHex = String.Empty;
[ObservableProperty]
private string displayStringBin = String.Empty;
#endregion
/// <summary>
/// Allows access to character of this instance.
/// </summary>
public char Character { get { return this.character.Character; } }
public CharacterDataViewModel(ExtendedChar character, int lineIdentifier)
{
this.character = character;
this.lineIdentifier = lineIdentifier;
this.DisplayString = character.ToUtf16String();
this.DisplayStringHex = character.ToHexString();
this.DisplayStringBin = character.ToBinaryString();
this.Time = character.Time;
}
}

@ -4,26 +4,27 @@ using CommunityToolkit.Mvvm.ComponentModel;
using MultiTerm.Core.Types;
using MultiTerm.Protocols.Model;
using System.Collections.ObjectModel;
using System.Text;
namespace MultiTerm.Core.ViewModel;
public partial class CommunicationDataViewModel : ObservableObject, ICommunicationDataViewModel<CharacterDataViewModel, ExtendedChar>
public partial class CommunicationDataViewModel : ObservableObject, ICommunicationDataViewModel<ByteDataViewModel, ExtendedByte>
{
private readonly IContext uiContext;
private int dataCharacterCount = 0;
private List<char>? listOfPreviousCharacters = null;
private List<byte>? listOfPreviousCharacters = null;
#region ICommunicationDataViewModel Implementation
[ObservableProperty]
private ObservableCollection<CharacterDataViewModel> data = new();
private ObservableCollection<ByteDataViewModel> data = new();
[ObservableProperty]
private string dataAsString = string.Empty;
[ObservableProperty]
private ObservableCollection<CharacterDataViewModel>? selected = new();
private ObservableCollection<ByteDataViewModel>? selected = new();
[ObservableProperty]
NewlineSeparatorType newlineSeparator = NewlineSeparatorType.None;
@ -42,29 +43,30 @@ public partial class CommunicationDataViewModel : ObservableObject, ICommunicati
// int counter = 0;
// int lineNumber = 1;
// //List<ExtendedChar> listOfChars = new();
// foreach (var character in exampleData)
// foreach (var dataByte in exampleData)
// {
// if(++counter > 100 || character == '\n')
// if(++counter > 100 || dataByte == '\n')
// {
// //this.ReceivedData.Add();
// //listOfChars = new List<ExtendedChar>();
// counter = 0;
// lineNumber += 1;
// }
// var extdChar = new ExtendedChar(character);
// var extdChar = new ExtendedChar(dataByte);
// //listOfChars.Add(extdChar);
// this.ReceivedData.Add(new DataViewModel(extdChar, lineNumber));
// }
//}
}
public void HandleNewData(IEnumerable<ExtendedChar> newRawData)
public void HandleNewData(IEnumerable<ExtendedByte> newRawData)
{
// update collection and string, invoke ui thread if necessary
ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate
{
InsertNewNewCharactersIntoCollection(this.Data, ref this.dataCharacterCount, ref this.listOfPreviousCharacters, this.NewlineSeparator, newRawData);
this.DataAsString = InsertNewCharactersIntoString(this.DataAsString, newRawData);
this.DataAsString = InsertNewNewCharactersIntoCollection(this.Data, this.DataAsString,
ref this.dataCharacterCount, ref this.listOfPreviousCharacters,
this.NewlineSeparator, newRawData);
});
}
@ -76,51 +78,69 @@ public partial class CommunicationDataViewModel : ObservableObject, ICommunicati
// update collection, invoke ui thread if necessary
ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate
{
this.Data = ReorderCollection(this.Data, value);
this.DataAsString = ReorderString(this.Data, value);
var reorderedCollection = ReorderCollection(this.Data, value);
this.Data = reorderedCollection.Item1;
this.DataAsString = reorderedCollection.Item2;
});
}
public void Clear()
{
// update collection and string, invoke ui thread if necessary
ContextHelpers.InvokeIfNecessary(this.uiContext, (Action)delegate
{
this.DataAsString = string.Empty;
this.Data.Clear();
});
}
#region Collection Manipulation
/// <summary>
/// Function that reorders a given collection with item type <see cref="CharacterDataViewModel"/> using the given <paramref name="newlineSeparatorType"/>.
/// Function that reorders a given collection with item type <see cref="ByteDataViewModel"/> using the given <paramref name="newlineSeparatorType"/>.
/// Reordered collection is returned, but LineIdentifier is also overwritten in the <paramref name="currentCollection"/> parameter.
/// </summary>
/// <param name="currentCollection">items in this collection will be reordered</param>
/// <param name="newlineSeparatorType">separator between lines</param>
/// <returns>reordered collection</returns>
private static ObservableCollection<CharacterDataViewModel> ReorderCollection(ObservableCollection<CharacterDataViewModel> currentCollection, NewlineSeparatorType newlineSeparatorType)
/// <returns>reordered collection and string</returns>
private static Tuple<ObservableCollection<ByteDataViewModel>, string> ReorderCollection(ObservableCollection<ByteDataViewModel> currentCollection, NewlineSeparatorType newlineSeparatorType)
{
ObservableCollection<CharacterDataViewModel> newCollection = new();
ObservableCollection<ByteDataViewModel> newCollection = new();
StringBuilder stringBuilder = new();
// local vars
int lineCounter = 0;
List<char>? previousCharacters = null;
List<byte>? previousBytes = null;
// iterate through items
foreach (CharacterDataViewModel item in currentCollection)
foreach (ByteDataViewModel item in currentCollection)
{
item.LineIdentifier = lineCounter;
newCollection.Add(item);
stringBuilder.Append(item.DisplayStringChar);
switch (ShouldIntroduceNewlineAfterThisCharacter(item.Character, previousCharacters, newlineSeparatorType))
switch (ShouldIntroduceNewlineAfterThisByte(item.Byte, previousBytes, newlineSeparatorType))
{
case IntroduceNewlineAfterThisCharacterResult.NoNewline:
// a decision could be made, previousCharacters can be cleared
if (previousCharacters != null) { previousCharacters = null; }
case ShouldIntroduceNewlineAfterThisByteResult.NoNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// nothing to do
break;
case IntroduceNewlineAfterThisCharacterResult.IntroduceNewline:
// a decision could be made, previousCharacters can be cleared
if (previousCharacters != null) { previousCharacters = null; }
// increase line count and break
case ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// increase line count
lineCounter++;
// append line in string
stringBuilder.AppendLine();
break;
case IntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters:
// first time that more characters are required => create new list
previousCharacters ??= new List<char>();
// add current character to list
previousCharacters.Add(item.Character);
case ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters:
// first time that more bytes are required => create new list
previousBytes ??= new List<byte>();
// add current byte to list
previousBytes.Add(item.Byte);
break;
default:
@ -128,65 +148,77 @@ public partial class CommunicationDataViewModel : ObservableObject, ICommunicati
}
}
return newCollection;
return Tuple.Create(newCollection, stringBuilder.ToString());
}
/// <summary>
/// Function that handles a collection of new characters that should end up in the collection <paramref name="dataCollection"/>.
/// Function that handles a collection of new bytes that should end up in the collection <paramref name="dataCollection"/>.
/// In case a new line is required, according to the given <paramref name="newlineSeparatorType"/>, it is automatically introduced.
/// Following parameters need to be referenced and stored outside: <paramref name="collectionLineCounter"/> and <paramref name="previousCharacters"/>.
/// Following parameters need to be referenced and stored outside: <paramref name="collectionLineCounter"/> and <paramref name="previousBytes"/>.
/// </summary>
/// <param name="dataCollection">collection to add the characters to</param>
/// <param name="dataCollection">collection to add the bytes to</param>
/// <param name="dataCollectionAsString">collection as string</param>
/// <param name="collectionLineCounter">current line count</param>
/// <param name="previousCharacters">list of previous character, newest at the last position of the list, null if nothing is stored</param>
/// <param name="previousBytes">list of previous bytes, newest at the last position of the list, null if nothing is stored</param>
/// <param name="newlineSeparatorType">separator between seperate lines</param>
/// <param name="newCharacters">characters to add to the <paramref name="dataCollection"/></param>
/// <param name="newBytes">bytes to add to the <paramref name="dataCollection"/></param>
/// <returns>string representation of data</returns>
/// <exception cref="Exception">in case of any error</exception>
private static void InsertNewNewCharactersIntoCollection(ObservableCollection<CharacterDataViewModel> dataCollection,
private static string InsertNewNewCharactersIntoCollection(
ObservableCollection<ByteDataViewModel> dataCollection,
string dataCollectionAsString,
ref int collectionLineCounter,
ref List<char>? previousCharacters,
ref List<byte>? previousBytes,
NewlineSeparatorType newlineSeparatorType,
IEnumerable<ExtendedChar> newCharacters)
IEnumerable<ExtendedByte> newBytes)
{
// go through every character
foreach (var newExtdChar in newCharacters)
StringBuilder stringBuilder = new();
stringBuilder.Append(dataCollectionAsString);
// go through every byte
foreach (ExtendedByte newByte in newBytes)
{
// add to collection with the current counter, invoking UI context if necssary
// add to collection and string with the current counter, invoking UI context if necssary
var currentLineCounter = collectionLineCounter;
dataCollection.Add(new CharacterDataViewModel(newExtdChar, currentLineCounter));
dataCollection.Add(new ByteDataViewModel(newByte, currentLineCounter));
stringBuilder.Append(newByte.ToCharacterString());
switch (ShouldIntroduceNewlineAfterThisCharacter(newExtdChar.Character, previousCharacters, newlineSeparatorType))
switch (ShouldIntroduceNewlineAfterThisByte(newByte.Byte, previousBytes, newlineSeparatorType))
{
case IntroduceNewlineAfterThisCharacterResult.NoNewline:
// a decision could be made, previousCharacters can be cleared
if (previousCharacters != null) { previousCharacters = null; }
case ShouldIntroduceNewlineAfterThisByteResult.NoNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// nothing to do
break;
case IntroduceNewlineAfterThisCharacterResult.IntroduceNewline:
// a decision could be made, previousCharacters can be cleared
if (previousCharacters != null) { previousCharacters = null; }
// increase line count and break
case ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline:
// a decision could be made, previousBytes can be cleared
if (previousBytes != null) { previousBytes = null; }
// increase line count
collectionLineCounter++;
// append line in string
stringBuilder.AppendLine();
break;
case IntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters:
// first time that more characters are required => create new list
previousCharacters ??= new List<char>();
// add current character to list
previousCharacters.Add(newExtdChar.Character);
case ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters:
// first time that more bytes are required => create new list
previousBytes ??= new List<byte>();
// add current byte to list
previousBytes.Add(newByte.Byte);
break;
default:
throw new Exception($"'{nameof(InsertNewNewCharactersIntoCollection)}()' failed because of error when checking if a newline should be introduced.");
}
}
return stringBuilder.ToString();
}
/// <summary>
/// Result type for <see cref="ShouldIntroduceNewlineAfterThisCharacter"/>
/// Result type for <see cref="ShouldIntroduceNewlineAfterThisByte"/>
/// </summary>
public enum IntroduceNewlineAfterThisCharacterResult
public enum ShouldIntroduceNewlineAfterThisByteResult
{
/// <summary>
/// No newline is required.
@ -194,7 +226,7 @@ public partial class CommunicationDataViewModel : ObservableObject, ICommunicati
NoNewline,
/// <summary>
/// A newline shall be introduced after this character.
/// A newline shall be introduced after this byte.
/// </summary>
IntroduceNewline,
@ -205,17 +237,17 @@ public partial class CommunicationDataViewModel : ObservableObject, ICommunicati
}
/// <summary>
/// Function to check wether a newline shall be introduced after the given character.
/// Since some newline sequences will require multiple characters in correct order, a more complex handling is required, which is possible using this function.
/// Function to check wether a newline shall be introduced after the given <paramref name="dataByte"/>.
/// Since some newline sequences will require multiple bytes in correct order, a more complex handling is required, which is possible using this function.
/// </summary>
/// <param name="character">the current character in the collection (or a single character)</param>
/// <param name="previousCharacters">list of previous character, newest at the last position of the list, null if not required</param>
/// <param name="dataByte">the current dataByte in the collection (or a single byte)</param>
/// <param name="previousBytes">list of previous bytes, newest at the last position of the list, null if not required</param>
/// <param name="newlineSeparatorType">separator type</param>
/// <returns>complex result of type <see cref="IntroduceNewlineAfterThisCharacterResult"/></returns>
/// <returns>enum result of type <see cref="ShouldIntroduceNewlineAfterThisByteResult"/></returns>
/// <exception cref="NotImplementedException">if the handling for the <paramref name="newlineSeparatorType"/> is not implemented</exception>
private static IntroduceNewlineAfterThisCharacterResult ShouldIntroduceNewlineAfterThisCharacter(char character, List<char>? previousCharacters, NewlineSeparatorType newlineSeparatorType)
private static ShouldIntroduceNewlineAfterThisByteResult ShouldIntroduceNewlineAfterThisByte(byte dataByte, List<byte>? previousBytes, NewlineSeparatorType newlineSeparatorType)
{
var result = IntroduceNewlineAfterThisCharacterResult.NoNewline;
var result = ShouldIntroduceNewlineAfterThisByteResult.NoNewline;
switch (newlineSeparatorType)
{
@ -223,63 +255,41 @@ public partial class CommunicationDataViewModel : ObservableObject, ICommunicati
break;
case NewlineSeparatorType.CR:
if (character == '\r')
if (dataByte == (byte)'\r')
{
result = IntroduceNewlineAfterThisCharacterResult.IntroduceNewline;
result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline;
}
break;
case NewlineSeparatorType.LF:
if (character == '\n')
if (dataByte == (byte)'\n')
{
result = IntroduceNewlineAfterThisCharacterResult.IntroduceNewline;
result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline;
}
break;
case NewlineSeparatorType.CR_LF:
if (character == '\r')
if (dataByte == (byte)'\r')
{
result = IntroduceNewlineAfterThisCharacterResult.RequiresMoreCharacters;
result = ShouldIntroduceNewlineAfterThisByteResult.RequiresMoreCharacters;
}
if (character == '\n')
if (dataByte == (byte)'\n')
{
if (previousCharacters != null && previousCharacters.Last() == '\r')
if (previousBytes != null && previousBytes.Last() == (byte)'\r')
{
result = IntroduceNewlineAfterThisCharacterResult.IntroduceNewline;
result = ShouldIntroduceNewlineAfterThisByteResult.IntroduceNewline;
}
}
break;
default:
throw new NotImplementedException($"'{nameof(ShouldIntroduceNewlineAfterThisCharacter)}()' does not implement handling for {nameof(NewlineSeparatorType)} {newlineSeparatorType}");
throw new NotImplementedException($"'{nameof(ShouldIntroduceNewlineAfterThisByte)}()' does not implement handling for {nameof(NewlineSeparatorType)} {newlineSeparatorType}");
}
return result;
}
#endregion
#region String Manipulation
// TODO Implement Line Handling
private static string InsertNewCharactersIntoString(string dataAsString, IEnumerable<ExtendedChar> newRawData)
{
string newDataAsString = dataAsString;
foreach (ExtendedChar character in newRawData)
{
newDataAsString += character.Character;
}
return newDataAsString;
}
private static string ReorderString(ObservableCollection<CharacterDataViewModel> collection, NewlineSeparatorType value)
{
// Not yet implemented
return "Not implemented";
}
#endregion
}

@ -32,4 +32,9 @@ public interface ICommunicationDataViewModel<T_Data, T_Raw> where T_Data : IData
/// </summary>
/// <param name="data">collection of new data to insert</param>
void HandleNewData(IEnumerable<T_Raw> data);
/// <summary>
/// Allows to clear the data.
/// </summary>
void Clear();
}

@ -14,9 +14,9 @@ public interface IDataViewModel
TimeOnly Time { get; }
/// <summary>
/// Hosts a displayable string of the character (UTF-16 encoded).
/// Hosts a displayable string of the character (.NET: UTF-16 encoded).
/// </summary>
string DisplayString { get; }
string DisplayStringChar { get; }
/// <summary>
/// Hosts a string of the character (hexadecimal format).

@ -62,13 +62,13 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
/// Holds communication data that was received via the communication protocol.
/// </summary>
[ObservableProperty]
private ICommunicationDataViewModel<CharacterDataViewModel, ExtendedChar> receivedData;
private ICommunicationDataViewModel<ByteDataViewModel, ExtendedByte> receivedData;
/// <summary>
/// Holds communication data that was sent via the communication protocol.
/// </summary>
[ObservableProperty]
private ICommunicationDataViewModel<CharacterDataViewModel, ExtendedChar> sentData;
private ICommunicationDataViewModel<ByteDataViewModel, ExtendedByte> sentData;
/// <summary>
/// Defines at which newline sequence the displayed data is wrapped. Defaults to none.
@ -137,7 +137,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
// guard null
if (this.ReceivedData == null) { throw new NullReferenceException($"{nameof(CommunicationProtocol_ReceivedDataEvent)} found {nameof(this.ReceivedData)} to be null."); }
// handover data
this.ReceivedData.HandleNewData(e.ReceivedCharacters);
this.ReceivedData.HandleNewData(e.ReceivedData);
}
private void CommunicationProtocol_SentDataEvent(object? sender, SentDataEventArgs e)
@ -145,7 +145,7 @@ public abstract partial class TerminalViewModel : ObservableObject, ITerminalVie
// guard null
if (this.SentData == null) { throw new NullReferenceException($"{nameof(CommunicationProtocol_SentDataEvent)} found {nameof(this.SentData)} to be null."); }
// handover data
this.SentData.HandleNewData(e.SentCharacters);
this.SentData.HandleNewData(e.SentData);
}
#endregion

@ -0,0 +1,131 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Text;
namespace MultiTerm.Protocols.Model;
/// <summary>
/// A class that represents a single <see cref="byte"/> and contains some additional information and methods.
/// A time can be stored in combination with this <see cref="byte"/> using the <see cref="Time"/> property. E.g. to represent arrived or sent time.
/// Several methods to display the <see cref="byte"/> in other formats are provided.
/// </summary>
public partial class ExtendedByte
{
/// <summary>
/// Data in the form of a byte.
/// </summary>
public byte Byte { get; set; }
/// <summary>
/// Time that is associated with the <see cref="Byte"/>.
/// E.g. to represent arrived or sent time.
/// </summary>
public TimeOnly Time { get; private set; }
/// <summary>
/// Creates an instance of <see cref="ExtendedByte"/> with a given <paramref name="dataByte"/>.
/// Sets <see cref="Time"/> to now.
/// </summary>
/// <param name="dataByte">data</param>
public ExtendedByte(byte dataByte) : this(dataByte, TimeOnly.FromDateTime(DateTime.Now)) { }
/// <summary>
/// Creates an instance of <see cref="ExtendedByte"/> with a given <paramref name="dataByte"/> and <paramref name="time"/>.
/// </summary>
/// <param name="dataByte">data</param>
/// <param name="time">time</param>
public ExtendedByte(byte dataByte, TimeOnly time)
{
this.Byte = dataByte;
this.Time = time;
}
/// <summary>
/// Returns this <see cref="Byte"/> as ASCII encoded String.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Encoding.ASCII.GetString(new byte[] { this.Byte });
}
/// <summary>
/// Returns displayable character as string.
/// ASCII control sequences are replaced with unicode control pictures.
/// </summary>
/// <returns>string with single character</returns>
public string ToCharacterString()
{
char character = (char)this.Byte;
string characterString;
// character is ASCII encoded and is a control sequence
if (char.IsAscii(character) && character <= '\x001F')
{
// conver to unicode Control Picture (see https://en.wikipedia.org/wiki/Control_Pictures)
characterString = ((char)('\u2400' + character)).ToString();
}
// else just convert using ASCII code
else if(char.IsAscii(character))
{
characterString = Encoding.ASCII.GetString(new byte[] { this.Byte });
}
else
{
characterString = "?";
}
return characterString;
}
/// <summary>
/// Returns displayable hex string.
/// </summary>
/// <returns></returns>
public string ToHexString()
{
// hexadecimal converter for byte to string
static string hexConverter(byte ch) => Convert.ToHexString(new byte[] { ch });
// extract bytes from character and convert to correct format
return ConvertToString(new byte[] { this.Byte }, hexConverter, 2);
}
/// <summary>
/// Returns displayable binary string.
/// </summary>
/// <returns></returns>
public string ToBinaryString()
{
// binary converter for byte to string
static string binaryConverter(byte ch) => Convert.ToString(ch, 2);
// extract bytes from character and convert to correct format
return ConvertToString(new byte[] { this.Byte }, binaryConverter, 8);
}
/// <summary>
/// Converts a <paramref name="byteArray"/> to a string, using the given <paramref name="conversionFunction"/> to convert a single byte to a character.
/// <paramref name="oneByteWidth"/> defines how many characters the result string should contain (the left side will be padded with zeroes).
/// </summary>
/// <returns>converte string</returns>
private static string ConvertToString(byte[] byteArray, Converter<byte, string> conversionFunction, int oneByteWidth)
{
// foreach byte convert to binary and add to string
string resultString = String.Empty;
// go through whole array
for (int i = 0; i < byteArray.Length; i++)
{
// add byte to string in correct format, padding left with zeros up to width of one byte
resultString += $"{conversionFunction(byteArray[i]).PadLeft(oneByteWidth, '0')}";
// if not last byte add space separator
if (i < byteArray.Length - 1)
{
resultString += " ";
}
}
return resultString;
}
}

@ -1,156 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System.Text;
namespace MultiTerm.Protocols.Model;
/// <summary>
/// A class that represents a Character and contains some additional information and methods.
/// A time can be stored in combination with this Character using the <see cref="Time"/> property. E.g. to represent arrived or sent time.
/// Several methods to display the Character in other formats than Unicode are provided.
/// </summary>
public partial class ExtendedChar
{
/// <summary>
/// Data in the form of a character. UTF-16 code unit.
/// </summary>
public char Character { get; set; }
/// <summary>
/// Time that is associated with the <see cref="Character"/>.
/// E.g. to represent arrived or sent time.
/// </summary>
public TimeOnly Time { get; private set; }
/// <summary>
/// Creates an instance of ExtendedChar with a given <paramref name="character"/>.
/// Sets <see cref="Time"/> to now.
/// </summary>
/// <param name="character">data</param>
public ExtendedChar(char character) : this(character, TimeOnly.FromDateTime(DateTime.Now)) { }
/// <summary>
/// Creates an instance of ExtendedChar with a given <paramref name="character"/> and <paramref name="time"/>.
/// Initializes string properties <see cref="StringUtf16"/>, <see cref="StringHex"/> and <see cref="StringBin"/>.
/// </summary>
/// <param name="character">data</param>
/// <param name="time">time</param>
public ExtendedChar(char character, TimeOnly time)
{
this.Character = character;
this.Time = time;
}
public override string ToString()
{
return this.Character.ToString();
}
public string ToUtf16String()
{
string characterString;
// character is ASCII encoded and is a control sequence
if (char.IsAscii(this.Character) && this.Character <= '\x001F')
{
// conver to unicode Control Picture (see https://en.wikipedia.org/wiki/Control_Pictures)
characterString = ((char)('\u2400' + this.Character)).ToString();
// TODO Remove
//characterString = this.Character switch
//{
// '\t' => "\\t",
// ' ' => " ",
// '\n' => "\u240A",
// '\r' => "\u240D",
// '\v' => "\\v",
// '\f' => "\\f",
// _ => this.Character.ToString()
//};
}
// TODO Remove
//else if (char.IsControl(this.Character))
//{
// characterString = "<CTRL>";
//}
else
{
characterString = this.Character.ToString();
}
return characterString;
}
public string ToHexString()
{
// hexadecimal converter for byte to string
Converter<byte, string> hexConverter = (ch) => Convert.ToHexString(new byte[] { ch });
// extract bytes from character and convert to correct format
return ConvertToString(GetCharacterByteArray(this.Character), hexConverter, 2);
//// foreach byte convert to hex and add to string
//string hexString = String.Empty;
//for (int i = 0; i < byteArray.Length; i++)
//{
// // add byte to string in binary format, padding left with zeros up to 8
// hexString += $"{Convert.ToString(byteArray[i], 2).PadLeft(8, '0')}";
// // if not last byte add space separator
// if (i < byteArray.Length - 1)
// {
// hexString += " ";
// }
//}
//return hexString;
//// get byte array from character and convert to hexadecimal
//return Convert.ToHexString(GetCharacterByteArray(this.Character)).PadLeft(2);
}
public string ToBinaryString()
{
// binary converter for byte to string
Converter<byte, string> binaryConverter = (ch) => Convert.ToString(ch, 2);
// extract bytes from character and convert to correct format
return ConvertToString(GetCharacterByteArray(this.Character), binaryConverter, 8);
}
private static string ConvertToString(byte[] byteArray, Converter<byte, string> conversionFunction, int oneByteWidth)
{
// foreach byte convert to binary and add to string
string resultString = String.Empty;
// go through whole array
for (int i = 0; i < byteArray.Length; i++)
{
// add byte to string in correct format, padding left with zeros up to width of one byte
resultString += $"{conversionFunction(byteArray[i]).PadLeft(oneByteWidth, '0')}";
// if not last byte add space separator
if (i < byteArray.Length - 1)
{
resultString += " ";
}
}
return resultString;
}
private static byte[] GetCharacterByteArray(char character)
{
byte[] byteArray = Array.Empty<byte>();
if (char.IsAscii(character))
{
byteArray = Encoding.ASCII.GetBytes(character.ToString());
}
else
{
// extract bytes from utf16 character
byteArray = Encoding.Unicode.GetBytes(character.ToString());
}
return byteArray;
}
}

@ -4,10 +4,10 @@ namespace MultiTerm.Protocols;
public class ReceivedDataEventArgs : EventArgs
{
public IEnumerable<ExtendedChar> ReceivedCharacters { get; private set; }
public IEnumerable<ExtendedByte> ReceivedData { get; private set; }
public ReceivedDataEventArgs(IEnumerable<ExtendedChar> receivedCharacters)
public ReceivedDataEventArgs(IEnumerable<ExtendedByte> receivedData)
{
this.ReceivedCharacters = receivedCharacters;
this.ReceivedData = receivedData;
}
}

@ -4,10 +4,10 @@ namespace MultiTerm.Protocols;
public class SentDataEventArgs : EventArgs
{
public IEnumerable<ExtendedChar> SentCharacters { get; private set; }
public IEnumerable<ExtendedByte> SentData { get; private set; }
public SentDataEventArgs(IEnumerable<ExtendedChar> sentCharacters)
public SentDataEventArgs(IEnumerable<ExtendedByte> sentData)
{
this.SentCharacters = sentCharacters;
this.SentData = sentData;
}
}

@ -76,14 +76,14 @@ public class SerialProtocol : CommunicationProtocol
while(ct.IsCancellationRequested == false)
{
// reads character based on configured encoding (here ASCII)
int readCharacter = serialPort.ReadChar();
if (readCharacter != -1) // -1 = timeout
int readByte = serialPort.ReadByte();
if (readByte != -1) // -1 = end of stream
{
// create extended char type
var character = new ExtendedChar((char)readCharacter);
var character = new ExtendedByte((byte)readByte);
// report new data with event
this.OnReceivedData(new ReceivedDataEventArgs(new ExtendedChar[] { character }));
this.OnReceivedData(new ReceivedDataEventArgs(new ExtendedByte[] { character }));
}
}
}

@ -27,7 +27,7 @@ public class MultiFormatDataView : Control
#region Dependency Properties
public static readonly DependencyProperty DataSourceProperty =
DependencyProperty.Register("DataSource",
typeof(ICommunicationDataViewModel<CharacterDataViewModel, ExtendedChar>), typeof(MultiFormatDataView),
typeof(ICommunicationDataViewModel<ByteDataViewModel, ExtendedByte>), typeof(MultiFormatDataView),
new PropertyMetadata(null, OnDataSourcePropertyChanged));
public static readonly DependencyProperty SelectorItemsSourceProperty =
@ -69,9 +69,9 @@ public class MultiFormatDataView : Control
/// .NET Property for DataSourceProperty.
/// </summary>
[Bindable(true)]
public ICommunicationDataViewModel<CharacterDataViewModel, ExtendedChar> DataSource
public ICommunicationDataViewModel<ByteDataViewModel, ExtendedByte> DataSource
{
get { return (ICommunicationDataViewModel<CharacterDataViewModel, ExtendedChar>)GetValue(DataSourceProperty); }
get { return (ICommunicationDataViewModel<ByteDataViewModel, ExtendedByte>)GetValue(DataSourceProperty); }
set { SetValue(DataSourceProperty, value); }
}
@ -195,12 +195,12 @@ public class MultiFormatDataView : Control
// manually create collection view
ICollectionView cv = CollectionViewSource.GetDefaultView(mfdv.DataSource.Data);
// add grouping
PropertyGroupDescription groupDescription = new(nameof(CharacterDataViewModel.LineIdentifier));
PropertyGroupDescription groupDescription = new(nameof(ByteDataViewModel.LineIdentifier));
cv.GroupDescriptions.Add(groupDescription);
// add live grouping
if (cv is ICollectionViewLiveShaping cvLiveShaping && cvLiveShaping.CanChangeLiveGrouping)
{
cvLiveShaping.LiveGroupingProperties.Add(nameof(CharacterDataViewModel.LineIdentifier));
cvLiveShaping.LiveGroupingProperties.Add(nameof(ByteDataViewModel.LineIdentifier));
cvLiveShaping.IsLiveGrouping = true;
}
// save collection view
@ -247,7 +247,8 @@ public class MultiFormatDataView : Control
private void OnClearButtonClicked(object sender, RoutedEventArgs e)
{
// raise clear requested event
this.DataSource.Data.Clear();
RoutedEventArgs args = new(ClearRequestedEvent);
RaiseEvent(args);
}
#region Selected Items handling
@ -260,11 +261,11 @@ public class MultiFormatDataView : Control
if (this.tbCharOnlyView != null && this.tbCharOnlyView.SelectionLength > 0) { this.tbCharOnlyView.Select(0, 0); }
// otherwise update internal list
foreach (CharacterDataViewModel item in e.RemovedItems)
foreach (ByteDataViewModel item in e.RemovedItems)
{
this.DataSource.Selected.Remove(item);
}
foreach (CharacterDataViewModel item in e.AddedItems)
foreach (ByteDataViewModel item in e.AddedItems)
{
this.DataSource.Selected.Add(item);
}
@ -272,7 +273,7 @@ public class MultiFormatDataView : Control
private void TextBoxCharOnlyView_SelectionChanged(object sender, RoutedEventArgs e)
{
var newSelection = new ObservableCollection<CharacterDataViewModel>();
var newSelection = new ObservableCollection<ByteDataViewModel>();
int selectionStartIndex = this.tbCharOnlyView!.SelectionStart;
// TEMP OLD
// extract text from the beginning to the start of the selected text

@ -56,7 +56,7 @@
</StackPanel.Triggers>
<!-- Character -->
<Border BorderThickness="0">
<Label Content="{Binding DisplayString}" Padding="0" Margin="2 0" VerticalAlignment="Center">
<Label Content="{Binding DisplayStringChar}" Padding="0" Margin="2 0" VerticalAlignment="Center">
<Label.Style>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
@ -161,6 +161,7 @@
Grid.Column="0" Grid.Row="0" Grid.RowSpan="2"
IsReadOnly="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="true"
ScrollViewer.PanningMode="VerticalOnly">

@ -57,11 +57,11 @@
Converter={StaticResource EnumValToDescConverter}}"
SelectorDescription="Newline Separator">
<!--<behaviors:Interaction.Triggers>
<behaviors:Interaction.Triggers>
<behaviors:EventTrigger EventName="ClearRequested" SourceObject="{Binding ElementName=mfdvReceive}">
<behaviors:InvokeCommandAction Command="{Binding CommunicationData.ClearReceivedDataCommand}" />
<behaviors:InvokeCommandAction Command="{Binding ReceivedData.ClearCommand}" />
</behaviors:EventTrigger>
</behaviors:Interaction.Triggers>-->
</behaviors:Interaction.Triggers>
</custom_controls:MultiFormatDataView>
</GroupBox>

@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiTerm.Wpf.CustomControl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiTerm.Protocols", "MultiTerm.Protocols\MultiTerm.Protocols.csproj", "{D35B996A-91EE-4A6A-BA82-C74684AF4572}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiTerm.Protocols.Tests", "MultiTerm.Core.Tests\MultiTerm.Protocols.Tests.csproj", "{E75D3FF4-61FB-4A7F-A75D-B9E69A2FAE78}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -39,6 +41,10 @@ Global
{D35B996A-91EE-4A6A-BA82-C74684AF4572}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D35B996A-91EE-4A6A-BA82-C74684AF4572}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D35B996A-91EE-4A6A-BA82-C74684AF4572}.Release|Any CPU.Build.0 = Release|Any CPU
{E75D3FF4-61FB-4A7F-A75D-B9E69A2FAE78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E75D3FF4-61FB-4A7F-A75D-B9E69A2FAE78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E75D3FF4-61FB-4A7F-A75D-B9E69A2FAE78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E75D3FF4-61FB-4A7F-A75D-B9E69A2FAE78}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

Loading…
Cancel
Save