├── .editorconfig ├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── Sharp7.Rx.Tests ├── Sharp7.Rx.Tests.csproj ├── ValueConverterTests │ ├── ConvertBothWays.cs │ ├── ConverterTestBase.cs │ ├── ReadFromBuffer.cs │ └── WriteToBuffer.cs ├── VariableAddressTests │ └── MatchesType.cs └── VariableNameParserTests.cs ├── Sharp7.Rx.sln └── Sharp7.Rx ├── AssemblyInfo.cs ├── Basics ├── ConcurrentSubjectDictionary.cs ├── DisposableItem.cs └── LimitedConcurrencyLevelTaskScheduler.cs ├── CacheVariableNameParser.cs ├── Enums ├── ConnectionState.cs ├── DbType.cs ├── Operand.cs └── TransmissionMode.cs ├── Exceptions └── S7Exception.cs ├── Extensions ├── DisposableExtensions.cs ├── ObservableExtensions.cs ├── OperandExtensions.cs ├── PlcExtensions.cs └── S7VariableExtensions.cs ├── Interfaces ├── IPlc.cs └── IVariableNameParser.cs ├── S7ErrorCodes.cs ├── Settings └── PlcConnectionSettings.cs ├── Sharp7.Rx.csproj ├── Sharp7.Rx.csproj.DotSettings ├── Sharp7Connector.cs ├── Sharp7Plc.cs ├── Utils └── SignatureConverter.cs ├── ValueConverter.cs ├── VariableAddress.cs ├── VariableNameParser.cs └── linqpad-samples ├── Create Notification.linq ├── Establish connection.linq ├── FileOrder.txt ├── Multiple notifications.linq └── Write and read value.linq /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Baseline 7 | [*] 8 | charset = utf-8-bom 9 | indent_style = space 10 | indent_size = 4 11 | tab_width = 4 12 | trim_trailing_whitespace = true 13 | max_line_length = 200 14 | insert_final_newline = true 15 | 16 | [*.{json,xml,csproj,config}] 17 | indent_size = 2 18 | 19 | [Directory.*.props] 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - prerelease 8 | pull_request: 9 | branches: [ master ] 10 | 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: windows-latest 16 | env: 17 | version: 2.0.${{ github.run_number }}${{ github.ref != 'refs/heads/master' && '-prerelease' || '' }} 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Setup .NET Core 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: 8.0.x 25 | - name: Install NUnit.ConsoleRunner 26 | run: nuget install NUnit.ConsoleRunner -Version 3.17.0 -DirectDownload -OutputDirectory . 27 | - name: Install dependencies 28 | run: dotnet restore 29 | - name: Build 30 | run: dotnet build --configuration Release --no-restore /p:version=${{ env.version }} 31 | - name: Tests 32 | run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net8.0\Sharp7.Rx.Tests.dll" 33 | - name: NugetPublish 34 | if: github.event_name == 'push' 35 | run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.${{ env.version }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sharp7Reactive 2 | 3 | [![Release](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml) 4 | ![Licence](https://img.shields.io/github/license/evopro-ag/Sharp7Reactive.svg) 5 | [![Nuget Version](https://img.shields.io/nuget/v/Sharp7.Rx.svg)](https://www.nuget.org/packages/Sharp7.Rx/) 6 | 7 | 8 | This is an additional library for the usage if [Sharp7](https://github.com/fbarresi/sharp7). 9 | It combines the S7 communication library with the power of System.Reactive. 10 | 11 | ## Main features 12 | - Completly free and ready for usage (the library is already widely used in many enterprise environments) 13 | - Connection status observation and auto-reconnect 14 | - Type safe and with generics 15 | - Threadsafe (Sharp7 is basically not threadsafe) 16 | 17 | ## Quick start 18 | 19 | Install the [Sharp7.Rx Nuget package](https://www.nuget.org/packages/Sharp7.Rx). 20 | 21 | The example below shows you how to create and use the Sharp7Rx PLC. 22 | 23 | ```csharp 24 | using (var disposables = new CompositeDisposable()) 25 | { 26 | // create a new PLC 27 | var plc = new Sharp7Plc("10.30.3.10", 0, 2); 28 | disposables.Add(plc); 29 | 30 | // initialize and connect to the plc 31 | await plc.InitializeConnection(); 32 | 33 | await plc.SetValue("DB2.DBX0.4", true); // set a bit 34 | var value = await plc.GetValue("DB2.Int4"); // get an 16 bit integer 35 | Console.WriteLine(value) 36 | 37 | // create a nofication for data change in the plc 38 | var notification = plc.CreateNotification("DB1.DBX0.2", TransmissionMode.OnChange) 39 | .Where(b => b) //select rising edge 40 | .Do(_ => doStuff()) 41 | .Subscribe(); 42 | disposables.Add(notification); 43 | 44 | //wait for enter before ending the program 45 | Console.ReadLine(); 46 | 47 | } 48 | ``` 49 | 50 | The best way to test your PLC application is running your [SoftPLC](https://github.com/fbarresi/softplc) locally. 51 | 52 | ## Examples 53 | 54 | This library comes with integrated [LinqPad](https://www.linqpad.net/) examples - even for the free edition. Just download the [Sharp7.Rx Nuget package](https://www.nuget.org/packages/Sharp7.Rx) after pressing `Ctrl + Shift + P` and browse the "Samples". 55 | 56 | [Sharp7Monitor](https://github.com/Peter-B-/Sharp7.Monitor) is a console application for monitoring S7 variables over RFC1006, based on this library. 57 | 58 | ## Addressing rules 59 | 60 | Sharp7Reactive uses a syntax for identifying addresses similar to official siemens syntax. 61 | Every address has the form (case unsensitive) `DB..`. 62 | 63 | | Example | Explaination | 64 | | ------------------------------------ | ----------------------------------------------------------------- | 65 | | `DB42.Int4` or
`DB42.DBD4` | Datablock 42, 16 bit integer from bytes 4 to 5 (zero based index) | 66 | | `DB42.Bit0.7` or
`DB42.DBX0.7` | Datablock 42, bit from byte 0, position 7 | 67 | | `DB42.Byte4.25` or
`DB42.DBB4.25` | Datablock 42, 25 bytes from byte 4 to 29 (zero based index) | 68 | 69 | Here is a table of supported data types: 70 | 71 | |.Net Type|Identifier |Description |Length or bit |Example |Example remark | 72 | |---------|-----------------------------|----------------------------------------------|----------------------------------------|-------------------|--------------------------| 73 | |bool |bit, dbx |Bit as boolean value |Bit index (0 .. 7) |`Db200.Bit2.2` |Reads bit 3 | 74 | |byte |byte, dbb, b* |8 bit unsigned integer | |`Db200.Byte4` | | 75 | |byte[] |byte, dbb, b* |Array of bytes |Array length in bytes |`Db200.Byte4.16` | | 76 | |short |int, dbw, w* |16 bit signed integer | |`Db200.Int4` | | 77 | |ushort |uint |16 bit unsigned integer | |`Db200.UInt4` | | 78 | |int |dint, dbd |32 bit signed integer | |`Db200.DInt4` | | 79 | |uint |udint |32 bit unsigned integer | |`Db200.UDInt4` | | 80 | |long |lint |64 bit signed integer | |`Db200.LInt4` | | 81 | |ulong |ulint, dul*, dulint*, dulong*|64 bit unsigned integer | |`Db200.ULInt4` | | 82 | |float |real, d* |32 bit float | |`Db200.Real4` | | 83 | |double |lreal |64 bit float | |`Db200.LReal4` | | 84 | |string |string, s* |ASCII text string with string size |String length in bytes (1 .. 254) |`Db200.String4.16` |Uses 18 bytes = 16 + 2 | 85 | |string |wstring |UTF-16 Big Endian text string with string size|String length in characters (1 .. 16382)|`Db200.WString4.16`|Uses 36 bytes = 16 * 2 + 4| 86 | |string |byte[] |ASCII string as byte array |String length in bytes |`Db200.Byte4.16` |Uses 16 bytes | 87 | 88 | > Identifiers marked with * are kept for compatability reasons and might be removed in the future. 89 | 90 | ## Performance considerations 91 | 92 | Frequent reads of variables using `GetValue` can cause performance pressure on the S7 PLC, resulting in an increase of cycle time. 93 | 94 | If you frequently read variables, like polling triggers, use `CreateNotification`. Internally all variable polling initialized with `CreateNotification` is pooled to a single (or some) multi-variable-reads. 95 | 96 | You can provide a cycle time (delay between consecutive multi variable reads) in the `Sharp7Plc` constructor: 97 | 98 | ```csharp 99 | public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null) 100 | ``` 101 | 102 | The default value for `multiVarRequestCycleTime` is 100 ms, the minimal value is 5 ms. 103 | 104 | ## Would you like to contribute? 105 | 106 | Yes, please! 107 | 108 | Try the library and feel free to open an issue or ask for support. 109 | 110 | Don't forget to **star this project**! 111 | -------------------------------------------------------------------------------- /Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 12.0 6 | enable 7 | enable 8 | latest-Recommended 9 | 10 | 14 | $(NoWarn);CA1859;CA1852 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Shouldly; 3 | 4 | namespace Sharp7.Rx.Tests.ValueConverterTests; 5 | 6 | [TestFixture] 7 | internal class ConvertBothWays : ConverterTestBase 8 | { 9 | [TestCaseSource(nameof(GetValidTestCases))] 10 | public void Convert(ConverterTestCase tc) 11 | { 12 | //Arrange 13 | var buffer = new byte[tc.VariableAddress.BufferLength]; 14 | 15 | var write = CreateWriteMethod(tc); 16 | var read = CreateReadMethod(tc); 17 | 18 | //Act 19 | write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); 20 | var result = read.Invoke(null, [buffer, tc.VariableAddress]); 21 | 22 | //Assert 23 | result.ShouldBe(tc.Value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Sharp7.Rx.Interfaces; 3 | 4 | namespace Sharp7.Rx.Tests.ValueConverterTests; 5 | 6 | internal abstract class ConverterTestBase 7 | { 8 | protected static readonly IVariableNameParser Parser = new VariableNameParser(); 9 | 10 | public static MethodInfo CreateReadMethod(ConverterTestCase tc) 11 | { 12 | var convertMi = typeof(ConverterTestBase).GetMethod(nameof(ReadFromBuffer)); 13 | var convert = convertMi!.MakeGenericMethod(tc.Value.GetType()); 14 | return convert; 15 | } 16 | 17 | public static MethodInfo CreateWriteMethod(ConverterTestCase tc) 18 | { 19 | var writeMi = typeof(ConverterTestBase).GetMethod(nameof(WriteToBuffer)); 20 | var write = writeMi!.MakeGenericMethod(tc.Value.GetType()); 21 | return write; 22 | } 23 | 24 | public static IEnumerable GetValidTestCases() 25 | { 26 | yield return new ConverterTestCase(true, "DB99.bit5.4", [0x10]); 27 | yield return new ConverterTestCase(false, "DB99.bit5.4", [0x00]); 28 | 29 | yield return new ConverterTestCase((byte) 18, "DB99.Byte5", [0x12]); 30 | yield return new ConverterTestCase((short) 4660, "DB99.Int5", [0x12, 0x34]); 31 | yield return new ConverterTestCase((short) -3532, "DB99.Int5", [0xF2, 0x34]); 32 | yield return new ConverterTestCase((ushort) 4660, "DB99.UInt5", [0x12, 0x34]); 33 | yield return new ConverterTestCase((ushort) 62004, "DB99.UInt5", [0xF2, 0x34]); 34 | yield return new ConverterTestCase(305419879, "DB99.DInt5", [0x12, 0x34, 0x56, 0x67]); 35 | yield return new ConverterTestCase(-231451033, "DB99.DInt5", [0xF2, 0x34, 0x56, 0x67]); 36 | yield return new ConverterTestCase(305419879u, "DB99.UDInt5", [0x12, 0x34, 0x56, 0x67]); 37 | yield return new ConverterTestCase(4063516263u, "DB99.UDInt5", [0xF2, 0x34, 0x56, 0x67]); 38 | yield return new ConverterTestCase(1311768394163015151L, "DB99.LInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); 39 | yield return new ConverterTestCase(-994074615050678801L, "DB99.LInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); 40 | yield return new ConverterTestCase(1311768394163015151uL, "DB99.ULInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); 41 | yield return new ConverterTestCase(17452669458658872815uL, "DB99.ULInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); 42 | yield return new ConverterTestCase(0.25f, "DB99.Real5", [0x3E, 0x80, 0x00, 0x00]); 43 | yield return new ConverterTestCase(0.25, "DB99.LReal5", [0x3F, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); 44 | 45 | yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.Byte5.4", [0x12, 0x34, 0x56, 0x67]); 46 | 47 | yield return new ConverterTestCase("ABCD", "DB99.String10.4", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44]); 48 | yield return new ConverterTestCase("ABCD", "DB99.String10.6", [0x06, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00]); 49 | yield return new ConverterTestCase("ABCD", "DB99.WString10.4", [0x00, 0x04, 0x00, 0x04, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44]); 50 | yield return new ConverterTestCase("ABCD", "DB99.WString10.6", [0x00, 0x06, 0x00, 0x04, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00]); 51 | yield return new ConverterTestCase("ABCD", "DB99.Byte5.4", [0x41, 0x42, 0x43, 0x44]); 52 | yield return new ConverterTestCase("A\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80A", "DB99.WString10.10", 53 | [0x0, 0xA, 0x0, 0x9, 0x0, 0x41, 0xD8, 0x3D, 0xDC, 0x69, 0xD8, 0x3C, 0xDF, 0xFD, 0x20, 0xD, 0xD8, 0x3D, 0xDE, 0x80, 0x0, 0x41, 0x0, 0x0]); 54 | 55 | yield return new ConverterTestCase(true, "DB99.DBx0.0", [0x01]); 56 | yield return new ConverterTestCase(false, "DB99.DBx0.0", [0x00]); 57 | yield return new ConverterTestCase(true, "DB99.DBx0.4", [0x10]); 58 | yield return new ConverterTestCase(false, "DB99.DBx0.4", [0]); 59 | yield return new ConverterTestCase((byte) 18, "DB99.DBB0", [0x12]); 60 | yield return new ConverterTestCase((short) 4660, "DB99.INT0", [0x12, 0x34]); 61 | yield return new ConverterTestCase((short) -3532, "DB99.INT0", [0xF2, 0x34]); 62 | yield return new ConverterTestCase(305419879, "DB99.DINT0", [0x12, 0x34, 0x56, 0x67]); 63 | yield return new ConverterTestCase(-231451033, "DB99.DINT0", [0xF2, 0x34, 0x56, 0x67]); 64 | yield return new ConverterTestCase(1311768394163015151uL, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); 65 | yield return new ConverterTestCase(17452669458658872815uL, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); 66 | yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.DBB0.4", [0x12, 0x34, 0x56, 0x67]); 67 | yield return new ConverterTestCase(0.25f, "DB99.D0", [0x3E, 0x80, 0x00, 0x00]); 68 | } 69 | 70 | /// 71 | /// This helper method exists, since I could not manage to invoke a generic method 72 | /// with a Span<T> parameter. 73 | /// 74 | public static TValue ReadFromBuffer(byte[] buffer, VariableAddress address) => 75 | ValueConverter.ReadFromBuffer(buffer, address); 76 | 77 | /// 78 | /// This helper method exists, since I could not manage to invoke a generic method 79 | /// with a Span<T> parameter. 80 | /// 81 | public static void WriteToBuffer(byte[] buffer, TValue value, VariableAddress address) => 82 | ValueConverter.WriteToBuffer(buffer, value, address); 83 | 84 | public record ConverterTestCase(object Value, string Address, byte[] Data) 85 | { 86 | public VariableAddress VariableAddress => Parser.Parse(Address); 87 | 88 | public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}"; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Shouldly; 3 | 4 | namespace Sharp7.Rx.Tests.ValueConverterTests; 5 | 6 | [TestFixture] 7 | internal class ReadFromBuffer : ConverterTestBase 8 | { 9 | [TestCaseSource(nameof(GetValidTestCases))] 10 | [TestCaseSource(nameof(GetAdditinalReadTestCases))] 11 | public void Read(ConverterTestCase tc) 12 | { 13 | //Arrange 14 | var convert = CreateReadMethod(tc); 15 | 16 | //Act 17 | var result = convert.Invoke(null, [tc.Data, tc.VariableAddress]); 18 | 19 | //Assert 20 | result.ShouldBe(tc.Value); 21 | } 22 | 23 | public static IEnumerable GetAdditinalReadTestCases() 24 | { 25 | yield return new ConverterTestCase(true, "DB0.DBx0.4", [0x1F]); 26 | yield return new ConverterTestCase(false, "DB0.DBx0.4", [0xEF]); 27 | yield return new ConverterTestCase("ABCD", "DB0.string0.6", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00]); // Length in address exceeds PLC string length 28 | } 29 | 30 | [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] 31 | public void UnsupportedType(T template, string address, byte[] data) 32 | { 33 | //Arrange 34 | var variableAddress = Parser.Parse(address); 35 | 36 | //Act 37 | Should.Throw(() => ValueConverter.ReadFromBuffer(data, variableAddress)); 38 | } 39 | 40 | [TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})] 41 | [TestCase((short) 123, "DB12.INT3", new byte[] {0xF2})] 42 | [TestCase("ABC", "DB0.string0.6", new byte[] {0x01, 0x02, 0x03})] 43 | public void BufferTooSmall(T template, string address, byte[] data) 44 | { 45 | //Arrange 46 | var variableAddress = Parser.Parse(address); 47 | 48 | //Act 49 | Should.Throw(() => ValueConverter.ReadFromBuffer(data, variableAddress)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Shouldly; 3 | 4 | namespace Sharp7.Rx.Tests.ValueConverterTests; 5 | 6 | [TestFixture] 7 | internal class WriteToBuffer : ConverterTestBase 8 | { 9 | [TestCaseSource(nameof(GetValidTestCases))] 10 | [TestCaseSource(nameof(GetAdditinalWriteTestCases))] 11 | public void Write(ConverterTestCase tc) 12 | { 13 | //Arrange 14 | var buffer = new byte[tc.VariableAddress.BufferLength]; 15 | var write = CreateWriteMethod(tc); 16 | 17 | //Act 18 | write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); 19 | 20 | //Assert 21 | buffer.ShouldBe(tc.Data); 22 | } 23 | 24 | public static IEnumerable GetAdditinalWriteTestCases() 25 | { 26 | yield return new ConverterTestCase("a", "DB0.Byte80.3", [0x61, 0x00, 0x00]); // short string 27 | yield return new ConverterTestCase("abc", "DB0.Byte80.3", [0x61, 0x62, 0x63]); // matching string 28 | yield return new ConverterTestCase("abcxx", "DB0.Byte80.3", [0x61, 0x62, 0x63]); // long string 29 | 30 | yield return new ConverterTestCase("a", "DB0.string0.3", [0x03, 0x01, 0x61, 0x00, 0x00]); // short string 31 | yield return new ConverterTestCase("abc", "DB0.string0.3", [0x03, 0x03, 0x61, 0x62, 0x63]); // matching string 32 | yield return new ConverterTestCase("abcxx", "DB0.string0.3", [0x03, 0x03, 0x61, 0x62, 0x63]); // long string 33 | 34 | yield return new ConverterTestCase("a", "DB0.wstring0.3", [0x00, 0x03, 0x00, 0x01, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00]); // short string 35 | yield return new ConverterTestCase("abc", "DB0.wstring0.3", [0x00, 0x03, 0x00, 0x03, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63]); // matching string 36 | yield return new ConverterTestCase("abcxx", "DB0.wstring0.3", [0x00, 0x03, 0x00, 0x03, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63]); // long string 37 | 38 | 39 | yield return new ConverterTestCase("aaaaBCDE", "DB0.string0.4", [0x04, 0x04, 0x61, 0x61, 0x61, 0x61]); // Length in address exceeds PLC string length 40 | yield return new ConverterTestCase("aaaaBCDE", "DB0.WString0.4", [0x00, 0x04, 0x00, 0x04, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61]); // Length in address exceeds PLC string length 41 | yield return new ConverterTestCase("aaaaBCDE", "DB0.DBB0.4", [0x61, 0x61, 0x61, 0x61]); // Length in address exceeds PLC array length 42 | 43 | // Length in address exceeds PLC string length, multi char unicode point 44 | yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.WString0.2", [0x00, 0x02, 0x00, 0x02, 0xD8, 0x3D, 0xDC, 0x69]); 45 | 46 | // Length in address exceeds PLC string length, multi char unicode point 47 | yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.String0.2", [0x02, 0x02, 0x3F, 0x3F]); 48 | 49 | // Length in address exceeds PLC string length, multi char unicode point 50 | yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.DBB0.4", [0x3F, 0x3F, 0x3F, 0x3F]); 51 | } 52 | 53 | [TestCase(18, "DB0.DInt12", 3)] 54 | [TestCase(0.25f, "DB0.Real1", 3)] 55 | [TestCase("test", "DB0.String1.10", 9)] 56 | public void BufferToSmall(T input, string address, int bufferSize) 57 | { 58 | //Arrange 59 | var variableAddress = Parser.Parse(address); 60 | var buffer = new byte[bufferSize]; 61 | 62 | //Act 63 | Should.Throw(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress)); 64 | } 65 | 66 | [TestCase((char) 18, "DB0.DBB0")] 67 | public void UnsupportedType(T input, string address) 68 | { 69 | //Arrange 70 | var variableAddress = Parser.Parse(address); 71 | var buffer = new byte[variableAddress.BufferLength]; 72 | 73 | //Act 74 | Should.Throw(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Sharp7.Rx.Extensions; 3 | using Sharp7.Rx.Interfaces; 4 | using Sharp7.Rx.Tests.ValueConverterTests; 5 | using Shouldly; 6 | 7 | namespace Sharp7.Rx.Tests.VariableAddressTests; 8 | 9 | [TestFixture] 10 | public class MatchesType 11 | { 12 | static readonly IVariableNameParser parser = new VariableNameParser(); 13 | 14 | private static readonly IReadOnlyList typeList = new[] 15 | { 16 | typeof(byte), 17 | typeof(byte[]), 18 | 19 | typeof(bool), 20 | typeof(short), 21 | typeof(ushort), 22 | typeof(int), 23 | typeof(uint), 24 | typeof(long), 25 | typeof(ulong), 26 | 27 | typeof(float), 28 | typeof(double), 29 | 30 | typeof(string), 31 | 32 | typeof(int[]), 33 | typeof(float[]), 34 | typeof(DateTime[]), 35 | typeof(object), 36 | }; 37 | 38 | [TestCaseSource(nameof(GetValid))] 39 | public void Supported(TestCase tc) => Check(tc.Type, tc.Address, true); 40 | 41 | [TestCaseSource(nameof(GetInvalid))] 42 | public void Unsupported(TestCase tc) => Check(tc.Type, tc.Address, false); 43 | 44 | 45 | public static IEnumerable GetValid() 46 | { 47 | return 48 | ConverterTestBase.GetValidTestCases() 49 | .Select(tc => new TestCase(tc.Value.GetType(), tc.Address)); 50 | } 51 | 52 | public static IEnumerable GetInvalid() 53 | { 54 | return 55 | ConverterTestBase.GetValidTestCases() 56 | .DistinctBy(tc => tc.Value.GetType()) 57 | .SelectMany(tc => 58 | typeList.Where(type => type != tc.Value.GetType()) 59 | .Select(type => new TestCase(type, tc.Address)) 60 | ) 61 | 62 | // Explicitly remove some valid combinations 63 | .Where(tc => !( 64 | (tc.Type == typeof(string) && tc.Address == "DB99.Byte5") || 65 | (tc.Type == typeof(string) && tc.Address == "DB99.Byte5.4") || 66 | (tc.Type == typeof(byte[]) && tc.Address == "DB99.Byte5") 67 | )) 68 | ; 69 | } 70 | 71 | 72 | private static void Check(Type type, string address, bool expected) 73 | { 74 | //Arrange 75 | var variableAddress = parser.Parse(address); 76 | 77 | //Act 78 | variableAddress.MatchesType(type).ShouldBe(expected); 79 | } 80 | 81 | public record TestCase(Type Type, string Address) 82 | { 83 | public override string ToString() => $"{Type.Name} {Address}"; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sharp7.Rx.Tests/VariableNameParserTests.cs: -------------------------------------------------------------------------------- 1 | using DeepEqual.Syntax; 2 | using NUnit.Framework; 3 | using Sharp7.Rx.Enums; 4 | using Shouldly; 5 | 6 | namespace Sharp7.Rx.Tests; 7 | 8 | [TestFixture] 9 | internal class VariableNameParserTests 10 | { 11 | [TestCaseSource(nameof(ValidTestCases))] 12 | public void Run(TestCase tc) 13 | { 14 | var parser = new VariableNameParser(); 15 | var resp = parser.Parse(tc.Input); 16 | resp.ShouldDeepEqual(tc.Expected); 17 | } 18 | 19 | [TestCase("DB506.Bit216", TestName = "Bit without Bit")] 20 | [TestCase("DB506.Bit216.8", TestName = "Bit to high")] 21 | [TestCase("DB506.String216", TestName = "String without Length")] 22 | [TestCase("DB506.WString216", TestName = "WString without Length")] 23 | [TestCase("DB506.Int216.1", TestName = "Int with Length")] 24 | [TestCase("DB506.UInt216.1", TestName = "UInt with Length")] 25 | [TestCase("DB506.DInt216.1", TestName = "DInt with Length")] 26 | [TestCase("DB506.UDInt216.1", TestName = "UDInt with Length")] 27 | [TestCase("DB506.LInt216.1", TestName = "LInt with Length")] 28 | [TestCase("DB506.ULInt216.1", TestName = "ULInt with Length")] 29 | [TestCase("DB506.Real216.1", TestName = "LReal with Length")] 30 | [TestCase("DB506.LReal216.1", TestName = "LReal with Length")] 31 | [TestCase("DB506.xx216", TestName = "Invalid type")] 32 | [TestCase("DB506.216", TestName = "No type")] 33 | [TestCase("DB506.Int216.", TestName = "Trailing dot")] 34 | [TestCase("x506.Int216", TestName = "Wrong type")] 35 | [TestCase("506.Int216", TestName = "No type")] 36 | [TestCase("", TestName = "empty")] 37 | [TestCase(" ", TestName = "space")] 38 | [TestCase(" DB506.Int216", TestName = "leading space")] 39 | [TestCase("DB506.Int216 ", TestName = "trailing space")] 40 | [TestCase("DB.Int216 ", TestName = "No db")] 41 | [TestCase("DB5061234.Int216.1", TestName = "DB too large")] 42 | public void Invalid(string? input) 43 | { 44 | var parser = new VariableNameParser(); 45 | Should.Throw(() => parser.Parse(input!)); 46 | } 47 | 48 | public static IEnumerable ValidTestCases() 49 | { 50 | yield return new TestCase("DB506.Bit216.2", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Bit, Start: 216, Length: 1, Bit: 2)); 51 | 52 | yield return new TestCase("DB506.String216.10", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.String, Start: 216, Length: 10)); 53 | yield return new TestCase("DB506.WString216.10", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.WString, Start: 216, Length: 10)); 54 | 55 | yield return new TestCase("DB506.Byte216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 1)); 56 | yield return new TestCase("DB506.Byte216.100", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 100)); 57 | yield return new TestCase("DB506.Int216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2)); 58 | yield return new TestCase("DB506.UInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.UInt, Start: 216, Length: 2)); 59 | yield return new TestCase("DB506.DInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.DInt, Start: 216, Length: 4)); 60 | yield return new TestCase("DB506.UDInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.UDInt, Start: 216, Length: 4)); 61 | yield return new TestCase("DB506.LInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.LInt, Start: 216, Length: 8)); 62 | yield return new TestCase("DB506.ULInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); 63 | 64 | yield return new TestCase("DB506.Real216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Single, Start: 216, Length: 4)); 65 | yield return new TestCase("DB506.LReal216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Double, Start: 216, Length: 8)); 66 | 67 | 68 | // Legacy 69 | yield return new TestCase("DB13.DBX3.1", new VariableAddress(Operand: Operand.Db, DbNo: 13, Type: DbType.Bit, Start: 3, Length: 1, Bit: 1)); 70 | yield return new TestCase("Db403.X5.2", new VariableAddress(Operand: Operand.Db, DbNo: 403, Type: DbType.Bit, Start: 5, Length: 1, Bit: 2)); 71 | yield return new TestCase("DB55DBX23.6", new VariableAddress(Operand: Operand.Db, DbNo: 55, Type: DbType.Bit, Start: 23, Length: 1, Bit: 6)); 72 | yield return new TestCase("DB1.S255.20", new VariableAddress(Operand: Operand.Db, DbNo: 1, Type: DbType.String, Start: 255, Length: 20)); 73 | yield return new TestCase("DB5.String887.20", new VariableAddress(Operand: Operand.Db, DbNo: 5, Type: DbType.String, Start: 887, Length: 20)); 74 | yield return new TestCase("DB506.B216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 1)); 75 | yield return new TestCase("DB506.DBB216.5", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 5)); 76 | yield return new TestCase("DB506.D216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Single, Start: 216, Length: 4)); 77 | yield return new TestCase("DB506.DINT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.DInt, Start: 216, Length: 4)); 78 | yield return new TestCase("DB506.INT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2)); 79 | yield return new TestCase("DB506.DBW216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2)); 80 | yield return new TestCase("DB506.DUL216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); 81 | yield return new TestCase("DB506.DULINT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); 82 | yield return new TestCase("DB506.DULONG216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); 83 | } 84 | 85 | public record TestCase(string Input, VariableAddress Expected) 86 | { 87 | public override string ToString() => Input; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sharp7.Rx.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34728.123 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharp7.Rx", "Sharp7.Rx\Sharp7.Rx.csproj", "{690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharp7.Rx.Tests", "Sharp7.Rx.Tests\Sharp7.Rx.Tests.csproj", "{1BDD07D2-6540-4ACF-81E7-98300421073B}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{3A9DEBA7-8F53-4554-869C-7C99F0A4932E}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | .gitignore = .gitignore 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflow", "Workflow", "{1CFDA2EA-49CF-4B96-A9C9-B12B21B3D78E}" 18 | ProjectSection(SolutionItems) = preProject 19 | .github\workflows\release.yml = .github\workflows\release.yml 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {1BDD07D2-6540-4ACF-81E7-98300421073B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {1BDD07D2-6540-4ACF-81E7-98300421073B}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {1BDD07D2-6540-4ACF-81E7-98300421073B}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {1BDD07D2-6540-4ACF-81E7-98300421073B}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(NestedProjects) = preSolution 41 | {1CFDA2EA-49CF-4B96-A9C9-B12B21B3D78E} = {3A9DEBA7-8F53-4554-869C-7C99F0A4932E} 42 | EndGlobalSection 43 | GlobalSection(ExtensibilityGlobals) = postSolution 44 | SolutionGuid = {ABA1FD47-15EE-4588-9BA7-0116C635BFC4} 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /Sharp7.Rx/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Sharp7.Rx.Tests")] 4 | -------------------------------------------------------------------------------- /Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Reactive; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | 6 | namespace Sharp7.Rx.Basics; 7 | 8 | internal class ConcurrentSubjectDictionary : IDisposable 9 | { 10 | private readonly object dictionaryLock = new object(); 11 | private readonly Func valueFactory; 12 | private ConcurrentDictionary dictionary; 13 | 14 | public ConcurrentSubjectDictionary() 15 | { 16 | dictionary = new ConcurrentDictionary(); 17 | } 18 | 19 | public ConcurrentSubjectDictionary(IEqualityComparer comparer) 20 | { 21 | dictionary = new ConcurrentDictionary(comparer); 22 | } 23 | 24 | public ConcurrentSubjectDictionary(TValue initialValue, IEqualityComparer comparer) 25 | { 26 | valueFactory = _ => initialValue; 27 | dictionary = new ConcurrentDictionary(comparer); 28 | } 29 | 30 | public ConcurrentSubjectDictionary(TValue initialValue) 31 | { 32 | valueFactory = _ => initialValue; 33 | dictionary = new ConcurrentDictionary(); 34 | } 35 | 36 | public ConcurrentSubjectDictionary(Func valueFactory = null) 37 | { 38 | this.valueFactory = valueFactory; 39 | dictionary = new ConcurrentDictionary(); 40 | } 41 | 42 | public IEnumerable ExistingKeys => dictionary.Keys; 43 | 44 | public bool IsDisposed { get; private set; } 45 | 46 | public void Dispose() 47 | { 48 | Dispose(true); 49 | GC.SuppressFinalize(this); 50 | } 51 | 52 | public DisposableItem GetOrCreateObservable(TKey key) 53 | { 54 | lock (dictionaryLock) 55 | { 56 | var subject = dictionary.AddOrUpdate( 57 | key, 58 | k => new SubjectWithRefCounter(CreateSubject(k)), 59 | (_, subjectWithRefCounter) => 60 | { 61 | subjectWithRefCounter.IncreaseCount(); 62 | return subjectWithRefCounter; 63 | }); 64 | 65 | return new DisposableItem(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key)); 66 | } 67 | } 68 | 69 | public bool TryGetObserver(TKey key, out IObserver subject) 70 | { 71 | if (dictionary.TryGetValue(key, out var subjectWithRefCount)) 72 | { 73 | subject = subjectWithRefCount.Subject.AsObserver(); 74 | return true; 75 | } 76 | 77 | subject = null; 78 | return false; 79 | } 80 | 81 | protected virtual void Dispose(bool disposing) 82 | { 83 | if (IsDisposed) 84 | return; 85 | if (disposing && dictionary != null) 86 | { 87 | foreach (var subjectWithRefCounter in dictionary) 88 | subjectWithRefCounter.Value.Subject.OnCompleted(); 89 | dictionary.Clear(); 90 | dictionary = null; 91 | } 92 | 93 | IsDisposed = true; 94 | } 95 | 96 | private ISubject CreateSubject(TKey key) 97 | { 98 | if (valueFactory == null) 99 | return new Subject(); 100 | return new BehaviorSubject(valueFactory(key)); 101 | } 102 | 103 | private void RemoveIfNoLongerInUse(TKey variableName) 104 | { 105 | lock (dictionaryLock) 106 | if (dictionary.TryGetValue(variableName, out var subjectWithRefCount)) 107 | if (subjectWithRefCount.DecreaseCount() < 1) 108 | dictionary.TryRemove(variableName, out _); 109 | } 110 | 111 | ~ConcurrentSubjectDictionary() 112 | { 113 | Dispose(false); 114 | } 115 | 116 | class SubjectWithRefCounter 117 | { 118 | private int counter = 1; 119 | 120 | public SubjectWithRefCounter(ISubject subject) 121 | { 122 | Subject = subject; 123 | } 124 | 125 | public ISubject Subject { get; } 126 | 127 | public int DecreaseCount() => Interlocked.Decrement(ref counter); 128 | public int IncreaseCount() => Interlocked.Increment(ref counter); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sharp7.Rx/Basics/DisposableItem.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx.Basics; 2 | 3 | internal class DisposableItem : IDisposable 4 | { 5 | private readonly Action disposeAction; 6 | 7 | bool disposed; 8 | 9 | public DisposableItem(IObservable observable, Action disposeAction) 10 | { 11 | this.disposeAction = disposeAction; 12 | Observable = observable; 13 | } 14 | 15 | public IObservable Observable { get; } 16 | 17 | public void Dispose() 18 | { 19 | Dispose(true); 20 | GC.SuppressFinalize(this); 21 | } 22 | 23 | protected virtual void Dispose(bool disposing) 24 | { 25 | if (disposed) return; 26 | 27 | if (disposing) 28 | { 29 | disposeAction(); 30 | } 31 | 32 | disposed = true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx.Basics; 2 | 3 | /// 4 | /// Provides a task scheduler that ensures a maximum concurrency level while 5 | /// running on top of the ThreadPool. 6 | /// from http://msdn.microsoft.com/en-us/library/ee789351.aspx 7 | /// 8 | internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler 9 | { 10 | /// Whether the current thread is processing work items. 11 | [ThreadStatic] private static bool currentThreadIsProcessingItems; 12 | 13 | /// The maximum concurrency level allowed by this scheduler. 14 | private readonly int maxDegreeOfParallelism; 15 | 16 | /// The list of tasks to be executed. 17 | private readonly LinkedList tasks = new LinkedList(); // protected by lock(_tasks) 18 | 19 | /// Whether the scheduler is currently processing work items. 20 | private int delegatesQueuedOrRunning; // protected by lock(_tasks) 21 | 22 | /// 23 | /// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the 24 | /// specified degree of parallelism. 25 | /// 26 | /// The maximum degree of parallelism provided by this scheduler. 27 | public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) 28 | { 29 | if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); 30 | this.maxDegreeOfParallelism = maxDegreeOfParallelism; 31 | } 32 | 33 | /// Gets the maximum concurrency level supported by this scheduler. 34 | public sealed override int MaximumConcurrencyLevel => maxDegreeOfParallelism; 35 | 36 | /// Gets an enumerable of the tasks currently scheduled on this scheduler. 37 | /// An enumerable of the tasks currently scheduled. 38 | protected sealed override IEnumerable GetScheduledTasks() 39 | { 40 | var lockTaken = false; 41 | try 42 | { 43 | Monitor.TryEnter(tasks, ref lockTaken); 44 | if (lockTaken) return tasks.ToArray(); 45 | else throw new NotSupportedException(); 46 | } 47 | finally 48 | { 49 | if (lockTaken) Monitor.Exit(tasks); 50 | } 51 | } 52 | 53 | /// Queues a task to the scheduler. 54 | /// The task to be queued. 55 | protected sealed override void QueueTask(Task task) 56 | { 57 | // Add the task to the list of tasks to be processed. If there aren't enough 58 | // delegates currently queued or running to process tasks, schedule another. 59 | lock (tasks) 60 | { 61 | tasks.AddLast(task); 62 | if (delegatesQueuedOrRunning < maxDegreeOfParallelism) 63 | { 64 | ++delegatesQueuedOrRunning; 65 | NotifyThreadPoolOfPendingWork(); 66 | } 67 | } 68 | } 69 | 70 | /// Attempts to remove a previously scheduled task from the scheduler. 71 | /// The task to be removed. 72 | /// Whether the task could be found and removed. 73 | protected sealed override bool TryDequeue(Task task) 74 | { 75 | lock (tasks) 76 | { 77 | return tasks.Remove(task); 78 | } 79 | } 80 | 81 | /// Attempts to execute the specified task on the current thread. 82 | /// The task to be executed. 83 | /// 84 | /// Whether the task could be executed on the current thread. 85 | protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) 86 | { 87 | // If this thread isn't already processing a task, we don't support inlining 88 | if (!currentThreadIsProcessingItems) return false; 89 | 90 | // If the task was previously queued, remove it from the queue 91 | if (taskWasPreviouslyQueued) TryDequeue(task); 92 | 93 | // Try to run the task. 94 | return TryExecuteTask(task); 95 | } 96 | 97 | /// 98 | /// Informs the ThreadPool that there's work to be executed for this scheduler. 99 | /// 100 | private void NotifyThreadPoolOfPendingWork() 101 | { 102 | ThreadPool.UnsafeQueueUserWorkItem(_ => 103 | { 104 | // Note that the current thread is now processing work items. 105 | // This is necessary to enable inlining of tasks into this thread. 106 | currentThreadIsProcessingItems = true; 107 | try 108 | { 109 | // Process all available items in the queue. 110 | while (true) 111 | { 112 | Task item; 113 | lock (tasks) 114 | { 115 | // When there are no more items to be processed, 116 | // note that we're done processing, and get out. 117 | if (tasks.Count == 0) 118 | { 119 | --delegatesQueuedOrRunning; 120 | break; 121 | } 122 | 123 | // Get the next item from the queue 124 | item = tasks.First.Value; 125 | tasks.RemoveFirst(); 126 | } 127 | 128 | // Execute the task we pulled out of the queue 129 | TryExecuteTask(item); 130 | } 131 | } 132 | // We're done processing items on the current thread 133 | finally 134 | { 135 | currentThreadIsProcessingItems = false; 136 | } 137 | }, null); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sharp7.Rx/CacheVariableNameParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Sharp7.Rx.Interfaces; 3 | 4 | namespace Sharp7.Rx; 5 | 6 | internal class CacheVariableNameParser : IVariableNameParser 7 | { 8 | private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); 9 | 10 | private readonly IVariableNameParser inner; 11 | 12 | public CacheVariableNameParser(IVariableNameParser inner) 13 | { 14 | this.inner = inner; 15 | } 16 | 17 | public VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); 18 | } 19 | -------------------------------------------------------------------------------- /Sharp7.Rx/Enums/ConnectionState.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx.Enums; 2 | 3 | public enum ConnectionState 4 | { 5 | Initial, 6 | Connected, 7 | DisconnectedByUser, 8 | ConnectionLost, 9 | Disposed 10 | } 11 | -------------------------------------------------------------------------------- /Sharp7.Rx/Enums/DbType.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx.Enums; 2 | 3 | // see https://support.industry.siemens.com/cs/mdm/109747174?c=88343664523&lc=de-DE 4 | internal enum DbType 5 | { 6 | Bit, 7 | 8 | /// 9 | /// ASCII string 10 | /// 11 | String, 12 | 13 | /// 14 | /// UTF16 string 15 | /// 16 | WString, 17 | 18 | Byte, 19 | 20 | /// 21 | /// Int16 22 | /// 23 | Int, 24 | 25 | /// 26 | /// UInt16 27 | /// 28 | UInt, 29 | 30 | /// 31 | /// Int32 32 | /// 33 | DInt, 34 | 35 | /// 36 | /// UInt32 37 | /// 38 | UDInt, 39 | 40 | /// 41 | /// Int64 42 | /// 43 | LInt, 44 | 45 | /// 46 | /// UInt64 47 | /// 48 | ULInt, 49 | 50 | Single, 51 | Double, 52 | } 53 | -------------------------------------------------------------------------------- /Sharp7.Rx/Enums/Operand.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx.Enums; 2 | 3 | internal enum Operand : byte 4 | { 5 | Input = 69, 6 | Output = 65, 7 | Marker = 77, 8 | Db = 68, 9 | } 10 | -------------------------------------------------------------------------------- /Sharp7.Rx/Enums/TransmissionMode.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx.Enums; 2 | 3 | public enum TransmissionMode 4 | { 5 | Cyclic = 3, 6 | OnChange = 4, 7 | } 8 | -------------------------------------------------------------------------------- /Sharp7.Rx/Exceptions/S7Exception.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx; 2 | 3 | public abstract class S7Exception : Exception 4 | { 5 | protected S7Exception(string message) : base(message) 6 | { 7 | } 8 | 9 | protected S7Exception(string message, Exception innerException) : base(message, innerException) 10 | { 11 | } 12 | } 13 | 14 | public class S7CommunicationException : S7Exception 15 | { 16 | public S7CommunicationException(string message, int s7ErrorCode, string s7ErrorText) : base(message) 17 | { 18 | S7ErrorCode = s7ErrorCode; 19 | S7ErrorText = s7ErrorText; 20 | } 21 | 22 | public S7CommunicationException(string message, Exception innerException, int s7ErrorCode, string s7ErrorText) : base(message, innerException) 23 | { 24 | S7ErrorCode = s7ErrorCode; 25 | S7ErrorText = s7ErrorText; 26 | } 27 | 28 | public int S7ErrorCode { get; } 29 | public string S7ErrorText { get; } 30 | } 31 | 32 | public class DataTypeMissmatchException : S7Exception 33 | { 34 | internal DataTypeMissmatchException(string message, Type type, VariableAddress address) : base(message) 35 | { 36 | Type = type; 37 | Address = address.ToString(); 38 | } 39 | 40 | internal DataTypeMissmatchException(string message, Exception innerException, Type type, VariableAddress address) : base(message, innerException) 41 | { 42 | Type = type; 43 | Address = address.ToString(); 44 | } 45 | 46 | public string Address { get; } 47 | 48 | public Type Type { get; } 49 | } 50 | 51 | public class UnsupportedS7TypeException : S7Exception 52 | { 53 | internal UnsupportedS7TypeException(string message, Type type, VariableAddress address) : base(message) 54 | { 55 | Type = type; 56 | Address = address.ToString(); 57 | } 58 | 59 | internal UnsupportedS7TypeException(string message, Exception innerException, Type type, VariableAddress address) : base(message, innerException) 60 | { 61 | Type = type; 62 | Address = address.ToString(); 63 | } 64 | 65 | public string Address { get; } 66 | 67 | public Type Type { get; } 68 | } 69 | 70 | public class InvalidS7AddressException : S7Exception 71 | { 72 | public InvalidS7AddressException(string message, string input) : base(message) 73 | { 74 | Input = input; 75 | } 76 | 77 | public InvalidS7AddressException(string message, Exception innerException, string input) : base(message, innerException) 78 | { 79 | Input = input; 80 | } 81 | 82 | public string Input { get; } 83 | } 84 | -------------------------------------------------------------------------------- /Sharp7.Rx/Extensions/DisposableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Disposables; 2 | 3 | namespace Sharp7.Rx.Extensions; 4 | 5 | internal static class DisposableExtensions 6 | { 7 | public static void AddDisposableTo(this IDisposable disposable, CompositeDisposable compositeDisposable) 8 | { 9 | compositeDisposable.Add(disposable); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sharp7.Rx/Extensions/ObservableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Concurrency; 2 | using System.Reactive.Disposables; 3 | using System.Reactive.Linq; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Sharp7.Rx.Extensions; 7 | 8 | internal static class ObservableExtensions 9 | { 10 | public static IObservable DisposeMany(this IObservable source) 11 | { 12 | return Observable.Create(obs => 13 | { 14 | var serialDisposable = new SerialDisposable(); 15 | var subscription = 16 | source.Subscribe( 17 | item => 18 | { 19 | serialDisposable.Disposable = item as IDisposable; 20 | obs.OnNext(item); 21 | }, 22 | obs.OnError, 23 | obs.OnCompleted); 24 | return new CompositeDisposable(serialDisposable, subscription); 25 | }); 26 | } 27 | 28 | public static IObservable LogAndRetry(this IObservable source, ILogger logger, string message) 29 | { 30 | return source 31 | .Do( 32 | _ => { }, 33 | ex => logger?.LogError(ex, message)) 34 | .Retry(); 35 | } 36 | 37 | public static IObservable LogAndRetryAfterDelay( 38 | this IObservable source, 39 | ILogger logger, 40 | TimeSpan retryDelay, 41 | string message, 42 | int retryCount = -1, 43 | IScheduler scheduler = null) 44 | { 45 | var sourceLogged = 46 | source 47 | .Do( 48 | _ => { }, 49 | ex => logger?.LogError(ex, message)); 50 | 51 | return RetryAfterDelay(sourceLogged, retryDelay, retryCount, scheduler); 52 | } 53 | 54 | public static IObservable RepeatAfterDelay( 55 | this IObservable source, 56 | TimeSpan retryDelay, 57 | int repeatCount = -1, 58 | IScheduler scheduler = null) 59 | { 60 | return RedoAfterDelay(source, retryDelay, repeatCount, scheduler, Observable.Repeat, Observable.Repeat); 61 | } 62 | 63 | public static IObservable RetryAfterDelay( 64 | this IObservable source, 65 | TimeSpan retryDelay, 66 | int retryCount = -1, 67 | IScheduler scheduler = null) 68 | { 69 | return RedoAfterDelay(source, retryDelay, retryCount, scheduler, Observable.Retry, Observable.Retry); 70 | } 71 | 72 | private static IObservable RedoAfterDelay(IObservable source, TimeSpan retryDelay, int retryCount, IScheduler scheduler, Func, IObservable> reDo, 73 | Func, int, IObservable> reDoCount) 74 | { 75 | scheduler = scheduler ?? TaskPoolScheduler.Default; 76 | var attempt = 0; 77 | 78 | var deferedObs = Observable.Defer(() => ((++attempt == 1) ? source : source.DelaySubscription(retryDelay, scheduler))); 79 | return retryCount > 0 ? reDoCount(deferedObs, retryCount) : reDo(deferedObs); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sharp7.Rx/Extensions/OperandExtensions.cs: -------------------------------------------------------------------------------- 1 | using Sharp7.Rx.Enums; 2 | 3 | namespace Sharp7.Rx.Extensions; 4 | 5 | internal static class OperandExtensions 6 | { 7 | public static S7Area ToArea(this Operand operand) => 8 | operand switch 9 | { 10 | Operand.Input => S7Area.PE, 11 | Operand.Output => S7Area.PA, 12 | Operand.Marker => S7Area.MK, 13 | Operand.Db => S7Area.DB, 14 | _ => throw new ArgumentOutOfRangeException(nameof(operand), operand, null) 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /Sharp7.Rx/Extensions/PlcExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive; 2 | using System.Reactive.Disposables; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Threading.Tasks; 5 | using Sharp7.Rx.Enums; 6 | using Sharp7.Rx.Interfaces; 7 | 8 | namespace Sharp7.Rx.Extensions; 9 | 10 | public static class PlcExtensions 11 | { 12 | public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, 13 | bool initialTransfer) 14 | { 15 | return Observable.Create(async observer => 16 | { 17 | var subscriptions = new CompositeDisposable(); 18 | 19 | var notification = plc 20 | .CreateNotification(triggerAddress, TransmissionMode.OnChange) 21 | .Publish() 22 | .RefCount(); 23 | 24 | if (initialTransfer) 25 | { 26 | await plc.ConnectionState.FirstAsync(state => state == ConnectionState.Connected).ToTask(); 27 | var initialValue = await ReadData(plc, readData); 28 | observer.OnNext(initialValue); 29 | } 30 | 31 | notification 32 | .Where(trigger => trigger) 33 | .SelectMany(_ => ReadDataAndAcknowlodge(plc, readData, ackTriggerAddress)) 34 | .Subscribe(observer) 35 | .AddDisposableTo(subscriptions); 36 | 37 | notification 38 | .Where(trigger => !trigger) 39 | .SelectMany(async _ => 40 | { 41 | await plc.SetValue(ackTriggerAddress, false); 42 | return Unit.Default; 43 | }) 44 | .Subscribe() 45 | .AddDisposableTo(subscriptions); 46 | 47 | return subscriptions; 48 | }); 49 | } 50 | 51 | public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData) 52 | { 53 | return CreateDatatransferWithHandshake(plc, triggerAddress, ackTriggerAddress, readData, false); 54 | } 55 | 56 | private static async Task ReadData(IPlc plc, Func> receiveData) 57 | { 58 | return await receiveData(plc); 59 | } 60 | 61 | private static async Task ReadDataAndAcknowlodge(IPlc plc, Func> readData, string ackTriggerAddress) 62 | { 63 | try 64 | { 65 | return await ReadData(plc, readData); 66 | } 67 | finally 68 | { 69 | await plc.SetValue(ackTriggerAddress, true); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sharp7.Rx/Extensions/S7VariableExtensions.cs: -------------------------------------------------------------------------------- 1 | using Sharp7.Rx.Enums; 2 | 3 | namespace Sharp7.Rx.Extensions; 4 | 5 | internal static class VariableAddressExtensions 6 | { 7 | private static readonly Dictionary> supportedTypeMap = new() 8 | { 9 | {typeof(bool), a => a.Type == DbType.Bit}, 10 | {typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte}, 11 | {typeof(byte), a => a.Type == DbType.Byte && a.Length == 1}, 12 | {typeof(short), a => a.Type == DbType.Int}, 13 | {typeof(ushort), a => a.Type == DbType.UInt}, 14 | {typeof(int), a => a.Type == DbType.DInt}, 15 | {typeof(uint), a => a.Type == DbType.UDInt}, 16 | {typeof(long), a => a.Type == DbType.LInt}, 17 | {typeof(ulong), a => a.Type == DbType.ULInt}, 18 | {typeof(float), a => a.Type == DbType.Single}, 19 | {typeof(double), a => a.Type == DbType.Double}, 20 | {typeof(byte[]), a => a.Type == DbType.Byte}, 21 | }; 22 | 23 | public static bool MatchesType(this VariableAddress address, Type type) => 24 | supportedTypeMap.TryGetValue(type, out var map) && map(address); 25 | 26 | public static Type GetClrType(this VariableAddress address) => 27 | address.Type switch 28 | { 29 | DbType.Bit => typeof(bool), 30 | DbType.String => typeof(string), 31 | DbType.WString => typeof(string), 32 | DbType.Byte => address.Length == 1 ? typeof(byte) : typeof(byte[]), 33 | DbType.Int => typeof(short), 34 | DbType.UInt => typeof(ushort), 35 | DbType.DInt => typeof(int), 36 | DbType.UDInt => typeof(uint), 37 | DbType.LInt => typeof(long), 38 | DbType.ULInt => typeof(ulong), 39 | DbType.Single => typeof(float), 40 | DbType.Double => typeof(double), 41 | _ => throw new ArgumentOutOfRangeException(nameof(address)) 42 | }; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sharp7.Rx/Interfaces/IPlc.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Sharp7.Rx.Enums; 3 | 4 | namespace Sharp7.Rx.Interfaces; 5 | 6 | [NoReorder] 7 | public interface IPlc : IDisposable 8 | { 9 | IObservable ConnectionState { get; } 10 | 11 | Task SetValue(string variableName, TValue value, CancellationToken token = default); 12 | 13 | Task GetValue(string variableName, CancellationToken token = default); 14 | Task GetValue(string variableName, CancellationToken token = default); 15 | 16 | IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); 17 | IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); 18 | } 19 | -------------------------------------------------------------------------------- /Sharp7.Rx/Interfaces/IVariableNameParser.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | namespace Sharp7.Rx.Interfaces; 3 | 4 | internal interface IVariableNameParser 5 | { 6 | VariableAddress Parse(string input); 7 | } 8 | -------------------------------------------------------------------------------- /Sharp7.Rx/S7ErrorCodes.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace Sharp7.Rx; 4 | 5 | public static class S7ErrorCodes 6 | { 7 | /// 8 | /// This list is not exhaustive and should be considered work in progress. 9 | /// 10 | private static readonly HashSet notDisconnectedErrorCodes = 11 | [ 12 | 0x000000, // OK 13 | 0xC00000, // CPU: Item not available 14 | 0x900000 // CPU: Address out of range 15 | ]; 16 | 17 | private static readonly IReadOnlyDictionary additionalErrorTexts = new Dictionary 18 | { 19 | {0xC00000, "This happens when the DB does not exist."}, 20 | {0x900000, "This happens when the DB is not long enough."}, 21 | { 22 | 0x40000, """ 23 | This can happen when the cpu MPI address or rack is wrong, the DB is "optimized", or "PUT/GET communication" is not enabled. 24 | See https://snap7.sourceforge.net/snap7_client.html#target_compatibility. 25 | """ 26 | } 27 | }; 28 | 29 | /// 30 | /// Some error codes indicate connection lost, in which case, the driver tries to reestablish connection. 31 | /// Other error codes indicate a user error, like reading from an unavailable DB or exceeding 32 | /// the DBs range. In this case the driver should not consider the connection to be lost. 33 | /// 34 | public static bool AssumeConnectionLost(int errorCode) => 35 | !notDisconnectedErrorCodes.Contains(errorCode); 36 | 37 | public static string? GetAdditionalErrorText(int errorCode) => 38 | additionalErrorTexts.GetValueOrDefault(errorCode); 39 | } 40 | -------------------------------------------------------------------------------- /Sharp7.Rx/Settings/PlcConnectionSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Sharp7.Rx.Settings; 2 | 3 | internal class PlcConnectionSettings 4 | { 5 | public int CpuMpiAddress { get; set; } 6 | public string IpAddress { get; set; } 7 | public int Port { get; set; } 8 | public int RackNumber { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /Sharp7.Rx/Sharp7.Rx.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 12.0 7 | disable 8 | enable 9 | latest-Recommended 10 | 11 | true 12 | true 13 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 14 | 15 | evopro system engineering AG 16 | evopro system engineering AG 17 | Reactive framework for Sharp7, the Ethernet S7 PLC communication suite. Handling RFC1006 connections to Siemens S7 300, 1200 and 1500. 18 | linqpad-samples 19 | https://github.com/evopro-ag/Sharp7Reactive 20 | Apache-2.0 21 | true 22 | snupkg 23 | 24 | 30 | $(NoWarn);CA1848;CA2254;CA1859;CS1591 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | true 48 | linqpad-samples\;content 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Sharp7.Rx/Sharp7.Rx.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /Sharp7.Rx/Sharp7Connector.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Disposables; 2 | using System.Reactive.Linq; 3 | using System.Reactive.Subjects; 4 | using Microsoft.Extensions.Logging; 5 | using Sharp7.Rx.Basics; 6 | using Sharp7.Rx.Enums; 7 | using Sharp7.Rx.Extensions; 8 | using Sharp7.Rx.Interfaces; 9 | using Sharp7.Rx.Settings; 10 | 11 | namespace Sharp7.Rx; 12 | 13 | internal class Sharp7Connector: IDisposable 14 | { 15 | private readonly BehaviorSubject connectionStateSubject = new(Enums.ConnectionState.Initial); 16 | private readonly int cpuSlotNr; 17 | 18 | private readonly CompositeDisposable disposables = new(); 19 | private readonly string ipAddress; 20 | private readonly int port; 21 | private readonly int rackNr; 22 | private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1); 23 | private readonly IVariableNameParser variableNameParser; 24 | private bool disposed; 25 | 26 | private S7Client sharp7; 27 | 28 | 29 | public Sharp7Connector(PlcConnectionSettings settings, IVariableNameParser variableNameParser) 30 | { 31 | this.variableNameParser = variableNameParser; 32 | ipAddress = settings.IpAddress; 33 | cpuSlotNr = settings.CpuMpiAddress; 34 | port = settings.Port; 35 | rackNr = settings.RackNumber; 36 | 37 | ReconnectDelay = TimeSpan.FromSeconds(5); 38 | } 39 | 40 | public IObservable ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable(); 41 | 42 | public ILogger Logger { get; set; } 43 | 44 | public TimeSpan ReconnectDelay { get; set; } 45 | 46 | private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected; 47 | 48 | public void Dispose() 49 | { 50 | Dispose(true); 51 | GC.SuppressFinalize(this); 52 | } 53 | 54 | public async Task Connect() 55 | { 56 | if (sharp7 == null) 57 | throw new InvalidOperationException("S7 driver is not initialized."); 58 | 59 | try 60 | { 61 | var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler); 62 | if (errorCode == 0) 63 | { 64 | connectionStateSubject.OnNext(Enums.ConnectionState.Connected); 65 | return true; 66 | } 67 | else 68 | { 69 | var errorText = EvaluateErrorCode(errorCode); 70 | Logger.LogError("Failed to establish initial connection: {Error}", errorText); 71 | } 72 | } 73 | catch (Exception ex) 74 | { 75 | connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); 76 | Logger.LogError(ex, "Failed to establish initial connection."); 77 | } 78 | 79 | return false; 80 | } 81 | 82 | 83 | public async Task Disconnect() 84 | { 85 | connectionStateSubject.OnNext(Enums.ConnectionState.DisconnectedByUser); 86 | await CloseConnection(); 87 | } 88 | 89 | public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) 90 | { 91 | if (variableNames.IsEmpty()) 92 | return new Dictionary(); 93 | 94 | var s7MultiVar = new S7MultiVar(sharp7); 95 | 96 | var buffers = variableNames 97 | .Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)}) 98 | .Select(x => 99 | { 100 | var buffer = new byte[x.Address.BufferLength]; 101 | #pragma warning disable CS0618 // Type or member is obsolete, no matching overload. 102 | s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNo, x.Address.Start, x.Address.BufferLength, ref buffer); 103 | #pragma warning restore CS0618 104 | return new {x.VariableName, Buffer = buffer}; 105 | }) 106 | .ToArray(); 107 | 108 | var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); 109 | 110 | EnsureSuccessOrThrow(result, $"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); 111 | 112 | return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); 113 | } 114 | 115 | public Task InitializeAsync() 116 | { 117 | try 118 | { 119 | sharp7 = new S7Client(); 120 | sharp7.PLCPort = port; 121 | 122 | var subscription = 123 | ConnectionState 124 | .Where(state => state == Enums.ConnectionState.ConnectionLost) 125 | .Take(1) 126 | .SelectMany(_ => Reconnect()) 127 | .RepeatAfterDelay(ReconnectDelay) 128 | .LogAndRetry(Logger, "Error while reconnecting to S7.") 129 | .Subscribe(); 130 | 131 | disposables.Add(subscription); 132 | } 133 | catch (Exception ex) 134 | { 135 | Logger?.LogError(ex, "S7 driver could not be initialized"); 136 | } 137 | 138 | return Task.FromResult(true); 139 | } 140 | 141 | public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token) 142 | { 143 | EnsureConnectionValid(); 144 | 145 | var buffer = new byte[bytesToRead]; 146 | 147 | 148 | var result = 149 | await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dbNo, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); 150 | token.ThrowIfCancellationRequested(); 151 | 152 | EnsureSuccessOrThrow(result, $"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead}"); 153 | 154 | return buffer; 155 | } 156 | 157 | public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token) 158 | { 159 | EnsureConnectionValid(); 160 | 161 | var buffer = new[] {value ? (byte) 0xff : (byte) 0}; 162 | 163 | var offsetStart = (startByteAddress * 8) + bitAdress; 164 | 165 | var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler); 166 | token.ThrowIfCancellationRequested(); 167 | 168 | EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress}"); 169 | } 170 | 171 | public async Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, ushort bytesToWrite, CancellationToken token) 172 | { 173 | EnsureConnectionValid(); 174 | 175 | var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, startByteAddress, bytesToWrite, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); 176 | token.ThrowIfCancellationRequested(); 177 | 178 | EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}"); 179 | } 180 | 181 | 182 | protected virtual void Dispose(bool disposing) 183 | { 184 | if (!disposed) 185 | { 186 | if (disposing) 187 | { 188 | disposables.Dispose(); 189 | 190 | if (sharp7 != null) 191 | { 192 | sharp7.Disconnect(); 193 | sharp7 = null; 194 | } 195 | 196 | connectionStateSubject?.OnNext(Enums.ConnectionState.Disposed); 197 | connectionStateSubject?.OnCompleted(); 198 | connectionStateSubject?.Dispose(); 199 | } 200 | 201 | disposed = true; 202 | } 203 | } 204 | 205 | private async Task CloseConnection() 206 | { 207 | if (sharp7 == null) 208 | throw new InvalidOperationException("S7 driver is not initialized."); 209 | 210 | await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); 211 | } 212 | 213 | private void EnsureConnectionValid() 214 | { 215 | if (disposed) 216 | throw new ObjectDisposedException(nameof(Sharp7Connector)); 217 | 218 | if (sharp7 == null) 219 | throw new InvalidOperationException("S7 driver is not initialized."); 220 | 221 | if (!IsConnected) 222 | throw new InvalidOperationException("Plc is not connected"); 223 | } 224 | 225 | private void EnsureSuccessOrThrow(int result, string message) 226 | { 227 | if (result == 0) return; 228 | 229 | var errorText = EvaluateErrorCode(result); 230 | var completeMessage = $"{message}: {errorText}"; 231 | 232 | var additionalErrorText = S7ErrorCodes.GetAdditionalErrorText(result); 233 | if (additionalErrorText != null) 234 | completeMessage += Environment.NewLine + additionalErrorText; 235 | 236 | throw new S7CommunicationException(completeMessage, result, errorText); 237 | } 238 | 239 | private string EvaluateErrorCode(int errorCode) 240 | { 241 | if (errorCode == 0) 242 | return null; 243 | 244 | if (sharp7 == null) 245 | throw new InvalidOperationException("S7 driver is not initialized."); 246 | 247 | var errorText = $"0x{errorCode:X}, {sharp7.ErrorText(errorCode)}"; 248 | Logger?.LogError($"S7 Error {errorText}"); 249 | 250 | if (S7ErrorCodes.AssumeConnectionLost(errorCode)) 251 | SetConnectionLostState(); 252 | 253 | return errorText; 254 | } 255 | 256 | private async Task Reconnect() 257 | { 258 | await CloseConnection(); 259 | 260 | return await Connect(); 261 | } 262 | 263 | private void SetConnectionLostState() 264 | { 265 | if (connectionStateSubject.Value == Enums.ConnectionState.ConnectionLost) return; 266 | 267 | connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); 268 | } 269 | 270 | ~Sharp7Connector() 271 | { 272 | Dispose(false); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /Sharp7.Rx/Sharp7Plc.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Diagnostics; 3 | using System.Reactive; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Threading.Tasks; 7 | using System.Reflection; 8 | using Microsoft.Extensions.Logging; 9 | using Sharp7.Rx.Basics; 10 | using Sharp7.Rx.Enums; 11 | using Sharp7.Rx.Extensions; 12 | using Sharp7.Rx.Interfaces; 13 | using Sharp7.Rx.Settings; 14 | using Sharp7.Rx.Utils; 15 | 16 | namespace Sharp7.Rx; 17 | 18 | public class Sharp7Plc : IPlc 19 | { 20 | private static readonly ArrayPool arrayPool = ArrayPool.Shared; 21 | 22 | private static readonly MethodInfo getValueMethod = typeof(Sharp7Plc).GetMethods() 23 | .Single(m => m.Name == nameof(GetValue) && m.GetGenericArguments().Length == 1); 24 | 25 | private static readonly MethodInfo createNotificationMethod = typeof(Sharp7Plc).GetMethods() 26 | .Single(m => m.Name == nameof(CreateNotification) && m.GetGenericArguments().Length == 1); 27 | 28 | private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); 29 | private readonly List performanceCounter = new(1000); 30 | private readonly PlcConnectionSettings plcConnectionSettings; 31 | private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser()); 32 | private bool disposed; 33 | private int initialized; 34 | 35 | private IDisposable notificationSubscription; 36 | private Sharp7Connector s7Connector; 37 | 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// Polling interval for multi variable read from PLC. 47 | /// 48 | /// 49 | /// This is the wait time between two successive reads from PLC and determines the 50 | /// time resolution for all variable reads related with CreateNotification. 51 | /// 52 | /// 53 | /// Default is 100 ms. The minimum supported time is 5 ms. 54 | /// 55 | /// 56 | public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null) 57 | { 58 | plcConnectionSettings = new PlcConnectionSettings {IpAddress = ipAddress, RackNumber = rackNumber, CpuMpiAddress = cpuMpiAddress, Port = port}; 59 | s7Connector = new Sharp7Connector(plcConnectionSettings, variableNameParser); 60 | ConnectionState = s7Connector.ConnectionState; 61 | 62 | if (multiVarRequestCycleTime != null) 63 | { 64 | if (multiVarRequestCycleTime < TimeSpan.FromMilliseconds(5)) 65 | MultiVarRequestCycleTime = TimeSpan.FromMilliseconds(5); 66 | else 67 | MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; 68 | } 69 | } 70 | 71 | public IObservable ConnectionState { get; } 72 | 73 | public ILogger Logger 74 | { 75 | get => s7Connector.Logger; 76 | set => s7Connector.Logger = value; 77 | } 78 | 79 | public TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1); 80 | 81 | public int MultiVarRequestMaxItems { get; set; } = 16; 82 | 83 | public void Dispose() 84 | { 85 | Dispose(true); 86 | GC.SuppressFinalize(this); 87 | } 88 | 89 | /// 90 | /// Create an Observable for a given variable. Multiple notifications are automatically combined into a multi-variable subscription to 91 | /// reduce network trafic and PLC workload. 92 | /// 93 | /// 94 | /// 95 | /// 96 | /// 97 | public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) 98 | { 99 | return Observable.Create(observer => 100 | { 101 | var address = ParseAndVerify(variableName, typeof(TValue)); 102 | 103 | var disp = new CompositeDisposable(); 104 | var disposableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); 105 | disposableContainer.AddDisposableTo(disp); 106 | 107 | var observable = 108 | // Read variable with GetValue first. 109 | // This will propagate any errors due to reading from invalid addresses. 110 | Observable.FromAsync(() => GetValue(variableName)) 111 | .Concat( 112 | disposableContainer.Observable 113 | .Select(bytes => ValueConverter.ReadFromBuffer(bytes, address)) 114 | ); 115 | 116 | if (transmissionMode == TransmissionMode.OnChange) 117 | observable = observable.DistinctUntilChanged(); 118 | 119 | observable.Subscribe(observer) 120 | .AddDisposableTo(disp); 121 | 122 | return disp; 123 | }); 124 | } 125 | 126 | /// 127 | /// Read PLC variable as generic variable. 128 | /// 129 | /// 130 | /// 131 | /// 132 | /// 133 | public async Task GetValue(string variableName, CancellationToken token = default) 134 | { 135 | var address = ParseAndVerify(variableName, typeof(TValue)); 136 | 137 | var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.BufferLength, address.DbNo, token); 138 | return ValueConverter.ReadFromBuffer(data, address); 139 | } 140 | 141 | /// 142 | /// Read PLC variable as object. 143 | /// The return type is automatically infered from the variable name. 144 | /// 145 | /// 146 | /// 147 | /// The actual return type is infered from the variable name. 148 | public async Task GetValue(string variableName, CancellationToken token = default) 149 | { 150 | var address = variableNameParser.Parse(variableName); 151 | var clrType = address.GetClrType(); 152 | 153 | var genericGetValue = getValueMethod!.MakeGenericMethod(clrType); 154 | 155 | var task = genericGetValue.Invoke(this, [variableName, token]) as Task; 156 | 157 | await task!; 158 | var taskType = typeof(Task<>).MakeGenericType(clrType); 159 | var propertyInfo = taskType.GetProperty(nameof(Task.Result)); 160 | var result = propertyInfo!.GetValue(task); 161 | 162 | return result; 163 | } 164 | 165 | /// 166 | /// Write value to the PLC. 167 | /// 168 | /// 169 | /// 170 | /// 171 | /// 172 | /// 173 | public async Task SetValue(string variableName, TValue value, CancellationToken token = default) 174 | { 175 | var address = ParseAndVerify(variableName, typeof(TValue)); 176 | 177 | if (typeof(TValue) == typeof(bool)) 178 | { 179 | // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would 180 | // overwrite other bits within this byte. 181 | 182 | await s7Connector.WriteBit(address.Operand, address.Start, address.Bit!.Value, (bool) (object) value, address.DbNo, token); 183 | } 184 | else 185 | { 186 | var buffer = arrayPool.Rent(address.BufferLength); 187 | try 188 | { 189 | ValueConverter.WriteToBuffer(buffer, value, address); 190 | 191 | await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNo, address.BufferLength, token); 192 | } 193 | finally 194 | { 195 | arrayPool.Return(buffer); 196 | } 197 | } 198 | } 199 | 200 | /// 201 | /// Creates an observable of object for a variable. 202 | /// The return type is automatically infered from the variable name. 203 | /// 204 | /// 205 | /// 206 | /// The return type is infered from the variable name. 207 | public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) 208 | { 209 | var address = variableNameParser.Parse(variableName); 210 | var clrType = address.GetClrType(); 211 | 212 | var genericCreateNotification = createNotificationMethod!.MakeGenericMethod(clrType); 213 | 214 | var genericNotification = genericCreateNotification.Invoke(this, [variableName, transmissionMode]); 215 | 216 | return SignatureConverter.ConvertToObjectObservable(genericNotification, clrType); 217 | } 218 | 219 | /// 220 | /// Trigger PLC connection and start notification loop. 221 | /// 222 | /// This method returns immediately and does not wait for the connection to be established. 223 | /// 224 | /// 225 | /// Always true 226 | [Obsolete($"Use {nameof(InitializeConnection)} or {nameof(TriggerConnection)}.")] 227 | public async Task InitializeAsync() 228 | { 229 | await TriggerConnection(); 230 | return true; 231 | } 232 | 233 | 234 | /// 235 | /// Initialize PLC connection and wait for connection to be established. 236 | /// 237 | /// 238 | /// 239 | public async Task InitializeConnection(CancellationToken token = default) => await DoInitializeConnection(true, token); 240 | 241 | /// 242 | /// Initialize PLC and trigger connection. This method will not wait for the connection to be established. 243 | /// 244 | /// 245 | /// 246 | public async Task TriggerConnection(CancellationToken token = default) => await DoInitializeConnection(false, token); 247 | 248 | protected virtual void Dispose(bool disposing) 249 | { 250 | if (disposed) return; 251 | disposed = true; 252 | 253 | if (disposing) 254 | { 255 | notificationSubscription?.Dispose(); 256 | notificationSubscription = null; 257 | 258 | if (s7Connector != null) 259 | { 260 | s7Connector.Disconnect().Wait(); 261 | s7Connector.Dispose(); 262 | s7Connector = null; 263 | } 264 | 265 | multiVariableSubscriptions.Dispose(); 266 | } 267 | } 268 | 269 | private async Task DoInitializeConnection(bool waitForConnection, CancellationToken token) 270 | { 271 | if (Interlocked.Exchange(ref initialized, 1) == 1) return; 272 | 273 | await s7Connector.InitializeAsync(); 274 | 275 | // Triger connection. 276 | // The initial connection might fail. In this case a reconnect is initiated. 277 | // So we ignore any errors and wait for ConnectionState Connected afterward. 278 | _ = Task.Run(async () => 279 | { 280 | try 281 | { 282 | await s7Connector.Connect(); 283 | } 284 | catch (Exception e) 285 | { 286 | Logger?.LogError(e, "Intiial PLC connection failed."); 287 | } 288 | }, token); 289 | 290 | if (waitForConnection) 291 | await s7Connector.ConnectionState 292 | .FirstAsync(c => c == Enums.ConnectionState.Connected) 293 | .ToTask(token); 294 | 295 | StartNotificationLoop(); 296 | } 297 | 298 | private async Task GetAllValues(Sharp7Connector connector) 299 | { 300 | if (multiVariableSubscriptions.ExistingKeys.IsEmpty()) 301 | return Unit.Default; 302 | 303 | var stopWatch = Stopwatch.StartNew(); 304 | foreach (var partsOfMultiVarRequest in multiVariableSubscriptions.ExistingKeys.Buffer(MultiVarRequestMaxItems)) 305 | { 306 | var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList); 307 | 308 | foreach (var pair in multiVarRequest) 309 | if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject)) 310 | subject.OnNext(pair.Value); 311 | } 312 | 313 | stopWatch.Stop(); 314 | performanceCounter.Add(stopWatch.ElapsedMilliseconds); 315 | 316 | PrintAndResetPerformanceStatistik(); 317 | 318 | return Unit.Default; 319 | } 320 | 321 | private VariableAddress ParseAndVerify(string variableName, Type type) 322 | { 323 | var address = variableNameParser.Parse(variableName); 324 | if (!address.MatchesType(type)) 325 | throw new DataTypeMissmatchException($"Address \"{variableName}\" does not match type {type}.", type, address); 326 | 327 | return address; 328 | } 329 | 330 | private void PrintAndResetPerformanceStatistik() 331 | { 332 | if (performanceCounter.Count == performanceCounter.Capacity) 333 | { 334 | var average = performanceCounter.Average(); 335 | var min = performanceCounter.Min(); 336 | var max = performanceCounter.Max(); 337 | 338 | Logger?.LogTrace("PLC {Plc} notification perf: {Elements} calls, min {Min}, max {Max}, avg {Avg}, variables {Vars}, batch size {BatchSize}", 339 | plcConnectionSettings.IpAddress, 340 | performanceCounter.Capacity, min, max, average, 341 | multiVariableSubscriptions.ExistingKeys.Count(), 342 | MultiVarRequestMaxItems); 343 | performanceCounter.Clear(); 344 | } 345 | } 346 | 347 | private void StartNotificationLoop() 348 | { 349 | if (notificationSubscription != null) 350 | // notification loop already running 351 | return; 352 | 353 | var subscription = 354 | ConnectionState 355 | .FirstAsync(states => states == Enums.ConnectionState.Connected) 356 | .SelectMany(_ => GetAllValues(s7Connector)) 357 | .RepeatAfterDelay(MultiVarRequestCycleTime) 358 | .LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, "Error while getting batch notifications from plc") 359 | .Subscribe(); 360 | 361 | if (Interlocked.CompareExchange(ref notificationSubscription, subscription, null) != null) 362 | // Subscription has already been created (race condition). Dispose new subscription. 363 | subscription.Dispose(); 364 | } 365 | 366 | ~Sharp7Plc() 367 | { 368 | Dispose(false); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /Sharp7.Rx/Utils/SignatureConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using System.Reflection; 3 | 4 | namespace Sharp7.Rx.Utils; 5 | 6 | internal static class SignatureConverter 7 | { 8 | private static readonly MethodInfo convertToObjectObservableMethod = 9 | typeof(SignatureConverter) 10 | .GetMethods(BindingFlags.Public | BindingFlags.Static) 11 | .Single(m => m.Name == nameof(ConvertToObjectObservable) && m.GetGenericArguments().Length == 1); 12 | 13 | public static IObservable ConvertToObjectObservable(IObservable obs) => obs.Select(o => (object) o); 14 | 15 | public static IObservable ConvertToObjectObservable(object observable, Type sourceType) 16 | { 17 | var convertGeneric = convertToObjectObservableMethod.MakeGenericMethod(sourceType); 18 | 19 | return convertGeneric.Invoke(null, [observable]) as IObservable; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sharp7.Rx/ValueConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Text; 3 | using Sharp7.Rx.Enums; 4 | 5 | namespace Sharp7.Rx; 6 | 7 | internal static class ValueConverter 8 | { 9 | private static readonly Dictionary writeFunctions = new() 10 | { 11 | { 12 | typeof(bool), (data, address, value) => 13 | { 14 | var byteValue = (bool) value ? (byte) 1 : (byte) 0; 15 | var shifted = (byte) (byteValue << address.Bit!); 16 | data[0] = shifted; 17 | } 18 | }, 19 | 20 | {typeof(byte), (data, _, value) => data[0] = (byte) value}, 21 | { 22 | typeof(byte[]), (data, address, value) => 23 | { 24 | var source = (byte[]) value; 25 | 26 | var length = Math.Min(Math.Min(source.Length, data.Length), address.Length); 27 | 28 | source.AsSpan(0, length).CopyTo(data); 29 | } 30 | }, 31 | 32 | {typeof(short), (data, _, value) => BinaryPrimitives.WriteInt16BigEndian(data, (short) value)}, 33 | {typeof(ushort), (data, _, value) => BinaryPrimitives.WriteUInt16BigEndian(data, (ushort) value)}, 34 | {typeof(int), (data, _, value) => BinaryPrimitives.WriteInt32BigEndian(data, (int) value)}, 35 | {typeof(uint), (data, _, value) => BinaryPrimitives.WriteUInt32BigEndian(data, (uint) value)}, 36 | {typeof(long), (data, _, value) => BinaryPrimitives.WriteInt64BigEndian(data, (long) value)}, 37 | {typeof(ulong), (data, _, value) => BinaryPrimitives.WriteUInt64BigEndian(data, (ulong) value)}, 38 | 39 | {typeof(float), (data, _, value) => BinaryPrimitives.WriteSingleBigEndian(data, (float) value)}, 40 | {typeof(double), (data, _, value) => BinaryPrimitives.WriteDoubleBigEndian(data, (double) value)}, 41 | 42 | { 43 | typeof(string), (data, address, value) => 44 | { 45 | if (value is not string stringValue) throw new ArgumentException("Value must be of type string", nameof(value)); 46 | 47 | 48 | switch (address.Type) 49 | { 50 | case DbType.String: 51 | EncodeString(data); 52 | return; 53 | case DbType.WString: 54 | EncodeWString(data); 55 | return; 56 | case DbType.Byte: 57 | 58 | var readOnlySpan = stringValue.AsSpan(0, Math.Min(address.Length, stringValue.Length)); 59 | Encoding.ASCII.GetBytes(readOnlySpan, data); 60 | return; 61 | default: 62 | throw new DataTypeMissmatchException($"Cannot write string to {address.Type}", typeof(string), address); 63 | } 64 | 65 | void EncodeString(Span span) 66 | { 67 | var encodedLength = Encoding.ASCII.GetByteCount(stringValue); 68 | var length = Math.Min(address.Length, encodedLength); 69 | 70 | span[0] = (byte) address.Length; 71 | span[1] = (byte) length; 72 | 73 | Encoding.ASCII.GetBytes(stringValue.AsSpan(0, length), span[2..]); 74 | } 75 | 76 | void EncodeWString(Span span) 77 | { 78 | var length = Math.Min(address.Length, stringValue.Length); 79 | 80 | BinaryPrimitives.WriteUInt16BigEndian(span, address.Length); 81 | BinaryPrimitives.WriteUInt16BigEndian(span[2..], (ushort) length); 82 | 83 | var readOnlySpan = stringValue.AsSpan(0, length); 84 | Encoding.BigEndianUnicode.GetBytes(readOnlySpan, span[4..]); 85 | } 86 | } 87 | } 88 | }; 89 | 90 | private static readonly Dictionary readFunctions = new() 91 | { 92 | {typeof(bool), (buffer, address) => (buffer[0] >> address.Bit & 1) > 0}, 93 | 94 | {typeof(byte), (buffer, _) => buffer[0]}, 95 | {typeof(byte[]), (buffer, _) => buffer.ToArray()}, 96 | 97 | {typeof(short), (buffer, _) => BinaryPrimitives.ReadInt16BigEndian(buffer)}, 98 | {typeof(ushort), (buffer, _) => BinaryPrimitives.ReadUInt16BigEndian(buffer)}, 99 | {typeof(int), (buffer, _) => BinaryPrimitives.ReadInt32BigEndian(buffer)}, 100 | {typeof(uint), (buffer, _) => BinaryPrimitives.ReadUInt32BigEndian(buffer)}, 101 | {typeof(long), (buffer, _) => BinaryPrimitives.ReadInt64BigEndian(buffer)}, 102 | {typeof(ulong), (buffer, _) => BinaryPrimitives.ReadUInt64BigEndian(buffer)}, 103 | {typeof(float), (buffer, _) => BinaryPrimitives.ReadSingleBigEndian(buffer)}, 104 | {typeof(double), (buffer, _) => BinaryPrimitives.ReadDoubleBigEndian(buffer)}, 105 | 106 | { 107 | typeof(string), (buffer, address) => 108 | { 109 | return address.Type switch 110 | { 111 | DbType.String => ParseString(buffer), 112 | DbType.WString => ParseWString(buffer), 113 | DbType.Byte => Encoding.ASCII.GetString(buffer), 114 | _ => throw new DataTypeMissmatchException($"Cannot read string from {address.Type}", typeof(string), address) 115 | }; 116 | 117 | string ParseString(Span data) 118 | { 119 | // First byte is maximal length 120 | // Second byte is actual length 121 | // https://support.industry.siemens.com/cs/mdm/109747174?c=94063831435&lc=de-DE 122 | 123 | var length = Math.Min(address.Length, data[1]); 124 | 125 | return Encoding.ASCII.GetString(data.Slice(2, length)); 126 | } 127 | 128 | string ParseWString(Span data) 129 | { 130 | // First 2 bytes are maximal length 131 | // Second 2 bytes are actual length 132 | // https://support.industry.siemens.com/cs/mdm/109747174?c=94063855243&lc=de-DE 133 | 134 | // the length of the string is two bytes per character 135 | var statedStringLength = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(2, 2)); 136 | var length = Math.Min(address.Length, statedStringLength) * 2; 137 | 138 | return Encoding.BigEndianUnicode.GetString(data.Slice(4, length)); 139 | } 140 | } 141 | }, 142 | }; 143 | 144 | public static TValue ReadFromBuffer(Span buffer, VariableAddress address) 145 | { 146 | if (buffer.Length < address.BufferLength) 147 | throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); 148 | 149 | var type = typeof(TValue); 150 | 151 | if (!readFunctions.TryGetValue(type, out var readFunc)) 152 | throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); 153 | 154 | var result = readFunc(buffer, address); 155 | return (TValue) result; 156 | } 157 | 158 | public static void WriteToBuffer(Span buffer, TValue value, VariableAddress address) 159 | { 160 | if (buffer.Length < address.BufferLength) 161 | throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); 162 | 163 | var type = typeof(TValue); 164 | 165 | if (!writeFunctions.TryGetValue(type, out var writeFunc)) 166 | throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); 167 | 168 | writeFunc(buffer, address, value); 169 | } 170 | 171 | private delegate object ReadFunc(Span data, VariableAddress address); 172 | 173 | private delegate void WriteFunc(Span data, VariableAddress address, object value); 174 | } 175 | -------------------------------------------------------------------------------- /Sharp7.Rx/VariableAddress.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Sharp7.Rx.Enums; 3 | 4 | namespace Sharp7.Rx; 5 | 6 | [NoReorder] 7 | internal record VariableAddress(Operand Operand, ushort DbNo, DbType Type, ushort Start, ushort Length, byte? Bit = null) 8 | { 9 | public Operand Operand { get; } = Operand; 10 | public ushort DbNo { get; } = DbNo; 11 | public ushort Start { get; } = Start; 12 | public ushort Length { get; } = Length; 13 | public byte? Bit { get; } = Bit; 14 | public DbType Type { get; } = Type; 15 | 16 | public ushort BufferLength => Type switch 17 | { 18 | DbType.String => (ushort) (Length + 2), 19 | DbType.WString => (ushort) (Length * 2 + 4), 20 | _ => Length 21 | }; 22 | 23 | public override string ToString() => 24 | Type switch 25 | { 26 | DbType.Bit => $"{Operand}{DbNo}.{Type}{Start}.{Bit}", 27 | DbType.String => $"{Operand}{DbNo}.{Type}{Start}.{Length}", 28 | DbType.WString => $"{Operand}{DbNo}.{Type}{Start}.{Length}", 29 | DbType.Byte => Length == 1 ? $"{Operand}{DbNo}.{Type}{Start}" : $"{Operand}{DbNo}.{Type}{Start}.{Length}", 30 | _ => $"{Operand}{DbNo}.{Type}{Start}", 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /Sharp7.Rx/VariableNameParser.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System.Globalization; 3 | using System.Text.RegularExpressions; 4 | using Sharp7.Rx.Enums; 5 | using Sharp7.Rx.Interfaces; 6 | 7 | namespace Sharp7.Rx; 8 | 9 | internal class VariableNameParser : IVariableNameParser 10 | { 11 | private static readonly Regex regex = new(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", 12 | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); 13 | 14 | private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) 15 | { 16 | {"bit", DbType.Bit}, 17 | 18 | {"string", DbType.String}, 19 | {"wstring", DbType.WString}, 20 | 21 | {"byte", DbType.Byte}, 22 | {"int", DbType.Int}, 23 | {"uint", DbType.UInt}, 24 | {"dint", DbType.DInt}, 25 | {"udint", DbType.UDInt}, 26 | {"lint", DbType.LInt}, 27 | {"ulint", DbType.ULInt}, 28 | 29 | {"real", DbType.Single}, 30 | {"lreal", DbType.Double}, 31 | 32 | // S7 notation 33 | {"dbb", DbType.Byte}, 34 | {"dbw", DbType.Int}, 35 | {"dbx", DbType.Bit}, 36 | {"dbd", DbType.DInt}, 37 | 38 | // used for legacy compatability 39 | {"b", DbType.Byte}, 40 | {"d", DbType.Single}, 41 | {"dul", DbType.ULInt}, 42 | {"dulint", DbType.ULInt}, 43 | {"dulong", DbType.ULInt}, 44 | {"s", DbType.String}, 45 | {"w", DbType.Int}, 46 | {"x", DbType.Bit}, 47 | }; 48 | 49 | public VariableAddress Parse(string input) 50 | { 51 | ArgumentNullException.ThrowIfNull(input); 52 | 53 | var match = regex.Match(input); 54 | if (!match.Success) 55 | throw new InvalidS7AddressException($"Invalid S7 address \"{input}\". Expect format \"DB.(.)\".", input); 56 | 57 | var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); 58 | 59 | if (!ushort.TryParse(match.Groups["dbNo"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var dbNr)) 60 | throw new InvalidS7AddressException($"\"{match.Groups["dbNo"].Value}\" is an invalid DB number in \"{input}\"", input); 61 | 62 | if (!ushort.TryParse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var start)) 63 | throw new InvalidS7AddressException($"\"{match.Groups["start"].Value}\" is an invalid start bit in \"{input}\"", input); 64 | 65 | if (!types.TryGetValue(match.Groups["type"].Value, out var type)) 66 | throw new InvalidS7AddressException($"\"{match.Groups["type"].Value}\" is an invalid type in \"{input}\"", input); 67 | 68 | ushort length = type switch 69 | { 70 | DbType.Bit => 1, 71 | 72 | DbType.String => GetLength(), 73 | DbType.WString => GetLength(), 74 | 75 | DbType.Byte => GetLength(1), 76 | 77 | DbType.Int => 2, 78 | DbType.DInt => 4, 79 | DbType.ULInt => 8, 80 | DbType.UInt => 2, 81 | DbType.UDInt => 4, 82 | DbType.LInt => 8, 83 | 84 | DbType.Single => 4, 85 | DbType.Double => 8, 86 | _ => throw new ArgumentOutOfRangeException($"DbType {type} is not supported") 87 | }; 88 | 89 | switch (type) 90 | { 91 | case DbType.Bit: 92 | case DbType.String: 93 | case DbType.WString: 94 | case DbType.Byte: 95 | break; 96 | case DbType.Int: 97 | case DbType.UInt: 98 | case DbType.DInt: 99 | case DbType.UDInt: 100 | case DbType.LInt: 101 | case DbType.ULInt: 102 | case DbType.Single: 103 | case DbType.Double: 104 | default: 105 | if (match.Groups["bitOrLength"].Success) 106 | throw new InvalidS7AddressException($"{type} address must not have a length: \"{input}\"", input); 107 | break; 108 | } 109 | 110 | byte? bit = type == DbType.Bit ? GetBit() : null; 111 | 112 | 113 | var s7VariableAddress = new VariableAddress(Operand: operand, DbNo: dbNr, Type: type, Start: start, Length: length, Bit: bit); 114 | 115 | return s7VariableAddress; 116 | 117 | ushort GetLength(ushort? defaultValue = null) 118 | { 119 | if (!match.Groups["bitOrLength"].Success) 120 | { 121 | if (defaultValue.HasValue) 122 | return defaultValue.Value; 123 | throw new InvalidS7AddressException($"Variable of type {type} must have a length set. Example \"db12.byte10.3\", found \"{input}\"", input); 124 | } 125 | 126 | if (!ushort.TryParse(match.Groups["bitOrLength"].Value, out var result)) 127 | throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid length in \"{input}\"", input); 128 | 129 | return result; 130 | } 131 | 132 | byte GetBit() 133 | { 134 | if (!match.Groups["bitOrLength"].Success) 135 | throw new InvalidS7AddressException($"Variable of type {type} must have a bit number set. Example \"db12.bit10.3\", found \"{input}\"", input); 136 | 137 | if (!byte.TryParse(match.Groups["bitOrLength"].Value, out var result)) 138 | throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid bit number in \"{input}\"", input); 139 | 140 | if (result > 7) 141 | throw new InvalidS7AddressException($"Bit must be between 0 and 7 but is {result} in \"{input}\"", input); 142 | 143 | return result; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Sharp7.Rx/linqpad-samples/Create Notification.linq: -------------------------------------------------------------------------------- 1 | 2 | Sharp7.Rx 3 | Sharp7.Rx 4 | System.Reactive.Linq 5 | System.Reactive.Threading.Tasks 6 | System.Threading.Tasks 7 | 8 | 9 | var ip = "10.30.3.221"; // Set IP address of S7 10 | var db = 3; // Set to an existing DB 11 | 12 | // For rack number and cpu mpi address see 13 | // https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot 14 | var rackNumber = 0; 15 | var cpuMpiAddress = 0; 16 | 17 | using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); 18 | 19 | await plc.InitializeConnection(); 20 | 21 | "Connection established".Dump(); 22 | 23 | // create an IObservable 24 | var observable = plc.CreateNotification($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange); 25 | 26 | observable.Dump(); 27 | 28 | for (int i = 0; i < 10; i++) 29 | { 30 | await plc.SetValue($"DB{db}.Int6", (short)i); 31 | await Task.Delay(300); 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /Sharp7.Rx/linqpad-samples/Establish connection.linq: -------------------------------------------------------------------------------- 1 | 2 | Sharp7.Rx 3 | Sharp7.Rx 4 | System.Reactive.Linq 5 | System.Reactive.Threading.Tasks 6 | System.Threading.Tasks 7 | 8 | 9 | // Set IP address of S7 10 | var ip = "10.30.3.221"; 11 | 12 | // For rack number and cpu mpi address see 13 | // https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot 14 | var rackNumber = 0; 15 | var cpuMpiAddress = 0; 16 | 17 | // Create Sharp7Plc 18 | using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); 19 | 20 | // Initialize connection 21 | await plc.InitializeConnection(); 22 | 23 | // wait for connection to be established 24 | await plc.ConnectionState 25 | .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) 26 | .ToTask(); 27 | 28 | "Connection established".Dump(); 29 | 30 | try 31 | { 32 | await Task.Delay(Timeout.Infinite, this.QueryCancelToken); 33 | } 34 | catch (TaskCanceledException) 35 | { 36 | "Script stopped by user. Disconnecting by disposing plc.".Dump(); 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /Sharp7.Rx/linqpad-samples/FileOrder.txt: -------------------------------------------------------------------------------- 1 | Establish connection.linq 2 | Write and read value.linq 3 | Create Notification.linq 4 | Multiple notifications.linq -------------------------------------------------------------------------------- /Sharp7.Rx/linqpad-samples/Multiple notifications.linq: -------------------------------------------------------------------------------- 1 | 2 | Sharp7.Rx 3 | Sharp7.Rx 4 | System.Reactive.Linq 5 | System.Reactive.Threading.Tasks 6 | System.Threading.Tasks 7 | 8 | 9 | var ip = "10.30.3.221"; // Set IP address of S7 10 | var db = 3; // Set to an existing DB 11 | 12 | // For rack number and cpu mpi address see 13 | // https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot 14 | var rackNumber = 0; 15 | var cpuMpiAddress = 0; 16 | 17 | using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); 18 | 19 | plc.ConnectionState.Dump(); 20 | 21 | await plc.InitializeConnection(); 22 | 23 | // create an IObservable 24 | plc.CreateNotification($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Int 6"); 25 | plc.CreateNotification($"DB{db}.Real10", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Real 10"); 26 | 27 | 28 | 29 | for (int i = 0; i < 15; i++) 30 | { 31 | switch (i%3) 32 | { 33 | case 0: 34 | await plc.SetValue($"DB{db}.Int6", (short)i); 35 | break; 36 | case 1: 37 | await plc.SetValue($"DB{db}.Real10", i * 0.123f); 38 | break; 39 | } 40 | 41 | await Task.Delay(300); 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /Sharp7.Rx/linqpad-samples/Write and read value.linq: -------------------------------------------------------------------------------- 1 | 2 | Sharp7.Rx 3 | Sharp7.Rx 4 | System.Reactive.Linq 5 | System.Reactive.Threading.Tasks 6 | System.Threading.Tasks 7 | 8 | 9 | var ip = "10.30.3.221"; // Set IP address of S7 10 | var db = 3; // Set to an existing DB 11 | 12 | // For rack number and cpu mpi address see 13 | // https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot 14 | var rackNumber = 0; 15 | var cpuMpiAddress = 0; 16 | 17 | using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); 18 | 19 | await plc.InitializeConnection(); 20 | 21 | "Connection established".Dump(); 22 | 23 | for (int i = 0; i < 10; i++) 24 | { 25 | await plc.SetValue($"DB{db}.Int6", (short)i); 26 | var value = await plc.GetValue($"DB{db}.Int6"); 27 | value.Dump(); 28 | 29 | await Task.Delay(200); 30 | } 31 | 32 | 33 | --------------------------------------------------------------------------------