Chapter 6 综合练习1
本文示例源码地址是: StdUnit.Tags.WPFDemo ,位于分支 eg-1 中.
你可以直接使用StdUnit.Tags.Templates 模板,执行命令
dotnet new tags.wpf来创建一个脚手架,并基于它来微调成这里代码。
我们打算新建一个简单的WPF项目,该项目涉及PLC两个信号:
- 心跳请求,位于 DB200.100.0, 固定以0.5Hz的频率闪烁。
- 心跳响应,位于 DB201.100.0, 跟随心跳请求这个信号跳动
创建完一个模板项目后,请添加如下依赖:
- StdUnit.Tags.Core
- StdUnit.Tags
- StdUnit.Tags.S7
- StdUnit.Tags.RxExtensions
以及日志相关依赖:
- Serilog
- Serilog.AspNetCore
- Serilog.Sinks.Console
- Serilog.Sinks.File
项目描述和逻辑
让我们建立一个xml文件来表示通道和这两个测点:
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Channel name="S7-1" driver="S7" >
<IpAddr>172.16.10.20</IpAddr>
<Rack>0</Rack>
<Slot>1</Slot>
</Channel>
<TagGrp name="IoBox" isEntry="true" isEnabled="true" channel="S7-1" scanInterval="0">
<TagGrp name="通用状态">
<TagCbnt name="PLC" address="DB200.100">
<Tag name="心跳请求" address="$$100.0" type="BIT" access="RO" />
</TagCbnt>
<TagCbnt name="MST" address="DB201.100" access="R1W">
<Tag name="心跳响应" address="$$100.0" type="BIT" />
</TagCbnt>
</TagGrp>
</TagGrp>
</Project>
然后再添加一个心跳随动逻辑组件:
public class HeartBeatLogicet : LogicetBase
{
private readonly ITag heartReq;
private readonly ITag heartAck;
private readonly ILogger<HeartBeatLogicet> _logger;
public HeartBeatLogicet(ITagsProject project, ILogger<HeartBeatLogicet> logger) : base(project)
{
var grp = this.Tags.SelectGrp("IoBox/通用状态");
heartReq = grp.SelectTag("PLC/心跳请求");
heartAck = grp.SelectTag("MST/心跳响应");
this._logger = logger;
}
public override int Order => 2;
public override bool MatchEntry(ITagGrp entry) => entry.TagName() == "IoBox";
public override Task ProcessAsync(ITagGrp entry, ITagChannel? thisChannel)
{
var now = Stopwatch.GetTimestamp();
heartAck.Value = heartReq.Value;
return Task.CompletedTask;
}
}
组合到一起
现在,我们可以把上面这些代码组合到一起了。
首先创建一个静态扩展方法,用于注册Tags相关服务:
public static IServiceCollection AddWpfDemoTags(this IServiceCollection services)
{
services.AddLogging();
services.AddTagsProjectServices(builder =>
{
builder.EnableXmlSchemaValidation();
builder.AddS7Support();
});
return services;
}
然后为App添加一个静态方法,以构造一个ServiceCollection,调用上面的AddWpfDemoTags():
public partial class App : Application
{
private static ServiceCollection ConfigureServiceCollections()
{
var services = new ServiceCollection();
services.AddSerilog((sp, lc) => lc
.ReadFrom.Services(sp)
.WriteTo.Console()
.WriteTo.File("logs/log.txt", outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] {Message}{NewLine}{Exception}", rollingInterval: RollingInterval.Day)
.Enrich.FromLogContext()
);
services.AddWpfDemoTags();
return services;
}
接着为App指定Startup=Application_Startup,以在程序启动的时候就自动启动测点项目:
public partial class App : Application
{
public IServiceProvider? Root { get; private set; }
internal ITagsProjectCtrl? Ctrl { get; private set; }
private void Application_Startup(object sender, StartupEventArgs e)
{
ServiceCollection services = ConfigureServiceCollections();
this.Root = services.BuildServiceProvider();
this.Ctrl = this.Root.GetRequiredService<ITagsProjectCtrl>();
var th = new Thread(async () =>
{
var loggerFactory = this.Root.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<App>();
var dir = Path.Combine(AppContext.BaseDirectory, "Tags");
await this.Ctrl.StartPollAsync(dir, null, (proj,sp, ct) =>
{
proj.Logicets.Add(new HeartBeatLogicet(
proj,
loggerFactory.CreateLogger<HeartBeatLogicet>()
));
proj.RunnerStarted += (grp, ch) => {
logger.LogInformation("Tags处理开始,grp={grpName}", grp.TagName());
return Task.CompletedTask;
};
proj.RunnerCrashed += (grp, ch, ex) => {
logger.LogError(ex, "Tags处理异常,grp={grpName}", grp.TagName());
return Task.CompletedTask;
};
return Task.CompletedTask;
});
});
th.IsBackground = true;
th.Start();
}
最后,我们还需要在程序退出的时候自动停止我们的测点项目:
protected override async void OnExit(ExitEventArgs e)
{
if (this.Ctrl is not null)
{
await this.Ctrl.StopAsync();
}
Environment.Exit(0);
}
至此,运行这个项目,PLC上已经可以观测到你在跟随它的心跳随动了。
UI 监控
但光有后台逻辑还不够直观,让我们再花两分钟编写一个简单界面来显示这个两个信号的实时值:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="Label" >
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style TargetType="TextBlock" >
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</Grid.Resources>
<Label Grid.Row="0" Grid.Column="0">心跳请求</Label>
<TextBlock Name="txtReq" Grid.Row="0" Grid.Column="1" />
<Label Grid.Row="1" Grid.Column="0">心跳响应</Label>
<TextBlock Name="txtAck" Grid.Row="1" Grid.Column="1" />
</Grid>
让我们用code-behind的方式把测点的值实时绑定到界面上:
public partial class MainWindow : Window, IDisposable
{
private CompositeDisposable _disposables;
public MainWindow()
{
InitializeComponent();
this._disposables = new CompositeDisposable();
var app = App.Current as App ?? throw new InvalidCastException("App.Current is not of type App");
var projobs = app.Ctrl.ObserveStartedOrStopped()
.Select(evt => evt.IsStarted ? evt.Project : null)
// 测点项目由后台线程在 Application_Startup 中启动,可能早于本窗口构造;
// StartedOrStopped 是普通事件、不会重放历史记录,因此用当前 Project 作首值兜底。
.Prepend(app.Ctrl.Project)
.DistinctUntilChanged()
.Publish()
.RefCount();
this._disposable = projobs.Select(proj => Observable.Create<Unit>(observer => {
return proj is null ?
Disposable.Empty :
SubscribeTags(proj!.Tags);
}))
.Switch()
.Subscribe();
}
private IDisposable SubscribeTags(ITagGrp tags)
{
var req = tags.SelectTag("IoBox/通用状态/PLC/心跳请求");
var ack = tags.SelectTag("IoBox/通用状态/MST/心跳响应");
CompositeDisposable d = new CompositeDisposable();
req.Watch()
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(evt =>
{
this.Dispatcher.Invoke(() =>
{
this.txtReq.Text = evt.EventArgs.NewValue?.ToString();
});
})
.DisposeWith(d);
ack.Watch()
.ObserveOn(DispatcherScheduler.Current)
.Subscribe(evt =>
{
this.Dispatcher.Invoke(() =>
{
this.txtAck.Text = evt.EventArgs.NewValue?.ToString();
});
})
.DisposeWith(d);
return d;
}
public void Dispose()
{
_disposables.Dispose();
}
}
小结
至此,你已经完成了一个简单的测点项目。不管看起来有多简单,我都强烈推荐你亲自动手一试,哪怕是克隆上面的源码仓库到本地,做些实验也很好。