ESP8266 WiFi物联网智能插座—上位机软件实现

1、软件架构

上位机主要作为下位机数据上传服务端以及节点调试的控制端,可以等效认为是专属版本调试工具。针对智能插座协议,对于下位机进行可视化监测和管理。 软件技术架构如下,主要为针对 Windows 的PC 端应用程序,采用WPF以及C# 实现功能开发,其中包含MVVM架构。

// 日志库-Log4net // 通信库-SuperSocket // WPF组件库-HandyControl // 插件库-G2Cy.Plugins.NETCore.WPF

添加图片注释,不超过 140 字(可选)

项目PCB、原理图和代码下载:https://download.csdn.net/download/m0_38106923/89089246

2、开发环境

主要在Windows10操作系统中,使用Visual Studio 2022 进行开发,项目源码结构如下:

添加图片注释,不超过 140 字(可选)

  • G2CyHome.Models : 包含UI部分通用的一些依赖类,例如工具,协议枚举、命令控制类等。
  • G2CyHome.Wpf : 包含主程序相关窗体和类。
  • G2CyHome.WpfOutlet : 主要包含插座UI组件相关类。

3、程序设计

上位机测试程序主要功能如下,其中主要包括:服务配置、节点数据以及节点控制。

添加图片注释,不超过 140 字(可选)

4、程序功能

4.1、服务配置

服务配置,主要在当前同局域网下,启动Socket 服务,对应端口和IP 与同局域网下位机形成通信,基础代码逻辑如下,包括UI、ViewModel以及服务。 1)UI部分

添加图片注释,不超过 140 字(可选)

主要代码如下:

```

<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>

```

2)ViewModel部分,主要代码在ServerCfgVM中。

``` ///

/// 服务配置实体 /// 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 socketHostBuilder = CreateSocketServerBuilder (); // socket服务配置 socketHostBuilder.ConfigureSuperSocket(config => { config.ClearIdleSessionInterval = 60; config.DefaultTextEncoding = Encoding.UTF8; config.IdleSessionTimeOut = 150; config.Name = "deviceserver"; }); server = socketHostBuilder .UsePackageDecoder () .UsePackageHandler(async (s, p) => { // 更新页面数据 IServiceProvider serviceProvider = Program.DefaultHost.Services; var Vm = serviceProvider.GetRequiredService ();

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 { ///

/// 是否发现头部 /// private bool _foundHeader; /// /// 头部长度 /// private readonly int _headerSize; /// /// 目标数据包总长度 /// private int _totalSize; /// /// 校验长度 /// private int _verifySize; // 设置对应从接收缓冲区中获取的头部字节长度 public RequestModelPipelineFilter() : base(3) { _verifySize = 1; _headerSize = 3; } /// /// 从Header头部获取内容长度 /// /// 数据字节包 /// 内容长度 protected override int GetBodyLengthFromHeader(ref ReadOnlySequence buffer) { var reader = new SequenceReader (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]; ///

/// 设备节点类型 /// public DeviceNodeType NodeType { get; set; } /// /// 功能码 /// public FeatureType FeatureType { get; set; } public Msg Type MsgType { 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展示部分

添加图片注释,不超过 140 字(可选)

设备列表:

添加图片注释,不超过 140 字(可选)

主要代码:

<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>

内容部分:

添加图片注释,不超过 140 字(可选)

主要代码:

<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

``` ///

/// 设备响应回发接口 /// public interface IDeviceCallback { /// /// 客户端IP /// [JsonPropertyName("clientip")] public string ClientIP { get; set; } /// /// 设备标识 /// [JsonPropertyName("device id")] public int DeviceId { get; set; } /// /// 软件版本号 /// [JsonPropertyName("software version")] public string SoftwareVersion { get; set; } /// /// 硬件版本号 /// [JsonPropertyName("hardware version")] public string HardwareVersion { get; set; } /// /// 状态码:0 响应成功,1 响应失败 /// 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展示部分

添加图片注释,不超过 140 字(可选)

主要代码:

<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、程序功能特点

程序目前设计采用插件方式加载,数据协议提供数据调试和程序本地日志查看。

添加图片注释,不超过 140 字(可选)

本地日志封装如下:

``` ///

/// log4net日志记录 /// 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;
}

```

项目PCB、原理图和代码下载:https://download.csdn.net/download/m0_38106923/89089246

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_44079197

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值