├── .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 |
--------------------------------------------------------------------------------