Multi Connection Listener on the .Net Communication Driver
You are probably saying to yourself "Hey, you promised that the next episode of 'Build your own SD Card Explorer' will be published, so how come Saragani posts about something else?"Well, I actually said that the next episode will be posted next week :-)It is going to have a lot of code and it will be a lot of work writing it so I might split it to few pieces.Besides, I can use this blog entry to post about new features of the Com Driver.I wrote this multi listener a while ago, but the source code of the communication driver was not published since then.You might assume that writing the Multi-Listener was harder than writing the single listener, but it is actually the opposite..Net gives you an easy way to write a multi-listener and for having a single listener I had to manipulate the system to have just 1 connection (Lets say that in order to stop listening, I had to close the port, but then it caused the socket to be disposed so I had to create a new instance... and events from other threads and some other stuff sometimes caused Threads races).We didn't write the Multi Listener on the first place since we actually needed a single socket listener for the apps that use the communication driver, and we didn't extend the single listener to to multi listener because it will break the compatibility.Lets start:There are 2 new channels: ListenerServer and ListenerClient (Where ListenerClient have an internal constructor, cause it can only be created by the ListenerServer).ListenerServer is just like the EthernetListener, but it allows multiple connections (It means that several PLCs can connect to the PC on the same time and to the same port).Unlike the EthernetListener, the ListenerServer does not contain any logic for sending and receiving data. When communication is being accepted, the ListenerServer creates a new ListenerClient. If it manages to create a PLC object then it will be returned to the user by an event. The Plc will contain the ListenerClient.Few notes:1) The ListenerServer contains only 2 types of statuses: Listening and Disconnected (“Connected†is irrelevant since it is a property of the Client…)2) When a PLC object that uses a ListenerClient channel have the connection closed, the PLC object can’t be used anymore (It is being disposed).3) In the EthernetListener (The old Single connection Listener) calling PLC.Disconnect or ethernetListener.Disconnect closes the client connection. With the new ListenerServer PLC.Disconnect closes the connection of the specific PLC (Closes a specific client), and ListenerServer.Disconnect closes the Listen (The status changes from Listening to Disconnected).4) Just like before, if the Message Queue is not empty then Plc.Disconnect doesn’t close the connection. When using ListenerServer/ListenerClient, you can call PLC.Dispose which closes the connection no matter how many messages are in the queue.a. It is a good practice to Dispose a PLC if the ListenerClient is no longer needed/In use (To close unused sockets). b. When the PLC object has no references then it will be collected by the GC and therefore the ListenerClient will also be collected by the GC. The destructor of the ListenerClient currently does not contain any “Socket.Close†(This might change in the future). I believe that the socket will be closed when the channel will be collected by the GC, but you can never tell when the GC will decide to run.c. Dispose a PLC only when it’s ListenerClient is no longer needed. For example:
PLC plc; PLC plc2; private void Listen() { try { PLCFactory.GetChannel(LOCAL_PORT, out listenerServer); if (listenerServer == null) listenerServer = new ListenerServer(LOCAL_PORT, 3, 3000); listenerServer.OnConnectionAccepted += new ListenerServer.ConnectionAcceptedDelegate(listenerServer_OnConnectionAccepted); listenerServer.OnConnectionStatusChanged += new ListenerServer.ConnectionStatusChangedDelegate(listenerServer_OnConnectionStatusChanged); PLCFactory.GetPLC(listenerServer); listening = true; } catch (Exception ex) { MessageBox.Show(ex.Message); } } void listenerServer_OnConnectionAccepted(PLC plcFromListener) { plc = plcFromListener; plc2 = PLCFactory.GetPLC((plcFromListener.PLCChannel as ListenerClient),2); // Get the PLC on Unit ID 2 (Assuming that plcFromListener is Unit ID 1. plcFromListener.Dispose(); // This will also cause plc2 to disconnect cause plc2 is using the same resource (The same ListenerClient) }
However, if plc was created from one from of the OnConnectionAccepted event calls and plc2 was created from a different call of OnConnectionAccepted then those 2 PLC object will have different ListenerClient object, so disposing 1 PLC does not affect the other one.As I'm a fan of WPF and Binding, then I will write the example in WPF. It doesn't affect how you use the Com Driver, but it makes my life easier when the UI doesn't have any Business Logic.If I also change properties from another thread, then the UI will not crash since OnPropertyChanged protects me (it uses the Dispatcher in order to use the UI thread).If you are using WinForms then you will have to use this.BeginInvoke on the form etc...Lets start an MVVM solution.Since we support several clients then we will need a class that represents a single client (And we will have a list of all the clients in order to keep tracking on them and disconnect them if needed).I'll call this class ClientViewModel and it will inherit the ViewModelBase.
public class ClientViewModel : ViewModelBase { }
The Client will need to contain the PLC Object, we will want to show the IP of the client, the PLC Model, the OS Version and a value of an Operand (Lets take SDW 0 since it has an Auto Increment).I'll make it simple, I will have all those properties (Except the PLC object) as Strings).I also need a Timer that will Tick every once in a while and request the Value of SDW 0.
public class ClientViewModel : ViewModelBase { private PLC plc; private string clientIP; private string plcModel; private string osVersion; private string plcName; private string sdw0; Timer timer; }
What I also need is a way to Disconnect, Run, Stop, Restart etc the PLC so I will use a DelegateCommand which is a class in the MVVM template that inherits from the ICommand Interface.I also need a way to tell the main class (MainViewModel) that the Client has disconnected, so I will use an Even in this case:
public class ClientViewModel : ViewModelBase { private PLC plc; private string clientIP; private string plcModel; private string osVersion; private string plcName; private string sdw0; Timer timer; private DelegateCommand disconnectClientCommand; private DelegateCommand resetPlcCommand; private DelegateCommand stopPlcCommand; private DelegateCommand runPlcCommand; public event EventHandler OnConnectionClosed;
Now lets start adding the code to this class:
public class ClientViewModel : ViewModelBase { private PLC plc; private string clientIP; private string plcModel; private string osVersion; private string plcName; private string sdw0; Timer timer; private DelegateCommand disconnectClientCommand; private DelegateCommand resetPlcCommand; private DelegateCommand stopPlcCommand; private DelegateCommand runPlcCommand; public event EventHandler OnConnectionClosed; public ClientViewModel(PLC plcFromListener) { plc = plcFromListener; (plc.PLCChannel as ListenerClient).OnConnectionClosed += new ListenerClient.ConnectionClosedDelegate(ClientViewModel_OnConnectionClosed); ClientIP = (plc.PLCChannel as ListenerClient).RemoteIP; PlcVersion version = plc.Version; PlcModel = version.OPLCModel; OsVersion = version.OSVersion + "(" + version.OSBuildNum + ")"; Action action = delegate() { PlcName = plc.PlcName; }; action.BeginInvoke(null, null); timer = new Timer(100); timer.Elapsed += new ElapsedEventHandler(timer_Elapsed); timer.Start(); } void ClientViewModel_OnConnectionClosed(ListenerClient ethernetListener) { if (timer != null) timer.Stop(); if (OnConnectionClosed != null) OnConnectionClosed(this, null); } public string ClientIP { get { return clientIP; } internal set { clientIP = value; OnPropertyChanged("ClientIP"); } } public string PlcModel { get { return plcModel; } internal set { plcModel = value; OnPropertyChanged("PlcModel"); } } public string OsVersion { get { return osVersion; } internal set { osVersion = value; OnPropertyChanged("OsVersion"); } } public string PlcName { get { return plcName; } internal set { plcName = value; OnPropertyChanged("PlcName"); } } public string Sdw0 { get { return sdw0; } internal set { sdw0 = value; OnPropertyChanged("Sdw0"); } } public ICommand DisconnectClientCommand { get { if (disconnectClientCommand == null) { disconnectClientCommand = new DelegateCommand(disconnectClient); } return disconnectClientCommand; } } private void disconnectClient() { try { // When using a Listener Server, it is important to close the connection to the PLC // if it's not needed anymore. plc.Disconnect closes the connection only if there are no // pending messages in the queue (This is why it is important to abort the communication before disconnecting). // However, plc.Dispose will allow you to close the connection anyway (if other PLC objects use the same connection // which is the same ListenerClient) then they will become diposed as well. // Dispose method is only relevant to ListenerClient connections. plc.Dispose(); } catch { } } public ICommand ResetPlcCommand { get { if (resetPlcCommand == null) { resetPlcCommand = new DelegateCommand(resetPlc); } return resetPlcCommand; } } private void resetPlc() { try { // Reseting the PLC will cause the connection to be closed. // If the PLC is connected directly to the PC using a cable then the PC should get a disconnection event. // However, if there is a hub in the middle the PC will get the disconnection event only after it tries to communicate with the PLC. // In this example we read SDW 0 every 100ms which will cause the connection to be "tested" just like mentioned above. plc.Reset(); } catch { } } public ICommand StopPlcCommand { get { if (stopPlcCommand == null) { stopPlcCommand = new DelegateCommand(stopPlc); } return stopPlcCommand; } } private void stopPlc() { try { plc.Stop(); } catch { } } public ICommand RunPlcCommand { get { if (runPlcCommand == null) { runPlcCommand = new DelegateCommand(runPlc); } return runPlcCommand; } } private void runPlc() { try { plc.Run(); } catch { } } void timer_Elapsed(object sender, ElapsedEventArgs e) { try { ReadWriteRequest[] rw = new ReadWriteRequest[1]; rw[0] = new ReadOperands() { NumberOfOperands = 1, OperandType = OperandTypes.SDW, StartAddress = 0, }; plc.ReadWrite(ref rw); Sdw0 = ((rw[0].ResponseValues as object[])[0]).ToString(); } catch (ComDriveExceptions cde) { if (cde.ErrorCode == ComDriveExceptions.ComDriveException.ObjectDisposed) { plc.Dispose(); } } catch (Exception ex) { } } }
We are done with the ClientViewModel... Now we can start with the MainViewModel.We want a list of ClientViewModels, so we will use an ObservableCollection that can be easily binded to a ListBox or any ItemsContainer:
public class MainViewModel : ViewModelBase { private ObservableCollection<ClientViewModel> clients = new ObservableCollection<ClientViewModel>(); }
We need an option of the UI to tell the MainViewModel to Start or Stop Listening. We will again use a DelegateCommand.
public class MainViewModel : ViewModelBase { private ObservableCollection<ClientViewModel> clients = new ObservableCollection<ClientViewModel>(); private DelegateCommand exitCommand; private DelegateCommand listenCommand; private DelegateCommand disconnectCommand; }
We also want a boolean that will tell the UI if we are Listening or not, we need a reference to the ListenerServer (The Server Channel that we have created) in order to tell it to Start Listening or Stop Listening.I'm going to have the port that we listen to as a Const:private const int LOCAL_PORT = 20275;I also need a Status (Ready, Listening, etc). Lets make it a String.And just one last thing:Observable Connection isn't thread safe, so if the collection is binded to the UI and we alter it from a different thread then we will get an Excetion.Therefore, we need a Dispatcher.The code is:
public class MainViewModel : ViewModelBase { private ObservableCollection<ClientViewModel> clients = new ObservableCollection<ClientViewModel>(); private DelegateCommand exitCommand; private DelegateCommand listenCommand; private DelegateCommand disconnectCommand; private bool listening = false; private ListenerServer listenerServer; private const int LOCAL_PORT = 20275; private string status; private Dispatcher dispatcher; #region Constructor public MainViewModel() { Status = "Ready"; dispatcher = Dispatcher.CurrentDispatcher; } #endregion }
Lets start addting some properties (Public properties with OnPropertyChanged:
public ObservableCollection<ClientViewModel> Clients { get { return clients; } internal set { clients = value; OnPropertyChanged("Clients"); } } public string Status { get { return status; } internal set { status = value; OnPropertyChanged("Status"); } }
We also need to add the ICommand properties we make the program Exit, Listen or Disconnect (Close the Listener).
public ICommand ExitCommand { get { if (exitCommand == null) { exitCommand = new DelegateCommand(Exit); } return exitCommand; } } private void Exit() { Application.Current.Shutdown(); } public ICommand ListenCommand { get { if (listenCommand == null) { listenCommand = new DelegateCommand(Listen, canListen); } return listenCommand; } } private bool canListen() { return !listening; } private void Listen() { try { PLCFactory.GetChannel(LOCAL_PORT, out listenerServer); if (listenerServer == null) listenerServer = new ListenerServer(LOCAL_PORT, 3, 3000); listenerServer.OnConnectionAccepted += new ListenerServer.ConnectionAcceptedDelegate(listenerServer_OnConnectionAccepted); listenerServer.OnConnectionStatusChanged += new ListenerServer.ConnectionStatusChangedDelegate(listenerServer_OnConnectionStatusChanged); PLCFactory.GetPLC(listenerServer); listening = true; } catch (Exception ex) { MessageBox.Show(ex.Message); } } void listenerServer_OnConnectionStatusChanged(EthernetListener.ConnectionStatus connectionStatus) { // You can also use a timer to scan the status every, lets say, 1 second. updateStatus(connectionStatus); } private void updateStatus(EthernetListener.ConnectionStatus connectionStatus) { string text = ""; switch (connectionStatus) { case EthernetListener.ConnectionStatus.Listening: text = "Listening, Connected Clients: " + getNumberOfClients().ToString(); break; case EthernetListener.ConnectionStatus.Disconnected: text = "Disconnected, Connected Clients: " + getNumberOfClients().ToString(); break; } Status = text; } void listenerServer_OnConnectionAccepted(PLC plcFromListener) { // This event is not raised from the GUIs thread. Therefore we need to invoke it on the // Main thread using the dispatcher. It is important to create the viewmodels in the GUI threa // cause we want the dispatcher of the viewmodels to be associated with the right thread. Action action = delegate() { ClientViewModel clientViewModel = new ClientViewModel(plcFromListener); clientViewModel.OnConnectionClosed += new EventHandler(clientViewModel_OnConnectionClosed); clients.Add(clientViewModel); updateStatus(listenerServer.Status); refreshIndexes(); }; dispatcher.Invoke(action, null); } void clientViewModel_OnConnectionClosed(object sender, EventArgs e) { try { Action action = delegate() { clients.Remove(sender as ClientViewModel); updateStatus(listenerServer.Status); refreshIndexes(); }; dispatcher.Invoke(action, null); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void refreshIndexes() { ICollectionView view = CollectionViewSource.GetDefaultView(Clients) as ListCollectionView; view.Refresh(); } public ICommand DisconnectCommand { get { if (disconnectCommand == null) { disconnectCommand = new DelegateCommand(Disconnect, canDisconnect); } return disconnectCommand; } } private bool canDisconnect() { return listening; } private void Disconnect() { if (listenerServer != null) { try { listenerServer.Disconnect(); listenerServer.OnConnectionAccepted -= new ListenerServer.ConnectionAcceptedDelegate(listenerServer_OnConnectionAccepted); listening = false; } catch (Exception ex) { MessageBox.Show(ex.Message); } } } private int getNumberOfClients() { int result = 0; Details detail = ListenerClientsInfo.GetDetails(LOCAL_PORT); if (detail != null) { result = detail.Count; } return result; }
This ends the MainViewModel code... The compelte MainViewModel Code is:
public class MainViewModel : ViewModelBase { private ObservableCollection<ClientViewModel> clients = new ObservableCollection<ClientViewModel>(); private DelegateCommand exitCommand; private DelegateCommand listenCommand; private DelegateCommand disconnectCommand; private bool listening = false; private ListenerServer listenerServer; private const int LOCAL_PORT = 20275; private string status; private Dispatcher dispatcher; #region Constructor public MainViewModel() { Status = "Ready"; dispatcher = Dispatcher.CurrentDispatcher; } #endregion public ObservableCollection<ClientViewModel> Clients { get { return clients; } internal set { clients = value; OnPropertyChanged("Clients"); } } public string Status { get { return status; } internal set { status = value; OnPropertyChanged("Status"); } } public ICommand ExitCommand { get { if (exitCommand == null) { exitCommand = new DelegateCommand(Exit); } return exitCommand; } } private void Exit() { Application.Current.Shutdown(); } public ICommand ListenCommand { get { if (listenCommand == null) { listenCommand = new DelegateCommand(Listen, canListen); } return listenCommand; } } private bool canListen() { return !listening; } private void Listen() { try { PLCFactory.GetChannel(LOCAL_PORT, out listenerServer); if (listenerServer == null) listenerServer = new ListenerServer(LOCAL_PORT, 3, 3000); listenerServer.OnConnectionAccepted += new ListenerServer.ConnectionAcceptedDelegate(listenerServer_OnConnectionAccepted); listenerServer.OnConnectionStatusChanged += new ListenerServer.ConnectionStatusChangedDelegate(listenerServer_OnConnectionStatusChanged); PLCFactory.GetPLC(listenerServer); listening = true; } catch (Exception ex) { MessageBox.Show(ex.Message); } } void listenerServer_OnConnectionStatusChanged(EthernetListener.ConnectionStatus connectionStatus) { // You can also use a timer to scan the status every, lets say, 1 second. updateStatus(connectionStatus); } private void updateStatus(EthernetListener.ConnectionStatus connectionStatus) { string text = ""; switch (connectionStatus) { case EthernetListener.ConnectionStatus.Listening: text = "Listening, Connected Clients: " + getNumberOfClients().ToString(); break; case EthernetListener.ConnectionStatus.Disconnected: text = "Disconnected, Connected Clients: " + getNumberOfClients().ToString(); break; } Status = text; } void listenerServer_OnConnectionAccepted(PLC plcFromListener) { // This event is not raised from the GUIs thread. Therefore we need to invoke it on the // Main thread using the dispatcher. It is important to create the viewmodels in the GUI threa // cause we want the dispatcher of the viewmodels to be associated with the right thread. Action action = delegate() { ClientViewModel clientViewModel = new ClientViewModel(plcFromListener); clientViewModel.OnConnectionClosed += new EventHandler(clientViewModel_OnConnectionClosed); clients.Add(clientViewModel); updateStatus(listenerServer.Status); refreshIndexes(); }; dispatcher.Invoke(action, null); } void clientViewModel_OnConnectionClosed(object sender, EventArgs e) { try { Action action = delegate() { clients.Remove(sender as ClientViewModel); updateStatus(listenerServer.Status); refreshIndexes(); }; dispatcher.Invoke(action, null); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void refreshIndexes() { ICollectionView view = CollectionViewSource.GetDefaultView(Clients) as ListCollectionView; view.Refresh(); } public ICommand DisconnectCommand { get { if (disconnectCommand == null) { disconnectCommand = new DelegateCommand(Disconnect, canDisconnect); } return disconnectCommand; } } private bool canDisconnect() { return listening; } private void Disconnect() { if (listenerServer != null) { try { listenerServer.Disconnect(); listenerServer.OnConnectionAccepted -= new ListenerServer.ConnectionAcceptedDelegate(listenerServer_OnConnectionAccepted); listening = false; } catch (Exception ex) { MessageBox.Show(ex.Message); } } } private int getNumberOfClients() { int result = 0; Details detail = ListenerClientsInfo.GetDetails(LOCAL_PORT); if (detail != null) { result = detail.Count; } return result; } }
Now it is time to design the UI :-)The MainView which is a Window that comes with the MVVM Template and it contains Key Binding to the Exit Command (So we can do a Ctr+X).Beside that we also need a Converter so the items in the Listbox will have an Index.
class PositionConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { ListBoxItem item = value as ListBoxItem; ListBox view = ItemsControl.ItemsControlFromItemContainer(item) as ListBox; int index = view.ItemContainerGenerator.IndexFromContainer(item); return index.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new Exception("The method or operation is not implemented."); } }
The UI Xaml Is:
<Window x:Class="Listener_Server_Example.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:Listener_Server_Example.Commands" xmlns:local="clr-namespace:Listener_Server_Example" Title="Main Window" Height="400" Width="800" WindowState="Maximized"> <Window.Resources> <!-- Allows a KeyBinding to be associated with a command defined in the View Model --> <c:CommandReference x:Key="ExitCommandReference" Command="{Binding ExitCommand}" /> <local:PositionConverter x:Key="positionConverter"/> </Window.Resources> <Window.InputBindings> <KeyBinding Key="X" Modifiers="Control" Command="{StaticResource ExitCommandReference}" /> </Window.InputBindings> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Command="{Binding ExitCommand}" Header="E_xit" InputGestureText="Ctrl-X" /> </MenuItem> </Menu> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ListView SelectionMode="Single" Grid.Row="0" ItemsSource="{Binding Clients}" x:Name="listView1"> <ListView.View> <GridView AllowsColumnReorder="False"> <GridViewColumn Header="Index" DisplayMemberBinding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBoxItem}, Converter={StaticResource positionConverter}}" Width="50" /> <GridViewColumn Header="Client IP" DisplayMemberBinding="{Binding ClientIP}" Width="Auto"/> <GridViewColumn Header="PLC Model" DisplayMemberBinding="{Binding PlcModel}" Width="Auto"/> <GridViewColumn Header="OS Version" DisplayMemberBinding="{Binding OsVersion}" Width="Auto"/> <GridViewColumn Header="PLC Name" DisplayMemberBinding="{Binding PlcName}" Width="Auto"/> <GridViewColumn Header="SDW 0" DisplayMemberBinding="{Binding Sdw0}" Width="Auto"/> <GridViewColumn Header="Actions" Width="Auto"> <GridViewColumn.CellTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Button Content="Disconnect Client" ToolTip="Closes the connection to the PLC" Command="{Binding DisconnectClientCommand}" Margin="2" /> <Button Content="Reset PLC" ToolTip="Reset PLC" Command="{Binding ResetPlcCommand}" Margin="2"/> <Button Content="Stop PLC" ToolTip="Stop PLC" Command="{Binding StopPlcCommand}" Margin="2" /> <Button Content="Run PLC" ToolTip="Run PLC" Command="{Binding RunPlcCommand}" Margin="2"/> </StackPanel> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView> <StackPanel Orientation="Horizontal" Grid.Row="1"> <Button Content="Listen" Margin="2" ToolTip="Start Listening to port 20275" Command="{Binding ListenCommand}"/> <Button Content="Disconnect" Margin="2" ToolTip="Stop Listening to port 20275. Connected clients will still be connected." Command="{Binding DisconnectCommand}"/> </StackPanel> <StatusBar Grid.Row="2"> <StackPanel Orientation="Horizontal"> <TextBlock Text="Status: " Margin="2"/> <TextBlock Text="{Binding Status}" Margin="2"/> </StackPanel> </StatusBar> </Grid> </DockPanel></Window>
We have a ListView with several columns and a DataTemplate With Disconnect, Reset, etc buttons.I've attached the complete solution.If you find bugs or have suggestions to improve stuff then please don’t hesitate to contact me.Thank you.
0 Comments
Recommended Comments
There are no comments to display.
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now