├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── CHANGELOG.md ├── CodeMaid.config ├── LICENSE.txt ├── Modbus.sln ├── README.md ├── src ├── ConsoleDemo │ ├── ConsoleDemo.csproj │ ├── Logger │ │ └── ConsoleLogger.cs │ ├── Program.cs │ └── Properties │ │ └── launchSettings.json ├── Modbus.Common │ ├── Consts.cs │ ├── Enums.cs │ ├── Interfaces │ │ ├── IModbusClient.cs │ │ └── IModbusServer.cs │ ├── Modbus.Common.csproj │ ├── Structures │ │ ├── Coil.cs │ │ ├── DiscreteInput.cs │ │ ├── ModbusException.cs │ │ ├── ModbusObject.cs │ │ └── Register.cs │ └── Util │ │ ├── Checksum.cs │ │ ├── DataBuffer.cs │ │ ├── Extensions.cs │ │ └── ModbusDevice.cs ├── Modbus.Proxy │ ├── Extensions.cs │ ├── Modbus.Proxy.csproj │ ├── ModbusTcpSerialProxy.cs │ ├── ModbusTcpTcpProxy.cs │ └── ProxyDevice.cs ├── Modbus.Serial │ ├── Client │ │ └── ModbusClient.cs │ ├── Enums.cs │ ├── Modbus.Serial.csproj │ ├── Protocol │ │ ├── Request.cs │ │ └── Response.cs │ ├── Server │ │ └── ModbusServer.cs │ └── Util │ │ ├── Extensions.cs │ │ ├── RequestTask.cs │ │ ├── SafeUnixHandle.cs │ │ ├── SerialRS485.cs │ │ ├── UnixIOException.cs │ │ └── UnsafeNativeMethods.cs └── Modbus.Tcp │ ├── Client │ └── ModbusClient.cs │ ├── Modbus.Tcp.csproj │ ├── Protocol │ ├── Request.cs │ └── Response.cs │ ├── Server │ └── ModbusServer.cs │ └── Util │ ├── Extensions.cs │ └── QueuedRequest.cs └── test └── UnitTests ├── ChecksumTests.cs ├── ConsoleLogger.cs ├── DataBufferTests.cs ├── Extensions.cs ├── ExtensionsTests.cs ├── ModbusTcpTests.cs ├── StructureTests.cs └── UnitTests.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | # Documentation: 2 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 3 | 4 | # Top-most EditorConfig file 5 | root = true 6 | 7 | [*] 8 | insert_final_newline = true 9 | end_of_line = crlf 10 | indent_style = tab 11 | 12 | [*.{cs,vb}] 13 | # Sort using and Import directives with System.* appearing first 14 | dotnet_sort_system_directives_first = true 15 | 16 | # Avoid "this." and "Me." if not necessary 17 | dotnet_style_qualification_for_event = false:warning 18 | dotnet_style_qualification_for_field = false:warning 19 | dotnet_style_qualification_for_method = false:warning 20 | dotnet_style_qualification_for_property = false:warning 21 | 22 | # Use language keywords instead of framework type names for type references 23 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 24 | dotnet_style_predefined_type_for_member_access = true:warning 25 | 26 | # Suggest explicit accessibility modifiers 27 | dotnet_style_require_accessibility_modifiers = always:suggestion 28 | 29 | # Suggest more modern language features when available 30 | dotnet_style_explicit_tuple_names = true:warning 31 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 32 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 33 | dotnet_style_prefer_conditional_expression_over_assignment = true:none 34 | dotnet_style_prefer_conditional_expression_over_return = true:none 35 | dotnet_style_coalesce_expression = true:suggestion 36 | dotnet_style_null_propagation = true:suggestion 37 | 38 | # Definitions 39 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate, type_parameter 40 | dotnet_naming_symbols.methods_properties.applicable_kinds = method, local_function, property 41 | dotnet_naming_symbols.public_symbols.applicable_kinds = property, method, field, event 42 | dotnet_naming_symbols.public_symbols.applicable_accessibilities = public 43 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 44 | dotnet_naming_symbols.constant_fields.required_modifiers = const 45 | dotnet_naming_symbols.private_protected_internal_fields.applicable_kinds = field 46 | dotnet_naming_symbols.private_protected_internal_fields.applicable_accessibilities = private, protected, internal 47 | dotnet_naming_symbols.parameters_locals.applicable_kinds = parameter, local 48 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 49 | dotnet_naming_style.camel_case_style.capitalization = camel_case 50 | 51 | # Name all types using PascalCase 52 | dotnet_naming_rule.types_must_be_capitalized.symbols = types 53 | dotnet_naming_rule.types_must_be_capitalized.style = pascal_case_style 54 | dotnet_naming_rule.types_must_be_capitalized.severity = warning 55 | 56 | # Name all methods and properties using PascalCase 57 | dotnet_naming_rule.methods_properties_must_be_capitalized.symbols = methods_properties 58 | dotnet_naming_rule.methods_properties_must_be_capitalized.style = pascal_case_style 59 | dotnet_naming_rule.methods_properties_must_be_capitalized.severity = warning 60 | 61 | # Name all public members using PascalCase 62 | dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols 63 | dotnet_naming_rule.public_members_must_be_capitalized.style = pascal_case_style 64 | dotnet_naming_rule.public_members_must_be_capitalized.severity = warning 65 | 66 | # Name all constant fields using PascalCase 67 | dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields 68 | dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case_style 69 | dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = suggestion 70 | 71 | # Name all private and internal fields using camelCase 72 | dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.symbols = private_protected_internal_fields 73 | dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.style = camel_case_style 74 | dotnet_naming_rule.private_protected_internal_fields_must_be_camel_case.severity = warning 75 | 76 | # Name all parameters and locals using camelCase 77 | dotnet_naming_rule.parameters_locals_must_be_camel_case.symbols = parameters_locals 78 | dotnet_naming_rule.parameters_locals_must_be_camel_case.style = camel_case_style 79 | dotnet_naming_rule.parameters_locals_must_be_camel_case.severity = warning 80 | 81 | [*.cs] 82 | csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion 83 | 84 | # Only use "var" when it's obvious what the variable type is 85 | csharp_style_var_for_built_in_types = false:warning 86 | csharp_style_var_when_type_is_apparent = true:suggestion 87 | csharp_style_var_elsewhere = false:none 88 | 89 | # Prefer method-like constructs to have a block body 90 | csharp_style_expression_bodied_methods = false:none 91 | csharp_style_expression_bodied_constructors = false:none 92 | csharp_style_expression_bodied_operators = false:none 93 | 94 | # Prefer property-like constructs to have an expression-body 95 | csharp_style_expression_bodied_properties = true:none 96 | csharp_style_expression_bodied_indexers = true:none 97 | csharp_style_expression_bodied_accessors = true:none 98 | 99 | # Suggest more modern language features when available 100 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 101 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 102 | csharp_style_inlined_variable_declaration = true:suggestion 103 | csharp_style_throw_expression = true:suggestion 104 | csharp_style_conditional_delegate_call = true:suggestion 105 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 106 | 107 | # Newline settings 108 | csharp_new_line_before_open_brace = all 109 | csharp_new_line_before_else = true 110 | csharp_new_line_before_catch = true 111 | csharp_new_line_before_finally = true 112 | csharp_new_line_before_members_in_object_initializers = true 113 | csharp_new_line_before_members_in_anonymous_types = true 114 | 115 | # Indentation preferences 116 | csharp_indent_block_contents = true 117 | csharp_indent_braces = false 118 | csharp_indent_case_contents = true 119 | csharp_indent_switch_labels = true 120 | csharp_indent_labels = one_less_than_current 121 | 122 | # Space preferences 123 | csharp_space_after_cast = false 124 | csharp_space_after_colon_in_inheritance_clause = true 125 | csharp_space_after_comma = true 126 | csharp_space_after_dot = false 127 | csharp_space_after_keywords_in_control_flow_statements = true 128 | csharp_space_after_semicolon_in_for_statement = true 129 | csharp_space_around_binary_operators = before_and_after 130 | csharp_space_around_declaration_statements = do_not_ignore 131 | csharp_space_before_colon_in_inheritance_clause = true 132 | csharp_space_before_comma = false 133 | csharp_space_before_dot = false 134 | csharp_space_before_open_square_brackets = false 135 | csharp_space_before_semicolon_in_for_statement = false 136 | csharp_space_between_empty_square_brackets = false 137 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 138 | csharp_space_between_method_call_name_and_opening_parenthesis = false 139 | csharp_space_between_method_call_parameter_list_parentheses = false 140 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 141 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 142 | csharp_space_between_method_declaration_parameter_list_parentheses = false 143 | csharp_space_between_parentheses = false 144 | csharp_space_between_square_brackets = false 145 | 146 | [*.{xml,csproj,targets,props,json,yml}] 147 | indent_size = 2 148 | indent_style = space 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.suo 3 | *.user 4 | *.sln.docstates 5 | .vs/ 6 | *.lock.json 7 | 8 | # Downloaded NuGet packages 9 | /packages/*/ 10 | 11 | # Build results 12 | [aA]rtifacts 13 | [oO]bj 14 | [bB]in 15 | 16 | # MSTest test Results 17 | [Tt]est[Rr]esult*/ 18 | [Bb]uild[Ll]og.* 19 | 20 | # Visual Studio profiler 21 | *.psess 22 | *.vsp 23 | *.vspx 24 | 25 | # Windows Store app package directory 26 | AppPackages/ 27 | 28 | # SQL Server files 29 | App_Data/*.mdf 30 | App_Data/*.ldf 31 | 32 | # Others 33 | *_i.c 34 | *_p.c 35 | *_i.h 36 | *.ilk 37 | *.meta 38 | *.obj 39 | *.pch 40 | *.pdb 41 | *.pgc 42 | *.pgd 43 | *.rsp 44 | *.sbr 45 | *.tlb 46 | *.tli 47 | *.tlh 48 | *.tmp 49 | *.tmp_proj 50 | *.log 51 | *.vspscc 52 | *.vssscc 53 | .builds 54 | *.pidb 55 | *.svclog 56 | *.scc 57 | sql/ 58 | *.[Cc]ache 59 | ClientBin/ 60 | [Ss]tyle[Cc]op.* 61 | ~$* 62 | *~ 63 | *.dbmdl 64 | *.dbproj.schemaview 65 | *.[Pp]ublish.xml 66 | *.pfx 67 | *.p12 68 | *.publishsettings 69 | *.bak 70 | .internal 71 | .local 72 | .tmp 73 | 74 | # ReSharper is a .NET coding add-in 75 | _ReSharper*/ 76 | *.[Rr]e[Ss]harper 77 | *.DotSettings.user 78 | 79 | # TeamCity is a build add-in 80 | _TeamCity* 81 | 82 | # DotCover is a Code Coverage Tool 83 | *.dotCover 84 | 85 | # NCrunch 86 | *.ncrunch* 87 | _NCrunch_* 88 | .*crunch*.local.xml 89 | 90 | # Dotfuscator 91 | Dotfuscated/ 92 | 93 | # PowerShell build framework tools 94 | build/buildscript/private.ps1 95 | 96 | # Windows/other detritus 97 | Thumbs.db 98 | ehthumbs.db 99 | Desktop.ini 100 | $RECYCLE.BIN/ 101 | .DS_Store 102 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: mcr.microsoft.com/dotnet/sdk:latest 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | build: 8 | stage: build 9 | tags: 10 | - docker 11 | script: 12 | - cat $NUGET_CONFIG > nuget.config 13 | - dotnet restore 14 | - dotnet build --no-restore -c Release 15 | - dotnet nuget push -s amget -k $AMGET_API_KEY --skip-duplicate src/Modbus.Common/bin/Release/*.nupkg 16 | - dotnet nuget push -s amget -k $AMGET_API_KEY --skip-duplicate src/Modbus.Tcp/bin/Release/*.nupkg 17 | - dotnet nuget push -s amget -k $AMGET_API_KEY --skip-duplicate src/Modbus.Serial/bin/Release/*.nupkg 18 | - dotnet nuget push -s amget -k $AMGET_API_KEY --skip-duplicate src/Modbus.Proxy/bin/Release/*.nupkg 19 | artifacts: 20 | paths: 21 | - src/Modbus.Common/bin/Release/*.nupkg 22 | - src/Modbus.Common/bin/Release/*.snupkg 23 | - src/Modbus.Tcp/bin/Release/*.nupkg 24 | - src/Modbus.Tcp/bin/Release/*.snupkg 25 | - src/Modbus.Serial/bin/Release/*.nupkg 26 | - src/Modbus.Serial/bin/Release/*.snupkg 27 | - src/Modbus.Proxy/bin/Release/*.nupkg 28 | - src/Modbus.Proxy/bin/Release/*.snupkg 29 | expire_in: 1 month 30 | 31 | deploy: 32 | stage: deploy 33 | tags: 34 | - docker 35 | only: 36 | - tags 37 | script: 38 | - cat $NUGET_CONFIG > nuget.config 39 | - dotnet restore 40 | - dotnet build --no-restore -c Release 41 | - dotnet nuget push -s nuget -k $NUGET_API_KEY --skip-duplicate src/Modbus.Common/bin/Release/*.nupkg 42 | - dotnet nuget push -s nuget -k $NUGET_API_KEY --skip-duplicate src/Modbus.Tcp/bin/Release/*.nupkg 43 | - dotnet nuget push -s nuget -k $NUGET_API_KEY --skip-duplicate src/Modbus.Serial/bin/Release/*.nupkg 44 | - dotnet nuget push -s nuget -k $NUGET_API_KEY --skip-duplicate src/Modbus.Proxy/bin/Release/*.nupkg 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | **Note:** I'll try to keep the changelog up-to-date, but please be patient, I might forget it. 4 | 5 | ---- 6 | 7 | ## 1.2.0 (2021-07-08) 8 | 9 | ### Added (1 change) 10 | 11 | - New property to disable the transaction id check (not recommended but requested) 12 | 13 | ### Fixed (1 change) 14 | 15 | - Optimized an issue with non-responding Modbus TCP servers. 16 | 17 | 18 | 19 | ## 1.1.0 (2021-04-30) 20 | 21 | ### Added (1 change) 22 | 23 | - Extensions to convert from/to naive data types have a parameter to `inverseRegisters` order. (by @Luke092) (Closing Issue #15 more precisely) 24 | 25 | ### Fixed (1 change) 26 | 27 | - Better async behaviour for serial client. 28 | 29 | ### Updated (2 changes) 30 | 31 | - Updated to C# 9.0 32 | - Updated packages 33 | 34 | 35 | 36 | ## 1.0.4 (2021-04-13) 37 | 38 | ### Added (1 change) 39 | 40 | - Extension to convert from/to string enhanced by switch to flip bytes (endianness). (Closing Issue #15) 41 | 42 | 43 | 44 | ## 1.0.3 (2021-03-29) 45 | 46 | ### Fixed (1 change) 47 | 48 | - Resolved null reference errors while reconnecting (TCP and Serial). (Closing Issue #14) 49 | 50 | ### Added (1 change) 51 | 52 | - Extensions to convert values back to `ModbusObject`s are now available. 53 | 54 | 55 | 56 | ## 1.0.2 (2020-12-08) 57 | 58 | ### Fixed (1 change) 59 | 60 | - Removed a potential dead-lock scenario when disconnecting while on reconnect. 61 | 62 | ### Added (1 change) 63 | 64 | - Modbus clients have a optional `CancellationToken` on `Connect()` and `Disconnect()`. 65 | 66 | 67 | 68 | ## 1.0.1 (2020-11-30) 69 | 70 | ### Fixed (1 change) 71 | 72 | - The Modbus TCP client on unix systems creates an `IOException` on connection loss - catch that exception instead of the `EndOfStreamException`. 73 | 74 | 75 | 76 | ## 1.0.0 (2020-11-28) 77 | 78 | ### Fixed (3 changes) 79 | 80 | ##### Client 81 | 82 | - The Modbus TCP client will now recognize (again) a connection close from the remote and start the reconnect cycle 83 | 84 | ##### Server 85 | 86 | - The Modbus TCP server recognizes the connection close from the client as well and terminate the running tasks. 87 | - The Modbus TCP server catches the rising `NotImplementedException`s and will not crash anymore on invalid requests. 88 | 89 | ### Added (5 changes) 90 | 91 | - Added this changelog. 92 | - Added a `ModbusObject` as base class for `Coil`s, `DiscreteInput`s and `Register`s. The property `Type` of `ModbusObjectType` will tell you the actual meaning. 93 | For naming reasons the old classes are still present and used. 94 | - `*.snupkg` (Debug Symbols) for the NuGet packages are available on my Gitlab. 95 | - New package `AMWD.Modbus.Proxy` 96 | - *TCP to TCP* can be used when a remote device only accepts one connection but multiple clients want to request the information (or as Gateway [on a NAT device]). 97 | - *TCP to Serial* can be used to bring a Modbus RTU (RS485) device into your local network. 98 | - Added a custom console logger to the console demo application. 99 | 100 | ### Changed (5 changes) 101 | 102 | - The `Value` property of all structure classes is replaced by `RegisterValue` for 16-bit values ([Holding | Input] `Register`) and `BoolValue` for 1-bit values (`Coil`, `DiscreteInput`). 103 | - All servers and clients can use an `ILogger` implementation for detailed logging in the instances. 104 | - Using more `TimeSpan` for timeouts instead of integer values with different meanings (seconds, milliseconds?!?). 105 | - Updated Gitlab CI procedure for automatic deployment to NuGet.org. 106 | - Code cleanup and C# 8.0 107 | 108 | ### Removed (1 change) 109 | 110 | - git attributes not needed and therefore deleted. 111 | - `FakeSynchronizationContext` was never used. 112 | 113 | 114 | 115 | ## 0.9.10 (2020-07-07) 116 | 117 | ### Fixed (1 change) 118 | 119 | - Changed handling with a broken serial port for a clean shutdown. 120 | 121 | 122 | 123 | ## 0.9.9 (2020-06-29) 124 | 125 | ### Fixed (2 changes) 126 | 127 | - Fixed an issue where the serial client crashed during a request. 128 | - SemVer tagging only allowes 3 sections (major, minor, patch). 129 | 130 | ### Added (2 changes) 131 | 132 | - More specific evaluation of property ranges for the serial client. 133 | - Added license information to the nuget packages. 134 | 135 | 136 | 137 | ## 0.9.8.1 (2020-06-24) 138 | 139 | ### Fixed (1 change) 140 | 141 | - Fixed an issue with an incompatible buffer size on the serial client. 142 | 143 | 144 | 145 | ## 0.9.8 (2020-06-19) 146 | 147 | ### Fixed (2 changes) 148 | 149 | - Requests from clients can be cancelled with a `CancelleationToken`. 150 | - Read/Write async on serial connections is now available. 151 | (The .NET implementation ignores cancellation tokens) 152 | 153 | ### Added (3 changes) 154 | 155 | - New package to create Modbus request proxies. 156 | Needed to translate requests from TCP to Serial and vice versa or to make a device with only one (1) connection at a time available to multiple clients. 157 | **Note:** Not deployed on NuGet.org. 158 | - Enhanced the console demo application to provide a server. 159 | - Modbus servers have custom request handlers. 160 | 161 | ### Changed (2 changes) 162 | 163 | - The clients have a new parameter to provide a `CancellationToken`. 164 | - The `ModbusDevice` uses a more flexible `ReaderWriterLockSlim`. 165 | 166 | 167 | 168 | ## 0.9.7 (2020-05-10) 169 | 170 | ### Fixed (1 change) 171 | 172 | - Reverted changes from a merge request to keep .NET Standard 2.0 as target framework. 173 | 174 | 175 | 176 | ## 0.9.6 (2020-05-10) 177 | 178 | - Merging requests #10 and #11 into the `master`. 179 | 180 | ### Fixed (1 change) 181 | 182 | - Less garbage collecting needed while reading a network stream (from [mishun](https://github.com/mishun)). 183 | 184 | ### Added (1 change) 185 | 186 | - Additional constructor parameter to change the max. connect timeout (from [madfisht3](https://github.com/madfisht3)). 187 | 188 | 189 | 190 | ## 0.9.5 (2020-05-08) 191 | 192 | ### Fixed (2 changes) 193 | 194 | - Serial client did not filter an error response function code. 195 | - Reverted some incompatible package versions. 196 | 197 | ### Added (2 changes) 198 | 199 | - `.editorconfig` added for a unified look and feel. 200 | - `CodMaid.config` added for a unified code cleanup and structure. 201 | 202 | 203 | 204 | ## 0.9.4 (2020-02-14) 205 | 206 | ### Fixed (2 changes) 207 | 208 | - Null reference error on disconnect fixed. 209 | - `ReceiveLoop` not accessing a disposed stream. 210 | 211 | ### Added (2 changes) 212 | 213 | - Logging available for the Modbus TCP server. 214 | - Some internal extensions. 215 | 216 | ### Changed (1 change) 217 | 218 | - Version tag on compiletime changed (NetRevisionTask). 219 | 220 | ### Removed (1 change) 221 | 222 | - `Task.Forget()` only internal available to prevent conflicts with other implementations. 223 | 224 | 225 | 226 | ## 0.9.3 (2019-07-24) 227 | 228 | ### Fixed (2 changes) 229 | 230 | - Catch some exceptions to keep the client more quiet. 231 | - Fixed an issue during diconnect when the client was reconnecting. 232 | 233 | ### Added (2 changes) 234 | 235 | - Custom console logger for UnitTests. 236 | - More UnitTests. 237 | 238 | ### Changed (3 changes) 239 | 240 | - Modbus TCP client uses `TaskCompletionSource`. 241 | - Modbus Serial cient enhanced to be more async. 242 | - Updating to C# 7.1 and more up-to-date packages. 243 | 244 | 245 | 246 | ## 0.9.2 (2019-04-12) 247 | 248 | ### Fixed (1 change) 249 | 250 | - Modbus TCP server was not able to write multiple registers (from [hmarius](https://github.com/hmarius)). 251 | 252 | ### Added (1 change) 253 | 254 | - Change dual driver (RS232/RS485) to RS485 state (tested with sysWORXX CTR 700). 255 | 256 | 257 | 258 | ## 0.9.1 (2019-04-02) 259 | 260 | ### Fixed (1 change) 261 | 262 | - First working Modbus serial implementation. 263 | 264 | ### Added (3 changes) 265 | 266 | - Implementation of function 43 (0x2B read device information). 267 | - UnitTests. 268 | - Gitlab CI builds. 269 | 270 | ### Changed (1 change) 271 | 272 | - NuGet information of projects updated. 273 | 274 | 275 | 276 | ## 0.9.0 (2018-06-14) | *First Release* 277 | 278 | - Modbus TCP Client implementation with all common functions available. 279 | - Modbus TCP Server implementation (not well tested). 280 | - Basic Modbus Serial implementation (not tested!). 281 | -------------------------------------------------------------------------------- /CodeMaid.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | False 12 | 13 | 14 | True 15 | 16 | 17 | .*\.Designer\.cs||.*\.resx||packages.config||.*\.min\.js||.*\.min\.css 18 | 19 | 20 | True 21 | 22 | 23 | True 24 | 25 | 26 | True 27 | 28 | 29 | True 30 | 31 | 32 | True 33 | 34 | 35 | True 36 | 37 | 38 | True 39 | 40 | 41 | True 42 | 43 | 44 | True 45 | 46 | 47 | True 48 | 49 | 50 | True 51 | 52 | 53 | True 54 | 55 | 56 | True 57 | 58 | 59 | True 60 | 61 | 62 | True 63 | 64 | 66 | True 67 | 68 | 70 | True 71 | 72 | 74 | False 75 | 76 | 77 | True 78 | 79 | 81 | False 82 | 83 | 85 | True 86 | 87 | 88 | True 89 | 90 | 92 | True 93 | 94 | 96 | False 97 | 98 | 100 | False 101 | 102 | 104 | True 105 | 106 | 107 | True 108 | 109 | 111 | False 112 | 113 | 115 | False 116 | 117 | 119 | True 120 | 121 | 123 | True 124 | 125 | 127 | True 128 | 129 | 131 | False 132 | 133 | 134 | True 135 | 136 | 138 | False 139 | 140 | 142 | True 143 | 144 | 146 | True 147 | 148 | 150 | True 151 | 152 | 154 | False 155 | 156 | 158 | False 159 | 160 | 162 | True 163 | 164 | 166 | False 167 | 168 | 170 | True 171 | 172 | 174 | False 175 | 176 | 178 | False 179 | 180 | 182 | False 183 | 184 | 185 | True 186 | 187 | 189 | True 190 | 191 | 193 | True 194 | 195 | 197 | True 198 | 199 | 201 | True 202 | 203 | 205 | True 206 | 207 | 209 | True 210 | 211 | 213 | True 214 | 215 | 217 | True 218 | 219 | 221 | True 222 | 223 | 224 | True 225 | 226 | 227 | True 228 | 229 | 230 | True 231 | 232 | 233 | True 234 | 235 | 236 | True 237 | 238 | 239 | True 240 | 241 | 243 | True 244 | 245 | 247 | True 248 | 249 | 250 | False 251 | 252 | 253 | True 254 | 255 | 257 | True 258 | 259 | 260 | 0 261 | 262 | 264 | True 265 | 266 | 268 | True 269 | 270 | 271 | True 272 | 273 | 275 | True 276 | 277 | 279 | True 280 | 281 | 282 | True 283 | 284 | 285 | False 286 | 287 | 289 | 290 | 291 | 292 | 293 | False 294 | 295 | 296 | False 297 | 298 | 299 | True 300 | 301 | 302 | 100 303 | 304 | 305 | False 306 | 307 | 308 | False 309 | 310 | 311 | False 312 | 313 | 314 | False 315 | 316 | 317 | False 318 | 319 | 321 | False 322 | 323 | 324 | 0 325 | 326 | 327 | False 328 | 329 | 330 | False 331 | 332 | 333 | False 334 | 335 | 336 | 337 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-2021 Andreas Müller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Modbus.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30711.63 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0A7CE6D2-374F-4EBC-BFD3-DE2587A6701C}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | .gitignore = .gitignore 10 | .gitlab-ci.yml = .gitlab-ci.yml 11 | CodeMaid.config = CodeMaid.config 12 | LICENSE.txt = LICENSE.txt 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D4E6650D-2156-4660-B531-0B2AAD475BA1}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3B6130C0-9168-4208-A677-29DD20301220}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleDemo", "src\ConsoleDemo\ConsoleDemo.csproj", "{2E6A165D-23DE-40D2-8A43-FE6EFA86F076}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Common", "src\Modbus.Common\Modbus.Common.csproj", "{71D10922-FE10-4A77-B4AC-E947797E9CFE}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Serial", "src\Modbus.Serial\Modbus.Serial.csproj", "{30629B0B-B815-4951-AE56-9A0038210ECB}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Tcp", "src\Modbus.Tcp\Modbus.Tcp.csproj", "{BF60D1D4-3767-4EC4-AC53-C1772958BE14}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "test\UnitTests\UnitTests.csproj", "{33EB4212-B2B4-4CFA-A374-1DD9D2675822}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modbus.Proxy", "src\Modbus.Proxy\Modbus.Proxy.csproj", "{F2428E7C-FB89-48F2-8E4B-85F8E44F2319}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {2E6A165D-23DE-40D2-8A43-FE6EFA86F076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {2E6A165D-23DE-40D2-8A43-FE6EFA86F076}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {2E6A165D-23DE-40D2-8A43-FE6EFA86F076}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {2E6A165D-23DE-40D2-8A43-FE6EFA86F076}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {71D10922-FE10-4A77-B4AC-E947797E9CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {71D10922-FE10-4A77-B4AC-E947797E9CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {71D10922-FE10-4A77-B4AC-E947797E9CFE}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {71D10922-FE10-4A77-B4AC-E947797E9CFE}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {30629B0B-B815-4951-AE56-9A0038210ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {30629B0B-B815-4951-AE56-9A0038210ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {30629B0B-B815-4951-AE56-9A0038210ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {30629B0B-B815-4951-AE56-9A0038210ECB}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {BF60D1D4-3767-4EC4-AC53-C1772958BE14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {BF60D1D4-3767-4EC4-AC53-C1772958BE14}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {BF60D1D4-3767-4EC4-AC53-C1772958BE14}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {BF60D1D4-3767-4EC4-AC53-C1772958BE14}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {33EB4212-B2B4-4CFA-A374-1DD9D2675822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {33EB4212-B2B4-4CFA-A374-1DD9D2675822}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {33EB4212-B2B4-4CFA-A374-1DD9D2675822}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {33EB4212-B2B4-4CFA-A374-1DD9D2675822}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {F2428E7C-FB89-48F2-8E4B-85F8E44F2319}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {F2428E7C-FB89-48F2-8E4B-85F8E44F2319}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {F2428E7C-FB89-48F2-8E4B-85F8E44F2319}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {F2428E7C-FB89-48F2-8E4B-85F8E44F2319}.Release|Any CPU.Build.0 = Release|Any CPU 62 | EndGlobalSection 63 | GlobalSection(SolutionProperties) = preSolution 64 | HideSolutionNode = FALSE 65 | EndGlobalSection 66 | GlobalSection(NestedProjects) = preSolution 67 | {2E6A165D-23DE-40D2-8A43-FE6EFA86F076} = {D4E6650D-2156-4660-B531-0B2AAD475BA1} 68 | {71D10922-FE10-4A77-B4AC-E947797E9CFE} = {D4E6650D-2156-4660-B531-0B2AAD475BA1} 69 | {30629B0B-B815-4951-AE56-9A0038210ECB} = {D4E6650D-2156-4660-B531-0B2AAD475BA1} 70 | {BF60D1D4-3767-4EC4-AC53-C1772958BE14} = {D4E6650D-2156-4660-B531-0B2AAD475BA1} 71 | {33EB4212-B2B4-4CFA-A374-1DD9D2675822} = {3B6130C0-9168-4208-A677-29DD20301220} 72 | {F2428E7C-FB89-48F2-8E4B-85F8E44F2319} = {D4E6650D-2156-4660-B531-0B2AAD475BA1} 73 | EndGlobalSection 74 | GlobalSection(ExtensibilityGlobals) = postSolution 75 | SolutionGuid = {FC7F1581-0E3C-4E35-9DBC-CC7952DE19C1} 76 | EndGlobalSection 77 | EndGlobal 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Impotant Information!** 2 | > As this implementation has way too many pitfalls, I decided to re-write the whole library. 3 | > You can find the new implementation here: https://github.com/AM-WD/AMWD.Protocols.Modbus 4 | 5 | # Modbus 6 | 7 | Implements the Modbus communication protocol, written as a .NET Standard 2.0 library. 8 | 9 | 10 | 11 | ## Example 12 | 13 | You can use the clients without any big knowledge about the protocol: 14 | ```cs 15 | string host = "modbus-device.local"; 16 | int port = 502; 17 | 18 | using var client = new ModbusClient(host, port); 19 | await client.Connect(); 20 | 21 | byte deviceIdentifier = 5; 22 | ushort startAddress = 19000; 23 | ushort count = 2; 24 | 25 | var registers = await client.ReadHoldingRegisters(deviceIdentifier, startAddress, count); 26 | float voltage = registers.GetSingle(); 27 | 28 | Console.WriteLine($"The voltage between L1 and N is: {voltage:N2}V"); 29 | ``` 30 | 31 | For the people who have seen some devices: yes, it's a request to a Janitza device ;-). 32 | 33 | ## License 34 | 35 | All packages published under the [MIT license](LICENSE.txt). 36 | -------------------------------------------------------------------------------- /src/ConsoleDemo/ConsoleDemo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ConsoleDemo/Logger/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace ConsoleDemo.Logger 5 | { 6 | internal class ConsoleLogger : ILogger 7 | { 8 | private readonly object syncObj = new object(); 9 | 10 | public ConsoleLogger() 11 | { 12 | } 13 | 14 | public ConsoleLogger(string name) 15 | { 16 | Name = name; 17 | } 18 | 19 | public ConsoleLogger(string name, ConsoleLogger parentLogger) 20 | { 21 | Name = name; 22 | ParentLogger = parentLogger; 23 | } 24 | 25 | public string Name { get; } 26 | 27 | public ConsoleLogger ParentLogger { get; } 28 | 29 | public bool DisableColors { get; set; } 30 | 31 | public string TimestampFormat { get; set; } 32 | 33 | public LogLevel MinLevel { get; set; } 34 | 35 | public IDisposable BeginScope(TState state) 36 | { 37 | throw new NotImplementedException(); 38 | } 39 | 40 | public bool IsEnabled(LogLevel logLevel) 41 | { 42 | return logLevel >= MinLevel; 43 | } 44 | 45 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 46 | { 47 | if (!IsEnabled(logLevel)) 48 | return; 49 | 50 | if (formatter == null) 51 | throw new ArgumentNullException(nameof(formatter)); 52 | 53 | string message = formatter(state, exception); 54 | 55 | if (!string.IsNullOrEmpty(message) || exception != null) 56 | { 57 | if (ParentLogger != null) 58 | { 59 | ParentLogger.WriteMessage(Name, logLevel, eventId.Id, message, exception); 60 | } 61 | else 62 | { 63 | WriteMessage(Name, logLevel, eventId.Id, message, exception); 64 | } 65 | } 66 | } 67 | 68 | private void WriteMessage(string name, LogLevel logLevel, int eventId, string message, Exception exception) 69 | { 70 | if (exception != null) 71 | { 72 | if (!string.IsNullOrEmpty(message)) 73 | { 74 | message += Environment.NewLine + exception.ToString(); 75 | } 76 | else 77 | { 78 | message = exception.ToString(); 79 | } 80 | } 81 | 82 | bool changedColor; 83 | string timestampPadding = ""; 84 | lock (syncObj) 85 | { 86 | if (!string.IsNullOrEmpty(TimestampFormat)) 87 | { 88 | changedColor = false; 89 | if (!DisableColors) 90 | { 91 | switch (logLevel) 92 | { 93 | case LogLevel.Trace: 94 | Console.ForegroundColor = ConsoleColor.DarkGray; 95 | changedColor = true; 96 | break; 97 | } 98 | } 99 | string timestamp = DateTime.Now.ToString(TimestampFormat) + " "; 100 | Console.Write(timestamp); 101 | timestampPadding = new string(' ', timestamp.Length); 102 | 103 | if (changedColor) 104 | Console.ResetColor(); 105 | } 106 | 107 | changedColor = false; 108 | if (!DisableColors) 109 | { 110 | switch (logLevel) 111 | { 112 | case LogLevel.Trace: 113 | Console.ForegroundColor = ConsoleColor.DarkGray; 114 | changedColor = true; 115 | break; 116 | case LogLevel.Information: 117 | Console.ForegroundColor = ConsoleColor.DarkGreen; 118 | changedColor = true; 119 | break; 120 | case LogLevel.Warning: 121 | Console.ForegroundColor = ConsoleColor.Yellow; 122 | changedColor = true; 123 | break; 124 | case LogLevel.Error: 125 | Console.ForegroundColor = ConsoleColor.Black; 126 | Console.BackgroundColor = ConsoleColor.Red; 127 | changedColor = true; 128 | break; 129 | case LogLevel.Critical: 130 | Console.ForegroundColor = ConsoleColor.White; 131 | Console.BackgroundColor = ConsoleColor.Red; 132 | changedColor = true; 133 | break; 134 | } 135 | } 136 | Console.Write(GetLogLevelString(logLevel)); 137 | 138 | if (changedColor) 139 | Console.ResetColor(); 140 | 141 | changedColor = false; 142 | if (!DisableColors) 143 | { 144 | switch (logLevel) 145 | { 146 | case LogLevel.Trace: 147 | Console.ForegroundColor = ConsoleColor.DarkGray; 148 | changedColor = true; 149 | break; 150 | } 151 | } 152 | Console.WriteLine(": " + (!string.IsNullOrEmpty(name) ? "[" + name + "] " : "") + message.Replace("\n", "\n " + timestampPadding)); 153 | 154 | if (changedColor) 155 | Console.ResetColor(); 156 | } 157 | } 158 | 159 | private static string GetLogLevelString(LogLevel logLevel) 160 | { 161 | switch (logLevel) 162 | { 163 | case LogLevel.Trace: 164 | return "trce"; 165 | case LogLevel.Debug: 166 | return "dbug"; 167 | case LogLevel.Information: 168 | return "info"; 169 | case LogLevel.Warning: 170 | return "warn"; 171 | case LogLevel.Error: 172 | return "fail"; 173 | case LogLevel.Critical: 174 | return "crit"; 175 | default: 176 | throw new ArgumentOutOfRangeException(nameof(logLevel)); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/ConsoleDemo/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Ports; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using AMWD.Modbus.Common; 10 | using AMWD.Modbus.Common.Interfaces; 11 | using AMWD.Modbus.Common.Structures; 12 | using AMWD.Modbus.Common.Util; 13 | using AMWD.Modbus.Serial; 14 | using ConsoleDemo.Logger; 15 | using Microsoft.Extensions.Logging; 16 | using SerialClient = AMWD.Modbus.Serial.Client.ModbusClient; 17 | using SerialServer = AMWD.Modbus.Serial.Server.ModbusServer; 18 | using TcpClient = AMWD.Modbus.Tcp.Client.ModbusClient; 19 | using TcpServer = AMWD.Modbus.Tcp.Server.ModbusServer; 20 | 21 | namespace ConsoleDemo 22 | { 23 | internal class Program 24 | { 25 | private static readonly string[] yesList = new[] { "y", "j", "yes", "ja" }; 26 | 27 | private static async Task Main(string[] _) 28 | { 29 | var cts = new CancellationTokenSource(); 30 | var logger = new ConsoleLogger 31 | { 32 | //MinLevel = LogLevel.Trace, 33 | MinLevel = LogLevel.Information, 34 | TimestampFormat = "HH:mm:ss.fff" 35 | }; 36 | Console.CancelKeyPress += (s, a) => 37 | { 38 | cts.Cancel(); 39 | a.Cancel = true; 40 | }; 41 | 42 | Console.WriteLine("Modbus Console Demo"); 43 | Console.WriteLine(); 44 | 45 | try 46 | { 47 | Console.Write("What to start? [1] Client, [2] Server: "); 48 | int type = Convert.ToInt32(Console.ReadLine().Trim()); 49 | 50 | switch (type) 51 | { 52 | case 1: 53 | return await RunClientAsync(logger, cts.Token); 54 | case 2: 55 | return await RunServerAsync(logger, cts.Token); 56 | default: 57 | Console.Error.WriteLine($"Unknown option: {type}"); 58 | return 1; 59 | } 60 | } 61 | catch (Exception ex) 62 | { 63 | logger.LogError(ex, $"App terminated unexpected: {ex.InnerException?.Message ?? ex.Message}"); 64 | return 1; 65 | } 66 | } 67 | 68 | private static async Task RunClientAsync(ILogger logger, CancellationToken cancellationToken) 69 | { 70 | Console.Write("Connection Type [1] TCP, [2] RS485: "); 71 | int cType = Convert.ToInt32(Console.ReadLine().Trim()); 72 | 73 | IModbusClient client = null; 74 | try 75 | { 76 | switch (cType) 77 | { 78 | case 1: 79 | { 80 | Console.Write("Hostname: "); 81 | string host = Console.ReadLine().Trim(); 82 | Console.Write("Port: "); 83 | int port = Convert.ToInt32(Console.ReadLine().Trim()); 84 | 85 | client = new TcpClient(host, port, logger); 86 | } 87 | break; 88 | case 2: 89 | { 90 | Console.Write("Interface: "); 91 | string port = Console.ReadLine().Trim(); 92 | 93 | Console.Write("Baud: "); 94 | int baud = Convert.ToInt32(Console.ReadLine().Trim()); 95 | 96 | Console.Write("Stop-Bits [0|1|2|3=1.5]: "); 97 | int stopBits = Convert.ToInt32(Console.ReadLine().Trim()); 98 | 99 | Console.Write("Parity [0] None [1] Odd [2] Even [3] Mark [4] Space: "); 100 | int parity = Convert.ToInt32(Console.ReadLine().Trim()); 101 | 102 | Console.Write("Handshake [0] None [1] X-On/Off [2] RTS [3] RTS+X-On/Off: "); 103 | int handshake = Convert.ToInt32(Console.ReadLine().Trim()); 104 | 105 | Console.Write("Timeout (ms): "); 106 | int timeout = Convert.ToInt32(Console.ReadLine().Trim()); 107 | 108 | Console.Write("Set Driver to RS485 [0] No [1] Yes: "); 109 | int setDriver = Convert.ToInt32(Console.ReadLine().Trim()); 110 | 111 | client = new SerialClient(port) 112 | { 113 | BaudRate = (BaudRate)baud, 114 | DataBits = 8, 115 | StopBits = (StopBits)stopBits, 116 | Parity = (Parity)parity, 117 | Handshake = (Handshake)handshake, 118 | SendTimeout = TimeSpan.FromMilliseconds(timeout), 119 | ReceiveTimeout = TimeSpan.FromMilliseconds(timeout) 120 | }; 121 | 122 | if (setDriver == 1) 123 | { 124 | ((SerialClient)client).DriverEnableRS485 = true; 125 | } 126 | } 127 | break; 128 | default: 129 | Console.Error.WriteLine($"Unknown type: {cType}"); 130 | return 1; 131 | } 132 | 133 | await Task.WhenAny(client.Connect(), Task.Delay(Timeout.Infinite, cancellationToken)); 134 | if (cancellationToken.IsCancellationRequested) 135 | return 0; 136 | 137 | while (!cancellationToken.IsCancellationRequested) 138 | { 139 | Console.Write("Device ID: "); 140 | byte id = Convert.ToByte(Console.ReadLine().Trim()); 141 | 142 | Console.Write("Function [1] Read Register, [2] Device Info, [9] Write Register : "); 143 | int fn = Convert.ToInt32(Console.ReadLine().Trim()); 144 | 145 | switch (fn) 146 | { 147 | case 1: 148 | { 149 | ushort address = 0; 150 | ushort count = 0; 151 | string type = ""; 152 | 153 | Console.WriteLine(); 154 | Console.Write("Address : "); 155 | address = Convert.ToUInt16(Console.ReadLine().Trim()); 156 | Console.Write("DataType: "); 157 | type = Console.ReadLine().Trim(); 158 | if (type == "string") 159 | { 160 | Console.Write("Register Count: "); 161 | count = Convert.ToUInt16(Console.ReadLine().Trim()); 162 | } 163 | 164 | Console.WriteLine(); 165 | Console.Write("Run as loop? [y/N]: "); 166 | string loop = Console.ReadLine().Trim().ToLower(); 167 | int interval = 0; 168 | if (yesList.Contains(loop)) 169 | { 170 | Console.Write("Loop interval (milliseconds): "); 171 | interval = Convert.ToInt32(Console.ReadLine().Trim()); 172 | } 173 | 174 | Console.WriteLine(); 175 | do 176 | { 177 | try 178 | { 179 | Console.Write("Result : "); 180 | List result = null; 181 | switch (type.Trim().ToLower()) 182 | { 183 | case "byte": 184 | result = await client.ReadHoldingRegisters(id, address, 1); 185 | Console.WriteLine(result?.First().GetByte()); 186 | break; 187 | case "ushort": 188 | result = await client.ReadHoldingRegisters(id, address, 1); 189 | Console.WriteLine(result?.First().GetUInt16()); 190 | break; 191 | case "uint": 192 | result = await client.ReadHoldingRegisters(id, address, 2); 193 | Console.WriteLine(result?.GetUInt32()); 194 | break; 195 | case "ulong": 196 | result = await client.ReadHoldingRegisters(id, address, 4); 197 | Console.WriteLine(result?.GetUInt64()); 198 | break; 199 | case "sbyte": 200 | result = await client.ReadHoldingRegisters(id, address, 1); 201 | Console.WriteLine(result?.First().GetSByte()); 202 | break; 203 | case "short": 204 | result = await client.ReadHoldingRegisters(id, address, 1); 205 | Console.WriteLine(result?.First().GetInt16()); 206 | break; 207 | case "int": 208 | result = await client.ReadHoldingRegisters(id, address, 2); 209 | Console.WriteLine(result?.GetInt32()); 210 | break; 211 | case "long": 212 | result = await client.ReadHoldingRegisters(id, address, 4); 213 | Console.WriteLine(result?.GetInt64()); 214 | break; 215 | case "float": 216 | result = await client.ReadHoldingRegisters(id, address, 2); 217 | Console.WriteLine(result?.GetSingle()); 218 | break; 219 | case "double": 220 | result = await client.ReadHoldingRegisters(id, address, 4); 221 | Console.WriteLine(result?.GetDouble()); 222 | break; 223 | case "string": 224 | result = await client.ReadHoldingRegisters(id, address, count); 225 | Console.WriteLine(); 226 | Console.WriteLine("UTF8: " + result?.GetString(count)); 227 | Console.WriteLine("Unicode: " + result?.GetString(count, 0, Encoding.Unicode)); 228 | Console.WriteLine("BigEndianUnicode: " + result?.GetString(count, 0, Encoding.BigEndianUnicode)); 229 | break; 230 | default: 231 | Console.Write("DataType unknown"); 232 | break; 233 | } 234 | } 235 | catch 236 | { } 237 | await Task.Delay(TimeSpan.FromMilliseconds(interval), cancellationToken); 238 | } 239 | while (interval > 0 && !cancellationToken.IsCancellationRequested); 240 | } 241 | break; 242 | case 2: 243 | { 244 | Console.Write("[1] Basic, [2] Regular, [3] Extended: "); 245 | int cat = Convert.ToInt32(Console.ReadLine().Trim()); 246 | 247 | Dictionary info = null; 248 | switch (cat) 249 | { 250 | case 1: 251 | info = await client.ReadDeviceInformation(id, DeviceIDCategory.Basic); 252 | break; 253 | case 2: 254 | info = await client.ReadDeviceInformation(id, DeviceIDCategory.Regular); 255 | break; 256 | case 3: 257 | info = await client.ReadDeviceInformation(id, DeviceIDCategory.Extended); 258 | break; 259 | } 260 | if (info != null) 261 | { 262 | foreach (var kvp in info) 263 | { 264 | Console.WriteLine($"{kvp.Key}: {kvp.Value}"); 265 | } 266 | } 267 | } 268 | break; 269 | case 9: 270 | { 271 | Console.Write("Address: "); 272 | ushort address = Convert.ToUInt16(Console.ReadLine().Trim()); 273 | 274 | Console.Write("Bytes (HEX): "); 275 | string byteStr = Console.ReadLine().Trim(); 276 | byteStr = byteStr.Replace(" ", "").ToLower(); 277 | 278 | byte[] bytes = Enumerable.Range(0, byteStr.Length) 279 | .Where(i => i % 2 == 0) 280 | .Select(i => Convert.ToByte(byteStr.Substring(i, 2), 16)) 281 | .ToArray(); 282 | 283 | var registers = Enumerable.Range(0, bytes.Length) 284 | .Where(i => i % 2 == 0) 285 | .Select(i => 286 | { 287 | return new Register 288 | { 289 | Type = ModbusObjectType.HoldingRegister, 290 | Address = address++, 291 | HiByte = bytes[i], 292 | LoByte = bytes[i + 1] 293 | }; 294 | }) 295 | .ToList(); 296 | 297 | if (!await client.WriteRegisters(id, registers)) 298 | throw new Exception($"Writing '{byteStr}' to address {address} failed"); 299 | } 300 | break; 301 | } 302 | 303 | Console.Write("New Request? [y/N]: "); 304 | string again = Console.ReadLine().Trim().ToLower(); 305 | if (!yesList.Contains(again)) 306 | return 0; 307 | } 308 | 309 | return 0; 310 | } 311 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 312 | { 313 | return 0; 314 | } 315 | finally 316 | { 317 | Console.WriteLine("Disposing"); 318 | client?.Dispose(); 319 | Console.WriteLine("Disposed"); 320 | } 321 | } 322 | 323 | private static async Task RunServerAsync(ILogger logger, CancellationToken cancellationToken) 324 | { 325 | Console.Write("Connection Type [1] TCP, [2] RS485: "); 326 | int cType = Convert.ToInt32(Console.ReadLine().Trim()); 327 | 328 | IModbusServer server = null; 329 | try 330 | { 331 | switch (cType) 332 | { 333 | case 1: 334 | { 335 | Console.Write("Bind IP address: "); 336 | var ip = IPAddress.Parse(Console.ReadLine().Trim()); 337 | 338 | Console.Write("Port: "); 339 | int port = Convert.ToInt32(Console.ReadLine().Trim()); 340 | 341 | var tcp = new TcpServer(port, ip, logger) 342 | { 343 | Timeout = TimeSpan.FromSeconds(3) 344 | }; 345 | 346 | server = tcp; 347 | } 348 | break; 349 | case 2: 350 | { 351 | Console.Write("Interface: "); 352 | string port = Console.ReadLine().Trim(); 353 | 354 | Console.Write("Baud: "); 355 | int baud = Convert.ToInt32(Console.ReadLine().Trim()); 356 | 357 | Console.Write("Stop-Bits [0|1|2|3=1.5]: "); 358 | int stopBits = Convert.ToInt32(Console.ReadLine().Trim()); 359 | 360 | Console.Write("Parity [0] None [1] Odd [2] Even [3] Mark [4] Space: "); 361 | int parity = Convert.ToInt32(Console.ReadLine().Trim()); 362 | 363 | Console.Write("Handshake [0] None [1] X-On/Off [2] RTS [3] RTS+X-On/Off: "); 364 | int handshake = Convert.ToInt32(Console.ReadLine().Trim()); 365 | 366 | Console.Write("Timeout (ms): "); 367 | int timeout = Convert.ToInt32(Console.ReadLine().Trim()); 368 | 369 | server = new SerialServer(port) 370 | { 371 | BaudRate = (BaudRate)baud, 372 | DataBits = 8, 373 | StopBits = (StopBits)stopBits, 374 | Parity = (Parity)parity, 375 | Handshake = (Handshake)handshake, 376 | Timeout = TimeSpan.FromMilliseconds(timeout) 377 | }; 378 | } 379 | break; 380 | default: 381 | throw new ArgumentException("Type unknown"); 382 | } 383 | 384 | server.AddDevice(1); 385 | server.AddDevice(5); 386 | server.AddDevice(10); 387 | 388 | Register.Create(123.45f, 100, false).ForEach(r => server.SetHoldingRegister(1, r)); 389 | 390 | Console.WriteLine("Server is running... press CTRL+C to exit."); 391 | await Task.Delay(Timeout.Infinite, cancellationToken); 392 | } 393 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 394 | { } 395 | finally 396 | { 397 | server?.Dispose(); 398 | } 399 | return 0; 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/ConsoleDemo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ConsoleDemo": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Modbus.Common/Consts.cs: -------------------------------------------------------------------------------- 1 | namespace AMWD.Modbus.Common 2 | { 3 | /// 4 | /// Contains all constants used in Modbus. 5 | /// 6 | public static class Consts 7 | { 8 | #region Error/Exception 9 | 10 | /// 11 | /// The Bit-Mask to filter the error-state of a Modbus response. 12 | /// 13 | public const byte ErrorMask = 0x80; 14 | 15 | #endregion Error/Exception 16 | 17 | #region Protocol limitations 18 | 19 | /// 20 | /// The lowest accepted device id on TCP protocol. 21 | /// 22 | public const byte MinDeviceIdTcp = 0x00; 23 | 24 | /// 25 | /// The lowest accepted device id on RTU protocol. 26 | /// 27 | public const byte MinDeviceIdRtu = 0x01; 28 | 29 | /// 30 | /// The highest accepted device id. 31 | /// 32 | public const byte MaxDeviceId = 0xFF; // 255 33 | 34 | /// 35 | /// The lowest address. 36 | /// 37 | public const ushort MinAddress = 0x0000; 38 | 39 | /// 40 | /// The highest address. 41 | /// 42 | public const ushort MaxAddress = 0xFFFF; // 65535 43 | 44 | /// 45 | /// The lowest number of requested data sets. 46 | /// 47 | public const ushort MinCount = 0x01; 48 | 49 | /// 50 | /// The highest number of requested coils to read. 51 | /// 52 | public const ushort MaxCoilCountRead = 0x7D0; // 2000 53 | 54 | /// 55 | /// The highest number of requested coils to write. 56 | /// 57 | public const ushort MaxCoilCountWrite = 0x7B0; // 1968 58 | 59 | /// 60 | /// The highest number of requested registers to read. 61 | /// 62 | public const ushort MaxRegisterCountRead = 0x7D; // 125 63 | 64 | /// 65 | /// The highest number of requested registers to write. 66 | /// 67 | public const ushort MaxRegisterCountWrite = 0x7B; // 123 68 | 69 | #endregion Protocol limitations 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Modbus.Common/Enums.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace AMWD.Modbus.Common 4 | { 5 | /// 6 | /// Lists the Modbus request types. 7 | /// 8 | public enum MessageType 9 | { 10 | /// 11 | /// The type is not set. 12 | /// 13 | Unset, 14 | /// 15 | /// The request reads data. 16 | /// 17 | Read, 18 | /// 19 | /// The request writes one data set. 20 | /// 21 | WriteSingle, 22 | /// 23 | /// The request writes multiple data sets. 24 | /// 25 | WriteMultiple 26 | } 27 | 28 | /// 29 | /// Lists the Modbus function codes. 30 | /// 31 | public enum FunctionCode : byte 32 | { 33 | /// 34 | /// Read coils (Fn 1). 35 | /// 36 | [Description("Read Coils")] 37 | ReadCoils = 0x01, 38 | /// 39 | /// Read discrete inputs (Fn 2). 40 | /// 41 | [Description("Read Discrete Inputs")] 42 | ReadDiscreteInputs = 0x02, 43 | /// 44 | /// Reads holding registers (Fn 3). 45 | /// 46 | [Description("Read Holding Registers")] 47 | ReadHoldingRegisters = 0x03, 48 | /// 49 | /// Reads input registers (Fn 4). 50 | /// 51 | [Description("Read Input Registers")] 52 | ReadInputRegisters = 0x04, 53 | /// 54 | /// Writes a single coil (Fn 5). 55 | /// 56 | [Description("Write Single Coil")] 57 | WriteSingleCoil = 0x05, 58 | /// 59 | /// Writes a single register (Fn 6). 60 | /// 61 | [Description("Write Single Register")] 62 | WriteSingleRegister = 0x06, 63 | /// 64 | /// Writes multiple coils (Fn 15). 65 | /// 66 | [Description("Write Multiple Coils")] 67 | WriteMultipleCoils = 0x0F, 68 | /// 69 | /// Writes multiple registers (Fn 16). 70 | /// 71 | [Description("Write Multiple Registers")] 72 | WriteMultipleRegisters = 0x10, 73 | /// 74 | /// Tunnels service requests and method invocations (Fn 43). 75 | /// 76 | /// 77 | /// This function code needs additional information about its type of request. 78 | /// 79 | [Description("MODBUS Encapsulated Interface (MEI)")] 80 | EncapsulatedInterface = 0x2B 81 | } 82 | 83 | /// 84 | /// Lists the possible MEI types. 85 | /// 86 | /// 87 | /// MEI = MODBUS Encapsulated Interface (Fn 43). 88 | /// 89 | public enum MEIType : byte 90 | { 91 | /// 92 | /// The request contains data of CANopen 93 | /// 94 | [Description("CANopen General Reference Request and Response PDU")] 95 | CANOpenGeneralReference = 0x0D, 96 | /// 97 | /// The request contains data to read specific device information. 98 | /// 99 | [Description("Read Device Information")] 100 | ReadDeviceInformation = 0x0E 101 | } 102 | 103 | /// 104 | /// Lists the category of the device information. 105 | /// 106 | public enum DeviceIDCategory : byte 107 | { 108 | /// 109 | /// Read the basic information (mandatory). 110 | /// 111 | [Description("Basic Information Block")] 112 | Basic = 0x01, 113 | /// 114 | /// Read the regular information (optional). 115 | /// 116 | [Description("Regular Information Block")] 117 | Regular = 0x02, 118 | /// 119 | /// Read the extended information (optional, requires multiple requests). 120 | /// 121 | [Description("Extended Information Block")] 122 | Extended = 0x03, 123 | /// 124 | /// Read an individual object. 125 | /// 126 | [Description("Individual Object")] 127 | Individual = 0x04 128 | } 129 | 130 | /// 131 | /// List of known object ids of the device information. 132 | /// 133 | public enum DeviceIDObject : byte 134 | { 135 | /// 136 | /// The vendor name (mandatory). 137 | /// 138 | VendorName = 0x00, 139 | /// 140 | /// The product code (mandatory). 141 | /// 142 | ProductCode = 0x01, 143 | /// 144 | /// The major and minor revision (mandatory). 145 | /// 146 | MajorMinorRevision = 0x02, 147 | 148 | /// 149 | /// The vendor url (optional). 150 | /// 151 | VendorUrl = 0x03, 152 | /// 153 | /// The product name (optional). 154 | /// 155 | ProductName = 0x04, 156 | /// 157 | /// The model name (optional). 158 | /// 159 | ModelName = 0x05, 160 | /// 161 | /// The application name (optional). 162 | /// 163 | UserApplicationName = 0x06 164 | } 165 | 166 | /// 167 | /// Lists the Modbus exception codes. 168 | /// 169 | public enum ErrorCode : byte 170 | { 171 | /// 172 | /// No error. 173 | /// 174 | [Description("No error")] 175 | NoError = 0, 176 | /// 177 | /// Function code not valid/supported. 178 | /// 179 | [Description("Illegal function")] 180 | IllegalFunction = 1, 181 | /// 182 | /// Data address not in range. 183 | /// 184 | [Description("Illegal data address")] 185 | IllegalDataAddress = 2, 186 | /// 187 | /// The data value to set is not valid. 188 | /// 189 | [Description("Illegal data value")] 190 | IllegalDataValue = 3, 191 | /// 192 | /// Slave device produced a failure. 193 | /// 194 | [Description("Slave device failure")] 195 | SlaveDeviceFailure = 4, 196 | /// 197 | /// Ack 198 | /// 199 | [Description("Acknowledge")] 200 | Acknowledge = 5, 201 | /// 202 | /// Slave device is working on another task. 203 | /// 204 | [Description("Slave device busy")] 205 | SlaveDeviceBusy = 6, 206 | /// 207 | /// nAck 208 | /// 209 | [Description("Negative acknowledge")] 210 | NegativeAcknowledge = 7, 211 | /// 212 | /// Momory Parity Error. 213 | /// 214 | [Description("Memory parity error")] 215 | MemoryParityError = 8, 216 | /// 217 | /// Gateway of the device could not be reached. 218 | /// 219 | [Description("Gateway path unavailable")] 220 | GatewayPath = 10, 221 | /// 222 | /// Gateway device did no resopond. 223 | /// 224 | [Description("Gateway target device failed to respond")] 225 | GatewayTargetDevice = 11 226 | } 227 | 228 | /// 229 | /// Defines the specific type. 230 | /// 231 | public enum ModbusObjectType 232 | { 233 | /// 234 | /// The type is unknown (should not happen). 235 | /// 236 | Unknown, 237 | /// 238 | /// The discrete value is a coil (read/write). 239 | /// 240 | Coil, 241 | /// 242 | /// The discrete value is an input (read only). 243 | /// 244 | DiscreteInput, 245 | /// 246 | /// The value is an holding register (read/write). 247 | /// 248 | HoldingRegister, 249 | /// 250 | /// The value is an input register (read only). 251 | /// 252 | InputRegister 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/Modbus.Common/Interfaces/IModbusClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using AMWD.Modbus.Common.Structures; 6 | 7 | namespace AMWD.Modbus.Common.Interfaces 8 | { 9 | /// 10 | /// Represents the interface for a Modbus client. 11 | /// 12 | public interface IModbusClient : IDisposable 13 | { 14 | #region Properties 15 | 16 | /// 17 | /// Gets the result of the asynchronous initialization of this instance. 18 | /// 19 | Task ConnectingTask { get; } 20 | 21 | /// 22 | /// Gets a value indicating whether the connection is established. 23 | /// 24 | bool IsConnected { get; } 25 | 26 | /// 27 | /// Gets or sets the max reconnect timespan until the reconnect is aborted. 28 | /// 29 | TimeSpan ReconnectTimeSpan { get; set; } 30 | 31 | /// 32 | /// Gets or sets the send timeout in milliseconds. Default: 1000. 33 | /// 34 | TimeSpan SendTimeout { get; set; } 35 | 36 | /// 37 | /// Gets or sets the receive timeout in milliseconds. Default: 1000; 38 | /// 39 | TimeSpan ReceiveTimeout { get; set; } 40 | 41 | /// 42 | /// Gets or sets a value indicating whether to disable the transaction id check. NOT RECOMMENDED 43 | /// 44 | bool DisableTransactionId { get; set; } 45 | 46 | #endregion Properties 47 | 48 | #region Control 49 | 50 | /// 51 | /// Connects the client to the server. 52 | /// 53 | /// A cancellation token to abort the action. 54 | /// An awaitable task. 55 | Task Connect(CancellationToken cancellationToken = default); 56 | 57 | /// 58 | /// Disconnects the client. 59 | /// 60 | /// A cancellation token to abort the action. 61 | /// An awaitable task. 62 | Task Disconnect(CancellationToken cancellationToken = default); 63 | 64 | #endregion Control 65 | 66 | #region Read methods 67 | 68 | /// 69 | /// Reads one or more coils of a device. (Modbus function 1). 70 | /// 71 | /// The id to address the device (slave). 72 | /// The first coil number to read. 73 | /// The number of coils to read. 74 | /// A cancellation token to abort the action. 75 | /// A list of coils or null on error. 76 | Task> ReadCoils(byte deviceId, ushort startAddress, ushort count, CancellationToken cancellationToken = default); 77 | 78 | /// 79 | /// Reads one or more discrete inputs of a device. (Modbus function 2). 80 | /// 81 | /// The id to address the device (slave). 82 | /// The first discrete input number to read. 83 | /// The number of discrete inputs to read. 84 | /// A cancellation token to abort the action. 85 | /// A list of discrete inputs or null on error. 86 | Task> ReadDiscreteInputs(byte deviceId, ushort startAddress, ushort count, CancellationToken cancellationToken = default); 87 | 88 | /// 89 | /// Reads one or more holding registers of a device. (Modbus function 3). 90 | /// 91 | /// The id to address the device (slave). 92 | /// The first register number to read. 93 | /// The number of registers to read. 94 | /// A cancellation token to abort the action. 95 | /// A list of registers or null on error. 96 | Task> ReadHoldingRegisters(byte deviceId, ushort startAddress, ushort count, CancellationToken cancellationToken = default); 97 | 98 | /// 99 | /// Reads one or more input registers of a device. (Modbus function 4). 100 | /// 101 | /// The id to address the device (slave). 102 | /// The first register number to read. 103 | /// The number of registers to read. 104 | /// A cancellation token to abort the action. 105 | /// A list of registers or null on error. 106 | Task> ReadInputRegisters(byte deviceId, ushort startAddress, ushort count, CancellationToken cancellationToken = default); 107 | 108 | /// 109 | /// Reads device information. (Modbus function 43). 110 | /// 111 | /// The id to address the device (slave). 112 | /// The category to read (basic, regular, extended, individual). 113 | /// The first object id to read. 114 | /// A cancellation token to abort the action. 115 | /// A map of device information and their content as string. 116 | Task> ReadDeviceInformation(byte deviceId, DeviceIDCategory categoryId, DeviceIDObject objectId = DeviceIDObject.VendorName, CancellationToken cancellationToken = default); 117 | 118 | /// 119 | /// Reads device information. (Modbus function 43). 120 | /// 121 | /// The id to address the device (slave). 122 | /// The category to read (basic, regular, extended, individual). 123 | /// The first object id to read. 124 | /// A cancellation token to abort the action. 125 | /// A map of device information and their content as raw bytes. 126 | Task> ReadDeviceInformationRaw(byte deviceId, DeviceIDCategory categoryId, DeviceIDObject objectId = DeviceIDObject.VendorName, CancellationToken cancellationToken = default); 127 | 128 | #endregion Read methods 129 | 130 | #region Write methods 131 | 132 | /// 133 | /// Writes a single coil status to the Modbus device. (Modbus function 5) 134 | /// 135 | /// The id to address the device (slave). 136 | /// The coil to write. 137 | /// A cancellation token to abort the action. 138 | /// true on success, otherwise false. 139 | Task WriteSingleCoil(byte deviceId, ModbusObject coil, CancellationToken cancellationToken = default); 140 | 141 | /// 142 | /// Writes a single holding register to the Modbus device. (Modbus function 6) 143 | /// 144 | /// The id to address the device (slave). 145 | /// The register to write. 146 | /// A cancellation token to abort the action. 147 | /// true on success, otherwise false. 148 | Task WriteSingleRegister(byte deviceId, ModbusObject register, CancellationToken cancellationToken = default); 149 | 150 | /// 151 | /// Writes multiple coil status to the Modbus device. (Modbus function 15) 152 | /// 153 | /// The id to address the device (slave). 154 | /// A list of coils to write. 155 | /// A cancellation token to abort the action. 156 | /// true on success, otherwise false. 157 | Task WriteCoils(byte deviceId, IEnumerable coils, CancellationToken cancellationToken = default); 158 | 159 | /// 160 | /// Writes multiple holding registers to the Modbus device. (Modbus function 16) 161 | /// 162 | /// The id to address the device (slave). 163 | /// A list of registers to write. 164 | /// A cancellation token to abort the action. 165 | /// true on success, otherwise false. 166 | Task WriteRegisters(byte deviceId, IEnumerable registers, CancellationToken cancellationToken = default); 167 | 168 | #endregion Write methods 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Modbus.Common/Interfaces/IModbusServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using AMWD.Modbus.Common.Structures; 5 | 6 | namespace AMWD.Modbus.Common.Interfaces 7 | { 8 | /// 9 | /// Represents the interface of a Modbus server. 10 | /// 11 | public interface IModbusServer : IDisposable 12 | { 13 | #region Events 14 | 15 | /// 16 | /// Raised when a coil was written. 17 | /// 18 | event EventHandler InputWritten; 19 | 20 | /// 21 | /// Raised when a holding register was written. 22 | /// 23 | event EventHandler RegisterWritten; 24 | 25 | #endregion Events 26 | 27 | #region Properties 28 | 29 | /// 30 | /// Gets the result of the asynchronous initialization of this instance. 31 | /// 32 | Task Initialization { get; } 33 | 34 | /// 35 | /// Gets the UTC timestamp of the server start. 36 | /// 37 | DateTime StartTime { get; } 38 | 39 | /// 40 | /// Gets a value indicating whether the server is running. 41 | /// 42 | bool IsRunning { get; } 43 | 44 | /// 45 | /// Gets or sets read/write timeout. 46 | /// 47 | TimeSpan Timeout { get; set; } 48 | 49 | /// 50 | /// Gets a list of device ids the server handles. 51 | /// 52 | List DeviceIds { get; } 53 | 54 | #endregion Properties 55 | 56 | #region Public methods 57 | 58 | //void Start(); 59 | 60 | //void Stop(); 61 | 62 | #region Coils 63 | 64 | /// 65 | /// Returns a coil of a device. 66 | /// 67 | /// The device id. 68 | /// The address of the coil. 69 | /// The coil. 70 | Coil GetCoil(byte deviceId, ushort coilNumber); 71 | 72 | /// 73 | /// Sets the status of a coild to a device. 74 | /// 75 | /// The device id. 76 | /// The address of the coil. 77 | /// The status of the coil. 78 | void SetCoil(byte deviceId, ushort coilNumber, bool value); 79 | 80 | /// 81 | /// Sets the status of a coild to a device. 82 | /// 83 | /// The device id. 84 | /// The coil. 85 | void SetCoil(byte deviceId, ModbusObject coil); 86 | 87 | #endregion Coils 88 | 89 | #region Discrete Inputs 90 | 91 | /// 92 | /// Returns a discrete input of a device. 93 | /// 94 | /// The device id. 95 | /// The discrete input address. 96 | /// The discrete input. 97 | DiscreteInput GetDiscreteInput(byte deviceId, ushort inputNumber); 98 | 99 | /// 100 | /// Sets a discrete input of a device. 101 | /// 102 | /// The device id. 103 | /// The discrete input address. 104 | /// A value inidcating whether the input is set. 105 | void SetDiscreteInput(byte deviceId, ushort inputNumber, bool value); 106 | 107 | /// 108 | /// Sets a discrete input of a device. 109 | /// 110 | /// The device id. 111 | /// The discrete input to set. 112 | void SetDiscreteInput(byte deviceId, ModbusObject discreteInput); 113 | 114 | #endregion Discrete Inputs 115 | 116 | #region Input Registers 117 | 118 | /// 119 | /// Returns an input register of a device. 120 | /// 121 | /// The device id. 122 | /// The input register address. 123 | /// The input register. 124 | Register GetInputRegister(byte deviceId, ushort registerNumber); 125 | 126 | /// 127 | /// Sets an input register of a device. 128 | /// 129 | /// The device id. 130 | /// The input register address. 131 | /// The register value. 132 | void SetInputRegister(byte deviceId, ushort registerNumber, ushort value); 133 | 134 | /// 135 | /// Sets an input register of a device. 136 | /// 137 | /// The device id. 138 | /// The input register address. 139 | /// The High-Byte value. 140 | /// The Low-Byte value. 141 | void SetInputRegister(byte deviceId, ushort registerNumber, byte highByte, byte lowByte); 142 | 143 | /// 144 | /// Sets an input register of a device. 145 | /// 146 | /// The device id. 147 | /// The input register. 148 | void SetInputRegister(byte deviceId, ModbusObject register); 149 | 150 | #endregion Input Registers 151 | 152 | #region Holding Registers 153 | 154 | /// 155 | /// Returns a holding register of a device. 156 | /// 157 | /// The device id. 158 | /// The holding register address. 159 | /// The holding register. 160 | Register GetHoldingRegister(byte deviceId, ushort registerNumber); 161 | 162 | /// 163 | /// Sets a holding register of a device. 164 | /// 165 | /// The device id. 166 | /// The holding register address. 167 | /// The register value. 168 | void SetHoldingRegister(byte deviceId, ushort registerNumber, ushort value); 169 | 170 | /// 171 | /// Sets a holding register of a device. 172 | /// 173 | /// The device id. 174 | /// The holding register address. 175 | /// The high byte value. 176 | /// The low byte value. 177 | void SetHoldingRegister(byte deviceId, ushort registerNumber, byte highByte, byte lowByte); 178 | 179 | /// 180 | /// Sets a holding register of a device. 181 | /// 182 | /// The device id. 183 | /// The register. 184 | void SetHoldingRegister(byte deviceId, ModbusObject register); 185 | 186 | #endregion Holding Registers 187 | 188 | #region Devices 189 | 190 | /// 191 | /// Adds a new device to the server. 192 | /// 193 | /// The id of the new device. 194 | /// true on success, otherwise false. 195 | bool AddDevice(byte deviceId); 196 | 197 | /// 198 | /// Removes a device from the server. 199 | /// 200 | /// The device id to remove. 201 | /// true on success, otherwise false. 202 | bool RemoveDevice(byte deviceId); 203 | 204 | #endregion Devices 205 | 206 | #endregion Public methods 207 | } 208 | 209 | /// 210 | /// Provides information of the write action. 211 | /// 212 | public class WriteEventArgs : EventArgs 213 | { 214 | /// 215 | /// Initializes a new instance of the class using a single coil. 216 | /// 217 | /// The device id. 218 | /// The coil. 219 | public WriteEventArgs(byte deviceId, Coil coil) 220 | { 221 | DeviceId = deviceId; 222 | Coils = new List { coil }; 223 | } 224 | 225 | /// 226 | /// Initializes a new instance of the class using a list of coils. 227 | /// 228 | /// The device id. 229 | /// A list of coils. 230 | public WriteEventArgs(byte deviceId, List coils) 231 | { 232 | DeviceId = deviceId; 233 | Coils = coils; 234 | } 235 | 236 | /// 237 | /// Initializes a new instance of the class using a single register. 238 | /// 239 | /// The device id. 240 | /// The register. 241 | public WriteEventArgs(byte deviceId, Register register) 242 | { 243 | DeviceId = deviceId; 244 | Registers = new List { register }; 245 | } 246 | 247 | /// 248 | /// Initializes a new instance of the class using a list of registers. 249 | /// 250 | /// The device id. 251 | /// A list of registers. 252 | public WriteEventArgs(byte deviceId, List registers) 253 | { 254 | DeviceId = deviceId; 255 | Registers = registers; 256 | } 257 | 258 | /// 259 | /// Gets the device id of the written values. 260 | /// 261 | public byte DeviceId { get; private set; } 262 | 263 | /// 264 | /// Gets a list of written coils. 265 | /// 266 | public List Coils { get; private set; } 267 | 268 | /// 269 | /// Gets a list of written holding registers. 270 | /// 271 | public List Registers { get; private set; } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/Modbus.Common/Modbus.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 9.0 6 | 7 | AMWD.Modbus.Common 8 | AMWD.Modbus.Common 9 | {semvertag:master} 10 | false 11 | false 12 | true 13 | true 14 | 1.2.0 15 | 16 | AMWD.Modbus.Common 17 | Modbus.Common 18 | Common files for AMWD.Modbus.TCP and AMWD.Modbus.Serial packages. 19 | AM.WD 20 | Andreas Müller 21 | © {copyright:2018-} AM.WD 22 | https://github.com/AndreasAmMueller/Modbus.git 23 | MIT 24 | Modbus 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Modbus.Common/Structures/Coil.cs: -------------------------------------------------------------------------------- 1 | namespace AMWD.Modbus.Common.Structures 2 | { 3 | /// 4 | /// Represents the contents of a coil on a Modbus device. 5 | /// 6 | public class Coil : ModbusObject 7 | { 8 | /// 9 | public override ModbusObjectType Type => ModbusObjectType.Coil; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Modbus.Common/Structures/DiscreteInput.cs: -------------------------------------------------------------------------------- 1 | namespace AMWD.Modbus.Common.Structures 2 | { 3 | /// 4 | /// Represents the contents of a discrete input on a Modbus device. 5 | /// 6 | public class DiscreteInput : ModbusObject 7 | { 8 | /// 9 | public override ModbusObjectType Type => ModbusObjectType.DiscreteInput; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Modbus.Common/Structures/ModbusException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AMWD.Modbus.Common.Util; 3 | 4 | namespace AMWD.Modbus.Common.Structures 5 | { 6 | /// 7 | /// Represents errors that occurr during Modbus requests. 8 | /// 9 | public class ModbusException : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public ModbusException() 15 | : base() 16 | { } 17 | 18 | /// 19 | /// Initializes a new instance of the class 20 | /// with a specified error message. 21 | /// 22 | /// The specified error message. 23 | public ModbusException(string message) 24 | : base(message) 25 | { } 26 | 27 | /// 28 | /// Initializes a new instance of the class 29 | /// with a specified error message and a reference to the inner exception that is the cause of this exception. 30 | /// 31 | /// The specified error message. 32 | /// The inner exception. 33 | public ModbusException(string message, Exception innerException) 34 | : base(message, innerException) 35 | { } 36 | 37 | /// 38 | /// Gets or sets the error/exception code. 39 | /// 40 | public ErrorCode ErrorCode { get; set; } 41 | 42 | /// 43 | /// Gets the error message. 44 | /// 45 | public string ErrorMessage => ErrorCode.GetDescription(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Modbus.Common/Structures/ModbusObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AMWD.Modbus.Common.Structures 4 | { 5 | /// 6 | /// The abstract basis class for all types of modbus register. 7 | /// 8 | public class ModbusObject 9 | { 10 | /// 11 | /// Gets the explicit type. 12 | /// 13 | public virtual ModbusObjectType Type { get; set; } 14 | 15 | #region Properties 16 | 17 | /// 18 | /// Gets or sets the address. 19 | /// 20 | public ushort Address { get; set; } 21 | 22 | /// 23 | /// Gets or sets the High-Byte of the register. 24 | /// 25 | public byte HiByte { get; set; } 26 | 27 | /// 28 | /// Gets or sets the Low-Byte of the register. 29 | /// 30 | public byte LoByte { get; set; } 31 | 32 | /// 33 | /// Gets or sets the value of the register as WORD. 34 | /// 35 | public ushort RegisterValue 36 | { 37 | get 38 | { 39 | byte[] blob = new[] { HiByte, LoByte }; 40 | if (BitConverter.IsLittleEndian) 41 | Array.Reverse(blob); 42 | 43 | return BitConverter.ToUInt16(blob, 0); 44 | } 45 | set 46 | { 47 | byte[] blob = BitConverter.GetBytes(value); 48 | if (BitConverter.IsLittleEndian) 49 | Array.Reverse(blob); 50 | 51 | HiByte = blob[0]; 52 | LoByte = blob[1]; 53 | } 54 | } 55 | 56 | /// 57 | /// Gets or sets a value indicating whether the discrete value is set. 58 | /// 59 | public bool BoolValue 60 | { 61 | get 62 | { 63 | return HiByte > 0 || LoByte > 0; 64 | } 65 | set 66 | { 67 | HiByte = 0; 68 | LoByte = (byte)(value ? 1 : 0); 69 | } 70 | } 71 | 72 | #endregion Properties 73 | 74 | #region Overrides 75 | 76 | /// 77 | public override string ToString() 78 | => Type switch 79 | { 80 | ModbusObjectType.Coil => $"Coil #{Address} | {BoolValue}", 81 | ModbusObjectType.DiscreteInput => $"Discrete Input #{Address} | {BoolValue}", 82 | ModbusObjectType.HoldingRegister => $"Holding Register #{Address} | Hi: {HiByte:X2} Lo: {LoByte:X2} | {RegisterValue}", 83 | ModbusObjectType.InputRegister => $"Input Register #{Address} | Hi: {HiByte:X2} Lo: {LoByte:X2} | {RegisterValue}", 84 | _ => base.ToString(), 85 | }; 86 | 87 | /// 88 | public override bool Equals(object obj) 89 | { 90 | if (obj is not ModbusObject register) 91 | return false; 92 | 93 | return Type == register.Type 94 | && Address == register.Address 95 | && HiByte == register.HiByte 96 | && LoByte == register.LoByte; 97 | } 98 | 99 | /// 100 | public override int GetHashCode() 101 | => base.GetHashCode() ^ 102 | Address.GetHashCode() ^ 103 | HiByte.GetHashCode() ^ 104 | LoByte.GetHashCode(); 105 | 106 | #endregion Overrides 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Modbus.Common/Structures/Register.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AMWD.Modbus.Common.Structures 6 | { 7 | /// 8 | /// Represents an holding register to keep the modbus typical naming. 9 | /// 10 | /// 11 | /// For programming use the abstract class . 12 | /// 13 | public class Register : ModbusObject 14 | { 15 | #region Creates 16 | 17 | #region unsigned 18 | 19 | /// 20 | /// Initializes a new register from a byte. 21 | /// 22 | /// The byte value. 23 | /// The register address. 24 | /// Flag to create an input register. 25 | /// 26 | public static Register Create(byte value, ushort address, bool isInput = false) 27 | => new() 28 | { 29 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 30 | Address = address, 31 | RegisterValue = value 32 | }; 33 | 34 | /// 35 | /// Initializes a new register from a unsigned short. 36 | /// 37 | /// The uint16 value. 38 | /// The register address. 39 | /// Flag to create an input register. 40 | /// 41 | public static Register Create(ushort value, ushort address, bool isInput = false) 42 | => new() 43 | { 44 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 45 | Address = address, RegisterValue = value 46 | }; 47 | 48 | /// 49 | /// Initializes new registers from an unsigned int. 50 | /// 51 | /// The uint32 value. 52 | /// The register address. 53 | /// Flag to create an input register. 54 | /// 55 | public static List Create(uint value, ushort address, bool isInput = false) 56 | { 57 | if (address + 1 > Consts.MaxAddress) 58 | throw new ArgumentOutOfRangeException(nameof(address)); 59 | 60 | var list = new List(); 61 | byte[] blob = BitConverter.GetBytes(value); 62 | if (BitConverter.IsLittleEndian) 63 | Array.Reverse(blob); 64 | 65 | for (int i = 0; i < blob.Length / 2; i++) 66 | { 67 | int bytePos = i * 2; 68 | list.Add(new Register 69 | { 70 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 71 | Address = Convert.ToUInt16(address + i), 72 | HiByte = blob[bytePos], 73 | LoByte = blob[bytePos + 1] 74 | }); 75 | } 76 | 77 | return list; 78 | } 79 | 80 | /// 81 | /// Initializes new registers from an unsigned long. 82 | /// 83 | /// The uint64 value. 84 | /// The register address. 85 | /// Flag to create an input register. 86 | /// 87 | public static List Create(ulong value, ushort address, bool isInput = false) 88 | { 89 | if (address + 3 > Consts.MaxAddress) 90 | throw new ArgumentOutOfRangeException(nameof(address)); 91 | 92 | var list = new List(); 93 | byte[] blob = BitConverter.GetBytes(value); 94 | if (BitConverter.IsLittleEndian) 95 | Array.Reverse(blob); 96 | 97 | for (int i = 0; i < blob.Length / 2; i++) 98 | { 99 | int bytePos = i * 2; 100 | list.Add(new Register 101 | { 102 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 103 | Address = Convert.ToUInt16(address + i), 104 | HiByte = blob[bytePos], 105 | LoByte = blob[bytePos + 1] 106 | }); 107 | } 108 | 109 | return list; 110 | } 111 | 112 | #endregion unsigned 113 | 114 | #region signed 115 | 116 | /// 117 | /// Initializes a new register from a signed byte. 118 | /// 119 | /// The sbyte value. 120 | /// The register address. 121 | /// Flag to create an input register. 122 | /// 123 | public static Register Create(sbyte value, ushort address, bool isInput = false) 124 | => new() 125 | { 126 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 127 | Address = address, 128 | RegisterValue = (ushort)value 129 | }; 130 | 131 | /// 132 | /// Initializes a new register from a short. 133 | /// 134 | /// The int16 value. 135 | /// The register address. 136 | /// Flag to create an input register. 137 | /// 138 | public static Register Create(short value, ushort address, bool isInput = false) 139 | { 140 | byte[] blob = BitConverter.GetBytes(value); 141 | if (BitConverter.IsLittleEndian) 142 | Array.Reverse(blob); 143 | 144 | return new Register 145 | { 146 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 147 | Address = address, 148 | HiByte = blob[0], 149 | LoByte = blob[1] 150 | }; 151 | } 152 | 153 | /// 154 | /// Initializes new registers from an int. 155 | /// 156 | /// The int32 value. 157 | /// The register address. 158 | /// Flag to create an input register. 159 | /// 160 | public static List Create(int value, ushort address, bool isInput = false) 161 | { 162 | if (address + 1 > Consts.MaxAddress) 163 | throw new ArgumentOutOfRangeException(nameof(address)); 164 | 165 | var list = new List(); 166 | byte[] blob = BitConverter.GetBytes(value); 167 | if (BitConverter.IsLittleEndian) 168 | Array.Reverse(blob); 169 | 170 | for (int i = 0; i < blob.Length / 2; i++) 171 | { 172 | int bytePos = i * 2; 173 | list.Add(new Register 174 | { 175 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 176 | Address = Convert.ToUInt16(address + i), 177 | HiByte = blob[bytePos], 178 | LoByte = blob[bytePos + 1] 179 | }); 180 | } 181 | 182 | return list; 183 | } 184 | 185 | /// 186 | /// Initializes new registers from a long. 187 | /// 188 | /// The int64 value. 189 | /// The register address. 190 | /// Flag to create an input register. 191 | /// 192 | public static List Create(long value, ushort address, bool isInput = false) 193 | { 194 | if (address + 3 > Consts.MaxAddress) 195 | throw new ArgumentOutOfRangeException(nameof(address)); 196 | 197 | var list = new List(); 198 | byte[] blob = BitConverter.GetBytes(value); 199 | if (BitConverter.IsLittleEndian) 200 | Array.Reverse(blob); 201 | 202 | for (int i = 0; i < blob.Length / 2; i++) 203 | { 204 | int bytePos = i * 2; 205 | list.Add(new Register 206 | { 207 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 208 | Address = Convert.ToUInt16(address + i), 209 | HiByte = blob[bytePos], 210 | LoByte = blob[bytePos + 1] 211 | }); 212 | } 213 | 214 | return list; 215 | } 216 | 217 | #endregion signed 218 | 219 | #region floating point 220 | 221 | /// 222 | /// Initializes new registers from a float. 223 | /// 224 | /// The single value. 225 | /// The register address. 226 | /// Flag to create an input register. 227 | /// 228 | public static List Create(float value, ushort address, bool isInput = false) 229 | { 230 | if (address + 1 > Consts.MaxAddress) 231 | throw new ArgumentOutOfRangeException(nameof(address)); 232 | 233 | var list = new List(); 234 | byte[] blob = BitConverter.GetBytes(value); 235 | if (BitConverter.IsLittleEndian) 236 | Array.Reverse(blob); 237 | 238 | for (int i = 0; i < blob.Length / 2; i++) 239 | { 240 | int bytePos = i * 2; 241 | list.Add(new Register 242 | { 243 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 244 | Address = Convert.ToUInt16(address + i), 245 | HiByte = blob[bytePos], 246 | LoByte = blob[bytePos + 1] 247 | }); 248 | } 249 | 250 | return list; 251 | } 252 | 253 | /// 254 | /// Initializes new registers from a double. 255 | /// 256 | /// The double value. 257 | /// The register address. 258 | /// Flag to create an input register. 259 | /// 260 | public static List Create(double value, ushort address, bool isInput = false) 261 | { 262 | if (address + 3 > Consts.MaxAddress) 263 | throw new ArgumentOutOfRangeException(nameof(address)); 264 | 265 | byte[] blob = BitConverter.GetBytes(value); 266 | if (BitConverter.IsLittleEndian) 267 | Array.Reverse(blob); 268 | 269 | var list = new List(); 270 | for (int i = 0; i < blob.Length / 2; i++) 271 | { 272 | int bytePos = i * 2; 273 | list.Add(new Register 274 | { 275 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 276 | Address = Convert.ToUInt16(address + i), 277 | HiByte = blob[bytePos], 278 | LoByte = blob[bytePos + 1] 279 | }); 280 | } 281 | 282 | return list; 283 | } 284 | 285 | #endregion floating point 286 | 287 | #region String 288 | 289 | /// 290 | /// Initializes new registers from a string. 291 | /// 292 | /// The string. 293 | /// The register address. 294 | /// The encoding of the string. Default: . 295 | /// Flag to create an input register. 296 | /// 297 | public static List Create(string str, ushort address, Encoding encoding = null, bool isInput = false) 298 | { 299 | if (encoding == null) 300 | encoding = Encoding.UTF8; 301 | 302 | var list = new List(); 303 | byte[] blob = encoding.GetBytes(str); 304 | int numRegister = (int)Math.Ceiling(blob.Length / 2.0); 305 | 306 | if (address + numRegister > Consts.MaxAddress) 307 | throw new ArgumentOutOfRangeException(nameof(address)); 308 | 309 | for (int i = 0; i < numRegister; i++) 310 | { 311 | int bytePos = i * 2; 312 | try 313 | { 314 | list.Add(new Register 315 | { 316 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 317 | Address = Convert.ToUInt16(address + i), 318 | HiByte = blob[bytePos], 319 | LoByte = blob[bytePos + 1] 320 | }); 321 | } 322 | catch 323 | { 324 | list.Add(new Register 325 | { 326 | Type = isInput ? ModbusObjectType.InputRegister : ModbusObjectType.HoldingRegister, 327 | Address = Convert.ToUInt16(address + i), 328 | HiByte = blob[bytePos] 329 | }); 330 | } 331 | } 332 | 333 | return list; 334 | } 335 | 336 | #endregion String 337 | 338 | #endregion Creates 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/Modbus.Common/Util/Checksum.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AMWD.Modbus.Common.Util 4 | { 5 | /// 6 | /// Helper class for checksums. 7 | /// 8 | public static class Checksum 9 | { 10 | /// 11 | /// Calculates the CRC checksum with 16 bits of an array. 12 | /// 13 | /// The array with data. 14 | /// CRC16 Checksum as byte array. [0] = low byte, [1] = high byte. 15 | public static byte[] CRC16(this byte[] array) 16 | => array.CRC16(0, array.Length); 17 | 18 | /// 19 | /// Calculates the CRC checksum with 16 bits of an array. 20 | /// 21 | /// The array with data. 22 | /// The first byte to use. 23 | /// The number of bytes to use. 24 | /// CRC16 Checksum as byte array. [0] = low byte, [1] = high byte. 25 | public static byte[] CRC16(this byte[] array, int start, int length) 26 | { 27 | if (array == null || array.Length == 0) 28 | throw new ArgumentNullException(nameof(array)); 29 | 30 | if (start < 0 || start >= array.Length) 31 | throw new ArgumentOutOfRangeException(nameof(start)); 32 | 33 | if (length <= 0 || (start + length) > array.Length) 34 | throw new ArgumentOutOfRangeException(nameof(length)); 35 | 36 | ushort crc16 = 0xFFFF; 37 | byte lsb; 38 | 39 | for (int i = start; i < (start + length); i++) 40 | { 41 | crc16 = (ushort)(crc16 ^ array[i]); 42 | for (int j = 0; j < 8; j++) 43 | { 44 | lsb = (byte)(crc16 & 1); 45 | crc16 = (ushort)(crc16 >> 1); 46 | if (lsb == 1) 47 | { 48 | crc16 = (ushort)(crc16 ^ 0xA001); 49 | } 50 | } 51 | } 52 | 53 | byte[] b = new byte[2]; 54 | b[0] = (byte)crc16; 55 | b[1] = (byte)(crc16 >> 8); 56 | 57 | return b; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Modbus.Common/Util/ModbusDevice.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using AMWD.Modbus.Common.Structures; 4 | 5 | namespace AMWD.Modbus.Common.Util 6 | { 7 | /// 8 | /// Represents a Modbus device. 9 | /// 10 | public class ModbusDevice 11 | { 12 | private readonly ReaderWriterLockSlim coilsLock = new(); 13 | private readonly ReaderWriterLockSlim discreteInputsLock = new(); 14 | private readonly ReaderWriterLockSlim inputRegistersLock = new(); 15 | private readonly ReaderWriterLockSlim holdingRegistersLock = new(); 16 | 17 | private readonly List coils = new(); 18 | private readonly List discreteInputs = new(); 19 | private readonly Dictionary inputRegisters = new(); 20 | private readonly Dictionary holdingRegisters = new(); 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The device id. 26 | public ModbusDevice(byte id) 27 | { 28 | DeviceId = id; 29 | } 30 | 31 | /// 32 | /// Gets the device id. 33 | /// 34 | public byte DeviceId { get; private set; } 35 | 36 | #region Coils 37 | 38 | /// 39 | /// Gets a coil at an address. 40 | /// 41 | /// The address of the coil. 42 | /// 43 | public Coil GetCoil(ushort address) 44 | { 45 | using (coilsLock.GetReadLock()) 46 | { 47 | return new Coil { Address = address, BoolValue = coils.Contains(address) }; 48 | } 49 | } 50 | 51 | /// 52 | /// Sets a coil value. 53 | /// 54 | /// The address. 55 | /// A value indicating whether the coil is active. 56 | public void SetCoil(ushort address, bool value) 57 | { 58 | using (coilsLock.GetWriteLock()) 59 | { 60 | if (value && !coils.Contains(address)) 61 | { 62 | coils.Add(address); 63 | } 64 | if (!value && coils.Contains(address)) 65 | { 66 | coils.Remove(address); 67 | } 68 | } 69 | } 70 | 71 | #endregion Coils 72 | 73 | #region Discrete Input 74 | 75 | /// 76 | /// Gets an input. 77 | /// 78 | /// The address. 79 | /// 80 | public DiscreteInput GetInput(ushort address) 81 | { 82 | using (discreteInputsLock.GetReadLock()) 83 | { 84 | return new DiscreteInput { Address = address, BoolValue = discreteInputs.Contains(address) }; 85 | } 86 | } 87 | 88 | /// 89 | /// Sets an input. 90 | /// 91 | /// The address. 92 | /// A value indicating whether the input is active. 93 | public void SetInput(ushort address, bool value) 94 | { 95 | using (discreteInputsLock.GetWriteLock()) 96 | { 97 | if (value && !discreteInputs.Contains(address)) 98 | { 99 | discreteInputs.Add(address); 100 | } 101 | if (!value && discreteInputs.Contains(address)) 102 | { 103 | discreteInputs.Remove(address); 104 | } 105 | } 106 | } 107 | 108 | #endregion Discrete Input 109 | 110 | #region Input Register 111 | 112 | /// 113 | /// Gets an input register. 114 | /// 115 | /// The address. 116 | /// 117 | public Register GetInputRegister(ushort address) 118 | { 119 | using (inputRegistersLock.GetReadLock()) 120 | { 121 | if (inputRegisters.TryGetValue(address, out ushort value)) 122 | return new Register { Address = address, RegisterValue = value, Type = ModbusObjectType.InputRegister }; 123 | } 124 | return new Register { Address = address, Type = ModbusObjectType.InputRegister }; 125 | } 126 | 127 | /// 128 | /// Sets an input register. 129 | /// 130 | /// The address. 131 | /// The value. 132 | public void SetInputRegister(ushort address, ushort value) 133 | { 134 | using (inputRegistersLock.GetWriteLock()) 135 | { 136 | if (value > 0) 137 | { 138 | inputRegisters[address] = value; 139 | } 140 | else 141 | { 142 | inputRegisters.Remove(address); 143 | } 144 | } 145 | } 146 | 147 | #endregion Input Register 148 | 149 | #region Holding Register 150 | 151 | /// 152 | /// Gets an holding register. 153 | /// 154 | /// The address. 155 | /// 156 | public Register GetHoldingRegister(ushort address) 157 | { 158 | using (holdingRegistersLock.GetReadLock()) 159 | { 160 | if (holdingRegisters.TryGetValue(address, out ushort value)) 161 | return new Register { Address = address, RegisterValue = value, Type = ModbusObjectType.HoldingRegister }; 162 | } 163 | return new Register { Address = address, Type = ModbusObjectType.HoldingRegister }; 164 | } 165 | 166 | /// 167 | /// Sets an holding register. 168 | /// 169 | /// The address. 170 | /// The value to set. 171 | public void SetHoldingRegister(ushort address, ushort value) 172 | { 173 | using (holdingRegistersLock.GetWriteLock()) 174 | { 175 | if (value > 0) 176 | { 177 | holdingRegisters[address] = value; 178 | } 179 | else 180 | { 181 | holdingRegisters.Remove(address); 182 | } 183 | } 184 | } 185 | 186 | #endregion Holding Register 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/Modbus.Proxy/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace AMWD.Modbus.Proxy 5 | { 6 | internal static class Extensions 7 | { 8 | public static IDisposable GetReadLock(this ReaderWriterLockSlim rwLock, int millisecondsTimeout = -1) 9 | { 10 | if (!rwLock.TryEnterReadLock(millisecondsTimeout)) 11 | throw new TimeoutException("Trying to enter a read lock."); 12 | 13 | return new DisposableReaderWriterLockSlim(rwLock, DisposableReaderWriterLockSlim.LockMode.ReadLock); 14 | } 15 | 16 | public static IDisposable GetReadLock(this ReaderWriterLockSlim rwLock, TimeSpan timeSpan) 17 | { 18 | if (!rwLock.TryEnterReadLock(timeSpan)) 19 | throw new TimeoutException("Trying to enter a read lock."); 20 | 21 | return new DisposableReaderWriterLockSlim(rwLock, DisposableReaderWriterLockSlim.LockMode.ReadLock); 22 | } 23 | 24 | public static IDisposable GetUpgradableReadLock(this ReaderWriterLockSlim rwLock, int millisecondsTimeout = -1) 25 | { 26 | if (!rwLock.TryEnterUpgradeableReadLock(millisecondsTimeout)) 27 | throw new TimeoutException("Trying to enter an upgradable read lock."); 28 | 29 | return new DisposableReaderWriterLockSlim(rwLock, DisposableReaderWriterLockSlim.LockMode.UpgradableReadLock); 30 | } 31 | 32 | public static IDisposable GetUpgradableReadLock(this ReaderWriterLockSlim rwLock, TimeSpan timeSpan) 33 | { 34 | if (!rwLock.TryEnterUpgradeableReadLock(timeSpan)) 35 | throw new TimeoutException("Trying to enter an upgradable read lock."); 36 | 37 | return new DisposableReaderWriterLockSlim(rwLock, DisposableReaderWriterLockSlim.LockMode.UpgradableReadLock); 38 | } 39 | 40 | public static IDisposable GetWriteLock(this ReaderWriterLockSlim rwLock, int millisecondsTimeout = -1) 41 | { 42 | if (!rwLock.TryEnterWriteLock(millisecondsTimeout)) 43 | throw new TimeoutException("Trying to enter a write lock."); 44 | 45 | return new DisposableReaderWriterLockSlim(rwLock, DisposableReaderWriterLockSlim.LockMode.WriteLock); 46 | } 47 | 48 | public static IDisposable GetWriteLock(this ReaderWriterLockSlim rwLock, TimeSpan timeSpan) 49 | { 50 | if (!rwLock.TryEnterWriteLock(timeSpan)) 51 | throw new TimeoutException("Trying to enter a write lock."); 52 | 53 | return new DisposableReaderWriterLockSlim(rwLock, DisposableReaderWriterLockSlim.LockMode.WriteLock); 54 | } 55 | 56 | private class DisposableReaderWriterLockSlim : IDisposable 57 | { 58 | private readonly ReaderWriterLockSlim rwLock; 59 | private LockMode mode; 60 | 61 | public DisposableReaderWriterLockSlim(ReaderWriterLockSlim rwLock, LockMode mode) 62 | { 63 | this.rwLock = rwLock; 64 | this.mode = mode; 65 | } 66 | 67 | public void Dispose() 68 | { 69 | if (rwLock == null || mode == LockMode.None) 70 | return; 71 | 72 | if (mode == LockMode.ReadLock) 73 | rwLock.ExitReadLock(); 74 | 75 | if (mode == LockMode.UpgradableReadLock && rwLock.IsWriteLockHeld) 76 | rwLock.ExitWriteLock(); 77 | 78 | if (mode == LockMode.UpgradableReadLock) 79 | rwLock.ExitUpgradeableReadLock(); 80 | 81 | if (mode == LockMode.WriteLock) 82 | rwLock.ExitWriteLock(); 83 | 84 | mode = LockMode.None; 85 | } 86 | 87 | public enum LockMode 88 | { 89 | None, 90 | ReadLock, 91 | UpgradableReadLock, 92 | WriteLock 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Modbus.Proxy/Modbus.Proxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 9.0 6 | 7 | AMWD.Modbus.Proxy 8 | AMWD.Modbus.Proxy 9 | {semvertag:master} 10 | false 11 | false 12 | true 13 | true 14 | 1.1.1 15 | 16 | AMWD.Modbus.Proxy 17 | Modbus.Proxy 18 | Using AMWD.Modbus.TCP and AMWD.Modbus.Serial to build proxies. 19 | AM.WD 20 | Andreas Müller 21 | © {copyright:2018-} AM.WD 22 | https://github.com/AndreasAmMueller/Modbus.git 23 | MIT 24 | Modbus, TCP, Serial, RTU, Proxy 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Modbus.Proxy/ProxyDevice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | 5 | namespace AMWD.Modbus.Proxy 6 | { 7 | internal class ProxyDevice 8 | { 9 | #region Fields 10 | 11 | private readonly ReaderWriterLockSlim coilsLock = new(); 12 | private readonly ReaderWriterLockSlim discreteInputsLock = new(); 13 | private readonly ReaderWriterLockSlim inputRegistersLock = new(); 14 | private readonly ReaderWriterLockSlim holdingRegistersLock = new(); 15 | 16 | private readonly Dictionary coils = new(); 17 | private readonly Dictionary discreteInputs = new(); 18 | private readonly Dictionary inputRegisters = new(); 19 | private readonly Dictionary holdingRegisters = new(); 20 | 21 | #endregion Fields 22 | 23 | #region Coils 24 | 25 | public (DateTime Timestamp, bool Value) GetCoil(ushort address) 26 | { 27 | using (coilsLock.GetReadLock()) 28 | { 29 | if (coils.TryGetValue(address, out var value)) 30 | return value; 31 | } 32 | return (DateTime.UtcNow, false); 33 | } 34 | 35 | public void SetCoil(ushort address, bool value) 36 | { 37 | using (coilsLock.GetWriteLock()) 38 | { 39 | coils[address] = (DateTime.UtcNow, value); 40 | } 41 | } 42 | 43 | #endregion Coils 44 | 45 | #region Discrete Inputs 46 | 47 | public (DateTime Timestamp, bool Value) GetDiscreteInput(ushort address) 48 | { 49 | using (discreteInputsLock.GetReadLock()) 50 | { 51 | if (discreteInputs.TryGetValue(address, out var value)) 52 | return value; 53 | } 54 | return (DateTime.UtcNow, false); 55 | } 56 | 57 | public void SetDiscreteInput(ushort address, bool value) 58 | { 59 | using (discreteInputsLock.GetWriteLock()) 60 | { 61 | discreteInputs[address] = (DateTime.UtcNow, value); 62 | } 63 | } 64 | 65 | #endregion Discrete Inputs 66 | 67 | #region Input Registers 68 | 69 | public (DateTime Timestamp, ushort Value) GetInputRegister(ushort address) 70 | { 71 | using (inputRegistersLock.GetReadLock()) 72 | { 73 | if (inputRegisters.TryGetValue(address, out var value)) 74 | return value; 75 | } 76 | return (DateTime.UtcNow, 0); 77 | } 78 | 79 | public void SetInputRegister(ushort address, ushort value) 80 | { 81 | using (inputRegistersLock.GetWriteLock()) 82 | { 83 | inputRegisters[address] = (DateTime.UtcNow, value); 84 | } 85 | } 86 | 87 | #endregion Input Registers 88 | 89 | #region Holding Registers 90 | 91 | public (DateTime Timestamp, ushort Value) GetHoldingRegister(ushort address) 92 | { 93 | using (holdingRegistersLock.GetReadLock()) 94 | { 95 | if (holdingRegisters.TryGetValue(address, out var value)) 96 | return value; 97 | } 98 | return (DateTime.UtcNow, 0); 99 | } 100 | 101 | public void SetHoldingRegister(ushort address, ushort value) 102 | { 103 | using (holdingRegistersLock.GetWriteLock()) 104 | { 105 | holdingRegisters[address] = (DateTime.UtcNow, value); 106 | } 107 | } 108 | 109 | #endregion Holding Registers 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Enums.cs: -------------------------------------------------------------------------------- 1 | namespace AMWD.Modbus.Serial 2 | { 3 | /// 4 | /// Defines the baud rates for a serial connection. 5 | /// 6 | public enum BaudRate : int 7 | { 8 | /// 9 | /// 2400 Baud. 10 | /// 11 | Baud2400 = 2400, 12 | /// 13 | /// 4800 Baud. 14 | /// 15 | Baud4800 = 4800, 16 | /// 17 | /// 9600 Baud. 18 | /// 19 | Baud9600 = 9600, 20 | /// 21 | /// 19200 Baud. 22 | /// 23 | Baud19200 = 19200, 24 | /// 25 | /// 38400 Baud. 26 | /// 27 | Baud38400 = 38400, 28 | /// 29 | /// 57600 Baud. 30 | /// 31 | Baud57600 = 57600, 32 | /// 33 | /// 115200 Baud. 34 | /// 35 | Baud115200 = 115200 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Modbus.Serial.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 9.0 6 | 7 | AMWD.Modbus.Serial 8 | AMWD.Modbus.Serial 9 | {semvertag:master} 10 | false 11 | false 12 | true 13 | true 14 | 1.1.1 15 | 16 | AMWD.Modbus.Serial 17 | Modbus.Serial 18 | Small library to connect via Modbus RTU on remote devices. 19 | AM.WD 20 | Andreas Müller 21 | © {copyright:2018-} AM.WD 22 | https://github.com/AndreasAmMueller/Modbus.git 23 | MIT 24 | Modbus, Serial, RTU 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Protocol/Request.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using AMWD.Modbus.Common; 4 | using AMWD.Modbus.Common.Util; 5 | 6 | namespace AMWD.Modbus.Serial.Protocol 7 | { 8 | /// 9 | /// Represents the request from a client to the server. 10 | /// 11 | public class Request 12 | { 13 | #region Constructors 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// 19 | /// The transaction id is automatically set to a unique number. 20 | /// 21 | public Request() 22 | { } 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The serialized request from the client. 28 | public Request(byte[] bytes) 29 | { 30 | if (bytes?.Any() != true) 31 | throw new ArgumentNullException(nameof(bytes)); 32 | 33 | Deserialize(bytes); 34 | } 35 | 36 | #endregion Constructors 37 | 38 | #region Properties 39 | 40 | /// 41 | /// Gets the id to identify the device. 42 | /// 43 | public byte DeviceId { get; set; } 44 | 45 | /// 46 | /// Gets or sets the function code. 47 | /// 48 | public FunctionCode Function { get; set; } 49 | 50 | /// 51 | /// Gets or sets the (first) address. 52 | /// 53 | public ushort Address { get; set; } 54 | 55 | /// 56 | /// Gets or sets the number of elements. 57 | /// 58 | public ushort Count { get; set; } 59 | 60 | /// 61 | /// Gets or sets the data bytes. 62 | /// 63 | public byte[] Bytes 64 | { 65 | get { return Data?.Buffer ?? new byte[0]; } 66 | set { Data = new DataBuffer(value); } 67 | } 68 | 69 | /// 70 | /// Gets or sets the data. 71 | /// 72 | internal DataBuffer Data { get; set; } 73 | 74 | #region MODBUS Encapsulated Interface Transport 75 | 76 | /// 77 | /// Gets or sets the Encapsulated Interface type. 78 | /// Only needed on . 79 | /// 80 | public MEIType MEIType { get; set; } 81 | 82 | #region Device Information 83 | 84 | /// 85 | /// Gets or sets the Device ID code (category). 86 | /// Only needed on and . 87 | /// 88 | public DeviceIDCategory MEICategory { get; set; } 89 | 90 | /// 91 | /// Gets or sets the first Object ID to read. 92 | /// 93 | public DeviceIDObject MEIObject { get; set; } 94 | 95 | #endregion Device Information 96 | 97 | #endregion MODBUS Encapsulated Interface Transport 98 | 99 | #endregion Properties 100 | 101 | #region Serialization 102 | 103 | /// 104 | /// Serializes the request ready to send via serial. 105 | /// 106 | /// 107 | public byte[] Serialize() 108 | { 109 | var buffer = new DataBuffer(2); 110 | 111 | buffer.SetByte(0, DeviceId); 112 | buffer.SetByte(1, (byte)Function); 113 | 114 | switch (Function) 115 | { 116 | case FunctionCode.ReadCoils: 117 | case FunctionCode.ReadDiscreteInputs: 118 | case FunctionCode.ReadHoldingRegisters: 119 | case FunctionCode.ReadInputRegisters: 120 | buffer.AddUInt16(Address); 121 | buffer.AddUInt16(Count); 122 | break; 123 | case FunctionCode.WriteMultipleCoils: 124 | case FunctionCode.WriteMultipleRegisters: 125 | buffer.AddUInt16(Address); 126 | buffer.AddUInt16(Count); 127 | if (Data?.Length > 0) 128 | buffer.AddBytes(Data.Buffer); 129 | break; 130 | case FunctionCode.WriteSingleCoil: 131 | case FunctionCode.WriteSingleRegister: 132 | buffer.AddUInt16(Address); 133 | if (Data?.Length > 0) 134 | buffer.AddBytes(Data.Buffer); 135 | break; 136 | case FunctionCode.EncapsulatedInterface: 137 | buffer.AddByte((byte)MEIType); 138 | switch (MEIType) 139 | { 140 | case MEIType.CANOpenGeneralReference: 141 | if (Data?.Length > 0) 142 | buffer.AddBytes(Data.Buffer); 143 | break; 144 | case MEIType.ReadDeviceInformation: 145 | buffer.AddByte((byte)MEICategory); 146 | buffer.AddByte((byte)MEIObject); 147 | break; 148 | default: 149 | throw new NotImplementedException(); 150 | } 151 | break; 152 | default: 153 | throw new NotImplementedException(); 154 | } 155 | 156 | byte[] crc = Checksum.CRC16(buffer.Buffer); 157 | buffer.AddBytes(crc); 158 | 159 | return buffer.Buffer; 160 | } 161 | 162 | private void Deserialize(byte[] bytes) 163 | { 164 | var buffer = new DataBuffer(bytes); 165 | 166 | DeviceId = buffer.GetByte(0); 167 | Function = (FunctionCode)buffer.GetByte(1); 168 | 169 | byte[] crcBuff = buffer.GetBytes(buffer.Length - 3, 2); 170 | byte[] crcCalc = Checksum.CRC16(bytes, 0, bytes.Length - 2); 171 | 172 | if (crcBuff[0] != crcCalc[0] || crcBuff[1] != crcCalc[1]) 173 | throw new InvalidOperationException("Data not valid (CRC check failed)."); 174 | 175 | switch (Function) 176 | { 177 | case FunctionCode.ReadCoils: 178 | case FunctionCode.ReadDiscreteInputs: 179 | case FunctionCode.ReadHoldingRegisters: 180 | case FunctionCode.ReadInputRegisters: 181 | Address = buffer.GetUInt16(2); 182 | Count = buffer.GetUInt16(4); 183 | break; 184 | case FunctionCode.WriteMultipleCoils: 185 | case FunctionCode.WriteMultipleRegisters: 186 | Address = buffer.GetUInt16(2); 187 | Count = buffer.GetUInt16(4); 188 | Data = new DataBuffer(buffer.GetBytes(6, buffer.Length - 8)); 189 | break; 190 | case FunctionCode.WriteSingleCoil: 191 | case FunctionCode.WriteSingleRegister: 192 | Address = buffer.GetUInt16(2); 193 | Data = new DataBuffer(buffer.GetBytes(4, buffer.Length - 6)); 194 | break; 195 | case FunctionCode.EncapsulatedInterface: 196 | MEIType = (MEIType)buffer.GetByte(8); 197 | switch (MEIType) 198 | { 199 | case MEIType.CANOpenGeneralReference: 200 | Data = new DataBuffer(buffer.Buffer.Skip(9).ToArray()); 201 | break; 202 | case MEIType.ReadDeviceInformation: 203 | MEICategory = (DeviceIDCategory)buffer.GetByte(9); 204 | MEIObject = (DeviceIDObject)buffer.GetByte(10); 205 | break; 206 | default: 207 | throw new NotImplementedException($"Unknown MEI type: {MEIType}"); 208 | } 209 | break; 210 | default: 211 | throw new NotImplementedException($"Unknown function code: {Function}"); 212 | } 213 | } 214 | 215 | #endregion Serialization 216 | 217 | #region Overrides 218 | 219 | /// 220 | public override string ToString() 221 | => $"Request | Device#{DeviceId}, Fn: {Function}, Address: {Address}, Count: {Count} | {string.Join(" ", Bytes.Select(b => b.ToString("X2")))}"; 222 | 223 | /// 224 | public override int GetHashCode() 225 | => base.GetHashCode() ^ 226 | DeviceId.GetHashCode() ^ 227 | Function.GetHashCode() ^ 228 | Address.GetHashCode() ^ 229 | Count.GetHashCode() ^ 230 | Bytes.GetHashCode(); 231 | 232 | /// 233 | public override bool Equals(object obj) 234 | { 235 | if (obj is not Request req) 236 | return false; 237 | 238 | return req.DeviceId == DeviceId && 239 | req.Function == Function && 240 | req.Address == Address && 241 | req.Count == Count && 242 | Data.Equals(req.Data); 243 | } 244 | 245 | #endregion Overrides 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Protocol/Response.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using AMWD.Modbus.Common; 4 | using AMWD.Modbus.Common.Util; 5 | using AMWD.Modbus.Serial.Util; 6 | 7 | namespace AMWD.Modbus.Serial.Protocol 8 | { 9 | /// 10 | /// Represents the response from the server to a client. 11 | /// 12 | public class Response 13 | { 14 | #region Constructors 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The corresponding request. 20 | public Response(Request request) 21 | { 22 | DeviceId = request.DeviceId; 23 | Function = request.Function; 24 | Address = request.Address; 25 | Count = request.Count; 26 | } 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The serialized response. 32 | public Response(byte[] bytes) 33 | { 34 | if (bytes?.Any() != true) 35 | throw new ArgumentNullException(nameof(bytes)); 36 | 37 | Deserialize(bytes); 38 | } 39 | 40 | #endregion Constructors 41 | 42 | #region Properties 43 | 44 | /// 45 | /// Gets the id to identify the device. 46 | /// 47 | public byte DeviceId { get; private set; } 48 | 49 | /// 50 | /// Gets the function code. 51 | /// 52 | public FunctionCode Function { get; private set; } 53 | 54 | /// 55 | /// Gets a value indicating whether an error occurred. 56 | /// 57 | public bool IsError => ErrorCode > 0; 58 | 59 | /// 60 | /// Gets or sets the error/exception code. 61 | /// 62 | public ErrorCode ErrorCode { get; set; } 63 | 64 | /// 65 | /// Gets the error message. 66 | /// 67 | public string ErrorMessage => ErrorCode.GetDescription(); 68 | 69 | /// 70 | /// Gets or sets the register address. 71 | /// 72 | public ushort Address { get; set; } 73 | 74 | /// 75 | /// Gets or sets the number of registers. 76 | /// 77 | public ushort Count { get; set; } 78 | 79 | /// 80 | /// Gets or sets the data. 81 | /// 82 | public DataBuffer Data { get; set; } 83 | 84 | /// 85 | /// Gets a value indicating whether the response is a result of an timeout. 86 | /// 87 | public bool IsTimeout { get; private set; } 88 | 89 | #region MODBUS Encapsulated Interface Transport 90 | 91 | /// 92 | /// Gets or sets the Encapsulated Interface type. 93 | /// Only needed on . 94 | /// 95 | public MEIType MEIType { get; set; } 96 | 97 | #region Device Information 98 | 99 | /// 100 | /// Gets or sets the Device ID code (category). 101 | /// Only needed on and . 102 | /// 103 | public DeviceIDCategory MEICategory { get; set; } 104 | 105 | /// 106 | /// Gets or sets the first Object ID to read. 107 | /// 108 | public DeviceIDObject MEIObject { get; set; } 109 | 110 | /// 111 | /// Gets or sets the conformity level of the device information. 112 | /// 113 | public byte ConformityLevel { get; set; } 114 | 115 | /// 116 | /// Gets or sets a value indicating whether further requests are needed to gather all device information. 117 | /// 118 | public bool MoreRequestsNeeded { get; set; } 119 | 120 | /// 121 | /// Gets or sets the object id to start with the next request. 122 | /// 123 | public byte NextObjectId { get; set; } 124 | 125 | /// 126 | /// Gets or sets the number of objects in list (appending). 127 | /// 128 | public byte ObjectCount { get; set; } 129 | 130 | #endregion Device Information 131 | 132 | #endregion MODBUS Encapsulated Interface Transport 133 | 134 | #endregion Properties 135 | 136 | #region Serialization 137 | 138 | /// 139 | /// Serializes the response to send. 140 | /// 141 | /// 142 | public byte[] Serialize() 143 | { 144 | var buffer = new DataBuffer(2); 145 | 146 | buffer.SetByte(0, DeviceId); 147 | 148 | byte fn = (byte)Function; 149 | if (IsError) 150 | { 151 | fn = (byte)(fn & Consts.ErrorMask); 152 | buffer.AddByte((byte)ErrorCode); 153 | } 154 | else 155 | { 156 | switch (Function) 157 | { 158 | case FunctionCode.ReadCoils: 159 | case FunctionCode.ReadDiscreteInputs: 160 | case FunctionCode.ReadHoldingRegisters: 161 | case FunctionCode.ReadInputRegisters: 162 | buffer.AddByte((byte)Data.Length); 163 | buffer.AddBytes(Data.Buffer); 164 | break; 165 | case FunctionCode.WriteMultipleCoils: 166 | case FunctionCode.WriteMultipleRegisters: 167 | buffer.AddUInt16(Address); 168 | buffer.AddUInt16(Count); 169 | break; 170 | case FunctionCode.WriteSingleCoil: 171 | case FunctionCode.WriteSingleRegister: 172 | buffer.AddUInt16(Address); 173 | buffer.AddBytes(Data.Buffer); 174 | break; 175 | case FunctionCode.EncapsulatedInterface: 176 | buffer.AddByte((byte)MEIType); 177 | switch (MEIType) 178 | { 179 | case MEIType.CANOpenGeneralReference: 180 | if (Data?.Length > 0) 181 | buffer.AddBytes(Data.Buffer); 182 | break; 183 | case MEIType.ReadDeviceInformation: 184 | buffer.AddByte((byte)MEICategory); 185 | buffer.AddByte(ConformityLevel); 186 | buffer.AddByte((byte)(MoreRequestsNeeded ? 0xFF : 0x00)); 187 | buffer.AddByte(NextObjectId); 188 | buffer.AddByte(ObjectCount); 189 | buffer.AddBytes(Data.Buffer); 190 | break; 191 | default: 192 | throw new NotImplementedException(); 193 | } 194 | break; 195 | default: 196 | throw new NotImplementedException(); 197 | } 198 | } 199 | 200 | buffer.SetByte(1, fn); 201 | 202 | byte[] crc = Checksum.CRC16(buffer.Buffer); 203 | buffer.AddBytes(crc); 204 | 205 | return buffer.Buffer; 206 | } 207 | 208 | private void Deserialize(byte[] bytes) 209 | { 210 | // Response timed out => device not available 211 | if (bytes.All(b => b == 0)) 212 | { 213 | IsTimeout = true; 214 | return; 215 | } 216 | 217 | var buffer = new DataBuffer(bytes); 218 | 219 | byte[] crcBuff = buffer.GetBytes(buffer.Length - 2, 2); 220 | byte[] crcCalc = Checksum.CRC16(bytes, 0, bytes.Length - 2); 221 | 222 | if (crcBuff[0] != crcCalc[0] || crcBuff[1] != crcCalc[1]) 223 | throw new InvalidOperationException("Data not valid (CRC check failed)."); 224 | 225 | DeviceId = buffer.GetByte(0); 226 | 227 | byte fn = buffer.GetByte(1); 228 | if ((fn & Consts.ErrorMask) > 0) 229 | { 230 | Function = (FunctionCode)(fn ^ Consts.ErrorMask); 231 | ErrorCode = (ErrorCode)buffer.GetByte(2); 232 | } 233 | else 234 | { 235 | Function = (FunctionCode)fn; 236 | 237 | switch (Function) 238 | { 239 | case FunctionCode.ReadCoils: 240 | case FunctionCode.ReadDiscreteInputs: 241 | case FunctionCode.ReadHoldingRegisters: 242 | case FunctionCode.ReadInputRegisters: 243 | byte length = buffer.GetByte(2); 244 | // following bytes + 3 byte head + 2 byte CRC 245 | if (buffer.Length < length + 5) 246 | throw new ArgumentException("Too less data."); 247 | if (buffer.Length > length + 5) 248 | { 249 | if (buffer.Buffer.Skip(length + 5).Any(b => b != 0)) 250 | throw new ArgumentException("Too many data."); 251 | 252 | buffer = new DataBuffer(bytes.Take(length + 5)); 253 | } 254 | Data = new DataBuffer(buffer.GetBytes(3, buffer.Length - 5)); 255 | break; 256 | case FunctionCode.WriteMultipleCoils: 257 | case FunctionCode.WriteMultipleRegisters: 258 | Address = buffer.GetUInt16(2); 259 | Count = buffer.GetUInt16(4); 260 | break; 261 | case FunctionCode.WriteSingleCoil: 262 | case FunctionCode.WriteSingleRegister: 263 | Address = buffer.GetUInt16(2); 264 | Data = new DataBuffer(buffer.GetBytes(4, buffer.Length - 6)); 265 | break; 266 | case FunctionCode.EncapsulatedInterface: 267 | MEIType = (MEIType)buffer.GetByte(2); 268 | switch (MEIType) 269 | { 270 | case MEIType.CANOpenGeneralReference: 271 | Data = new DataBuffer(buffer.Buffer.Skip(3).ToArray()); 272 | break; 273 | case MEIType.ReadDeviceInformation: 274 | MEICategory = (DeviceIDCategory)buffer.GetByte(3); 275 | ConformityLevel = buffer.GetByte(4); 276 | MoreRequestsNeeded = buffer.GetByte(5) > 0; 277 | NextObjectId = buffer.GetByte(6); 278 | ObjectCount = buffer.GetByte(7); 279 | Data = new DataBuffer(buffer.Buffer.Skip(8).ToArray()); 280 | break; 281 | default: 282 | throw new NotImplementedException($"Unknown MEI type: {MEIType}"); 283 | } 284 | break; 285 | default: 286 | throw new NotImplementedException($"Unknown function code: {Function}"); 287 | } 288 | } 289 | } 290 | 291 | #endregion Serialization 292 | 293 | #region Overrides 294 | 295 | /// 296 | public override string ToString() 297 | => $"Response | Device#{DeviceId}, Fn: {Function}, Error: {IsError}, Address: {Address}, Count: {Count} | {string.Join(" ", Data.Buffer.Select(b => b.ToString("X2")))}"; 298 | 299 | /// 300 | public override int GetHashCode() 301 | => base.GetHashCode() ^ 302 | DeviceId.GetHashCode() ^ 303 | Function.GetHashCode() ^ 304 | Address.GetHashCode() ^ 305 | Count.GetHashCode() ^ 306 | Data.GetHashCode(); 307 | 308 | /// 309 | public override bool Equals(object obj) 310 | { 311 | if (obj is not Response res) 312 | return false; 313 | 314 | return res.DeviceId == DeviceId && 315 | res.Function == Function && 316 | res.Address == Address && 317 | res.Count == Count && 318 | Data.Equals(res.Data); 319 | } 320 | 321 | #endregion Overrides 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Util/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using System.IO.Ports; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace AMWD.Modbus.Serial.Util 11 | { 12 | /// 13 | /// Contains some extensions to handle some features more easily. 14 | /// 15 | internal static class Extensions 16 | { 17 | #region Enums 18 | 19 | /// 20 | /// Tries to return an attribute of an enum value. 21 | /// 22 | /// The attribute type. 23 | /// The enum value. 24 | /// The first attribute of the type present or null. 25 | public static T GetAttribute(this Enum enumValue) 26 | where T : Attribute 27 | { 28 | if (enumValue != null) 29 | { 30 | var fi = enumValue.GetType().GetField(enumValue.ToString()); 31 | var attrs = (T[])fi?.GetCustomAttributes(typeof(T), inherit: false); 32 | return attrs?.FirstOrDefault(); 33 | } 34 | return default; 35 | } 36 | 37 | /// 38 | /// Tries to read the description of an enum value. 39 | /// 40 | /// The enum value. 41 | /// The description or the 42 | public static string GetDescription(this Enum enumValue) 43 | => enumValue.GetAttribute()?.Description ?? enumValue.ToString(); 44 | 45 | #endregion Enums 46 | 47 | #region Task handling 48 | 49 | /// 50 | /// Forgets about the result of the task. (Prevent compiler warning). 51 | /// 52 | /// The task to forget. 53 | public static async void Forget(this Task task) 54 | { 55 | try 56 | { 57 | await task; 58 | } 59 | catch 60 | { /* Task forgotten, so keep everything quiet. */ } 61 | } 62 | 63 | #endregion Task handling 64 | 65 | #region Async fixes 66 | 67 | // Inspired by: https://stackoverflow.com/a/54610437/11906695 68 | public static async Task ReadAsync(this SerialPort serialPort, byte[] buffer, int offset, int count, CancellationToken cancellationToken) 69 | { 70 | // serial port read/write timeouts seem to be ignored, so ensure the timeouts. 71 | using (var cts = new CancellationTokenSource(serialPort.ReadTimeout)) 72 | using (cancellationToken.Register(() => cts.Cancel())) 73 | { 74 | var ctr = default(CancellationTokenRegistration); 75 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 76 | { 77 | // The async stream implementation on windows is a bit broken. 78 | // this kicks it back to us. 79 | ctr = cts.Token.Register(() => serialPort.DiscardInBuffer()); 80 | } 81 | 82 | try 83 | { 84 | return await serialPort.BaseStream.ReadAsync(buffer, offset, count, cts.Token); 85 | } 86 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 87 | { 88 | cancellationToken.ThrowIfCancellationRequested(); 89 | return 0; 90 | } 91 | catch (OperationCanceledException) when (cts.IsCancellationRequested) 92 | { 93 | throw new TimeoutException("No bytes to read within the ReadTimeout."); 94 | } 95 | catch (IOException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) 96 | { 97 | throw new TimeoutException("No bytes to read within the ReadTimeout."); 98 | } 99 | finally 100 | { 101 | ctr.Dispose(); 102 | } 103 | } 104 | } 105 | 106 | public static async Task WriteAsync(this SerialPort serialPort, byte[] buffer, int offset, int count, CancellationToken cancellationToken) 107 | { 108 | // serial port read/write timeouts seem to be ignored, so ensure the timeouts. 109 | using (var cts = new CancellationTokenSource(serialPort.WriteTimeout)) 110 | using (cancellationToken.Register(() => cts.Cancel())) 111 | { 112 | var ctr = default(CancellationTokenRegistration); 113 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 114 | { 115 | // The async stream implementation on windows is a bit broken. 116 | // this kicks it back to us. 117 | ctr = cts.Token.Register(() => serialPort.DiscardOutBuffer()); 118 | } 119 | 120 | try 121 | { 122 | await serialPort.BaseStream.WriteAsync(buffer, offset, count, cts.Token); 123 | } 124 | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) 125 | { 126 | cancellationToken.ThrowIfCancellationRequested(); 127 | } 128 | catch (OperationCanceledException) when (cts.IsCancellationRequested) 129 | { 130 | throw new TimeoutException("No bytes written within the WriteTimeout."); 131 | } 132 | catch (IOException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) 133 | { 134 | throw new TimeoutException("No bytes written within the WriteTimeout."); 135 | } 136 | finally 137 | { 138 | ctr.Dispose(); 139 | } 140 | } 141 | } 142 | 143 | #endregion Async fixes 144 | 145 | #region Exception 146 | 147 | public static string GetMessage(this Exception exception) 148 | => exception.InnerException?.Message ?? exception.Message; 149 | 150 | #endregion Exception 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Util/RequestTask.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using AMWD.Modbus.Serial.Protocol; 4 | 5 | namespace AMWD.Modbus.Serial.Util 6 | { 7 | /// 8 | /// Implements a structure to enqueue a request to perform including the option to cancel. 9 | /// 10 | internal class RequestTask 11 | { 12 | /// 13 | /// Gets or sets the enqueued request. 14 | /// 15 | public Request Request { get; set; } 16 | 17 | /// 18 | /// Gets or sets the task completion source to resolve when the request is done. 19 | /// 20 | public TaskCompletionSource TaskCompletionSource { get; set; } 21 | 22 | /// 23 | /// Gets or sets the registration to cancel this request. 24 | /// 25 | public CancellationTokenRegistration Registration { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Util/SafeUnixHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ConstrainedExecution; 3 | using System.Runtime.InteropServices; 4 | using System.Security.Permissions; 5 | 6 | namespace AMWD.Modbus.Serial.Util 7 | { 8 | /// 9 | /// Implements a safe handle for unix systems. 10 | /// Found on https://stackoverflow.com/a/10388107 11 | /// 12 | [SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)] 13 | [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)] 14 | internal sealed class SafeUnixHandle : SafeHandle 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 20 | private SafeUnixHandle() 21 | : base(new IntPtr(-1), true) 22 | { } 23 | 24 | /// 25 | public override bool IsInvalid 26 | { 27 | get { return handle == new IntPtr(-1); } 28 | } 29 | 30 | /// 31 | [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 32 | protected override bool ReleaseHandle() 33 | { 34 | return UnsafeNativeMethods.Close(handle) != -1; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Util/SerialRS485.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace AMWD.Modbus.Serial.Util 5 | { 6 | /// 7 | /// Represents the structure of the driver settings for RS485. 8 | /// 9 | [StructLayout(LayoutKind.Sequential, Size = 32)] 10 | internal struct SerialRS485 11 | { 12 | /// 13 | /// The flags to change the driver state. 14 | /// 15 | public RS485Flags Flags; 16 | 17 | /// 18 | /// The delay in milliseconds before send. 19 | /// 20 | public uint RtsDelayBeforeSend; 21 | 22 | /// 23 | /// The delay in milliseconds after send. 24 | /// 25 | public uint RtsDelayAfterSend; 26 | } 27 | 28 | /// 29 | /// The flags for the driver state. 30 | /// 31 | [Flags] 32 | internal enum RS485Flags : uint 33 | { 34 | /// 35 | /// RS485 is enabled. 36 | /// 37 | Enabled = 1, 38 | /// 39 | /// RS485 uses RTS on send. 40 | /// 41 | RtsOnSend = 2, 42 | /// 43 | /// RS485 uses RTS after send. 44 | /// 45 | RtsAfterSend = 4, 46 | /// 47 | /// Receive during send (duplex). 48 | /// 49 | RxDuringTx = 16 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Util/UnixIOException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Runtime.Serialization; 4 | using System.Security.Permissions; 5 | 6 | namespace AMWD.Modbus.Serial.Util 7 | { 8 | /// 9 | /// Represents a unix specific IO exception. 10 | /// Found on https://stackoverflow.com/a/10388107 11 | /// 12 | [Serializable] 13 | public class UnixIOException : ExternalException 14 | { 15 | /// 16 | /// Initializes a new instance of a class. 17 | /// 18 | [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)] 19 | public UnixIOException() 20 | : this(Marshal.GetLastWin32Error()) 21 | { } 22 | 23 | /// 24 | /// Initializes a new instance of a class. 25 | /// 26 | /// The error number. 27 | [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)] 28 | public UnixIOException(int error) 29 | : this(error, GetErrorMessage(error)) 30 | { } 31 | 32 | /// 33 | /// Initializes a new instance of a class. 34 | /// 35 | /// The error message. 36 | [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)] 37 | public UnixIOException(string message) 38 | : this(Marshal.GetLastWin32Error(), message) 39 | { } 40 | 41 | /// 42 | /// Initializes a new instance of a class. 43 | /// 44 | /// The error number. 45 | /// The error message. 46 | public UnixIOException(int error, string message) 47 | : base(message) 48 | { 49 | NativeErrorCode = error; 50 | } 51 | 52 | /// 53 | /// Initializes a new instance of a class. 54 | /// 55 | /// The error message. 56 | /// An inner exception. 57 | public UnixIOException(string message, Exception innerException) 58 | : base(message, innerException) 59 | { } 60 | 61 | /// 62 | /// Initializes a new instance of a class. 63 | /// 64 | /// The serialization information. 65 | /// The stream context. 66 | protected UnixIOException(SerializationInfo info, StreamingContext context) 67 | : base(info, context) 68 | { 69 | NativeErrorCode = info.GetInt32("NativeErrorCode"); 70 | } 71 | 72 | /// 73 | /// Gets the native error code set by the unix system. 74 | /// 75 | public int NativeErrorCode { get; } 76 | 77 | /// 78 | public override void GetObjectData(SerializationInfo info, StreamingContext context) 79 | { 80 | if (info == null) 81 | throw new ArgumentNullException(nameof(info)); 82 | 83 | info.AddValue("NativeErrorCode", NativeErrorCode); 84 | base.GetObjectData(info, context); 85 | } 86 | 87 | private static string GetErrorMessage(int errno) 88 | { 89 | try 90 | { 91 | var ptr = UnsafeNativeMethods.StrError(errno); 92 | return Marshal.PtrToStringAnsi(ptr); 93 | } 94 | catch 95 | { 96 | return $"Unknown error (0x{errno:x})"; 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Modbus.Serial/Util/UnsafeNativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.ConstrainedExecution; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace AMWD.Modbus.Serial.Util 6 | { 7 | /// 8 | /// Definitions of the unsafe system methods. 9 | /// Found on https://stackoverflow.com/a/10388107 10 | /// 11 | internal static class UnsafeNativeMethods 12 | { 13 | /// 14 | /// A flag for . 15 | /// 16 | internal const int O_RDWR = 2; 17 | /// 18 | /// A flag for . 19 | /// 20 | internal const int O_NOCTTY = 256; 21 | /// 22 | /// A flag for . 23 | /// 24 | internal const uint TIOCGRS485 = 0x542E; 25 | /// 26 | /// A flag for . 27 | /// 28 | internal const uint TIOCSRS485 = 0x542F; 29 | 30 | /// 31 | /// Opens a handle to a defined path (serial port). 32 | /// 33 | /// The path to open the handle. 34 | /// The flags for the handle. 35 | /// 36 | [DllImport("libc", EntryPoint = "open", SetLastError = true)] 37 | internal static extern SafeUnixHandle Open(string path, uint flag); 38 | 39 | /// 40 | /// Performs an ioctl request to the open handle. 41 | /// 42 | /// The handle. 43 | /// The request. 44 | /// The data structure to read / write. 45 | /// 46 | [DllImport("libc", EntryPoint = "ioctl", SetLastError = true)] 47 | internal static extern int IoCtl(SafeUnixHandle handle, uint request, ref SerialRS485 serialRs485); 48 | 49 | /// 50 | /// Closes an open handle. 51 | /// 52 | /// The handle. 53 | /// 54 | [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 55 | [DllImport("libc", EntryPoint = "close", SetLastError = true)] 56 | internal static extern int Close(IntPtr handle); 57 | 58 | /// 59 | /// Converts the given error number (errno) into a readable string. 60 | /// 61 | /// The error number (errno). 62 | /// 63 | [DllImport("libc", EntryPoint = "strerror", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] 64 | internal static extern IntPtr StrError(int errno); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Modbus.Tcp/Modbus.Tcp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 9.0 6 | 7 | AMWD.Modbus.Tcp 8 | AMWD.Modbus.Tcp 9 | {semvertag:master} 10 | false 11 | false 12 | true 13 | true 14 | 1.1.1 15 | 16 | AMWD.Modbus.Tcp 17 | Modbus.TCP 18 | Small library to connect via Modbus TCP on remote devices. 19 | AM.WD 20 | Andreas Müller 21 | © {copyright:2018-} AM.WD 22 | https://github.com/AndreasAmMueller/Modbus.git 23 | MIT 24 | Modbus, TCP 25 | true 26 | snupkg 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Modbus.Tcp/Protocol/Request.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AMWD.Modbus.Common; 5 | using AMWD.Modbus.Common.Util; 6 | 7 | namespace AMWD.Modbus.Tcp.Protocol 8 | { 9 | /// 10 | /// Represents the request from a client to the server. 11 | /// 12 | public class Request 13 | { 14 | #region Constructors 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// 20 | /// The transaction id is automatically set to a unique number. 21 | /// 22 | public Request() 23 | { } 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The serialized request from the client. 29 | public Request(IEnumerable bytes) 30 | { 31 | if (bytes == null) 32 | throw new ArgumentNullException(nameof(bytes)); 33 | 34 | Deserialize(bytes); 35 | } 36 | 37 | #endregion Constructors 38 | 39 | #region Properties 40 | 41 | /// 42 | /// Gets the unique transaction id of the request. 43 | /// 44 | public ushort TransactionId { get; set; } 45 | 46 | /// 47 | /// Gets the id to identify the device. 48 | /// 49 | public byte DeviceId { get; set; } 50 | 51 | /// 52 | /// Gets or sets the function code. 53 | /// 54 | public FunctionCode Function { get; set; } 55 | 56 | /// 57 | /// Gets or sets the (first) address. 58 | /// 59 | public ushort Address { get; set; } 60 | 61 | /// 62 | /// Gets or sets the number of elements. 63 | /// 64 | public ushort Count { get; set; } 65 | 66 | /// 67 | /// Gets or sets the data bytes. 68 | /// 69 | public byte[] Bytes 70 | { 71 | get { return Data?.Buffer ?? new byte[0]; } 72 | set { Data = new DataBuffer(value); } 73 | } 74 | 75 | /// 76 | /// Gets or sets the data. 77 | /// 78 | public DataBuffer Data { get; set; } 79 | 80 | #region MODBUS Encapsulated Interface Transport 81 | 82 | /// 83 | /// Gets or sets the Encapsulated Interface type. 84 | /// Only needed on . 85 | /// 86 | public MEIType MEIType { get; set; } 87 | 88 | #region Device Information 89 | 90 | /// 91 | /// Gets or sets the Device ID code (category). 92 | /// Only needed on and . 93 | /// 94 | public DeviceIDCategory MEICategory { get; set; } 95 | 96 | /// 97 | /// Gets or sets the first Object ID to read. 98 | /// 99 | public DeviceIDObject MEIObject { get; set; } 100 | 101 | #endregion Device Information 102 | 103 | #endregion MODBUS Encapsulated Interface Transport 104 | 105 | #endregion Properties 106 | 107 | #region Serialization 108 | 109 | /// 110 | /// Serializes the request ready to send via tcp. 111 | /// 112 | /// 113 | public byte[] Serialize() 114 | { 115 | var buffer = new DataBuffer(8); 116 | 117 | buffer.SetUInt16(0, TransactionId); 118 | buffer.SetUInt16(2, 0x0000); // Protocol ID 119 | 120 | buffer.SetByte(6, DeviceId); 121 | buffer.SetByte(7, (byte)Function); 122 | 123 | switch (Function) 124 | { 125 | case FunctionCode.ReadCoils: 126 | case FunctionCode.ReadDiscreteInputs: 127 | case FunctionCode.ReadHoldingRegisters: 128 | case FunctionCode.ReadInputRegisters: 129 | buffer.AddUInt16(Address); 130 | buffer.AddUInt16(Count); 131 | break; 132 | case FunctionCode.WriteMultipleCoils: 133 | case FunctionCode.WriteMultipleRegisters: 134 | buffer.AddUInt16(Address); 135 | buffer.AddUInt16(Count); 136 | buffer.AddByte((byte)(Data?.Length ?? 0)); 137 | if (Data?.Length > 0) 138 | buffer.AddBytes(Data.Buffer); 139 | break; 140 | case FunctionCode.WriteSingleCoil: 141 | case FunctionCode.WriteSingleRegister: 142 | buffer.AddUInt16(Address); 143 | if (Data?.Length > 0) 144 | buffer.AddBytes(Data.Buffer); 145 | break; 146 | case FunctionCode.EncapsulatedInterface: 147 | buffer.AddByte((byte)MEIType); 148 | switch (MEIType) 149 | { 150 | case MEIType.CANOpenGeneralReference: 151 | if (Data?.Length > 0) 152 | buffer.AddBytes(Data.Buffer); 153 | break; 154 | case MEIType.ReadDeviceInformation: 155 | buffer.AddByte((byte)MEICategory); 156 | buffer.AddByte((byte)MEIObject); 157 | break; 158 | default: 159 | throw new NotImplementedException(); 160 | } 161 | break; 162 | default: 163 | throw new NotImplementedException(); 164 | } 165 | 166 | int len = buffer.Length - 6; 167 | buffer.SetUInt16(4, (ushort)len); 168 | 169 | return buffer.Buffer; 170 | } 171 | 172 | private void Deserialize(IEnumerable bytes) 173 | { 174 | var buffer = new DataBuffer(bytes); 175 | 176 | TransactionId = buffer.GetUInt16(0); 177 | ushort ident = buffer.GetUInt16(2); 178 | if (ident != 0) 179 | throw new ArgumentException("Protocol identifier not valid."); 180 | 181 | ushort length = buffer.GetUInt16(4); 182 | if (buffer.Length < length + 6) 183 | throw new ArgumentException("Too less data."); 184 | 185 | if (buffer.Length > length + 6) 186 | { 187 | if (buffer.Buffer.Skip(length + 6).Any(b => b != 0)) 188 | throw new ArgumentException("Too many data."); 189 | 190 | buffer = new DataBuffer(bytes.Take(length + 6)); 191 | } 192 | 193 | DeviceId = buffer.GetByte(6); 194 | Function = (FunctionCode)buffer.GetByte(7); 195 | 196 | switch (Function) 197 | { 198 | case FunctionCode.ReadCoils: 199 | case FunctionCode.ReadDiscreteInputs: 200 | case FunctionCode.ReadHoldingRegisters: 201 | case FunctionCode.ReadInputRegisters: 202 | Address = buffer.GetUInt16(8); 203 | Count = buffer.GetUInt16(10); 204 | break; 205 | case FunctionCode.WriteMultipleCoils: 206 | case FunctionCode.WriteMultipleRegisters: 207 | Address = buffer.GetUInt16(8); 208 | Count = buffer.GetUInt16(10); 209 | Data = new DataBuffer(buffer.Buffer.Skip(12)); 210 | break; 211 | case FunctionCode.WriteSingleCoil: 212 | case FunctionCode.WriteSingleRegister: 213 | Address = buffer.GetUInt16(8); 214 | Data = new DataBuffer(buffer.Buffer.Skip(10)); 215 | break; 216 | case FunctionCode.EncapsulatedInterface: 217 | MEIType = (MEIType)buffer.GetByte(8); 218 | switch (MEIType) 219 | { 220 | case MEIType.CANOpenGeneralReference: 221 | Data = new DataBuffer(buffer.Buffer.Skip(9)); 222 | break; 223 | case MEIType.ReadDeviceInformation: 224 | MEICategory = (DeviceIDCategory)buffer.GetByte(9); 225 | MEIObject = (DeviceIDObject)buffer.GetByte(10); 226 | break; 227 | default: 228 | throw new NotImplementedException($"Unknown MEI type: {MEIType}."); 229 | } 230 | break; 231 | default: 232 | throw new NotImplementedException($"Unknown function code: {Function}."); 233 | } 234 | } 235 | 236 | #endregion Serialization 237 | 238 | #region Overrides 239 | 240 | /// 241 | public override string ToString() 242 | => $"Request#{TransactionId} | Device#{DeviceId}, Fn: {Function}, Address: {Address}, Count: {Count} | {string.Join(" ", Bytes.Select(b => b.ToString("X2")))}"; 243 | 244 | /// 245 | public override int GetHashCode() 246 | => base.GetHashCode() ^ 247 | TransactionId.GetHashCode() ^ 248 | DeviceId.GetHashCode() ^ 249 | Function.GetHashCode() ^ 250 | Address.GetHashCode() ^ 251 | Count.GetHashCode() ^ 252 | Bytes.GetHashCode(); 253 | 254 | /// 255 | public override bool Equals(object obj) 256 | { 257 | if (obj is not Request req) 258 | return false; 259 | 260 | return req.TransactionId == TransactionId && 261 | req.DeviceId == DeviceId && 262 | req.Function == Function && 263 | req.Address == Address && 264 | req.Count == Count && 265 | Data.Equals(req.Data); 266 | } 267 | 268 | #endregion Overrides 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Modbus.Tcp/Protocol/Response.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using AMWD.Modbus.Common; 5 | using AMWD.Modbus.Common.Util; 6 | using AMWD.Modbus.Tcp.Util; 7 | 8 | namespace AMWD.Modbus.Tcp.Protocol 9 | { 10 | /// 11 | /// Represents the response from the server to a client. 12 | /// 13 | public class Response 14 | { 15 | #region Constructors 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The corresponding request. 21 | public Response(Request request) 22 | { 23 | TransactionId = request.TransactionId; 24 | DeviceId = request.DeviceId; 25 | Function = request.Function; 26 | Address = request.Address; 27 | Count = request.Count; 28 | Data = new DataBuffer(); 29 | } 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The serialized response. 35 | public Response(byte[] response) 36 | { 37 | Deserialize(response); 38 | } 39 | 40 | #endregion Constructors 41 | 42 | #region Properties 43 | 44 | /// 45 | /// Gets the unique transaction id. 46 | /// 47 | public ushort TransactionId { get; private set; } 48 | 49 | /// 50 | /// Gets the id to identify the device. 51 | /// 52 | public byte DeviceId { get; private set; } 53 | 54 | /// 55 | /// Gets the function code. 56 | /// 57 | public FunctionCode Function { get; private set; } 58 | 59 | /// 60 | /// Gets a value indicating whether an error occurred. 61 | /// 62 | public bool IsError => ErrorCode > 0; 63 | 64 | /// 65 | /// Gets or sets the error/exception code. 66 | /// 67 | public ErrorCode ErrorCode { get; set; } 68 | 69 | /// 70 | /// Gets the error message. 71 | /// 72 | public string ErrorMessage => ErrorCode.GetDescription(); 73 | 74 | /// 75 | /// Gets or sets the register address. 76 | /// 77 | public ushort Address { get; set; } 78 | 79 | /// 80 | /// Gets or sets the number of registers. 81 | /// 82 | public ushort Count { get; set; } 83 | 84 | /// 85 | /// Gets or sets the data. 86 | /// 87 | public DataBuffer Data { get; set; } 88 | 89 | /// 90 | /// Gets a value indicating whether the response timed out. 91 | /// 92 | public bool IsTimeout { get; private set; } 93 | 94 | #region MODBUS Encapsulated Interface Transport 95 | 96 | /// 97 | /// Gets or sets the Encapsulated Interface type. 98 | /// Only needed on . 99 | /// 100 | public MEIType MEIType { get; set; } 101 | 102 | #region Device Information 103 | 104 | /// 105 | /// Gets or sets the Device ID code (category). 106 | /// Only needed on and . 107 | /// 108 | public DeviceIDCategory MEICategory { get; set; } 109 | 110 | /// 111 | /// Gets or sets the first Object ID to read. 112 | /// 113 | public DeviceIDObject MEIObject { get; set; } 114 | 115 | /// 116 | /// Gets or sets the conformity level of the device information. 117 | /// 118 | public byte ConformityLevel { get; set; } 119 | 120 | /// 121 | /// Gets or sets a value indicating whether further requests are needed to gather all device information. 122 | /// 123 | public bool MoreRequestsNeeded { get; set; } 124 | 125 | /// 126 | /// Gets or sets the object id to start with the next request. 127 | /// 128 | public byte NextObjectId { get; set; } 129 | 130 | /// 131 | /// Gets or sets the number of objects in list (appending). 132 | /// 133 | public byte ObjectCount { get; set; } 134 | 135 | #endregion Device Information 136 | 137 | #endregion MODBUS Encapsulated Interface Transport 138 | 139 | #endregion Properties 140 | 141 | #region Serialization 142 | 143 | /// 144 | /// Serializes the response to send. 145 | /// 146 | /// 147 | public byte[] Serialize() 148 | { 149 | var buffer = new DataBuffer(8); 150 | 151 | buffer.SetUInt16(0, TransactionId); 152 | buffer.SetUInt16(2, 0x0000); 153 | buffer.SetByte(6, DeviceId); 154 | 155 | byte fn = (byte)Function; 156 | if (IsError) 157 | { 158 | fn = (byte)(fn | Consts.ErrorMask); 159 | buffer.AddByte((byte)ErrorCode); 160 | } 161 | else 162 | { 163 | switch (Function) 164 | { 165 | case FunctionCode.ReadCoils: 166 | case FunctionCode.ReadDiscreteInputs: 167 | case FunctionCode.ReadHoldingRegisters: 168 | case FunctionCode.ReadInputRegisters: 169 | buffer.AddByte((byte)Data.Length); 170 | buffer.AddBytes(Data.Buffer); 171 | break; 172 | case FunctionCode.WriteMultipleCoils: 173 | case FunctionCode.WriteMultipleRegisters: 174 | buffer.AddUInt16(Address); 175 | buffer.AddUInt16(Count); 176 | break; 177 | case FunctionCode.WriteSingleCoil: 178 | case FunctionCode.WriteSingleRegister: 179 | buffer.AddUInt16(Address); 180 | buffer.AddBytes(Data.Buffer); 181 | break; 182 | case FunctionCode.EncapsulatedInterface: 183 | buffer.AddByte((byte)MEIType); 184 | switch (MEIType) 185 | { 186 | case MEIType.CANOpenGeneralReference: 187 | if (Data?.Length > 0) 188 | buffer.AddBytes(Data.Buffer); 189 | break; 190 | case MEIType.ReadDeviceInformation: 191 | buffer.AddByte((byte)MEICategory); 192 | buffer.AddByte(ConformityLevel); 193 | buffer.AddByte((byte)(MoreRequestsNeeded ? 0xFF : 0x00)); 194 | buffer.AddByte(NextObjectId); 195 | buffer.AddByte(ObjectCount); 196 | buffer.AddBytes(Data.Buffer); 197 | break; 198 | default: 199 | throw new NotImplementedException(); 200 | } 201 | break; 202 | default: 203 | throw new NotImplementedException(); 204 | } 205 | } 206 | 207 | buffer.SetByte(7, fn); 208 | 209 | int len = buffer.Length - 6; 210 | buffer.SetUInt16(4, (ushort)len); 211 | 212 | return buffer.Buffer; 213 | } 214 | 215 | private void Deserialize(byte[] bytes) 216 | { 217 | // Response timed out => device not available 218 | if (bytes.All(b => b == 0)) 219 | { 220 | IsTimeout = true; 221 | return; 222 | } 223 | 224 | var buffer = new DataBuffer(bytes); 225 | ushort ident = buffer.GetUInt16(2); 226 | if (ident != 0) 227 | throw new ArgumentException("Protocol identifier not valid."); 228 | 229 | ushort length = buffer.GetUInt16(4); 230 | if (buffer.Length < length + 6) 231 | throw new ArgumentException("Too less data."); 232 | 233 | if (buffer.Length > length + 6) 234 | { 235 | if (buffer.Buffer.Skip(length + 6).Any(b => b != 0)) 236 | throw new ArgumentException("Too many data."); 237 | 238 | buffer = new DataBuffer(bytes.Take(length + 6)); 239 | } 240 | 241 | TransactionId = buffer.GetUInt16(0); 242 | DeviceId = buffer.GetByte(6); 243 | 244 | byte fn = buffer.GetByte(7); 245 | if ((fn & Consts.ErrorMask) > 0) 246 | { 247 | Function = (FunctionCode)(fn ^ Consts.ErrorMask); 248 | ErrorCode = (ErrorCode)buffer.GetByte(8); 249 | } 250 | else 251 | { 252 | Function = (FunctionCode)fn; 253 | 254 | switch (Function) 255 | { 256 | case FunctionCode.ReadCoils: 257 | case FunctionCode.ReadDiscreteInputs: 258 | case FunctionCode.ReadHoldingRegisters: 259 | case FunctionCode.ReadInputRegisters: 260 | length = buffer.GetByte(8); 261 | if (buffer.Length != length + 9) 262 | throw new ArgumentException("Payload missing."); 263 | 264 | Data = new DataBuffer(buffer.Buffer.Skip(9).ToArray()); 265 | break; 266 | case FunctionCode.WriteMultipleCoils: 267 | case FunctionCode.WriteMultipleRegisters: 268 | Address = buffer.GetUInt16(8); 269 | Count = buffer.GetUInt16(10); 270 | break; 271 | case FunctionCode.WriteSingleCoil: 272 | case FunctionCode.WriteSingleRegister: 273 | Address = buffer.GetUInt16(8); 274 | Data = new DataBuffer(buffer.Buffer.Skip(10).ToArray()); 275 | break; 276 | case FunctionCode.EncapsulatedInterface: 277 | MEIType = (MEIType)buffer.GetByte(8); 278 | switch (MEIType) 279 | { 280 | case MEIType.CANOpenGeneralReference: 281 | Data = new DataBuffer(buffer.Buffer.Skip(9).ToArray()); 282 | break; 283 | case MEIType.ReadDeviceInformation: 284 | MEICategory = (DeviceIDCategory)buffer.GetByte(9); 285 | ConformityLevel = buffer.GetByte(10); 286 | MoreRequestsNeeded = buffer.GetByte(11) > 0; 287 | NextObjectId = buffer.GetByte(12); 288 | ObjectCount = buffer.GetByte(13); 289 | Data = new DataBuffer(buffer.Buffer.Skip(14).ToArray()); 290 | break; 291 | default: 292 | throw new NotImplementedException($"Unknown MEI type: {MEIType}."); 293 | } 294 | break; 295 | default: 296 | throw new NotImplementedException($"Unknown function code: {Function}."); 297 | } 298 | } 299 | } 300 | 301 | #endregion Serialization 302 | 303 | #region Overrides 304 | 305 | /// 306 | public override string ToString() 307 | { 308 | var sb = new StringBuilder(); 309 | if (Data != null) 310 | { 311 | foreach (byte b in Data.Buffer) 312 | { 313 | if (sb.Length > 0) 314 | sb.Append(" "); 315 | 316 | sb.Append(b.ToString("X2")); 317 | } 318 | } 319 | 320 | return $"Response#{TransactionId} | Device#{DeviceId}, Fn: {Function}, Error: {IsError}, Address: {Address}, Count: {Count} | {sb}"; 321 | } 322 | 323 | /// 324 | public override int GetHashCode() 325 | => base.GetHashCode() ^ 326 | TransactionId.GetHashCode() ^ 327 | DeviceId.GetHashCode() ^ 328 | Function.GetHashCode() ^ 329 | Address.GetHashCode() ^ 330 | Count.GetHashCode() ^ 331 | Data.GetHashCode(); 332 | 333 | /// 334 | public override bool Equals(object obj) 335 | { 336 | if (obj is not Response res) 337 | return false; 338 | 339 | return res.TransactionId == TransactionId && 340 | res.DeviceId == DeviceId && 341 | res.Function == Function && 342 | res.Address == Address && 343 | res.Count == Count && 344 | Data.Equals(res.Data); 345 | } 346 | 347 | #endregion Overrides 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/Modbus.Tcp/Util/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace AMWD.Modbus.Tcp.Util 9 | { 10 | internal static class Extensions 11 | { 12 | #region Enums 13 | 14 | public static T GetAttribute(this Enum enumValue) 15 | where T : Attribute 16 | { 17 | if (enumValue != null) 18 | { 19 | var fi = enumValue.GetType().GetField(enumValue.ToString()); 20 | var attrs = (T[])fi?.GetCustomAttributes(typeof(T), inherit: false); 21 | return attrs?.FirstOrDefault(); 22 | } 23 | return default; 24 | } 25 | 26 | public static string GetDescription(this Enum enumValue) 27 | => enumValue.GetAttribute()?.Description ?? enumValue.ToString(); 28 | 29 | #endregion Enums 30 | 31 | #region Task handling 32 | 33 | public static async void Forget(this Task task) 34 | { 35 | try 36 | { 37 | await task; 38 | } 39 | catch 40 | { /* Task forgotten, so keep everything quiet. */ } 41 | } 42 | 43 | #endregion Task handling 44 | 45 | #region Stream 46 | 47 | public static async Task ReadExpectedBytes(this Stream stream, int expectedBytes, CancellationToken cancellationToken = default) 48 | { 49 | byte[] buffer = new byte[expectedBytes]; 50 | int offset = 0; 51 | do 52 | { 53 | int count = await stream.ReadAsync(buffer, offset, expectedBytes - offset, cancellationToken); 54 | if (count < 1) 55 | throw new EndOfStreamException($"Expected to read {buffer.Length - offset} more bytes, but end of stream is reached"); 56 | 57 | offset += count; 58 | } 59 | while (expectedBytes - offset > 0 && !cancellationToken.IsCancellationRequested); 60 | 61 | cancellationToken.ThrowIfCancellationRequested(); 62 | return buffer; 63 | } 64 | 65 | #endregion Stream 66 | 67 | #region Exception 68 | 69 | public static string GetMessage(this Exception exception) 70 | => exception.InnerException?.Message ?? exception.Message; 71 | 72 | #endregion Exception 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Modbus.Tcp/Util/QueuedRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using AMWD.Modbus.Tcp.Protocol; 4 | 5 | namespace AMWD.Modbus.Tcp.Util 6 | { 7 | internal class QueuedRequest 8 | { 9 | public ushort TransactionId { get; set; } 10 | 11 | public CancellationTokenRegistration Registration { get; set; } 12 | 13 | public TaskCompletionSource TaskCompletionSource { get; set; } 14 | 15 | public CancellationTokenSource CancellationTokenSource { get; set; } 16 | 17 | public CancellationTokenSource TimeoutCancellationTokenSource { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/UnitTests/ChecksumTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using AMWD.Modbus.Common.Util; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace UnitTests 6 | { 7 | [TestClass] 8 | public class ChecksumTests 9 | { 10 | [TestMethod] 11 | public void Crc16Test() 12 | { 13 | byte[] bytes = Encoding.ASCII.GetBytes("0123456789"); 14 | byte[] expected = new byte[] { 77, 67 }; 15 | 16 | byte[] crc = bytes.CRC16(); 17 | 18 | CollectionAssert.AreEqual(expected, crc); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/UnitTests/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace UnitTests 5 | { 6 | internal class ConsoleLogger : ILogger 7 | { 8 | private readonly object syncObj = new object(); 9 | 10 | public ConsoleLogger() 11 | { 12 | } 13 | 14 | public ConsoleLogger(string name) 15 | { 16 | Name = name; 17 | } 18 | 19 | public ConsoleLogger(string name, ConsoleLogger parentLogger) 20 | { 21 | Name = name; 22 | ParentLogger = parentLogger; 23 | } 24 | 25 | public string Name { get; } 26 | 27 | public ConsoleLogger ParentLogger { get; } 28 | 29 | public bool DisableColors { get; set; } 30 | 31 | public string TimestampFormat { get; set; } 32 | 33 | public LogLevel MinLevel { get; set; } 34 | 35 | public IDisposable BeginScope(TState state) 36 | { 37 | throw new NotImplementedException(); 38 | } 39 | 40 | public bool IsEnabled(LogLevel logLevel) 41 | { 42 | return logLevel >= MinLevel; 43 | } 44 | 45 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 46 | { 47 | if (!IsEnabled(logLevel)) 48 | return; 49 | 50 | if (formatter == null) 51 | throw new ArgumentNullException(nameof(formatter)); 52 | 53 | string message = formatter(state, exception); 54 | 55 | if (!string.IsNullOrEmpty(message) || exception != null) 56 | { 57 | if (ParentLogger != null) 58 | { 59 | ParentLogger.WriteMessage(Name, logLevel, eventId.Id, message, exception); 60 | } 61 | else 62 | { 63 | WriteMessage(Name, logLevel, eventId.Id, message, exception); 64 | } 65 | } 66 | } 67 | 68 | private void WriteMessage(string name, LogLevel logLevel, int eventId, string message, Exception exception) 69 | { 70 | if (exception != null) 71 | { 72 | if (!string.IsNullOrEmpty(message)) 73 | { 74 | message += Environment.NewLine + exception.ToString(); 75 | } 76 | else 77 | { 78 | message = exception.ToString(); 79 | } 80 | } 81 | 82 | bool changedColor; 83 | string timestampPadding = ""; 84 | lock (syncObj) 85 | { 86 | if (!string.IsNullOrEmpty(TimestampFormat)) 87 | { 88 | changedColor = false; 89 | if (!DisableColors) 90 | { 91 | switch (logLevel) 92 | { 93 | case LogLevel.Trace: 94 | Console.ForegroundColor = ConsoleColor.DarkGray; 95 | changedColor = true; 96 | break; 97 | } 98 | } 99 | string timestamp = DateTime.Now.ToString(TimestampFormat) + " "; 100 | Console.Write(timestamp); 101 | timestampPadding = new string(' ', timestamp.Length); 102 | 103 | if (changedColor) 104 | Console.ResetColor(); 105 | } 106 | 107 | changedColor = false; 108 | if (!DisableColors) 109 | { 110 | switch (logLevel) 111 | { 112 | case LogLevel.Trace: 113 | Console.ForegroundColor = ConsoleColor.DarkGray; 114 | changedColor = true; 115 | break; 116 | case LogLevel.Information: 117 | Console.ForegroundColor = ConsoleColor.DarkGreen; 118 | changedColor = true; 119 | break; 120 | case LogLevel.Warning: 121 | Console.ForegroundColor = ConsoleColor.Yellow; 122 | changedColor = true; 123 | break; 124 | case LogLevel.Error: 125 | Console.ForegroundColor = ConsoleColor.Black; 126 | Console.BackgroundColor = ConsoleColor.Red; 127 | changedColor = true; 128 | break; 129 | case LogLevel.Critical: 130 | Console.ForegroundColor = ConsoleColor.White; 131 | Console.BackgroundColor = ConsoleColor.Red; 132 | changedColor = true; 133 | break; 134 | } 135 | } 136 | Console.Write(GetLogLevelString(logLevel)); 137 | 138 | if (changedColor) 139 | Console.ResetColor(); 140 | 141 | changedColor = false; 142 | if (!DisableColors) 143 | { 144 | switch (logLevel) 145 | { 146 | case LogLevel.Trace: 147 | Console.ForegroundColor = ConsoleColor.DarkGray; 148 | changedColor = true; 149 | break; 150 | } 151 | } 152 | Console.WriteLine(": " + (!string.IsNullOrEmpty(name) ? "[" + name + "] " : "") + message.Replace("\n", "\n " + timestampPadding)); 153 | 154 | if (changedColor) 155 | Console.ResetColor(); 156 | } 157 | } 158 | 159 | private static string GetLogLevelString(LogLevel logLevel) 160 | { 161 | switch (logLevel) 162 | { 163 | case LogLevel.Trace: 164 | return "trce"; 165 | case LogLevel.Debug: 166 | return "dbug"; 167 | case LogLevel.Information: 168 | return "info"; 169 | case LogLevel.Warning: 170 | return "warn"; 171 | case LogLevel.Error: 172 | return "fail"; 173 | case LogLevel.Critical: 174 | return "crit"; 175 | default: 176 | throw new ArgumentOutOfRangeException(nameof(logLevel)); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /test/UnitTests/DataBufferTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using AMWD.Modbus.Common.Util; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace UnitTests 6 | { 7 | [TestClass] 8 | public class DataBufferTests 9 | { 10 | [TestMethod] 11 | public void ConstructorTest() 12 | { 13 | var db1 = new DataBuffer(); 14 | Assert.AreEqual(0, db1.Length); 15 | Assert.AreEqual(0, db1.Buffer.Length); 16 | CollectionAssert.AreEqual(new byte[0], db1.Buffer); 17 | 18 | var db2 = new DataBuffer(12); 19 | Assert.AreEqual(12, db2.Length); 20 | Assert.AreEqual(12, db2.Buffer.Length); 21 | Assert.IsTrue(db2.Buffer.All(b => b == 0)); 22 | CollectionAssert.AreEqual(new byte[12], db2.Buffer); 23 | 24 | var bytes = new byte[] { 1, 3, 2, 4, 6, 5, 7, 8, 9 }; 25 | var db3 = new DataBuffer(bytes); 26 | Assert.AreEqual(9, db3.Length); 27 | Assert.AreEqual(9, db3.Buffer.Length); 28 | CollectionAssert.AreEqual(bytes, db3.Buffer); 29 | Assert.AreNotEqual(bytes, db3.Buffer); 30 | bytes[0] = 0; 31 | CollectionAssert.AreNotEqual(bytes, db3.Buffer); 32 | 33 | var db4 = new DataBuffer(db3); 34 | Assert.AreNotEqual(db3.Buffer, db4.Buffer); 35 | CollectionAssert.AreEqual(db3.Buffer, db4.Buffer); 36 | } 37 | 38 | #region Unsigned 39 | 40 | [TestMethod] 41 | public void ByteTests() 42 | { 43 | } 44 | 45 | #endregion Unsigned 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/UnitTests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace UnitTests 7 | { 8 | /// 9 | /// Contains some extensions to handle some features more easily. 10 | /// 11 | internal static class Extensions 12 | { 13 | #region Enums 14 | 15 | public static T GetAttribute(this Enum enumValue) 16 | where T : Attribute 17 | { 18 | if (enumValue != null) 19 | { 20 | var fi = enumValue.GetType().GetField(enumValue.ToString()); 21 | var attrs = (T[])fi?.GetCustomAttributes(typeof(T), inherit: false); 22 | return attrs?.FirstOrDefault(); 23 | } 24 | return default(T); 25 | } 26 | 27 | public static string GetDescription(this Enum enumValue) 28 | { 29 | return enumValue.GetAttribute()?.Description ?? enumValue.ToString(); 30 | } 31 | 32 | #endregion Enums 33 | 34 | #region Task handling 35 | 36 | internal static async void Forget(this Task task) 37 | { 38 | try 39 | { 40 | await task; 41 | } 42 | catch 43 | { /* Task forgotten, so keep everything quiet. */ } 44 | } 45 | 46 | #endregion Task handling 47 | 48 | #region Exception 49 | 50 | public static string GetMessage(this Exception exception) 51 | { 52 | return exception.InnerException?.Message ?? exception.Message; 53 | } 54 | 55 | #endregion Exception 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/UnitTests/ExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using AMWD.Modbus.Common.Util; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace UnitTests 6 | { 7 | [TestClass] 8 | public class ExtensionsTests 9 | { 10 | [TestMethod] 11 | public void UInt8Test() 12 | { 13 | byte expected = 170; 14 | var register = expected.ToModbusRegister(1); 15 | byte actual = register.GetByte(); 16 | 17 | Assert.AreEqual(expected, actual, "Byte-conversion to register and back failed"); 18 | } 19 | 20 | [TestMethod] 21 | public void UInt16Test() 22 | { 23 | ushort expected = 1234; 24 | var register = expected.ToModbusRegister(1); 25 | ushort actual = register.GetUInt16(); 26 | 27 | Assert.AreEqual(expected, actual, "UInt16-conversion to register and back failed"); 28 | } 29 | 30 | [TestMethod] 31 | public void UInt32Test() 32 | { 33 | uint expected = 703010; 34 | var registers = expected.ToModbusRegister(1); 35 | uint actual = registers.GetUInt32(); 36 | 37 | Assert.AreEqual(expected, actual, "UInt32-conversion to register and back failed"); 38 | Assert.AreEqual(1, registers[0].Address, "Addressing first register failed"); 39 | Assert.AreEqual(2, registers[1].Address, "Addressing second register failed"); 40 | } 41 | 42 | [TestMethod] 43 | public void UInt64Test() 44 | { 45 | ulong expected = ulong.MaxValue - byte.MaxValue; 46 | var registers = expected.ToModbusRegister(1); 47 | ulong actual = registers.GetUInt64(); 48 | 49 | Assert.AreEqual(expected, actual, "UInt64-conversion to register and back failed"); 50 | Assert.AreEqual(1, registers[0].Address, "Addressing first register failed"); 51 | Assert.AreEqual(2, registers[1].Address, "Addressing second register failed"); 52 | Assert.AreEqual(3, registers[2].Address, "Addressing third register failed"); 53 | Assert.AreEqual(4, registers[3].Address, "Addressing fourth register failed"); 54 | } 55 | 56 | [TestMethod] 57 | public void Int8Test() 58 | { 59 | sbyte expected = -85; 60 | var register = expected.ToModbusRegister(100); 61 | sbyte actual = register.GetSByte(); 62 | 63 | Assert.AreEqual(expected, actual, "SByte-conversion to register and back failed"); 64 | } 65 | 66 | [TestMethod] 67 | public void Int16Test() 68 | { 69 | short expected = -4321; 70 | var register = expected.ToModbusRegister(100); 71 | short actual = register.GetInt16(); 72 | 73 | Assert.AreEqual(expected, actual, "Int16-conversion to register and back failed"); 74 | } 75 | 76 | [TestMethod] 77 | public void Int32Test() 78 | { 79 | int expected = -4020; 80 | var registers = expected.ToModbusRegister(100); 81 | int actual = registers.GetInt32(); 82 | 83 | Assert.AreEqual(expected, actual, "Int32-conversion to register and back failed"); 84 | Assert.AreEqual(100, registers[0].Address, "Addressing first register failed"); 85 | Assert.AreEqual(101, registers[1].Address, "Addressing second register failed"); 86 | } 87 | 88 | [TestMethod] 89 | public void Int64Test() 90 | { 91 | long expected = long.MinValue + short.MaxValue; 92 | var registers = expected.ToModbusRegister(100); 93 | long actual = registers.GetInt64(); 94 | 95 | Assert.AreEqual(expected, actual, "Int64-conversion to register and back failed"); 96 | Assert.AreEqual(100, registers[0].Address, "Addressing first register failed"); 97 | Assert.AreEqual(101, registers[1].Address, "Addressing second register failed"); 98 | Assert.AreEqual(102, registers[2].Address, "Addressing third register failed"); 99 | Assert.AreEqual(103, registers[3].Address, "Addressing fourth register failed"); 100 | } 101 | 102 | [TestMethod] 103 | public void SingleTest() 104 | { 105 | float expected = 1.4263f; 106 | var registers = expected.ToModbusRegister(50); 107 | float actual = registers.GetSingle(); 108 | 109 | Assert.AreEqual(expected, actual, "Single-conversion to register and back failed"); 110 | Assert.AreEqual(50, registers[0].Address, "Addressing first register failed"); 111 | Assert.AreEqual(51, registers[1].Address, "Addressing second register failed"); 112 | } 113 | 114 | [TestMethod] 115 | public void DoubleTest() 116 | { 117 | double expected = double.MinValue + byte.MaxValue; 118 | var registers = expected.ToModbusRegister(50); 119 | double actual = registers.GetDouble(); 120 | 121 | Assert.AreEqual(expected, actual, "Double-conversion to register and back failed"); 122 | Assert.AreEqual(50, registers[0].Address, "Addressing first register failed"); 123 | Assert.AreEqual(51, registers[1].Address, "Addressing second register failed"); 124 | Assert.AreEqual(52, registers[2].Address, "Addressing third register failed"); 125 | Assert.AreEqual(53, registers[3].Address, "Addressing fourth register failed"); 126 | } 127 | 128 | [TestMethod] 129 | public void StringTest() 130 | { 131 | string expected = "0123456789123"; 132 | var registers = expected.ToModbusRegister(42); 133 | string actual = registers.GetString(7); 134 | 135 | Assert.AreEqual(expected, actual, "String-conversion to register and back failed"); 136 | Assert.AreEqual(42, registers[0].Address, "Addressing first register failed"); 137 | Assert.AreEqual(43, registers[1].Address, "Addressing second register failed"); 138 | Assert.AreEqual(44, registers[2].Address, "Addressing third register failed"); 139 | Assert.AreEqual(45, registers[3].Address, "Addressing fourth register failed"); 140 | Assert.AreEqual(46, registers[4].Address, "Addressing fifth register failed"); 141 | Assert.AreEqual(47, registers[5].Address, "Addressing sixth register failed"); 142 | Assert.AreEqual(48, registers[6].Address, "Addressing seventh register failed"); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /test/UnitTests/ModbusTcpTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using AMWD.Modbus.Common; 11 | using AMWD.Modbus.Common.Structures; 12 | using AMWD.Modbus.Tcp.Client; 13 | using AMWD.Modbus.Tcp.Server; 14 | using Microsoft.VisualStudio.TestTools.UnitTesting; 15 | 16 | namespace UnitTests 17 | { 18 | [TestClass] 19 | public class ModbusTcpTests 20 | { 21 | #region Modbus Client 22 | 23 | #region Control 24 | 25 | [TestMethod] 26 | public async Task ClientConnectTest() 27 | { 28 | using var server = new MiniTestServer(); 29 | server.Start(); 30 | 31 | using var client = new ModbusClient(IPAddress.Loopback, server.Port); 32 | await client.Connect(); 33 | Assert.IsTrue(client.IsConnected, "Client shoud be connected"); 34 | 35 | await client.Disconnect(); 36 | Assert.IsFalse(client.IsConnected, "Client should not be connected"); 37 | } 38 | 39 | [TestMethod] 40 | public async Task ClientReconnectTest() 41 | { 42 | using var server = new MiniTestServer(); 43 | server.Start(); 44 | 45 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 46 | await client.Connect(); 47 | Assert.IsTrue(client.IsConnected, "Client should be connected"); 48 | 49 | await server.Stop(); 50 | await EnsureWait(); // time to start reconnect 51 | Assert.IsFalse(client.IsConnected, "Client should not be connected"); 52 | 53 | server.Start(); 54 | await client.ConnectingTask; 55 | Assert.IsTrue(client.IsConnected, "Client should be connected again"); 56 | } 57 | 58 | [TestMethod] 59 | public async Task ClientEventsTest() 60 | { 61 | int connectEvents = 0; 62 | int disconnectEvents = 0; 63 | using var server = new MiniTestServer(); 64 | server.Start(); 65 | 66 | using (var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger())) 67 | { 68 | client.Connected += (sender, args) => 69 | { 70 | connectEvents++; 71 | }; 72 | client.Disconnected += (sender, args) => 73 | { 74 | disconnectEvents++; 75 | }; 76 | 77 | Assert.AreEqual(0, connectEvents, "No connet events"); 78 | Assert.AreEqual(0, disconnectEvents, "No disconnect events"); 79 | 80 | await client.Connect(); 81 | Assert.IsTrue(client.IsConnected, "Client should be connected"); 82 | 83 | await EnsureWait(); // get events raised 84 | Assert.AreEqual(1, connectEvents, "One connect event"); 85 | Assert.AreEqual(0, disconnectEvents, "No disconnect events"); 86 | 87 | await server.Stop(); 88 | 89 | await EnsureWait(); // time to set all information 90 | Assert.IsFalse(client.IsConnected, "Client should not be connected"); 91 | 92 | await EnsureWait(); // get events raised 93 | Assert.AreEqual(1, connectEvents, "One connect event"); 94 | Assert.AreEqual(1, disconnectEvents, "One disconnect event"); 95 | 96 | server.Start(); 97 | await client.ConnectingTask; 98 | Assert.IsTrue(client.IsConnected, "Client should be connected"); 99 | 100 | await EnsureWait(); // get events raised 101 | Assert.AreEqual(2, connectEvents, "Two connect events"); 102 | } 103 | 104 | await EnsureWait(); // get events raised 105 | Assert.AreEqual(2, disconnectEvents, "Two disconnect events"); 106 | } 107 | 108 | #endregion Control 109 | 110 | #region Read 111 | 112 | [TestMethod] 113 | public async Task ClientReadExceptionTest() 114 | { 115 | byte[] expectedRequest = new byte[] { 0, 0, 0, 6, 2, 1, 0, 24, 0, 2 }; 116 | string expectedExceptionMessage = ErrorCode.GatewayTargetDevice.GetDescription(); 117 | 118 | using var server = new MiniTestServer 119 | { 120 | RequestHandler = (request, clientIp) => 121 | { 122 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 123 | Console.WriteLine("Server sending error response"); 124 | return new byte[] { request[0], request[1], 0, 0, 0, 3, 2, 129, 11 }; 125 | } 126 | }; 127 | server.Start(); 128 | 129 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 130 | await client.Connect(); 131 | Assert.IsTrue(client.IsConnected); 132 | 133 | try 134 | { 135 | var response = await client.ReadCoils(2, 24, 2); 136 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 137 | Assert.Fail("Exception not thrown"); 138 | } 139 | catch (ModbusException ex) 140 | { 141 | Assert.AreEqual(expectedExceptionMessage, ex.Message); 142 | } 143 | } 144 | 145 | [TestMethod] 146 | public async Task ClientReadCoilsTest() 147 | { 148 | // Function Code 0x01 149 | 150 | byte[] expectedRequest = new byte[] { 0, 0, 0, 6, 12, 1, 0, 20, 0, 10 }; 151 | var expectedResponse = new List 152 | { 153 | new Coil { Address = 20, BoolValue = true }, 154 | new Coil { Address = 21, BoolValue = false }, 155 | new Coil { Address = 22, BoolValue = true }, 156 | new Coil { Address = 23, BoolValue = true }, 157 | new Coil { Address = 24, BoolValue = false }, 158 | new Coil { Address = 25, BoolValue = false }, 159 | new Coil { Address = 26, BoolValue = true }, 160 | new Coil { Address = 27, BoolValue = true }, 161 | new Coil { Address = 28, BoolValue = true }, 162 | new Coil { Address = 29, BoolValue = false }, 163 | }; 164 | 165 | using var server = new MiniTestServer 166 | { 167 | RequestHandler = (request, clientIp) => 168 | { 169 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 170 | Console.WriteLine("Server sending response"); 171 | return new byte[] { request[0], request[1], 0, 0, 0, 5, 12, 1, 2, 205, 1 }; 172 | } 173 | }; 174 | server.Start(); 175 | 176 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 177 | await client.Connect(); 178 | Assert.IsTrue(client.IsConnected); 179 | 180 | var coils = await client.ReadCoils(12, 20, 10); 181 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 182 | CollectionAssert.AreEqual(expectedResponse, coils, "Response is incorrect"); 183 | } 184 | 185 | [TestMethod] 186 | public async Task ClientReadDiscreteInputsTest() 187 | { 188 | // Function Code 0x02 189 | 190 | byte[] expectedRequest = new byte[] { 0, 0, 0, 6, 1, 2, 0, 12, 0, 2 }; 191 | var expectedResponse = new List 192 | { 193 | new DiscreteInput { Address = 12, BoolValue = true }, 194 | new DiscreteInput { Address = 13, BoolValue = true } 195 | }; 196 | 197 | using var server = new MiniTestServer 198 | { 199 | RequestHandler = (request, clientIp) => 200 | { 201 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 202 | Console.WriteLine("Server sending response"); 203 | return new byte[] { request[0], request[1], 0, 0, 0, 4, 1, 2, 1, 3 }; 204 | } 205 | }; 206 | server.Start(); 207 | 208 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 209 | await client.Connect(); 210 | Assert.IsTrue(client.IsConnected); 211 | 212 | var inputs = await client.ReadDiscreteInputs(1, 12, 2); 213 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 214 | CollectionAssert.AreEqual(expectedResponse, inputs, "Response is incorrect"); 215 | } 216 | 217 | [TestMethod] 218 | public async Task ClientReadHoldingRegisterTest() 219 | { 220 | // Function Code 0x03 221 | 222 | byte[] expectedRequest = new byte[] { 0, 0, 0, 6, 5, 3, 0, 10, 0, 2 }; 223 | var expectedResponse = new List 224 | { 225 | new Register { Address = 10, RegisterValue = 3, Type = ModbusObjectType.HoldingRegister }, 226 | new Register { Address = 11, RegisterValue = 7, Type = ModbusObjectType.HoldingRegister } 227 | }; 228 | 229 | using var server = new MiniTestServer 230 | { 231 | RequestHandler = (request, clientIp) => 232 | { 233 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 234 | Console.WriteLine("Server sending response"); 235 | return new byte[] { request[0], request[1], 0, 0, 0, 7, 5, 3, 4, 0, 3, 0, 7 }; 236 | } 237 | }; 238 | server.Start(); 239 | 240 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 241 | await client.Connect(); 242 | Assert.IsTrue(client.IsConnected); 243 | 244 | var registers = await client.ReadHoldingRegisters(5, 10, 2); 245 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 246 | CollectionAssert.AreEqual(expectedResponse, registers, "Response is incorrect"); 247 | } 248 | 249 | [TestMethod] 250 | public async Task ClientReadInputRegisterTest() 251 | { 252 | // Function Code 0x04 253 | 254 | byte[] expectedRequest = new byte[] { 0, 0, 0, 6, 3, 4, 0, 6, 0, 3 }; 255 | var expectedResponse = new List 256 | { 257 | new Register { Address = 6, RegisterValue = 123, Type = ModbusObjectType.InputRegister }, 258 | new Register { Address = 7, RegisterValue = 0, Type = ModbusObjectType.InputRegister }, 259 | new Register { Address = 8, RegisterValue = 12345, Type = ModbusObjectType.InputRegister } 260 | }; 261 | 262 | using var server = new MiniTestServer 263 | { 264 | RequestHandler = (request, clientIp) => 265 | { 266 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 267 | Console.WriteLine("Server sending response"); 268 | return new byte[] { request[0], request[1], 0, 0, 0, 9, 3, 4, 6, 0, 123, 0, 0, 48, 57 }; 269 | } 270 | }; 271 | server.Start(); 272 | 273 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 274 | await client.Connect(); 275 | Assert.IsTrue(client.IsConnected); 276 | 277 | var registers = await client.ReadInputRegisters(3, 6, 3); 278 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 279 | CollectionAssert.AreEqual(expectedResponse, registers, "Response is incorrect"); 280 | } 281 | 282 | [TestMethod] 283 | public async Task ClientReadDeviceInformationBasicTest() 284 | { 285 | byte[] expectedRequest = new byte[] { 0, 0, 0, 5, 13, 43, 14, 1, 0 }; 286 | var expectedResponse = new Dictionary 287 | { 288 | { DeviceIDObject.VendorName, "AM.WD" }, 289 | { DeviceIDObject.ProductCode, "Mini-Test" }, 290 | { DeviceIDObject.MajorMinorRevision, "1.2.3.4" } 291 | }; 292 | 293 | using var server = new MiniTestServer 294 | { 295 | RequestHandler = (request, clientIp) => 296 | { 297 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 298 | Console.WriteLine("Server sending response"); 299 | 300 | var bytes = new List(); 301 | bytes.AddRange(request.Take(2)); 302 | bytes.AddRange(new byte[] { 0, 0, 0, 0, 13, 43, 14, 1, 1, 0, 0, (byte)expectedResponse.Count }); 303 | int len = 8; 304 | foreach (var kvp in expectedResponse) 305 | { 306 | byte[] b = Encoding.ASCII.GetBytes(kvp.Value); 307 | bytes.Add((byte)kvp.Key); 308 | len++; 309 | bytes.Add((byte)b.Length); 310 | len++; 311 | bytes.AddRange(b); 312 | len += b.Length; 313 | } 314 | bytes[5] = (byte)len; 315 | 316 | return bytes.ToArray(); 317 | } 318 | }; 319 | server.Start(); 320 | 321 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 322 | await client.Connect(); 323 | Assert.IsTrue(client.IsConnected); 324 | 325 | var deviceInfo = await client.ReadDeviceInformation(13, DeviceIDCategory.Basic); 326 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 327 | CollectionAssert.AreEqual(expectedResponse, deviceInfo, "Response is incorrect"); 328 | } 329 | 330 | [TestMethod] 331 | public async Task ClientReadDeviceInformationIndividualTest() 332 | { 333 | byte[] expectedRequest = new byte[] { 0, 0, 0, 5, 13, 43, 14, 4, (byte)DeviceIDObject.ModelName }; 334 | var expectedResponse = new Dictionary 335 | { 336 | { DeviceIDObject.ModelName, "TestModel" } 337 | }; 338 | 339 | using var server = new MiniTestServer 340 | { 341 | RequestHandler = (request, clientIp) => 342 | { 343 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 344 | Console.WriteLine("Server sending response"); 345 | 346 | var bytes = new List(); 347 | bytes.AddRange(request.Take(2)); 348 | bytes.AddRange(new byte[] { 0, 0, 0, 0, 13, 43, 14, 4, 2, 0, 0, (byte)expectedResponse.Count }); 349 | int len = 8; 350 | foreach (var kvp in expectedResponse) 351 | { 352 | byte[] b = Encoding.ASCII.GetBytes(kvp.Value); 353 | bytes.Add((byte)kvp.Key); 354 | len++; 355 | bytes.Add((byte)b.Length); 356 | len++; 357 | bytes.AddRange(b); 358 | len += b.Length; 359 | } 360 | bytes[5] = (byte)len; 361 | 362 | return bytes.ToArray(); 363 | } 364 | }; 365 | server.Start(); 366 | 367 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 368 | await client.Connect(); 369 | Assert.IsTrue(client.IsConnected); 370 | 371 | var deviceInfo = await client.ReadDeviceInformation(13, DeviceIDCategory.Individual, DeviceIDObject.ModelName); 372 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 373 | CollectionAssert.AreEqual(expectedResponse, deviceInfo, "Response is incorrect"); 374 | } 375 | 376 | #endregion Read 377 | 378 | #region Write 379 | 380 | [TestMethod] 381 | public async Task ClientWriteSingleCoilTest() 382 | { 383 | // Function Code 0x05 384 | 385 | byte[] expectedRequest = new byte[] { 0, 0, 0, 6, 1, 5, 0, 173, 255, 0 }; 386 | 387 | using var server = new MiniTestServer 388 | { 389 | RequestHandler = (request, clientIp) => 390 | { 391 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 392 | Console.WriteLine("Server sending response"); 393 | return request; 394 | } 395 | }; 396 | server.Start(); 397 | 398 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 399 | await client.Connect(); 400 | Assert.IsTrue(client.IsConnected); 401 | 402 | var coil = new Coil 403 | { 404 | Address = 173, 405 | BoolValue = true 406 | }; 407 | bool success = await client.WriteSingleCoil(1, coil); 408 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 409 | Assert.IsTrue(success); 410 | } 411 | 412 | [TestMethod] 413 | public async Task ClientWriteSingleRegisterTest() 414 | { 415 | // Function Code 0x06 416 | 417 | byte[] expectedRequest = new byte[] { 0, 0, 0, 6, 2, 6, 0, 5, 48, 57 }; 418 | 419 | using var server = new MiniTestServer 420 | { 421 | RequestHandler = (request, clientIp) => 422 | { 423 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 424 | Console.WriteLine("Server sending response"); 425 | return request; 426 | } 427 | }; 428 | server.Start(); 429 | 430 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 431 | await client.Connect(); 432 | Assert.IsTrue(client.IsConnected); 433 | 434 | var register = new Register 435 | { 436 | Type = ModbusObjectType.HoldingRegister, 437 | Address = 5, 438 | RegisterValue = 12345 439 | }; 440 | bool success = await client.WriteSingleRegister(2, register); 441 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 442 | Assert.IsTrue(success); 443 | } 444 | 445 | [TestMethod] 446 | public async Task ClientWriteCoilsTest() 447 | { 448 | // Function Code 0x0F 449 | 450 | byte[] expectedRequest = new byte[] { 0, 0, 0, 9, 4, 15, 0, 20, 0, 10, 2, 205, 1 }; 451 | 452 | using var server = new MiniTestServer 453 | { 454 | RequestHandler = (request, clientIp) => 455 | { 456 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 457 | Console.WriteLine("Server sending response"); 458 | return new byte[] { request[0], request[1], 0, 0, 0, 6, 4, 15, 0, 20, 0, 10 }; 459 | } 460 | }; 461 | server.Start(); 462 | 463 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 464 | await client.Connect(); 465 | Assert.IsTrue(client.IsConnected); 466 | 467 | var coils = new List 468 | { 469 | new Coil { Address = 20, BoolValue = true }, 470 | new Coil { Address = 21, BoolValue = false }, 471 | new Coil { Address = 22, BoolValue = true }, 472 | new Coil { Address = 23, BoolValue = true }, 473 | new Coil { Address = 24, BoolValue = false }, 474 | new Coil { Address = 25, BoolValue = false }, 475 | new Coil { Address = 26, BoolValue = true }, 476 | new Coil { Address = 27, BoolValue = true }, 477 | new Coil { Address = 28, BoolValue = true }, 478 | new Coil { Address = 29, BoolValue = false }, 479 | }; 480 | bool success = await client.WriteCoils(4, coils); 481 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 482 | Assert.IsTrue(success); 483 | } 484 | 485 | [TestMethod] 486 | public async Task ClientWriteRegistersTest() 487 | { 488 | // Function Code 0x10 489 | 490 | byte[] expectedRequest = new byte[] { 0, 0, 0, 11, 10, 16, 0, 2, 0, 2, 4, 0, 10, 1, 2 }; 491 | 492 | using var server = new MiniTestServer 493 | { 494 | RequestHandler = (request, clientIp) => 495 | { 496 | CollectionAssert.AreEqual(expectedRequest, request.Skip(2).ToArray(), "Request is incorrect"); 497 | Console.WriteLine("Server sending response"); 498 | return new byte[] { request[0], request[1], 0, 0, 0, 6, 10, 16, 0, 2, 0, 2 }; 499 | } 500 | }; 501 | server.Start(); 502 | 503 | using var client = new ModbusClient(IPAddress.Loopback, server.Port, new ConsoleLogger()); 504 | await client.Connect(); 505 | Assert.IsTrue(client.IsConnected); 506 | 507 | var registers = new List 508 | { 509 | new Register { Address = 2, RegisterValue = 10, Type = ModbusObjectType.HoldingRegister }, 510 | new Register { Address = 3, RegisterValue = 258, Type = ModbusObjectType.HoldingRegister } 511 | }; 512 | bool success = await client.WriteRegisters(10, registers); 513 | Assert.IsTrue(string.IsNullOrWhiteSpace(server.LastError), server.LastError); 514 | Assert.IsTrue(success); 515 | } 516 | 517 | #endregion Write 518 | 519 | #endregion Modbus Client 520 | 521 | #region Modbus Server 522 | 523 | [TestMethod] 524 | public async Task ServerStartTest() 525 | { 526 | int port = 0; 527 | using (var testServer = new MiniTestServer()) 528 | { 529 | testServer.Start(); 530 | port = testServer.Port; 531 | } 532 | 533 | using var server = new ModbusServer(port); 534 | await server.Initialization; 535 | Assert.IsTrue(server.IsRunning); 536 | } 537 | 538 | #endregion Modbus Server 539 | 540 | #region TestServer 541 | 542 | internal delegate byte[] MiniTestServerRequestHandler(byte[] request, IPEndPoint endPoint); 543 | 544 | internal class MiniTestServer : IDisposable 545 | { 546 | private TcpListener listener; 547 | private CancellationTokenSource cts; 548 | 549 | private Task runTask; 550 | 551 | public MiniTestServer(int port = 0) 552 | { 553 | Port = port; 554 | } 555 | 556 | public int Port { get; private set; } 557 | 558 | public string LastError { get; private set; } 559 | 560 | public MiniTestServerRequestHandler RequestHandler { get; set; } 561 | 562 | public void Start() 563 | { 564 | cts = new CancellationTokenSource(); 565 | 566 | listener = new TcpListener(IPAddress.Loopback, Port); 567 | listener.Start(); 568 | 569 | Port = ((IPEndPoint)listener.LocalEndpoint).Port; 570 | 571 | Console.WriteLine("Server started: " + Port); 572 | runTask = Task.Run(() => RunServer(cts.Token)); 573 | } 574 | 575 | public async Task Stop() 576 | { 577 | listener.Stop(); 578 | cts.Cancel(); 579 | await runTask; 580 | Console.WriteLine("Server stopped"); 581 | } 582 | 583 | public void Dispose() 584 | { 585 | try 586 | { 587 | Stop().Wait(); 588 | } 589 | catch 590 | { } 591 | } 592 | 593 | private async Task RunServer(CancellationToken ct) 594 | { 595 | while (!ct.IsCancellationRequested) 596 | { 597 | try 598 | { 599 | var client = await listener.AcceptTcpClientAsync(); 600 | try 601 | { 602 | var clientEndPoint = (IPEndPoint)client.Client.RemoteEndPoint; 603 | 604 | using var stream = client.GetStream(); 605 | 606 | SpinWait.SpinUntil(() => stream.DataAvailable || ct.IsCancellationRequested); 607 | if (ct.IsCancellationRequested) 608 | { 609 | Console.WriteLine("Server cancel => WaitData"); 610 | return; 611 | } 612 | 613 | byte[] buffer = new byte[100]; 614 | var bytes = new List(); 615 | do 616 | { 617 | int count = await stream.ReadAsync(buffer, 0, buffer.Length, ct); 618 | bytes.AddRange(buffer.Take(count)); 619 | } 620 | while (stream.DataAvailable && !ct.IsCancellationRequested); 621 | 622 | if (ct.IsCancellationRequested) 623 | { 624 | Console.WriteLine("Server cancel => DataRead"); 625 | return; 626 | } 627 | 628 | Debug.WriteLine($"Server data read done: {bytes.Count} bytes"); 629 | if (RequestHandler != null) 630 | { 631 | Console.WriteLine("Server send RequestHandler"); 632 | try 633 | { 634 | byte[] response = RequestHandler(bytes.ToArray(), clientEndPoint); 635 | Console.WriteLine($"Server response: {response?.Length ?? -1}"); 636 | if (response != null) 637 | { 638 | await stream.WriteAsync(response, 0, response.Length, ct); 639 | Console.WriteLine("Server response written"); 640 | } 641 | } 642 | catch (Exception ex) 643 | { 644 | LastError = ex.GetMessage(); 645 | } 646 | } 647 | } 648 | finally 649 | { 650 | client?.Dispose(); 651 | } 652 | } 653 | catch (OperationCanceledException) when (ct.IsCancellationRequested) 654 | { } 655 | catch (ObjectDisposedException) when (ct.IsCancellationRequested) 656 | { } 657 | catch (Exception ex) 658 | { 659 | string msg = ex.GetMessage(); 660 | Console.WriteLine($"Server exception: " + msg); 661 | } 662 | } 663 | } 664 | } 665 | 666 | #endregion TestServer 667 | 668 | // Time for the scheduler to launch a thread to start the reconnect 669 | private async Task EnsureWait() 670 | { 671 | await Task.Delay(100); 672 | } 673 | } 674 | } 675 | -------------------------------------------------------------------------------- /test/UnitTests/StructureTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | namespace UnitTests 4 | { 5 | [TestClass] 6 | public class StructureTests 7 | { 8 | public void CoilTest() 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------