| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # 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模æå¨è§£å³æ¹æ¡** |
| | | |
| | | ```bash |
| | | cd D:\Git\ShanMeiXinNengYuan\Code\WCS |
| | | dotnet new sln -n WIDESEAWCS_S7Simulator |
| | | ``` |
| | | |
| | | 颿è¾åº: å建 `WIDESEAWCS_S7Simulator.sln` |
| | | |
| | | - [ ] **Step 2: å建Core项ç®** |
| | | |
| | | ```bash |
| | | 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项ç®** |
| | | |
| | | ```bash |
| | | dotnet new classlib -n WIDESEAWCS_S7Simulator.Application -f net6.0 |
| | | dotnet sln add WIDESEAWCS_S7Simulator.Application/WIDESEAWCS_S7Simulator.Application.csproj |
| | | ``` |
| | | |
| | | - [ ] **Step 4: å建Server项ç®** |
| | | |
| | | ```bash |
| | | dotnet new webapi -n WIDESEAWCS_S7Simulator.Server -f net6.0 |
| | | dotnet sln add WIDESEAWCS_S7Simulator.Server/WIDESEAWCS_S7Simulator.Server.csproj |
| | | ``` |
| | | |
| | | - [ ] **Step 5: å建Web项ç®** |
| | | |
| | | ```bash |
| | | dotnet new webapp -n WIDESEAWCS_S7Simulator.Web -f net6.0 |
| | | dotnet sln add WIDESEAWCS_S7Simulator.Web/WIDESEAWCS_S7Simulator.Web.csproj |
| | | ``` |
| | | |
| | | - [ ] **Step 6: å建Tests项ç®** |
| | | |
| | | ```bash |
| | | dotnet new xunit -n WIDESEAWCS_S7Simulator.UnitTests -f net6.0 |
| | | dotnet sln add WIDESEAWCS_S7Simulator.UnitTests/WIDESEAWCS_S7Simulator.UnitTests.csproj |
| | | ``` |
| | | |
| | | - [ ] **Step 7: æ·»å 项ç®å¼ç¨** |
| | | |
| | | ```bash |
| | | 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项ç®** |
| | | |
| | | ```bash |
| | | 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项ç®** |
| | | |
| | | ```bash |
| | | cd ../WIDESEAWCS_S7Simulator.Server |
| | | dotnet add package Serilog.AspNetCore |
| | | dotnet add package AutoMapper |
| | | dotnet add package Microsoft.AspNetCore.SignalR |
| | | ``` |
| | | |
| | | - [ ] **Step 10: æ·»å NuGetå
å°Web项ç®** |
| | | |
| | | ```bash |
| | | cd ../WIDESEAWCS_S7Simulator.Web |
| | | dotnet add package Microsoft.AspNetCore.SignalR.Client |
| | | ``` |
| | | |
| | | - [ ] **Step 11: éªè¯è§£å³æ¹æ¡æå»º** |
| | | |
| | | ```bash |
| | | cd .. |
| | | dotnet build |
| | | ``` |
| | | |
| | | 颿è¾åº: æå»ºæå |
| | | |
| | | - [ ] **Step 12: æäº¤é¡¹ç®ç»æ** |
| | | |
| | | ```bash |
| | | 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ç±»åæä¸¾** |
| | | |
| | | ```csharp |
| | | 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: å建å®ä¾ç¶ææä¸¾** |
| | | |
| | | ```csharp |
| | | 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æä»¶** |
| | | |
| | | ```bash |
| | | rm WIDESEAWCS_S7Simulator.Core/Class1.cs |
| | | ``` |
| | | |
| | | - [ ] **Step 4: éªè¯æå»º** |
| | | |
| | | ```bash |
| | | cd WIDESEAWCS_S7Simulator.Core |
| | | dotnet build |
| | | ``` |
| | | |
| | | - [ ] **Step 5: æäº¤æä¸¾ç±»å** |
| | | |
| | | ```bash |
| | | 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: å建å
ååºåé
ç½®å®ä½** |
| | | |
| | | ```csharp |
| | | 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: å建å®ä¾é
ç½®å®ä½** |
| | | |
| | | ```csharp |
| | | 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: å建客æ·ç«¯è¿æ¥å®ä½** |
| | | |
| | | ```csharp |
| | | 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: å建å®ä¾ç¶æå®ä½** |
| | | |
| | | ```csharp |
| | | 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: éªè¯æå»º** |
| | | |
| | | ```bash |
| | | cd WIDESEAWCS_S7Simulator.Core |
| | | dotnet build |
| | | ``` |
| | | |
| | | - [ ] **Step 6: æäº¤æ ¸å¿å®ä½** |
| | | |
| | | ```bash |
| | | 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: å建å
ååºåæ¥å£** |
| | | |
| | | ```csharp |
| | | 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: å建å
ååºååºç±»** |
| | | |
| | | ```csharp |
| | | 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: éªè¯æå»º** |
| | | |
| | | ```bash |
| | | cd WIDESEAWCS_S7Simulator.Core |
| | | dotnet build |
| | | ``` |
| | | |
| | | - [ ] **Step 4: æäº¤å
ååºååºç±»** |
| | | |
| | | ```bash |
| | | 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åºæµè¯** |
| | | |
| | | ```csharp |
| | | 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: è¿è¡æµè¯éªè¯å¤±è´¥** |
| | | |
| | | ```bash |
| | | cd WIDESEAWCS_S7Simulator.UnitTests |
| | | dotnet test Memory/MRegionTests.cs -v n |
| | | ``` |
| | | |
| | | 颿è¾åº: æµè¯å¤±è´¥ï¼MRegionç±»ä¸åå¨ï¼ |
| | | |
| | | - [ ] **Step 3: å®ç°MRegionç±»** |
| | | |
| | | ```csharp |
| | | 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: è¿è¡æµè¯éªè¯éè¿** |
| | | |
| | | ```bash |
| | | dotnet test Memory/MRegionTests.cs -v n |
| | | ``` |
| | | |
| | | 颿è¾åº: æææµè¯éè¿ |
| | | |
| | | - [ ] **Step 5: æäº¤Måºå®ç°** |
| | | |
| | | ```bash |
| | | 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åºæµè¯** |
| | | |
| | | ```csharp |
| | | 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: è¿è¡æµè¯éªè¯å¤±è´¥** |
| | | |
| | | ```bash |
| | | dotnet test Memory/DBRegionTests.cs -v n |
| | | ``` |
| | | |
| | | 颿è¾åº: æµè¯å¤±è´¥ |
| | | |
| | | - [ ] **Step 3: å®ç°DBRegionç±»** |
| | | |
| | | ```csharp |
| | | 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: è¿è¡æµè¯éªè¯éè¿** |
| | | |
| | | ```bash |
| | | dotnet test Memory/DBRegionTests.cs -v n |
| | | ``` |
| | | |
| | | 颿è¾åº: æææµè¯éè¿ |
| | | |
| | | - [ ] **Step 5: æäº¤DBåºå®ç°** |
| | | |
| | | ```bash |
| | | 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åºï¼è¾å
¥åºï¼** |
| | | |
| | | ```csharp |
| | | 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åºï¼è¾åºåºï¼** |
| | | |
| | | ```csharp |
| | | 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åºï¼å®æ¶å¨åºï¼** |
| | | |
| | | ```csharp |
| | | 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åºï¼è®¡æ°å¨åºï¼** |
| | | |
| | | ```csharp |
| | | 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: éªè¯æå»º** |
| | | |
| | | ```bash |
| | | cd WIDESEAWCS_S7Simulator.Core |
| | | dotnet build |
| | | ``` |
| | | |
| | | - [ ] **Step 6: æäº¤I/Q/T/Cåºå®ç°** |
| | | |
| | | ```bash |
| | | 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: å建å
åå卿¥å£** |
| | | |
| | | ```csharp |
| | | 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: ç¼åå
åå卿µè¯** |
| | | |
| | | ```csharp |
| | | 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: è¿è¡æµè¯éªè¯å¤±è´¥** |
| | | |
| | | ```bash |
| | | dotnet test Memory/MemoryStoreTests.cs -v n |
| | | ``` |
| | | |
| | | 颿è¾åº: æµè¯å¤±è´¥ |
| | | |
| | | - [ ] **Step 4: å®ç°MemoryStoreç±»** |
| | | |
| | | ```csharp |
| | | 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: è¿è¡æµè¯éªè¯éè¿** |
| | | |
| | | ```bash |
| | | dotnet test Memory/MemoryStoreTests.cs -v n |
| | | ``` |
| | | |
| | | 颿è¾åº: æææµè¯éè¿ |
| | | |
| | | - [ ] **Step 6: æäº¤å
ååå¨å®ç°** |
| | | |
| | | ```bash |
| | | 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ï¼æ¶µçäºé¡¹ç®åºç¡è®¾æ½ãæ ¸å¿å®ä½åå
ååå¨å®ç°ã |
| | | |
| | | **æ¯å¦ç»§ç»æ§è¡ä¸ä¸é¨åï¼æä¹
åæå¡åæå¡å¨å®ä¾ï¼ï¼** |