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
| 文件 | 职责 |
|---|---|
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 |
实例管理器实现 |
| 文件 | 职责 |
|---|---|
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配置 |
| 文件 | 职责 |
|---|---|
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 |
程序入口 |
| 文件 | 职责 |
|---|---|
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文件 |
| 文件 | 职责 |
|---|---|
Memory/MRegionTests.cs |
M区单元测试 |
Memory/DBRegionTests.cs |
DB区单元测试 |
Memory/MemoryStoreTests.cs |
内存存储单元测试 |
Server/S7ServerInstanceTests.cs |
服务器实例单元测试 |
Persistence/FilePersistenceServiceTests.cs |
持久化服务单元测试 |
cd D:\Git\ShanMeiXinNengYuan\Code\WCS
dotnet new sln -n WIDESEAWCS_S7Simulator
预期输出: 创建 WIDESEAWCS_S7Simulator.sln
cd WIDESEAWCS_S7Simulator
dotnet new classlib -n WIDESEAWCS_S7Simulator.Core -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Core/WIDESEAWCS_S7Simulator.Core.csproj
dotnet new classlib -n WIDESEAWCS_S7Simulator.Application -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Application/WIDESEAWCS_S7Simulator.Application.csproj
dotnet new webapi -n WIDESEAWCS_S7Simulator.Server -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Server/WIDESEAWCS_S7Simulator.Server.csproj
dotnet new webapp -n WIDESEAWCS_S7Simulator.Web -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.Web/WIDESEAWCS_S7Simulator.Web.csproj
dotnet new xunit -n WIDESEAWCS_S7Simulator.UnitTests -f net6.0
dotnet sln add WIDESEAWCS_S7Simulator.UnitTests/WIDESEAWCS_S7Simulator.UnitTests.csproj
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
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
cd ../WIDESEAWCS_S7Simulator.Server
dotnet add package Serilog.AspNetCore
dotnet add package AutoMapper
dotnet add package Microsoft.AspNetCore.SignalR
cd ../WIDESEAWCS_S7Simulator.Web
dotnet add package Microsoft.AspNetCore.SignalR.Client
cd ..
dotnet build
预期输出: 构建成功
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>"
Files:
- Create: WIDESEAWCS_S7Simulator.Core/Enums/SiemensPLCType.cs
- Create: WIDESEAWCS_S7Simulator.Core/Enums/InstanceStatus.cs
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
}
}
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
}
}
rm WIDESEAWCS_S7Simulator.Core/Class1.cs
cd WIDESEAWCS_S7Simulator.Core
dotnet build
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>"
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
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;
}
}
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();
}
}
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; }
}
}
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; }
}
}
cd WIDESEAWCS_S7Simulator.Core
dotnet build
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>"
Files:
- Create: WIDESEAWCS_S7Simulator.Core/Interfaces/IMemoryRegion.cs
- Create: WIDESEAWCS_S7Simulator.Core/Memory/MemoryRegion.cs
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();
}
}
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();
}
}
}
cd WIDESEAWCS_S7Simulator.Core
dotnet build
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>"
Files:
- Create: WIDESEAWCS_S7Simulator.Core/Memory/MRegion.cs
- Test: WIDESEAWCS_S7Simulator.UnitTests/Memory/MRegionTests.cs
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);
}
}
}
cd WIDESEAWCS_S7Simulator.UnitTests
dotnet test Memory/MRegionTests.cs -v n
预期输出: 测试失败(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);
}
}
}
dotnet test Memory/MRegionTests.cs -v n
预期输出: 所有测试通过
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>"
Files:
- Create: WIDESEAWCS_S7Simulator.Core/Memory/DBRegion.cs
- Test: WIDESEAWCS_S7Simulator.UnitTests/Memory/DBRegionTests.cs
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);
}
}
}
dotnet test Memory/DBRegionTests.cs -v n
预期输出: 测试失败
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();
}
}
}
dotnet test Memory/DBRegionTests.cs -v n
预期输出: 所有测试通过
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>"
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
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();
}
}
}
}
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();
}
}
}
}
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);
}
}
}
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);
}
}
}
cd WIDESEAWCS_S7Simulator.Core
dotnet build
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>"
Files:
- Create: WIDESEAWCS_S7Simulator.Core/Interfaces/IMemoryStore.cs
- Create: WIDESEAWCS_S7Simulator.Core/Memory/MemoryStore.cs
- Test: WIDESEAWCS_S7Simulator.UnitTests/Memory/MemoryStoreTests.cs
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);
}
}
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"));
}
}
}
dotnet test Memory/MemoryStoreTests.cs -v n
预期输出: 测试失败
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();
}
}
}
dotnet test Memory/MemoryStoreTests.cs -v n
预期输出: 所有测试通过
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,涵盖了项目基础设施、核心实体和内存存储实现。
是否继续执行下一部分(持久化服务和服务器实例)?