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.
- Read more...
- 0 comments
- 2,260 views