mirror of
https://github.com/OpenLogics/MewtocolNet.git
synced 2025-12-06 11:11:23 +00:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e2743603c | ||
|
|
6685258052 | ||
|
|
99fa6a7eca | ||
|
|
19f6d44a43 | ||
|
|
18d5118b43 | ||
|
|
48b04dfb7f | ||
|
|
9ca31fe748 | ||
|
|
3bc790238e | ||
|
|
df0de8830e | ||
|
|
19db0a9daa | ||
|
|
6dd2a1688a | ||
|
|
8bd1ce6b4d | ||
|
|
79d6f7fa54 | ||
|
|
a58e60d4f9 | ||
|
|
c6bddc61fb | ||
|
|
300d84daf2 | ||
|
|
78b12afbc3 | ||
|
|
30fec39731 | ||
|
|
372043f18e | ||
|
|
5e8baaddfd | ||
|
|
97b1a665d7 | ||
|
|
2afcea2767 | ||
|
|
5d4ef322ee | ||
|
|
590a3a3723 | ||
|
|
19944f9fd9 | ||
|
|
993c3cba7e | ||
|
|
47e50078b0 | ||
|
|
42d6ee607d | ||
|
|
bd907b80df | ||
|
|
b17fcfeff9 | ||
|
|
b3909e3f4b | ||
|
|
40016a5fa5 | ||
|
|
4551d24128 | ||
|
|
aed1690a7c | ||
|
|
806b45beef | ||
|
|
5b4a9966b1 | ||
|
|
28752f4396 | ||
|
|
8e5051ed83 | ||
|
|
938918e4df | ||
|
|
642aed666c | ||
|
|
a8ee01dfd7 | ||
|
|
a0e54bf8e1 | ||
|
|
eca2798859 | ||
|
|
7677ff5015 | ||
|
|
aff5ca9fda | ||
|
|
5ff29e3ffc | ||
|
|
dba8de159b | ||
|
|
5221664071 | ||
|
|
81676ec2e5 | ||
|
|
7f3db93f94 | ||
|
|
0668ad2f86 | ||
|
|
354c4d6428 | ||
|
|
a00f56074f | ||
|
|
4fb9910d54 | ||
|
|
111eacb785 | ||
|
|
a06a69be3f | ||
|
|
5ed2580bb6 | ||
|
|
c41b0cef16 | ||
|
|
ec78fad881 | ||
|
|
82d9aa8b16 | ||
|
|
74cb3bd59f | ||
|
|
b62e0b06e6 | ||
|
|
e5020253c9 | ||
|
|
aa8441fa33 | ||
|
|
b64a82d5bb | ||
|
|
3f3e3abe15 | ||
|
|
f4af3dd05e | ||
|
|
d695108fd4 | ||
|
|
38b83affd7 | ||
|
|
1f33ebb8d3 | ||
|
|
4d6eee5585 | ||
|
|
d24201b5d9 | ||
|
|
bf78156a9d | ||
|
|
eec1479406 | ||
|
|
fa2762c082 | ||
|
|
9d3e895c3b | ||
|
|
c1ce66c253 | ||
|
|
a399e0fa16 | ||
|
|
9bcffad77b | ||
|
|
3a7b787949 | ||
|
|
8fdff367f6 | ||
|
|
eb70dac5a8 | ||
|
|
d6c00097bc | ||
|
|
4666d3071b | ||
|
|
32c20e7360 | ||
|
|
daecd73a6d | ||
|
|
82821e773d | ||
|
|
6c7e91f648 | ||
|
|
fbd53c850f | ||
|
|
d8c18bbf34 | ||
|
|
88cdc1a36c | ||
|
|
864389a4ad | ||
|
|
5d6cef91ec | ||
|
|
2615f8c428 | ||
|
|
03341d3663 | ||
|
|
6c324bcdff | ||
|
|
befadb67a5 | ||
|
|
6cb45ac27d | ||
|
|
fcb62960ec | ||
|
|
fd85a87f5d | ||
|
|
bd6a6cf177 | ||
|
|
638b173847 | ||
|
|
aa282fe156 | ||
|
|
fe3c849934 | ||
|
|
953e22503a | ||
|
|
d956525538 | ||
|
|
616d102dea | ||
|
|
6d3b5adf7d | ||
|
|
a0da9e77fe | ||
|
|
ab7f5ca302 | ||
|
|
f338acfe8a | ||
|
|
8c2ba1f68f | ||
|
|
c332cd9f86 | ||
|
|
15cc2e245d | ||
|
|
bc765b870a | ||
|
|
a9bd2962b4 | ||
|
|
7be52efb7e | ||
|
|
b48f86d23d | ||
|
|
f5f1e3bea9 | ||
|
|
88ad175145 | ||
|
|
ebee8a0623 | ||
|
|
50a11a3497 | ||
|
|
2f9c86d1a4 | ||
|
|
62bb1b422a | ||
|
|
431c5895f0 | ||
|
|
ebde5421ab | ||
|
|
c1678ae6b4 | ||
|
|
e2f53f9c4a | ||
|
|
844fcfc76d | ||
|
|
111482ec2d | ||
|
|
b72771c13e | ||
|
|
caa138c130 | ||
|
|
e3b6fdf780 | ||
|
|
5dfa48a89a | ||
|
|
128b9f9705 | ||
|
|
69d728f345 | ||
|
|
c01204ce57 | ||
|
|
da2cb6b58a | ||
|
|
96827f3baf | ||
|
|
2df944da8c | ||
|
|
f5e6d7d6af | ||
|
|
ff3cc41aea | ||
|
|
d5b701186e | ||
|
|
0702ab7d0a | ||
|
|
5adadbfc46 | ||
|
|
739f83bf0c | ||
|
|
a9bd792af1 | ||
|
|
e6ffc92db6 | ||
|
|
7bd07bb520 | ||
|
|
f99af9511b | ||
|
|
b13b2d0199 | ||
|
|
364e0637bb | ||
|
|
bba90106cb | ||
|
|
3dea18d285 | ||
|
|
ad61361008 | ||
|
|
0e0659000a | ||
|
|
8cc47b496a | ||
|
|
9eb09fc7ec | ||
|
|
9b8fca6561 | ||
|
|
6205f81931 | ||
|
|
a8960f12c8 | ||
|
|
0b670b1a27 | ||
|
|
63a1348704 | ||
|
|
61fe2aff65 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||||
101
.github/workflows/build-pipeline.yml
vendored
101
.github/workflows/build-pipeline.yml
vendored
@@ -1,101 +0,0 @@
|
|||||||
name: Build pipeline
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
#Check the online status of the test PLCs first
|
|
||||||
check-plcs-online:
|
|
||||||
name: 'Test PLC online status'
|
|
||||||
runs-on: [self-hosted, linux, x64, womed-local-linux]
|
|
||||||
steps:
|
|
||||||
- name: 'Ping FPX-H-C30T'
|
|
||||||
run: ping 192.168.115.210 -w 5
|
|
||||||
- name: 'Ping FPX-H-C14R'
|
|
||||||
run: ping 192.168.115.212 -w 5
|
|
||||||
- name: 'Ping FPX-C30T'
|
|
||||||
run: ping 192.168.115.213 -w 5
|
|
||||||
|
|
||||||
#Run unit tests on the test PLCs
|
|
||||||
run-unit-tests:
|
|
||||||
name: 'Run unit tests'
|
|
||||||
needs: check-plcs-online
|
|
||||||
runs-on: [self-hosted, linux, x64, womed-local-linux]
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
dotnet-version: [ '6.0.x' ]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: 'Setup dotnet ${{ matrix.dotnet-version }}'
|
|
||||||
uses: actions/setup-dotnet@v2
|
|
||||||
with:
|
|
||||||
dotnet-version: ${{ matrix.dotnet-version }}
|
|
||||||
- name: 'Run tests'
|
|
||||||
run: |
|
|
||||||
cd '${{ github.workspace }}/MewtocolTests'
|
|
||||||
dotnet test --logger "trx;logfilename=testresults.trx"
|
|
||||||
#Generate a test report
|
|
||||||
- name: 'Generate Test Report'
|
|
||||||
uses: dorny/test-reporter@v1
|
|
||||||
if: success() || failure() # run this step even if previous step failed
|
|
||||||
with:
|
|
||||||
name: Unit Test Report # Name of the check run which will be created
|
|
||||||
path: '${{ github.workspace }}/MewtocolTests/TestResults/testresults.trx' # Path to test results
|
|
||||||
reporter: dotnet-trx # Format of test results
|
|
||||||
|
|
||||||
#Run code ql check
|
|
||||||
#code-ql-check:
|
|
||||||
# uses: ./.github/workflows/codeql.yml
|
|
||||||
|
|
||||||
#Publish to nuget if version tag change detected
|
|
||||||
publish-and-push-nuget:
|
|
||||||
name: 'Build and publish to NuGet'
|
|
||||||
needs: [run-unit-tests, ]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
# Publish
|
|
||||||
- name: publish on version change
|
|
||||||
id: publish_nuget
|
|
||||||
uses: brandedoutcast/publish-nuget@v2
|
|
||||||
with:
|
|
||||||
# Filepath of the project to be packaged, relative to root of repository
|
|
||||||
PROJECT_FILE_PATH: MewtocolNet/MewtocolNet.csproj
|
|
||||||
|
|
||||||
# Configuration to build and package
|
|
||||||
# BUILD_CONFIGURATION: Release
|
|
||||||
|
|
||||||
# Platform target to compile (default is empty/AnyCPU)
|
|
||||||
# BUILD_PLATFORM: x64
|
|
||||||
|
|
||||||
# NuGet package id, used for version detection & defaults to project name
|
|
||||||
PACKAGE_NAME: Mewtocol.NET
|
|
||||||
|
|
||||||
# Filepath with version info, relative to root of repository & defaults to PROJECT_FILE_PATH
|
|
||||||
# VERSION_FILE_PATH: Directory.Build.props
|
|
||||||
|
|
||||||
# Regex pattern to extract version info in a capturing group
|
|
||||||
# VERSION_REGEX: ^\s*<Version>(.*)<\/Version>\s*$
|
|
||||||
|
|
||||||
# Useful with external providers like Nerdbank.GitVersioning, ignores VERSION_FILE_PATH & VERSION_REGEX
|
|
||||||
# VERSION_STATIC: 1.0.0
|
|
||||||
|
|
||||||
# Flag to toggle git tagging, enabled by default
|
|
||||||
# TAG_COMMIT: true
|
|
||||||
|
|
||||||
# Format of the git tag, [*] gets replaced with actual version
|
|
||||||
# TAG_FORMAT: v*
|
|
||||||
|
|
||||||
# API key to authenticate with NuGet server
|
|
||||||
NUGET_KEY: ${{secrets.NUGET_KEY}}
|
|
||||||
|
|
||||||
# NuGet server uri hosting the packages, defaults to https://api.nuget.org
|
|
||||||
# NUGET_SOURCE: https://api.nuget.org
|
|
||||||
|
|
||||||
# Flag to toggle pushing symbols along with nuget package to the server, disabled by default
|
|
||||||
# INCLUDE_SYMBOLS: false
|
|
||||||
178
.github/workflows/publish-pipeline.yml
vendored
Normal file
178
.github/workflows/publish-pipeline.yml
vendored
Normal 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
|
||||||
111
.github/workflows/test-pipeline.yml
vendored
Normal file
111
.github/workflows/test-pipeline.yml
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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:
|
||||||
|
|
||||||
|
- name: 'Setup dotnet'
|
||||||
|
uses: actions/setup-dotnet@v2
|
||||||
|
with:
|
||||||
|
dotnet-version: |
|
||||||
|
6.0.x
|
||||||
|
7.0.x
|
||||||
|
|
||||||
|
- name: 'Checkout'
|
||||||
|
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: '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
|
||||||
|
|
||||||
17
AutoTools.ChmDataExtract/AutoTools.ChmDataExtract.csproj
Normal file
17
AutoTools.ChmDataExtract/AutoTools.ChmDataExtract.csproj
Normal 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>
|
||||||
510
AutoTools.ChmDataExtract/Program.cs
Normal file
510
AutoTools.ChmDataExtract/Program.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
AutoTools.DocBuilder/AutoTools.DocBuilder.csproj
Normal file
16
AutoTools.DocBuilder/AutoTools.DocBuilder.csproj
Normal 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>
|
||||||
2
AutoTools.DocBuilder/Docs/program-read-write.md
Normal file
2
AutoTools.DocBuilder/Docs/program-read-write.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Reading and writing programs
|
||||||
|
|
||||||
291
AutoTools.DocBuilder/Docs/reverse_engineering_data.md
Normal file
291
AutoTools.DocBuilder/Docs/reverse_engineering_data.md
Normal 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 |
|
||||||
16
AutoTools.DocBuilder/Docs/system-registers.md
Normal file
16
AutoTools.DocBuilder/Docs/system-registers.md
Normal 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|
|
||||||
152
AutoTools.DocBuilder/Program.cs
Normal file
152
AutoTools.DocBuilder/Program.cs
Normal 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());
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
using MewtocolNet.Logging;
|
|
||||||
using MewtocolNet;
|
|
||||||
using System;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
namespace Examples;
|
|
||||||
|
|
||||||
public class ExampleScenarios {
|
|
||||||
|
|
||||||
public static bool MewtocolLoggerEnabled = false;
|
|
||||||
|
|
||||||
public void SetupLogger () {
|
|
||||||
|
|
||||||
//attaching the logger
|
|
||||||
Logger.LogLevel = LogLevel.Verbose;
|
|
||||||
Logger.OnNewLogMessage((date, msg) => {
|
|
||||||
if(MewtocolLoggerEnabled)
|
|
||||||
Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}");
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Scenario("Permament connection with poller")]
|
|
||||||
public async Task RunCyclicPollerAsync () {
|
|
||||||
|
|
||||||
Console.WriteLine("Starting poller scenario");
|
|
||||||
|
|
||||||
int runTime = 10000;
|
|
||||||
int remainingTime = runTime;
|
|
||||||
|
|
||||||
//setting up a new PLC interface and register collection
|
|
||||||
MewtocolInterface interf = new MewtocolInterface("192.168.115.210");
|
|
||||||
TestRegisters registers = new TestRegisters();
|
|
||||||
|
|
||||||
//attaching the register collection and an automatic poller
|
|
||||||
interf.WithRegisterCollection(registers).WithPoller();
|
|
||||||
|
|
||||||
await interf.ConnectAsync();
|
|
||||||
|
|
||||||
_ = Task.Factory.StartNew(async () => {
|
|
||||||
|
|
||||||
while (interf.IsConnected) {
|
|
||||||
|
|
||||||
//flip the bool register each tick and wait for it to be registered
|
|
||||||
await interf.SetRegisterAsync(nameof(registers.TestBool1), !registers.TestBool1);
|
|
||||||
|
|
||||||
Console.Title = $"Polling Paused: {interf.PollingPaused}, " +
|
|
||||||
$"Poller active: {interf.PollerActive}, " +
|
|
||||||
$"Speed UP: {interf.BytesPerSecondUpstream} B/s, " +
|
|
||||||
$"Speed DOWN: {interf.BytesPerSecondDownstream} B/s, " +
|
|
||||||
$"Poll delay: {interf.PollerDelayMs} ms, " +
|
|
||||||
$"Queued MSGs: {interf.QueuedMessages}";
|
|
||||||
|
|
||||||
Console.Clear();
|
|
||||||
Console.WriteLine("Underlying registers on tick: \n");
|
|
||||||
|
|
||||||
foreach (var register in interf.Registers) {
|
|
||||||
|
|
||||||
Console.WriteLine($"{register.GetCombinedName()} / {register.GetRegisterPLCName()} - Value: {register.GetValueString()}");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"{registers.TestBool1}");
|
|
||||||
Console.WriteLine($"{registers.TestDuplicate}");
|
|
||||||
|
|
||||||
remainingTime -= 1000;
|
|
||||||
|
|
||||||
Console.WriteLine($"\nStopping in: {remainingTime}ms");
|
|
||||||
|
|
||||||
await Task.Delay(1000);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
await Task.Delay(runTime);
|
|
||||||
interf.Disconnect();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Scenario("Dispose and disconnect connection")]
|
|
||||||
public async Task RunDisposalAndDisconnectAsync () {
|
|
||||||
|
|
||||||
//automatic disposal
|
|
||||||
using (var interf = new MewtocolInterface("192.168.115.210")) {
|
|
||||||
|
|
||||||
await interf.ConnectAsync();
|
|
||||||
|
|
||||||
if (interf.IsConnected) {
|
|
||||||
|
|
||||||
Console.WriteLine("Opened connection");
|
|
||||||
|
|
||||||
await Task.Delay(5000);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("Disposed, closed connection");
|
|
||||||
|
|
||||||
//manual close
|
|
||||||
var interf2 = new MewtocolInterface("192.168.115.210");
|
|
||||||
|
|
||||||
await interf2.ConnectAsync();
|
|
||||||
|
|
||||||
if (interf2.IsConnected) {
|
|
||||||
|
|
||||||
Console.WriteLine("Opened connection");
|
|
||||||
|
|
||||||
await Task.Delay(5000);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interf2.Disconnect();
|
|
||||||
|
|
||||||
Console.WriteLine("Disconnected, closed connection");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Scenario("Test auto enums and bitwise, needs the example program from MewtocolNet/PLC_Test")]
|
|
||||||
public async Task RunEnumsBitwiseAsync () {
|
|
||||||
|
|
||||||
Console.WriteLine("Starting auto enums and bitwise");
|
|
||||||
|
|
||||||
//setting up a new PLC interface and register collection
|
|
||||||
MewtocolInterface interf = new MewtocolInterface("192.168.115.210");
|
|
||||||
TestRegistersEnumBitwise registers = new TestRegistersEnumBitwise();
|
|
||||||
|
|
||||||
//attaching the register collection and an automatic poller
|
|
||||||
interf.WithRegisterCollection(registers).WithPoller();
|
|
||||||
|
|
||||||
registers.PropertyChanged += (s, e) => {
|
|
||||||
|
|
||||||
Console.Clear();
|
|
||||||
|
|
||||||
var props = registers.GetType().GetProperties();
|
|
||||||
|
|
||||||
foreach (var prop in props) {
|
|
||||||
|
|
||||||
var val = prop.GetValue(registers);
|
|
||||||
string printVal = val?.ToString() ?? "null";
|
|
||||||
|
|
||||||
if (val is BitArray bitarr) {
|
|
||||||
printVal = bitarr.ToBitString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Write($"{prop.Name} - ");
|
|
||||||
|
|
||||||
if(printVal == "True") {
|
|
||||||
Console.ForegroundColor = ConsoleColor.Green;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Write($"{printVal}");
|
|
||||||
|
|
||||||
Console.ResetColor();
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
await interf.ConnectAsync();
|
|
||||||
|
|
||||||
//use the async method to make sure the cycling is stopped
|
|
||||||
await interf.SetRegisterAsync(nameof(registers.StartCyclePLC), false);
|
|
||||||
|
|
||||||
await Task.Delay(5000);
|
|
||||||
|
|
||||||
//set the register without waiting for it async
|
|
||||||
registers.StartCyclePLC = true;
|
|
||||||
|
|
||||||
await Task.Delay(5000);
|
|
||||||
|
|
||||||
registers.StartCyclePLC = false;
|
|
||||||
|
|
||||||
await Task.Delay(2000);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
82
Examples/Examples.BasicEthernet/Program.cs
Normal file
82
Examples/Examples.BasicEthernet/Program.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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;
|
||||||
|
Logger.OnNewLogMessage((t, l, m) => { Console.WriteLine(m); });
|
||||||
|
|
||||||
|
//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}");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
148
Examples/Examples.BasicRegisterReadWrite/Program.cs
Normal file
148
Examples/Examples.BasicRegisterReadWrite/Program.cs
Normal 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]}");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
Examples/Examples.Polling/Examples.Polling.csproj
Normal file
16
Examples/Examples.Polling/Examples.Polling.csproj
Normal 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>
|
||||||
90
Examples/Examples.Polling/Program.cs
Normal file
90
Examples/Examples.Polling/Program.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
31
Examples/Examples.ProgramReadWrite/Program.cs
Normal file
31
Examples/Examples.ProgramReadWrite/Program.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
Examples/Examples.WPF/App.xaml
Normal file
11
Examples/Examples.WPF/App.xaml
Normal 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>
|
||||||
85
Examples/Examples.WPF/App.xaml.cs
Normal file
85
Examples/Examples.WPF/App.xaml.cs
Normal 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);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
Examples/Examples.WPF/AssemblyInfo.cs
Normal file
10
Examples/Examples.WPF/AssemblyInfo.cs
Normal 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)
|
||||||
|
)]
|
||||||
62
Examples/Examples.WPF/Converters/ColorHashConverter.cs
Normal file
62
Examples/Examples.WPF/Converters/ColorHashConverter.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
14
Examples/Examples.WPF/Converters/NegationConverter.cs
Normal file
14
Examples/Examples.WPF/Converters/NegationConverter.cs
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
16
Examples/Examples.WPF/Examples.WPF.csproj
Normal file
16
Examples/Examples.WPF/Examples.WPF.csproj
Normal 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>
|
||||||
169
Examples/Examples.WPF/MainWindow.xaml
Normal file
169
Examples/Examples.WPF/MainWindow.xaml
Normal 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>
|
||||||
57
Examples/Examples.WPF/MainWindow.xaml.cs
Normal file
57
Examples/Examples.WPF/MainWindow.xaml.cs
Normal 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);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
40
Examples/Examples.WPF/ViewModels/AppViewModel.cs
Normal file
40
Examples/Examples.WPF/ViewModels/AppViewModel.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
117
Examples/Examples.WPF/ViewModels/ConnectViewViewModel.cs
Normal file
117
Examples/Examples.WPF/ViewModels/ConnectViewViewModel.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
36
Examples/Examples.WPF/ViewModels/PlcDataViewViewModel.cs
Normal file
36
Examples/Examples.WPF/ViewModels/PlcDataViewViewModel.cs
Normal 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!;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
Examples/Examples.WPF/ViewModels/ViewModelBase.cs
Normal file
18
Examples/Examples.WPF/ViewModels/ViewModelBase.cs
Normal 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
129
Examples/Examples.WPF/Views/ConnectView.xaml
Normal file
129
Examples/Examples.WPF/Views/ConnectView.xaml
Normal 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>
|
||||||
161
Examples/Examples.WPF/Views/ConnectView.xaml.cs
Normal file
161
Examples/Examples.WPF/Views/ConnectView.xaml.cs
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
350
Examples/Examples.WPF/Views/PlcDataView.xaml
Normal file
350
Examples/Examples.WPF/Views/PlcDataView.xaml
Normal 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>
|
||||||
81
Examples/Examples.WPF/Views/PlcDataView.xaml.cs
Normal file
81
Examples/Examples.WPF/Views/PlcDataView.xaml.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Examples;
|
|
||||||
|
|
||||||
class Program {
|
|
||||||
|
|
||||||
static ExampleScenarios ExampleSzenarios = new ExampleScenarios();
|
|
||||||
|
|
||||||
static void Main(string[] args) {
|
|
||||||
|
|
||||||
AppDomain.CurrentDomain.UnhandledException += (s,e) => {
|
|
||||||
Console.WriteLine(e.ExceptionObject.ToString());
|
|
||||||
};
|
|
||||||
|
|
||||||
TaskScheduler.UnobservedTaskException += (s,e) => {
|
|
||||||
Console.WriteLine(e.Exception.ToString());
|
|
||||||
};
|
|
||||||
|
|
||||||
ExampleSzenarios.SetupLogger();
|
|
||||||
|
|
||||||
LoopInput();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void LoopInput () {
|
|
||||||
|
|
||||||
Console.WriteLine("All available scenarios\n");
|
|
||||||
|
|
||||||
var methods = ExampleSzenarios.GetType().GetMethods();
|
|
||||||
var invokeableMethods = new List<MethodInfo>();
|
|
||||||
|
|
||||||
for (int i = 0, j = 0; i < methods.Length; i++) {
|
|
||||||
|
|
||||||
MethodInfo method = methods[i];
|
|
||||||
var foundAtt = method.GetCustomAttribute(typeof(ScenarioAttribute));
|
|
||||||
|
|
||||||
if(foundAtt != null && foundAtt is ScenarioAttribute att) {
|
|
||||||
|
|
||||||
Console.WriteLine($"[{j + 1}] {method.Name}() - {att.Description}");
|
|
||||||
invokeableMethods.Add(method);
|
|
||||||
|
|
||||||
j++;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
||||||
Console.WriteLine("\nEnter a number to excecute a example");
|
|
||||||
Console.ResetColor();
|
|
||||||
|
|
||||||
Console.WriteLine("\nOther possible commands: \n\n" +
|
|
||||||
"'toggle logger' - toggle the built in mewtocol logger on/off\n" +
|
|
||||||
"'exit' - to close this program \n" +
|
|
||||||
"'clear' - to clear the output \n");
|
|
||||||
|
|
||||||
Console.Write("> ");
|
|
||||||
|
|
||||||
var line = Console.ReadLine();
|
|
||||||
|
|
||||||
if (line == "toggle logger") {
|
|
||||||
|
|
||||||
ExampleScenarios.MewtocolLoggerEnabled = !ExampleScenarios.MewtocolLoggerEnabled;
|
|
||||||
|
|
||||||
Console.WriteLine(ExampleScenarios.MewtocolLoggerEnabled ? "Logger enabled" : "Logger disabled");
|
|
||||||
|
|
||||||
} else if (line == "exit") {
|
|
||||||
|
|
||||||
Environment.Exit(0);
|
|
||||||
|
|
||||||
} else if (line == "clear") {
|
|
||||||
|
|
||||||
Console.Clear();
|
|
||||||
|
|
||||||
} else if (int.TryParse(line, out var lineNum)) {
|
|
||||||
|
|
||||||
var index = Math.Clamp(lineNum - 1, 0, invokeableMethods.Count - 1);
|
|
||||||
|
|
||||||
var task = (Task)invokeableMethods.ElementAt(index).Invoke(ExampleSzenarios, null);
|
|
||||||
|
|
||||||
task.Wait();
|
|
||||||
|
|
||||||
Console.ForegroundColor = ConsoleColor.Green;
|
|
||||||
Console.WriteLine("The program ran to completition");
|
|
||||||
Console.ResetColor();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
Console.WriteLine("Wrong input");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
LoopInput();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Examples;
|
|
||||||
|
|
||||||
public class ScenarioAttribute : Attribute {
|
|
||||||
|
|
||||||
public string Description { get; private set; }
|
|
||||||
|
|
||||||
public ScenarioAttribute(string description) {
|
|
||||||
|
|
||||||
Description = description;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,80 +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; }
|
|
||||||
|
|
||||||
private int testDuplicate;
|
|
||||||
|
|
||||||
[Register(1000)]
|
|
||||||
public int TestDuplicate {
|
|
||||||
get => testDuplicate;
|
|
||||||
set => AutoSetter(value, ref testDuplicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
//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; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
using MewtocolNet;
|
|
||||||
using MewtocolNet.RegisterAttributes;
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
namespace Examples {
|
|
||||||
|
|
||||||
public class TestRegistersEnumBitwise : RegisterCollectionBase {
|
|
||||||
|
|
||||||
private bool startCyclePLC;
|
|
||||||
|
|
||||||
[Register(50, RegisterType.R)]
|
|
||||||
public bool StartCyclePLC {
|
|
||||||
get => startCyclePLC;
|
|
||||||
set => AutoSetter(value, ref startCyclePLC);
|
|
||||||
}
|
|
||||||
|
|
||||||
//the enum you want to read out
|
|
||||||
public enum CurrentState {
|
|
||||||
|
|
||||||
Undefined = 0,
|
|
||||||
State1 = 1,
|
|
||||||
State2 = 2,
|
|
||||||
//If you leave an enum empty it still works
|
|
||||||
//State3 = 3,
|
|
||||||
State4 = 4,
|
|
||||||
State5 = 5,
|
|
||||||
State6 = 6,
|
|
||||||
State7 = 7,
|
|
||||||
State8 = 8,
|
|
||||||
State9 = 9,
|
|
||||||
State10 = 10,
|
|
||||||
State11 = 11,
|
|
||||||
State12 = 12,
|
|
||||||
State13 = 13,
|
|
||||||
State14 = 14,
|
|
||||||
State15 = 15,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//automatically convert the short (PLC int) register to an enum
|
|
||||||
[Register(500)]
|
|
||||||
public CurrentState TestEnum16 { get; private set; }
|
|
||||||
|
|
||||||
//also works for 32bit registers
|
|
||||||
[Register(501, BitCount.B32)]
|
|
||||||
public CurrentState TestEnum32 { get; private set; }
|
|
||||||
|
|
||||||
//get the whole bit array from DT503
|
|
||||||
|
|
||||||
[Register(503)]
|
|
||||||
public BitArray TestBitRegister16 { get; private set; }
|
|
||||||
|
|
||||||
//you can also extract single bits from DT503
|
|
||||||
|
|
||||||
[Register(503, 0, BitCount.B16)]
|
|
||||||
public bool BitValue0 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 1, BitCount.B16)]
|
|
||||||
public bool BitValue1 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 2, BitCount.B16)]
|
|
||||||
public bool BitValue2 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 3, BitCount.B16)]
|
|
||||||
public bool BitValue3 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 4, BitCount.B16)]
|
|
||||||
public bool BitValue4 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 5, BitCount.B16)]
|
|
||||||
public bool BitValue5 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 6, BitCount.B16)]
|
|
||||||
public bool BitValue6 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 7, BitCount.B16)]
|
|
||||||
public bool BitValue7 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 8, BitCount.B16)]
|
|
||||||
public bool BitValue8 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 9, BitCount.B16)]
|
|
||||||
public bool BitValue9 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 10, BitCount.B16)]
|
|
||||||
public bool BitValue10 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 11, BitCount.B16)]
|
|
||||||
public bool BitValue11 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 12, BitCount.B16)]
|
|
||||||
public bool BitValue12 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 13, BitCount.B16)]
|
|
||||||
public bool BitValue13 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 14, BitCount.B16)]
|
|
||||||
public bool BitValue14 { get; private set; }
|
|
||||||
|
|
||||||
[Register(503, 15, BitCount.B16)]
|
|
||||||
public bool BitValue15 { get; private set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
21
LICENSE.md
21
LICENSE.md
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022 WOLF Medizintechnik GmbH
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
19
MewTerminal/Commands/ClearCommand.cs
Normal file
19
MewTerminal/Commands/ClearCommand.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
MewTerminal/Commands/CommandLineExcecuteable.cs
Normal file
20
MewTerminal/Commands/CommandLineExcecuteable.cs
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
35
MewTerminal/Commands/ListSupportCommand.cs
Normal file
35
MewTerminal/Commands/ListSupportCommand.cs
Normal 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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
46
MewTerminal/Commands/OnlineCommands/OnlineCommand.cs
Normal file
46
MewTerminal/Commands/OnlineCommands/OnlineCommand.cs
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
102
MewTerminal/Commands/ScanCommand.cs
Normal file
102
MewTerminal/Commands/ScanCommand.cs
Normal 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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
MewTerminal/Helpers/Helpers.cs
Normal file
65
MewTerminal/Helpers/Helpers.cs
Normal 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");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
MewTerminal/MewTerminal.csproj
Normal file
39
MewTerminal/MewTerminal.csproj
Normal 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
93
MewTerminal/Program.cs
Normal 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
18
MewTerminal/app.manifest
Normal 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>
|
||||||
142
MewtocolNet.sln
142
MewtocolNet.sln
@@ -5,9 +5,27 @@ VisualStudioVersion = 17.5.33103.201
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MewtocolTests", "MewtocolTests\MewtocolTests.csproj", "{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}"
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -31,18 +49,6 @@ Global
|
|||||||
{8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x64.Build.0 = Release|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{8B7863E7-5E82-4990-9138-2C0C24629982}.Release|x86.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
@@ -55,8 +61,116 @@ Global
|
|||||||
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x64.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{C1BF3AB0-CDFE-4070-A759-C3B25A20ABE1}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
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
|
EndGlobal
|
||||||
|
|||||||
5326
MewtocolNet/AutoGeneratedData/FPFunction.cs
Normal file
5326
MewtocolNet/AutoGeneratedData/FPFunction.cs
Normal file
File diff suppressed because it is too large
Load Diff
148
MewtocolNet/ComCassette/CassetteFinder.cs
Normal file
148
MewtocolNet/ComCassette/CassetteFinder.cs
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
219
MewtocolNet/ComCassette/CassetteInformation.cs
Normal file
219
MewtocolNet/ComCassette/CassetteInformation.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
19
MewtocolNet/ComCassette/CassetteStatus.cs
Normal file
19
MewtocolNet/ComCassette/CassetteStatus.cs
Normal 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,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
103
MewtocolNet/CustomTypes/DWord.cs
Normal file
103
MewtocolNet/CustomTypes/DWord.cs
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
92
MewtocolNet/CustomTypes/DateAndTime.cs
Normal file
92
MewtocolNet/CustomTypes/DateAndTime.cs
Normal 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}";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
MewtocolNet/CustomTypes/MewtocolExtensionType.cs
Normal file
7
MewtocolNet/CustomTypes/MewtocolExtensionType.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MewtocolNet {
|
||||||
|
|
||||||
|
internal interface MewtocolExtTypeInit1Word { }
|
||||||
|
|
||||||
|
internal interface MewtocolExtTypeInit2Word { }
|
||||||
|
|
||||||
|
}
|
||||||
103
MewtocolNet/CustomTypes/Word.cs
Normal file
103
MewtocolNet/CustomTypes/Word.cs
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
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"},
|
{21, "NACK error"},
|
||||||
{22, "WACK error"},
|
{22, "WACK error"},
|
||||||
@@ -40,7 +39,6 @@ namespace MewtocolNet.Links {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
12
MewtocolNet/Documentation/PlcCodeTestedAttribute.cs
Normal file
12
MewtocolNet/Documentation/PlcCodeTestedAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MewtocolNet.Documentation {
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||||
|
internal class PlcCodeTestedAttribute : Attribute {
|
||||||
|
|
||||||
|
public PlcCodeTestedAttribute() { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
MewtocolNet/Documentation/PlcEXRTAttribute.cs
Normal file
12
MewtocolNet/Documentation/PlcEXRTAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MewtocolNet.Documentation {
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||||
|
internal class PlcEXRTAttribute : Attribute {
|
||||||
|
|
||||||
|
public PlcEXRTAttribute() { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
MewtocolNet/Documentation/PlcLegacyAttribute.cs
Normal file
12
MewtocolNet/Documentation/PlcLegacyAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MewtocolNet.Documentation {
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||||
|
internal class PlcLegacyAttribute : Attribute {
|
||||||
|
|
||||||
|
public PlcLegacyAttribute() { }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
75
MewtocolNet/Documentation/docs.xml
Normal file
75
MewtocolNet/Documentation/docs.xml
Normal 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>
|
||||||
9
MewtocolNet/Events/PlcConnectionArgs.cs
Normal file
9
MewtocolNet/Events/PlcConnectionArgs.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MewtocolNet.Events {
|
||||||
|
|
||||||
|
public delegate void PlcConnectionEventHandler(object sender, PlcConnectionArgs e);
|
||||||
|
|
||||||
|
public class PlcConnectionArgs : EventArgs { }
|
||||||
|
|
||||||
|
}
|
||||||
19
MewtocolNet/Events/PlcModeArgs.cs
Normal file
19
MewtocolNet/Events/PlcModeArgs.cs
Normal 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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
98
MewtocolNet/Events/ReconnectArgs.cs
Normal file
98
MewtocolNet/Events/ReconnectArgs.cs
Normal 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));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
MewtocolNet/Events/RegisterChanged.cs
Normal file
22
MewtocolNet/Events/RegisterChanged.cs
Normal 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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
MewtocolNet/Extensions/AsyncExtensions.cs
Normal file
26
MewtocolNet/Extensions/AsyncExtensions.cs
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
MewtocolNet/Extensions/SerialPortExtensions.cs
Normal file
34
MewtocolNet/Extensions/SerialPortExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
88
MewtocolNet/Helpers/CRCCalculator.cs
Normal file
88
MewtocolNet/Helpers/CRCCalculator.cs
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
MewtocolNet/Helpers/LinqHelpers.cs
Normal file
30
MewtocolNet/Helpers/LinqHelpers.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
514
MewtocolNet/Helpers/MewtocolHelpers.cs
Normal file
514
MewtocolNet/Helpers/MewtocolHelpers.cs
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
125
MewtocolNet/Helpers/ParsedPlcName.cs
Normal file
125
MewtocolNet/Helpers/ParsedPlcName.cs
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
MewtocolNet/Helpers/PlcBitConverter.cs
Normal file
28
MewtocolNet/Helpers/PlcBitConverter.cs
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
87
MewtocolNet/Helpers/PlcFormat.cs
Normal file
87
MewtocolNet/Helpers/PlcFormat.cs
Normal 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"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
224
MewtocolNet/IPlc.cs
Normal file
224
MewtocolNet/IPlc.cs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
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>
|
||||||
|
/// Builds and adds registers to the device
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder"></param>
|
||||||
|
void BuildRegisters(Action<RBuildMulti> builder);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all registers atached to the interface
|
||||||
|
/// </summary>
|
||||||
|
void ClearAllRegisters();
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
MewtocolNet/IPlcEthernet.cs
Normal file
37
MewtocolNet/IPlcEthernet.cs
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
MewtocolNet/IPlcSerial.cs
Normal file
61
MewtocolNet/IPlcSerial.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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>
|
||||||
|
/// Is RTS (Request to send) enabled?
|
||||||
|
/// </summary>
|
||||||
|
bool RtsEnabled { 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="_rtsEnable">Is RTS (Request to send) enabled?</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, bool _rtsEnable = true, int _station = 1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to establish a connection with the device asynchronously
|
||||||
|
/// </summary>
|
||||||
|
Task<ConnectResult> ConnectAsync(Func<Task> callBack, Action onTryingConfig);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
MewtocolNet/InternalEnums/CommandState.cs
Normal file
12
MewtocolNet/InternalEnums/CommandState.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace MewtocolNet {
|
||||||
|
|
||||||
|
internal enum CommandState {
|
||||||
|
|
||||||
|
Initial,
|
||||||
|
LineFeed,
|
||||||
|
RequestedNextFrame,
|
||||||
|
Complete
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
112
MewtocolNet/Logging/Logger.cs
Normal file
112
MewtocolNet/Logging/Logger.cs
Normal 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.Console;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
using System;
|
namespace MewtocolNet.Logging {
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace MewtocolNet.Logging {
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The loglevel of the logging module
|
/// The loglevel of the logging module
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum LogLevel {
|
public enum LogLevel {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs nothing
|
||||||
|
/// </summary>
|
||||||
|
None = -1,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logs only errors
|
/// Logs only errors
|
||||||
/// </summary>
|
/// </summary>
|
||||||
12
MewtocolNet/Logging/LoggerTargets.cs
Normal file
12
MewtocolNet/Logging/LoggerTargets.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MewtocolNet.Logging {
|
||||||
|
[Flags]
|
||||||
|
public enum LoggerTargets {
|
||||||
|
|
||||||
|
None = 0,
|
||||||
|
Console = 1,
|
||||||
|
Trace = 2,
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
498
MewtocolNet/Mewtocol.cs
Normal file
498
MewtocolNet/Mewtocol.cs
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
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="rtsEnabled">Is RTS (Request to send) enabled?</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, bool rtsEnabled = true, int station = 0xEE) {
|
||||||
|
|
||||||
|
var instance = new MewtocolInterfaceSerial();
|
||||||
|
instance.ConfigureConnection(portName, (int)baudRate, (int)dataBits, parity, stopBits, rtsEnabled, 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="rtsEnabled">Is RTS (Request to send) enabled?</param>
|
||||||
|
/// <param name="station">Plc station number 0xEE for direct communication</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static PostInit<IPlcSerial> SerialAuto(string portName, bool rtsEnabled = true, int station = 0xEE) {
|
||||||
|
|
||||||
|
var instance = new MewtocolInterfaceSerial();
|
||||||
|
instance.ConfigureConnection(portName, station, rtsEnable: rtsEnabled);
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,490 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MewtocolNet.Logging;
|
|
||||||
using MewtocolNet.RegisterAttributes;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True if the poller is actvice (can be paused)
|
|
||||||
/// </summary>
|
|
||||||
public bool PollerActive => !pollerTaskStopped;
|
|
||||||
|
|
||||||
internal event Action PolledCycle;
|
|
||||||
|
|
||||||
internal volatile bool pollerTaskRunning;
|
|
||||||
internal volatile bool pollerTaskStopped;
|
|
||||||
internal volatile bool pollerIsPaused;
|
|
||||||
internal volatile bool pollerFirstCycle = false;
|
|
||||||
|
|
||||||
internal bool usePoller = false;
|
|
||||||
|
|
||||||
#region Register Polling
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kills the poller completely
|
|
||||||
/// </summary>
|
|
||||||
internal void KillPoller () {
|
|
||||||
|
|
||||||
pollerTaskRunning = false;
|
|
||||||
pollerTaskStopped = true;
|
|
||||||
|
|
||||||
ClearRegisterVals();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
|
|
||||||
pollerFirstCycle = true;
|
|
||||||
|
|
||||||
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++;
|
|
||||||
pollerFirstCycle = false;
|
|
||||||
|
|
||||||
await Task.Delay(pollerDelayMs);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerIsPaused = !pollerTaskRunning;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerIsPaused = false;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PropertyRegisterWasSet (string propName, object value) {
|
|
||||||
|
|
||||||
SetRegister(propName, value);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#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) {
|
|
||||||
|
|
||||||
IRegister 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) {
|
|
||||||
|
|
||||||
IRegister toAdd = null;
|
|
||||||
|
|
||||||
//as number registers
|
|
||||||
if (_type == RegisterType.DT_short) {
|
|
||||||
toAdd = new NRegister<short>(_address, _name).WithCollectionType(_colType);
|
|
||||||
}
|
|
||||||
if (_type == RegisterType.DT_ushort) {
|
|
||||||
toAdd = new NRegister<ushort>(_address, _name).WithCollectionType(_colType);
|
|
||||||
}
|
|
||||||
if (_type == RegisterType.DDT_int) {
|
|
||||||
toAdd = new NRegister<int>(_address, _name).WithCollectionType(_colType);
|
|
||||||
}
|
|
||||||
if (_type == RegisterType.DDT_uint) {
|
|
||||||
toAdd = new NRegister<uint>(_address, _name).WithCollectionType(_colType);
|
|
||||||
}
|
|
||||||
if (_type == RegisterType.DDT_float) {
|
|
||||||
toAdd = new NRegister<float>(_address, _name).WithCollectionType(_colType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toAdd == null) {
|
|
||||||
toAdd = new BRegister(_address, _type, _name).WithCollectionType(_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");
|
|
||||||
}
|
|
||||||
|
|
||||||
IRegister 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 register adding for auto register collection building
|
|
||||||
internal void AddRegister<T> (Type _colType, int _address, PropertyInfo boundProp, int _length = 1, 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.MemoryAddress == _address) && _isBitwise) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IRegister reg = null;
|
|
||||||
|
|
||||||
string propName = boundProp.Name;
|
|
||||||
|
|
||||||
//rename the property name to prevent duplicate names in case of a bitwise prop
|
|
||||||
if(_isBitwise && regType == typeof(short))
|
|
||||||
propName = $"Auto_Bitwise_DT{_address}";
|
|
||||||
|
|
||||||
if (_isBitwise && regType == typeof(int))
|
|
||||||
propName = $"Auto_Bitwise_DDT{_address}";
|
|
||||||
|
|
||||||
if (regType == typeof(short)) {
|
|
||||||
reg = new NRegister<short>(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType);
|
|
||||||
} else if (regType == typeof(ushort)) {
|
|
||||||
reg = new NRegister<ushort>(_address, propName).WithCollectionType(_colType);
|
|
||||||
} else if (regType == typeof(int)) {
|
|
||||||
reg = new NRegister<int>(_address, propName, _isBitwise, _enumType).WithCollectionType(_colType);
|
|
||||||
} else if (regType == typeof(uint)) {
|
|
||||||
reg = new NRegister<uint>(_address, propName).WithCollectionType(_colType);
|
|
||||||
} else if (regType == typeof(float)) {
|
|
||||||
reg = new NRegister<float>(_address, propName).WithCollectionType(_colType);
|
|
||||||
} else if (regType == typeof(string)) {
|
|
||||||
reg = new SRegister(_address, _length, propName).WithCollectionType(_colType);
|
|
||||||
} else if (regType == typeof(TimeSpan)) {
|
|
||||||
reg = new NRegister<TimeSpan>(_address, propName).WithCollectionType(_colType);
|
|
||||||
} else if (regType == typeof(bool)) {
|
|
||||||
reg = new BRegister(_address, RegisterType.R, propName).WithCollectionType(_colType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reg == null) {
|
|
||||||
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() == reg.GetRegisterPLCName()) && !_isBitwise) {
|
|
||||||
throw new NotSupportedException($"Cannot add a register multiple times, " +
|
|
||||||
$"make sure that all register attributes or AddRegister assignments have different adresses.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Registers.Add(reg);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Register accessing
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a register that was added by its name
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IRegister 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 : IRegister {
|
|
||||||
try {
|
|
||||||
|
|
||||||
var reg = Registers.FirstOrDefault(x => x.Name == name);
|
|
||||||
return (T)reg;
|
|
||||||
|
|
||||||
} catch (InvalidCastException) {
|
|
||||||
|
|
||||||
return default(T);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Register Reading
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of all added registers
|
|
||||||
/// </summary>
|
|
||||||
public List<IRegister> GetAllRegisters () {
|
|
||||||
|
|
||||||
return Registers;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Event Invoking
|
|
||||||
|
|
||||||
internal void InvokeRegisterChanged (IRegister reg) {
|
|
||||||
|
|
||||||
RegisterChanged?.Invoke(reg);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void InvokePolledCycleDone () {
|
|
||||||
|
|
||||||
PolledCycle?.Invoke();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace MewtocolNet {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An interface for all register types
|
|
||||||
/// </summary>
|
|
||||||
public interface IRegister {
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets called whenever the value was changed
|
|
||||||
/// </summary>
|
|
||||||
event Action<object> ValueChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the register
|
|
||||||
/// </summary>
|
|
||||||
string Name { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The current value of the register
|
|
||||||
/// </summary>
|
|
||||||
object Value { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The plc memory address of the register
|
|
||||||
/// </summary>
|
|
||||||
int MemoryAddress { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates if the register is processed bitwise
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True if bitwise</returns>
|
|
||||||
bool IsUsedBitwise();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates a string describing the starting memory address of the register
|
|
||||||
/// </summary>
|
|
||||||
string GetStartingMemoryArea();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current value formatted as a readable string
|
|
||||||
/// </summary>
|
|
||||||
string GetValueString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds the identifier for the mewtocol query string
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
string BuildMewtocolQuery();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a register string that prepends the memory address fe. DDT or DT, X, Y etc
|
|
||||||
/// </summary>
|
|
||||||
string GetRegisterString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a combined name for the attached property to uniquely identify the property register binding
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
string GetCombinedName();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name of the class that contains the attached property
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
string GetContainerName();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a register name after the PLC convention <br/>
|
|
||||||
/// Example <code>DDT100, XA, X6, Y1, DT3300</code>
|
|
||||||
/// </summary>
|
|
||||||
string GetRegisterPLCName();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears the current value of the register and resets it to default
|
|
||||||
/// </summary>
|
|
||||||
void ClearValue();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Triggers a notifychanged update event
|
|
||||||
/// </summary>
|
|
||||||
void TriggerNotifyChange();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of the class collection its attached property is in or null
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The class name or null if manually added</returns>
|
|
||||||
Type GetCollectionType();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds a readable string with all important register informations
|
|
||||||
/// </summary>
|
|
||||||
string ToString();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,235 +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();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsDoubleNumericRegisterType (this Type type) {
|
|
||||||
|
|
||||||
//Type[] singles = new Type[] {
|
|
||||||
// typeof(short),
|
|
||||||
// typeof(ushort),
|
|
||||||
//};
|
|
||||||
|
|
||||||
Type[] doubles = new Type[] {
|
|
||||||
typeof(int),
|
|
||||||
typeof(uint),
|
|
||||||
typeof(float),
|
|
||||||
typeof(TimeSpan),
|
|
||||||
};
|
|
||||||
|
|
||||||
return doubles.Contains(type);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsNumericSupportedType (this Type type) {
|
|
||||||
|
|
||||||
Type[] supported = new Type[] {
|
|
||||||
typeof(short),
|
|
||||||
typeof(ushort),
|
|
||||||
typeof(int),
|
|
||||||
typeof(uint),
|
|
||||||
typeof(float),
|
|
||||||
typeof(TimeSpan),
|
|
||||||
};
|
|
||||||
|
|
||||||
return supported.Contains(type);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,904 +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;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Timers;
|
|
||||||
|
|
||||||
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<IRegister> 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<IRegister> Registers { get; set; } = new List<IRegister>();
|
|
||||||
|
|
||||||
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.GetStartingMemoryArea()}".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();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearRegisterVals () {
|
|
||||||
|
|
||||||
for (int i = 0; i < Registers.Count; i++) {
|
|
||||||
|
|
||||||
var reg = Registers[i];
|
|
||||||
reg.ClearValue();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#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, prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.PropertyType == typeof(ushort)) {
|
|
||||||
AddRegister<ushort>(collection.GetType(), cAttribute.MemoryArea, prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.PropertyType == typeof(int)) {
|
|
||||||
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.PropertyType == typeof(uint)) {
|
|
||||||
AddRegister<uint>(collection.GetType(), cAttribute.MemoryArea, prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.PropertyType == typeof(float)) {
|
|
||||||
AddRegister<float>(collection.GetType(), cAttribute.MemoryArea, prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.PropertyType == typeof(string)) {
|
|
||||||
AddRegister<string>(collection.GetType(), cAttribute.MemoryArea, prop, cAttribute.StringLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.PropertyType.IsEnum) {
|
|
||||||
|
|
||||||
if (cAttribute.BitCount == BitCount.B16) {
|
|
||||||
AddRegister<short>(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType);
|
|
||||||
} else {
|
|
||||||
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, prop, _enumType: prop.PropertyType);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//read number as bit array
|
|
||||||
if (prop.PropertyType == typeof(BitArray)) {
|
|
||||||
|
|
||||||
if (cAttribute.BitCount == BitCount.B16) {
|
|
||||||
AddRegister<short>(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true);
|
|
||||||
} else {
|
|
||||||
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, prop, _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, prop, _isBitwise: true);
|
|
||||||
} else {
|
|
||||||
AddRegister<int>(collection.GetType(), cAttribute.MemoryArea, prop, _isBitwise: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.PropertyType == typeof(TimeSpan)) {
|
|
||||||
AddRegister<TimeSpan>(collection.GetType(), cAttribute.MemoryArea, prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterChanged += (reg) => {
|
|
||||||
|
|
||||||
//register is used bitwise
|
|
||||||
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.MemoryAddress);
|
|
||||||
|
|
||||||
if(bitWiseFound != null) {
|
|
||||||
|
|
||||||
var casted = (RegisterAttribute)bitWiseFound;
|
|
||||||
var bitIndex = casted.AssignedBitIndex;
|
|
||||||
|
|
||||||
BitArray bitAr = null;
|
|
||||||
|
|
||||||
if (reg is NRegister<short> reg16) {
|
|
||||||
var bytes = BitConverter.GetBytes((short)reg16.Value);
|
|
||||||
bitAr = new BitArray(bytes);
|
|
||||||
} else if(reg is NRegister<int> reg32) {
|
|
||||||
var bytes = BitConverter.GetBytes((int)reg32.Value);
|
|
||||||
bitAr = new BitArray(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bitAr != null && bitIndex < bitAr.Length && bitIndex >= 0) {
|
|
||||||
|
|
||||||
//set the specific bit index if needed
|
|
||||||
prop.SetValue(collection, bitAr[bitIndex]);
|
|
||||||
collection.TriggerPropertyChanged(prop.Name);
|
|
||||||
|
|
||||||
} else if (bitAr != null) {
|
|
||||||
|
|
||||||
//set the specific bit array if needed
|
|
||||||
prop.SetValue(collection, bitAr);
|
|
||||||
collection.TriggerPropertyChanged(prop.Name);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//updating normal properties
|
|
||||||
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) {
|
|
||||||
|
|
||||||
HashSet<Type> NumericTypes = new HashSet<Type> {
|
|
||||||
typeof(bool),
|
|
||||||
typeof(short),
|
|
||||||
typeof(ushort),
|
|
||||||
typeof(int),
|
|
||||||
typeof(uint),
|
|
||||||
typeof(float),
|
|
||||||
typeof(TimeSpan),
|
|
||||||
typeof(string)
|
|
||||||
};
|
|
||||||
|
|
||||||
var regValue = ((IRegister)reg).Value;
|
|
||||||
|
|
||||||
if (NumericTypes.Any(x => foundToUpdate.PropertyType == x)) {
|
|
||||||
foundToUpdate.SetValue(collection, regValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundToUpdate.PropertyType.IsEnum) {
|
|
||||||
foundToUpdate.SetValue(collection, regValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
if (queuedMessages > 0)
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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.BuildMewtocolQuery()}";
|
|
||||||
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.BuildMewtocolQuery()}{(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.BuildMewtocolQuery()}";
|
|
||||||
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.BuildMewtocolQuery()}{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.BuildMewtocolQuery()}";
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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}";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user