You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
14 KiB
376 lines
14 KiB
using Common.AppSettings;
|
|
using Common.Helpers;
|
|
using Common.Messaging;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using MultiTerm.Core.Model;
|
|
using MultiTerm.Core.Types;
|
|
using MultiTerm.Protocols;
|
|
using MultiTerm.Protocols.Model;
|
|
using MultiTerm.Protocols.Types;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.ObjectModel;
|
|
|
|
namespace MultiTerm.Core.ViewModel;
|
|
|
|
public abstract partial class TerminalViewModel : ObservableObject, ITerminalViewModel
|
|
{
|
|
private const string defaultReceiveNewlineSeparatorAppSettingsKey = "DefaultReceiveNewlineSeparator";
|
|
private const string defaultSendNewlineSeparatorAppSettingsKey = "DefaultSendNewlineSeparator";
|
|
private readonly IAppSettingsProvider appSettings;
|
|
private readonly IMessenger messenger;
|
|
|
|
private readonly RecurringTimer displayNewDataTimer; // Timer for data updates
|
|
private const int displayNewDataTimerIntervalMs = 20; // after x milliseconds new data is fed forward to the UI
|
|
private readonly ConcurrentQueue<IEnumerable<ExtendedByte>> receivedDataQueue = new(); // Queue to buffer received data from communication protocol
|
|
private readonly ConcurrentQueue<IEnumerable<ExtendedByte>> sentDataQueue = new(); // Queue to buffer sent data (as reported from communication protocol)
|
|
|
|
public abstract TerminalViewType ViewType { get; }
|
|
|
|
private ProtocolType protocolType;
|
|
|
|
public ProtocolType ProtocolType
|
|
{
|
|
get { return this.protocolType; }
|
|
set
|
|
{
|
|
this.protocolType = value;
|
|
this.UpdateTitle();
|
|
}
|
|
}
|
|
|
|
#region Title
|
|
/// <summary>
|
|
/// Terminal title, to allow user to distinguish between different terminals.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private string title = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Update the Property <see cref="Title"/>. To be called when it was changed.
|
|
/// </summary>
|
|
private void UpdateTitle()
|
|
{
|
|
if(this.CommunicationProtocol == null)
|
|
{
|
|
this.Title = $"new";
|
|
}
|
|
else
|
|
{
|
|
this.Title = $"{this.CommunicationProtocol?.InstanceIdentifier}";
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Observable Properties
|
|
/// <summary>
|
|
/// Defines at which newline sequence the displayed data is wrapped. Defaults to none.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private NewlineSeparatorType dataDisplayNewlineSeparatorType = NewlineSeparatorType.None;
|
|
|
|
/// <summary>
|
|
/// Defines which Newline sequence is attached to data when it is sent. Defaults to none.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private NewlineSeparatorType sendNewlineSeparatorType = NewlineSeparatorType.None;
|
|
|
|
[ObservableProperty]
|
|
private ICommunicationProtocol? communicationProtocol;
|
|
|
|
[ObservableProperty]
|
|
private IProtocolSettingsViewModel? protocolSettings;
|
|
|
|
/// <summary>
|
|
/// <see cref="ProtocolConnectionState"/> of <see cref="CommunicationProtocol"/> as ObservableProperty.
|
|
/// Required since <see cref="CommunicationProtocol"/> does not implement <see cref="ObservableObject"/>.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private ProtocolConnectionState communicationProtocolConnectionState;
|
|
|
|
/// <summary>
|
|
/// <see cref="ICommunicationProtocol.LongInstanceIdentifier"/> as ObservableProperty.
|
|
/// Required since <see cref="CommunicationProtocol"/> does not implement <see cref="ObservableObject"/>.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
private string communicationProtocolLongIdentifier = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Collection of sent messages as <see cref="IHistoryElement"/>.
|
|
/// </summary>
|
|
[ObservableProperty]
|
|
ObservableCollection<IHistoryElement> sentMessagesHistory = new();
|
|
#endregion
|
|
|
|
|
|
public TerminalViewModel(IAppSettingsProvider appSettings, IMessenger messenger)
|
|
{
|
|
this.appSettings = appSettings;
|
|
this.messenger = messenger;
|
|
this.displayNewDataTimer = new RecurringTimer(displayNewDataTimerIntervalMs, delegate() { this.UpdateDisplayedData(); });
|
|
this.displayNewDataTimer.Start();
|
|
}
|
|
|
|
~TerminalViewModel()
|
|
{
|
|
this.displayNewDataTimer?.Stop();
|
|
|
|
if (this.CommunicationProtocol != null)
|
|
{
|
|
// disconnect communication protocol
|
|
this.OnViewModelRequestedDisconnect(this, EventArgs.Empty);
|
|
// unsubscribe events
|
|
this.CommunicationProtocol.ReceivedDataEvent -= CommunicationProtocol_ReceivedDataEvent;
|
|
this.CommunicationProtocol.SentDataEvent -= CommunicationProtocol_SentDataEvent;
|
|
this.CommunicationProtocol.ConnectionStateChangedEvent -= CommunicationProtocol_ConnectionStateChangedEvent;
|
|
// remove reference to object
|
|
this.CommunicationProtocol = null;
|
|
}
|
|
}
|
|
|
|
#region Communication Protocol
|
|
partial void OnCommunicationProtocolChanged(ICommunicationProtocol? value)
|
|
{
|
|
this.InitializeCommunicationProtocol();
|
|
}
|
|
|
|
private void InitializeCommunicationProtocol()
|
|
{
|
|
if (this.CommunicationProtocol != null)
|
|
{
|
|
this.CommunicationProtocol.ReceivedDataEvent += CommunicationProtocol_ReceivedDataEvent;
|
|
this.CommunicationProtocol.SentDataEvent += CommunicationProtocol_SentDataEvent;
|
|
this.CommunicationProtocol.ConnectionStateChangedEvent += CommunicationProtocol_ConnectionStateChangedEvent;
|
|
}
|
|
|
|
// initializes default newline separators, updates them directly
|
|
this.InitializeNewlineSeparatorsFromAppSettings();
|
|
}
|
|
|
|
private void CommunicationProtocol_ConnectionStateChangedEvent(object? sender, ProtocolConnectionState e)
|
|
{
|
|
// update internal state
|
|
this.CommunicationProtocolConnectionState = e;
|
|
this.CommunicationProtocolLongIdentifier = this.CommunicationProtocol!.LongInstanceIdentifier;
|
|
|
|
// If ViewModel still displays connected after unintentional disconnect => force to disconnected state
|
|
// allows user to change settings
|
|
if (e == ProtocolConnectionState.UnintentionallyDisconnected && this.ProtocolSettings!.DisplaysConnected == true)
|
|
{
|
|
this.ProtocolSettings.ForceConnectedState(false);
|
|
}
|
|
}
|
|
|
|
#region Communication Protocol Data Handling
|
|
/// <summary>
|
|
/// Add newly received data to display.
|
|
/// </summary>
|
|
public abstract void DisplayNewReceivedData(IEnumerable<ExtendedByte> newData);
|
|
/// <summary>
|
|
/// Add newly sent data to display.
|
|
/// </summary>
|
|
public abstract void DisplayNewSentData(IEnumerable<ExtendedByte> newData);
|
|
|
|
private void CommunicationProtocol_ReceivedDataEvent(object? sender, ReceivedDataEventArgs e)
|
|
{
|
|
this.receivedDataQueue.Enqueue(e.ReceivedData);
|
|
}
|
|
|
|
private void CommunicationProtocol_SentDataEvent(object? sender, SentDataEventArgs e)
|
|
{
|
|
this.sentDataQueue.Enqueue(e.SentData);
|
|
}
|
|
|
|
private async void UpdateDisplayedData()
|
|
{
|
|
// display new data in seperate tasks
|
|
if(this.receivedDataQueue.TryDequeue(out var newReceivedData))
|
|
{
|
|
await Task.Run(() => this.DisplayNewReceivedData(newReceivedData));
|
|
}
|
|
if(this.sentDataQueue.TryDequeue(out var newSentData))
|
|
{
|
|
await Task.Run(() => this.DisplayNewSentData(newSentData));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Sends the given data to this instances <see cref="CommunicationProtocol"/>.
|
|
/// Appends configured <see cref="SendNewlineSeparatorType"/> to the end of string.
|
|
/// </summary>
|
|
/// <param name="data">data in form of enumerable (e.g. array)</param>
|
|
/// <returns>when data could be sent successfully</returns>
|
|
protected bool SendToCommunicationProtocol(IEnumerable<byte> data)
|
|
{
|
|
// guard null values
|
|
if(this.CommunicationProtocol == null) { throw new NullReferenceException($"'{nameof(SendToCommunicationProtocol)}()' was called but {nameof(CommunicationProtocol)} is null."); }
|
|
|
|
// inform user and quit if communication protocol is not connected
|
|
if (this.CommunicationProtocol.State != ProtocolConnectionState.Connected)
|
|
{
|
|
this.messenger.Send<IUserInterfaceMessage>(new ProtocolNotConnectedUIMessage("Cannot send message."));
|
|
return false;
|
|
}
|
|
|
|
// add newline sequence to end of data
|
|
byte[] newlineSequence = this.SendNewlineSeparatorType switch
|
|
{
|
|
NewlineSeparatorType.None => Array.Empty<byte>(),
|
|
NewlineSeparatorType.CR => new byte[] { (byte)'\r' },
|
|
NewlineSeparatorType.LF => new byte[] { (byte)'\n' },
|
|
NewlineSeparatorType.CR_LF => new byte[] { (byte)'\r', (byte)'\n' },
|
|
_ => throw new NotImplementedException($"'{nameof(SendToCommunicationProtocol)}()' " +
|
|
$"does not implement handling for {nameof(NewlineSeparatorType)} {this.SendNewlineSeparatorType}.")
|
|
};
|
|
|
|
// join data and newline sequence
|
|
var dataWithNewlineSequence = data.Concat(newlineSequence);
|
|
|
|
// send
|
|
return this.CommunicationProtocol.SendBytes(dataWithNewlineSequence.ToArray());
|
|
}
|
|
|
|
#region Protocol Settings
|
|
partial void OnProtocolSettingsChanged(IProtocolSettingsViewModel? value)
|
|
{
|
|
// register event handler for connection request from viewmodel
|
|
if (value != null)
|
|
{
|
|
this.ProtocolSettings!.ConnectRequested += OnViewModelRequestedConnect;
|
|
this.ProtocolSettings!.DisconnectRequested += OnViewModelRequestedDisconnect;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Connect/Disconnect
|
|
private void OnViewModelRequestedConnect(object? sender, ConnectionRequestEventArgs e)
|
|
{
|
|
// guard uninitialized CommunicationProtocol
|
|
if (CommunicationProtocol == null) { throw new Exception($"To call '{nameof(OnViewModelRequestedConnect)}()', CommunicationProtocol must not be null!"); }
|
|
|
|
e.Success = this.CommunicationProtocol.Connect(e.Settings);
|
|
this.UpdateTitle();
|
|
}
|
|
|
|
private void OnViewModelRequestedDisconnect(object? sender, EventArgs e)
|
|
{
|
|
// guard uninitialized CommunicationProtocol
|
|
if (CommunicationProtocol == null) { throw new Exception($"To call '{nameof(OnViewModelRequestedConnect)}()', CommunicationProtocol must not be null!"); }
|
|
|
|
this.CommunicationProtocol.Disconnect();
|
|
}
|
|
#endregion
|
|
|
|
#region Closing handling
|
|
public event EventHandler? ClosingEvent;
|
|
|
|
/// <summary>
|
|
/// Method to override if any closing actions are required.
|
|
/// Closing can be cancelled using the return value.
|
|
/// </summary>
|
|
/// <returns>true if closing is allowed. false if it shall be cancelled.</returns>
|
|
protected virtual bool ClosingActions() { return true; }
|
|
|
|
[RelayCommand]
|
|
public void CloseRequest()
|
|
{
|
|
// run internal closing actions check if closing is allowed
|
|
if (this.ClosingActions() == true)
|
|
{
|
|
// disconnect communication protocol
|
|
this.OnViewModelRequestedDisconnect(this, EventArgs.Empty);
|
|
// raise event
|
|
ClosingEvent?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
public void ForceClose()
|
|
{
|
|
// internal closing options, ignore cancellation
|
|
this.ClosingActions();
|
|
// disconnect communication protocol
|
|
this.OnViewModelRequestedDisconnect(this, EventArgs.Empty);
|
|
// do not raise closing event, since caller knows about closing
|
|
}
|
|
#endregion
|
|
|
|
#region Newline Separator handling
|
|
/// <summary>
|
|
/// Gets the default settings for both newline separators from the AppSettings Provider.
|
|
/// Writes them to the local variables <see cref="DataDisplayNewlineSeparatorType"/> and <see cref="SendNewlineSeparatorType"/>.
|
|
/// In case there are no default settings, it does not overwrite the local variables.
|
|
/// </summary>
|
|
private void InitializeNewlineSeparatorsFromAppSettings()
|
|
{
|
|
// get newline separators from persistent settings, if not available catch exceptions. apply to displayed data variable.
|
|
this.appSettings.TryReadSetting(defaultReceiveNewlineSeparatorAppSettingsKey, out string settingValueReceiveNLSep);
|
|
try
|
|
{
|
|
this.DataDisplayNewlineSeparatorType = EnumHelpers.ParseEnum<NewlineSeparatorType>(settingValueReceiveNLSep);
|
|
}
|
|
catch { }
|
|
|
|
// apply to send newline separator
|
|
this.appSettings.TryReadSetting(defaultSendNewlineSeparatorAppSettingsKey, out string settingValueSendNLSep);
|
|
try
|
|
{
|
|
this.SendNewlineSeparatorType = EnumHelpers.ParseEnum<NewlineSeparatorType>(settingValueSendNLSep);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Act when DisplayData NewlineSeparator Changed.
|
|
/// For example rearrange data according to new newline separator.
|
|
/// </summary>
|
|
/// <param name="newType"></param>
|
|
protected abstract void ActOnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType newType);
|
|
|
|
partial void OnDataDisplayNewlineSeparatorTypeChanged(NewlineSeparatorType value)
|
|
{
|
|
// inform derivatives
|
|
this.ActOnDataDisplayNewlineSeparatorTypeChanged(value);
|
|
}
|
|
#endregion
|
|
|
|
#region History
|
|
/// <summary>
|
|
/// Creates a clone of the passed element and saves that to the <see cref="SentMessagesHistory"/>.
|
|
/// </summary>
|
|
/// <param name="element">element to add to the history</param>
|
|
protected void AddToSendHistory(IHistoryElement element)
|
|
{
|
|
// shallow copy of element (so that element can be changed externally)
|
|
var copy = (IHistoryElement)element.Clone();
|
|
|
|
// add to list
|
|
this.SentMessagesHistory.Add(copy);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shall handle the insertion of an element from the <see cref="SentMessagesHistory"/>
|
|
/// into the current sendable message area.
|
|
/// </summary>
|
|
/// <param name="element">copy of element as object</param>
|
|
protected abstract void HandleInsertElementFromHistory(object element);
|
|
|
|
/// <summary>
|
|
/// Allows to insert an element from the history into the current send message.
|
|
/// Creates a copy of the object, so it can be changed externally.
|
|
/// </summary>
|
|
[RelayCommand]
|
|
public void InsertElementFromHistory(IHistoryElement element)
|
|
{
|
|
// shallow copy of element (so that element can be changed externally)
|
|
var copy = (IHistoryElement)element.Clone();
|
|
|
|
// let inheriting classes handle it
|
|
this.HandleInsertElementFromHistory(copy);
|
|
}
|
|
#endregion
|
|
}
|
|
|