1、软件架构
上位机主要作为下位机数据上传服务端以及节点调试的控制端,可以等效认为是专属版本调试工具。针对智能插座协议,对于下位机进行可视化监测和管理。
软件技术架构如下,主要为针对 Windows
的PC
端应用程序,采用WPF
以及C#
实现功能开发,其中包含MVVM
架构。
// 日志库-Log4net
// 通信库-SuperSocket
// WPF组件库-HandyControl
// 插件库-G2Cy.Plugins.NETCore.WPF
2、开发环境
主要在Windows10
操作系统中,使用Visual Studio 2022
进行开发,项目源码结构如下:
G2CyHome.Models
: 包含UI部分通用的一些依赖类,例如工具,协议枚举、命令控制类等。G2CyHome.Wpf
: 包含主程序相关窗体和类。G2CyHome.WpfOutlet
: 主要包含插座UI组件相关类。
3、程序设计
上位机测试程序主要功能如下,其中主要包括:服务配置、节点数据以及节点控制。
4、程序功能
4.1、服务配置
服务配置,主要在当前同局域网下,启动Socket
服务,对应端口和IP
与同局域网下位机形成通信,基础代码逻辑如下,包括UI
、ViewModel
以及服务。
1)UI
部分
主要代码如下:
<DockPanel>
<TextBlock Text="v2023.09.27.0001" DockPanel.Dock="Bottom" HorizontalAlignment="Center" Padding="{DynamicResource DefaultControlPadding}" FontSize="{DynamicResource MainFontSize}" Foreground="{DynamicResource SecondaryTextBrush}"></TextBlock>
<Border DockPanel.Dock="Top" Height="{DynamicResource HeaderHeight}" Padding="38,0,0,0" Background="Transparent" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource BackgroundBrush}">
<StackPanel Orientation="Horizontal">
<Path Data="{DynamicResource LogoGeometry}" Fill="{DynamicResource InfoBrush}" Width="30" Height="30"></Path>
<TextBlock Text="调试终端" FontWeight="Normal" Foreground="{DynamicResource TextIconBrush}" FontSize="{DynamicResource HeadFontSize}" VerticalAlignment="Center" Margin="10,6,0,0"></TextBlock>
</StackPanel>
</Border>
<StackPanel Margin="12,18,18,0">
<TextBlock Text="服务配置" LineHeight="33.6" FontSize="{DynamicResource TitleFontSize}" Foreground="{DynamicResource PrimaryTextBrush}"></TextBlock>
<hc:ComboBox x:Name="cmbx_type" SelectedIndex="{Binding Proto,Mode=OneWay}" hc:TitleElement.Title="协议类型" Padding="{DynamicResource TextboxPadding}" BorderThickness="1" Background="{DynamicResource BackgroundBrush}" BorderBrush="{DynamicResource BorderBrush}">
<ComboBoxItem Content="TCP"></ComboBoxItem>
<ComboBoxItem Content="UDP"></ComboBoxItem>
</hc:ComboBox>
<hc:ComboBox x:Name="cmbx_ip" SelectedIndex="0" hc:TitleElement.Title="IP地址" SelectedValue="{Binding IP,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Margin="0,10,0,0" Padding="{DynamicResource TextboxPadding}" BorderThickness="1" Background="{DynamicResource BackgroundBrush}" BorderBrush="{DynamicResource BorderBrush}">
</hc:ComboBox>
<TextBlock Text="默认选择localhost" FontSize="{DynamicResource MainFontSize}" Margin="0,7,0,0" Foreground="{DynamicResource SecondaryTextBrush}"></TextBlock>
<UniformGrid Rows="1">
<hc:TextBox x:Name="txt_port" hc:TitleElement.Title="监听端口" Text="{Binding Port,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Width="68.8" Margin="0,10,0,0" Padding="{DynamicResource TextboxPadding}" BorderThickness="1" Background="{DynamicResource BackgroundBrush}" BorderBrush="{DynamicResource BorderBrush}"></hc:TextBox>
<ContentControl x:Name="socket_status" Content="停止" Foreground="{DynamicResource PrimaryTextBrush}"
hc:TitleElement.Title="服务状态"
hc:TitleElement.TitlePlacement="Top"
Width="Auto" Margin="10,10,0,0" HorizontalContentAlignment="Left" Padding="{DynamicResource TextboxPadding}" Template="{StaticResource ContentTopTemplate}">
</ContentControl>
</UniformGrid>
<TextBlock Text="建议端口1000~65535间" FontSize="{DynamicResource MainFontSize}" Margin="0,7,0,0" Foreground="{DynamicResource SecondaryTextBrush}"></TextBlock>
<UniformGrid Rows="1" Margin="0,12,0,0">
<Button x:Name="btn_start" Click="btn_start_Click" Padding="{DynamicResource ButtonPadding}" hc:BackgroundSwitchElement.MouseHoverBackground="{DynamicResource InfoBrush}" hc:BackgroundSwitchElement.MouseDownBackground="{DynamicResource InfoBrush}" HorizontalAlignment="Left" VerticalAlignment="Center" Style="{DynamicResource ButtonCustom}" hc:BorderElement.CornerRadius="{DynamicResource MainCornerRadius}" Content="开启" Background="{DynamicResource InfoBrush}"></Button>
<Button x:Name="btn_stop" IsEnabled="False" Click="btn_stop_Click" Padding="{DynamicResource ButtonPadding}" HorizontalAlignment="Right" VerticalAlignment="Center" Style="{DynamicResource ButtonCustom}" hc:BorderElement.CornerRadius="4" Content="停止" BorderThickness="1" BorderBrush="{DynamicResource BorderBrush}" Background="{DynamicResource DarkPrimaryBrush}"></Button>
</UniformGrid>
</StackPanel>
</DockPanel>
2)ViewModel
部分,主要代码在ServerCfgVM
中。
/// <summary>
/// 服务配置实体
/// </summary>
public class ServerCfgVM : VMBase
{
public ServerCfgVM()
{
Port = 6886;
Proto = 0;
SSIds = WiFiUtils.GetWiFiSSID().ToList();
SelectedMode = 0;// 默认为运行模式
ModeIsEnabled = true;// 默认为启用状态
ResetEnabled = true;// 默认为启用状态
}
private string iP;
// 协议格式
private int proto;
public int Proto
{
get { return proto; }
set { proto = value; RaisePropertyChanged(); }
}
// IP地址
public string IP {
get {
return iP;
} set { iP = value; RaisePropertyChanged(); } }
// 端口号
private int port;
private List<string> sSIds;
public int Port
{
get {
return port;
}
set { port = value; RaisePropertyChanged(); }
}
/// <summary>
/// WIFI列表
/// </summary>
public List<string> SSIds { get => sSIds; set { sSIds = value; RaisePropertyChanged(); } }
private string ssid;
/// <summary>
/// 选中ssid
/// </summary>
public string Ssid
{
get { return ssid; }
set { ssid = value; RaisePropertyChanged(); }
}
/// <summary>
/// 选中模式
/// </summary>
private int selectedMode;
/// <summary>
/// 选中模式
/// </summary>
public int SelectedMode
{
get { return selectedMode; }
set { selectedMode = value; RaisePropertyChanged(); }
}
private bool modeIsEnabled;
/// <summary>
/// 是否启用模式切换
/// </summary>
public bool ModeIsEnabled
{
get { return modeIsEnabled; }
set { modeIsEnabled = value;RaisePropertyChanged(); }
}
private bool configEnabled;
/// <summary>
/// 是否启用配置下发
/// </summary>
public bool ConfigEnabled
{
get { return configEnabled; }
set { configEnabled = value; RaisePropertyChanged(); }
}
private bool resetEnabled;
/// <summary>
/// 是否启用配置重置
/// </summary>
public bool ResetEnabled
{
get { return resetEnabled; }
set { resetEnabled = value; RaisePropertyChanged(); }
}
}
3)服务部分
服务主要为Socket
服务端,配置项用于对服务进行监听和关闭服务管理。
try
{
SuperSocketHostBuilder<RequestModel> socketHostBuilder = CreateSocketServerBuilder<RequestModel, RequestModelPipelineFilter>();
// socket服务配置
socketHostBuilder.ConfigureSuperSocket(config =>
{
config.ClearIdleSessionInterval = 60;
config.DefaultTextEncoding = Encoding.UTF8;
config.IdleSessionTimeOut = 150;
config.Name = "deviceserver";
});
server = socketHostBuilder
.UsePackageDecoder<MyPackageDecoder>()
.UsePackageHandler(async (s, p) =>
{
// 更新页面数据
IServiceProvider serviceProvider = Program.DefaultHost.Services;
var Vm = serviceProvider.GetRequiredService<DeviceListVM>();
try
{
DeviceVm deviceSession = Vm.AppSessions.FirstOrDefault(x => x.SessionID == s.SessionID);
if (deviceSession != null)
{
// 判定设备类型
switch (p.NodeType)
{
// 智能插座
case DeviceNodeType.Outlet:
// 执行插座处理逻辑
break;
case DeviceNodeType.None:
default:
break;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
}
})
.UseSession<AppSession>()
.UseClearIdleSession()
.UseHostedService<CustomSocketService>()
.UseInProcSessionContainer()
.BuildAsServer();
await server.StartAsync();
UpdateEnabled(true);
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
}
4.2、节点数据
在保证数据服务监听已经启动的情况下,采集来自目标选中节点的传输数据。
1)节点协议解析
数据字节包解析类RequestModelPipelineFilter
:
public class RequestModelPipelineFilter : FixedHeaderPipelineFilter<RequestModel>
{
/// <summary>
/// 是否发现头部
/// </summary>
private bool _foundHeader;
/// <summary>
/// 头部长度
/// </summary>
private readonly int _headerSize;
/// <summary>
/// 目标数据包总长度
/// </summary>
private int _totalSize;
/// <summary>
/// 校验长度
/// </summary>
private int _verifySize;
// 设置对应从接收缓冲区中获取的头部字节长度
public RequestModelPipelineFilter()
: base(3)
{
_verifySize = 1;
_headerSize = 3;
}
/// <summary>
/// 从Header头部获取内容长度
/// </summary>
/// <param name="buffer">数据字节包</param>
/// <returns>内容长度</returns>
protected override int GetBodyLengthFromHeader(ref ReadOnlySequence<byte> buffer)
{
var reader = new SequenceReader<byte>(buffer);
reader.Advance(buffer.Length - 1);
//reader.TryRead(out byte length);
byte[] bytes = reader.Sequence.Slice(1, 2).ToArray();
//Array.Reverse(bytes);
int length = BitConverter.ToInt16(bytes, 0);
return length;
}
/// <summary>
/// 过滤执行函数
/// </summary>
/// <param name="reader">数据字节包</param>
/// <returns>数据包实例</returns>
public override RequestModel Filter(ref SequenceReader<byte> reader)
{
if (!_foundHeader)
{
if (reader.Length < _headerSize)
{
return null;
}
ReadOnlySequence<byte> buffer = reader.Sequence.Slice(0, _headerSize);
int bodyLengthFromHeader = GetBodyLengthFromHeader(ref buffer);
if (bodyLengthFromHeader < 0)
{
throw new ProtocolException("Failed to get body length from the package header.");
}
if (bodyLengthFromHeader == 0)
{
try
{
return DecodePackage(ref buffer);
}
finally
{
reader.Advance(_headerSize);
// 重置是否找到头部
_foundHeader = false;
}
}
_foundHeader = true;
// 总长度
_totalSize = bodyLengthFromHeader;
}
int totalSize = _totalSize;
// 判定当前实际数据包长度是否小于目标数据包总长度
if (reader.Length < totalSize)
{
return null;
}
ReadOnlySequence<byte> buffer2 = reader.Sequence.Slice(0, totalSize);
try
{
return DecodePackage(ref buffer2);
}
finally
{
reader.Advance(totalSize);
// 重置是否找到头部
_foundHeader = false;
}
}
}
数据接收类RequestModel
:
public class RequestModel
{
public byte[] Data { get; set; }=new byte[0];
/// <summary>
/// 设备节点类型
/// </summary>
public DeviceNodeType NodeType { get; set; }
/// <summary>
/// 功能码
/// </summary>
public FeatureType FeatureType { get; set; }
public Msg_Type Msg_Type { get; set; }
public byte[] GetBytes(Encoding encoding)
{
return this.ToJson().ToBytes(encoding);
}
/// <summary>
/// 构建包数据
/// </summary>
/// <param name="buffer">原始字节组</param>
public static RequestModel CreatedModel(ReadOnlySequence<byte> buffer)
{
RequestModel requestModel = new RequestModel();
// 填充数据
var reader = new SequenceReader<byte>(buffer);
// 获取产品类型
reader.TryRead(out byte devicetype);
// 高位(设备类型)
byte heigth = (byte)(devicetype & 0xf0);
requestModel.NodeType = (DeviceNodeType)Enum.ToObject(typeof(DeviceNodeType), heig
// 低位(功能码)
byte lower = (byte)(devicetype & 0x0f);
requestModel.FeatureType = (FeatureType)Enum.ToObject(typeof(FeatureType), lower);
int lentype = (int)buffer.Length-3;//包含校验位-crc校验(2)
// 跳过总长度(2)
reader.Advance(2);
try
{
//string datastr = reader.ReadString(Encoding.UTF8, lentype);
byte[] datas = reader.Sequence.Slice(reader.Position,lentype).ToArray();
requestModel.Data = datas;
}
catch (System.Text.Json.JsonException ex)
{
// 异常处理
return new RequestModel();
}
return requestModel;
}
public static bool VerifyCRC(ReadOnlySequence<byte> buffer)
{
byte[] nobytes = buffer.Slice(0,buffer.Length-2).ToArray();
byte[] flagbytes = buffer.Slice(buffer.Length-2).ToArray();
byte[] crcs = CRCUtils.Crc18(nobytes, 0, nobytes.Length);
for (int i = 0; i < 2; i++)
{
if (flagbytes[i] != crcs[i])
{
return false;
}
}
return true;
}
}
public enum Msg_Type
{
// 节点数据上报
Upload,
// 控制回发
Call
}
2)UI
展示部分
设备列表:
主要代码:
<DockPanel>
<Border BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,1,0">
<DockPanel Width="192" Background="{DynamicResource SecondaryBackgroundBrush}">
<Border DockPanel.Dock="Top" Padding="0,12,0,12" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1">
<TextBlock Text="设备列表" FontSize="{DynamicResource TitleFontSize}" Foreground="{DynamicResource PrimaryTextBrush}"
VerticalAlignment="Bottom" HorizontalAlignment="Center"></TextBlock>
</Border>
<ListBox x:Name="lbx_devices" ItemsSource="{Binding AppSessions}" FontSize="{DynamicResource SecondFontSize}"
Foreground="{DynamicResource SecondaryTextBrush}"
SelectedItem="{Binding SeletectedSession,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
Style="{DynamicResource ListBoxCustom}"
SelectionChanged="ListBox_SelectionChanged" Background="Transparent"
BorderThickness="0" d:ItemsSource="{d:SampleData ItemCount=5}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSeleted,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
<Setter Property="hc:IconElement.Geometry" Value="{Binding DeviceType,Converter={StaticResource StringGeometryConvert}}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<hc:SimplePanel Height="60">
<Border x:Name="Bg"></Border>
<Border x:Name="selectline" BorderThickness="0,0,6,0" CornerRadius="0,3,3,0" BorderBrush="{DynamicResource InfoBrush}" Height="{TemplateBinding Height}" HorizontalAlignment="Left"></Border>
<hc:SimplePanel Margin="40,0,0,0">
<Path x:Name="icon" Data="{Binding Path=(hc:IconElement.Geometry),RelativeSource={RelativeSource Mode=TemplatedParent}}"
Width="{DynamicResource LargeFontSize}"
Height="{DynamicResource LargeFontSize}"
Fill="{DynamicResource InfoBrush}" HorizontalAlignment="Left"></Path>
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Margin="36,0,0,0" VerticalAlignment="Center">
<TextBlock x:Name="maintxt" FontSize="{DynamicResource MainFontSize}" Foreground="{DynamicResource PrimaryTextBrush}" Text="{Binding DeviceId, StringFormat=0x\{0:X4\}}"></TextBlock>
<ContentPresenter x:Name="content"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Content="{Binding DeviceName}"></ContentPresenter>
</StackPanel>
</hc:SimplePanel>
</hc:SimplePanel>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="selectline" Property="Visibility" Value="Visible"/>
<Setter Property="Background" TargetName="Bg" Value="{DynamicResource BackgroundBrush}"/>
<Setter Property="Fill" TargetName="icon" Value="{DynamicResource InfoBrush}"/>
<Setter Property="Opacity" TargetName="content" Value="1"/>
<Setter Property="Opacity" TargetName="maintxt" Value="1"/>
<Setter Property="Opacity" TargetName="icon" Value="1"/>
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter TargetName="selectline" Property="Visibility" Value="Collapsed"/>
<Setter Property="Background" TargetName="Bg" Value="Transparent"/>
<Setter Property="Opacity" TargetName="content" Value=".5"/>
<Setter Property="Fill" TargetName="icon" Value="{DynamicResource PrimaryTextBrush}"/>
<Setter Property="Opacity" TargetName="content" Value=".5"/>
<Setter Property="Opacity" TargetName="maintxt" Value="0.5"/>
<Setter Property="Opacity" TargetName="icon" Value=".5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</DockPanel>
</Border>
<Border Padding="0,5.8,0,0" Background="{DynamicResource SecondaryBackgroundBrush}">
<ContentControl DataContext="{Binding ElementName=lbx_devices,Path=SelectedItem}" Name="DeviceContent"></ContentControl>
</Border>
</DockPanel>
内容部分:
主要代码:
<TabControl x:Class="G2CyHome.WpfOutlet.Views.OutletFeature"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Style="{DynamicResource TabControlCapsuleSolid}"
Background="Transparent"
BorderThickness="0,1,0,0"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
DataContextChanged="TabControl_DataContextChanged"
SelectionChanged="TabControl_SelectionChanged">
<TabItem Height="{DynamicResource TabHeadSize}" hc:TitleElement.Background="{DynamicResource InfoBrush}" Background="Transparent" Padding="8" BorderBrush="{DynamicResource BorderBrush}">
<TabItem.Header>
<DockPanel>
<Path Data="{DynamicResource FeatureGeometry}" Margin="0,0,8,0" Height="{DynamicResource TabLogoSize}" Width="{DynamicResource TabLogoSize}" Fill="{DynamicResource PrimaryTextBrush}"></Path>
<TextBlock Text="节点数据" Margin="0,0,8,0" Foreground="{DynamicResource PrimaryTextBrush}" FontSize="{DynamicResource TitleFontSize}" VerticalAlignment="Center"></TextBlock>
</DockPanel>
</TabItem.Header>
<DockPanel>
<hc:UniformSpacingPanel Margin="33,24,33,0" VerticalSpacing="64" Orientation="Vertical">
<StackPanel DataContext="{Binding DeviceData}">
<TextBlock Text="基本信息" Foreground="{DynamicResource SecondaryTextBrush}" FontSize="{DynamicResource TitleFontSize}"></TextBlock>
<hc:Divider Margin="0,7" LineStrokeThickness="1" LineStrokeDashArray="2, 2" Orientation="Horizontal" MaxWidth="2000"/>
<hc:Row Gutter="24" Margin="0,10">
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="节点ID" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Path=Device_Id, StringFormat=0x\{0:X4\}, Mode=OneWay}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="软件版本" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Software_Version,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="硬件版本" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Hardware_Version,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</hc:Col>
</hc:Row>
<hc:Row Gutter="24" Margin="0,10">
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="节点IP" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding ClientIP,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="出厂时间" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Release_Time,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="负载时间" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Run_Time,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</hc:Col>
</hc:Row>
</StackPanel>
<StackPanel DataContext="{Binding DeviceData}">
<TextBlock Text="周期时间" Foreground="{DynamicResource SecondaryTextBrush}" FontSize="{DynamicResource TitleFontSize}"></TextBlock>
<hc:Divider Margin="0,7" LineStrokeThickness="1" LineStrokeDashArray="2, 2" Orientation="Horizontal" MaxWidth="2000"/>
<hc:UniformSpacingPanel Spacing="42" Orientation="Horizontal">
<hc:TextBox hc:TitleElement.Title="数据上传周期(秒)" Width="250" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Upload_Cycle,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
<hc:TextBox hc:TitleElement.Title="数据采样周期(毫秒)" Width="250" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Sample_Cycle,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</hc:UniformSpacingPanel>
</StackPanel>
<StackPanel DataContext="{Binding DeviceData}">
<TextBlock Text="电参数据" Foreground="{DynamicResource SecondaryTextBrush}" FontSize="{DynamicResource TitleFontSize}"></TextBlock>
<hc:Divider Margin="0,7" LineStrokeThickness="1" LineStrokeDashArray="2, 2" Orientation="Horizontal" MaxWidth="2000"/>
<hc:UniformSpacingPanel ItemWidth="160" Orientation="Horizontal" Margin="0,10,0,0">
<DockPanel>
<TextBlock Text="V" VerticalAlignment="Center" DockPanel.Dock="Right" Margin="5,0,15,0"></TextBlock>
<hc:TextBox hc:TitleElement.Title="电压" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Voltage, Mode=OneWay, StringFormat=\{0:F\}, UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</DockPanel>
<DockPanel>
<TextBlock Text="mA" VerticalAlignment="Center" DockPanel.Dock="Right" Margin="5,0,15,0"></TextBlock>
<hc:TextBox hc:TitleElement.Title="电流" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Current,Mode=OneWay, StringFormat=\{0:F\},UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</DockPanel>
<DockPanel>
<TextBlock Text="Kw/h" VerticalAlignment="Center" DockPanel.Dock="Right" Margin="5,0,15,0"></TextBlock>
<hc:TextBox hc:TitleElement.Title="功率" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Power,Mode=OneWay, StringFormat=\{0:F\},UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</DockPanel>
<DockPanel>
<TextBlock Text="Kw" VerticalAlignment="Center" DockPanel.Dock="Right" Margin="5,0,15,0"></TextBlock>
<hc:TextBox hc:TitleElement.Title="电量" Style="{DynamicResource TitleTextBoxStyle}" Text="{Binding Electricity,Mode=OneWay, StringFormat=\{0:F\},UpdateSourceTrigger=PropertyChanged}"></hc:TextBox>
</DockPanel>
</hc:UniformSpacingPanel>
</StackPanel>
</hc:UniformSpacingPanel>
</DockPanel>
</TabItem>
<!--数据控制-->
<!--数据调试-->
</TabControl>
4.3、节点控制
1)协议下发
涉及到的下发协议主要包含:控制继电器开关、节点配置、模式切换、控制状态回发。
主要通过数据包协议类型FeatureType
进行区分判定:
/// <summary>
/// 功能类型
/// </summary>
public enum FeatureType:byte
{
/// <summary>
/// 0x01 设备数据上传功能码
/// </summary>
[Description("数据上传")]
Upload = 0x01,
/// <summary>
/// 0x02 下发响应功能码
/// </summary>
[Description("下发响应")]
Callback = 0x02,
/// <summary>
/// 0x05 节点控制功能码
/// </summary>
[Description("节点控制")]
Push = 0x05,
/// <summary>
/// 0x04 节点配置功能码
/// </summary>
[Description("节点配置")]
Config= 0x04,
/// <summary>
/// 0x03 模式切换功能码
/// </summary>
[Description("模式切换")]
ModeCfg = 0x03,
/// <summary>
/// 0x06 升级响应功能码
/// </summary>
[Description("升级响应")]
UpdateAck = 0x06,
}
功能类型通过不同类型接口进行下发指令区分:
例如 控制回发类接口,实现IDeviceCallback
/// <summary>
/// 设备响应回发接口
/// </summary>
public interface IDeviceCallback<T>
{
/// <summary>
/// 客户端IP
/// </summary>
[JsonPropertyName("clientip")]
public string ClientIP { get; set; }
/// <summary>
/// 设备标识
/// </summary>
[JsonPropertyName("device_id")]
public int Device_Id { get; set; }
/// <summary>
/// 软件版本号
/// </summary>
[JsonPropertyName("software_version")]
public string Software_Version { get; set; }
/// <summary>
/// 硬件版本号
/// </summary>
[JsonPropertyName("hardware_version")]
public string Hardware_Version { get; set; }
/// <summary>
/// 状态码:0 响应成功,1 响应失败
/// </summary>
public bool State_Code { get; set; }
// 字节转对象
T BytesToObject(byte[] bytes);
}
设备回发基类:
// 设备回发基类
public class DeviceCallback : VMBase, IDeviceCallback<DeviceCallback> {
public virtual DeviceCallback BytesToObject(byte[] bytes)
{
return null;
}
}
插座控制回发类实现内容如下:
/// <summary>
/// 智能插座响应类
/// </summary>
public class OutletCallback:DeviceCallback
{
public override DeviceCallback BytesToObject(byte[] bytes)
{
SequenceReader<byte> read = new SequenceReader<byte>(new ReadOnlySequence<byte>(bytes));
// 设备id
byte[] deviceid = new byte[2];
read.TryCopyTo(deviceid);
read.Advance(deviceid.Length);
Device_Id = BitConverter.ToUInt16(deviceid);
// 软件版本
byte[] software = new byte[15];
read.TryCopyTo(software);
read.Advance(software.Length);
Software_Version = software.GetStrFromBytes(15);
// 硬件版本
byte[] hardware = new byte[15];
read.TryCopyTo(hardware);
Hardware_Version = hardware.GetStrFromBytes(15);
read.Advance(hardware.Length);
// 状态码
read.TryRead(out byte state);
// 高位(回发功能码) 高位转低位
byte heigth = (byte)((state & 0xf0)>>4);
FeatureCallType = (FeatureType)Enum.ToObject(typeof(FeatureType), heigth);
// 低位(状态码)
byte lower = (byte)(state & 0x0f);
State_Code = lower == 0x00;
return this;
}
}
2)UI
展示部分
主要代码:
<TabControl x:Class="G2CyHome.WpfOutlet.Views.OutletFeature"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Style="{DynamicResource TabControlCapsuleSolid}"
Background="Transparent"
BorderThickness="0,1,0,0"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
DataContextChanged="TabControl_DataContextChanged"
SelectionChanged="TabControl_SelectionChanged">
<!--节点数据-->
<TabItem Height="{DynamicResource TabHeadSize}" Background="Transparent" hc:TitleElement.Background="{DynamicResource InfoBrush}" Padding="8" BorderBrush="{DynamicResource BorderBrush}">
<TabItem.Header>
<DockPanel>
<Path Data="{DynamicResource ModeGeometry}" Margin="0,0,8,0" Width="{DynamicResource TabLogoSize}" Height="{DynamicResource TabLogoSize}" Fill="{DynamicResource PrimaryTextBrush}"></Path>
<TextBlock Text="节点控制" Margin="0,0,8,0" Foreground="{DynamicResource PrimaryTextBrush}" FontSize="{DynamicResource TitleFontSize}" VerticalAlignment="Center"></TextBlock>
</DockPanel>
</TabItem.Header>
<DockPanel>
<hc:UniformSpacingPanel Margin="33,24,33,0" VerticalSpacing="24" Orientation="Vertical">
<StackPanel>
<DockPanel>
<ContentControl DockPanel.Dock="Right" hc:TitleElement.Title="" VerticalAlignment="Center">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="{DynamicResource SecondFontSize}" VerticalAlignment="Center" Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=(hc:TitleElement.Title)}" Foreground="{DynamicResource PrimaryTextBrush}" ></TextBlock>
<Path x:Name="Icon" VerticalAlignment="Center" Data="{DynamicResource ModeGeometry}" Height="{DynamicResource MainFontSize}" Margin="8,0,8,0" Width="{DynamicResource MainFontSize}" Fill="{DynamicResource SuccessBrush}">
</Path>
<TextBlock FontSize="{DynamicResource SecondFontSize}" VerticalAlignment="Center" Text="运行模式" Foreground="{DynamicResource PrimaryTextBrush}" ></TextBlock>
</StackPanel>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<TextBlock Text="继电器控制" Foreground="{DynamicResource SecondaryTextBrush}" FontSize="{DynamicResource TitleFontSize}" VerticalAlignment="Center"></TextBlock>
</DockPanel>
<hc:Divider Margin="0, 7" LineStrokeThickness="1" LineStrokeDashArray="2, 2" Orientation="Horizontal" MaxWidth="2000"/>
<hc:UniformSpacingPanel HorizontalSpacing="80" Orientation="Horizontal" DataContext="{Binding DeviceData}">
<StackPanel Orientation="Horizontal">
<ContentControl x:Name="content_status" Content="{Binding Relay_Statestr,UpdateSourceTrigger=PropertyChanged,Mode=OneWay}" VerticalAlignment="Center" Margin="0,10,0,10" hc:TitleElement.Title="继电器状态">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="{DynamicResource MainFontSize}" VerticalAlignment="Center" Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=(hc:TitleElement.Title)}" Foreground="{DynamicResource PrimaryTextBrush}" ></TextBlock>
<Path x:Name="Icon" VerticalAlignment="Center" HorizontalAlignment="Center" Data="{DynamicResource SwitchGeometry}" Height="{DynamicResource MainFontSize}" Margin="8,0,8,0" Width="{DynamicResource MainFontSize}" Fill="{DynamicResource BorderBrush}">
</Path>
<TextBlock x:Name="txt_status" FontSize="{DynamicResource MainFontSize}" VerticalAlignment="Center" HorizontalAlignment="Center" Text="关闭" Foreground="{DynamicResource PrimaryTextBrush}" ></TextBlock>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="Content" Value="1">
<Setter Property="Fill" TargetName="Icon" Value="{DynamicResource InfoBrush}"/>
<Setter Property="Opacity" Value="1" TargetName="txt_status"/>
<Setter Property="Text" Value="开启" TargetName="txt_status"/>
</Trigger>
<Trigger Property="Content" Value="0">
<Setter Property="Fill" TargetName="Icon" Value="{DynamicResource BorderBrush}"/>
<Setter Property="Opacity" Value=".5" TargetName="txt_status"/>
<Setter Property="Text" Value="关闭" TargetName="txt_status"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<ToggleButton x:Name="check_switch" BorderThickness="0" Width="40" IsEnabled="{Binding IsSwitch,Mode=OneWay}" IsChecked="{Binding Relay_State,Mode=OneWay,Converter={StaticResource Int2BooleanConverter}}" Cursor="Hand" Height="40" Margin="5" HorizontalAlignment="Center" Style="{StaticResource ToggleButtonFlip}">
<hc:StatusSwitchElement.CheckedElement>
<Border Background="{DynamicResource InfoBrush}">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="开" Foreground="{DynamicResource TextIconBrush}"/>
</Border>
</hc:StatusSwitchElement.CheckedElement>
<Border Background="{DynamicResource BackgroundBrush}">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="关" Foreground="{DynamicResource TextIconBrush}"/>
</Border>
</ToggleButton>
</StackPanel>
</hc:UniformSpacingPanel>
</StackPanel>
<StackPanel>
<DockPanel DataContext="{Binding DeviceCfg}">
<ContentControl DockPanel.Dock="Right" Content="{Binding Relay_State,UpdateSourceTrigger=PropertyChanged,Mode=OneWay}" VerticalAlignment="Center" Margin="10,10,0,10" hc:TitleElement.Title="">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="{DynamicResource SecondFontSize}" VerticalAlignment="Center" Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=(hc:TitleElement.Title)}" Foreground="{DynamicResource PrimaryTextBrush}" ></TextBlock>
<Path x:Name="Icon" VerticalAlignment="Center" Data="{DynamicResource ModeGeometry}" Height="{DynamicResource MainFontSize}" Margin="8,0,8,0" Width="{DynamicResource MainFontSize}" Fill="{DynamicResource WarningBrush}">
</Path>
<TextBlock FontSize="{DynamicResource SecondFontSize}" VerticalAlignment="Center" Text="配置模式" Foreground="{DynamicResource PrimaryTextBrush}" ></TextBlock>
</StackPanel>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<TextBlock Text="配置项下发" Foreground="{DynamicResource SecondaryTextBrush}" FontSize="20" VerticalAlignment="Center"></TextBlock>
</DockPanel>
<hc:Divider Margin="0,7" LineStrokeThickness="1" LineStrokeDashArray="2, 2" Orientation="Horizontal" MaxWidth="2000"/>
<hc:Row Gutter="24" Margin="0,10" DataContext="{Binding ServerCfg}">
<hc:Col Span="8">
<hc:TextBox Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding IP,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" hc:TitleElement.Title="服务地址" hc:TitleElement.TitlePlacement="Left" Background="{DynamicResource BackgroundBrush}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox VerticalContentAlignment="Center" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding Port,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" hc:TitleElement.Title="服务端口" Background="{DynamicResource BackgroundBrush}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox Text="{Binding Ssid}" VerticalContentAlignment="Center" VerticalAlignment="Center" FontSize="{DynamicResource MainFontSize}" hc:TitleElement.VerticalAlignment="Center" Background="{DynamicResource BackgroundBrush}" hc:TitleElement.Title="服务WIFI" hc:TitleElement.TitlePlacement="Left"></hc:TextBox>
</hc:Col>
</hc:Row>
<hc:Row Gutter="24" Margin="0,10" DataContext="{Binding DeviceCfg}">
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="出厂时间" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding Release_Time,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Background="{DynamicResource BackgroundBrush}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="软件版本" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding Software_Version,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Background="{DynamicResource BackgroundBrush}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="硬件版本" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding Hardware_Version,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Background="{DynamicResource BackgroundBrush}"></hc:TextBox>
</hc:Col>
</hc:Row>
<hc:Row Gutter="24" Margin="0,10" DataContext="{Binding DeviceCfg}">
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="设备新Id" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding Device_NewId,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Background="{DynamicResource BackgroundBrush}" ></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<!--<hc:TextBox Name="txt_nodetype" hc:TitleElement.Title="设备类型" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding NodeType,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}" Background="{DynamicResource BackgroundBrush}" ></hc:TextBox>-->
</hc:Col>
<hc:Col Span="8">
</hc:Col>
</hc:Row>
<hc:Row Gutter="24" Margin="0,10" DataContext="{Binding DeviceCfg}">
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="上传周期(秒)" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding Upload_Cycle,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Background="{DynamicResource BackgroundBrush}"></hc:TextBox>
</hc:Col>
<hc:Col Span="8">
<hc:TextBox hc:TitleElement.Title="采样周期(毫秒)" Style="{DynamicResource TitleTextBoxBorderStyle}" Text="{Binding Sample_Cycle,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Background="{DynamicResource BackgroundBrush}" ></hc:TextBox>
</hc:Col>
<hc:Col Span="8"></hc:Col>
</hc:Row>
<hc:Row Gutter="24" Margin="0,10" DataContext="{Binding ServerCfg}">
<hc:Col Span="8">
<Button FontSize="{DynamicResource MainFontSize}" Content="配置节点" IsEnabled="{Binding ConfigEnabled,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}" Padding="8" hc:BackgroundSwitchElement.MouseHoverBackground="{DynamicResource InfoBrush}" hc:BackgroundSwitchElement.MouseDownBackground="{DynamicResource InfoBrush}" HorizontalAlignment="Left" VerticalAlignment="Center" Style="{DynamicResource ButtonCustom}" hc:BorderElement.CornerRadius="4" Margin="0,0,0,0" Background="{DynamicResource InfoBrush}" Click="Button_Click_2"></Button>
</hc:Col>
<hc:Col Span="8">
<Button FontSize="{DynamicResource MainFontSize}" Content="升级节点" Padding="8" hc:BackgroundSwitchElement.MouseHoverBackground="{DynamicResource InfoBrush}" hc:BackgroundSwitchElement.MouseDownBackground="{DynamicResource InfoBrush}" HorizontalAlignment="Left" VerticalAlignment="Center" Style="{DynamicResource ButtonCustom}" hc:BorderElement.CornerRadius="4" Margin="0,0,0,0" Background="{DynamicResource InfoBrush}" Click="Button_Click_1"></Button>
</hc:Col>
<hc:Col Span="8">
<Button FontSize="{DynamicResource MainFontSize}" Content="重启节点" Padding="8" hc:BackgroundSwitchElement.MouseHoverBackground="{DynamicResource InfoBrush}" hc:BackgroundSwitchElement.MouseDownBackground="{DynamicResource InfoBrush}" HorizontalAlignment="Left" VerticalAlignment="Center" Style="{DynamicResource ButtonCustom}" hc:BorderElement.CornerRadius="4" Margin="0,0,0,0" Background="{DynamicResource InfoBrush}" Click="Button_Click_3"></Button>
</hc:Col>
<hc:Col Span="8">
<Button FontSize="{DynamicResource MainFontSize}" Content="配置重置" IsEnabled="{Binding ResetEnabled,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}" Padding="8" hc:BackgroundSwitchElement.MouseHoverBackground="{DynamicResource InfoBrush}" hc:BackgroundSwitchElement.MouseDownBackground="{DynamicResource InfoBrush}" HorizontalAlignment="Left" VerticalAlignment="Center" Style="{DynamicResource ButtonCustom}" hc:BorderElement.CornerRadius="4" Margin="0,0,0,0" Background="{DynamicResource InfoBrush}" Name="btn_reset" Click="btn_reset_Click"></Button>
</hc:Col>
</hc:Row>
</StackPanel>
</hc:UniformSpacingPanel>
</DockPanel>
</TabItem>
<!--节点调试-->
</TabControl>
5、程序功能特点
程序目前设计采用插件方式加载,数据协议提供数据调试和程序本地日志查看。
本地日志封装如下:
/// <summary>
/// log4net日志记录
/// </summary>
public class Log4NetLogger : ILogger
{
private readonly ILog _log;
/// <summary>
/// 初始化一个<see cref="Log4NetLogger"/>类型的新实例
/// </summary>
public Log4NetLogger(string loggerRepository, string name)
{
_log = LogManager.GetLogger(loggerRepository, name);
}
/// <summary>Writes a log entry.</summary>
/// <param name="logLevel">日志级别,将按这个级别写不同的日志</param>
/// <param name="eventId">事件编号.</param>
/// <param name="state">The entry to be written. Can be also an object.</param>
/// <param name="exception">The exception related to this entry.</param>
/// <param name="formatter">Function to create a <c>string</c> message of the <paramref name="state" /> and <paramref name="exception" />.</param>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
string message = null;
if (formatter != null)
{
message = formatter(state, exception);
}
if (!string.IsNullOrEmpty(message) || exception != null)
{
switch (logLevel)
{
case LogLevel.Trace:
case LogLevel.Debug:
_log.Debug(message);
break;
case LogLevel.Information:
_log.Info(message);
break;
case LogLevel.Warning:
_log.Warn(message);
break;
case LogLevel.Error:
_log.Error(message, exception);
break;
case LogLevel.Critical:
_log.Fatal(message, exception);
break;
case LogLevel.None:
break;
default:
_log.Warn($"遇到未知的日志级别 {logLevel}, 使用Info级别写入日志。");
_log.Info(message, exception);
break;
}
}
}
/// <summary>
/// Checks if the given <paramref name="logLevel" /> is enabled.
/// </summary>
/// <param name="logLevel">level to be checked.</param>
/// <returns><c>true</c> if enabled.</returns>
public bool IsEnabled(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace:
case LogLevel.Debug:
return _log.IsDebugEnabled;
case LogLevel.Information:
return _log.IsInfoEnabled;
case LogLevel.Warning:
return _log.IsWarnEnabled;
case LogLevel.Error:
return _log.IsErrorEnabled;
case LogLevel.Critical:
return _log.IsFatalEnabled;
case LogLevel.None:
return false;
default:
throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null);
}
}
/// <summary>Begins a logical operation scope.</summary>
/// <param name="state">The identifier for the scope.</param>
/// <returns>An IDisposable that ends the logical operation scope on dispose.</returns>
public IDisposable BeginScope<TState>(TState state)
{
return null;
}