├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── dotnet.yml ├── .gitignore ├── .idea └── .idea.secs4net │ └── .idea │ ├── .gitignore │ ├── encodings.xml │ ├── indexLayout.xml │ └── vcs.xml ├── .nuget └── NuGet.Config ├── Directory.Build.props ├── LICENSE ├── LINQPad.md ├── README.md ├── _config.yml ├── common.json ├── common.sml ├── common ├── Channel.Extensions.cs ├── Encoding.Extensions.cs ├── EnumerableExtensions.cs ├── ItemAssertions.cs ├── RuntimeHelpers.cs ├── SecsMessageAssertions.cs └── Usings.cs ├── global.json ├── samples ├── DeviceWorkerService │ ├── DeviceLogger.cs │ ├── DeviceWorker.cs │ ├── DeviceWorkerService.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ServiceProvider.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── SecsDevice │ ├── Form1.Designer.cs │ ├── Form1.cs │ ├── Form1.resx │ ├── Program.cs │ ├── Properties │ │ └── DataSources │ │ │ └── RecvMessage.datasource │ ├── ReadMe.txt │ └── SecsDevice.csproj └── WpfVisualizer │ ├── App.xaml │ ├── App.xaml.cs │ ├── ReplyExpectedToStringConverter.cs │ ├── SecsMessageList.cs │ ├── SecsMessageTreeView.cs │ ├── Themes │ └── Generic.xaml │ ├── VeiwModelToTreeViewItemConverter.cs │ ├── ViewModel │ ├── SecsItemViewModel.cs │ ├── SecsMessageCollectionViewModel.cs │ ├── SecsMessageViewModel.cs │ └── TreeViewItemViewModel.cs │ ├── Window1.xaml │ ├── Window1.xaml.cs │ ├── WpfVisualizer.csproj │ └── common.json ├── secs4net.sln ├── src ├── Secs4Net.Json │ ├── ItemJsonConverter.cs │ ├── JsonReader.cs │ ├── JsonWriter.cs │ └── Secs4Net.Json.csproj ├── Secs4Net.Sml │ ├── MemoryExtensions.cs │ ├── Secs4Net.Sml.csproj │ ├── SmlReader.cs │ └── SmlWriter.cs └── Secs4Net │ ├── ConnectionState.cs │ ├── Extensions │ ├── ExtensionHelper.cs │ └── ReverseEndiannessHelper.cs │ ├── HsmsConnection.cs │ ├── ISecsConnection.cs │ ├── ISecsGemLogger.cs │ ├── Item.Decode.cs │ ├── Item.Encode.cs │ ├── Item.Factory.cs │ ├── Item.List.cs │ ├── Item.Memory.cs │ ├── Item.MemoryOwner.cs │ ├── Item.String.cs │ ├── Item.cs │ ├── MessageHeader.cs │ ├── MessageIdGenerator.cs │ ├── MessageType.cs │ ├── PipeDecoder.cs │ ├── PrimaryMessageWrapper.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Secs4Net.csproj │ ├── SecsException.cs │ ├── SecsFormat.cs │ ├── SecsGem.cs │ ├── SecsGemOptions.cs │ ├── SecsMessage.cs │ └── Usings.cs └── test ├── Benchmarks ├── BenchmarkConfig.cs ├── BenchmarkDotNet.Artifacts │ └── results │ │ ├── Benchmarks.ComplexItemEncodeDecode-report-github.md │ │ ├── Benchmarks.ItemEncodeDecode-report-github.md │ │ ├── Benchmarks.JsonSerialization-report-github.md │ │ ├── Benchmarks.PipeDecoding-report-github.md │ │ ├── Benchmarks.RequestResponse-report-github.md │ │ ├── Benchmarks.ReverseEndianness-report-github.md │ │ └── Benchmarks.SmlSerialization-report-github.md ├── Benchmarks.csproj ├── ComplexItemEncodeDecode.cs ├── Directory.Build.props ├── ItemEncodeDecode.cs ├── JsonSerialization.cs ├── PipeDecoding.cs ├── Program.cs ├── RequestResponse.cs ├── ReverseEndianness.cs └── SmlSerialization.cs ├── Secs4Net.Json.UnitTests ├── Json.UnitTest.cs └── Secs4Net.Json.UnitTests.csproj ├── Secs4Net.Sml.UnitTests ├── Secs4Net.Sml.UnitTests.csproj └── Sml.UnitTest.cs └── Secs4Net.UnitTests ├── ChunkedMemory.cs ├── Item.UnitTest.cs ├── MessageHeader.UnitTests.cs ├── PipeConnection.cs ├── PipeDecoder.UnitTests.cs ├── Secs4Net.UnitTests.csproj └── SecsGem.UnitTests.cs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "base" ] 9 | pull_request: 10 | branches: [ "base" ] 11 | 12 | jobs: 13 | build: 14 | permissions: 15 | contents: read 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: 8.0.x 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --no-restore 29 | - name: Test 30 | run: dotnet test -f net8.0 --no-build --verbosity normal --collect "Code Coverage" 31 | - name: Install dotnet-coverage 32 | run: dotnet tool install -g dotnet-coverage 33 | - name: Convert .coverage report to cobertura 34 | run: dotnet-coverage merge $GITHUB_WORKSPACE/test/**/TestResults/**/*.coverage -f cobertura -o $GITHUB_WORKSPACE/report.cobertura.xml 35 | - name: Upload coverage reports to Codecov 36 | uses: codecov/codecov-action@v3 37 | env: 38 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 39 | - name: Pack 40 | run: dotnet pack -c Release 41 | - name: Push 42 | run: dotnet nuget push "**/*.nupkg" --skip-duplicate -k ${{ secrets.NUGET }} -s https://api.nuget.org/v3/index.json 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | .vs/ 10 | 11 | # Build results 12 | [Dd]ebug/ 13 | [Dd]ebugPublic/ 14 | [Rr]elease/ 15 | [Rr]eleases/ 16 | x64/ 17 | x86/ 18 | build/ 19 | bld/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | 23 | # Roslyn cache directories 24 | *.ide/ 25 | 26 | # MSTest test Results 27 | [Tt]est[Rr]esult*/ 28 | [Bb]uild[Ll]og.* 29 | 30 | #NUNIT 31 | *.VisualState.xml 32 | TestResult.xml 33 | 34 | # Build Results of an ATL Project 35 | [Dd]ebugPS/ 36 | [Rr]eleasePS/ 37 | dlldata.c 38 | 39 | *_i.c 40 | *_p.c 41 | *_i.h 42 | *.ilk 43 | *.meta 44 | *.obj 45 | *.pch 46 | *.pdb 47 | *.pgc 48 | *.pgd 49 | *.rsp 50 | *.sbr 51 | *.tlb 52 | *.tli 53 | *.tlh 54 | *.tmp 55 | *.tmp_proj 56 | *.log 57 | *.vspscc 58 | *.vssscc 59 | .builds 60 | *.pidb 61 | *.svclog 62 | *.scc 63 | 64 | # Chutzpah Test files 65 | _Chutzpah* 66 | 67 | # Visual C++ cache files 68 | ipch/ 69 | *.aps 70 | *.ncb 71 | *.opensdf 72 | *.sdf 73 | *.cachefile 74 | 75 | # Visual Studio profiler 76 | *.psess 77 | *.vsp 78 | *.vspx 79 | 80 | # TFS 2012 Local Workspace 81 | $tf/ 82 | 83 | # Guidance Automation Toolkit 84 | *.gpState 85 | 86 | # ReSharper is a .NET coding add-in 87 | _ReSharper*/ 88 | *.[Rr]e[Ss]harper 89 | *.DotSettings.user 90 | 91 | # JustCode is a .NET coding addin-in 92 | .JustCode 93 | 94 | # TeamCity is a build add-in 95 | _TeamCity* 96 | 97 | # DotCover is a Code Coverage Tool 98 | *.dotCover 99 | 100 | # NCrunch 101 | _NCrunch_* 102 | .*crunch*.local.xml 103 | 104 | # MightyMoose 105 | *.mm.* 106 | AutoTest.Net/ 107 | 108 | # Web workbench (sass) 109 | .sass-cache/ 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.[Pp]ublish.xml 129 | *.azurePubxml 130 | # TODO: Comment the next line if you want to checkin your web deploy settings 131 | # but database connection strings (with potential passwords) will be unencrypted 132 | *.pubxml 133 | *.publishproj 134 | 135 | # NuGet Packages 136 | *.nupkg 137 | # The packages folder can be ignored because of Package Restore 138 | **/packages/* 139 | # except build/, which is used as an MSBuild target. 140 | !**/packages/build/ 141 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 142 | #!**/packages/repositories.config 143 | 144 | # Windows Azure Build Output 145 | csx/ 146 | *.build.csdef 147 | 148 | # Windows Store app package directory 149 | AppPackages/ 150 | 151 | # Others 152 | sql/ 153 | *.Cache 154 | ClientBin/ 155 | [Ss]tyle[Cc]op.* 156 | ~$* 157 | *~ 158 | *.dbmdl 159 | *.dbproj.schemaview 160 | *.pfx 161 | *.publishsettings 162 | node_modules/ 163 | 164 | # RIA/Silverlight projects 165 | Generated_Code/ 166 | 167 | # Backup & report files from converting an old project file 168 | # to a newer Visual Studio version. Backup files are not needed, 169 | # because we have git ;-) 170 | _UpgradeReport_Files/ 171 | Backup*/ 172 | UpgradeLog*.XML 173 | UpgradeLog*.htm 174 | 175 | # SQL Server files 176 | *.mdf 177 | *.ldf 178 | 179 | # Business Intelligence projects 180 | *.rdl.data 181 | *.bim.layout 182 | *.bim_*.settings 183 | 184 | # Microsoft Fakes 185 | FakesAssemblies/ 186 | 187 | # ========================= 188 | # Operating System Files 189 | # ========================= 190 | 191 | # OSX 192 | # ========================= 193 | 194 | .DS_Store 195 | .AppleDouble 196 | .LSOverride 197 | 198 | # Thumbnails 199 | ._* 200 | 201 | # Files that might appear on external disk 202 | .Spotlight-V100 203 | .Trashes 204 | 205 | # Directories potentially created on remote AFP share 206 | .AppleDB 207 | .AppleDesktop 208 | Network Trash Folder 209 | Temporary Items 210 | .apdisk 211 | 212 | # Windows 213 | # ========================= 214 | 215 | # Windows image file caches 216 | Thumbs.db 217 | ehthumbs.db 218 | 219 | # Folder config file 220 | Desktop.ini 221 | 222 | # Recycle Bin used on file shares 223 | $RECYCLE.BIN/ 224 | 225 | # Windows Installer files 226 | *.cab 227 | *.msi 228 | *.msm 229 | *.msp 230 | 231 | # Windows shortcuts 232 | *.lnk 233 | yarn.lock 234 | **/wwwroot/app/ 235 | *.etl 236 | -------------------------------------------------------------------------------- /.idea/.idea.secs4net/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /modules.xml 6 | /contentModel.xml 7 | /projectSettingsUpdater.xml 8 | /.idea.secs4net.iml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.secs4net/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.secs4net/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.secs4net/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.4.4 4 | https://github.com/mkjeff/secs4net 5 | mkjeff 6 | MIT 7 | enable 8 | 12.0 9 | true 10 | true 11 | true 12 | true 13 | snupkg 14 | true 15 | true 16 | true 17 | Copyright © secs4net 2024 18 | https://mkjeff.github.io/secs4net/ 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Hsin-chang He 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LINQPad.md: -------------------------------------------------------------------------------- 1 | ## LINQPad 2 | If you like to use LINQPad to dump `Item` object in a simplified format, you probably need a [custom dump method](http://www.linqpad.net/CustomizingDump.aspx) method. Adjust your LINQPad **My Extensions** file as follow 3 | ```cs 4 | static object ToDump(object obj) 5 | { 6 | var objType = obj.GetType(); 7 | if (Type.GetType("Secs4Net.Item,Secs4Net") is { } itemType && objType.IsSubclassOf(itemType)) 8 | { 9 | return Dump(obj); 10 | } 11 | return obj; 12 | } 13 | 14 | static object Dump(dynamic item) 15 | { 16 | return (int)item.Format switch 17 | { 18 | (int)SecsFormat.List => new { item.Format, Values = OnDemandList(item), }, 19 | (int)SecsFormat.ASCII => new { item.Format, Values = item.GetString() }, 20 | (int)SecsFormat.JIS8 => new { item.Format, Values = item.GetString() }, 21 | (int)SecsFormat.Binary => new { item.Format, Values = OnDemandMemory(item) }, 22 | (int)SecsFormat.Boolean => new { item.Format, Values = OnDemandMemory(item) }, 23 | (int)SecsFormat.I8 => new { item.Format, Values = OnDemandMemory(item) }, 24 | (int)SecsFormat.I1 => new { item.Format, Values = OnDemandMemory(item) }, 25 | (int)SecsFormat.I2 => new { item.Format, Values = OnDemandMemory(item) }, 26 | (int)SecsFormat.I4 => new { item.Format, Values = OnDemandMemory(item) }, 27 | (int)SecsFormat.F8 => new { item.Format, Values = OnDemandMemory(item)}, 28 | (int)SecsFormat.F4 => new { item.Format, Values = OnDemandMemory(item) }, 29 | (int)SecsFormat.U8 => new { item.Format, Values = OnDemandMemory(item) }, 30 | (int)SecsFormat.U1 => new { item.Format, Values = OnDemandMemory(item) }, 31 | (int)SecsFormat.U2 => new { item.Format, Values = OnDemandMemory(item)}, 32 | (int)SecsFormat.U4 => new { item.Format, Values = OnDemandMemory(item) }, 33 | _ => $"Unsupported Format: {item.Format}", 34 | }; 35 | 36 | static object OnDemandList(dynamic item) => Util.OnDemand($"[{item.Count}]", () => item.Items); 37 | static object OnDemandMemory(dynamic item) => Util.OnDemand($"[{item.Count}]", () => item.GetMemory()); 38 | } 39 | 40 | // You can also define namespaces, non-static classes, enums, etc. 41 | enum SecsFormat 42 | { 43 | List = 0b_0000_00, 44 | Binary = 0b_0010_00, 45 | Boolean = 0b_0010_01, 46 | ASCII = 0b_0100_00, 47 | JIS8 = 0b_0100_01, 48 | I8 = 0b_0110_00, 49 | I1 = 0b_0110_01, 50 | I2 = 0b_0110_10, 51 | I4 = 0b_0111_00, 52 | F8 = 0b_1000_00, 53 | F4 = 0b_1001_00, 54 | U8 = 0b_1010_00, 55 | U1 = 0b_1010_01, 56 | U2 = 0b_1010_10, 57 | U4 = 0b_1011_00, 58 | } 59 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secs4net 2 | 3 | [![.NET](https://github.com/mkjeff/secs4net/actions/workflows/dotnet.yml/badge.svg)](https://github.com/mkjeff/secs4net/actions/workflows/dotnet.yml) [![Nuget](https://img.shields.io/nuget/dt/secs4net)](https://www.nuget.org/stats/packages/Secs4Net?groupby=Version) [![NuGet](https://img.shields.io/nuget/v/secs4net.svg)](https://www.nuget.org/packages/Secs4Net) [![codecov](https://codecov.io/gh/mkjeff/secs4net/graph/badge.svg?token=AgiQxizSvE)](https://codecov.io/gh/mkjeff/secs4net) 4 | 5 | **Project Description** 6 | 7 | SECS-II/HSMS-SS/GEM implementation on .NET. This library provides an easy way to communicate with SEMI-standard compatible devices. 8 | 9 | **Getting started** 10 | 11 | ## Install Nuget package 12 | > dotnet add package Secs4Net 13 | 14 | ## Configure via .NET dependency injection 15 | [Sample code reference](https://github.com/mkjeff/secs4net/blob/base/samples/DeviceWorkerService/ServiceProvider.cs) 16 | ```cs 17 | public void ConfigureServices(IServiceCollection services) 18 | { 19 | // "secs4net" configuration section in the appsettings.json 20 | // "secs4net": { 21 | // "DeviceId": 0, 22 | // "IsActive": true, 23 | // "IpAddress": "127.0.0.1", 24 | // "Port": 5000 25 | // } 26 | services.AddSecs4Net(Configuration); 27 | } 28 | 29 | class DeviceLogger : ISecsGemLogger 30 | { 31 | // implement ISecsGemLogger methods 32 | } 33 | ``` 34 | 35 | ## Basic usage 36 | ```cs 37 | try 38 | { 39 | var s3f17 = new SecsMessage(3, 17) 40 | { 41 | Name = "CreateProcessJob", 42 | SecsItem = L( 43 | U4(0), 44 | L( 45 | L( 46 | A("Id"), 47 | B(0x0D), 48 | L( 49 | A("carrier id"), 50 | L( 51 | U1(1)), 52 | L( 53 | U1(1), 54 | A("recipe"), 55 | L()), 56 | Boolean(true), 57 | L())))) 58 | }; 59 | 60 | //access list 61 | s3f17.SecsItem[1][0][0] == A("Id"); 62 | 63 | foreach(var item in s3f17.SecsItem[1][0][2].Items) 64 | { 65 | 66 | } 67 | 68 | //access an unmanaged array item 69 | byte b2 = s3f17.SecsItem[0].FirstValue(); // with different type 70 | s3f17.SecsItem[0].FirstValue() = 0; // change original value 71 | byte b3 = s3f17.SecsItem[0].GetFirstValueOrDefault(fallbackValueWhenItemIsEmpty); 72 | Memory bytes = s3f17.SecsItem[0].GetMemory(); 73 | 74 | // access string item 75 | string str = s3f17.SecsItem[1][0][0].GetString(); // str = "Id" 76 | 77 | //await the secondary message 78 | var s3f18 = await secsGem.SendAsync(s3f17); 79 | 80 | // process message with LINQ 81 | var query = 82 | from a in s3f18.SecsItem[3] 83 | select new { 84 | num = a.FirstValue(), 85 | }; 86 | } 87 | catch(SecsException) 88 | { 89 | // exception when 90 | // T3 timeout 91 | // device reply SxF0 92 | // device reply S9Fx 93 | } 94 | ``` 95 | 96 | ## Handle primary messages 97 | ```cs 98 | await foreach (var e in secsGem.GetPrimaryMessageAsync(cancellationToken)) 99 | { 100 | using var primaryMsg = e.PrimaryMessage; 101 | //do something for primary message 102 | 103 | // reply secondary message to device 104 | using var secondaryMsg = new SecsMessage(...); 105 | await e.TryReplyAsync(secondaryMsg); 106 | }; 107 | ``` 108 | 109 | ## Creates `Item` via LINQ 110 | ```cs 111 | using static Secs4Net.Item; 112 | 113 | var s16f15 = 114 | new SecsMessage(16, 15) 115 | { 116 | Name = "CreateProcessJob", 117 | SecsItem = L( 118 | U4(0), 119 | L( 120 | from pj in tx.ProcessJobs 121 | select 122 | L( 123 | A(pj.Id), 124 | B(0x0D), 125 | L( 126 | from carrier in pj.Carriers 127 | select 128 | L( 129 | A(carrier.Id), 130 | L( 131 | from slotInfo in carrier.SlotMap 132 | select 133 | U1(slotInfo.SlotNo)))), 134 | L( 135 | U1(1), 136 | A(pj.RecipeId), 137 | L()), 138 | Boolean(true), 139 | L())))); 140 | ``` 141 | 142 | ## Change the `Item` value (restricted) 143 | > Basic rule: The `Item.Count` has been fixed while the item was created. 144 | 145 | You can only overwrite values on existing memory. String Item is immutable, coz C# `string` is immutable as well. 146 | 147 | ## Reuse array for large item values 148 | All unmanaged data Item can created from `IMemoryOwner` or `Memory`. 149 | 150 | The following sample uses the implementation of `IMemoryOwner` from [`Microsoft.Toolkit.HighPerformance`](https://docs.microsoft.com/en-us/windows/communitytoolkit/high-performance/memoryowner) that has been referenced internally by secs4net.. 151 | 152 | ```cs 153 | var largeArrayOwner = MemoryOwner.Allocate(size: 65535); 154 | 155 | // feed the value into largeArrayOwner.Memory or largeArrayOwner.Span 156 | FillLargeArray(largeArrayOwner.Memory); 157 | 158 | using var s6f11 = new SecsMessage(6, 11, replyExpected: false) 159 | { 160 | Name = "LargeDataEvent", 161 | SecsItem = L( 162 | L( 163 | I2(1121), 164 | A(""), 165 | I4(largeArrayOwner))), // create Item from largeArrayOwner 166 | }; 167 | 168 | // apply using on received message as well. coz the item decoded by PipeDecoder also uses MemoryOwner when the data array is big. 169 | using var s6f12 = await secsGem.SendAsync(s6f11); 170 | ``` 171 | > `IMemoryOwner`, `Item`, and `SecsMessage` have implemented `IDisposable` don't forget to `Dispose` it when they don't need anymore. 172 | Otherwise, the array will not return to the pool till GC collects. 173 | 174 | > Since the length of the max encoded bytes in a single non-List Item was `16,777,215`(3 bytes), we split raw data into separated items. 175 | In that case, creating the Items from sliced `Memory` is more efficient. 176 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pages-themes/leap-day@v0.2.0 2 | plugins: 3 | - jekyll-remote-theme # add this line to the plugins list if you already have one 4 | -------------------------------------------------------------------------------- /common/Channel.Extensions.cs: -------------------------------------------------------------------------------- 1 | #if !NET 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace System.Threading.Channels 6 | { 7 | internal static class ChannelExtensions 8 | { 9 | public static async IAsyncEnumerable ReadAllAsync(this ChannelReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) 10 | { 11 | while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) 12 | { 13 | while (reader.TryRead(out var item)) 14 | { 15 | yield return item; 16 | } 17 | } 18 | } 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /common/Encoding.Extensions.cs: -------------------------------------------------------------------------------- 1 | #if !NET 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | using System.Buffers; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace System.Text 7 | { 8 | internal static class EncodingExtenstions 9 | { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | internal static unsafe int GetBytes(this Encoding encoding, string str, Span span) 12 | { 13 | fixed (char* chars = str.AsSpan()) 14 | { 15 | fixed (byte* bytes = span) 16 | { 17 | return encoding.GetBytes(chars, str.Length, bytes, span.Length); 18 | } 19 | } 20 | } 21 | 22 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 23 | internal static unsafe int GetCharCount(this Encoding encoding, ReadOnlySpan span) 24 | { 25 | fixed (byte* bytes = span) 26 | { 27 | return encoding.GetCharCount(bytes, span.Length); 28 | } 29 | } 30 | 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | internal static unsafe string GetString(this Encoding encoding, ReadOnlySpan bytes) 33 | { 34 | using var spanOwner = SpanOwner.Allocate((int)bytes.Length); 35 | bytes.CopyTo(spanOwner.Span); 36 | 37 | fixed (byte* spanBytes = spanOwner.Span) 38 | { 39 | return encoding.GetString(spanBytes, spanOwner.Length); 40 | } 41 | } 42 | 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | internal static unsafe string GetString(this Encoding encoding, in ReadOnlySequence bytes) 45 | { 46 | using var spanOwner = SpanOwner.Allocate((int)bytes.Length); 47 | bytes.CopyTo(spanOwner.Span); 48 | 49 | fixed (byte* spanBytes = spanOwner.Span) 50 | { 51 | return encoding.GetString(spanBytes, spanOwner.Length); 52 | } 53 | } 54 | } 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /common/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | #if !NET 2 | using System.Collections.Generic; 3 | 4 | namespace System.Linq 5 | { 6 | public static class EnumerableExt 7 | { 8 | public static IEnumerable Chunk(this IEnumerable items, int maxItems) 9 | { 10 | return items.Select((item, inx) => new { item, inx }) 11 | .GroupBy(x => x.inx / maxItems) 12 | .Select(g => g.Select(x => x.item).ToArray()); 13 | } 14 | } 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /common/ItemAssertions.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Execution; 3 | using FluentAssertions.Primitives; 4 | using System; 5 | 6 | namespace Secs4Net; 7 | 8 | internal static class ItemExtensions 9 | { 10 | public static ItemAssertions Should(this Item? instance) => new(instance); 11 | } 12 | 13 | internal sealed class ItemAssertions : ReferenceTypeAssertions 14 | { 15 | public ItemAssertions(Item? instance) : base(instance) { } 16 | 17 | protected override string Identifier => "item"; 18 | 19 | 20 | public AndConstraint BeEquivalentTo(Item? expectation, string because = "", params object[] becauseArgs) 21 | { 22 | if (Subject?.Equals(expectation) != true) 23 | { 24 | new ItemValidator().IsEquals(new ItemEquivalencyValidationContext 25 | { 26 | Reason = new Reason(because, becauseArgs), 27 | Subject = Subject, 28 | Expectation = expectation, 29 | }); 30 | } 31 | 32 | return new AndConstraint(this); 33 | } 34 | 35 | public AndConstraint NotBeEquivalentTo(Item expectation, string because = "", params object[] becauseArgs) 36 | { 37 | if (expectation.Equals(Subject)) 38 | { 39 | Execute.Assertion 40 | .BecauseOf(because, becauseArgs) 41 | .FailWith("Expected item not be equivalent, but they are."); 42 | } 43 | 44 | return new AndConstraint(this); 45 | } 46 | } 47 | 48 | internal sealed class ItemEquivalencyValidationContext 49 | { 50 | public Reason Reason { get; init; } = default!; 51 | public Item? Subject { get; init; } = default!; 52 | public Item? Expectation { get; init; } = default!; 53 | } 54 | 55 | internal sealed class ItemValidator 56 | { 57 | public bool IsEquals(ItemEquivalencyValidationContext context) 58 | { 59 | if (context.Subject is not Item subjectValue || 60 | context.Expectation is not Item expectationValue) 61 | { 62 | return false; 63 | } 64 | 65 | return IsMatch(path: "item", subjectValue, expectationValue, context); 66 | } 67 | 68 | private static bool IsMatch(string path, Item subject, Item expectation, ItemEquivalencyValidationContext context) 69 | { 70 | if (subject.Format != expectation.Format) 71 | { 72 | Execute.Assertion 73 | .BecauseOf(context.Reason) 74 | .FailWith("Expected {0} to be {1}, but found {2}", 75 | path + ".Format", expectation.Format, subject.Format); 76 | 77 | return false; 78 | } 79 | 80 | if (subject.Count != expectation.Count) 81 | { 82 | Execute.Assertion 83 | .BecauseOf(context.Reason) 84 | .FailWith("Expected {0} to be {1}, but found {2}", 85 | path + ".Count", expectation.Count, subject.Count); 86 | return false; 87 | } 88 | 89 | if (subject.Count == 0) 90 | { 91 | return true; 92 | } 93 | 94 | #pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. 95 | return expectation.Format switch 96 | #pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. 97 | { 98 | SecsFormat.List => IsMatchList(path, subject, expectation, context), 99 | SecsFormat.ASCII or SecsFormat.JIS8 => IsMatchStringItem(path, subject, expectation, context), 100 | SecsFormat.Binary => IsMatchArrayItem(path, subject, expectation, context), 101 | SecsFormat.Boolean => IsMatchArrayItem(path, subject, expectation, context), 102 | SecsFormat.I8 => IsMatchArrayItem(path, subject, expectation, context), 103 | SecsFormat.I1 => IsMatchArrayItem(path, subject, expectation, context), 104 | SecsFormat.I2 => IsMatchArrayItem(path, subject, expectation, context), 105 | SecsFormat.I4 => IsMatchArrayItem(path, subject, expectation, context), 106 | SecsFormat.F8 => IsMatchArrayItem(path, subject, expectation, context), 107 | SecsFormat.F4 => IsMatchArrayItem(path, subject, expectation, context), 108 | SecsFormat.U8 => IsMatchArrayItem(path, subject, expectation, context), 109 | SecsFormat.U1 => IsMatchArrayItem(path, subject, expectation, context), 110 | SecsFormat.U2 => IsMatchArrayItem(path, subject, expectation, context), 111 | SecsFormat.U4 => IsMatchArrayItem(path, subject, expectation, context), 112 | }; 113 | 114 | bool IsMatchStringItem(string path, Item subject, Item expectation, ItemEquivalencyValidationContext context) 115 | { 116 | var subjectString = subject.GetString(); 117 | var expectationString = expectation.GetString(); 118 | if (subjectString.Equals(expectationString, StringComparison.Ordinal)) 119 | { 120 | return true; 121 | } 122 | 123 | Execute.Assertion 124 | .BecauseOf(context.Reason) 125 | .FailWith("Expected {0} to be {1}, but found {2}", 126 | path + ".GetString()", expectationString, subjectString); 127 | 128 | return false; 129 | } 130 | 131 | bool IsMatchArrayItem(string path, Item subject, Item expectation, ItemEquivalencyValidationContext context) where T : unmanaged, IEquatable 132 | { 133 | var subjectSpan = subject.GetMemory().Span; 134 | var expectationSpan = expectation.GetMemory().Span; 135 | if (subjectSpan.SequenceEqual(expectationSpan)) 136 | { 137 | return true; 138 | } 139 | 140 | Execute.Assertion 141 | .BecauseOf(context.Reason) 142 | .FailWith("Expected {0} to be {1}, but found {2}", 143 | path + $".GetMemory<{typeof(T).Name}>()", 144 | expectationSpan.ToArray(), 145 | subjectSpan.ToArray()); 146 | return false; 147 | } 148 | 149 | bool IsMatchList(string path, Item subjectList, Item expectationList, ItemEquivalencyValidationContext context) 150 | { 151 | for (int i = 0, count = subjectList.Count; i < count; i++) 152 | { 153 | if (!IsMatch(path + $"[{i}]", subjectList[i], expectationList[i], context)) 154 | { 155 | return false; 156 | } 157 | } 158 | 159 | return true; 160 | } 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /common/RuntimeHelpers.cs: -------------------------------------------------------------------------------- 1 | #if !NET 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | // The .NET Foundation licenses this file to you under the MIT license. 4 | // See the LICENSE file in the project root for more information. 5 | 6 | namespace System.Runtime.CompilerServices 7 | { 8 | internal static class RuntimeHelpers 9 | { 10 | /// 11 | /// Slices the specified array using the specified range. 12 | /// 13 | public static T[] GetSubArray(T[] array, Range range) 14 | { 15 | if (array == null) 16 | { 17 | throw new ArgumentNullException(); 18 | } 19 | 20 | (int offset, int length) = range.GetOffsetAndLength(array.Length); 21 | 22 | if (default(T)! != null || typeof(T[]) == array.GetType()) // TODO-NULLABLE: default(T) == null warning (https://github.com/dotnet/roslyn/issues/34757) 23 | { 24 | // We know the type of the array to be exactly T[]. 25 | 26 | if (length == 0) 27 | { 28 | return []; 29 | } 30 | 31 | var dest = new T[length]; 32 | Array.Copy(array, offset, dest, 0, length); 33 | return dest; 34 | } 35 | else 36 | { 37 | // The array is actually a U[] where U:T. 38 | T[] dest = (T[])Array.CreateInstance(array.GetType().GetElementType()!, length); 39 | Array.Copy(array, offset, dest, 0, length); 40 | return dest; 41 | } 42 | } 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /common/SecsMessageAssertions.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Execution; 3 | using FluentAssertions.Primitives; 4 | using System.Collections.Generic; 5 | 6 | namespace Secs4Net; 7 | 8 | internal static class SecsMessageExtensions 9 | { 10 | public static SecsMessageAssertions Should(this SecsMessage? instance) => new(instance); 11 | } 12 | 13 | internal sealed class SecsMessageAssertions : ReferenceTypeAssertions 14 | { 15 | public SecsMessageAssertions(SecsMessage? instance) : base(instance) { } 16 | 17 | protected override string Identifier => "message"; 18 | 19 | public AndConstraint BeEquivalentTo(SecsMessage expectation, string because = "", params object[] becauseArgs) 20 | { 21 | if (!IsMessageEquals(expectation, Subject)) 22 | { 23 | new ObjectAssertions(Subject).BeEquivalentTo(expectation, 24 | options => options.ComparingByMembers() 25 | .Excluding(a => a.SecsItem) 26 | .Excluding(a => a.Name), 27 | because, becauseArgs); 28 | 29 | if (Subject?.SecsItem is not null) 30 | { 31 | Subject.SecsItem.Should().BeEquivalentTo(expectation.SecsItem); 32 | } 33 | else if (expectation.SecsItem is not null) 34 | { 35 | Execute.Assertion 36 | .BecauseOf(because, becauseArgs) 37 | .FailWith("Expected message.SecsItem is not null, but is"); 38 | } 39 | } 40 | 41 | return new AndConstraint(this); 42 | } 43 | 44 | public AndConstraint NotBeEquivalentTo(SecsMessage expectation, string because = "", params object[] becauseArgs) 45 | { 46 | if (IsMessageEquals(expectation, Subject)) 47 | { 48 | Execute.Assertion 49 | .BecauseOf(because, becauseArgs) 50 | .FailWith("Expected message not be equivalent, but they are."); 51 | } 52 | 53 | return new AndConstraint(this); 54 | } 55 | 56 | private static bool IsMessageEquals(SecsMessage left, SecsMessage? right) 57 | { 58 | if (right is null) 59 | { 60 | return false; 61 | } 62 | 63 | // exclude Name property 64 | return left.S == right.S 65 | && left.F == right.F 66 | && left.ReplyExpected == right.ReplyExpected 67 | && EqualityComparer.Default.Equals(left.SecsItem!, right.SecsItem!); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /common/Usings.cs: -------------------------------------------------------------------------------- 1 | global using static global::Secs4Net.Item; 2 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.0", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/DeviceLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Secs4Net; 3 | using Secs4Net.Sml; 4 | using System; 5 | 6 | namespace DeviceWorkerService; 7 | 8 | internal sealed class DeviceLogger(ILogger logger) : ISecsGemLogger 9 | { 10 | public void MessageIn(SecsMessage msg, int id) => logger.LogTrace($"<-- [0x{id:X8}] {msg.ToSml()}"); 11 | public void MessageOut(SecsMessage msg, int id) => logger.LogTrace($"--> [0x{id:X8}] {msg.ToSml()}"); 12 | public void Debug(string msg) => logger.LogDebug(msg); 13 | public void Info(string msg) => logger.LogInformation(msg); 14 | public void Warning(string msg) => logger.LogWarning(msg); 15 | public void Error(string msg, SecsMessage? message, Exception? ex) => logger.LogError(ex, $"{msg} {message}\n"); 16 | } 17 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/DeviceWorker.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | using Secs4Net; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace DeviceWorkerService; 9 | 10 | internal sealed class DeviceWorker : BackgroundService 11 | { 12 | private readonly ILogger _logger; 13 | private readonly ISecsConnection _hsmsConnection; 14 | private readonly ISecsGem _secsGem; 15 | 16 | public DeviceWorker(ILogger logger, ISecsConnection hsmsConnection, ISecsGem secsGem) 17 | { 18 | _logger = logger; 19 | _hsmsConnection = hsmsConnection; 20 | _secsGem = secsGem; 21 | 22 | _hsmsConnection.ConnectionChanged += delegate 23 | { 24 | switch (_hsmsConnection.State) 25 | { 26 | case ConnectionState.Retry: 27 | _logger.LogError($"Connection loss, try to reconnect."); 28 | break; 29 | case ConnectionState.Connecting: 30 | case ConnectionState.Connected: 31 | _logger.LogWarning(_hsmsConnection.State.ToString()); 32 | break; 33 | default: 34 | _logger.LogInformation($"Connection state = {_hsmsConnection.State}"); 35 | break; 36 | } 37 | }; 38 | } 39 | 40 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 41 | { 42 | try 43 | { 44 | _hsmsConnection.Start(stoppingToken); 45 | await foreach (var e in _secsGem.GetPrimaryMessageAsync(stoppingToken)) 46 | { 47 | using var primaryMessage = e.PrimaryMessage; 48 | _logger.LogInformation($"Received primary message: {primaryMessage}"); 49 | try 50 | { 51 | using var secondaryMessage = new SecsMessage(primaryMessage.S, (byte)(primaryMessage.F + 1)) 52 | { 53 | SecsItem = primaryMessage.SecsItem, 54 | }; 55 | await e.TryReplyAsync(secondaryMessage, stoppingToken); 56 | } 57 | catch (Exception ex) 58 | { 59 | _logger.LogError(ex, "Exception occurred when processing primary message"); 60 | } 61 | } 62 | } 63 | catch (Exception ex) 64 | { 65 | if (stoppingToken.IsCancellationRequested) 66 | { 67 | return; 68 | } 69 | _logger.LogError(ex, "Unhandled exception occurred on primary messages processing"); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/DeviceWorkerService.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | true 6 | false 7 | true 8 | dotnet-DeviceWorkerService-D257E021-F238-499C-8ACD-858186E73C0A 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/Program.cs: -------------------------------------------------------------------------------- 1 | using DeviceWorkerService; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | Host.CreateDefaultBuilder(args) 6 | .ConfigureServices((hostContext, services) => 7 | { 8 | services.AddSecs4Net(hostContext.Configuration); 9 | services.AddHostedService(); 10 | }).Build().Run(); 11 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "DeviceWorkerService": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/ServiceProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Secs4Net; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace DeviceWorkerService; 7 | 8 | public static class ServiceProvider 9 | { 10 | public static IServiceCollection AddSecs4Net<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TLogger>(this IServiceCollection services, IConfiguration configuration) 11 | where TLogger : class, ISecsGemLogger 12 | { 13 | var configSection = configuration.GetSection("secs4net"); 14 | services.Configure(configSection); 15 | services.AddSingleton(); 16 | services.AddSingleton(); 17 | services.AddSingleton(); 18 | return services; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information", 7 | "DeviceWorkerService": "Trace" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/DeviceWorkerService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "secs4net": { 10 | "DeviceId": 1, 11 | "IsActive": false, 12 | "IpAddress": "127.0.0.1", 13 | "Port": 5002 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/SecsDevice/Form1.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | 61 | False 62 | 63 | 64 | False 65 | 66 | 67 | False 68 | 69 | 70 | False 71 | 72 | 73 | False 74 | 75 | 76 | False 77 | 78 | 79 | DVLA:'S2F45' W 80 | <L[2] 81 | <U1[1] 0> 82 | <L[1] 83 | <L[2] 84 | <U1[0]> 85 | <L[1] 86 | <L[2] 87 | <B[0]> 88 | <L[2] 89 | <U1[0]> 90 | <U1[0]> 91 | > 92 | > 93 | > 94 | > 95 | > 96 | > 97 | . 98 | 99 | 100 | False 101 | 102 | 103 | False 104 | 105 | 106 | 17, 17 107 | 108 | 109 | False 110 | 111 | 112 | False 113 | 114 | 115 | False 116 | 117 | -------------------------------------------------------------------------------- /samples/SecsDevice/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Windows.Forms; 4 | 5 | namespace SecsDevice; 6 | 7 | static class Program 8 | { 9 | /// 10 | /// The main entry point for the application. 11 | /// 12 | [STAThread] 13 | static void Main() 14 | { 15 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 16 | Secs4Net.Item.JIS8Encoding = Encoding.GetEncoding(50222); 17 | #if NET 18 | Application.SetHighDpiMode(HighDpiMode.SystemAware); 19 | #endif 20 | Application.EnableVisualStyles(); 21 | Application.SetCompatibleTextRenderingDefault(false); 22 | Application.Run(new Form1()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/SecsDevice/Properties/DataSources/RecvMessage.datasource: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | SecsDevice.RecvMessage, SecsDevice, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null 10 | -------------------------------------------------------------------------------- /samples/SecsDevice/ReadMe.txt: -------------------------------------------------------------------------------- 1 | This sample program is the demo of Secs4Net.You can simulate a device/host to communicate with other HSMS-SS/SECS-II device. 2 | Try to send/reply message with following SML format, notice the end of angle brackets of the list item.(or refer to received message textbox) 3 | This just a simple demo,expand it by yourself for fun. 4 | 5 | S6F11ReadyToLoad: 'S6F11' W 6 | 8 | 9 | 12 | 14 | > 15 | > 16 | > 17 | > 18 | . 19 | 20 | S6F12: 'S6F12' 21 | 22 | . -------------------------------------------------------------------------------- /samples/SecsDevice/SecsDevice.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net8.0-windows;net472 6 | true 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace SecsMessageVisuallizer 4 | { 5 | /// 6 | /// Interaction logic for App.xaml 7 | /// 8 | public partial class App : Application { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/ReplyExpectedToStringConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Data; 3 | 4 | namespace SecsMessageVisuallizer 5 | { 6 | [ValueConversion(typeof(bool), typeof(string))] 7 | public class ReplyExpectedToStringConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 10 | => ((bool)value) ? "'W'" : string.Empty; 11 | 12 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) 13 | { 14 | throw new NotImplementedException(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/SecsMessageList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Collections.ObjectModel; 4 | using System.IO; 5 | using Secs4Net.Json; 6 | 7 | namespace Secs4Net 8 | { 9 | public sealed class SecsMessageList : ObservableCollection { 10 | public SecsMessageList(string jsonFile) : base(File.OpenRead(jsonFile).ToSecsMessages()) { } 11 | 12 | public SecsMessage? this[byte s, byte f, string name] => this[s, f].FirstOrDefault(m => m.Name == name); 13 | 14 | public IEnumerable this[byte s, byte f] => this.Where(m => m.S == s && m.F == f); 15 | 16 | public SecsMessage? this[string name] => this.FirstOrDefault(m => m.Name == name); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/SecsMessageTreeView.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace SecsMessageVisuallizer 5 | { 6 | /// 7 | /// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file. 8 | /// 9 | /// Step 1a) Using this custom control in a XAML file that exists in the current project. 10 | /// Add this XmlNamespace attribute to the root element of the markup file where it is 11 | /// to be used: 12 | /// 13 | /// xmlns:MyNamespace="clr-namespace:SecsMessageVisuallizer" 14 | /// 15 | /// 16 | /// Step 1b) Using this custom control in a XAML file that exists in a different project. 17 | /// Add this XmlNamespace attribute to the root element of the markup file where it is 18 | /// to be used: 19 | /// 20 | /// xmlns:MyNamespace="clr-namespace:SecsMessageVisuallizer;assembly=SecsMessageVisuallizer" 21 | /// 22 | /// You will also need to add a project reference from the project where the XAML file lives 23 | /// to this project and Rebuild to avoid compilation errors: 24 | /// 25 | /// Right click on the target project in the Solution Explorer and 26 | /// "Add Reference"->"Projects"->[Browse to and select this project] 27 | /// 28 | /// 29 | /// Step 2) 30 | /// Go ahead and use your control in the XAML file. 31 | /// 32 | /// 33 | /// 34 | /// 35 | public class SecsMessageTreeView : TreeView 36 | { 37 | static SecsMessageTreeView() 38 | { 39 | DefaultStyleKeyProperty.OverrideMetadata(typeof(SecsMessageTreeView), new FrameworkPropertyMetadata(typeof(SecsMessageTreeView))); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/VeiwModelToTreeViewItemConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Data; 3 | using System.Windows.Controls; 4 | 5 | namespace SecsMessageVisuallizer 6 | { 7 | class VeiwModelToTreeViewItemConverter:IValueConverter { 8 | #region IValueConverter Members 9 | 10 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 11 | if (value is TreeView tv) 12 | { 13 | if (tv.ItemContainerGenerator.ContainerFromItem(tv.SelectedItem) is TreeViewItem tvItem) 14 | return tvItem.IsFocused; 15 | } 16 | return false; 17 | } 18 | 19 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { 20 | throw new NotImplementedException(); 21 | } 22 | 23 | #endregion 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/ViewModel/SecsItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using Secs4Net; 2 | using Secs4Net.Json; 3 | using System.Text.Json; 4 | 5 | namespace SecsMessageVisuallizer.ViewModel 6 | { 7 | public class SecsItemViewModel : TreeViewItemViewModel 8 | { 9 | readonly Item _secsItem; 10 | public SecsItemViewModel(Item item, SecsMessageViewModel secsMsg) 11 | : base(secsMsg, lazyLoadChildren: item.Format == SecsFormat.List && item.Count > 0) 12 | { 13 | _secsItem = item; 14 | } 15 | 16 | public SecsItemViewModel(Item item, SecsItemViewModel parentItem) 17 | : base(parentItem, lazyLoadChildren: item.Format == SecsFormat.List && item.Count > 0) 18 | { 19 | _secsItem = item; 20 | } 21 | 22 | protected override void LoadChildren() 23 | { 24 | foreach (Item item in _secsItem.Items) 25 | { 26 | base.Children.Add(new SecsItemViewModel(item, this)); 27 | } 28 | } 29 | 30 | public string Name 31 | => _secsItem.ToString(); 32 | 33 | private static readonly JsonSerializerOptions JsonOptions = new() 34 | { 35 | WriteIndented = true, 36 | Converters = 37 | { 38 | new ItemJsonConverter(), 39 | } 40 | }; 41 | 42 | public override string ToString() 43 | => JsonSerializer.Serialize(_secsItem, JsonOptions); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/ViewModel/SecsMessageCollectionViewModel.cs: -------------------------------------------------------------------------------- 1 | using Secs4Net; 2 | using System.Collections.Generic; 3 | 4 | namespace SecsMessageVisuallizer.ViewModel 5 | { 6 | public class SecsMessageCollectionViewModel 7 | { 8 | readonly SecsMessageList _msgList; 9 | 10 | public SecsMessageCollectionViewModel(SecsMessageList secsMsgList) 11 | { 12 | _msgList = secsMsgList; 13 | } 14 | 15 | public IEnumerable SecsMessages 16 | { 17 | get 18 | { 19 | foreach (SecsMessage msg in _msgList) 20 | yield return new SecsMessageViewModel(msg); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/ViewModel/SecsMessageViewModel.cs: -------------------------------------------------------------------------------- 1 | using Secs4Net; 2 | using Secs4Net.Json; 3 | using System.Text.Json; 4 | 5 | namespace SecsMessageVisuallizer.ViewModel 6 | { 7 | public class SecsMessageViewModel : TreeViewItemViewModel 8 | { 9 | readonly SecsMessage _secsMsg; 10 | public SecsMessageViewModel(SecsMessage secsMsg) 11 | : base(null, false) 12 | { 13 | _secsMsg = secsMsg; 14 | if (secsMsg.SecsItem != null) 15 | base.Children.Add(new SecsItemViewModel(secsMsg.SecsItem, this)); 16 | } 17 | 18 | public byte StreamNumber => _secsMsg.S; 19 | public byte FunctionNumber => _secsMsg.F; 20 | public string? Name 21 | { 22 | get { return _secsMsg.Name; } 23 | set 24 | { 25 | if (value != null && value != _secsMsg.Name) 26 | { 27 | _secsMsg.Name = value; 28 | base.OnPropertyChanged(); 29 | } 30 | } 31 | } 32 | public bool ReplyExpected => _secsMsg.ReplyExpected; 33 | 34 | private static readonly JsonSerializerOptions JsonOptions = new() 35 | { 36 | WriteIndented = true, 37 | Converters = 38 | { 39 | new ItemJsonConverter(), 40 | } 41 | }; 42 | 43 | public override string ToString() 44 | => JsonSerializer.Serialize(_secsMsg, JsonOptions); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/ViewModel/TreeViewItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace SecsMessageVisuallizer.ViewModel 7 | { 8 | public class TreeViewItemViewModel : INotifyPropertyChanged 9 | { 10 | static readonly TreeViewItemViewModel DummyChild = new TreeViewItemViewModel(); 11 | 12 | readonly ObservableCollection _children = new(); 13 | readonly TreeViewItemViewModel? _parent; 14 | 15 | bool _isExpanded; 16 | bool _isSelected; 17 | 18 | protected TreeViewItemViewModel(TreeViewItemViewModel? parent, bool lazyLoadChildren) 19 | { 20 | _parent = parent; 21 | if (lazyLoadChildren) 22 | _children.Add(DummyChild); 23 | } 24 | 25 | // This is used to create the DummyChild instance. 26 | private TreeViewItemViewModel() 27 | { 28 | } 29 | 30 | /// 31 | /// Returns the logical child items of this object. 32 | /// 33 | public ObservableCollection Children => _children; 34 | 35 | /// 36 | /// Returns true if this object's Children have not yet been populated. 37 | /// 38 | public bool HasDummyChild => Children.Count == 1 && Children[0] == DummyChild; 39 | 40 | /// 41 | /// Gets/sets whether the TreeViewItem 42 | /// associated with this object is expanded. 43 | /// 44 | public bool IsExpanded 45 | { 46 | get { return _isExpanded; } 47 | set 48 | { 49 | if (value != _isExpanded) 50 | { 51 | _isExpanded = value; 52 | OnPropertyChanged(); 53 | } 54 | 55 | // Expand all the way up to the root. 56 | if (_isExpanded && _parent != null) 57 | _parent.IsExpanded = true; 58 | 59 | // Lazy load the child items, if necessary. 60 | if (HasDummyChild) 61 | { 62 | Children.Remove(DummyChild); 63 | LoadChildren(); 64 | } 65 | } 66 | } 67 | 68 | /// 69 | /// Gets/sets whether the TreeViewItem 70 | /// associated with this object is selected. 71 | /// 72 | public bool IsSelected 73 | { 74 | get { return _isSelected; } 75 | set { SetField(ref _isSelected, value); } 76 | } 77 | 78 | /// 79 | /// Invoked when the child items need to be loaded on demand. 80 | /// Subclasses can override this to populate the Children collection. 81 | /// 82 | protected virtual void LoadChildren() 83 | { 84 | } 85 | 86 | public TreeViewItemViewModel? Parent => _parent; 87 | 88 | public event PropertyChangedEventHandler? PropertyChanged; 89 | 90 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 91 | { 92 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 93 | } 94 | 95 | protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) 96 | { 97 | if (EqualityComparer.Default.Equals(field, value)) 98 | return false; 99 | field = value; 100 | OnPropertyChanged(propertyName); 101 | return true; 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/Window1.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/Window1.xaml.cs: -------------------------------------------------------------------------------- 1 | using Secs4Net; 2 | using SecsMessageVisuallizer.ViewModel; 3 | using System.Windows; 4 | 5 | namespace SecsMessageVisuallizer { 6 | /// 7 | /// Interaction logic for Window1.xaml 8 | /// 9 | public partial class Window1 : Window { 10 | public Window1() { 11 | InitializeComponent(); 12 | DataContext = new SecsMessageCollectionViewModel(new SecsMessageList("common.json")); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/WpfVisualizer/WpfVisualizer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net8.0-windows;net472 6 | true 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Always 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Secs4Net.Json/ItemJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Secs4Net.Json; 6 | 7 | public sealed class ItemJsonConverter : JsonConverter 8 | { 9 | public override Item? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | return JsonElement.TryParseValue(ref reader, out var element) 12 | ? element.GetValueOrDefault().ToItem() 13 | : null; 14 | } 15 | 16 | public override void Write(Utf8JsonWriter writer, Item item, JsonSerializerOptions options) 17 | { 18 | item.WriteTo(writer); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Secs4Net.Json/JsonReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text.Json; 7 | using static Secs4Net.Item; 8 | 9 | namespace Secs4Net.Json; 10 | 11 | public static class JsonReader 12 | { 13 | public static Item ToItem(this JsonElement jsonObject) 14 | { 15 | var json = jsonObject.EnumerateObject().First(); 16 | #if NET 17 | var format = Enum.Parse(json.Name); 18 | #else 19 | Enum.TryParse(json.Name, out var format); 20 | #endif 21 | var value = json.Value; 22 | return format switch 23 | { 24 | SecsFormat.List => CreateList(value), 25 | SecsFormat.ASCII => A(value.GetString()), 26 | SecsFormat.JIS8 => J(value.GetString()), 27 | SecsFormat.Binary => CreateBinary(value), 28 | SecsFormat.Boolean => CreateBoolean(value), 29 | SecsFormat.F4 => CreateF4(value), 30 | SecsFormat.F8 => CreateF8(value), 31 | SecsFormat.I1 => CreateI1(value), 32 | SecsFormat.I2 => CreateI2(value), 33 | SecsFormat.I4 => CreateI4(value), 34 | SecsFormat.I8 => CreateI8(value), 35 | SecsFormat.U1 => CreateU1(value), 36 | SecsFormat.U2 => CreateU2(value), 37 | SecsFormat.U4 => CreateU4(value), 38 | SecsFormat.U8 => CreateU8(value), 39 | _ => ThrowHelper(format), 40 | }; 41 | 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | static Item CreateList(JsonElement jsonArray) 44 | { 45 | var items = new Item[jsonArray.GetArrayLength()]; 46 | int i = 0; 47 | foreach (var a in jsonArray.EnumerateArray()) 48 | { 49 | items[i++] = a.ToItem(); 50 | } 51 | return L(items); 52 | } 53 | 54 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 55 | static Item CreateBinary(JsonElement jsonArray) 56 | { 57 | var values = new byte[jsonArray.GetArrayLength()]; 58 | int i = 0; 59 | foreach (var a in jsonArray.EnumerateArray()) 60 | { 61 | values[i++] = a.GetByte(); 62 | } 63 | return B(values); 64 | } 65 | 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | static Item CreateBoolean(JsonElement jsonArray) 68 | { 69 | var values = new bool[jsonArray.GetArrayLength()]; 70 | int i = 0; 71 | foreach (var a in jsonArray.EnumerateArray()) 72 | { 73 | values[i++] = a.GetBoolean(); 74 | } 75 | return Boolean(values); 76 | } 77 | 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | static Item CreateF4(JsonElement jsonArray) 80 | { 81 | var values = new float[jsonArray.GetArrayLength()]; 82 | int i = 0; 83 | foreach (var a in jsonArray.EnumerateArray()) 84 | { 85 | values[i++] = a.GetSingle(); 86 | } 87 | return F4(values); 88 | } 89 | 90 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 91 | static Item CreateF8(JsonElement jsonArray) 92 | { 93 | var values = new double[jsonArray.GetArrayLength()]; 94 | int i = 0; 95 | foreach (var a in jsonArray.EnumerateArray()) 96 | { 97 | values[i++] = a.GetDouble(); 98 | } 99 | return F8(values); 100 | } 101 | 102 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 103 | static Item CreateI1(JsonElement jsonArray) 104 | { 105 | var values = new sbyte[jsonArray.GetArrayLength()]; 106 | int i = 0; 107 | foreach (var a in jsonArray.EnumerateArray()) 108 | { 109 | values[i++] = a.GetSByte(); 110 | } 111 | return I1(values); 112 | } 113 | 114 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 115 | static Item CreateI2(JsonElement jsonArray) 116 | { 117 | var values = new short[jsonArray.GetArrayLength()]; 118 | int i = 0; 119 | foreach (var a in jsonArray.EnumerateArray()) 120 | { 121 | values[i++] = a.GetInt16(); 122 | } 123 | return I2(values); 124 | } 125 | 126 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 127 | static Item CreateI4(JsonElement jsonArray) 128 | { 129 | var values = new int[jsonArray.GetArrayLength()]; 130 | int i = 0; 131 | foreach (var a in jsonArray.EnumerateArray()) 132 | { 133 | values[i++] = a.GetInt32(); 134 | } 135 | return I4(values); 136 | } 137 | 138 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 139 | static Item CreateI8(JsonElement jsonArray) 140 | { 141 | var values = new long[jsonArray.GetArrayLength()]; 142 | int i = 0; 143 | foreach (var a in jsonArray.EnumerateArray()) 144 | { 145 | values[i++] = a.GetInt64(); 146 | } 147 | return I8(values); 148 | } 149 | 150 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 151 | static Item CreateU1(JsonElement jsonArray) 152 | { 153 | var values = new byte[jsonArray.GetArrayLength()]; 154 | int i = 0; 155 | foreach (var a in jsonArray.EnumerateArray()) 156 | { 157 | values[i++] = a.GetByte(); 158 | } 159 | return U1(values); 160 | } 161 | 162 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 163 | static Item CreateU2(JsonElement jsonArray) 164 | { 165 | var values = new ushort[jsonArray.GetArrayLength()]; 166 | int i = 0; 167 | foreach (var a in jsonArray.EnumerateArray()) 168 | { 169 | values[i++] = a.GetUInt16(); 170 | } 171 | return U2(values); 172 | } 173 | 174 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 175 | static Item CreateU4(JsonElement jsonArray) 176 | { 177 | var values = new uint[jsonArray.GetArrayLength()]; 178 | int i = 0; 179 | foreach (var a in jsonArray.EnumerateArray()) 180 | { 181 | values[i++] = a.GetUInt32(); 182 | } 183 | return U4(values); 184 | } 185 | 186 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 187 | static Item CreateU8(JsonElement jsonArray) 188 | { 189 | var values = new ulong[jsonArray.GetArrayLength()]; 190 | int i = 0; 191 | foreach (var a in jsonArray.EnumerateArray()) 192 | { 193 | values[i++] = a.GetUInt64(); 194 | } 195 | return U8(values); 196 | } 197 | 198 | static Item ThrowHelper(SecsFormat format) => throw new ArgumentOutOfRangeException($"Unknown item format: {format}"); 199 | } 200 | 201 | public static IEnumerable ToSecsMessages(this Stream stream) 202 | { 203 | var options = new JsonSerializerOptions 204 | { 205 | Converters = { 206 | new ItemJsonConverter() 207 | } 208 | }; 209 | 210 | return JsonSerializer.Deserialize>(stream, options)!; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Secs4Net.Json/JsonWriter.cs: -------------------------------------------------------------------------------- 1 | using Secs4Net.Extensions; 2 | using System; 3 | using System.IO; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Text.Json; 7 | 8 | namespace Secs4Net.Json; 9 | 10 | public static class JsonWriter 11 | { 12 | public static string ToJson(this Item item, JsonWriterOptions options = default) 13 | { 14 | using var mem = new MemoryStream(); 15 | using var jwtr = new Utf8JsonWriter(mem, options); 16 | item.WriteTo(jwtr); 17 | jwtr.Flush(); 18 | return Encoding.UTF8.GetString(mem.ToArray()); 19 | } 20 | 21 | public static void WriteTo(this Item item, Utf8JsonWriter writer) 22 | { 23 | writer.WriteStartObject(); 24 | 25 | writer.WritePropertyName(item.Format.GetName()); 26 | 27 | if (item.Format == SecsFormat.ASCII || item.Format == SecsFormat.JIS8) 28 | { 29 | writer.WriteStringValue(item.GetString()); 30 | } 31 | else 32 | { 33 | writer.WriteStartArray(); 34 | switch (item.Format) 35 | { 36 | case SecsFormat.List: 37 | foreach (var a in item.Items) 38 | { 39 | a.WriteTo(writer); 40 | } 41 | break; 42 | case SecsFormat.Boolean: 43 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteBooleanValue(value)); 44 | break; 45 | case SecsFormat.I8: 46 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 47 | break; 48 | case SecsFormat.I1: 49 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 50 | break; 51 | case SecsFormat.I2: 52 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 53 | break; 54 | case SecsFormat.I4: 55 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 56 | break; 57 | case SecsFormat.F4: 58 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 59 | break; 60 | case SecsFormat.F8: 61 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 62 | break; 63 | case SecsFormat.U8: 64 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 65 | break; 66 | case SecsFormat.U1: 67 | case SecsFormat.Binary: 68 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 69 | break; 70 | case SecsFormat.U2: 71 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 72 | break; 73 | case SecsFormat.U4: 74 | WriteArrayValue(writer, item, static (writer, value) => writer.WriteNumberValue(value)); 75 | break; 76 | } 77 | writer.WriteEndArray(); 78 | } 79 | 80 | writer.WriteEndObject(); 81 | 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | static void WriteArrayValue(Utf8JsonWriter writer, Item item, Action write) where T : unmanaged, IEquatable 84 | { 85 | var span = item.GetMemory().Span; 86 | foreach (ref readonly var value in span) 87 | { 88 | write.Invoke(writer, value); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Secs4Net.Json/Secs4Net.Json.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0;netstandard2.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Secs4Net.Sml/MemoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace System; 6 | 7 | internal delegate TResult SpanParser(ReadOnlySpan span); 8 | 9 | //https://github.com/bbartels/coreclr/blob/master/src/System.Private.CoreLib/shared/System/MemoryExtensions.Split.cs 10 | // https://github.com/dotnet/runtime/pull/295 11 | 12 | internal static partial class MemoryExtensions 13 | { 14 | /// 15 | /// Returns an enumerator that iterates through a , 16 | /// which is split by separator . 17 | /// 18 | /// The source span which should be iterated over. 19 | /// The separator used to separate the . 20 | /// The which should be applied with this operation. 21 | /// Returns an enumerator for the specified sequence. 22 | public static SpanSplitEnumerator Split(in this ReadOnlySpan span, char separator, StringSplitOptions options = StringSplitOptions.None) 23 | => new(span, separator, options == StringSplitOptions.RemoveEmptyEntries); 24 | 25 | public static bool IsEmpty(this SpanSplitEnumerator source) where T : unmanaged, IEquatable 26 | => !source.MoveNext(); 27 | 28 | public static TResult[] ToArray(ref this SpanSplitEnumerator source, SpanParser selector, int? size) 29 | { 30 | if (size.HasValue) 31 | { 32 | var list = new TResult[size.GetValueOrDefault()]; 33 | ref var r0 = ref MemoryMarshal.GetReference(list.AsSpan()); 34 | uint i = 0; 35 | foreach (var span in source) 36 | { 37 | Unsafe.Add(ref r0, i++) = selector.Invoke(span); 38 | if (i == list.Length) 39 | { 40 | break; 41 | } 42 | } 43 | 44 | return list; 45 | } 46 | else 47 | { 48 | var list = new List(); 49 | foreach (var span in source) 50 | { 51 | list.Add(selector.Invoke(span)); 52 | } 53 | 54 | return list.ToArray(); 55 | } 56 | } 57 | } 58 | 59 | internal ref struct SpanSplitEnumerator where T : unmanaged, IEquatable 60 | { 61 | private ReadOnlySpan _sequence; 62 | private readonly T _separator; 63 | private SpanSplitInfo _spanSplitInfo; 64 | 65 | private bool ShouldRemoveEmptyEntries => _spanSplitInfo.HasFlag(SpanSplitInfo.RemoveEmptyEntries); 66 | private bool IsFinished => _spanSplitInfo.HasFlag(SpanSplitInfo.FinishedEnumeration); 67 | 68 | /// 69 | /// Gets the element at the current position of the enumerator. 70 | /// 71 | public ReadOnlySpan Current { get; private set; } 72 | 73 | /// 74 | /// Returns the current enumerator. 75 | /// 76 | /// Returns the current enumerator. 77 | public SpanSplitEnumerator GetEnumerator() => this; 78 | 79 | internal SpanSplitEnumerator(ReadOnlySpan span, T separator, bool removeEmptyEntries) 80 | { 81 | Current = default; 82 | _sequence = span; 83 | _separator = separator; 84 | _spanSplitInfo = default(SpanSplitInfo) | (removeEmptyEntries ? SpanSplitInfo.RemoveEmptyEntries : 0); 85 | } 86 | 87 | /// 88 | /// Advances the enumerator to the next element in the . 89 | /// 90 | /// Returns whether there is another item in the enumerator. 91 | public bool MoveNext() 92 | { 93 | if (IsFinished) { return false; } 94 | 95 | do 96 | { 97 | int index = _sequence.IndexOf(_separator); 98 | if (index < 0) 99 | { 100 | Current = _sequence; 101 | _spanSplitInfo |= SpanSplitInfo.FinishedEnumeration; 102 | return !(ShouldRemoveEmptyEntries && Current.IsEmpty); 103 | } 104 | 105 | Current = _sequence[..index]; 106 | _sequence = _sequence[(index + 1)..]; 107 | } while (Current.IsEmpty && ShouldRemoveEmptyEntries); 108 | 109 | return true; 110 | } 111 | 112 | [Flags] 113 | private enum SpanSplitInfo : byte 114 | { 115 | RemoveEmptyEntries = 0x1, 116 | FinishedEnumeration = 0x2 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Secs4Net.Sml/Secs4Net.Sml.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0;netstandard2.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Secs4Net/ConnectionState.cs: -------------------------------------------------------------------------------- 1 | namespace Secs4Net; 2 | 3 | public enum ConnectionState 4 | { 5 | Connecting, 6 | Connected, 7 | Selected, 8 | Retry, 9 | } 10 | -------------------------------------------------------------------------------- /src/Secs4Net/Extensions/ExtensionHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Secs4Net.Extensions; 5 | 6 | public static class SecsExtension 7 | { 8 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 9 | public static string GetName(this SecsFormat format) 10 | { 11 | return format switch 12 | { 13 | SecsFormat.List => nameof(SecsFormat.List), 14 | SecsFormat.Binary => nameof(SecsFormat.Binary), 15 | SecsFormat.Boolean => nameof(SecsFormat.Boolean), 16 | SecsFormat.ASCII => nameof(SecsFormat.ASCII), 17 | SecsFormat.JIS8 => nameof(SecsFormat.JIS8), 18 | SecsFormat.I8 => nameof(SecsFormat.I8), 19 | SecsFormat.I1 => nameof(SecsFormat.I1), 20 | SecsFormat.I2 => nameof(SecsFormat.I2), 21 | SecsFormat.I4 => nameof(SecsFormat.I4), 22 | SecsFormat.F8 => nameof(SecsFormat.F8), 23 | SecsFormat.F4 => nameof(SecsFormat.F4), 24 | SecsFormat.U8 => nameof(SecsFormat.U8), 25 | SecsFormat.U1 => nameof(SecsFormat.U1), 26 | SecsFormat.U2 => nameof(SecsFormat.U2), 27 | SecsFormat.U4 => nameof(SecsFormat.U4), 28 | _ => ThrowHelper(format), 29 | }; 30 | 31 | [DoesNotReturn] 32 | static string ThrowHelper(SecsFormat format) => throw new ArgumentOutOfRangeException(nameof(format), (int)format, "Invalid enum value"); 33 | } 34 | 35 | #if NET 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | internal static unsafe Span AsBytes(this scoped ref T value) where T : unmanaged 38 | => MemoryMarshal.CreateSpan(ref Unsafe.As(ref value), sizeof(T)); 39 | 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | internal static unsafe ReadOnlySpan AsReadOnlyBytes(this scoped ref T value) where T : unmanaged 42 | => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref value), sizeof(T)); 43 | #else 44 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 45 | internal static unsafe Span AsBytes(this scoped ref T value) where T : unmanaged 46 | => new(Unsafe.AsPointer(ref value), sizeof(T)); 47 | 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | internal static unsafe ReadOnlySpan AsReadOnlyBytes(this scoped ref T value) where T : unmanaged 50 | => new(Unsafe.AsPointer(ref value), sizeof(T)); 51 | #endif 52 | 53 | #if !NET 54 | internal static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 55 | { 56 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 57 | 58 | // This disposes the registration as soon as one of the tasks trigger 59 | using (cancellationToken.Register(static state => ((TaskCompletionSource)state!).TrySetResult(null), tcs)) 60 | { 61 | var resultTask = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false); 62 | if (resultTask == tcs.Task) 63 | { 64 | // Operation cancelled 65 | throw new OperationCanceledException(cancellationToken); 66 | } 67 | 68 | await task.ConfigureAwait(false); 69 | } 70 | } 71 | 72 | internal static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 73 | { 74 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 75 | 76 | // This disposes the registration as soon as one of the tasks trigger 77 | using (cancellationToken.Register(static state => ((TaskCompletionSource)state!).TrySetResult(null), tcs)) 78 | { 79 | var resultTask = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false); 80 | if (resultTask == tcs.Task) 81 | { 82 | // Operation cancelled 83 | throw new OperationCanceledException(cancellationToken); 84 | } 85 | 86 | return await task.ConfigureAwait(false); 87 | } 88 | } 89 | #endif 90 | } 91 | -------------------------------------------------------------------------------- /src/Secs4Net/ISecsConnection.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace Secs4Net; 4 | 5 | public interface ISecsConnection 6 | { 7 | public event EventHandler? ConnectionChanged; 8 | 9 | /// 10 | /// Connection state 11 | /// 12 | ConnectionState State { get; } 13 | 14 | /// 15 | /// Is Active or Passive mode 16 | /// 17 | bool IsActive { get; } 18 | 19 | /// 20 | /// When is the IP address will be treated remote device's IP address, 21 | /// opposite the connection will bind on this IP address as Passive mode. 22 | /// 23 | IPAddress IpAddress { get; } 24 | 25 | /// 26 | /// When is the port number will be treated remote device's TCP port number, 27 | /// opposite the connection will bind on the port number as Passive mode. 28 | /// 29 | int Port { get; } 30 | 31 | /// 32 | /// Remote device's IP address.
33 | /// In Active mode, it is the same as IPAddress property
34 | /// In Passive mode, remote IP Address can be resolved successfully only 35 | /// when is or , 36 | /// otherwise, return "N/A" 37 | ///
38 | string DeviceIpAddress { get; } 39 | bool LinkTestEnabled { get; set; } 40 | void Start(CancellationToken cancellation); 41 | void Reconnect(); 42 | 43 | internal Task SendAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken); 44 | internal IAsyncEnumerable<(MessageHeader header, Item? rootItem)> GetDataMessages(CancellationToken cancellation); 45 | } 46 | -------------------------------------------------------------------------------- /src/Secs4Net/ISecsGemLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Secs4Net; 2 | 3 | public interface ISecsGemLogger 4 | { 5 | #if NET 6 | void MessageIn(SecsMessage msg, int id) => Trace.WriteLine($"<-- [0x{id:X8}] {msg}"); 7 | void MessageOut(SecsMessage msg, int id) => Trace.WriteLine($"--> [0x{id:X8}] {msg}"); 8 | void Debug(string msg) => Trace.WriteLine(msg); 9 | void Info(string msg) => Trace.TraceInformation(msg); 10 | void Warning(string msg) => Trace.TraceWarning(msg); 11 | void Error(string msg) => Error(msg, message: null, ex: null); 12 | void Error(string msg, Exception ex) => Error(msg, message: null, ex); 13 | void Error(string msg, SecsMessage? message, Exception? ex) => Trace.TraceError($"{msg} {message}\n {ex}"); 14 | #else 15 | void MessageIn(SecsMessage msg, int id); 16 | void MessageOut(SecsMessage msg, int id); 17 | void Debug(string msg); 18 | void Info(string msg); 19 | void Warning(string msg); 20 | void Error(string msg); 21 | void Error(string msg, Exception ex); 22 | void Error(string msg, SecsMessage? message, Exception? ex); 23 | #endif 24 | } 25 | -------------------------------------------------------------------------------- /src/Secs4Net/Item.Decode.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.HighPerformance; 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | using System.Buffers; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | 8 | namespace Secs4Net; 9 | 10 | public partial class Item 11 | { 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | internal static void DecodeFormatAndLengthByteCount(in ReadOnlySequence sourceBytes, out SecsFormat format, out byte lengthByteCount) 14 | { 15 | #if NET 16 | var formatSeqFirstSpan = sourceBytes.FirstSpan; 17 | #else 18 | var formatSeqFirstSpan = sourceBytes.First.Span; 19 | #endif 20 | 21 | #if DEBUG 22 | byte formatAndLengthByte = formatSeqFirstSpan[0]; 23 | #else 24 | byte formatAndLengthByte = formatSeqFirstSpan.DangerousGetReference(); 25 | #endif 26 | format = (SecsFormat)(formatAndLengthByte >> 2); 27 | lengthByteCount = (byte)(formatAndLengthByte & 0b00000011); 28 | } 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | [SkipLocalsInit] 32 | internal static int DecodeDataLength(in ReadOnlySequence sourceBytes) 33 | { 34 | var dataLength = 0; 35 | var lengthBytes = dataLength.AsBytes(); 36 | sourceBytes.CopyTo(lengthBytes); 37 | lengthBytes[..(int)sourceBytes.Length].Reverse(); 38 | return dataLength; 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.NoInlining)] 42 | [SkipLocalsInit] 43 | public static Item DecodeFromFullBuffer(ref ReadOnlySequence bytes) 44 | { 45 | var formatSeq = bytes.Slice(0, 1); 46 | DecodeFormatAndLengthByteCount(formatSeq, out var format, out var lengthByteCount); 47 | 48 | var dataLengthSeq = bytes.Slice(formatSeq.End, lengthByteCount); 49 | var dataLength = DecodeDataLength(dataLengthSeq); 50 | bytes = bytes.Slice(dataLengthSeq.End); 51 | 52 | if (format == SecsFormat.List) 53 | { 54 | if (dataLength == 0) 55 | { 56 | return L(); 57 | } 58 | 59 | var items = new Item[dataLength]; 60 | foreach (ref var subItem in items.AsSpan()) 61 | { 62 | subItem = DecodeFromFullBuffer(ref bytes); 63 | } 64 | 65 | return L(items); 66 | } 67 | 68 | var dataItemBytes = bytes.Slice(0, dataLength); 69 | var item = DecodeDataItem(format, dataItemBytes); 70 | bytes = bytes.Slice(dataItemBytes.End); 71 | return item; 72 | } 73 | 74 | internal static Item DecodeDataItem(SecsFormat format, in ReadOnlySequence bytes) 75 | { 76 | var length = (int)bytes.Length; 77 | return (format, length) switch 78 | { 79 | (SecsFormat.ASCII, 0) => A(), 80 | (SecsFormat.ASCII, >= 256) => A(ASCIIEncoding.GetString(bytes)), 81 | (SecsFormat.ASCII, _) => A(DecodePooledString(length, bytes, ASCIIEncoding)), 82 | 83 | (SecsFormat.JIS8, 0) => J(), 84 | (SecsFormat.JIS8, >= 256) => J(JIS8Encoding.GetString(bytes)), 85 | (SecsFormat.JIS8, _) => J(DecodePooledString(length, bytes, JIS8Encoding)), 86 | 87 | (SecsFormat.Binary, 0) => B(), 88 | (SecsFormat.Binary, >= 1024) => B(DecodeMemoryOwner(length, bytes)), 89 | (SecsFormat.Binary, _) => B(DecodeMemory(length, bytes)), 90 | 91 | (SecsFormat.Boolean, 0) => Boolean(), 92 | (SecsFormat.Boolean, >= 1024) => Boolean(DecodeMemoryOwner(length, bytes)), 93 | (SecsFormat.Boolean, _) => Boolean(DecodeMemory(length, bytes)), 94 | 95 | (SecsFormat.I8, 0) => I8(), 96 | (SecsFormat.I8, >= 1024) => I8(DecodeMemoryOwner(length, bytes)), 97 | (SecsFormat.I8, _) => I8(DecodeMemory(length, bytes)), 98 | 99 | (SecsFormat.I1, 0) => I1(), 100 | (SecsFormat.I1, >= 1024) => I1(DecodeMemoryOwner(length, bytes)), 101 | (SecsFormat.I1, _) => I1(DecodeMemory(length, bytes)), 102 | 103 | (SecsFormat.I2, 0) => I2(), 104 | (SecsFormat.I2, >= 1024) => I2(DecodeMemoryOwner(length, bytes)), 105 | (SecsFormat.I2, _) => I2(DecodeMemory(length, bytes)), 106 | 107 | (SecsFormat.I4, 0) => I4(), 108 | (SecsFormat.I4, >= 1024) => I4(DecodeMemoryOwner(length, bytes)), 109 | (SecsFormat.I4, _) => I4(DecodeMemory(length, bytes)), 110 | 111 | (SecsFormat.F8, 0) => F8(), 112 | (SecsFormat.F8, >= 1024) => F8(DecodeMemoryOwner(length, bytes)), 113 | (SecsFormat.F8, _) => F8(DecodeMemory(length, bytes)), 114 | 115 | (SecsFormat.F4, 0) => F4(), 116 | (SecsFormat.F4, >= 1024) => F4(DecodeMemoryOwner(length, bytes)), 117 | (SecsFormat.F4, _) => F4(DecodeMemory(length, bytes)), 118 | 119 | (SecsFormat.U8, 0) => U8(), 120 | (SecsFormat.U8, >= 1024) => U8(DecodeMemoryOwner(length, bytes)), 121 | (SecsFormat.U8, _) => U8(DecodeMemory(length, bytes)), 122 | 123 | (SecsFormat.U1, 0) => U1(), 124 | (SecsFormat.U1, >= 1024) => U1(DecodeMemoryOwner(length, bytes)), 125 | (SecsFormat.U1, _) => U1(DecodeMemory(length, bytes)), 126 | 127 | (SecsFormat.U2, 0) => U2(), 128 | (SecsFormat.U2, >= 1024) => U2(DecodeMemoryOwner(length, bytes)), 129 | (SecsFormat.U2, _) => U2(DecodeMemory(length, bytes)), 130 | 131 | (SecsFormat.U4, 0) => U4(), 132 | (SecsFormat.U4, >= 1024) => U4(DecodeMemoryOwner(length, bytes)), 133 | (SecsFormat.U4, _) => U4(DecodeMemory(length, bytes)), 134 | _ => ThrowHelper(), 135 | }; 136 | 137 | [SkipLocalsInit] 138 | static string DecodePooledString(int length, in ReadOnlySequence bytes, Encoding encoding) 139 | { 140 | using var spanOwner = SpanOwner.Allocate(length); 141 | var span = spanOwner.Span; 142 | bytes.CopyTo(span); 143 | return StringPool.Shared.GetOrAdd(span, encoding); 144 | } 145 | 146 | [SkipLocalsInit] 147 | static unsafe Memory DecodeMemory(int length, in ReadOnlySequence bytes) where T : unmanaged, IEquatable 148 | { 149 | var memory = new T[length / sizeof(T)]; 150 | var span = memory.AsSpan(); 151 | bytes.CopyTo(span.AsBytes()); 152 | ReverseEndiannessHelper.Reverse(span); 153 | return memory; 154 | } 155 | 156 | [SkipLocalsInit] 157 | static unsafe IMemoryOwner DecodeMemoryOwner(int length, in ReadOnlySequence bytes) where T : unmanaged, IEquatable 158 | { 159 | var owner = MemoryOwner.Allocate(length / sizeof(T)); 160 | var span = owner.Span; 161 | bytes.CopyTo(span.AsBytes()); 162 | ReverseEndiannessHelper.Reverse(span); 163 | return owner; 164 | } 165 | 166 | [DoesNotReturn] 167 | static Item ThrowHelper() => throw new ArgumentOutOfRangeException(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Secs4Net/Item.Encode.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Secs4Net; 6 | 7 | public partial class Item 8 | { 9 | /// 10 | /// Encode Item header 11 | /// 12 | /// List item count or value bytes length 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | [SkipLocalsInit] 15 | private static void EncodeItemHeader(SecsFormat format, int count, IBufferWriter buffer) 16 | { 17 | ref var lengthRef0 = ref Unsafe.As(ref Unsafe.AsRef(in count)); 18 | ref var encodedRef0 = ref MemoryMarshal.GetReference(buffer.GetSpan(sizeHint: 4)); 19 | var formatByte = (int)format << 2; 20 | if (count <= 0xff) 21 | {// 1 byte 22 | 23 | encodedRef0 = (byte)(formatByte | 1); 24 | Unsafe.Add(ref encodedRef0, 1u) = lengthRef0; 25 | buffer.Advance(2); 26 | return; 27 | } 28 | if (count <= 0xff_ff) 29 | {// 2 byte 30 | encodedRef0 = (byte)(formatByte | 2); 31 | Unsafe.Add(ref encodedRef0, 1u) = Unsafe.Add(ref lengthRef0, 1u); 32 | Unsafe.Add(ref encodedRef0, 2u) = lengthRef0; 33 | buffer.Advance(3); 34 | return; 35 | } 36 | if (count <= 0xff_ff_ff) 37 | {// 3 byte 38 | encodedRef0 = (byte)(formatByte | 3); 39 | Unsafe.Add(ref encodedRef0, 1u) = Unsafe.Add(ref lengthRef0, 2u); 40 | Unsafe.Add(ref encodedRef0, 2u) = Unsafe.Add(ref lengthRef0, 1u); 41 | Unsafe.Add(ref encodedRef0, 3u) = lengthRef0; 42 | buffer.Advance(4); 43 | return; 44 | } 45 | 46 | ThrowHelper(count); 47 | 48 | [DoesNotReturn] 49 | static void ThrowHelper(int count) => throw new ArgumentOutOfRangeException(nameof(count), count, $@"Item length:{count} is overflow"); 50 | } 51 | 52 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 53 | [SkipLocalsInit] 54 | private static void EncodeEmptyItem(SecsFormat format, IBufferWriter buffer) 55 | { 56 | var span = buffer.GetSpan(sizeHint: 2); 57 | ref var r0 = ref MemoryMarshal.GetReference(span); 58 | Unsafe.Add(ref r0, 0u) = (byte)(((int)format << 2) | 1); 59 | Unsafe.Add(ref r0, 1u) = 0; 60 | buffer.Advance(2); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Secs4Net/Item.List.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Secs4Net; 5 | 6 | partial class Item 7 | { 8 | [DebuggerTypeProxy(typeof(ItemDebugView))] 9 | [SkipLocalsInit] 10 | private sealed class ListItem : Item 11 | { 12 | private readonly Item[] _value; 13 | 14 | internal ListItem(Item[] value) 15 | : base(SecsFormat.List) 16 | => _value = value; 17 | 18 | public override void Dispose() 19 | { 20 | foreach (ref readonly var a in _value.AsSpan()) 21 | { 22 | a.Dispose(); 23 | } 24 | } 25 | 26 | public override int Count => _value.Length; 27 | 28 | public override Item this[int index] 29 | { 30 | get => _value[index]; 31 | set => _value[index] = value; 32 | } 33 | 34 | public override Item[] Items => _value; 35 | 36 | public override void EncodeTo(IBufferWriter buffer) 37 | { 38 | var arr = _value.AsSpan(); 39 | if (arr.Length == 0) 40 | { 41 | EncodeEmptyItem(Format, buffer); 42 | return; 43 | } 44 | 45 | EncodeItemHeader(Format, arr.Length, buffer); 46 | foreach (ref readonly var item in arr) 47 | { 48 | item.EncodeTo(buffer); 49 | } 50 | } 51 | 52 | private protected override bool IsEquals(Item other) 53 | => Format == other.Format && IsListEquals(_value, Unsafe.As(other)._value); 54 | 55 | [MethodImpl(MethodImplOptions.NoInlining)] 56 | private static bool IsListEquals(Item[] listLeft, Item[] listRight) 57 | { 58 | if (listLeft.Length != listRight.Length) 59 | { 60 | return false; 61 | } 62 | 63 | for (int i = 0, count = listLeft.Length; i < count; i++) 64 | { 65 | if (!listLeft[i].IsEquals(listRight[i])) 66 | { 67 | return false; 68 | } 69 | } 70 | 71 | return true; 72 | } 73 | 74 | private sealed class ItemDebugView(ListItem item) 75 | { 76 | 77 | [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] 78 | public Item[] Items => item._value; 79 | 80 | public EncodedByteDebugView EncodedBytes { get; } = new EncodedByteDebugView(item); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Secs4Net/Item.Memory.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.HighPerformance; 2 | using System.Buffers; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Secs4Net; 7 | 8 | public partial class Item 9 | { 10 | [DebuggerTypeProxy(typeof(MemoryItem<>.ItemDebugView))] 11 | [SkipLocalsInit] 12 | private class MemoryItem : Item where T : unmanaged, IEquatable 13 | { 14 | private readonly Memory _value; 15 | 16 | internal MemoryItem(SecsFormat format, Memory value) 17 | : base(format) 18 | => _value = value; 19 | 20 | public sealed override int Count => _value.Length; 21 | 22 | public sealed override ref TResult FirstValue() 23 | { 24 | var span = _value.Span; 25 | if (span.Length * Unsafe.SizeOf() < Unsafe.SizeOf()) 26 | { 27 | ThrowHelper(); 28 | } 29 | 30 | return ref Unsafe.As(ref MemoryMarshal.GetReference(span)); 31 | 32 | [DoesNotReturn] 33 | static void ThrowHelper() => throw new IndexOutOfRangeException($"The item is empty or data length less than sizeof({typeof(T).Name})"); 34 | } 35 | 36 | public sealed override TResult FirstValueOrDefault(TResult defaultValue = default) 37 | { 38 | var span = _value.Span; 39 | if (span.Length * Unsafe.SizeOf() < Unsafe.SizeOf()) 40 | { 41 | return defaultValue; 42 | } 43 | 44 | return Unsafe.As(ref MemoryMarshal.GetReference(span)); 45 | } 46 | 47 | public sealed override Memory GetMemory() 48 | => _value.Cast(); 49 | 50 | public sealed override unsafe void EncodeTo(IBufferWriter buffer) 51 | { 52 | var value = _value; 53 | if (value.IsEmpty) 54 | { 55 | EncodeEmptyItem(Format, buffer); 56 | return; 57 | } 58 | 59 | var valueByteSpan = MemoryMarshal.AsBytes(value.Span); 60 | var byteLength = valueByteSpan.Length; 61 | 62 | EncodeItemHeader(Format, byteLength, buffer); 63 | 64 | var bufferByteSpan = buffer.GetSpan(byteLength)[..byteLength]; 65 | valueByteSpan.CopyTo(bufferByteSpan); 66 | ReverseEndiannessHelper.Reverse(Cast(bufferByteSpan)); 67 | buffer.Advance(byteLength); 68 | 69 | #if NET 70 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 71 | static Span Cast(Span bytes) 72 | => MemoryMarshal.CreateSpan(ref Unsafe.As(ref MemoryMarshal.GetReference(bytes)), bytes.Length / sizeof(T)); 73 | #else 74 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 75 | static Span Cast(Span bytes) 76 | => new(Unsafe.AsPointer(ref MemoryMarshal.GetReference(bytes)), bytes.Length / sizeof(T)); 77 | #endif 78 | } 79 | 80 | private protected sealed override bool IsEquals(Item other) 81 | => Format == other.Format && _value.Span.SequenceEqual(Unsafe.As>(other)._value.Span); 82 | 83 | private sealed class ItemDebugView(MemoryItem item) 84 | { 85 | public Span Value => item._value.Span; 86 | public EncodedByteDebugView EncodedBytes { get; } = new EncodedByteDebugView(item); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Secs4Net/Item.MemoryOwner.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace Secs4Net; 4 | 5 | partial class Item 6 | { 7 | private sealed class MemoryOwnerItem : MemoryItem where T : unmanaged, IEquatable 8 | { 9 | private readonly IMemoryOwner _owner; 10 | 11 | internal MemoryOwnerItem(SecsFormat format, IMemoryOwner memoryOwner) 12 | : base(format, memoryOwner.Memory) 13 | => _owner = memoryOwner; 14 | 15 | public override void Dispose() 16 | => _owner.Dispose(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Secs4Net/Item.String.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Runtime.CompilerServices; 3 | using System.Text; 4 | 5 | namespace Secs4Net; 6 | 7 | partial class Item 8 | { 9 | [DebuggerTypeProxy(typeof(ItemDebugView))] 10 | [SkipLocalsInit] 11 | private sealed class StringItem : Item 12 | { 13 | private readonly string _value; 14 | 15 | internal StringItem(SecsFormat format, string value) 16 | : base(format) 17 | => _value = value; 18 | 19 | public override int Count 20 | => _value.Length; 21 | 22 | public override string GetString() 23 | => _value; 24 | 25 | public override void EncodeTo(IBufferWriter buffer) 26 | { 27 | if (_value.Length == 0) 28 | { 29 | EncodeEmptyItem(Format, buffer); 30 | return; 31 | } 32 | 33 | var encoder = Format == SecsFormat.ASCII ? ASCIIEncoding : JIS8Encoding; 34 | var byteLength = encoder.GetByteCount(_value); 35 | EncodeItemHeader(Format, byteLength, buffer); 36 | var length = encoder.GetBytes(_value, buffer.GetSpan(byteLength)); 37 | buffer.Advance(byteLength); 38 | Debug.Assert(length == byteLength); 39 | } 40 | 41 | private protected override bool IsEquals(Item other) 42 | => Format == other.Format && _value.Equals(other.GetString(), StringComparison.Ordinal); 43 | 44 | private sealed class ItemDebugView(Item.StringItem item) 45 | { 46 | public string Value => item._value; 47 | public EncodedByteDebugView EncodedBytes { get; } = new EncodedByteDebugView(item); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Secs4Net/Item.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.HighPerformance.Buffers; 2 | using System.Buffers; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | 6 | namespace Secs4Net; 7 | 8 | public abstract partial class Item : IEquatable, IDisposable 9 | { 10 | public static Encoding JIS8Encoding { get; set; } = Encoding.UTF8; 11 | public static Encoding ASCIIEncoding { get; set; } = Encoding.ASCII; 12 | 13 | public SecsFormat Format { get; } 14 | 15 | static Item() 16 | { 17 | if (!BitConverter.IsLittleEndian) 18 | { 19 | throw new PlatformNotSupportedException("secs4net is only work on little endian platform."); 20 | } 21 | } 22 | 23 | private protected Item(SecsFormat format) 24 | { 25 | Format = format; 26 | } 27 | 28 | public abstract int Count { get; } 29 | 30 | /// 31 | /// Encode the item to SECS binary format 32 | /// 33 | public abstract void EncodeTo(IBufferWriter buffer); 34 | 35 | /// 36 | /// Indexer of List items. 37 | /// Be careful of setter operation. Since the original slot will be overridden. 38 | /// So, it has no chance to be Disposed along with the List's Dispose method. 39 | /// You can invoke method on the original item by yourself or till the GC collects it. 40 | /// 41 | /// When the item's is not 42 | /// 43 | public virtual Item this[int index] 44 | { 45 | get => throw ThrowNotSupportException(Format); 46 | set => throw ThrowNotSupportException(Format); 47 | } 48 | 49 | /// 50 | /// Get List's sub-items 51 | /// 52 | /// When the item's is not 53 | public virtual Item[] Items 54 | => throw ThrowNotSupportException(Format); 55 | 56 | /// 57 | /// Get the first element of item array value 58 | /// 59 | /// 60 | /// 61 | /// When item is empty or data length less than sizeof() 62 | /// when the item's is or or 63 | public virtual ref T FirstValue() where T : unmanaged, IEquatable 64 | => throw ThrowNotSupportException(Format); 65 | 66 | /// 67 | /// Get the first element of item array value 68 | /// 69 | /// 70 | /// 71 | /// when is or or 72 | public virtual T FirstValueOrDefault(T defaultValue = default) where T : unmanaged, IEquatable 73 | => throw ThrowNotSupportException(Format); 74 | 75 | /// 76 | /// Get item array as 77 | /// 78 | /// when is or or 79 | public virtual Memory GetMemory() where T : unmanaged, IEquatable 80 | => throw ThrowNotSupportException(Format); 81 | 82 | /// 83 | /// Get item string value 84 | /// 85 | /// 86 | /// when the is not or 87 | public virtual string GetString() 88 | => throw ThrowNotSupportException(Format); 89 | 90 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 91 | private static NotSupportedException ThrowNotSupportException(SecsFormat format, [CallerMemberName] string? memberName = null) 92 | => new($"{memberName} is not supported, since the item's {nameof(Format)} is {format}"); 93 | 94 | public virtual void Dispose() 95 | { 96 | GC.SuppressFinalize(this); 97 | } 98 | 99 | public static bool operator !=(Item? r1, Item? r2) 100 | => !(r1 == r2); 101 | 102 | public static bool operator ==(Item? r1, Item? r2) 103 | => ReferenceEquals(r1, r2) || (r1?.Equals(r2) ?? false); 104 | 105 | public sealed override bool Equals(object? obj) 106 | => Equals(obj as Item); 107 | 108 | public bool Equals(Item? other) 109 | => other is not null && IsEquals(other); 110 | 111 | private protected abstract bool IsEquals(Item other); 112 | 113 | public sealed override string ToString() => $"{Format.GetName()} [{Count}]"; 114 | 115 | internal byte[] GetEncodedBytes() 116 | { 117 | using var buffer = new ArrayPoolBufferWriter(); 118 | EncodeTo(buffer); 119 | return buffer.WrittenSpan.ToArray(); 120 | } 121 | 122 | [DebuggerDisplay("Encoded Bytes")] 123 | private sealed class EncodedByteDebugView(Item item) 124 | { 125 | 126 | [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] 127 | public byte[] Bytes => item.GetEncodedBytes(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Secs4Net/MessageHeader.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Buffers.Binary; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Secs4Net; 6 | 7 | public readonly record struct MessageHeader 8 | { 9 | public int Id { get; init; } 10 | public ushort DeviceId { get; init; } 11 | public MessageType MessageType { get; init; } 12 | public byte S { get; init; } 13 | public byte F { get; init; } 14 | public bool ReplyExpected { get; init; } 15 | 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | [SkipLocalsInit] 18 | internal void EncodeTo(IBufferWriter buffer) 19 | { 20 | var span = buffer.GetSpan(sizeHint: 10); 21 | BinaryPrimitives.WriteUInt16BigEndian(span, DeviceId); 22 | ref var r0 = ref MemoryMarshal.GetReference(span); 23 | Unsafe.Add(ref r0, 2u) = (byte)(S | (ReplyExpected ? 0b1000_0000 : 0)); 24 | Unsafe.Add(ref r0, 3u) = F; 25 | Unsafe.Add(ref r0, 4u) = 0; 26 | Unsafe.Add(ref r0, 5u) = (byte)MessageType; 27 | BinaryPrimitives.WriteInt32BigEndian(span[6..], Id); 28 | buffer.Advance(10); 29 | } 30 | 31 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 32 | [SkipLocalsInit] 33 | internal static void Decode(ReadOnlySpan span, out MessageHeader header) 34 | { 35 | ref var r0 = ref MemoryMarshal.GetReference(span); 36 | var s = Unsafe.Add(ref r0, 2u); 37 | header = new MessageHeader 38 | { 39 | DeviceId = (ushort)(BinaryPrimitives.ReadUInt16BigEndian(span) & 0b01111111_11111111), 40 | ReplyExpected = (s & 0b1000_0000) != 0, 41 | S = (byte)(s & 0b0111_1111), 42 | F = Unsafe.Add(ref r0, 3u), 43 | MessageType = (MessageType)Unsafe.Add(ref r0, 5u), 44 | Id = BinaryPrimitives.ReadInt32BigEndian(span[6..]), 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Secs4Net/MessageIdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Secs4Net; 4 | 5 | internal static class MessageIdGenerator 6 | { 7 | #if NET 8 | private static int _id = Random.Shared.Next(); 9 | #else 10 | private static int _id = new Random(Guid.NewGuid().GetHashCode()).Next(); 11 | #endif 12 | 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public static int NewId() => Interlocked.Increment(ref _id); 15 | } 16 | -------------------------------------------------------------------------------- /src/Secs4Net/MessageType.cs: -------------------------------------------------------------------------------- 1 | namespace Secs4Net; 2 | 3 | public enum MessageType : byte 4 | { 5 | DataMessage = 0b0000_0000, 6 | SelectRequest = 0b0000_0001, 7 | SelectResponse = 0b0000_0010, 8 | Deselect_req = 0b0000_0011, 9 | Deselect_rsp = 0b0000_0100, 10 | LinkTestRequest = 0b0000_0101, 11 | LinkTestResponse = 0b0000_0110, 12 | Reject_req = 0b0000_0111, 13 | SeparateRequest = 0b0000_1001 14 | } 15 | -------------------------------------------------------------------------------- /src/Secs4Net/PrimaryMessageWrapper.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.HighPerformance.Buffers; 2 | 3 | namespace Secs4Net; 4 | 5 | public sealed class PrimaryMessageWrapper 6 | { 7 | private readonly SemaphoreSlim _semaphoreSlim = new(initialCount: 1); 8 | private readonly WeakReference _secsGem; 9 | public SecsMessage PrimaryMessage { get; } 10 | public int Id { get; } 11 | public SecsMessage? SecondaryMessage { get; private set; } 12 | 13 | internal PrimaryMessageWrapper(SecsGem secsGem, SecsMessage primaryMessage, int id) 14 | { 15 | _secsGem = new WeakReference(secsGem); 16 | PrimaryMessage = primaryMessage; 17 | Id = id; 18 | } 19 | 20 | /// 21 | /// If the message has already replied, method will return false. 22 | /// 23 | /// Reply S9F7 if parameter is null 24 | /// true, if reply success. 25 | public async Task TryReplyAsync(SecsMessage? replyMessage = null, CancellationToken cancellation = default) 26 | { 27 | if (!PrimaryMessage.ReplyExpected) 28 | { 29 | throw new SecsException("The message does not need to reply"); 30 | } 31 | 32 | if (!_secsGem.TryGetTarget(out var secsGem)) 33 | { 34 | throw new SecsException("Hsms connector loss, the message has no chance to reply via the ReplyAsync method"); 35 | } 36 | 37 | if (replyMessage is null) 38 | { 39 | var headerBytes = new byte[10]; 40 | var buffer = new MemoryBufferWriter(headerBytes); 41 | new MessageHeader 42 | { 43 | DeviceId = secsGem.DeviceId, 44 | ReplyExpected = PrimaryMessage.ReplyExpected, 45 | S = PrimaryMessage.S, 46 | F = PrimaryMessage.F, 47 | MessageType = MessageType.DataMessage, 48 | Id = Id 49 | }.EncodeTo(buffer); 50 | replyMessage = new SecsMessage(9, 7, replyExpected: false) 51 | { 52 | Name = "Unknown Message", 53 | SecsItem = Item.B(headerBytes), 54 | }; 55 | } 56 | else 57 | { 58 | replyMessage.ReplyExpected = false; 59 | } 60 | 61 | await _semaphoreSlim.WaitAsync(cancellation).ConfigureAwait(false); 62 | try 63 | { 64 | if (SecondaryMessage is not null) 65 | { 66 | return false; 67 | } 68 | 69 | int id = replyMessage.S == 9 ? MessageIdGenerator.NewId() : Id; 70 | await secsGem.SendDataMessageAsync(replyMessage, id, cancellation).ConfigureAwait(false); 71 | SecondaryMessage = replyMessage; 72 | return true; 73 | } 74 | finally 75 | { 76 | _semaphoreSlim.Release(); 77 | } 78 | } 79 | 80 | public override string ToString() => PrimaryMessage.ToString(); 81 | } 82 | -------------------------------------------------------------------------------- /src/Secs4Net/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Secs4Net { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Secs4Net.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Unrecognized Device Id. 65 | /// 66 | internal static string S9F1 { 67 | get { 68 | return ResourceManager.GetString("S9F1", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Data Too Long. 74 | /// 75 | internal static string S9F11 { 76 | get { 77 | return ResourceManager.GetString("S9F11", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Conversation Timeout. 83 | /// 84 | internal static string S9F13 { 85 | get { 86 | return ResourceManager.GetString("S9F13", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Unrecognized Stream Type. 92 | /// 93 | internal static string S9F3 { 94 | get { 95 | return ResourceManager.GetString("S9F3", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Unrecognized Function Type. 101 | /// 102 | internal static string S9F5 { 103 | get { 104 | return ResourceManager.GetString("S9F5", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to Illegal Data. 110 | /// 111 | internal static string S9F7 { 112 | get { 113 | return ResourceManager.GetString("S9F7", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to Transaction Timer Timeout. 119 | /// 120 | internal static string S9F9 { 121 | get { 122 | return ResourceManager.GetString("S9F9", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to S9Fy message reply.. 128 | /// 129 | internal static string S9Fy { 130 | get { 131 | return ResourceManager.GetString("S9Fy", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to Stream number must be less than 127. 137 | /// 138 | internal static string SecsMessageStreamNumberMustLessThan127 { 139 | get { 140 | return ResourceManager.GetString("SecsMessageStreamNumberMustLessThan127", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to Equipment is not online mode. 146 | /// 147 | internal static string SxF0 { 148 | get { 149 | return ResourceManager.GetString("SxF0", resourceCulture); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized string similar to T3 Timeout!. 155 | /// 156 | internal static string T3Timeout { 157 | get { 158 | return ResourceManager.GetString("T3Timeout", resourceCulture); 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Secs4Net/Secs4Net.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0;netstandard2.0 5 | true 6 | True 7 | true 8 | README.md 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <_Parameter1>Secs4Net.UnitTests 18 | 19 | 20 | <_Parameter1>Benchmarks 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | True 44 | \ 45 | 46 | 47 | 48 | 49 | 50 | True 51 | True 52 | Resources.resx 53 | 54 | 55 | 56 | 57 | 58 | ResXFileCodeGenerator 59 | Resources.Designer.cs 60 | Secs4Net 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/Secs4Net/SecsException.cs: -------------------------------------------------------------------------------- 1 | namespace Secs4Net; 2 | 3 | public sealed class SecsException : Exception 4 | { 5 | public SecsMessage? SecsMessage { get; } 6 | 7 | public SecsException(SecsMessage? secsMessage, string errorMessage) 8 | : base(errorMessage) 9 | { 10 | SecsMessage = secsMessage; 11 | } 12 | 13 | public SecsException(string msg) 14 | : base(msg) 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Secs4Net/SecsFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Secs4Net; 2 | 3 | public enum SecsFormat 4 | { 5 | List = 0b_0000_00, 6 | Binary = 0b_0010_00, 7 | Boolean = 0b_0010_01, 8 | ASCII = 0b_0100_00, 9 | JIS8 = 0b_0100_01, 10 | I8 = 0b_0110_00, 11 | I1 = 0b_0110_01, 12 | I2 = 0b_0110_10, 13 | I4 = 0b_0111_00, 14 | F8 = 0b_1000_00, 15 | F4 = 0b_1001_00, 16 | U8 = 0b_1010_00, 17 | U1 = 0b_1010_01, 18 | U2 = 0b_1010_10, 19 | U4 = 0b_1011_00, 20 | } 21 | -------------------------------------------------------------------------------- /src/Secs4Net/SecsGemOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Secs4Net; 2 | 3 | public class SecsGemOptions 4 | { 5 | public ushort DeviceId { get; set; } 6 | 7 | /// 8 | /// Configure connection as Active or Passive mode. 9 | /// 10 | public bool IsActive { get; set; } 11 | 12 | /// 13 | /// When is the IP address will be treated remote device's IP address, 14 | /// opposite the connection will bind on this IP address as Passive mode. 15 | /// Default value is "127.0.0.1". 16 | /// 17 | public string IpAddress { get; set; } = "127.0.0.1"; 18 | 19 | /// 20 | /// When is the port number will be treated remote device's TCP port number, 21 | /// opposite the connection will bind on the port number as Passive mode. 22 | /// Default value is 5000. 23 | /// 24 | public int Port { get; set; } = 5000; 25 | 26 | /// 27 | /// Configure the timeout interval in milliseconds between the primary message sent till to receive the secondary message. 28 | /// Default value is 45000 milliseconds. 29 | /// 30 | public int T3 { get; set; } = 45000; 31 | 32 | /// 33 | /// Configure the timeout interval in milliseconds between the connection state transition from to . 34 | /// Default value is 10000 milliseconds. 35 | /// 36 | public int T5 { get; set; } = 10000; 37 | 38 | /// 39 | /// Configure the timeout interval in milliseconds between the control message sent till to receive the reply message. 40 | /// Default value is 5000 milliseconds. 41 | /// 42 | public int T6 { get; set; } = 5000; 43 | 44 | /// 45 | /// Configure the timeout interval in milliseconds between the connection state transition from to . 46 | /// Default value is 10000 milliseconds. 47 | /// 48 | public int T7 { get; set; } = 10000; 49 | 50 | /// 51 | /// Configure the timeout interval in milliseconds between the chunk received to next chunk during decoding a . 52 | /// Default value is 5000 milliseconds. 53 | /// 54 | public int T8 { get; set; } = 5000; 55 | 56 | /// 57 | /// Configure the timer interval in milliseconds between each request. 58 | /// Default value is 60000. 59 | /// 60 | public int LinkTestInterval { get; set; } = 60000; 61 | 62 | /// 63 | /// Configure a value that specifies the size of the receive buffer of the System.Net.Sockets.Socket. 64 | /// Default value is 8192 bytes. 65 | /// 66 | public int SocketReceiveBufferSize { get; set; } = 8192; 67 | 68 | /// 69 | /// Configure the initial buffer size in bytes for the encoding. 70 | /// Default value is 4096 bytes. 71 | /// 72 | public int EncodeBufferInitialSize { get; set; } = 4096; 73 | } 74 | -------------------------------------------------------------------------------- /src/Secs4Net/SecsMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Secs4Net; 2 | 3 | public sealed class SecsMessage : IDisposable 4 | { 5 | public override string ToString() => $"'S{S}F{F}' {(ReplyExpected ? "W" : string.Empty)} {Name ?? string.Empty}"; 6 | 7 | /// 8 | /// message stream number 9 | /// 10 | public byte S { get; } 11 | 12 | /// 13 | /// message function number 14 | /// 15 | public byte F { get; } 16 | 17 | /// 18 | /// expect reply message 19 | /// 20 | public bool ReplyExpected { get; internal set; } 21 | 22 | public string? Name { get; set; } 23 | 24 | /// 25 | /// the root item of message 26 | /// 27 | public Item? SecsItem { get; set; } 28 | 29 | /// 30 | /// constructor of SecsMessage 31 | /// 32 | /// message stream number 33 | /// message function number 34 | /// expect reply message 35 | public SecsMessage(byte s, byte f, bool replyExpected = true) 36 | { 37 | if (s > 0b0111_1111) 38 | { 39 | throw new ArgumentOutOfRangeException(nameof(s), s, Resources.SecsMessageStreamNumberMustLessThan127); 40 | } 41 | 42 | S = s; 43 | F = f; 44 | ReplyExpected = replyExpected; 45 | } 46 | 47 | public void Dispose() 48 | { 49 | SecsItem?.Dispose(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Secs4Net/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Diagnostics; 3 | global using System.Threading; 4 | global using System.Threading.Tasks; 5 | global using System.Collections.Generic; 6 | global using System.Linq; 7 | global using System.Runtime.InteropServices; 8 | global using Secs4Net.Extensions; 9 | -------------------------------------------------------------------------------- /test/Benchmarks/BenchmarkConfig.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Columns; 2 | using BenchmarkDotNet.Configs; 3 | using BenchmarkDotNet.Exporters; 4 | using BenchmarkDotNet.Loggers; 5 | using System.Linq; 6 | 7 | namespace Benchmarks; 8 | 9 | public class BenchmarkConfig : ManualConfig 10 | { 11 | public BenchmarkConfig() 12 | { 13 | this.WithCultureInfo(System.Globalization.CultureInfo.InvariantCulture); 14 | 15 | WithOption(ConfigOptions.DisableLogFile, true); 16 | WithOption(ConfigOptions.DontOverwriteResults, false); 17 | AddLogger(ConsoleLogger.Ascii); 18 | AddExporter(MarkdownExporter.GitHub); 19 | 20 | AddColumnProvider( 21 | DefaultColumnProviders.Descriptor, 22 | DefaultColumnProviders.Params, 23 | new SimpleColumnProvider(JobCharacteristicColumn.AllColumns.Where(c => c.ColumnName == "Runtime").ToArray()), 24 | DefaultColumnProviders.Statistics, 25 | DefaultColumnProviders.Metrics); 26 | 27 | SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default 28 | .WithRatioStyle(RatioStyle.Trend); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.ComplexItemEncodeDecode-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | Unknown processor 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | Job-ERYFHQ : .NET 6.0.26 (6.0.2623.60508), X64 RyuJIT AVX2 8 | Job-GVMCZA : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 9 | Job-BVWEYE : .NET Framework 4.8.1 (4.8.9181.0), X64 RyuJIT VectorSize=256 10 | 11 | 12 | ``` 13 | | Method | Runtime | ItemCount | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 14 | |------- |------------------- |---------- |-----------:|----------:|----------:|-------------:|--------:|----------:|------------:| 15 | | **Encode** | **.NET 6.0** | **0** | **191.4 ns** | **2.29 ns** | **2.15 ns** | **3.91x faster** | **0.05x** | **40 B** | **1.00x more** | 16 | | Encode | .NET 8.0 | 0 | 121.2 ns | 1.29 ns | 1.08 ns | 6.17x faster | 0.08x | 40 B | 1.00x more | 17 | | Encode | .NET Framework 4.8 | 0 | 747.9 ns | 7.58 ns | 6.72 ns | baseline | | 40 B | | 18 | | | | | | | | | | | | 19 | | Decode | .NET 6.0 | 0 | 1,497.3 ns | 23.17 ns | 21.67 ns | 2.86x faster | 0.04x | 560 B | 1.00x less | 20 | | Decode | .NET 8.0 | 0 | 1,245.6 ns | 17.28 ns | 15.32 ns | 3.44x faster | 0.07x | 560 B | 1.00x less | 21 | | Decode | .NET Framework 4.8 | 0 | 4,283.4 ns | 67.62 ns | 63.25 ns | baseline | | 562 B | | 22 | | | | | | | | | | | | 23 | | **Encode** | **.NET 6.0** | **64** | **750.1 ns** | **10.31 ns** | **9.14 ns** | **3.72x faster** | **0.07x** | **40 B** | **1.00x more** | 24 | | Encode | .NET 8.0 | 64 | 616.0 ns | 10.61 ns | 9.92 ns | 4.54x faster | 0.10x | 40 B | 1.00x more | 25 | | Encode | .NET Framework 4.8 | 64 | 2,793.4 ns | 40.06 ns | 37.47 ns | baseline | | 40 B | | 26 | | | | | | | | | | | | 27 | | Decode | .NET 6.0 | 64 | 2,669.1 ns | 26.61 ns | 23.59 ns | 2.67x faster | 0.03x | 7704 B | 1.01x less | 28 | | Decode | .NET 8.0 | 64 | 2,363.6 ns | 45.89 ns | 42.93 ns | 3.01x faster | 0.07x | 7704 B | 1.01x less | 29 | | Decode | .NET Framework 4.8 | 64 | 7,113.2 ns | 67.42 ns | 59.77 ns | baseline | | 7751 B | | 30 | | | | | | | | | | | | 31 | | **Encode** | **.NET 6.0** | **128** | **1,141.0 ns** | **20.50 ns** | **19.17 ns** | **3.33x faster** | **0.07x** | **40 B** | **1.00x more** | 32 | | Encode | .NET 8.0 | 128 | 978.5 ns | 19.06 ns | 17.83 ns | 3.89x faster | 0.10x | 40 B | 1.00x more | 33 | | Encode | .NET Framework 4.8 | 128 | 3,799.2 ns | 44.26 ns | 39.23 ns | baseline | | 40 B | | 34 | | | | | | | | | | | | 35 | | Decode | .NET 6.0 | 128 | 3,271.0 ns | 30.54 ns | 27.07 ns | 2.60x faster | 0.05x | 9952 B | 1.01x less | 36 | | Decode | .NET 8.0 | 128 | 2,896.4 ns | 49.87 ns | 46.65 ns | 2.94x faster | 0.06x | 9952 B | 1.01x less | 37 | | Decode | .NET Framework 4.8 | 128 | 8,500.2 ns | 154.15 ns | 144.19 ns | baseline | | 10006 B | | 38 | -------------------------------------------------------------------------------- /test/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonSerialization-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | Unknown processor 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | Job-ISCVXC : .NET 6.0.26 (6.0.2623.60508), X64 RyuJIT AVX2 8 | Job-DINVIM : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 9 | Job-OKZTLT : .NET Framework 4.8.1 (4.8.9181.0), X64 RyuJIT VectorSize=256 10 | 11 | 12 | ``` 13 | | Method | Runtime | ItemCount | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | 14 | |----------- |------------------- |---------- |-------------:|-----------:|-----------:|-------------:|--------:|----------:|------------:| 15 | | **Serialize** | **.NET 6.0** | **0** | **2.081 μs** | **0.0400 μs** | **0.0428 μs** | **3.06x faster** | **0.07x** | **1.02 KB** | **1.01x less** | 16 | | Serialize | .NET 8.0 | 0 | 1.413 μs | 0.0173 μs | 0.0145 μs | 4.50x faster | 0.07x | 1.02 KB | 1.01x less | 17 | | Serialize | .NET Framework 4.8 | 0 | 6.365 μs | 0.0584 μs | 0.0518 μs | baseline | | 1.03 KB | | 18 | | | | | | | | | | | | 19 | | Deserialze | .NET 6.0 | 0 | 14.862 μs | 0.1605 μs | 0.1423 μs | 3.25x faster | 0.05x | 9.55 KB | 1.49x less | 20 | | Deserialze | .NET 8.0 | 0 | 8.736 μs | 0.1747 μs | 0.1548 μs | 5.54x faster | 0.13x | 9.55 KB | 1.49x less | 21 | | Deserialze | .NET Framework 4.8 | 0 | 48.260 μs | 0.6721 μs | 0.6287 μs | baseline | | 14.23 KB | | 22 | | | | | | | | | | | | 23 | | **Serialize** | **.NET 6.0** | **64** | **30.436 μs** | **0.2796 μs** | **0.2479 μs** | **3.48x faster** | **0.05x** | **15.05 KB** | **2.87x less** | 24 | | Serialize | .NET 8.0 | 64 | 24.001 μs | 0.2593 μs | 0.2298 μs | 4.41x faster | 0.06x | 15.05 KB | 2.87x less | 25 | | Serialize | .NET Framework 4.8 | 64 | 105.791 μs | 1.1450 μs | 1.0710 μs | baseline | | 43.17 KB | | 26 | | | | | | | | | | | | 27 | | Deserialze | .NET 6.0 | 64 | 143.098 μs | 1.7480 μs | 1.5495 μs | 4.13x faster | 0.07x | 42.23 KB | 1.11x less | 28 | | Deserialze | .NET 8.0 | 64 | 92.824 μs | 1.1412 μs | 1.0675 μs | 6.38x faster | 0.17x | 42.23 KB | 1.11x less | 29 | | Deserialze | .NET Framework 4.8 | 64 | 592.253 μs | 11.3500 μs | 11.6557 μs | baseline | | 47.02 KB | | 30 | | | | | | | | | | | | 31 | | **Serialize** | **.NET 6.0** | **128** | **56.813 μs** | **0.5838 μs** | **0.5461 μs** | **3.50x faster** | **0.08x** | **28.3 KB** | **2.99x less** | 32 | | Serialize | .NET 8.0 | 128 | 47.051 μs | 0.4885 μs | 0.4569 μs | 4.23x faster | 0.05x | 28.3 KB | 2.99x less | 33 | | Serialize | .NET Framework 4.8 | 128 | 199.065 μs | 3.2163 μs | 2.8512 μs | baseline | | 84.5 KB | | 34 | | | | | | | | | | | | 35 | | Deserialze | .NET 6.0 | 128 | 268.396 μs | 4.9262 μs | 4.6080 μs | 4.20x faster | 0.08x | 74.61 KB | 1.06x less | 36 | | Deserialze | .NET 8.0 | 128 | 172.645 μs | 2.3313 μs | 2.0666 μs | 6.53x faster | 0.11x | 74.61 KB | 1.06x less | 37 | | Deserialze | .NET Framework 4.8 | 128 | 1,127.991 μs | 13.8243 μs | 12.2549 μs | baseline | | 79.45 KB | | 38 | -------------------------------------------------------------------------------- /test/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.PipeDecoding-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | Unknown processor 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | Job-ERYFHQ : .NET 6.0.26 (6.0.2623.60508), X64 RyuJIT AVX2 8 | Job-GVMCZA : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 9 | Job-BVWEYE : .NET Framework 4.8.1 (4.8.9181.0), X64 RyuJIT VectorSize=256 10 | 11 | 12 | ``` 13 | | Method | Runtime | InputChunkSize | MessageCount | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | 14 | |----------------- |------------------- |--------------- |------------- |---------:|----------:|----------:|---------:|-------------:|--------:|---------:|-------:|----------:|------------:| 15 | | **Chunked_Sequence** | **.NET 6.0** | **16** | **500** | **4.027 ms** | **0.0764 ms** | **0.0715 ms** | **4.016 ms** | **2.06x faster** | **0.08x** | **156.2500** | **7.8125** | **2.02 MB** | **1.04x less** | 16 | | Chunked_Sequence | .NET 8.0 | 16 | 500 | 3.089 ms | 0.0497 ms | 0.1273 ms | 3.035 ms | 2.70x faster | 0.14x | 160.1563 | 7.8125 | 2.02 MB | 1.04x less | 17 | | Chunked_Sequence | .NET Framework 4.8 | 16 | 500 | 8.214 ms | 0.1628 ms | 0.2675 ms | 8.214 ms | baseline | | 343.7500 | - | 2.09 MB | | 18 | | | | | | | | | | | | | | | | 19 | | **Chunked_Sequence** | **.NET 6.0** | **64** | **500** | **2.915 ms** | **0.0362 ms** | **0.0338 ms** | **2.925 ms** | **2.14x faster** | **0.03x** | **160.1563** | **7.8125** | **2.02 MB** | **1.04x less** | 20 | | Chunked_Sequence | .NET 8.0 | 64 | 500 | 2.348 ms | 0.0308 ms | 0.0273 ms | 2.343 ms | 2.66x faster | 0.03x | 156.2500 | 7.8125 | 2.02 MB | 1.04x less | 21 | | Chunked_Sequence | .NET Framework 4.8 | 64 | 500 | 6.254 ms | 0.0465 ms | 0.0412 ms | 6.246 ms | baseline | | 343.7500 | 7.8125 | 2.09 MB | | 22 | | | | | | | | | | | | | | | | 23 | | **Chunked_Sequence** | **.NET 6.0** | **256** | **500** | **2.837 ms** | **0.0297 ms** | **0.0248 ms** | **2.835 ms** | **2.11x faster** | **0.02x** | **160.1563** | **7.8125** | **2.02 MB** | **1.04x less** | 24 | | Chunked_Sequence | .NET 8.0 | 256 | 500 | 2.380 ms | 0.0463 ms | 0.0514 ms | 2.376 ms | 2.52x faster | 0.05x | 156.2500 | 7.8125 | 2.02 MB | 1.04x less | 25 | | Chunked_Sequence | .NET Framework 4.8 | 256 | 500 | 5.989 ms | 0.0412 ms | 0.0344 ms | 5.983 ms | baseline | | 343.7500 | 7.8125 | 2.1 MB | | 26 | -------------------------------------------------------------------------------- /test/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestResponse-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | Unknown processor 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | Job-ERYFHQ : .NET 6.0.26 (6.0.2623.60508), X64 RyuJIT AVX2 8 | Job-GVMCZA : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 9 | Job-BVWEYE : .NET Framework 4.8.1 (4.8.9181.0), X64 RyuJIT VectorSize=256 10 | 11 | 12 | ``` 13 | | Method | Runtime | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | 14 | |----------- |------------------- |------ |------------:|----------:|------------:|------------:|-------------:|--------:|--------:|--------:|----------:|------------:| 15 | | **Sequential** | **.NET 6.0** | **16** | **628.5 μs** | **12.56 μs** | **26.76 μs** | **627.7 μs** | **1.92x faster** | **0.07x** | **0.9766** | **-** | **21.05 KB** | **5.65x less** | 16 | | Sequential | .NET 8.0 | 16 | 866.4 μs | 17.09 μs | 41.27 μs | 871.7 μs | 1.50x faster | 0.18x | 0.9766 | - | 20.79 KB | 5.72x less | 17 | | Sequential | .NET Framework 4.8 | 16 | 1,235.3 μs | 6.95 μs | 6.51 μs | 1,232.5 μs | baseline | | 18.5547 | 1.9531 | 118.99 KB | | 18 | | | | | | | | | | | | | | | 19 | | Parallel | .NET 6.0 | 16 | 1,406.4 μs | 221.29 μs | 652.48 μs | 1,394.3 μs | 4.46x faster | 3.74x | - | - | 19.12 KB | 6.11x less | 20 | | Parallel | .NET 8.0 | 16 | 474.7 μs | 13.03 μs | 37.82 μs | 476.4 μs | 8.85x faster | 2.53x | 0.9766 | - | 18.94 KB | 6.17x less | 21 | | Parallel | .NET Framework 4.8 | 16 | 4,230.2 μs | 399.77 μs | 1,178.73 μs | 3,956.0 μs | baseline | | 15.6250 | 7.8125 | 116.88 KB | | 22 | | | | | | | | | | | | | | | 23 | | **Sequential** | **.NET 6.0** | **64** | **3,687.4 μs** | **73.49 μs** | **188.37 μs** | **3,690.2 μs** | **1.62x faster** | **0.72x** | **3.9063** | **-** | **83.67 KB** | **5.95x less** | 24 | | Sequential | .NET 8.0 | 64 | 3,294.2 μs | 123.77 μs | 364.94 μs | 3,418.5 μs | 1.77x faster | 0.66x | - | - | 82.67 KB | 6.02x less | 25 | | Sequential | .NET Framework 4.8 | 64 | 5,889.9 μs | 805.71 μs | 2,375.65 μs | 5,411.4 μs | baseline | | 78.1250 | 7.8125 | 497.6 KB | | 26 | | | | | | | | | | | | | | | 27 | | Parallel | .NET 6.0 | 64 | 1,585.1 μs | 36.65 μs | 105.16 μs | 1,562.8 μs | 7.78x faster | 1.32x | 3.9063 | - | 74.31 KB | 5.78x less | 28 | | Parallel | .NET 8.0 | 64 | 1,908.2 μs | 285.32 μs | 827.75 μs | 1,638.1 μs | 7.63x faster | 3.28x | 3.9063 | - | 72.17 KB | 5.95x less | 29 | | Parallel | .NET Framework 4.8 | 64 | 12,267.3 μs | 593.38 μs | 1,740.28 μs | 12,084.2 μs | baseline | | 62.5000 | 15.6250 | 429.38 KB | | 30 | -------------------------------------------------------------------------------- /test/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.ReverseEndianness-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | Unknown processor 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | Job-ISCVXC : .NET 6.0.26 (6.0.2623.60508), X64 RyuJIT AVX2 8 | Job-DINVIM : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 9 | Job-OKZTLT : .NET Framework 4.8.1 (4.8.9181.0), X64 RyuJIT VectorSize=256 10 | 11 | 12 | ``` 13 | | Method | Runtime | Categories | Size | Mean | Error | StdDev | Median | Ratio | RatioSD | 14 | |------------------------ |------------------- |----------- |----- |----------:|---------:|----------:|----------:|-------------:|--------:| 15 | | ReverseEndiannessHelper | .NET 6.0 | Double | 64 | 36.78 ns | 0.848 ns | 2.501 ns | 37.53 ns | 6.24x faster | 0.69x | 16 | | ReverseEndiannessHelper | .NET 8.0 | Double | 64 | 33.94 ns | 0.698 ns | 1.737 ns | 34.65 ns | 6.69x faster | 0.52x | 17 | | ReverseEndiannessHelper | .NET Framework 4.8 | Double | 64 | 226.66 ns | 4.501 ns | 11.457 ns | 231.04 ns | baseline | | 18 | | | | | | | | | | | | 19 | | ReverseEndiannessHelper | .NET 6.0 | Int16 | 64 | 35.47 ns | 0.911 ns | 2.685 ns | 36.48 ns | 2.10x faster | 0.20x | 20 | | ReverseEndiannessHelper | .NET 8.0 | Int16 | 64 | 39.72 ns | 0.805 ns | 1.047 ns | 40.22 ns | 1.87x faster | 0.11x | 21 | | ReverseEndiannessHelper | .NET Framework 4.8 | Int16 | 64 | 74.31 ns | 1.507 ns | 3.640 ns | 75.52 ns | baseline | | 22 | | | | | | | | | | | | 23 | | ReverseEndiannessHelper | .NET 6.0 | Int32 | 64 | 28.68 ns | 0.594 ns | 1.646 ns | 29.17 ns | 2.78x faster | 0.25x | 24 | | ReverseEndiannessHelper | .NET 8.0 | Int32 | 64 | 27.09 ns | 0.562 ns | 1.481 ns | 27.70 ns | 2.94x faster | 0.21x | 25 | | ReverseEndiannessHelper | .NET Framework 4.8 | Int32 | 64 | 78.90 ns | 1.588 ns | 2.653 ns | 80.29 ns | baseline | | 26 | | | | | | | | | | | | 27 | | ReverseEndiannessHelper | .NET 6.0 | Int64 | 64 | 29.44 ns | 0.578 ns | 0.618 ns | 29.59 ns | 3.93x faster | 0.90x | 28 | | ReverseEndiannessHelper | .NET 8.0 | Int64 | 64 | 39.21 ns | 0.904 ns | 2.665 ns | 39.99 ns | 3.28x faster | 0.29x | 29 | | ReverseEndiannessHelper | .NET Framework 4.8 | Int64 | 64 | 128.85 ns | 4.954 ns | 14.606 ns | 134.95 ns | baseline | | 30 | | | | | | | | | | | | 31 | | ReverseEndiannessHelper | .NET 6.0 | Single | 64 | 37.45 ns | 0.761 ns | 1.503 ns | 38.10 ns | 4.59x faster | 0.32x | 32 | | ReverseEndiannessHelper | .NET 8.0 | Single | 64 | 34.50 ns | 0.685 ns | 0.914 ns | 34.75 ns | 4.97x faster | 0.32x | 33 | | ReverseEndiannessHelper | .NET Framework 4.8 | Single | 64 | 172.60 ns | 3.468 ns | 8.763 ns | 176.16 ns | baseline | | 34 | | | | | | | | | | | | 35 | | ReverseEndiannessHelper | .NET 6.0 | UInt16 | 64 | 31.14 ns | 0.494 ns | 0.462 ns | 31.27 ns | 1.96x faster | 0.24x | 36 | | ReverseEndiannessHelper | .NET 8.0 | UInt16 | 64 | 36.98 ns | 1.316 ns | 3.880 ns | 37.95 ns | 1.73x faster | 0.17x | 37 | | ReverseEndiannessHelper | .NET Framework 4.8 | UInt16 | 64 | 63.23 ns | 1.307 ns | 3.853 ns | 64.30 ns | baseline | | 38 | | | | | | | | | | | | 39 | | ReverseEndiannessHelper | .NET 6.0 | UInt32 | 64 | 28.75 ns | 0.668 ns | 1.970 ns | 29.48 ns | 2.83x faster | 0.38x | 40 | | ReverseEndiannessHelper | .NET 8.0 | UInt32 | 64 | 27.35 ns | 0.615 ns | 1.813 ns | 28.06 ns | 2.96x faster | 0.36x | 41 | | ReverseEndiannessHelper | .NET Framework 4.8 | UInt32 | 64 | 79.82 ns | 1.614 ns | 3.187 ns | 81.08 ns | baseline | | 42 | | | | | | | | | | | | 43 | | ReverseEndiannessHelper | .NET 6.0 | UInt64 | 64 | 29.49 ns | 0.597 ns | 0.929 ns | 29.78 ns | 2.41x faster | 0.09x | 44 | | ReverseEndiannessHelper | .NET 8.0 | UInt64 | 64 | 38.88 ns | 0.978 ns | 2.884 ns | 40.00 ns | 1.91x faster | 0.36x | 45 | | ReverseEndiannessHelper | .NET Framework 4.8 | UInt64 | 64 | 70.88 ns | 0.913 ns | 1.550 ns | 70.64 ns | baseline | | 46 | -------------------------------------------------------------------------------- /test/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.SmlSerialization-report-github.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3) 4 | Unknown processor 5 | .NET SDK 8.0.101 6 | [Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 7 | Job-ISCVXC : .NET 6.0.26 (6.0.2623.60508), X64 RyuJIT AVX2 8 | Job-DINVIM : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 9 | Job-OKZTLT : .NET Framework 4.8.1 (4.8.9181.0), X64 RyuJIT VectorSize=256 10 | 11 | 12 | ``` 13 | | Method | Runtime | ItemCount | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | 14 | |----------- |------------------- |---------- |-----------:|-----------:|-----------:|-----------:|-------------:|--------:|----------:|------------:| 15 | | **Serialize** | **.NET 6.0** | **0** | **4.273 μs** | **0.0847 μs** | **0.0750 μs** | **4.296 μs** | **2.08x faster** | **0.06x** | **9.78 KB** | **1.19x less** | 16 | | Serialize | .NET 8.0 | 0 | 3.261 μs | 0.0568 μs | 0.0504 μs | 3.253 μs | 2.73x faster | 0.06x | 9.78 KB | 1.19x less | 17 | | Serialize | .NET Framework 4.8 | 0 | 8.917 μs | 0.1684 μs | 0.1802 μs | 8.910 μs | baseline | | 11.62 KB | | 18 | | | | | | | | | | | | | 19 | | Deserialze | .NET 6.0 | 0 | 3.681 μs | 0.0454 μs | 0.0379 μs | 3.684 μs | 2.90x faster | 0.04x | 5.26 KB | 2.05x less | 20 | | Deserialze | .NET 8.0 | 0 | 2.879 μs | 0.0553 μs | 0.0517 μs | 2.878 μs | 3.70x faster | 0.05x | 5.26 KB | 2.05x less | 21 | | Deserialze | .NET Framework 4.8 | 0 | 10.670 μs | 0.0977 μs | 0.0866 μs | 10.655 μs | baseline | | 10.76 KB | | 22 | | | | | | | | | | | | | 23 | | **Serialize** | **.NET 6.0** | **64** | **38.140 μs** | **0.7336 μs** | **0.6503 μs** | **38.225 μs** | **3.47x faster** | **0.23x** | **24.87 KB** | **2.61x less** | 24 | | Serialize | .NET 8.0 | 64 | 30.346 μs | 0.5505 μs | 0.4880 μs | 30.377 μs | 4.36x faster | 0.24x | 30.87 KB | 2.10x less | 25 | | Serialize | .NET Framework 4.8 | 64 | 133.818 μs | 3.2974 μs | 9.7223 μs | 136.810 μs | baseline | | 64.91 KB | | 26 | | | | | | | | | | | | | 27 | | Deserialze | .NET 6.0 | 64 | 37.218 μs | 0.3944 μs | 0.3496 μs | 37.261 μs | 5.87x faster | 1.51x | 23.77 KB | 7.07x less | 28 | | Deserialze | .NET 8.0 | 64 | 42.174 μs | 2.7085 μs | 7.9860 μs | 46.032 μs | 6.17x faster | 1.57x | 23.77 KB | 7.07x less | 29 | | Deserialze | .NET Framework 4.8 | 64 | 249.978 μs | 8.9619 μs | 26.4245 μs | 259.593 μs | baseline | | 168.05 KB | | 30 | | | | | | | | | | | | | 31 | | **Serialize** | **.NET 6.0** | **128** | **124.015 μs** | **4.6209 μs** | **13.6248 μs** | **128.919 μs** | **2.00x faster** | **0.16x** | **40.71 KB** | **2.92x less** | 32 | | Serialize | .NET 8.0 | 128 | 94.919 μs | 1.8861 μs | 5.0017 μs | 97.008 μs | 2.59x faster | 0.29x | 52.71 KB | 2.25x less | 33 | | Serialize | .NET Framework 4.8 | 128 | 246.127 μs | 8.0884 μs | 23.8489 μs | 256.717 μs | baseline | | 118.74 KB | | 34 | | | | | | | | | | | | | 35 | | Deserialze | .NET 6.0 | 128 | 111.931 μs | 4.4010 μs | 12.9764 μs | 116.768 μs | 4.29x faster | 0.59x | 40.42 KB | 7.97x less | 36 | | Deserialze | .NET 8.0 | 128 | 83.434 μs | 2.1832 μs | 6.4372 μs | 85.326 μs | 5.71x faster | 0.52x | 40.42 KB | 7.97x less | 37 | | Deserialze | .NET Framework 4.8 | 128 | 476.006 μs | 17.5744 μs | 51.8184 μs | 498.305 μs | baseline | | 322.05 KB | | 38 | -------------------------------------------------------------------------------- /test/Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0;net48 5 | Exe 6 | 12.0 7 | 8 | 9 | 10 | AnyCPU 11 | pdbonly 12 | true 13 | true 14 | true 15 | Release 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/Benchmarks/ComplexItemEncodeDecode.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | using Secs4Net; 4 | using System.Buffers; 5 | using System.Text; 6 | 7 | namespace Benchmarks; 8 | 9 | [Config(typeof(BenchmarkConfig))] 10 | [MemoryDiagnoser(displayGenColumns: false)] 11 | //[NativeMemoryProfiler] 12 | public class ComplexItemEncodeDecode 13 | { 14 | private Item _item; 15 | private byte[] _encodedBytes; 16 | 17 | [Params(0, 64, 128)] 18 | public int ItemCount { get; set; } 19 | 20 | [GlobalSetup] 21 | public void Setup() 22 | { 23 | _item = 24 | L( 25 | L(), 26 | U1(MemoryOwner.Allocate(ItemCount)), 27 | U2(MemoryOwner.Allocate(ItemCount)), 28 | U4(MemoryOwner.Allocate(ItemCount)), 29 | F4(MemoryOwner.Allocate(ItemCount)), 30 | A(CreateString(ItemCount, Encoding.ASCII)), 31 | //J(CreateString(Math.Min(ItemCount, 512))), //JIS encoding cost more memory in coreclr 32 | F8(MemoryOwner.Allocate(ItemCount)), 33 | L( 34 | I1(MemoryOwner.Allocate(ItemCount)), 35 | I2(MemoryOwner.Allocate(ItemCount)), 36 | I4(MemoryOwner.Allocate(ItemCount)), 37 | F4(MemoryOwner.Allocate(ItemCount)), 38 | L( 39 | I1(MemoryOwner.Allocate(ItemCount)), 40 | I2(MemoryOwner.Allocate(ItemCount)), 41 | I4(MemoryOwner.Allocate(ItemCount)), 42 | F4(MemoryOwner.Allocate(ItemCount)), 43 | Boolean(MemoryOwner.Allocate(ItemCount)), 44 | B(MemoryOwner.Allocate(ItemCount)), 45 | L( 46 | A(CreateString(ItemCount, Encoding.ASCII)), 47 | //J(CreateString(Math.Min(ItemCount, 512))), 48 | Boolean(MemoryOwner.Allocate(ItemCount)), 49 | B(MemoryOwner.Allocate(ItemCount))), 50 | F8(MemoryOwner.Allocate(ItemCount))), 51 | Boolean(MemoryOwner.Allocate(ItemCount)), 52 | B(MemoryOwner.Allocate(ItemCount)), 53 | L( 54 | A(CreateString(ItemCount, Encoding.ASCII)), 55 | //J(CreateString(Math.Min(ItemCount, 512))), 56 | Boolean(MemoryOwner.Allocate(ItemCount)), 57 | B(MemoryOwner.Allocate(ItemCount))), 58 | F8(MemoryOwner.Allocate(ItemCount))), 59 | U1(MemoryOwner.Allocate(ItemCount)), 60 | U2(MemoryOwner.Allocate(ItemCount)), 61 | U4(MemoryOwner.Allocate(ItemCount)), 62 | F4(MemoryOwner.Allocate(ItemCount))); 63 | 64 | using var buffer = new ArrayPoolBufferWriter(); 65 | _item.EncodeTo(buffer); 66 | _encodedBytes = buffer.WrittenMemory.ToArray(); 67 | 68 | static string CreateString(int count, Encoding encoding) 69 | { 70 | if (count == 0) 71 | { 72 | return string.Empty; 73 | } 74 | using var spanOwner = SpanOwner.Allocate(count); 75 | return encoding.GetString(spanOwner.Span); 76 | } 77 | } 78 | 79 | [GlobalCleanup] 80 | public void Cleanup() 81 | { 82 | _item.Dispose(); 83 | } 84 | 85 | [Benchmark] 86 | public int Encode() 87 | { 88 | using var buffer = new ArrayPoolBufferWriter(_encodedBytes.Length); 89 | _item.EncodeTo(buffer); 90 | return buffer.WrittenCount; 91 | } 92 | 93 | [Benchmark] 94 | public long Decode() 95 | { 96 | var source = new ReadOnlySequence(_encodedBytes); 97 | using var item = Item.DecodeFromFullBuffer(ref source); 98 | return source.Length; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/Benchmarks/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/Benchmarks/ItemEncodeDecode.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | using Secs4Net; 4 | using System; 5 | using System.Buffers; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace Benchmarks; 10 | 11 | [Config(typeof(BenchmarkConfig))] 12 | [MemoryDiagnoser(displayGenColumns: false)] 13 | //[NativeMemoryProfiler] 14 | public class ItemEncodeDecode 15 | { 16 | private ArrayPoolBufferWriter _encodedBuffer; 17 | private int _estimateEncodedByteLength; 18 | private Item _item; 19 | 20 | [ParamsAllValues] 21 | public SecsFormat Format { get; set; } 22 | 23 | [Params(0, 1024)] 24 | public int Size { get; set; } 25 | 26 | [GlobalSetup] 27 | public void GlobalSetup() 28 | { 29 | _item = Format switch 30 | { 31 | SecsFormat.List => L(Enumerable.Repeat(L(), Size)), 32 | SecsFormat.Binary => B(CreateArray(Size)), 33 | SecsFormat.Boolean => Boolean(CreateArray(Size)), 34 | SecsFormat.ASCII => A(CreateString(Size, Encoding.ASCII)), 35 | SecsFormat.JIS8 => J(CreateString(Size, Item.JIS8Encoding)), 36 | SecsFormat.I8 => I8(CreateArray(Size)), 37 | SecsFormat.I1 => I1(CreateArray(Size)), 38 | SecsFormat.I2 => I2(CreateArray(Size)), 39 | SecsFormat.I4 => I4(CreateArray(Size)), 40 | SecsFormat.F8 => F8(CreateArray(Size)), 41 | SecsFormat.F4 => F4(CreateArray(Size)), 42 | SecsFormat.U8 => U8(CreateArray(Size)), 43 | SecsFormat.U1 => U1(CreateArray(Size)), 44 | SecsFormat.U2 => U2(CreateArray(Size)), 45 | SecsFormat.U4 => U4(CreateArray(Size)), 46 | _ => throw new ArgumentOutOfRangeException(nameof(Format), Format, "invalid format"), 47 | }; 48 | 49 | _encodedBuffer = new ArrayPoolBufferWriter(); 50 | _item.EncodeTo(_encodedBuffer); 51 | 52 | _estimateEncodedByteLength = _encodedBuffer.WrittenCount; 53 | 54 | static IMemoryOwner CreateArray(int count) where T : unmanaged => MemoryOwner.Allocate(count); 55 | 56 | static string CreateString(int count, Encoding encoding) 57 | { 58 | if (count == 0) 59 | { 60 | return string.Empty; 61 | } 62 | using var spanOwner = SpanOwner.Allocate(count); 63 | return encoding.GetString(spanOwner.Span); 64 | } 65 | } 66 | 67 | [GlobalCleanup] 68 | public void GlobalCleanup() 69 | { 70 | _item.Dispose(); 71 | _encodedBuffer.Dispose(); 72 | } 73 | 74 | [Benchmark] 75 | public int EncodeTo() 76 | { 77 | using var buffer = new ArrayPoolBufferWriter(_estimateEncodedByteLength); 78 | _item.EncodeTo(buffer); 79 | return buffer.WrittenCount; 80 | } 81 | 82 | [Benchmark] 83 | public int DecodeFromFullBuffer() 84 | { 85 | var seq = new ReadOnlySequence(_encodedBuffer.WrittenMemory); 86 | using var item = Item.DecodeFromFullBuffer(ref seq); 87 | return item.Count; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/Benchmarks/JsonSerialization.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | using Secs4Net; 4 | using Secs4Net.Json; 5 | using System.Text; 6 | using System.Text.Json; 7 | 8 | namespace Benchmarks; 9 | 10 | [Config(typeof(BenchmarkConfig))] 11 | [MemoryDiagnoser(displayGenColumns: false)] 12 | //[NativeMemoryProfiler] 13 | public class JsonSerialization 14 | { 15 | private static readonly JsonSerializerOptions JsonSerializerOptions = new() 16 | { 17 | Converters = { new ItemJsonConverter() }, 18 | }; 19 | 20 | private SecsMessage _message; 21 | private string _json; 22 | 23 | [Params(0, 64, 128)] 24 | public int ItemCount { get; set; } 25 | 26 | [GlobalSetup] 27 | public void Setup() 28 | { 29 | _message = new(s: 1, f: 2, replyExpected: false) 30 | { 31 | Name = "Test", 32 | SecsItem = L( 33 | L(), 34 | U1(MemoryOwner.Allocate(ItemCount)), 35 | U2(MemoryOwner.Allocate(ItemCount)), 36 | U4(MemoryOwner.Allocate(ItemCount)), 37 | F4(MemoryOwner.Allocate(ItemCount)), 38 | A(CreateString(ItemCount, Encoding.ASCII)), 39 | J(CreateString(ItemCount, Item.JIS8Encoding)), //JIS encoding cost more memory in coreclr 40 | F8(MemoryOwner.Allocate(ItemCount)), 41 | L( 42 | I1(MemoryOwner.Allocate(ItemCount)), 43 | I2(MemoryOwner.Allocate(ItemCount)), 44 | I4(MemoryOwner.Allocate(ItemCount)), 45 | F4(MemoryOwner.Allocate(ItemCount)), 46 | L( 47 | I1(MemoryOwner.Allocate(ItemCount)), 48 | I2(MemoryOwner.Allocate(ItemCount)), 49 | I4(MemoryOwner.Allocate(ItemCount)), 50 | F4(MemoryOwner.Allocate(ItemCount)), 51 | Boolean(MemoryOwner.Allocate(ItemCount)), 52 | B(MemoryOwner.Allocate(ItemCount)), 53 | L( 54 | A(CreateString(ItemCount, Encoding.ASCII)), 55 | J(CreateString(ItemCount, Item.JIS8Encoding)), 56 | Boolean(MemoryOwner.Allocate(ItemCount)), 57 | B(MemoryOwner.Allocate(ItemCount))), 58 | F8(MemoryOwner.Allocate(ItemCount))), 59 | Boolean(MemoryOwner.Allocate(ItemCount)), 60 | B(MemoryOwner.Allocate(ItemCount)), 61 | L( 62 | A(CreateString(ItemCount, Encoding.ASCII)), 63 | J(CreateString(ItemCount, Item.JIS8Encoding)), 64 | Boolean(MemoryOwner.Allocate(ItemCount)), 65 | B(MemoryOwner.Allocate(ItemCount))), 66 | F8(MemoryOwner.Allocate(ItemCount))), 67 | U1(MemoryOwner.Allocate(ItemCount)), 68 | U2(MemoryOwner.Allocate(ItemCount)), 69 | U4(MemoryOwner.Allocate(ItemCount)), 70 | F4(MemoryOwner.Allocate(ItemCount))), 71 | }; 72 | 73 | _json = JsonSerializer.Serialize(_message, JsonSerializerOptions); 74 | 75 | static string CreateString(int count, Encoding encoding) 76 | { 77 | if (count == 0) 78 | { 79 | return string.Empty; 80 | } 81 | using var spanOwner = SpanOwner.Allocate(count); 82 | return encoding.GetString(spanOwner.Span); 83 | } 84 | } 85 | 86 | [GlobalCleanup] 87 | public void Cleanup() 88 | { 89 | _message.Dispose(); 90 | } 91 | 92 | [Benchmark] 93 | public string Serialize() 94 | => JsonSerializer.Serialize(_message, JsonSerializerOptions); 95 | 96 | [Benchmark] 97 | public SecsMessage Deserialze() 98 | => JsonSerializer.Deserialize(_json, JsonSerializerOptions); 99 | } 100 | -------------------------------------------------------------------------------- /test/Benchmarks/PipeDecoding.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | using Secs4Net; 4 | using System; 5 | using System.IO.Pipelines; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Benchmarks; 12 | 13 | [Config(typeof(BenchmarkConfig))] 14 | [MemoryDiagnoser] 15 | public class PipeDecoding 16 | { 17 | private byte[][] _encodedBytes; 18 | 19 | [Params(16, 64, 256)] 20 | public int InputChunkSize { get; set; } 21 | 22 | [Params(500)] 23 | public int MessageCount { get; set; } 24 | 25 | [GlobalSetup()] 26 | public void Setup() 27 | { 28 | const int ItemCount = 16; 29 | using var message = new SecsMessage(s: 1, f: 2, replyExpected: false) 30 | { 31 | SecsItem = L( 32 | L(), 33 | U1(MemoryOwner.Allocate(ItemCount)), 34 | U2(MemoryOwner.Allocate(ItemCount)), 35 | U4(MemoryOwner.Allocate(ItemCount)), 36 | F4(MemoryOwner.Allocate(ItemCount)), 37 | A(CreateString(ItemCount, Encoding.ASCII)), 38 | J(CreateString(ItemCount, Item.JIS8Encoding)), 39 | F8(MemoryOwner.Allocate(ItemCount)), 40 | L( 41 | I1(MemoryOwner.Allocate(ItemCount)), 42 | I2(MemoryOwner.Allocate(ItemCount)), 43 | I4(MemoryOwner.Allocate(ItemCount)), 44 | F4(MemoryOwner.Allocate(ItemCount)), 45 | L( 46 | I1(MemoryOwner.Allocate(ItemCount)), 47 | I2(MemoryOwner.Allocate(ItemCount)), 48 | I4(MemoryOwner.Allocate(ItemCount)), 49 | F4(MemoryOwner.Allocate(ItemCount)), 50 | Boolean(MemoryOwner.Allocate(ItemCount)), 51 | B(MemoryOwner.Allocate(ItemCount)), 52 | L( 53 | A(CreateString(ItemCount, Encoding.ASCII)), 54 | J(CreateString(ItemCount, Item.JIS8Encoding)), 55 | Boolean(MemoryOwner.Allocate(ItemCount)), 56 | B(MemoryOwner.Allocate(ItemCount))), 57 | F8(MemoryOwner.Allocate(ItemCount))), 58 | Boolean(MemoryOwner.Allocate(ItemCount)), 59 | B(MemoryOwner.Allocate(ItemCount)), 60 | L( 61 | A(CreateString(ItemCount, Encoding.ASCII)), 62 | J(CreateString(ItemCount, Item.JIS8Encoding)), 63 | Boolean(MemoryOwner.Allocate(ItemCount)), 64 | B(MemoryOwner.Allocate(ItemCount))), 65 | F8(MemoryOwner.Allocate(ItemCount))), 66 | U1(MemoryOwner.Allocate(ItemCount)), 67 | U2(MemoryOwner.Allocate(ItemCount)), 68 | U4(MemoryOwner.Allocate(ItemCount)), 69 | F4(MemoryOwner.Allocate(ItemCount))), 70 | }; 71 | 72 | using var buffer = new ArrayPoolBufferWriter(); 73 | 74 | for (var i = 0; i < MessageCount; i++) 75 | { 76 | SecsGem.EncodeMessage(message, 1000 + i, deviceId: 0, buffer); 77 | } 78 | 79 | _encodedBytes = buffer.WrittenSpan.ToArray().Chunk(InputChunkSize).ToArray(); 80 | 81 | static string CreateString(int count, Encoding encoding) 82 | { 83 | if (count == 0) 84 | { 85 | return string.Empty; 86 | } 87 | using var spanOwner = SpanOwner.Allocate(count); 88 | return encoding.GetString(spanOwner.Span); 89 | } 90 | } 91 | 92 | [Benchmark] 93 | public async Task Chunked_Sequence() 94 | { 95 | var pipe = new Pipe(); 96 | var decoder = new PipeDecoder(pipe.Reader, pipe.Writer); 97 | 98 | using var cts = new CancellationTokenSource(); 99 | 100 | var decode = Task.Run(async () => 101 | { 102 | try 103 | { 104 | await decoder.StartAsync(cts.Token).ConfigureAwait(false); 105 | } 106 | catch (OperationCanceledException) { } 107 | }); 108 | 109 | var input = Task.Run(async () => 110 | { 111 | foreach (var chunk in _encodedBytes) 112 | { 113 | await decoder.Input.WriteAsync(chunk).ConfigureAwait(false); 114 | } 115 | }); 116 | 117 | var output = Task.Run(async () => 118 | { 119 | var count = 1; 120 | await foreach (var (_, rootItem) in decoder.GetDataMessages(default).ConfigureAwait(false)) 121 | { 122 | rootItem.Dispose(); 123 | if (count++ == MessageCount) 124 | { 125 | break; 126 | } 127 | } 128 | cts.Cancel(); 129 | }); 130 | 131 | await Task.WhenAll(input, output).ConfigureAwait(false); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /test/Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using Benchmarks; 3 | 4 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly) 5 | .Run(args, new BenchmarkConfig()); 6 | -------------------------------------------------------------------------------- /test/Benchmarks/RequestResponse.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using Microsoft.Extensions.Options; 3 | using Secs4Net; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Benchmarks; 9 | 10 | [Config(typeof(BenchmarkConfig))] 11 | //[EtwProfiler] 12 | [MemoryDiagnoser] 13 | public class RequestResponse 14 | { 15 | private static readonly SecsMessage ping = new(s: 1, f: 13) 16 | { 17 | SecsItem = A("Ping"), 18 | }; 19 | 20 | private static readonly SecsMessage pong = new(s: 1, f: 14, replyExpected: false) 21 | { 22 | SecsItem = A("Pong"), 23 | }; 24 | 25 | private CancellationTokenSource _cts; 26 | private SecsGem _secsGem1; 27 | private SecsGem _secsGem2; 28 | private HsmsConnection _connection1; 29 | private HsmsConnection _connection2; 30 | 31 | [Params(16, 64)] 32 | public int Count { get; set; } 33 | 34 | [GlobalSetup] 35 | public void Setup() 36 | { 37 | var logger = new Logger(); 38 | 39 | _connection1 = new HsmsConnection(Options.Create(new SecsGemOptions 40 | { 41 | IsActive = true, 42 | DeviceId = 0, 43 | T3 = 60000, 44 | }), logger); 45 | 46 | _connection2 = new HsmsConnection(Options.Create(new SecsGemOptions 47 | { 48 | IsActive = false, 49 | DeviceId = 0, 50 | T3 = 60000, 51 | }), logger); 52 | 53 | var options = Options.Create(new SecsGemOptions 54 | { 55 | DeviceId = 0, 56 | }); 57 | _secsGem1 = new SecsGem(options, _connection1, logger); 58 | _secsGem2 = new SecsGem(options, _connection2, logger); 59 | 60 | _cts = new CancellationTokenSource(); 61 | _connection1.Start(_cts.Token); 62 | _connection2.Start(_cts.Token); 63 | 64 | SpinWait.SpinUntil(() => _connection2.State == ConnectionState.Selected); 65 | 66 | Task.Run(async () => 67 | { 68 | await foreach (var a in _secsGem2.GetPrimaryMessageAsync(_cts.Token)) 69 | { 70 | using var primaryMessage = a.PrimaryMessage; 71 | await a.TryReplyAsync(pong, _cts.Token); 72 | } 73 | }); 74 | } 75 | 76 | [GlobalCleanup] 77 | public async Task Cleanup() 78 | { 79 | _cts?.Cancel(); 80 | _cts?.Dispose(); 81 | _secsGem1?.Dispose(); 82 | _secsGem2?.Dispose(); 83 | if (_connection1 is not null) 84 | { 85 | await _connection1.DisposeAsync(); 86 | } 87 | 88 | if (_connection2 is not null) 89 | { 90 | await _connection2.DisposeAsync(); 91 | } 92 | } 93 | 94 | [Benchmark(Description = "Sequential")] 95 | public async Task SequentialSendAsync() 96 | { 97 | for (int i = 0; i < Count; i++) 98 | { 99 | using var pong = await _secsGem1.SendAsync(ping); 100 | } 101 | return Count; 102 | } 103 | 104 | [Benchmark(Description = "Parallel")] 105 | public async Task ParallelSendAsync() 106 | { 107 | var tasks = new Task[Count]; 108 | for (int i = 0; i < Count; i++) 109 | { 110 | tasks[i] = _secsGem1.SendAsync(ping); 111 | } 112 | var replies = await Task.WhenAll(tasks).ConfigureAwait(false); 113 | 114 | Array.ForEach(replies, a => a.Dispose()); 115 | return replies.Length; 116 | } 117 | 118 | private sealed class Logger : ISecsGemLogger 119 | { 120 | public void Debug(string msg) { } 121 | public void Error(string msg) { } 122 | public void Error(string msg, Exception ex) { } 123 | public void Error(string msg, SecsMessage message, Exception ex) { } 124 | public void Info(string msg) { } 125 | public void MessageIn(SecsMessage msg, int id) { } 126 | public void MessageOut(SecsMessage msg, int id) { } 127 | public void Warning(string msg) { } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/Benchmarks/SmlSerialization.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CommunityToolkit.HighPerformance.Buffers; 3 | using Secs4Net; 4 | using Secs4Net.Sml; 5 | using System.IO; 6 | using System.Text; 7 | 8 | namespace Benchmarks; 9 | 10 | [Config(typeof(BenchmarkConfig))] 11 | [MemoryDiagnoser(displayGenColumns: false)] 12 | //[NativeMemoryProfiler] 13 | public class SmlSerialization 14 | { 15 | private SecsMessage _message; 16 | private string _sml; 17 | 18 | [Params(0, 64, 128)] 19 | public int ItemCount { get; set; } 20 | 21 | [GlobalSetup] 22 | public void Setup() 23 | { 24 | _message = new(s: 1, f: 2, replyExpected: false) 25 | { 26 | Name = "Test", 27 | SecsItem = L( 28 | L(), 29 | U1(MemoryOwner.Allocate(ItemCount)), 30 | U2(MemoryOwner.Allocate(ItemCount)), 31 | U4(MemoryOwner.Allocate(ItemCount)), 32 | F4(MemoryOwner.Allocate(ItemCount)), 33 | A(CreateString(ItemCount, Encoding.ASCII)), 34 | J(CreateString(ItemCount, Item.JIS8Encoding)), //JIS encoding cost more memory in coreclr 35 | F8(MemoryOwner.Allocate(ItemCount)), 36 | L( 37 | I1(MemoryOwner.Allocate(ItemCount)), 38 | I2(MemoryOwner.Allocate(ItemCount)), 39 | I4(MemoryOwner.Allocate(ItemCount)), 40 | F4(MemoryOwner.Allocate(ItemCount)), 41 | L( 42 | I1(MemoryOwner.Allocate(ItemCount)), 43 | I2(MemoryOwner.Allocate(ItemCount)), 44 | I4(MemoryOwner.Allocate(ItemCount)), 45 | F4(MemoryOwner.Allocate(ItemCount)), 46 | Boolean(MemoryOwner.Allocate(ItemCount)), 47 | B(MemoryOwner.Allocate(ItemCount)), 48 | L( 49 | A(CreateString(ItemCount, Encoding.ASCII)), 50 | J(CreateString(ItemCount, Item.JIS8Encoding)), 51 | Boolean(MemoryOwner.Allocate(ItemCount)), 52 | B(MemoryOwner.Allocate(ItemCount))), 53 | F8(MemoryOwner.Allocate(ItemCount))), 54 | Boolean(MemoryOwner.Allocate(ItemCount)), 55 | B(MemoryOwner.Allocate(ItemCount)), 56 | L( 57 | A(CreateString(ItemCount, Encoding.ASCII)), 58 | J(CreateString(ItemCount, Item.JIS8Encoding)), 59 | Boolean(MemoryOwner.Allocate(ItemCount)), 60 | B(MemoryOwner.Allocate(ItemCount))), 61 | F8(MemoryOwner.Allocate(ItemCount))), 62 | U1(MemoryOwner.Allocate(ItemCount)), 63 | U2(MemoryOwner.Allocate(ItemCount)), 64 | U4(MemoryOwner.Allocate(ItemCount)), 65 | F4(MemoryOwner.Allocate(ItemCount))), 66 | }; 67 | 68 | _sml = _message.ToSml(); 69 | 70 | static string CreateString(int count, Encoding encoding) 71 | { 72 | if (count == 0) 73 | { 74 | return string.Empty; 75 | } 76 | using var spanOwner = SpanOwner.Allocate(count); 77 | return encoding.GetString(spanOwner.Span); 78 | } 79 | } 80 | 81 | [GlobalCleanup] 82 | public void Cleanup() 83 | { 84 | _message.Dispose(); 85 | } 86 | 87 | [Benchmark] 88 | public string Serialize() 89 | { 90 | using var sw = new StringWriter(); 91 | _message.WriteSmlTo(sw); 92 | sw.Flush(); 93 | return _sml; 94 | } 95 | 96 | [Benchmark] 97 | public SecsMessage Deserialze() 98 | => _sml.ToSecsMessage(); 99 | } 100 | -------------------------------------------------------------------------------- /test/Secs4Net.Json.UnitTests/Json.UnitTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Secs4Net.Json.UnitTests; 9 | 10 | public class JsonUnitTest 11 | { 12 | private readonly SecsMessage EmptyMessage = new(s: 1, f: 2, replyExpected: false); 13 | private readonly SecsMessage message = new(s: 1, f: 2, replyExpected: false) 14 | { 15 | Name = "Test", 16 | SecsItem = 17 | L( 18 | L(), 19 | U1(122, 34), 20 | U2(34531, 23123), 21 | U4(2123513, 52451141), 22 | F4(23123.21323f, 2324.221f), 23 | A("A string"), 24 | J("sdsad"), 25 | F8(231.00002321d, 0.2913212312d), 26 | L( 27 | U1(122, 34), 28 | U2(34531, 23123), 29 | U4(2123513, 52451141), 30 | F4(23123.21323f, 2324.221f), 31 | Boolean(true, false, false, true), 32 | B(0x1C, 0x01, 0xFF), 33 | L( 34 | A("A string"), 35 | J("sdsad"), 36 | Boolean(true, false, false, true), 37 | B(0x1C, 0x01, 0xFF)), 38 | F8(231.00002321d, 0.2913212312d))) 39 | }; 40 | 41 | [Fact] 42 | public void SecsMessage_Can_Serialize_And_Deserialize_With_JsonConverter() 43 | { 44 | var options = new JsonSerializerOptions 45 | { 46 | Converters = { 47 | new ItemJsonConverter() 48 | } 49 | }; 50 | 51 | var sml = JsonSerializer.Serialize(message, options); 52 | var deserialized = JsonSerializer.Deserialize(sml, options); 53 | 54 | deserialized.Should().NotBeNull().And.BeEquivalentTo(message); 55 | } 56 | 57 | [Fact] 58 | public void Empty_SecsMessage_Can_Serialize_And_Deserialize_With_JsonConverter() 59 | { 60 | var options = new JsonSerializerOptions 61 | { 62 | Converters = { 63 | new ItemJsonConverter() 64 | } 65 | }; 66 | 67 | var sml = JsonSerializer.Serialize(EmptyMessage, options); 68 | var deserialized = JsonSerializer.Deserialize(sml, options); 69 | 70 | deserialized.Should().NotBeNull().And.BeEquivalentTo(EmptyMessage); 71 | deserialized!.SecsItem.Should().BeNull(); 72 | } 73 | 74 | [Fact] 75 | public async Task Multiple_SecsMessage_Can_Serialize_And_Deserialize_From_Stream() 76 | { 77 | var options = new JsonSerializerOptions 78 | { 79 | Converters = { 80 | new ItemJsonConverter() 81 | } 82 | }; 83 | 84 | var messages = Enumerable.Repeat(message, 5).ToList(); 85 | 86 | var memoryStream = new MemoryStream(); 87 | await JsonSerializer.SerializeAsync(memoryStream, messages, options); 88 | await memoryStream.FlushAsync(); 89 | memoryStream.Position = 0; 90 | 91 | var i = 0; 92 | await foreach (var m in JsonSerializer.DeserializeAsyncEnumerable(memoryStream, options)) 93 | { 94 | m.Should().BeEquivalentTo(messages[i]); 95 | i++; 96 | } 97 | 98 | i.Should().Be(messages.Count); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/Secs4Net.Json.UnitTests/Secs4Net.Json.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0;net472 5 | false 6 | False 7 | False 8 | cobertura 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/Secs4Net.Sml.UnitTests/Secs4Net.Sml.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0;net472 5 | false 6 | False 7 | False 8 | cobertura 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/Secs4Net.Sml.UnitTests/Sml.UnitTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Secs4Net.Sml.UnitTests; 8 | 9 | public class SmlUnitTest 10 | { 11 | private readonly SecsMessage message = new(s: 1, f: 2, replyExpected: false) 12 | { 13 | Name = "Test", 14 | SecsItem = 15 | L( 16 | L(), 17 | U1(122, 34, 0), 18 | U2(34531, 23123, 24), 19 | U4(2123513, 52451141, 1), 20 | F4(23123.21323f, 2324.221f, -20.131f), 21 | A("A string"), 22 | Boolean(true, false, false, true), 23 | B(0x1C, 0x01, 0xFF), 24 | L( 25 | A("A string"), 26 | J("sdsad"), 27 | Boolean(true, false, false, true), 28 | B(0x1C, 0x01, 0xFF)), 29 | F8(231.00002321d, 0.2913212312d), 30 | J("sdsad"), 31 | F8(231.00002321d, 0.2913212312d, -124.42002d), 32 | L( 33 | I1(122, 34, -13), 34 | I2(4531, -23123, 12), 35 | I4(2123513, 52451141, -11), 36 | F4(23123.21323f, 2324.221f), 37 | Boolean(true, false, false, true), 38 | B(0x1C, 0x01, 0xFF), 39 | L( 40 | A("A string"), 41 | J("sdsad"), 42 | Boolean(true, false, false, true), 43 | B(0x1C, 0x01, 0xFF)), 44 | F8(231.00002321d, 0.2913212312d))) 45 | }; 46 | 47 | [Fact] 48 | public void SecsMessage_Can_Serialize_And_Deserialize() 49 | { 50 | var sml = message.ToSml(); 51 | var deserialized = sml.ToSecsMessage(); 52 | 53 | deserialized.Should().BeEquivalentTo(message); 54 | } 55 | 56 | [Fact] 57 | public async Task Multiple_SecsMessage_Can_Serialize_And_Deserialize_From_Stream() 58 | { 59 | var messages = Enumerable.Repeat(message, 5).ToList(); 60 | 61 | var memoryStream = new MemoryStream(); 62 | using var writer = new StreamWriter(memoryStream); 63 | foreach (var m in messages) 64 | { 65 | m.WriteSmlTo(writer); 66 | } 67 | writer.Flush(); 68 | 69 | memoryStream.Position = 0; 70 | 71 | 72 | using var reader = new StreamReader(memoryStream); 73 | var i = 0; 74 | await foreach (var m in reader.ToSecsMessages()) 75 | { 76 | m.Should().BeEquivalentTo(messages[i]); 77 | i++; 78 | } 79 | 80 | i.Should().Be(messages.Count); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/Secs4Net.UnitTests/ChunkedMemory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Secs4Net; 6 | 7 | [EditorBrowsable(EditorBrowsableState.Never)] 8 | public struct ChunkedMemory 9 | { 10 | private readonly Memory _source; 11 | private readonly int _chunkSize; 12 | private int _start; 13 | private int _end; 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | internal ChunkedMemory(Memory span, int size) 17 | { 18 | _source = span; 19 | _chunkSize = size; 20 | _start = 0; 21 | _end = 0; 22 | } 23 | 24 | /// 25 | /// Implements the duck-typed method. 26 | /// 27 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 28 | public readonly ChunkedMemory GetEnumerator() 29 | => this; 30 | 31 | /// 32 | /// Gets the duck-typed property. 33 | /// 34 | public readonly Memory Current 35 | { 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | get => _source[_start.._end]; 38 | } 39 | 40 | /// 41 | /// Implements the duck-typed method. 42 | /// 43 | /// whether a new element is available, otherwise 44 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 45 | public bool MoveNext() 46 | { 47 | int length = _source.Length; 48 | if (_end < length) 49 | { 50 | _start = _end; 51 | _end = _start + _chunkSize; 52 | if (_end > length) 53 | { 54 | _end = length; 55 | } 56 | return true; 57 | } 58 | return false; 59 | } 60 | } 61 | 62 | [EditorBrowsable(EditorBrowsableState.Never)] 63 | public struct ChunkedReadOnlyMemory 64 | { 65 | private readonly ReadOnlyMemory _source; 66 | private readonly int _chunkSize; 67 | private int _start; 68 | private int _end; 69 | 70 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 71 | internal ChunkedReadOnlyMemory(ReadOnlyMemory span, int size) 72 | { 73 | _source = span; 74 | _chunkSize = size; 75 | _start = 0; 76 | _end = 0; 77 | } 78 | 79 | /// 80 | /// Implements the duck-typed method. 81 | /// 82 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 83 | public readonly ChunkedReadOnlyMemory GetEnumerator() 84 | => this; 85 | 86 | /// 87 | /// Gets the duck-typed property. 88 | /// 89 | public readonly ReadOnlyMemory Current 90 | { 91 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 92 | get => _source[_start.._end]; 93 | } 94 | 95 | /// 96 | /// Implements the duck-typed method. 97 | /// 98 | /// whether a new element is available, otherwise 99 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 100 | public bool MoveNext() 101 | { 102 | int length = _source.Length; 103 | if (_end < length) 104 | { 105 | _start = _end; 106 | _end = _start + _chunkSize; 107 | if (_end > length) 108 | { 109 | _end = length; 110 | } 111 | return true; 112 | } 113 | return false; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test/Secs4Net.UnitTests/MessageHeader.UnitTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Secs4Net.UnitTests; 4 | 5 | public class MessageHeaderUnitTests 6 | { 7 | [Fact] 8 | public void Header_should_ignore_direction_in_device_id() 9 | { 10 | /* 11 | * Equipment to Host 12 | * Device ID 1 13 | * Reply expected 14 | * S6F11 15 | * Select request 16 | * ID: 1 17 | */ 18 | var headerBytes = new byte[] { 128, 1, 134, 11, 0, 1, 0, 0, 0, 1 }; 19 | MessageHeader.Decode(headerBytes, out MessageHeader header); 20 | 21 | Assert.Multiple( 22 | () => Assert.Equal(1, header.DeviceId), 23 | () => Assert.True(header.ReplyExpected), 24 | () => Assert.Equal(6, header.S), 25 | () => Assert.Equal(11, header.F), 26 | () => Assert.Equal(MessageType.SelectRequest, header.MessageType), 27 | () => Assert.Equal(1, header.Id)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/Secs4Net.UnitTests/PipeConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Pipelines; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Secs4Net; 9 | 10 | public sealed class PipeConnection : ISecsConnection 11 | { 12 | private readonly SemaphoreSlim _sendLock = new(initialCount: 1); 13 | private readonly PipeDecoder _decoder; 14 | 15 | public PipeConnection(PipeReader decoderReader, PipeWriter decoderInput) 16 | { 17 | _decoder = new PipeDecoder(decoderReader, decoderInput); 18 | } 19 | 20 | public void Start(CancellationToken cancellation) 21 | => Task.Run(() => _decoder.StartAsync(cancellation)); 22 | 23 | Task ISecsConnection.SendAsync(ReadOnlyMemory source, CancellationToken cancellation) 24 | => SendAsync(source, cancellation); 25 | 26 | private async Task SendAsync(ReadOnlyMemory source, CancellationToken cancellation) 27 | { 28 | await _sendLock.WaitAsync(cancellation).ConfigureAwait(false); 29 | try 30 | { 31 | _ = await _decoder.Input.WriteAsync(source, cancellation).ConfigureAwait(false); 32 | } 33 | finally 34 | { 35 | _sendLock.Release(); 36 | } 37 | } 38 | 39 | IAsyncEnumerable<(MessageHeader header, Item? rootItem)> ISecsConnection.GetDataMessages(CancellationToken cancellation) 40 | => _decoder.GetDataMessages(cancellation); 41 | 42 | bool ISecsConnection.LinkTestEnabled { get; set; } 43 | public ConnectionState State { get; } = ConnectionState.Selected; 44 | bool ISecsConnection.IsActive { get; } 45 | IPAddress ISecsConnection.IpAddress { get; } = IPAddress.Any; 46 | int ISecsConnection.Port { get; } 47 | string ISecsConnection.DeviceIpAddress { get; } = string.Empty; 48 | void ISecsConnection.Reconnect() { } 49 | event EventHandler? ISecsConnection.ConnectionChanged { add { } remove { } } 50 | } 51 | -------------------------------------------------------------------------------- /test/Secs4Net.UnitTests/PipeDecoder.UnitTests.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.HighPerformance.Buffers; 2 | using FluentAssertions; 3 | using System; 4 | using System.IO.Pipelines; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Secs4Net.UnitTests; 10 | 11 | public class PipeDecoderUnitTests 12 | { 13 | private readonly SecsMessage message = new(s: 1, f: 2, replyExpected: false) 14 | { 15 | SecsItem = 16 | L( 17 | L(), 18 | U1(122, 34), 19 | U2(34531, 23123), 20 | U4(2123513, 52451141), 21 | F4(23123.21323f, 2324.221f), 22 | A("A string"), 23 | J("sdsad"), 24 | F8(231.00002321d, 0.2913212312d), 25 | L( 26 | U1(122, 34), 27 | U2(34531, 23123), 28 | U4(2123513, 52451141), 29 | F4(23123.21323f, 2324.221f), 30 | Boolean(true, false, false, true), 31 | B(0x1C, 0x01, 0xFF), 32 | L( 33 | A("A string"), 34 | J("sdsad"), 35 | Boolean(true, false, false, true), 36 | B(0x1C, 0x01, 0xFF)), 37 | F8(231.00002321d, 0.2913212312d))) 38 | }; 39 | 40 | [Fact] 41 | public void Message_Equals_Should_Be_True() 42 | { 43 | var subject = new SecsMessage(s: 1, f: 2, replyExpected: false) 44 | { 45 | SecsItem = 46 | L( 47 | L(), 48 | U1(122, 34), 49 | U2(34531, 23123), 50 | U4(2123513, 52451141), 51 | F4(23123.21323f, 2324.221f), 52 | A("A string"), 53 | J("sdsad"), 54 | F8(231.00002321d, 0.2913212312d), 55 | L( 56 | U1(122, 34), 57 | U2(34531, 23123), 58 | U4(2123513, 52451141), 59 | F4(23123.21323f, 2324.221f), 60 | Boolean(true, false, false, true), 61 | B(0x1C, 0x01, 0xFF), 62 | L( 63 | A("A string"), 64 | J("sdsad"), 65 | Boolean(true, false, false, true), 66 | B(0x1C, 0x01, 0xFF)), 67 | F8(231.00002321d, 0.2913212312d))) 68 | }; 69 | 70 | subject.Should().BeEquivalentTo(message); 71 | } 72 | 73 | [Fact] 74 | public async Task Message_Can_Decode_From_Full_Buffer() 75 | { 76 | var messageIds = Enumerable.Range(start: 10001, count: 4).ToArray(); 77 | using var buffer = new ArrayPoolBufferWriter(); 78 | 79 | foreach (var id in messageIds) 80 | { 81 | SecsGem.EncodeMessage(message, id, deviceId: 0, buffer); 82 | } 83 | var encodedBytes = buffer.WrittenMemory; 84 | 85 | var pipe = new Pipe(); 86 | var decoder = new PipeDecoder(pipe.Reader, pipe.Writer); 87 | 88 | _ = Task.Run(() => 89 | { 90 | Func act = () => decoder.StartAsync(default); 91 | act.Should().NotThrowAsync(); 92 | }); 93 | 94 | await decoder.Input.WriteAsync(encodedBytes); 95 | 96 | var decodeMessages = await decoder.GetDataMessages(default) 97 | .Take(messageIds.Length) 98 | .Select(m => new 99 | { 100 | m.header.Id, 101 | Message = new SecsMessage(m.header.S, m.header.F, m.header.ReplyExpected) 102 | { 103 | SecsItem = m.rootItem, 104 | }, 105 | }) 106 | .ToListAsync(); 107 | 108 | foreach (var (id, index) in messageIds.Select((a, index) => (a, index))) 109 | { 110 | decodeMessages[index].Id.Should().Be(id); 111 | decodeMessages[index].Message.Should().BeEquivalentTo(message); 112 | } 113 | } 114 | 115 | [Fact] 116 | public async Task Message_Can_Decode_From_Chunked_Sequence() 117 | { 118 | var messageIds = Enumerable.Range(start: 10001, count: 4).ToArray(); 119 | using var buffer = new ArrayPoolBufferWriter(); 120 | 121 | foreach (var id in messageIds) 122 | { 123 | SecsGem.EncodeMessage(message, id, deviceId: 0, buffer); 124 | } 125 | var encodedBytes = buffer.WrittenMemory; 126 | 127 | var pipe = new Pipe(); 128 | var decoder = new PipeDecoder(pipe.Reader, pipe.Writer); 129 | 130 | _ = Task.Run(() => 131 | { 132 | Func act = () => decoder.StartAsync(default); 133 | act.Should().NotThrowAsync(); 134 | }); 135 | 136 | _ = Task.Run(async () => 137 | { 138 | #if NET 139 | var random = Random.Shared; 140 | #else 141 | var random = new Random(13); 142 | #endif 143 | foreach (var chunk in new ChunkedReadOnlyMemory(encodedBytes, size: 23)) 144 | { 145 | await Task.Delay(200); //simulate a slow connection 146 | 147 | if (random.Next() % 2 == 0) 148 | { 149 | await decoder.Input.WriteAsync(ReadOnlyMemory.Empty); 150 | } 151 | 152 | await decoder.Input.WriteAsync(chunk); 153 | } 154 | }); 155 | 156 | var decodeMessages = await decoder.GetDataMessages(default) 157 | .Take(messageIds.Length) 158 | .Select(m => new 159 | { 160 | m.header.Id, 161 | Message = new SecsMessage(m.header.S, m.header.F, m.header.ReplyExpected) 162 | { 163 | SecsItem = m.rootItem, 164 | }, 165 | }) 166 | .ToListAsync(); 167 | 168 | foreach (var (id, index) in messageIds.Select((a, index) => (a, index))) 169 | { 170 | decodeMessages[index].Id.Should().Be(id); 171 | decodeMessages[index].Message.Should().BeEquivalentTo(message); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /test/Secs4Net.UnitTests/Secs4Net.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net8.0;net472 5 | false 6 | False 7 | False 8 | cobertura 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | --------------------------------------------------------------------------------