├── .clang-format ├── .github └── workflows │ ├── checkmarx-analysis.yml │ ├── codacy-analysis.yml │ ├── codeql-analysis.yml │ ├── devskim-analysis.yml │ ├── flawfinder-analysis.yml │ ├── msvc-analysis.yml │ ├── synopsys-io-analysis.yml │ ├── sysdig-scan-analysis.yml │ └── veracode-analysis.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── THIRD_PARTY.md ├── dependencies.yaml ├── examples ├── CMakeLists.txt ├── modbus_tcp_client_example.cpp └── modbus_udp_client_example.cpp ├── include ├── consts.hpp └── modbus │ ├── exceptions.hpp │ ├── modbus_client.hpp │ └── utils.hpp ├── lib └── connection │ ├── CMakeLists.txt │ ├── examples │ ├── CMakeLists.txt │ ├── connection_example.cpp │ └── udp_connection_example.cpp │ ├── include │ └── connection │ │ ├── connection.hpp │ │ ├── exceptions.hpp │ │ ├── serial_connection_helper.hpp │ │ └── utils.hpp │ └── src │ ├── rtu.cpp │ ├── serial_connection_helper.cpp │ ├── tcp.cpp │ ├── udp.cpp │ └── utils.cpp ├── src ├── modbus_client.cpp ├── modbus_ip_client.cpp ├── modbus_rtu_client.cpp └── utils.cpp └── tests ├── CMakeLists.txt ├── test_rtu.cpp └── test_serial_helper.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: true 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Right 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Never 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortEnumsOnASingleLine: false 18 | AllowShortFunctionsOnASingleLine: None 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortIfStatementsOnASingleLine: Never 21 | AllowShortLoopsOnASingleLine: false 22 | AlwaysBreakAfterDefinitionReturnType: None 23 | AlwaysBreakAfterReturnType: None 24 | AlwaysBreakBeforeMultilineStrings: false 25 | AlwaysBreakTemplateDeclarations: MultiLine 26 | BinPackArguments: true 27 | BinPackParameters: true 28 | BraceWrapping: 29 | AfterCaseLabel: false 30 | AfterClass: false 31 | AfterControlStatement: false 32 | AfterEnum: false 33 | AfterFunction: false 34 | AfterNamespace: false 35 | AfterObjCDeclaration: false 36 | AfterStruct: false 37 | AfterUnion: false 38 | AfterExternBlock: false 39 | BeforeCatch: false 40 | BeforeElse: false 41 | IndentBraces: false 42 | SplitEmptyFunction: true 43 | SplitEmptyRecord: true 44 | SplitEmptyNamespace: true 45 | BreakBeforeBinaryOperators: None 46 | BreakBeforeBraces: Attach 47 | BreakBeforeInheritanceComma: false 48 | BreakInheritanceList: BeforeColon 49 | BreakBeforeTernaryOperators: true 50 | BreakConstructorInitializersBeforeComma: false 51 | BreakConstructorInitializers: AfterColon 52 | BreakAfterJavaFieldAnnotations: false 53 | BreakStringLiterals: true 54 | ColumnLimit: 120 55 | CommentPragmas: '^ IWYU pragma:' 56 | CompactNamespaces: false 57 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 58 | ConstructorInitializerIndentWidth: 4 59 | ContinuationIndentWidth: 4 60 | Cpp11BracedListStyle: true 61 | DeriveLineEnding: true 62 | DerivePointerAlignment: false 63 | DisableFormat: false 64 | ExperimentalAutoDetectBinPacking: false 65 | FixNamespaceComments: true 66 | ForEachMacros: 67 | - foreach 68 | - Q_FOREACH 69 | - BOOST_FOREACH 70 | IncludeBlocks: Preserve 71 | IncludeCategories: 72 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 73 | Priority: 2 74 | SortPriority: 0 75 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 76 | Priority: 3 77 | SortPriority: 0 78 | - Regex: '.*' 79 | Priority: 1 80 | SortPriority: 0 81 | IncludeIsMainRegex: '(Test)?$' 82 | IncludeIsMainSourceRegex: '' 83 | IndentCaseLabels: false 84 | IndentGotoLabels: true 85 | IndentPPDirectives: None 86 | IndentWidth: 4 87 | IndentWrappedFunctionNames: false 88 | JavaScriptQuotes: Leave 89 | JavaScriptWrapImports: true 90 | KeepEmptyLinesAtTheStartOfBlocks: true 91 | MacroBlockBegin: '' 92 | MacroBlockEnd: '' 93 | MaxEmptyLinesToKeep: 1 94 | NamespaceIndentation: None 95 | ObjCBinPackProtocolList: Auto 96 | ObjCBlockIndentWidth: 2 97 | ObjCSpaceAfterProperty: false 98 | ObjCSpaceBeforeProtocolList: true 99 | PenaltyBreakAssignment: 2 100 | PenaltyBreakBeforeFirstCallParameter: 19 101 | PenaltyBreakComment: 300 102 | PenaltyBreakFirstLessLess: 120 103 | PenaltyBreakString: 1000 104 | PenaltyBreakTemplateDeclaration: 10 105 | PenaltyExcessCharacter: 1000000 106 | PenaltyReturnTypeOnItsOwnLine: 60 107 | PointerAlignment: Left 108 | ReflowComments: true 109 | SortIncludes: true 110 | SortUsingDeclarations: true 111 | SpaceAfterCStyleCast: false 112 | SpaceAfterLogicalNot: false 113 | SpaceAfterTemplateKeyword: true 114 | SpaceBeforeAssignmentOperators: true 115 | SpaceBeforeCpp11BracedList: false 116 | SpaceBeforeCtorInitializerColon: true 117 | SpaceBeforeInheritanceColon: true 118 | SpaceBeforeParens: ControlStatements 119 | SpaceBeforeRangeBasedForLoopColon: true 120 | SpaceInEmptyBlock: false 121 | SpaceInEmptyParentheses: false 122 | SpacesBeforeTrailingComments: 1 123 | SpacesInAngles: false 124 | SpacesInConditionalStatement: false 125 | SpacesInContainerLiterals: true 126 | SpacesInCStyleCastParentheses: false 127 | SpacesInParentheses: false 128 | SpacesInSquareBrackets: false 129 | SpaceBeforeSquareBrackets: false 130 | Standard: Latest 131 | StatementMacros: 132 | - Q_UNUSED 133 | - QT_REQUIRE_VERSION 134 | TabWidth: 8 135 | UseCRLF: false 136 | UseTab: Never 137 | ... 138 | 139 | -------------------------------------------------------------------------------- /.github/workflows/checkmarx-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This is a basic workflow to help you get started with Using Checkmarx CxFlow Action 7 | 8 | name: CxFlow 9 | 10 | on: 11 | push: 12 | branches: [ main ] 13 | pull_request: 14 | # The branches below must be a subset of the branches above 15 | branches: [ main ] 16 | schedule: 17 | - cron: '31 4 * * 3' 18 | 19 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel - this job is specifically configured to use the Checkmarx CxFlow Action 20 | jobs: 21 | # This workflow contains a single job called "build" 22 | build: 23 | # The type of runner that the job will run on - Ubuntu is required as Docker is leveraged for the action 24 | runs-on: ubuntu-latest 25 | 26 | # Steps require - checkout code, run CxFlow Action, Upload SARIF report (optional) 27 | steps: 28 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 29 | - uses: actions/checkout@v2 30 | # Runs the Checkmarx Scan leveraging the latest version of CxFlow - REFER to Action README for list of inputs 31 | - name: Checkmarx CxFlow Action 32 | uses: checkmarx-ts/checkmarx-cxflow-github-action@04e6403dbbfee0fd3fb076e5791202c31c54fe6b 33 | with: 34 | project: GithubActionTest 35 | team: '\CxServer\SP\Checkmarx' 36 | checkmarx_url: ${{ secrets.CHECKMARX_URL }} 37 | checkmarx_username: ${{ secrets.CHECKMARX_USERNAME }} 38 | checkmarx_password: ${{ secrets.CHECKMARX_PASSWORD }} 39 | checkmarx_client_secret: ${{ secrets.CHECKMARX_CLIENT_SECRET }} 40 | # Upload the Report for CodeQL/Security Alerts 41 | - name: Upload SARIF file 42 | uses: github/codeql-action/upload-sarif@v1 43 | with: 44 | sarif_file: cx.sarif 45 | -------------------------------------------------------------------------------- /.github/workflows/codacy-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow checks out code, performs a Codacy security scan 7 | # and integrates the results with the 8 | # GitHub Advanced Security code scanning feature. For more information on 9 | # the Codacy security scan action usage and parameters, see 10 | # https://github.com/codacy/codacy-analysis-cli-action. 11 | # For more information on Codacy Analysis CLI in general, see 12 | # https://github.com/codacy/codacy-analysis-cli. 13 | 14 | name: Codacy Security Scan 15 | 16 | on: 17 | push: 18 | branches: [ main ] 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: [ main ] 22 | schedule: 23 | - cron: '45 0 * * 1' 24 | 25 | jobs: 26 | codacy-security-scan: 27 | name: Codacy Security Scan 28 | runs-on: ubuntu-latest 29 | steps: 30 | # Checkout the repository to the GitHub Actions runner 31 | - name: Checkout code 32 | uses: actions/checkout@v2 33 | 34 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 35 | - name: Run Codacy Analysis CLI 36 | uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b 37 | with: 38 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 39 | # You can also omit the token and run the tools that support default configurations 40 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 41 | verbose: true 42 | output: results.sarif 43 | format: sarif 44 | # Adjust severity of non-security issues 45 | gh-code-scanning-compat: true 46 | # Force 0 exit code to allow SARIF file generation 47 | # This will handover control about PR rejection to the GitHub side 48 | max-allowed-issues: 2147483647 49 | 50 | # Upload the SARIF file generated in the previous step 51 | - name: Upload SARIF results file 52 | uses: github/codeql-action/upload-sarif@v1 53 | with: 54 | sarif_file: results.sarif 55 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '28 21 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'cpp' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/devskim-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: DevSkim 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | branches: [ main ] 13 | schedule: 14 | - cron: '19 13 * * 6' 15 | 16 | jobs: 17 | lint: 18 | name: DevSkim 19 | runs-on: ubuntu-20.04 20 | permissions: 21 | actions: read 22 | contents: read 23 | security-events: write 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Run DevSkim scanner 29 | uses: microsoft/DevSkim-Action@v1 30 | 31 | - name: Upload DevSkim scan results to GitHub Security tab 32 | uses: github/codeql-action/upload-sarif@v1 33 | with: 34 | sarif_file: devskim-results.sarif 35 | -------------------------------------------------------------------------------- /.github/workflows/flawfinder-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: flawfinder 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [ main ] 14 | schedule: 15 | - cron: '15 6 * * 2' 16 | 17 | jobs: 18 | flawfinder: 19 | name: Flawfinder 20 | runs-on: ubuntu-latest 21 | permissions: 22 | actions: read 23 | contents: read 24 | security-events: write 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v2 28 | 29 | - name: flawfinder_scan 30 | uses: david-a-wheeler/flawfinder@8e4a779ad59dbfaee5da586aa9210853b701959c 31 | with: 32 | arguments: '--sarif ./' 33 | output: 'flawfinder_results.sarif' 34 | 35 | - name: Upload analysis results to GitHub Security tab 36 | uses: github/codeql-action/upload-sarif@v1 37 | with: 38 | sarif_file: ${{github.workspace}}/flawfinder_results.sarif 39 | -------------------------------------------------------------------------------- /.github/workflows/msvc-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # 6 | # Find more information at: 7 | # https://github.com/microsoft/msvc-code-analysis-action 8 | 9 | name: Microsoft C++ Code Analysis 10 | 11 | on: 12 | push: 13 | branches: [ main ] 14 | pull_request: 15 | branches: [ main ] 16 | schedule: 17 | - cron: '39 1 * * 6' 18 | 19 | env: 20 | # Path to the CMake build directory. 21 | build: '${{ github.workspace }}/build' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: windows-latest 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v2 31 | 32 | - name: Configure CMake 33 | run: cmake -B ${{ env.build }} 34 | 35 | # Build is not required unless generated source files are used 36 | # - name: Build CMake 37 | # run: cmake --build ${{ env.build }} 38 | 39 | - name: Initialize MSVC Code Analysis 40 | uses: microsoft/msvc-code-analysis-action@04825f6d9e00f87422d6bf04e1a38b1f3ed60d99 41 | # Provide a unique ID to access the sarif output path 42 | id: run-analysis 43 | with: 44 | cmakeBuildDirectory: ${{ env.build }} 45 | # Ruleset file that will determine what checks will be run 46 | ruleset: NativeRecommendedRules.ruleset 47 | 48 | # Upload SARIF file to GitHub Code Scanning Alerts 49 | - name: Upload SARIF to GitHub 50 | uses: github/codeql-action/upload-sarif@v1 51 | with: 52 | sarif_file: ${{ steps.run-analysis.outputs.sarif }} 53 | 54 | # Upload SARIF file as an Artifact to download and view 55 | # - name: Upload SARIF as an Artifact 56 | # uses: actions/upload-artifact@v2 57 | # with: 58 | # name: sarif-file 59 | # path: ${{ steps.run-analysis.outputs.sarif }} 60 | -------------------------------------------------------------------------------- /.github/workflows/synopsys-io-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Synopsys Intelligent Security Scan 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [ main ] 14 | schedule: 15 | - cron: '39 15 * * 5' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | permissions: 22 | actions: read 23 | contents: read 24 | security-events: write 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | 30 | - name: Synopsys Intelligent Security Scan 31 | id: prescription 32 | uses: synopsys-sig/intelligent-security-scan@48eedfcd42bc342a294dc495ac452797b2d9ff08 33 | with: 34 | ioServerUrl: ${{secrets.IO_SERVER_URL}} 35 | ioServerToken: ${{secrets.IO_SERVER_TOKEN}} 36 | workflowServerUrl: ${{secrets.WORKFLOW_SERVER_URL}} 37 | additionalWorkflowArgs: --polaris.url=${{secrets.POLARIS_SERVER_URL}} --polaris.token=${{secrets.POLARIS_ACCESS_TOKEN}} 38 | stage: "IO" 39 | 40 | # Please note that the ID in previous step was set to prescription 41 | # in order for this logic to work also make sure that POLARIS_ACCESS_TOKEN 42 | # is defined in settings 43 | - name: Static Analysis with Polaris 44 | if: ${{steps.prescription.outputs.sastScan == 'true' }} 45 | run: | 46 | export POLARIS_SERVER_URL=${{ secrets.POLARIS_SERVER_URL}} 47 | export POLARIS_ACCESS_TOKEN=${{ secrets.POLARIS_ACCESS_TOKEN}} 48 | wget -q ${{ secrets.POLARIS_SERVER_URL}}/api/tools/polaris_cli-linux64.zip 49 | unzip -j polaris_cli-linux64.zip -d /tmp 50 | /tmp/polaris analyze -w 51 | 52 | # Please note that the ID in previous step was set to prescription 53 | # in order for this logic to work 54 | - name: Software Composition Analysis with Black Duck 55 | if: ${{steps.prescription.outputs.scaScan == 'true' }} 56 | uses: blackducksoftware/github-action@9ea442b34409737f64743781e9adc71fd8e17d38 57 | with: 58 | args: '--blackduck.url="${{ secrets.BLACKDUCK_URL}}" --blackduck.api.token="${{ secrets.BLACKDUCK_TOKEN}}" --detect.tools="SIGNATURE_SCAN,DETECTOR"' 59 | 60 | - name: Synopsys Intelligent Security Scan 61 | if: ${{ steps.prescription.outputs.sastScan == 'true' || steps.prescription.outputs.scaScan == 'true' }} 62 | uses: synopsys-sig/intelligent-security-scan@48eedfcd42bc342a294dc495ac452797b2d9ff08 63 | with: 64 | ioServerUrl: ${{secrets.IO_SERVER_URL}} 65 | ioServerToken: ${{secrets.IO_SERVER_TOKEN}} 66 | workflowServerUrl: ${{secrets.WORKFLOW_SERVER_URL}} 67 | additionalWorkflowArgs: --IS_SAST_ENABLED=${{steps.prescription.outputs.sastScan}} --IS_SCA_ENABLED=${{steps.prescription.outputs.scaScan}} 68 | --polaris.project.name={{PROJECT_NAME}} --polaris.url=${{secrets.POLARIS_SERVER_URL}} --polaris.token=${{secrets.POLARIS_ACCESS_TOKEN}} 69 | --blackduck.project.name={{PROJECT_NAME}}:{{PROJECT_VERSION}} --blackduck.url=${{secrets.BLACKDUCK_URL}} --blackduck.api.token=${{secrets.BLACKDUCK_TOKEN}} 70 | stage: "WORKFLOW" 71 | 72 | - name: Upload SARIF file 73 | if: ${{steps.prescription.outputs.sastScan == 'true' }} 74 | uses: github/codeql-action/upload-sarif@v1 75 | with: 76 | # Path to SARIF file relative to the root of the repository 77 | sarif_file: workflowengine-results.sarif.json 78 | -------------------------------------------------------------------------------- /.github/workflows/sysdig-scan-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Sysdig - Build, scan, push and upload sarif report 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | branches: [ main ] 13 | schedule: 14 | - cron: '44 21 * * 4' 15 | 16 | jobs: 17 | 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Build the Docker image 26 | # Tag image to be built 27 | # Change ${{ github.repository }} variable by another image name if you want but don't forget changing also image-tag below 28 | run: docker build . --file Dockerfile --tag ${{ github.repository }}:latest 29 | 30 | - name: Sysdig Secure Inline Scan 31 | id: scan 32 | uses: sysdiglabs/scan-action@768d7626a14897e0948ea89c8437dd46a814b163 33 | with: 34 | # Tag of the image to analyse. 35 | # Change ${{ github.repository }} variable by another image name if you want but don't forget changing also image-tag above 36 | image-tag: ${{ github.repository }}:latest 37 | # API token for Sysdig Scanning auth 38 | sysdig-secure-token: ${{ secrets.SYSDIG_SECURE_TOKEN}} 39 | # Sysdig secure endpoint. Please read: https://docs.sysdig.com/en/docs/administration/saas-regions-and-ip-ranges/ 40 | # US-East https://secure.sysdig.com 41 | # US-West https://us2.app.sysdig.com 42 | # EU https://eu1.app.sysdig.com 43 | sysdig-secure-url: https://us2.app.sysdig.com 44 | dockerfile-path: ./Dockerfile 45 | input-type: docker-daemon 46 | ignore-failed-scan: true 47 | # Sysdig inline scanner requires privileged rights 48 | run-as-user: root 49 | 50 | - uses: github/codeql-action/upload-sarif@v1 51 | #Upload SARIF file 52 | if: always() 53 | with: 54 | sarif_file: ${{ steps.scan.outputs.sarifReport }} 55 | -------------------------------------------------------------------------------- /.github/workflows/veracode-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow will initiate a Veracode Static Analysis Pipeline scan, return a results.json and convert to SARIF for upload as a code scanning alert 2 | 3 | name: Veracode Static Analysis Pipeline Scan 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | # The branches below must be a subset of the branches above 10 | branches: [ main ] 11 | schedule: 12 | - cron: '26 0 * * 5' 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a job to build and submit pipeline scan, you will need to customize the build process accordingly and make sure the artifact you build is used as the file input to the pipeline scan file parameter 17 | build-and-pipeline-scan: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | steps: 21 | 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it and copies all sources into ZIP file for submitting for analysis. Replace this section with your applications build steps 23 | - uses: actions/checkout@v2 24 | with: 25 | repository: '' 26 | 27 | - uses: papeloto/action-zip@v1 28 | with: 29 | files: / 30 | recursive: true 31 | dest: veracode-pipeline-scan-results-to-sarif.zip 32 | 33 | - uses: actions/upload-artifact@v1 34 | with: 35 | name: my-artifact 36 | path: veracode-pipeline-scan-results-to-sarif.zip 37 | 38 | # download the Veracode Static Analysis Pipeline scan jar 39 | - uses: wei/curl@master 40 | with: 41 | args: -O https://downloads.veracode.com/securityscan/pipeline-scan-LATEST.zip 42 | - run: unzip -o pipeline-scan-LATEST.zip 43 | 44 | - uses: actions/setup-java@v1 45 | with: 46 | java-version: 1.8 47 | - run: java -jar pipeline-scan.jar --veracode_api_id "${{secrets.VERACODE_API_ID}}" --veracode_api_key "${{secrets.VERACODE_API_KEY}}" --fail_on_severity="Very High, High" --file veracode-pipeline-scan-results-to-sarif.zip 48 | continue-on-error: true 49 | - uses: actions/upload-artifact@v1 50 | with: 51 | name: ScanResults 52 | path: results.json 53 | - name: Convert pipeline scan output to SARIF format 54 | id: convert 55 | uses: veracode/veracode-pipeline-scan-results-to-sarif@master 56 | with: 57 | pipeline-results-json: results.json 58 | - uses: github/codeql-action/upload-sarif@v1 59 | with: 60 | # Path to SARIF file relative to the root of the repository 61 | sarif_file: veracode-results.sarif 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .vscode 3 | CMakeCache.txt 4 | *.swp 5 | workspace.yaml 6 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | 3 | project(modbus 4 | VERSION 0.3.1 5 | ) 6 | 7 | find_package(everest-cmake 0.1 REQUIRED 8 | PATHS ../everest-cmake 9 | ) 10 | 11 | # options 12 | option(BUILD_EXAMPLES "Build example programs" OFF) 13 | option(MODBUS_INSTALL "Install the library (shared data might be installed anyway)" ${EVC_MAIN_PROJECT}) 14 | option(${PROJECT_NAME}_BUILD_TESTING "Build unit tests, used if included as dependency" OFF) 15 | option(BUILD_TESTING "Build unit tests, used if standalone project" OFF) 16 | if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING) 17 | set(LIBMODBUS_BUILD_TESTING ON) 18 | endif() 19 | # dependencies 20 | if (NOT DISABLE_EDM) 21 | evc_setup_edm() 22 | 23 | # In EDM mode, we can't install exports (because the dependencies usually do not install their exports) 24 | set(MODBUS_INSTALL OFF) 25 | else() 26 | find_package(everest-log REQUIRED) 27 | 28 | if(LIBMODBUS_BUILD_TESTING) 29 | find_package(GTest REQUIRED) 30 | endif() 31 | endif() 32 | 33 | add_subdirectory(lib/connection) 34 | 35 | add_library(modbus) 36 | add_library(everest::modbus ALIAS modbus) 37 | target_sources(modbus 38 | PRIVATE 39 | src/modbus_client.cpp 40 | src/modbus_ip_client.cpp 41 | src/modbus_rtu_client.cpp 42 | src/utils.cpp 43 | ) 44 | 45 | target_link_libraries(modbus 46 | PUBLIC 47 | modbus::connection 48 | PRIVATE 49 | everest::log 50 | ) 51 | 52 | target_include_directories(modbus 53 | PUBLIC 54 | $ 55 | $ 56 | ) 57 | 58 | # packaging 59 | if (MODBUS_INSTALL) 60 | install( 61 | TARGETS modbus modbus_connection 62 | EXPORT modbus-targets 63 | ) 64 | 65 | install( 66 | DIRECTORY include/ lib/connection/include/ 67 | TYPE INCLUDE 68 | ) 69 | 70 | evc_setup_package( 71 | NAME everest-modbus 72 | NAMESPACE everest 73 | EXPORT modbus-targets 74 | ADDITIONAL_CONTENT 75 | "find_dependency(everest-log)" 76 | ) 77 | endif() 78 | 79 | if (BUILD_EXAMPLES) 80 | add_subdirectory(${PROJECT_SOURCE_DIR}/examples) 81 | add_subdirectory(${PROJECT_SOURCE_DIR}/lib/connection/examples) 82 | endif() 83 | 84 | if(LIBMODBUS_BUILD_TESTING) 85 | include(CTest) 86 | enable_testing() 87 | add_subdirectory(${PROJECT_SOURCE_DIR}/tests) 88 | endif() 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EVerest - MODBUS 2 | 3 | This is an (not fully done) implementation of the MODBUS communication protocol. 4 | 5 | All documentation and the issue tracking can be found in our main repository here: https://github.com/EVerest/everest 6 | 7 | ### Getting started 8 | 9 | The easiest way to build the project is with CMake. In order to execute the examples, make sure you are in the root folder and execute: 10 | 11 | ``` 12 | cmake -S . -B build/ 13 | make -C build 14 | ``` 15 | 16 | After this is done, the examples should be compiled and saved into the build folder. 17 | -------------------------------------------------------------------------------- /THIRD_PARTY.md: -------------------------------------------------------------------------------- 1 | _Use this file to list out any third-party dependencies used by this project. You may choose to point to a Gemfile or other language specific packaging file for details._ 2 | -------------------------------------------------------------------------------- /dependencies.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | liblog: 3 | git: https://github.com/EVerest/liblog.git 4 | git_tag: v0.2.1 5 | options: ["BUILD_EXAMPLES OFF"] 6 | 7 | gtest: 8 | # GoogleTest now follows the Abseil Live at Head philosophy. We recommend updating to the latest commit in the main branch as often as possible. 9 | git: https://github.com/google/googletest.git 10 | git_tag: release-1.12.1 11 | cmake_condition: "LIBMODBUS_BUILD_TESTING" 12 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(modbus_tcp_example modbus_tcp_client_example.cpp) 2 | target_link_libraries(modbus_tcp_example 3 | PRIVATE modbus 4 | ) 5 | 6 | add_executable(modbus_udp_client_example modbus_udp_client_example.cpp) 7 | target_link_libraries(modbus_udp_client_example 8 | PRIVATE modbus 9 | ) 10 | -------------------------------------------------------------------------------- /examples/modbus_tcp_client_example.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | int main() { 12 | everest::connection::TCPConnection conn("127.0.0.1", 502); 13 | if (!conn.is_valid()) 14 | return 0; 15 | everest::modbus::ModbusIPClient client(conn); 16 | std::vector v = client.read_holding_register(1, 40000, 1); 17 | everest::modbus::utils::print_message_hex(v); 18 | } 19 | -------------------------------------------------------------------------------- /examples/modbus_udp_client_example.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | int main() { 12 | everest::connection::UDPConnection conn("127.0.0.1", 502); 13 | if (!conn.is_valid()) 14 | return 0; 15 | everest::modbus::ModbusIPClient client(conn); 16 | std::vector v = client.read_holding_register(1, 40000, 1); 17 | everest::modbus::utils::print_message_hex(v); 18 | } 19 | -------------------------------------------------------------------------------- /include/consts.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #ifndef MODBUS_CONSTS_H 4 | #define MODBUS_CONSTS_H 5 | 6 | #include 7 | 8 | namespace everest { 9 | namespace modbus { 10 | namespace consts { 11 | 12 | // General MODBUS constants 13 | // TODO: this constant should have the value 5...? 14 | constexpr uint16_t READ_REGISTER_COMMAND_LENGTH = 6; 15 | constexpr uint8_t READ_HOLDING_REGISTER_FUNCTION_CODE = 3; 16 | constexpr uint8_t READ_INPUT_REGISTER_FUNCTION_CODE = 4; 17 | 18 | // MODBUS/RTU specific constants 19 | namespace rtu { 20 | constexpr uint16_t MAX_ADU = 256; 21 | constexpr uint16_t MAX_PDU = 253; 22 | constexpr uint16_t MAX_REGISTER_PER_MESSAGE = 125; 23 | } // namespace rtu 24 | 25 | // MODBUS/TCP specific constants 26 | namespace tcp { 27 | constexpr uint16_t MAX_ADU = 260; 28 | constexpr uint16_t MAX_PDU = 253; 29 | constexpr uint16_t PROTOCOL_ID = 0; 30 | constexpr uint8_t MBAP_HEADER_LENGTH = 7; 31 | constexpr uint16_t DEFAULT_PORT = 502; 32 | } // namespace tcp 33 | 34 | } // namespace consts 35 | } // namespace modbus 36 | }; // namespace everest 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /include/modbus/exceptions.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #ifndef MODBUS_EXCEPTIONS_H 4 | #define MODBUS_EXCEPTIONS_H 5 | 6 | #include 7 | 8 | namespace everest { 9 | 10 | namespace modbus { 11 | 12 | namespace exceptions { 13 | 14 | class message_size_exception : public std::runtime_error { 15 | public: 16 | explicit message_size_exception(const std::string& what_arg) : std::runtime_error(what_arg) {} 17 | }; 18 | 19 | class unmatched_response : public std::runtime_error { 20 | public: 21 | explicit unmatched_response(const std::string& what_arg) : std::runtime_error(what_arg) {} 22 | }; 23 | 24 | class checksum_error : public std::runtime_error { 25 | public: 26 | explicit checksum_error( const std::string& what_arg ) : std::runtime_error ( what_arg ) {} 27 | }; 28 | 29 | class modbus_exception : public std::runtime_error { 30 | public: 31 | std::uint8_t modbus_exception_code; 32 | modbus_exception( const std::string& what_arg , std::uint8_t exception_code ) : 33 | std::runtime_error ( what_arg ), 34 | modbus_exception_code( exception_code ) 35 | {} 36 | }; 37 | 38 | class empty_response : public std::runtime_error { 39 | public: 40 | explicit empty_response( const std::string& what_arg ) : std::runtime_error ( what_arg ) {} 41 | }; 42 | 43 | class should_never_happen : public std::runtime_error { 44 | public: 45 | explicit should_never_happen( const std::string& what_arg ) : std::runtime_error ( what_arg ) {} 46 | }; 47 | 48 | } // namespace exceptions 49 | } // namespace modbus 50 | }; // namespace everest 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /include/modbus/modbus_client.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #ifndef MODBUS_CLIENT_H 5 | #define MODBUS_CLIENT_H 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace everest { 15 | namespace modbus { 16 | 17 | class ModbusClient { 18 | public: 19 | ModbusClient(connection::Connection& conn_); 20 | virtual ~ModbusClient() = default; 21 | // read_holding_register needs to be virtual, since the rtu format differs from the ip/udp formats. 22 | virtual const std::vector read_holding_register(uint8_t unit_id, uint16_t first_register_address, 23 | uint16_t num_registers_to_read, 24 | bool return_only_registers_bytes = true) const; 25 | 26 | protected: 27 | const virtual std::vector full_message_from_body(const std::vector& body, uint16_t message_length, 28 | uint8_t unit_id) const = 0; 29 | 30 | virtual uint16_t validate_response(const std::vector& response, 31 | const std::vector& request) const = 0; 32 | 33 | // message size including protocol data (addressing, error check, mbap) 34 | virtual std::size_t max_adu_size() const = 0; 35 | // message size without protocol data (addressing, error check, mbap), function code and payload data only 36 | virtual std::size_t max_pdu_size() const = 0; 37 | 38 | ModbusClient(const ModbusClient&) = delete; 39 | ModbusClient& operator=(const ModbusClient&) = delete; 40 | connection::Connection& conn; 41 | }; 42 | 43 | class ModbusIPClient : public ModbusClient { 44 | public: 45 | ModbusIPClient(connection::Connection& conn_); 46 | virtual ~ModbusIPClient() = default; 47 | const std::vector full_message_from_body(const std::vector& body, uint16_t message_length, 48 | uint8_t unit_id) const override; 49 | uint16_t validate_response(const std::vector& response, 50 | const std::vector& request) const override; 51 | // message size including protocol data (addressing, error check, mbap) 52 | virtual std::size_t max_adu_size() const override { 53 | return everest::modbus::consts::tcp::MAX_ADU; 54 | } 55 | // message size without protocol data (addressing, error check, mbap), function code and payload data only 56 | virtual std::size_t max_pdu_size() const override { 57 | return everest::modbus::consts::tcp::MAX_PDU; 58 | } 59 | }; 60 | 61 | class ModbusTCPClient : public ModbusIPClient { 62 | public: 63 | ModbusTCPClient(connection::TCPConnection& conn_); 64 | ~ModbusTCPClient() override = default; 65 | }; 66 | 67 | class ModbusUDPClient : public ModbusIPClient { 68 | public: 69 | ModbusUDPClient(connection::UDPConnection& conn_); 70 | ~ModbusUDPClient() override = default; 71 | }; 72 | 73 | using DataVectorUint16 = std::vector; 74 | using DataVectorUint8 = std::vector; 75 | 76 | enum struct ByteOrder { 77 | BigEndian, 78 | LittleEndian 79 | }; 80 | 81 | class ModbusDataContainerUint16 { 82 | 83 | public: 84 | ModbusDataContainerUint16(ByteOrder byte_order, // which byteorder does the parameter payload have 85 | const DataVectorUint16& payload) : 86 | m_byte_order(byte_order), m_payload(payload) { 87 | } 88 | 89 | DataVectorUint8 get_payload_as_bigendian() const; 90 | 91 | std::size_t size() const { 92 | return m_payload.size(); 93 | } 94 | 95 | protected: 96 | ByteOrder m_byte_order; 97 | DataVectorUint16 m_payload; 98 | }; 99 | 100 | class ModbusRTUClient : public ModbusClient { 101 | public: 102 | ModbusRTUClient(connection::Connection& conn_, bool ignore_echo); 103 | explicit ModbusRTUClient(connection::Connection& conn_) : ModbusRTUClient(conn_, false){} 104 | virtual ~ModbusRTUClient() override; 105 | 106 | // throws derived from std::runtime_error, see include/modbus/exceptions.hpp 107 | const DataVectorUint8 read_holding_register(uint8_t unit_id, uint16_t first_register_address, 108 | uint16_t num_registers_to_read, 109 | bool return_only_registers_bytes = true) const override; 110 | 111 | // HACK warning! No virtual method! 112 | const DataVectorUint8 read_input_register(uint8_t unit_id, uint16_t first_register_address, 113 | uint16_t num_registers_to_read, 114 | bool return_only_registers_bytes = true) const; 115 | // throws derived from std::runtime_error, see include/modbus/exceptions.hpp 116 | DataVectorUint8 write_multiple_registers( 117 | uint8_t unit_id, uint16_t first_register_address, uint16_t num_registers_to_write, 118 | const ModbusDataContainerUint16& payload, 119 | bool return_only_registers_bytes) const; // errors will be reported by exception std::runtime_error 120 | 121 | // message size including protocol data (addressing, error check, mbap) 122 | virtual std::size_t max_adu_size() const override { 123 | return everest::modbus::consts::rtu::MAX_ADU; 124 | } 125 | // message size without protocol data (addressing, error check, mbap), function code and payload data only 126 | virtual std::size_t max_pdu_size() const override { 127 | return everest::modbus::consts::rtu::MAX_PDU; 128 | } 129 | 130 | static DataVectorUint8 response_without_protocol_data(const DataVectorUint8& raw_response, 131 | std::size_t payload_length); 132 | 133 | protected: 134 | const DataVectorUint8 full_message_from_body(const DataVectorUint8& body, uint16_t message_length, 135 | std::uint8_t unit_id) const override; 136 | uint16_t validate_response(const DataVectorUint8& response, const DataVectorUint8& request) const override; 137 | bool ignore_echo; 138 | }; 139 | 140 | } // namespace modbus 141 | }; // namespace everest 142 | #endif 143 | -------------------------------------------------------------------------------- /include/modbus/utils.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #ifndef MODBUS_UTILS_H 4 | #define MODBUS_UTILS_H 5 | 6 | #include 7 | #include 8 | 9 | #include "modbus_client.hpp" 10 | 11 | namespace everest { 12 | namespace modbus { 13 | namespace utils { 14 | 15 | // General use utils 16 | std::vector build_read_command_message_body(std::uint8_t function_code, uint16_t first_register_address, 17 | uint16_t num_registers_to_read); 18 | std::vector build_read_holding_register_message_body(uint16_t first_register_address, 19 | uint16_t num_registers_to_read); 20 | std::vector build_read_input_register_message_body(uint16_t first_register_address, 21 | uint16_t num_registers_to_read); 22 | std::vector build_write_multiple_register_body(uint16_t first_register_address, 23 | uint16_t num_registers_to_write, 24 | const ::everest::modbus::ModbusDataContainerUint16& payload); 25 | std::vector extract_body_from_response(const std::vector& response, int num_data_bytes); 26 | std::vector extract_registers_bytes_from_response_body(const std::vector& response_body); 27 | std::vector extract_register_bytes_from_response(const std::vector& response, int num_data_bytes); 28 | void print_message_hex(const std::vector& message); 29 | void print_message_first_N_bytes(unsigned char* message, int N); 30 | 31 | using PayloadType = unsigned char; 32 | using CRCResultType = std::uint16_t; 33 | CRCResultType calcCRC_16_ANSI(const PayloadType* payload, std::size_t payload_length); 34 | 35 | // MODBUS/IP specific utils 36 | namespace ip { 37 | // Utility funcs 38 | std::vector make_mbap_header(uint16_t message_length, uint8_t unit_id); 39 | uint16_t check_mbap_header(const std::vector& sent_message, const std::vector& received_message); 40 | } // namespace ip 41 | 42 | } // namespace utils 43 | } // namespace modbus 44 | }; // namespace everest 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /lib/connection/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(modbus_connection) 2 | add_library(modbus::connection ALIAS modbus_connection) 3 | set_target_properties(modbus_connection PROPERTIES OUTPUT_NAME modbus_connection) 4 | target_sources(modbus_connection 5 | PRIVATE 6 | src/rtu.cpp 7 | src/serial_connection_helper.cpp 8 | src/tcp.cpp 9 | src/udp.cpp 10 | src/utils.cpp 11 | ) 12 | 13 | target_include_directories(modbus_connection 14 | PUBLIC 15 | $ 16 | ) 17 | 18 | target_link_libraries(modbus_connection 19 | PRIVATE everest::log 20 | ) 21 | -------------------------------------------------------------------------------- /lib/connection/examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(tcp_connection_example connection_example.cpp) 2 | target_link_libraries(tcp_connection_example 3 | PRIVATE modbus_connection 4 | ) 5 | 6 | add_executable(udp_connection_example udp_connection_example.cpp) 7 | target_link_libraries(udp_connection_example 8 | PRIVATE modbus_connection 9 | ) 10 | -------------------------------------------------------------------------------- /lib/connection/examples/connection_example.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | // simple example 12 | int main() { 13 | std::unique_ptr tcp_conn = std::make_unique("127.0.0.1", 502); 14 | tcp_conn->make_connection(); 15 | std::vector bytes({0x3F, 0x67, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x9C, 0x41, 0x00, 0x01}); 16 | tcp_conn->send_bytes(bytes); 17 | std::vector received_bytes = tcp_conn->receive_bytes(40); 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /lib/connection/examples/udp_connection_example.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | int main() { 12 | everest::connection::UDPConnection udp_conn("127.0.0.1", 502); 13 | std::vector bytes({0x3F, 0x67, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x9C, 0x41, 0x00, 0x01}); 14 | udp_conn.send_bytes(bytes); 15 | std::vector received_bytes = udp_conn.receive_bytes(40); 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /lib/connection/include/connection/connection.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // TODO: move this into a more beautiful place... the class SerialDevice is not a helper anymore. 12 | #include 13 | 14 | namespace everest { 15 | namespace connection { 16 | class Connection { 17 | private: 18 | Connection(const Connection&) = delete; 19 | Connection& operator=(const Connection&) = delete; 20 | 21 | protected: 22 | int connection_status; 23 | 24 | public: 25 | Connection() : connection_status(-1){}; 26 | virtual ~Connection() = default; 27 | virtual int make_connection() = 0; 28 | virtual int close_connection() = 0; 29 | virtual int send_bytes(const std::vector& bytes_to_send) = 0; 30 | // result of receive_bytes is a vector that has the size of received bytes 31 | virtual std::vector receive_bytes(unsigned int number_of_bytes) = 0; 32 | virtual bool is_valid() const = 0; 33 | }; 34 | 35 | class TCPConnection : public Connection { 36 | private: 37 | int port; 38 | std::string address; 39 | int socket_fd; 40 | 41 | public: 42 | TCPConnection(const std::string& address_, const int& port_); 43 | ~TCPConnection(); 44 | int make_connection(); 45 | int send_bytes(const std::vector& bytes_to_send); 46 | std::vector receive_bytes(unsigned int number_of_bytes); 47 | int close_connection(); 48 | bool is_valid() const; 49 | }; 50 | 51 | class UDPConnection : public Connection { 52 | private: 53 | int port; 54 | std::string address; 55 | int socket_fd; 56 | 57 | public: 58 | UDPConnection(const std::string& address_, const int& port_); 59 | ~UDPConnection(); 60 | int make_connection(); 61 | int send_bytes(const std::vector& bytes_to_send); 62 | std::vector receive_bytes(unsigned int number_of_bytes); 63 | int close_connection(); 64 | bool is_valid() const; 65 | }; 66 | 67 | //////////////////////////////////////////////////////////////////////////////// 68 | // 69 | // RTUConnection assumes that there is only *one* device on the serial line, there 70 | // is no collision handling implemented. 71 | 72 | class RTUConnection : public Connection { 73 | private: 74 | ::everest::connection::SerialDevice& m_serial_device; 75 | 76 | public: 77 | explicit RTUConnection(::everest::connection::SerialDevice& serialDevice); 78 | ~RTUConnection() { 79 | m_serial_device.close(); 80 | } 81 | virtual int make_connection() override; // throws derived from std::runtime_error 82 | virtual int close_connection() override; 83 | virtual int 84 | send_bytes(const std::vector& bytes_to_send) override; // throws derived from std::runtime_error 85 | virtual std::vector 86 | receive_bytes(unsigned int number_of_bytes) override; // throws derived from std::runtime_error 87 | virtual bool is_valid() const override; 88 | }; 89 | 90 | } // namespace connection 91 | }; // namespace everest 92 | -------------------------------------------------------------------------------- /lib/connection/include/connection/exceptions.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace everest { 9 | 10 | namespace connection { 11 | 12 | namespace exceptions { 13 | 14 | class connection_error : public std::runtime_error { 15 | public: 16 | connection_error(const std::string& what_arg) : std::runtime_error(what_arg) { 17 | } 18 | }; 19 | 20 | class communication_error : public std::runtime_error { 21 | public: 22 | communication_error(const std::string& what_arg) : std::runtime_error(what_arg) { 23 | } 24 | }; 25 | 26 | namespace tcp { 27 | class tcp_connection_error : public connection_error { 28 | public: 29 | tcp_connection_error(const std::string& what_arg) : connection_error(what_arg) { 30 | } 31 | }; 32 | }; // namespace tcp 33 | 34 | namespace udp { 35 | class udp_socket_error : public connection_error { 36 | public: 37 | udp_socket_error(const std::string& what_arg) : connection_error(what_arg) { 38 | } 39 | }; 40 | }; // namespace udp 41 | 42 | namespace tty { 43 | class tty_error : public connection_error { 44 | 45 | public: 46 | int error_number; 47 | tty_error(const std::string& what_arg, int tty_error_number) : 48 | connection_error(what_arg), error_number(tty_error_number) { 49 | } 50 | }; 51 | } // namespace tty 52 | 53 | } // namespace exceptions 54 | } // namespace connection 55 | }; // namespace everest 56 | -------------------------------------------------------------------------------- /lib/connection/include/connection/serial_connection_helper.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #ifndef SERIAL_CONNECTION_HELPER_H_ 5 | #define SERIAL_CONNECTION_HELPER_H_ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace everest { 15 | namespace connection { 16 | 17 | struct SerialDeviceConfiguration { 18 | 19 | termios m_tty_config{}; 20 | std::string m_device; 21 | 22 | enum struct InitFromDevice { 23 | DoInit, 24 | DontInit 25 | }; 26 | 27 | unsigned int initial_read_timeout_deciseconds{50}; // wait for first input 28 | unsigned int default_read_timeout_deciseconds{2}; // max wait during reading / wait for end of transmission 29 | 30 | explicit SerialDeviceConfiguration(std::string device); // throws if the device could not be opened. 31 | 32 | SerialDeviceConfiguration() = default; 33 | 34 | enum struct BaudRate { 35 | Baud_0 = B0, 36 | Baud_50 = B50, 37 | Baud_75 = B75, 38 | Baud_110 = B110, 39 | Baud_134 = B134, 40 | Baud_150 = B150, 41 | Baud_200 = B200, 42 | Baud_300 = B300, 43 | Baud_600 = B600, 44 | Baud_1200 = B1200, 45 | Baud_1800 = B1800, 46 | Baud_2400 = B2400, 47 | Baud_4800 = B4800, 48 | Baud_9600 = B9600, 49 | Baud_19200 = B19200, 50 | Baud_38400 = B38400, 51 | Baud_57600 = B57600, 52 | Baud_115200 = B115200, 53 | Baud_230400 = B230400, 54 | Baud_460800 = B460800, 55 | Baud_500000 = B500000, 56 | Baud_576000 = B576000, 57 | Baud_921600 = B921600, 58 | Baud_1000000 = B1000000, 59 | Baud_1152000 = B1152000, 60 | Baud_1500000 = B1500000, 61 | Baud_2000000 = B2000000, 62 | }; 63 | 64 | struct BaudrateFromIntResult { 65 | BaudRate baud; 66 | bool conversion_ok; 67 | }; 68 | 69 | /** 70 | * convert integer to SerialDeviceConfiguration::BaudRate. Returns Baud_9600 in case the argument has no 71 | * corresponging SerialDeviceConfiguration::BaudRate value, and BaudrateFromIntResult.conversion_ok is set to false. 72 | * 73 | */ 74 | static BaudrateFromIntResult baudrate_from_integer(int); 75 | 76 | enum struct DataBits { 77 | Bit_8 = CS8, 78 | Bit_7 = CS7, 79 | Bit_6 = CS6, 80 | Bit_5 = CS5 81 | }; 82 | 83 | enum struct StopBits { 84 | One, 85 | Two 86 | }; 87 | 88 | enum struct Parity { 89 | None, 90 | Even, 91 | Odd 92 | }; 93 | 94 | void get_current_config(); 95 | 96 | SerialDeviceConfiguration& set_baud_rate(BaudRate); 97 | SerialDeviceConfiguration& set_data_bits(DataBits); 98 | SerialDeviceConfiguration& set_stop_bits(StopBits); 99 | SerialDeviceConfiguration& set_parity(Parity); 100 | 101 | // set cread, clocal, disable canonical, disable echo, disable signal chars 102 | SerialDeviceConfiguration& set_sensible_defaults(); 103 | }; 104 | 105 | static_assert(std::is_copy_constructible::value, 106 | "SerialDeviceConfiguration needs to be copy constructible!"); 107 | 108 | class SerialDevice { 109 | 110 | int m_fd = -1; 111 | SerialDeviceConfiguration m_serial_device_configuration; 112 | 113 | protected: 114 | SerialDevice() { 115 | } 116 | 117 | public: 118 | explicit SerialDevice(const SerialDeviceConfiguration& serialDeviceConfiguration) : 119 | m_serial_device_configuration(serialDeviceConfiguration) { 120 | } 121 | 122 | virtual void open(); 123 | virtual void close(); 124 | virtual SerialDeviceConfiguration& get_serial_device_config(); 125 | 126 | virtual ::size_t write(const unsigned char* const buffer, ::size_t count); 127 | virtual ::size_t read(unsigned char* buffer, ::size_t count); 128 | virtual void drain(); 129 | }; 130 | 131 | // can be used to record conversations with real hardware for later usage in tests. 132 | class SerialDeviceLogToStream : public SerialDevice { 133 | 134 | protected: 135 | std::ostream* m_stream; 136 | 137 | public: 138 | explicit SerialDeviceLogToStream(const SerialDeviceConfiguration& serialDeviceConfiguration, 139 | std::ostream* stream = &std::clog) : 140 | SerialDevice(serialDeviceConfiguration), m_stream(stream) { 141 | } 142 | 143 | virtual ::size_t write(const unsigned char* const buffer, ::size_t count) override; 144 | virtual ::size_t read(unsigned char* buffer, ::size_t count) override; 145 | 146 | void set_stream(std::ostream* stream) { 147 | m_stream = stream; 148 | } 149 | }; 150 | 151 | } // namespace connection 152 | } // namespace everest 153 | 154 | bool operator==(const termios& lhs, const termios& rhs); 155 | 156 | #endif // SERIAL_CONNECTION_HELPER_H_ 157 | -------------------------------------------------------------------------------- /lib/connection/include/connection/utils.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace everest { 12 | namespace connection { 13 | namespace utils { 14 | std::string get_bytes_hex_string(const std::vector& bytes); 15 | } 16 | } // namespace connection 17 | }; // namespace everest 18 | -------------------------------------------------------------------------------- /lib/connection/src/rtu.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace everest::connection; 9 | 10 | RTUConnection::RTUConnection(::everest::connection::SerialDevice& serialdevice) : m_serial_device(serialdevice) { 11 | make_connection(); 12 | } 13 | 14 | int RTUConnection::make_connection() { 15 | 16 | try { 17 | close_connection(); // is this ok? 18 | 19 | // serial device will be opened configured by the config object of m_serial_device 20 | m_serial_device.open(); 21 | 22 | connection_status = 1; // connection should be valid 23 | 24 | } catch (const std::runtime_error& e) { 25 | EVLOG_error << "Error making RTU connection: " << e.what() << std::endl; 26 | throw; 27 | } 28 | 29 | return connection_status; 30 | } 31 | 32 | int RTUConnection::close_connection() { 33 | 34 | connection_status = -1; 35 | m_serial_device.close(); 36 | return 0; 37 | } 38 | 39 | int RTUConnection::send_bytes(const std::vector& bytes_to_send) { 40 | 41 | try { 42 | auto bytes_written = m_serial_device.write(bytes_to_send.data(), bytes_to_send.size()); 43 | return bytes_written; 44 | } catch (const std::runtime_error& e) { 45 | close_connection(); 46 | EVLOG_error << "Error writing on RTU connection: " << e.what() << std::endl; 47 | throw; 48 | } 49 | return -1; 50 | } 51 | 52 | std::vector RTUConnection::receive_bytes(unsigned int number_of_bytes) { 53 | 54 | if (not is_valid()) { 55 | throw std::runtime_error("attempt to read on invalid rtu connection."); 56 | } 57 | 58 | std::vector result(number_of_bytes); 59 | 60 | try { 61 | auto bytes_read = m_serial_device.read(result.data(), number_of_bytes); 62 | result.resize(bytes_read); 63 | } catch (const std::runtime_error& e) { 64 | close_connection(); 65 | EVLOG_error << "Error reading on RTU connection: " << e.what() << std::endl; 66 | throw; 67 | } 68 | return result; 69 | } 70 | 71 | bool RTUConnection::is_valid() const { 72 | return connection_status != -1; 73 | } 74 | -------------------------------------------------------------------------------- /lib/connection/src/serial_connection_helper.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #include 5 | #include 6 | 7 | #include // Error integer and strerror() function 8 | #include // Contains file controls like O_RDWR 9 | #include 10 | #include // Contains POSIX terminal control definitions 11 | #include // write(), read(), close() 12 | 13 | // TODO: when SerialDevice class is mature enough, the serial_connection_helper can be moved there. 14 | namespace everest { 15 | namespace connection { 16 | namespace serial_connection_helper { 17 | 18 | int open_serial_device(const std::string& device); 19 | int close_serial_device(int serial_port_fd); 20 | void get_default_configuration(int serial_port_fd, termios* tty); 21 | void set_default_configuration(termios* tty); 22 | void update_timeout_configuration(termios* tty, unsigned int timeout_deciseconds); 23 | void set_baudrate(termios* tty, everest::connection::SerialDeviceConfiguration::BaudRate); 24 | void configure_device(int serial_port_fd, termios* tty); 25 | ::size_t write_to_device(int serial_port_fd, const unsigned char* const buffer, ::size_t count); 26 | ::size_t read_from_device(int serial_port_fd, unsigned char* buffer, ::size_t count, termios* tty_config, 27 | unsigned int initial_timeout_deciseconds, unsigned int timeout_deciseconds); 28 | } // namespace serial_connection_helper 29 | } // namespace connection 30 | } // namespace everest 31 | 32 | bool operator==(const termios& lhs, const termios& rhs) { 33 | return 34 | #if defined(_HAVE_STRUCT_TERMIOS_C_ISPEED) && _HAVE_STRUCT_TERMIOS_C_ISPEED 35 | (lhs.c_ispeed == rhs.c_ispeed) and 36 | #endif 37 | #if defined(_HAVE_STRUCT_TERMIOS_C_OSPEED) && _HAVE_STRUCT_TERMIOS_C_OSPEED 38 | (lhs.c_ospeed == rhs.c_ospeed) and 39 | #endif 40 | (lhs.c_cflag == rhs.c_cflag) and (lhs.c_iflag == rhs.c_iflag) and (lhs.c_lflag == rhs.c_lflag) and 41 | (lhs.c_line == rhs.c_line) and (lhs.c_oflag == rhs.c_oflag); 42 | } 43 | 44 | everest::connection::SerialDeviceConfiguration::BaudrateFromIntResult 45 | everest::connection::SerialDeviceConfiguration::baudrate_from_integer(int baudrate) { 46 | switch (baudrate) { 47 | case 1200: 48 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_1200, true}; 49 | case 2400: 50 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_2400, true}; 51 | case 4800: 52 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_4800, true}; 53 | case 9600: 54 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_9600, true}; 55 | case 19200: 56 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_19200, true}; 57 | case 38400: 58 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_38400, true}; 59 | case 57600: 60 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_57600, true}; 61 | case 115200: 62 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_115200, true}; 63 | case 230400: 64 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_230400, true}; 65 | default: 66 | return {everest::connection::SerialDeviceConfiguration::BaudRate::Baud_9600, false}; 67 | } 68 | } 69 | 70 | namespace ecs = everest::connection::serial_connection_helper; 71 | 72 | everest::connection::SerialDeviceConfiguration::SerialDeviceConfiguration(std::string device) : m_device(device) { 73 | 74 | int fd = ecs::open_serial_device(device); 75 | ecs::get_default_configuration(fd, &m_tty_config); 76 | ecs::close_serial_device(fd); 77 | } 78 | 79 | void everest::connection::SerialDevice::open() { 80 | 81 | close(); 82 | m_fd = ::ecs::open_serial_device(get_serial_device_config().m_device); 83 | ::ecs::configure_device(m_fd, &get_serial_device_config().m_tty_config); 84 | } 85 | 86 | void everest::connection::SerialDevice::close() { 87 | 88 | ::ecs::close_serial_device(m_fd); 89 | m_fd = -1; 90 | } 91 | 92 | everest::connection::SerialDeviceConfiguration& everest::connection::SerialDevice::get_serial_device_config() { 93 | 94 | return m_serial_device_configuration; 95 | } 96 | 97 | ::size_t everest::connection::SerialDevice::write(const unsigned char* const buffer, ::size_t count) { 98 | 99 | return ::ecs::write_to_device(m_fd, buffer, count); 100 | } 101 | 102 | void everest::connection::SerialDevice::drain() { 103 | tcdrain(m_fd); 104 | } 105 | 106 | ::size_t everest::connection::SerialDevice::read(unsigned char* buffer, ::size_t count) { 107 | 108 | return ::ecs::read_from_device(m_fd, buffer, count, &get_serial_device_config().m_tty_config, 109 | get_serial_device_config().initial_read_timeout_deciseconds, 110 | get_serial_device_config().default_read_timeout_deciseconds); 111 | } 112 | 113 | ::size_t everest::connection::SerialDeviceLogToStream::write(const unsigned char* const buffer, ::size_t count) { 114 | 115 | (*m_stream) << get_serial_device_config().m_device << " write: \n"; 116 | (*m_stream) << std::hex; 117 | for (::size_t index = 0; index < count; ++index) 118 | (*m_stream) << (unsigned)buffer[index] << " "; 119 | (*m_stream) << "\n\n" << std::flush; 120 | return SerialDevice::write(buffer, count); 121 | } 122 | 123 | ::size_t everest::connection::SerialDeviceLogToStream::read(unsigned char* buffer, ::size_t count) { 124 | 125 | auto delay = get_serial_device_config().m_tty_config.c_cc[VTIME]; 126 | ::size_t bytes_read = SerialDevice::read(buffer, count); 127 | (*m_stream) << get_serial_device_config().m_device << " read: \n with delay : " << (int)delay << "\n"; 128 | (*m_stream) << std::hex; 129 | for (::size_t index = 0; index < bytes_read; ++index) 130 | (*m_stream) << (unsigned)buffer[index] << " "; 131 | (*m_stream) << "\n\n" << std::flush; 132 | return bytes_read; 133 | } 134 | 135 | int ecs::open_serial_device(const std::string& device) { 136 | 137 | int fd = open(device.c_str(), O_RDWR); 138 | 139 | if (not(fd == -1)) 140 | return fd; 141 | 142 | throw everest::connection::exceptions::tty::tty_error( 143 | "Error open serial device: " + device + " Reason: " + strerror(errno), errno); 144 | } 145 | 146 | int ecs::close_serial_device(int serial_port_fd) { 147 | 148 | return ::close(serial_port_fd); 149 | } 150 | 151 | void ecs::get_default_configuration(int serial_port_fd, termios* tty) { 152 | 153 | if (tcgetattr(serial_port_fd, tty) != 0) { 154 | int myerror = errno; 155 | throw everest::connection::exceptions::tty::tty_error( 156 | "Error " + std::to_string(myerror) + " from tcgetattr: " + strerror(myerror), myerror); 157 | } 158 | } 159 | 160 | void ecs::set_default_configuration(termios* tty) { 161 | 162 | // we are a bit verbose with these settings... 163 | // BSM power meter used this as default: 164 | // 19200 Baud 165 | cfsetspeed(tty, B19200); 166 | // Parity 167 | tty->c_cflag |= PARENB; 168 | // 8 Data bits 169 | tty->c_cflag &= ~CSIZE; 170 | tty->c_cflag |= CS8; 171 | // 1 Stop bit 172 | tty->c_cflag &= ~CSTOPB; 173 | // no CRTSCTS (?) 174 | tty->c_cflag &= ~CRTSCTS; 175 | 176 | // turn off modem specific stuff... 177 | tty->c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1) 178 | 179 | // disable canonical mode (in canonical mode, data is processed after newline). 180 | tty->c_lflag &= ~ICANON; 181 | 182 | tty->c_lflag &= ~ECHO; // Disable echo 183 | tty->c_lflag &= ~ECHOE; // Disable erasure 184 | tty->c_lflag &= ~ECHONL; // Disable new-line echo 185 | tty->c_lflag &= ~ECHOCTL; // 186 | tty->c_lflag &= ~ECHOKE; 187 | tty->c_lflag &= ~ECHOK; 188 | 189 | tty->c_lflag &= ~IEXTEN; 190 | 191 | tty->c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP 192 | 193 | tty->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | 194 | IXON); // Disable any special handling of received bytes 195 | 196 | tty->c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars) 197 | tty->c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed 198 | } 199 | 200 | void ecs::update_timeout_configuration(termios* tty, unsigned int timeout_deciseconds) { 201 | // VMIN = 0, VTIME = 0: No blocking, return immediately with what is available 202 | // play with this... we will see if this is needed. 203 | // VMIN is a character count ranging from 0 to 255 characters, and VTIME is time measured in 0.1 second intervals, 204 | // (0 to 25.5 seconds). 205 | tty->c_cc[VTIME] = timeout_deciseconds; // wait at most for timeout_deciseconds for the first char to read 206 | tty->c_cc[VMIN] = 0; 207 | } 208 | 209 | void ecs::set_baudrate(termios* tty, everest::connection::SerialDeviceConfiguration::BaudRate baudrate) { 210 | cfsetspeed(tty, 211 | static_cast>(baudrate)); 212 | } 213 | 214 | void ecs::configure_device(int serial_port_fd, termios* tty) { 215 | 216 | // Save tty settings, also checking for error 217 | if (tcsetattr(serial_port_fd, TCSANOW, tty) != 0) { 218 | int myerror = errno; 219 | throw everest::connection::exceptions::tty::tty_error( 220 | "Error " + std::to_string(myerror) + " from tcsetattr: " + strerror(myerror), myerror); 221 | } 222 | } 223 | 224 | ::size_t ecs::write_to_device(int serial_port_fd, const unsigned char* const buffer, ::size_t count) { 225 | 226 | ::size_t bytes_written_sum = 0; 227 | 228 | while (bytes_written_sum < count) { 229 | // write() writes up to count bytes from the buffer starting at buf to the file referred to by the file 230 | // descriptor fd. On success, the number of bytes written is returned. On error, -1 is returned, and errno is 231 | // set to indicate the error. 232 | ssize_t bytes_written = ::write(serial_port_fd, buffer, count); 233 | if (bytes_written == -1) { 234 | // handle error 235 | int myerror = errno; 236 | throw everest::connection::exceptions::tty::tty_error( 237 | "Error: " + std::to_string(myerror) + " from ::write: " + strerror(myerror), myerror); 238 | } 239 | bytes_written_sum += bytes_written; 240 | } 241 | 242 | tcdrain(serial_port_fd); 243 | return bytes_written_sum; 244 | } 245 | 246 | ::ssize_t readByteFromDevice(int fd, unsigned char* charToRead) { 247 | 248 | ssize_t read_result = ::read(fd, charToRead, 1); 249 | if (not(read_result == -1)) 250 | return read_result; 251 | 252 | int myerror = errno; 253 | throw everest::connection::exceptions::tty::tty_error( 254 | "Error: " + std::to_string(myerror) + " from ::read: " + strerror(myerror), myerror); 255 | } 256 | 257 | ::size_t ecs::read_from_device(int serial_port_fd, unsigned char* buffer, ::size_t count, termios* tty_config, 258 | unsigned int initial_timeout_deciseconds, unsigned int timeout_deciseconds) { 259 | 260 | static_assert(std::is_unsigned::value, "need an unsigned type here. "); 261 | update_timeout_configuration(tty_config, initial_timeout_deciseconds); 262 | configure_device(serial_port_fd, tty_config); 263 | 264 | if (count == 0) 265 | return 0; 266 | 267 | // read at least one byte 268 | std::size_t index = 0; 269 | ::size_t bytes_read = readByteFromDevice(serial_port_fd, &buffer[index++]); 270 | if (bytes_read == 0) 271 | return 0; 272 | 273 | // dont wait that long after the end of transmission as it is done at the beginning. 274 | update_timeout_configuration(tty_config, timeout_deciseconds); 275 | configure_device(serial_port_fd, tty_config); 276 | 277 | while (bytes_read < count) { 278 | 279 | auto bytes_current = readByteFromDevice(serial_port_fd, &buffer[index++]); 280 | if (bytes_current == 0) 281 | return bytes_read; 282 | bytes_read += bytes_current; 283 | } 284 | return bytes_read; 285 | } 286 | 287 | using namespace std::string_literals; 288 | 289 | void everest::connection::SerialDeviceConfiguration::SerialDeviceConfiguration::get_current_config() { 290 | 291 | int serial_port = open(m_device.c_str(), O_RDWR); 292 | if (tcgetattr(serial_port, &m_tty_config) != 0) { 293 | throw everest::connection::exceptions::tty::tty_error(""s + __PRETTY_FUNCTION__ + "Error : " + strerror(errno), 294 | errno); 295 | } 296 | close(serial_port); 297 | } 298 | 299 | everest::connection::SerialDeviceConfiguration& 300 | everest::connection::SerialDeviceConfiguration::set_baud_rate(BaudRate baudrate) { 301 | cfsetspeed(&m_tty_config, 302 | static_cast>(baudrate)); 303 | return *this; 304 | } 305 | 306 | everest::connection::SerialDeviceConfiguration& 307 | everest::connection::SerialDeviceConfiguration::set_data_bits(DataBits data_bits) { 308 | m_tty_config.c_cflag &= ~CSIZE; // clear bits 309 | m_tty_config.c_cflag |= 310 | static_cast>(data_bits); 311 | return *this; 312 | } 313 | 314 | everest::connection::SerialDeviceConfiguration& 315 | everest::connection::SerialDeviceConfiguration::set_stop_bits(StopBits stop_bits) { 316 | 317 | switch (stop_bits) { 318 | case StopBits::One: 319 | m_tty_config.c_cflag &= ~CSTOPB; 320 | break; 321 | case StopBits::Two: 322 | m_tty_config.c_cflag |= CSTOPB; 323 | break; 324 | } 325 | return *this; 326 | } 327 | 328 | everest::connection::SerialDeviceConfiguration& 329 | everest::connection::SerialDeviceConfiguration::set_parity(Parity parity) { 330 | 331 | switch (parity) { 332 | case Parity::None: 333 | m_tty_config.c_cflag &= ~PARENB; 334 | m_tty_config.c_cflag &= ~PARODD; 335 | break; 336 | case Parity::Even: 337 | m_tty_config.c_cflag |= PARENB; 338 | m_tty_config.c_cflag &= ~PARODD; 339 | break; 340 | case Parity::Odd: 341 | m_tty_config.c_cflag |= PARENB; 342 | m_tty_config.c_cflag |= PARODD; 343 | break; 344 | } 345 | return *this; 346 | } 347 | 348 | everest::connection::SerialDeviceConfiguration& 349 | everest::connection::SerialDeviceConfiguration::set_sensible_defaults() { 350 | 351 | ecs::set_default_configuration(&m_tty_config); 352 | ecs::update_timeout_configuration(&m_tty_config, initial_read_timeout_deciseconds); 353 | return *this; 354 | } 355 | -------------------------------------------------------------------------------- /lib/connection/src/tcp.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace everest::connection; 19 | 20 | TCPConnection::TCPConnection(const std::string& address_, const int& port_) : 21 | address(address_), port(port_), socket_fd(-1) { 22 | make_connection(); 23 | } 24 | 25 | TCPConnection::~TCPConnection() { 26 | close_connection(); 27 | } 28 | 29 | int TCPConnection::make_connection() { 30 | 31 | // Opening socket locally 32 | EVLOG_debug << "Attempting to create TCP socket connection with endpoint " << address << ":" << port << "."; 33 | socket_fd = socket(AF_INET, SOCK_STREAM, 0); 34 | if (socket_fd == -1) { 35 | std::stringstream error_message; 36 | error_message << "TCP Socket creation error while connecting to endpoint " << address << ":" << port << "."; 37 | EVLOG_error << error_message.str(); 38 | throw exceptions::tcp::tcp_connection_error(error_message.str()); 39 | } 40 | EVLOG_debug << "Successfully opened TCP socket with endpoint " << address << ":" << port << ". fd = " << socket_fd; 41 | 42 | // Setting up address struct 43 | struct sockaddr_in server_address; 44 | server_address.sin_family = AF_INET; 45 | server_address.sin_port = htons(port); 46 | server_address.sin_addr.s_addr = inet_addr(address.c_str()); 47 | 48 | // Connecting 49 | connection_status = connect(socket_fd, (struct sockaddr*)&server_address, sizeof(server_address)); 50 | if (connection_status == -1) { 51 | std::stringstream error_message; 52 | error_message << "TCP socket connection establishment failed while trying to reach endpoint " << address << ":" 53 | << port << ". fd = " << socket_fd; 54 | EVLOG_error << error_message.str(); 55 | throw exceptions::tcp::tcp_connection_error(error_message.str()); 56 | } 57 | EVLOG_debug << "Succesfully opened TCP socket connection with endpoint " << address << ":" << port 58 | << ". fd = " << socket_fd; 59 | 60 | return connection_status; 61 | } 62 | 63 | int TCPConnection::close_connection() { 64 | 65 | EVLOG_debug << "Attempting to close connection for socket with fd = " << socket_fd << "."; 66 | int close_status = close(socket_fd); 67 | 68 | if (close_status == -1) { 69 | std::stringstream error_message; 70 | error_message << "Failed to close TCP socket with fd = " << socket_fd << "."; 71 | EVLOG_error << error_message.str(); 72 | throw exceptions::tcp::tcp_connection_error(error_message.str()); 73 | } 74 | EVLOG_debug << "Closed socket with fd = " << socket_fd << "."; 75 | socket_fd = -1; 76 | connection_status = -1; 77 | return close_status; 78 | } 79 | 80 | bool TCPConnection::is_valid() const { 81 | if (connection_status == -1 || socket_fd == -1) 82 | return false; 83 | return true; 84 | } 85 | 86 | int TCPConnection::send_bytes(const std::vector& bytes_to_send) { 87 | 88 | if (!is_valid()) { 89 | std::stringstream error_message; 90 | error_message << "MODBUS TCP - No connection established with " << address << ":" << port << "."; 91 | EVLOG_error << error_message.str(); 92 | throw exceptions::tcp::tcp_connection_error(error_message.str()); 93 | } 94 | 95 | int message_len = bytes_to_send.size(); 96 | EVLOG_debug << "Attempting to send message to " << address << ":" << port << " - " 97 | << utils::get_bytes_hex_string(bytes_to_send) << "- Size = " << message_len; 98 | 99 | // Trying to send 100 | int bytes_sent = send(socket_fd, (unsigned char*)bytes_to_send.data(), message_len, 0); 101 | if (bytes_sent == -1) { 102 | std::stringstream error_message; 103 | error_message << "MODBUS TCP - Error while sending message: " << bytes_to_send.data(); 104 | EVLOG_error << error_message.str(); 105 | throw exceptions::communication_error(error_message.str()); 106 | } 107 | 108 | EVLOG_debug << "Successfully sent " << bytes_sent << " bytes."; 109 | return bytes_sent; 110 | } 111 | 112 | std::vector TCPConnection::receive_bytes(unsigned int number_of_bytes) { 113 | 114 | if (!is_valid()) { 115 | std::stringstream error_message; 116 | error_message << "No connection established with " << address << ":" << port << "."; 117 | EVLOG_error << error_message.str(); 118 | throw exceptions::tcp::tcp_connection_error(error_message.str()); 119 | } 120 | 121 | // Attempting to receive 122 | std::vector received_bytes; 123 | received_bytes.reserve(number_of_bytes); 124 | uint8_t response_buffer[number_of_bytes]; 125 | 126 | int num_bytes_received = recv(socket_fd, &response_buffer, sizeof(response_buffer), 0); 127 | if (num_bytes_received == -1) { 128 | EVLOG_error << "No bytes received from " << address << ":" << port 129 | << ". Closing connection and returning preallocated buffer."; 130 | close_connection(); 131 | return received_bytes; 132 | } 133 | 134 | received_bytes.assign(response_buffer, response_buffer + num_bytes_received); 135 | EVLOG_debug << received_bytes.size() << " bytes received from " << address << ":" << port << " - " 136 | << utils::get_bytes_hex_string(received_bytes); 137 | return received_bytes; 138 | } 139 | -------------------------------------------------------------------------------- /lib/connection/src/udp.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace everest::connection; 19 | 20 | UDPConnection::UDPConnection(const std::string& address_, const int& port_) : 21 | port(port_), address(address_), socket_fd(-1) { 22 | make_connection(); 23 | } 24 | 25 | UDPConnection::~UDPConnection() { 26 | close_connection(); 27 | } 28 | 29 | int UDPConnection::make_connection() { 30 | 31 | /* NOTE: UDP is connectionless, so this function just operates by opening a socket locally through the 'socket' 32 | * syscall. The recipient is not involved in this step. The make_connection() and close_connection() methods are 33 | * implemented just to setup things locally and to comply with the proposed interface. One should bear that in mind 34 | * while using this class. */ 35 | 36 | // Opening socket locally 37 | EVLOG_debug << "Attempting to create UDP socket connection with endpoint " << address << ":" << port << "."; 38 | socket_fd = socket(AF_INET, SOCK_DGRAM, 0); 39 | if (socket_fd == -1) { 40 | std::stringstream error_message; 41 | error_message << "UDP Socket creation error: fd = " << socket_fd; 42 | throw exceptions::udp::udp_socket_error(error_message.str()); 43 | } 44 | EVLOG_debug << "Successfully opened local UDP socket"; 45 | 46 | // Setting up address struct 47 | struct sockaddr_in server_address; 48 | server_address.sin_family = AF_INET; 49 | server_address.sin_port = htons(port); 50 | server_address.sin_addr.s_addr = inet_addr(address.c_str()); 51 | 52 | connection_status = connect(socket_fd, (struct sockaddr*)&server_address, sizeof(server_address)); 53 | if (connection_status == -1) { 54 | std::stringstream error_message; 55 | error_message << "UDP socket open failed. fd = " << socket_fd; 56 | EVLOG_error << error_message.str(); 57 | throw exceptions::udp::udp_socket_error(error_message.str()); 58 | } 59 | 60 | return connection_status; 61 | } 62 | 63 | int UDPConnection::close_connection() { 64 | 65 | EVLOG_debug << "Attempting to close UDP socket with fd = " << socket_fd << "."; 66 | int close_status = close(socket_fd); 67 | 68 | if (close_status == -1) { 69 | std::stringstream error_message; 70 | error_message << "Failed to close UDP socket with fd = " << socket_fd << "."; 71 | EVLOG_error << error_message.str(); 72 | throw exceptions::udp::udp_socket_error(error_message.str()); 73 | } 74 | EVLOG_debug << "Closed UDP socket with fd = " << socket_fd << "."; 75 | socket_fd = -1; 76 | connection_status = -1; 77 | return close_status; 78 | } 79 | 80 | bool UDPConnection::is_valid() const { 81 | if (connection_status == -1 || socket_fd == -1) 82 | return false; 83 | return true; 84 | } 85 | 86 | int UDPConnection::send_bytes(const std::vector& bytes_to_send) { 87 | 88 | if (!is_valid()) { 89 | std::stringstream error_message; 90 | error_message << "MODBUS UDP - No connection established with " << address << ":" << port << "."; 91 | EVLOG_error << error_message.str(); 92 | throw exceptions::udp::udp_socket_error(error_message.str()); 93 | } 94 | 95 | int message_len = bytes_to_send.size(); 96 | EVLOG_debug << "Attempting to send message to " << address << ":" << port << " - " 97 | << utils::get_bytes_hex_string(bytes_to_send) << "- Size = " << message_len; 98 | 99 | // Trying to send 100 | int bytes_sent = sendto(socket_fd, (unsigned char*)bytes_to_send.data(), message_len, 0, (struct sockaddr*)NULL, 101 | sizeof(struct sockaddr)); 102 | if (bytes_sent == -1) { 103 | std::stringstream error_message; 104 | error_message << "MODBUS UDP - Error while sending message: " << bytes_to_send.data(); 105 | EVLOG_error << error_message.str(); 106 | throw exceptions::communication_error(error_message.str()); 107 | } 108 | 109 | EVLOG_debug << "Successfully sent " << bytes_sent << " bytes."; 110 | return bytes_sent; 111 | } 112 | 113 | std::vector UDPConnection::receive_bytes(unsigned int number_of_bytes) { 114 | 115 | if (!is_valid()) { 116 | std::stringstream error_message; 117 | error_message << "No connection established with " << address << ":" << port << "."; 118 | EVLOG_error << error_message.str(); 119 | throw exceptions::udp::udp_socket_error(error_message.str()); 120 | } 121 | 122 | // Attempting to receive 123 | std::vector received_bytes; 124 | received_bytes.reserve(number_of_bytes); 125 | uint8_t response_buffer[number_of_bytes]; 126 | 127 | int num_bytes_received = 128 | recvfrom(socket_fd, &response_buffer, sizeof(response_buffer), 0, (struct sockaddr*)NULL, NULL); 129 | if (num_bytes_received == -1) { 130 | EVLOG_error << "No bytes received from " << address << ":" << port 131 | << ". Closing connection and returning preallocated buffer."; 132 | close_connection(); 133 | return received_bytes; 134 | } 135 | 136 | received_bytes.assign(response_buffer, response_buffer + num_bytes_received); 137 | EVLOG_debug << received_bytes.size() << " bytes received from " << address << ":" << port << " - " 138 | << utils::get_bytes_hex_string(received_bytes); 139 | return received_bytes; 140 | } 141 | -------------------------------------------------------------------------------- /lib/connection/src/utils.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | 5 | using namespace everest::connection; 6 | 7 | std::string utils::get_bytes_hex_string(const std::vector& bytes) { 8 | std::stringstream buffer; 9 | for (auto it = bytes.begin(); it != bytes.end(); it++) { 10 | buffer << std::hex << (int)*it << " "; 11 | } 12 | return buffer.str(); 13 | } 14 | -------------------------------------------------------------------------------- /src/modbus_client.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace everest::modbus; 10 | 11 | ModbusClient::ModbusClient(connection::Connection& conn_) : conn(conn_) { 12 | EVLOG_debug << "Initialized ModbusClient"; 13 | } 14 | 15 | const std::vector ModbusClient::read_holding_register(uint8_t unit_id, uint16_t first_register_address, 16 | uint16_t num_registers_to_read, 17 | bool return_only_registers_bytes) const { 18 | std::vector body = 19 | utils::build_read_holding_register_message_body(first_register_address, num_registers_to_read); 20 | std::vector full_message = 21 | full_message_from_body(body, consts::READ_REGISTER_COMMAND_LENGTH, unit_id); 22 | conn.send_bytes(full_message); 23 | std::vector response = conn.receive_bytes(max_adu_size()); 24 | int num_register_bytes = validate_response(response, full_message); 25 | 26 | if (return_only_registers_bytes) 27 | return utils::extract_register_bytes_from_response(response, num_register_bytes); 28 | 29 | return response; 30 | } 31 | -------------------------------------------------------------------------------- /src/modbus_ip_client.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | 6 | using namespace everest::modbus; 7 | 8 | ModbusIPClient::ModbusIPClient(connection::Connection& conn_) : ModbusClient(conn_) { 9 | } 10 | 11 | const std::vector ModbusIPClient::full_message_from_body(const std::vector& body, 12 | uint16_t message_length, uint8_t unit_id) const { 13 | // Creates and prepend MBAP header 14 | std::vector mbap_header = utils::ip::make_mbap_header(message_length, unit_id); 15 | std::vector full_message; 16 | full_message.reserve(mbap_header.size() + body.size()); 17 | full_message.insert(full_message.end(), mbap_header.begin(), mbap_header.end()); 18 | full_message.insert(full_message.end(), body.begin(), body.end()); 19 | return full_message; 20 | } 21 | 22 | uint16_t ModbusIPClient::validate_response(const std::vector& response, 23 | const std::vector& request) const { 24 | return modbus::utils::ip::check_mbap_header(request, response); 25 | } 26 | 27 | ModbusTCPClient::ModbusTCPClient(connection::TCPConnection& conn_) : ModbusIPClient(conn_) { 28 | } 29 | ModbusUDPClient::ModbusUDPClient(connection::UDPConnection& conn_) : ModbusIPClient(conn_) { 30 | } 31 | -------------------------------------------------------------------------------- /src/modbus_rtu_client.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace everest::modbus; 15 | 16 | /* 17 | * Note on implemetation: 18 | * It is known that we often copy data from one vector to the other ( full_message_from_body, helper function in utils ) 19 | * This is by design of the older parts of this library. 20 | */ 21 | 22 | ModbusRTUClient::ModbusRTUClient(connection::Connection& conn_, bool ignore_echo) : ModbusClient(conn_), ignore_echo(ignore_echo) { 23 | } 24 | 25 | ModbusRTUClient::~ModbusRTUClient() { 26 | conn.close_connection(); 27 | } 28 | 29 | #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 30 | 31 | union SwapIt { 32 | uint16_t value_16; 33 | struct { 34 | unsigned char low; 35 | unsigned char hi; 36 | } value_8; 37 | }; 38 | 39 | #endif 40 | 41 | DataVectorUint8 ModbusRTUClient::response_without_protocol_data(const DataVectorUint8& raw_response, 42 | std::size_t payload_length) { 43 | // strip address and function bytes, but include bytecount byte 44 | const int offset_protocol_bytes{3}; 45 | return DataVectorUint8(raw_response.cbegin() + offset_protocol_bytes, 46 | raw_response.cbegin() + offset_protocol_bytes + payload_length); 47 | } 48 | 49 | const DataVectorUint8 ModbusRTUClient::read_holding_register(uint8_t unit_id, uint16_t first_register_address, 50 | uint16_t num_registers_to_read, 51 | bool return_only_registers_bytes) const { 52 | 53 | using namespace std::string_literals; 54 | 55 | #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 56 | 57 | if (num_registers_to_read > everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE) 58 | throw everest::modbus::exceptions::message_size_exception( 59 | ""s + __PRETTY_FUNCTION__ + " Requested number of 16 bit registers " + 60 | std::to_string(num_registers_to_read) + " would exceed allowed message size of " + 61 | std::to_string(everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE) + " registers "); 62 | 63 | DataVectorUint8 body = 64 | utils::build_read_holding_register_message_body(first_register_address, num_registers_to_read); 65 | 66 | DataVectorUint8 full_message = full_message_from_body(body, consts::READ_REGISTER_COMMAND_LENGTH, unit_id); 67 | 68 | conn.send_bytes(full_message); 69 | modbus::DataVectorUint8 response = conn.receive_bytes(max_adu_size()); 70 | uint16_t payload_size = validate_response(response, full_message); 71 | return return_only_registers_bytes ? response_without_protocol_data(response, payload_size) : response; 72 | 73 | #else 74 | 75 | static_assert(false, "implementation currently done for little endian only"); 76 | 77 | #endif 78 | } 79 | 80 | // HACK: is not a virtual function, not implementing the modbus interface. 81 | const DataVectorUint8 ModbusRTUClient::read_input_register(uint8_t unit_id, uint16_t first_register_address, 82 | uint16_t num_registers_to_read, 83 | bool return_only_registers_bytes) const { 84 | 85 | std::vector body = 86 | utils::build_read_input_register_message_body(first_register_address, num_registers_to_read); 87 | std::vector full_message = full_message_from_body(body, consts::READ_REGISTER_COMMAND_LENGTH, unit_id); 88 | conn.send_bytes(full_message); 89 | std::vector response = conn.receive_bytes(max_adu_size()); 90 | 91 | if (ignore_echo && response.size() > full_message.size()) { 92 | auto request = std::vector(response.begin(), response.begin() + full_message.size()); 93 | if (full_message == request) { 94 | response = std::vector(response.begin() + full_message.size(), response.end()); 95 | } 96 | } 97 | 98 | int num_register_bytes = validate_response(response, full_message); 99 | 100 | if (return_only_registers_bytes) { 101 | return utils::extract_register_bytes_from_response(response, num_register_bytes + 2); 102 | } 103 | 104 | return response; 105 | } 106 | 107 | DataVectorUint8 ModbusDataContainerUint16::get_payload_as_bigendian() const { 108 | 109 | DataVectorUint8 result; 110 | result.reserve(m_payload.size() * sizeof(DataVectorUint16::value_type)); 111 | 112 | if (m_byte_order == ByteOrder::LittleEndian) 113 | for (uint16_t value : m_payload) { 114 | SwapIt s{value}; 115 | result.push_back(s.value_8.hi); 116 | result.push_back(s.value_8.low); 117 | } 118 | else 119 | for (uint16_t value : m_payload) { 120 | SwapIt s{value}; 121 | result.push_back(s.value_8.low); 122 | result.push_back(s.value_8.hi); 123 | } 124 | 125 | return result; 126 | } 127 | 128 | DataVectorUint8 everest::modbus::ModbusRTUClient::write_multiple_registers(uint8_t unit_id, 129 | uint16_t first_register_address, 130 | uint16_t num_registers_to_write, 131 | const ModbusDataContainerUint16& payload, 132 | bool return_only_registers_bytes) const { 133 | 134 | #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 135 | 136 | using namespace std::string_literals; 137 | 138 | if (num_registers_to_write > everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE) 139 | throw everest::modbus::exceptions::message_size_exception( 140 | ""s + __PRETTY_FUNCTION__ + " Requested number of 16 bit registers " + 141 | std::to_string(num_registers_to_write) + " would exceed allowed message size of " + 142 | std::to_string(everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE) + " registers !"); 143 | 144 | DataVectorUint8 body = 145 | utils::build_write_multiple_register_body(first_register_address, num_registers_to_write, payload); 146 | DataVectorUint8 full_message = full_message_from_body(body, body.size() /* unused parameter */, unit_id); 147 | 148 | conn.send_bytes(full_message); 149 | modbus::DataVectorUint8 response = conn.receive_bytes(max_adu_size()); 150 | uint16_t payload_size = validate_response(response, full_message); 151 | return return_only_registers_bytes ? response_without_protocol_data(response, payload_size) : response; 152 | 153 | #else 154 | 155 | static_assert(false, "implementation currently done for little endian only"); 156 | 157 | #endif 158 | } 159 | 160 | const everest::modbus::DataVectorUint8 ModbusRTUClient::full_message_from_body(const DataVectorUint8& body, 161 | uint16_t /* message_length */, 162 | std::uint8_t unit_id) const { 163 | 164 | // body now is the modbus command code and the payload. 165 | DataVectorUint8 full_message; // need an empty vector 166 | full_message.reserve(body.size() + 1 + // unit_id 167 | 2); // crc16 168 | 169 | full_message.push_back(unit_id); 170 | std::copy(body.cbegin(), body.cend(), std::back_inserter(full_message)); 171 | 172 | SwapIt swapIt; 173 | swapIt.value_16 = everest::modbus::utils::calcCRC_16_ANSI(full_message.data(), full_message.size()); 174 | full_message.push_back(swapIt.value_8.hi); 175 | full_message.push_back(swapIt.value_8.low); 176 | 177 | return full_message; 178 | } 179 | 180 | uint16_t everest::modbus::ModbusRTUClient::validate_response(const DataVectorUint8& response, 181 | const DataVectorUint8& request) const { 182 | 183 | using namespace std::string_literals; 184 | 185 | if (response.size() > max_adu_size()) 186 | throw everest::modbus::exceptions::should_never_happen( 187 | ""s + __PRETTY_FUNCTION__ + " response size " + std::to_string(response.size()) + 188 | " is larger than max allowed message size " + std::to_string(max_adu_size()) + " !"); 189 | 190 | if (response.empty()) 191 | throw everest::modbus::exceptions::empty_response(""s + __PRETTY_FUNCTION__ + 192 | " response is empty, maybe timeout on reading device. "); 193 | 194 | // FIXME: What happens in case the request was a broadcast? 195 | if (response.at(0) != request.at(0)) 196 | throw everest::modbus::exceptions::unmatched_response(""s + __PRETTY_FUNCTION__ + 197 | " request / response unit id mismatch. "); 198 | 199 | if (not((response.at(1) & 0x80) == 0)) { 200 | uint8_t exception_code = response.at(2); 201 | std::stringstream ss; 202 | std::string error_message; 203 | switch (exception_code) { 204 | case 0x01: 205 | error_message = "ILLEGAL FUNCTION"; 206 | break; 207 | case 0x02: 208 | error_message = "ILLEGAL DATA ADDRESS"; 209 | break; 210 | case 0x03: 211 | error_message = "ILLEGAL DATA VALUE"; 212 | break; 213 | case 0x04: 214 | error_message = "SERVER DEVICE FAILURE"; 215 | break; 216 | case 0x05: 217 | error_message = "ACKNOWLEDGE"; 218 | break; 219 | case 0x06: 220 | error_message = "SERVER DEVICE BUSY"; 221 | break; 222 | // case 0x07: does not exist 223 | case 0x08: 224 | error_message = "MEMORY PARITY ERROR"; 225 | break; 226 | // case 0x09: does not exist 227 | case 0x0a: 228 | error_message = "GATEWAY PATH UNAVAILABLE"; 229 | break; 230 | case 0x0b: 231 | error_message = "GATEWAY TARGET DEVICE FAILED TO RESPOND"; 232 | break; 233 | default: 234 | error_message = "UNKNOWN ERROR"; 235 | } 236 | ss << __PRETTY_FUNCTION__ << " response returned an error code: " << std::hex << (int)exception_code << " ( " 237 | << error_message << " ) "; 238 | throw everest::modbus::exceptions::modbus_exception(ss.str(), exception_code); 239 | } 240 | 241 | if (response.at(1) != request.at(1)) 242 | throw everest::modbus::exceptions::unmatched_response(""s + __PRETTY_FUNCTION__ + 243 | " request / response function id mismatch. "); 244 | 245 | SwapIt crcResponseCalculated; 246 | crcResponseCalculated.value_16 = ::everest::modbus::utils::calcCRC_16_ANSI(response.data(), response.size() - 2); 247 | 248 | SwapIt crcResponseData; 249 | crcResponseData.value_8.hi = response.at(response.size() - 2); 250 | crcResponseData.value_8.low = response.at(response.size() - 1); 251 | 252 | if (crcResponseCalculated.value_16 != crcResponseData.value_16) 253 | throw everest::modbus::exceptions::checksum_error(""s + __PRETTY_FUNCTION__ + " checksum error "); 254 | 255 | uint16_t result_size = response.at(2); 256 | 257 | return result_size; 258 | } 259 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | namespace everest { 17 | namespace modbus { 18 | 19 | std::vector utils::ip::make_mbap_header(uint16_t message_length, uint8_t unit_id) { 20 | 21 | // Header buffer 22 | std::vector mbap_header(consts::tcp::MBAP_HEADER_LENGTH); 23 | 24 | // Generating random 2 byte transaction ID 25 | // uint16_t transac tion_id = 1; 26 | srand(time(0)); 27 | uint16_t transaction_id = rand() % std::numeric_limits::max(); 28 | 29 | // Adding transaction ID bytes 30 | mbap_header[0] = (transaction_id >> 8) & 0xFF; 31 | mbap_header[1] = transaction_id & 0xFF; 32 | 33 | // Adding protocol ID bytes 34 | mbap_header[2] = (consts::tcp::PROTOCOL_ID >> 8) & 0xFF; 35 | mbap_header[3] = consts::tcp::PROTOCOL_ID & 0xFF; 36 | 37 | // Adding message length bytes 38 | mbap_header[4] = (message_length >> 8) & 0xFF; 39 | mbap_header[5] = message_length & 0xFF; 40 | 41 | // Adding unit/slave ID 42 | mbap_header[6] = unit_id; 43 | 44 | return mbap_header; 45 | } 46 | 47 | std::vector utils::build_read_command_message_body(std::uint8_t function_code, uint16_t first_register_address, 48 | uint16_t num_registers_to_read) { 49 | 50 | std::vector message_body(consts::READ_REGISTER_COMMAND_LENGTH - 1); 51 | 52 | // Adding read function code 53 | message_body[0] = function_code; 54 | 55 | // Adding first register data address 56 | message_body[1] = (first_register_address >> 8) & 0xFF; 57 | message_body[2] = first_register_address & 0xFF; 58 | 59 | // Adding requested register number 60 | message_body[3] = (num_registers_to_read >> 8) & 0xFF; 61 | message_body[4] = num_registers_to_read & 0xFF; 62 | 63 | return message_body; 64 | } 65 | 66 | std::vector utils::build_read_holding_register_message_body(uint16_t first_register_address, 67 | uint16_t num_registers_to_read) { 68 | return build_read_command_message_body(consts::READ_HOLDING_REGISTER_FUNCTION_CODE, first_register_address, 69 | num_registers_to_read); 70 | } 71 | 72 | std::vector utils::build_read_input_register_message_body(uint16_t first_register_address, 73 | uint16_t num_registers_to_read) { 74 | return build_read_command_message_body(consts::READ_INPUT_REGISTER_FUNCTION_CODE, first_register_address, 75 | num_registers_to_read); 76 | } 77 | 78 | std::vector 79 | utils::build_write_multiple_register_body(uint16_t first_register_address, uint16_t num_registers_to_write, 80 | const ::everest::modbus::ModbusDataContainerUint16& payload) { 81 | 82 | std::vector message_body; 83 | message_body.reserve(1 + // function code 84 | 2 + // starting address 85 | 2 + // quantity of registers 86 | payload.size()); 87 | 88 | message_body.push_back(0x10); // function code 89 | 90 | // first register address 91 | message_body.push_back((first_register_address >> 8) & 0xff); // hibyte 92 | message_body.push_back(first_register_address & 0xff); // lowbyte 93 | 94 | // number of registers to write 95 | message_body.push_back((num_registers_to_write >> 8) & 0xff); // hibyte 96 | message_body.push_back(num_registers_to_write & 0xff); // lowbyte 97 | 98 | // byte count: for now only 16 bit register, so bytecount is num_registers_to_write * 2 99 | message_body.push_back(num_registers_to_write * 2); 100 | std::vector payload_big_endian = payload.get_payload_as_bigendian(); 101 | std::copy(payload_big_endian.cbegin(), payload_big_endian.cend(), std::back_inserter(message_body)); 102 | 103 | return message_body; 104 | } 105 | 106 | uint16_t utils::ip::check_mbap_header(const std::vector& sent_message, 107 | const std::vector& received_message) { 108 | 109 | // Validating echoed transaction ID 110 | bool transaction_id_match = (sent_message[0] == received_message[0] && sent_message[1] == received_message[1]); 111 | if (!transaction_id_match) 112 | throw exceptions::unmatched_response("MODBUS TCP - Sent and received transaction ID's do not match."); 113 | 114 | // Validating echoed protocol ID 115 | bool protocol_id_match = (sent_message[2] == received_message[2] && sent_message[3] == received_message[3]); 116 | if (!protocol_id_match) 117 | throw exceptions::unmatched_response("MODBUS TCP - Sent and received protocol ID's do not match."); 118 | 119 | // Validating echoed unit id 120 | bool unit_id_match = (sent_message[6] == received_message[6]); 121 | if (!unit_id_match) 122 | throw exceptions::unmatched_response("MODBUS TCP - Sent and received unit ID's do not match." 123 | ""); 124 | 125 | // Validating echoed function code 126 | bool function_code_match = (sent_message[7] == received_message[7]); 127 | if (!function_code_match) 128 | throw exceptions::unmatched_response("MODBUS TCP - Sent and received function codes do not match." 129 | ""); 130 | 131 | // Extracting number of bytes to follow 132 | uint16_t number_of_following_bytes = 0; 133 | number_of_following_bytes = (received_message[4] << 8) | received_message[5]; 134 | 135 | return number_of_following_bytes; 136 | } 137 | 138 | std::vector utils::extract_body_from_response(const std::vector& response, int num_data_bytes) { 139 | std::vector response_body(response.end() - num_data_bytes, response.end()); 140 | return response_body; 141 | } 142 | 143 | std::vector utils::extract_register_bytes_from_response(const std::vector& response, 144 | int num_data_bytes) { 145 | return utils::extract_registers_bytes_from_response_body(response); 146 | } 147 | 148 | std::vector utils::extract_registers_bytes_from_response_body(const std::vector& response_body) { 149 | uint8_t num_register_bytes = response_body.at(2); 150 | std::vector register_bytes = 151 | std::vector(response_body.begin() + 3, response_body.begin() + 3 + num_register_bytes); 152 | return register_bytes; 153 | } 154 | 155 | void utils::print_message_hex(const std::vector& message) { 156 | for (int n : message) { 157 | printf("%.2X ", n); 158 | } 159 | printf("\n"); 160 | } 161 | 162 | void utils::print_message_first_N_bytes(unsigned char* message, int N) { 163 | for (int i = 0; i < N; i++) { 164 | printf("%.2X ", message[i]); 165 | } 166 | printf("\n"); 167 | } 168 | 169 | utils::CRCResultType utils::calcCRC_16_ANSI(const utils::PayloadType* payload, std::size_t payload_length) { 170 | 171 | // https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Polynomial_representations_of_cyclic_redundancy_checks 172 | // implementation stolen from: https://modbus.org/docs/PI_MBUS_300.pdf 173 | 174 | // High-Order Byte Table 175 | /* Table of CRC values for high–order byte */ 176 | const utils::PayloadType auchCRCHi[] = { 177 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 178 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 179 | 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 180 | 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 181 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 182 | 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 183 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 184 | 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 185 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 186 | 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 187 | 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 188 | 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 189 | 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 190 | 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 191 | 0x00, 0xC1, 0x81, 0x40}; 192 | 193 | /* Table of CRC values for low–order byte */ 194 | const utils::PayloadType auchCRCLo[] = { 195 | 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 196 | 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 197 | 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 198 | 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 199 | 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 200 | 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 201 | 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 202 | 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 203 | 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 204 | 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 205 | 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 206 | 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 207 | 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 208 | 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 209 | 0x41, 0x81, 0x80, 0x40}; 210 | 211 | utils::PayloadType uchCRCHi = 0xff; 212 | utils::PayloadType uchCRCLo = 0xff; 213 | 214 | while (payload_length--) { 215 | std::size_t uIndex = uchCRCHi ^ *payload++; 216 | uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex]; 217 | uchCRCLo = auchCRCLo[uIndex]; 218 | } 219 | 220 | return (uchCRCHi << 8 | uchCRCLo); 221 | } 222 | } // namespace modbus 223 | } // namespace everest 224 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(${TEST_TARGET_NAME} ${PROJECT_NAME}_tests) 2 | add_executable(${TEST_TARGET_NAME}_rtu test_rtu.cpp) 3 | target_link_libraries(${TEST_TARGET_NAME}_rtu 4 | PRIVATE 5 | everest::modbus 6 | GTest::gtest_main 7 | GTest::gmock 8 | ) 9 | 10 | 11 | add_executable(${TEST_TARGET_NAME}_serial_helper test_serial_helper.cpp) 12 | target_link_libraries(${TEST_TARGET_NAME}_serial_helper 13 | PRIVATE 14 | everest::modbus 15 | GTest::gtest_main 16 | GTest::gmock 17 | ) 18 | 19 | 20 | include(GoogleTest) 21 | 22 | gtest_discover_tests(${TEST_TARGET_NAME}_rtu) 23 | gtest_discover_tests(${TEST_TARGET_NAME}_serial_helper) 24 | -------------------------------------------------------------------------------- /tests/test_rtu.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | using namespace everest::modbus::utils; 16 | 17 | TEST(RTUTests, test_crc16) { 18 | 19 | // check crc16 implementation used for modbus rtu 20 | 21 | { 22 | // example from https://en.wikipedia.org/wiki/Modbus#Modbus_RTU_frame_format 23 | const PayloadType payload[] = {0x01, 0x04, 0x02, 0xFF, 0xFF}; 24 | const CRCResultType expectedCRC{0xb880}; 25 | CRCResultType crcResult = calcCRC_16_ANSI(payload, sizeof(payload) / sizeof(PayloadType)); 26 | ASSERT_EQ(crcResult, expectedCRC); 27 | } 28 | 29 | // the following sample data generated with https://github.com/chargeITmobility/bsm-python 30 | { 31 | // bsmtool --trace --device /dev/ttyUSB0 get ac_meter 32 | // out: 2A 03 9C 9C 00 69 6D 81 33 | // request 34 | const PayloadType payload[]{0x2A, 0x03, 0x9C, 0x9C, 0x00, 0x69}; 35 | const CRCResultType expectedCRC = 0x6d81; 36 | CRCResultType crcResult = calcCRC_16_ANSI(payload, sizeof(payload) / sizeof(PayloadType)); 37 | ASSERT_EQ(crcResult, expectedCRC); 38 | } 39 | 40 | { 41 | // bsmtool --trace --device /dev/ttyUSB0 get ac_meter 42 | // response 43 | const PayloadType payload[] = { 44 | 0x2A, 0x03, 0xD2, 0x00, 0x13, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, 0x80, 0x00, 0x08, 0xEF, 0x00, 45 | 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0xFF, 0xFF, 0x01, 0xF4, 0xFF, 0xFF, 0x00, 46 | 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x01, 0xFF, 0xFE, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 48 | 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x09, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 53 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00}; 56 | const CRCResultType expectedCRC = 0xE9A2; 57 | CRCResultType crcResult = calcCRC_16_ANSI(payload, sizeof(payload) / sizeof(PayloadType)); 58 | ASSERT_EQ(crcResult, expectedCRC); 59 | } 60 | 61 | { 62 | // bsmtool --trace --device /dev/ttyUSB0 get common 63 | // request 64 | // /dev/ttyUSB0:42[addr=40004] ->2A039C440042ADA5 65 | 66 | const PayloadType payload[]{0x2A, 0x03, 0x9C, 0x44, 0x00, 0x42}; 67 | const CRCResultType expectedCRC = 0xada5; 68 | CRCResultType crcResult = calcCRC_16_ANSI(payload, sizeof(payload) / sizeof(PayloadType)); 69 | ASSERT_EQ(crcResult, expectedCRC); 70 | } 71 | 72 | { 73 | // bsmtool --trace --device /dev/ttyUSB0 get common 74 | // response 75 | // /dev/ttyUSB0:42[addr=40004] 76 | // <--2A0384424155455220456C656374726F6E69630000000000000000000000000000000042534D2D57533336412D4830312D313331312D3030303000000000000000000000000000000000000000000000000000312E393A333243413A414646340000003231303730303139000000000000000000000000000000000000000000000000002A8000C856 77 | 78 | const PayloadType payload[]{ 79 | 0x2A, 0x03, 0x84, 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 80 | 0x69, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 81 | 0x00, 0x42, 0x53, 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 82 | 0x31, 0x31, 0x2D, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 83 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2E, 84 | 0x39, 0x3A, 0x33, 0x32, 0x43, 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, 0x00, 0x00, 0x00, 0x32, 0x31, 0x30, 85 | 0x37, 0x30, 0x30, 0x31, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 86 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x80, 0x00}; 87 | const CRCResultType expectedCRC = 0xc856; 88 | CRCResultType crcResult = calcCRC_16_ANSI(payload, sizeof(payload) / sizeof(PayloadType)); 89 | ASSERT_EQ(crcResult, expectedCRC); 90 | } 91 | } 92 | 93 | TEST(RTUTests, test_ModbusDataContainerUint16) { 94 | 95 | using namespace everest::modbus; 96 | 97 | ModbusDataContainerUint16 big_endian(ByteOrder::BigEndian, {0x00ff, 0x2442}); 98 | ModbusDataContainerUint16 little_endian(ByteOrder::LittleEndian, {0xff00, 0x4224}); 99 | 100 | { 101 | DataVectorUint8 res = big_endian.get_payload_as_bigendian(); 102 | ASSERT_EQ(res.size(), big_endian.size() * 2); // number of elements in container 103 | ASSERT_EQ(res[0], 0xff); 104 | ASSERT_EQ(res[1], 0x00); 105 | ASSERT_EQ(res[2], 0x42); 106 | ASSERT_EQ(res[3], 0x24); 107 | } 108 | 109 | { 110 | DataVectorUint8 res = little_endian.get_payload_as_bigendian(); 111 | ASSERT_EQ(res.size(), little_endian.size() * 2); // number of elements in container 112 | ASSERT_EQ(res[0], 0xff); 113 | ASSERT_EQ(res[1], 0x00); 114 | ASSERT_EQ(res[2], 0x42); 115 | ASSERT_EQ(res[3], 0x24); 116 | } 117 | } 118 | 119 | //////////////////////////////////////////////////////////////////////////////// 120 | // 121 | // test serial connection helper stuff 122 | 123 | TEST(RTUTestHardware, test_lowlevel_serial) { 124 | 125 | using namespace everest::connection; 126 | 127 | ASSERT_THROW(SerialDeviceConfiguration("/dev/does_not_exist"), std::runtime_error); 128 | 129 | SerialDeviceConfiguration serial_device_config("/dev/ttyUSB0"); 130 | 131 | serial_device_config.set_sensible_defaults(); 132 | // .set_baud_rate(SerialDeviceConfiguration::BaudRate::Baud_19200); 133 | // .set_stop_bits(SerialDeviceConfiguration::StopBits::One ) 134 | // .set_parity( SerialDeviceConfiguration::Parity::None ) 135 | // .set_data_bits( SerialDeviceConfiguration::DataBits::Bit_8 ); 136 | 137 | using CFlagType = decltype(termios::c_cflag); 138 | 139 | ASSERT_EQ(cfgetispeed(&serial_device_config.m_tty_config), static_cast(B19200)); 140 | ASSERT_EQ(cfgetospeed(&serial_device_config.m_tty_config), static_cast(B19200)); 141 | 142 | ASSERT_EQ(serial_device_config.m_tty_config.c_cflag & CS8, static_cast(CS8)); // 8 databits 143 | ASSERT_EQ(serial_device_config.m_tty_config.c_cflag & CSTOPB, static_cast(0)); // stop bits 144 | ASSERT_EQ(serial_device_config.m_tty_config.c_cflag & PARENB, static_cast(PARENB)); // parity 145 | 146 | SerialDevice serial_device(serial_device_config); 147 | 148 | ASSERT_EQ(serial_device.get_serial_device_config().m_tty_config, serial_device_config.m_tty_config); 149 | 150 | serial_device.open(); 151 | 152 | // Test starts here. 153 | // test data stolen from: 154 | // bsmtool --trace --device /dev/ttyUSB0 get common 155 | const unsigned char request_common_model[]{0x2A, 0x03, 0x9C, 0x44, 0x00, 0x42, 0xAD, 0xA5}; 156 | 157 | // writeToDevice goes to connection object? 158 | auto bytes_written = serial_device.write(request_common_model, sizeof(request_common_model)); 159 | 160 | ASSERT_EQ(bytes_written, sizeof(request_common_model)); // the common model has this size on this device... 161 | 162 | std::vector readbuffer(::everest::modbus::consts::rtu::MAX_ADU); 163 | ::size_t bytes_read = serial_device.read(readbuffer.data(), readbuffer.size()); 164 | 165 | EXPECT_EQ(bytes_read, (unsigned)137); // the common model has this size on this device... 166 | ASSERT_GT(bytes_read, (unsigned)0); // does not make sense to continue testing if we dont read anything... 167 | ASSERT_LE(bytes_read, ::everest::modbus::consts::rtu::MAX_ADU); // make sure we dont read past the end. 168 | 169 | serial_device.close(); 170 | } 171 | 172 | TEST(RTUTestHardware, test_lowlevel_serial_error) { 173 | 174 | using namespace everest::connection; 175 | 176 | SerialDeviceConfiguration cfg; 177 | cfg.set_sensible_defaults(); 178 | 179 | SerialDevice serial_device(cfg); 180 | 181 | serial_device.close(); 182 | 183 | unsigned char buffer[42]{}; 184 | 185 | // test on invalid fd 186 | ASSERT_THROW(serial_device.write(buffer, sizeof(buffer)), std::runtime_error); 187 | ASSERT_THROW(serial_device.read(buffer, sizeof(buffer)), std::runtime_error); 188 | } 189 | 190 | TEST(RTUTestHardware, test_read_holding_register) { 191 | 192 | // test the Modbus::read_holding_register to read sunspec get common model on real hardware. 193 | // Please keep in mind that this test likely to fail if other hardware than powemeter "BSM-WS36A-H01-1311-0000" from 194 | // Gebr. Bauer GbR is used. ;) 195 | using namespace everest::connection; 196 | 197 | SerialDeviceConfiguration cfg("/dev/ttyUSB0"); 198 | cfg.set_sensible_defaults(); 199 | 200 | SerialDevice serial_device(cfg); 201 | 202 | using DataVector = std::vector; 203 | 204 | DataVector outgoing_rtu_get_common{0x2A, 0x03, 0x9C, 0x44, 0x00, 0x42, 0xAD, 0xA5}; 205 | DataVector incomming_rtu_response{ 206 | 0x2A, 0x03, 0x84, 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 0x69, 207 | 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 208 | 0x53, 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 0x31, 0x31, 0x2D, 209 | 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 210 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2E, 0x39, 0x3A, 0x33, 0x32, 0x43, 211 | 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, 0x00, 0x00, 0x00, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 0x31, 0x39, 0x00, 212 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 213 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x80, 0x00, 0xc8, 0x56}; 214 | DataVector stripped_incomming_rtu_response{ 215 | 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 0x69, 0x63, 0x00, 216 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x53, 217 | 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 0x31, 0x31, 0x2D, 218 | 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 219 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2E, 0x39, 0x3A, 0x33, 220 | 0x32, 0x43, 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, 0x00, 0x00, 0x00, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 221 | 0x31, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 222 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x80, 0x00}; 223 | 224 | RTUConnection connection(serial_device); 225 | 226 | everest::modbus::ModbusRTUClient client(connection); 227 | DataVector result = 228 | client.read_holding_register(42, // device address 229 | 40004, // register address 230 | 66, // number of regs to read 231 | false // for unknown reasons: if this is false, the raw message is returned. 232 | ); 233 | 234 | ASSERT_EQ(result, incomming_rtu_response); 235 | } 236 | 237 | class MockSerialDevice : public ::everest::connection::SerialDevice { 238 | 239 | public: 240 | MOCK_METHOD(void, open, ()); 241 | MOCK_METHOD(void, close, ()); 242 | MOCK_METHOD(everest::connection::SerialDeviceConfiguration&, get_serial_device_config, ()); 243 | MOCK_METHOD(void, update_timeout_configuration, (unsigned int)); 244 | MOCK_METHOD(::size_t, write, (const unsigned char* const buffer, ::size_t count)); 245 | MOCK_METHOD(::size_t, read, (unsigned char* buffer, ::size_t count)); 246 | }; 247 | 248 | TEST(RTUClientTest, test_rtu_client_read_holding_register) { 249 | 250 | // write a sunspec get_common request and receive the response 251 | // this is done twice to check the "unpacking" of the response ( stripping the protocol part from the response data 252 | // ). 253 | 254 | using namespace ::everest::connection; 255 | 256 | using ::testing::_; // this is the wildcard parameter 257 | using ::testing::Args; 258 | using ::testing::DoAll; 259 | using ::testing::ElementsAreArray; 260 | using ::testing::NiceMock; 261 | using ::testing::Return; 262 | using ::testing::SetArrayArgument; 263 | 264 | // RTUConnectionConfiguration config; 265 | // https://github.com/google/googletest/blob/main/docs/gmock_cook_book.md#knowing-when-to-expect 266 | // We dont want warnings about unineresting calls, so we use the NiceMock here 267 | NiceMock serial_device; 268 | 269 | using DataVector = std::vector; 270 | 271 | // outgoing message: 272 | // see test_crc16 273 | // 0x2A Address --> bsm default address is 42 / 0x2A 274 | // 0x03 Function --> read holding register 275 | // 0x9C data --> Starting address Hi --> 40004 / maps to datamodel 40003 276 | // 0x44 data --> Starting address Lo 277 | // 0x00 data --> Number of points Hi --> read 66 points, which is the payload length of "Standard SunSpec model 278 | // with general information", model number 1 0x42 data --> Number of points Lo 0xAD crc16 --> Error check 279 | // 0x42 crc16 --> Error check 280 | DataVector outgoing_rtu_get_common{0x2A, 0x03, 0x9C, 0x44, 0x00, 0x42, 0xAD, 0xA5}; 281 | DataVector incomming_rtu_response{ 282 | 0x2A, 0x03, 0x84, 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 0x69, 283 | 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 284 | 0x53, 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 0x31, 0x31, 0x2D, 285 | 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 286 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2E, 0x39, 0x3A, 0x33, 0x32, 0x43, 287 | 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, 0x00, 0x00, 0x00, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 0x31, 0x39, 0x00, 288 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 289 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x80, 0x00, 0xc8, 0x56}; 290 | DataVector stripped_incomming_rtu_response{ 291 | 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 0x69, 0x63, 0x00, 292 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x53, 293 | 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 0x31, 0x31, 0x2D, 294 | 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 295 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2E, 0x39, 0x3A, 0x33, 296 | 0x32, 0x43, 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, 0x00, 0x00, 0x00, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 297 | 0x31, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 298 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x80, 0x00}; 299 | 300 | // https://google.github.io/googletest/reference/actions.html 301 | // https://google.github.io/googletest/reference/matchers.html 302 | 303 | EXPECT_CALL(serial_device, read(_, _)) 304 | .WillOnce(DoAll(SetArrayArgument<0>(incomming_rtu_response.begin(), incomming_rtu_response.end()), 305 | Return(incomming_rtu_response.size()))) 306 | .WillOnce(DoAll(SetArrayArgument<0>(incomming_rtu_response.begin(), incomming_rtu_response.end()), 307 | Return(incomming_rtu_response.size()))); 308 | 309 | // Args(m) The tuple of the k selected (using 0-based indices) arguments matches m, e.g. Args<1, 310 | // 2>(Eq()) Args is a *Matcher* !!! 311 | EXPECT_CALL(serial_device, write(_, 312 | _ // c style array here. 313 | )) 314 | .With(::testing::Args<0, 1>(ElementsAreArray( 315 | outgoing_rtu_get_common))) // Seems that c style arrays have their own style of matching parameters 316 | .WillOnce(Return(outgoing_rtu_get_common.size())) 317 | .WillOnce(Return(outgoing_rtu_get_common.size())); 318 | 319 | RTUConnection connection(serial_device); 320 | 321 | everest::modbus::ModbusRTUClient client(connection); 322 | { 323 | DataVector result = 324 | client.read_holding_register(42, // device address 325 | 40004, // register address 326 | 66, // number of regs to read 327 | false // for unknown reasons: if this is false, the raw message is returned. 328 | ); 329 | 330 | // a note on comparison: the raw result has the size of the max adu of a rtu message, which in this case is 331 | // larger than the size of incomming_rtu_response result.resize( incomming_rtu_response.size() ); // resize, 332 | // otherwise we would have to compare items "by hand" 333 | ASSERT_EQ(result, incomming_rtu_response); 334 | } 335 | 336 | { 337 | DataVector result = 338 | client.read_holding_register(42, // device address 339 | 40004, // register address 340 | 66, // number of regs to read 341 | true // for unknown reasons: if this is false, the raw message is returned. 342 | ); 343 | 344 | ASSERT_EQ(result.size(), stripped_incomming_rtu_response.size()); 345 | ASSERT_EQ(result, stripped_incomming_rtu_response); 346 | } 347 | } 348 | 349 | TEST(RTUClientTest, test_rtu_client_read_input_register) { 350 | 351 | FAIL() << "\n\n(imagine this message displayed in red, blinking...)\n\n *** needs to be implemented, currently we dont have data for this. ***\n\n"; 352 | 353 | } 354 | 355 | 356 | 357 | 358 | TEST(RTUClientTest, test_rtu_client_write_multiple_register) { 359 | 360 | // test the writer_multiple_registers 361 | // TODO: the mocking is on a to high level. Will have to make the serial_connection_helper stuff mockable. 362 | 363 | using namespace ::everest::modbus; 364 | using namespace ::everest::connection; 365 | 366 | using ::testing::_; 367 | using ::testing::DoAll; 368 | using ::testing::ElementsAreArray; 369 | using ::testing::NiceMock; 370 | using ::testing::Return; 371 | using ::testing::SetArrayArgument; 372 | 373 | // stolen from modbus spec 374 | DataVectorUint8 outgoing_rtu_write_multiple_register{ 375 | 0x2A, // unit id 376 | 0x10, // write multiple register 377 | 0x00, // start address hi 378 | 0x01, // start address lo 379 | 0x00, // quantity of registers hi 380 | 0x02, // quantity of registers lo 381 | 0x04, // byte count 382 | 0x00, // reg0 hi 383 | 0x0a, // reg0 lo 384 | 0x01, // reg1 hi 385 | 0x02, // reg1 lo 386 | 387 | 0x1C, 388 | 0xd4 // crc16 389 | }; 390 | 391 | ModbusDataContainerUint16 payload(ByteOrder::LittleEndian, {0x000a, 0x0102}); 392 | 393 | DataVectorUint8 incomming_rtu_response{0x2A, // unit id 394 | 0x10, 395 | 0x00, // start address hi 396 | 0x01, // start address lo 397 | 0x00, // quantity of registers hi 398 | 0x02, // quantity of registers lo 399 | 0x16, // crc16 400 | 0x13}; 401 | 402 | NiceMock serial_device; 403 | EXPECT_CALL(serial_device, read(_, _)) 404 | .WillOnce(DoAll(SetArrayArgument<0>(incomming_rtu_response.begin(), incomming_rtu_response.end()), 405 | Return(incomming_rtu_response.size()))); 406 | 407 | EXPECT_CALL(serial_device, write(_, 408 | _ // c style array here. 409 | )) 410 | .With(::testing::Args<0, 1>( 411 | ElementsAreArray(outgoing_rtu_write_multiple_register))) // Seems that c style arrays have their own style 412 | // of matching parameters 413 | .WillOnce(Return(outgoing_rtu_write_multiple_register.size())); 414 | 415 | RTUConnection connection(serial_device); 416 | 417 | ModbusRTUClient client(connection); 418 | 419 | DataVectorUint8 response = 420 | client.write_multiple_registers(0x2a, // unit id 421 | 0x0001, // start address 422 | 0x02, // number of register to write 423 | payload, // errors will be reported by exception std::runtime_error 424 | false // return full response 425 | ); 426 | 427 | ASSERT_EQ(response, incomming_rtu_response); 428 | } 429 | 430 | TEST(RTUClientTest, test_rtu_client_crc_error) { 431 | 432 | // test if an exception is thrown on crc error 433 | 434 | using namespace ::everest::modbus; 435 | using namespace ::everest::connection; 436 | 437 | using ::testing::_; 438 | using ::testing::DoAll; 439 | using ::testing::ElementsAreArray; 440 | using ::testing::NiceMock; 441 | using ::testing::Return; 442 | using ::testing::SetArrayArgument; 443 | 444 | // stolen from modbus spec, write a writer_multiple_registers request to the device 445 | DataVectorUint8 outgoing_rtu_write_multiple_register{ 446 | 0x2A, // unit id 447 | 0x10, // write multiple register 448 | 0x00, // start address hi 449 | 0x01, // start address lo 450 | 0x00, // quantity of registers hi 451 | 0x02, // quantity of registers lo 452 | 0x04, // byte count 453 | 0x00, // reg0 hi 454 | 0x0a, // reg0 lo 455 | 0x01, // reg1 hi 456 | 0x02, // reg1 lo 457 | 458 | 0x1C, 459 | 0xd4 // crc16 460 | }; 461 | 462 | ModbusDataContainerUint16 payload(ByteOrder::LittleEndian, {0x000a, 0x0102}); 463 | 464 | DataVectorUint8 incomming_rtu_response{0x2A, // unit id 465 | 0x10, 466 | 0x00, // start address hi 467 | 0x01, // start address lo 468 | 0x00, // quantity of registers hi 469 | 0x02, // quantity of registers lo 470 | 471 | 0x00, // wrong crc16, this should trigger an exception. 472 | 0x00}; 473 | 474 | NiceMock serial_device; 475 | 476 | EXPECT_CALL(serial_device, read(_, _)) 477 | .WillOnce(DoAll(SetArrayArgument<0>(incomming_rtu_response.begin(), incomming_rtu_response.end()), 478 | Return(incomming_rtu_response.size()))); 479 | 480 | EXPECT_CALL(serial_device, write(_, 481 | _ // c style array here. 482 | )) 483 | .With(::testing::Args<0, 1>( 484 | ElementsAreArray(outgoing_rtu_write_multiple_register))) // Seems that c style arrays have their own style 485 | // of matching parameters 486 | .WillOnce(Return(outgoing_rtu_write_multiple_register.size())); 487 | 488 | // RTUConnection connection( config, serial_device ); 489 | RTUConnection connection(serial_device); 490 | 491 | ModbusRTUClient client(connection); 492 | 493 | ASSERT_THROW(client.write_multiple_registers(0x2a, 0x0001, 0x02, payload, false), 494 | std::runtime_error); // exception thrown on crc error 495 | } 496 | 497 | TEST(RTUClientTest, test_rtu_client_error_responses) { 498 | 499 | // we "send" a valid request "outside", and have a error response return. 500 | // we test if an exception (std::runtime_error) is thrown on error responses. 501 | 502 | using namespace ::everest::modbus; 503 | using namespace ::everest::connection; 504 | 505 | using ::testing::_; 506 | using ::testing::DoAll; 507 | using ::testing::ElementsAreArray; 508 | using ::testing::NiceMock; 509 | using ::testing::Return; 510 | using ::testing::SetArrayArgument; 511 | 512 | // stolen from modbus spec 513 | DataVectorUint8 outgoing_rtu_write_multiple_register{ 514 | 0x2A, // unit id 515 | 0x10, // write multiple register 516 | 0x00, // start address hi 517 | 0x01, // start address lo 518 | 0x00, // quantity of registers hi 519 | 0x02, // quantity of registers lo 520 | 0x04, // byte count 521 | 0x00, // reg0 hi 522 | 0x0a, // reg0 lo 523 | 0x01, // reg1 hi 524 | 0x02, // reg1 lo 525 | 526 | 0x1C, 527 | 0xd4 // crc16 528 | }; 529 | 530 | DataVectorUint8 incomming_rtu_error_response{0x2A, // unit id 531 | 0x90, // error code for 0x10 (write multiple register) 532 | 0x04}; // some valid error exception code. 533 | 534 | ModbusDataContainerUint16 payload(ByteOrder::LittleEndian, {0x000a, 0x0102}); 535 | 536 | NiceMock serial_device; 537 | 538 | RTUConnection connection(serial_device); 539 | 540 | EXPECT_CALL(serial_device, read(_, _)) 541 | .WillOnce(DoAll(SetArrayArgument<0>(incomming_rtu_error_response.begin(), incomming_rtu_error_response.end()), 542 | Return(incomming_rtu_error_response.size()))); 543 | 544 | EXPECT_CALL(serial_device, write(_, 545 | _ // c style array here. 546 | )) 547 | .With(::testing::Args<0, 1>( 548 | ElementsAreArray(outgoing_rtu_write_multiple_register))) // Seems that c style arrays have their own style 549 | // of matching parameters 550 | .WillOnce(Return(outgoing_rtu_write_multiple_register.size())); 551 | 552 | ModbusRTUClient client(connection); 553 | 554 | ASSERT_THROW(client.write_multiple_registers(0x2a, // unit id 555 | 0x0001, // start address 556 | 0x02, // number of register to write 557 | payload, // errors will be reported by exception std::runtime_error 558 | false // return full response 559 | ), 560 | std::runtime_error); 561 | } 562 | 563 | TEST(RTUClientTest, test_rtu_message_size_error) { 564 | 565 | // test throw when trying to read more register than allowed by modbus protocol 566 | using namespace ::everest::modbus; 567 | using namespace ::everest::connection; 568 | 569 | ::testing::NiceMock serial_device; 570 | RTUConnection connection(serial_device); 571 | ModbusRTUClient client(connection); 572 | 573 | EXPECT_THROW(client.read_holding_register(42, 40000, everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE + 1), 574 | ::everest::modbus::exceptions::message_size_exception); 575 | 576 | // some nonsense payload 577 | ModbusDataContainerUint16 payload(ByteOrder::LittleEndian, {0x000a, 0x0102}); 578 | EXPECT_THROW(client.write_multiple_registers(42, 40000, everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE + 1, 579 | payload, true), 580 | ::everest::modbus::exceptions::message_size_exception); 581 | } 582 | 583 | TEST(RTUClientTest, test_response_without_protocol_data) { 584 | 585 | // test the unpacking of response data ( stripping the protocol part from the response data ) 586 | 587 | using namespace everest::modbus; 588 | 589 | // This is a read holding request. till now, response_without_protocol_data till now assumes that the data is 590 | // processes ist a read_holding_register communication. The data returning from response_without_protocol_data now 591 | // includes the number of bytes field of the response. byte count here 132 / 0x84 592 | DataVectorUint8 dv_raw_resrponse{ 593 | 0x2A, 0x03, 0x84, 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 0x69, 594 | 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 595 | 0x53, 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 0x31, 0x31, 0x2D, 596 | 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 597 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2E, 0x39, 0x3A, 0x33, 0x32, 0x43, 598 | 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, 0x00, 0x00, 0x00, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 0x31, 0x39, 0x00, 599 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 600 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x80, 0x00, 0xc8, 0x56}; 601 | DataVectorUint8 dv_stripped_response{ 602 | 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 0x69, 0x63, 0x00, 603 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x53, 604 | 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 0x31, 0x31, 0x2D, 605 | 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 606 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2E, 0x39, 0x3A, 0x33, 607 | 0x32, 0x43, 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, 0x00, 0x00, 0x00, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 608 | 0x31, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 609 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0x80, 0x00}; 610 | 611 | ASSERT_EQ(ModbusRTUClient::response_without_protocol_data(dv_raw_resrponse, 0x84), dv_stripped_response); 612 | } 613 | -------------------------------------------------------------------------------- /tests/test_serial_helper.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | // Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | TEST(TestSerialHelper, testSerialHelperConfiguration) { 10 | 11 | using namespace everest::connection; 12 | 13 | SerialDeviceConfiguration config{}; 14 | 15 | config.set_sensible_defaults() 16 | .set_baud_rate(SerialDeviceConfiguration::BaudRate::Baud_110) 17 | .set_data_bits(SerialDeviceConfiguration::DataBits::Bit_7) 18 | .set_stop_bits(SerialDeviceConfiguration::StopBits::One) 19 | .set_parity(SerialDeviceConfiguration::Parity::Even); 20 | 21 | EXPECT_EQ(cfgetispeed(&config.m_tty_config), B110); // baud 22 | EXPECT_EQ(config.m_tty_config.c_cflag & CS7, CS7); // databits 23 | EXPECT_EQ(config.m_tty_config.c_cflag & CSTOPB, 0); // stop bits 24 | EXPECT_EQ(config.m_tty_config.c_cflag & PARENB, PARENB); // parity 25 | 26 | auto c_flags = CREAD | CLOCAL; 27 | EXPECT_EQ(config.m_tty_config.c_cflag & c_flags, c_flags); 28 | 29 | // check disabled flags 30 | auto l_flags = ICANON | ECHO | ECHOE | ECHONL | ISIG; 31 | EXPECT_EQ(config.m_tty_config.c_lflag & l_flags, 0); 32 | } 33 | 34 | TEST(TestSerialHelper, BaudFromInt) { 35 | 36 | using namespace everest::connection; 37 | 38 | { 39 | SerialDeviceConfiguration::BaudrateFromIntResult result = 40 | SerialDeviceConfiguration::baudrate_from_integer(19200); 41 | EXPECT_EQ(result.baud, SerialDeviceConfiguration::BaudRate::Baud_19200); 42 | EXPECT_TRUE(result.conversion_ok); 43 | } 44 | 45 | { 46 | SerialDeviceConfiguration::BaudrateFromIntResult result = SerialDeviceConfiguration::baudrate_from_integer(666); 47 | EXPECT_EQ(result.baud, SerialDeviceConfiguration::BaudRate::Baud_9600); 48 | EXPECT_FALSE(result.conversion_ok); 49 | } 50 | } 51 | --------------------------------------------------------------------------------