wpf 使用EasyModbusTCP库创建从PLC提取数据的服务

emeijp43  于 12个月前  发布在  其他
关注(0)|答案(1)|浏览(350)

首先-我不是程序员,几个月前才开始学习。
我没有创建一个WPF程序使用EasyModbusTCP库的工作!甚至设法读取Modbus异步,所以它不冻结视图。
现在需要扩展这个程序,从modbus中提取超过600个线圈和寄存器。沿着,我在CommunityToolkit的帮助下转移到MVVM模式。
我不知道,老实说,我被如何在ViewModel中创建服务所淹没。
应用程序现在是复杂的显示完整的代码,所以我会尽可能简化。我的视图是具有ItemsControl的UserControl,另一个UserControl的集合作为ItemsSource。
ConductivityView XAML:

<ItemsControl Grid.Row="2" ItemsSource="{Binding ConductivityTanks}">
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type vm:ConductivityGridElementViewModel}">
                <controls:ConductivityGridElement DataContext="{Binding}"></controls:ConductivityGridElement>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

CodeBehind:

public partial class BConductivityView : UserControl
{
    ConductivityViewModel conductivityViewModel;
    public BConductivityView()
    {
        InitializeComponent();
        conductivityViewModel = new ConductivityViewModel();
        this.DataContext = conductivityViewModel;
    }
}

在我的ConductivityViewModel中,我需要插入未来的Modbus拉取服务并使其成为可扩展的。

public partial class ConductivityViewModel : ObservableObject
{ 
    [ObservableProperty]
    private ObservableCollection<ConductivityGridElementViewModel> _conductivityTanks;

    public ConductivityViewModel()
    {
        _conductivityTanks = new ObservableCollection<ConductivityGridElementViewModel>();

        ConductivityTanks.Clear();
        //Add some initial dummy data to display
        for (int i = 0; i <= 32; i++)
        {
            ConductivityTanks.Add(new ConductivityGridElementViewModel
            {
                Id = i.ToString(),
                Use = true,
                TankNo = i.ToString(),
                Sp = (1000 + i).ToString(),
                Pv = (1500 + i).ToString(),
                Valve = false,
                TimeLeft = (4 + i).ToString(),
                Timer = (5).ToString(),
                SensorMinimum = "0",
                SensorMaximum = "10000",
                Raw = "1670"
            });
        }

        public void ModbusReadAsync()
        {
             // Make async call to start modbus pull and update all ConductivityGridElement in ConductivityTanks
        }
    }
}

我的Modbus服务类读取PLC并在静态类中分配静态字段,所有这些看起来像这样:

public static class ModbusService
{
    public static CancellationTokenSource cts = new CancellationTokenSource();
    public static ModbusClient MB = new ModbusClient();
    private static bool[] coils1 = new bool[120];
    private static bool[] coils2 = new bool[40];
    private static int[] registers1 = new int[121];
    private static int[] registers2 = new int[121];
    private static int[] registers3 = new int[121];

    public static void ConnectToPLC(string IP, int Port, bool ReConnect)
    {
        cts.Cancel();
        ConnectionExist = false;
        MB.Disconnect();
        MB = new ModbusClient(IP, Port);
        try
        {
            MB.Connect();
            ConnectionExist = true;
            ReadPLCAsync();
        }
        catch (Exception)
        {
            MessageBox.Show("Connection error.\r\nStart without reading PLC.");
        }
    }

    public static async void ReadPLCAsync()
    {
        await Task.Run(async () =>
        {
            await Read(cts.Token);
        });
    }

    private static async Task<bool> Read(CancellationToken cancellationToken)
    {
        while (ConnectionExist)
        {
            try
            {
                coils1 = MB.ReadCoils(0, 120);
                coils2 = MB.ReadCoils(121, 40);

                registers1 = MB.ReadHoldingRegisters(0, 122);
                registers2 = MB.ReadHoldingRegisters(122, 122);
                registers3 = MB.ReadHoldingRegisters(244, 122);

                General.CPUinRUN = coils1[0];
                General.ValveON = coils1[1];
                General.SDCardEject = coils1[2];
                General.SDCardEject = coils1[3];
                Conductivity.MasterON = coils1[4];
                Fresh.MasterON = coils1[5];
                Level.MasterON = coils1[6];
                General.SendEmail = coils1[7];

                Array.Copy(coils1, 8, Conductivity.Valve, 0, 32);
                Array.Copy(coils1, 40, Conductivity.CValveMan, 0, 32);

                Array.Copy(coils1, 72, Fresh.FValve, 0, 32);
                Fresh.RunOnSaturday = coils1[104];
                Fresh.RunOnSunday = coils1[105];
                Fresh.RunOnWeekend = coils1[106];

                Array.Copy(coils1, 107, Level.L_Alarm, 0, 10);
                Array.Copy(coils1, 117, Level.L_AlarmOFF, 0, 5);
                Array.Copy(coils2, 0, Level.L_AlarmOFF, 5, 5);
                Array.Copy(coils2, 5, Level.L_Filling, 0, 10);
                Array.Copy(coils2, 15, Level.L_InRange, 0, 10);
                Array.Copy(coils2, 25, Level.L_Valve, 0, 10);

                Conductivity.ON = coils2[35];
                Fresh.ON = coils2[36];
                Level.ON = coils2[37];
            }
            catch (ConnectionException)
            {
                ConnectionExist = false;
                MBC.MB.Disconnect();
                MessageBox.Show("Problem reading PLC from MBC class.");

            }
            await Task.Delay(1000);
        }
        return true;

    }

}

一般来说,应用程序看起来像这样

46scxncf

46scxncf1#

好吧,我会非常详细地介绍我的实现,所以它可以是有用的评论和建议更好的方法来做,因为我不是c#程序员,没有MVVM社区工具包的想法。主要问题是
1.组织将通过EasyModbus接收的数据结构。
1.使用ModBus实现异步和恒定周期性从PLC读取数据并刷新View。
1.为多个视图和相应的视图模型实现导航。
第一次导航:我所有的视图都是在单独的用户控件(HomeView,ConductivityView等)中实现的。每个视图都有自己的ViewModel类。对于导航,我创建了两个类:BaseViewModel和MainViewModel。BaseViewModel类为空,并从ObservableObject继承。MainViewModel类继承自BaseViewModel。在这里,我用[ObservableProperty]属性示例化了所有ViewModel。主视图模型:

public partial class MainViewModel : BaseViewModel

{

#region Navigation View Models 
[ObservableProperty]
HomeViewModel _homeViewModel;
[ObservableProperty]
ConductivityViewModel _conductivityViewModel;
[ObservableProperty]
PHViewModel _pHViewModel;
[ObservableProperty]
LevelViewModel _levelViewModel;
[ObservableProperty]
FreshViewModel _freshViewModel;
[ObservableProperty]
LogsViewModel _logsViewModel;
[ObservableProperty]
SettingsViewModel _settingsViewModel;
#endregion

private Timer _timer;

[ObservableProperty]
private BaseViewModel _selectedViewModel;

public MainViewModel()
{
    HomeViewModel = new HomeViewModel();
    ConductivityViewModel = new ConductivityViewModel();
    PHViewModel = new PHViewModel();
    LevelViewModel = new LevelViewModel();
    FreshViewModel = new FreshViewModel();
    LogsViewModel = new LogsViewModel();
    SettingsViewModel = new SettingsViewModel();

   

    SelectedViewModel = HomeViewModel;

    
    _timer = new Timer(UpdateModel, null, 0, 1000);
}

[RelayCommand]
public void ChangeView(string parameter)
{
    if (parameter == "Home")
    {
        SelectedViewModel = HomeViewModel;
    }
    else if (parameter == "Conductivity")
    {
        SelectedViewModel = ConductivityViewModel;

    }
    else if (parameter == "pH")
    {
        SelectedViewModel = PHViewModel;
    }
    else if (parameter == "Level")
    {
        SelectedViewModel = LevelViewModel;
    }
    else if (parameter == "Fresh")
    {
        SelectedViewModel = FreshViewModel;
    }
    else if (parameter == "Logs")
    {
        SelectedViewModel = LogsViewModel;
    }
    else if (parameter == "Settings")
    {
        SelectedViewModel = SettingsViewModel;
    }

}

在MainWindow.xaml中,我使用ListBox创建导航面板。我是using behaviors to access the "Command" property。只有一个“Home”按钮。每个页面都有多个ListBoxItem副本,并有相应的名称和路径。

<ListBox Grid.Column="0" SelectionMode="Single" x:Name="sidebar" BorderThickness="0" Background="Transparent" ScrollViewer.HorizontalScrollBarVisibility="Disabled" >
            <!-- Home -->
            <ListBoxItem x:Name="HomeLBI" IsSelected="True">
                <ListBoxItem.Resources>
                    <Style TargetType="{x:Type ListBoxItem}">
                        <Setter Property="HorizontalAlignment" Value="Left"/>
                        <Setter Property="Cursor" Value="Hand"/>
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                    <Border x:Name="back" Background="Transparent" Margin="10 6"
                                            BorderBrush="{TemplateBinding BorderBrush}"
                                            BorderThickness="{TemplateBinding BorderThickness}"
                                            CornerRadius="8" Padding="{TemplateBinding Padding}">
                                        <Grid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition Width="Auto"/>
                                                <ColumnDefinition Width="Auto"/>
                                            </Grid.ColumnDefinitions>
                                            <Path Grid.Column="0" x:Name="icon" Stretch="Uniform" Data="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" Fill="White" Height="30" Width="30"/>
                                            <TextBlock Grid.Column="1" Text="Home" Foreground="White" FontSize="22" Background="Transparent" HorizontalAlignment="Left" VerticalAlignment="Center"  Margin="20 0" />
                                        </Grid>
                                    </Border>

                                    <ControlTemplate.Triggers>

                                        <Trigger Property="IsMouseOver" Value="True">
                                            <Setter Property="Background" TargetName="back" Value="transparent"/>
                                            <Setter Property="Fill" TargetName="icon" Value="White"/>
                                            <Setter Property="Effect">
                                                <Setter.Value>
                                                    <DropShadowEffect Color="#99ff99" ShadowDepth="1" Direction="-90" BlurRadius="5"/>
                                                </Setter.Value>
                                            </Setter>
                                        </Trigger>

                                        <Trigger Property="IsSelected" Value="True">
                                            <Setter Property="Background" TargetName="back" Value="transparent"/>
                                            <Setter Property="Fill" TargetName="icon" Value="White"/>
                                            <Setter Property="Effect">
                                                <Setter.Value>
                                                    <DropShadowEffect Color="#99ff99" ShadowDepth="1" Direction="-90" BlurRadius="5"/>
                                                </Setter.Value>
                                            </Setter>
                                        </Trigger>

                                        <Trigger Property="IsSelected" Value="False">
                                            <Setter Property="Background" TargetName="back" Value="transparent"/>
                                            <Setter Property="Fill" TargetName="icon" Value="#FFB1B1B1"/>
                                        </Trigger>

                                    </ControlTemplate.Triggers>

                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </ListBoxItem.Resources>
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Selected" SourceObject="{Binding ElementName=HomeLBI}">
                        <i:InvokeCommandAction Command="{Binding ChangeViewCommand}" CommandParameter="Home"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </ListBoxItem>
        </ListBox>

为了使应用程序能够识别每个页面上将要使用的数据类型,我使用DataTemplates创建了ResourceDictionary,并将其引入App. xaml。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation".......>

<DataTemplate DataType="{x:Type viewmodel:HomeViewModel}">
    <view:AHomeView/>
</DataTemplate>

<DataTemplate DataType="{x:Type viewmodel:ConductivityViewModel}">
    <view:BConductivityView/>
</DataTemplate>

<DataTemplate DataType="{x:Type viewmodel:PHViewModel}">
    <view:CpHView/>
</DataTemplate>

<DataTemplate DataType="{x:Type viewmodel:LevelViewModel}">
    <view:DAutoLevelView/>
</DataTemplate>

<DataTemplate DataType="{x:Type viewmodel:FreshViewModel}">
    <view:EFreshAddView/>
</DataTemplate>

<DataTemplate DataType="{x:Type viewmodel:LogsViewModel}">
    <view:FLogsView/>
</DataTemplate>

<DataTemplate DataType="{x:Type viewmodel:SettingsViewModel}">
    <view:GSettingsView/>
</DataTemplate> </ResourceDictionary>

这最后一步就像魔术一样(魔术,因为当时感觉就像)。这是导航。
我最大的挑战是创建MVVM的模型部分。对于像用Modbus异步读取PLC这样的任务,没有太多的信息。所以我必须要有创造力。我读到的每一篇关于模型的文章都对我没有帮助。我决定用我的方式创建两组模型。第一个集合以ViewModel可以轻松使用的形式表示数据。这些数据显示在View中,所以我可以创建它的Collection并在特定的ViewModel中操作它。我把它们命名为模特。第二种模式-在形式,Easymodbus从PLC读取,它很容易浏览该数据。我把它们命名为数据模型。模型的例子。正如你所看到的,我实现了[ObservableProperty]属性,这样模型中的更改就可以传播到视图中:

public partial class ConductivityModel : ObservableObject
{
    [ObservableProperty]
    private string? _id;
    [ObservableProperty]
    private bool? _inUse;
    [ObservableProperty]
    private string? _tankNo;
    [ObservableProperty]
    private string? _sp;
    [ObservableProperty]
    private string? _pv;
    [ObservableProperty]
    private bool? _valve;
    [ObservableProperty]
    private string? _timeLeft;
    [ObservableProperty]
    private string? _timer;
    [ObservableProperty]
    private string? _sensorMinimum;
    [ObservableProperty]
    private string? _sensorMaximum;
    [ObservableProperty]
    private string? _raw;
}

DataModels是具有静态成员的静态类。EasyModbus给我数组类型的数据。下面是其中一个数据模型:

public static class ConductivityDataModel
{
    public static bool TurnON;
    public static bool IsON;        
    public static int[] Tank = new int[100];
    public static int[] SP = new int[100];
    public static int[] PV = new int[100];
    public static bool[] IsValveOn = new bool[100];
    public static bool[] TurnManualyOn = new bool[100];
    public static bool[] IsValveManualyOn = new bool[100];
    public static int[] TimeLeft = new int[100];
    public static int[] Timer = new int[100];
    public static int[] SensorMin = new int[100];
    public static int[] SensorMax = new int[100];
    public static int[] Raw = new int[100];
   
}

这是我的模特们。为了阅读Modbus,我创建了Modbus类。在这个类中,我有几个静态成员用于存储数据和几个方法。我不得不深入研究DNC编程,使其更有效。读取每个Modbus调用的dec没有太大意义,因为它是客户端服务器协议(我已经通过艰苦的方式学习了它),但我认为将数据分配给变量可以是dec和几个组。请参阅寄存器的最大数量,我可以在一个去读是125.所以我不得不把我的阅读分成几个小的。所以我决定利用这个“停机时间”通过将数据复制到DataModel来做一些工作。

{
public class MB
{
    public static CancellationTokenSource cancellationSource = new CancellationTokenSource();
    public static ModbusClient MBC = new ModbusClient();
    public static bool ConnectionExist = false;
    private static bool[] coils1 = new bool[120];
    private static int[] registers1 = new int[121];
    private static int[] registers2 = new int[121];
    private static int[] registers3 = new int[121];

    public static void Disconnect()
    {            
        ConnectionExist = false;
        cancellationSource.Cancel();
        Thread.Sleep(1000);
        cancellationSource.Dispose();
        MBC.Disconnect();            
    }        
    
    public static void ConnectToPLC(string IP, int Port, bool IsReconnecting)
    {
        if (IsReconnecting)
            ConnectionExist = false;
        cancellationSource.Cancel();
        Thread.Sleep(1500);
        if (IsReconnecting)
            MBC.Disconnect();
        cancellationSource.Dispose();
        cancellationSource = new CancellationTokenSource();
        MB.MBC = new ModbusClient(IP, Port);
        try
        {
            MBC.Connect();                
            ConnectionExist = true;
        }
        catch (Exception)
        {
            cancellationSource.Cancel();
            ConnectionExist = false;
            MessageBox.Show("Connection error.\r\nStart without reading PLC.");
        }
    }

    public static void StartReadPLC()
    {
        new Thread(() =>
        {
            try
            {
                Read(cancellationSource.Token);
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Canceled!");
            }
        }).Start();
    }

    public static async void Read(CancellationToken token)
    {            
        while (true)
        {                
            if (token.IsCancellationRequested)
                return;
            try
            {
                ConnectionExist = MBC.Available(30);
                if (!ConnectionExist)
                {
                    token.ThrowIfCancellationRequested();
                    MBC.Disconnect();
                    throw new ConnectionException();                        
                }

                //Conductivity
                coils1 = MBC.ReadCoils(0, 96);
                await Task.Run(() =>
                {
                    Array.Copy(coils1, 0, ConductivityDataModel.IsValveOn, 0, 32);
                    Array.Copy(coils1, 32, ConductivityDataModel.TurnManualyOn, 0, 32);
                    Array.Copy(coils1, 64, ConductivityDataModel.IsValveManualyOn, 0, 32);
                });

                registers1 = MBC.ReadHoldingRegisters(0, 96);
                await Task.Run(() =>
                {
                    Array.Copy(registers1, 0, ConductivityDataModel.Tank, 0, 32);
                    Array.Copy(registers1, 32, ConductivityDataModel.SP, 0, 32);
                    Array.Copy(registers1, 64, ConductivityDataModel.PV, 0, 32);
                });
                registers2 = MBC.ReadHoldingRegisters(96, 96);
                await Task.Run(() =>
                {
                    Array.Copy(registers2, 0, ConductivityDataModel.TimeLeft, 0, 32);
                    Array.Copy(registers2, 32, ConductivityDataModel.Timer, 0, 32);
                    Array.Copy(registers2, 64, ConductivityDataModel.SensorMin, 0, 32);
                });
                registers3 = MBC.ReadHoldingRegisters(192, 64);
                await Task.Run(() =>
                {
                    Array.Copy(registers3, 0, ConductivityDataModel.SensorMax, 0, 32);
                    Array.Copy(registers3, 32, ConductivityDataModel.Raw, 0, 32);
                });
                //Fresh Add
                FreshDataModel.IsValveOn = MBC.ReadCoils(96, 32);                    
                registers1 = MBC.ReadHoldingRegisters(256, 64);
                await Task.Run(() =>
                {
                    Array.Copy(registers1, 0, FreshDataModel.TimerSet, 0, 32);
                    Array.Copy(registers1, 32, FreshDataModel.Tank, 0, 32);
                });
                //Level
                coils1 = MBC.ReadCoils(128, 50);
                await Task.Run(() =>
                {
                    Array.Copy(coils1, 0, LevelDataModel.IsTankAlarm, 0, 10);
                    Array.Copy(coils1, 10, LevelDataModel.IsAlarmOFF, 0, 10);
                    Array.Copy(coils1, 20, LevelDataModel.IsFilling, 0, 10);
                    Array.Copy(coils1, 30, LevelDataModel.IsInRange, 0, 10);
                    Array.Copy(coils1, 40, LevelDataModel.IsValve, 0, 10);
                });
                LevelDataModel.Tank = MBC.ReadHoldingRegisters(320, 10);
                //General Com
                coils1 = MBC.ReadCoils(499, 15);
                await Task.Run(() =>
                {
                    GeneralDataModel.CPUinRUN = coils1[0];
                    GeneralDataModel.IsValvePowerON = coils1[1];
                    GeneralDataModel.UseLogsToSD = coils1[2];
                    GeneralDataModel.SDCardEject = coils1[3];
                    GeneralDataModel.SendEmail = coils1[4];
                    GeneralDataModel.IsTestMode = coils1[14];
                });
                await Task.Run(() =>
                {
                    FreshDataModel.IsRunOnSaturday = coils1[5];
                    FreshDataModel.IsRunOnSunday = coils1[6];
                    FreshDataModel.IsRunOnWeekend = coils1[7];
                    ConductivityDataModel.TurnON = coils1[8];
                    ConductivityDataModel.IsON = coils1[9];
                    FreshDataModel.TurnON = coils1[10];
                    FreshDataModel.IsON = coils1[11];
                    LevelDataModel.TurnON = coils1[12];
                    LevelDataModel.IsON = coils1[13];
                });
            }
            catch
            {
                ConnectionExist = false;
                MBC.Disconnect();
                MessageBox.Show("Connection to PLC lost.");
                cancellationSource.Cancel();
            };
            await Task.Delay(1000);
        }
    }
}

要连接到PLC和启动定期读取数据,我有几个方法在主窗口CodeBehind。这将触发Modbus类创建Modbus客户端,连接到PLC并启动阅读PLC的后台循环。我意识到这种方法是不正确的,但我仍然在设置工作,能够改变IP和保存它。

InitializeComponent();
        DataContext = new MainViewModel();
        MB.ConnectToPLC("127.0.0.1", 502, false);
        MB.StartReadPLC();

作为触发视图更新状态的一部分,我在MainViewModel中设置了Timer(参见开头的代码)。对我来说就像蒙着眼睛走在黑暗的森林里。我不知道怎么回事。假设我使用回调函数而不是Timer.Tick事件。无论如何,它每1000毫秒调用一次UpdateModel方法。这里我只有ConductivityViewModel更新它的值。但这也适用于所有其他ViewModel。

public void UpdateModel(object callback)
{
    for (int i = 0; i <= 32; i++)
    {
        ConductivityViewModel.ConductivityCollection[i].TankNo = ConductivityDataModel.Tank[i].ToString();
        ConductivityViewModel.ConductivityCollection[i].Sp = ConductivityDataModel.SP[i].ToString();
        ConductivityViewModel.ConductivityCollection[i].Pv = ConductivityDataModel.PV[i].ToString();
        ConductivityViewModel.ConductivityCollection[i].Valve = ConductivityDataModel.IsValveOn[i];
        ConductivityViewModel.ConductivityCollection[i].TimeLeft = ConductivityDataModel.TimeLeft[i].ToString();
        ConductivityViewModel.ConductivityCollection[i].Timer = ConductivityDataModel.Timer[i].ToString();
        ConductivityViewModel.ConductivityCollection[i].SensorMinimum = ConductivityDataModel.SensorMin[i].ToString();
        ConductivityViewModel.ConductivityCollection[i].SensorMaximum = ConductivityDataModel.SensorMax[i].ToString();
        ConductivityViewModel.ConductivityCollection[i].Raw = ConductivityDataModel.Raw[i].ToString();

    }

希望这对任何处理modbus库或MVVM导航的人都有帮助。

相关问题