编辑 | blame | 历史 | 原始文档

S7 PLC模拟器实施计划

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

目标: 构建一个基于HSL Communication库的西门子S7 PLC模拟器系统,支持多实例、Web管理界面和数据持久化。

架构: 采用DDD分层架构,Core层实现领域逻辑,Application层提供应用服务,Server层提供Web API,Web层提供管理界面。使用HSL Communication库实现S7服务器,SignalR实现实时状态推送。

技术栈: ASP.NET Core 6.0, HslCommunication 12.6.3, SignalR, Razor Pages, Serilog, AutoMapper, xUnit


文件结构映射

Core项目

文件 职责
Enums/SiemensPLCType.cs PLC型号枚举
Enums/InstanceStatus.cs 实例状态枚举
Entities/InstanceConfig.cs 实例配置实体
Entities/InstanceState.cs 实例状态实体
Entities/S7ClientConnection.cs 客户端连接实体
Entities/MemoryRegionConfig.cs 内存区域配置
Interfaces/IMemoryRegion.cs 内存区域接口
Interfaces/IMemoryStore.cs 内存存储接口
Interfaces/IS7ServerInstance.cs 服务器实例接口
Interfaces/ISimulatorInstanceManager.cs 实例管理器接口
Interfaces/IPersistenceService.cs 持久化服务接口
Memory/MemoryRegion.cs 内存区域基类
Memory/MRegion.cs M区实现
Memory/DBRegion.cs DB区实现
Memory/IRegion.cs I区实现
Memory/QRegion.cs Q区实现
Memory/TRegion.cs T区实现
Memory/CRegion.cs C区实现
Memory/MemoryStore.cs 内存存储实现
Persistence/FilePersistenceService.cs 文件持久化服务
Server/S7ServerInstance.cs S7服务器实例实现
Manager/SimulatorInstanceManager.cs 实例管理器实现

Application项目

文件 职责
DTOs/InstanceDTO.cs 实例数据传输对象
DTOs/CreateInstanceDTO.cs 创建实例DTO
DTOs/UpdateInstanceDTO.cs 更新实例DTO
DTOs/InstanceDetailDTO.cs 实例详情DTO
DTOs/MemoryReadDTO.cs 内存读取DTO
DTOs/MemoryWriteDTO.cs 内存写入DTO
DTOs/ClientConnectionDTO.cs 客户端连接DTO
DTOs/InstanceStateEventArgs.cs 状态事件参数
DTOs/ClientConnectionEventArgs.cs 客户端连接事件参数
Services/SimulatorInstanceAppService.cs 实例应用服务
Services/MemoryAppService.cs 内存应用服务
Services/ClientAppService.cs 客户端应用服务
Profiles/MappingProfile.cs AutoMapper配置

Server项目

文件 职责
Controllers/SimulatorInstancesController.cs 实例列表API
Controllers/SimulatorInstanceController.cs 实例控制API
Controllers/MemoryController.cs 内存操作API
Controllers/ClientsController.cs 客户端管理API
Hubs/SimulatorHub.cs SignalR实时推送
Infrastructure/DependencyInjection.cs DI配置
Infrastructure/Middleware/ExceptionMiddleware.cs 异常处理中间件
Program.cs 程序入口

Web项目

文件 职责
Pages/Index.cshtml 实例列表页
Pages/Index.cshtml.cs 实例列表页模型
Pages/Create.cshtml 创建实例页
Pages/Create.cshtml.cs 创建实例页模型
Pages/Edit.cshtml 编辑实例页
Pages/Edit.cshtml.cs 编辑实例页模型
Pages/Details.cshtml 实例详情页
Pages/Details.cshtml.cs 实例详情页模型
Pages/Shared/_Layout.cshtml 布局页
wwwroot/css/site.css 样式文件
wwwroot/js/site.js JavaScript文件

Tests项目

文件 职责
Memory/MRegionTests.cs M区单元测试
Memory/DBRegionTests.cs DB区单元测试
Memory/MemoryStoreTests.cs 内存存储单元测试
Server/S7ServerInstanceTests.cs 服务器实例单元测试
Persistence/FilePersistenceServiceTests.cs 持久化服务单元测试

Chunk 1: 项目基础设施

Task 1: 创建解决方案和项目结构

  • [ ] Step 1: 在WCS目录下创建S7模拟器解决方案
cd D:\Git\ShanMeiXinNengYuan\Code\WCS
dotnet new sln -n WIDESEAWCS_S7Simulator

预期输出: 创建 WIDESEAWCS_S7Simulator.sln

  • [ ] Step 2: 创建Core项目
cd WIDESEAWCS_S7Simulator
dotnet new classlib -n WIDESEAWCS_S7Simulator.Core -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Core/WIDESEAWCS_S7Simulator.Core.csproj
  • [ ] Step 3: 创建Application项目
dotnet new classlib -n WIDESEAWCS_S7Simulator.Application -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Application/WIDESEAWCS_S7Simulator.Application.csproj
  • [ ] Step 4: 创建Server项目
dotnet new webapi -n WIDESEAWCS_S7Simulator.Server -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Server/WIDESEAWCS_S7Simulator.Server.csproj
  • [ ] Step 5: 创建Web项目
dotnet new webapp -n WIDESEAWCS_S7Simulator.Web -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Web/WIDESEAWCS_S7Simulator.Web.csproj
  • [ ] Step 6: 创建Tests项目
dotnet new xunit -n WIDESEAWCS_S7Simulator.UnitTests -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.UnitTests/WIDESEAWCS_S7Simulator.UnitTests.csproj
  • [ ] Step 7: 添加项目引用
cd WIDESEAWCS_S7Simulator.Application
dotnet add reference ../WIDESEAWCS_S7Simulator.Core/WIDESEAWCS_S7Simulator.Core.csproj

cd ../WIDESEAWCS_S7Simulator.Server
dotnet add reference ../WIDESEAWCS_S7Simulator.Core/WIDESEAWCS_S7Simulator.Core.csproj
dotnet add reference ../WIDESEAWCS_S7Simulator.Application/WIDESEAWCS_S7Simulator.Application.csproj

cd ../WIDESEAWCS_S7Simulator.Web
dotnet add reference ../WIDESEAWCS_S7Simulator.Application/WIDESEAWCS_S7Simulator.Application.csproj

cd ../WIDESEAWCS_S7Simulator.UnitTests
dotnet add reference ../WIDESEAWCS_S7Simulator.Core/WIDESEAWCS_S7Simulator.Core.csproj
  • [ ] Step 8: 添加NuGet包到Core项目
cd ../WIDESEAWCS_S7Simulator.Core
dotnet add package HslCommunication --version 12.6.3
dotnet add package Microsoft.Extensions.Logging.Abstractions
dotnet add package Microsoft.Extensions.Configuration.Abstractions
dotnet add package Newtonsoft.Json
  • [ ] Step 9: 添加NuGet包到Server项目
cd ../WIDESEAWCS_S7Simulator.Server
dotnet add package Serilog.AspNetCore
dotnet add package AutoMapper
dotnet add package Microsoft.AspNetCore.SignalR
  • [ ] Step 10: 添加NuGet包到Web项目
cd ../WIDESEAWCS_S7Simulator.Web
dotnet add package Microsoft.AspNetCore.SignalR.Client
  • [ ] Step 11: 验证解决方案构建
cd ..
dotnet build

预期输出: 构建成功

  • [ ] Step 12: 提交项目结构
git add .
git commit -m "feat: create S7 simulator solution structure

- Add Core, Application, Server, Web projects
- Configure project references
- Add required NuGet packages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Chunk 2: 核心枚举和实体

Task 2: 创建枚举类型

Files:
- Create: WIDESEAWCS_S7Simulator.Core/Enums/SiemensPLCType.cs
- Create: WIDESEAWCS_S7Simulator.Core/Enums/InstanceStatus.cs

  • [ ] Step 1: 创建PLC类型枚举
namespace WIDESEAWCS_S7Simulator.Core.Enums
{
    /// <summary>
    /// 西门子PLC型号
    /// </summary>
    public enum SiemensPLCType
    {
        /// <summary>
        /// S7-200 Smart
        /// </summary>
        S7200Smart = 0,

        /// <summary>
        /// S7-1200
        /// </summary>
        S71200 = 1,

        /// <summary>
        /// S7-1500
        /// </summary>
        S71500 = 2,

        /// <summary>
        /// S7-300
        /// </summary>
        S7300 = 3,

        /// <summary>
        /// S7-400
        /// </summary>
        S7400 = 4
    }
}
  • [ ] Step 2: 创建实例状态枚举
namespace WIDESEAWCS_S7Simulator.Core.Enums
{
    /// <summary>
    /// S7服务器实例运行状态
    /// </summary>
    public enum InstanceStatus
    {
        /// <summary>
        /// 已停止
        /// </summary>
        Stopped = 0,

        /// <summary>
        /// 启动中
        /// </summary>
        Starting = 1,

        /// <summary>
        /// 运行中
        /// </summary>
        Running = 2,

        /// <summary>
        /// 停止中
        /// </summary>
        Stopping = 3,

        /// <summary>
        /// 错误
        /// </summary>
        Error = 4
    }
}
  • [ ] Step 3: 删除默认生成的Class1.cs文件
rm WIDESEAWCS_S7Simulator.Core/Class1.cs
  • [ ] Step 4: 验证构建
cd WIDESEAWCS_S7Simulator.Core
dotnet build
  • [ ] Step 5: 提交枚举类型
git add .
git commit -m "feat: add PLC type and instance status enums

- Add SiemensPLCType enum (S7-200/1200/1500/300/400)
- Add InstanceStatus enum (Stopped/Starting/Running/Stopping/Error)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 3: 创建核心实体

Files:
- Create: WIDESEAWCS_S7Simulator.Core/Entities/MemoryRegionConfig.cs
- Create: WIDESEAWCS_S7Simulator.Core/Entities/InstanceConfig.cs
- Create: WIDESEAWCS_S7Simulator.Core/Entities/S7ClientConnection.cs
- Create: WIDESEAWCS_S7Simulator.Core/Entities/InstanceState.cs

  • [ ] Step 1: 创建内存区域配置实体
using Newtonsoft.Json;

namespace WIDESEAWCS_S7Simulator.Core.Entities
{
    /// <summary>
    /// 内存区域配置
    /// </summary>
    public class MemoryRegionConfig
    {
        /// <summary>
        /// M区大小(字节),默认1KB
        /// </summary>
        [JsonProperty("mRegionSize")]
        public int MRegionSize { get; set; } = 1024;

        /// <summary>
        /// DB块数量,默认100个
        /// </summary>
        [JsonProperty("dbBlockCount")]
        public int DBBlockCount { get; set; } = 100;

        /// <summary>
        /// 每个DB块大小(字节),默认1KB
        /// </summary>
        [JsonProperty("dbBlockSize")]
        public int DBBlockSize { get; set; } = 1024;

        /// <summary>
        /// I区大小(字节),默认256字节
        /// </summary>
        [JsonProperty("iRegionSize")]
        public int IRegionSize { get; set; } = 256;

        /// <summary>
        /// Q区大小(字节),默认256字节
        /// </summary>
        [JsonProperty("qRegionSize")]
        public int QRegionSize { get; set; } = 256;

        /// <summary>
        /// T区数量,默认64个
        /// </summary>
        [JsonProperty("tRegionCount")]
        public int TRegionCount { get; set; } = 64;

        /// <summary>
        /// C区数量,默认64个
        /// </summary>
        [JsonProperty("cRegionCount")]
        public int CRegionCount { get; set; } = 64;
    }
}
  • [ ] Step 2: 创建实例配置实体
using Newtonsoft.Json;
using WIDESEAWCS_S7Simulator.Core.Enums;

namespace WIDESEAWCS_S7Simulator.Core.Entities
{
    /// <summary>
    /// S7服务器实例配置
    /// </summary>
    public class InstanceConfig
    {
        /// <summary>
        /// 实例唯一标识
        /// </summary>
        [JsonProperty("id")]
        public string Id { get; set; } = string.Empty;

        /// <summary>
        /// 实例名称
        /// </summary>
        [JsonProperty("name")]
        public string Name { get; set; } = string.Empty;

        /// <summary>
        /// PLC型号
        /// </summary>
        [JsonProperty("plcType")]
        public SiemensPLCType PLCType { get; set; }

        /// <summary>
        /// 监听端口
        /// </summary>
        [JsonProperty("port")]
        public int Port { get; set; }

        /// <summary>
        /// HSL激活码
        /// </summary>
        [JsonProperty("activationKey")]
        public string ActivationKey { get; set; } = string.Empty;

        /// <summary>
        /// 是否自动启动
        /// </summary>
        [JsonProperty("autoStart")]
        public bool AutoStart { get; set; }

        /// <summary>
        /// 内存区域配置
        /// </summary>
        [JsonProperty("memoryConfig")]
        public MemoryRegionConfig MemoryConfig { get; set; } = new();
    }
}
  • [ ] Step 3: 创建客户端连接实体
namespace WIDESEAWCS_S7Simulator.Core.Entities
{
    /// <summary>
    /// S7客户端连接信息
    /// </summary>
    public class S7ClientConnection
    {
        /// <summary>
        /// 客户端唯一标识
        /// </summary>
        public string ClientId { get; set; } = string.Empty;

        /// <summary>
        /// 客户端IP地址和端口
        /// </summary>
        public string RemoteEndPoint { get; set; } = string.Empty;

        /// <summary>
        /// 连接时间
        /// </summary>
        public DateTime ConnectedTime { get; set; }

        /// <summary>
        /// 最后活动时间
        /// </summary>
        public DateTime LastActivityTime { get; set; }
    }
}
  • [ ] Step 4: 创建实例状态实体
using WIDESEAWCS_S7Simulator.Core.Enums;

namespace WIDESEAWCS_S7Simulator.Core.Entities
{
    /// <summary>
    /// S7服务器实例状态
    /// </summary>
    public class InstanceState
    {
        /// <summary>
        /// 实例ID
        /// </summary>
        public string InstanceId { get; set; } = string.Empty;

        /// <summary>
        /// 运行状态
        /// </summary>
        public InstanceStatus Status { get; set; }

        /// <summary>
        /// 当前连接的客户端数量
        /// </summary>
        public int ClientCount { get; set; }

        /// <summary>
        /// 累计处理请求数
        /// </summary>
        public long TotalRequests { get; set; }

        /// <summary>
        /// 启动时间
        /// </summary>
        public DateTime? StartTime { get; set; }

        /// <summary>
        /// 最后活动时间
        /// </summary>
        public DateTime? LastActivityTime { get; set; }

        /// <summary>
        /// 连接的客户端列表
        /// </summary>
        public List<S7ClientConnection> Clients { get; set; } = new();

        /// <summary>
        /// 错误信息(当状态为Error时)
        /// </summary>
        public string? ErrorMessage { get; set; }
    }
}
  • [ ] Step 5: 验证构建
cd WIDESEAWCS_S7Simulator.Core
dotnet build
  • [ ] Step 6: 提交核心实体
git add .
git commit -m "feat: add core entities

- Add MemoryRegionConfig for memory region sizes
- Add InstanceConfig for server configuration
- Add S7ClientConnection for client info
- Add InstanceState for server state tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Chunk 3: 内存存储实现

Task 4: 创建内存区域接口和基类

Files:
- Create: WIDESEAWCS_S7Simulator.Core/Interfaces/IMemoryRegion.cs
- Create: WIDESEAWCS_S7Simulator.Core/Memory/MemoryRegion.cs

  • [ ] Step 1: 创建内存区域接口
namespace WIDESEAWCS_S7Simulator.Core.Interfaces
{
    /// <summary>
    /// 内存区域接口
    /// </summary>
    public interface IMemoryRegion
    {
        /// <summary>
        /// 区域类型(M/DB/I/Q/T/C)
        /// </summary>
        string RegionType { get; }

        /// <summary>
        /// 区域大小(字节)
        /// </summary>
        int Size { get; }

        /// <summary>
        /// 读取字节数据
        /// </summary>
        /// <param name="offset">偏移量</param>
        /// <param name="length">长度</param>
        /// <returns>字节数组</returns>
        byte[] Read(ushort offset, ushort length);

        /// <summary>
        /// 写入字节数据
        /// </summary>
        /// <param name="offset">偏移量</param>
        /// <param name="data">数据</param>
        void Write(ushort offset, byte[] data);

        /// <summary>
        /// 清空区域
        /// </summary>
        void Clear();
    }
}
  • [ ] Step 2: 创建内存区域基类
using System.Threading;
using WIDESEAWCS_S7Simulator.Core.Interfaces;

namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// 内存区域基类
    /// </summary>
    public abstract class MemoryRegion : IMemoryRegion
    {
        /// <summary>
        /// 内存数据
        /// </summary>
        protected readonly byte[] _memory;

        /// <summary>
        /// 读写锁(支持并发访问)
        /// </summary>
        protected readonly ReaderWriterLockSlim _lock;

        /// <summary>
        /// 区域类型
        /// </summary>
        public abstract string RegionType { get; }

        /// <summary>
        /// 区域大小(字节)
        /// </summary>
        public int Size { get; }

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="size">区域大小</param>
        protected MemoryRegion(int size)
        {
            Size = size;
            _memory = new byte[size];
            _lock = new ReaderWriterLockSlim();
        }

        /// <summary>
        /// 读取字节数据
        /// </summary>
        public virtual byte[] Read(ushort offset, ushort length)
        {
            _lock.EnterReadLock();
            try
            {
                if (offset + length > Size)
                    throw new ArgumentOutOfRangeException(
                        $"读取超出{RegionType}区范围: offset={offset}, length={length}, size={Size}");

                byte[] result = new byte[length];
                Array.Copy(_memory, offset, result, 0, length);
                return result;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// 写入字节数据
        /// </summary>
        public virtual void Write(ushort offset, byte[] data)
        {
            _lock.EnterWriteLock();
            try
            {
                if (offset + data.Length > Size)
                    throw new ArgumentOutOfRangeException(
                        $"写入超出{RegionType}区范围: offset={offset}, length={data.Length}, size={Size}");

                Array.Copy(data, 0, _memory, offset, data.Length);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        /// 清空区域
        /// </summary>
        public virtual void Clear()
        {
            _lock.EnterWriteLock();
            try
            {
                Array.Clear(_memory, 0, Size);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        /// 释放资源
        /// </summary>
        public virtual void Dispose()
        {
            _lock?.Dispose();
        }
    }
}
  • [ ] Step 3: 验证构建
cd WIDESEAWCS_S7Simulator.Core
dotnet build
  • [ ] Step 4: 提交内存区域基类
git add .
git commit -m "feat: add memory region interface and base class

- Add IMemoryRegion interface
- Add MemoryRegion base class with thread-safe read/write
- Support offset-based read/write operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 5: 实现M区(位存储器)

Files:
- Create: WIDESEAWCS_S7Simulator.Core/Memory/MRegion.cs
- Test: WIDESEAWCS_S7Simulator.UnitTests/Memory/MRegionTests.cs

  • [ ] Step 1: 编写M区测试
using Xunit;
using WIDESEAWCS_S7Simulator.Core.Memory;

namespace WIDESEAWCS_S7Simulator.UnitTests.Memory
{
    public class MRegionTests
    {
        [Fact]
        public void Constructor_WithValidSize_CreatesRegion()
        {
            // Arrange & Act
            var region = new MRegion(1024);

            // Assert
            Assert.Equal("M", region.RegionType);
            Assert.Equal(1024, region.Size);
        }

        [Fact]
        public void Read_WithinBounds_ReturnsData()
        {
            // Arrange
            var region = new MRegion(1024);
            var testData = new byte[] { 0x12, 0x34, 0x56, 0x78 };
            region.Write(0, testData);

            // Act
            var result = region.Read(0, 4);

            // Assert
            Assert.Equal(testData, result);
        }

        [Fact]
        public void Read_OutOfBounds_ThrowsArgumentOutOfRange()
        {
            // Arrange
            var region = new MRegion(100);

            // Act & Assert
            Assert.Throws<ArgumentOutOfRangeException>(() => region.Read(0, 101));
        }

        [Fact]
        public void Write_WithinBounds_WritesData()
        {
            // Arrange
            var region = new MRegion(1024);
            var testData = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };

            // Act
            region.Write(100, testData);
            var result = region.Read(100, 4);

            // Assert
            Assert.Equal(testData, result);
        }

        [Fact]
        public void Write_OutOfBounds_ThrowsArgumentOutOfRange()
        {
            // Arrange
            var region = new MRegion(100);
            var testData = new byte[] { 0x01, 0x02 };

            // Act & Assert
            Assert.Throws<ArgumentOutOfRangeException>(() => region.Write(99, testData));
        }

        [Fact]
        public void ReadBit_ValidBit_ReturnsCorrectValue()
        {
            // Arrange
            var region = new MRegion(1024);
            region.Write(0, new byte[] { 0xFF }); // 所有位为1

            // Act
            var result = region.ReadBit(0, 0);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void WriteBit_ValidBit_SetsCorrectValue()
        {
            // Arrange
            var region = new MRegion(1024);

            // Act
            region.WriteBit(0, 3, true);
            var result = region.ReadBit(0, 3);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void WriteBit_InvalidBitOffset_ThrowsArgumentOutOfRange()
        {
            // Arrange
            var region = new MRegion(1024);

            // Act & Assert
            Assert.Throws<ArgumentOutOfRangeException>(() => region.WriteBit(0, 8, true));
        }

        [Fact]
        public void Clear_ZerosAllMemory()
        {
            // Arrange
            var region = new MRegion(100);
            region.Write(0, new byte[] { 0xFF, 0xFF, 0xFF });

            // Act
            region.Clear();
            var result = region.Read(0, 3);

            // Assert
            Assert.Equal(new byte[] { 0, 0, 0 }, result);
        }

        [Fact]
        public void ConcurrentReadWrite_ThreadSafe()
        {
            // Arrange
            var region = new MRegion(1024);
            var exceptions = new System.Collections.Concurrent.ConcurrentBag<Exception>();
            var cts = new CancellationTokenSource();
            cts.CancelAfter(1000); // 1秒后取消

            // Act
            var writeTask = Task.Run(() =>
            {
                try
                {
                    var data = new byte[] { 0xAA, 0xBB };
                    while (!cts.Token.IsCancellationRequested)
                    {
                        region.Write(0, data);
                    }
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }, cts.Token);

            var readTask = Task.Run(() =>
            {
                try
                {
                    while (!cts.Token.IsCancellationRequested)
                    {
                        region.Read(0, 2);
                    }
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }, cts.Token);

            Task.WaitAll(writeTask, readTask);

            // Assert
            Assert.Empty(exceptions);
        }
    }
}
  • [ ] Step 2: 运行测试验证失败
cd WIDESEAWCS_S7Simulator.UnitTests
dotnet test Memory/MRegionTests.cs -v n

预期输出: 测试失败(MRegion类不存在)

  • [ ] Step 3: 实现MRegion类
using System;
using WIDESEAWCS_S7Simulator.Core.Interfaces;

namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// M区(位存储器/Merker)实现
    /// </summary>
    public class MRegion : MemoryRegion, IMemoryRegion
    {
        /// <summary>
        /// 区域类型
        /// </summary>
        public override string RegionType => "M";

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="size">区域大小(字节)</param>
        public MRegion(int size) : base(size)
        {
        }

        /// <summary>
        /// 读取位
        /// </summary>
        /// <param name="byteOffset">字节偏移</param>
        /// <param name="bitOffset">位偏移(0-7)</param>
        /// <returns>位值</returns>
        public bool ReadBit(ushort byteOffset, byte bitOffset)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            _lock.EnterReadLock();
            try
            {
                if (byteOffset >= Size)
                    throw new ArgumentOutOfRangeException(nameof(byteOffset), "字节偏移超出范围");

                return (_memory[byteOffset] & (1 << bitOffset)) != 0;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// 写入位
        /// </summary>
        /// <param name="byteOffset">字节偏移</param>
        /// <param name="bitOffset">位偏移(0-7)</param>
        /// <param name="value">位值</param>
        public void WriteBit(ushort byteOffset, byte bitOffset, bool value)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            _lock.EnterWriteLock();
            try
            {
                if (byteOffset >= Size)
                    throw new ArgumentOutOfRangeException(nameof(byteOffset), "字节偏移超出范围");

                if (value)
                    _memory[byteOffset] |= (byte)(1 << bitOffset);
                else
                    _memory[byteOffset] &= (byte)~(1 << bitOffset);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        /// 读取字(Word,2字节)
        /// </summary>
        public ushort ReadWord(ushort byteOffset)
        {
            var data = Read(byteOffset, 2);
            return (ushort)((data[0] << 8) | data[1]);
        }

        /// <summary>
        /// 写入字(Word,2字节)
        /// </summary>
        public void WriteWord(ushort byteOffset, ushort value)
        {
            var data = new byte[] { (byte)(value >> 8), (byte)(value & 0xFF) };
            Write(byteOffset, data);
        }

        /// <summary>
        /// 读取双字(DWord,4字节)
        /// </summary>
        public uint ReadDWord(ushort byteOffset)
        {
            var data = Read(byteOffset, 4);
            return (uint)((data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]);
        }

        /// <summary>
        /// 写入双字(DWord,4字节)
        /// </summary>
        public void WriteDWord(ushort byteOffset, uint value)
        {
            var data = new byte[] {
                (byte)(value >> 24),
                (byte)((value >> 16) & 0xFF),
                (byte)((value >> 8) & 0xFF),
                (byte)(value & 0xFF)
            };
            Write(byteOffset, data);
        }

        /// <summary>
        /// 读取整数(Int,2字节,有符号)
        /// </summary>
        public short ReadInt(ushort byteOffset)
        {
            return (short)ReadWord(byteOffset);
        }

        /// <summary>
        /// 写入整数(Int,2字节,有符号)
        /// </summary>
        public void WriteInt(ushort byteOffset, short value)
        {
            WriteWord(byteOffset, (ushort)value);
        }

        /// <summary>
        /// 读取双整数(DInt,4字节,有符号)
        /// </summary>
        public int ReadDInt(ushort byteOffset)
        {
            return (int)ReadDWord(byteOffset);
        }

        /// <summary>
        /// 写入双整数(DInt,4字节,有符号)
        /// </summary>
        public void WriteDInt(ushort byteOffset, int value)
        {
            WriteDWord(byteOffset, (uint)value);
        }

        /// <summary>
        /// 读取浮点数(Real,4字节)
        /// </summary>
        public float ReadReal(ushort byteOffset)
        {
            var bytes = Read(byteOffset, 4);
            return BitConverter.ToSingle(bytes, 0);
        }

        /// <summary>
        /// 写入浮点数(Real,4字节)
        /// </summary>
        public void WriteReal(ushort byteOffset, float value)
        {
            var bytes = BitConverter.GetBytes(value);
            Write(byteOffset, bytes);
        }
    }
}
  • [ ] Step 4: 运行测试验证通过
dotnet test Memory/MRegionTests.cs -v n

预期输出: 所有测试通过

  • [ ] Step 5: 提交M区实现
git add .
git commit -m "feat: implement M region (Merker memory)

- Add MRegion class with bit/word/dword/int/dint/real operations
- Support individual bit read/write with thread-safety
- Add comprehensive unit tests including concurrent access test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 6: 实现DB区(数据块)

Files:
- Create: WIDESEAWCS_S7Simulator.Core/Memory/DBRegion.cs
- Test: WIDESEAWCS_S7Simulator.UnitTests/Memory/DBRegionTests.cs

  • [ ] Step 1: 编写DB区测试
using Xunit;
using WIDESEAWCS_S7Simulator.Core.Memory;

namespace WIDESEAWCS_S7Simulator.UnitTests.Memory
{
    public class DBRegionTests
    {
        [Fact]
        public void Constructor_WithValidParameters_CreatesRegion()
        {
            // Arrange & Act
            var region = new DBRegion(100, 1024);

            // Assert
            Assert.Equal("DB", region.RegionType);
            Assert.Equal(100 * 1024, region.Size);
        }

        [Fact]
        public void Read_ValidDBNumberAndOffset_ReturnsData()
        {
            // Arrange
            var region = new DBRegion(10, 1024);
            var testData = new byte[] { 0x12, 0x34, 0x56, 0x78 };
            region.Write(1, 0, testData);

            // Act
            var result = region.Read(1, 0, 4);

            // Assert
            Assert.Equal(testData, result);
        }

        [Fact]
        public void Read_InvalidDBNumber_ThrowsArgumentException()
        {
            // Arrange
            var region = new DBRegion(10, 1024);

            // Act & Assert
            Assert.Throws<ArgumentException>(() => region.Read(99, 0, 1));
        }

        [Fact]
        public void Read_OutOfBounds_ThrowsArgumentOutOfRange()
        {
            // Arrange
            var region = new DBRegion(10, 100);

            // Act & Assert
            Assert.Throws<ArgumentOutOfRangeException>(() => region.Read(1, 0, 101));
        }

        [Fact]
        public void Write_MultipleDBs_StoresSeparately()
        {
            // Arrange
            var region = new DBRegion(10, 100);
            var data1 = new byte[] { 0x01, 0x02 };
            var data2 = new byte[] { 0x03, 0x04 };

            // Act
            region.Write(1, 0, data1);
            region.Write(2, 0, data2);

            var result1 = region.Read(1, 0, 2);
            var result2 = region.Read(2, 0, 2);

            // Assert
            Assert.Equal(data1, result1);
            Assert.Equal(data2, result2);
        }

        [Fact]
        public void Clear_AllBlocks_ZerosAllMemory()
        {
            // Arrange
            var region = new DBRegion(5, 100);
            region.Write(1, 0, new byte[] { 0xFF });
            region.Write(2, 0, new byte[] { 0xFF });

            // Act
            region.Clear();

            // Assert
            Assert.Equal(new byte[] { 0x00 }, region.Read(1, 0, 1));
            Assert.Equal(new byte[] { 0x00 }, region.Read(2, 0, 1));
        }

        [Fact]
        public void Read_IntType_ReturnsCorrectValue()
        {
            // Arrange
            var region = new DBRegion(10, 1024);
            region.WriteInt(1, 0, 12345);

            // Act
            var result = region.ReadInt(1, 0);

            // Assert
            Assert.Equal(12345, result);
        }

        [Fact]
        public void Read_DIntType_ReturnsCorrectValue()
        {
            // Arrange
            var region = new DBRegion(10, 1024);
            region.WriteDInt(1, 0, 123456789);

            // Act
            var result = region.ReadDInt(1, 0);

            // Assert
            Assert.Equal(123456789, result);
        }

        [Fact]
        public void Read_RealType_ReturnsCorrectValue()
        {
            // Arrange
            var region = new DBRegion(10, 1024);
            region.WriteReal(1, 0, 3.14159f);

            // Act
            var result = region.ReadReal(1, 0);

            // Assert
            Assert.Equal(3.14159f, result, 4);
        }

        [Fact]
        public void Read_BoolType_ReturnsCorrectValue()
        {
            // Arrange
            var region = new DBRegion(10, 1024);
            region.WriteBool(1, 0, 0, true);

            // Act
            var result = region.ReadBool(1, 0, 0);

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void ReadString_WriteAndReadString_ReturnsOriginalString()
        {
            // Arrange
            var region = new DBRegion(10, 1024);
            var testString = "Hello";

            // Act
            region.WriteString(1, 0, 254, testString); // 最大长度254
            var result = region.ReadString(1, 0, 254);

            // Assert
            Assert.Equal(testString, result);
        }
    }
}
  • [ ] Step 2: 运行测试验证失败
dotnet test Memory/DBRegionTests.cs -v n

预期输出: 测试失败

  • [ ] Step 3: 实现DBRegion类
using System;
using System.Collections.Generic;
using System.Text;
using WIDESEAWCS_S7Simulator.Core.Interfaces;

namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// DB区(数据块)实现
    /// </summary>
    public class DBRegion : MemoryRegion, IMemoryRegion
    {
        /// <summary>
        /// DB块字典(块号 -> 数据)
        /// </summary>
        private readonly Dictionary<ushort, byte[]> _blocks;

        /// <summary>
        /// DB块数量
        /// </summary>
        private readonly ushort _blockCount;

        /// <summary>
        /// 每个DB块大小
        /// </summary>
        private readonly int _blockSize;

        /// <summary>
        /// 区域类型
        /// </summary>
        public override string RegionType => "DB";

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="blockCount">DB块数量</param>
        /// <param name="blockSize">每个DB块大小</param>
        public DBRegion(ushort blockCount, int blockSize) : base(blockCount * blockSize)
        {
            _blockCount = blockCount;
            _blockSize = blockSize;
            _blocks = new Dictionary<ushort, byte[]>();

            // 预分配所有DB块
            for (ushort i = 1; i <= blockCount; i++)
            {
                _blocks[i] = new byte[blockSize];
            }
        }

        /// <summary>
        /// 读取指定DB块的数据
        /// </summary>
        /// <param name="dbNumber">DB块号</param>
        /// <param name="offset">块内偏移</param>
        /// <param name="length">长度</param>
        /// <returns>字节数组</returns>
        public byte[] Read(ushort dbNumber, ushort offset, ushort length)
        {
            _lock.EnterReadLock();
            try
            {
                if (!_blocks.ContainsKey(dbNumber))
                    throw new ArgumentException($"DB块不存在: DB{dbNumber}");

                if (offset + length > _blockSize)
                    throw new ArgumentOutOfRangeException(
                        $"读取超出DB{dbNumber}范围: offset={offset}, length={length}, size={_blockSize}");

                byte[] block = _blocks[dbNumber];
                byte[] result = new byte[length];
                Array.Copy(block, offset, result, 0, length);
                return result;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// 写入指定DB块的数据
        /// </summary>
        /// <param name="dbNumber">DB块号</param>
        /// <param name="offset">块内偏移</param>
        /// <param name="data">数据</param>
        public void Write(ushort dbNumber, ushort offset, byte[] data)
        {
            _lock.EnterWriteLock();
            try
            {
                if (!_blocks.ContainsKey(dbNumber))
                    throw new ArgumentException($"DB块不存在: DB{dbNumber}");

                if (offset + data.Length > _blockSize)
                    throw new ArgumentOutOfRangeException(
                        $"写入超出DB{dbNumber}范围: offset={offset}, length={data.Length}, size={_blockSize}");

                byte[] block = _blocks[dbNumber];
                Array.Copy(data, 0, block, offset, data.Length);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        /// 读取数据(默认DB1)
        /// </summary>
        public override byte[] Read(ushort offset, ushort length)
        {
            return Read(1, offset, length);
        }

        /// <summary>
        /// 写入数据(默认DB1)
        /// </summary>
        public override void Write(ushort offset, byte[] data)
        {
            Write(1, offset, data);
        }

        /// <summary>
        /// 清空所有DB块
        /// </summary>
        public override void Clear()
        {
            _lock.EnterWriteLock();
            try
            {
                foreach (var block in _blocks.Values)
                {
                    Array.Clear(block, 0, block.Length);
                }
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        /// 读取位
        /// </summary>
        public bool ReadBool(ushort dbNumber, ushort byteOffset, byte bitOffset)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            var data = Read(dbNumber, byteOffset, 1);
            return (data[0] & (1 << bitOffset)) != 0;
        }

        /// <summary>
        /// 写入位
        /// </summary>
        public void WriteBool(ushort dbNumber, ushort byteOffset, byte bitOffset, bool value)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            var data = Read(dbNumber, byteOffset, 1);
            if (value)
                data[0] |= (byte)(1 << bitOffset);
            else
                data[0] &= (byte)~(1 << bitOffset);

            Write(dbNumber, byteOffset, data);
        }

        /// <summary>
        /// 读取整数(Int,2字节)
        /// </summary>
        public short ReadInt(ushort dbNumber, ushort offset)
        {
            var data = Read(dbNumber, offset, 2);
            return (short)((data[0] << 8) | data[1]);
        }

        /// <summary>
        /// 写入整数(Int,2字节)
        /// </summary>
        public void WriteInt(ushort dbNumber, ushort offset, short value)
        {
            var data = new byte[] { (byte)(value >> 8), (byte)(value & 0xFF) };
            Write(dbNumber, offset, data);
        }

        /// <summary>
        /// 读取双整数(DInt,4字节)
        /// </summary>
        public int ReadDInt(ushort dbNumber, ushort offset)
        {
            var data = Read(dbNumber, offset, 4);
            return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
        }

        /// <summary>
        /// 写入双整数(DInt,4字节)
        /// </summary>
        public void WriteDInt(ushort dbNumber, ushort offset, int value)
        {
            var data = new byte[] {
                (byte)(value >> 24),
                (byte)((value >> 16) & 0xFF),
                (byte)((value >> 8) & 0xFF),
                (byte)(value & 0xFF)
            };
            Write(dbNumber, offset, data);
        }

        /// <summary>
        /// 读取浮点数(Real,4字节)
        /// </summary>
        public float ReadReal(ushort dbNumber, ushort offset)
        {
            var bytes = Read(dbNumber, offset, 4);
            return BitConverter.ToSingle(bytes, 0);
        }

        /// <summary>
        /// 写入浮点数(Real,4字节)
        /// </summary>
        public void WriteReal(ushort dbNumber, ushort offset, float value)
        {
            var bytes = BitConverter.GetBytes(value);
            Write(dbNumber, offset, bytes);
        }

        /// <summary>
        /// 读取字符串(西门子S7字符串格式:首字节是最大长度,第二字节是实际长度)
        /// </summary>
        public string ReadString(ushort dbNumber, ushort offset, byte maxLength)
        {
            var data = Read(dbNumber, offset, (ushort)(maxLength + 2));
            byte actualLength = data[1];

            if (actualLength > maxLength)
                actualLength = maxLength;

            return Encoding.ASCII.GetString(data, 2, actualLength);
        }

        /// <summary>
        /// 写入字符串(西门子S7字符串格式)
        /// </summary>
        public void WriteString(ushort dbNumber, ushort offset, byte maxLength, string value)
        {
            if (string.IsNullOrEmpty(value))
                value = string.Empty;

            byte[] bytes = Encoding.ASCII.GetBytes(value);
            byte length = (byte)Math.Min(bytes.Length, maxLength);

            byte[] data = new byte[maxLength + 2];
            data[0] = maxLength;
            data[1] = length;
            Array.Copy(bytes, 0, data, 2, length);

            Write(dbNumber, offset, data);
        }

        /// <summary>
        /// 释放资源
        /// </summary>
        public override void Dispose()
        {
            _blocks.Clear();
            base.Dispose();
        }
    }
}
  • [ ] Step 4: 运行测试验证通过
dotnet test Memory/DBRegionTests.cs -v n

预期输出: 所有测试通过

  • [ ] Step 5: 提交DB区实现
git add .
git commit -m "feat: implement DB region (Data Blocks)

- Add DBRegion class supporting multiple data blocks
- Support bool/int/dint/real/string data types
- Add comprehensive unit tests for all data types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 7: 实现I/Q/T/C区

Files:
- Create: WIDESEAWCS_S7Simulator.Core/Memory/IRegion.cs
- Create: WIDESEAWCS_S7Simulator.Core/Memory/QRegion.cs
- Create: WIDESEAWCS_S7Simulator.Core/Memory/TRegion.cs
- Create: WIDESEAWCS_S7Simulator.Core/Memory/CRegion.cs

  • [ ] Step 1: 实现I区(输入区)
namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// I区(输入区/Input)实现
    /// </summary>
    public class IRegion : MemoryRegion
    {
        /// <summary>
        /// 区域类型
        /// </summary>
        public override string RegionType => "I";

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="size">区域大小(字节)</param>
        public IRegion(int size) : base(size)
        {
        }

        /// <summary>
        /// 读取位
        /// </summary>
        public bool ReadBit(ushort byteOffset, byte bitOffset)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            _lock.EnterReadLock();
            try
            {
                if (byteOffset >= Size)
                    throw new ArgumentOutOfRangeException(nameof(byteOffset));

                return (_memory[byteOffset] & (1 << bitOffset)) != 0;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// 写入位
        /// </summary>
        public void WriteBit(ushort byteOffset, byte bitOffset, bool value)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            _lock.EnterWriteLock();
            try
            {
                if (byteOffset >= Size)
                    throw new ArgumentOutOfRangeException(nameof(byteOffset));

                if (value)
                    _memory[byteOffset] |= (byte)(1 << bitOffset);
                else
                    _memory[byteOffset] &= (byte)~(1 << bitOffset);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
    }
}
  • [ ] Step 2: 实现Q区(输出区)
namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// Q区(输出区/Output)实现
    /// </summary>
    public class QRegion : MemoryRegion
    {
        /// <summary>
        /// 区域类型
        /// </summary>
        public override string RegionType => "Q";

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="size">区域大小(字节)</param>
        public QRegion(int size) : base(size)
        {
        }

        /// <summary>
        /// 读取位
        /// </summary>
        public bool ReadBit(ushort byteOffset, byte bitOffset)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            _lock.EnterReadLock();
            try
            {
                if (byteOffset >= Size)
                    throw new ArgumentOutOfRangeException(nameof(byteOffset));

                return (_memory[byteOffset] & (1 << bitOffset)) != 0;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        /// 写入位
        /// </summary>
        public void WriteBit(ushort byteOffset, byte bitOffset, bool value)
        {
            if (bitOffset > 7)
                throw new ArgumentOutOfRangeException(nameof(bitOffset), "位偏移必须在0-7之间");

            _lock.EnterWriteLock();
            try
            {
                if (byteOffset >= Size)
                    throw new ArgumentOutOfRangeException(nameof(byteOffset));

                if (value)
                    _memory[byteOffset] |= (byte)(1 << bitOffset);
                else
                    _memory[byteOffset] &= (byte)~(1 << bitOffset);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
    }
}
  • [ ] Step 3: 实现T区(定时器区)
namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// T区(定时器区/Timer)实现
    /// </summary>
    public class TRegion : MemoryRegion
    {
        /// <summary>
        /// 区域类型
        /// </summary>
        public override string RegionType => "T";

        /// <summary>
        /// 每个定时器占用的字节数(S7定时器为2字节)
        /// </summary>
        private const int TimerSize = 2;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="timerCount">定时器数量</param>
        public TRegion(int timerCount) : base(timerCount * TimerSize)
        {
        }

        /// <summary>
        /// 读取定时器值
        /// </summary>
        /// <param name="timerNumber">定时器号</param>
        /// <returns>定时器值(毫秒)</returns>
        public ushort ReadTimer(ushort timerNumber)
        {
            ushort offset = (ushort)((timerNumber - 1) * TimerSize);
            var data = Read(offset, TimerSize);
            return (ushort)((data[0] << 8) | data[1]);
        }

        /// <summary>
        /// 写入定时器值
        /// </summary>
        /// <param name="timerNumber">定时器号</param>
        /// <param name="value">定时器值(毫秒)</param>
        public void WriteTimer(ushort timerNumber, ushort value)
        {
            ushort offset = (ushort)((timerNumber - 1) * TimerSize);
            var data = new byte[] { (byte)(value >> 8), (byte)(value & 0xFF) };
            Write(offset, data);
        }
    }
}
  • [ ] Step 4: 实现C区(计数器区)
namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// C区(计数器区/Counter)实现
    /// </summary>
    public class CRegion : MemoryRegion
    {
        /// <summary>
        /// 区域类型
        /// </summary>
        public override string RegionType => "C";

        /// <summary>
        /// 每个计数器占用的字节数(S7计数器为2字节)
        /// </summary>
        private const int CounterSize = 2;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="counterCount">计数器数量</param>
        public CRegion(int counterCount) : base(counterCount * CounterSize)
        {
        }

        /// <summary>
        /// 读取计数器值
        /// </summary>
        /// <param name="counterNumber">计数器号</param>
        /// <returns>计数器值</returns>
        public ushort ReadCounter(ushort counterNumber)
        {
            ushort offset = (ushort)((counterNumber - 1) * CounterSize);
            var data = Read(offset, CounterSize);
            return (ushort)((data[0] << 8) | data[1]);
        }

        /// <summary>
        /// 写入计数器值
        /// </summary>
        /// <param name="counterNumber">计数器号</param>
        /// <param name="value">计数器值</param>
        public void WriteCounter(ushort counterNumber, ushort value)
        {
            ushort offset = (ushort)((counterNumber - 1) * CounterSize);
            var data = new byte[] { (byte)(value >> 8), (byte)(value & 0xFF) };
            Write(offset, data);
        }
    }
}
  • [ ] Step 5: 验证构建
cd WIDESEAWCS_S7Simulator.Core
dotnet build
  • [ ] Step 6: 提交I/Q/T/C区实现
git add .
git commit -m "feat: implement I/Q/T/C regions

- Add IRegion (Input) with bit operations
- Add QRegion (Output) with bit operations
- Add TRegion (Timer) with timer read/write
- Add CRegion (Counter) with counter read/write

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 8: 实现内存存储

Files:
- Create: WIDESEAWCS_S7Simulator.Core/Interfaces/IMemoryStore.cs
- Create: WIDESEAWCS_S7Simulator.Core/Memory/MemoryStore.cs
- Test: WIDESEAWCS_S7Simulator.UnitTests/Memory/MemoryStoreTests.cs

  • [ ] Step 1: 创建内存存储接口
namespace WIDESEAWCS_S7Simulator.Core.Interfaces
{
    /// <summary>
    /// 内存存储接口
    /// </summary>
    public interface IMemoryStore
    {
        /// <summary>
        /// 读取字节数据
        /// </summary>
        /// <param name="address">地址(如 "M100", "DB1.DBD0")</param>
        /// <param name="length">长度</param>
        /// <returns>字节数组</returns>
        byte[] ReadBytes(string address, ushort length);

        /// <summary>
        /// 读取指定类型数据
        /// </summary>
        T Read<T>(string address) where T : struct;

        /// <summary>
        /// 写入字节数据
        /// </summary>
        /// <param name="address">地址</param>
        /// <param name="data">数据</param>
        void WriteBytes(string address, byte[] data);

        /// <summary>
        /// 写入指定类型数据
        /// </summary>
        void Write<T>(string address, T value) where T : struct;

        /// <summary>
        /// 获取内存区域
        /// </summary>
        IMemoryRegion GetRegion(string regionType);

        /// <summary>
        /// 清空所有内存
        /// </summary>
        void Clear();

        /// <summary>
        /// 导出内存数据
        /// </summary>
        Dictionary<string, byte[]> Export();

        /// <summary>
        /// 导入内存数据
        /// </summary>
        void Import(Dictionary<string, byte[]> data);
    }
}
  • [ ] Step 2: 编写内存存储测试
using Xunit;
using WIDESEAWCS_S7Simulator.Core.Memory;
using WIDESEAWCS_S7Simulator.Core.Entities;

namespace WIDESEAWCS_S7Simulator.UnitTests.Memory
{
    public class MemoryStoreTests
    {
        [Fact]
        public void Constructor_WithConfig_CreatesAllRegions()
        {
            // Arrange
            var config = new MemoryRegionConfig
            {
                MRegionSize = 1024,
                DBBlockCount = 10,
                DBBlockSize = 1024,
                IRegionSize = 256,
                QRegionSize = 256,
                TRegionCount = 32,
                CRegionCount = 32
            };

            // Act
            var store = new MemoryStore(config);

            // Assert
            Assert.NotNull(store.GetRegion("M"));
            Assert.NotNull(store.GetRegion("DB"));
            Assert.NotNull(store.GetRegion("I"));
            Assert.NotNull(store.GetRegion("Q"));
            Assert.NotNull(store.GetRegion("T"));
            Assert.NotNull(store.GetRegion("C"));
        }

        [Fact]
        public void WriteBytes_MRegion_WritesToMRegion()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());
            var data = new byte[] { 0x12, 0x34 };

            // Act
            store.WriteBytes("M100", data);
            var result = store.ReadBytes("M100", 2);

            // Assert
            Assert.Equal(data, result);
        }

        [Fact]
        public void Write_ReadInt_ReturnsCorrectValue()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());

            // Act
            store.Write(100, 12345);
            var result = store.Read<int>("M100");

            // Assert
            Assert.Equal(12345, result);
        }

        [Fact]
        public void Write_ReadDInt_ReturnsCorrectValue()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());

            // Act
            store.Write("M100", 123456789);
            var result = store.Read<int>("M100");

            // Assert
            Assert.Equal(123456789, result);
        }

        [Fact]
        public void Write_ReadReal_ReturnsCorrectValue()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());

            // Act
            store.Write("M100", 3.14159f);
            var result = store.Read<float>("M100");

            // Assert
            Assert.Equal(3.14159f, result, 4);
        }

        [Fact]
        public void WriteBytes_DBRegion_WritesToSpecificDB()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());
            var data = new byte[] { 0xAA, 0xBB };

            // Act
            store.WriteBytes("DB1.DBD0", data);
            var result = store.ReadBytes("DB1.DBD0", 2);

            // Assert
            Assert.Equal(data, result);
        }

        [Fact]
        public void Write_IRegion_WritesToIRegion()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());

            // Act
            store.Write("I0", true);
            var result = store.Read<bool>("I0");

            // Assert
            Assert.True(result);
        }

        [Fact]
        public void Clear_AllRegions_ZerosAllMemory()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());
            store.Write("M100", 12345);

            // Act
            store.Clear();
            var result = store.Read<int>("M100");

            // Assert
            Assert.Equal(0, result);
        }

        [Fact]
        public void ExportImport_PreservesAllMemory()
        {
            // Arrange
            var store = new MemoryStore(new MemoryRegionConfig());
            store.Write("M100", 12345);
            store.Write("DB1.DBD0", 67890);

            // Act
            var exported = store.Export();
            var newStore = new MemoryStore(new MemoryRegionConfig());
            newStore.Import(exported);

            // Assert
            Assert.Equal(12345, newStore.Read<int>("M100"));
            Assert.Equal(67890, newStore.Read<int>("DB1.DBD0"));
        }
    }
}
  • [ ] Step 3: 运行测试验证失败
dotnet test Memory/MemoryStoreTests.cs -v n

预期输出: 测试失败

  • [ ] Step 4: 实现MemoryStore类
using System.Collections.Generic;
using System.Text.RegularExpressions;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Interfaces;

namespace WIDESEAWCS_S7Simulator.Core.Memory
{
    /// <summary>
    /// 内存存储实现
    /// </summary>
    public class MemoryStore : IMemoryStore
    {
        /// <summary>
        /// 内存区域字典
        /// </summary>
        private readonly Dictionary<string, IMemoryRegion> _regions;

        /// <summary>
        /// 内存配置
        /// </summary>
        private readonly MemoryRegionConfig _config;

        /// <summary>
        /// 地址解析正则表达式
        /// </summary>
        private static readonly Regex AddressRegex = new Regex(
            @"^(M|DB(\d+)\.)?(I|Q|T|C)?(\d+)(?:\.([BWD]))?$",
            RegexOptions.IgnoreCase | RegexOptions.Compiled);

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="config">内存配置</param>
        public MemoryStore(MemoryRegionConfig config)
        {
            _config = config;
            _regions = new Dictionary<string, IMemoryRegion>();
            InitializeRegions();
        }

        /// <summary>
        /// 初始化所有内存区域
        /// </summary>
        private void InitializeRegions()
        {
            _regions["M"] = new MRegion(_config.MRegionSize);
            _regions["DB"] = new DBRegion(
                (ushort)_config.DBBlockCount,
                _config.DBBlockSize);
            _regions["I"] = new IRegion(_config.IRegionSize);
            _regions["Q"] = new QRegion(_config.QRegionSize);
            _regions["T"] = new TRegion(_config.TRegionCount);
            _regions["C"] = new CRegion(_config.CRegionCount);
        }

        /// <summary>
        /// 获取内存区域
        /// </summary>
        public IMemoryRegion GetRegion(string regionType)
        {
            return _regions.TryGetValue(regionType.ToUpper(), out var region)
                ? region
                : throw new KeyNotFoundException($"内存区域不存在: {regionType}");
        }

        /// <summary>
        /// 读取字节数据
        /// </summary>
        public byte[] ReadBytes(string address, ushort length)
        {
            var (regionType, offset, dbNumber) = ParseAddress(address);
            var region = GetRegion(regionType);

            if (regionType == "DB" && dbNumber.HasValue)
            {
                return ((DBRegion)region).Read(dbNumber.Value, offset, length);
            }

            return region.Read(offset, length);
        }

        /// <summary>
        /// 读取指定类型数据
        /// </summary>
        public T Read<T>(string address) where T : struct
        {
            var type = typeof(T);
            var (regionType, offset, dbNumber) = ParseAddress(address);
            var region = GetRegion(regionType);

            if (type == typeof(bool))
            {
                byte bitOffset = (byte)(offset % 8);
                ushort byteOffset = (ushort)(offset / 8);

                if (regionType == "M")
                    return (T)(object)((MRegion)region).ReadBit(byteOffset, bitOffset);
                else if (regionType == "I")
                    return (T)(object)((IRegion)region).ReadBit(byteOffset, bitOffset);
                else if (regionType == "Q")
                    return (T)(object)((QRegion)region).ReadBit(byteOffset, bitOffset);
                else if (regionType == "DB" && dbNumber.HasValue)
                    return (T)(object)((DBRegion)region).ReadBool(dbNumber.Value, byteOffset, bitOffset);
            }
            else if (type == typeof(short) || type == typeof(int))
            {
                if (regionType == "M")
                    return (T)(object)((MRegion)region).ReadInt(offset);
                else if (regionType == "DB" && dbNumber.HasValue)
                    return (T)(object)((DBRegion)region).ReadInt(dbNumber.Value, offset);
            }
            else if (type == typeof(int))
            {
                if (regionType == "M")
                    return (T)(object)((MRegion)region).ReadDInt(offset);
                else if (regionType == "DB" && dbNumber.HasValue)
                    return (T)(object)((DBRegion)region).ReadDInt(dbNumber.Value, offset);
            }
            else if (type == typeof(float))
            {
                if (regionType == "M")
                    return (T)(object)((MRegion)region).ReadReal(offset);
                else if (regionType == "DB" && dbNumber.HasValue)
                    return (T)(object)((DBRegion)region).ReadReal(dbNumber.Value, offset);
            }

            // 默认读取字节
            var bytes = ReadBytes(address, 2);
            if (type == typeof(short))
                return (T)(object)((short)((bytes[0] << 8) | bytes[1]));

            throw new NotSupportedException($"不支持的类型: {type.Name}");
        }

        /// <summary>
        /// 写入字节数据
        /// </summary>
        public void WriteBytes(string address, byte[] data)
        {
            var (regionType, offset, dbNumber) = ParseAddress(address);
            var region = GetRegion(regionType);

            if (regionType == "DB" && dbNumber.HasValue)
            {
                ((DBRegion)region).Write(dbNumber.Value, offset, data);
            }
            else
            {
                region.Write(offset, data);
            }
        }

        /// <summary>
        /// 写入指定类型数据
        /// </summary>
        public void Write<T>(string address, T value) where T : struct
        {
            var type = typeof(T);
            var (regionType, offset, dbNumber) = ParseAddress(address);
            var region = GetRegion(regionType);

            if (type == typeof(bool))
            {
                byte bitOffset = (byte)(offset % 8);
                ushort byteOffset = (ushort)(offset / 8);
                bool boolValue = (bool)(object)value;

                if (regionType == "M")
                    ((MRegion)region).WriteBit(byteOffset, bitOffset, boolValue);
                else if (regionType == "I")
                    ((IRegion)region).WriteBit(byteOffset, bitOffset, boolValue);
                else if (regionType == "Q")
                    ((QRegion)region).WriteBit(byteOffset, bitOffset, boolValue);
                else if (regionType == "DB" && dbNumber.HasValue)
                    ((DBRegion)region).WriteBool(dbNumber.Value, byteOffset, bitOffset, boolValue);
            }
            else if (type == typeof(short))
            {
                short shortValue = (short)(object)value;
                if (regionType == "M")
                    ((MRegion)region).WriteInt(offset, shortValue);
                else if (regionType == "DB" && dbNumber.HasValue)
                    ((DBRegion)region).WriteInt(dbNumber.Value, offset, shortValue);
            }
            else if (type == typeof(int))
            {
                int intValue = (int)(object)value;
                if (regionType == "M")
                    ((MRegion)region).WriteDInt(offset, intValue);
                else if (regionType == "DB" && dbNumber.HasValue)
                    ((DBRegion)region).WriteDInt(dbNumber.Value, offset, intValue);
            }
            else if (type == typeof(float))
            {
                float floatValue = (float)(object)value;
                if (regionType == "M")
                    ((MRegion)region).WriteReal(offset, floatValue);
                else if (regionType == "DB" && dbNumber.HasValue)
                    ((DBRegion)region).WriteReal(dbNumber.Value, offset, floatValue);
            }
            else
            {
                throw new NotSupportedException($"不支持的类型: {type.Name}");
            }
        }

        /// <summary>
        /// 清空所有内存
        /// </summary>
        public void Clear()
        {
            foreach (var region in _regions.Values)
            {
                region.Clear();
            }
        }

        /// <summary>
        /// 导出内存数据
        /// </summary>
        public Dictionary<string, byte[]> Export()
        {
            var data = new Dictionary<string, byte[]>();

            foreach (var kvp in _regions)
            {
                var region = kvp.Value;
                var regionData = region.Read(0, (ushort)region.Size);
                data[$"{kvp.Key}_data"] = regionData;
            }

            return data;
        }

        /// <summary>
        /// 导入内存数据
        /// </summary>
        public void Import(Dictionary<string, byte[]> data)
        {
            foreach (var kvp in data)
            {
                if (kvp.Key.EndsWith("_data"))
                {
                    var regionType = kvp.Key.Replace("_data", "");
                    if (_regions.ContainsKey(regionType))
                    {
                        _regions[regionType].Write(0, kvp.Value);
                    }
                }
            }
        }

        /// <summary>
        /// 解析地址
        /// </summary>
        /// <param name="address">地址字符串</param>
        /// <returns>(区域类型, 偏移量, DB块号)</returns>
        private (string regionType, ushort offset, ushort? dbNumber) ParseAddress(string address)
        {
            address = address.ToUpper().Trim();

            // 解析 M100
            if (address.StartsWith("M"))
            {
                var offset = ushort.Parse(address.Substring(1));
                return ("M", offset, null);
            }

            // 解析 I0, Q0, T1, C1
            if (address.StartsWith("I"))
                return ("I", ushort.Parse(address.Substring(1)), null);
            if (address.StartsWith("Q"))
                return ("Q", ushort.Parse(address.Substring(1)), null);
            if (address.StartsWith("T"))
                return ("T", ushort.Parse(address.Substring(1)), null);
            if (address.StartsWith("C"))
                return ("C", ushort.Parse(address.Substring(1)), null);

            // 解析 DB1.DBD0 或 DB1.DBW0
            if (address.StartsWith("DB"))
            {
                var parts = address.Substring(2).Split('.');
                var dbNumber = ushort.Parse(parts[0]);
                var offsetPart = parts[1];

                // DBD = DWord (4字节), DBW = Word (2字节), DBB = Byte (1字节)
                ushort offset;
                if (offsetPart.StartsWith("DBD"))
                    offset = ushort.Parse(offsetPart.Substring(3));
                else if (offsetPart.StartsWith("DBW"))
                    offset = ushort.Parse(offsetPart.Substring(3));
                else if (offsetPart.StartsWith("DBB"))
                    offset = ushort.Parse(offsetPart.Substring(3));
                else if (offsetPart.StartsWith("DBX"))
                {
                    // DBX = Bit (位地址)
                    var bitAddress = offsetPart.Substring(3).Split('.');
                    offset = ushort.Parse(bitAddress[0]);
                    // 转换为字节偏移+位偏移
                    offset = (ushort)(offset * 8 + byte.Parse(bitAddress[1]));
                }
                else
                    offset = ushort.Parse(offsetPart);

                return ("DB", offset, dbNumber);
            }

            throw new ArgumentException($"无效的地址格式: {address}");
        }

        /// <summary>
        /// 释放资源
        /// </summary>
        public void Dispose()
        {
            foreach (var region in _regions.Values)
            {
                if (region is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
            _regions.Clear();
        }
    }
}
  • [ ] Step 5: 运行测试验证通过
dotnet test Memory/MemoryStoreTests.cs -v n

预期输出: 所有测试通过

  • [ ] Step 6: 提交内存存储实现
git add .
git commit -m "feat: implement MemoryStore with address parsing

- Add MemoryStore class managing all memory regions
- Support M/DB/I/Q/T/C address parsing
- Support bool/short/int/float data types
- Add export/import functionality for persistence
- Add comprehensive unit tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

计划继续中... 由于计划很长,我将分多个chunk保存。这是Chunk 1-3,涵盖了项目基础设施、核心实体和内存存储实现。

是否继续执行下一部分(持久化服务和服务器实例)?