191 Commits

Author SHA1 Message Date
Felix Weiß
8bd1ce6b4d Disabled live plc tests for now 2023-08-24 09:39:28 +02:00
Felix Weiß
79d6f7fa54 Update publish-pipeline.yml 2023-08-24 09:35:36 +02:00
Felix Weiß
a58e60d4f9 Update csproj nuget references 2023-08-22 16:36:34 +02:00
Felix Weiß
c6bddc61fb Update test-pipeline.yml 2023-08-22 15:36:13 +02:00
Felix Weiß
300d84daf2 Update test-pipeline.yml 2023-08-22 15:20:23 +02:00
Felix Weiß
78b12afbc3 Update README.md 2023-08-22 15:15:43 +02:00
Felix Weiß
30fec39731 Update publish-pipeline.yml 2023-08-22 15:12:49 +02:00
Felix Weiß
372043f18e Update test-pipeline.yml 2023-08-22 15:12:08 +02:00
Felix Weiß
5e8baaddfd Update issue templates 2023-08-21 17:38:12 +02:00
Felix Weiß
97b1a665d7 (pipeline) Fix upload asset path 2023-08-21 17:07:52 +02:00
Felix Weiß
2afcea2767 (pipeline) fix pathing again 2023-08-21 17:03:12 +02:00
Felix Weiß
5d4ef322ee (pipeline) fix pathing 2023-08-21 16:57:58 +02:00
Felix Weiß
590a3a3723 (pipeline) Fix deploy mode to Release 2023-08-21 16:39:01 +02:00
Felix Weiß
19944f9fd9 (pipeline) Fix broken restore 2023-08-21 16:28:34 +02:00
Felix Weiß
993c3cba7e Move examples from root to /Examples 2023-08-21 16:21:55 +02:00
Felix Weiß
47e50078b0 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-21 16:10:17 +02:00
Felix Weiß
42d6ee607d (pipeline) fix publish 2023-08-21 16:10:08 +02:00
Felix Weiß
bd907b80df Update README.md 2023-08-21 15:39:59 +02:00
Felix Weiß
b17fcfeff9 Update README.md 2023-08-21 15:38:36 +02:00
Felix Weiß
b3909e3f4b Update README.md 2023-08-21 15:36:20 +02:00
Felix Weiß
40016a5fa5 Update README.md 2023-08-21 15:24:33 +02:00
Felix Weiß
4551d24128 (pipeline) Fix wrong push name for auto_docs branches 2023-08-21 14:41:47 +02:00
Felix Weiß
aed1690a7c Fix docbuilder not knowing linux user paths 2023-08-21 14:36:52 +02:00
Felix Weiß
806b45beef (pipeline) docbuilder custom branch 2023-08-21 14:30:28 +02:00
Felix Weiß
5b4a9966b1 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-21 14:13:58 +02:00
Felix Weiß
28752f4396 (pipeline) Add ammend auto docs 2023-08-21 14:13:33 +02:00
GitHub Action
8e5051ed83 (AUTO_DOC) add documentation for branch master 2023-08-21 12:11:20 +00:00
Felix Weiß
938918e4df Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-21 14:09:17 +02:00
Felix Weiß
642aed666c (householding) formatting fixes 2023-08-21 14:09:12 +02:00
GitHub Action
a8ee01dfd7 (AUTO_DOC) add documentation for branch master 2023-08-21 12:07:34 +00:00
Felix Weiß
a0e54bf8e1 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-21 14:05:26 +02:00
Felix Weiß
eca2798859 (pipeline) Add merge last commits for auto doc 2023-08-21 14:05:13 +02:00
GitHub Action
7677ff5015 (AUTO_DOC) add documentation for branch master 2023-08-21 11:47:04 +00:00
Felix Weiß
aff5ca9fda Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-21 13:44:38 +02:00
Felix Weiß
5ff29e3ffc Fix not supported extended msg header on older plcs 2023-08-21 13:44:30 +02:00
GitHub Action
dba8de159b (AUTO_DOC) add documentation for branch master 2023-08-16 09:27:45 +00:00
Felix Weiß
5221664071 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-16 11:25:27 +02:00
Felix Weiß
81676ec2e5 Fix removed nuget conf 2023-08-16 11:24:55 +02:00
GitHub Action
7f3db93f94 (AUTO_DOC) add documentation for branch master 2023-08-15 17:34:25 +00:00
Felix Weiß
0668ad2f86 Add reconnect task cancellation
- fix missing build methods for array registers
- add reconnect injection task
- add slim semaphore for async message single thread handling
2023-08-15 19:31:45 +02:00
Felix Weiß
354c4d6428 Removed polling message for area 2023-08-14 19:43:57 +02:00
Felix Weiß
a00f56074f Added intial connection result 2023-08-14 19:42:55 +02:00
Felix Weiß
4fb9910d54 Adjusted for new mewtocol ver 2023-08-13 23:31:34 +02:00
Felix Weiß
111eacb785 Fixed bool support and duplicate register refs 2023-08-09 18:08:47 +02:00
Felix Weiß
a06a69be3f Generized memory areas 2023-08-08 17:41:18 +02:00
Felix Weiß
5ed2580bb6 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-07 18:05:10 +02:00
Felix Weiß
c41b0cef16 Fix queue and dc issues 2023-08-07 18:05:02 +02:00
GitHub Action
ec78fad881 (AUTO_DOC) add documentation for branch master 2023-08-02 15:37:31 +00:00
Felix Weiß
82d9aa8b16 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-08-02 17:35:16 +02:00
Felix Weiß
74cb3bd59f Fixed canceling messages 2023-08-02 17:35:09 +02:00
GitHub Action
b62e0b06e6 (AUTO_DOC) add documentation for branch master 2023-07-31 16:01:24 +00:00
Felix Weiß
e5020253c9 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-07-31 17:59:25 +02:00
Felix Weiß
aa8441fa33 Gone from async queue 2023-07-31 17:59:19 +02:00
GitHub Action
b64a82d5bb (AUTO_DOC) add documentation for branch master 2023-07-28 13:53:51 +00:00
Felix Weiß
3f3e3abe15 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-07-28 15:51:41 +02:00
Felix Weiß
f4af3dd05e Register str data auto updates 2023-07-28 15:51:31 +02:00
GitHub Action
d695108fd4 (AUTO_DOC) add documentation for branch master 2023-07-27 17:06:01 +00:00
Felix Weiß
38b83affd7 Fix multiple poller situational problems 2023-07-27 19:03:02 +02:00
Felix Weiß
1f33ebb8d3 F func params parser 2023-07-26 22:47:35 +02:00
Felix Weiß
4d6eee5585 Added missing F functions set 2023-07-26 00:20:01 +02:00
Felix Weiß
d24201b5d9 Source gen F functions 2023-07-25 16:07:19 +02:00
Felix Weiß
bf78156a9d New functions 2023-07-23 15:29:29 +02:00
GitHub Action
eec1479406 (AUTO_DOC) add documentation for branch master 2023-07-21 16:30:21 +00:00
Felix Weiß
fa2762c082 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-07-21 18:28:36 +02:00
Felix Weiß
9d3e895c3b Fix docbuilder csproj name 2023-07-21 18:28:30 +02:00
Felix Weiß
c1ce66c253 Update README.md 2023-07-21 18:25:28 +02:00
Felix Weiß
a399e0fa16 Fix typecodes 2023-07-21 18:23:20 +02:00
Felix Weiß
9bcffad77b Fix some of the old tests
- add new examples
- fix struct builder
- complete array interfaces
2023-07-20 23:28:58 +02:00
Felix Weiß
3a7b787949 Fix Rbuild layout 2023-07-18 17:33:10 +02:00
Felix Weiß
8fdff367f6 Restricted register generics 2023-07-18 00:51:33 +02:00
Felix Weiß
eb70dac5a8 Multiple fixes 2023-07-17 17:44:20 +02:00
Felix Weiß
d6c00097bc Simplify registers
- add array conversion
- fixed dt area merging
2023-07-17 00:41:27 +02:00
Felix Weiß
4666d3071b Array support first addition 2023-07-14 17:45:31 +02:00
Felix Weiß
32c20e7360 Add Dword / Word 2023-07-14 00:33:42 +02:00
Felix Weiß
daecd73a6d Add more xml doc
- fixed dynamically sized registers auto dt area resize
- add more anonymous register r/w
- add more register builder type casting methods
2023-07-13 00:34:06 +02:00
Felix Weiß
82821e773d Change register list source
- add poller levels
- add new register builder pattern
2023-07-12 18:54:07 +02:00
Felix Weiß
6c7e91f648 Add underlying byte data for registers
- change backend logic for register r/w
- remade interface builder pattern for better syntactic sugar
- refined tests
2023-07-12 00:32:56 +02:00
Felix Weiß
fbd53c850f Change datatypes to uint for registers
- add new tests / fix old tests
- change register builder pattern
- add register attribute from string name
- fix register update triggers
2023-07-11 01:37:44 +02:00
Felix Weiß
d8c18bbf34 Moved MewExplorer to a new repo 2023-07-10 11:00:34 +02:00
Felix Weiß
88cdc1a36c Merge PlcSeriesType into PlcType by using 16 bits 2023-07-07 17:59:22 +02:00
Felix Weiß
864389a4ad Fixed LinqHelpers namespace 2023-07-07 16:46:53 +02:00
Felix Weiß
5d6cef91ec Add plc series types to support FP7 and EcoLogiX
- add new checksum calculation for Mewtocol 7
2023-07-07 16:37:04 +02:00
Felix Weiß
2615f8c428 Update README.md 2023-07-07 00:23:22 +02:00
Felix Weiß
03341d3663 Update test-pipeline.yml 2023-07-07 00:18:41 +02:00
Felix Weiß
6c324bcdff Update test-pipeline.yml 2023-07-07 00:15:19 +02:00
Felix Weiß
befadb67a5 Dev (#9)
* Added autodoc builder step

* Added ignore badges branch for push events

* Fixed doc builder
2023-07-07 00:11:30 +02:00
Felix Weiß
6cb45ac27d Update README.md 2023-07-06 23:43:32 +02:00
Felix Weiß
fcb62960ec Removed MewExplorer from test pipeline 2023-07-06 23:33:18 +02:00
Felix Weiß
fd85a87f5d Added maui workloads 2023-07-06 23:08:28 +02:00
Felix Weiß
bd6a6cf177 Fixed missing dotnet 7 2023-07-06 23:04:40 +02:00
Felix Weiß
638b173847 Fixed env var in test pipeline 2023-07-06 23:01:33 +02:00
Felix Weiß
aa282fe156 Merge pull request #7 from WOmed/AutoStringParse
Auto string parse
2023-07-06 22:54:19 +02:00
Felix Weiß
fe3c849934 Fixed needs in pipeline 2023-07-06 22:49:04 +02:00
Felix Weiß
953e22503a Added new logic for test pipeline 2023-07-06 22:46:17 +02:00
Felix Weiß
d956525538 Removed old examples
- added instant register update if write was successful
2023-07-06 22:20:01 +02:00
Felix Weiß
616d102dea Added new console for all examples later on 2023-07-06 18:51:54 +02:00
Felix Weiß
6d3b5adf7d Remove wrong md 2023-07-06 11:46:51 +02:00
Felix Weiß
a0da9e77fe Added missing PLC types for FP2, FP10SH, FM-M, FP10 2023-07-06 11:39:34 +02:00
Felix Weiß
ab7f5ca302 Added new PLC types and auto documentation 2023-07-06 00:14:40 +02:00
Felix Weiß
f338acfe8a Fixes 2023-07-03 01:25:29 +02:00
Felix Weiß
8c2ba1f68f Added serial port support
- complete restructure of codebase
2023-06-30 18:39:19 +02:00
Felix Weiß
c332cd9f86 Added COM cassette features 2023-06-29 18:13:28 +02:00
Felix Weiß
15cc2e245d Added blazor MewExplorer project 2023-06-28 23:31:07 +02:00
Felix Weiß
bc765b870a Added new conversions
- added compile target for net6
2023-06-28 18:59:28 +02:00
Felix Weiß
a9bd2962b4 Added performance improvements for cyclic polling by using single frame building of multiple registers
- cleaned and refactored codebase
2023-06-27 20:44:11 +02:00
Felix Weiß
7be52efb7e Further implementation
- added new parsing method
2023-06-26 19:13:04 +02:00
Felix Weiß
b48f86d23d Added register builder for booleans and numerics 2023-06-25 22:45:07 +02:00
Felix Weiß
f5f1e3bea9 Namespaces fixed 2023-06-23 15:45:14 +02:00
Felix Weiß
88ad175145 Restructured files
- householding
2023-06-23 15:40:34 +02:00
Felix Weiß
ebee8a0623 Changed boolean register usage and attributes 2023-06-23 13:51:11 +02:00
Felix Weiß
50a11a3497 Update test-pipeline.yml 2023-06-18 23:21:52 +02:00
Felix Weiß
2f9c86d1a4 Update test-pipeline.yml 2023-06-18 23:14:16 +02:00
Felix Weiss
62bb1b422a Added more unit tests (#4) 2023-06-18 22:51:28 +02:00
Felix Weiß
431c5895f0 Update README.md 2023-06-18 22:26:35 +02:00
Felix Weiß
ebde5421ab Update README.md 2023-06-18 22:21:57 +02:00
Felix Weiß
c1678ae6b4 Update test-pipeline.yml 2023-06-18 22:16:19 +02:00
Felix Weiß
e2f53f9c4a Update README.md 2023-06-18 22:09:18 +02:00
Felix Weiß
844fcfc76d Update test-pipeline.yml 2023-06-18 21:55:03 +02:00
Felix Weiß
111482ec2d Update test-pipeline.yml 2023-06-18 21:52:16 +02:00
Felix Weiß
b72771c13e Update test-pipeline.yml 2023-06-18 21:48:09 +02:00
Felix Weiß
caa138c130 Update test-pipeline.yml 2023-06-18 21:37:43 +02:00
Felix Weiß
e3b6fdf780 Update test-pipeline.yml 2023-06-17 13:15:12 +02:00
Felix Weiß
5dfa48a89a Update test-pipeline.yml 2023-06-17 12:54:36 +02:00
Felix Weiß
128b9f9705 Update test-pipeline.yml 2023-06-17 12:43:32 +02:00
Felix Weiß
69d728f345 Update test-pipeline.yml 2023-06-17 11:41:06 +02:00
Felix Weiß
c01204ce57 Update test-pipeline.yml 2023-06-17 11:33:23 +02:00
Felix Weiß
da2cb6b58a Update test-pipeline.yml 2023-06-17 11:28:21 +02:00
Felix Weiß
96827f3baf Update test-pipeline.yml 2023-06-17 11:21:08 +02:00
Felix Weiß
2df944da8c Update test-pipeline.yml 2023-06-17 11:17:21 +02:00
Felix Weiß
f5e6d7d6af Added new tests 2023-06-16 16:29:01 +02:00
Felix Weiß
ff3cc41aea Update README.md 2023-06-16 15:05:34 +02:00
Felix Weiß
d5b701186e Fixed caching again 2023-06-16 14:36:49 +02:00
Felix Weiß
0702ab7d0a Fixed caching 2023-06-16 14:32:54 +02:00
Felix Weiß
5adadbfc46 fixed caching 2023-06-16 14:24:16 +02:00
Felix Weiß
739f83bf0c Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-06-16 14:19:05 +02:00
Felix Weiß
a9bd792af1 added test result saving 2023-06-16 14:18:51 +02:00
Felix Weiß
e6ffc92db6 Update README.md 2023-06-16 12:21:41 +02:00
Felix Weiß
7bd07bb520 Fixed env.VERSION for sed step 2023-06-16 12:10:57 +02:00
Felix Weiß
f99af9511b Fix wrong ver 2023-06-16 12:03:44 +02:00
Felix Weiß
b13b2d0199 Fixed sed escape 2023-06-16 11:49:17 +02:00
Felix Weiß
364e0637bb fixed versioning auto release 2023-06-16 11:42:04 +02:00
Felix Weiß
bba90106cb Made test-pipeline.yml callable 2023-06-16 11:22:48 +02:00
Felix Weiß
3dea18d285 Speperated test and publish pipeline 2023-06-16 11:18:08 +02:00
Sandoun
ad61361008 Use setup dotnet v2 2023-06-16 10:39:41 +02:00
Sandoun
0e0659000a Fixed ci no write permission 2023-06-16 10:34:57 +02:00
Sandoun
8cc47b496a Fixed new publish pipeline 2023-06-16 10:30:14 +02:00
Sandoun
9eb09fc7ec Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-06-16 10:24:25 +02:00
Sandoun
9b8fca6561 Changed package publisher to gh 2023-06-16 10:24:19 +02:00
Felix Weiß
6205f81931 Update README.md 2023-06-16 10:05:30 +02:00
Sandoun
a8960f12c8 Changed to opencover test format 2023-06-16 10:00:15 +02:00
Felix Weiß
0b670b1a27 Update build-pipeline.yml 2023-06-16 09:48:15 +02:00
Felix Weiß
63a1348704 Update README.md 2023-06-15 21:03:31 +02:00
Felix Weiß
61fe2aff65 Update build-pipeline.yml 2023-06-15 21:02:55 +02:00
Felix Weiß
314ce9c053 Merge pull request #5 from WOmed/PropAutoSetters
Prop auto setters
2023-06-15 20:51:34 +02:00
Sandoun
7864915967 Added auto prop sending
- updated readme
2023-06-15 20:49:14 +02:00
Sandoun
62f4a48bf9 Housekeeping 2023-06-15 20:23:48 +02:00
Sandoun
a78a405c25 update readme 2023-06-15 20:08:20 +02:00
Felix Weiß
09f4da54a9 Made registers use the IRegister interface
- cleanup and refactoring
- fully implemented auto prop register generator unit tests #4
- added plc test program c30 fpx-h
- fixed bitarray setback
- cleaned up examples and added new ones with addition of attributes for later additions
2023-06-15 20:04:38 +02:00
Felix Weiss
6ca8e9de96 Added mewtocol logger to unit test output 2023-06-13 23:11:56 +02:00
Felix Weiss
53a0856634 Change test result step name 2023-06-13 22:58:50 +02:00
Felix Weiß
2b173aeb95 Update build-pipeline.yml 2023-06-13 22:44:49 +02:00
Felix Weiß
2680bbb07b Update build-pipeline.yml 2023-06-13 22:41:46 +02:00
Felix Weiß
9fd178424f Update build-pipeline.yml 2023-06-13 22:35:38 +02:00
Felix Weiß
e96496cff8 Update README.md 2023-06-13 13:52:40 +02:00
Felix Weiß
39ff1a5c5b Made code ql only callable 2023-06-13 13:02:21 +02:00
Felix Weiß
6c50324696 Code ql test 2023-06-13 12:58:37 +02:00
Felix Weiß
affa2ea83f Merged CI into one workflow file 2023-06-13 12:49:33 +02:00
Felix Weiß
af101812a8 Reordered CI 2023-06-13 12:35:34 +02:00
Felix Weiß
35f2786129 Added test CI on push 2023-06-13 12:30:08 +02:00
Felix Weiß
062eb75876 CI use only net6 2023-06-13 12:09:29 +02:00
Felix Weiß
c771e747eb CI only .net 5 and 6 2023-06-13 12:04:30 +02:00
Felix Weiß
dc24b33297 CI fix 2023-06-13 12:01:28 +02:00
Felix Weiß
68aa2ad12a Unit test CI 2023-06-13 11:56:09 +02:00
Felix Weiß
bb1cc8c011 Merge pull request #3 from WOmed/dev
Added new unit tests
2023-06-13 11:45:38 +02:00
Felix Weiß
24525521af Added new unit tests 2023-06-13 11:42:49 +02:00
Felix Weiß
435624f8a2 Update test-on-hardware.yml 2023-06-13 10:41:11 +02:00
Felix Weiß
7c3f279576 Create test-on-hardware.yml 2023-06-13 10:36:33 +02:00
Felix Weiß
6001402991 Merge branch 'master' of https://github.com/WOmed/MewtocolNet 2023-02-27 16:01:54 +01:00
Felix Weiß
fb2bd8d56d Added test structure 2023-02-27 16:01:47 +01:00
Felix Weiß
a71d545bf3 Create codeql.yml 2023-02-24 10:11:51 +01:00
Felix Weiß
43c7f72ac4 Added auto register reset to default typed value on disconnect 2022-10-21 11:56:58 +02:00
Felix Weiß
2e35ed87af Updated hyperlinks in readme 2022-10-20 10:16:11 +02:00
Felix Weiß
a7f97a72ea Package id change in readme 2022-10-20 10:11:13 +02:00
Felix Weiß
51870166e4 Update publish.yml 2022-10-20 10:04:49 +02:00
Felix Weiß
b43e9bd201 Package id 2022-10-20 10:02:59 +02:00
Felix Weiß
e313dbc3ec Changed package ID 2022-10-20 10:01:53 +02:00
Felix Weiß
bdf9f93f97 Update MewtocolNet.csproj 2022-10-20 09:50:58 +02:00
Felix Weiß
fe816ab78e Update publish.yml 2022-10-20 09:46:08 +02:00
Felix Weiß
48a5977185 Create publish.yml 2022-10-20 09:36:35 +02:00
Felix Weiß
c69f63c191 Update README.md 2022-09-27 10:21:37 +02:00
Felix Weiß
6a2f278dd1 Fixed negative queued msges
- version count up
2022-09-27 10:18:01 +02:00
184 changed files with 21380 additions and 3579 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

70
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
workflow_call:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'csharp' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,23 +0,0 @@
name: .NET Windows
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
dotnet-version: [ '3.0', '3.1.x', '5.0.x' ]
steps:
- uses: actions/checkout@v3
- name: Setup dotnet ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ matrix.dotnet-version }}
# You can test your matrix by printing the current dotnet version
- name: Display dotnet version
run: dotnet --version

178
.github/workflows/publish-pipeline.yml vendored Normal file
View File

@@ -0,0 +1,178 @@
name: Publish pipeline
on:
workflow_dispatch:
inputs:
build_pre_release:
description: 'Mark as pre-release'
required: true
default: false
type: boolean
version_tag:
description: 'The version number formatted as X.X.X'
default: '0.1.0'
required: true
type: string
permissions: write-all
jobs:
test-pipeline:
name: 'Invoke the test pipeline'
uses: ./.github/workflows/test-pipeline.yml
secrets: inherit
#Deploy package
publish-package:
name: 'Build and publish package'
needs: test-pipeline
runs-on: [self-hosted, linux, ARM64]
steps:
- uses: actions/checkout@v3
- name: 'RenameVersionTag'
shell: bash
run: |
PRE_REL_STR='-pre'
EMPTY_STR=
if ${{ github.event.inputs.build_pre_release }}
then
echo "prerelease_append=$PRE_REL_STR" >> $GITHUB_ENV
else
echo "prerelease_append=$EMPTY_STR" >> $GITHUB_ENV
fi
- name: Set .csproj version to ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
run: |
sed -i 's/<Version>[0-9].[0-9].[0-9]<\/Version>/<Version>${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}<\/Version>/g' MewtocolNet/MewtocolNet.csproj
less MewtocolNet/MewtocolNet.csproj
- name: 'Bump version and push tag v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}'
id: tag_version
uses: mathieudutour/github-tag-action@v6.1
with:
tag_prefix: v
custom_tag: ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: 'Build Changelog'
uses: mikepenz/release-changelog-builder-action@v3
id: github_release_log
with:
commitMode: true
configurationJson: |
{
"template": "#{{CHANGELOG}}\n\n<details>\n<summary>Uncategorized</summary>\n\n#{{UNCATEGORIZED}}\n</details>",
"pr_template": "- #{{TITLE}} #{{MERGE_SHA}}",
"trim_values" : true,
"ignore_labels": [ "ignore" ],
"categories": [
{
"title": "## 🚀 Added Features",
"labels": ["feature", "features", "add", "added", "implemented", "impl", "new"]
},
{
"title": "## 🔃 Changed Features",
"labels": ["change", "changed"]
},
{
"title": "## ❌ Removed Features",
"labels": ["remove", "removed"]
},
{
"title": "## 🪲 Fixes",
"labels": ["fix", "fixed", "fixes"]
},
{
"title": "## 📖 Documentation",
"labels": ["doc", "docs", "documentation"]
},
{
"title": "## 🧪 Tests",
"labels": ["test", "tests", "unittests"]
},
{
"title": "## 📦 Dependencies",
"labels": ["dependencies", "package", "nuget"]
},
{
"title": "## 🏠 Householding",
"labels": ["upgraded", "upgrade", "update", "clear", "delete", "deleted", "version"]
},
{
"title": "## 🌎 Localization",
"labels": ["lang", "language", "localization", "locale", "translation", "translations"]
},
{
"title": "## 💬 Other",
"labels": ["other"]
}
],
"duplicate_filter": {
"pattern": ".*",
"on_property": "title",
"method": "match"
},
"transformers": [
{
"pattern": "^.*?\\s*\\(?(\\w+)\\)? (.*)",
"target": "- $1 $2"
}
],
"label_extractor": [
{
"pattern": "^.*?\\s*\\(?(\\w{3,})\\)? (.*)",
"target": "$1",
"flags": "gis"
}
]
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: '6.0.x'
- name: Restore dependencies
run: dotnet restore ./MewtocolNet/MewtocolNet.csproj
- name: Build as ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
run: dotnet build "MewtocolNet" --no-incremental -c:Release
- name: Pack as ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
run: dotnet pack "MewtocolNet" -c:Release
- name: Publish as ${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
run: |
cd '${{ github.workspace }}/Builds/MewtocolNet'
ls -l
dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{ secrets.GITHUB_TOKEN }} --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
dotnet nuget push "*.nupkg" --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source "https://api.nuget.org/v3/index.json"
- name: 'Create Release v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}'
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
release_name: v${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}
body: |
## Changelog
${{ steps.github_release_log.outputs.changelog }}
**Total commits:** ${{ steps.github_release_log.outputs.commits }}
draft: false
prerelease: ${{ github.event.inputs.build_pre_release }}
- name: 'Upload package to latest release'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: "${{ steps.create_release.outputs.upload_url }}"
asset_path: ${{ github.workspace }}/Builds/MewtocolNet/Mewtocol.NET.${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}.nupkg
asset_name: Mewtocol.NET.${{ github.event.inputs.version_tag }}${{ env.prerelease_append }}.nupkg
asset_content_type: application/zip

109
.github/workflows/test-pipeline.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: Test pipeline
on:
workflow_dispatch:
workflow_call:
inputs:
cache-id:
default: 'test-results'
required: false
type: string
push:
branches-ignore:
- badges
paths-ignore:
- '**.md'
permissions: write-all
jobs:
#Run unit tests on the test PLCs
run-unit-tests:
name: 'Run tests and documentation'
runs-on: self-hosted
steps:
- uses: actions/checkout@v3
- name: 'Extract branch name'
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
id: extract_branch
- name: 'Setup dotnet'
uses: actions/setup-dotnet@v2
with:
dotnet-version: |
6.0.x
7.0.x
- name: 'Run tests'
run: dotnet test "./MewtocolTests" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=../Builds/TestResults/coverage.opencover.xml
- name: 'Run docbuilder'
continue-on-error: true
run: |
dotnet run --project "./AutoTools.DocBuilder/AutoTools.DocBuilder.csproj" "~/plctypes.md"
- name: 'Switch and Commit to docs branch'
continue-on-error: true
run: |
git fetch
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git checkout --orphan ${{ steps.extract_branch.outputs.branch }}_auto_docs
git rm -rf .
cp ~/plctypes.md ./plctypes.md &&
git add "./plctypes.md" -f &&
git commit -m "Update documentation"
- name: 'Push docs commit'
uses: ad-m/github-push-action@master
continue-on-error: true
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ steps.extract_branch.outputs.branch }}_auto_docs
- name: Report Generator
uses: danielpalme/ReportGenerator-GitHub-Action@5.1.22
with:
reports: './Builds/TestResults/coverage.opencover.xml'
targetdir: './Builds/TestResults'
reporttypes: HtmlSummary;MarkdownSummaryGithub;Badges
historydir: './Builds/Hist'
title: Report
- name: Markdown report and copy for badges branch
run: |
cat './Builds/TestResults/SummaryGithub.md' >> $GITHUB_STEP_SUMMARY
cp ./Builds/TestResults/badge_combined.svg ~/badge_combined.svg
cp ./Builds/TestResults/summary.html ~/summary.html
ls -l ~
- name: Cache test results
if: ${{ github.event_name == 'workflow_call' }}
uses: actions/cache/save@v3
with:
key: ${{ inputs.cache-id }}-${{ steps.extract_branch.outputs.branch }}
path: |
${{ github.workspace }}/Builds/TestResults
- name: Commit badge
continue-on-error: true
run: |
git fetch
git checkout badges
cp ~/summary.html ./Builds/TestResults/summary_${{ steps.extract_branch.outputs.branch }}.html
cp ~/badge_combined.svg ./Builds/TestResults/badge_combined_${{ steps.extract_branch.outputs.branch }}.svg
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add "./Builds/TestResults/badge_combined_${{ steps.extract_branch.outputs.branch }}.svg" -f
git add "./Builds/TestResults/summary_${{ steps.extract_branch.outputs.branch }}.html" -f
git commit -m "Add/Update badge for branch ${{ steps.extract_branch.outputs.branch }}"
- name: 'Push badge commit'
uses: ad-m/github-push-action@master
if: ${{ success() }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: badges

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj"/>
<PackageReference Include="HtmlAgilityPack" Version="1.11.50" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,510 @@
using System.Collections.Specialized;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using MewtocolNet;
using MewtocolNet.Helpers;
namespace AutoTools.ChmDataExtract;
internal class Program {
const string sysVarsLoc = @"Panasonic-ID SUNX Control\Control FPWIN Pro 7\Mak\Res_Eng\SysVars.chm";
const string funcNamesLoc = @"Panasonic-ID SUNX Control\Control FPWIN Pro 7\Mak\Res_Eng\FPWINPro.chm";
static Dictionary<string, List<PlcType>> plcGroups = new() {
{ "FP7 CPS41/31 E/ES", new List<PlcType> {
PlcType.FP7_120k__CPS31E,
PlcType.FP7_196k__CPS41E,
PlcType.FP7_120k__CPS31ES,
PlcType.FP7_196k__CPS41ES,
}},
{ "FP7 CPS31/31S", new List<PlcType> {
PlcType.FP7_120k__CPS31,
PlcType.FP7_120k__CPS31S,
}},
{ "FP7 CPS21", new List<PlcType> {
PlcType.FP7_64k__CPS21,
}},
{ "ELC500", new List<PlcType> {
PlcType.ECOLOGIX_0k__ELC500,
}},
{ "FP-SIGMA 12k", new List<PlcType> {
PlcType.FPdSIGMA_12k,
PlcType.FPdSIGMA_16k,
}},
{ "FP-SIGMA 32k", new List<PlcType> {
PlcType.FPdSIGMA_32k,
PlcType.FPdSIGMA_40k,
}},
{ "FP0R 16k/32k C types", new List<PlcType> {
PlcType.FP0R_16k__C10_C14_C16,
PlcType.FP0R_32k__C32,
}},
{ "FP0R 32k T32", new List<PlcType> {
PlcType.FP0R_32k__T32,
PlcType.FP0R_32k__F32,
}},
{ "FP2 16k", new List<PlcType> {
PlcType.FP2_16k,
}},
{ "FP2 32k", new List<PlcType> {
PlcType.FP2_32k,
}},
{ "FP2SH 32k/60k/120k", new List<PlcType> {
PlcType.FP2SH_60k,
PlcType.FP2SH_60k,
PlcType.FP2SH_120k,
}},
{ "FP-X 16k/32k R-types", new List<PlcType> {
PlcType.FPdX_16k__C14R,
PlcType.FPdX_32k__C30R_C60R,
}},
{ "FP-X 16k/32k T-types", new List<PlcType> {
PlcType.FPdX_16k__C14TsP,
PlcType.FPdX_32k__C30TsP_C60TsP_C38AT_C40T,
}},
{"FP0H C32T/P ET/EP", new List<PlcType> {
PlcType.FP0H_32k__C32TsP,
PlcType.FP0H_32k__C32ETsEP,
}},
{ "FP-X 16k/32k L-types", new List<PlcType> {
PlcType.FPdX_16k__L14,
PlcType.FPdX_32k__L30_L60,
}},
{ "FP-X 2.5k C40RT0A", new List<PlcType> {
PlcType.FPdX_2c5k__C40RT0A,
}},
{ "FP-X0 2.5k L14,L30", new List<PlcType> {
PlcType.FPdX0_2c5k__L14_L30,
}},
{ "FP-X0 8k L40,L60", new List<PlcType> {
PlcType.FPdX0_8k__L40_L60,
}},
{ "FP-e 2.7k", new List<PlcType> {
PlcType.FPde_2c7k,
}},
{ "FP-XH 16k/32k R-types", new List<PlcType> {
PlcType.FPdXH_16k__C14R,
PlcType.FPdXH_32k__C30R_C40R_C60R,
}},
{ "FP-XH 16k/32k T-types", new List<PlcType> {
PlcType.FPdXH_16k__C14TsP,
PlcType.FPdXH_32k__C30TsP_C40T_C60TsP,
PlcType.FPdXH_32k__C30TsP_C40T_C60TsP,
PlcType.FPdXH_32k__C38AT,
PlcType.FPdXH_32k__C40ET_C60ET,
PlcType.FPdXH_32k__C60ETF,
}},
{ "FP-XH M4/M8 types", new List<PlcType> {
PlcType.FPdXH_32k__M4TsL,
PlcType.FPdXH_32k__M8N16TsP,
PlcType.FPdXH_32k__M8N30T,
}},
};
class AddressException {
public string ExceptionTitle;
public string ForSysRegister;
}
internal class FPFunction {
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string RedundantName { get; set; } = null!;
public string Description { get; set; } = null!;
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string[]>? ParametersIn { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string[]>? ParametersOut { get; set; }
}
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
static async Task AsyncMain () {
GetFunctionNames();
//await GetSystemRegisters();
}
static void GetFunctionNames () {
var functions = new Dictionary<string, FPFunction>();
var progLoc = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var progFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
var sysVarsPath = Path.Combine(progFilesPath, funcNamesLoc);
Directory.SetCurrentDirectory(progLoc);
File.Copy(sysVarsPath, "./FPWINPro.chm", true);
var startInfo = new ProcessStartInfo {
WorkingDirectory = progLoc,
FileName = "hh.exe",
Arguments = $"-decompile ./DecompFuncs ./FPWINPro.chm",
};
//call the hh.exe decompiler for chm
if (!File.Exists("./DecompFuncs/topics/availability.html")) {
var proc = Process.Start(startInfo)!;
proc.WaitForExit();
}
var doc = new HtmlDocument();
doc.Load("./DecompFuncs/topics/availability.html");
//[contains(@class, 'table mainbody')]
foreach (HtmlNode table in doc.DocumentNode.SelectNodes("//table[1]")) {
var rows = table?.SelectSingleNode("tbody")?.SelectNodes("tr");
if (rows == null) continue;
foreach (var row in rows) {
var columns = row.SelectNodes("td");
if (columns == null) continue;
var itemRow = columns?.FirstOrDefault()?.SelectSingleNode("p/a[contains(@class,'xref')]");
string rowName = itemRow?.InnerText ?? "Unnamed";
if (!Regex.IsMatch(rowName, @"^F[0-9]{1,3}_.*$")) continue;
FPFunction functionIns = new FPFunction();
Console.Write($"Var: {rowName, -20}");
var href = itemRow?.GetAttributeValue("href", null);
if (href != null) {
//Console.Write($" {href}");
var docSub = new HtmlDocument();
docSub.Load($"./DecompFuncs{href}");
var noteSection = docSub.DocumentNode.SelectSingleNode("//section/div[contains(@class,'note note')]");
var xrefRedundant = noteSection?.SelectSingleNode("p/a[contains(@class,'xref')]");
var xrefNodeContent = noteSection?.SelectSingleNode("p/span");
//get params in / out
var inOutDefinitionNodes = docSub.DocumentNode.SelectNodes("//p[contains(@class,'p inoutput')]");
if (inOutDefinitionNodes != null) {
foreach (var ioTypeNode in inOutDefinitionNodes) {
var nodeInOutType = ioTypeNode.InnerText.SanitizeLinebreakFormatting();
Console.Write($"{nodeInOutType}: ");
var currentSibling = ioTypeNode;
while (true) {
if (currentSibling.NextSibling == null) break;
currentSibling = currentSibling.NextSibling;
if (currentSibling.HasClass("inoutput")) {
break;
}
var paramNodes = currentSibling.SelectNodes("dt");
if (paramNodes == null) continue;
foreach (var paramNode in paramNodes) {
var paramName = paramNode.SelectSingleNode("span[1]")?.InnerText?.SanitizeBracketFormatting();
var paramTypes = paramNode.SelectSingleNode("span[2]")?.InnerText?.SanitizeBracketFormatting();
if (paramName != null && paramTypes != null) {
if (functionIns.ParametersIn == null)
functionIns.ParametersIn = new Dictionary<string, string[]>();
if (functionIns.ParametersOut == null)
functionIns.ParametersOut = new Dictionary<string, string[]>();
Console.Write($"{paramName} {paramTypes}");
var splitParamNames = paramName.Split(", ");
foreach (var splitName in splitParamNames) {
if (nodeInOutType == "Input") {
if (functionIns.ParametersIn.ContainsKey(splitName)) break;
functionIns.ParametersIn.Add(splitName, paramTypes.SanitizeBracketFormatting().Split(", "));
} else {
if (functionIns.ParametersOut.ContainsKey(splitName)) break;
functionIns.ParametersOut.Add(splitName, paramTypes.SanitizeBracketFormatting().Split(", "));
}
}
}
}
}
}
}
HtmlNode? descrSection = null;
if (xrefRedundant != null && xrefNodeContent != null && xrefNodeContent.InnerText.StartsWith("This is a redundant F instruction")) {
descrSection = docSub.DocumentNode.SelectSingleNode("//section[2]");
functionIns.RedundantName = xrefRedundant.InnerText;
//Console.Write($"{xrefRedundant.InnerText}");
} else {
descrSection = docSub.DocumentNode.SelectSingleNode("//section[1]");
}
if (descrSection != null) {
var descrText = descrSection?.InnerText;
if(descrText != null) {
descrText = descrText.SanitizeLinebreakFormatting().Replace("\"", "\\\"");
functionIns.Description = descrText;
//Console.Write($" {descrText}");
}
}
}
functions.Add(rowName, functionIns);
Console.WriteLine();
//compatibility matrix
//for (int i = 1; i < columns?.Count - 1; i++) {
// bool isChecked = columns?.ElementAtOrDefault(i)?.SelectSingleNode("p")?.InnerHtml != "";
// Console.Write($"{(isChecked ? "1" : "0")}, ");
//}
}
}
BuildFunctionNamesDictFile(functions);
}
static void BuildFunctionNamesDictFile (Dictionary<string, FPFunction> dict) {
var sb = new StringBuilder();
sb.AppendLine("using System.Collections.Generic;\n");
sb.AppendLine("namespace MewtocolNet.AutoGeneratedData {\n");
sb.AppendLine("\tpublic class FPFunction {\n");
sb.AppendLine("\t\tpublic string RedundantName { get; private set; }\n");
sb.AppendLine("\t\tpublic string Description { get; private set; }\n");
sb.AppendLine("\t\tpublic Dictionary<string, string[]> ParametersIn { get; private set; }\n");
sb.AppendLine("\t\tpublic Dictionary<string, string[]> ParametersOut { get; private set; }\n");
sb.AppendLine("\t\tpublic static readonly Dictionary<string, FPFunction> functions = new Dictionary<string, FPFunction> {\n");
foreach (var item in dict) {
sb.AppendLine($"\t\t\t{{ \"{item.Key}\", new FPFunction {{");
if(item.Value.RedundantName != null)
sb.AppendLine($"\t\t\t\tRedundantName = \"{item.Value.RedundantName}\",");
if(item.Value.Description != null)
sb.AppendLine($"\t\t\t\tDescription = \"{item.Value.Description}\",");
if (item.Value.ParametersIn != null) {
sb.AppendLine("\t\t\t\tParametersIn = new Dictionary<string, string[]> {");
foreach (var paramIn in item.Value.ParametersIn) {
sb.AppendLine($"\t\t\t\t\t{{ \"{paramIn.Key}\", new string[] {{");
foreach (var paramType in paramIn.Value) {
sb.AppendLine($"\t\t\t\t\t\t\"{paramType}\",");
}
sb.AppendLine("\t\t\t\t\t}},");
}
sb.AppendLine("\t\t\t\t},");
}
if (item.Value.ParametersOut != null) {
sb.AppendLine("\t\t\t\tParametersOut = new Dictionary<string, string[]> {");
foreach (var paramOut in item.Value.ParametersOut) {
sb.AppendLine($"\t\t\t\t\t{{ \"{paramOut.Key}\", new string[] {{");
foreach (var paramType in paramOut.Value) {
sb.AppendLine($"\t\t\t\t\t\t\"{paramType}\",");
}
sb.AppendLine("\t\t\t\t\t}},");
}
sb.AppendLine("\t\t\t\t},");
}
sb.AppendLine($"\t\t\t}}}},");
}
sb.AppendLine("\t\t};\n");
sb.AppendLine("\t}\n");
sb.AppendLine("}");
File.WriteAllText("../../../../MewtocolNet/AutoGeneratedData/FPFunction.cs", sb.ToString());
}
static async Task GetSystemRegisters () {
var addressExceptions = new List<AddressException>();
var progLoc = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var progFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
var sysVarsPath = Path.Combine(progFilesPath, sysVarsLoc);
Directory.SetCurrentDirectory(progLoc);
File.Copy(sysVarsPath, "./SysVars.chm", true);
var startInfo = new ProcessStartInfo {
WorkingDirectory = progLoc,
FileName = "hh.exe",
Arguments = $"-decompile ./Decomp ./SysVars.chm",
};
//call the hh.exe decompiler for chm
if (!File.Exists("./Decomp/topics/availability.html")) {
var proc = Process.Start(startInfo)!;
proc.WaitForExit();
}
var doc = new HtmlDocument();
doc.Load("./Decomp/topics/availability.html");
//[contains(@class, 'table mainbody')]
foreach (HtmlNode table in doc.DocumentNode.SelectNodes("//table[1]")) {
var rows = table?.SelectSingleNode("tbody")?.SelectNodes("tr");
if (rows == null) continue;
string lastRegisterName = "Name";
int iSystemRegister = 0;
foreach (var row in rows) {
var columns = row.SelectNodes("td");
if (columns == null) continue;
//get var name
var varNameNode = columns?.FirstOrDefault()?.SelectSingleNode("p/a[contains(@class,'xref')]");
string registerAddress;
int iterateStart;
if (varNameNode != null) {
lastRegisterName = varNameNode.InnerText;
//get second col
var regAddressNode = columns?.ElementAtOrDefault(1)?.SelectSingleNode("p");
registerAddress = regAddressNode?.InnerText ?? "Null";
iterateStart = 2;
} else {
//get first col
var regAddressNode = columns?.ElementAtOrDefault(0)?.SelectSingleNode("p");
registerAddress = regAddressNode?.InnerText ?? "Null";
iterateStart = 1;
}
//filter the address for annotations
var regexAnnotation = new Regex(@"\(.*\)");
var matchAnnotation = regexAnnotation.Match(registerAddress);
if (matchAnnotation.Success) {
registerAddress = regexAnnotation.Replace(registerAddress, "");
addressExceptions.Add(new AddressException {
ForSysRegister = lastRegisterName,
ExceptionTitle = matchAnnotation.Value,
});
}
Console.Write($"Var: {lastRegisterName} | {registerAddress} ".PadRight(100, ' '));
for (int i = iterateStart, j = 0; i < columns?.Count + 1; i++) {
if (j >= plcGroups.Count - 1) continue;
var group = plcGroups.Keys.ToList()[j];
bool isChecked = columns?.ElementAtOrDefault(i)?.SelectSingleNode("p")?.InnerHtml != "";
Console.Write($"{(isChecked ? "1" : "0")}, ");
j++;
}
Console.WriteLine();
}
}
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,2 @@
# Reading and writing programs

View File

@@ -0,0 +1,291 @@
# Open Questions
- What is the HW information byte for?
- What are the last bytes of the EXRT message for?
# Mewtocol Init
| PLC TYPE | CPU Code | Machine Code | HW Information|
|--------------|--------------|--------------|---------------|
| FPX C14 R | 20 | 70 | 02 |
| FPX C30 T | 20 | 77 | 02 |
| FPX-H C14 R | 20 | A0 | 01 |
| FPX-H C30 T | 20 | A5 | 01 |
## FPX 16k C14R Actual Response Examples
### %EE$RT
|Reponse Byte|Description|
|------------|-----------|
| 20 | Model code |
| 25 | Version |
| 16 | Prog capacity |
| 80 | Op mode |
| 00 | Link unit |
| E1 | Error flag |
| 2D00 | Self diag error |
### %EE$EX00RT Normal Operation
|Reponse Byte|Description|
|------------|-----------|
| 00 | Extended mode
| 32 | Data item count
| 70 | Machine type
| 00 | Version (Fixed to 00)
| 16 | Prog capacity in K
| 80 | Operation mode / status
| 00 | Link unit
| E1 | Error flag
| 2D00 | Self diag error
| 50 | Version
| 02 | Hardware information
| 0 | Number of programs
| 4100 | Program size BCD
| 1600 | Header size (no. of words) bcd
| 1604 | System register size
| 96230000001480004 | ?
What are the last bytes?
FP-X 16k C14R
96 23 00 00 00 14 80 00 4
FP-X 32k C30T/P
96 23 00 00 00 14 80 00 4
FP-XH 32k C30T/P
96 23 00 00 00 40 00 00 4
FP-XH 16k C14R
96 23 00 00 00 40 00 00 4
## FP-XH 16k C14R Actual Response Examples
### %EE$RT
|Reponse Byte|Description|
|------------|-----------|
| 20 | Model code |
| 16 | Version |
| 16 | Prog capacity |
| 81 | Op mode |
| 00 | Link unit |
| 60 | Error flag |
| 0000 | Self diag error |
## FP-X0 2.5k L14,L30
### %EE$RT
|Reponse Byte|Description|
|------------|-----------|
| 72 | Model code |
| 12 | Version |
| 02 | Prog capacity |
| 82 | Op mode |
| 00 | Link unit |
| 00 | Error flag |
| 0000 | Self diag error |
### %EE$EX00RT
|Reponse Byte|Description|
|------------|-----------|
| 00 | Extended mode
| 32 | Data item count
| 72 | Machine type
| 00 | Version (Fixed to 00)
| 02 | Prog capacity in K
| 82 | Operation mode / status
| 00 | Link unit
| 00 | Error flag
| 0000 | Self diag error
| 23 | Version
| 01 | Hardware information
| 0 | Number of programs
| 4100 | Program size BCD
| 0301 | Header size (no. of words) bcd
| 2819 | System register size
| 0000001480004 | ?
## FP0 2.7k C10,C14
### %EE$RT
|Reponse Byte|Description|
|------------|-----------|
| 05 | Model code |
| 12 | Version |
| 03 | Prog capacity |
| 82 | Op mode |
| 00 | Link unit |
| 00 | Error flag |
| 0000 | Self diag error |
### %EE$EX00RT
|Reponse Byte|Description|
|------------|-----------|
| 00 | Extended mode
| 32 | Data item count
| 40 | Machine type
| 00 | Version (Fixed to 00)
| 03 | Prog capacity in K
| 82 | Operation mode / status
| 00 | Link unit
| 00 | Error flag
| 0000 | Self diag error
| 23 | Version
| 01 | Hardware information
| 0 | Number of programs
| 4100 | Program size BCD
| 0301 | Header size (no. of words) bcd
| 2819 | System register size
| 20130000080070004 | ?
## FP0 5k C32,SL1
### %EE$RT
|Reponse Byte|Description|
|------------|-----------|
| 06 | Model code |
| 12 | Version |
| 05 | Prog capacity |
| 82 | Op mode |
| 00 | Link unit |
| 00 | Error flag |
| 0000 | Self diag error |
### %EE$EX00RT
|Reponse Byte|Description|
|------------|-----------|
| 00 | Extended mode
| 32 | Data item count
| 41 | Machine type
| 00 | Version (Fixed to 00)
| 03 | Prog capacity in K
| 82 | Operation mode / status
| 00 | Link unit
| 00 | Error flag
| 0000 | Self diag error
| 23 | Version
| 01 | Hardware information
| 0 | Number of programs
| 4100 | Program size BCD
| 0501 | Header size (no. of words) bcd
| 2819 | System register size
| 20130000080070004 | ?
## FP0 10k
### %EE$RT
|Reponse Byte|Description|
|------------|-----------|
| 42 | Model code |
| 12 | Version |
| 10 | Prog capacity |
| 82 | Op mode |
| 00 | Link unit |
| 00 | Error flag |
| 0000 | Self diag error |
### %EE$EX00RT
|Reponse Byte|Description|
|------------|-----------|
| 00 | Extended mode
| 32 | Data item count
| 42 | Machine type
| 00 | Version (Fixed to 00)
| 10 | Prog capacity in K
| 82 | Operation mode / status
| 00 | Link unit
| 00 | Error flag
| 0000 | Self diag error
| 23 | Version
| 01 | Hardware information
| 0 | Number of programs
| 4100 | Program size BCD
| 1001 | Header size (no. of words) bcd
| 2819 | System register size
| 20130000080070004 | ?
## FP2SH 60k
### %EE$RT
|Reponse Byte|Description|
|------------|-----------|
| 60 | Model code |
| 12 | Version |
| 00 | Prog capacity |
| 82 | Op mode |
| 00 | Link unit |
| 00 | Error flag |
| 0000 | Self diag error |
### %EE$EX00RT
|Reponse Byte|Description|
|------------|-----------|
| 00 | Extended mode
| 32 | Data item count
| 60 | Machine type
| 00 | Version (Fixed to 00)
| 00 | Prog capacity in K
| 82 | Operation mode / status
| 00 | Link unit
| 00 | Error flag
| 0000 | Self diag error
| 23 | Version
| 01 | Hardware information
| 0 | Number of programs
| 4100 | Program size BCD
| 6001 | Header size (no. of words) bcd
| 2819 | System register size
| 20130000000080004 | ?
# Mewtocol-7 Com
## Getting the status of the plc
`%EE#RT`
Normal plc would return the default, see above.
The FP7 series returns:
`%EE$RT0000000100000000`
> This indicates that the device uses the Mewtocol 7 format
When trying to use EXRT the FP7 returns not supported
[R] `%EE#EX00RT00`
[S] `%EE!42`
Then request the status from the PLC using Mewtocol 7
[R] `>@EEE00#30STRD00`
[O] `>@EEE00$30STRD`
|Reponse Byte|Description|
|-------------|-----------|
| 00 | Read status (always 0) |
| 07 | series code: 7 = FP7 |
| 10 | model code: 3 = CPS41E, 4 = CPS31E, 5 = CPS31, 6 = CPS41ES, 7 = CPS31ES, 8 = CPS31S, 9 = CPS21, 10 = ElC500 |
| 0000 | user special order code (Not important) |
| 0453 | latest cpu version = 4.53 |
| 0453 | communication cpu version = 4.53 |
| 0453 | operation cpu version = 4.53 |
| 01 | op mode = 4.53 |
| 00 | error flag |
| 0000 | self diag error |
| 00 | sd card information |

View File

@@ -0,0 +1,16 @@
3 byte system registers, read with RR
|RR Adress|Interpreting type|Description|
|-|-|-|
|RR000|uint16|Program size steps capacity|
|RR005|uint16|Start address counter|
|RR006|uint16|Start address timer/counter|
|RR007|uint16|Start WR area (self reliant)|
|RR008 - RR009|uint32|Start DT area (self reliant)|
4 byte / 1 word system registers read with R
|WR Adress|Interpreting type|Description|
|-|-|-|
|R900|uint16|Self diag error|
|R902|uint16|Mode info|

View File

@@ -0,0 +1,152 @@
//This program builds Markdown and docs for all kinds of data
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Reflection.PortableExecutable;
using System.Text;
using MewtocolNet;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("Building docs for PLC types...");
var entryLoc = Assembly.GetEntryAssembly();
ArgumentNullException.ThrowIfNull(entryLoc);
string filePath = null!;
if (args.Length == 0) {
filePath = Path.Combine(entryLoc.Location, @"..\..\..\..\Docs\plctypes.md");
} else {
var userPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
filePath = args[0].Replace("~/", $"{userPath}/");
}
Console.WriteLine($"{filePath}");
StringBuilder markdownBuilder = new StringBuilder();
var plcNames = Enum.GetNames<PlcType>().Where(x => x != PlcType.Unknown.ToString()).OrderBy(x => x).ToArray();
void WritePlcTypeTable(string[] names) {
var groups = names.Select(x => x.ToNameDecompose())
.GroupBy(x => x.Group)
.SelectMany(g => g.OrderBy(x => x.Size))
.GroupBy(x => x.Group);
markdownBuilder.AppendLine("<table>");
bool isFirstIt = true;
foreach (var group in groups) {
group.OrderBy(x => x.TypeCode);
bool isFirstGroup = true;
foreach (var enu in group) {
string cpuOrMachCode = null!;
cpuOrMachCode = enu.TypeCode.ToString("X6");
ArgumentNullException.ThrowIfNull(enu);
//first iteration
if (isFirstIt) {
markdownBuilder.AppendLine("<tr>");
markdownBuilder.AppendLine($"<th>Type</th>");
markdownBuilder.AppendLine($"<th>Capacity</th>");
markdownBuilder.AppendLine($"<th>Code</th>");
markdownBuilder.AppendLine($"<th>Enum</th>");
markdownBuilder.AppendLine($"<th>DCNT</th>");
markdownBuilder.AppendLine($"<th>EXRT</th>");
markdownBuilder.AppendLine($"<th>Tested</th>");
markdownBuilder.AppendLine("</tr>");
isFirstIt = false;
}
if (isFirstGroup) {
markdownBuilder.AppendLine("<tr>");
markdownBuilder.AppendLine($"<td colspan=\"7\" height=50>📟 <b>{group.Key}</b> </td>");
markdownBuilder.AppendLine("</tr>");
isFirstGroup = false;
}
markdownBuilder.AppendLine("<tr>");
markdownBuilder.AppendLine($"<td> {(enu.SubTypes.Length == 0 ? "-" : string.Join(", ", enu.SubTypes))} </td>");
markdownBuilder.AppendLine($"<td> {enu.Size}k </td>");
markdownBuilder.AppendLine($"<td><code>0x{cpuOrMachCode}</code></td>");
if (enu.IsDiscontinuedModel) {
markdownBuilder.AppendLine($"<td><i>{enu.EncodedName}</i></td>");
markdownBuilder.AppendLine($"<td align=center>⚠️</td>");
} else {
markdownBuilder.AppendLine($"<td colspan=\"2\"><i>{enu.EncodedName}</i></td>");
}
markdownBuilder.AppendLine($"<td align=center> {(enu.UsesEXRT ? "" : "")} </td>");
markdownBuilder.AppendLine($"<td align=center> {(enu.WasTestedLive ? "" : "")} </td>");
markdownBuilder.AppendLine("</tr>");
}
isFirstIt = false;
}
markdownBuilder.AppendLine("</table>");
markdownBuilder.AppendLine("\n");
}
markdownBuilder.AppendLine($"# PLC Type Table");
markdownBuilder.AppendLine($"Auto Generated @ **{DateTime.Now.ToUniversalTime().ToString("u", CultureInfo.InvariantCulture)}**\n");
markdownBuilder.AppendLine(
$"All supported PLC types for auto recognition are listed in this table. " +
$"Other ones might also be supported but are shown as unknown in the library. " +
$"Some models are never uniquely identifiable by their typecode and need extra hints like Prog Capacity in EXRT or RT. \n\n" +
$"Typecode explained:\n" +
$"```\n" +
$"From left to right\n" +
$"0x\n" +
$"07 <= extended code (00 for non Mewtocol 7 devices)\n" +
$"0120 <= for 120k (Prog capacity), with RT/EXRT/MEW7 override order\n" +
$"A5 <= Is the actual typecode, with RT/EXRT/MEW7 override order\n" +
$"```"
);
markdownBuilder.AppendLine($"> <b>Discontinued PLCs</b><br>");
markdownBuilder.AppendLine($"> These are PLCs that are no longer sold by Panasonic. Marked with ⚠️\n");
markdownBuilder.AppendLine($"> <b>EXRT PLCs</b><br>");
markdownBuilder.AppendLine($"> These are PLCs that utilize the basic `%EE#RT` and `%EE#EX00RT` command. All newer models do this. Old models only use the `%EE#RT` command.\n");
WritePlcTypeTable(plcNames);
File.WriteAllText(filePath, markdownBuilder.ToString());

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,81 @@
using MewtocolNet;
using MewtocolNet.Logging;
namespace Examples.BasicUsage;
internal class Program {
const string IP = "192.168.115.210";
const int PORT = 9094;
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
static async Task AsyncMain () {
//the library provides a logging tool, comment this out if needed
Logger.LogLevel = LogLevel.Critical;
//create a new interface to the plc using ethernet / tcp ip
//the using keyword is optional, if you want to use your PLC instance
//globally leave it
//you can also specify the source ip with:
//Mewtocol.Ethernet(IP, PORT).FromSource("192.168.113.2", 46040).Build()
using (var plc = Mewtocol.Ethernet(IP, PORT).Build()) {
//connect async to the plc
await plc.ConnectAsync();
//check if the connection was established
if (!plc.IsConnected) {
Console.WriteLine("Failed to connect to the plc...");
Environment.Exit(1);
}
//print basic plc info
Console.WriteLine(plc.PlcInfo);
//check if the plc is not in RUN mode, change to run
if(!plc.PlcInfo.IsRunMode) await plc.SetOperationModeAsync(true);
//get information about the plc
Console.WriteLine($"PLC type: {plc.PlcInfo.TypeName}");
Console.WriteLine($"Capacity: {plc.PlcInfo.ProgramCapacity}k");
Console.WriteLine($"Error: {plc.PlcInfo.SelfDiagnosticError}k");
//set the plc to prog mode
//await plc.SetOperationModeAsync(false);
//do disconnect use
plc.Disconnect();
//or
//await plc.DisconnectAsync();
//you can then change the connection settings for example to another PLC
plc.ConfigureConnection("192.168.115.212", 9094);
await plc.ConnectAsync();
plc.Disconnect();
plc.ConfigureConnection("192.168.115.214", 9094);
await plc.ConnectAsync();
plc.Disconnect();
plc.ConfigureConnection("192.168.178.55", 9094);
await plc.ConnectAsync();
}
//you can also find any applicable source endpoints by using:
foreach (var endpoint in Mewtocol.GetSourceEndpoints()) {
Console.WriteLine($"Usable endpoint: {endpoint}");
}
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,148 @@
using MewtocolNet;
using MewtocolNet.Logging;
using MewtocolNet.Registers;
namespace Examples.BasicRegisterReadWrite;
internal class Program {
//const string PLC_IP = "192.168.178.55";
const string PLC_IP = "192.168.115.210";
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
static async Task AsyncMain() {
Console.Clear();
//the library provides a logging tool, comment this out if needed
Logger.LogLevel = LogLevel.Critical;
//here we collect our built registers
IRegister<short> simpleNumberRegister = null!;
IRegister<ushort> simpleNumberRegister2 = null!;
IRegister<Word> simpleWordRegister = null!;
IArrayRegister<Word> overlapWordRegister = null!;
IStringRegister stringRegister = null!;
IArrayRegister<string> stringArrayRegister = null!;
IArrayRegister2D<short> simpleNumberRegister2Dim = null!;
//create a new interface to the plc using ethernet / tcp ip
using var plc = Mewtocol.Ethernet(PLC_IP)
.WithRegisters(b => {
//a simple INT at register address DT1000
b.Struct<short>("DT1000").Build(out simpleNumberRegister);
b.Struct<ushort>("DT1001").Build(out simpleNumberRegister2);
//you can also read the same array as an other data type
//not how they are at the same address, that means their values are linked
b.Struct<Word>("DT1000").Build(out simpleWordRegister);
//same link feature is also true for arrays that overlap their addresses
//this will go from DT999 to DT1001 because its a 3 Word array
b.Struct<Word>("DT999").AsArray(3).Build(out overlapWordRegister);
//strings area not stacially sized, they use a different constructor
b.String("DT1024", 32).Build(out stringRegister);
//string can also be arrayed
b.String("DT2030", 5).AsArray(3).Build(out stringArrayRegister);
//a simple 2 dimensional ARRAY [0..2, 0..2] OF INT at DT2003
b.Struct<short>("DT2003").AsArray(3, 3).Build(out simpleNumberRegister2Dim);
b.Struct<bool>("R19A").Build();
})
.Build();
//you can explain the internal register layout and which ones are linked by
Console.WriteLine(plc.Explain());
//connect async to the plc
await plc.ConnectAsync();
//check if the connection was established
if (!plc.IsConnected) {
Console.WriteLine("Failed to connect to the plc...");
Environment.Exit(1);
}
//restarts the program, this will make sure the global registers of the plc get reset each run
await plc.RestartProgramAsync();
//from this point on we can modify our registers
//read the value of the the register
short readValue = await simpleNumberRegister.ReadAsync();
ushort readValue2 = await simpleNumberRegister2.ReadAsync();
//show the value
Console.WriteLine($"Read value for {nameof(simpleNumberRegister)}: {readValue}");
Console.WriteLine($"Read value for {nameof(simpleNumberRegister2)}: {readValue2}");
//write the value
await simpleNumberRegister.WriteAsync(30);
//show the value
Console.WriteLine($"Value of {nameof(simpleNumberRegister)}: {simpleNumberRegister.Value}");
//because the two registers at DT1000 are linked by their memory address in the plc,
//both of their values got updated
Console.WriteLine($"Value of {nameof(simpleWordRegister)}: {simpleWordRegister.Value}");
//also the overlapping word array register value got updated, but only at the DT positions that were read
int i = 0;
overlapWordRegister.Value.ToList().ForEach(x => {
Console.WriteLine($"Value of {nameof(overlapWordRegister)}[{i}]: {x}");
i++;
});
//you can even read out a word bitwise
Console.WriteLine($"Bits of {nameof(simpleWordRegister)}: {simpleWordRegister.Value?.ToStringBits()}");
//or as a single bit
Console.WriteLine($"Bit at index 3 of {nameof(simpleWordRegister)}: {simpleWordRegister.Value?[3]}");
//reading / writing the string register
//await stringRegister.WriteAsync("Lorem ipsum dolor sit amet, cons");
await stringRegister.ReadAsync();
Console.WriteLine($"Value of {nameof(stringRegister)}: {stringRegister.Value}");
//reading writing a string array register
await stringArrayRegister.ReadAsync();
i = 0;
stringArrayRegister.Value.ToList().ForEach(x => {
Console.WriteLine($"Value of {nameof(stringArrayRegister)}[{i}]: {x}");
i++;
});
//you can either set the whole array at once,
//this will be slow if you only want to update one item
await stringArrayRegister.WriteAsync(new string[] {
"Test1",
"Test2",
"Test3",
});
//or update just one
//COMING LATER
//same thing also works for 2 dim arrays
await simpleNumberRegister2Dim.ReadAsync();
//the array is multi dimensional but can also be iterated per element
foreach (var item in simpleNumberRegister2Dim)
Console.WriteLine($"Element of {nameof(simpleNumberRegister2Dim)}: {item}");
//you can also use the array indexer accessors
Console.WriteLine($"Element [1, 2] of {nameof(simpleNumberRegister2Dim)}: {simpleNumberRegister2Dim[1, 2]}");
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,90 @@
using MewtocolNet.Logging;
using MewtocolNet.Registers;
using MewtocolNet;
namespace Examples.Polling;
internal class Program {
const string PLC_IP = "192.168.178.55";
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
static async Task AsyncMain() {
Console.Clear();
//the library provides a logging tool, comment this out if needed
Logger.LogLevel = LogLevel.Change;
//create a new interface to the plc using ethernet / tcp ip
using var plc = Mewtocol.Ethernet(PLC_IP)
.WithPoller() // <= use this in the builder pattern to automatically poll registers
.WithInterfaceSettings(s => {
//this means registers at the same address
//or that are contained by overlapping arrays
//always get assigned the same poll level
s.PollLevelOverwriteMode = PollLevelOverwriteMode.Highest;
//this means combine all registers that are not farther
//than 8 words apart from another into one tcp message,
//this safes message frames but a to large number can block read write traffic
s.MaxOptimizationDistance = 8;
})
.WithCustomPollLevels(l => {
//this makes registers at polllevel 2 only run all 3 iterations
l.SetLevel(2, 3);
//this makes registers at polllevel 3 only run all 5 seconds
l.SetLevel(3, TimeSpan.FromSeconds(5));
})
.WithRegisters(b => {
b.Struct<short>("DT1000").Build();
b.Struct<Word>("DT1000").Build();
b.Struct<ushort>("DT1001").Build();
b.Struct<Word>("DT999").AsArray(3).Build();
//we dont want to poll the string register as fast as we can
//so we assign it to level 2 to run only all 3 iterations
b.String("DT1024", 32).PollLevel(2).Build();
//we want to poll this array only at the first iteration,
//and after this only if we need the data
b.Struct<Word>("DT2000").AsArray(3).PollLevel(PollLevel.FirstIteration).Build();
//we want to poll this string array all 5 seconds
b.String("DT2030", 5).AsArray(3).PollLevel(3).Build();
//this is a fairly large array, so we never poll it automatically
//only if we need the data
b.Struct<short>("DT2003").AsArray(3, 3).PollLevel(PollLevel.Never).Build();
})
.Build();
//this explains the underlying data structure for the poller
Console.WriteLine(plc.Explain());
await plc.ConnectAsync(async () => {
//we want to restart the program before the poller starts
await plc.RestartProgramAsync();
});
if (!plc.IsConnected) {
Console.WriteLine("Failed to connect to the plc...");
Environment.Exit(1);
}
Console.ReadLine();
}
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,31 @@
using MewtocolNet;
using MewtocolNet.Logging;
namespace Examples.ProgramReadWrite;
internal class Program {
static void Main(string[] args) => Task.Run(AsyncMain).Wait();
//MewtocolNet.ProgramParsing.PlcBinaryProgram.ParseFromFile(@"C:\Users\fwe\Documents\sps\FPXH_C30_Test1.fp").AnalyzeProgram();
static async Task AsyncMain () {
Logger.LogLevel = LogLevel.Error;
using (var plc = Mewtocol.Ethernet("192.168.178.55").Build()) {
await plc.ConnectAsync();
var prog = await plc.ReadProgramAsync();
if (prog != null) {
prog.AnalyzeProgram();
}
}
}
}

View File

@@ -0,0 +1,11 @@
<Application x:Class="Examples.WPF.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Examples.WPF"
xmlns:conv="clr-namespace:Examples.WPF.Converters"
StartupUri="MainWindow.xaml">
<Application.Resources>
<conv:NegationConverter x:Key="bInv"/>
<conv:ColorHashConverter x:Key="hashColor"/>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,85 @@
using Examples.WPF.ViewModels;
using MewtocolNet;
using MewtocolNet.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Navigation;
namespace Examples.WPF;
public partial class App : Application {
public static AppViewModel ViewModel { get; private set; } = null!;
public static new App Current = null!;
public static new MainWindow MainWindow = null!;
public static ObservableCollection<TextBlock> LoggerItems = null!;
internal static event Action? LogEventProcessed;
protected override void OnStartup(StartupEventArgs e) {
ViewModel = new AppViewModel();
Current = this;
LoggerItems = new();
Logger.LogLevel = LogLevel.Verbose;
Logger.DefaultTargets = LoggerTargets.Trace;
Logger.OnNewLogMessage((d, l, m) => {
Application.Current.Dispatcher.BeginInvoke(() => {
Brush msgColor = null!;
switch (l) {
case LogLevel.Error:
msgColor = Brushes.Red;
break;
case LogLevel.Change:
msgColor = Brushes.Blue;
break;
case LogLevel.Verbose:
msgColor = Brushes.Gold;
break;
case LogLevel.Critical:
msgColor = Brushes.Gray;
break;
}
if (LoggerItems.Count > 1000) LoggerItems.RemoveAt(0);
var contRun = msgColor == null ? new Run(m) : new Run(m) {
Foreground = msgColor,
};
LoggerItems.Add(new TextBlock {
Inlines = {
new Run($"[{d:hh:mm:ss:ff}] ") {
Foreground = Brushes.LimeGreen,
},
contRun
}
});
LogEventProcessed?.Invoke();
}, System.Windows.Threading.DispatcherPriority.Background);
});
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,62 @@
using System;
using System.Drawing;
using System.Globalization;
using System.Windows.Data;
namespace Examples.WPF.Converters;
[ValueConversion(typeof(bool), typeof(bool))]
public class ColorHashConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var hashCode = value.GetHashCode();
var randColor = GenerateRandomVibrantColor(new Random(hashCode));
System.Windows.Media.Brush outBrush = new System.Windows.Media.SolidColorBrush(new System.Windows.Media.Color {
R = randColor.R,
G = randColor.G,
B = randColor.B,
A = 255,
});
return outBrush;
}
private Color GenerateRandomVibrantColor(Random random) {
byte red = (byte)random.Next(256);
byte green = (byte)random.Next(256);
byte blue = (byte)random.Next(256);
Color color = Color.FromArgb(255, red, green, blue);
// Ensure the color is vibrant and colorful
while (!IsVibrantColor(color)) {
red = (byte)random.Next(256);
green = (byte)random.Next(256);
blue = (byte)random.Next(256);
color = Color.FromArgb(255,red, green, blue);
}
return color;
}
private bool IsVibrantColor(Color color) {
int minBrightness = 100;
int maxBrightness = 200;
int minSaturation = 150;
int brightness = (int)(color.GetBrightness() * 255);
int saturation = (int)(color.GetSaturation() * 255);
return brightness >= minBrightness && brightness <= maxBrightness && saturation >= minSaturation;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

View File

@@ -0,0 +1,14 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Examples.WPF.Converters;
[ValueConversion(typeof(bool), typeof(bool))]
public class NegationConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value != null ? !(bool)value : false;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value != null ? !(bool)value : false;
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPublishable>false</IsPublishable>
<IsPackable>false</IsPackable>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\MewtocolNet\MewtocolNet.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,169 @@
<Window x:Class="Examples.WPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Examples.WPF"
d:DataContext="{d:DesignInstance local:MainWindow, IsDesignTimeCreatable=True}"
mc:Ignorable="d"
MinWidth="500"
MinHeight="400"
Height="850"
Width="1200"
Title="MewtocolNet WPF Demo">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
<RowDefinition MinHeight="30" Height="150"/>
</Grid.RowDefinitions>
<ContentControl x:Name="mainContent"/>
<GridSplitter Grid.Row="1"
HorizontalAlignment="Stretch"
Background="Gray"
ShowsPreview="true"
Height="5">
<GridSplitter.Template>
<ControlTemplate>
<Separator/>
</ControlTemplate>
</GridSplitter.Template>
</GridSplitter>
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Border Background="LightGray"/>
<StackPanel HorizontalAlignment="Left"
Orientation="Horizontal">
<TextBlock Text="Logger"
Margin="5"/>
<Border Width="1"
Margin="5"
Background="Gray"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Rx"
Margin="5"/>
<Ellipse IsEnabled="{Binding AppViewModel.Plc.IsReceiving, Mode=OneWay}"
Fill="Lime"
Width="10"
Height="10">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".1"/>
</Trigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock Text="Tx"
Margin="5"/>
<Ellipse Fill="Orange"
IsEnabled="{Binding AppViewModel.Plc.IsSending, Mode=OneWay}"
Width="10"
Height="10">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value=".1"/>
</Trigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<Border Width="1"
Margin="5"
Background="Gray"/>
<TextBlock Text="{Binding AppViewModel.Plc.BytesPerSecondDownstream, Mode=OneWay}"
VerticalAlignment="Center"/>
<Border Width="1"
Margin="5"
Background="Gray"/>
<TextBlock Text="{Binding AppViewModel.Plc.BytesPerSecondUpstream, Mode=OneWay}"
VerticalAlignment="Center"/>
<Border Width="1"
Margin="5"
Background="Gray"/>
<TextBlock Text="{Binding AppViewModel.Plc.QueuedMessages, StringFormat='{}Q: {0}', Mode=OneWay}"
VerticalAlignment="Center"/>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding AppViewModel.PlcIsNull}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right">
<Button Content="Expand"
Margin="2"/>
<ToggleButton Content="Autoscroll"
IsChecked="True"
Margin="2"
x:Name="autoScrollBtn"/>
</StackPanel>
<ListBox Grid.Row="1"
Background="Black"
Foreground="White"
BorderThickness="0"
VirtualizingPanel.IsVirtualizing="true"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ItemsSource="{Binding LoggerItems, Mode=OneWay}"
x:Name="loggerList">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ListBox>
<Border Background="Black"
Grid.Row="2">
<TextBlock Text=">"
Foreground="White"
Margin="5,0,0,0"/>
</Border>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,57 @@
using Examples.WPF.ViewModels;
using Examples.WPF.Views;
using MewtocolNet;
using MewtocolNet.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Examples.WPF;
public partial class MainWindow : Window {
public ObservableCollection<TextBlock> LoggerItems => App.LoggerItems;
public AppViewModel AppViewModel => App.ViewModel;
public MainWindow() {
InitializeComponent();
this.DataContext = this;
App.MainWindow = this;
mainContent.Content = new ConnectView();
loggerList.PreviewMouseWheel += (s, e) => {
autoScrollBtn.IsChecked = false;
};
App.LogEventProcessed += () => {
Application.Current.Dispatcher.BeginInvoke(() => {
if (autoScrollBtn?.IsChecked != null && autoScrollBtn.IsChecked.Value)
loggerList.ScrollIntoView(App.LoggerItems.Last());
}, System.Windows.Threading.DispatcherPriority.Send);
};
}
}

View File

@@ -0,0 +1,34 @@
using MewtocolNet;
using MewtocolNet.RegisterAttributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Examples.WPF.RegisterCollections;
public class TestRegisterCollection : RegisterCollection {
[Register("R11A")]
public bool? TestR11A { get; set; }
[Register("R11A")]
public bool TestR11A_Duplicate_NonNullable { get; set; }
[Register("R16B")]
public bool TestR16B { get; set; }
[Register("R902")]
public bool Test { get; set; }
[BitRegister("DT1000", 0), PollLevel(3)]
public bool? TestDT100_Word_Duplicate_SingleBit { get; set; }
[Register("DT1000")]
public Word TestDT100_Word_Duplicate { get; set; }
[BitRegister("DDT1010", 1)]
public bool? TestDDT1010_DWord_Duplicate_SingleBit { get; set; }
}

View File

@@ -0,0 +1,40 @@
using Examples.WPF.RegisterCollections;
using MewtocolNet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Examples.WPF.ViewModels {
public class AppViewModel : ViewModelBase {
private IPlc? plc;
private TestRegisterCollection testRegCollection = null!;
public bool PlcIsNull => plc == null;
public bool PlcIsNotNull => plc != null;
public IPlc? Plc {
get => plc;
set {
plc = value;
OnPropChange();
OnPropChange(nameof(PlcIsNull));
OnPropChange(nameof(PlcIsNotNull));
}
}
public TestRegisterCollection TestRegCollection {
get => testRegCollection;
set {
testRegCollection = value;
OnPropChange();
}
}
}
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Configuration.Internal;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
using MewtocolNet;
using MewtocolNet.ComCassette;
namespace Examples.WPF.ViewModels;
internal class ConnectViewViewModel : ViewModelBase {
private bool hasComports = false;
private IEnumerable<int> baudRates = null!;
private IEnumerable<string> comPorts = null!;
private IEnumerable<CassetteInformation> foundCassettes = null!;
private string selectedIP = "192.168.115.210";
private string selectedPort = "9094";
private bool isConnecting;
public IEnumerable<int> BaudRates {
get => baudRates;
set {
baudRates = value;
OnPropChange();
}
}
public IEnumerable<string> ComPorts {
get => comPorts;
set {
comPorts = value;
OnPropChange();
}
}
public IEnumerable<CassetteInformation> FoundCassettes {
get => foundCassettes;
set {
foundCassettes = value;
OnPropChange();
}
}
public bool HasComports {
get => hasComports;
set {
hasComports = value;
OnPropChange();
}
}
public string SelectedIP {
get { return selectedIP; }
set {
selectedIP = value;
OnPropChange();
}
}
public string SelectedPort {
get { return selectedPort; }
set {
selectedPort = value;
OnPropChange();
}
}
public bool IsConnecting {
get { return isConnecting; }
set {
isConnecting = value;
OnPropChange();
}
}
private DispatcherTimer tm;
public ConnectViewViewModel() {
BaudRates = Mewtocol.GetUseableBaudRates();
ScanTimerTick(null, null!);
tm = new DispatcherTimer {
Interval = TimeSpan.FromSeconds(3),
};
tm.Tick += ScanTimerTick;
tm.Start();
}
private async void ScanTimerTick(object? sender, EventArgs e) {
ComPorts = Mewtocol.GetSerialPortNames();
HasComports = ComPorts != null && ComPorts.Count() > 0;
var found = await CassetteFinder.FindClientsAsync(timeoutMs: 1000);
if (FoundCassettes == null || !Enumerable.SequenceEqual(found, FoundCassettes))
FoundCassettes = found;
}
internal void SelectedCassette (CassetteInformation cassette) {
SelectedIP = cassette.IPAddress.ToString();
SelectedPort = cassette.Port.ToString();
}
internal void EndTimer() => tm.Stop();
}

View File

@@ -0,0 +1,36 @@
using Examples.WPF.RegisterCollections;
using MewtocolNet;
using MewtocolNet.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Examples.WPF.ViewModels;
public class PlcDataViewViewModel : ViewModelBase {
private ReconnectArgs plcCurrentReconnectArgs = null!;
public IPlc Plc => App.ViewModel.Plc!;
public TestRegisterCollection RegCollection => App.ViewModel.TestRegCollection;
public ReconnectArgs PlcCurrentReconnectArgs {
get => plcCurrentReconnectArgs;
set {
plcCurrentReconnectArgs = value;
OnPropChange();
}
}
public PlcDataViewViewModel () {
Plc.ReconnectTryStarted += (s, e) => PlcCurrentReconnectArgs = e;
Plc.Reconnected += (s, e) => PlcCurrentReconnectArgs = null!;
Plc.Disconnected += (s, e) => PlcCurrentReconnectArgs = null!;
}
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Examples.WPF.ViewModels;
public abstract class ViewModelBase : INotifyPropertyChanged {
public event PropertyChangedEventHandler? PropertyChanged;
public void PropChange(string _name) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(_name));
}
protected void OnPropChange([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -0,0 +1,129 @@
<UserControl x:Class="Examples.WPF.Views.ConnectView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Examples.WPF.Views"
xmlns:vm="clr-namespace:Examples.WPF.ViewModels"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:ConnectViewViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="10">
<StackPanel>
<StackPanel.Resources>
<Style TargetType="TextBox">
<Setter Property="Padding" Value="5"/>
</Style>
</StackPanel.Resources>
<StackPanel>
<TextBlock Text="Connect to a PLC"
FontSize="24"/>
<Label Content="Set your connection type"/>
<ComboBox SelectedIndex="0" x:Name="conTypeCombo">
<ComboBoxItem>Ethernet</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding HasComports}">Serial</ComboBoxItem>
</ComboBox>
</StackPanel>
<Separator/>
<StackPanel MinWidth="200">
<TextBlock Text="Cassettes"/>
<DataGrid ItemsSource="{Binding FoundCassettes}"
SelectionChanged="SelectedCassette"
AutoGenerateColumns="False"
MinHeight="150"
MaxHeight="200"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width=".5*"/>
<DataGridTextColumn Header="IP" Binding="{Binding IPAddress}" Width=".3*"/>
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width=".3*"/>
<DataGridCheckBoxColumn Header="DHCP" Binding="{Binding UsesDHCP}" Width="auto"/>
<DataGridTextColumn Header="MAC" Binding="{Binding MacAddressStr, Mode=OneWay}" Width="auto"/>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="auto"/>
</DataGrid.Columns>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Port}" Value="0">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
<TextBlock Text="Connection"/>
<StackPanel Orientation="Horizontal">
<Label Content="IP Address"
VerticalAlignment="Center"/>
<TextBox Text="{Binding SelectedIP}"
VerticalAlignment="Center"/>
<Label Content="Port"
VerticalAlignment="Center"/>
<TextBox Text="{Binding SelectedPort}"
VerticalAlignment="Center"/>
<Button Content="Connect"
Click="ClickedConnectEth"
VerticalAlignment="Center"
Padding="5"
Margin="10,0,0,0">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnecting}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedIndex, ElementName=conTypeCombo}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
</StackPanel>
<StackPanel MinWidth="200"
HorizontalAlignment="Left">
<StackPanel Orientation="Vertical">
<Label Content="COM Port"/>
<ComboBox ItemsSource="{Binding ComPorts}"
SelectedIndex="0"/>
<Label Content="BaudRate"/>
<ComboBox ItemsSource="{Binding BaudRates}"
SelectedIndex="0"/>
</StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedIndex, ElementName=conTypeCombo}" Value="1">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,161 @@
using Examples.WPF.RegisterCollections;
using Examples.WPF.ViewModels;
using MewtocolNet;
using MewtocolNet.ComCassette;
using MewtocolNet.Logging;
using MewtocolNet.Registers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Examples.WPF.Views;
/// <summary>
/// Interaktionslogik für ConnectView.xaml
/// </summary>
public partial class ConnectView : UserControl {
private ConnectViewViewModel viewModel;
public ConnectView() {
InitializeComponent();
viewModel = new ConnectViewViewModel();
this.DataContext = viewModel;
Unloaded += (s, e) => viewModel.EndTimer();
}
private void SelectedCassette(object sender, SelectionChangedEventArgs e) {
var cassette = (CassetteInformation)((DataGrid)sender).SelectedItem;
if (cassette == null) return;
viewModel.SelectedCassette(cassette);
}
private void ClickedConnectEth(object sender, RoutedEventArgs e) {
Application.Current.Dispatcher.BeginInvoke(async () => {
viewModel.IsConnecting = true;
var parsedInt = int.Parse(viewModel.SelectedPort);
IRegister<short> heartbeatSetter = null!;
IRegister<bool> outputContactReference = null!;
IRegister<bool> testBoolReference = null!;
IRegister<Word> wordRefTest = null!;
//build a new interface
App.ViewModel.Plc = Mewtocol.Ethernet(viewModel.SelectedIP, parsedInt)
.WithPoller()
.WithInterfaceSettings(setting => {
setting.TryReconnectAttempts = 10;
setting.TryReconnectDelayMs = 2000;
setting.SendReceiveTimeoutMs = 1000;
setting.HeartbeatIntervalMs = 3000;
setting.MaxDataBlocksPerWrite = 128;
setting.MaxOptimizationDistance = 20;
})
.WithCustomPollLevels(lvl => {
lvl.SetLevel(2, 3);
lvl.SetLevel(3, TimeSpan.FromSeconds(5));
lvl.SetLevel(4, TimeSpan.FromSeconds(10));
})
.WithRegisterCollections(collector => {
App.ViewModel.TestRegCollection = collector.AddCollection<TestRegisterCollection>();
})
.WithRegisters(b => {
b.Bool("X4").Build();
b.Bool("Y4").Build(out outputContactReference);
b.Bool("R10A").PollLevel(PollLevel.FirstIteration).Build(out testBoolReference);
b.Struct<short>("DT1000").Build(out heartbeatSetter);
//these will be merged into one
b.Struct<Word>("DT1000").Build(out wordRefTest);
b.Struct<Word>("DT1000").Build(out wordRefTest);
b.Struct<ushort>("DT1001").PollLevel(2).Build();
b.Struct<Word>("DT1002").PollLevel(2).Build();
b.Struct<int>("DDT1010").PollLevel(2).Build();
b.Struct<uint>("DDT1012").PollLevel(2).Build();
b.Struct<DWord>("DDT1014").PollLevel(2).Build();
b.Struct<float>("DDT1016").PollLevel(2).Build();
b.Struct<TimeSpan>("DDT1018").PollLevel(2).Build();
b.Struct<DateAndTime>("DDT1020").PollLevel(2).Build();
b.Struct<DateAndTime>("DDT1022").PollLevel(2).Build();
b.String("DT1028", 32).PollLevel(3).Build();
b.String("DT1046", 5).PollLevel(4).Build();
b.Struct<Word>("DT1000").AsArray(5).PollLevel(1).Build();
})
.WithHeartbeatTask(async (plc) => {
var randShort = (short)new Random().Next(short.MinValue, short.MaxValue);
//write direct
//await heartbeatSetter.WriteAsync(randShort);
//or by anonymous
await plc.Register.Struct<short>("DT1000").WriteAsync(randShort);
//write a register without a reference
bool randBool = new Random().Next(0, 2) == 1;
await plc.Register.Bool("Y4").WriteAsync(randBool);
if (testBoolReference.Value != null)
await testBoolReference.WriteAsync(!testBoolReference.Value.Value);
await plc.Register.Struct<DateAndTime>("DDT1022").WriteAsync(DateAndTime.FromDateTime(DateTime.UtcNow));
})
.Build();
//connect to it
await App.ViewModel.Plc.ConnectAsync(async () => {
await App.ViewModel.Plc.RestartProgramAsync();
});
await App.ViewModel.Plc.AwaitFirstDataCycleAsync();
if (App.ViewModel.Plc.IsConnected) {
App.MainWindow.mainContent.Content = new PlcDataView();
}
viewModel.IsConnecting = false;
}, System.Windows.Threading.DispatcherPriority.Send);
}
}

View File

@@ -0,0 +1,350 @@
<UserControl x:Class="Examples.WPF.Views.PlcDataView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:Examples.WPF.ViewModels"
xmlns:local="clr-namespace:Examples.WPF.Views"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:PlcDataViewViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="450"
d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="PLC">
<MenuItem Header="Disconnect" IsEnabled="{Binding Plc.IsConnected}"
Click="ClickedDisconnect"/>
<MenuItem Header="Connect" IsEnabled="{Binding Plc.IsConnected, Converter={StaticResource bInv}}"
Click="ClickedConnect"/>
<MenuItem Header="Set Random DT1000" IsEnabled="{Binding Plc.IsConnected}"
Click="ClickedSetRandom"/>
<MenuItem Header="Queue test" IsEnabled="{Binding Plc.IsConnected}"
Click="ClickedAddQueueTest"/>
<MenuItem Header="Toggle OP mode" IsEnabled="{Binding Plc.IsConnected}"
Click="ClickedToggleRunMode"/>
</MenuItem>
</Menu>
<StackPanel Margin="10"
Grid.Row="1">
<TextBlock IsEnabled="{Binding Plc.IsConnected}">
<Run Text="{Binding Plc.PlcInfo.TypeName, Mode=OneWay}"
FontSize="24"
BaselineAlignment="Center"
FontWeight="SemiBold"/>
<Run Text="{Binding Plc.PlcInfo.CpuVersion, StringFormat='v{0}', Mode=OneWay}"
FontSize="24"
FontWeight="Light"/>
<Ellipse Width="10"
Height="10"
Fill="Lime"
IsEnabled="{Binding Plc.IsConnected}"/>
<Run>
<Run.Style>
<Style TargetType="Run">
<Style.Triggers>
<DataTrigger Binding="{Binding Plc.IsRunMode, Mode=OneWay}" Value="True">
<Setter Property="Text" Value="RUN MODE"/>
</DataTrigger>
<DataTrigger Binding="{Binding Plc.IsRunMode, Mode=OneWay}" Value="False">
<Setter Property="Text" Value="NO RUN MODE"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Run.Style>
</Run>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock IsEnabled="{Binding Plc.IsConnected, Converter={StaticResource bInv}}">
<Run Text="Disconnected"
FontSize="24"
BaselineAlignment="Center"
FontWeight="SemiBold"/>
<Ellipse Width="10"
Height="10"
Fill="Red"
IsEnabled="{Binding Plc.IsConnected}"/>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding Plc.PlcInfo.TypeCode, StringFormat='#{0:X}', Mode=OneWay}"
FontSize="18"/>
<ItemsControl ItemsSource="{Binding Plc.PlcInfo.OperationModeTags, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="LightGray"
CornerRadius="5"
Margin="2"
Padding="5">
<TextBlock Text="{Binding}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl ItemsSource="{Binding Plc.PlcInfo.HardwareInformationTags, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="LightGray"
CornerRadius="5"
Margin="2"
Padding="5">
<TextBlock Text="{Binding}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock>
<Run Text="Reconnecting..."/>
<Run Text="{Binding PlcCurrentReconnectArgs.ReconnectTry, Mode=OneWay, StringFormat='{}{0}/'}"/>
<Run Text="{Binding PlcCurrentReconnectArgs.MaxAttempts, Mode=OneWay}"/> in
<Run Text="{Binding PlcCurrentReconnectArgs.RetryCountDownRemaining, Mode=OneWay}"/>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding PlcCurrentReconnectArgs, Mode=OneWay}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.5*"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<GridSplitter Grid.Column="1"
Grid.Row="1"
Grid.RowSpan="3"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Background="Gray"
ShowsPreview="true"
Width="10">
<GridSplitter.Template>
<ControlTemplate>
<Rectangle Fill="Gray"
Width="2"
Margin="2"/>
</ControlTemplate>
</GridSplitter.Template>
</GridSplitter>
<TextBlock Text="Underlying Registers"
FontSize="18"
Margin="10"/>
<DataGrid Grid.Row="1"
Grid.RowSpan="3"
AutoGenerateColumns="False"
IsReadOnly="True"
ItemsSource="{Binding Plc.Registers, Mode=OneWay}">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoGenerated}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="FP Address" Binding="{Binding PLCAddressName}"/>
<DataGridTextColumn Header="Type" Binding="{Binding UnderlyingSystemType.Name}"/>
<DataGridTemplateColumn Header="Value" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border HorizontalAlignment="Left"
Padding="2,0">
<TextBlock Text="{Binding ValueStr}"/>
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding ValueStr}" Value="True">
<Setter Property="Background" Value="Blue"/>
<Setter Property="TextElement.Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Poll Level" Binding="{Binding PollLevel, Mode=OneWay}"/>
<DataGridTextColumn Header="Update Frequency" Binding="{Binding UpdateFreqHz, StringFormat='{}{0} Hz',Mode=OneWay}"/>
<DataGridTemplateColumn Width="15">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding MemoryArea, Mode=OneWay, Converter={StaticResource hashColor}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<TextBlock Text="Memory Areas"
Grid.Column="2"
FontSize="18"
Margin="10"/>
<Border Grid.Column="2"
Grid.Row="1"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
BorderBrush="LightBlue"
BorderThickness="1.5">
<DataGrid IsReadOnly="True"
AutoGenerateColumns="False"
ItemsSource="{Binding Plc.MemoryAreas, Mode=OneWay}">
<DataGrid.Columns>
<DataGridTextColumn Header="Address Range" Binding="{Binding AddressRange, Mode=OneWay}"/>
<DataGridTemplateColumn Width="15">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding Converter={StaticResource hashColor}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Words" Binding="{Binding UnderlyingWordsString, Mode=OneWay}"/>
</DataGrid.Columns>
</DataGrid>
</Border>
<TextBlock Text="Property Bindings"
Grid.Column="2"
Grid.Row="2"
FontSize="18"
Margin="10"/>
<Border Grid.Column="2"
Grid.Row="3"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
BorderBrush="LightBlue"
BorderThickness="1.5">
<ScrollViewer>
<StackPanel>
<TextBlock Text="Boolean internal relays"/>
<TextBlock>
<Run Text="R11A Nullable property: "/>
<Run Text="{Binding RegCollection.TestR11A}"
FontWeight="Bold"/>
</TextBlock>
<TextBlock>
<Run Text="R11A Duplicate non nullable property: "/>
<Run Text="{Binding RegCollection.TestR11A_Duplicate_NonNullable}"
FontWeight="Bold"/>
</TextBlock>
<TextBlock>
<Run Text="R16B"/>
<Run Text="{Binding RegCollection.TestR16B}"
FontWeight="Bold"/>
</TextBlock>
<TextBlock>
<Run Text="DT1000 Word duplicate: "/>
<Run Text="{Binding RegCollection.TestDT100_Word_Duplicate, Mode=OneWay}"
FontWeight="Bold"/>
</TextBlock>
<TextBlock>
<Run Text="DT1000 Word direct bit read (Idx 0): "/>
<Run Text="{Binding RegCollection.TestDT100_Word_Duplicate_SingleBit, Mode=OneWay}"
FontWeight="Bold"/>
</TextBlock>
<TextBlock>
<Run Text="DDT1010 DWord direct bit read (Idx 1): "/>
<Run Text="{Binding RegCollection.TestDDT1010_DWord_Duplicate_SingleBit, Mode=OneWay}"
FontWeight="Bold"/>
</TextBlock>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,81 @@
using Examples.WPF.ViewModels;
using MewtocolNet.Registers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Examples.WPF.Views;
public partial class PlcDataView : UserControl {
private PlcDataViewViewModel viewModel;
public PlcDataView() {
InitializeComponent();
viewModel = new PlcDataViewViewModel();
this.DataContext = viewModel;
}
private void ClickedDisconnect(object sender, RoutedEventArgs e) {
viewModel.Plc.Disconnect();
}
private async void ClickedConnect(object sender, RoutedEventArgs e) {
await viewModel.Plc.ConnectAsync();
}
private async void ClickedSetRandom(object sender, RoutedEventArgs e) {
var reg = (IRegister<ushort>?)viewModel.Plc.GetAllRegisters()?.FirstOrDefault(x => x.PLCAddressName == "DT1001");
if(reg != null) {
await reg.WriteAsync((ushort)new Random().Next(ushort.MinValue, ushort.MaxValue));
}
}
private async void ClickedAddQueueTest(object sender, RoutedEventArgs e) {
var tasks = new List<Task<short>>();
for (int i = 0; i < 100; i++) {
var t = viewModel.Plc.Register.Struct<short>("DT1000").ReadAsync();
tasks.Add(t);
}
var list = await Task.WhenAll(tasks);
Console.WriteLine();
}
private async void ClickedToggleRunMode(object sender, RoutedEventArgs e) {
await viewModel.Plc.ToggleOperationModeAsync();
}
}

View File

@@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -1,207 +0,0 @@
using System;
using System.Threading.Tasks;
using MewtocolNet;
using MewtocolNet.Logging;
using MewtocolNet.Registers;
namespace Examples;
class Program {
static void Main(string[] args) {
Console.WriteLine("Enter your scenario number:\n" +
"1 = Permanent connection\n" +
"2 = Dispose connection");
var line = Console.ReadLine();
if(line == "1") {
Scenario1();
}
if (line == "2") {
Scenario2();
}
Console.ReadLine();
}
private static bool isProgressReadout = false;
static void Scenario1 () {
Task.Factory.StartNew(async () => {
//attaching the logger
Logger.LogLevel = LogLevel.Critical;
Logger.OnNewLogMessage((date, msg) => {
Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}");
});
//setting up a new PLC interface and register collection
MewtocolInterface interf = new MewtocolInterface("10.237.191.3");
TestRegisters registers = new TestRegisters();
//attaching the register collection and an automatic poller
interf.WithRegisterCollection(registers).WithPoller();
_ = Task.Factory.StartNew(async () => {
while (true) {
if (isProgressReadout) continue;
Console.Title = $"Polling Paused: {interf.PollingPaused}, " +
$"Speed UP: {interf.BytesPerSecondUpstream} B/s, " +
$"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " +
$"Poll delay: {interf.PollerDelayMs} ms, " +
$"Queued MSGs: {interf.QueuedMessages}";
await Task.Delay(1000);
}
});
await interf.ConnectAsync((plcinf) => AfterConnect(interf, registers));
});
}
static void AfterConnect (MewtocolInterface interf, TestRegisters registers) {
//reading a value from the register collection
Console.WriteLine($"BitValue is: {registers.BitValue}");
Console.WriteLine($"TestEnum is: {registers.TestEnum}");
_ = Task.Factory.StartNew(async () => {
while(true) {
isProgressReadout = true;
await interf.ReadByteRange(1000, 2000, (p) => {
var totSteps = 10;
var cSteps = totSteps * p;
string progBar = "";
for (int i = 0; i < totSteps; i++) {
if(i < (int)cSteps) {
progBar += "⬛";
} else {
progBar += "⬜";
}
}
Console.Title = $"Prog read range: {(p * 100).ToString("N1")}% {progBar} Queued MSGs: {interf.QueuedMessages}";
});
isProgressReadout = false;
await Task.Delay(3000);
}
});
//writing a value to the registers
_ = Task.Factory.StartNew(async () => {
//set plc to run mode if not already
await interf.SetOperationMode(OPMode.Run);
int startAdress = 10000;
int entryByteSize = 20 * 20;
var bytes = await interf.ReadByteRange(startAdress, entryByteSize);
Console.WriteLine($"Bytes: {string.Join('-', bytes)}");
await Task.Delay(2000);
await interf.SetRegisterAsync(nameof(registers.TestInt32), 100);
//adds 10 each time the plc connects to the PLCs INT regíster
interf.SetRegister(nameof(registers.TestInt16), (short)(registers.TestInt16 + 10));
//adds 1 each time the plc connects to the PLCs DINT regíster
interf.SetRegister(nameof(registers.TestInt32), (registers.TestInt32 + 1));
//adds 11.11 each time the plc connects to the PLCs REAL regíster
interf.SetRegister(nameof(registers.TestFloat32), (float)(registers.TestFloat32 + 11.11));
//writes 'Hello' to the PLCs string register
interf.SetRegister(nameof(registers.TestString2), "Hello");
//set the current second to the PLCs TIME register
interf.SetRegister(nameof(registers.TestTime), TimeSpan.FromSeconds(DateTime.Now.Second));
//test pausing poller
bool pollerPaused = false;
while (true) {
await Task.Delay(5000);
pollerPaused = !pollerPaused;
if (pollerPaused) {
Console.WriteLine("Pausing poller");
await interf.PausePollingAsync();
//interf.PollerDelayMs += 10;
Console.WriteLine("Paused poller");
} else {
interf.ResumePolling();
Console.WriteLine("Resumed poller");
}
}
});
}
static void Scenario2 () {
Logger.LogLevel = LogLevel.Critical;
Logger.OnNewLogMessage((date, msg) => {
Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}");
});
Task.Factory.StartNew(async () => {
//automatic endpoint
using (var interf = new MewtocolInterface("10.237.191.3")) {
await interf.ConnectAsync();
if (interf.IsConnected) {
await Task.Delay(5000);
}
interf.Disconnect();
}
//manual endpoint
using (var interf = new MewtocolInterface("10.237.191.3")) {
interf.HostEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Parse("10.237.191.77"), 0);
await interf.ConnectAsync();
if(interf.IsConnected) {
await Task.Delay(5000);
}
interf.Disconnect();
}
});
}
}

View File

@@ -1,75 +0,0 @@
using MewtocolNet;
using MewtocolNet.RegisterAttributes;
using System;
using System.Collections;
namespace Examples {
public class TestRegisters : RegisterCollectionBase {
//corresponds to a R100 boolean register in the PLC
[Register(1000, RegisterType.R)]
public bool TestBool1 { get; private set; }
[Register(1000)]
public int TestDuplicate { get; private set; }
//corresponds to a XD input of the PLC
[Register(RegisterType.X, SpecialAddress.D)]
public bool TestBoolInputXD { get; private set; }
//corresponds to a DT1101 - DT1104 string register in the PLC with (STRING[4])
//[Register(1101, 4)]
//public string TestString1 { get; private set; }
//corresponds to a DT7000 16 bit int register in the PLC
[Register(899)]
public short TestInt16 { get; private set; }
//corresponds to a DTD7001 - DTD7002 32 bit int register in the PLC
[Register(7001)]
public int TestInt32 { get; private set; }
//corresponds to a DTD7001 - DTD7002 32 bit float register in the PLC (REAL)
[Register(7003)]
public float TestFloat32 { get; private set; }
//corresponds to a DT7005 - DT7009 string register in the PLC with (STRING[5])
[Register(7005, 5)]
public string TestString2 { get; private set; }
//corresponds to a DT7010 as a 16bit word/int and parses the word as single bits
[Register(7010)]
public BitArray TestBitRegister { get; private set; }
//corresponds to a DT1204 as a 16bit word/int takes the bit at index 9 and writes it back as a boolean
[Register(1204, 9, BitCount.B16)]
public bool BitValue { get; private set; }
[Register(1204, 5, BitCount.B16)]
public bool FillTest { get; private set; }
//corresponds to a DT7012 - DT7013 as a 32bit time value that gets parsed as a timespan (TIME)
//the smallest value to communicate to the PLC is 10ms
[Register(7012)]
public TimeSpan TestTime { get; private set; }
public enum CurrentState {
Undefined = 0,
State1 = 1,
State2 = 2,
//State3 = 3,
State4 = 4,
State5 = 5,
StateBetween = 100,
State6 = 6,
State7 = 7,
}
[Register(50)]
public CurrentState TestEnum { get; private set; }
[Register(100)]
public TimeSpan TsTest2 { get; private set; }
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using CommandLine;
using MewtocolNet;
using MewtocolNet.ComCassette;
using Spectre.Console;
namespace MewTerminal.Commands;
[Verb("clear", HelpText = "Clears console", Hidden = true)]
internal class ClearCommand : CommandLineExcecuteable {
public override void Run() {
Console.Clear();
}
}

View File

@@ -0,0 +1,20 @@
using CommandLine.Text;
using CommandLine;
using MewtocolNet.Logging;
namespace MewTerminal.Commands;
public abstract class CommandLineExcecuteable {
static UnParserSettings UnparserSet = new UnParserSettings {
PreferShortName = true,
};
[Option('v', "verbosity", HelpText = "Sets the Loglevel verbosity", Default = LogLevel.None)]
public LogLevel LogLevel { get; set; } = LogLevel.None;
public virtual void Run() { }
public virtual Task RunAsync () => Task.CompletedTask;
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using CommandLine;
using MewtocolNet;
using MewtocolNet.ComCassette;
using MewtocolNet.Logging;
using Spectre.Console;
namespace MewTerminal.Commands;
[Verb("support-list", HelpText = "Lists all supported PLC types")]
internal class ListSupportCommand : CommandLineExcecuteable {
public override void Run () {
var plcs = Enum.GetValues<PlcType>().Cast<PlcType>();
var lst = new List<ParsedPlcName>();
foreach (var plcT in plcs) {
var decomp = plcT.ToNameDecompose();
if (decomp == null) continue;
lst.Add(decomp);
}
AnsiConsole.Write(lst.ToTable());
}
}

View File

@@ -0,0 +1,46 @@
using CommandLine;
using MewtocolNet;
using Spectre.Console;
namespace MewTerminal.Commands.OnlineCommands;
internal class OnlineCommand : CommandLineExcecuteable {
[Option('e', "ethernet", Default = "127.0.0.1:9094", HelpText = "Ethernet config")]
public string? EthernetStr { get; set; }
[Option('s', "serial", Default = null, HelpText = "Serial port config")]
public string? SerialStr { get; set; }
public override async Task RunAsync() {
try {
if (!string.IsNullOrEmpty(SerialStr)) {
} else {
var split = EthernetStr.Split(":");
string ip = split[0];
int port = int.Parse(split[1]);
using (var plc = Mewtocol.Ethernet(ip, port).Build()) {
await AfterSetup(plc);
}
}
} catch (Exception ex) {
AnsiConsole.WriteLine($"[red]{ex.Message.ToString()}[/]");
}
}
internal virtual async Task AfterSetup(IPlc plc) => throw new NotImplementedException();
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using CommandLine;
using MewtocolNet;
using MewtocolNet.ComCassette;
using MewtocolNet.Logging;
using Spectre.Console;
namespace MewTerminal.Commands;
[Verb("scan", HelpText = "Scans all network PLCs")]
internal class ScanCommand : CommandLineExcecuteable {
[Option("ip", HelpText = "IP of the source adapter" )]
public string? IPSource { get; set; }
[Option("timeout", Default = 100)]
public int? TimeoutMS { get; set; }
[Option("plc", Required = false, HelpText = "Gets the PLC types")]
public bool GetPLCTypes { get; set; }
private class PLCCassetteTypeInfo {
public CassetteInformation Cassette { get; set; }
public PLCInfo PLCInf { get; set; }
}
public override async Task RunAsync () {
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.StartAsync("Scanning...", async ctx => {
var query = await CassetteFinder.FindClientsAsync(IPSource, TimeoutMS ?? 100);
var found = query.Select(x => new PLCCassetteTypeInfo { Cassette = x }).ToList();
if (found.Count > 0 && GetPLCTypes) {
foreach (var item in found) {
ctx.Status($"Getting cassette PLC {item.Cassette.IPAddress}:{item.Cassette.Port}")
.Spinner(Spinner.Known.Dots);
var dev = Mewtocol.Ethernet(item.Cassette.IPAddress, item.Cassette.Port).Build();
dev.ConnectTimeout = 1000;
await dev.ConnectAsync();
item.PLCInf = dev.PlcInfo;
dev.Disconnect();
}
}
if (found.Count() > 0) {
AnsiConsole.MarkupLineInterpolated($"✅ Found {found.Count()} devices...");
} else {
AnsiConsole.MarkupLineInterpolated($"❌ Found no devices");
return;
}
if (found.Any(x => x.PLCInf != PLCInfo.None)) {
AnsiConsole.Write(found.Select(x => new {
x.Cassette.Name,
PLC = x.PLCInf.TypeCode.ToName(),
IsRun = x.PLCInf.OperationMode.HasFlag(OPMode.RunMode),
IP = x.Cassette.IPAddress,
x.Cassette.Port,
DHCP = x.Cassette.UsesDHCP,
MAC = x.Cassette.MacAddress,
Ver = x.Cassette.FirmwareVersion,
x.Cassette.Status,
}).ToTable());
} else {
AnsiConsole.Write(found.Select(x => new {
x.Cassette.Name,
IP = x.Cassette.IPAddress,
x.Cassette.Port,
DHCP = x.Cassette.UsesDHCP,
MAC = x.Cassette.MacAddress,
Ver = x.Cassette.FirmwareVersion,
x.Cassette.Status,
}).ToTable());
}
});
}
}

View File

@@ -0,0 +1,65 @@
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace MewTerminal;
internal static class Helpers {
internal static Table ToTable<T> (this IEnumerable<T> data, params string[] markups) {
// Create a table
var table = new Table();
var type = typeof(T);
var props = type.GetProperties();
bool isFirst = true;
foreach (var item in data) {
var rowVals = new List<string>();
foreach (var prop in props) {
if(isFirst) table.AddColumn(prop.Name.SplitCamelCase());
var propVal = prop.GetValue(item);
string strVal = propVal?.ToString() ?? "null";
if (propVal is byte[] bArr) {
strVal = string.Join(" ", bArr.Select(x => x.ToString("X2")));
}
if (propVal is string[] sArr) {
strVal = string.Join(", ", sArr);
}
strVal = strVal.Replace("[", "");
strVal = strVal.Replace("]", "");
rowVals.Add(strVal);
}
isFirst = false;
table.AddRow(rowVals.ToArray());
}
return table;
}
private static string SplitCamelCase (this string str) {
return Regex.Replace(Regex.Replace(str, @"(\P{Ll})(\P{Ll}\p{Ll})", "$1 $2"), @"(\p{Ll})(\P{Ll})", "$1 $2");
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<ApplicationManifest>app.manifest</ApplicationManifest>
<NoWarn>IL2026,IL2027,IL2090</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Spectre.Console" Version="0.47.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MewtocolNet\MewtocolNet.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<AssemblyName>mewcmd</AssemblyName>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
<TrimMode>partial</TrimMode>
<SelfContained>True</SelfContained>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AllowedReferenceRelatedFileExtensions>none</AllowedReferenceRelatedFileExtensions>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<OutputPath>..\Builds\MewTerminal</OutputPath>
</PropertyGroup>
</Project>

93
MewTerminal/Program.cs Normal file
View File

@@ -0,0 +1,93 @@
using CommandLine;
using CommandLine.Text;
using MewTerminal.Commands;
using MewTerminal.Commands.OnlineCommands;
using MewtocolNet.Logging;
using Spectre.Console;
using System.Globalization;
using System.Reflection;
namespace MewTerminal;
internal class Program {
static void Main(string[] args) {
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
Console.OutputEncoding = System.Text.Encoding.UTF8;
Logger.OnNewLogMessage((dt, lv, msg) => {
AnsiConsole.WriteLine($"{msg}");
});
#if DEBUG
Console.Clear();
var firstArg = new string[] { "help" };
start:
if(firstArg == null) {
Console.WriteLine("Enter arguments [DEBUG MODE]");
args = Console.ReadLine().SplitArgs();
}
//print help first time
InitParser(firstArg ?? args);
firstArg = null;
goto start;
#else
InitParser(args);
#endif
}
private static Type[] LoadVerbs() {
var lst = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetCustomAttribute<VerbAttribute>() != null)
.ToArray();
return lst;
}
static void InitParser (string[] args) {
var types = LoadVerbs();
var parseRes = Parser.Default.ParseArguments(args, types);
var helpText = HelpText.AutoBuild(parseRes, h => {
h.AddEnumValuesToHelpText = true;
return h;
}, e => e);
parseRes.WithNotParsed(err => {
});
if(parseRes?.Value != null && parseRes.Value is CommandLineExcecuteable exc) {
Logger.LogLevel = exc.LogLevel;
exc.Run();
var task = Task.Run(exc.RunAsync);
task.Wait();
}
}
}

18
MewTerminal/app.manifest Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="mewterminal.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
</application>
</compatibility>
</assembly>

View File

@@ -1,11 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
# Visual Studio Version 17
VisualStudioVersion = 17.5.33103.201
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewtocolNet", "MewtocolNet\MewtocolNet.csproj", "{8B7863E7-5E82-4990-9138-2C0C24629982}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolNet", "MewtocolNet\MewtocolNet.csproj", "{8B7863E7-5E82-4990-9138-2C0C24629982}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples", "Examples\Examples.csproj", "{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MewTerminal", "MewTerminal\MewTerminal.csproj", "{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AutoTools", "AutoTools", "{BAEF983A-EFF2-48DF-A74E-57084166BB4D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoTools.ChmDataExtract", "AutoTools.ChmDataExtract\AutoTools.ChmDataExtract.csproj", "{5A9DE453-AD64-4F8D-8215-3BB26674D164}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoTools.DocBuilder", "AutoTools.DocBuilder\AutoTools.DocBuilder.csproj", "{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.BasicEthernet", "Examples\Examples.BasicEthernet\Examples.BasicEthernet.csproj", "{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{FDBC39C6-5C06-4249-BDBF-068562D02AD6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.BasicRegisterReadWrite", "Examples\Examples.BasicRegisterReadWrite\Examples.BasicRegisterReadWrite.csproj", "{C630B87F-0DFD-4B3B-9845-B5627E9612D5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Polling", "Examples\Examples.Polling\Examples.Polling.csproj", "{6615556F-EF18-442F-8FF7-A7943A503C92}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.ProgramReadWrite", "Examples\Examples.ProgramReadWrite\Examples.ProgramReadWrite.csproj", "{8311E099-F164-4700-BA1A-F5214B38E0FA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.WPF", "Examples\Examples.WPF\Examples.WPF.csproj", "{339CC94E-68AB-4D2E-BCC1-84D81082EC93}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -16,9 +36,6 @@ Global
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B7863E7-5E82-4990-9138-2C0C24629982}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -32,17 +49,128 @@ Global
{8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x64.Build.0 = Release|Any CPU
{8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x86.ActiveCfg = Release|Any CPU
{8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x86.Build.0 = Release|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x64.ActiveCfg = Debug|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x64.Build.0 = Debug|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x86.ActiveCfg = Debug|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Debug|x86.Build.0 = Debug|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|Any CPU.Build.0 = Release|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x64.ActiveCfg = Release|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x64.Build.0 = Release|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x86.ActiveCfg = Release|Any CPU
{D1F2FA26-3752-44BA-9DCB-4BC2436C5957}.Release|x86.Build.0 = Release|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|x64.ActiveCfg = Debug|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|x64.Build.0 = Debug|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|x86.ActiveCfg = Debug|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|x86.Build.0 = Debug|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|Any CPU.Build.0 = Release|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x64.ActiveCfg = Release|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x64.Build.0 = Release|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x86.ActiveCfg = Release|Any CPU
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x86.Build.0 = Release|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x64.ActiveCfg = Debug|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x64.Build.0 = Debug|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x86.ActiveCfg = Debug|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Debug|x86.Build.0 = Debug|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|Any CPU.Build.0 = Release|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x64.ActiveCfg = Release|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x64.Build.0 = Release|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x86.ActiveCfg = Release|Any CPU
{D1E751C6-296F-4CF1-AE28-C6D4388C7CF1}.Release|x86.Build.0 = Release|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x64.ActiveCfg = Debug|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x64.Build.0 = Debug|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x86.ActiveCfg = Debug|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Debug|x86.Build.0 = Debug|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|Any CPU.Build.0 = Release|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x64.ActiveCfg = Release|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x64.Build.0 = Release|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x86.ActiveCfg = Release|Any CPU
{5A9DE453-AD64-4F8D-8215-3BB26674D164}.Release|x86.Build.0 = Release|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x64.ActiveCfg = Debug|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x64.Build.0 = Debug|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x86.ActiveCfg = Debug|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Debug|x86.Build.0 = Debug|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|Any CPU.Build.0 = Release|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x64.ActiveCfg = Release|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x64.Build.0 = Release|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x86.ActiveCfg = Release|Any CPU
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA}.Release|x86.Build.0 = Release|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x64.ActiveCfg = Debug|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x64.Build.0 = Debug|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x86.ActiveCfg = Debug|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Debug|x86.Build.0 = Debug|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|Any CPU.Build.0 = Release|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x64.ActiveCfg = Release|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x64.Build.0 = Release|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x86.ActiveCfg = Release|Any CPU
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E}.Release|x86.Build.0 = Release|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x64.ActiveCfg = Debug|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x64.Build.0 = Debug|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x86.ActiveCfg = Debug|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Debug|x86.Build.0 = Debug|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|Any CPU.Build.0 = Release|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x64.ActiveCfg = Release|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x64.Build.0 = Release|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x86.ActiveCfg = Release|Any CPU
{C630B87F-0DFD-4B3B-9845-B5627E9612D5}.Release|x86.Build.0 = Release|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x64.ActiveCfg = Debug|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x64.Build.0 = Debug|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x86.ActiveCfg = Debug|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Debug|x86.Build.0 = Debug|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|Any CPU.Build.0 = Release|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x64.ActiveCfg = Release|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x64.Build.0 = Release|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x86.ActiveCfg = Release|Any CPU
{6615556F-EF18-442F-8FF7-A7943A503C92}.Release|x86.Build.0 = Release|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x64.ActiveCfg = Debug|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x64.Build.0 = Debug|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x86.ActiveCfg = Debug|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Debug|x86.Build.0 = Debug|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|Any CPU.Build.0 = Release|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x64.ActiveCfg = Release|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x64.Build.0 = Release|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x86.ActiveCfg = Release|Any CPU
{8311E099-F164-4700-BA1A-F5214B38E0FA}.Release|x86.Build.0 = Release|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|Any CPU.Build.0 = Debug|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x64.ActiveCfg = Debug|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x64.Build.0 = Debug|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x86.ActiveCfg = Debug|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Debug|x86.Build.0 = Debug|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|Any CPU.ActiveCfg = Release|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|Any CPU.Build.0 = Release|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x64.ActiveCfg = Release|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x64.Build.0 = Release|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x86.ActiveCfg = Release|Any CPU
{339CC94E-68AB-4D2E-BCC1-84D81082EC93}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5A9DE453-AD64-4F8D-8215-3BB26674D164} = {BAEF983A-EFF2-48DF-A74E-57084166BB4D}
{00ACA0AB-3988-4EF7-98A6-B39A36B136DA} = {BAEF983A-EFF2-48DF-A74E-57084166BB4D}
{9FAC7072-1AD7-45D7-B257-E46F9CC8BE8E} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
{C630B87F-0DFD-4B3B-9845-B5627E9612D5} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
{6615556F-EF18-442F-8FF7-A7943A503C92} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
{8311E099-F164-4700-BA1A-F5214B38E0FA} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
{339CC94E-68AB-4D2E-BCC1-84D81082EC93} = {FDBC39C6-5C06-4249-BDBF-068562D02AD6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4ABB8137-CD8F-4691-9802-9ED371012F47}
EndGlobalSection
EndGlobal

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
using MewtocolNet.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace MewtocolNet.ComCassette {
/// <summary>
/// Provides a interface to modify and find PLC network cassettes also known as COM5
/// </summary>
public class CassetteFinder {
public static async Task<IEnumerable<CassetteInformation>> FindClientsAsync(string ipSource = null, int timeoutMs = 100) {
Logger.Log("Scanning for cassettes over UDP");
var from = new IPEndPoint(IPAddress.Any, 0);
var interfacesTasks = new List<Task<List<CassetteInformation>>>();
var usableInterfaces = Mewtocol.GetUseableNetInterfaces();
if (ipSource == null) {
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (NetworkInterface netInterface in usableInterfaces) {
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
var unicastInfo = ipProps.UnicastAddresses
.FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
var ep = new IPEndPoint(unicastInfo.Address, 0);
interfacesTasks.Add(FindClientsForEndpoint(ep, timeoutMs, netInterface.Name));
}
} else {
from = new IPEndPoint(IPAddress.Parse(ipSource), 0);
var netInterface = usableInterfaces.FirstOrDefault(x => x.GetIPProperties().UnicastAddresses.Any(y => y.Address.ToString() == ipSource));
if (netInterface == null)
throw new NotSupportedException($"The host endpoint {ipSource}, is not available");
interfacesTasks.Add(FindClientsForEndpoint(from, timeoutMs, netInterface.Name));
}
//run the interface querys
var grouped = await Task.WhenAll(interfacesTasks);
var decomposed = new List<CassetteInformation>();
foreach (var grp in grouped) {
foreach (var cassette in grp) {
if (decomposed.Any(x => x.MacAddress.SequenceEqual(cassette.MacAddress))) continue;
decomposed.Add(cassette);
}
}
Logger.Log($"Found {decomposed.Count} cassettes");
return decomposed.OrderBy(x => x.IPAddress.ToString());
}
private static async Task<List<CassetteInformation>> FindClientsForEndpoint(IPEndPoint from, int timeoutMs, string ipEndpointName) {
var cassettesFound = new List<CassetteInformation>();
int plcPort = 9090;
// Byte msg to request the status transmission of all plcs
byte[] requestCode = new byte[] { 0x88, 0x40, 0x00 };
// The start code of the status transmission response
byte[] startCode = new byte[] { 0x88, 0xC0, 0x00 };
using (var udpClient = new UdpClient()) {
udpClient.EnableBroadcast = true;
udpClient.Client.Bind(from);
//broadcast packet to all devices (plc specific package)
udpClient.Send(requestCode, requestCode.Length, "255.255.255.255", plcPort);
//canceling after no new data was read
CancellationTokenSource tSource = new CancellationTokenSource();
var tm = new System.Timers.Timer(timeoutMs);
tm.Elapsed += (s, e) => {
tSource.Cancel();
tm.Stop();
};
tm.Start();
//wait for devices to send response
try {
byte[] recvBuffer = null;
while (!tSource.Token.IsCancellationRequested) {
if (tSource.Token.IsCancellationRequested) break;
var res = await udpClient.ReceiveAsync().WithCancellation(tSource.Token);
if (res.Buffer == null) break;
recvBuffer = res.Buffer;
if (recvBuffer.SearchBytePattern(startCode) == 0) {
tm.Stop();
tm.Start();
var parsed = CassetteInformation.FromBytes(recvBuffer, from, ipEndpointName);
if (parsed != null) cassettesFound.Add(parsed);
}
}
}
catch (OperationCanceledException) { }
catch (SocketException) { }
}
return cassettesFound;
}
}
}

View File

@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
//WARNING! The whole UDP protocol was reverse engineered and is not fully implemented..
namespace MewtocolNet.ComCassette {
/// <summary>
/// Information about the COM cassette
/// </summary>
public class CassetteInformation {
/// <summary>
/// Indicates if the cassette is currently configurating
/// </summary>
public bool IsConfigurating { get; private set; }
/// <summary>
/// Name of the COM cassette
/// </summary>
public string Name { get; set; }
/// <summary>
/// Indicates the usage of DHCP
/// </summary>
public bool UsesDHCP { get; set; }
/// <summary>
/// IP Address of the COM cassette
/// </summary>
public IPAddress IPAddress { get; set; }
/// <summary>
/// Subnet mask of the cassette
/// </summary>
public IPAddress SubnetMask { get; set; }
/// <summary>
/// Default gateway of the cassette
/// </summary>
public IPAddress GatewayAddress { get; set; }
/// <summary>
/// Mac address of the cassette
/// </summary>
public byte[] MacAddress { get; private set; }
/// <summary>
/// Mac address of the cassette formatted as a MAC string (XX:XX:XX:XX:XX)
/// </summary>
public string MacAddressStr => MacAddress.ToHexString(":");
/// <summary>
/// The source endpoint the cassette is reachable from
/// </summary>
public IPEndPoint Endpoint { get; private set; }
/// <summary>
/// The name of the endpoint the device is reachable from, or null if not specifically defined
/// </summary>
public string EndpointName { get; private set; }
/// <summary>
/// Firmware version as string
/// </summary>
public string FirmwareVersion { get; private set; }
/// <summary>
/// The tcp port of the cassette
/// </summary>
public int Port { get; private set; }
/// <summary>
/// Status of the cassette
/// </summary>
public CassetteStatus Status { get; private set; }
internal static CassetteInformation FromBytes(byte[] bytes, IPEndPoint endpoint, string endpointName) {
// Receive data package explained:
// 0 3 4 8 12 17 22 24 27 29 31 32
// 88 C0 00 | 00 | C0 A8 73 D4 | FF FF FF 00 | C0 A8 73 3C | 00 | C0 8F 60 53 1C | 01 10 | 23 86 | 00 | 25 | 00 | 00 | 00 | 0D | (byte) * (n) NAME LEN
// Header |DHCP| IPv4 addr. | Subnet Mask | IPv4 Gatwy | | Mac Addr. | Ver. | Port | | | |STAT| | Name LEN | Name
// 1 or 0 Procuct Type? StatusCode Length of Name
//get ips / mac
var dhcpOn = bytes.Skip(3).First() != 0x00;
var ipAdd = new IPAddress(bytes.Skip(4).Take(4).ToArray());
var subnetMask = new IPAddress(bytes.Skip(8).Take(4).ToArray());
var gateWaysAdd = new IPAddress(bytes.Skip(12).Take(4).ToArray());
var macAdd = bytes.Skip(17).Take(5).ToArray();
var firmwareV = string.Join(".", bytes.Skip(22).Take(2).Select(x => x.ToString("X1")).ToArray());
var port = BitConverter.ToUInt16(bytes.Skip(24).Take(2).Reverse().ToArray(), 0);
var status = (CassetteStatus)bytes.Skip(29).First();
//missing blocks, later
//get name
var name = Encoding.ASCII.GetString(bytes.Skip(32).ToArray());
return new CassetteInformation {
Name = name,
UsesDHCP = dhcpOn,
IPAddress = ipAdd,
SubnetMask = subnetMask,
GatewayAddress = gateWaysAdd,
MacAddress = macAdd,
Endpoint = endpoint,
EndpointName = endpointName,
FirmwareVersion = firmwareV,
Port = port,
Status = status,
};
}
public async Task SendNewConfigAsync() {
if (IsConfigurating) return;
// this command gets sent to a specific plc ip address to overwrite the cassette config
// If dhcp is set to 1 the ip is ignored but still must be valid
// 88 41 00 | 00 | C0 8F 61 07 1B | 05 | 54 65 73 74 31 | 05 | 46 50 58 45 54 | 00 | C0 A8 01 07 | FF FF FF 00 | C0 A8 73 3C
// Header | | | 5 | T e s t 1 | 05 | F P X E T |0||1| 192.168.1.7 | 255.255... | 192.168.115.60
// Header | | Mac Address |LEN>| ASCII Name |LEN>| Static |DHCP| Target IP | Subnet Mask | Gateway
IsConfigurating = true;
List<byte> sendBytes = new List<byte>();
//add cmd header
sendBytes.AddRange(new byte[] { 0x88, 0x41, 0x00, 0x00 });
//add mac
sendBytes.AddRange(MacAddress);
//add name length
sendBytes.Add((byte)Name.Length);
//add name
sendBytes.AddRange(Encoding.ASCII.GetBytes(Name));
//FPXET
var subname = Encoding.ASCII.GetBytes("TESTFP");
//add sub name length
sendBytes.Add((byte)subname.Length);
//add subname
sendBytes.AddRange(subname);
//add dhcp 0 | 1
sendBytes.Add((byte)(UsesDHCP ? 0x01 : 0x00));
//add ip address
sendBytes.AddRange(IPAddress.GetAddressBytes());
//add subnet mask ip address
sendBytes.AddRange(SubnetMask.GetAddressBytes());
//add gateway ip
sendBytes.AddRange(GatewayAddress.GetAddressBytes());
var sendBytesArr = sendBytes.ToArray();
using (var udpClient = new UdpClient()) {
udpClient.Client.Bind(Endpoint);
//broadcast packet to all devices (plc specific package)
await udpClient.SendAsync(sendBytesArr, sendBytesArr.Length, "255.255.255.255", 9090);
}
}
public static bool operator ==(CassetteInformation a, CassetteInformation b) => EqualProps(a, b);
public static bool operator !=(CassetteInformation a, CassetteInformation b) => !EqualProps(a, b);
private static bool EqualProps (CassetteInformation a, CassetteInformation b) {
if (a is null && b is null) return true;
if (!(a is null) && b is null) return false;
if (!(b is null) && a is null) return false;
return a.Name == b.Name &&
a.UsesDHCP == b.UsesDHCP &&
a.IPAddress.ToString() == b.IPAddress.ToString() &&
a.SubnetMask.ToString() == b.SubnetMask.ToString() &&
a.GatewayAddress.ToString() == b.GatewayAddress.ToString() &&
a.MacAddressStr == b.MacAddressStr &&
a.FirmwareVersion == b.FirmwareVersion &&
a.Port == b.Port &&
a.Status == b.Status;
}
public override bool Equals(object obj) {
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
return false;
} else {
return (CassetteInformation)obj == this;
}
}
}
}

View File

@@ -0,0 +1,19 @@
namespace MewtocolNet.ComCassette {
/// <summary>
/// Needs a list of all status codes.. hard to reverse engineer
/// </summary>
public enum CassetteStatus {
/// <summary>
/// Cassette is running as intended
/// </summary>
Normal = 0,
/// <summary>
/// Cassette DHCP resolution error
/// </summary>
DHCPError = 2,
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace MewtocolNet {
/// <summary>
/// A DWord is a 16 bit value of 2 bytes
/// </summary>
public struct DWord : MewtocolExtTypeInit2Word {
private int bitLength;
internal uint value;
public uint Value => value;
public DWord(uint bytes) {
value = bytes;
bitLength = Marshal.SizeOf(value) * 8;
}
public DWord(byte[] bytes) {
bytes = bytes.Take(4).ToArray();
value = BitConverter.ToUInt32(bytes, 0);
bitLength = Marshal.SizeOf(value) * 8;
}
//operations
public static DWord operator -(DWord a, DWord b) => new DWord() {
value = (ushort)(a.value - b.value)
};
public static DWord operator +(DWord a, DWord b) => new DWord() {
value = (ushort)(a.value + b.value)
};
public static DWord operator *(DWord a, DWord b) => new DWord() {
value = (ushort)(a.value * b.value)
};
public static DWord operator /(DWord a, DWord b) => new DWord() {
value = (ushort)(a.value / b.value)
};
public static bool operator ==(DWord a, DWord b) => a.value == b.value;
public static bool operator !=(DWord a, DWord b) => a.value != b.value;
/// <summary>
/// Gets or sets the bit value at the given position
/// </summary>
public bool this[int bitIndex] {
get {
if (bitIndex > bitLength - 1 && bitLength != 0)
throw new IndexOutOfRangeException($"The word bit index was out of range ({bitIndex}/{bitLength - 1})");
if (bitLength == 0) return false;
return (value & (1 << bitIndex)) != 0;
}
}
public override bool Equals(object obj) {
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
return false;
} else {
return (DWord)obj == this;
}
}
public override int GetHashCode() => (int)value;
public byte[] ToByteArray() => BitConverter.GetBytes(value);
//string ops
public override string ToString() => $"0x{value.ToString("X8")}";
public string ToStringBits() {
return Convert.ToString(value, 2).PadLeft(bitLength, '0');
}
public string ToStringBitsPlc() {
var parts = Convert.ToString(value, 2)
.PadLeft(Marshal.SizeOf(value) * 8, '0')
.SplitInParts(4);
return string.Join("_", parts);
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace MewtocolNet {
/// <summary>
/// A DateAndTime struct of 4 bytes represented as seconds from 2001-01-01 in the PLC<br/>
/// This also works for the PLC type TIME_OF_DAY and DATE
/// </summary>
public struct DateAndTime : MewtocolExtTypeInit2Word {
internal DateTime value;
public DateAndTime(int year = 2001, int month = 1, int day = 1, int hour = 0, int minute = 0, int second = 0) {
var minDate = MinDate;
var maxDate = MaxDate;
if (year < 2001 || year > 2099)
throw new NotSupportedException("Year must be between 2001 and 2099");
if (month < 1 || month > 12)
throw new NotSupportedException("Month must be between 1 and 12");
if (day < 1 || day > 32)
throw new NotSupportedException("Day must be between 1 and 32");
if (day < 1 || day > 32)
throw new NotSupportedException("Month must be between 1 and 32");
var dt = new DateTime(year, month, day, hour, minute, second);
if (dt < minDate)
throw new Exception($"The minimal DATE_AND_TIME repesentation is {minDate}");
if (dt > maxDate)
throw new Exception($"The maximal DATE_AND_TIME repesentation is {maxDate}");
value = dt;
}
public static DateAndTime FromBytes(byte[] bytes) {
var secondsFrom = BitConverter.ToUInt32(bytes, 0);
return FromDateTime(MinDate + TimeSpan.FromSeconds(secondsFrom));
}
public static DateAndTime FromDateTime(DateTime time) => new DateAndTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
//operations
public static TimeSpan operator -(DateAndTime a, DateAndTime b) => a.value - b.value;
public static DateAndTime operator +(DateAndTime a, TimeSpan b) => FromDateTime(a.value + b);
public static bool operator ==(DateAndTime a, DateAndTime b) => a.value == b.value;
public static bool operator !=(DateAndTime a, DateAndTime b) => a.value != b.value;
public override bool Equals(object obj) {
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
return false;
} else {
return (DateAndTime)obj == this;
}
}
public override int GetHashCode() => value.GetHashCode();
public byte[] ToByteArray() => BitConverter.GetBytes(SecondsSinceStart());
public DateTime ToDateTime() => value;
private uint SecondsSinceStart() => (uint)(value - MinDate).TotalSeconds;
private static DateTime MinDate => new DateTime(2001, 01, 01, 0, 0, 0);
private static DateTime MaxDate => new DateTime(2099, 12, 31, 23, 59, 59);
//string ops
public override string ToString() => $"DT#{value:yyyy-MM-dd-HH:mm:ss}";
}
}

View File

@@ -0,0 +1,7 @@
namespace MewtocolNet {
internal interface MewtocolExtTypeInit1Word { }
internal interface MewtocolExtTypeInit2Word { }
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace MewtocolNet {
/// <summary>
/// A word is a 16 bit value of 2 bytes
/// </summary>
public struct Word : MewtocolExtTypeInit1Word {
private int bitLength;
internal ushort value;
public ushort Value => value;
public Word(ushort bytes) {
value = bytes;
bitLength = Marshal.SizeOf(value) * 8;
}
public Word(byte[] bytes) {
bytes = bytes.Take(2).ToArray();
value = BitConverter.ToUInt16(bytes, 0);
bitLength = Marshal.SizeOf(value) * 8;
}
//operations
public static Word operator -(Word a, Word b) => new Word() {
value = (ushort)(a.value - b.value)
};
public static Word operator +(Word a, Word b) => new Word() {
value = (ushort)(a.value + b.value)
};
public static Word operator *(Word a, Word b) => new Word() {
value = (ushort)(a.value * b.value)
};
public static Word operator /(Word a, Word b) => new Word() {
value = (ushort)(a.value / b.value)
};
public static bool operator ==(Word a, Word b) => a.value == b.value;
public static bool operator !=(Word a, Word b) => a.value != b.value;
/// <summary>
/// Gets the bit value at the given position
/// </summary>
public bool this[int bitIndex] {
get {
if (bitIndex > bitLength - 1 && bitLength != 0)
throw new IndexOutOfRangeException($"The word bit index was out of range ({bitIndex}/{bitLength - 1})");
if (bitLength == 0) return false;
return (value & (1 << bitIndex)) != 0;
}
}
public override bool Equals(object obj) {
if ((obj == null) || !this.GetType().Equals(obj.GetType())) {
return false;
} else {
return (Word)obj == this;
}
}
public override int GetHashCode() => (int)value;
public byte[] ToByteArray() => BitConverter.GetBytes(value);
//string ops
public override string ToString() => $"0x{value.ToString("X4")}";
public string ToStringBits() {
return Convert.ToString(value, 2).PadLeft(bitLength, '0');
}
public string ToStringBitsPlc() {
var parts = Convert.ToString(value, 2)
.PadLeft(Marshal.SizeOf(value) * 8, '0')
.SplitInParts(4);
return string.Join("_", parts);
}
}
}

View File

@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
namespace MewtocolNet.Links {
namespace MewtocolNet.DataLists {
internal class LinkedData {
internal class CodeDescriptions {
internal static Dictionary<int, string> ErrorCodes = new System.Collections.Generic.Dictionary<int, string> {
internal static Dictionary<int, string> Error = new Dictionary<int, string> {
{21, "NACK error"},
{22, "WACK error"},
@@ -40,7 +39,6 @@ namespace MewtocolNet.Links {
};
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace MewtocolNet.Documentation {
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
internal class PlcCodeTestedAttribute : Attribute {
public PlcCodeTestedAttribute() { }
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace MewtocolNet.Documentation {
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
internal class PlcEXRTAttribute : Attribute {
public PlcEXRTAttribute() { }
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace MewtocolNet.Documentation {
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
internal class PlcLegacyAttribute : Attribute {
public PlcLegacyAttribute() { }
}
}

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<extradoc>
<class name="support-conv-types">
<list type="bullet">
<item>
<term>
<see cref="bool"/>
</term>
<description>Boolean R/X/Y registers</description>
</item>
<item>
<term>
<see cref="short"/>
</term>
<description>16 bit signed integer</description>
</item>
<item>
<term>
<see cref="ushort"/>
</term>
<description>16 bit un-signed integer</description>
</item>
<item>
<term>
<see cref="Word"/>
</term>
<description>16 bit word (2 bytes)</description>
</item>
<item>
<term>
<see cref="int"/>
</term>
<description>32 bit signed integer</description>
</item>
<item>
<term>
<see cref="uint"/>
</term>
<description>32 bit un-signed integer</description>
</item>
<item>
<term>
<see cref="DWord"/>
</term>
<description>32 bit word (4 bytes)</description>
</item>
<item>
<term>
<see cref="float"/>
</term>
<description>32 bit floating point</description>
</item>
<item>
<term>
<see cref="TimeSpan"/>
</term>
<description>
32 bit time from <see cref="PlcVarType.TIME"/> interpreted as <see cref="TimeSpan"/>
</description>
</item>
<item>
<term>
<see cref="Enum"/>
</term>
<description>16 or 32 bit enums, also supports flags</description>
</item>
<item>
<term>
<see cref="string"/>
</term>
<description>String of chars, the interface will automatically get the length</description>
</item>
</list>
</class>
</extradoc>

View File

@@ -0,0 +1,9 @@
using System;
namespace MewtocolNet.Events {
public delegate void PlcConnectionEventHandler(object sender, PlcConnectionArgs e);
public class PlcConnectionArgs : EventArgs { }
}

View File

@@ -0,0 +1,19 @@
using System;
namespace MewtocolNet.Events {
public delegate void PlcModeChangedEventHandler(object sender, PlcModeArgs e);
public class PlcModeArgs : EventArgs {
public OPMode LastMode { get; internal set; }
public OPMode NowMode { get; internal set; }
public bool ProgToRun { get; internal set; }
public bool RunToProg { get; internal set; }
}
}

View File

@@ -0,0 +1,98 @@
using MewtocolNet.Registers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
namespace MewtocolNet.Events {
public delegate void PlcReconnectEventHandler(object sender, ReconnectArgs e);
public class ReconnectArgs : EventArgs, INotifyPropertyChanged {
private TimeSpan retryCountDownRemaining;
public event PropertyChangedEventHandler PropertyChanged;
public event Action Reconnected;
public int ReconnectTry { get; internal set; }
public int MaxAttempts { get; internal set; }
public TimeSpan RetryCountDownTime { get; internal set; }
public TimeSpan RetryCountDownRemaining {
get => retryCountDownRemaining;
private set {
retryCountDownRemaining = value;
OnPropChange();
}
}
private bool isReconnected;
public bool IsReconnected {
get { return isReconnected; }
set {
isReconnected = value;
OnPropChange();
}
}
private System.Timers.Timer countDownTimer;
internal ReconnectArgs(int currentAttempt, int totalAttempts, TimeSpan delayBetween) {
ReconnectTry = currentAttempt;
MaxAttempts = totalAttempts;
RetryCountDownTime = delayBetween;
//start countdown timer
RetryCountDownRemaining = RetryCountDownTime;
var interval = 100;
var intervalTS = TimeSpan.FromMilliseconds(interval);
countDownTimer = new System.Timers.Timer(100);
countDownTimer.Elapsed += (s, e) => {
if (RetryCountDownRemaining <= TimeSpan.Zero) {
StopTimer();
return;
}
RetryCountDownRemaining -= intervalTS;
};
countDownTimer.Start();
}
internal void ConnectionSuccess () {
IsReconnected = true;
StopTimer();
Reconnected?.Invoke();
}
private void StopTimer () {
countDownTimer?.Stop();
RetryCountDownRemaining = TimeSpan.Zero;
}
private void OnPropChange([CallerMemberName] string propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,22 @@
using MewtocolNet.Registers;
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet.Events {
public delegate void RegisterChangedEventHandler(object sender, RegisterChangedArgs e);
public class RegisterChangedArgs : EventArgs {
public IRegister Register { get; internal set; }
public object Value { get; internal set; }
public object PreviousValue { get; internal set; }
public string PreviousValueString { get; internal set; }
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MewtocolNet {
internal static class AsyncExtensions {
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken) {
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) {
if (task != await Task.WhenAny(task, tcs.Task)) {
return default(T);
}
}
if (task.IsCanceled) return default(T);
return task.Result;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.IO.Ports;
using System.Threading.Tasks;
namespace MewtocolNet {
internal static class SerialPortExtensions {
public async static Task WriteAsync(this SerialPort serialPort, byte[] buffer, int offset, int count) {
await serialPort.BaseStream.WriteAsync(buffer, 0, buffer.Length);
}
public async static Task ReadAsync(this SerialPort serialPort, byte[] buffer, int offset, int count) {
var bytesToRead = count;
var temp = new byte[count];
while (bytesToRead > 0) {
var readBytes = await serialPort.BaseStream.ReadAsync(temp, 0, bytesToRead);
Array.Copy(temp, 0, buffer, offset + count - bytesToRead, readBytes);
bytesToRead -= readBytes;
}
}
public async static Task<byte[]> ReadAsync(this SerialPort serialPort, int count) {
var buffer = new byte[count];
await serialPort.ReadAsync(buffer, 0, count);
return buffer;
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Text;
namespace MewtocolNet {
internal static class CRCCalculator {
private static readonly ushort[] crcTable_CRC16_MCRF4XX = {
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
};
internal static string BCC_Mew(this string msg) {
byte[] bytes = Encoding.ASCII.GetBytes(msg);
byte crc = CRC8(bytes);
return msg.Insert(msg.Length, crc.ToString("X2"));
}
internal static string BCC_Mew7(this string msg) {
byte[] bytes = Encoding.ASCII.GetBytes(msg);
ushort crc = CRC16_MCRF4XX(bytes);
return msg.Insert(msg.Length, crc.ToString("X4"));
}
private static ushort CRC16_MCRF4XX(byte[] bytes) {
int icrc = 0xFFFF;
for (int i = 0; i < bytes.Length; i++) {
icrc = (icrc >> 8) ^ crcTable_CRC16_MCRF4XX[(icrc ^ bytes[i]) & 0xff];
}
byte[] ret = BitConverter.GetBytes(icrc);
return BitConverter.ToUInt16(ret, 0);
}
private static byte CRC8(byte[] bytes) {
byte xorTotalByte = 0;
for (int i = 0; i < bytes.Length; i++)
xorTotalByte ^= bytes[i];
return xorTotalByte;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace MewtocolNet.Helpers {
internal static class LinqHelpers {
internal static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
return DistinctBy(source, keySelector, null);
}
internal static IEnumerable<TSource> DistinctBy<TSource, TKey>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer) {
if (source == null) throw new ArgumentNullException(nameof(source));
if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
return _(); IEnumerable<TSource> _() {
var knownKeys = new HashSet<TKey>(comparer);
foreach (var element in source) {
if (knownKeys.Add(keySelector(element)))
yield return element;
}
}
}
}
}

View File

@@ -0,0 +1,514 @@
using MewtocolNet.Documentation;
using MewtocolNet.Registers;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Threading;
namespace MewtocolNet {
/// <summary>
/// Contains helper methods
/// </summary>
public static class MewtocolHelpers {
#region Async extensions
internal static Task WhenCanceled(this CancellationToken cancellationToken) {
var tcs = new TaskCompletionSource<bool>();
cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
return tcs.Task;
}
#endregion
#region Byte and string operation helpers
public static T SetFlag<T>(this Enum value, T flag, bool set) {
Type underlyingType = Enum.GetUnderlyingType(value.GetType());
dynamic valueAsInt = Convert.ChangeType(value, underlyingType);
dynamic flagAsInt = Convert.ChangeType(flag, underlyingType);
if (set) {
valueAsInt |= flagAsInt;
} else {
valueAsInt &= ~flagAsInt;
}
return (T)valueAsInt;
}
public static int DetermineTypeByteIntialSize(this Type type) {
//enums can only be of numeric types
if (type.IsEnum) return Marshal.SizeOf(Enum.GetUnderlyingType(type));
//strings get always set with 4 bytes because the first 4 bytes contain the length
if (type == typeof(string)) return 4;
if (type == typeof(TimeSpan)) return 4;
if (type == typeof(DateTime)) return 4;
if (type.Namespace.StartsWith("System")) return Marshal.SizeOf(type);
if (typeof(MewtocolExtTypeInit1Word).IsAssignableFrom(type)) return 2;
if (typeof(MewtocolExtTypeInit2Word).IsAssignableFrom(type)) return 4;
throw new Exception("Type not supported");
}
/// <summary>
/// Searches a byte array for a pattern
/// </summary>
/// <param name="src"></param>
/// <param name="pattern"></param>
/// <returns>The start index of the found pattern or -1</returns>
public static int SearchBytePattern(this byte[] src, byte[] pattern) {
int maxFirstCharSlot = src.Length - pattern.Length + 1;
for (int i = 0; i < maxFirstCharSlot; i++) {
if (src[i] != pattern[0]) // compare only first byte
continue;
// found a match on first byte, now try to match rest of the pattern
for (int j = pattern.Length - 1; j >= 1; j--) {
if (src[i + j] != pattern[j]) break;
if (j == 1) return i;
}
}
return -1;
}
/// <summary>
/// Converts a string (after converting to upper case) to ascii bytes
/// </summary>
internal static byte[] BytesFromHexASCIIString(this string _str) {
ASCIIEncoding ascii = new ASCIIEncoding();
byte[] bytes = ascii.GetBytes(_str.ToUpper());
return bytes;
}
/// <summary>
/// Parses a return message as RCS single bit
/// </summary>
internal static bool? ParseRCSingleBit(this string _onString) {
_onString = _onString.Replace("\r", "");
var res = new Regex(@"\%([0-9a-fA-F]{2})\$RC(.)").Match(_onString);
if (res.Success) {
string val = res.Groups[2].Value;
return val == "1";
}
return null;
}
/// <summary>
/// Parses a return message as RCS multiple bits
/// </summary>
internal static BitArray ParseRCMultiBit(this string _onString) {
_onString = _onString.Replace("\r", "");
var res = new Regex(@"\%([0-9a-fA-F]{2})\$RC(?<bits>(?:0|1){0,8})(..)").Match(_onString);
if (res.Success) {
string val = res.Groups["bits"].Value;
return new BitArray(val.Select(c => c == '1').ToArray());
}
return null;
}
/// <summary>
/// Parses a return string from the PLC as a raw byte array <br/>
/// Example:
/// <code>
/// ↓Start ↓end
/// %01$RD0100020010\r
/// </code>
/// This will return the byte array:
/// <code>
/// [0x01, 0x00, 0x02, 0x00]
/// </code>
/// </summary>
/// <param name="_onString"></param>
/// <returns>A <see cref="T:byte[]"/> or null of failed</returns>
internal static byte[] ParseResponseStringAsBytes(this string _onString) {
_onString = _onString.Replace("\r", "");
var res = new Regex(@"(?:\%|\<)([0-9a-fA-F]{2})\$(?:RD|RP|RC)(?<data>.*)(?<csum>..)").Match(_onString);
if (res.Success) {
string val = res.Groups["data"].Value;
var parts = val.SplitInParts(2).ToArray();
var bytes = new byte[parts.Length];
for (int i = 0; i < bytes.Length; i++) {
bytes[i] = byte.Parse(parts[i], NumberStyles.HexNumber);
}
return bytes;
}
return null;
}
/// <summary>
/// Splits a string into even parts
/// </summary>
internal static IEnumerable<string> SplitInParts(this string s, int partLength) {
if (s == null)
throw new ArgumentNullException(nameof(s));
if (partLength <= 0)
throw new ArgumentException("Part length has to be positive.", nameof(partLength));
for (var i = 0; i < s.Length; i += partLength)
yield return s.Substring(i, Math.Min(partLength, s.Length - i));
}
/// <summary>
/// Splits a string by uppercase words
/// </summary>
internal static IEnumerable<string> SplitByAlternatingCase(this string str) {
var words = new List<string>();
var result = new StringBuilder();
for (int i = 0; i < str.Length; i++) {
char lastCh = str[Math.Max(0, i - 1)];
char ch = str[i];
if (char.IsUpper(ch) && char.IsLower(lastCh) && result.Length > 0) {
words.Add(result.ToString().Trim());
result.Clear();
}
result.Append(ch);
}
if (!string.IsNullOrEmpty(result.ToString()))
words.Add(result.ToString().Trim());
return words;
}
/// <summary>
/// Splits a string by uppercase words and joins them with the given seperator
/// </summary>
internal static string JoinSplitByUpperCase(this string str, string seperator = " ") => string.Join(seperator, str.SplitByAlternatingCase());
internal static string Ellipsis(this string str, int maxLength) {
if (string.IsNullOrEmpty(str) || str.Length <= maxLength)
return str;
return $"{str.Substring(0, maxLength - 3)}...";
}
internal static string SanitizeLinebreakFormatting (this string str) {
str = str.Replace("\r", "").Replace("\n", "").Trim();
return Regex.Replace(str, @"\s+", " ");
}
internal static string SanitizeBracketFormatting(this string str) {
return str.Replace("(", "").Replace(")", "").Trim();
}
/// <summary>
/// Converts a hex string (AB01C1) to a byte array
/// </summary>
internal static byte[] HexStringToByteArray(this string hex) {
if (hex == null) return null;
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
/// <summary>
/// Converts a byte array to a hexadecimal string
/// </summary>
/// <param name="seperator">Seperator between the hex numbers</param>
/// <param name="arr">The byte array</param>
public static string ToHexString(this byte[] arr, string seperator = "") {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < arr.Length; i++) {
byte b = arr[i];
sb.Append(b.ToString("X2"));
if (i < arr.Length - 1) sb.Append(seperator);
}
return sb.ToString();
}
/// <summary>
/// Switches byte order from mixed to big endian
/// </summary>
internal static byte[] BigToMixedEndian(this byte[] arr) {
List<byte> oldBL = new List<byte>(arr);
List<byte> tempL = new List<byte>();
//make the input list even
if (arr.Length % 2 != 0)
oldBL.Add((byte)0);
for (int i = 0; i < oldBL.Count; i += 2) {
byte firstByte = oldBL[i];
byte lastByte = oldBL[i + 1];
tempL.Add(lastByte);
tempL.Add(firstByte);
}
return tempL.ToArray();
}
#endregion
#region Comparerers
/// <summary>
/// Checks if the register type is boolean
/// </summary>
internal static bool IsBoolean(this RegisterPrefix type) {
return type == RegisterPrefix.X || type == RegisterPrefix.Y || type == RegisterPrefix.R;
}
/// <summary>
/// Checks if the register type numeric
/// </summary>
internal static bool IsNumericDTDDT(this RegisterPrefix type) {
return type == RegisterPrefix.DT || type == RegisterPrefix.DDT;
}
/// <summary>
/// Checks if the register type is an physical in or output of the plc
/// </summary>
internal static bool IsPhysicalInOutType(this RegisterPrefix type) {
return type == RegisterPrefix.X || type == RegisterPrefix.Y;
}
internal static bool CompareIsDuplicate(this Register reg1, Register compare) {
bool valCompare = reg1.RegisterType == compare.RegisterType &&
reg1.MemoryAddress == compare.MemoryAddress &&
reg1.GetRegisterAddressLen() == compare.GetRegisterAddressLen() &&
reg1.GetSpecialAddress() == compare.GetSpecialAddress();
return valCompare;
}
internal static bool CompareIsDuplicateNonCast(this Register toInsert, Register compare, List<Type> allowOverlappingTypes) {
foreach (var type in allowOverlappingTypes) {
if (toInsert.GetType() == type) return false;
}
bool valCompare = toInsert.GetType() != compare.GetType() &&
toInsert.MemoryAddress == compare.MemoryAddress &&
toInsert.GetRegisterAddressLen() == compare.GetRegisterAddressLen() &&
toInsert.GetSpecialAddress() == compare.GetSpecialAddress();
return valCompare;
}
internal static bool CompareIsNameDuplicate(this Register reg1, Register compare) {
return (reg1.Name != null || compare.Name != null) && reg1.Name == compare.Name;
}
#endregion
#region PLC Type Enum Parsing
/// <summary>
/// Gets synonim names for a plc type enum
/// </summary>
/// <returns>All or just one of there are no synonims for the same <see cref="PlcType"/></returns>
public static string[] GetSynonims (this PlcType plcType) {
return Enum.GetNames(typeof(PlcType)).Where(n => Enum.Parse(typeof(PlcType), n).Equals(plcType)).ToArray();
}
/// <summary>
/// Converts the enum to a plc name string
/// </summary>
public static string ToName(this PlcType plcT) {
if (plcT == 0) return "Unknown";
if(!Enum.IsDefined(typeof(PlcType), plcT)) return "Unknown";
return ParsedPlcName.PlcDeconstruct(plcT.ToString()).ToString();
}
/// <summary>
/// Converts the enum to a decomposed <see cref="ParsedPlcName"/> struct
/// </summary>
public static ParsedPlcName ToNameDecompose(this PlcType plcT) {
if ((int)plcT == 0)
throw new NotSupportedException("No plc type found");
if (!Enum.IsDefined(typeof(PlcType), plcT)) return null;
return ParsedPlcName.PlcDeconstruct(plcT.ToString());
}
internal static ParsedPlcName ToNameDecompose(this string plcEnumString) {
return ParsedPlcName.PlcDeconstruct(plcEnumString);
}
/// <summary>
/// Checks if the PLC type is discontinued
/// </summary>
public static bool IsDiscontinued(this PlcType plcT) {
var memberInfos = plcT.GetType().GetMember(plcT.ToString());
var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType());
var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcLegacyAttribute), false);
if (valueAttributes != null) {
var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcLegacyAttribute));
if (found != null) return true;
}
return false;
}
#if DEBUG
internal static bool WasTestedLive(this PlcType plcT) {
var memberInfos = plcT.GetType().GetMember(plcT.ToString());
var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType());
var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcCodeTestedAttribute), false);
if (valueAttributes != null) {
var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcCodeTestedAttribute));
if (found != null) return true;
}
return false;
}
internal static bool IsEXRTPLC(this PlcType plcT) {
var memberInfos = plcT.GetType().GetMember(plcT.ToString());
var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == plcT.GetType());
var valueAttributes = enumValueMemberInfo?.GetCustomAttributes(typeof(PlcEXRTAttribute), false);
if (valueAttributes != null) {
var found = valueAttributes.FirstOrDefault(x => x.GetType() == typeof(PlcEXRTAttribute));
if (found != null) return true;
}
return false;
}
#endif
#endregion
#region Mapping
/// <summary>
/// Maps the source object to target object.
/// </summary>
/// <typeparam name="T">Type of target object.</typeparam>
/// <typeparam name="TU">Type of source object.</typeparam>
/// <param name="target">Target object.</param>
/// <param name="source">Source object.</param>
/// <returns>Updated target object.</returns>
internal static T Map<T, TU>(this T target, TU source) {
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
var tprops = target.GetType().GetProperties();
tprops.Where(x => x.CanWrite == true).ToList().ForEach(prop => {
// check whether source object has the the property
var sp = source.GetType().GetProperty(prop.Name);
if (sp != null) {
// if yes, copy the value to the matching property
var value = sp.GetValue(source, null);
target.GetType().GetProperty(prop.Name).SetValue(target, value, null);
}
});
var tfields = target.GetType().GetFields(flags);
tfields.ToList().ForEach(field => {
var sp = source.GetType().GetField(field.Name, flags);
if (sp != null) {
// if yes, copy the value to the matching property
var value = sp.GetValue(source);
target.GetType().GetField(field.Name, flags).SetValue(target, value);
}
});
return target;
}
#endregion
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
namespace MewtocolNet {
/// <summary>
/// A structure containing the PLC name parsed
/// </summary>
public class ParsedPlcName {
/// <summary>
/// Whole name of the PLC
/// </summary>
public string WholeName { get; internal set; } = "Unknown PLC";
/// <summary>
/// The family group of the PLC
/// </summary>
public string Group { get; internal set; } = "Unknown Group";
/// <summary>
/// The Memory size of the PLC
/// </summary>
public float Size { get; internal set; }
/// <summary>
/// The subtype strings of the plc
/// </summary>
public string[] SubTypes { get; internal set; } = new string[] { "N/A" };
/// <summary>
/// Typecode of the parsed string
/// </summary>
public int TypeCode { get; internal set; }
/// <summary>
/// The encoded name, same as enum name
/// </summary>
public string EncodedName { get; internal set; } = "Unknown";
/// <summary>
/// True if the model is discontinued
/// </summary>
public bool IsDiscontinuedModel { get; internal set; }
internal bool WasTestedLive { get; set; }
internal bool UsesEXRT { get; set; }
/// <inheritdoc/>
public override string ToString() => WholeName;
internal static ParsedPlcName PlcDeconstruct(string wholeStr) {
if(wholeStr == "Unknown") {
return new ParsedPlcName();
}
var reg = new Regex(@"(?<group>[A-Za-z0-9]*)_(?<size>[A-Za-z0-9]*)(?:__)?(?<additional>.*)");
var match = reg.Match(wholeStr);
if (match.Success) {
string groupStr = SanitizePlcEncodedString(match.Groups["group"].Value);
string sizeStr = SanitizePlcEncodedString(match.Groups["size"].Value);
float sizeFl = float.Parse(sizeStr.Replace("k", ""), NumberStyles.Float, CultureInfo.InvariantCulture);
string additionalStr = match.Groups["additional"].Value;
string[] subTypes = additionalStr.Split('_').Select(x => SanitizePlcEncodedString(x)).ToArray();
string wholeName = $"{groupStr} {sizeFl:0.##}k{(subTypes.Length > 0 ? " " : "")}{string.Join(",", subTypes)}";
if (string.IsNullOrEmpty(subTypes[0]))
subTypes = Array.Empty<string>();
int typeCode = 999;
bool discontinued = false, exrt = false, tested = false;
if(Enum.TryParse(wholeStr, out PlcType t)) {
typeCode = (int)t;
discontinued = t.IsDiscontinued();
#if Debug
exrt = t.IsEXRTPLC();
tested = t.WasTestedLive();
#endif
}
return new ParsedPlcName {
Group = groupStr,
Size = sizeFl,
SubTypes = subTypes,
WholeName = wholeName,
EncodedName = wholeStr,
TypeCode = typeCode,
IsDiscontinuedModel = discontinued,
UsesEXRT = exrt,
WasTestedLive = tested,
};
} else {
throw new FormatException($"The plc enum was not formatted correctly: {wholeStr}");
}
}
private static string SanitizePlcEncodedString(string input) {
input = input.Replace("d", "-");
input = input.Replace("c", ".");
input = input.Replace("s", "/");
return input;
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MewtocolNet.Helpers {
internal static class PlcBitConverter {
internal static string ToVersionNumber(IEnumerable<byte> inBytes, int startIndex = 0) {
return string.Join(".", inBytes.Skip(startIndex).Take(4).Reverse().Select(x => x.ToString()));
}
internal static DateTime ToDateTime(IEnumerable<byte> inBytes, int startIndex = 0) {
var offDate = new DateTime(2001, 01, 01);
var secondsOff = BitConverter.ToUInt32(inBytes.ToArray(), startIndex);
return offDate + TimeSpan.FromSeconds(secondsOff);
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace MewtocolNet {
public static class PlcFormat {
/// <summary>
/// Gets the TimeSpan as a PLC representation string fe.
/// <code>
/// T#1h10m30s20ms
/// </code>
/// </summary>
/// <param name="timespan"></param>
/// <returns></returns>
public static string ToPlcTime(this TimeSpan timespan) {
if (timespan == null || timespan == TimeSpan.Zero)
return $"T#0s";
StringBuilder sb = new StringBuilder("T#");
int millis = timespan.Milliseconds;
int seconds = timespan.Seconds;
int minutes = timespan.Minutes;
int hours = timespan.Hours;
int days = timespan.Days;
if (days > 0) sb.Append($"{days}d");
if (hours > 0) sb.Append($"{hours}h");
if (minutes > 0) sb.Append($"{minutes}m");
if (seconds > 0) sb.Append($"{seconds}s");
if (millis > 0) sb.Append($"{millis}ms");
return sb.ToString();
}
public static TimeSpan ParsePlcTime(string plcTimeFormat) {
var reg = new Regex(@"(?:T|t)#(?:(?<d>[0-9]{1,2})d)?(?:(?<h>[0-9]{1,2})h)?(?:(?<m>[0-9]{1,2})m)?(?:(?<s>[0-9]{1,2})s)?(?:(?<ms>[0-9]{1,3})ms)?");
var match = reg.Match(plcTimeFormat);
if (match.Success) {
var days = match.Groups["d"].Value;
var hours = match.Groups["h"].Value;
var minutes = match.Groups["m"].Value;
var seconds = match.Groups["s"].Value;
var milliseconds = match.Groups["ms"].Value;
TimeSpan retTime = TimeSpan.Zero;
if (!string.IsNullOrEmpty(days)) retTime += TimeSpan.FromDays(int.Parse(days));
if (!string.IsNullOrEmpty(hours)) retTime += TimeSpan.FromHours(int.Parse(hours));
if (!string.IsNullOrEmpty(minutes)) retTime += TimeSpan.FromMinutes(int.Parse(minutes));
if (!string.IsNullOrEmpty(seconds)) retTime += TimeSpan.FromSeconds(int.Parse(seconds));
if (!string.IsNullOrEmpty(milliseconds)) retTime += TimeSpan.FromMilliseconds(int.Parse(milliseconds));
if ((retTime.TotalMilliseconds % 10) != 0)
throw new NotSupportedException("Plc times can't have a millisecond component lower than 10ms");
return retTime;
}
return TimeSpan.Zero;
}
/// <summary>
/// Turns a bit array into a 0 and 1 string
/// </summary>
public static string ToBitString(this BitArray arr) {
var bits = new bool[arr.Length];
arr.CopyTo(bits, 0);
return string.Join("", bits.Select(x => x ? "1" : "0"));
}
}
}

213
MewtocolNet/IPlc.cs Normal file
View File

@@ -0,0 +1,213 @@
using MewtocolNet.Events;
using MewtocolNet.ProgramParsing;
using MewtocolNet.RegisterBuilding;
using MewtocolNet.RegisterBuilding.BuilderPatterns;
using MewtocolNet.Registers;
using MewtocolNet.UnderlyingRegisters;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
namespace MewtocolNet {
/// <summary>
/// Provides a interface for Panasonic PLCs
/// </summary>
public interface IPlc : IDisposable, INotifyPropertyChanged {
/// <summary>
/// Fires when the interface is fully connected to a PLC
/// </summary>
event PlcConnectionEventHandler Connected;
/// <summary>
/// Fires when a reconnect attempt was successfull
/// </summary>
event PlcConnectionEventHandler Reconnected;
/// <summary>
/// Fires when the interfaces makes a reconnect try to the PLC
/// </summary>
event PlcReconnectEventHandler ReconnectTryStarted;
/// <summary>
/// Fires when the plc/interface connection was fully closed
/// </summary>
event PlcConnectionEventHandler Disconnected;
/// <summary>
/// Fires when the value of a register changes
/// </summary>
event RegisterChangedEventHandler RegisterChanged;
/// <summary>
/// Plc mode was changed
/// </summary>
event PlcModeChangedEventHandler ModeChanged;
/// <summary>
/// The current connection state of the interface
/// </summary>
bool IsConnected { get; }
/// <summary>
/// This device is sending a message to the plc
/// </summary>
bool IsSending { get; }
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
int BytesPerSecondUpstream { get; }
/// <summary>
/// This device is receiving a message from the plc
/// </summary>
bool IsReceiving { get; }
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
int BytesPerSecondDownstream { get; }
/// <summary>
/// Current poller cycle duration
/// </summary>
int PollerCycleDurationMs { get; }
/// <summary>
/// Shorthand indicator if the plc is in RUN mode
/// </summary>
bool IsRunMode { get; }
/// <summary>
/// Currently queued message count
/// </summary>
int QueuedMessages { get; }
/// <summary>
/// Generic information about the connected PLC
/// </summary>
PLCInfo PlcInfo { get; }
/// <summary>
/// The station number of the PLC
/// </summary>
int StationNumber { get; }
/// <summary>
/// A connection info string
/// </summary>
string ConnectionInfo { get; }
/// <summary>
/// The initial connection timeout in milliseconds
/// </summary>
int ConnectTimeout { get; set; }
IEnumerable<IRegister> Registers { get; }
RBuildAnon Register { get; }
/// <summary>
/// Tries to establish a connection with the device asynchronously
/// </summary>
/// <param name="onConnected">A callback for excecuting something inside the plc connetion process</param>
/// <returns></returns>
Task<ConnectResult> ConnectAsync(Func<Task> onConnected = null);
/// <summary>
/// Disconnects the device from its current plc connection
/// and awaits the end of all asociated tasks
/// </summary>
Task DisconnectAsync();
/// <summary>
/// Disconnects the device from its current plc connection
/// </summary>
void Disconnect();
/// <summary>
/// Stops a running reconnect task
/// </summary>
void StopReconnecting();
/// <summary>
/// Adds a task to each reconnect cycle that is run before each individual try
/// </summary>
void WithReconnectTask(Func<int, Task> callback);
/// <summary>
/// Changes the PLCs operation mode to the given one
/// </summary>
/// <param name="setRun">True for RUN mode, false for PROG mode</param>
/// <returns>The success state of the write operation</returns>
Task<bool> SetOperationModeAsync(bool setRun);
/// <summary>
/// Toggles between RUN and PROG mode
/// </summary>
/// <returns>The success state of the write operation</returns>
Task<bool> ToggleOperationModeAsync();
/// <summary>
/// Restarts the plc program
/// </summary>
/// <returns>The success state of the write operation</returns>
Task<bool> RestartProgramAsync();
/// <summary>
/// Reads the program from the connected plc
/// </summary>
/// <returns></returns>
Task<PlcBinaryProgram> ReadProgramAsync();
/// <summary>
/// Factory resets the PLC, this includes the current program
/// and data in the EEPROM
/// </summary>
/// <returns></returns>
Task FactoryResetAsync();
/// <summary>
/// Use this to await the first poll iteration after connecting,
/// This also completes if the initial connection fails
/// </summary>
Task AwaitFirstDataCycleAsync();
/// <summary>
/// Runs a single poller cycle manually,
/// useful if you want to use a custom update frequency
/// </summary>
/// <returns>The number of inidvidual mewtocol commands sent</returns>
Task<int> UpdateAsync();
/// <summary>
/// Gets the connection info string
/// </summary>
string GetConnectionInfo();
/// <summary>
/// Gets a register from the plc by name
/// </summary>
IRegister GetRegister(string name);
/// <summary>
/// Gets all registers from the plc
/// </summary>
IEnumerable<IRegister> GetAllRegisters();
/// <summary>
/// Explains the register internal layout at this moment in time
/// </summary>
string Explain();
/// <summary>
/// A readonly list of the underlying memory areas
/// </summary>
IReadOnlyList<IMemoryArea> MemoryAreas { get; }
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Net;
using System.Threading.Tasks;
namespace MewtocolNet {
/// <summary>
/// Provides a interface for Panasonic PLCs over a ethernet connection
/// </summary>
public interface IPlcEthernet : IPlc {
/// <summary>
/// The current IP of the PLC connection
/// </summary>
string IpAddress { get; }
/// <summary>
/// The current port of the PLC connection
/// </summary>
int Port { get; }
/// <summary>
/// The host ip endpoint, leave it null to use an automatic interface
/// </summary>
IPEndPoint HostEndpoint { get; }
/// <summary>
/// Configures the serial interface
/// </summary>
/// <param name="_ip">IP adress of the PLC</param>
/// <param name="_port">Port of the PLC</param>
/// <param name="_station">Station Number of the PLC</param>
void ConfigureConnection(string _ip, int _port = 9094, int _station = 1);
}
}

55
MewtocolNet/IPlcSerial.cs Normal file
View File

@@ -0,0 +1,55 @@
using System;
using System.IO.Ports;
using System.Threading.Tasks;
namespace MewtocolNet {
/// <summary>
/// Provides a interface for Panasonic PLCs over a serial port connection
/// </summary>
public interface IPlcSerial : IPlc {
/// <summary>
/// Port name of the serial port that this device is configured for
/// </summary>
string PortName { get; }
/// <summary>
/// The serial connection baud rate that this device is configured for
/// </summary>
int SerialBaudRate { get; }
/// <summary>
/// The serial connection data bits
/// </summary>
int SerialDataBits { get; }
/// <summary>
/// The serial connection parity
/// </summary>
Parity SerialParity { get; }
/// <summary>
/// The serial connection stop bits
/// </summary>
StopBits SerialStopBits { get; }
/// <summary>
/// Sets up the connection settings for the device
/// </summary>
/// <param name="_portName">Port name of COM port</param>
/// <param name="_baudRate">The serial connection baud rate</param>
/// <param name="_dataBits">The serial connection data bits</param>
/// <param name="_parity">The serial connection parity</param>
/// <param name="_stopBits">The serial connection stop bits</param>
/// <param name="_station">The station number of the PLC</param>
void ConfigureConnection(string _portName, int _baudRate = 19200, int _dataBits = 8, Parity _parity = Parity.Odd, StopBits _stopBits = StopBits.One, int _station = 1);
/// <summary>
/// Tries to establish a connection with the device asynchronously
/// </summary>
Task<ConnectResult> ConnectAsync(Func<Task> callBack, Action onTryingConfig);
}
}

View File

@@ -0,0 +1,12 @@
namespace MewtocolNet {
internal enum CommandState {
Initial,
LineFeed,
RequestedNextFrame,
Complete
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Diagnostics;
namespace MewtocolNet.Logging {
/// <summary>
/// Logging module for all PLCs
/// </summary>
public static class Logger {
/// <summary>
/// Sets the loglevel for the global logging module
/// </summary>
public static LogLevel LogLevel { get; set; }
/// <summary>
/// Defines the default output logger targets
/// </summary>
public static LoggerTargets DefaultTargets { get; set; } = LoggerTargets.None;
internal static Action<DateTime, LogLevel, string> LogInvoked;
static Logger () {
var isConsoleApplication = Console.LargestWindowWidth != 0;
OnNewLogMessage((d, l, m) => {
if(isConsoleApplication || DefaultTargets.HasFlag(LoggerTargets.Console)) {
switch (l) {
case LogLevel.Error:
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
break;
case LogLevel.Info:
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
break;
case LogLevel.Change:
Console.ForegroundColor = ConsoleColor.DarkBlue;
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
break;
case LogLevel.Verbose:
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
break;
case LogLevel.Critical:
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($"{d:hh:mm:ss:ff} {m}");
break;
}
Console.ResetColor();
}
});
LogInvoked += (d, l, m) => {
if (DefaultTargets.HasFlag(LoggerTargets.Trace)) {
Trace.WriteLine($"{d:hh:mm:ss:ff} {m}");
}
};
}
//for static calling purposes only
internal static void Start() { }
/// <summary>
/// Gets invoked whenever a new log message is ready
/// </summary>
public static void OnNewLogMessage(Action<DateTime, LogLevel, string> onMsg, LogLevel? maxLevel = null) {
if (maxLevel == null) maxLevel = LogLevel;
LogInvoked += (t, l, m) => {
if ((int)l <= (int)maxLevel) {
onMsg(t, l, m);
}
};
}
internal static void Log(string message, LogLevel loglevel, MewtocolInterface sender = null) {
if (sender == null) {
LogInvoked?.Invoke(DateTime.Now, loglevel, message);
} else {
LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionInfo()}] {message}");
}
}
internal static void LogError (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Error, sender);
internal static void Log (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Info, sender);
internal static void LogChange (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Change, sender);
internal static void LogVerbose (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Verbose, sender);
internal static void LogCritical (string message, MewtocolInterface sender = null) => Log(message, LogLevel.Critical, sender);
}
}

View File

@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet.Logging {
namespace MewtocolNet.Logging {
/// <summary>
/// The loglevel of the logging module
/// </summary>
public enum LogLevel {
/// <summary>
/// Logs nothing
/// </summary>
None = -1,
/// <summary>
/// Logs only errors
/// </summary>

View File

@@ -0,0 +1,12 @@
using System;
namespace MewtocolNet.Logging {
[Flags]
public enum LoggerTargets {
None = 0,
Console = 1,
Trace = 2,
}
}

496
MewtocolNet/Mewtocol.cs Normal file
View File

@@ -0,0 +1,496 @@
using MewtocolNet.RegisterAttributes;
using MewtocolNet.RegisterBuilding;
using MewtocolNet.RegisterBuilding.BuilderPatterns;
using MewtocolNet.SetupClasses;
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
using static MewtocolNet.Mewtocol;
namespace MewtocolNet {
/// <summary>
/// Builder helper for mewtocol interfaces
/// </summary>
public static class Mewtocol {
#region Data Access
/// <summary>
/// Lists all usable COM port names
/// </summary>
/// <returns></returns>
public static IEnumerable<string> GetSerialPortNames () => SerialPort.GetPortNames();
/// <summary>
/// Lists all usable serial baud rates
/// </summary>
/// <returns></returns>
public static IEnumerable<int> GetUseableBaudRates() => Enum.GetValues(typeof(BaudRate)).Cast<BaudRate>().Select(x => (int)x);
/// <summary>
/// Lists all useable source endpoints of the device this is running on for usage with PLCs
/// </summary>
public static IEnumerable<IPEndPoint> GetSourceEndpoints() {
foreach (var netIf in GetUseableNetInterfaces()) {
var addressInfo = netIf.GetIPProperties().UnicastAddresses
.FirstOrDefault(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
yield return new IPEndPoint(addressInfo.Address, 9094);
}
}
/// <summary>
/// Lists all useable network interfaces of the device this is running on for usage with PLCs
/// </summary>
public static IEnumerable<NetworkInterface> GetUseableNetInterfaces() {
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) {
bool isEthernet =
netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet ||
netInterface.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit ||
netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx ||
netInterface.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT ||
netInterface.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet;
bool isWlan = netInterface.NetworkInterfaceType == NetworkInterfaceType.Wireless80211;
bool isUsable = netInterface.OperationalStatus == OperationalStatus.Up;
if (!isUsable) continue;
if (!(isWlan || isEthernet)) continue;
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
var hasUnicastInfo = ipProps.UnicastAddresses
.Any(x => x.Address.AddressFamily == AddressFamily.InterNetwork);
if (!hasUnicastInfo) continue;
yield return netInterface;
}
}
#endregion
#region Interface building step 1
/// <summary>
/// Builds a ethernet based Mewtocol Interface
/// </summary>
/// <param name="ip"></param>
/// <param name="port"></param>
/// <param name="station">Plc station number 0xEE for direct communication</param>
/// <returns></returns>
public static PostInitEth<IPlcEthernet> Ethernet(string ip, int port = 9094, int station = 0xEE) {
var instance = new MewtocolInterfaceTcp();
instance.ConfigureConnection(ip, port, station);
return new PostInitEth<IPlcEthernet> {
intf = instance
};
}
/// <summary>
/// Builds a ethernet based Mewtocol Interface
/// </summary>
/// <param name="ip"></param>
/// <param name="port"></param>
/// <param name="station">Plc station number 0xEE for direct communication</param>
/// <returns></returns>
public static PostInitEth<IPlcEthernet> Ethernet(IPAddress ip, int port = 9094, int station = 0xEE) {
var instance = new MewtocolInterfaceTcp();
instance.ConfigureConnection(ip, port, station);
return new PostInitEth<IPlcEthernet> {
intf = instance
};
}
/// <summary>
/// Builds a serial port based Mewtocol Interface
/// </summary>
/// <param name="portName">System port name</param>
/// <param name="baudRate">Baud rate of the plc toolport</param>
/// <param name="dataBits">DataBits of the plc toolport</param>
/// <param name="parity">Parity rate of the plc toolport</param>
/// <param name="stopBits">Stop bits of the plc toolport</param>
/// <param name="station">Plc station number 0xEE for direct communication</param>
/// <returns></returns>
public static PostInit<IPlcSerial> Serial(string portName, BaudRate baudRate = BaudRate._19200, DataBits dataBits = DataBits.Eight, Parity parity = Parity.Odd, StopBits stopBits = StopBits.One, int station = 0xEE) {
var instance = new MewtocolInterfaceSerial();
instance.ConfigureConnection(portName, (int)baudRate, (int)dataBits, parity, stopBits, station);
return new PostInit<IPlcSerial> {
intf = instance
};
}
/// <summary>
/// Builds a serial mewtocol interface that finds the correct settings for the given port name automatically
/// </summary>
/// <param name="portName"></param>
/// <param name="station">Plc station number 0xEE for direct communication</param>
/// <returns></returns>
public static PostInit<IPlcSerial> SerialAuto(string portName, int station = 0xEE) {
var instance = new MewtocolInterfaceSerial();
instance.ConfigureConnection(portName, station);
instance.ConfigureConnectionAuto();
return new PostInit<IPlcSerial> {
intf = instance
};
}
#endregion
#region Interface building step 2
public class PollLevelConfigurator {
internal Dictionary<int, PollLevelConfig> levelConfigs = new Dictionary<int, PollLevelConfig>();
/// <summary>
/// Sets the poll level for the given key
/// </summary>
/// <param name="level">The level to reference</param>
/// <param name="interval">Delay between poll requests</param>
public PollLevelConfigurator SetLevel(int level, TimeSpan interval) {
if (level == PollLevel.Always || level == PollLevel.Never || level == PollLevel.FirstIteration)
throw new NotSupportedException("The poll level is reserved for the library");
if (!levelConfigs.ContainsKey(level)) {
levelConfigs.Add(level, new PollLevelConfig {
delay = interval,
});
} else {
throw new NotSupportedException("Can't set poll levels multiple times");
}
return this;
}
public PollLevelConfigurator SetLevel(int level, int skipNth) {
if (level == PollLevel.Always || level == PollLevel.Never || level == PollLevel.FirstIteration)
throw new NotSupportedException("The poll level is reserved for the library");
if (!levelConfigs.ContainsKey(level)) {
levelConfigs.Add(level, new PollLevelConfig {
skipNth = skipNth,
});
} else {
throw new NotSupportedException("Can't set poll levels multiple times");
}
return this;
}
}
public class RegCollector {
internal List<RegisterCollection> collections = new List<RegisterCollection>();
public T AddCollection<T>(T collection) where T : RegisterCollection {
collections.Add(collection);
return collection;
}
public T AddCollection<T>() where T : RegisterCollection {
var instance = (RegisterCollection)Activator.CreateInstance(typeof(T));
collections.Add(instance);
return (T)instance;
}
}
public class PostInitEth<T> : PostInit<T> {
/// <summary>
/// Sets the source of the outgoing ethernet connection
/// </summary>
public PostInit<T> FromSource (IPEndPoint endpoint) {
if(endpoint == null)
throw new ArgumentNullException("Endpoint can't be null", nameof(endpoint));
if(intf is MewtocolInterfaceTcp imew) {
imew.HostEndpoint = endpoint;
}
return this;
}
/// <summary>
/// Sets the source of the outgoing ethernet connection
/// </summary>
/// <param name="ip">IP address of the source interface (Format: 127.0.0.1)</param>
/// <param name="port">Port of the source interface</param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public PostInit<T> FromSource(string ip, int port) {
if (intf is MewtocolInterfaceTcp imew) {
if(port < IPEndPoint.MinPort)
throw new ArgumentException($"Source port cant be smaller than {IPEndPoint.MinPort}", nameof(port));
if (port > IPEndPoint.MaxPort)
throw new ArgumentException($"Source port cant be larger than {IPEndPoint.MaxPort}", nameof(port));
if (!IPAddress.TryParse(ip, out var ipParsed))
throw new ArgumentException("Failed to parse the source IP", nameof(ip));
imew.HostEndpoint = new IPEndPoint(ipParsed, port);
}
return this;
}
}
public class PostInit<T> {
internal T intf;
/// <summary>
/// Attaches a auto poller to the interface that reads all registers
/// cyclic
/// </summary>
/// <returns></returns>
public PostInit<T> WithPoller() {
if (intf is MewtocolInterface imew) {
imew.usePoller = true;
}
return this;
}
/// <summary>
/// General setting for the memory manager
/// </summary>
public PostInit<T> WithInterfaceSettings(Action<InterfaceSettings> settings) {
var res = new InterfaceSettings();
settings.Invoke(res);
if (res.MaxOptimizationDistance < 0)
throw new NotSupportedException($"A value lower than 0 is not allowed for " +
$"{nameof(InterfaceSettings.MaxOptimizationDistance)}");
if (res.MaxDataBlocksPerWrite < 1)
throw new NotSupportedException($"A value lower than 1 is not allowed for " +
$"{nameof(InterfaceSettings.MaxDataBlocksPerWrite)}");
if (intf is MewtocolInterface imew) {
imew.memoryManager.maxOptimizationDistance = res.MaxOptimizationDistance;
imew.memoryManager.pollLevelOrMode = res.PollLevelOverwriteMode;
imew.maxDataBlocksPerWrite = res.MaxDataBlocksPerWrite;
imew.heartbeatIntervalMs = res.HeartbeatIntervalMs;
imew.tryReconnectAttempts = res.TryReconnectAttempts;
imew.tryReconnectDelayMs = res.TryReconnectDelayMs;
imew.alwaysGetMetadata = res.AlwaysGetMetadata;
}
return this;
}
/// <summary>
/// A builder for poll custom levels
/// </summary>
public PostInit<T> WithCustomPollLevels(Action<PollLevelConfigurator> levels) {
var res = new PollLevelConfigurator();
levels.Invoke(res);
if (intf is MewtocolInterface imew) {
imew.memoryManager.pollLevelConfigs = res.levelConfigs;
}
return this;
}
/// <summary>
/// A builder for attaching register collections
/// </summary>
public PostInit<T> WithRegisterCollections(Action<RegCollector> collector) {
try {
var res = new RegCollector();
collector.Invoke(res);
if (intf is MewtocolInterface imew) {
imew.WithRegisterCollections(res.collections);
}
return this;
} catch {
throw;
}
}
/// <summary>
/// A builder for attaching register collections
/// </summary>
public PostInit<T> WithRegisters(Action<RBuildMulti> builder) {
try {
var plc = (MewtocolInterface)(object)intf;
var regBuilder = new RBuildMulti(plc);
builder.Invoke(regBuilder);
plc.AddRegisters(regBuilder.assembler.assembled.ToArray());
return this;
} catch {
throw;
}
}
/// <summary>
/// Adds a task to each reconnect cycle that is run before each individual try
/// </summary>
public PostInit<T> WithReconnectTask(Func<int, Task> callback) {
var plc = (MewtocolInterface)(object)intf;
plc.onBeforeReconnectTryTask = callback;
return this;
}
/// <summary>
/// Adds a task to each heartbeat cycle that is run before each individual cycle request
/// </summary>
public EndInitSetup<T> WithHeartbeatTask(Func<IPlc,Task> heartBeatAsync, bool executeInProg = false) {
try {
var plc = (MewtocolInterface)(object)this.intf;
plc.heartbeatCallbackTask = heartBeatAsync;
plc.execHeartBeatCallbackTaskInProg = executeInProg;
return new EndInitSetup<T> {
postInit = this,
};
} catch {
throw;
}
}
/// <summary>
/// Repeats the passed method each time the hearbeat is triggered,
/// use
/// </summary>
/// <param name="heartBeatAsync"></param>
/// <returns></returns>
public EndInitSetup<T> WithHeartbeatTask(Func<Task> heartBeatAsync, bool executeInProg = false) => WithHeartbeatTask(heartBeatAsync, executeInProg);
/// <summary>
/// Disables all heartbeat tasks
/// </summary>
/// <returns></returns>
public EndInitSetup<T> DisableHeartBeat() {
try {
var plc = (MewtocolInterface)(object)this.intf;
plc.disableHeartbeat = true;
return new EndInitSetup<T> {
postInit = this,
};
} catch {
throw;
}
}
/// <summary>
/// Builds and returns the final plc interface
/// </summary>
public T Build() => (T)(object)((MewtocolInterface)(object)intf).Build();
}
#endregion
#region Interface building step 4
public class EndInitSetup<T> {
internal PostInit<T> postInit;
/// <summary>
/// Builds and returns the final plc interface
/// </summary>
public T Build() => (T)(object)((MewtocolInterface)(object)postInit.intf).Build();
}
#endregion
}
}

View File

@@ -1,67 +0,0 @@
using System;
namespace MewtocolNet.Registers {
/// <summary>
/// Contains information about the plc and its cpu
/// </summary>
public partial class CpuInfo {
/// <summary>
/// The cpu type of the plc
/// </summary>
public CpuType Cputype { get; set; }
/// <summary>
/// Program capacity in 1K steps
/// </summary>
public int ProgramCapacity { get; set; }
/// <summary>
/// Version of the cpu
/// </summary>
public string CpuVersion { get; set; }
internal static CpuInfo BuildFromHexString (string _cpuType, string _cpuVersion, string _progCapacity) {
CpuInfo retInf = new CpuInfo();
switch (_cpuType) {
case "02":
retInf.Cputype = CpuType.FP5_16K;
break;
case "03":
retInf.Cputype = CpuType.FP3_C_10K;
break;
case "04":
retInf.Cputype = CpuType.FP1_M_0_9K;
break;
case "05":
retInf.Cputype = CpuType.FP0_FP1_2_7K;
break;
case "06":
retInf.Cputype = CpuType.FP0_FP1_5K_10K;
break;
case "12":
retInf.Cputype = CpuType.FP5_24K;
break;
case "13":
retInf.Cputype = CpuType.FP3_C_16K;
break;
case "20":
retInf.Cputype = CpuType.FP_Sigma_X_H_30K_60K_120K;
break;
case "50":
retInf.Cputype = CpuType.FP2_16K_32K;
break;
}
retInf.ProgramCapacity = Convert.ToInt32(_progCapacity);
retInf.CpuVersion = _cpuVersion.Insert(1, ".");
return retInf;
}
}
}

View File

@@ -1,460 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MewtocolNet.Logging;
using MewtocolNet.Registers;
namespace MewtocolNet {
/// <summary>
/// The PLC com interface class
/// </summary>
public partial class MewtocolInterface {
/// <summary>
/// True if the auto poller is currently paused
/// </summary>
public bool PollingPaused => pollerIsPaused;
internal event Action PolledCycle;
internal volatile bool pollerTaskRunning;
internal volatile bool pollerTaskStopped;
internal volatile bool pollerIsPaused;
internal bool usePoller = false;
#region Register Polling
/// <summary>
/// Kills the poller completely
/// </summary>
internal void KillPoller () {
pollerTaskRunning = false;
pollerTaskStopped = true;
}
/// <summary>
/// Pauses the polling and waits for the last message to be sent
/// </summary>
/// <returns></returns>
public async Task PausePollingAsync () {
if (!pollerTaskRunning)
return;
pollerTaskRunning = false;
while (!pollerIsPaused) {
if (pollerIsPaused)
break;
await Task.Delay(10);
}
pollerTaskRunning = false;
}
/// <summary>
/// Resumes the polling
/// </summary>
public void ResumePolling () {
pollerTaskRunning = true;
}
/// <summary>
/// Attaches a continous reader that reads back the Registers and Contacts
/// </summary>
internal void AttachPoller () {
if (pollerTaskRunning)
return;
Task.Factory.StartNew(async () => {
Logger.Log("Poller is attaching", LogLevel.Info, this);
int iteration = 0;
pollerTaskStopped = false;
pollerTaskRunning = true;
pollerIsPaused = false;
while (!pollerTaskStopped) {
while (pollerTaskRunning) {
if (iteration >= Registers.Count + 1) {
iteration = 0;
//invoke cycle polled event
InvokePolledCycleDone();
continue;
}
if (iteration >= Registers.Count) {
await GetPLCInfoAsync();
iteration++;
continue;
}
var reg = Registers[iteration];
if (reg is NRegister<short> shortReg) {
var lastVal = shortReg.Value;
var readout = (await ReadNumRegister(shortReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(shortReg);
}
}
if (reg is NRegister<ushort> ushortReg) {
var lastVal = ushortReg.Value;
var readout = (await ReadNumRegister(ushortReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(ushortReg);
}
}
if (reg is NRegister<int> intReg) {
var lastVal = intReg.Value;
var readout = (await ReadNumRegister(intReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(intReg);
}
}
if (reg is NRegister<uint> uintReg) {
var lastVal = uintReg.Value;
var readout = (await ReadNumRegister(uintReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(uintReg);
}
}
if (reg is NRegister<float> floatReg) {
var lastVal = floatReg.Value;
var readout = (await ReadNumRegister(floatReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(floatReg);
}
}
if (reg is NRegister<TimeSpan> tsReg) {
var lastVal = tsReg.Value;
var readout = (await ReadNumRegister(tsReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(tsReg);
}
}
if (reg is BRegister boolReg) {
var lastVal = boolReg.Value;
var readout = (await ReadBoolRegister(boolReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(boolReg);
}
}
if (reg is SRegister stringReg) {
var lastVal = stringReg.Value;
var readout = (await ReadStringRegister(stringReg)).Register.Value;
if (lastVal != readout) {
InvokeRegisterChanged(stringReg);
}
}
iteration++;
await Task.Delay(pollerDelayMs);
}
pollerIsPaused = !pollerTaskRunning;
}
pollerIsPaused = false;
});
}
#endregion
#region Register Adding
/// <summary>
/// Adds a PLC memory register to the watchlist <para/>
/// The registers can be read back by attaching <see cref="WithPoller"/>
/// </summary>
/// <param name="_address">The address of the register in the PLCs memory</param>
/// <param name="_type">
/// The memory area type
/// <para>X = Physical input area (bool)</para>
/// <para>Y = Physical input area (bool)</para>
/// <para>R = Internal relay area (bool)</para>
/// <para>DT = Internal data area (short/ushort)</para>
/// <para>DDT = Internal relay area (int/uint)</para>
/// </param>
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
public void AddRegister (int _address, RegisterType _type, string _name = null) {
Register toAdd = null;
//as number registers
if (_type == RegisterType.DT_short) {
toAdd = new NRegister<short>(_address, _name);
}
if (_type == RegisterType.DT_ushort) {
toAdd = new NRegister<ushort>(_address, _name);
}
if (_type == RegisterType.DDT_int) {
toAdd = new NRegister<int>(_address, _name);
}
if (_type == RegisterType.DDT_uint) {
toAdd = new NRegister<uint>(_address, _name);
}
if (_type == RegisterType.DDT_float) {
toAdd = new NRegister<float>(_address, _name);
}
if(toAdd == null) {
toAdd = new BRegister(_address, _type, _name);
}
Registers.Add(toAdd);
}
internal void AddRegister (Type _colType, int _address, RegisterType _type, string _name = null) {
Register toAdd = null;
//as number registers
if (_type == RegisterType.DT_short) {
toAdd = new NRegister<short>(_address, _name);
}
if (_type == RegisterType.DT_ushort) {
toAdd = new NRegister<ushort>(_address, _name);
}
if (_type == RegisterType.DDT_int) {
toAdd = new NRegister<int>(_address, _name);
}
if (_type == RegisterType.DDT_uint) {
toAdd = new NRegister<uint>(_address, _name);
}
if (_type == RegisterType.DDT_float) {
toAdd = new NRegister<float>(_address, _name);
}
if (toAdd == null) {
toAdd = new BRegister(_address, _type, _name);
}
toAdd.collectionType = _colType;
Registers.Add(toAdd);
}
/// <summary>
/// Adds a PLC memory register to the watchlist <para/>
/// The registers can be read back by attaching <see cref="WithPoller"/>
/// </summary>
/// <param name="_spAddress">The special address of the register in the PLCs memory</param>
/// <param name="_type">
/// The memory area type
/// <para>X = Physical input area (bool)</para>
/// <para>Y = Physical input area (bool)</para>
/// <para>R = Internal relay area (bool)</para>
/// <para>DT = Internal data area (short/ushort)</para>
/// <para>DDT = Internal relay area (int/uint)</para>
/// </param>
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
public void AddRegister (SpecialAddress _spAddress, RegisterType _type, string _name = null) {
//as bool registers
Registers.Add(new BRegister(_spAddress, _type, _name));
}
internal void AddRegister (Type _colType, SpecialAddress _spAddress, RegisterType _type, string _name = null) {
var reg = new BRegister(_spAddress, _type, _name);
reg.collectionType = _colType;
//as bool registers
Registers.Add(reg);
}
/// <summary>
/// Adds a PLC memory register to the watchlist <para/>
/// The registers can be read back by attaching <see cref="WithPoller"/>
/// </summary>
/// <typeparam name="T">
/// The type of the register translated from C# to IEC 61131-3 types
/// <para>C# ------ IEC</para>
/// <para>short => INT/WORD</para>
/// <para>ushort => UINT</para>
/// <para>int => DOUBLE</para>
/// <para>uint => UDOUBLE</para>
/// <para>float => REAL</para>
/// <para>string => STRING</para>
/// </typeparam>
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
/// <param name="_address">The address of the register in the PLCs memory</param>
/// <param name="_length">The length of the string (Can be ignored for other types)</param>
public void AddRegister<T>(int _address, int _length = 1, string _name = null) {
Type regType = typeof(T);
if (regType != typeof(string) && _length != 1) {
throw new NotSupportedException($"_lenght parameter only allowed for register of type string");
}
Register toAdd;
if (regType == typeof(short)) {
toAdd = new NRegister<short>(_address, _name);
} else if (regType == typeof(ushort)) {
toAdd = new NRegister<ushort>(_address, _name);
} else if (regType == typeof(int)) {
toAdd = new NRegister<int>(_address, _name);
} else if (regType == typeof(uint)) {
toAdd = new NRegister<uint>(_address, _name);
} else if (regType == typeof(float)) {
toAdd = new NRegister<float>(_address, _name);
} else if (regType == typeof(string)) {
toAdd = new SRegister(_address, _length, _name);
} else if (regType == typeof(TimeSpan)) {
toAdd = new NRegister<TimeSpan>(_address, _name);
} else if (regType == typeof(bool)) {
toAdd = new BRegister(_address, RegisterType.R, _name);
} else {
throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" +
$"Allowed are: short, ushort, int, uint, float and string");
}
if (Registers.Any(x => x.GetRegisterPLCName() == toAdd.GetRegisterPLCName())) {
throw new NotSupportedException($"Cannot add a register multiple times, " +
$"make sure that all register attributes or AddRegister assignments have different adresses.");
}
}
internal void AddRegister<T> (Type _colType, int _address, int _length = 1, string _name = null, bool _isBitwise = false, Type _enumType = null) {
Type regType = typeof(T);
if (regType != typeof(string) && _length != 1) {
throw new NotSupportedException($"_lenght parameter only allowed for register of type string");
}
if (Registers.Any(x => x.MemoryAdress == _address) && _isBitwise) {
return;
}
Register reg = null;
if (regType == typeof(short)) {
reg = new NRegister<short>(_address, _name, _isBitwise);
} else if (regType == typeof(ushort)) {
reg = new NRegister<ushort>(_address, _name);
} else if (regType == typeof(int)) {
reg = new NRegister<int>(_address, _name, _isBitwise, _enumType);
} else if (regType == typeof(uint)) {
reg = new NRegister<uint>(_address, _name);
} else if (regType == typeof(float)) {
reg = new NRegister<float>(_address, _name);
} else if (regType == typeof(string)) {
reg = new SRegister(_address, _length, _name);
} else if (regType == typeof(TimeSpan)) {
reg = new NRegister<TimeSpan>(_address, _name);
} else if (regType == typeof(bool)) {
reg = new BRegister(_address, RegisterType.R, _name);
}
if (reg == null) {
throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" +
$"Allowed are: short, ushort, int, uint, float and string");
} else {
if (Registers.Any(x => x.GetRegisterPLCName() == reg.GetRegisterPLCName()) && !_isBitwise) {
throw new NotSupportedException($"Cannot add a register multiple times, " +
$"make sure that all register attributes or AddRegister assignments have different adresses.");
}
reg.collectionType = _colType;
Registers.Add(reg);
}
}
#endregion
#region Register accessing
/// <summary>
/// Gets a register that was added by its name
/// </summary>
/// <returns></returns>
public Register GetRegister (string name) {
return Registers.FirstOrDefault(x => x.Name == name);
}
/// <summary>
/// Gets a register that was added by its name
/// </summary>
/// <typeparam name="T">The type of register</typeparam>
/// <returns>A casted register or the <code>default</code> value</returns>
public T GetRegister<T> (string name) where T : Register {
try {
var reg = Registers.FirstOrDefault(x => x.Name == name);
return reg as T;
} catch (InvalidCastException) {
return default(T);
}
}
#endregion
#region Register Reading
/// <summary>
/// Gets a list of all added registers
/// </summary>
public List<Register> GetAllRegisters () {
return Registers;
}
#endregion
#region Event Invoking
internal void InvokeRegisterChanged (Register reg) {
RegisterChanged?.Invoke(reg);
}
internal void InvokePolledCycleDone () {
PolledCycle?.Invoke();
}
#endregion
}
}

View File

@@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet.Logging {
/// <summary>
/// Logging module for all PLCs
/// </summary>
public static class Logger {
/// <summary>
/// Sets the loglevel for the logger module
/// </summary>
public static LogLevel LogLevel { get; set; }
internal static Action<DateTime, string> LogInvoked;
/// <summary>
/// Gets invoked whenever a new log message is ready
/// </summary>
public static void OnNewLogMessage (Action<DateTime, string> onMsg) {
LogInvoked += (t, m) => {
onMsg(t, m);
};
}
internal static void Log (string message, LogLevel loglevel, MewtocolInterface sender = null) {
if ((int)loglevel <= (int)LogLevel) {
if (sender == null) {
LogInvoked?.Invoke(DateTime.Now, message);
} else {
LogInvoked?.Invoke(DateTime.Now, $"[{sender.GetConnectionPortInfo()}] {message}");
}
}
}
}
}

View File

@@ -1,202 +0,0 @@
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using MewtocolNet.Registers;
using System.Collections;
namespace MewtocolNet {
/// <summary>
/// Contains helper methods
/// </summary>
public static class MewtocolHelpers {
/// <summary>
/// Turns a bit array into a 0 and 1 string
/// </summary>
public static string ToBitString (this BitArray arr) {
var bits = new bool[arr.Length];
arr.CopyTo(bits, 0);
return string.Join("", bits.Select(x => x ? "1" : "0"));
}
internal static byte[] ToHexASCIIBytes (this string _str) {
ASCIIEncoding ascii = new ASCIIEncoding();
byte[] bytes = ascii.GetBytes(_str.ToUpper());
return bytes;
}
internal static string BuildBCCFrame (this string asciiArr) {
Encoding ae = Encoding.ASCII;
byte[] b = ae.GetBytes(asciiArr);
byte xorTotalByte = 0;
for(int i = 0; i < b.Length; i++)
xorTotalByte ^= b[i];
return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2"));
}
internal static byte[] ParseDTBytes (this string _onString ,int _blockSize = 4) {
var res = new Regex(@"\%([0-9]{2})\$RD(.{"+_blockSize+"})").Match(_onString);
if(res.Success) {
string val = res.Groups[2].Value;
return val.HexStringToByteArray();
}
return null;
}
internal static string ParseDTByteString (this string _onString, int _blockSize = 4) {
if (_onString == null)
return null;
var res = new Regex(@"\%([0-9]{2})\$RD(.{" + _blockSize + "})").Match(_onString);
if (res.Success) {
string val = res.Groups[2].Value;
return val;
}
return null;
}
internal static bool? ParseRCSingleBit (this string _onString, int _blockSize = 4) {
var res = new Regex(@"\%([0-9]{2})\$RC(.)").Match(_onString);
if (res.Success) {
string val = res.Groups[2].Value;
return val == "1";
}
return null;
}
internal static string ParseDTString (this string _onString) {
var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString);
if(res.Success) {
string val = res.Groups[2].Value;
return val.GetStringFromAsciiHex()?.Trim();
}
return null;
}
internal static string ReverseByteOrder (this string _onString) {
if(_onString == null) return null;
//split into 2 chars
var stringBytes = _onString.SplitInParts(2).ToList();
stringBytes.Reverse();
return string.Join("", stringBytes);
}
internal static IEnumerable<String> SplitInParts (this string s, int partLength) {
if (s == null)
throw new ArgumentNullException(nameof(s));
if (partLength <= 0)
throw new ArgumentException("Part length has to be positive.", nameof(partLength));
for (var i = 0; i < s.Length; i += partLength)
yield return s.Substring(i, Math.Min(partLength, s.Length - i));
}
internal static string BuildDTString (this string _inString, short _stringReservedSize) {
StringBuilder sb = new StringBuilder();
//clamp string lenght
if (_inString.Length > _stringReservedSize) {
_inString = _inString.Substring(0, _stringReservedSize);
}
//actual string content
var hexstring = _inString.GetAsciiHexFromString();
var sizeBytes = BitConverter.GetBytes((short)(hexstring.Length / 2)).ToHexString();
if (hexstring.Length >= 2) {
var remainderBytes = (hexstring.Length / 2) % 2;
if (remainderBytes != 0) {
hexstring += "20";
}
}
var reservedSizeBytes = BitConverter.GetBytes(_stringReservedSize).ToHexString();
//reserved string count bytes
sb.Append(reservedSizeBytes);
//string count actual bytes
sb.Append(sizeBytes);
sb.Append(hexstring);
return sb.ToString();
}
internal static string GetStringFromAsciiHex (this string input) {
if (input.Length % 2 != 0)
return null;
byte[] bytes = new byte[input.Length / 2];
for (int i = 0; i < input.Length; i += 2) {
String hex = input.Substring(i, 2);
bytes[i/2] = Convert.ToByte(hex, 16);
}
return Encoding.ASCII.GetString(bytes);
}
internal static string GetAsciiHexFromString (this string input) {
var bytes = new ASCIIEncoding().GetBytes(input);
return bytes.ToHexString();
}
internal static byte[] HexStringToByteArray(this string hex) {
if (hex == null)
return null;
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
internal static string ToHexString (this byte[] arr) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < arr.Length; i++) {
byte b = arr[i];
sb.Append(b.ToString("X2"));
}
return sb.ToString();
}
internal static byte[] BigToMixedEndian (this byte[] arr) {
List<byte> oldBL = new List<byte>(arr);
List<byte> tempL = new List<byte>();
//make the input list even
if(arr.Length % 2 != 0)
oldBL.Add((byte)0);
for (int i = 0; i < oldBL.Count; i+=2) {
byte firstByte = oldBL[i];
byte lastByte = oldBL[i + 1];
tempL.Add(lastByte);
tempL.Add(firstByte);
}
return tempL.ToArray();
}
}
}

View File

@@ -1,915 +0,0 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using MewtocolNet.Registers;
using MewtocolNet.RegisterAttributes;
using MewtocolNet.Logging;
using System.Collections;
using System.Diagnostics;
using System.ComponentModel;
using System.Net;
using System.Threading;
using MewtocolNet.Queue;
namespace MewtocolNet {
/// <summary>
/// The PLC com interface class
/// </summary>
public partial class MewtocolInterface : INotifyPropertyChanged, IDisposable {
/// <summary>
/// Gets triggered when the PLC connection was established
/// </summary>
public event Action<PLCInfo> Connected;
/// <summary>
/// Gets triggered when the PLC connection was closed or lost
/// </summary>
public event Action Disconnected;
/// <summary>
/// Gets triggered when a registered data register changes its value
/// </summary>
public event Action<Register> RegisterChanged;
/// <summary>
/// Gets triggered when a property of the interface changes
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private int connectTimeout = 3000;
/// <summary>
/// The initial connection timeout in milliseconds
/// </summary>
public int ConnectTimeout {
get { return connectTimeout; }
set { connectTimeout = value; }
}
private volatile int pollerDelayMs = 0;
/// <summary>
/// Delay for each poller cycle in milliseconds, default = 0
/// </summary>
public int PollerDelayMs {
get => pollerDelayMs;
set {
pollerDelayMs = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollerDelayMs)));
}
}
private volatile int queuedMessages;
/// <summary>
/// Currently queued Messages
/// </summary>
public int QueuedMessages {
get => queuedMessages;
}
/// <summary>
/// The host ip endpoint, leave it null to use an automatic interface
/// </summary>
public IPEndPoint HostEndpoint { get; set; }
private bool isConnected;
/// <summary>
/// The current connection state of the interface
/// </summary>
public bool IsConnected {
get => isConnected;
private set {
isConnected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsConnected)));
}
}
private bool disposed;
/// <summary>
/// True if the current interface was disposed
/// </summary>
public bool Disposed {
get { return disposed; }
private set { disposed = value; }
}
private PLCInfo plcInfo;
/// <summary>
/// Generic information about the connected PLC
/// </summary>
public PLCInfo PlcInfo {
get => plcInfo;
private set {
plcInfo = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PlcInfo)));
}
}
/// <summary>
/// The registered data registers of the PLC
/// </summary>
public List<Register> Registers { get; set; } = new List<Register>();
private string ip;
private int port;
private int stationNumber;
private int cycleTimeMs = 25;
private int bytesTotalCountedUpstream = 0;
private int bytesTotalCountedDownstream = 0;
/// <summary>
/// The current IP of the PLC connection
/// </summary>
public string IpAddress => ip;
/// <summary>
/// The current port of the PLC connection
/// </summary>
public int Port => port;
/// <summary>
/// The station number of the PLC
/// </summary>
public int StationNumber => stationNumber;
/// <summary>
/// The duration of the last message cycle
/// </summary>
public int CycleTimeMs {
get { return cycleTimeMs; }
private set {
cycleTimeMs = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CycleTimeMs)));
}
}
private int bytesPerSecondUpstream = 0;
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
public int BytesPerSecondUpstream {
get { return bytesPerSecondUpstream; }
private set {
bytesPerSecondUpstream = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondUpstream)));
}
}
private int bytesPerSecondDownstream = 0;
/// <summary>
/// The current transmission speed in bytes per second
/// </summary>
public int BytesPerSecondDownstream {
get { return bytesPerSecondDownstream; }
private set {
bytesPerSecondDownstream = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BytesPerSecondDownstream)));
}
}
internal NetworkStream stream;
internal TcpClient client;
internal readonly SerialQueue queue = new SerialQueue();
private int RecBufferSize = 128;
internal int SendExceptionsInRow = 0;
internal bool ImportantTaskRunning = false;
private Stopwatch speedStopwatchUpstr;
private Stopwatch speedStopwatchDownstr;
#region Initialization
/// <summary>
/// Builds a new Interfacer for a PLC
/// </summary>
/// <param name="_ip">IP adress of the PLC</param>
/// <param name="_port">Port of the PLC</param>
/// <param name="_station">Station Number of the PLC</param>
public MewtocolInterface (string _ip, int _port = 9094, int _station = 1) {
ip = _ip;
port = _port;
stationNumber = _station;
Connected += MewtocolInterface_Connected;
void MewtocolInterface_Connected (PLCInfo obj) {
if (usePoller)
AttachPoller();
IsConnected = true;
}
RegisterChanged += (o) => {
string address = $"{o.GetRegisterString()}{o.MemoryAdress}".PadRight(5, (char)32);
Logger.Log($"{address} " +
$"{(o.Name != null ? $"({o.Name}) " : "")}" +
$"changed to \"{o.GetValueString()}\"", LogLevel.Change, this);
};
}
#endregion
#region Setup
/// <summary>
/// Trys to connect to the PLC by the IP given in the constructor
/// </summary>
/// <param name="OnConnected">
/// Gets called when a connection with a PLC was established
/// <para/>
/// If <see cref="WithPoller"/> is used it waits for the first data receive cycle to complete
/// </param>
/// <param name="OnFailed">Gets called when an error or timeout during connection occurs</param>
/// <returns></returns>
public async Task<MewtocolInterface> ConnectAsync (Action<PLCInfo> OnConnected = null, Action OnFailed = null) {
Logger.Log("Connecting to PLC...", LogLevel.Info, this);
var plcinf = await GetPLCInfoAsync();
if (plcinf != null) {
Logger.Log("Connected", LogLevel.Info, this);
Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this);
Connected?.Invoke(plcinf);
if (OnConnected != null) {
if (!usePoller) {
OnConnected(plcinf);
return this;
}
PolledCycle += OnPollCycleDone;
void OnPollCycleDone () {
OnConnected(plcinf);
PolledCycle -= OnPollCycleDone;
}
}
} else {
if (OnFailed != null) {
OnFailed();
Disconnected?.Invoke();
Logger.Log("Initial connection failed", LogLevel.Info, this);
}
}
return this;
}
/// <summary>
/// Changes the connections parameters of the PLC, only applyable when the connection is offline
/// </summary>
/// <param name="_ip">Ip adress</param>
/// <param name="_port">Port number</param>
/// <param name="_station">Station number</param>
public void ChangeConnectionSettings (string _ip, int _port, int _station = 1) {
if (IsConnected)
throw new Exception("Cannot change the connection settings while the PLC is connected");
ip = _ip;
port = _port;
stationNumber = _station;
}
/// <summary>
/// Closes the connection all cyclic polling
/// </summary>
public void Disconnect () {
if (!IsConnected)
return;
OnMajorSocketExceptionWhileConnected();
}
/// <summary>
/// Attaches a poller to the interface that continously
/// polls the registered data registers and writes the values to them
/// </summary>
public MewtocolInterface WithPoller () {
usePoller = true;
return this;
}
#endregion
#region TCP connection state handling
private async Task ConnectTCP () {
if (!IPAddress.TryParse(ip, out var targetIP)) {
throw new ArgumentException("The IP adress of the PLC was no valid format");
}
try {
if(HostEndpoint != null) {
client = new TcpClient(HostEndpoint) {
ReceiveBufferSize = RecBufferSize,
NoDelay = false,
};
var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Verbose, this);
} else {
client = new TcpClient() {
ReceiveBufferSize = RecBufferSize,
NoDelay = false,
ExclusiveAddressUse = true,
};
}
var result = client.BeginConnect(targetIP, port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout));
if(!success || !client.Connected) {
OnMajorSocketExceptionWhileConnecting();
return;
}
if(HostEndpoint == null) {
var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Verbose, this);
}
stream = client.GetStream();
stream.ReadTimeout = 1000;
await Task.CompletedTask;
} catch (SocketException) {
OnMajorSocketExceptionWhileConnecting();
}
}
private void OnMajorSocketExceptionWhileConnecting () {
Logger.Log("The PLC connection timed out", LogLevel.Error, this);
CycleTimeMs = 0;
IsConnected = false;
KillPoller();
}
private void OnMajorSocketExceptionWhileConnected () {
if (IsConnected) {
Logger.Log("The PLC connection was closed", LogLevel.Error, this);
CycleTimeMs = 0;
IsConnected = false;
Disconnected?.Invoke();
KillPoller();
client.Close();
}
}
#endregion
#region Register Collection
/// <summary>
/// Attaches a register collection object to
/// the interface that can be updated automatically.
/// <para/>
/// Just create a class inheriting from <see cref="RegisterCollectionBase"/>
/// and assert some propertys with the custom <see cref="RegisterAttribute"/>.
/// </summary>
/// <param name="collection">A collection inherting the <see cref="RegisterCollectionBase"/> class</param>
public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collection) {
collection.PLCInterface = this;
var props = collection.GetType().GetProperties();
foreach (var prop in props) {
var attributes = prop.GetCustomAttributes(true);
string propName = prop.Name;
foreach (var attr in attributes) {
if (attr is RegisterAttribute cAttribute) {
if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex == -1) {
if (cAttribute.SpecialAddress == SpecialAddress.None) {
AddRegister(collection.GetType(), cAttribute.MemoryArea, cAttribute.RegisterType, _name: propName);
} else {
AddRegister(collection.GetType(), cAttribute.SpecialAddress, cAttribute.RegisterType, _name: propName);
}
}
if (prop.PropertyType == typeof(short)) {
AddRegister<short>(collection.GetType(), cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(ushort)) {
AddRegister<ushort>(collection.GetType(), cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(int)) {
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(uint)) {
AddRegister<uint>(collection.GetType(), cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(float)) {
AddRegister<float>(collection.GetType(), cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(string)) {
AddRegister<string>(collection.GetType(), cAttribute.MemoryArea, cAttribute.StringLength, _name: propName);
}
if (prop.PropertyType.IsEnum) {
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, _name: propName, _enumType: prop.PropertyType);
}
//read number as bit array
if (prop.PropertyType == typeof(BitArray)) {
if (cAttribute.BitCount == BitCount.B16) {
AddRegister<short>(collection.GetType(), cAttribute.MemoryArea, _name: propName, _isBitwise: true);
} else {
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, _name: propName, _isBitwise: true);
}
}
//read number as bit array by invdividual properties
if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex != -1) {
//var bitwiseCount = Registers.Count(x => x.Value.isUsedBitwise);
if (cAttribute.BitCount == BitCount.B16) {
AddRegister<short>(collection.GetType(), cAttribute.MemoryArea, _name: $"Auto_Bitwise_DT{cAttribute.MemoryArea}", _isBitwise: true);
} else {
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, _name: $"Auto_Bitwise_DDT{cAttribute.MemoryArea}", _isBitwise: true);
}
}
if (prop.PropertyType == typeof(TimeSpan)) {
AddRegister<TimeSpan>(collection.GetType(), cAttribute.MemoryArea, _name: propName);
}
}
}
}
RegisterChanged += (reg) => {
//if the register is also used bitwise assign the boolean bit value to the according prop
if(reg.isUsedBitwise) {
for (int i = 0; i < props.Length; i++) {
var prop = props[i];
var bitWiseFound = prop.GetCustomAttributes(true)
.FirstOrDefault(y => y.GetType() == typeof(RegisterAttribute) && ((RegisterAttribute)y).MemoryArea == reg.MemoryAdress);
if(bitWiseFound != null && reg is NRegister<short> reg16) {
var casted = (RegisterAttribute)bitWiseFound;
var bitIndex = casted.AssignedBitIndex;
var bytes = BitConverter.GetBytes(reg16.Value);
BitArray bitAr = new BitArray(bytes);
if (bitIndex < bitAr.Length && bitIndex >= 0) {
prop.SetValue(collection, bitAr[bitIndex]);
collection.TriggerPropertyChanged(prop.Name);
}
} else if (bitWiseFound != null && reg is NRegister<int> reg32) {
var casted = (RegisterAttribute)bitWiseFound;
var bitIndex = casted.AssignedBitIndex;
var bytes = BitConverter.GetBytes(reg32.Value);
BitArray bitAr = new BitArray(bytes);
prop.SetValue(collection, bitAr[bitIndex]);
collection.TriggerPropertyChanged(prop.Name);
}
}
}
var foundToUpdate = props.FirstOrDefault(x => x.Name == reg.Name);
if (foundToUpdate != null) {
var foundAttributes = foundToUpdate.GetCustomAttributes(true);
var foundAttr = foundAttributes.FirstOrDefault(x => x.GetType() == typeof(RegisterAttribute));
if (foundAttr == null)
return;
var registerAttr = (RegisterAttribute)foundAttr;
//check if bit parse mode
if (registerAttr.AssignedBitIndex == -1) {
//setting back booleans
if (foundToUpdate.PropertyType == typeof(bool)) {
foundToUpdate.SetValue(collection, ((BRegister)reg).Value);
}
//setting back numbers
if (foundToUpdate.PropertyType == typeof(short)) {
foundToUpdate.SetValue(collection, ((NRegister<short>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(ushort)) {
foundToUpdate.SetValue(collection, ((NRegister<ushort>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(int)) {
foundToUpdate.SetValue(collection, ((NRegister<int>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(uint)) {
foundToUpdate.SetValue(collection, ((NRegister<uint>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(float)) {
foundToUpdate.SetValue(collection, ((NRegister<float>)reg).Value);
}
if (foundToUpdate.PropertyType.IsEnum) {
foundToUpdate.SetValue(collection, ((NRegister<int>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(TimeSpan)) {
foundToUpdate.SetValue(collection, ((NRegister<TimeSpan>)reg).Value);
}
//setting back strings
if (foundToUpdate.PropertyType == typeof(string)) {
foundToUpdate.SetValue(collection, ((SRegister)reg).Value);
}
}
if (foundToUpdate.PropertyType == typeof(BitArray)) {
//setting back bit registers
if (reg is NRegister<short> shortReg) {
var bytes = BitConverter.GetBytes(shortReg.Value);
BitArray bitAr = new BitArray(bytes);
foundToUpdate.SetValue(collection, bitAr);
}
if (reg is NRegister<int> intReg) {
var bytes = BitConverter.GetBytes(intReg.Value);
BitArray bitAr = new BitArray(bytes);
foundToUpdate.SetValue(collection, bitAr);
}
}
collection.TriggerPropertyChanged(foundToUpdate.Name);
}
};
if (collection != null)
collection.OnInterfaceLinked(this);
Connected += (i) => {
if (collection != null)
collection.OnInterfaceLinkedAndOnline(this);
};
return this;
}
#endregion
#region Register Writing
/// <summary>
/// Sets a register in the PLCs memory
/// </summary>
/// <param name="registerName">The name the register was given to or a property name from the RegisterCollection class</param>
/// <param name="value">The value to write to the register</param>
public void SetRegister (string registerName, object value) {
var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName);
if (foundRegister == null) {
throw new Exception($"Register with the name {registerName} was not found");
}
_ = SetRegisterAsync(registerName, value);
}
/// <summary>
/// Sets a register in the PLCs memory asynchronously, returns the result status from the PLC
/// </summary>
/// <param name="registerName">The name the register was given to or a property name from the RegisterCollection class</param>
/// <param name="value">The value to write to the register</param>
public async Task<bool> SetRegisterAsync (string registerName, object value) {
var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName);
if (foundRegister == null) {
throw new Exception($"Register with the name {registerName} was not found");
}
if (foundRegister.GetType() == typeof(BRegister)) {
return await WriteBoolRegister((BRegister)foundRegister, (bool)value);
}
if (foundRegister.GetType() == typeof(NRegister<short>)) {
return await WriteNumRegister((NRegister<short>)foundRegister, (short)value);
}
if (foundRegister.GetType() == typeof(NRegister<ushort>)) {
return await WriteNumRegister((NRegister<ushort>)foundRegister, (ushort)value);
}
if (foundRegister.GetType() == typeof(NRegister<int>)) {
return await WriteNumRegister((NRegister<int>)foundRegister, (int)value);
}
if (foundRegister.GetType() == typeof(NRegister<uint>)) {
return await WriteNumRegister((NRegister<uint>)foundRegister, (uint)value);
}
if (foundRegister.GetType() == typeof(NRegister<float>)) {
return await WriteNumRegister((NRegister<float>)foundRegister, (float)value);
}
if (foundRegister.GetType() == typeof(NRegister<TimeSpan>)) {
return await WriteNumRegister((NRegister<TimeSpan>)foundRegister, (TimeSpan)value);
}
if (foundRegister.GetType() == typeof(SRegister)) {
return await WriteStringRegister((SRegister)foundRegister, (string)value);
}
return false;
}
#endregion
#region Low level command handling
/// <summary>
/// Calculates checksum and sends a command to the PLC then awaits results
/// </summary>
/// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param>
/// <returns>Returns the result</returns>
public async Task<CommandResult> SendCommandAsync (string _msg) {
_msg = _msg.BuildBCCFrame();
_msg += "\r";
//send request
try {
queuedMessages++;
var response = await queue.Enqueue(() => SendSingleBlock(_msg));
queuedMessages--;
if (response == null) {
return new CommandResult {
Success = false,
Error = "0000",
ErrorDescription = "null result"
};
}
//error catching
Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase);
Match m = errorcheck.Match(response.ToString());
if (m.Success) {
string eCode = m.Groups[1].Value;
string eDes = Links.LinkedData.ErrorCodes[Convert.ToInt32(eCode)];
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Response is: {response}");
Logger.Log($"Error on command {_msg.Replace("\r", "")} the PLC returned error code: {eCode}, {eDes}", LogLevel.Error);
Console.ResetColor();
return new CommandResult {
Success = false,
Error = eCode,
ErrorDescription = eDes
};
}
return new CommandResult {
Success = true,
Error = "0000",
Response = response.ToString()
};
} catch {
return new CommandResult {
Success = false,
Error = "0000",
ErrorDescription = "null result"
};
}
}
private async Task<string> SendSingleBlock (string _blockString) {
if (client == null || !client.Connected ) {
await ConnectTCP();
}
if (client == null || !client.Connected)
return null;
var message = _blockString.ToHexASCIIBytes();
//time measuring
if(speedStopwatchUpstr == null) {
speedStopwatchUpstr = Stopwatch.StartNew();
}
if(speedStopwatchUpstr.Elapsed.TotalSeconds >= 1) {
speedStopwatchUpstr.Restart();
bytesTotalCountedUpstream = 0;
}
//send request
using (var sendStream = new MemoryStream(message)) {
await sendStream.CopyToAsync(stream);
Logger.Log($"[--------------------------------]", LogLevel.Critical, this);
Logger.Log($"--> OUT MSG: {_blockString}", LogLevel.Critical, this);
}
//calc upstream speed
bytesTotalCountedUpstream += message.Length;
var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000);
if (perSecUpstream <= 10000)
BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero);
//await result
StringBuilder response = new StringBuilder();
try {
byte[] responseBuffer = new byte[128 * 16];
bool endLineCode = false;
bool startMsgCode = false;
while (!endLineCode && !startMsgCode) {
do {
//time measuring
if (speedStopwatchDownstr == null) {
speedStopwatchDownstr = Stopwatch.StartNew();
}
if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) {
speedStopwatchDownstr.Restart();
bytesTotalCountedDownstream = 0;
}
int bytes = await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length);
endLineCode = responseBuffer.Any(x => x == 0x0D);
startMsgCode = responseBuffer.Count(x => x == 0x25) > 1;
if (!endLineCode && !startMsgCode) break;
response.Append(Encoding.UTF8.GetString(responseBuffer, 0, bytes));
}
while (stream.DataAvailable);
}
} catch (IOException) {
OnMajorSocketExceptionWhileConnected();
return null;
} catch (SocketException) {
OnMajorSocketExceptionWhileConnected();
return null;
}
if(!string.IsNullOrEmpty(response.ToString())) {
Logger.Log($"<-- IN MSG: {response}", LogLevel.Critical, this);
bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(response.ToString());
var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000);
if(perSecUpstream <= 10000)
BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero);
return response.ToString();
} else {
return null;
}
}
#endregion
#region Disposing
/// <summary>
/// Disposes the current interface and clears all its members
/// </summary>
public void Dispose () {
if (Disposed) return;
Disconnect();
GC.SuppressFinalize(this);
Disposed = true;
}
#endregion
#region Accessing Info
/// <summary>
/// Gets the connection info string
/// </summary>
public string GetConnectionPortInfo () {
return $"{IpAddress}:{Port}";
}
#endregion
}
}

View File

@@ -1,416 +0,0 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MewtocolNet.Registers;
using System.Linq;
using System.Globalization;
using MewtocolNet.Logging;
namespace MewtocolNet {
public partial class MewtocolInterface {
#region PLC info getters
/// <summary>
/// Gets generic information about the PLC
/// </summary>
/// <returns>A PLCInfo class</returns>
public async Task<PLCInfo> GetPLCInfoAsync () {
var resu = await SendCommandAsync("%01#RT");
if(!resu.Success) return null;
var reg = new Regex(@"\%([0-9]{2})\$RT([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{4})..", RegexOptions.IgnoreCase);
Match m = reg.Match(resu.Response);
if(m.Success) {
string station = m.Groups[1].Value;
string cpu = m.Groups[2].Value;
string version = m.Groups[3].Value;
string capacity = m.Groups[4].Value;
string operation = m.Groups[5].Value;
string errorflag = m.Groups[7].Value;
string error = m.Groups[8].Value;
PLCInfo retInfo = new PLCInfo {
CpuInformation = CpuInfo.BuildFromHexString(cpu, version, capacity),
OperationMode = PLCMode.BuildFromHex(operation),
ErrorCode = error,
StationNumber = int.Parse(station ?? "0"),
};
PlcInfo = retInfo;
return retInfo;
}
return null;
}
#endregion
#region Operation mode changing
/// <summary>
/// Changes the PLCs operation mode to the given one
/// </summary>
/// <param name="mode">The mode to change to</param>
/// <returns>The success state of the write operation</returns>
public async Task<bool> SetOperationMode (OPMode mode) {
string modeChar = mode == OPMode.Prog ? "P" : "R";
string requeststring = $"%{GetStationNumber()}#RM{modeChar}";
var result = await SendCommandAsync(requeststring);
if (result.Success) {
Logger.Log($"operation mode was changed to {mode}", LogLevel.Info, this);
} else {
Logger.Log("Operation mode change failed", LogLevel.Error, this);
}
return result.Success;
}
#endregion
#region Byte range writingv / reading to registers
/// <summary>
/// Writes a byte array to a span over multiple registers at once,
/// Rembember the plc can only store word so in order to write to a word array
/// your byte array should be double the size
/// </summary>
/// /// <param name="start">start address of the array</param>
/// <param name="byteArr"></param>
/// <returns></returns>
public async Task<bool> WriteByteRange (int start, byte[] byteArr) {
string byteString = byteArr.BigToMixedEndian().ToHexString();
var wordLength = byteArr.Length / 2;
if (byteArr.Length % 2 != 0)
wordLength++;
string startStr = start.ToString().PadLeft(5, '0');
string endStr = (start + wordLength - 1).ToString().PadLeft(5, '0');
string requeststring = $"%{GetStationNumber()}#WDD{startStr}{endStr}{byteString}";
var result = await SendCommandAsync(requeststring);
return result.Success;
}
/// <summary>
/// Reads the bytes from the start adress for counts byte length
/// </summary>
/// <param name="start">Start adress</param>
/// <param name="count">Number of bytes to get</param>
/// <param name="onProgress">Gets invoked when the progress changes, contains the progress as a double</param>
/// <returns>A byte array or null of there was an error</returns>
public async Task<byte[]> ReadByteRange (int start, int count, Action<double> onProgress = null) {
var byteList = new List<byte>();
var wordLength = count / 2;
if (count % 2 != 0)
wordLength++;
//read blocks of max 4 words per msg
for (int i = 0; i < wordLength; i+=8) {
int curWordStart = start + i;
int curWordEnd = curWordStart + 7;
string startStr = curWordStart.ToString().PadLeft(5, '0');
string endStr = (curWordEnd).ToString().PadLeft(5, '0');
string requeststring = $"%{GetStationNumber()}#RDD{startStr}{endStr}";
var result = await SendCommandAsync(requeststring);
if (result.Success && !string.IsNullOrEmpty(result.Response)) {
var bytes = result.Response.ParseDTByteString(8 * 4).HexStringToByteArray();
if (bytes == null) {
return null;
}
byteList.AddRange(bytes.BigToMixedEndian().Take(count).ToArray());
}
if(onProgress != null)
onProgress((double)i / wordLength);
}
return byteList.ToArray();
}
#endregion
#region Bool register reading / writing
/// <summary>
/// Reads the given boolean register from the PLC
/// </summary>
/// <param name="_toRead">The register to read</param>
public async Task<BRegisterResult> ReadBoolRegister (BRegister _toRead) {
string requeststring = $"%{GetStationNumber()}#RCS{_toRead.BuildMewtocolIdent()}";
var result = await SendCommandAsync(requeststring);
if(!result.Success) {
return new BRegisterResult {
Result = result,
Register = _toRead
};
}
var resultBool = result.Response.ParseRCSingleBit();
if(resultBool != null) {
_toRead.SetValueFromPLC(resultBool.Value);
}
var finalRes = new BRegisterResult {
Result = result,
Register = _toRead
};
return finalRes;
}
/// <summary>
/// Writes to the given bool register on the PLC
/// </summary>
/// <param name="_toWrite">The register to write to</param>
/// <param name="value">The value to write</param>
/// <returns>The success state of the write operation</returns>
public async Task<bool> WriteBoolRegister (BRegister _toWrite, bool value) {
string requeststring = $"%{GetStationNumber()}#WCS{_toWrite.BuildMewtocolIdent()}{(value ? "1" : "0")}";
var result = await SendCommandAsync(requeststring);
return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WC");
}
#endregion
#region Number register reading / writing
/// <summary>
/// Reads the given numeric register from the PLC
/// </summary>
/// <typeparam name="T">Type of number (short, ushort, int, uint, float)</typeparam>
/// <param name="_toRead">The register to read</param>
/// <returns>A result with the given NumberRegister containing the readback value and a result struct</returns>
public async Task<NRegisterResult<T>> ReadNumRegister<T> (NRegister<T> _toRead) {
Type numType = typeof(T);
string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolIdent()}";
var result = await SendCommandAsync(requeststring);
var failedResult = new NRegisterResult<T> {
Result = result,
Register = _toRead
};
if (!result.Success || string.IsNullOrEmpty(result.Response)) {
return failedResult;
}
if (numType == typeof(short)) {
var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder();
if (resultBytes == null) return failedResult;
var val = short.Parse(resultBytes, NumberStyles.HexNumber);
_toRead.SetValueFromPLC(val);
} else if (numType == typeof(ushort)) {
var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder();
if (resultBytes == null) return failedResult;
var val = ushort.Parse(resultBytes, NumberStyles.HexNumber);
_toRead.SetValueFromPLC(val);
} else if (numType == typeof(int)) {
var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder();
if (resultBytes == null) return failedResult;
var val = int.Parse(resultBytes, NumberStyles.HexNumber);
_toRead.SetValueFromPLC(val);
} else if (numType == typeof(uint)) {
var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder();
if (resultBytes == null) return failedResult;
var val = uint.Parse(resultBytes, NumberStyles.HexNumber);
_toRead.SetValueFromPLC(val);
} else if (numType == typeof(float)) {
var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder();
if (resultBytes == null) return failedResult;
//convert to unsigned int first
var val = uint.Parse(resultBytes, NumberStyles.HexNumber);
byte[] floatVals = BitConverter.GetBytes(val);
float finalFloat = BitConverter.ToSingle(floatVals, 0);
_toRead.SetValueFromPLC(finalFloat);
} else if (numType == typeof(TimeSpan)) {
var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder();
if (resultBytes == null) return failedResult;
//convert to unsigned int first
var vallong = long.Parse(resultBytes, NumberStyles.HexNumber);
var valMillis = vallong * 10;
var ts = TimeSpan.FromMilliseconds(valMillis);
//minmax writable / readable value is 10ms
_toRead.SetValueFromPLC(ts);
}
var finalRes = new NRegisterResult<T> {
Result = result,
Register = _toRead
};
return finalRes;
}
/// <summary>
/// Reads the given numeric register from the PLC
/// </summary>
/// <typeparam name="T">Type of number (short, ushort, int, uint, float)</typeparam>
/// <param name="_toWrite">The register to write</param>
/// <param name="_value">The value to write</param>
/// <returns>The success state of the write operation</returns>
public async Task<bool> WriteNumRegister<T> (NRegister<T> _toWrite, T _value) {
byte[] toWriteVal;
Type numType = typeof(T);
if (numType == typeof(short)) {
toWriteVal = BitConverter.GetBytes(Convert.ToInt16(_value));
} else if (numType == typeof(ushort)) {
toWriteVal = BitConverter.GetBytes(Convert.ToUInt16(_value));
} else if (numType == typeof(int)) {
toWriteVal = BitConverter.GetBytes(Convert.ToInt32(_value));
} else if (numType == typeof(uint)) {
toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value));
} else if (numType == typeof(float)) {
var fl = _value as float?;
if (fl == null)
throw new NullReferenceException("Float cannot be null");
toWriteVal = BitConverter.GetBytes(fl.Value);
} else if (numType == typeof(TimeSpan)) {
var fl = _value as TimeSpan?;
if (fl == null)
throw new NullReferenceException("Timespan cannot be null");
var tLong = (uint)(fl.Value.TotalMilliseconds / 10);
toWriteVal = BitConverter.GetBytes(tLong);
} else {
toWriteVal = null;
}
string requeststring = $"%{GetStationNumber()}#WD{_toWrite.BuildMewtocolIdent()}{toWriteVal.ToHexString()}";
var result = await SendCommandAsync(requeststring);
return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WD");
}
#endregion
#region String register reading / writing
//string is build up like this
//04 00 04 00 53 50 33 35 13
//0, 1 = reserved size
//1, 2 = current size
//3,4,5,6 = ASCII encoded chars (SP35)
//7,8 = checksum
/// <summary>
/// Reads back the value of a string register
/// </summary>
/// <param name="_toRead">The register to read</param>
/// <param name="_stationNumber">The station number of the PLC</param>
/// <returns></returns>
public async Task<SRegisterResult> ReadStringRegister (SRegister _toRead, int _stationNumber = 1) {
string requeststring = $"%{GetStationNumber()}#RD{_toRead.BuildMewtocolIdent()}";
var result = await SendCommandAsync(requeststring);
if (result.Success)
_toRead.SetValueFromPLC(result.Response.ParseDTString());
return new SRegisterResult {
Result = result,
Register = _toRead
};
}
/// <summary>
/// Writes a string to a string register
/// </summary>
/// <param name="_toWrite">The register to write</param>
/// <param name="_value">The value to write, if the strings length is longer than the cap size it gets trimmed to the max char length</param>
/// <param name="_stationNumber">The station number of the PLC</param>
/// <returns>The success state of the write operation</returns>
public async Task<bool> WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) {
if (_value == null) _value = "";
if(_value.Length > _toWrite.ReservedSize) {
throw new ArgumentException("Write string size cannot be longer than reserved string size");
}
string stationNum = GetStationNumber();
string dataString = _value.BuildDTString(_toWrite.ReservedSize);
string dataArea = _toWrite.BuildCustomIdent(dataString.Length / 4);
string requeststring = $"%{stationNum}#WD{dataArea}{dataString}";
var result = await SendCommandAsync(requeststring);
return result.Success && result.Response.StartsWith($"%{ GetStationNumber()}$WD");
}
#endregion
#region Helpers
internal string GetStationNumber () {
return StationNumber.ToString().PadLeft(2, '0');
}
#endregion
}
}

View File

@@ -1,46 +0,0 @@
namespace MewtocolNet {
/// <summary>
/// CPU type of the PLC
/// </summary>
public enum CpuType {
/// <summary>
/// FP 0 / FP 2.7K
/// </summary>
FP0_FP1_2_7K,
/// <summary>
/// FP0 / FP1, 5K / 10K
/// </summary>
FP0_FP1_5K_10K,
/// <summary>
/// FP1 M 0.9K
/// </summary>
FP1_M_0_9K,
/// <summary>
/// FP2 16k / 32k
/// </summary>
FP2_16K_32K,
/// <summary>
/// FP3 C 10K
/// </summary>
FP3_C_10K,
/// <summary>
/// FP3 C 16K
/// </summary>
FP3_C_16K,
/// <summary>
/// FP5 16K
/// </summary>
FP5_16K,
/// <summary>
/// FP 5 24K
/// </summary>
FP5_24K,
/// <summary>
/// Includes panasonic FPX, FPX-H, Sigma
/// </summary>
FP_Sigma_X_H_30K_60K_120K
}
}

View File

@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet {
/// <summary>
/// CPU type of the PLC
/// </summary>
public enum OPMode {
/// <summary>
/// PLC run mode
/// </summary>
Run,
/// <summary>
/// PLC programming mode
/// </summary>
Prog,
}
}

View File

@@ -1,43 +0,0 @@
namespace MewtocolNet.Registers {
/// <summary>
/// Contains generic information about the plc
/// </summary>
public class PLCInfo {
/// <summary>
/// Contains information about the PLCs cpu
/// </summary>
public CpuInfo CpuInformation {get;set;}
/// <summary>
/// Contains information about the PLCs operation modes
/// </summary>
public PLCMode OperationMode {get;set;}
/// <summary>
/// Current error code of the PLC
/// </summary>
public string ErrorCode {get;set;}
/// <summary>
/// Current station number of the PLC
/// </summary>
public int StationNumber { get;set;}
/// <summary>
/// Generates a string containing some of the most important informations
/// </summary>
/// <returns></returns>
public override string ToString () {
return $"Type: {CpuInformation.Cputype},\n" +
$"Capacity: {CpuInformation.ProgramCapacity}k\n" +
$"CPU v: {CpuInformation.CpuVersion}\n" +
$"Station Num: {StationNumber}\n" +
$"--------------------------------\n" +
$"OP Mode: {(OperationMode.RunMode ? "Run" : "Prog")}\n" +
$"Error Code: {ErrorCode}";
}
}
}

View File

@@ -1,92 +0,0 @@
using System;
namespace MewtocolNet.Registers {
/// <summary>
/// All modes
/// </summary>
public class PLCMode {
/// <summary>
/// PLC is running
/// </summary>
public bool RunMode { get; set; }
/// <summary>
/// PLC is in test
/// </summary>
public bool TestRunMode { get; set; }
/// <summary>
/// BreakExcecuting
/// </summary>
public bool BreakExcecuting { get; set; }
/// <summary>
/// BreakValid
/// </summary>
public bool BreakValid { get; set; }
/// <summary>
/// PLC output is enabled
/// </summary>
public bool OutputEnabled { get; set; }
/// <summary>
/// PLC runs step per step
/// </summary>
public bool StepRunMode { get; set; }
/// <summary>
/// Message executing
/// </summary>
public bool MessageExecuting { get; set; }
/// <summary>
/// PLC is in remote mode
/// </summary>
public bool RemoteMode { get; set; }
/// <summary>
/// Gets operation mode from 2 digit hex number
/// </summary>
internal static PLCMode BuildFromHex (string _hexString) {
string lower = Convert.ToString(Convert.ToInt32(_hexString.Substring(0, 1)), 2).PadLeft(4, '0');
string higher = Convert.ToString(Convert.ToInt32(_hexString.Substring(1, 1)), 2).PadLeft(4, '0');
string combined = lower + higher;
var retMode = new PLCMode();
for (int i = 0; i < 8; i++) {
char digit = combined[i];
bool state = false;
if (digit.ToString() == "1")
state = true;
switch (i) {
case 0:
retMode.RunMode = state;
break;
case 1:
retMode.TestRunMode = state;
break;
case 2:
retMode.BreakExcecuting = state;
break;
case 3:
retMode.BreakValid = state;
break;
case 4:
retMode.OutputEnabled = state;
break;
case 5:
retMode.StepRunMode = state;
break;
case 6:
retMode.MessageExecuting = state;
break;
case 7:
retMode.RemoteMode = state;
break;
}
}
return retMode;
}
}
}

View File

@@ -1,118 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet.RegisterAttributes {
/// <summary>
/// The size of the bitwise register
/// </summary>
public enum BitCount {
/// <summary>
/// 16 bit
/// </summary>
B16,
/// <summary>
/// 32 bit
/// </summary>
B32
}
/// <summary>
/// Defines the behavior of a register property
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class RegisterAttribute : Attribute {
internal int MemoryArea;
internal int StringLength;
internal RegisterType RegisterType;
internal SpecialAddress SpecialAddress = SpecialAddress.None;
internal BitCount BitCount;
internal int AssignedBitIndex = -1;
/// <summary>
/// Attribute for string type or numeric registers
/// </summary>
/// <param name="memoryArea">The area in the plcs memory</param>
/// <param name="stringLength">The max string length in the plc</param>
public RegisterAttribute (int memoryArea, int stringLength = 1) {
MemoryArea = memoryArea;
StringLength = stringLength;
}
/// <summary>
/// Attribute for boolean registers
/// </summary>
/// <param name="memoryArea">The area in the plcs memory</param>
/// <param name="type">The type of boolean register</param>
public RegisterAttribute (int memoryArea, RegisterType type) {
if (type.ToString().StartsWith("DT"))
throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically");
MemoryArea = memoryArea;
RegisterType = type;
SpecialAddress = SpecialAddress.None;
}
/// <summary>
/// Attribute for boolean registers
/// </summary>
/// <param name="spAdress">The special area in the plcs memory</param>
/// <param name="type">The type of boolean register</param>
public RegisterAttribute (RegisterType type, SpecialAddress spAdress) {
if (type.ToString().StartsWith("DT"))
throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically");
RegisterType = type;
SpecialAddress = spAdress;
}
/// <summary>
/// Attribute to read numeric registers as bitwise
/// </summary>
/// <param name="memoryArea">The area in the plcs memory</param>
/// <param name="bitcount">The number of bits to parse</param>
public RegisterAttribute (int memoryArea, BitCount bitcount) {
MemoryArea = memoryArea;
StringLength = 0;
BitCount = bitcount;
}
/// <summary>
/// Attribute to read numeric registers as bitwise
/// </summary>
/// <param name="memoryArea">The area in the plcs memory</param>
/// <param name="bitcount">The number of bits to parse</param>
/// <param name="assignBit">The index of the bit that gets linked to the bool</param>
public RegisterAttribute (int memoryArea, uint assignBit, BitCount bitcount) {
if(assignBit > 15 && bitcount == BitCount.B16) {
throw new NotSupportedException("The assignBit parameter cannot be greater than 15 in a 16 bit var");
}
if (assignBit > 31 && bitcount == BitCount.B32) {
throw new NotSupportedException("The assignBit parameter cannot be greater than 31 in a 32 bit var");
}
MemoryArea = memoryArea;
StringLength = 0;
BitCount = bitcount;
AssignedBitIndex = (int)assignBit;
}
}
}

View File

@@ -1,71 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet {
/// <summary>
/// The special register type
/// </summary>
public enum RegisterType {
/// <summary>
/// Physical input as a bool (Relay)
/// </summary>
X,
/// <summary>
/// Physical output as a bool (Relay)
/// </summary>
Y,
/// <summary>
/// Internal as a bool (Relay)
/// </summary>
R,
/// <summary>
/// Data area as a short (Register)
/// </summary>
DT_short,
/// <summary>
/// Data area as an unsigned short (Register)
/// </summary>
DT_ushort,
/// <summary>
/// Double data area as an integer (Register)
/// </summary>
DDT_int,
/// <summary>
/// Double data area as an unsigned integer (Register)
/// </summary>
DDT_uint,
/// <summary>
/// Double data area as an floating point number (Register)
/// </summary>
DDT_float,
}
/// <summary>
/// The special input / output channel address
/// </summary>
public enum SpecialAddress {
#pragma warning disable CS1591
/// <summary>
/// No defined
/// </summary>
None,
A = -10,
B = -11,
C = -12,
D = -13,
E = -14,
F = -15,
#pragma warning restore
}
}

View File

@@ -1,33 +0,0 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace MewtocolNet.Registers {
/// <summary>
/// The formatted result of a ascii command
/// </summary>
public struct CommandResult {
/// <summary>
/// Success state of the message
/// </summary>
public bool Success {get;set;}
/// <summary>
/// Response text of the message
/// </summary>
public string Response {get;set;}
/// <summary>
/// Error code of the message
/// </summary>
public string Error {get;set;}
/// <summary>
/// Error text of the message
/// </summary>
public string ErrorDescription {get;set;}
}
}

View File

@@ -1,81 +0,0 @@
using System;
using System.Text;
using MewtocolNet;
namespace MewtocolNet.Registers {
/// <summary>
/// Defines a register containing a boolean
/// </summary>
public class BRegister : Register {
internal RegisterType RegType { get; private set; }
internal SpecialAddress SpecialAddress { get; private set; }
internal bool LastValue;
/// <summary>
/// The value of the register
/// </summary>
public bool Value => LastValue;
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <param name="_address">Memory start adress max 99999</param>
/// <param name="_type">Type of boolean register</param>
/// <param name="_name">Name of the register</param>
public BRegister (int _address, RegisterType _type = RegisterType.R, string _name = null) {
if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999");
memoryAdress = _address;
name = _name;
RegType = _type;
}
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <param name="_address">Memory start adress max 99999</param>
/// <param name="_type">Type of boolean register</param>
/// <param name="_name">Name of the register</param>
public BRegister (SpecialAddress _address, RegisterType _type = RegisterType.R, string _name = null) {
if (_address == SpecialAddress.None)
throw new NotSupportedException("Special adress cant be none");
SpecialAddress = _address;
name = _name;
RegType = _type;
}
/// <summary>
/// Builds the register area name
/// </summary>
public override string BuildMewtocolIdent () {
//build area code from register type
StringBuilder asciistring = new StringBuilder(RegType.ToString());
if(SpecialAddress == SpecialAddress.None) {
asciistring.Append(MemoryAdress.ToString().PadLeft(4, '0'));
} else {
asciistring.Append(SpecialAddress.ToString().PadLeft(4, '0'));
}
return asciistring.ToString();
}
internal void SetValueFromPLC (bool val) {
LastValue = val;
TriggerChangedEvnt(this);
TriggerNotifyChange();
}
}
}

View File

@@ -1,20 +0,0 @@
namespace MewtocolNet.Registers {
/// <summary>
/// Result for a boolean register
/// </summary>
public class BRegisterResult {
/// <summary>
/// The command result
/// </summary>
public CommandResult Result { get; set; }
/// <summary>
/// The used register
/// </summary>
public BRegister Register { get; set; }
}
}

View File

@@ -1,85 +0,0 @@
using System;
namespace MewtocolNet.Registers {
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <typeparam name="T">The type of the numeric value</typeparam>
public class NRegister<T> : Register {
internal T LastValue;
/// <summary>
/// The value of the register
/// </summary>
public T Value => LastValue;
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <param name="_adress">Memory start adress max 99999</param>
/// <param name="_name">Name of the register</param>
public NRegister (int _adress, string _name = null) {
if (_adress > 99999)
throw new NotSupportedException("Memory adresses cant be greater than 99999");
memoryAdress = _adress;
name = _name;
Type numType = typeof(T);
if (numType == typeof(short)) {
memoryLength = 0;
} else if (numType == typeof(ushort)) {
memoryLength = 0;
} else if (numType == typeof(int)) {
memoryLength = 1;
} else if (numType == typeof(uint)) {
memoryLength = 1;
} else if (numType == typeof(float)) {
memoryLength = 1;
} else if (numType == typeof(TimeSpan)) {
memoryLength = 1;
} else {
throw new NotSupportedException($"The type {numType} is not allowed for Number Registers");
}
}
internal NRegister(int _adress, string _name = null, bool isBitwise = false, Type _enumType = null) {
if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999");
memoryAdress = _adress;
name = _name;
Type numType = typeof(T);
if (numType == typeof(short)) {
memoryLength = 0;
} else if (numType == typeof(ushort)) {
memoryLength = 0;
} else if (numType == typeof(int)) {
memoryLength = 1;
} else if (numType == typeof(uint)) {
memoryLength = 1;
} else if (numType == typeof(float)) {
memoryLength = 1;
} else if (numType == typeof(TimeSpan)) {
memoryLength = 1;
} else {
throw new NotSupportedException($"The type {numType} is not allowed for Number Registers");
}
isUsedBitwise = isBitwise;
enumType = _enumType;
}
internal void SetValueFromPLC (object val) {
LastValue = (T)val;
TriggerChangedEvnt(this);
TriggerNotifyChange();
}
}
}

Some files were not shown because too many files have changed in this diff Show More