diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d7bf69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +.config +*.o +*.pyc + +# gtags +GTAGS +GRTAGS +GPATH + +# emacs +.dir-locals.el + +# emacs temp file suffixes +*~ +.#* +\#*# + +# eclipse setting +.settings + +# MacOS directory files +.DS_Store + +# Build related files +build* +sdkconfig +sdkconfig.old +managed_components +dependencies.lock +.vscode +doxygen_output/** +dist + +# Doc build artifacts +docs/_build/ +docs/doxygen-warning-log.txt +docs/sphinx-warning-log.txt +docs/sphinx-warning-log-sanitized.txt +docs/xml/ +docs/xml_in/ +docs/man/ +docs/doxygen_sqlite3.db + +TEST_LOGS + + +# gcov coverage reports +*.gcda +*.gcno +coverage.info +coverage_report/ + +# VS Code Settings +.vscode/ +/.pytest_cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..6bea4ff --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,348 @@ +stages: + - build + - target_test + - deploy + +variables: + # System environment + ESP_DOCS_ENV_IMAGE: "$CI_DOCKER_REGISTRY/esp-idf-doc-env-v5.3:1-1" + ESP_DOCS_PATH: "$CI_PROJECT_DIR" + TEST_DIR: "$CI_PROJECT_DIR" + + # GitLab-CI environment + GET_SOURCES_ATTEMPTS: "10" + ARTIFACT_DOWNLOAD_ATTEMPTS: "10" + GIT_SUBMODULE_STRATEGY: none + +# Define a matrix for IDF versions and their corresponding targets +.options_list: +# versions: +# IDF_VER: ["latest", "v5.3", "v5.2", "v5.0"] + markers: + TEST_MARKER: + - "tcp" + - "serial" + - "generic" +# - "tcp_p4" +# - "serial_p4" + +.setup_idf_tools: &setup_idf_tools | + tools/idf_tools.py --non-interactive install && eval "$(tools/idf_tools.py --non-interactive export)" || exit 1 + +.add_gh_key_remote: &add_gh_key_remote | + command -v ssh-agent >/dev/null || exit 1 + eval $(ssh-agent -s) + printf '%s\n' "${GH_PUSH_KEY}" | tr -d '\r' | ssh-add - > /dev/null + mkdir -p ~/.ssh && chmod 700 ~/.ssh + [[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config || ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts + git remote remove github || true + git remote add github ${GH_PUSH_REPO} + +after_script: + # Just for cleaning space, no other causes + - git clean -ffdx + +.build_template: + stage: build + tags: + - build + variables: + SIZE_INFO_LOCATION: "${TEST_DIR}/size_info.txt" + IDF_CCACHE_ENABLE: "1" + after_script: + # Show ccache statistics if enabled globally + - test "$CI_CCACHE_STATS" == 1 && test -n "$(which ccache)" && ccache --show-stats || true + dependencies: [] + +.before_script_build_jobs: + before_script: + - pip install idf-component-manager --upgrade + - pip install "idf_build_apps~=1.0.1" + +.check_idf_ver: &check_idf_ver | + export IDF_PATH=$(find /opt -type d -name "*idf*" \ + \( -exec test -f '{}/tools/idf.py' \; -and -exec test -f '{}/tools/idf_tools.py' \; \ + \) -print -quit) + if [ -z "${IDF_PATH}" ];then + echo "IDF version is not found." + else + cd ${IDF_PATH} + export IDF_DESCRIBE=$(git describe) + export IDF_VERSION=${IDF_DESCRIBE%-*} + echo "$IDF_VERSION" >> $TEST_DIR/idf_version_info.txt + echo "ESP-IDF: $IDF_VERSION" + fi + +# Note: this script builds the folder against all targets and then deletes +# all other artifacts except the esp32 to decrease the size of artifacs (may cause failures) +.build_cur_folder: &build_cur_folder | + echo "Build job ${CI_JOB_NAME}, folder: ${PWD##*/}, targets: ${TEST_TARGETS}" + python -m idf_build_apps build -v -p . \ + --recursive \ + --target all \ + --default-build-targets ${TEST_TARGETS} \ + --config "sdkconfig.ci.*=" --build-dir "build_@t_@w" \ + --check-warnings \ + --ignore-warning-file ../tools/ignore_build_warnings.txt \ + --collect-size-info $SIZE_INFO_LOCATION \ + --manifest-rootpath . \ + --manifest-file .build-test-rules.yml \ + --parallel-count ${CI_NODE_TOTAL:-1} \ + --parallel-index ${CI_NODE_INDEX:-1} + echo "delete build folders:" $(find . -type d -regex '^\./.*build_esp32[a-z]+[0-9]+[_a-z]*' -print -exec rm -rf {} +) + ls -lh > test_dir_${PWD##*/}.txt + +# This template gets expanded multiple times, once for every IDF version. +# IDF version is specified by setting the espressif/idf image tag. +# +# TEST_TARGETS sets the list of IDF_TARGET values to build the test for. +# It should contain only the targets with optimized assembly implementations. +# +.build_pytest_template: + stage: build + extends: + - .build_template + - .before_script_build_jobs + script: + # The script below will build all test applications defined in environment variable $TEST_TARGETS + - *check_idf_ver + # This is workaround to build library under esp-idf v4.4 + - pip install idf-component-manager --upgrade + - cd ${TEST_DIR}/test_apps + - *build_cur_folder + - cd ${TEST_DIR}/examples + - export TEST_TARGETS="esp32" # override to build only on esp32 target to decrease build time + - *build_cur_folder + variables: + TEST_TARGETS: "esp32" + artifacts: + name: artifacts_${CI_JOB_NAME} + paths: + - "**/build*/size.json" + - "**/build*/build.log" + - "**/build*/build_log.txt" + - "**/build*/*.bin" + - "**/build*/*.elf" + - "**/build*/*.map" + - "**/build*/flasher_args.json" + - "**/build*/flash_project_args" + - "**/build*/config/sdkconfig.json" + - "**/build*/bootloader/*.bin" + - "**/build*/partition_table/*.bin" + - "**/idf_version_info.txt" + - "**/test_dir*.txt" + - $SIZE_INFO_LOCATION + when: always + expire_in: 3 weeks + +build_idf_latest: + extends: .build_pytest_template + image: espressif/idf:latest + variables: + TEST_TARGETS: "esp32 esp32s2 esp32s3 esp32c2 esp32c3 esp32c5 esp32c6 esp32h2" + +build_idf_v5.3: + extends: .build_pytest_template + image: espressif/idf:release-v5.3 + variables: + TEST_TARGETS: "esp32 esp32s2 esp32s3 esp32c2 esp32c3 esp32c6 esp32h2" + +build_idf_v5.2: + extends: .build_pytest_template + image: espressif/idf:release-v5.2 + variables: + TEST_TARGETS: "esp32 esp32s2 esp32s3 esp32c2 esp32c3" + +build_idf_v5.0: + extends: .build_pytest_template + image: espressif/idf:release-v5.0 + variables: + TEST_TARGETS: "esp32 esp32s2 esp32s3 esp32c2 esp32c3" + +.target_test_template: + stage: target_test + timeout: 1 hour + variables: + GIT_DEPTH: 1 + SUBMODULES_TO_FETCH: "none" + cache: + # Usually do not need submodule-cache in target_test + - key: pip-cache + paths: + - .cache/pip + policy: pull + +.before_script_pytest_jobs: + before_script: + # Install or upgrade pytest-embedded to perform test cases + - pip install -r ${TEST_DIR}/tools/test_requirements.txt + # Upgrade the packages (workaround for esp-idf v5.0) + - pip install --only-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf --upgrade + +.test_cur_folder: &test_cur_folder | + export IDF_VER=$(cat ${TEST_DIR}/idf_version_info.txt) + echo "Start test job: ${CI_JOB_NAME}, version: ${IDF_VER%-*}, folder: ${PWD##*/}" + python -m pytest --junit-xml=${TEST_DIR}/${PWD##*/}/results_${IDF_VER%-*}_${PWD##*/}.xml --target=${IDF_TARGET} -m multi_dut_modbus_${TEST_MARKER} + ls -lh > test_dir_${PWD##*/}.txt + +.test_template: + image: "$CI_DOCKER_REGISTRY/target-test-env-v5.3:1" + stage: target_test + extends: + - .before_script_pytest_jobs + tags: + - multi_dut_modbus_${TEST_MARKER} + variables: + IDF_TARGET: "esp32" # the only esp32 runners are available for now + script: + - cd ${TEST_DIR}/test_apps/ + - *test_cur_folder + - cd ${TEST_DIR}/examples/ + - *test_cur_folder + artifacts: + name: artifacts_${CI_JOB_NAME} + paths: + - "${TEST_DIR}/**/*.log" + - "${TEST_DIR}/**/*.xml" + - "${TEST_DIR}/**/results_*.xml" + - "${TEST_DIR}/**/pytest_embedded_log/" + - "${TEST_DIR}/**/test_dir*.txt" + - "${TEST_DIR}/**/idf_version_info.txt" + - "${TEST_DIR}/**/log.html" + - "${TEST_DIR}/**/report.html" + - "${TEST_DIR}/**/*.pcap" + reports: + junit: ${TEST_DIR}/results_${IDF_VER%-*}.xml + when: always + expire_in: 1 week + +target_test_latest: + stage: target_test + image: "$CI_DOCKER_REGISTRY/target-test-env-v5.4:1" + extends: .test_template + parallel: + matrix: + - !reference [.options_list, markers] + needs: + job: build_idf_latest + artifacts: true + after_script: [] + +target_test_v5.3: + stage: target_test + image: "$CI_DOCKER_REGISTRY/target-test-env-v5.3:1" + extends: .test_template + parallel: + matrix: + - !reference [.options_list, markers] + needs: + job: build_idf_v5.3 + artifacts: true + after_script: [] + +target_test_v5.2: + stage: target_test + image: "$CI_DOCKER_REGISTRY/target-test-env-v5.2:2" + extends: .test_template + parallel: + matrix: + - !reference [.options_list, markers] + needs: + job: build_idf_v5.2 + artifacts: true + after_script: [] + +target_test_v5.0: + stage: target_test + image: "$CI_DOCKER_REGISTRY/target-test-env-v5.0:3" + extends: .test_template + parallel: + matrix: + - !reference [.options_list, markers] + needs: + job: build_idf_v5.0 + artifacts: true + after_script: [] + +build_docs: + stage: build + image: $ESP_DOCS_ENV_IMAGE + tags: + - build_docs + artifacts: + when: always + paths: + - docs/_build/*/*/*.txt + - docs/_build/*/*/html/* + expire_in: 4 days + # No cleaning when the artifacts + after_script: [] + script: + - cd docs + - pip install -r requirements.txt + - ./generate_docs + +.deploy_docs_template: + stage: deploy + image: $ESP_DOCS_ENV_IMAGE + tags: + - deploy_docs + needs: + - build_docs + only: + changes: + - "docs/**/*" + script: + - source ${CI_PROJECT_DIR}/docs/utils.sh + - add_doc_server_ssh_keys $DOCS_DEPLOY_PRIVATEKEY $DOCS_DEPLOY_SERVER $DOCS_DEPLOY_SERVER_USER + - export GIT_VER=$(git describe --always) + - export GIT_TAGS=$(git fetch --tags; git tag -l;) + - echo "Deploy ${PWD##*/}, ${DOCS_BUILD_DIR}, Ref name; ${CI_COMMIT_REF_NAME}" + - echo "Version; ${GIT_VER}, tags; ${GIT_TAGS}" + - pip install -r ${CI_PROJECT_DIR}/docs/requirements.txt + - deploy-docs + +deploy_docs_preview: + extends: + - .deploy_docs_template + except: + refs: + - main + variables: + TYPE: "preview" + DOCS_BUILD_DIR: "${CI_PROJECT_DIR}/docs/_build/" + DOCS_DEPLOY_PRIVATEKEY: "$DOCS_PREVIEW_PRIVATEKEY" + DOCS_DEPLOY_SERVER: "$DOCS_PREVIEW_SERVER" + DOCS_DEPLOY_SERVER_USER: "$DOCS_PREVIEW_SERVER_USER" + DOCS_DEPLOY_PATH: "$DOCS_PREVIEW_PATH" + DOCS_DEPLOY_URL_BASE: "$DOCS_PREVIEW_URL_BASE" + +deploy_docs_production: + extends: + - .deploy_docs_template + only: + refs: + - main + variables: + TYPE: "production" + DOCS_BUILD_DIR: "${CI_PROJECT_DIR}/docs/_build/" + DOCS_DEPLOY_PRIVATEKEY: "$DOCS_PROD_DEPLOY_KEY" + DOCS_DEPLOY_SERVER: "$DOCS_PROD_SERVER" + DOCS_DEPLOY_SERVER_USER: "$DOCS_PROD_SERVER_USER" + DOCS_DEPLOY_PATH: "$DOCS_PROD_PATH" + DOCS_DEPLOY_URL_BASE: "https://docs.espressif.com/projects/esp-modbus" + +upload_to_component_manager: + stage: deploy + image: python:3.10-alpine + tags: + - deploy + rules: + - if: '$CI_COMMIT_BRANCH == "main"' # the main branch is a component release branch for v2 + - if: '$FORCE_PUSH_COMPONENT == "1"' + when: manual + script: + - pip install idf-component-manager + - export IDF_COMPONENT_API_TOKEN=${ESP_MODBUS_API_KEY} + - export COMP_VERSION=$(grep 'version:' idf_component.yml | head -n 1 | awk '{print $2}' | tr -d '"') + - compote component upload --namespace=espressif --name=esp-modbus --allow-existing --version=${COMP_VERSION} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cb32015 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: +- repo: https://github.com/igrr/astyle_py.git + rev: master + hooks: + - id: astyle_py + args: ['--style=otbs', '--attach-namespaces', '--attach-classes', '--indent=spaces=4', '--convert-tabs', '--align-pointer=name', '--align-reference=name', '--keep-one-line-statements', '--pad-header', '--pad-oper'] + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + types_or: [c, c++] + - id: end-of-file-fixer + types_or: [c, c++] + - id: check-merge-conflict + - id: mixed-line-ending + types_or: [c, c++] + args: ['--fix=lf'] + description: Forces to replace line ending by the UNIX 'lf' character \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0f536bc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,92 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +set(srcs + "mb_controller/common/esp_modbus_master.c" + "mb_controller/common/esp_modbus_slave.c" + "mb_controller/common/esp_modbus_master_serial.c" + "mb_controller/common/esp_modbus_slave_serial.c" + "mb_controller/common/esp_modbus_master_tcp.c" + "mb_controller/common/esp_modbus_slave_tcp.c" + "mb_controller/serial/mbc_serial_master.c" + "mb_controller/serial/mbc_serial_slave.c" + "mb_controller/tcp/mbc_tcp_master.c" + "mb_controller/tcp/mbc_tcp_slave.c" + "mb_objects/mb_master.c" + "mb_objects/mb_slave.c" + "mb_objects/functions/mbfunccoils_master.c" + "mb_objects/functions/mbfunccoils.c" + "mb_objects/functions/mbfuncdiag.c" + "mb_objects/functions/mbfuncdisc_master.c" + "mb_objects/functions/mbfuncdisc.c" + "mb_objects/functions/mbfuncholding_master.c" + "mb_objects/functions/mbfuncholding.c" + "mb_objects/functions/mbfuncinput_master.c" + "mb_objects/functions/mbfuncinput.c" + "mb_objects/functions/mbfuncother.c" + "mb_objects/functions/mbutils.c" + "mb_ports/common/port_event.c" + "mb_ports/common/port_other.c" + "mb_ports/common/port_timer.c" + "mb_ports/common/mb_transaction.c" + "mb_ports/serial/port_serial.c" + "mb_ports/tcp/port_tcp_master.c" + "mb_ports/tcp/port_tcp_slave.c" + "mb_ports/tcp/port_tcp_driver.c" + "mb_ports/tcp/port_tcp_utils.c" + "mb_transports/rtu/rtu_master.c" + "mb_transports/rtu/rtu_slave.c" + "mb_transports/rtu/mbcrc.c" + "mb_transports/ascii/ascii_master.c" + "mb_transports/ascii/ascii_slave.c" + "mb_transports/ascii/ascii_lrc.c" + "mb_transports/tcp/tcp_master.c" + "mb_transports/tcp/tcp_slave.c" +) + +set(include_dirs mb_transports mb_controller/common/include mb_objects/include mb_ports/common mb_ports/serial mb_ports/tcp) + +set(priv_include_dirs mb_controller/serial mb_controller/tcp mb_controller/common mb_transports/rtu mb_transports/ascii mb_transports/tcp) + +if(CONFIG_FMB_EXT_TYPE_SUPPORT) + list(APPEND srcs "mb_controller/common/mb_endianness_utils.c") +endif() + +add_prefix(srcs "${CMAKE_CURRENT_LIST_DIR}/modbus/" ${srcs}) +add_prefix(include_dirs "${CMAKE_CURRENT_LIST_DIR}/modbus/" ${include_dirs}) +add_prefix(priv_include_dirs "${CMAKE_CURRENT_LIST_DIR}/modbus/" ${priv_include_dirs}) + +message(STATUS "DEBUG: Use esp-modbus component folder: ${CMAKE_CURRENT_LIST_DIR}.") + +set(requires driver) +set(priv_requires esp_netif esp_event vfs) + +# esp_timer component was introduced in v4.2 +if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER "4.1") + list(APPEND requires esp_timer) +endif() + +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "${include_dirs}" + PRIV_INCLUDE_DIRS "${priv_include_dirs}" + REQUIRES ${requires} + PRIV_REQUIRES ${priv_requires} + LDFRAGMENTS linker.lf) + +# This is an alternative of macro `idf_component_optional_requires(PUBLIC mdns)` to support all versions of esp-idf +set(optional_reqs mdns espressif__mdns) +idf_build_get_property(build_components BUILD_COMPONENTS) +message(STATUS "build_components = ${build_components}") +foreach(req ${optional_reqs} ${exclude_comps}) + if(req IN_LIST build_components) + idf_component_get_property(req_lib ${req} COMPONENT_LIB) + target_link_libraries(${COMPONENT_LIB} PRIVATE ${req_lib}) + message(STATUS "Req ${req} is found and added into ${COMPONENT_NAME} dependencies.") + target_compile_definitions(${COMPONENT_LIB} PUBLIC -DMB_MDNS_IS_INCLUDED) + endif() +endforeach() + +# target_link_options(${COMPONENT_LIB} INTERFACE -fsanitize=undefined -fsanitize=alignment) #-fsanitize=address -fsanitize=undefined +# target_link_options(${COMPONENT_LIB} INTERFACE -fsanitize=address) +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-strict-aliasing -Wno-write-strings -Werror) + +message(STATUS "The mdns included is: ${MB_MDNS_IS_INCLUDED}") diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..ade1e31 --- /dev/null +++ b/Kconfig @@ -0,0 +1,218 @@ +menu "Modbus configuration" + + config FMB_COMM_MODE_TCP_EN + bool "Enable Modbus stack support for TCP communication mode" + default y + help + Enable Modbus TCP option for stack. + + config FMB_TCP_PORT_DEFAULT + int "Modbus TCP port number" + range 0 65535 + default 502 + depends on FMB_COMM_MODE_TCP_EN + help + Modbus default port number used by Modbus TCP stack + + config FMB_TCP_PORT_MAX_CONN + int "Maximum allowed connections for TCP stack" + range 1 8 + default 5 + depends on FMB_COMM_MODE_TCP_EN + help + Maximum allowed connections number for Modbus TCP stack. + This is used by Modbus master and slave port layer to establish connections. + This parameter may decrease performance of Modbus stack and can cause + increasing of processing time (increase only if absolutely necessary). + + config FMB_TCP_CONNECTION_TOUT_SEC + int "Modbus TCP connection timeout" + range 1 7200 + default 20 + depends on FMB_COMM_MODE_TCP_EN + help + Modbus TCP connection timeout in seconds. + Once expired the current connection with the client will be closed + and Modbus slave will be waiting for new connection to accept. + + config FMB_TCP_UID_ENABLED + bool "Modbus TCP enable UID (Unit Identifier) support" + default n + depends on FMB_COMM_MODE_TCP_EN + help + If this option is set the Modbus stack uses UID (Unit Identifier) field in MBAP frame. + Else the UID is ignored by master and slave. + + config FMB_COMM_MODE_RTU_EN + bool "Enable Modbus stack support for RTU mode" + default y + help + Enable RTU Modbus communication mode option for Modbus serial stack. + + config FMB_COMM_MODE_ASCII_EN + bool "Enable Modbus stack support for ASCII mode" + default y + help + Enable ASCII Modbus communication mode option for Modbus serial stack. + + config FMB_MASTER_TIMEOUT_MS_RESPOND + int "Slave respond timeout (Milliseconds)" + default 10000 + range 150 30000 + help + If master sends a frame which is not broadcast, it has to wait some time for slave response. + if slave is not respond in this time, the master will process timeout error. + + config FMB_MASTER_DELAY_MS_CONVERT + int "Slave conversion delay (Milliseconds)" + default 200 + range 150 2000 + help + If master sends a broadcast frame, it has to wait conversion time to delay, + then master can send next frame. + + config FMB_QUEUE_LENGTH + int "Modbus serial task queue length" + range 0 200 + default 20 + help + Modbus serial driver queue length. It is used by event queue task. + See the serial driver API for more information. + + config FMB_PORT_TASK_STACK_SIZE + int "Modbus port task stack size" + range 2048 16384 + default 4096 + help + Modbus port task stack size for rx/tx event processing. + It may be adjusted when debugging is enabled (for example). + + config FMB_BUFFER_SIZE + int "Modbus RX/TX buffer size" + range 256 2048 + default 260 if FMB_COMM_MODE_TCP_EN + default 256 if !FMB_COMM_MODE_TCP_EN + help + Modbus RX/TX buffer size for UART driver initialization. + This buffer is used for modbus frame transfer. The Modbus protocol maximum + frame size is 260 bytes (TCP). Bigger size can be used for non standard implementations. + + config FMB_SERIAL_ASCII_BITS_PER_SYMB + int "Number of data bits per ASCII character" + default 8 + range 7 8 + depends on FMB_COMM_MODE_ASCII_EN + help + This option defines the number of data bits per ASCII character. + + config FMB_SERIAL_ASCII_TIMEOUT_RESPOND_MS + int "Response timeout for ASCII communication mode (ms)" + default 1000 + range 200 5000 + depends on FMB_COMM_MODE_ASCII_EN + help + This option defines response timeout of slave in milliseconds for ASCII communication mode. + Thus the timeout will expire and allow the master program to handle the error. + + config FMB_PORT_TASK_PRIO + int "Modbus port task priority" + range 3 23 + default 10 + help + Modbus port data processing task priority. + The priority of Modbus controller task is equal to (CONFIG_FMB_PORT_TASK_PRIO - 1). + + choice FMB_PORT_TASK_AFFINITY + prompt "Modbus task affinity" + default FMB_PORT_TASK_AFFINITY_CPU0 + depends on !FREERTOS_UNICORE + help + Allows setting the core affinity of the Modbus controller task, i.e. whether the task is pinned to + particular CPU, or allowed to run on any CPU. + + config FMB_PORT_TASK_AFFINITY_NO_AFFINITY + bool "No affinity" + config FMB_PORT_TASK_AFFINITY_CPU0 + bool "CPU0" + config FMB_PORT_TASK_AFFINITY_CPU1 + bool "CPU1" + + endchoice + + config FMB_PORT_TASK_AFFINITY + hex + default FREERTOS_NO_AFFINITY if FMB_PORT_TASK_AFFINITY_NO_AFFINITY || FREERTOS_UNICORE + default 0x0 if FMB_PORT_TASK_AFFINITY_CPU0 + default 0x1 if FMB_PORT_TASK_AFFINITY_CPU1 + + config FMB_CONTROLLER_SLAVE_ID_SUPPORT + bool "Modbus controller slave ID support" + default y + help + Modbus slave ID support enable. + When enabled the Modbus command is supported by stack. + + config FMB_CONTROLLER_SLAVE_ID + hex "Modbus controller slave ID" + range 0 4294967295 + default 0x00112233 + depends on FMB_CONTROLLER_SLAVE_ID_SUPPORT + help + Modbus slave ID value to identify modbus device + in the network using command. + Most significant byte of ID is used as short device ID and + other three bytes used as long ID. + + config FMB_CONTROLLER_NOTIFY_TIMEOUT + int "Modbus controller notification timeout (ms)" + range 0 200 + default 20 + help + Modbus controller notification timeout in milliseconds. + This timeout is used to send notification about accessed parameters. + + config FMB_CONTROLLER_NOTIFY_QUEUE_SIZE + int "Modbus controller notification queue size" + range 0 200 + default 20 + help + Modbus controller notification queue size. + The notification queue is used to get information about accessed parameters. + + config FMB_CONTROLLER_STACK_SIZE + int "Modbus controller stack size" + range 2048 32768 + default 4096 + help + Modbus controller task stack size. The Stack size may be adjusted when + debug mode is used which requires more stack size (for example). + + config FMB_EVENT_QUEUE_TIMEOUT + int "Modbus stack event queue timeout (ms)" + range 10 500 + default 20 + help + Modbus stack event queue timeout in milliseconds. This may help to optimize + Modbus stack event processing time. + + config FMB_TIMER_USE_ISR_DISPATCH_METHOD + bool "Modbus timer uses ISR dispatch method" + default n + select ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD + select UART_ISR_IN_IRAM + help + If this option is set the Modbus stack uses ISR dispatch method + to send timeout events from the callback function called from ISR. + This option has dependency with the UART_ISR_IN_IRAM option which places UART interrupt + handler into IRAM to prevent delays related to processing of UART events. + + config FMB_EXT_TYPE_SUPPORT + bool "Modbus uses extended types to support third party devices" + default n + help + If this option is set the Modbus stack supports extended list of types + in data dictionary and conversion API to work with the extended types + otherwise the only legacy types are supported. The extended types include + integer, float, double types with different endianness and size. + +endmenu diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..88866e8 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# ESP-Modbus Library + +## Overview + +An Espressif ESP-Modbus Library (esp-modbus) is a library to support Modbus communication in the networks based on RS485, WiFi, Ethernet interfaces. The Modbus is a data communications protocol originally published by Modicon (now Schneider Electric) in 1979 for use with its programmable logic controllers (PLCs). + +* [ESP-Modbus component on GitHub](https://github.com/espressif/esp-modbus/tree/main) + +This library is to be used with Espressif’s IoT Development Framework, [ESP_IDF](https://github.com/espressif/esp-idf). The packages from this repository are uploaded to Espressif’s component repository. + +* [esp-modbus component in component repository](https://components.espressif.com/component/espressif/esp-modbus) + +You can add the component to your project via `idf.py add-dependency`. More information about idf-component-manager can be found in [Espressif API guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-component-manager.html) or [PyPi registry](https://pypi.org/project/idf-component-manager). + +The ESP-Modbus library can be used with ESP-IDF v5.0 and later. Some ESP-IDF releases include an earlier version of ESP-Modbus library inside freemodbus component. To use ESP-Modbus with these releases, users need to exclude the built-in freemodbus component from the build process, and update application components to depend on esp-modbus component instead. To exclude freemodbus component from compilation, add the following line to the project CMakeLists.txt file: + +``` +set(EXCLUDE_COMPONENTS freemodbus) +``` + +ESP-IDF v5.x and later releases do not include freemodbus component, so no extra steps are necessary when adding esp-modbus component. + +## Peculiarities Of Current Release + +The current release esp-modbus corresponds to the version `v2.x.x` (refer to idf_component.yml file) and supports creation of several instances of modbus master and slave objects. The public API interface is changed to allow creation of multiple communication objects with its own communication parameters, and the constructor API returns the handle to the interface structure that must be used as a first parameter for each API call for this particular object. For more information about the interface API and related changes see the official documentation for this release, described below. The goal of this beta release is to introduce new features and changes to the end users and get their feedback. The appropriate information or feature requests can be shared over on discussion page of the project. + +* [Discussions](https://github.com/espressif/esp-modbus/discussions/categories/general) +* [Issues](https://github.com/espressif/esp-modbus/issues) + +## Documentation + +The documentation can be found on the link below: + +* [ESP-Modbus documentation (English)](https://docs.espressif.com/projects/esp-modbus/en/stable/esp32/index.html) + +## Application Examples + +The examples below demonstrate the ESP-Modbus library of serial, TCP ports for slave and master implementations accordingly. + +- [Modbus Serial slave example](https://github.com/espressif/esp-modbus/tree/main/examples/serial/mb_serial_slave) + +- [Modbus Serial master example](https://github.com/espressif/esp-modbus/tree/main/examples/serial/mb_serial_master) + +- [Modbus TCP slave example](https://github.com/espressif/esp-modbus/tree/main/examples/tcp/mb_tcp_slave) + +- [Modbus TCP master example](https://github.com/espressif/esp-modbus/tree/main/examples/tcp/mb_tcp_master) + +Please refer to the specific example README.md for details. + +## Protocol References + +- [Modbus Organization with protocol specifications](https://modbus.org/specs.php) + +## Contributing + +We welcome contributions to this project in the form of bug reports, feature requests and pull requests. + +Issue reports and feature requests can be submitted using Github Issues: https://github.com/espressif/esp-modbus/issues. Please check if the issue has already been reported before opening a new one. + +Contributions in the form of pull requests should follow ESP-IDF project's [contribution guidelines](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/contribute/index.html). We kindly ask developers to start a discussion on an issue before proposing large changes to the project. + +## Licence + +The initial ESP-Modbus project was based on [FreeMODBUS library](https://github.com/cwalter-at/freemodbus), Copyright (c) 2006 Christian Walter and licensed under the BSD 3-clause license. + +Modbus Master related code is Copyright (c) 2013 Armink and licensed under BSD 3-clause license. + +All original code in this repository is Copyright (c) 2016-2022 Espressif Systems (Shanghai) Co. Ltd. + +The project is distributed under Apache 2.0 license. See the accompanying [LICENSE file](https://github.com/espressif/esp-modbus/blob/master/LICENSE) for a copy. \ No newline at end of file diff --git a/component.mk b/component.mk new file mode 100644 index 0000000..a596857 --- /dev/null +++ b/component.mk @@ -0,0 +1,29 @@ +INCLUDEDIRS := common/include +PRIV_INCLUDEDIRS := common port modbus modbus/ascii modbus/functions +PRIV_INCLUDEDIRS += modbus/rtu modbus/tcp modbus/include +PRIV_INCLUDEDIRS += serial_slave/port serial_slave/modbus_controller +PRIV_INCLUDEDIRS += serial_master/port serial_master/modbus_controller +PRIV_INCLUDEDIRS += tcp_slave/port tcp_slave/modbus_controller +PRIV_INCLUDEDIRS += tcp_master/port tcp_master/modbus_controller +SRCDIRS := common +SRCDIRS += modbus modbus/ascii modbus/functions modbus/rtu modbus/tcp +SRCDIRS += serial_slave/port serial_slave/modbus_controller +SRCDIRS += serial_master/port serial_master/modbus_controller +SRCDIRS += tcp_slave/port tcp_slave/modbus_controller +SRCDIRS += tcp_master/port tcp_master/modbus_controller +SRCDIRS += port + +COMPONENT_PRIV_INCLUDEDIRS = $(addprefix freemodbus/, \ + $(PRIV_INCLUDEDIRS) \ + ) + +COMPONENT_SRCDIRS = $(addprefix freemodbus/, \ + $(SRCDIRS) \ + ) + +COMPONENT_ADD_INCLUDEDIRS = $(addprefix freemodbus/, \ + $(INCLUDEDIRS) \ + ) + + + diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..888c0d3 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,58 @@ +# This is Doxygen configuration file +# +# Doxygen provides over 260 configuration statements +# To make this file easier to follow, +# it contains only statements that are non-default +# +# NOTE: +# It is recommended not to change defaults unless specifically required +# Test any changes how they affect generated documentation +# Make sure that correct warnings are generated to flag issues with documented code +# +# For the complete list of configuration statements see: +# http://doxygen.nl/manual/config.html + + +PROJECT_NAME = "IDF Programming Guide" + +## The 'INPUT' statement below is used as input by script 'gen-df-input.py' +## to automatically generate API reference list files heder_file.inc +## These files are placed in '_inc' directory +## and used to include in API reference documentation + +INPUT = \ + $(PROJECT_PATH)/modbus/mb_controller/common/include/esp_modbus_common.h \ + $(PROJECT_PATH)/modbus/mb_controller/common/include/esp_modbus_slave.h \ + $(PROJECT_PATH)/modbus/mb_controller/common/include/esp_modbus_master.h \ + $(PROJECT_PATH)/modbus/mb_controller/common/include/mb_endianness_utils.h + +## Get warnings for functions that have no documentation for their parameters or return value +## +WARN_NO_PARAMDOC = YES + +## Enable preprocessing and remove __attribute__(...) expressions from the INPUT files +## +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +PREDEFINED = \ + $(ENV_DOXYGEN_DEFINES) \ + +## Do not complain about not having dot +## +HAVE_DOT = NO + +## Generate XML that is required for Breathe +## +GENERATE_XML = YES +XML_OUTPUT = xml + +GENERATE_HTML = NO +HAVE_DOT = NO +GENERATE_LATEX = NO +GENERATE_MAN = YES +GENERATE_RTF = NO + +## Skip distracting progress messages +## +QUIET = YES diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..daec062 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,11 @@ +# ESP-Modbus Library + +This folder represents the official documentation for the ESP-Modbus library (**esp-modbus component documentation**). The Modbus is a data communications protocol originally published by Modicon (now Schneider Electric) in 1979 for use with its programmable logic controllers (PLCs). The Modbus has become a de facto standard communication protocol and is now a commonly available means of connecting industrial electronic devices. This library supports Modbus communication in the networks that are based on RS485 or Ethernet interfaces. + +# Hosted Documentation + +* English: https://docs.espressif.com/projects/esp-modbus/ + +# Building Documentation + +The documentation is built using the python package `esp-docs`, which can be installed by running `pip install esp-docs`. Running `build-docs --help` will give a summary of available options. For more information see the `esp-docs` documentation at https://github.com/espressif/esp-docs/blob/master/README.md \ No newline at end of file diff --git a/docs/_static/404-page__en.svg b/docs/_static/404-page__en.svg new file mode 100644 index 0000000..928ec63 --- /dev/null +++ b/docs/_static/404-page__en.svg @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/diag_frame.diag b/docs/_static/diag_frame.diag new file mode 100644 index 0000000..5ba19c6 --- /dev/null +++ b/docs/_static/diag_frame.diag @@ -0,0 +1,37 @@ +# Modbus float_abcd frame structure diagram + +blockdiag mb_float_frame { + # global properties + span_width = 5; + span_height = 5; + node_height = 30; + default_fontsize = 15; + default_group_color = lightgrey; + class spacer [shape=none, width=10]; + # tuning node properties and connections + 0,1,2 [class=spacer]; + 0; note + 1; header + 2; response -- uid -- cmd -- len -- fl_abcd -- crc + group float_abcd_packet { + label = "PDU"; + color = gray; + shape = line; + style = dashed; + group{uid,resp_uid};group{cmd,resp_cmd};group{len,resp_len};group{crc,resp_crc}; + group float_abcd{ + color = blue; + shape = line; + style = dashed; + fl_abcd;dt_abcd; + } + } + note[label="1: Unit Identificator, 2: Function code, 3: Data length, 4: Float data array, 5: Checksum",colwidth=6,color=lightyellow,shape=roundedbox] + header[label="FLOAT_ABCD = 0x4640e400 = 12345.0",colwidth=6,color=lightgreen] + response[label="RX:",color=yellow]; + uid[label="UID",numbered=1];cmd[label="FC",numbered=2]; + len[label="LENGTH",numbered=3];crc[label="CRC",numbered=5]; + resp_uid[label="0x01"];resp_cmd[label="0x03"];resp_len[label="0x08"];resp_crc[label="0x9065"]; + fl_abcd[label="FLOAT_ABCD",color=lightgreen,numbered=4]; + dt_abcd[label="0xE4004640",shape=note]; +} diff --git a/docs/_static/modbus-data-mapping.png b/docs/_static/modbus-data-mapping.png new file mode 100644 index 0000000..18fd2cf Binary files /dev/null and b/docs/_static/modbus-data-mapping.png differ diff --git a/docs/_static/modbus-segment.png b/docs/_static/modbus-segment.png new file mode 100644 index 0000000..80fb6bf Binary files /dev/null and b/docs/_static/modbus-segment.png differ diff --git a/docs/_static/modbus_docs_versions.js b/docs/_static/modbus_docs_versions.js new file mode 100644 index 0000000..6f74638 --- /dev/null +++ b/docs/_static/modbus_docs_versions.js @@ -0,0 +1,22 @@ +var DOCUMENTATION_VERSIONS = { + DEFAULTS: { has_targets: true, + supported_targets: [ "esp32", "esp32s2", "esp32s3", "esp32c2","esp32c3", "esp32c5", "esp32c6", "esp32c61", "esp32s2", "esp32s3, esp32p4" ] + }, + VERSIONS: [ + { name: "latest" }, + { name: "v1.0.1", old:true }, + { name: "v2.0.0", old:false } + ], + IDF_TARGETS: [ + { text: "ESP32", value: "esp32"}, + { text: "ESP32-S2", value: "esp32s2"}, + { text: "ESP32-S3", value: "esp32s3"}, + { text: "ESP32-C2", value: "esp32c2"}, + { text: "ESP32-C3", value: "esp32c3"}, + { text: "ESP32-C6", value: "esp32c6"}, + { text: "ESP32-H2", value: "esp32h2"}, + { text: "ESP32-P4", value: "esp32p4"}, + { text: "ESP32-C5", value: "esp32c5"}, + { text: "ESP32-C61", value: "esp32c61"} + ] +}; diff --git a/docs/_static/modbus_frame_examples.diag b/docs/_static/modbus_frame_examples.diag new file mode 100644 index 0000000..c9eb2a2 --- /dev/null +++ b/docs/_static/modbus_frame_examples.diag @@ -0,0 +1,61 @@ +# Modbus frame packaging examples + +blockdiag mb_master_frames { + # global properties + span_width = 5; + span_height = 5; + node_height = 30; + default_group_color = lightgrey; + default_fontsize = 15; + # tuning node properties and connections + group 16bit_packets { + label = "16bit frame"; + color = red; + shape = line; + style = dashed; + 16bit_notes; + } + group 32bit_packets { + label = "32bit frame"; + color = green; + shape = line; + style = dashed; + group{32bit_notes}; + } + group 64bit_packets { + label = "64bit frame"; + color = blue; + shape = line; + style = dashed; + 64bit_notes; + } + 16bit_notes[label="(UINT16, INT16) VALUE = 0x3039 = 12345", width=600, color=orange, shape = roundedbox]; + req_u16_hd1[label= "TX:| UID | FC | REG_START | REG_LEN | CRC |", color=lightyellow, width=520, colwidth=2, shape = roundedbox ,group=16bit_packets]; + req_u16_frm1[label="TX:| 01 | 03 | 00 04 | 00 02 | 85 CA |", color=lightgrey, width=540, colwidth=2,group=16bit_packets]; + rsp_u16_hd1[label= "RX:| UID | FC | LEN | UINT16_AB1 | UINT16_AB2 | CRC |", color=lightyellow, width=540, colwidth=2, shape = roundedbox ,group=16bit_packets]; + rsp_u16_frm1[label="RX:| 01 | 03 | 04 | 30 39 | 30 39 | F1 2C |", color=lightgrey, width=540, colwidth=2,group=16bit_packets]; + rsp_u16_hd2[label= "RX:| UID | FC | LEN | UINT16_BA1 | UINT16_BA2 | CRC |\n ", color=lightyellow, width=540, colwidth=2, shape = roundedbox, group=16bit_packets]; + rsp_u16_frm2[label="RX:| 01 | 03 | 04 | 39 30 | 39 30 | E4 E4 |\n", color=lightgrey, width=540, colwidth=2,group=16bit_packets]; + 32bit_notes[label="(UINT32, INT32, FLOAT32) VALUE = 0x4640e400 = 12345.0", width=600, color=lightgreen, shape = roundedbox]; + req_fl_hd1[label= "TX:| UID | FC | REG_START | REG_LEN | CRC |", color=lightyellow, width=540, colwidth=2, shape = roundedbox ,group=32bit_packets]; + req_fl_frm1[label="TX:| 01 | 03 | 00 XX | 00 04 | C5 CB |", color=lightgrey, width=540, colwidth=2,group=32bit_packets]; + rsp_fl_hd1[label= "RX:| UID | FC | LEN | FLOAT_ABCD1 | FLOAT_ABCD2 | CRC |", color=lightyellow, width=540, colwidth=2, shape = roundedbox ,group=32bit_packets]; + rsp_fl_frm1[label="RX:| 01 | 03 | 08 | E4 00 46 40 | E4 00 46 40 | 90 65 |", color=lightgrey, width=540, colwidth=2,group=32bit_packets]; + rsp_fl_hd2[label= "RX:| UID | FC | LEN | FLOAT_CDAB1 | FLOAT_CDAB2 | CRC |\n ", color=lightyellow, width=540, colwidth=2, shape = roundedbox, group=32bit_packets]; + rsp_fl_frm2[label="RX:| 01 | 03 | 08 | 46 40 E4 00 | 46 40 E4 00 | 18 71 |\n", color=lightgrey, width=540, colwidth=2,group=32bit_packets]; + rsp_fl_hd3[label= "RX:| UID | FC | LEN | FLOAT_BADC1 | FLOAT_BADC2 | CRC |\n ", color=lightyellow, width=540, colwidth=2, shape = roundedbox, group=32bit_packets]; + rsp_fl_frm3[label="RX:| 01 | 03 | 08 | 00 E4 40 46 | 00 E4 40 46 | 46 D3 |\n", color=lightgrey, width=540, colwidth=2,group=32bit_packets]; + rsp_fl_hd4[label= "RX:| UID | FC | LEN | FLOAT_DCAB1 | FLOAT_DCAB2 | CRC |\n ", color=lightyellow, width=540, colwidth=2, shape = roundedbox, group=32bit_packets]; + rsp_fl_frm4[label="RX:| 01 | 03 | 08 | 40 46 00 E4 | 40 46 00 E4 | 32 6B |\n", color=lightgrey, width=540, colwidth=2,group=32bit_packets]; + 64bit_notes[label="(UINT64, INT64, FLOAT64) VALUE = 0x40c81c8000000000 = 12345.0", width=600, color=lightblue, shape = roundedbox]; + req_dbl_hd1[label= "TX:| UID | FC | REG_START | REG_LEN | CRC |", color=lightyellow, width=540, colwidth=2, shape = roundedbox ,group=64bit_packets]; + req_dbl_frm1[label="TX:| 01 | 03 | 00 28 | 00 08 | C4 04 |", color=lightgrey, width=540, colwidth=2,group=64bit_packets]; + rsp_dbl_hd1[label= "RX:| UID | FC | LEN | DOUBLE_ABCDEFGH1 | DOUBLE_ABCDEFGH2 | CRC |", color=lightyellow, width=540, colwidth=2, shape = roundedbox ,group=64bit_packets]; + rsp_dbl_frm1[label="RX:| 01 | 03 | 10 | 00 00 00 00 1C 80 40 C8 | 00 00 00 00 1C 80 40 C8 | 9F 4B |", color=lightgrey, width=540, colwidth=2,group=64bit_packets]; + rsp_dbl_hd2[label= "RX:| UID | FC | LEN | DOUBLE_HGFEDCBA1 | DOUBLE_HGFEDCBA2 | CRC |\n ", color=lightyellow, width=540, colwidth=2, shape = roundedbox, group=64bit_packets]; + rsp_dbl_frm2[label="RX:| 01 | 03 | 10 | C8 40 80 1C 00 00 00 00 | C8 40 80 1C 00 00 00 00 | DF D3 |\n", color=lightgrey, width=540, colwidth=2,group=64bit_packets]; + rsp_dbl_hd3[label= "RX:| UID | FC | LEN | DOUBLE_GHEFCDAB1 | DOUBLE_GHEFCDAB2 | CRC |\n ", color=lightyellow, width=540, colwidth=2, shape = roundedbox, group=64bit_packets]; + rsp_dbl_frm3[label="RX:| 01 | 03 | 10 | 40 C8 1C 80 00 00 00 00 | 40 C8 1C 80 00 00 00 00 | B1 9C |\n", color=lightgrey, width=540, colwidth=2,group=64bit_packets]; + rsp_dbl_hd4[label= "RX:| UID | FC | LEN | DOUBLE_BADCFEHG1 | DOUBLE_BADCFEHG2 | CRC |\n ", color=lightyellow, width=540, colwidth=2, shape = roundedbox, group=64bit_packets]; + rsp_dbl_frm4[label="RX:| 01 | 03 | 10 | 00 00 00 00 80 1C C8 40 | 00 00 00 00 80 1C C8 40 | 86 94 |\n", color=lightgrey, width=540, colwidth=2,group=64bit_packets]; +} diff --git a/docs/conf_common.py b/docs/conf_common.py new file mode 100644 index 0000000..9e090d1 --- /dev/null +++ b/docs/conf_common.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Common (non-language-specific) configuration for Sphinx +# + +# type: ignore +# pylint: disable=wildcard-import +# pylint: disable=undefined-variable + +from __future__ import print_function, unicode_literals + +from esp_docs.conf_docs import * # noqa: F403,F401 + +extensions += ['sphinx_copybutton', + # Needed as a trigger for running doxygen + 'esp_docs.esp_extensions.dummy_build_system', + 'esp_docs.esp_extensions.run_doxygen' + ] + +# link roles config +github_repo = 'espressif/esp-modbus' + +# context used by sphinx_idf_theme +html_context['github_user'] = 'espressif' +html_context['github_repo'] = 'esp-modbus' +html_static_path = ['../_static'] + +# Extra options required by sphinx_idf_theme +project_slug = 'esp-modbus' +versions_url = './_static/modbus_docs_versions.js' + +idf_targets = [ 'esp32' ] +languages = ['en'] diff --git a/docs/en/applications_and_references.rst b/docs/en/applications_and_references.rst new file mode 100644 index 0000000..d76cce3 --- /dev/null +++ b/docs/en/applications_and_references.rst @@ -0,0 +1,78 @@ +Possible Communication Issues And Solutions +------------------------------------------- + +If the examples do not work as expected and slave and master boards are not able to communicate correctly, it is possible to find the reason for errors. The most important errors are described in master example output and formatted as below: + +.. highlight:: none + +:: + + E (1692332) MB_CONTROLLER_MASTER: mbc_master_get_parameter(111): SERIAL master get parameter failure error=(0x107) (ESP_ERR_TIMEOUT). + + +.. list-table:: Table 5 Modbus error codes and troubleshooting + :widths: 5 30 65 + :header-rows: 1 + + * - Error + - Description + - Possible solution + * - 0x106 + - ``ESP_ERR_NOT_SUPPORTED`` - Invalid register request - slave returned an exception because the requested register is not supported. + - Refer to slave register map. Check the master data dictionary for correctness. + * - 0x107 + - ``ESP_ERR_TIMEOUT`` - Slave response timeout - Modbus slave did not send response during configured slave response timeout. + - Measure and increase the maximum slave response timeout `idf.py menuconfig`, option ``CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND``. + Check physical connection or network configuration and make sure that the slave response can reach the master side. + If the application has some high performance tasks with higher priority than ``CONFIG_FMB_PORT_TASK_PRIO`` it is recommended to place Modbus tasks on the other core using an option ``CONFIG_FMB_PORT_TASK_AFFINITY``. + Configure the Modbus task's priority ``CONFIG_FMB_PORT_TASK_PRIO`` to ensure that the task gets sufficient processing time to handle Modbus stack events. + * - 0x108 + - ``ESP_ERR_INVALID_RESPONSE`` - Received unsupported response from slave or frame check failure. Master can not execute command handler because the command is either not supported or is incorrect. + - Check the physical connection then refer to register map of your slave to configure the master data dictionary properly. + * - 0x103 + - ``ESP_ERR_INVALID_STATE`` - Critical failure or FSM sequence failure or master FSM is busy processing previous request. + - Make sure your physical connection is working properly. Increase task stack size and check Modbus initialization sequence. + +Application Example +------------------- + +The examples below demonstrate the library port for serial, TCP slave and master implementations accordingly. The selection of stack is performed through KConfig menu option "Enable Modbus stack support ..." for appropriate communication mode and related configuration keys. + +.. _example_mb_slave: + +- `Modbus serial slave example `__ + +.. _example_mb_master: + +- `Modbus serial master example `__ + +.. _example_mb_tcp_master: + +- `Modbus TCP master example `__ + +.. _example_mb_tcp_slave: + +- `Modbus TCP slave example `__ + +Please refer to the specific example README.md for details. + +.. _modbus_organization: + +Protocol References +------------------- + + - `Modbus Organization with protocol specifications `__ + +API Reference +------------- + +.. include-build-file:: inc/esp_modbus_common.inc +.. include-build-file:: inc/esp_modbus_master.inc +.. include-build-file:: inc/esp_modbus_slave.inc + +.. _modbus_api_endianness_conversion: + +Modbus Endianness Conversion API Reference +------------------------------------------ + +.. include-build-file:: inc/mb_endianness_utils.inc \ No newline at end of file diff --git a/docs/en/conf.py b/docs/en/conf.py new file mode 100644 index 0000000..90f2495 --- /dev/null +++ b/docs/en/conf.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# English Language RTD & Sphinx config file +# +# Uses ../conf_common.py for most non-language-specific settings. + +# Importing conf_common adds all the non-language-specific +# parts to this conf module +try: + from conf_common import * # noqa: F403,F401 +except ImportError: + import os + import sys + sys.path.insert(0, os.path.abspath('../')) + from conf_common import * # noqa: F403,F401 + +import datetime + +current_year = datetime.datetime.now().year + +# General information about the project. +project = u'ESP-Modbus Programming Guide' +copyright = u'2019 - {}, Espressif Systems (Shanghai) Co., Ltd'.format(current_year) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = 'en' diff --git a/docs/en/index.rst b/docs/en/index.rst new file mode 100644 index 0000000..c16ebcd --- /dev/null +++ b/docs/en/index.rst @@ -0,0 +1,16 @@ +ESP-Modbus Library +================== + +An Espressif ESP-Modbus Library (esp-modbus) is a library to support Modbus communication in the networks based on RS485 or Ethernet interfaces. +The Modbus is a data communications protocol originally published by Modicon (now Schneider Electric) in 1979 for use with its programmable logic controllers (PLCs). + +.. toctree:: + :maxdepth: 1 + + The Overview, Messaging Model And Data Mapping + Modbus Port Initialization + Modbus Master API + Modbus Slave API + Applications and References + +.. note:: The ESP-Modbus library starting from version v2.0.0 supports creation of several instances of Modbus master and slave objects. The instance of each type shall be created using appropriate constructor API which uses the pointer to predefined object configuration options as first parameter and returns the pointer to created communication object as a second parameter. The returned pointer shall be saved and later be used as instance identifier in first parameter of each call of Modbus API functions. \ No newline at end of file diff --git a/docs/en/master_api_overview.rst b/docs/en/master_api_overview.rst new file mode 100644 index 0000000..9d5c4e4 --- /dev/null +++ b/docs/en/master_api_overview.rst @@ -0,0 +1,415 @@ +.. _modbus_api_master_overview: + +Modbus Master API Overview +-------------------------- + +The following overview describes how to setup Modbus master communication. The overview reflects a typical programming workflow and is broken down into the sections provided below: + +1. :ref:`modbus_api_port_initialization` - Initialization of Modbus controller interface for the selected port. +2. :ref:`modbus_api_master_configure_descriptor` - Configure data descriptors to access slave parameters. +3. :ref:`modbus_api_master_setup_communication_options` - Allows to setup communication options for selected port. +4. :ref:`modbus_api_master_start_communication` - Start stack and sending / receiving data. +5. :ref:`modbus_api_master_destroy` - Destroy Modbus controller and its resources. + +.. _modbus_api_master_configure_descriptor: + +Configuring Master Data Access +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The architectural approach of ESP_Modbus includes one level above standard Modbus IO driver. The additional layer is called Modbus controller and its goal is to add an abstraction such as CID - characteristic identifier. The CID is linked to a corresponding Modbus registers through the table called Data Dictionary and represents device physical parameter (such as temperature, humidity, etc.) in specific Modbus slave device. This approach allows the upper layer (e.g., MESH or MQTT) to be isolated from Modbus specifics thus simplify Modbus integration with other protocols/networks. + +The Data Dictionary is the list in the Modbus master which shall be defined by user to link each CID to its corresponding Modbus registers representation using Register Mapping table of the Modbus slave being used. +Each element in this data dictionary is of type :cpp:type:`mb_parameter_descriptor_t` and represents the description of one physical characteristic: + +.. list-table:: Table 1 Modbus master Data Dictionary description + :widths: 8 10 82 + :header-rows: 1 + + * - Field + - Description + - Detailed information + * - ``cid`` + - Characteristic ID + - The identifier of characteristic (must be unique). + * - ``param_key`` + - Characteristic Name + - String description of the characteristic. + * - ``param_units`` + - Characteristic Units + - Physical Units of the characteristic. + * - ``mb_slave_addr`` + - Modbus Slave Address + - The short address of the device with correspond parameter UID. + * - ``mb_param_type`` + - Modbus Register Type + - Type of Modbus register area. + :cpp:enumerator:`MB_PARAM_INPUT`, :cpp:enumerator:`MB_PARAM_HOLDING`, :cpp:enumerator:`MB_PARAM_COIL`, :cpp:enumerator:`MB_PARAM_DISCRETE` - represents Input , Holding, Coil and Discrete input register area accordingly; + * - ``mb_reg_start`` + - Modbus Register Start + - Relative register address of the characteristic in the register area. + * - ``mb_size`` + - Modbus Register Size + - Length of characteristic in registers (two bytes). + * - ``param_offset`` + - Instance Offset + - Offset to instance of the characteristic in bytes. It is used to calculate the absolute address to the characteristic in the storage structure. + It is optional field and can be set to zero if the parameter is not used in the application. + * - ``param_type`` + - Data Type + - Specifies type of the characteristic. Possible types are described in the section :ref:`modbus_mapping_complex_data_types`. + * - ``param_size`` + - Data Size + - The storage size of the characteristic (in bytes) describes the size of data to keep into data instance during mapping. For the :ref:`modbus_mapping_complex_data_types` this allows to define the data container of the corresponded type. + * - ``param_opts`` + - Parameter Options + - Limits, options of characteristic used during processing of alarm in user application (optional) + * - ``access`` + - Parameter access type + - Can be used in user application to define the behavior of the characteristic during processing of data in user application; + :cpp:enumerator:`PAR_PERMS_READ_WRITE_TRIGGER`, :cpp:enumerator:`PAR_PERMS_READ`, :cpp:enumerator:`PAR_PERMS_READ_WRITE_TRIGGER`; + +.. note:: The ``cid`` and ``param_key`` have to be unique. Please use the prefix to the parameter key if you have several similar parameters in your register map table. + +Examples Of Mapping +@@@@@@@@@@@@@@@@@@@ + +Please refer to section :ref:`modbus_mapping_complex_data_types` for more information about used data types. + +Example 1: Configure access to legacy parameter types is described below. + +.. list-table:: Table 2 Example Register mapping table of Modbus slave + :widths: 5 5 2 10 5 5 68 + :header-rows: 1 + + * - CID + - Register + - Length + - Range + - Type + - Units + - Description + * - 0 + - 30000 + - 4 + - MAX_UINT + - U32 + - Not defined + - Serial number of device (4 bytes) read-only + * - 1 + - 30002 + - 2 + - MAX_UINT + - U16 + - Not defined + - Software version (4 bytes) read-only + * - 2 + - 40000 + - 4 + - -20..40 + - FLOAT + - DegC + - Room temperature in DegC. Writing a temperature value to this register for single point calibration. + * - 3 + - 40002 + - 16 + - 1..100 bytes + - ASCII or binary array + - Not defined + - Device name (16 bytes) ASCII string. The type of `PARAM_TYPE_ASCII` allows to read/write complex parameter (string or binary data) that corresponds to one CID. + +.. code:: c + + // Enumeration of modbus slave addresses accessed by master device + enum { + MB_DEVICE_ADDR1 = 1, + MB_DEVICE_ADDR2, + MB_SLAVE_COUNT + }; + + // Enumeration of all supported CIDs for device + enum { + CID_SER_NUM1 = 0, + CID_SW_VER1, + CID_DEV_NAME1, + CID_TEMP_DATA_1, + CID_SER_NUM2, + CID_SW_VER2, + CID_DEV_NAME2, + CID_TEMP_DATA_2 + }; + + // Example Data Dictionary for Modbus parameters in 2 slaves in the segment + mb_parameter_descriptor_t device_parameters[] = { + // CID, Name, Units, Modbus addr, register type, Modbus Reg Start Addr, Modbus Reg read length, + // Instance offset (NA), Instance type, Instance length (bytes), Options (NA), Permissions + { CID_SER_NUM1, STR("Serial_number_1"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 0, 2, + 0, PARAM_TYPE_U32, 4, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_SW_VER1, STR("Software_version_1"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_DEV_NAME1, STR("Device name"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 2, 8, + 0, PARAM_TYPE_ASCII, 16, OPTS( 0, 0, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_TEMP_DATA_1, STR("Temperature_1"), STR("C"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 0, 2, + 0, PARAM_TYPE_FLOAT, 4, OPTS( 16, 30, 1 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_SER_NUM2, STR("Serial_number_2"), STR("--"), MB_DEVICE_ADDR2, MB_PARAM_INPUT, 0, 2, + 0, PARAM_TYPE_U32, 4, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_SW_VER2, STR("Software_version_2"), STR("--"), MB_DEVICE_ADDR2, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_DEV_NAME2, STR("Device name"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 2, 8, + 0, PARAM_TYPE_ASCII, 16, OPTS( 0, 0, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_TEMP_DATA_2, STR("Temperature_2"), STR("C"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, 0, 2, + 0, PARAM_TYPE_FLOAT, 4, OPTS( 20, 30, 1 ), PAR_PERMS_READ_WRITE_TRIGGER }, + }; + // Calculate number of parameters in the table + uint16_t num_device_parameters = (sizeof(device_parameters) / sizeof(device_parameters[0])); + +Example 2: Configure access using extended parameter types for third-party devices. + +.. list-table:: Table 3 Example Register mapping table of Modbus slave + :widths: 2 4 2 10 3 68 + :header-rows: 1 + + * - CID + - Register + - Length + - Range + - Units + - Description + * - 0 + - 40000 + - 4 + - 0 ... 255 + - No units + - :cpp:enumerator:`PARAM_TYPE_U8_A` - unsigned integer 8-bit + * - 1 + - 40002 + - 4 + - 0 ... 65535 + - No Units + - :cpp:enumerator:`PARAM_TYPE_U16_AB` uinsigned integer 16-bit + * - 3 + - 40004 + - 8 + - 0 ... Unsigned integer 32-bit range + - No units + - :cpp:enumerator:`PARAM_TYPE_U32_ABCD` - unsigned integer 32-bit in ABCD format + * - 4 + - 40008 + - 8 + - 0 ... Unsigned integer 32-bit range + - No units + - :cpp:enumerator:`PARAM_TYPE_FLOAT_CDAB` - FLOAT 32-bit value in CDAB format + * - 5 + - 400012 + - 16 + - 0 ... Unsigned integer 64-bit range + - No units + - :cpp:enumerator:`PARAM_TYPE_U64_ABCDEFGH` - Unsigned integer 64-bit value in ABCDEFGH format + * - 6 + - 400020 + - 16 + - 0 ... Unsigned integer 64-bit range + - No units + - :cpp:enumerator:`PARAM_TYPE_DOUBLE_HGFEDCBA` - Double precision 64-bit value in HGFEDCBA format + +.. code:: c + + #include "limits.h" + #include "mbcontroller.h" + + #define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1)) + #define HOLD_REG_START(field) (HOLD_OFFSET(field) >> 1) + #define HOLD_REG_SIZE(field) (sizeof(((holding_reg_params_t *)0)->field) >> 1) + + #pragma pack(push, 1) + // Example structure that contains parameter arrays of different types + // with different options of endianness. + typedef struct + { + uint16_t holding_u8_a[2]; + uint16_t holding_u16_ab[2]; + uint32_t holding_uint32_abcd[2]; + float holding_float_cdab[2]; + double holding_uint64_abcdefgh[2]; + double holding_double_hgfedcba[2]; + } holding_reg_params_t; + #pragma pack(pop) + + // Enumeration of modbus slave addresses accessed by master device + enum { + MB_DEVICE_ADDR1 = 1, // Short address of Modbus slave device + MB_SLAVE_COUNT + }; + + // Enumeration of all supported CIDs for device (used in parameter definition table) + enum { + CID_HOLD_U8_A = 0, + CID_HOLD_U16_AB, + CID_HOLD_UINT32_ABCD, + CID_HOLD_FLOAT_CDAB, + CID_HOLD_UINT64_ABCDEFGH, + CID_HOLD_DOUBLE_HGFEDCBA, + CID_COUNT + }; + + // Example Data Dictionary for to address parameters from slaves with different options of endianness + mb_parameter_descriptor_t device_parameters[] = { + // CID, Name, Units, Modbus addr, register type, Modbus Reg Start Addr, Modbus Reg read length, + // Instance offset (NA), Instance type, Instance length (bytes), Options (NA), Permissions + { CID_HOLD_U8_A, STR("U8_A"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + HOLD_REG_START(holding_u8_a), HOLD_REG_SIZE(holding_u8_a), + HOLD_OFFSET(holding_u8_a), PARAM_TYPE_U8_A, (HOLD_REG_SIZE(holding_u8_a) << 1), + OPTS( 0, UCHAR_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_U16_AB, STR("U16_AB"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + HOLD_REG_START(holding_u16_ab), HOLD_REG_SIZE(holding_u16_ab), + HOLD_OFFSET(holding_u16_ab), PARAM_TYPE_U16_AB, (HOLD_REG_SIZE(holding_u16_ab) << 1), + OPTS( 0, USHRT_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_ABCD, STR("UINT32_ABCD"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + HOLD_REG_START(holding_uint32_abcd), HOLD_REG_SIZE(holding_uint32_abcd), + HOLD_OFFSET(holding_uint32_abcd), PARAM_TYPE_U32_ABCD, (HOLD_REG_SIZE(holding_uint32_abcd) << 1), + OPTS( 0, ULONG_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_CDAB, STR("FLOAT_CDAB"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + HOLD_REG_START(holding_float_cdab), HOLD_REG_SIZE(holding_float_cdab), + HOLD_OFFSET(holding_float_cdab), PARAM_TYPE_FLOAT_CDAB, (HOLD_REG_SIZE(holding_float_cdab) << 1), + OPTS( 0, ULONG_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT64_ABCDEFGH, STR("UINT64_ABCDEFGH"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + HOLD_REG_START(holding_uint64_abcdefgh), HOLD_REG_SIZE(holding_uint64_abcdefgh), + HOLD_OFFSET(holding_uint64_abcdefgh), PARAM_TYPE_UINT64_ABCDEFGH, (HOLD_REG_SIZE(holding_uint64_abcdefgh) << 1), + OPTS( 0, ULLONG_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_HGFEDCBA, STR("DOUBLE_HGFEDCBA"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + HOLD_REG_START(holding_double_hgfedcba), HOLD_REG_SIZE(holding_double_hgfedcba), + HOLD_OFFSET(holding_double_hgfedcba), PARAM_TYPE_DOUBLE_HGFEDCBA, (HOLD_REG_SIZE(holding_double_hgfedcba) << 1), + OPTS( 0, ULLONG_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER } + }; + uint16_t num_device_parameters = (sizeof(device_parameters) / sizeof(device_parameters[0])); + +The example above describes the definition of just several extended types. The types described in the :ref:`modbus_mapping_complex_data_types` allow to address the most useful value formats from devices of known third-party vendors. +Once the type of characteristic is defined in data dictionary the stack is responsible for conversion of values to/from the corresponding type option into the format recognizable by compiler. + +.. note:: Please refer to your vendor device manual and its mapping table to select the types suitable for your device. + +The Modbus stack contains also the :ref:`modbus_api_endianness_conversion` - endianness conversion API functions that allow to convert values from/to each extended type into compiler representation. + +During initialization of the Modbus stack, a pointer to the Data Dictionary (called descriptor) must be provided as the parameter of the function below. + +:cpp:func:`mbc_master_set_descriptor`: + +Initialization of master descriptor. The descriptor represents an array of type :cpp:type:`mb_parameter_descriptor_t` and describes all the characteristics accessed by master. + +.. code:: c + + static void *master_handle = NULL; // Must exist in the module and be initialized prior to call + .... + // Set master data dictionary for initialized master instance - master_handle + ESP_ERROR_CHECK(mbc_master_set_descriptor(master_handle, &device_parameters[0], num_device_parameters)); + +The Data Dictionary can be initialized from SD card, MQTT or other source before start of stack. Once the initialization and setup is done, the Modbus controller allows the reading of complex parameters from any slave included in descriptor table using its CID. +Refer to :ref:`example TCP master `, :ref:`example Serial master ` for more information. + +.. _modbus_api_master_start_communication: + +Master Communication +^^^^^^^^^^^^^^^^^^^^ + +The starting of the Modbus controller is the final step in enabling communication. This is performed using function below: + +:cpp:func:`mbc_master_start` + +.. code:: c + + static void *master_handle = NULL; // Pointer to allocated interface structure + .... + esp_err_t err = mbc_master_start(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mb controller start fail, err = 0x%x.", (int)err); + } + +The list of functions below are used by the Modbus master stack from a user's application: + +:cpp:func:`mbc_master_send_request`: + +This function executes a blocking Modbus request. The master sends a data request (as defined in parameter request structure :cpp:type:`mb_param_request_t`) and then blocks until a response from corresponding slave and returns the status of command execution. This function provides a standard way for read/write access to Modbus devices in the network. + +.. note:: The function can be used to form the custom request with non-standard commands to resolve compatibility issues with the custom slaves. If it is not the case the regular API should be used: :cpp:func:`mbc_master_set_parameter`, :cpp:func:`mbc_master_get_parameter`. + +:cpp:func:`mbc_master_get_cid_info`: + +The function gets information about each characteristic supported in the data dictionary and returns the characteristic's description in the form of the :cpp:type:`mb_parameter_descriptor_t` structure. Each characteristic is accessed using its CID. + +:cpp:func:`mbc_master_get_parameter` + +The function reads the data of a characteristic defined in the parameters of a Modbus slave device. The additional data for request is taken from parameter description table. + +:cpp:func:`mbc_master_get_parameter_with` + +The function allows to read the data of a characteristic from any slave device addressed by `uid` parameter of the function instead of slave address defined in the data dictionary. In this case the ``mb_slave_addr`` field of the parameter descriptor :cpp:type:`mb_parameter_descriptor_t` shall be equal to ``MB_SLAVE_ADDR_PLACEHOLDER``. In case of TCP type of communication the connection phase should be completed prior call of this function. + +Example: + +.. code:: c + + static void *master_handle = NULL; + .... + const mb_parameter_descriptor_t* param_descriptor = NULL; + uint8_t temp_data[4] = {0}; // temporary buffer to hold maximum CID size + uint8_t type = 0; + .... + // Get the information for characteristic cid from data dictionary + esp_err_t err = mbc_master_get_cid_info(cid, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) { + err = mbc_master_get_parameter(master_handle, param_descriptor->cid, (uint8_t*)temp_data, &type); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%" PRIx32 ") read successful.", + param_descriptor->cid, + param_descriptor->param_key, + param_descriptor->param_units, + *(uint32_t*)temp_data); + } else { + ESP_LOGE(TAG, "Characteristic #%d (%s) read fail, err = 0x%x (%s).", + param_descriptor->cid, + param_descriptor->param_key, + (int)err, + (char*)esp_err_to_name(err)); + } + } else { + ESP_LOGE(TAG, "Could not get information for characteristic %d.", cid); + } + +:cpp:func:`mbc_master_set_parameter` + +The function writes characteristic's value defined as `cid` parameter in corresponded slave device. The additional data for parameter request is taken from master parameter description table. + +:cpp:func:`mbc_master_set_parameter_with` + +The function is similar to previous function but allows to set the data of a characteristic in any slave device addressed by `uid` parameter of the function instead of the slave address ``mb_slave_addr`` field defined in the data dictionary. The corresponded ``mb_slave_addr`` field for the characteristic in the object disctionary shall be defined as ``MB_SLAVE_ADDR_PLACEHOLDER``. + +.. note:: When the TCP mode of communication is used the functions above additionally check the connection state of the slave being accessed and return error if the slave connection is not actual. + +.. code:: c + + static void *master_handle = NULL; + .... + uint8_t type = 0; // Type of parameter + uint8_t temp_data[4] = {0}; // temporary buffer + // Read the characteristic from slave and save the data to temp_data instance + esp_err_t err = mbc_master_set_parameter(master_handle, CID_TEMP_DATA_2, (uint8_t*)temp_data, &type); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Set parameter data successfully."); + } else { + ESP_LOGE(TAG, "Set data fail, err = 0x%x (%s).", (int)err, (char*)esp_err_to_name(err)); + } + +.. _modbus_api_master_destroy: + +Modbus Master Teardown +^^^^^^^^^^^^^^^^^^^^^^ + +This function stops Modbus communication stack and destroys controller interface and free all used active objects. + +:cpp:func:`mbc_master_destroy` + +.. code:: c + + // Pointer to allocated interface structure, must be intitialized by constructor + static void *master_handle = NULL; + ... + ESP_ERROR_CHECK(mbc_master_destroy(master_handle)); diff --git a/docs/en/overview_messaging_and_mapping.rst b/docs/en/overview_messaging_and_mapping.rst new file mode 100644 index 0000000..dd235ed --- /dev/null +++ b/docs/en/overview_messaging_and_mapping.rst @@ -0,0 +1,283 @@ +ESP-Modbus +========== + +Overview +-------- + +The Modbus serial communication protocol is de facto standard protocol widely used to connect industrial electronic devices. Modbus allows communication among many devices connected to the same network, for example, a system that measures temperature and humidity and communicates the results to a computer. The Modbus protocol uses several types of data: Holding Registers, Input Registers, Coils (single bit output), Discrete Inputs. Versions of the Modbus protocol exist for serial port and for Ethernet and other protocols that support the Internet protocol suite. There are many variants of Modbus protocols, some of them are: + + * ``Modbus RTU`` — This is used in serial communication and makes use of a compact, binary representation of the data for protocol communication. The RTU format follows the commands/data with a cyclic redundancy check checksum as an error check mechanism to ensure the reliability of data. Modbus RTU is the most common implementation available for Modbus. A Modbus RTU message must be transmitted continuously without inter-character hesitations. Modbus messages are framed (separated) by idle (silent) periods. The RS-485 interface communication is usually used for this type. + * ``Modbus ASCII`` — This is used in serial communication and makes use of ASCII characters for protocol communication. The ASCII format uses a longitudinal redundancy check checksum. Modbus ASCII messages are framed by leading colon (":") and trailing newline (CR/LF). + * ``Modbus TCP/IP or Modbus TCP`` — This is a Modbus variant used for communications over TCP/IP networks, connecting over port 502. It does not require a checksum calculation, as lower layers already provide checksum protection. + +.. note:: This documentation (and included code snippets) requires some familiarity with the Modbus protocol. Refer to the Modbus Organization's with protocol specifications for specifics :ref:`modbus_organization`. + +.. _modbus_supported_communication_options: + +Modbus Supported Communication Options +-------------------------------------- + +The Modbus library supports the standard communication options as per Modbus specification stated below. + +.. list-table:: Standard Modbus communication options + :widths: 10 90 + :header-rows: 1 + + * - Modbus option + - Description of the option + * - RTU communication + - * 1 start bit + * 8 data bits, least significant bit sent first + * 1 bit for even / odd parity-no bit for no parity + * 1 stop bit if parity is used, 2 stop bits if no parity + * Cyclical Redundancy Check (CRC) + * - ASCII communication + - * 1 start bit + * 7-8 data bits, least significant bit sent first + * 1 bit for even / odd parity-no bit for no parity + * 1 stop bit if parity is used, 2 stop bits if no parity + * Longitudinal Redundancy Check (LRC) + * - TCP communication + - * Communications between client (master) - server (slave) over TCP/IP networks + * Connection uses the standard port 502 + * The frames do not require checksum calculation (provided by lower layers) + +Some vendors may use subset of communication options. In this case the detailed information is clarified in the device manual and it is possible to override the standard communication options for support of such devices. +Please refer to :ref:`modbus_api_slave_setup_communication_options`, :ref:`modbus_api_master_setup_communication_options` for more information. + +Messaging Model And Data Mapping +-------------------------------- + +Modbus is an application protocol that defines rules for messaging structure and data organization that are independent of the data transmission medium. Traditional serial Modbus is a register-based protocol that defines message transactions that occur between master(s) and slave devices (multiple masters are allowed on using Modbus TCP/IP). The slave devices listen for communication from the master and simply respond as instructed. The master(s) always controls communication and may communicate directly to one slave, or all connected slaves, but the slaves cannot communicate directly with each other. + +.. figure:: ../_static/modbus-segment.png + :align: center + :scale: 80% + :alt: Modbus segment diagram + :figclass: align-center + + Modbus segment diagram + +.. note:: It is assumed that the number of slaves and their register maps are known by the Modbus master before the start of stack. + +The register map of each slave device is usually part of its device manual. A Slave device usually permits configuration of its short slave address and communication options that are used within the device's network segment. + +The Modbus protocol allows devices to map data to four types of registers (Holding, Input, Discrete, Coil). The figure below illustrates an example mapping of a device's data to the four types of registers. + +.. figure:: ../_static/modbus-data-mapping.png + :align: center + :scale: 80% + :alt: Modbus data mapping + :figclass: align-center + + Modbus data mapping + +.. _modbus_mapping_complex_data_types: + +Mapping Of Complex Data Types +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + +As per section 4.2 of Modbus specification, "MODBUS uses a ``big-Endian`` representation for addresses and data items. This means that when a numerical quantity larger than a single byte is transmitted, the most significant byte is sent first". The biggest official structure defined by the Modbus specification is a 16-bit word register, which is 2 bytes. However, vendors sometimes group two or even four 16-bit registers together to be interpretted as 32-bit or 64-bit values, respectively. It is also possible when the Modbus vendors group many registers together for serial numbers, text strings, time/date, etc. Regardless of how the vendor intends the data to be interpreted, the Modbus protocol itself simply transfers 16-bit word registers. These values grouped from registers may use either little-endian or big-endian register order. + +.. note:: Each individual 16-bit register, is encoded in big-endian order (assuming the Modbus device abides by the Modbus specification). However, the 32-bit and 64-bit types naming conventions like ABCD or ABCDEFGH, does not take into account the network format byte order of frame. For example: the ABCD prefix for 32-bit values means the common Modbus mapping format and corresponds to the CDAB on network format (order in the frame). + +Common Data Types Supported By Modbus Vendors +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + +.. list-table:: Table 1 basic types used by Modbus vendors + :widths: 8 3 20 + :header-rows: 1 + + * - Type + - Range + - Format description + * - U8, I8 - Unsigned/Signed 8-bit type + - (0 .. 255)/(-128 .. 127) + - Common unsigned 8-bit type that is stored usually in one Modbus register. The value can be stored in HI or LO byte of the register or packed with the next byte into one 16 - bit register. + * - U16 - Unsigned integer 16-bit type + - 0 - 65535 + - Stored in one 16-bit register. The values can be stored with AB or BA endianness. + * - I16 - Signed integer 16-bit type + - -32768 to 32767 is allowed. + - Stored in one 16-bit register. The values can be stored with AB or BA forendiannessmat. + * - I32 - Signed long integer 32-bit type + - -2147483648 to 2147483647 is allowed. + - Stored in two consecutive 16-bit register. The values can be stored with ABCD - DCBA endianness (see below). + * - U32 - Unsigned long integer 32-bit type + - 0 to 4294967295 is allowed. + - Stored in two consecutive 16-bit register. The values can be stored with ABCD - DCBA endianness. + * - U64 Unsigned Long long integers (Unsigned integer 64) + - 0 to 18446744073709551615 is allowed. + - Stored in four consecutive 16-bit register. The values can be stored with ABCDEFGH - BADCFEHG endianness. + * - I64 Signed Long long integers (Signed integer 64) + - -9223372036854775808 to 9223372036854775807 is allowed. + - Stored in four consecutive 16-bit register. The values can be stored with ABCDEFGH - BADCFEHG endianness. + * - Floating point single precision 32-bit + - 1.17549435E-38 to 3.40282347E+38 is allowed. + - Stored in two consecutive 16-bit register per IEEE754. The values can be stored with ABCD - DCBA endianness. + * - Floating point double precision 64-bit + - +/-5.0E-324 to +/-1.7E+308 is allowed. + - Stored in four consecutive 16-bit register per IEEE754. The values can be stored with ABCDEFGH - BADCFEHG endianness. + +As showed in the table above the float and double types do not fit to the 16-bit register and reguire several consecutive registers be used to store the value. However, different manufacturers store the consecutive bytes in different order (not standardized). For example: The DCBA prefix means inversed Modbus format (BADC order on network format). + +.. list-table:: Table 2 Modbus byte order for extended types + :widths: 3 28 + :header-rows: 1 + + * - Postfix + - Format description + * - ABCD + - Big endian, high order byte first + * - CDAB + - Big endian, reversed register order (Little endian with byte swap) + * - BADC + - Little endian, reversed register order (Big endian with byte swap) + * - DCBA + - Little endian (Low order byte first) + +The extended data types are used to define all possible combinations of groupped values are represented below and correspond to ``param_type`` field of the data dictionary as described in the table below: + +.. list-table:: Table 3 Modbus extended data types of characteristics + :widths: 6 28 10 + :header-rows: 1 + + * - Type + - Format type description (common format) + - Format type (network format) + * - :cpp:enumerator:`PARAM_TYPE_U8` + - compatibility type corresponds to :cpp:enumerator:`PARAM_TYPE_U8_A` + - Unsigned integer 8 bit type + * - :cpp:enumerator:`PARAM_TYPE_U16` + - Unsigned integer 16 bit type, corresponds to :cpp:enumerator:`PARAM_TYPE_U16_AB` + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_U32` + - Default unsigned integer 32 bit type, corresponds to :cpp:enumerator:`PARAM_TYPE_U32_ABCD` + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_FLOAT` + - Default unsigned integer 32 bit type, corresponds to :cpp:enumerator:`PARAM_TYPE_FLOAT_ABCD` + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_ASCII` + - Default ASCII string format + - Packed ASCII string data + * - :cpp:enumerator:`PARAM_TYPE_BIN` + - Binary data type + - Default type for binary packed data + * - :cpp:enumerator:`PARAM_TYPE_I8_A` + - I8 signed integer in low byte of register, high byte is zero + - I8 signed integer LO + * - :cpp:enumerator:`PARAM_TYPE_I8_B` + - I8 signed integer in high byte of register, low byte is zero + - I8 signed integer HI + * - :cpp:enumerator:`PARAM_TYPE_U8_A` + - U8 unsigned integer written to low byte of register, high byte is zero + - U8 unsigned integer LO + * - :cpp:enumerator:`PARAM_TYPE_U8_B` + - U8 unsigned integer written to hi byte of register, low byte is zero + - U8 unsigned integer HI + * - :cpp:enumerator:`PARAM_TYPE_I16_AB` + - I16 signed integer, big endian + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_I16_BA` + - I16 signed integer, little endian + - Little endian + * - :cpp:enumerator:`PARAM_TYPE_U16_AB` + - U16 unsigned integer, big endian + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_U16_BA` + - U16 unsigned integer, little endian + - Little endian + * - :cpp:enumerator:`PARAM_TYPE_I32_ABCD` + - I32 ABCD signed integer, big endian + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_I32_CDAB` + - I32 CDAB signed integer, big endian, reversed register order + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_I32_BADC` + - I32 BADC signed integer, little endian, reversed register order + - Little endian + * - :cpp:enumerator:`PARAM_TYPE_I32_DCBA` + - I32 DCBA signed integer, little endian + - Big endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_U32_ABCD` + - U32 ABCD unsigned integer, big endian + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_U32_CDAB` + - U32 CDAB unsigned integer, big endian, reversed register order + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_U32_BADC` + - U32 BADC unsigned integer, little endian, reversed register order + - Little endian + * - :cpp:enumerator:`PARAM_TYPE_U32_DCBA` + - U32 DCBA unsigned integer, little endian + - Big endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_FLOAT_ABCD` + - Float ABCD floating point, big endian + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_FLOAT_CDAB` + - Float CDAB floating point, big endian, reversed register order + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_FLOAT_BADC` + - Float BADC floating point, little endian, reversed register order + - Little endian + * - :cpp:enumerator:`PARAM_TYPE_FLOAT_DCBA` + - Float DCBA floating point, little endian + - Big endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_I64_ABCDEFGH` + - I64, ABCDEFGH signed integer, big endian + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_I64_HGFEDCBA` + - I64, HGFEDCBA signed integer, little endian + - Big endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_I64_GHEFCDAB` + - I64, GHEFCDAB signed integer, big endian, reversed register order + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_I64_BADCFEHG` + - I64, BADCFEHG signed integer, little endian, reversed register order + - Little endian + * - :cpp:enumerator:`PARAM_TYPE_U64_ABCDEFGH` + - U64, ABCDEFGH unsigned integer, big endian + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_U64_HGFEDCBA` + - U64, HGFEDCBA unsigned integer, little endian + - Big endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_U64_GHEFCDAB` + - U64, GHEFCDAB unsigned integer, big endian, reversed register order + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_U64_BADCFEHG` + - U64, BADCFEHG unsigned integer, little endian, reversed register order + - Little endian + * - :cpp:enumerator:`PARAM_TYPE_DOUBLE_ABCDEFGH` + - Double ABCDEFGH floating point, big endian + - Little endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_DOUBLE_HGFEDCBA` + - Double HGFEDCBA floating point, little endian + - Big endian byte swap + * - :cpp:enumerator:`PARAM_TYPE_DOUBLE_GHEFCDAB` + - Double GHEFCDAB floating point, big endian, reversed register order + - Big endian + * - :cpp:enumerator:`PARAM_TYPE_DOUBLE_BADCFEHG` + - Double BADCFEHG floating point, little endian, reversed register order + - Little endian + +.. note:: The support for the extended data types should be enabled using the option ``CONFIG_FMB_EXT_TYPE_SUPPORT`` in kconfig menu. + +The below diagrams show how the extended data types appear on network layer. + +.. blockdiag:: /../_static/diag_frame.diag + :scale: 80% + :caption: Modbus master response with ABCD frame + :align: center + +.. blockdiag:: /../_static/modbus_frame_examples.diag + :scale: 80% + :caption: Modbus frame packaging examples (16-bit, 32-bit, 64-bit data) + :align: center + +The approach showed above can be used to pack the data into MBAP frames used by Modbus TCP as well as for other types with similar size. + +The following sections give an overview of how to use the ESP_Modbus component found under `components/freemodbus`. The sections cover initialization of a Modbus port, and the setup a master or slave device accordingly: + +- :ref:`modbus_api_port_initialization` +- :ref:`modbus_api_slave_overview` +- :ref:`modbus_api_master_overview` diff --git a/docs/en/port_initialization.rst b/docs/en/port_initialization.rst new file mode 100644 index 0000000..0ed1ad1 --- /dev/null +++ b/docs/en/port_initialization.rst @@ -0,0 +1,172 @@ +.. _modbus_api_port_initialization: + +Modbus Port Initialization +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ESP_Modbus supports Modbus SERIAL and TCP communication objects and an object must be initialized before calling any other Modbus API. The functions below are used to create and then initialize Modbus controller interface (either master or slave) over a particular transmission medium (either Serial or TCP/IP): + +- :cpp:func:`mbc_slave_create_serial` +- :cpp:func:`mbc_master_create_serial` +- :cpp:func:`mbc_master_create_tcp` +- :cpp:func:`mbc_slave_create_tcp` + +Calling the constructor function allows to create communication object with the specific communication options be defined in the configuration structure. The pointer to communication object is returned by constructor API and is being used as a handle for each following API call. + +.. code:: c + + // Pointer to allocate interface structure + // is used later as a first parameter for each API call + static void *master_handle = NULL; + ESP_ERROR_CHECK(mbc_master_create_serial(&config, &master_handle)); + ... + +.. code:: c + + static void *master_handle = NULL; + ESP_ERROR_CHECK(mbc_master_create_tcp(&config, &master_handle)); + ... + +.. code:: c + + static void *slave_handle = NULL; + ESP_ERROR_CHECK(mbc_slave_create_tcp(&config, &slave_handle)); + ... + +.. code:: c + + static void *slave_handle = NULL; + ESP_ERROR_CHECK(mbc_slave_create_serial(&config, &slave_handle)); + ... + +Refer to :ref:`modbus_api_master_setup_communication_options` and :ref:`modbus_api_slave_setup_communication_options` for more information on how to configure communication options for the master and slave object accordingly. + +.. _modbus_api_master_setup_communication_options: + +Master Communication Options +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The configuration structure is used to recognize the type of object being initialized. An example of initialization for Modbus serial master in RTU is below. The configuration structure provided as a parameter and is different for serial and TCP communication mode. + +.. code:: c + + #define MB_PORT_NUM 2 + #define MB_DEV_SPEED 115200 + static void *master_handle = NULL; + .... + // Initialize Modbus controller + mb_communication_info_t config = { + .ser_opts.port = MB_PORT_NUM, // master communication port number + .ser_opts.mode = MB_RTU, // mode of Modbus communication (MB_RTU, MB_ASCII) + .ser_opts.baudrate = MB_DEV_SPEED, // baud rate of the port + .ser_opts.parity = MB_PARITY_NONE, // parity option for the port + .ser_opts.uid = 0, // unused for master + .ser_opts.response_tout_ms = 1000, // slave response time for master (if = 0, taken from default config) + .ser_opts.data_bits = UART_DATA_8_BITS, // number of data bits for communication port + .ser_opts.stop_bits = UART_STOP_BITS_1 // number of stop bits for the communication port + }; + esp_err_t err = mbc_master_create_serial(&config, &master_handle); + if (master_handler == NULL || err != ESP_OK) { + ESP_LOGE(TAG, "mb controller initialization fail."); + } + +.. note:: RS485 communication requires call to UART specific APIs to setup communication mode and pins. Refer to the `UART communication section `__ in documentation. + +An example of initialization for Modbus TCP master is below. The Modbus master TCP requires additional definition of IP address table where number of addresses should be equal to number of unique slave addresses in master Modbus Data Dictionary. The Unit Identifier defined in the table below corresponds to UID (slave short address field) in the Data Dictionary. +The format of slave definition following the notation `UID;slave_host_ip_or_dns_name;port_number` and allows some variations as described in the example below. + +.. code:: c + + // This is public pointer for the module and used by master + // to resolve slave addresses and reconnect when connection is broken + static char *slave_ip_address_table[] = { + "01;mb_slave_tcp_01;502", // Define the slave using mdns host name ("mb_slave_tcp_01") with UID = 01 and communication port 502 + "200;mb_slave_tcp_c8;1502", // Definition of slave with mdns name "mb_slave_tcp_C8" and UID = 200, port = 1502 + "35;192.168.32.54;1502", // Definition of slave with the static IPV4 address and UID = 35, port = 502 + "12:2001:0db8:85a3:0000:0000:8a2e:0370:7334:502", // Definition of the slave with static IPV6 address and UID = 12, port = 502 + NULL // End of table condition (must be included) + }; + +.. code:: c + + #define MB_TCP_PORT 502 + static void *master_handle = NULL; + .... + mb_communication_info_t tcp_master_config = { + .tcp_opts.port = MB_TCP_PORT, // Default TCP Port number + .tcp_opts.mode = MB_TCP, // TCP mode of communication + .tcp_opts.addr_type = MB_IPV4, // type of IP address (MB_IPV4, MB_IPV6) + .tcp_opts.ip_addr_table = (void *)slave_ip_address_table, // list of slaves for master (must be defined) + .tcp_opts.uid = 0, // the UID unused for master + .tcp_opts.start_disconnected = false, // false - manage connections to all slaves before start + .tcp_opts.response_tout_ms = 2000, // slave response time in milliseconds for master, 0 - use default konfig + .tcp_opts.ip_netif_ptr = (void*)get_example_netif(), // the pointer to netif inteface + }; + esp_err_t err = mbc_master_create_tcp(pcomm_info, &master_handle); + if (master_handler == NULL || err != ESP_OK) { + ESP_LOGE(TAG, "mb controller initialization fail."); + } + +.. note:: Refer to `esp_netif component `__ for more information about network interface initialization. + +The slave IP addresses of the slaves can be resolved automatically by the stack using mDNS service as described in the example. In this case each slave has to use the mDNS service support and define its host name appropriately. +Refer to :ref:`example TCP master `, :ref:`example TCP slave ` for more information. + +.. note:: The Modbus Master TCP functionality is under testing and competition status will be announced later over official channels. + +.. _modbus_api_slave_setup_communication_options: + +Slave Communication Options +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The function initializes the Modbus controller interface and its active context (tasks, RTOS objects and other resources). + +This example code to initialize Modbus serial slave: + +.. code:: c + + #define MB_PORT_NUM 2 + #define MB_DEV_SPEED 115200 + #define MB_SLAVE_ADDR 1 + static void* slave_handle = NULL; + .... + mb_communication_info_t config = { + .ser_opts.port = MB_PORT_NUM, + .ser_opts.mode = MB_ASCII, // ASCII communication mode + .ser_opts.baudrate = MB_DEV_SPEED, + .ser_opts.parity = MB_PARITY_NONE, + .ser_opts.uid = MB_SLAVE_ADDR, // Modbus slave UID - Unit Identifier (short address) + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1 + }; + // Initialization and setup of Modbus serial slave in ASCII communication mode + esp_err_t err = mbc_slave_create_serial(&config, &slave_handle); + if (slave_handle == NULL || err != ESP_OK) { + ESP_LOGE(TAG, "mb controller initialization fail."); + } + +.. note:: RS485 communication requires call to UART specific APIs to setup communication mode and pins. Refer to the `UART communication section `__ in documentation. + +This example code to initialize Modbus TCP slave: + +.. code:: c + + #define MB_SLAVE_ADDR 1 + #define MB_TCP_PORT_NUMBER 1502 + static void* slave_handle = NULL; + .... + mb_communication_info_t tcp_slave_config = { + .tcp_opts.port = MB_TCP_PORT_NUMBER, // communication port number for Modbus slave + .tcp_opts.mode = MB_TCP, // mode of communication for slave + .tcp_opts.addr_type = MB_IPV4, // type of addressing being used + .tcp_opts.ip_addr_table = NULL, // Bind to any address + .tcp_opts.ip_netif_ptr = (void*)get_example_netif(),// the pointer to netif inteface + .tcp_opts.uid = MB_SLAVE_ADDR // Modbus slave Unit Identifier + }; + esp_err_t err = mbc_slave_create_tcp(&tcp_slave_config, &slave_handle); + if (slave_handle == NULL || err != ESP_OK) { + ESP_LOGE(TAG, "mb controller initialization fail."); + } + +.. note:: Refer to `esp_netif component `__ for more information about network interface initialization. + +.. note:: The Modbus Slave TCP functionality is under testing and the competition status will be announced later over official channels. diff --git a/docs/en/slave_api_overview.rst b/docs/en/slave_api_overview.rst new file mode 100644 index 0000000..7a08b48 --- /dev/null +++ b/docs/en/slave_api_overview.rst @@ -0,0 +1,213 @@ +.. _modbus_api_slave_overview: + +Modbus Slave API Overview +------------------------- + +The sections below represent typical programming workflow for the slave API which should be called in following order: + +1. :ref:`modbus_api_port_initialization` - Initialization of Modbus controller interface using communication options. +2. :ref:`modbus_api_slave_configure_descriptor` - Configure data descriptors to access slave parameters. +3. :ref:`modbus_api_slave_setup_communication_options` - Allows to setup communication options for selected port. +4. :ref:`modbus_api_slave_communication` - Start stack and sending / receiving data. Filter events when master accesses the register areas. +5. :ref:`modbus_api_slave_destroy` - Destroy Modbus controller and its resources. + +.. _modbus_api_slave_configure_descriptor: + +Configuring Slave Data Access +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following functions must be called when the Modbus controller slave port is already initialized. Refer to :ref:`modbus_api_port_initialization`. + +The slave stack requires the user to define structures (memory storage areas) that store the Modbus parameters accessed by stack. These structures should be prepared by the user and be assigned to the Modbus controller interface using :cpp:func:`mbc_slave_set_descriptor` API call before the start of communication. The slave task can call the :cpp:func:`mbc_slave_check_event` function which will block until the Modbus master access the slave. The slave task can then get information about the data being accessed. + +.. note:: One slave can define several area descriptors per each type of Modbus register area with different start_offset. + +Register area is defined by using the :cpp:type:`mb_register_area_descriptor_t` structure. + +.. list-table:: Table 3 Modbus register area descriptor + :widths: 8 92 + :header-rows: 1 + + * - Field + - Description + * - ``start_offset`` + - Zero based register relative offset for defined register area. Example: register address = 40002 ( 4x register area - Function 3 - holding register ), start_offset = 2 + * - ``type`` + - Type of the Modbus register area. Refer to :cpp:type:`mb_param_type_t` for more information. + * - ``address`` + - A pointer to the memory area which is used to store the register data for this area descriptor. + * - ``size`` + - The size of the memory area in bytes which is used to store register data. + +:cpp:func:`mbc_slave_set_descriptor` + +The function initializes Modbus communication descriptors for each type of Modbus register area (Holding Registers, Input Registers, Coils (single bit output), Discrete Inputs). Once areas are initialized and the :cpp:func:`mbc_slave_start()` API is called the Modbus stack can access the data in user data structures by request from master. + +.. code:: c + + #define MB_REG_INPUT_START_AREA0 (0) + #define MB_REG_HOLDING_START_AREA0 (0) + #define MB_REG_HOLD_CNT (100) + #define MB_REG_INPUT_CNT (100) + .... + static void *slave_handle = NULL; // Pointer to interface structure allocated by constructor + .... + mb_register_area_descriptor_t reg_area; // Modbus register area descriptor structure + unit16_t holding_reg_area[MB_REG_HOLD_CNT] = {0}; // storage area for holding registers + unit16_t input_reg_area[MB_REG_INPUT_CNT] = {0}; // storage area for input registers + + reg_area.type = MB_PARAM_HOLDING; // Set type of register area + reg_area.start_offset = MB_REG_HOLDING_START_AREA0; // Offset of register area in Modbus protocol + reg_area.address = (void*)&holding_reg_area[0]; // Set pointer to storage instance + reg_area.size = (sizeof(holding_reg_area) << 1); // Set the size of register storage area in bytes! + ESP_ERROR_CHECK(mbc_slave_set_descriptor(slave_handle, reg_area)); + + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = MB_REG_INPUT_START_AREA0; + reg_area.address = (void*)&input_reg_area[0]; + reg_area.size = (sizeof(input_reg_area) << 1); + ESP_ERROR_CHECK(mbc_slave_set_descriptor(slave_handle, reg_area)); + + +At least one area descriptor per each Modbus register type must be set in order to provide register access to its area. If the master tries to access an undefined area, the stack will generate a Modbus exception. + +The stack supports the extended data types when enabled through the the option ``CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND`` in kconfig menu. +In this case the mapped data values can be initialized to specific format using :ref:`modbus_api_endianness_conversion`. +Please refer to secton :ref:`modbus_mapping_complex_data_types` for more information about data types. + +Example initialization of mapped values: + +.. code:: c + + #include "mbcontroller.h" // for mbcontroller defines and api + val_32_arr holding_float_abcd[2] = {0}; + val_64_arr holding_double_ghefcdab[2] = {0}; + ... + // set the Modbus parameter to specific format + portENTER_CRITICAL(¶m_lock); // critical section is required if the stack is active + mb_set_float_abcd(&holding_float_abcd[0], (float)12345.0); + mb_set_float_abcd(&holding_float_abcd[1], (float)12345.0); + mb_set_double_ghefcdab(&holding_double_ghefcdab[0], (double)12345.0); + portEXIT_CRITICAL(¶m_lock); + ... + // The actual abcd formatted value can be converted to actual float represenatation as below + ESP_LOGI("TEST", "Test value abcd: %f", mb_get_float_abcd(&holding_float_abcd[0])); + ESP_LOGI("TEST", "Test value abcd: %f", mb_get_float_abcd(&holding_float_abcd[1])); + ESP_LOGI("TEST", "Test value ghefcdab: %lf", mb_get_double_ghefcdab(&holding_double_ghefcdab[0])); + ... + +.. _modbus_api_slave_communication: + +Slave Communication +^^^^^^^^^^^^^^^^^^^ + +The function below is used to start Modbus controller interface and allows communication. + +:cpp:func:`mbc_slave_start` + +.. code:: c + + static void* slave_handle = NULL; + .... + ESP_ERROR_CHECK(mbc_slave_start(slave_handle)); // The handle must be initialized prior to start call. + +:cpp:func:`mbc_slave_check_event` + +The blocking call to function waits for a event specified (represented as an event mask parameter). Once the master accesses the parameter and the event mask matches the parameter type, the application task will be unblocked and function will return the corresponding event :cpp:type:`mb_event_group_t` which describes the type of register access being done. + +:cpp:func:`mbc_slave_get_param_info` + +The function gets information about accessed parameters from the Modbus controller event queue. The KConfig ``CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE`` key can be used to configure the notification queue size. The timeout parameter allows a timeout to be specified when waiting for a notification. The :cpp:type:`mb_param_info_t` structure contains information about accessed parameter. + +.. list-table:: Table 4 Description of the register info structure: :cpp:type:`mb_param_info_t` + :widths: 10 90 + :header-rows: 1 + + * - Field + - Description + * - ``time_stamp`` + - the time stamp of the event when defined parameter is accessed + * - ``mb_offset`` + - start Modbus register accessed by master + * - ``type`` + - type of the Modbus register area being accessed (See the :cpp:type:`mb_event_group_t` for more information) + * - ``address`` + - memory address that corresponds to accessed register in defined area descriptor + * - ``size`` + - number of registers being accessed by master + +Example to get event when holding or input registers accessed in the slave: + +.. code:: c + + #define MB_READ_MASK (MB_EVENT_INPUT_REG_RD | MB_EVENT_HOLDING_REG_RD) + #define MB_WRITE_MASK (MB_EVENT_HOLDING_REG_WR) + #define MB_READ_WRITE_MASK (MB_READ_MASK | MB_WRITE_MASK) + #define MB_PAR_INFO_GET_TOUT (10 / portTICK_RATE_MS) + .... + static void *slave_handle = NULL; // communication object handle + .... + // Get the mask of the queued events, the function + // blocks while waiting for register access + (void)mbc_slave_check_event(mbc_slave_handle, MB_READ_WRITE_MASK); + // Obtain the parameter information from parameter queue regarding access from master + ESP_ERROR_CHECK(mbc_slave_get_param_info(mbc_slave_handle, ®_info, MB_PAR_INFO_GET_TOUT)); + const char* rw_str = (reg_info.type & MB_READ_MASK) ? "READ" : "WRITE"; + + // Filter events and process them accordingly + if (reg_info.type & (MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD)) { + ESP_LOGI(TAG, "HOLDING %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", + rw_str, + (uint32_t)reg_info.time_stamp, + (uint32_t)reg_info.mb_offset, + (uint32_t)reg_info.type, + (uint32_t)reg_info.address, + (uint32_t)reg_info.size); + } else if (reg_info.type & (MB_EVENT_INPUT_REG_RD)) { + ESP_LOGI(TAG, "INPUT %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", + rw_str, + (uint32_t)reg_info.time_stamp, + (uint32_t)reg_info.mb_offset, + (uint32_t)reg_info.type, + (uint32_t)reg_info.address, + (uint32_t)reg_info.size); + } + +:cpp:func:`mbc_slave_lock` + +:cpp:func:`mbc_slave_unlock` + +The direct access to slave register area from user application must be protected by critical section. The following functions can be used to protect access to the data from registered mapping area while the communication object is active. + +.. code:: c + + static void *slave_handle = NULL; // communication object handle + ... + (void)mbc_slave_lock(slave_handle); // ignore the returned error if the object is not actual + holding_reg_area[1] += 10; // the data is part of initialized register area accessed by slave + (void)mbc_slave_unlock(slave_handle); + +The access to registered area shared between several slave objects from user application must be protected by critical section base on spin lock: + +.. code:: c + + #include "freertos/FreeRTOS.h" + ... + static portMUX_TYPE g_spinlock = portMUX_INITIALIZER_UNLOCKED; + ... + portENTER_CRITICAL(¶m_lock); + holding_reg_area[2] = 123; + portEXIT_CRITICAL(¶m_lock); + +.. _modbus_api_slave_destroy: + +Modbus Slave Teardown +^^^^^^^^^^^^^^^^^^^^^ + +This function stops the Modbus communication stack, destroys the controller interface, and frees all used active objects allocated for the slave. + +:cpp:func:`mbc_slave_delete` + +.. code:: c + + ESP_ERROR_CHECK(mbc_slave_delete(slave_handle)); // delete the master communication object defined by its handle \ No newline at end of file diff --git a/docs/generate_docs b/docs/generate_docs new file mode 100755 index 0000000..ae86890 --- /dev/null +++ b/docs/generate_docs @@ -0,0 +1,39 @@ +#!/bin/bash + +exit_if_error() { + local exit_code=$1 + shift + [[ $exit_code ]] && # do nothing if no error code passed + ((exit_code != 0)) && { # do nothing if error code is 0 + printf 'ERROR: %s\n' "$@" >&2 + exit "$exit_code" + } +} + +rm -rf _build +build-docs --target esp32 --language en || exit_if_error $? "Documentation build fail." + +# Modifes target field of html files +ELEMENT="" + +FILES=$(find . -path "*/_build/en/esp32/html/*.html") + +for FILE in ${FILES} + do + echo ${ELEMENT} >> "${FILE}" + done + +exit_if_error $? "Documentation build fail." + +echo "Documentation build ok." diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..a92951c --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +esp-docs>=1.9.1,<2.0 +Pillow==9.5.0 diff --git a/docs/utils.sh b/docs/utils.sh new file mode 100644 index 0000000..84f3748 --- /dev/null +++ b/docs/utils.sh @@ -0,0 +1,18 @@ +# Bash helper functions for adding SSH keys + +function add_ssh_keys() { + local key_string="${1}" + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -n "${key_string}" >~/.ssh/id_rsa_base64 + base64 --decode --ignore-garbage ~/.ssh/id_rsa_base64 >~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa +} + +function add_doc_server_ssh_keys() { + local key_string="${1}" + local server_url="${2}" + local server_user="${3}" + add_ssh_keys "${key_string}" + echo -e "Host ${server_url}\n\tStrictHostKeyChecking no\n\tUser ${server_user}\n" >>~/.ssh/config +} diff --git a/examples/.build-test-rules.yml b/examples/.build-test-rules.yml new file mode 100644 index 0000000..3e6a43f --- /dev/null +++ b/examples/.build-test-rules.yml @@ -0,0 +1,27 @@ +tcp/mb_tcp_master: + disable_test: + - if: IDF_TARGET != "esp32" or CONFIG_NAME == "dummy_config" + reason: only manual test is performed + disable: + - if: CONFIG_NAME == "wifi" and SOC_WIFI_SUPPORTED != 1 + +tcp/mb_tcp_slave: + disable_test: + - if: IDF_TARGET != "esp32" or CONFIG_NAME == "dummy_config" + reason: only manual test is performed + disable: + - if: CONFIG_NAME == "wifi" and SOC_WIFI_SUPPORTED != 1 + +serial/mb_serial_master: + disable_test: + - if: IDF_TARGET != "esp32" or CONFIG_NAME == "dummy_config" + reason: only manual test is performed + disable: + - if: CONFIG_NAME == "default" and SOC_WIFI_SUPPORTED != 1 + +serial/mb_serial_slave: + disable_test: + - if: IDF_TARGET != "esp32" or CONFIG_NAME == "dummy_config" + reason: only manual test is performed + + diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..e8931d4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Test implementation + +This directory contains a set of ESP-IDF projects to be used as tests only, which aim to exercise various +configuration of components to check completely arbitrary functionality should it be building only, executing under +various conditions or combination with other components, including custom test frameworks. + +The tests in this folder are not intended to demonstrate the ESP-IDF functionality in any way. + +The examples can be found here: https://github.com/espressif/esp-idf/tree/master/examples/protocols/modbus diff --git a/examples/conftest.py b/examples/conftest.py new file mode 100644 index 0000000..1ae3dba --- /dev/null +++ b/examples/conftest.py @@ -0,0 +1,325 @@ +# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=W0621 # redefined-outer-name + +import logging +import os +import sys +from datetime import datetime +from enum import Enum +from typing import Any, Callable, Dict, Match, Optional, TextIO, Tuple + +import pexpect +import pytest +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch +from pytest_embedded.plugin import multi_dut_argument, multi_dut_fixture +from pytest_embedded_idf.app import IdfApp +from pytest_embedded_idf.dut import IdfDut +from pytest_embedded_idf.serial import IdfSerial + + +class Stages(Enum): + STACK_DEFAULT = 1 + STACK_IPV4 = 2 + STACK_IPV6 = 3 + STACK_INIT = 4 + STACK_CONNECT = 5 + STACK_START = 6 + STACK_PAR_OK = 7 + STACK_PAR_FAIL = 8 + STACK_DESTROY = 9 + +DEFAULT_SDKCONFIG = 'default' +ALLOWED_PERCENT_OF_FAILS = 10 + +class ModbusTestDut(IdfDut): + + TEST_IP_PROMPT = r'Waiting IP([0-9]{1,2}) from stdin:\r\r\n' + TEST_IP_SET_CONFIRM = r'.*IP\([0-9]+\) = \[([0-9a-zA-Z\.\:]+)\] set from stdin.*' + TEST_IP_ADDRESS_REGEXP = r'.*example_[a-z]+: .* IPv4 [a-z]+:.* ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*' + TEST_APP_NAME = r'I \([0-9]+\) [a-z_]+: Project name:\s+([_a-z]*)' + + TEST_EXPECT_STR_TIMEOUT = 120 + TEST_ACK_TIMEOUT = 60 + TEST_MAX_CIDS = 8 + + app: IdfApp + serial: IdfSerial + + def __init__(self, *args, **kwargs) -> None: # type: ignore + super().__init__(*args, **kwargs) + self.logger = logging.getLogger() + self.test_output: Optional[TextIO] = None + self.ip_address: Optional[str] = None + self.app_name: Optional[str] = None + self.param_fail_count = 0 + self.param_ok_count = 0 + self.test_stage = Stages.STACK_DEFAULT + self.dictionary = None + self.test_finish = False + self.test_status = False + + def close(self) -> None: + super().close() + + def dut_get_ip(self) -> Optional[str]: + if self.ip_address is None: + expect_address = self.expect(self.TEST_IP_ADDRESS_REGEXP, timeout=self.TEST_EXPECT_STR_TIMEOUT) + if isinstance(expect_address, Match): + self.ip_address = expect_address.group(1).decode('ascii') + return self.ip_address + + def dut_get_name(self) -> Optional[str]: + if self.app_name is None: + expect_name = self.expect(self.TEST_APP_NAME, timeout=self.TEST_EXPECT_STR_TIMEOUT) + if isinstance(expect_name, Match): + self.app_name = expect_name.group(1).decode('ascii') + return self.app_name + + def dut_send_ip(self, slave_ip: Optional[str]) -> Optional[int]: + ''' The function sends the slave IP address defined as a parameter to master + ''' + addr_num = 0 + self.expect(self.TEST_IP_PROMPT, timeout=self.TEST_ACK_TIMEOUT) + if isinstance(slave_ip, str): + for addr_num in range(0, self.TEST_MAX_CIDS): + message = r'IP{}={}'.format(addr_num, slave_ip) + self.logger.info('{} sent to master'.format(message)) + self.write(message) + return addr_num + + def get_expect_proc(self) -> Optional[object]: + expect_proc: object = None + try: + expect_proc = self.__getattribute__('pexpect_proc') + except: + expect_proc = self.__getattribute__('_p') + finally: + if (expect_proc and callable(getattr(expect_proc, 'expect'))): + return expect_proc + else : + return None + + def expect_any(self, *expect_items: Tuple[str, Callable], timeout: Optional[int]) -> None: + """ + expect_any(*expect_items, timeout=DEFAULT_TIMEOUT) + expect any of the patterns. + will call callback (if provided) if pattern match succeed and then return. + will pass match result to the callback. + + :raise ExpectTimeout: failed to match any one of the expect items before timeout + :raise UnsupportedExpectItem: pattern in expect_item is not string or compiled RegEx + + :arg expect_items: one or more expect items. + string, compiled RegEx pattern or (string or RegEx(string pattern), callback) + :keyword timeout: timeout for expect + :return: matched item + """ + def process_expected_item(item_raw: Tuple[str, Callable[..., Any]]) -> Dict[str, Any]: + # convert item raw data to standard dict + item = { + 'pattern': item_raw[0] if isinstance(item_raw, tuple) else item_raw, + 'callback': item_raw[1] if isinstance(item_raw, tuple) else None, + 'index': -1, + 'ret': None, + } + return item + + expect_items_list = [process_expected_item(item) for item in expect_items] + expect_patterns = [item['pattern'] for item in expect_items_list if item['pattern'] is not None] + match_item = None + + # Workaround: We need to use the original expect method of pexpect process which returns + # index of matched pattern instead of Match object returned by dut.expect() + expect_proc: Optional[object] = self.get_expect_proc() + + if expect_proc is not None: + match_index = expect_proc.expect(expect_patterns, timeout) + + if isinstance(match_index, int): + match_item = expect_items_list[match_index] # type: ignore + match_item['index'] = match_index # type: ignore , keep match index + if isinstance(expect_proc.match, Match) and len(expect_proc.match.groups()) > 0: + match_item['ret'] = expect_proc.match.groups() + if match_item['callback']: + match_item['callback'](match_item['ret']) # execution of callback function + else: + self.logger.error('%s: failed to parse output. Please check component versions.', self.app_name) + raise RuntimeError from None + + def dut_test_start(self, dictionary: Dict, timeout_value=TEST_EXPECT_STR_TIMEOUT) -> None: # type: ignore + """ The method to initialize and handle test stages + """ + def handle_get_ip4(data: Optional[Any]) -> None: + """ Handle get_ip v4 + """ + self.logger.info('%s[STACK_IPV4]: %s', self.app_name, str(data)) + self.test_stage = Stages.STACK_IPV4 + + def handle_get_ip6(data: Optional[Any]) -> None: + """ Handle get_ip v6 + """ + self.logger.info('%s[STACK_IPV6]: %s', self.app_name, str(data)) + self.test_stage = Stages.STACK_IPV6 + + def handle_init(data: Optional[Any]) -> None: + """ Handle init + """ + self.logger.info('%s[STACK_INIT]: %s', self.app_name, str(data)) + self.test_stage = Stages.STACK_INIT + + def handle_connect(data: Optional[Any]) -> None: + """ Handle connect + """ + self.logger.info('%s[STACK_CONNECT]: %s', self.app_name, str(data)) + self.test_stage = Stages.STACK_CONNECT + + def handle_test_start(data: Optional[Any]) -> None: + """ Handle connect + """ + self.logger.info('%s[STACK_START]: %s', self.app_name, str(data)) + self.test_stage = Stages.STACK_START + + def handle_par_ok(data: Optional[Any]) -> None: + """ Handle parameter ok + """ + self.logger.info('%s[READ_PAR_OK]: %s', self.app_name, str(data)) + if self.test_stage.value >= Stages.STACK_START.value: + self.param_ok_count += 1 + self.test_stage = Stages.STACK_PAR_OK + + def handle_par_fail(data: Optional[Any]) -> None: + """ Handle parameter fail + """ + self.logger.info('%s[READ_PAR_FAIL]: %s', self.app_name, str(data)) + self.param_fail_count += 1 + self.test_stage = Stages.STACK_PAR_FAIL + + def handle_destroy(data: Optional[Any]) -> None: + """ Handle destroy + """ + self.logger.info('%s[%s]: %s', self.app_name, Stages.STACK_DESTROY.name, str(data)) + self.test_stage = Stages.STACK_DESTROY + self.test_finish = True + + while not self.test_finish: + try: + self.expect_any((dictionary[Stages.STACK_IPV4], handle_get_ip4), + (dictionary[Stages.STACK_IPV6], handle_get_ip6), + (dictionary[Stages.STACK_INIT], handle_init), + (dictionary[Stages.STACK_CONNECT], handle_connect), + (dictionary[Stages.STACK_START], handle_test_start), + (dictionary[Stages.STACK_PAR_OK], handle_par_ok), + (dictionary[Stages.STACK_PAR_FAIL], handle_par_fail), + (dictionary[Stages.STACK_DESTROY], handle_destroy), + timeout=timeout_value) + except pexpect.TIMEOUT: + self.logger.info('%s, expect timeout on stage %s (%s seconds)', self.app_name, self.test_stage.name, timeout_value) + self.test_finish = True + + def dut_check_errors(self) -> None: + ''' Verify allowed percentage of errors for the dut + ''' + allowed_ok_percentage = ((self.param_ok_count / (self.param_ok_count + self.param_fail_count + 1)) * 100) + if self.param_ok_count and (allowed_ok_percentage > (100 - ALLOWED_PERCENT_OF_FAILS)): + self.logger.info('%s: ok_count: %d, fail count: %d', self.app_name, self.param_ok_count, self.param_fail_count) + else : + self.logger.error('%s: ok_count: %d, number of failed readings %d exceeds %d percent', self.app_name, self.param_ok_count, self.param_fail_count, ALLOWED_PERCENT_OF_FAILS) + raise RuntimeError from None + +############ +# Fixtures # +############ + +@pytest.fixture(scope='session', autouse=True) +def session_tempdir() -> str: + + _tmpdir = os.path.join( + os.path.dirname(__file__), + 'pytest_embedded_log', + datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), + ) + os.makedirs(_tmpdir, exist_ok=True) + return _tmpdir + + +@pytest.fixture(autouse=True) +@multi_dut_fixture +def junit_properties( + test_case_name: str, record_xml_attribute: Callable[[str, object], None] +) -> None: + """ + This fixture is autoused and will modify the junit report test case name to .. + """ + record_xml_attribute('name', test_case_name) + + +@pytest.fixture(scope='module') +def monkeypatch_module(request: FixtureRequest) -> MonkeyPatch: + mp = MonkeyPatch() + request.addfinalizer(mp.undo) + return mp + + +@pytest.fixture(scope='module', autouse=True) +def replace_dut_class(monkeypatch_module: MonkeyPatch) -> None: + monkeypatch_module.setattr('pytest_embedded_idf.IdfDut', ModbusTestDut) + + +@pytest.fixture +@multi_dut_argument +def config(request: FixtureRequest) -> str: + return getattr(request, 'param', None) or DEFAULT_SDKCONFIG + + +@pytest.fixture +@multi_dut_fixture +def build_dir(app_path: str, target: Optional[str], config: Optional[str]) -> str: + """ + Check local build dir with the following priority: + + 1. build__ + 2. build_ + 3. build_ + 4. build + + Args: + app_path: app path + target: target + config: config + + Returns: + valid build directory + """ + check_dirs = [] + + if target is not None and config is not None: + check_dirs.append(f'build_{target}_{config}') + if target is not None: + check_dirs.append(f'build_{target}') + if config is not None: + check_dirs.append(f'build_{config}') + check_dirs.append('build') + + for check_dir in check_dirs: + binary_path = os.path.join(app_path, check_dir) + if os.path.isdir(binary_path): + logging.info(f'find valid binary path: {binary_path}') + return check_dir + + logging.warning( + 'checking binary path: %s... missing... try another place', binary_path + ) + + if config is not None and 'dummy' in config: + logging.warning('no build dir valid for application: %s, config: %s. Skip test.', binary_path, config) + return None + + recommend_place = check_dirs[0] + logging.error( + f'no build dir valid. Please build the binary via "idf.py -B {recommend_place} build" and run pytest again' + ) + + sys.exit(1) diff --git a/examples/mb_example_common/CMakeLists.txt b/examples/mb_example_common/CMakeLists.txt new file mode 100644 index 0000000..c4854e9 --- /dev/null +++ b/examples/mb_example_common/CMakeLists.txt @@ -0,0 +1,5 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +idf_component_register(SRCS "modbus_params.c" + INCLUDE_DIRS "include") diff --git a/examples/mb_example_common/README.md b/examples/mb_example_common/README.md new file mode 100644 index 0000000..4a08b7e --- /dev/null +++ b/examples/mb_example_common/README.md @@ -0,0 +1,10 @@ +# Modbus Example Common + +This directory contains component that is common for Modbus master and slave examples. The component defines Modbus parameters that are shared between examples and provide code that you can copy and adapt into your own projects. +For more information please refer to Modbus example README.md files located in the folders: + +* `examples/protocols/modbus/serial/mb_master` Modbus serial master implementation (RTU and ASCII) +* `examples/protocols/modbus/serial/mb_slave` Modbus serial slave implementation (RTU and ASCII) +* `examples/protocols/modbus/serial/mb_master` Modbus serial master implementation (RTU and ASCII) +* `examples/protocols/modbus/tcp/mb_tcp_slave` Modbus serial slave implementation (TCP) +* `examples/protocols/modbus/tcp/mb_tcp_master` Modbus serial master implementation (TCP) \ No newline at end of file diff --git a/examples/mb_example_common/component.mk b/examples/mb_example_common/component.mk new file mode 100644 index 0000000..f0dc18c --- /dev/null +++ b/examples/mb_example_common/component.mk @@ -0,0 +1,5 @@ +# +# Component Makefile +# +COMPONENT_ADD_INCLUDEDIRS := include +COMPONENT_SRCDIRS := . diff --git a/examples/mb_example_common/include/modbus_params.h b/examples/mb_example_common/include/modbus_params.h new file mode 100644 index 0000000..fd19f92 --- /dev/null +++ b/examples/mb_example_common/include/modbus_params.h @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/*===================================================================================== + * Description: + * The Modbus parameter structures used to define Modbus instances that + * can be addressed by Modbus protocol. Define these structures per your needs in + * your application. Below is just an example of possible parameters. + *====================================================================================*/ +#ifndef _DEVICE_PARAMS +#define _DEVICE_PARAMS + +#include +#include "sdkconfig.h" + +// This file defines structure of modbus parameters which reflect correspond modbus address space +// for each modbus register type (coils, discreet inputs, holding registers, input registers) +#pragma pack(push, 1) +typedef struct +{ + uint8_t discrete_input0:1; + uint8_t discrete_input1:1; + uint8_t discrete_input2:1; + uint8_t discrete_input3:1; + uint8_t discrete_input4:1; + uint8_t discrete_input5:1; + uint8_t discrete_input6:1; + uint8_t discrete_input7:1; + uint8_t discrete_input_port1; + uint8_t discrete_input_port2; +} discrete_reg_params_t; +#pragma pack(pop) + +#pragma pack(push, 1) +typedef struct +{ + uint8_t coils_port0; + uint8_t coils_port1; + uint8_t coils_port2; +} coil_reg_params_t; +#pragma pack(pop) + +#pragma pack(push, 1) +typedef struct +{ + float input_data0; // 0 + float input_data1; // 2 + float input_data2; // 4 + float input_data3; // 6 + uint16_t data[150]; // 8 + 150 = 158 + float input_data4; // 158 + float input_data5; + float input_data6; + float input_data7; + uint16_t data_block1[150]; +} input_reg_params_t; +#pragma pack(pop) + +#pragma pack(push, 1) +typedef struct +{ +#if CONFIG_FMB_EXT_TYPE_SUPPORT + uint16_t holding_u8_a[2]; + uint16_t holding_u8_b[2]; + uint16_t holding_u16_ab[2]; + uint16_t holding_u16_ba[2]; + uint32_t holding_uint32_abcd[2]; + uint32_t holding_uint32_cdab[2]; + uint32_t holding_uint32_badc[2]; + uint32_t holding_uint32_dcba[2]; + float holding_float_abcd[2]; + float holding_float_cdab[2]; + float holding_float_badc[2]; + float holding_float_dcba[2]; + double holding_double_abcdefgh[2]; + double holding_double_hgfedcba[2]; + double holding_double_ghefcdab[2]; + double holding_double_badcfehg[2]; + uint32_t holding_area2_end; +#endif + float holding_data0; + float holding_data1; + float holding_data2; + float holding_data3; + uint16_t test_regs[150]; + float holding_data4; + float holding_data5; + float holding_data6; + float holding_data7; + uint32_t holding_area1_end; +} holding_reg_params_t; +#pragma pack(pop) + +extern holding_reg_params_t holding_reg_params; +extern input_reg_params_t input_reg_params; +extern coil_reg_params_t coil_reg_params; +extern discrete_reg_params_t discrete_reg_params; + +#endif // !defined(_DEVICE_PARAMS) diff --git a/examples/mb_example_common/modbus_params.c b/examples/mb_example_common/modbus_params.c new file mode 100644 index 0000000..8a5bbd4 --- /dev/null +++ b/examples/mb_example_common/modbus_params.c @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/*===================================================================================== + * Description: + * C file to define parameter storage instances + *====================================================================================*/ +#include +#include "modbus_params.h" + +// Here are the user defined instances for device parameters packed by 1 byte +// These are keep the values that can be accessed from Modbus master +holding_reg_params_t holding_reg_params = { 0 }; + +input_reg_params_t input_reg_params = { 0 }; + +coil_reg_params_t coil_reg_params = { 0 }; + +discrete_reg_params_t discrete_reg_params = { 0 }; diff --git a/examples/serial/README.md b/examples/serial/README.md new file mode 100644 index 0000000..2e5d0b7 --- /dev/null +++ b/examples/serial/README.md @@ -0,0 +1,82 @@ +# Modbus Master-Slave Example + +## Overview + +These two projects illustrate the communication between Modbus master and slave device in the segment. +Master initializes Modbus interface driver and then reads parameters from slave device in the segment. +After several successful read attempts slave sets the alarm relay (end of test condition). +Once master reads the alarm it stops communication and destroy driver. + +The examples: + +* `examples/protocols/modbus/serial/mb_master` - Modbus serial master ASCII/RTU +* `examples/protocols/modbus/serial/mb_slave` - Modbus serial slave ASCII/RTU + +See README.md for each individual project for more information. + +## How to use example + +### Hardware Required + +This example can be run on any commonly available ESP32 development board. +The master and slave boards should be connected to each other through the RS485 interface line driver. +See the connection schematic in README.md files of each example. + +### Configure the project + +This example test requires communication mode setting for master and slave be the same and slave address set to 1. +Please refer to README.md files of each example project for more information. + +## About common_component in this example + +The folder "mb_example_common" includes definitions of parameter structures for master and slave device (both projects share the same parameters). +However, currently it is for example purpose only and can be modified for particular application. + +## Example Output + +Example of Slave output: + +``` +I (343) SLAVE_TEST: Modbus slave stack initialized. +I (343) SLAVE_TEST: Start modbus test... +I (81463) SLAVE_TEST: HOLDING READ (81150420 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffb2868, SIZE:6 +I (82463) SLAVE_TEST: HOLDING READ (82150720 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffb2868, SIZE:6 +I (83573) SLAVE_TEST: HOLDING READ (83260630 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffb2868, SIZE:6 +I (84603) SLAVE_TEST: HOLDING READ (84290530 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffb2868, SIZE:6 +I (85703) SLAVE_TEST: HOLDING READ (85396692 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffb2868, SIZE:6 +``` + +Example of Modbus Master output: + +``` +I (399) MASTER_TEST: Modbus master stack initialized... +I (499) MASTER_TEST: Start modbus test... +I (549) MASTER_TEST: Characteristic #0 Data_channel_0 (Volts) value = 1.230000 (0x3f9d70a4) read successful. +I (629) MASTER_TEST: Characteristic #1 Humidity_1 (%rH) value = 12.100000 (0x4141999a) read successful. +I (709) MASTER_TEST: Characteristic #2 Temperature_1 (C) value = 3.560000 (0x4063d70a) read successful. +I (769) MASTER_TEST: Characteristic #3 Humidity_2 (%rH) value = 23.400000 (0x41bb3333) read successful. +I (829) MASTER_TEST: Characteristic #4 Temperature_2 (C) value = 5.890000 (0x40bc7ae1) read successful. +I (889) MASTER_TEST: Characteristic #5 Humidity_3 (%rH) value = 34.500000 (0x420a0000) read successful. +E (949) MB_CONTROLLER_MASTER: mbc_master_get_parameter(111): SERIAL master get parameter failure error=(0x108) (ESP_ERR_INVALID_RESPONSE). +E (949) MASTER_TEST: Characteristic #6 (RelayP1) read fail, err = 264 (ESP_ERR_INVALID_RESPONSE). +E (1029) MB_CONTROLLER_MASTER: mbc_master_get_parameter(111): SERIAL master get parameter failure error=(0x108) (ESP_ERR_INVALID_RESPONSE). +E (1029) MASTER_TEST: Characteristic #7 (RelayP2) read fail, err = 264 (ESP_ERR_INVALID_RESPONSE). +``` + +## Troubleshooting + +If the examples do not work as expected and slave and master boards are not able to communicate correctly it is possible to find the reason for errors. +The most important errors are described in master example output and formatted as below: + +``` +E (1692332) MB_CONTROLLER_MASTER: mbc_master_get_parameter(111): SERIAL master get parameter failure error=(0x107) (ESP_ERR_TIMEOUT). +``` + +ESP_ERR_TIMEOUT (0x107) - Modbus slave device does not respond during configured timeout. Check the connection and ability for communication using uart_echo_rs485 example or increase +Kconfig value CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND (CONFIG_FMB_SERIAL_ASCII_TIMEOUT_RESPOND_MS). + +ESP_ERR_NOT_SUPPORTED (0x106), ESP_ERR_INVALID_RESPONSE (0x108) - Modbus slave device does not support requested command or register and sent exeption response. + +ESP_ERR_INVALID_STATE (0x103) - Modbus stack is not configured correctly or can't work correctly due to critical failure. + + diff --git a/examples/serial/mb_serial_master/CMakeLists.txt b/examples/serial/mb_serial_master/CMakeLists.txt new file mode 100644 index 0000000..6245125 --- /dev/null +++ b/examples/serial/mb_serial_master/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +# Exclude old component feemodbus which exists in old versions +set(EXCLUDE_COMPONENTS freemodbus) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(modbus_serial_master) diff --git a/examples/serial/mb_serial_master/README.md b/examples/serial/mb_serial_master/README.md new file mode 100644 index 0000000..7b28f66 --- /dev/null +++ b/examples/serial/mb_serial_master/README.md @@ -0,0 +1,156 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | + +# Modbus Master Example + +This example demonstrates using of FreeModbus stack port implementation for ESP32 as a master device. +This implementation is able to read/write values of slave devices connected into Modbus segment. All parameters to be accessed are defined in data dictionary of the modbus master example source file. +The values represented as characteristics with its name and characteristic CID which are linked into registers of slave devices connected into Modbus segment. +The example implements simple control algorithm and checks parameters from slave device and gets alarm (relay in the slave device) when value of holding_data0 parameter exceeded limit. +The instances for the modbus parameters are common for master and slave examples and located in `examples/protocols/modbus/mb_example_common` folder. + +Example parameters definition: +-------------------------------------------------------------------------------------------------- +| Slave Address | Characteristic ID | Characteristic name | Description | +|---------------------|----------------------|----------------------|----------------------------| +| MB_DEVICE_ADDR1 | CID_INP_DATA_0, | Data_channel_0 | Data channel 1 | +| MB_DEVICE_ADDR1 | CID_HOLD_DATA_0, | Humidity_1 | Humidity 1 | +| MB_DEVICE_ADDR1 | CID_INP_DATA_1 | Temperature_1 | Sensor temperature | +| MB_DEVICE_ADDR1 | CID_HOLD_DATA_1, | Humidity_2 | Humidity 2 | +| MB_DEVICE_ADDR1 | CID_INP_DATA_2 | Temperature_2 | Ambient temperature | +| MB_DEVICE_ADDR1 | CID_HOLD_DATA_2 | Humidity_3 | Humidity 3 | +| MB_DEVICE_ADDR1 | CID_RELAY_P1 | RelayP1 | Alarm Relay outputs on/off | +| MB_DEVICE_ADDR1 | CID_RELAY_P2 | RelayP2 | Alarm Relay outputs on/off | +-------------------------------------------------------------------------------------------------- +Note: The Slave Address is the same for all parameters for example test but it can be changed in the ```Example Data (Object) Dictionary``` table of master example to address parameters from other slaves. +The Kconfig ```Modbus slave address``` - CONFIG_MB_SLAVE_ADDR parameter in slave example can be configured to create Modbus multi slave segment. + +Simplified Modbus connection schematic for example test: + ``` + MB_DEVICE_ADDR1 + ------------- ------------- + | | RS485 network | | + | Slave 1 |---<>--+---<>---| Master | + | | | | + ------------- ------------- +``` +Modbus multi slave segment connection schematic: +``` + MB_DEVICE_ADDR1 + ------------- + | | + | Slave 1 |---<>--+ + | | | + ------------- | + MB_DEVICE_ADDR2 | + ------------- | ------------- + | | | | | + | Slave 2 |---<>--+---<>---| Master | + | | | | | + ------------- | ------------- + MB_DEVICE_ADDR3 | + ------------- RS485 network + | | | + | Slave 3 |---<>--+ + | | + ------------- +``` + +## Hardware required : +Option 1: +PC (Modbus Slave app) + USB Serial adapter connected to USB port + RS485 line drivers + ESP32 based board + +Option 2: +Several ESP32 boards flashed with modbus_slave example software to represent slave device with specific slave address (See CONFIG_MB_SLAVE_ADDR). The slave addresses for each board have to be configured as defined in "connection schematic" above. +One ESP32 board flashed with modbus_master example. All the boards require connection of RS485 line drivers (see below). + +The MAX485 line driver is used as an example below but other similar chips can be used as well. +RS485 example circuit schematic for connection of master and slave devices into segment: +``` + VCC ---------------+ +--------------- VCC + | | + +-------x-------+ +-------x-------+ + RXD <------| RO | DIFFERENTIAL | RO|-----> RXD + | B|---------------|B | + TXD ------>| DI MAX485 | \ / | MAX485 DI|<----- TXD +ESP32 BOARD | | RS-485 side | | External PC (emulator) with USB to serial or + RTS --+--->| DE | / \ | DE|---+ ESP32 BOARD (slave) + | | A|---------------|A | | + +----| /RE | PAIR | /RE|---+-- RTS + +-------x-------+ +-------x-------+ + | | + --- --- + Modbus Master device Modbus Slave device + +``` + +## How to setup and use an example: + +### Configure the application +Start the command below to setup configuration: +``` +idf.py menuconfig +``` +Configure the UART pins used for modbus communication using and table below. +Define the communication mode parameter for master and slave in Kconfig - CONFIG_MB_COMM_MODE (must be the same for master and slave devices in one segment). +Configure the slave address for each slave in the Modbus segment (the CONFIG_MB_SLAVE_ADDR in Kconfig). +``` + ------------------------------------------------------------------------------------------------------------------------------ + | UART Interface | #define | Default pins for | Default pins for | External RS485 Driver Pin | + | | | ESP32 (C6) | ESP32-S2 (S3, C3, C2, H2) | | + | ----------------------|--------------------|-----------------------|---------------------------|---------------------------| + | Transmit Data (TxD) | CONFIG_MB_UART_TXD | GPIO23 | GPIO9 | DI | + | Receive Data (RxD) | CONFIG_MB_UART_RXD | GPIO22 | GPIO8 | RO | + | Request To Send (RTS) | CONFIG_MB_UART_RTS | GPIO18 | GPIO10 | ~RE/DE | + | Ground | n/a | GND | GND | GND | + ------------------------------------------------------------------------------------------------------------------------------ +``` +Note: Each target chip has different GPIO pins available for UART connection. Please refer to UART documentation for selected target for more information. + +Connect a USB-to-RS485 adapter to a computer, then connect the adapter's A/B output lines with the corresponding A/B output lines of the RS485 line driver connected to the ESP32 chip (see figure above). + +The communication parameters of Modbus stack allow to configure it appropriately but usually it is enough to use default settings. +See the help string of parameters for more information. + +### Setup external Modbus slave devices or emulator +Option 1: +Configure the external Modbus master software according to port configuration parameters used in the example. The Modbus Slave application can be used with this example to emulate slave devices with its parameters. Use official documentation for software to setup emulation of slave devices. + +Option 2: +Other option is to have the modbus_slave example application flashed into ESP32 based board and connect boards together as showed on the Modbus connection schematic above. See the Modbus slave API documentation to configure communication parameters and slave addresses as defined in "Example parameters definition" table above. + +### Build and flash software of master device +Build the project and flash it to the board, then run monitor tool to view serial output: +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output +Example output of the application: +``` +I (9035) MASTER_TEST: Characteristic #0 Data_channel_0 (Volts) value = 1.120000 (0x3f8f5c29) read successful. +I (9045) MASTER_TEST: Characteristic #1 Humidity_1 (%rH) value = 5.539999 (0x40b147ac) read successful. +I (9045) MASTER_TEST: Characteristic #2 Temperature_1 (C) value = 2.340000 (0x4015c28f) read successful. +I (9055) MASTER_TEST: Characteristic #3 Humidity_2 (%rH) value = 2.560000 (0x4023d70a) read successful. +I (9065) MASTER_TEST: Characteristic #4 Temperature_2 (C) value = 3.560000 (0x4063d70a) read successful. +I (9075) MASTER_TEST: Characteristic #5 Humidity_3 (%rH) value = 3.780000 (0x4071eb85) read successful. +I (9085) MASTER_TEST: Characteristic #6 RelayP1 (on/off) value = OFF (0x55) read successful. +I (9095) MASTER_TEST: Characteristic #7 RelayP2 (on/off) value = OFF (0xaa) read successful. +I (9605) MASTER_TEST: Characteristic #0 Data_channel_0 (Volts) value = 1.120000 (0x3f8f5c29) read successful. +I (9615) MASTER_TEST: Characteristic #1 Humidity_1 (%rH) value = 5.739999 (0x40b7ae12) read successful. +I (9615) MASTER_TEST: Characteristic #2 Temperature_1 (C) value = 2.340000 (0x4015c28f) read successful. +I (9625) MASTER_TEST: Characteristic #3 Humidity_2 (%rH) value = 2.560000 (0x4023d70a) read successful. +I (9635) MASTER_TEST: Characteristic #4 Temperature_2 (C) value = 3.560000 (0x4063d70a) read successful. +I (9645) MASTER_TEST: Characteristic #5 Humidity_3 (%rH) value = 3.780000 (0x4071eb85) read successful. +I (9655) MASTER_TEST: Characteristic #6 RelayP1 (on/off) value = OFF (0x55) read successful. +I (9665) MASTER_TEST: Characteristic #7 RelayP2 (on/off) value = ON (0xff) read successful. +I (10175) MASTER_TEST: Alarm triggered by cid #7. +I (10175) MASTER_TEST: Destroy master... + +``` +The example reads the characteristics from slave device(s), while alarm is not triggered in the slave device (See the "Example parameters definition"). The output line describes Timestamp, Cid of characteristic, Characteristic name (Units), Characteristic value (Hex). + diff --git a/examples/serial/mb_serial_master/component.mk b/examples/serial/mb_serial_master/component.mk new file mode 100644 index 0000000..14d428f --- /dev/null +++ b/examples/serial/mb_serial_master/component.mk @@ -0,0 +1,26 @@ +INCLUDEDIRS := common/include +PRIV_INCLUDEDIRS := common port modbus modbus/ascii modbus/functions +PRIV_INCLUDEDIRS += modbus/rtu modbus/tcp modbus/include +PRIV_INCLUDEDIRS += serial_slave/port serial_slave/modbus_controller +PRIV_INCLUDEDIRS += serial_master/port serial_master/modbus_controller +PRIV_INCLUDEDIRS += tcp_slave/port tcp_slave/modbus_controller +PRIV_INCLUDEDIRS += tcp_master/port tcp_master/modbus_controller +SRCDIRS := common +SRCDIRS += modbus modbus/ascii modbus/functions modbus/rtu modbus/tcp +SRCDIRS += serial_slave/port serial_slave/modbus_controller +SRCDIRS += serial_master/port serial_master/modbus_controller +SRCDIRS += tcp_slave/port tcp_slave/modbus_controller +SRCDIRS += tcp_master/port tcp_master/modbus_controller +SRCDIRS += port + +COMPONENT_PRIV_INCLUDEDIRS = $(addprefix freemodbus/, \ + $(PRIV_INCLUDEDIRS) \ + ) + +COMPONENT_SRCDIRS = $(addprefix freemodbus/, \ + $(SRCDIRS) \ + ) + +COMPONENT_ADD_INCLUDEDIRS = $(addprefix freemodbus/, \ + $(INCLUDEDIRS) \ + ) diff --git a/examples/serial/mb_serial_master/main/CMakeLists.txt b/examples/serial/mb_serial_master/main/CMakeLists.txt new file mode 100644 index 0000000..ae72f36 --- /dev/null +++ b/examples/serial/mb_serial_master/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(PROJECT_NAME "modbus_serial_master") + +idf_component_register(SRCS "serial_master.c" + INCLUDE_DIRS ".") diff --git a/examples/serial/mb_serial_master/main/Kconfig.projbuild b/examples/serial/mb_serial_master/main/Kconfig.projbuild new file mode 100644 index 0000000..55b325b --- /dev/null +++ b/examples/serial/mb_serial_master/main/Kconfig.projbuild @@ -0,0 +1,99 @@ +menu "Modbus Example Configuration" + + config MB_UART_PORT_ONE + bool + default y + depends on (ESP_CONSOLE_UART_NUM !=1) && (SOC_UART_NUM > 1) + + config MB_UART_PORT_TWO + bool + default y + depends on (ESP_CONSOLE_UART_NUM !=2) && (SOC_UART_NUM > 2) + + config MB_UART_PORT_NUM + int "UART port number" + range 0 2 if MB_UART_PORT_TWO + default 2 if MB_UART_PORT_TWO + range 0 1 if MB_UART_PORT_ONE + default 1 if MB_UART_PORT_ONE + help + UART communication port number for Modbus example. + + config MB_UART_BAUD_RATE + int "UART communication speed" + range 1200 115200 + default 115200 + help + UART communication speed for Modbus example. + + config MB_UART_RXD + int "UART RXD pin number" + range 0 34 if IDF_TARGET_ESP32 + range 0 23 if IDF_TARGET_ESP32C6 + range 0 56 if IDF_TARGET_ESP32P4 + default 22 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 47 if IDF_TARGET_ESP32S3 + range 0 19 if IDF_TARGET_ESP32C3 + range 0 20 if IDF_TARGET_ESP32C2 + range 0 27 if IDF_TARGET_ESP32H2 + default 8 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 8 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C2 || IDF_TARGET_ESP32H2 + help + GPIO number for UART RX pin. See UART documentation for more information + about available pin numbers for UART. + + config MB_UART_TXD + int "UART TXD pin number" + range 0 34 if IDF_TARGET_ESP32 + range 0 23 if IDF_TARGET_ESP32C6 + range 0 56 if IDF_TARGET_ESP32P4 + default 23 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 47 if IDF_TARGET_ESP32S3 + range 0 19 if IDF_TARGET_ESP32C3 + range 0 20 if IDF_TARGET_ESP32C2 + range 0 27 if IDF_TARGET_ESP32H2 + default 9 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 9 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C2 || IDF_TARGET_ESP32H2 + help + GPIO number for UART TX pin. See UART documentation for more information + about available pin numbers for UART. + + config MB_UART_RTS + int "UART RTS pin number" + range 0 34 if IDF_TARGET_ESP32 + range 0 56 if IDF_TARGET_ESP32P4 + range 0 23 if IDF_TARGET_ESP32C6 + default 18 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32C6 + default 20 if IDF_TARGET_ESP32P4 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 47 if IDF_TARGET_ESP32S3 + range 0 19 if IDF_TARGET_ESP32C3 + range 0 20 if IDF_TARGET_ESP32C2 + range 0 27 if IDF_TARGET_ESP32H2 + default 10 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 10 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C2 || IDF_TARGET_ESP32H2 + help + GPIO number for UART RTS pin. This pin is connected to + ~RE/DE pin of RS485 transceiver to switch direction. + See UART documentation for more information about available pin + numbers for UART. + + choice MB_COMM_MODE + prompt "Modbus communication mode" + default MB_COMM_MODE_RTU if CONFIG_FMB_COMM_MODE_RTU_EN + help + Selection of Modbus communication mode option for Modbus. + + config MB_COMM_MODE_RTU + bool "RTU mode" + depends on FMB_COMM_MODE_RTU_EN + + config MB_COMM_MODE_ASCII + bool "ASCII mode" + depends on FMB_COMM_MODE_ASCII_EN + + endchoice + +endmenu diff --git a/examples/serial/mb_serial_master/main/idf_component.yml b/examples/serial/mb_serial_master/main/idf_component.yml new file mode 100644 index 0000000..307f3db --- /dev/null +++ b/examples/serial/mb_serial_master/main/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../../" + mb_example_common: + path: "../../../mb_example_common" diff --git a/examples/serial/mb_serial_master/main/serial_master.c b/examples/serial/mb_serial_master/main/serial_master.c new file mode 100644 index 0000000..8526876 --- /dev/null +++ b/examples/serial/mb_serial_master/main/serial_master.c @@ -0,0 +1,499 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "string.h" +#include "esp_log.h" +#include "modbus_params.h" // for modbus parameters structures +#include "mbcontroller.h" +#include "sdkconfig.h" + +#define MB_PORT_NUM (CONFIG_MB_UART_PORT_NUM) // Number of UART port used for Modbus connection +#define MB_DEV_SPEED (CONFIG_MB_UART_BAUD_RATE) // The communication speed of the UART + +// Note: Some pins on target chip cannot be assigned for UART communication. +// See UART documentation for selected board and target to configure pins using Kconfig. + +// The number of parameters that intended to be used in the particular control process +#define MASTER_MAX_CIDS num_device_parameters + +// Number of reading of parameters from slave +#define MASTER_MAX_RETRY 30 + +// Timeout to update cid over Modbus +#define UPDATE_CIDS_TIMEOUT_MS (500) +#define UPDATE_CIDS_TIMEOUT_TICS (UPDATE_CIDS_TIMEOUT_MS / portTICK_PERIOD_MS) + +// Timeout between polls +#define POLL_TIMEOUT_MS (1) +#define POLL_TIMEOUT_TICS (POLL_TIMEOUT_MS / portTICK_PERIOD_MS) + +// The macro to get offset for parameter in the appropriate structure +#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1)) +#define INPUT_OFFSET(field) ((uint16_t)(offsetof(input_reg_params_t, field) + 1)) +#define COIL_OFFSET(field) ((uint16_t)(offsetof(coil_reg_params_t, field) + 1)) +// Discrete offset macro +#define DISCR_OFFSET(field) ((uint16_t)(offsetof(discrete_reg_params_t, field) + 1)) + +#define STR(fieldname) ((const char *)( fieldname )) +#define TEST_HOLD_REG_START(field) (HOLD_OFFSET(field) >> 1) +#define TEST_HOLD_REG_SIZE(field) (sizeof(((holding_reg_params_t *)0)->field) >> 1) + +#define TEST_INPUT_REG_START(field) (INPUT_OFFSET(field) >> 1) +#define TEST_INPUT_REG_SIZE(field) (sizeof(((input_reg_params_t *)0)->field) >> 1) + +#define TEST_VALUE (12345) // default test value +#define TEST_ASCII_BIN (0xAAAAAAAA) +#define TEST_ARR_REG_SZ (58) +#define TEST_HUMI_MIN (-40) +#define TEST_HUMI_MAX (50) +#define TEST_TEMP_MIN (0) +#define TEST_TEMP_MAX (100) + +// Options can be used as bit masks or parameter limits +#define OPTS(min_val, max_val, step_val) { .opt1 = min_val, .opt2 = max_val, .opt3 = step_val } + +#define EACH_ITEM(array, length) \ +(typeof(*(array)) *pitem = (array); (pitem < &((array)[length])); pitem++) + +static const char *TAG = "MASTER_TEST"; + +// Enumeration of modbus device addresses accessed by master device +enum { + MB_DEVICE_ADDR1 = 1 // Only one slave device used for the test (add other slave addresses here) +}; + +// Enumeration of all supported CIDs for device (used in parameter definition table) +enum { + CID_INP_DATA_0 = 0, + CID_HOLD_DATA_0, + CID_INP_DATA_1, + CID_HOLD_DATA_1, + CID_INP_DATA_2, + CID_HOLD_DATA_2, + CID_HOLD_TEST_REG, + CID_RELAY_P1, + CID_RELAY_P2, + CID_DISCR_P1, +#if CONFIG_FMB_EXT_TYPE_SUPPORT + CID_HOLD_U8_A, + CID_HOLD_U8_B, + CID_HOLD_U16_AB, + CID_HOLD_U16_BA, + CID_HOLD_UINT32_ABCD, + CID_HOLD_UINT32_CDAB, + CID_HOLD_UINT32_BADC, + CID_HOLD_UINT32_DCBA, + CID_HOLD_FLOAT_ABCD, + CID_HOLD_FLOAT_CDAB, + CID_HOLD_FLOAT_BADC, + CID_HOLD_FLOAT_DCBA, + CID_HOLD_DOUBLE_ABCDEFGH, + CID_HOLD_DOUBLE_HGFEDCBA, + CID_HOLD_DOUBLE_GHEFCDAB, + CID_HOLD_DOUBLE_BADCFEHG, +#endif + CID_COUNT +}; + +// Example Data (Object) Dictionary for Modbus parameters: +// The CID field in the table must be unique. +// Modbus Slave Addr field defines slave address of the device with correspond parameter. +// Modbus Reg Type - Type of Modbus register area (Holding register, Input Register and such). +// Reg Start field defines the start Modbus register number and Reg Size defines the number of registers for the characteristic accordingly. +// The Instance Offset defines offset in the appropriate parameter structure that will be used as instance to save parameter value. +// Data Type, Data Size specify type of the characteristic and its data size. +// Parameter Options field specifies the options that can be used to process parameter value (limits or masks). +// Access Mode - can be used to implement custom options for processing of characteristic (Read/Write restrictions, factory mode values and etc). +const mb_parameter_descriptor_t device_parameters[] = { + // { CID, Param Name, Units, Modbus Slave Addr, Modbus Reg Type, Reg Start, Reg Size, Instance Offset, Data Type, Data Size, Parameter Options, Access Mode} + { CID_INP_DATA_0, STR("Data_channel_0"), STR("Volts"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, + TEST_INPUT_REG_START(input_data0), TEST_INPUT_REG_SIZE(input_data0), + INPUT_OFFSET(input_data0), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DATA_0, STR("Humidity_1"), STR("%rH"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_data0), TEST_HOLD_REG_SIZE(holding_data0), + HOLD_OFFSET(holding_data0), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_HUMI_MIN, TEST_HUMI_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_INP_DATA_1, STR("Temperature_1"), STR("C"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, + TEST_INPUT_REG_START(input_data1), TEST_INPUT_REG_SIZE(input_data1), + INPUT_OFFSET(input_data1), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DATA_1, STR("Humidity_2"), STR("%rH"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_data1), TEST_HOLD_REG_SIZE(holding_data1), + HOLD_OFFSET(holding_data1), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_HUMI_MIN, TEST_HUMI_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_INP_DATA_2, STR("Temperature_2"), STR("C"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, + TEST_INPUT_REG_START(input_data2), TEST_INPUT_REG_SIZE(input_data2), + INPUT_OFFSET(input_data2), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DATA_2, STR("Humidity_3"), STR("%rH"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_data2), TEST_HOLD_REG_SIZE(holding_data2), + HOLD_OFFSET(holding_data2), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_HUMI_MIN, TEST_HUMI_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_TEST_REG, STR("Test_regs"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(test_regs), TEST_ARR_REG_SZ, + HOLD_OFFSET(test_regs), PARAM_TYPE_ASCII, (TEST_ARR_REG_SZ * 2), + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, TEST_ASCII_BIN ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_RELAY_P1, STR("RelayP1"), STR("on/off"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 2, 6, + COIL_OFFSET(coils_port0), PARAM_TYPE_U8, 1, + OPTS( 0xAA, 0x15, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_RELAY_P2, STR("RelayP2"), STR("on/off"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 10, 6, + COIL_OFFSET(coils_port1), PARAM_TYPE_U8, 1, + OPTS( 0x55, 0x2A, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_DISCR_P1, STR("DiscreteInpP1"), STR("on/off"), MB_DEVICE_ADDR1, MB_PARAM_DISCRETE, 2, 7, + DISCR_OFFSET(discrete_input_port1), PARAM_TYPE_U8, 1, + OPTS( 0xAA, 0x15, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, +#if CONFIG_FMB_EXT_TYPE_SUPPORT + { CID_HOLD_U8_A, STR("U8_A"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u8_a), TEST_HOLD_REG_SIZE(holding_u8_a), + HOLD_OFFSET(holding_u8_a), PARAM_TYPE_U8_A, (TEST_HOLD_REG_SIZE(holding_u8_a) << 1), + OPTS( CHAR_MIN, 0x0055, 0x0055 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_U8_B, STR("U8_B"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u8_b), TEST_HOLD_REG_SIZE(holding_u8_b), + HOLD_OFFSET(holding_u8_b), PARAM_TYPE_U8_B, (TEST_HOLD_REG_SIZE(holding_u8_b) << 1), + OPTS( 0, 0x5500, 0x5500 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_U16_AB, STR("U16_AB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u16_ab), TEST_HOLD_REG_SIZE(holding_u16_ab), + HOLD_OFFSET(holding_u16_ab), PARAM_TYPE_U16_AB, (TEST_HOLD_REG_SIZE(holding_u16_ab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_U16_BA, STR("U16_BA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u16_ba), TEST_HOLD_REG_SIZE(holding_u16_ba), + HOLD_OFFSET(holding_u16_ba), PARAM_TYPE_U16_BA, (TEST_HOLD_REG_SIZE(holding_u16_ab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_ABCD, STR("UINT32_ABCD"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_abcd), TEST_HOLD_REG_SIZE(holding_uint32_abcd), + HOLD_OFFSET(holding_uint32_abcd), PARAM_TYPE_U32_ABCD, (TEST_HOLD_REG_SIZE(holding_uint32_abcd) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_CDAB, STR("UINT32_CDAB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_cdab), TEST_HOLD_REG_SIZE(holding_uint32_cdab), + HOLD_OFFSET(holding_uint32_cdab), PARAM_TYPE_U32_CDAB, (TEST_HOLD_REG_SIZE(holding_uint32_cdab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_BADC, STR("UINT32_BADC"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_badc), TEST_HOLD_REG_SIZE(holding_uint32_badc), + HOLD_OFFSET(holding_uint32_badc), PARAM_TYPE_U32_BADC, (TEST_HOLD_REG_SIZE(holding_uint32_badc) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_DCBA, STR("UINT32_DCBA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_dcba), TEST_HOLD_REG_SIZE(holding_uint32_dcba), + HOLD_OFFSET(holding_uint32_dcba), PARAM_TYPE_U32_DCBA, (TEST_HOLD_REG_SIZE(holding_uint32_dcba) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_ABCD, STR("FLOAT_ABCD"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_abcd), TEST_HOLD_REG_SIZE(holding_float_abcd), + HOLD_OFFSET(holding_float_abcd), PARAM_TYPE_FLOAT_ABCD, (TEST_HOLD_REG_SIZE(holding_float_abcd) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_CDAB, STR("FLOAT_CDAB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_cdab), TEST_HOLD_REG_SIZE(holding_float_cdab), + HOLD_OFFSET(holding_float_cdab), PARAM_TYPE_FLOAT_CDAB, (TEST_HOLD_REG_SIZE(holding_float_cdab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_BADC, STR("FLOAT_BADC"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_badc), TEST_HOLD_REG_SIZE(holding_float_badc), + HOLD_OFFSET(holding_float_badc), PARAM_TYPE_FLOAT_BADC, (TEST_HOLD_REG_SIZE(holding_float_badc) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_DCBA, STR("FLOAT_DCBA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_dcba), TEST_HOLD_REG_SIZE(holding_float_dcba), + HOLD_OFFSET(holding_float_dcba), PARAM_TYPE_FLOAT_DCBA, (TEST_HOLD_REG_SIZE(holding_float_dcba) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_ABCDEFGH, STR("DOUBLE_ABCDEFGH"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_abcdefgh), TEST_HOLD_REG_SIZE(holding_double_abcdefgh), + HOLD_OFFSET(holding_double_abcdefgh), PARAM_TYPE_DOUBLE_ABCDEFGH, (TEST_HOLD_REG_SIZE(holding_double_abcdefgh) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_HGFEDCBA, STR("DOUBLE_HGFEDCBA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_hgfedcba), TEST_HOLD_REG_SIZE(holding_double_hgfedcba), + HOLD_OFFSET(holding_double_hgfedcba), PARAM_TYPE_DOUBLE_HGFEDCBA, (TEST_HOLD_REG_SIZE(holding_double_hgfedcba) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_GHEFCDAB, STR("DOUBLE_GHEFCDAB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_ghefcdab), TEST_HOLD_REG_SIZE(holding_double_ghefcdab), + HOLD_OFFSET(holding_double_ghefcdab), PARAM_TYPE_DOUBLE_GHEFCDAB, (TEST_HOLD_REG_SIZE(holding_double_ghefcdab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_BADCFEHG, STR("DOUBLE_BADCFEHG"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_badcfehg), TEST_HOLD_REG_SIZE(holding_double_badcfehg), + HOLD_OFFSET(holding_double_badcfehg), PARAM_TYPE_DOUBLE_BADCFEHG, (TEST_HOLD_REG_SIZE(holding_double_badcfehg) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER } +#endif +}; + +// Calculate number of parameters in the table +const uint16_t num_device_parameters = (sizeof(device_parameters)/sizeof(device_parameters[0])); + +static void *master_handle = NULL; + +// The function to get pointer to parameter storage (instance) according to parameter description table +static void *master_get_param_data(const mb_parameter_descriptor_t *param_descriptor) +{ + assert(param_descriptor != NULL); + void *instance_ptr = NULL; + if (param_descriptor->param_offset != 0) { + switch(param_descriptor->mb_param_type) + { + case MB_PARAM_HOLDING: + instance_ptr = ((void *)&holding_reg_params + param_descriptor->param_offset - 1); + break; + case MB_PARAM_INPUT: + instance_ptr = ((void *)&input_reg_params + param_descriptor->param_offset - 1); + break; + case MB_PARAM_COIL: + instance_ptr = ((void *)&coil_reg_params + param_descriptor->param_offset - 1); + break; + case MB_PARAM_DISCRETE: + instance_ptr = ((void *)&discrete_reg_params + param_descriptor->param_offset - 1); + break; + default: + instance_ptr = NULL; + break; + } + } else { + ESP_LOGE(TAG, "Wrong parameter offset for CID #%u", (unsigned)param_descriptor->cid); + assert(instance_ptr != NULL); + } + return instance_ptr; +} + +#define TEST_VERIFY_VALUES(handle, pdescr, pinst) (__extension__( \ +{ \ + assert(pinst); \ + assert(pdescr); \ + uint8_t type = 0; \ + esp_err_t err = ESP_FAIL; \ + err = mbc_master_get_parameter(handle, pdescr->cid, \ + (uint8_t *)pinst, &type); \ + if (err == ESP_OK) { \ + bool is_correct = true; \ + if (pdescr->param_opts.opt3) { \ + for EACH_ITEM(pinst, pdescr->param_size / sizeof(*pitem)) { \ + if (*pitem != (typeof(*(pinst)))pdescr->param_opts.opt3) { \ + *pitem = (typeof(*(pinst)))pdescr->param_opts.opt3; \ + ESP_LOGD(TAG, "Characteristic #%d (%s), initialize to 0x%" PRIx16 ".", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (uint16_t)pdescr->param_opts.opt3); \ + is_correct = false; \ + } \ + } \ + } \ + if (!is_correct) { \ + ESP_LOGE(TAG, "Characteristic #%d (%s), initialize.", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key); \ + err = mbc_master_set_parameter(handle, cid, (uint8_t *)pinst, &type); \ + if (err != ESP_OK) { \ + ESP_LOGE(TAG, "Characteristic #%d (%s) write fail, err = 0x%x (%s).", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (int)err, \ + (char *)esp_err_to_name(err)); \ + } else { \ + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (..) write successful.", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (char *)pdescr->param_units); \ + } \ + } \ + } else { \ + ESP_LOGE(TAG, "Characteristic #%d (%s) read fail, err = 0x%x (%s).", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (int)err, \ + (char *)esp_err_to_name(err)); \ + } \ + (err); \ +} \ +)) + +// User operation function to read slave values and check alarm +static void master_operation_func(void *arg) +{ + esp_err_t err = ESP_OK; + bool alarm_state = false; + const mb_parameter_descriptor_t *param_descriptor = NULL; + + ESP_LOGI(TAG, "Start modbus test..."); + + for(uint16_t retry = 0; retry <= MASTER_MAX_RETRY && (!alarm_state); retry++) { + // Read all found characteristics from slave(s) + for (uint16_t cid = 0; (err != ESP_ERR_NOT_FOUND) && cid < MASTER_MAX_CIDS; cid++) { + // Get data from parameters description table + // and use this information to fill the characteristics description table + // and having all required fields in just one table + err = mbc_master_get_cid_info(master_handle, cid, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) { + void *temp_data_ptr = master_get_param_data(param_descriptor); + assert(temp_data_ptr); + if ((param_descriptor->param_type == PARAM_TYPE_ASCII) && + (param_descriptor->cid == CID_HOLD_TEST_REG)) { + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint32_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint32_t *)temp_data_ptr); + } +#if CONFIG_FMB_EXT_TYPE_SUPPORT + } else if ((param_descriptor->cid >= CID_HOLD_U16_AB) + && (param_descriptor->cid <= CID_HOLD_U16_BA)) { + // Check the uint16 parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint16_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%" PRIx16 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint16_t *)temp_data_ptr); + } + } else if ((param_descriptor->cid >= CID_HOLD_U8_A) + && (param_descriptor->cid <= CID_HOLD_U8_B)) { + // Check the uint8 parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint16_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%" PRIx16 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint16_t *)temp_data_ptr); + } + } else if ((param_descriptor->cid >= CID_HOLD_UINT32_ABCD) + && (param_descriptor->cid <= CID_HOLD_UINT32_DCBA)) { + // Check the uint32 parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint32_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %" PRIu32 " (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint32_t *)temp_data_ptr, + *(uint32_t *)temp_data_ptr); + } + } else if ((param_descriptor->cid >= CID_HOLD_FLOAT_ABCD) + && (param_descriptor->cid <= CID_HOLD_FLOAT_DCBA)) { + // Check the float parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (float *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %f (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(float *)temp_data_ptr, + *(uint32_t *)temp_data_ptr); + } + } else if (param_descriptor->cid >= CID_HOLD_DOUBLE_ABCDEFGH) { + // Check the double parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (double *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %lf (0x%" PRIx64 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(double *)temp_data_ptr, + *(uint64_t *)temp_data_ptr); + } +#endif + } else if (cid <= CID_HOLD_DATA_2) { + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (float *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %f (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(float *)temp_data_ptr, + *(uint32_t *)temp_data_ptr); + } + float value = *(float *)temp_data_ptr; + if (((value > param_descriptor->param_opts.max) || + (value < param_descriptor->param_opts.min))) { + alarm_state = true; + break; + } + } else if ((cid >= CID_RELAY_P1) && (cid <= CID_DISCR_P1)) { + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint8_t *)temp_data_ptr) == ESP_OK) { + uint8_t state = *(uint8_t *)temp_data_ptr; + const char *rw_str = (state & param_descriptor->param_opts.opt1) ? "ON" : "OFF"; + if ((state & param_descriptor->param_opts.opt2) == param_descriptor->param_opts.opt2) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %s (0x%" PRIx8 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + (const char *)rw_str, + *(uint8_t *)temp_data_ptr); + } else { + ESP_LOGE(TAG, "Characteristic #%d %s (%s) value = %s (0x%" PRIx8 "), unexpected value.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + (const char *)rw_str, + *(uint8_t *)temp_data_ptr); + alarm_state = true; + break; + } + if (state & param_descriptor->param_opts.opt1) { + alarm_state = true; + break; + } + } + } + vTaskDelay(POLL_TIMEOUT_TICS); // timeout between polls + } + } + vTaskDelay(UPDATE_CIDS_TIMEOUT_TICS); + } + + if (alarm_state) { + ESP_LOGI(TAG, "Alarm triggered by cid #%u.", param_descriptor->cid); + } else { + ESP_LOGE(TAG, "Alarm is not triggered after %u retries.", + MASTER_MAX_RETRY); + } + ESP_LOGI(TAG, "Destroy master..."); + ESP_ERROR_CHECK(mbc_master_delete(master_handle)); +} + +// Modbus master initialization +static esp_err_t master_init(void) +{ + // Initialize Modbus controller + mb_communication_info_t comm = { + .ser_opts.port = MB_PORT_NUM, +#if CONFIG_MB_COMM_MODE_ASCII + .ser_opts.mode = MB_ASCII, +#elif CONFIG_MB_COMM_MODE_RTU + .ser_opts.mode = MB_RTU, +#endif + .ser_opts.baudrate = MB_DEV_SPEED, + .ser_opts.parity = MB_PARITY_NONE, + .ser_opts.uid = 0, + .ser_opts.response_tout_ms = 1000, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1 + }; + + esp_err_t err = mbc_master_create_serial(&comm, &master_handle); + MB_RETURN_ON_FALSE((master_handle != NULL), ESP_ERR_INVALID_STATE, TAG, + "mb controller initialization fail."); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, + "mb controller initialization fail, returns(0x%x).", (int)err); + + // Set UART pin numbers + err = uart_set_pin(MB_PORT_NUM, CONFIG_MB_UART_TXD, CONFIG_MB_UART_RXD, + CONFIG_MB_UART_RTS, UART_PIN_NO_CHANGE); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, + "mb serial set pin failure, uart_set_pin() returned (0x%x).", (int)err); + + err = mbc_master_start(master_handle); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, + "mb controller start fail, returned (0x%x).", (int)err); + + // Set driver mode to Half Duplex + err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, + "mb serial set mode failure, uart_set_mode() returned (0x%x).", (int)err); + + vTaskDelay(5); + err = mbc_master_set_descriptor(master_handle, &device_parameters[0], num_device_parameters); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, TAG, + "mb controller set descriptor fail, returns(0x%x).", (int)err); + ESP_LOGI(TAG, "Modbus master stack initialized..."); + return err; +} + +void app_main(void) +{ + // Initialization of device peripheral and objects + ESP_ERROR_CHECK(master_init()); + vTaskDelay(10); + + master_operation_func(NULL); +} diff --git a/examples/serial/mb_serial_master/sdkconfig.ci.ascii b/examples/serial/mb_serial_master/sdkconfig.ci.ascii new file mode 100644 index 0000000..931ef7a --- /dev/null +++ b/examples/serial/mb_serial_master/sdkconfig.ci.ascii @@ -0,0 +1,7 @@ +CONFIG_MB_COMM_MODE_ASCII=y +CONFIG_MB_COMM_MODE_RTU=n +CONFIG_MB_UART_BAUD_RATE=115200 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=200 +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=400 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y diff --git a/examples/serial/mb_serial_master/sdkconfig.ci.rtu b/examples/serial/mb_serial_master/sdkconfig.ci.rtu new file mode 100644 index 0000000..0545373 --- /dev/null +++ b/examples/serial/mb_serial_master/sdkconfig.ci.rtu @@ -0,0 +1,7 @@ +CONFIG_MB_COMM_MODE_ASCII=n +CONFIG_MB_COMM_MODE_RTU=y +CONFIG_MB_UART_BAUD_RATE=115200 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=200 +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=400 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y diff --git a/examples/serial/mb_serial_slave/CMakeLists.txt b/examples/serial/mb_serial_slave/CMakeLists.txt new file mode 100644 index 0000000..2b26eba --- /dev/null +++ b/examples/serial/mb_serial_slave/CMakeLists.txt @@ -0,0 +1,10 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +# Exclude old component feemodbus which exists in old versions +set(EXCLUDE_COMPONENTS freemodbus) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +project(modbus_serial_slave) diff --git a/examples/serial/mb_serial_slave/README.md b/examples/serial/mb_serial_slave/README.md new file mode 100644 index 0000000..ae04a42 --- /dev/null +++ b/examples/serial/mb_serial_slave/README.md @@ -0,0 +1,97 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | + +# Modbus Slave Example + +This example demonstrates using the port of the esp-modbus stack on an ESP32 target where the ESP32 target is operating as a network slave. The example allows an external Modbus host to read/write device parameters on the ESP32 target using the Modbus protocol. The parameters accessible through Modbus are located in `mb_example_common/modbus_params.h\c` source/header files that users can update to add/remove their own custom parameters. +These are represented in structures `holding_reg_params`, `input_reg_params`, `coil_reg_params`, `discrete_reg_params` for holding registers, input parameters, coils and discrete inputs accordingly. The app_main application demonstrates how to setup Modbus stack and use notifications about parameters change from host system. +The FreeModbus stack located in `modbus` folder and supports the ESP32 targets. There are some parameters of the port that can be configured in KConfig file to start stack correctly (See description below for more information). + +The slave example uses shared parameter structures defined in `examples/protocols/modbus/mb_example_common` folder. + +## Hardware required : +Option 1: +PC + USB Serial adapter connected to USB port + RS485 line drivers + ESP32 based board. +The MAX485 line driver is used as an example below but other similar chips can be used as well. + +Option 2: +The modbus_master example application configured as described in its README.md file and flashed into ESP32 based board. +Note: The ```Example Data (Object) Dictionary``` in the modbus_master example can be edited to address parameters from other slaves connected into Modbus segment. + +RS485 example circuit schematic: +``` + VCC ---------------+ +--------------- VCC + | | + +-------x-------+ +-------x-------+ + RXD <------| RO | DIFFERENTIAL | RO|-----> RXD + | B|---------------|B | + TXD ------>| DI MAX485 | \ / | MAX485 DI|<----- TXD +ESP32 board | | RS-485 side | | Modbus master + RTS --+--->| DE | / \ | DE|---+ + | | A|---------------|A | | + +----| /RE | PAIR | /RE|---+-- RTS + +-------x--------+ +-------x-------+ + | | + --- --- +``` + +## How to setup and use an example: + +### Configure the application +Start the command below to show the configuration menu: +``` +idf.py menuconfig +``` +Select Modbus Example Configuration menu item. +Configure the UART pins used for modbus communication using the command and table below. +``` + ------------------------------------------------------------------------------------------------------------------------------ + | UART Interface | #define | Default pins for | Default pins for | External RS485 Driver Pin | + | | | ESP32 (C6) | ESP32-S2 (S3, C3, C2, H2) | | + | ----------------------|--------------------|-----------------------|---------------------------|---------------------------| + | Transmit Data (TxD) | CONFIG_MB_UART_TXD | GPIO23 | GPIO9 | DI | + | Receive Data (RxD) | CONFIG_MB_UART_RXD | GPIO22 | GPIO8 | RO | + | Request To Send (RTS) | CONFIG_MB_UART_RTS | GPIO18 | GPIO10 | ~RE/DE | + | Ground | n/a | GND | GND | GND | + ------------------------------------------------------------------------------------------------------------------------------ +``` +Note: Each target chip has different GPIO pins available for UART connection. Please refer to UART documentation for selected target for more information. + +Define the ```Modbus communiction mode``` for slave in Kconfig - CONFIG_MB_COMM_MODE (must be the same for master and slave application). +Set ```Modbus slave address``` for the example application (by default for example script is set to 1). +The communication parameters of esp-modbus stack (Component config->Modbus configuration) allow to configure it appropriately but usually it is enough to use default settings. +See the help strings of parameters for more information. + +### Setup external Modbus master software +Option 1: +Configure the external Modbus master software according to port configuration parameters used in application. +As an example the Modbus Poll application can be used with this example. +Option 2: +Setup ESP32 based board and set modbus_master example configuration as described in its README.md file. +Setup one or more slave boards with different slave addresses and connect them into the same Modbus segment (See configuration above). +Note: The ```Modbus communiction mode``` parameter must be the same for master and slave example application to be able to communicate with each other. + +### Build and flash software +Build the project and flash it to the board, then run monitor tool to view serial output: +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output +Example output of the application: +``` +I (13941) SLAVE_TEST: INPUT READ (13651163 us), ADDR:1, TYPE:8, INST_ADDR:0x3ffb2fd0, SIZE:2 +I (13951) SLAVE_TEST: HOLDING READ (13656431 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffb2fe0, SIZE:2 +I (13961) SLAVE_TEST: INPUT READ (13665877 us), ADDR:3, TYPE:8, INST_ADDR:0x3ffb2fd4, SIZE:2 +I (13971) SLAVE_TEST: HOLDING READ (13676010 us), ADDR:3, TYPE:2, INST_ADDR:0x3ffb2fe4, SIZE:2 +I (13981) SLAVE_TEST: INPUT READ (13686130 us), ADDR:5, TYPE:8, INST_ADDR:0x3ffb2fd8, SIZE:2 +I (13991) SLAVE_TEST: HOLDING READ (13696267 us), ADDR:5, TYPE:2, INST_ADDR:0x3ffb2fe8, SIZE:2 +I (14001) SLAVE_TEST: COILS READ (13706331 us), ADDR:0, TYPE:32, INST_ADDR:0x3ffb2fcc, SIZE:8 +I (14001) SLAVE_TEST: Modbus controller destroyed. +``` +The output lines describe type of operation, its timestamp, modbus address, access type, storage address in parameter structure and number of registers accordingly. + diff --git a/examples/serial/mb_serial_slave/main/CMakeLists.txt b/examples/serial/mb_serial_slave/main/CMakeLists.txt new file mode 100644 index 0000000..9220403 --- /dev/null +++ b/examples/serial/mb_serial_slave/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(PROJECT_NAME "modbus_serial_slave") + +idf_component_register(SRCS "serial_slave.c" + INCLUDE_DIRS ".") diff --git a/examples/serial/mb_serial_slave/main/Kconfig.projbuild b/examples/serial/mb_serial_slave/main/Kconfig.projbuild new file mode 100644 index 0000000..8105254 --- /dev/null +++ b/examples/serial/mb_serial_slave/main/Kconfig.projbuild @@ -0,0 +1,108 @@ +menu "Modbus Example Configuration" + + config MB_UART_PORT_ONE + bool + default y + depends on (ESP_CONSOLE_UART_NUM !=1) && (SOC_UART_NUM > 1) + + config MB_UART_PORT_TWO + bool + default y + depends on (ESP_CONSOLE_UART_NUM !=2) && (SOC_UART_NUM > 2) + + config MB_UART_PORT_NUM + int "UART port number" + range 0 2 if MB_UART_PORT_TWO + default 2 if MB_UART_PORT_TWO + range 0 1 if MB_UART_PORT_ONE + default 1 if MB_UART_PORT_ONE + help + UART communication port number for Modbus example. + + config MB_UART_BAUD_RATE + int "UART communication speed" + range 1200 115200 + default 115200 + help + UART communication speed for Modbus example. + + config MB_UART_RXD + int "UART RXD pin number" + range 0 34 if IDF_TARGET_ESP32 + range 0 23 if IDF_TARGET_ESP32C6 + range 0 56 if IDF_TARGET_ESP32P4 + default 22 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 47 if IDF_TARGET_ESP32S3 + range 0 19 if IDF_TARGET_ESP32C3 + range 0 20 if IDF_TARGET_ESP32C2 + range 0 27 if IDF_TARGET_ESP32H2 + default 8 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 8 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C2 || IDF_TARGET_ESP32H2 + help + GPIO number for UART RX pin. See UART documentation for more information + about available pin numbers for UART. + + config MB_UART_TXD + int "UART TXD pin number" + range 0 34 if IDF_TARGET_ESP32 + range 0 56 if IDF_TARGET_ESP32P4 + range 0 23 if IDF_TARGET_ESP32C6 + default 23 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32C6 || IDF_TARGET_ESP32P4 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 47 if IDF_TARGET_ESP32S3 + range 0 19 if IDF_TARGET_ESP32C3 + range 0 20 if IDF_TARGET_ESP32C2 + range 0 27 if IDF_TARGET_ESP32H2 + default 9 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 9 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C2 || IDF_TARGET_ESP32H2 + help + GPIO number for UART TX pin. See UART documentation for more information + about available pin numbers for UART. + + config MB_UART_RTS + int "UART RTS pin number" + range 0 34 if IDF_TARGET_ESP32 + range 0 23 if IDF_TARGET_ESP32C6 + range 0 56 if IDF_TARGET_ESP32P4 + default 20 if IDF_TARGET_ESP32P4 + default 18 if IDF_TARGET_ESP32 || IDF_TARGET_ESP32C6 + range 0 46 if IDF_TARGET_ESP32S2 + range 0 47 if IDF_TARGET_ESP32S3 + range 0 19 if IDF_TARGET_ESP32C3 + range 0 20 if IDF_TARGET_ESP32C2 + range 0 27 if IDF_TARGET_ESP32H2 + default 10 if IDF_TARGET_ESP32S2 || IDF_TARGET_ESP32S3 + default 10 if IDF_TARGET_ESP32C3 || IDF_TARGET_ESP32C2 || IDF_TARGET_ESP32H2 + help + GPIO number for UART RTS pin. This pin is connected to + ~RE/DE pin of RS485 transceiver to switch direction. + See UART documentation for more information about available pin + numbers for UART. + + choice MB_COMM_MODE + prompt "Modbus communication mode" + default MB_COMM_MODE_RTU if CONFIG_FMB_COMM_MODE_RTU_EN + help + Selection of Modbus communication mode option for Modbus. + + config MB_COMM_MODE_RTU + bool "RTU mode" + depends on FMB_COMM_MODE_RTU_EN + + config MB_COMM_MODE_ASCII + bool "ASCII mode" + depends on FMB_COMM_MODE_ASCII_EN + + endchoice + + config MB_SLAVE_ADDR + int "Modbus slave address" + range 1 247 + default 1 + help + This is the Modbus slave address in the network. + It is used to organize Modbus network with several slaves connected into the same segment. + + +endmenu diff --git a/examples/serial/mb_serial_slave/main/idf_component.yml b/examples/serial/mb_serial_slave/main/idf_component.yml new file mode 100644 index 0000000..307f3db --- /dev/null +++ b/examples/serial/mb_serial_slave/main/idf_component.yml @@ -0,0 +1,7 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../../" + mb_example_common: + path: "../../../mb_example_common" diff --git a/examples/serial/mb_serial_slave/main/serial_slave.c b/examples/serial/mb_serial_slave/main/serial_slave.c new file mode 100644 index 0000000..c5a2b1c --- /dev/null +++ b/examples/serial/mb_serial_slave/main/serial_slave.c @@ -0,0 +1,283 @@ +/* + * SPDX-FileCopyrightText: 2016-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_err.h" +#include "mbcontroller.h" // for mbcontroller defines and api +#include "modbus_params.h" // for modbus parameters structures +#include "esp_log.h" // for log_write +#include "sdkconfig.h" + +#define MB_PORT_NUM (CONFIG_MB_UART_PORT_NUM) // Number of UART port used for Modbus connection +#define MB_SLAVE_ADDR (CONFIG_MB_SLAVE_ADDR) // The address of device in Modbus network +#define MB_DEV_SPEED (CONFIG_MB_UART_BAUD_RATE) // The communication speed of the UART + +// Note: Some pins on target chip cannot be assigned for UART communication. +// Please refer to documentation for selected board and target to configure pins using Kconfig. + +// Defines below are used to define register start address for each type of Modbus registers +#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) >> 1)) +#define INPUT_OFFSET(field) ((uint16_t)(offsetof(input_reg_params_t, field) >> 1)) +#define MB_REG_DISCRETE_INPUT_START (0x0000) +#define MB_REG_COILS_START (0x0000) +#define MB_REG_INPUT_START_AREA0 (INPUT_OFFSET(input_data0)) // register offset input area 0 +#define MB_REG_INPUT_START_AREA1 (INPUT_OFFSET(input_data4)) // register offset input area 1 +#define MB_REG_HOLDING_START_AREA0 (HOLD_OFFSET(holding_data0)) +#define MB_REG_HOLDING_START_AREA0_SIZE ((size_t)((HOLD_OFFSET(holding_data4) - HOLD_OFFSET(holding_data0)) << 1)) +#define MB_REG_HOLDING_START_AREA1 (HOLD_OFFSET(holding_data4)) +#define MB_REG_HOLDING_START_AREA1_SIZE ((size_t)((HOLD_OFFSET(holding_area1_end) - HOLD_OFFSET(holding_data4)) << 1)) +#define MB_REG_HOLDING_START_AREA2 (HOLD_OFFSET(holding_u8_a)) +#define MB_REG_HOLDING_START_AREA2_SIZE ((size_t)((HOLD_OFFSET(holding_area2_end) - HOLD_OFFSET(holding_u8_a)) << 1)) + +#define MB_PAR_INFO_GET_TOUT (10) // Timeout for get parameter info +#define MB_CHAN_DATA_MAX_VAL (6) +#define MB_CHAN_DATA_OFFSET (1.2f) +#define MB_READ_MASK (MB_EVENT_INPUT_REG_RD \ + | MB_EVENT_HOLDING_REG_RD \ + | MB_EVENT_DISCRETE_RD \ + | MB_EVENT_COILS_RD) +#define MB_WRITE_MASK (MB_EVENT_HOLDING_REG_WR \ + | MB_EVENT_COILS_WR) +#define MB_READ_WRITE_MASK (MB_READ_MASK | MB_WRITE_MASK) +#define MB_TEST_VALUE 12345.0 + +static const char *TAG = "SLAVE_TEST"; + +static void *mbc_slave_handle = NULL; + +// Set register values into known state +static void setup_reg_data(void) +{ + // Define initial state of parameters + discrete_reg_params.discrete_input0 = 1; + discrete_reg_params.discrete_input1 = 0; + discrete_reg_params.discrete_input2 = 1; + discrete_reg_params.discrete_input3 = 0; + discrete_reg_params.discrete_input4 = 1; + discrete_reg_params.discrete_input5 = 0; + discrete_reg_params.discrete_input6 = 1; + discrete_reg_params.discrete_input7 = 0; + + holding_reg_params.holding_data0 = 1.34; + holding_reg_params.holding_data1 = 2.56; + holding_reg_params.holding_data2 = 3.78; + holding_reg_params.holding_data3 = 4.90; + + holding_reg_params.holding_data4 = 5.67; + holding_reg_params.holding_data5 = 6.78; + holding_reg_params.holding_data6 = 7.79; + holding_reg_params.holding_data7 = 8.80; + +#if CONFIG_FMB_EXT_TYPE_SUPPORT + mb_set_uint8_a((val_16_arr *)&holding_reg_params.holding_u8_a[0], (uint8_t)0x55); + mb_set_uint8_a((val_16_arr *)&holding_reg_params.holding_u8_a[1], (uint8_t)0x55); + mb_set_uint8_b((val_16_arr *)&holding_reg_params.holding_u8_b[0], (uint8_t)0x55); + mb_set_uint8_b((val_16_arr *)&holding_reg_params.holding_u8_b[1], (uint8_t)0x55); + mb_set_uint16_ab((val_16_arr *)&holding_reg_params.holding_u16_ab[1], (uint16_t)MB_TEST_VALUE); + mb_set_uint16_ab((val_16_arr *)&holding_reg_params.holding_u16_ab[0], (uint16_t)MB_TEST_VALUE); + mb_set_uint16_ba((val_16_arr *)&holding_reg_params.holding_u16_ba[0], (uint16_t)MB_TEST_VALUE); + mb_set_uint16_ba((val_16_arr *)&holding_reg_params.holding_u16_ba[1], (uint16_t)MB_TEST_VALUE); + + mb_set_float_abcd((val_32_arr *)&holding_reg_params.holding_float_abcd[0], (float)MB_TEST_VALUE); + mb_set_float_abcd((val_32_arr *)&holding_reg_params.holding_float_abcd[1], (float)MB_TEST_VALUE); + mb_set_float_cdab((val_32_arr *)&holding_reg_params.holding_float_cdab[0], (float)MB_TEST_VALUE); + mb_set_float_cdab((val_32_arr *)&holding_reg_params.holding_float_cdab[1], (float)MB_TEST_VALUE); + mb_set_float_badc((val_32_arr *)&holding_reg_params.holding_float_badc[0], (float)MB_TEST_VALUE); + mb_set_float_badc((val_32_arr *)&holding_reg_params.holding_float_badc[1], (float)MB_TEST_VALUE); + mb_set_float_dcba((val_32_arr *)&holding_reg_params.holding_float_dcba[0], (float)MB_TEST_VALUE); + mb_set_float_dcba((val_32_arr *)&holding_reg_params.holding_float_dcba[1], (float)MB_TEST_VALUE); + + mb_set_uint32_abcd((val_32_arr *)&holding_reg_params.holding_uint32_abcd[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_abcd((val_32_arr *)&holding_reg_params.holding_uint32_abcd[1], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_cdab((val_32_arr *)&holding_reg_params.holding_uint32_cdab[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_cdab((val_32_arr *)&holding_reg_params.holding_uint32_cdab[1], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_badc((val_32_arr *)&holding_reg_params.holding_uint32_badc[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_badc((val_32_arr *)&holding_reg_params.holding_uint32_badc[1], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_dcba((val_32_arr *)&holding_reg_params.holding_uint32_dcba[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_dcba((val_32_arr *)&holding_reg_params.holding_uint32_dcba[1], (uint32_t)MB_TEST_VALUE); + + mb_set_double_abcdefgh((val_64_arr *)&holding_reg_params.holding_double_abcdefgh[0], (double)MB_TEST_VALUE); + mb_set_double_abcdefgh((val_64_arr *)&holding_reg_params.holding_double_abcdefgh[1], (double)MB_TEST_VALUE); + mb_set_double_hgfedcba((val_64_arr *)&holding_reg_params.holding_double_hgfedcba[0], (double)MB_TEST_VALUE); + mb_set_double_hgfedcba((val_64_arr *)&holding_reg_params.holding_double_hgfedcba[1], (double)MB_TEST_VALUE); + mb_set_double_ghefcdab((val_64_arr *)&holding_reg_params.holding_double_ghefcdab[0], (double)MB_TEST_VALUE); + mb_set_double_ghefcdab((val_64_arr *)&holding_reg_params.holding_double_ghefcdab[1], (double)MB_TEST_VALUE); + mb_set_double_badcfehg((val_64_arr *)&holding_reg_params.holding_double_badcfehg[0], (double)MB_TEST_VALUE); + mb_set_double_badcfehg((val_64_arr *)&holding_reg_params.holding_double_badcfehg[1], (double)MB_TEST_VALUE); +#endif + + coil_reg_params.coils_port0 = 0x55; + coil_reg_params.coils_port1 = 0xAA; + + input_reg_params.input_data0 = 1.12; + input_reg_params.input_data1 = 2.34; + input_reg_params.input_data2 = 3.56; + input_reg_params.input_data3 = 4.78; + + input_reg_params.input_data4 = 1.12; + input_reg_params.input_data5 = 2.34; + input_reg_params.input_data6 = 3.56; + input_reg_params.input_data7 = 4.78; +} + +// An example application of Modbus slave. It is based on esp-modbus stack. +// See deviceparams.h file for more information about assigned Modbus parameters. +// These parameters can be accessed from main application and also can be changed +// by external Modbus master host. +void app_main(void) +{ + mb_param_info_t reg_info; // keeps the Modbus registers access information + mb_register_area_descriptor_t reg_area; // Modbus register area descriptor structure + + // Set UART log level + esp_log_level_set(TAG, ESP_LOG_INFO); + + // Initialize Modbus controller + mb_communication_info_t comm_config = { + .ser_opts.port = MB_PORT_NUM, +#if CONFIG_MB_COMM_MODE_ASCII + .ser_opts.mode = MB_ASCII, +#elif CONFIG_MB_COMM_MODE_RTU + .ser_opts.mode = MB_RTU, +#endif + .ser_opts.baudrate = MB_DEV_SPEED, + .ser_opts.parity = MB_PARITY_NONE, + .ser_opts.uid = MB_SLAVE_ADDR, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1 + }; + + ESP_ERROR_CHECK(mbc_slave_create_serial(&comm_config, &mbc_slave_handle)); // Initialization of Modbus controller + + // The code below initializes Modbus register area descriptors + // for Modbus Holding Registers, Input Registers, Coils and Discrete Inputs + // Initialization should be done for each supported Modbus register area according to register map. + // When external master trying to access the register in the area that is not initialized + // by mbc_slave_set_descriptor() API call then Modbus stack + // will send exception response for this register area. + reg_area.type = MB_PARAM_HOLDING; // Set type of register area + reg_area.start_offset = MB_REG_HOLDING_START_AREA0; // Offset of register area in Modbus protocol + reg_area.address = (void*)&holding_reg_params.holding_data0; // Set pointer to storage instance + // Set the size of register storage instance in bytes + reg_area.size = MB_REG_HOLDING_START_AREA0_SIZE; + ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area)); + + // The second register area + reg_area.type = MB_PARAM_HOLDING; // Set type of register area + reg_area.start_offset = MB_REG_HOLDING_START_AREA1; + reg_area.address = (void*)&holding_reg_params.holding_data4; + reg_area.size = MB_REG_HOLDING_START_AREA1_SIZE; + ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area)); + +#if CONFIG_FMB_EXT_TYPE_SUPPORT + // The extended parameters register area + reg_area.type = MB_PARAM_HOLDING; + reg_area.start_offset = MB_REG_HOLDING_START_AREA2; + reg_area.address = (void*)&holding_reg_params.holding_u8_a; + reg_area.size = MB_REG_HOLDING_START_AREA2_SIZE; + ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area)); +#endif + + // Initialization of Input Registers area + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = MB_REG_INPUT_START_AREA0; + reg_area.address = (void*)&input_reg_params.input_data0; + reg_area.size = sizeof(float) << 2; + ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area)); + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = MB_REG_INPUT_START_AREA1; + reg_area.address = (void*)&input_reg_params.input_data4; + reg_area.size = sizeof(float) << 2; + ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area)); + + // Initialization of Coils register area + reg_area.type = MB_PARAM_COIL; + reg_area.start_offset = MB_REG_COILS_START; + reg_area.address = (void*)&coil_reg_params; + reg_area.size = sizeof(coil_reg_params); + ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area)); + + // Initialization of Discrete Inputs register area + reg_area.type = MB_PARAM_DISCRETE; + reg_area.start_offset = MB_REG_DISCRETE_INPUT_START; + reg_area.address = (void*)&discrete_reg_params; + reg_area.size = sizeof(discrete_reg_params); + ESP_ERROR_CHECK(mbc_slave_set_descriptor(mbc_slave_handle, reg_area)); + + setup_reg_data(); // Set values into known state + + // Starts of modbus controller and stack + ESP_ERROR_CHECK(mbc_slave_start(mbc_slave_handle)); + + // Set UART pin numbers + ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM, CONFIG_MB_UART_TXD, + CONFIG_MB_UART_RXD, CONFIG_MB_UART_RTS, + UART_PIN_NO_CHANGE)); + + // Set UART driver mode to Half Duplex + ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX)); + + ESP_LOGI(TAG, "Modbus slave stack initialized."); + ESP_LOGI(TAG, "Start modbus test..."); + + // The cycle below will be terminated when parameter holdingRegParams.dataChan0 + // incremented each access cycle reaches the CHAN_DATA_MAX_VAL value. + for(;holding_reg_params.holding_data0 < MB_CHAN_DATA_MAX_VAL;) { + // Check for read/write events of Modbus master for certain events + (void)mbc_slave_check_event(mbc_slave_handle, MB_READ_WRITE_MASK); + // Get parameter information from parameter queue + ESP_ERROR_CHECK(mbc_slave_get_param_info(mbc_slave_handle, ®_info, MB_PAR_INFO_GET_TOUT)); + const char* rw_str = (reg_info.type & MB_READ_MASK) ? "READ" : "WRITE"; + + // Filter events and process them accordingly + if(reg_info.type & (MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD)) { + ESP_LOGI(TAG, "HOLDING %s (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + rw_str, + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + if (reg_info.address == (uint8_t*)&holding_reg_params.holding_data0) + { + (void)mbc_slave_lock(mbc_slave_handle); + holding_reg_params.holding_data0 += MB_CHAN_DATA_OFFSET; + if (holding_reg_params.holding_data0 >= (MB_CHAN_DATA_MAX_VAL - MB_CHAN_DATA_OFFSET)) { + coil_reg_params.coils_port1 = 0xFF; + } + (void)mbc_slave_unlock(mbc_slave_handle); + } + } else if (reg_info.type & MB_EVENT_INPUT_REG_RD) { + ESP_LOGI(TAG, "INPUT READ (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + } else if (reg_info.type & MB_EVENT_DISCRETE_RD) { + ESP_LOGI(TAG, "DISCRETE READ (%" PRIu32 " us): ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + } else if (reg_info.type & (MB_EVENT_COILS_RD | MB_EVENT_COILS_WR)) { + ESP_LOGI(TAG, "COILS %s (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + rw_str, + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + if (coil_reg_params.coils_port1 == 0xFF) break; + } + } + // Destroy of Modbus controller on alarm + ESP_LOGI(TAG,"Modbus controller destroyed."); + vTaskDelay(100); + ESP_ERROR_CHECK(mbc_slave_delete(mbc_slave_handle)); +} diff --git a/examples/serial/mb_serial_slave/sdkconfig.ci.ascii b/examples/serial/mb_serial_slave/sdkconfig.ci.ascii new file mode 100644 index 0000000..20e4dc4 --- /dev/null +++ b/examples/serial/mb_serial_slave/sdkconfig.ci.ascii @@ -0,0 +1,6 @@ +CONFIG_MB_COMM_MODE_ASCII=y +CONFIG_MB_COMM_MODE_RTU=n +CONFIG_MB_SLAVE_ADDR=1 +CONFIG_MB_UART_BAUD_RATE=115200 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y \ No newline at end of file diff --git a/examples/serial/mb_serial_slave/sdkconfig.ci.rtu b/examples/serial/mb_serial_slave/sdkconfig.ci.rtu new file mode 100644 index 0000000..d9b445c --- /dev/null +++ b/examples/serial/mb_serial_slave/sdkconfig.ci.rtu @@ -0,0 +1,6 @@ +CONFIG_MB_COMM_MODE_ASCII=n +CONFIG_MB_COMM_MODE_RTU=y +CONFIG_MB_SLAVE_ADDR=1 +CONFIG_MB_UART_BAUD_RATE=115200 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y diff --git a/examples/serial/mb_serial_slave/sdkconfig.defaults b/examples/serial/mb_serial_slave/sdkconfig.defaults new file mode 100644 index 0000000..f2b0fee --- /dev/null +++ b/examples/serial/mb_serial_slave/sdkconfig.defaults @@ -0,0 +1,8 @@ +# +# Modbus configuration +# +CONFIG_MB_COMM_MODE_ASCII=y +CONFIG_MB_SLAVE_ADDR=1 +CONFIG_MB_UART_BAUD_RATE=115200 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y diff --git a/examples/serial/pytest_mb_master_slave.py b/examples/serial/pytest_mb_master_slave.py new file mode 100644 index 0000000..630a67e --- /dev/null +++ b/examples/serial/pytest_mb_master_slave.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# This is the script to reproduce the issue when the expect() is called from +# main thread in Multi DUT case. + +import logging +import os +from typing import Tuple + +import pytest + +from conftest import ModbusTestDut, Stages + +pattern_dict_slave = {Stages.STACK_IPV4: (r'I \([0-9]+\) example_connect: - IPv4 address: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_IPV6: (r'I \([0-9]+\) example_connect: - IPv6 address: (([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})'), + Stages.STACK_INIT: (r'I \(([0-9]+)\) MB_TCP_SLAVE_PORT: (Protocol stack initialized).'), + Stages.STACK_CONNECT: (r'I\s\(([0-9]+)\) MB_TCP_SLAVE_PORT: Socket \(#[0-9]+\), accept client connection from address: ' + r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_START: (r'I\s\(([0-9]+)\) SLAVE_TEST: (Start modbus test)'), + Stages.STACK_PAR_OK: (r'I\s\(([0-9]+)\) SLAVE_TEST: ([A-Z]+ [A-Z]+) \([a-zA-Z0-9_]+ us\),\s' + r'ADDR:([0-9]+), TYPE:[0-9]+, INST_ADDR:0x[a-zA-Z0-9]+, SIZE:[0-9]+'), + Stages.STACK_PAR_FAIL: (r'E \(([0-9]+)\) SLAVE_TEST: Response time exceeds configured [0-9]+ [ms], ignore packet'), + Stages.STACK_DESTROY: (r'I\s\(([0-9]+)\) SLAVE_TEST: (Modbus controller destroyed).')} + +pattern_dict_master = {Stages.STACK_IPV4: (r'I \([0-9]+\) example_connect: - IPv4 address: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_IPV6: (r'I \([0-9]+\) example_connect: - IPv6 address: (([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})'), + Stages.STACK_INIT: (r'I \(([0-9]+)\) MASTER_TEST: (Modbus master stack initialized)'), + Stages.STACK_CONNECT: (r'I\s\(([0-9]+)\) MB_TCP_MASTER_PORT: (Connected [0-9]+ slaves), start polling'), + Stages.STACK_START: (r'I \(([0-9]+)\) MASTER_TEST: (Start modbus test)'), + Stages.STACK_PAR_OK: (r'I \(([0-9]+)\) MASTER_TEST: Characteristic #[0-9]+ ([a-zA-Z0-9_]+)' + r'\s\([a-zA-Z\_\%\/]+\) value =[a-zA-Z0-9\.\s]* \((0x[a-zA-Z0-9]+)\)[,\sa-z]+ successful'), + Stages.STACK_PAR_FAIL: (r'.*E \(([0-9]+)\) MASTER_TEST: Characteristic #[0-9]+\s\(([a-zA-Z0-9_]+)\)\s' + r'read fail, err = [0-9]+ \([_A-Z]+\)'), + Stages.STACK_DESTROY: (r'I \(([0-9]+)\) MASTER_TEST: (Destroy master)...')} + +LOG_LEVEL = logging.DEBUG +LOGGER_NAME = 'modbus_test' +logger = logging.getLogger(LOGGER_NAME) + +test_configs = [ + 'rtu', + 'ascii' +] + +@pytest.mark.esp32 +@pytest.mark.multi_dut_modbus_serial +@pytest.mark.parametrize('config', test_configs, indirect=True) +@pytest.mark.parametrize( + 'count, app_path', [ + (2, f'{os.path.join(os.path.dirname(__file__), "mb_serial_master")}|{os.path.join(os.path.dirname(__file__), "mb_serial_slave")}') + ], + indirect=True +) +def test_modbus_serial_communication(config: str, dut: Tuple[ModbusTestDut, ModbusTestDut]) -> None: + dut_slave = dut[1] + dut_master = dut[0] + + logger.info('DUT: %s start.', dut_master.dut_get_name()) + logger.info('DUT: %s start.', dut_slave.dut_get_name()) + + dut_slave.dut_test_start(dictionary=pattern_dict_slave) + dut_master.dut_test_start(dictionary=pattern_dict_master) + + dut_slave.dut_check_errors() + dut_master.dut_check_errors() + +@pytest.mark.multi_dut_modbus_generic +@pytest.mark.parametrize('config', ['dummy_config']) +def test_modbus_serial_generic() -> None: + print('The generic serial example tests are not provided yet.') \ No newline at end of file diff --git a/examples/tcp/README.md b/examples/tcp/README.md new file mode 100644 index 0000000..5e62a00 --- /dev/null +++ b/examples/tcp/README.md @@ -0,0 +1,58 @@ +# Modbus TCP Master-Slave Example + +## Overview + +These two projects illustrate the communication between Modbus master and slave device in the segment. +Master initializes Modbus interface driver and then reads parameters from slave device in the segment. +After several successful read attempts slave sets the alarm relay (end of test condition). +Once master reads the alarm it stops communication and destroy driver. + +The examples: + +* `examples/protocols/modbus/tcp/mb_tcp_master` - Modbus TCP master +* `examples/protocols/modbus/tcp/mb_tcp_slave` - Modbus TCP slave + +See README.md for each individual project for more information. + +## How to use example + +### Hardware Required + +This example can be run on any commonly available ESP32(-S2) development board. +The master and slave boards should be connected to the same network (see the README.md file in example folder) and slave address `CONFIG_MB_SLAVE_ADDR` be defined for slave board(s). +See the connection schematic in README.md files of each example. + +### Configure the project + +This example test requires communication mode setting for master and slave be the same and slave address set to 1. +Please refer to README.md files of each example project for more information. This example uses the default option `CONFIG_MB_SLAVE_IP_FROM_STDIN` to resolve slave IP address and supports IPv4 address type for communication in this case. + +## About common_component in this example + +The folder "mb_example_common" one level above includes definitions of parameter structures for master and slave device (both projects share the same parameters). +However, currently it is for example purpose only and can be modified for particular application. + +## Example Output + +Refer to README.md file in the appropriate example folder for more information about master and slave log output. + +## Troubleshooting + +If the examples do not work as expected and slave and master boards are not able to communicate correctly it is possible to find the reason for errors. +The most important errors are described in master example output and formatted as below: + +``` +E (1692332) MB_CONTROLLER_MASTER: mbc_master_get_parameter(111): SERIAL master get parameter failure error=(0x107) (ESP_ERR_TIMEOUT). +``` + +ESP_ERR_TIMEOUT (0x107) - Modbus slave device does not respond during configured timeout. +Check ability for communication pinging each slave configured in the master parameter description table or use command on your host machine to find modbus slave using mDNS (requires `CONFIG_MB_MDNS_IP_RESOLVER` option be enabled): +```>dns-sd -L mb_slave_tcp_XX _modbus._tcp .``` +where XX is the short slave address (index) of the slave configured in the Kconfig of slave example. +Also it is possible to increase Kconfig value `CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND` to compensate network communication delays between master and slaves. + +ESP_ERR_NOT_SUPPORTED (0x106), ESP_ERR_INVALID_RESPONSE (0x108) - Modbus slave device does not support requested command or register and sent exeption response. + +ESP_ERR_INVALID_STATE (0x103) - Modbus stack is not configured correctly or can't work correctly due to critical failure. + + diff --git a/examples/tcp/mb_tcp_master/CMakeLists.txt b/examples/tcp/mb_tcp_master/CMakeLists.txt new file mode 100644 index 0000000..6c24ee8 --- /dev/null +++ b/examples/tcp/mb_tcp_master/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +set(EXCLUDE_COMPONENTS freemodbus) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(modbus_tcp_master) diff --git a/examples/tcp/mb_tcp_master/README.md b/examples/tcp/mb_tcp_master/README.md new file mode 100644 index 0000000..7f298e1 --- /dev/null +++ b/examples/tcp/mb_tcp_master/README.md @@ -0,0 +1,146 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | + +# Modbus TCP Master Example + +This example demonstrates using of FreeModbus stack port implementation for ESP32 targets as a TCP master device. +This implementation is able to read/write values of slave devices connected into Modbus segment. All parameters to be accessed are defined in data dictionary of the modbus master example source file. +The values represented as characteristics with its name and characteristic CID which are linked into registers of slave devices connected into Modbus segment. +The example implements simple control algorithm and checks parameters from slave device and gets alarm (relay in the slave device) when value of parameter exceeded limit. +The instances for the modbus parameters are common for master and slave examples and located in `examples/protocols/modbus/mb_example_common` folder. + +Example parameters definition: +-------------------------------------------------------------------------------------------------- +| Slave Address | Characteristic ID | Characteristic name | Description | +|---------------------|----------------------|----------------------|----------------------------| +| MB_DEVICE_ADDR1 | CID_INP_DATA_0, | Data_channel_0 | Data channel 1 | +| MB_DEVICE_ADDR1 | CID_HOLD_DATA_0, | Humidity_1 | Humidity 1 | +| MB_DEVICE_ADDR1 | CID_INP_DATA_1 | Temperature_1 | Sensor temperature | +| MB_DEVICE_ADDR1 | CID_HOLD_DATA_1, | Humidity_2 | Humidity 2 | +| MB_DEVICE_ADDR1 | CID_INP_DATA_2 | Temperature_2 | Ambient temperature | +| MB_DEVICE_ADDR1 | CID_HOLD_DATA_2 | Humidity_3 | Humidity 3 | +| MB_DEVICE_ADDR1 | CID_RELAY_P1 | RelayP1 | Alarm Relay outputs on/off | +| MB_DEVICE_ADDR1 | CID_RELAY_P2 | RelayP2 | Alarm Relay outputs on/off | +-------------------------------------------------------------------------------------------------- +Note: The Slave Address is the same for all parameters for example test but it can be changed in the `Example Data (Object) Dictionary` table of master example to address parameters from other slaves. +The Kconfig ```Modbus slave address``` - CONFIG_MB_SLAVE_ADDR parameter in slave example can be configured to create Modbus multi slave segment. + +Simplified Modbus connection schematic for example test: + ``` + MB_DEVICE_ADDR1 + ------------- ------------- + | | Network | | + | Slave 1 |---<>--+---<>---| Master | + | | | | + ------------- ------------- +``` +Modbus multi slave segment connection schematic: +``` + MB_DEVICE_ADDR1 + ------------- + | | + | Slave 1 |---<>--+ + | | | + ------------- | + MB_DEVICE_ADDR2 | + ------------- | ------------- + | | | | | + | Slave 2 |---<>--+---<>---| Master | + | | | | | + ------------- | ------------- + MB_DEVICE_ADDR3 | + ------------- Network (Ethernet or WiFi connection) + | | | + | Slave 3 |---<>--+ + | | + ------------- +``` + +## Hardware required : +Option 1: +PC (Modbus TCP Slave application) + ESP32 based development board with modbus_tcp_slave example. + +Option 2: +Several ESP32 based boards flashed with modbus_tcp_slave example software to represent slave devices. The IP slave addresses for each board have to be configured in `Modbus Example Configuration` menu according to the communication table of example. +One ESP32 based development board should be flashed with modbus_master example and connected to the same network. All the boards require configuration of network settings as described in `examples/common_components/protocol_examples_common`. + +## How to setup and use an example: + +### Configure the application +Start the command below to setup configuration: +``` +idf.py menuconfig +``` + +The communication parameters of Modbus stack allow to configure it appropriately but usually it is enough to use default settings. +See the help string of parameters for more information. +There are three ways to configure how the master example will obtain slave IP addresses in the network: +* Enable CONFIG_MB_MDNS_IP_RESOLVER option allows to query for modbus services provided by Modbus slaves in the network and automatically configure IP table. This requires to activate the same option for each slave with unique modbus slave address configured in `Modbus Example Configuration` menu. +* Enable CONFIG_MB_SLAVE_IP_FROM_STDIN option to define IP addresses of slaves manually. In order to enter the IP addresses wait for the prompt and type the string with IP address following format. Prompt: "Waiting IPN from stdin:", then enter the IP address of the slave to connect: "IPN=192.168.1.21", where N = (configured slave address - 1). +* Configure slave addresses manually as below: +``` +char* slave_ip_address_table[MB_DEVICE_COUNT] = { + "192.168.1.21", // Address corresponds to MB_DEVICE_ADDR1 and set to predefined value by user + "192.168.1.22", // Address corresponds to MB_DEVICE_ADDR2 of slave device in the Modbus data dictionary + NULL // Marker of end of list + }; +``` + +### Setup external Modbus slave devices or emulator +Option 1: +Configure the external Modbus master software according to port configuration parameters used in the example. The Modbus Slave application can be used with this example to emulate slave devices with its parameters. Use official documentation for software to setup emulation of slave devices. + +Option 2: +Other option is to have the modbus_slave example application flashed into ESP32 based board and connect boards together as showed on the Modbus connection schematic above. See the Modbus slave API documentation to configure communication parameters and slave addresses as defined in "Example parameters definition" table above. + +### Build and flash software of master device +Build the project and flash it to the board, then run monitor tool to view serial output: +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output +Example output of the application: +``` +I (4644) esp_netif_handlers: example_connect: sta ip: 192.168.1.39, mask: 255.255.255.0, gw: 192.168.1.1 +I (4644) example_connect: Got IPv4 event: Interface "example_connect: sta" address: 192.168.1.39 +I (5644) example_connect: Got IPv6 event: Interface "example_connect: sta" address: fe80:0000:0000:0000:bedd:c2ff:fed1:b210, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (5644) example_connect: Connected to example_connect: sta +I (5654) example_connect: - IPv4 address: 192.168.1.39 +I (5664) example_connect: - IPv6 address: fe80:0000:0000:0000:bedd:c2ff:fed1:b210, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (5674) uart: ESP_INTR_FLAG_IRAM flag not set while CONFIG_UART_ISR_IN_IRAM is enabled, flag updated +I (5684) MASTER_TEST: Leave IP(0) = [192.168.1.21] set by user. +I (5694) MASTER_TEST: IP(1) is not set in the table. +I (5694) MASTER_TEST: Configured 1 IP addresse(s). +I (5704) MASTER_TEST: Modbus master stack initialized... +I (5704) MB_TCP_MASTER_PORT: TCP master stack initialized. +I (5724) MB_TCP_MASTER_PORT: Host[IP]: "192.168.1.21"[192.168.1.21] +I (5724) MB_TCP_MASTER_PORT: Add slave IP: 192.168.1.21 +I (5734) MB_TCP_MASTER_PORT: Connecting to slaves... +-.-.-.I (5844) MB_TCP_MASTER_PORT: Connected 1 slaves, start polling... +I (6004) MASTER_TEST: Start modbus test... +I (6044) MASTER_TEST: Characteristic #0 Data_channel_0 (Volts) value = 1.120000 (0x3f8f5c29) read successful. +I (6054) MASTER_TEST: Characteristic #1 Humidity_1 (%rH) value = 1.340000 (0x3fab851f) read successful. +I (6074) MASTER_TEST: Characteristic #2 Temperature_1 (C) value = 2.340000 (0x4015c28f) read successful. +I (6084) MASTER_TEST: Characteristic #3 Humidity_2 (%rH) value = 2.560000 (0x4023d70a) read successful. +I (6094) MASTER_TEST: Characteristic #4 Temperature_2 (C) value = 3.560000 (0x4063d70a) read successful. +I (6104) MASTER_TEST: Characteristic #5 Humidity_3 (%rH) value = 3.780000 (0x4071eb85) read successful. +I (6124) MASTER_TEST: Characteristic #6 RelayP1 (on/off) value = OFF (0x55) read successful. +I (6134) MASTER_TEST: Characteristic #7 RelayP2 (on/off) value = OFF (0xaa) read successful. +I (6854) MASTER_TEST: Characteristic #0 Data_channel_0 (Volts) value = 1.120000 (0x3f8f5c29) read successful. +I (7064) MASTER_TEST: Characteristic #1 Humidity_1 (%rH) value = 1.740000 (0x3fdeb852) read successful. +I (7264) MASTER_TEST: Characteristic #2 Temperature_1 (C) value = 2.340000 (0x4015c28f) read successful. +... +I (45974) MASTER_TEST: Characteristic #4 Temperature_2 (C) value = 3.560000 (0x4063d70a) read successful. +I (46174) MASTER_TEST: Characteristic #5 Humidity_3 (%rH) value = 3.780000 (0x4071eb85) read successful. +I (46384) MASTER_TEST: Characteristic #6 RelayP1 (on/off) value = OFF (0x55) read successful. +I (46584) MASTER_TEST: Characteristic #7 RelayP2 (on/off) value = ON (0xff) read successful. +I (47094) MASTER_TEST: Alarm triggered by cid #7. +I (47094) MASTER_TEST: Destroy master... +``` +The example reads the characteristics from slave device(s), while alarm is not triggered in the slave device (See the "Example parameters definition"). The output line describes Timestamp, Cid of characteristic, Characteristic name (Units), Characteristic value (Hex data). + diff --git a/examples/tcp/mb_tcp_master/main/CMakeLists.txt b/examples/tcp/mb_tcp_master/main/CMakeLists.txt new file mode 100644 index 0000000..842f186 --- /dev/null +++ b/examples/tcp/mb_tcp_master/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(PROJECT_NAME "modbus_tcp_master") + +idf_component_register(SRCS "tcp_master.c" + INCLUDE_DIRS ".") diff --git a/examples/tcp/mb_tcp_master/main/Kconfig.projbuild b/examples/tcp/mb_tcp_master/main/Kconfig.projbuild new file mode 100644 index 0000000..31e15ae --- /dev/null +++ b/examples/tcp/mb_tcp_master/main/Kconfig.projbuild @@ -0,0 +1,16 @@ +menu "Modbus TCP Example Configuration" + + choice MB_SLAVE_IP_RESOLVER + prompt "Select method to resolve slave IP addresses" + help + Select method which is used to resolve slave IP addresses + and configure Master TCP IP stack. + + config MB_MDNS_IP_RESOLVER + bool "Resolve Modbus slave addresses using mDNS service." + + config MB_SLAVE_IP_FROM_STDIN + bool "Configure Modbus slave addresses from stdin" + endchoice + +endmenu diff --git a/examples/tcp/mb_tcp_master/main/idf_component.yml b/examples/tcp/mb_tcp_master/main/idf_component.yml new file mode 100644 index 0000000..5983a02 --- /dev/null +++ b/examples/tcp/mb_tcp_master/main/idf_component.yml @@ -0,0 +1,12 @@ +dependencies: + idf: + version: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../../" + espressif/mdns: + version: "^1.0.0" + mb_example_common: + path: "../../../mb_example_common" + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/examples/tcp/mb_tcp_master/main/tcp_master.c b/examples/tcp/mb_tcp_master/main/tcp_master.c new file mode 100644 index 0000000..71125a1 --- /dev/null +++ b/examples/tcp/mb_tcp_master/main/tcp_master.c @@ -0,0 +1,726 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// esp-modbus Master Example ESP32 + +#include +#include +#include "esp_log.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_netif.h" +#include "esp_mac.h" + +#include "mdns.h" +#include "protocol_examples_common.h" + +#include "modbus_params.h" // for modbus parameters structures +#include "mbcontroller.h" +#include "sdkconfig.h" + +#define MB_TCP_PORT (CONFIG_FMB_TCP_PORT_DEFAULT) // TCP port used by example + +// The number of parameters that intended to be used in the particular control process +#define MASTER_MAX_CIDS num_device_parameters + +// Number of reading of parameters from slave +#define MASTER_MAX_RETRY (10) + +// Timeout to update cid over Modbus +#define UPDATE_CIDS_TIMEOUT_MS (500) +#define UPDATE_CIDS_TIMEOUT_TICS (UPDATE_CIDS_TIMEOUT_MS / portTICK_PERIOD_MS) + +// Timeout between polls +#define POLL_TIMEOUT_MS (1) +#define POLL_TIMEOUT_TICS (POLL_TIMEOUT_MS / portTICK_PERIOD_MS) +#define MB_MDNS_PORT (502) + +// The macro to get offset for parameter in the appropriate structure +#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1)) +#define INPUT_OFFSET(field) ((uint16_t)(offsetof(input_reg_params_t, field) + 1)) +#define COIL_OFFSET(field) ((uint16_t)(offsetof(coil_reg_params_t, field) + 1)) +#define DISCR_OFFSET(field) ((uint16_t)(offsetof(discrete_reg_params_t, field) + 1)) + +#define STR(fieldname) ((const char *)( fieldname )) +#define TEST_HOLD_REG_START(field) (HOLD_OFFSET(field) >> 1) +#define TEST_HOLD_REG_SIZE(field) (sizeof(((holding_reg_params_t *)0)->field) >> 1) + +#define TEST_INPUT_REG_START(field) (INPUT_OFFSET(field) >> 1) +#define TEST_INPUT_REG_SIZE(field) (sizeof(((input_reg_params_t *)0)->field) >> 1) + +#define TEST_VALUE (12345) // default test value +#define TEST_ASCII_BIN (0xAAAAAAAA) +#define TEST_ARR_REG_SZ (58) +#define TEST_HUMI_MIN (-40) +#define TEST_HUMI_MAX (50) +#define TEST_TEMP_MIN (0) +#define TEST_TEMP_MAX (100) + +// Options can be used as bit masks or parameter limits +#define OPTS(min_val, max_val, step_val) { .opt1 = min_val, .opt2 = max_val, .opt3 = step_val } + +#define MB_ID_BYTE0(id) ((uint8_t)(id)) +#define MB_ID_BYTE1(id) ((uint8_t)(((uint16_t)(id) >> 8) & 0xFF)) +#define MB_ID_BYTE2(id) ((uint8_t)(((uint32_t)(id) >> 16) & 0xFF)) +#define MB_ID_BYTE3(id) ((uint8_t)(((uint32_t)(id) >> 24) & 0xFF)) + +#define MB_ID2STR(id) MB_ID_BYTE0(id), MB_ID_BYTE1(id), MB_ID_BYTE2(id), MB_ID_BYTE3(id) + +#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT +#define MB_DEVICE_ID (uint32_t)CONFIG_FMB_CONTROLLER_SLAVE_ID +#else +#define MB_DEVICE_ID (uint32_t)0x00112233 +#endif + +#define MB_MDNS_INSTANCE(pref) pref"mb_master_tcp" + +#define EACH_ITEM(array, length) \ +(typeof(*(array)) *pitem = (array); (pitem < &((array)[length])); pitem++) + +static const char *TAG = "MASTER_TEST"; + +// Enumeration of modbus device addresses accessed by master device +// Each address in the table is a index of TCP slave ip address in mb_communication_info_t::tcp_ip_addr table +enum { + MB_DEVICE_ADDR1 = 1, // Slave UID = 1 + MB_DEVICE_ADDR2, + MB_DEVICE_ADDR3, + MB_DEVICE_COUNT = 3 +}; + +// Enumeration of all supported CIDs for device (used in parameter definition table) +enum { + CID_INP_DATA_0 = 0, + CID_HOLD_DATA_0, + CID_INP_DATA_1, + CID_HOLD_DATA_1, + CID_INP_DATA_2, + CID_HOLD_DATA_2, + CID_HOLD_TEST_REG, + CID_RELAY_P1, + CID_RELAY_P2, + CID_DISCR_P1, +#if CONFIG_FMB_EXT_TYPE_SUPPORT + CID_HOLD_U8_A, + CID_HOLD_U8_B, + CID_HOLD_U16_AB, + CID_HOLD_U16_BA, + CID_HOLD_UINT32_ABCD, + CID_HOLD_UINT32_CDAB, + CID_HOLD_UINT32_BADC, + CID_HOLD_UINT32_DCBA, + CID_HOLD_FLOAT_ABCD, + CID_HOLD_FLOAT_CDAB, + CID_HOLD_FLOAT_BADC, + CID_HOLD_FLOAT_DCBA, + CID_HOLD_DOUBLE_ABCDEFGH, + CID_HOLD_DOUBLE_HGFEDCBA, + CID_HOLD_DOUBLE_GHEFCDAB, + CID_HOLD_DOUBLE_BADCFEHG, +#endif + CID_COUNT +}; + +// Example Data (Object) Dictionary for Modbus parameters: +// The CID field in the table must be unique. +// Modbus Slave Addr field defines slave address of the device with correspond parameter. +// Modbus Reg Type - Type of Modbus register area (Holding register, Input Register and such). +// Reg Start field defines the start Modbus register number and Reg Size defines the number of registers for the characteristic accordingly. +// The Instance Offset defines offset in the appropriate parameter structure that will be used as instance to save parameter value. +// Data Type, Data Size specify type of the characteristic and its data size. +// Parameter Options field specifies the options that can be used to process parameter value (limits or masks). +// Access Mode - can be used to implement custom options for processing of characteristic (Read/Write restrictions, factory mode values and etc). +const mb_parameter_descriptor_t device_parameters[] = { + // { CID, Param Name, Units, Modbus Slave Addr, Modbus Reg Type, Reg Start, Reg Size, Instance Offset, Data Type, Data Size, Parameter Options, Access Mode} + { CID_INP_DATA_0, STR("Data_channel_0"), STR("Volts"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, + TEST_INPUT_REG_START(input_data0), TEST_INPUT_REG_SIZE(input_data0), + INPUT_OFFSET(input_data0), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DATA_0, STR("Humidity_1"), STR("%rH"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_data0), TEST_HOLD_REG_SIZE(holding_data0), + HOLD_OFFSET(holding_data0), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_HUMI_MIN, TEST_HUMI_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_INP_DATA_1, STR("Temperature_1"), STR("C"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, + TEST_INPUT_REG_START(input_data1), TEST_INPUT_REG_SIZE(input_data1), + INPUT_OFFSET(input_data1), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DATA_1, STR("Humidity_2"), STR("%rH"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_data1), TEST_HOLD_REG_SIZE(holding_data1), + HOLD_OFFSET(holding_data1), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_HUMI_MIN, TEST_HUMI_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_INP_DATA_2, STR("Temperature_2"), STR("C"), MB_DEVICE_ADDR2, MB_PARAM_INPUT, + TEST_INPUT_REG_START(input_data2), TEST_INPUT_REG_SIZE(input_data2), + INPUT_OFFSET(input_data2), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DATA_2, STR("Humidity_3"), STR("%rH"), MB_DEVICE_ADDR3, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_data2), TEST_HOLD_REG_SIZE(holding_data2), + HOLD_OFFSET(holding_data2), PARAM_TYPE_FLOAT, 4, + OPTS( TEST_HUMI_MIN, TEST_HUMI_MAX, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_TEST_REG, STR("Test_regs"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(test_regs), TEST_ARR_REG_SZ, + HOLD_OFFSET(test_regs), PARAM_TYPE_ASCII, (TEST_ARR_REG_SZ * 2), + OPTS( TEST_TEMP_MIN, TEST_TEMP_MAX, TEST_ASCII_BIN ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_RELAY_P1, STR("RelayP1"), STR("on/off"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 2, 6, + COIL_OFFSET(coils_port0), PARAM_TYPE_U8, 1, + OPTS( 0xAA, 0x15, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_RELAY_P2, STR("RelayP2"), STR("on/off"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 10, 6, + COIL_OFFSET(coils_port1), PARAM_TYPE_U8, 1, + OPTS( 0x55, 0x2A, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_DISCR_P1, STR("DiscreteInpP1"), STR("on/off"), MB_DEVICE_ADDR1, MB_PARAM_DISCRETE, 2, 7, + DISCR_OFFSET(discrete_input_port1), PARAM_TYPE_U8, 1, + OPTS( 0xAA, 0x15, 0 ), PAR_PERMS_READ_WRITE_TRIGGER }, +#if CONFIG_FMB_EXT_TYPE_SUPPORT + { CID_HOLD_U8_A, STR("U8_A"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u8_a), TEST_HOLD_REG_SIZE(holding_u8_a), + HOLD_OFFSET(holding_u8_a), PARAM_TYPE_U8_A, (TEST_HOLD_REG_SIZE(holding_u8_a) << 1), + OPTS( CHAR_MIN, 0x0055, 0x0055 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_U8_B, STR("U8_B"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u8_b), TEST_HOLD_REG_SIZE(holding_u8_b), + HOLD_OFFSET(holding_u8_b), PARAM_TYPE_U8_B, (TEST_HOLD_REG_SIZE(holding_u8_b) << 1), + OPTS( 0, 0x5500, 0x5500 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_U16_AB, STR("U16_AB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u16_ab), TEST_HOLD_REG_SIZE(holding_u16_ab), + HOLD_OFFSET(holding_u16_ab), PARAM_TYPE_U16_AB, (TEST_HOLD_REG_SIZE(holding_u16_ab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_U16_BA, STR("U16_BA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_u16_ba), TEST_HOLD_REG_SIZE(holding_u16_ba), + HOLD_OFFSET(holding_u16_ba), PARAM_TYPE_U16_BA, (TEST_HOLD_REG_SIZE(holding_u16_ab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_ABCD, STR("UINT32_ABCD"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_abcd), TEST_HOLD_REG_SIZE(holding_uint32_abcd), + HOLD_OFFSET(holding_uint32_abcd), PARAM_TYPE_U32_ABCD, (TEST_HOLD_REG_SIZE(holding_uint32_abcd) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_CDAB, STR("UINT32_CDAB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_cdab), TEST_HOLD_REG_SIZE(holding_uint32_cdab), + HOLD_OFFSET(holding_uint32_cdab), PARAM_TYPE_U32_CDAB, (TEST_HOLD_REG_SIZE(holding_uint32_cdab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_BADC, STR("UINT32_BADC"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_badc), TEST_HOLD_REG_SIZE(holding_uint32_badc), + HOLD_OFFSET(holding_uint32_badc), PARAM_TYPE_U32_BADC, (TEST_HOLD_REG_SIZE(holding_uint32_badc) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_UINT32_DCBA, STR("UINT32_DCBA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_uint32_dcba), TEST_HOLD_REG_SIZE(holding_uint32_dcba), + HOLD_OFFSET(holding_uint32_dcba), PARAM_TYPE_U32_DCBA, (TEST_HOLD_REG_SIZE(holding_uint32_dcba) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_ABCD, STR("FLOAT_ABCD"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_abcd), TEST_HOLD_REG_SIZE(holding_float_abcd), + HOLD_OFFSET(holding_float_abcd), PARAM_TYPE_FLOAT_ABCD, (TEST_HOLD_REG_SIZE(holding_float_abcd) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_CDAB, STR("FLOAT_CDAB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_cdab), TEST_HOLD_REG_SIZE(holding_float_cdab), + HOLD_OFFSET(holding_float_cdab), PARAM_TYPE_FLOAT_CDAB, (TEST_HOLD_REG_SIZE(holding_float_cdab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_BADC, STR("FLOAT_BADC"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_badc), TEST_HOLD_REG_SIZE(holding_float_badc), + HOLD_OFFSET(holding_float_badc), PARAM_TYPE_FLOAT_BADC, (TEST_HOLD_REG_SIZE(holding_float_badc) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_FLOAT_DCBA, STR("FLOAT_DCBA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_float_dcba), TEST_HOLD_REG_SIZE(holding_float_dcba), + HOLD_OFFSET(holding_float_dcba), PARAM_TYPE_FLOAT_DCBA, (TEST_HOLD_REG_SIZE(holding_float_dcba) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_ABCDEFGH, STR("DOUBLE_ABCDEFGH"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_abcdefgh), TEST_HOLD_REG_SIZE(holding_double_abcdefgh), + HOLD_OFFSET(holding_double_abcdefgh), PARAM_TYPE_DOUBLE_ABCDEFGH, (TEST_HOLD_REG_SIZE(holding_double_abcdefgh) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_HGFEDCBA, STR("DOUBLE_HGFEDCBA"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_hgfedcba), TEST_HOLD_REG_SIZE(holding_double_hgfedcba), + HOLD_OFFSET(holding_double_hgfedcba), PARAM_TYPE_DOUBLE_HGFEDCBA, (TEST_HOLD_REG_SIZE(holding_double_hgfedcba) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_GHEFCDAB, STR("DOUBLE_GHEFCDAB"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_ghefcdab), TEST_HOLD_REG_SIZE(holding_double_ghefcdab), + HOLD_OFFSET(holding_double_ghefcdab), PARAM_TYPE_DOUBLE_GHEFCDAB, (TEST_HOLD_REG_SIZE(holding_double_ghefcdab) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_HOLD_DOUBLE_BADCFEHG, STR("DOUBLE_BADCFEHG"), STR("__"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, + TEST_HOLD_REG_START(holding_double_badcfehg), TEST_HOLD_REG_SIZE(holding_double_badcfehg), + HOLD_OFFSET(holding_double_badcfehg), PARAM_TYPE_DOUBLE_BADCFEHG, (TEST_HOLD_REG_SIZE(holding_double_badcfehg) << 1), + OPTS( 0, TEST_VALUE, TEST_VALUE ), PAR_PERMS_READ_WRITE_TRIGGER } +#endif +}; + +// Calculate number of parameters in the table +const uint16_t num_device_parameters = (sizeof(device_parameters) / sizeof(device_parameters[0])); + +static void* master_handle = NULL; + +const size_t ip_table_sz; + +// This table represents slave IP addresses that correspond to the short address field of the slave in device_parameters structure +// Modbus TCP stack shall use these addresses to be able to connect and read parameters from slave +char* slave_ip_address_table[MB_DEVICE_COUNT + 1] = { +#if CONFIG_MB_SLAVE_IP_FROM_STDIN + "FROM_STDIN", // Address corresponds to MB_DEVICE_ADDR1 and set to predefined value by user + "FROM_STDIN", // Address corresponds to MB_DEVICE_ADDR2 and set to predefined value by user + "FROM_STDIN", // Address corresponds to MB_DEVICE_ADDR3 and set to predefined value by user + NULL // End of table condition (must be included) +#elif CONFIG_MB_MDNS_IP_RESOLVER + // This is workaround for the test to use the same slave for all CIDs and ignore UID setting in the slave + "01;mb_slave_tcp_01;1502", + "02;mb_slave_tcp_01;1502", + "03;mb_slave_tcp_01;1502", + NULL // End of table condition (must be included) +#endif +}; + +const size_t ip_table_sz = (size_t)(sizeof(slave_ip_address_table) / sizeof(slave_ip_address_table[0])); + +#if CONFIG_MB_SLAVE_IP_FROM_STDIN + +// Scan IP address according to IPV settings +char *master_scan_addr(int *index, char *buffer) +{ + char *ip_str = NULL; + int a[8] = {0}; + int buf_cnt = 0; +#if !CONFIG_EXAMPLE_CONNECT_IPV6 + buf_cnt = sscanf(buffer, "IP%d=" IPSTR, index, &a[0], &a[1], &a[2], &a[3]); + if (buf_cnt == 5) { + if (-1 == asprintf(&ip_str, "%02x;" IPSTR, (int)(*index + 1), a[0], a[1], a[2], a[3])) { + abort(); + } + } +#else + buf_cnt = sscanf(buffer, "IP%d="IPV6STR, index, &a[0], &a[1], &a[2], &a[3], &a[4], &a[5], &a[6], &a[7]); + if (buf_cnt == 9) { + if (-1 == asprintf(&ip_str, "%02x;" IPV6STR, (int)(*index + 1), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])) { + abort(); + } + } +#endif + printf("IP string: %s", ip_str); + return ip_str; +} + +static int master_get_slave_ip_stdin(char **addr_table) +{ + char buf[128]; + int index; + char *ip_str = NULL; + int buf_cnt = 0; + int ip_cnt = 0; + + if (!addr_table) { + return 0; + } + + ESP_ERROR_CHECK(example_configure_stdin_stdout()); + while(1) { + if (addr_table[ip_cnt] && strcmp(addr_table[ip_cnt], "FROM_STDIN") == 0) { + printf("Waiting IP%d from stdin:\r\n", (int)ip_cnt); + while (fgets(buf, sizeof(buf), stdin) == NULL) { + fputs(buf, stdout); + } + buf_cnt = strlen(buf); + buf[buf_cnt - 1] = '\0'; + fputc('\n', stdout); + ip_str = master_scan_addr(&index, buf); + if (ip_str != NULL) { + ESP_LOGI(TAG, "IP(%d) = [%s] set from stdin.", (int)ip_cnt, ip_str); + if ((ip_cnt >= ip_table_sz) || (index != ip_cnt)) { + addr_table[ip_cnt] = NULL; + break; + } + addr_table[ip_cnt++] = ip_str; + } else { + // End of configuration + addr_table[ip_cnt++] = NULL; + break; + } + } else { + if (addr_table[ip_cnt]) { + ESP_LOGI(TAG, "Leave IP(%d) = [%s] set manually.", (int)ip_cnt, addr_table[ip_cnt]); + ip_cnt++; + } else { + ESP_LOGI(TAG, "IP(%d) is not set in the table.", (int)ip_cnt); + break; + } + } + } + return ip_cnt; +} + +#endif + +static void master_destroy_slave_list(char **table, size_t ip_table_size) +{ + for (int i = 0; ((i < ip_table_size) && table[i] != NULL); i++) { + if (table[i]) { +#if CONFIG_MB_SLAVE_IP_FROM_STDIN + free(table[i]); + table[i] = "FROM_STDIN"; +#elif CONFIG_MB_MDNS_IP_RESOLVER + table[i] = NULL; +#endif + } + } +} + +// The function to get pointer to parameter storage (instance) according to parameter description table +static void *master_get_param_data(const mb_parameter_descriptor_t *param_descriptor) +{ + assert(param_descriptor != NULL); + void *instance_ptr = NULL; + if (param_descriptor->param_offset != 0) { + switch(param_descriptor->mb_param_type) + { + case MB_PARAM_HOLDING: + instance_ptr = ((void *)&holding_reg_params + param_descriptor->param_offset - 1); + break; + case MB_PARAM_INPUT: + instance_ptr = ((void *)&input_reg_params + param_descriptor->param_offset - 1); + break; + case MB_PARAM_COIL: + instance_ptr = ((void *)&coil_reg_params + param_descriptor->param_offset - 1); + break; + case MB_PARAM_DISCRETE: + instance_ptr = ((void *)&discrete_reg_params + param_descriptor->param_offset - 1); + break; + default: + instance_ptr = NULL; + break; + } + } else { + ESP_LOGE(TAG, "Wrong parameter offset for CID #%u", param_descriptor->cid); + assert(instance_ptr != NULL); + } + return instance_ptr; +} + +#define TEST_VERIFY_VALUES(handle, pdescr, pinst) (__extension__( \ +{ \ + assert(pinst); \ + assert(pdescr); \ + uint8_t type = 0; \ + esp_err_t err = ESP_FAIL; \ + err = mbc_master_get_parameter(handle, pdescr->cid, (uint8_t *)pinst, &type); \ + if (err == ESP_OK) { \ + bool is_correct = true; \ + if (pdescr->param_opts.opt3) { \ + for EACH_ITEM(pinst, pdescr->param_size / sizeof(*pitem)) { \ + if (*pitem != (typeof(*(pinst)))pdescr->param_opts.opt3) { \ + *pitem = (typeof(*(pinst)))pdescr->param_opts.opt3; \ + ESP_LOGD(TAG, "Characteristic #%d (%s), initialize to 0x%" PRIx16 ".", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (uint16_t)pdescr->param_opts.opt3); \ + is_correct = false; \ + } \ + } \ + } \ + if (!is_correct) { \ + ESP_LOGE(TAG, "Characteristic #%d (%s), initialize.", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key); \ + err = mbc_master_set_parameter(handle, cid, (uint8_t *)pinst, &type); \ + if (err != ESP_OK) { \ + ESP_LOGE(TAG, "Characteristic #%d (%s) write fail, err = 0x%x (%s).", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (int)err, \ + (char *)esp_err_to_name(err)); \ + } else { \ + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (..) write successful.", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (char *)pdescr->param_units); \ + } \ + } \ + } else { \ + ESP_LOGE(TAG, "Characteristic #%d (%s) read fail, err = 0x%x (%s).", \ + (int)pdescr->cid, \ + (char *)pdescr->param_key, \ + (int)err, \ + (char *)esp_err_to_name(err)); \ + } \ + (err); \ +} \ +)) + +// User operation function to read slave values and check alarm +static void master_operation_func(void *arg) +{ + esp_err_t err = ESP_OK; + bool alarm_state = false; + const mb_parameter_descriptor_t *param_descriptor = NULL; + + ESP_LOGI(TAG, "Start modbus test..."); + + for(uint16_t retry = 0; retry <= MASTER_MAX_RETRY && (!alarm_state); retry++) { + // Read all found characteristics from slave(s) + for (uint16_t cid = 0; (err != ESP_ERR_NOT_FOUND) && cid < MASTER_MAX_CIDS; cid++) { + // Get data from parameters description table + // and use this information to fill the characteristics description table + // and having all required fields in just one table + err = mbc_master_get_cid_info(master_handle, cid, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) { + void *temp_data_ptr = master_get_param_data(param_descriptor); + assert(temp_data_ptr); + if ((param_descriptor->param_type == PARAM_TYPE_ASCII) && + (param_descriptor->cid == CID_HOLD_TEST_REG)) { + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint32_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint32_t *)temp_data_ptr); + } +#if CONFIG_FMB_EXT_TYPE_SUPPORT + } else if ((param_descriptor->cid >= CID_HOLD_U16_AB) + && (param_descriptor->cid <= CID_HOLD_U16_BA)) { + // Check the uint16 parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint16_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%" PRIx16 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint16_t *)temp_data_ptr); + } + } else if ((param_descriptor->cid >= CID_HOLD_U8_A) + && (param_descriptor->cid <= CID_HOLD_U8_B)) { + // Check the uint8 parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint16_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%" PRIx16 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint16_t *)temp_data_ptr); + } + } else if ((param_descriptor->cid >= CID_HOLD_UINT32_ABCD) + && (param_descriptor->cid <= CID_HOLD_UINT32_DCBA)) { + // Check the uint32 parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint32_t *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %" PRIu32 " (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(uint32_t *)temp_data_ptr, + *(uint32_t *)temp_data_ptr); + } + } else if ((param_descriptor->cid >= CID_HOLD_FLOAT_ABCD) + && (param_descriptor->cid <= CID_HOLD_FLOAT_DCBA)) { + // Check the float parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (float *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %f (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(float *)temp_data_ptr, + *(uint32_t *)temp_data_ptr); + } + } else if (param_descriptor->cid >= CID_HOLD_DOUBLE_ABCDEFGH) { + // Check the double parameters + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (double *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %lf (0x%" PRIx64 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(double *)temp_data_ptr, + *(uint64_t *)temp_data_ptr); + } +#endif + } else if (cid <= CID_HOLD_DATA_2) { + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (float *)temp_data_ptr) == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %f (0x%" PRIx32 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + *(float *)temp_data_ptr, + *(uint32_t *)temp_data_ptr); + } + float value = *(float *)temp_data_ptr; + if (((value > param_descriptor->param_opts.max) || + (value < param_descriptor->param_opts.min))) { + alarm_state = true; + break; + } + } else if ((cid >= CID_RELAY_P1) && (cid <= CID_DISCR_P1)) { + if (TEST_VERIFY_VALUES(master_handle, param_descriptor, (uint8_t *)temp_data_ptr) == ESP_OK) { + uint8_t state = *(uint8_t *)temp_data_ptr; + const char *rw_str = (state & param_descriptor->param_opts.opt1) ? "ON" : "OFF"; + if ((state & param_descriptor->param_opts.opt2) == param_descriptor->param_opts.opt2) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = %s (0x%" PRIx8 ") read successful.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + (const char *)rw_str, + *(uint8_t *)temp_data_ptr); + } else { + ESP_LOGE(TAG, "Characteristic #%d %s (%s) value = %s (0x%" PRIx8 "), unexpected value.", + (int)param_descriptor->cid, + (char *)param_descriptor->param_key, + (char *)param_descriptor->param_units, + (const char *)rw_str, + *(uint8_t *)temp_data_ptr); + alarm_state = true; + break; + } + if (state & param_descriptor->param_opts.opt1) { + alarm_state = true; + break; + } + } + } + vTaskDelay(POLL_TIMEOUT_TICS); // timeout between polls + } + } + vTaskDelay(UPDATE_CIDS_TIMEOUT_TICS); + } + + if (alarm_state) { + ESP_LOGI(TAG, "Alarm triggered by cid #%u.", param_descriptor->cid); + } else { + ESP_LOGE(TAG, "Alarm is not triggered after %u retries.", + MASTER_MAX_RETRY); + } + ESP_LOGI(TAG, "Destroy master..."); + vTaskDelay(1); +} + +static esp_err_t init_services(mb_tcp_addr_type_t ip_addr_type) +{ + esp_err_t result = nvs_flash_init(); + if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + result = nvs_flash_init(); + } + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "nvs_flash_init fail, returns(0x%x).", + (int)result); + result = esp_netif_init(); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_netif_init fail, returns(0x%x).", + (int)result); + result = esp_event_loop_create_default(); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_event_loop_create_default fail, returns(0x%x).", + (int)result); + // This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + // Read "Establishing Wi-Fi or Ethernet Connection" section in + // examples/protocols/README.md for more information about this function. + result = example_connect(); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "example_connect fail, returns(0x%x).", + (int)result); +#if CONFIG_EXAMPLE_CONNECT_WIFI + result = esp_wifi_set_ps(WIFI_PS_NONE); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_wifi_set_ps fail, returns(0x%x).", + (int)result); +#endif + +#if CONFIG_MB_SLAVE_IP_FROM_STDIN + int ip_cnt = master_get_slave_ip_stdin(slave_ip_address_table); + if (ip_cnt) { + ESP_LOGI(TAG, "Configured %d IP addresse(s).", ip_cnt); + } else { + ESP_LOGE(TAG, "Fail to get IP address from stdin. Continue."); + return ESP_ERR_NOT_FOUND; + } +#endif + return ESP_OK; +} + +static esp_err_t destroy_services(void) +{ + esp_err_t err = ESP_OK; + master_destroy_slave_list(slave_ip_address_table, ip_table_sz); + + err = example_disconnect(); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "example_disconnect fail, returns(0x%x).", + (int)err); + err = esp_event_loop_delete_default(); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_event_loop_delete_default fail, returns(0x%x).", + (int)err); + err = esp_netif_deinit(); + MB_RETURN_ON_FALSE((err == ESP_OK || err == ESP_ERR_NOT_SUPPORTED), ESP_ERR_INVALID_STATE, + TAG, + "esp_netif_deinit fail, returns(0x%x).", + (int)err); + err = nvs_flash_deinit(); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "nvs_flash_deinit fail, returns(0x%x).", + (int)err); + return err; +} + +// Modbus master initialization +static esp_err_t master_init(mb_communication_info_t *pcomm_info) +{ + esp_err_t err = mbc_master_create_tcp(pcomm_info, &master_handle); + MB_RETURN_ON_FALSE((master_handle != NULL), ESP_ERR_INVALID_STATE, + TAG, + "mb controller initialization fail."); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mb controller initialization fail, returns(0x%x).", + (int)err); + + err = mbc_master_set_descriptor(master_handle, &device_parameters[0], num_device_parameters); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mb controller set descriptor fail, returns(0x%x).", + (int)err); + ESP_LOGI(TAG, "Modbus master stack initialized..."); + + err = mbc_master_start(master_handle); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mb controller start fail, returns(0x%x).", + (int)err); + vTaskDelay(5); + return err; +} + +static esp_err_t master_destroy(void) +{ + esp_err_t err = mbc_master_delete(master_handle); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_master_destroy fail, returns(0x%x).", + (int)err); + ESP_LOGI(TAG, "Modbus master stack destroy..."); + return err; +} + +void app_main(void) +{ + mb_tcp_addr_type_t ip_addr_type; +#if !CONFIG_EXAMPLE_CONNECT_IPV6 + ip_addr_type = MB_IPV4; +#else + ip_addr_type = MB_IPV6; +#endif + ESP_ERROR_CHECK(init_services(ip_addr_type)); + + mb_communication_info_t tcp_master_config = { + .tcp_opts.port = MB_TCP_PORT, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = ip_addr_type, + .tcp_opts.ip_addr_table = (void *)slave_ip_address_table, + .tcp_opts.uid = 0, + .tcp_opts.start_disconnected = false, + .tcp_opts.response_tout_ms = CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND, + .tcp_opts.ip_netif_ptr = (void*)get_example_netif() + }; + + ESP_ERROR_CHECK(master_init(&tcp_master_config)); + + master_operation_func(NULL); + ESP_ERROR_CHECK(master_destroy()); + ESP_ERROR_CHECK(destroy_services()); +} diff --git a/examples/tcp/mb_tcp_master/sdkconfig.ci.ethernet b/examples/tcp/mb_tcp_master/sdkconfig.ci.ethernet new file mode 100644 index 0000000..fed2f1a --- /dev/null +++ b/examples/tcp/mb_tcp_master/sdkconfig.ci.ethernet @@ -0,0 +1,33 @@ +# +# Modbus configuration +# +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=1502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_SLAVE_IP_FROM_STDIN=y +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y + +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y +CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_MDC_GPIO=23 +CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 +CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 +CONFIG_EXAMPLE_ETH_PHY_ADDR=1 +CONFIG_EXAMPLE_ETHERNET_EMAC_TASK_STACK_SIZE=4096 + +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y +CONFIG_ETH_USE_SPI_ETHERNET=n diff --git a/examples/tcp/mb_tcp_master/sdkconfig.ci.wifi b/examples/tcp/mb_tcp_master/sdkconfig.ci.wifi new file mode 100644 index 0000000..91b458a --- /dev/null +++ b/examples/tcp/mb_tcp_master/sdkconfig.ci.wifi @@ -0,0 +1,20 @@ +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=1502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_MB_SLAVE_IP_FROM_STDIN=y +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y + +CONFIG_EXAMPLE_CONNECT_ETHERNET=n +CONFIG_EXAMPLE_CONNECT_WIFI=y +CONFIG_EXAMPLE_WIFI_SSID="${CI_WIFI_SSID}" +CONFIG_EXAMPLE_WIFI_PASSWORD="${CI_WIFI_PASSW}" diff --git a/examples/tcp/mb_tcp_master/sdkconfig.defaults b/examples/tcp/mb_tcp_master/sdkconfig.defaults new file mode 100644 index 0000000..df0cb06 --- /dev/null +++ b/examples/tcp/mb_tcp_master/sdkconfig.defaults @@ -0,0 +1,22 @@ +# +# Modbus configuration +# +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=1502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_MB_SLAVE_IP_FROM_STDIN=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_EXAMPLE_CONNECT_ETHERNET=n +CONFIG_EXAMPLE_CONNECT_WIFI=y +CONFIG_EXAMPLE_WIFI_SSID="${CI_WIFI_SSID}" +CONFIG_EXAMPLE_WIFI_PASSWORD="${CI_WIFI_PASSW}" + diff --git a/examples/tcp/mb_tcp_slave/CMakeLists.txt b/examples/tcp/mb_tcp_slave/CMakeLists.txt new file mode 100644 index 0000000..62531d1 --- /dev/null +++ b/examples/tcp/mb_tcp_slave/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +# Exclude old component feemodbus which exists in old versions +set(EXCLUDE_COMPONENTS freemodbus) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(modbus_tcp_slave) diff --git a/examples/tcp/mb_tcp_slave/README.md b/examples/tcp/mb_tcp_slave/README.md new file mode 100644 index 0000000..391bb89 --- /dev/null +++ b/examples/tcp/mb_tcp_slave/README.md @@ -0,0 +1,85 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | + +# Modbus Slave Example +The stack located in `modbus` folder and includes support for ESP32 target chips. There are some parameters that can be configured in KConfig file to start stack correctly (See description below for more information). The external Modbus host is able to read/write device parameters using Modbus protocol transport. The parameters accessible thorough Modbus are located in `mb_example_common/modbus_params.h\c` files and can be updated by user. +These are represented in structures holding_reg_params, input_reg_params, coil_reg_params, discrete_reg_params for holding registers, input parameters, coils and discrete inputs accordingly. The app_main application demonstrates how to setup Modbus stack and use notifications about parameters change from host system. + + +The slave example uses shared parameter structures defined in ```examples/protocols/modbus/mb_example_common``` folder. + +## Hardware required : +Option 1: +The ESP32 based development board flashed with modbus_tcp_slave example + external Modbus master host software. + +Option 2: +The modbus_tcp_master example application configured as described in its README.md file and flashed into ESP32 based board. +Note: The ```Example Data (Object) Dictionary``` in the modbus_tcp_master example can be edited to address parameters from other slaves connected into Modbus segment. + +## How to setup and use an example: + +### Configure the application +Start the command below to show the configuration menu: +``` +idf.py menuconfig +``` + +To configure the example to use Wi-Fi or Ethernet connection, open the project configuration menu and navigate to "Example Connection Configuration" menu. Select either "Wi-Fi" or "Ethernet" in the "Connect using" choice. +Follow the instructions in `examples/common_components/protocol_examples_common` for further configuration. + +The communication parameters of esp-modbus stack (Component config->Modbus configuration) allow to configure it appropriately but usually it is enough to use default settings. +See the help strings of parameters for more information. + +### Setup external Modbus master software +Option 1: +Configure the external Modbus master software according to port configuration parameters used in application. +As an example the Modbus Poll application can be used with this example. +Option 2: +Setup ESP32 based development board and set modbus_tcp_master example configuration as described in its README.md file. +Setup one or more slave boards and connect them into the same Modbus segment (See configuration above). + +### Build and flash software +Build the project and flash it to the board, then run monitor tool to view serial output: +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output +Example output of the application: +``` +I (4235) esp_netif_handlers: example_connect: sta ip: 192.168.1.21, mask: 255.255.255.0, gw: 192.168.1.1 +I (4235) example_connect: Got IPv4 event: Interface "example_connect: sta" address: 192.168.1.21 +I (4465) example_connect: Got IPv6 event: Interface "example_connect: sta" address: fe80:0000:0000:0000:7edf:a1ff:fe00:4039, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (4465) example_connect: Connected to example_connect: sta +I (4475) example_connect: - IPv4 address: 192.168.1.21 +I (4475) example_connect: - IPv6 address: fe80:0000:0000:0000:7edf:a1ff:fe00:4039, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (4495) MB_TCP_SLAVE_PORT: Socket (#54), listener on port: 502, errno=0 +I (4495) MB_TCP_SLAVE_PORT: Protocol stack initialized. +I (4505) SLAVE_TEST: Modbus slave stack initialized. +I (4505) SLAVE_TEST: Start modbus test... +I (41035) MB_TCP_SLAVE_PORT: Socket (#55), accept client connection from address: 192.168.1.39 +I (41225) SLAVE_TEST: INPUT READ (41704766 us), ADDR:1, TYPE:8, INST_ADDR:0x3ffcb878, SIZE:2 +I (41235) SLAVE_TEST: HOLDING READ (41719746 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffcb9b4, SIZE:2 +I (41255) SLAVE_TEST: INPUT READ (41732965 us), ADDR:3, TYPE:8, INST_ADDR:0x3ffcb87c, SIZE:2 +I (41265) SLAVE_TEST: HOLDING READ (41745923 us), ADDR:3, TYPE:2, INST_ADDR:0x3ffcb9b8, SIZE:2 +I (41275) SLAVE_TEST: INPUT READ (41759563 us), ADDR:5, TYPE:8, INST_ADDR:0x3ffcb880, SIZE:2 +I (41295) SLAVE_TEST: HOLDING READ (41772568 us), ADDR:5, TYPE:2, INST_ADDR:0x3ffcb9bc, SIZE:2 +I (41305) SLAVE_TEST: COILS WRITE (41785889 us), ADDR:0, TYPE:16, INST_ADDR:0x3ffcb874, SIZE:8 +I (41315) SLAVE_TEST: COILS WRITE (41799175 us), ADDR:8, TYPE:16, INST_ADDR:0x3ffcb875, SIZE:8 +I (41945) SLAVE_TEST: INPUT READ (42421629 us), ADDR:1, TYPE:8, INST_ADDR:0x3ffcb878, SIZE:2 +I (42145) SLAVE_TEST: HOLDING READ (42626497 us), ADDR:1, TYPE:2, INST_ADDR:0x3ffcb9b4, SIZE:2 +I (42345) SLAVE_TEST: INPUT READ (42831315 us), ADDR:3, TYPE:8, INST_ADDR:0x3ffcb87c, SIZE:2 +I (42555) SLAVE_TEST: HOLDING READ (43036111 us), ADDR:3, TYPE:2, INST_ADDR:0x3ffcb9b8, SIZE:2 +I (42755) SLAVE_TEST: INPUT READ (43240950 us), ADDR:5, TYPE:8, INST_ADDR:0x3ffcb880, SIZE:2 +I (42865) SLAVE_TEST: HOLDING READ (43343204 us), ADDR:5, TYPE:2, INST_ADDR:0x3ffcb9bc, SIZE:2 +...... +I (81265) SLAVE_TEST: HOLDING READ (81743698 us), ADDR:5, TYPE:2, INST_ADDR:0x3ffcb9bc, SIZE:2 +I (81465) SLAVE_TEST: COILS WRITE (81948482 us), ADDR:0, TYPE:16, INST_ADDR:0x3ffcb874, SIZE:8 +I (81465) SLAVE_TEST: Modbus controller destroyed. +``` +The output lines describe type of operation, its timestamp, modbus address, access type, storage address in parameter structure and number of registers accordingly. + diff --git a/examples/tcp/mb_tcp_slave/main/CMakeLists.txt b/examples/tcp/mb_tcp_slave/main/CMakeLists.txt new file mode 100644 index 0000000..1cf5f19 --- /dev/null +++ b/examples/tcp/mb_tcp_slave/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(PROJECT_NAME "modbus_tcp_slave") + +idf_component_register(SRCS "tcp_slave.c" + INCLUDE_DIRS ".") diff --git a/examples/tcp/mb_tcp_slave/main/Kconfig.projbuild b/examples/tcp/mb_tcp_slave/main/Kconfig.projbuild new file mode 100644 index 0000000..1e8160e --- /dev/null +++ b/examples/tcp/mb_tcp_slave/main/Kconfig.projbuild @@ -0,0 +1,12 @@ +menu "Modbus Example Configuration" + + config MB_SLAVE_ADDR + int "Modbus slave address" + range 1 247 if !FMB_TCP_UID_ENABLED + range 0 247 if FMB_TCP_UID_ENABLED + default 1 + help + This is the Modbus slave address in the network. + The address is used as an index to resolve slave ip address. + +endmenu diff --git a/examples/tcp/mb_tcp_slave/main/idf_component.yml b/examples/tcp/mb_tcp_slave/main/idf_component.yml new file mode 100644 index 0000000..aece724 --- /dev/null +++ b/examples/tcp/mb_tcp_slave/main/idf_component.yml @@ -0,0 +1,11 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../../" + espressif/mdns: + version: "^1.0.0" + mb_example_common: + path: "../../../mb_example_common" + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common diff --git a/examples/tcp/mb_tcp_slave/main/tcp_slave.c b/examples/tcp/mb_tcp_slave/main/tcp_slave.c new file mode 100644 index 0000000..26d0361 --- /dev/null +++ b/examples/tcp/mb_tcp_slave/main/tcp_slave.c @@ -0,0 +1,415 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// FreeModbus Slave Example ESP32 + +#include +#include "esp_err.h" +#include "sdkconfig.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "mdns.h" +#include "esp_netif.h" + +#if __has_include("esp_mac.h") +#include "esp_mac.h" +#endif + +#include "protocol_examples_common.h" + +#include "mbcontroller.h" // for mbcontroller defines and api +#include "modbus_params.h" // for modbus parameters structures + +#define MB_TCP_PORT_NUMBER (CONFIG_FMB_TCP_PORT_DEFAULT) + +// Defines below are used to define register start address for each type of Modbus registers +#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) >> 1)) +#define INPUT_OFFSET(field) ((uint16_t)(offsetof(input_reg_params_t, field) >> 1)) +#define MB_REG_DISCRETE_INPUT_START (0x0000) +#define MB_REG_COILS_START (0x0000) +#define MB_REG_INPUT_START_AREA0 (INPUT_OFFSET(input_data0)) // register offset input area 0 +#define MB_REG_INPUT_START_AREA1 (INPUT_OFFSET(input_data4)) // register offset input area 1 +#define MB_REG_HOLDING_START_AREA0 (HOLD_OFFSET(holding_data0)) +#define MB_REG_HOLDING_START_AREA0_SIZE ((size_t)((HOLD_OFFSET(holding_data4) - HOLD_OFFSET(holding_data0)) << 1)) +#define MB_REG_HOLDING_START_AREA1 (HOLD_OFFSET(holding_data4)) +#define MB_REG_HOLDING_START_AREA1_SIZE ((size_t)((HOLD_OFFSET(holding_area1_end) - HOLD_OFFSET(holding_data4)) << 1)) +#define MB_REG_HOLDING_START_AREA2 (HOLD_OFFSET(holding_u8_a)) +#define MB_REG_HOLDING_START_AREA2_SIZE ((size_t)((HOLD_OFFSET(holding_area2_end) - HOLD_OFFSET(holding_u8_a)) << 1)) + +#define MB_PAR_INFO_GET_TOUT (10) // Timeout for get parameter info +#define MB_CHAN_DATA_MAX_VAL (10) +#define MB_CHAN_DATA_OFFSET (1.1f) + +#define MB_READ_MASK (MB_EVENT_INPUT_REG_RD \ + | MB_EVENT_HOLDING_REG_RD \ + | MB_EVENT_DISCRETE_RD \ + | MB_EVENT_COILS_RD) +#define MB_WRITE_MASK (MB_EVENT_HOLDING_REG_WR \ + | MB_EVENT_COILS_WR) +#define MB_READ_WRITE_MASK (MB_READ_MASK | MB_WRITE_MASK) +#define MB_TEST_VALUE (12345.0) +#define MB_SLAVE_ADDR (CONFIG_MB_SLAVE_ADDR) + +static const char *TAG = "SLAVE_TEST"; + +static void *slave_handle = NULL; + +// Set register values into known state +static void setup_reg_data(void) +{ + // Define initial state of parameters + discrete_reg_params.discrete_input0 = 1; + discrete_reg_params.discrete_input1 = 0; + discrete_reg_params.discrete_input2 = 1; + discrete_reg_params.discrete_input3 = 0; + discrete_reg_params.discrete_input4 = 1; + discrete_reg_params.discrete_input5 = 0; + discrete_reg_params.discrete_input6 = 1; + discrete_reg_params.discrete_input7 = 0; + + holding_reg_params.holding_data0 = 1.34; + holding_reg_params.holding_data1 = 2.56; + holding_reg_params.holding_data2 = 3.78; + holding_reg_params.holding_data3 = 4.90; + + holding_reg_params.holding_data4 = 5.67; + holding_reg_params.holding_data5 = 6.78; + holding_reg_params.holding_data6 = 7.79; + holding_reg_params.holding_data7 = 8.80; + +#if CONFIG_FMB_EXT_TYPE_SUPPORT + mb_set_uint8_a((val_16_arr *)&holding_reg_params.holding_u8_a[0], (uint8_t)0x55); + mb_set_uint8_a((val_16_arr *)&holding_reg_params.holding_u8_a[1], (uint8_t)0x55); + mb_set_uint8_b((val_16_arr *)&holding_reg_params.holding_u8_b[0], (uint8_t)0x55); + mb_set_uint8_b((val_16_arr *)&holding_reg_params.holding_u8_b[1], (uint8_t)0x55); + mb_set_uint16_ab((val_16_arr *)&holding_reg_params.holding_u16_ab[1], (uint16_t)MB_TEST_VALUE); + mb_set_uint16_ab((val_16_arr *)&holding_reg_params.holding_u16_ab[0], (uint16_t)MB_TEST_VALUE); + mb_set_uint16_ba((val_16_arr *)&holding_reg_params.holding_u16_ba[0], (uint16_t)MB_TEST_VALUE); + mb_set_uint16_ba((val_16_arr *)&holding_reg_params.holding_u16_ba[1], (uint16_t)MB_TEST_VALUE); + + mb_set_float_abcd((val_32_arr *)&holding_reg_params.holding_float_abcd[0], (float)MB_TEST_VALUE); + mb_set_float_abcd((val_32_arr *)&holding_reg_params.holding_float_abcd[1], (float)MB_TEST_VALUE); + mb_set_float_cdab((val_32_arr *)&holding_reg_params.holding_float_cdab[0], (float)MB_TEST_VALUE); + mb_set_float_cdab((val_32_arr *)&holding_reg_params.holding_float_cdab[1], (float)MB_TEST_VALUE); + mb_set_float_badc((val_32_arr *)&holding_reg_params.holding_float_badc[0], (float)MB_TEST_VALUE); + mb_set_float_badc((val_32_arr *)&holding_reg_params.holding_float_badc[1], (float)MB_TEST_VALUE); + mb_set_float_dcba((val_32_arr *)&holding_reg_params.holding_float_dcba[0], (float)MB_TEST_VALUE); + mb_set_float_dcba((val_32_arr *)&holding_reg_params.holding_float_dcba[1], (float)MB_TEST_VALUE); + + mb_set_uint32_abcd((val_32_arr *)&holding_reg_params.holding_uint32_abcd[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_abcd((val_32_arr *)&holding_reg_params.holding_uint32_abcd[1], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_cdab((val_32_arr *)&holding_reg_params.holding_uint32_cdab[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_cdab((val_32_arr *)&holding_reg_params.holding_uint32_cdab[1], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_badc((val_32_arr *)&holding_reg_params.holding_uint32_badc[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_badc((val_32_arr *)&holding_reg_params.holding_uint32_badc[1], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_dcba((val_32_arr *)&holding_reg_params.holding_uint32_dcba[0], (uint32_t)MB_TEST_VALUE); + mb_set_uint32_dcba((val_32_arr *)&holding_reg_params.holding_uint32_dcba[1], (uint32_t)MB_TEST_VALUE); + + mb_set_double_abcdefgh((val_64_arr *)&holding_reg_params.holding_double_abcdefgh[0], (double)MB_TEST_VALUE); + mb_set_double_abcdefgh((val_64_arr *)&holding_reg_params.holding_double_abcdefgh[1], (double)MB_TEST_VALUE); + mb_set_double_hgfedcba((val_64_arr *)&holding_reg_params.holding_double_hgfedcba[0], (double)MB_TEST_VALUE); + mb_set_double_hgfedcba((val_64_arr *)&holding_reg_params.holding_double_hgfedcba[1], (double)MB_TEST_VALUE); + mb_set_double_ghefcdab((val_64_arr *)&holding_reg_params.holding_double_ghefcdab[0], (double)MB_TEST_VALUE); + mb_set_double_ghefcdab((val_64_arr *)&holding_reg_params.holding_double_ghefcdab[1], (double)MB_TEST_VALUE); + mb_set_double_badcfehg((val_64_arr *)&holding_reg_params.holding_double_badcfehg[0], (double)MB_TEST_VALUE); + mb_set_double_badcfehg((val_64_arr *)&holding_reg_params.holding_double_badcfehg[1], (double)MB_TEST_VALUE); +#endif + + coil_reg_params.coils_port0 = 0x55; + coil_reg_params.coils_port1 = 0xAA; + + input_reg_params.input_data0 = 1.12; + input_reg_params.input_data1 = 2.34; + input_reg_params.input_data2 = 3.56; + input_reg_params.input_data3 = 4.78; + input_reg_params.input_data4 = 1.12; + input_reg_params.input_data5 = 2.34; + input_reg_params.input_data6 = 3.56; + input_reg_params.input_data7 = 4.78; +} + +static void slave_operation_func(void *arg) +{ + mb_param_info_t reg_info; // keeps the Modbus registers access information + + ESP_LOGI(TAG, "Modbus slave stack initialized."); + ESP_LOGI(TAG, "Start modbus test..."); + // The cycle below will be terminated when parameter holding_data0 + // incremented each access cycle reaches the CHAN_DATA_MAX_VAL value. + for(;holding_reg_params.holding_data0 < MB_CHAN_DATA_MAX_VAL;) { + // Check for read/write events of Modbus master for certain events + (void)mbc_slave_check_event(slave_handle, MB_READ_WRITE_MASK); + ESP_ERROR_CHECK_WITHOUT_ABORT(mbc_slave_get_param_info(slave_handle, ®_info, MB_PAR_INFO_GET_TOUT)); + const char* rw_str = (reg_info.type & MB_READ_MASK) ? "READ" : "WRITE"; + // Filter events and process them accordingly + if(reg_info.type & (MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD)) { + // Get parameter information from parameter queue + ESP_LOGI(TAG, "HOLDING %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", + rw_str, + (unsigned)reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (int)reg_info.address, + (unsigned)reg_info.size); + if (reg_info.address == (uint8_t*)&holding_reg_params.holding_data0) + { + (void)mbc_slave_unlock(slave_handle); + holding_reg_params.holding_data0 += MB_CHAN_DATA_OFFSET; + if (holding_reg_params.holding_data0 >= (MB_CHAN_DATA_MAX_VAL - MB_CHAN_DATA_OFFSET)) { + coil_reg_params.coils_port1 = 0xFF; + ESP_LOGI(TAG, "Riched maximum value"); + } + (void)mbc_slave_unlock(slave_handle); + } + } else if (reg_info.type & MB_EVENT_INPUT_REG_RD) { + ESP_LOGI(TAG, "INPUT READ (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + } else if (reg_info.type & MB_EVENT_DISCRETE_RD) { + ESP_LOGI(TAG, "DISCRETE READ (%" PRIu32 " us): ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + } else if (reg_info.type & (MB_EVENT_COILS_RD | MB_EVENT_COILS_WR)) { + ESP_LOGI(TAG, "COILS %s (%" PRIu32 " us), ADDR:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 ", SIZE:%u", + rw_str, + reg_info.time_stamp, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + (unsigned)reg_info.size); + if (coil_reg_params.coils_port1 == 0xFF) { + ESP_LOGI(TAG, "Stop polling."); + break; + } + } + } + // Destroy of Modbus controller on alarm + ESP_LOGI(TAG,"Modbus controller destroyed."); + vTaskDelay(100); +} + +static esp_err_t init_services(void) +{ + esp_err_t result = nvs_flash_init(); + if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + result = nvs_flash_init(); + } + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "nvs_flash_init fail, returns(0x%x).", + (int)result); + result = esp_netif_init(); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_netif_init fail, returns(0x%x).", + (int)result); + result = esp_event_loop_create_default(); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_event_loop_create_default fail, returns(0x%x).", + (int)result); + // This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + // Read "Establishing Wi-Fi or Ethernet Connection" section in + // examples/protocols/README.md for more information about this function. + result = example_connect(); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "example_connect fail, returns(0x%x).", + (int)result); +#if CONFIG_EXAMPLE_CONNECT_WIFI + result = esp_wifi_set_ps(WIFI_PS_NONE); + MB_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_wifi_set_ps fail, returns(0x%x).", + (int)result); +#endif + return ESP_OK; +} + +static esp_err_t destroy_services(void) +{ + esp_err_t err = ESP_OK; + + err = example_disconnect(); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "example_disconnect fail, returns(0x%x).", + (int)err); + err = esp_event_loop_delete_default(); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_event_loop_delete_default fail, returns(0x%x).", + (int)err); + err = esp_netif_deinit(); + MB_RETURN_ON_FALSE((err == ESP_OK || err == ESP_ERR_NOT_SUPPORTED), ESP_ERR_INVALID_STATE, + TAG, + "esp_netif_deinit fail, returns(0x%x).", + (int)err); + err = nvs_flash_deinit(); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "nvs_flash_deinit fail, returns(0x%x).", + (int)err); + return err; +} + +// Modbus slave initialization +static esp_err_t slave_init(mb_communication_info_t *pcomm_info) +{ + mb_register_area_descriptor_t reg_area; // Modbus register area descriptor structure + + // Initialization of Modbus controller + esp_err_t err = mbc_slave_create_tcp(pcomm_info, &slave_handle); + MB_RETURN_ON_FALSE((err == ESP_OK && slave_handle != NULL), ESP_ERR_INVALID_STATE, + TAG, + "mb controller create fail."); + + // The code below initializes Modbus register area descriptors + // for Modbus Holding Registers, Input Registers, Coils and Discrete Inputs + // Initialization should be done for each supported Modbus register area according to register map. + // When external master trying to access the register in the area that is not initialized + // by mbc_slave_set_descriptor() API call then Modbus stack + // will send exception response for this register area. + reg_area.type = MB_PARAM_HOLDING; // Set type of register area + reg_area.start_offset = MB_REG_HOLDING_START_AREA0; // Offset of register area in Modbus protocol + reg_area.address = (void*)&holding_reg_params.holding_data0; // Set pointer to storage instance + reg_area.size = (MB_REG_HOLDING_START_AREA1 - MB_REG_HOLDING_START_AREA0) << 1; // Set the size of register storage instance + err = mbc_slave_set_descriptor(slave_handle, reg_area); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_set_descriptor fail, returns(0x%x).", + (int)err); + + reg_area.type = MB_PARAM_HOLDING; // Set type of register area + reg_area.start_offset = MB_REG_HOLDING_START_AREA1; // Offset of register area in Modbus protocol + reg_area.address = (void*)&holding_reg_params.holding_data4; // Set pointer to storage instance + reg_area.size = sizeof(float) << 2; // Set the size of register storage instance + err = mbc_slave_set_descriptor(slave_handle, reg_area); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_set_descriptor fail, returns(0x%x).", + (int)err); + +#if CONFIG_FMB_EXT_TYPE_SUPPORT + // The extended parameters register area + reg_area.type = MB_PARAM_HOLDING; + reg_area.start_offset = MB_REG_HOLDING_START_AREA2; + reg_area.address = (void*)&holding_reg_params.holding_u8_a; + reg_area.size = MB_REG_HOLDING_START_AREA2_SIZE; + err = mbc_slave_set_descriptor(slave_handle, reg_area); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_set_descriptor fail, returns(0x%x).", + (int)err); +#endif + + // Initialization of Input Registers area + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = MB_REG_INPUT_START_AREA0; + reg_area.address = (void*)&input_reg_params.input_data0; + reg_area.size = sizeof(float) << 2; + err = mbc_slave_set_descriptor(slave_handle, reg_area); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_set_descriptor fail, returns(0x%x).", + (int)err); + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = MB_REG_INPUT_START_AREA1; + reg_area.address = (void*)&input_reg_params.input_data4; + reg_area.size = sizeof(float) << 2; + err = mbc_slave_set_descriptor(slave_handle, reg_area); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_set_descriptor fail, returns(0x%x).", + (int)err); + + // Initialization of Coils register area + reg_area.type = MB_PARAM_COIL; + reg_area.start_offset = MB_REG_COILS_START; + reg_area.address = (void*)&coil_reg_params; + reg_area.size = sizeof(coil_reg_params); + err = mbc_slave_set_descriptor(slave_handle, reg_area); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_set_descriptor fail, returns(0x%x).", + (int)err); + + // Initialization of Discrete Inputs register area + reg_area.type = MB_PARAM_DISCRETE; + reg_area.start_offset = MB_REG_DISCRETE_INPUT_START; + reg_area.address = (void*)&discrete_reg_params; + reg_area.size = sizeof(discrete_reg_params); + err = mbc_slave_set_descriptor(slave_handle, reg_area); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_set_descriptor fail, returns(0x%x).", + (int)err); + + // Set values into known state + setup_reg_data(); + + // Starts of modbus controller and stack + err = mbc_slave_start(slave_handle); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_start fail, returns(0x%x).", + (int)err); + vTaskDelay(5); + return err; +} + +static esp_err_t slave_destroy(void) +{ + esp_err_t err = mbc_slave_delete(slave_handle); + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "mbc_slave_destroy fail, returns(0x%x).", + (int)err); + return err; +} + +// An example application of Modbus slave. It is based on esp-modbus stack. +// See deviceparams.h file for more information about assigned Modbus parameters. +// These parameters can be accessed from main application and also can be changed +// by external Modbus master host. +void app_main(void) +{ + ESP_ERROR_CHECK(init_services()); + + // Set UART log level + esp_log_level_set(TAG, ESP_LOG_INFO); + + mb_communication_info_t tcp_slave_config = { + .tcp_opts.port = MB_TCP_PORT_NUMBER, + .tcp_opts.mode = MB_TCP, +#if !CONFIG_EXAMPLE_CONNECT_IPV6 + .tcp_opts.addr_type = MB_IPV4, +#else + .tcp_opts.addr_type = MB_IPV6, +#endif + .tcp_opts.ip_addr_table = NULL, // Bind to any address + .tcp_opts.ip_netif_ptr = (void*)get_example_netif(), + .tcp_opts.uid = MB_SLAVE_ADDR + }; + + ESP_ERROR_CHECK(slave_init(&tcp_slave_config)); + + // The Modbus slave logic is located in this function (user handling of Modbus) + slave_operation_func(NULL); + + ESP_ERROR_CHECK(slave_destroy()); + ESP_ERROR_CHECK(destroy_services()); +} diff --git a/examples/tcp/mb_tcp_slave/sdkconfig.ci.ethernet b/examples/tcp/mb_tcp_slave/sdkconfig.ci.ethernet new file mode 100644 index 0000000..f3a0c7e --- /dev/null +++ b/examples/tcp/mb_tcp_slave/sdkconfig.ci.ethernet @@ -0,0 +1,32 @@ +# +# Modbus configuration +# +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=1502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_EXT_TYPE_SUPPORT=y +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_SLAVE_ADDR=1 +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y +CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_MDC_GPIO=23 +CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 +CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 +CONFIG_EXAMPLE_ETH_PHY_ADDR=1 +CONFIG_EXAMPLE_ETHERNET_EMAC_TASK_STACK_SIZE=4096 + +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y +CONFIG_ETH_USE_SPI_ETHERNET=n diff --git a/examples/tcp/mb_tcp_slave/sdkconfig.ci.wifi b/examples/tcp/mb_tcp_slave/sdkconfig.ci.wifi new file mode 100644 index 0000000..15966e8 --- /dev/null +++ b/examples/tcp/mb_tcp_slave/sdkconfig.ci.wifi @@ -0,0 +1,19 @@ +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=1502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_EXT_TYPE_SUPPORT=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_MB_SLAVE_ADDR=1 +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_EXAMPLE_CONNECT_ETHERNET=n +CONFIG_EXAMPLE_CONNECT_WIFI=y +CONFIG_EXAMPLE_WIFI_SSID="${CI_WIFI_SSID}" +CONFIG_EXAMPLE_WIFI_PASSWORD="${CI_WIFI_PASSW}" diff --git a/examples/tcp/mb_tcp_slave/sdkconfig.defaults b/examples/tcp/mb_tcp_slave/sdkconfig.defaults new file mode 100644 index 0000000..29ab85d --- /dev/null +++ b/examples/tcp/mb_tcp_slave/sdkconfig.defaults @@ -0,0 +1,21 @@ +# +# Modbus configuration +# +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=1502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_MB_SLAVE_ADDR=1 +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_EXAMPLE_WIFI_SSID="${CI_WIFI_SSID}" +CONFIG_EXAMPLE_WIFI_PASSWORD="${CI_WIFI_PASSW}" +CONFIG_EXAMPLE_CONNECT_ETHERNET=n +CONFIG_EXAMPLE_CONNECT_WIFI=y diff --git a/examples/tcp/pytest_mb_tcp_host_test_slave.py b/examples/tcp/pytest_mb_tcp_host_test_slave.py new file mode 100644 index 0000000..a99df5f --- /dev/null +++ b/examples/tcp/pytest_mb_tcp_host_test_slave.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# This is the script to reproduce the issue when the expect() is called from +# main thread in Multi DUT case. + +import logging +import os,sys +import subprocess + +# pytest required libraries +import pytest +from conftest import ModbusTestDut, Stages + + +TEST_DIR = os.path.abspath(os.path.dirname(__file__)) +TEST_ROBOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../tools/robot')) +LOG_LEVEL = logging.DEBUG +LOGGER_NAME = 'modbus_test' +logger = logging.getLogger(LOGGER_NAME) + +if os.name == 'nt': + CLOSE_FDS = False +else: + CLOSE_FDS = True + + +pattern_dict_slave = {Stages.STACK_IPV4: (r'I \([0-9]+\) example_[a-z]+: - IPv4 address: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_IPV6: (r'I \([0-9]+\) example_[a-z]+: - IPv6 address: (([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})'), + Stages.STACK_INIT: (r'I \(([0-9]+)\) MB_TCP_SLAVE_PORT: (Protocol stack initialized).'), + Stages.STACK_CONNECT: (r'I\s\(([0-9]+)\) MB_TCP_SLAVE_PORT: Socket \(#[0-9]+\), accept client connection from address: ' + r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_START: (r'I\s\(([0-9]+)\) SLAVE_TEST: (Start modbus test)'), + Stages.STACK_PAR_OK: (r'I\s\(([0-9]+)\) SLAVE_TEST: ([A-Z]+ [A-Z]+) \([a-zA-Z0-9_]+ us\),\s' + r'ADDR:([0-9]+), TYPE:[0-9]+, INST_ADDR:0x[a-zA-Z0-9]+, SIZE:[0-9]+'), + Stages.STACK_PAR_FAIL: (r'E \(([0-9]+)\) SLAVE_TEST: Response time exceeds configured [0-9]+ [ms], ignore packet'), + Stages.STACK_DESTROY: (r'I\s\(([0-9]+)\) SLAVE_TEST: (Modbus controller destroyed).')} + + +@pytest.mark.esp32 +@pytest.mark.multi_dut_modbus_tcp +@pytest.mark.parametrize('config', ['ethernet'], indirect=True) +@pytest.mark.parametrize( + 'count, app_path', [ + (1, f'{os.path.join(os.path.dirname(__file__), "mb_tcp_slave")}') + ], + indirect=True +) +def test_modbus_tcp_host_to_slave_communication(app_path, dut: ModbusTestDut) -> None: + logger.info('DUT: %s start.', dut.dut_get_name()) + dut_slave_ip_address = dut.dut_get_ip() + assert dut_slave_ip_address is not None, "The DUT could not get IP address. Abort." + dut_slave_ip_port = dut.app.sdkconfig.get('FMB_TCP_PORT_DEFAULT') + assert dut_slave_ip_port is not None, f"DUT port is not correct: {dut_slave_ip_port}" + logger.info(f'Start test for the slave: {app_path}, {dut_slave_ip_address}:{dut_slave_ip_port}') + try: + cmd = 'robot ' + \ + f'--variable MODBUS_DEF_SERVER_IP:{dut_slave_ip_address} ' + \ + f'--variable MODBUS_DEF_SERVER_PORT:{dut_slave_ip_port} ' + \ + f'{TEST_ROBOT_DIR}/ModbusTestSuite.robot' + p = subprocess.Popen(cmd, + stdin=None, stdout=None, stderr=None, + shell=True, + close_fds=CLOSE_FDS + ) + dut.dut_test_start(dictionary=pattern_dict_slave) + p.wait() + logger.info(f'Test for the node: {dut_slave_ip_address} is completed.') + dut.dut_check_errors() + + except subprocess.CalledProcessError as e: + logging.error('robot framework fail with error: %d', e.returncode) + logging.debug("Command ran: '%s'", e.cmd) + logging.debug('Command out:') + logging.debug(e.output) + logging.error('Check the correctneess of the suite script.') + raise e diff --git a/examples/tcp/pytest_mb_tcp_master_slave.py b/examples/tcp/pytest_mb_tcp_master_slave.py new file mode 100644 index 0000000..459c116 --- /dev/null +++ b/examples/tcp/pytest_mb_tcp_master_slave.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# This is the script to reproduce the issue when the expect() is called from +# main thread in Multi DUT case. + +import logging +import os +from typing import Tuple + +import pytest + +from conftest import ModbusTestDut, Stages + +pattern_dict_slave = {Stages.STACK_IPV4: (r'I \([0-9]+\) example_[a-z]+: - IPv4 address: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_IPV6: (r'I \([0-9]+\) example_[a-z]+: - IPv6 address: (([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})'), + Stages.STACK_INIT: (r'I \(([0-9]+)\) MB_TCP_SLAVE_PORT: (Protocol stack initialized).'), + Stages.STACK_CONNECT: (r'I\s\(([0-9]+)\) MB_TCP_SLAVE_PORT: Socket \(#[0-9]+\), accept client connection from address: ' + r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_START: (r'I\s\(([0-9]+)\) SLAVE_TEST: (Start modbus test)'), + Stages.STACK_PAR_OK: (r'I\s\(([0-9]+)\) SLAVE_TEST: ([A-Z]+ [A-Z]+) \([a-zA-Z0-9_]+ us\),\s' + r'ADDR:([0-9]+), TYPE:[0-9]+, INST_ADDR:0x[a-zA-Z0-9]+, SIZE:[0-9]+'), + Stages.STACK_PAR_FAIL: (r'E \(([0-9]+)\) SLAVE_TEST: Response time exceeds configured [0-9]+ [ms], ignore packet'), + Stages.STACK_DESTROY: (r'I\s\(([0-9]+)\) SLAVE_TEST: (Modbus controller destroyed).')} + +pattern_dict_master = {Stages.STACK_IPV4: (r'I \([0-9]+\) example_[a-z]+: - IPv4 address: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'), + Stages.STACK_IPV6: (r'I \([0-9]+\) example_[a-z]+: - IPv6 address: (([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})'), + Stages.STACK_INIT: (r'I \(([0-9]+)\) MASTER_TEST: (Modbus master stack initialized)'), + Stages.STACK_CONNECT: (r'I\s\(([0-9]+)\) MB_TCP_MASTER_PORT: (Connected [0-9]+ slaves), start polling'), + Stages.STACK_START: (r'I \(([0-9]+)\) MASTER_TEST: (Start modbus test)'), + Stages.STACK_PAR_OK: (r'I \(([0-9]+)\) MASTER_TEST: Characteristic #[0-9]+ ([a-zA-Z0-9_]+)' + r'\s\([a-zA-Z\_\%\/]+\) value =[a-zA-Z0-9\.\s]* \((0x[a-zA-Z0-9]+)\)[,\sa-z]+ successful'), + Stages.STACK_PAR_FAIL: (r'.*E \(([0-9]+)\) MASTER_TEST: Characteristic #[0-9]+\s\(([a-zA-Z0-9_]+)\)\s' + r'read fail, err = [x0-9]+ \([_A-Z]+\)'), + Stages.STACK_DESTROY: (r'I \(([0-9]+)\) MASTER_TEST: (Destroy master)...')} + +LOG_LEVEL = logging.DEBUG +LOGGER_NAME = 'modbus_test' +CONFORMANCE_TEST_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../tools/robot')) +logger = logging.getLogger(LOGGER_NAME) + +test_configs = [ + 'wifi', + 'ethernet' +] + +@pytest.mark.esp32 +@pytest.mark.multi_dut_modbus_tcp +@pytest.mark.parametrize('config', test_configs, indirect=True) +@pytest.mark.parametrize( + 'count, app_path', [ + (2, f'{os.path.join(os.path.dirname(__file__), "mb_tcp_master")}|{os.path.join(os.path.dirname(__file__), "mb_tcp_slave")}') + ], + indirect=True +) +def test_modbus_tcp_communication(dut: Tuple[ModbusTestDut, ModbusTestDut]) -> None: + dut_slave = dut[1] + dut_master = dut[0] + + logger.info('DUT: %s start.', dut_master.dut_get_name()) + logger.info('DUT: %s start.', dut_slave.dut_get_name()) + + dut_slave_ip_address = dut_slave.dut_get_ip() + dut_master.dut_send_ip(dut_slave_ip_address) + + dut_slave.dut_test_start(dictionary=pattern_dict_slave) + dut_master.dut_test_start(dictionary=pattern_dict_master) + + dut_slave.dut_check_errors() + dut_master.dut_check_errors() + +@pytest.mark.multi_dut_modbus_generic +@pytest.mark.parametrize('config', ['dummy_config']) +def test_modbus_tcp_generic(config) -> None: + logger.info('The generic tcp example tests are not provided yet.') + + diff --git a/idf_component.yml b/idf_component.yml new file mode 100644 index 0000000..fcb2288 --- /dev/null +++ b/idf_component.yml @@ -0,0 +1,16 @@ +version: "2.0.1" +description: ESP-MODBUS is the official Modbus library for Espressif SoCs. +url: https://github.com/espressif/esp-modbus +dependencies: + idf: ">=5.0" +files: + exclude: + - "docs/**/*" + - "docs" + - "test/**/*" + - "test" + - "pytest_embedded_log/**/*" + - "build*/**/*" + - "**/*.zip" + - "__pycache__/**/*" + diff --git a/linker.lf b/linker.lf new file mode 100644 index 0000000..635e6c2 --- /dev/null +++ b/linker.lf @@ -0,0 +1,23 @@ +[mapping:esp_modbus] +archive: libesp-modbus.a +entries: + * (default) + port_event: mb_port_event_set_err_type (noflash_text) + port_event: mb_port_event_post (noflash_text) + port_other: lock_obj (noflash_text) + port_other: unlock_obj (noflash_text) + + if FMB_TIMER_USE_ISR_DISPATCH_METHOD = y: + # tcp_master: mbm_tcp_transp_timer_expired (noflash_text) + tcp_slave: mbs_tcp_transp_timer_expired (noflash_text) + # port_tcp_slave: mbs_port_timer_expired (noflash_text) + port_tcp_master: mbm_port_timer_expired (noflash_text) + port_timer: timer_alarm_cb (noflash_text) + port_timer: mb_port_set_cur_timer_mode (noflash_text) + port_timer: mb_port_get_cur_timer_mode (noflash_text) + port_timer: mb_port_timer_disable (noflash_text) + ascii_master: mbm_ascii_transp_timer_expired (noflash_text) + ascii_slave: mbs_ascii_transp_timer_expired (noflash_text) + rtu_master: mbm_rtu_transp_timer_expired (noflash_text) + rtu_slave: mbs_rtu_transp_timer_expired (noflash_text) + diff --git a/modbus/mb_controller/common/esp_modbus_callbacks.h b/modbus/mb_controller/common/esp_modbus_callbacks.h new file mode 100644 index 0000000..831375b --- /dev/null +++ b/modbus/mb_controller/common/esp_modbus_callbacks.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// Stack callback functions prototypes + +#pragma once + +#include "mb_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef mb_err_enum_t (*reg_input_cb)(mb_base_t*, uint8_t *, uint16_t, uint16_t); +typedef mb_err_enum_t (*reg_holding_cb)(mb_base_t*, uint8_t *, uint16_t, uint16_t, mb_reg_mode_enum_t); +typedef mb_err_enum_t (*reg_coils_cb)(mb_base_t*, uint8_t *, uint16_t, uint16_t, mb_reg_mode_enum_t); +typedef mb_err_enum_t (*reg_discrete_cb)(mb_base_t*, uint8_t *, uint16_t, uint16_t); + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_controller/common/esp_modbus_master.c b/modbus/mb_controller/common/esp_modbus_master.c new file mode 100644 index 0000000..99bece9 --- /dev/null +++ b/modbus/mb_controller/common/esp_modbus_master.c @@ -0,0 +1,716 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_err.h" // for esp_err_t +#include "mbc_master.h" // for master interface define +#include "esp_modbus_master.h" // for public interface defines + +static const char TAG[] __attribute__((unused)) = "MB_CONTROLLER_MASTER"; + +// This file implements public API for Modbus master controller. + +/** + * Modbus controller delete function + */ +esp_err_t mbc_master_delete(void *ctx) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE(mbm_controller->delete, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + error = mbm_controller->delete (ctx); + MB_RETURN_ON_FALSE((error == ESP_OK), error, + TAG, "Master delete failure, error=(0x%x).", (uint16_t)error); + return error; +} + +/** + * Critical section lock function + */ +esp_err_t mbc_master_lock(void *ctx) +{ + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + mb_base_t *pmb_obj = (mb_base_t *)mbm_controller->mb_base; + MB_RETURN_ON_FALSE((pmb_obj && pmb_obj->lock), ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + CRITICAL_SECTION_LOCK(pmb_obj->lock); + return ESP_OK; +} + +/** + * Critical section unlock function + */ +esp_err_t mbc_master_unlock(void *ctx) +{ + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + mb_base_t *pmb_obj = (mb_base_t *)mbm_controller->mb_base; + MB_RETURN_ON_FALSE((pmb_obj && pmb_obj->lock), ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + CRITICAL_SECTION_UNLOCK(pmb_obj->lock); + return ESP_OK; +} + +esp_err_t mbc_master_get_cid_info(void *ctx, uint16_t cid, const mb_parameter_descriptor_t **param_info) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbm_controller->get_cid_info && mbm_controller->is_active), + ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly configured."); + error = mbm_controller->get_cid_info(ctx, cid, param_info); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master get cid info failure, error=(0x%x).", (uint16_t)error); + return error; +} + +/** + * Set parameter value for characteristic selected by name and cid + */ +esp_err_t mbc_master_set_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbm_controller->set_parameter && mbm_controller->is_active), + ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + error = mbm_controller->set_parameter(ctx, cid, value, type); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master set parameter failure, error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return ESP_OK; +} + +/** + * Set parameter value for characteristic selected by name and cid + */ +esp_err_t mbc_master_set_parameter_with(void *ctx, uint16_t cid, uint8_t uid, uint8_t *value, uint8_t *type) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbm_controller->set_parameter_with && mbm_controller->is_active), + ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + error = mbm_controller->set_parameter_with(ctx, cid, uid, value, type); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master set parameter failure, error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return ESP_OK; +} + +/** + * Get parameter data for corresponding characteristic + */ +esp_err_t mbc_master_get_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbm_controller->get_parameter && mbm_controller->is_active), + ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly configured."); + error = mbm_controller->get_parameter(ctx, cid, value, type); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master get parameter failure, error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return error; +} + +/** + * Get parameter data for corresponding characteristic + */ +esp_err_t mbc_master_get_parameter_with(void *ctx, uint16_t cid, uint8_t uid, uint8_t *value, uint8_t *type) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbm_controller->get_parameter_with && mbm_controller->is_active), + ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly configured."); + error = mbm_controller->get_parameter_with(ctx, cid, uid, value, type); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master get parameter failure, error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return error; +} + +/** + * Send custom Modbus request defined as mb_param_request_t structure + */ +esp_err_t mbc_master_send_request(void *ctx, mb_param_request_t *request, void *data_ptr) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbm_controller->send_request && mbm_controller->is_active), + ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly configured."); + error = mbm_controller->send_request(ctx, request, data_ptr); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master send request failure error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return ESP_OK; +} + +/** + * Set Modbus parameter description table + */ +esp_err_t mbc_master_set_descriptor(void *ctx, const mb_parameter_descriptor_t *descriptor, + const uint16_t num_elements) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE(mbm_controller->set_descriptor, + ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly configured."); + error = mbm_controller->set_descriptor(ctx, descriptor, num_elements); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master set descriptor failure, error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return ESP_OK; +} + +/** + * Modbus controller stack start function + */ +esp_err_t mbc_master_start(void *ctx) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE(mbm_controller->start, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + error = mbm_controller->start(ctx); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master start failure, error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return ESP_OK; +} + +/** + * Modbus controller stack stop function + */ +esp_err_t mbc_master_stop(void *ctx) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + mbm_controller_iface_t *mbm_controller = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE(mbm_controller->stop, ESP_ERR_INVALID_STATE, TAG, + "Master interface is not correctly initialized."); + error = mbm_controller->stop(ctx); + MB_RETURN_ON_FALSE((error == ESP_OK), error, TAG, + "Master stop failure, error=(0x%x) (%s).", + (uint16_t)error, esp_err_to_name(error)); + return ESP_OK; +} + +/* ----------------------- Callback functions for Modbus stack ---------------------------------*/ +// These are executed by modbus stack to read appropriate type of registers. + +/** + * Modbus master input register callback function. + * + * @param ctx interface context pointer + * @param reg_buffer input register buffer + * @param reg_addr input register address + * @param num_regs input register number + * + * @return result + */ +// Callback function for reading of MB Input Registers +// mbm_reg_input_cb_serial +mb_err_enum_t mbc_reg_input_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t reg_addr, uint16_t num_regs) +{ + MB_RETURN_ON_FALSE((reg_buffer), MB_EINVAL, TAG, + "Master stack processing error."); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(MB_MASTER_GET_IFACE_FROM_BASE(inst)); + // Number of input registers to be transferred + uint16_t num_input_regs = (uint16_t)mbm_opts->reg_buffer_size; + uint8_t *input_reg_buf = (uint8_t *)mbm_opts->reg_buffer_ptr; // Get instance address + uint16_t regs_cnt = num_regs; + mb_err_enum_t status = MB_ENOERR; + // If input or configuration parameters are incorrect then return an error to stack layer + if ((input_reg_buf) && (num_regs >= 1) && (num_input_regs == regs_cnt)) + { + CRITICAL_SECTION(inst->lock) + { + while (regs_cnt > 0) + { + _XFER_2_RD(input_reg_buf, reg_buffer); + regs_cnt -= 1; + } + } + } + else + { + status = MB_ENOREG; + } + return status; +} + +/** + * Modbus master holding register callback function. + * + * @param ctx interface context pointer + * @param reg_buffer holding register buffer + * @param reg_addr holding register address + * @param num_regs holding register number + * @param mode read or write + * + * @return result + */ +// Callback function for reading of MB Holding Registers +// Executed by stack when request to read/write holding registers is received +// mbm_reg_holding_cb_serial +mb_err_enum_t mbc_reg_holding_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t reg_addr, + uint16_t num_regs, mb_reg_mode_enum_t mode) +{ + MB_RETURN_ON_FALSE((reg_buffer), MB_EINVAL, TAG, "Master stack processing error."); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(MB_MASTER_GET_IFACE_FROM_BASE(inst)); + uint16_t num_hold_regs = (uint16_t)mbm_opts->reg_buffer_size; + uint8_t *holding_buf = (uint8_t *)mbm_opts->reg_buffer_ptr; + mb_err_enum_t status = MB_ENOERR; + uint16_t regs_cnt = num_regs; + // Check input and configuration parameters for correctness + if ((holding_buf) && (num_hold_regs == num_regs) && (num_regs >= 1)) + { + switch (mode) + { + case MB_REG_WRITE: + CRITICAL_SECTION(inst->lock) + { + while (regs_cnt > 0) + { + _XFER_2_RD(reg_buffer, holding_buf); + regs_cnt -= 1; + } + } + break; + case MB_REG_READ: + CRITICAL_SECTION(inst->lock) + { + while (regs_cnt > 0) + { + _XFER_2_WR(holding_buf, reg_buffer); + holding_buf += 2; + regs_cnt -= 1; + } + } + break; + } + } + else + { + status = MB_ENOREG; + } + return status; +} + +/** + * Modbus master coils callback function. + * + * @param ctx interface context pointer + * @param reg_buffer coils buffer + * @param reg_addr coils address + * @param ncoils coils number + * @param mode read or write + * + * @return result + */ +// Callback function for reading of MB Coils Registers +// mbm_reg_coils_cb_serial +mb_err_enum_t mbc_reg_coils_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t reg_addr, + uint16_t ncoils, mb_reg_mode_enum_t mode) +{ + MB_RETURN_ON_FALSE((reg_buffer), MB_EINVAL, TAG, "Master stack processing error."); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(MB_MASTER_GET_IFACE_FROM_BASE(inst)); + uint16_t num_coil_regs = (uint16_t)mbm_opts->reg_buffer_size; + uint8_t *coils_buf = (uint8_t *)mbm_opts->reg_buffer_ptr; + mb_err_enum_t status = MB_ENOERR; + uint16_t reg_index; + uint16_t coils_cnt = ncoils; + reg_addr--; // The address is already + 1 + if ((num_coil_regs >= 1) && (coils_buf) && (ncoils == num_coil_regs)) + { + reg_index = (reg_addr % 8); + switch (mode) + { + case MB_REG_WRITE: + CRITICAL_SECTION(inst->lock) + { + while (coils_cnt > 0) + { + uint8_t result = mb_util_get_bits((uint8_t *)coils_buf, reg_index - (reg_addr % 8), 1); + mb_util_set_bits(reg_buffer, reg_index - (reg_addr % 8), 1, result); + reg_index++; + coils_cnt--; + } + } + break; + case MB_REG_READ: + CRITICAL_SECTION(inst->lock) + { + while (coils_cnt > 0) + { + uint8_t result = mb_util_get_bits(reg_buffer, reg_index - (reg_addr % 8), 1); + mb_util_set_bits((uint8_t *)coils_buf, reg_index - (reg_addr % 8), 1, result); + reg_index++; + coils_cnt--; + } + } + break; + } // switch ( mode ) + } + else + { + // If the configuration or input parameters are incorrect then return error to stack + status = MB_ENOREG; + } + return status; +} + +/** + * Modbus master discrete callback function. + * + * @param ctx - pointer to interface structure + * @param reg_buffer discrete buffer + * @param reg_addr discrete address + * @param n_discrete discrete number + * + * @return result + */ +// Callback function for reading of MB Discrete Input Registers +// mbm_reg_discrete_cb_serial +mb_err_enum_t mbc_reg_discrete_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t reg_addr, + uint16_t n_discrete) +{ + MB_RETURN_ON_FALSE((reg_buffer), MB_EINVAL, TAG, "Master stack processing error."); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(MB_MASTER_GET_IFACE_FROM_BASE(inst)); + uint16_t num_discr_regs = (uint16_t)mbm_opts->reg_buffer_size; + uint8_t *discr_buf = (uint8_t *)mbm_opts->reg_buffer_ptr; + mb_err_enum_t status = MB_ENOERR; + uint16_t bit_index, num_reg; + uint8_t *temp_discr_buf; + num_reg = n_discrete; + temp_discr_buf = (uint8_t *)discr_buf; + // It is already plus one in Modbus function method. + reg_addr--; + if ((num_discr_regs >= 1) && (discr_buf) && (n_discrete >= 1) && (n_discrete == num_discr_regs)) + { + bit_index = (uint16_t)(reg_addr) % 8; // Get bit index + CRITICAL_SECTION(inst->lock) + { + while (num_reg > 0) + { + uint8_t result = mb_util_get_bits(reg_buffer, bit_index - (reg_addr % 8), 1); + mb_util_set_bits(temp_discr_buf, bit_index - (reg_addr % 8), 1, result); + bit_index++; + num_reg--; + } + } + } + else + { + status = MB_ENOREG; + } + return status; +} + +// Helper function to set parameter buffer according to its type +esp_err_t mbc_master_set_param_data(void* dest, void* src, mb_descr_type_t param_type, size_t param_size) +{ + esp_err_t err = ESP_OK; + MB_RETURN_ON_FALSE((src), ESP_ERR_INVALID_STATE, TAG,"incorrect data pointer."); + MB_RETURN_ON_FALSE((dest), ESP_ERR_INVALID_STATE, TAG,"incorrect data pointer."); + void *pdest = dest; + void *psrc = src; + + // Transfer parameter data into value of characteristic + switch(param_type) + { + case PARAM_TYPE_U8: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U8) { + *((uint8_t *)pdest) = *((uint8_t*)psrc); + } + break; + + case PARAM_TYPE_U16: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U16) { + *((uint16_t *)pdest) = *((uint16_t*)psrc); + } + break; + + case PARAM_TYPE_U32: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U32) { + *((uint32_t *)pdest) = *((uint32_t*)psrc); + } + break; + + case PARAM_TYPE_FLOAT: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_FLOAT) { + *((float *)pdest) = *(float*)psrc; + } + break; + + case PARAM_TYPE_ASCII: + case PARAM_TYPE_BIN: + memcpy((void *)dest, (void*)src, (size_t)param_size); + break; + +#if CONFIG_FMB_EXT_TYPE_SUPPORT + + case PARAM_TYPE_I8_A: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U8_REG) { + mb_set_int8_a((val_16_arr *)pdest, (*(int8_t*)psrc)); + ESP_LOGV(TAG, "Convert uint8 B[%d] 0x%04" PRIx16 " = 0x%04" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_I8_B: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U8_REG) { + mb_set_int8_b((val_16_arr *)pdest, (int8_t)((*(uint16_t*)psrc) >> 8)); + ESP_LOGV(TAG, "Convert int8 A[%d] 0x%02" PRIx16 " = 0x%02" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_U8_A: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U8_REG) { + mb_set_uint8_a((val_16_arr *)pdest, (*(uint8_t*)psrc)); + ESP_LOGV(TAG, "Convert uint8 A[%d] 0x%02" PRIx16 " = %02" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_U8_B: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U8_REG) { + uint8_t data = (uint8_t)((*(uint16_t*)psrc) >> 8); + mb_set_uint8_b((val_16_arr *)pdest, data); + ESP_LOGV(TAG, "Convert uint8 B[%d] 0x%02" PRIx16 " = 0x%02" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_I16_AB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I16) { + mb_set_int16_ab((val_16_arr *)pdest, *(int16_t*)psrc); + ESP_LOGV(TAG, "Convert int16 AB[%d] 0x%04" PRIx16 " = 0x%04" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_I16_BA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I16) { + mb_set_int16_ba((val_16_arr *)pdest, *(int16_t*)psrc); + ESP_LOGV(TAG, "Convert int16 BA[%d] 0x%04" PRIx16 " = 0x%04" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_U16_AB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U16) { + mb_set_uint16_ab((val_16_arr *)pdest, *(uint16_t*)psrc); + ESP_LOGV(TAG, "Convert uint16 AB[%d] 0x%02" PRIx16 " = 0x%02" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_U16_BA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U16) { + mb_set_uint16_ba((val_16_arr *)pdest, *(uint16_t*)psrc); + ESP_LOGV(TAG, "Convert uint16 BA[%d] 0x%02" PRIx16 " = 0x%02" PRIx16, i, *(uint16_t *)psrc, *(uint16_t *)pdest); + } + break; + + case PARAM_TYPE_I32_ABCD: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I32) { + mb_set_int32_abcd((val_32_arr *)pdest, *(int32_t *)psrc); + ESP_LOGV(TAG, "Convert int32 ABCD[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_U32_ABCD: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U32) { + mb_set_uint32_abcd((val_32_arr *)pdest, *(uint32_t *)psrc); + ESP_LOGV(TAG, "Convert uint32 ABCD[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_FLOAT_ABCD: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_FLOAT) { + mb_set_float_abcd((val_32_arr *)pdest, *(float *)psrc); + ESP_LOGV(TAG, "Convert float ABCD[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_I32_CDAB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I32) { + mb_set_int32_cdab((val_32_arr *)pdest, *(int32_t *)psrc); + ESP_LOGV(TAG, "Convert int32 CDAB[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_U32_CDAB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U32) { + mb_set_uint32_cdab((val_32_arr *)pdest, *(uint32_t *)psrc); + ESP_LOGV(TAG, "Convert uint32 CDAB[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_FLOAT_CDAB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_FLOAT) { + mb_set_float_cdab((val_32_arr *)pdest, *(float *)psrc); + ESP_LOGV(TAG, "Convert float CDAB[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_I32_BADC: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I32) { + mb_set_int32_badc((val_32_arr *)pdest, *(int32_t *)psrc); + ESP_LOGV(TAG, "Convert int32 BADC[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_U32_BADC: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U32) { + mb_set_uint32_badc((val_32_arr *)pdest, *(uint32_t *)psrc); + ESP_LOGV(TAG, "Convert uint32 BADC[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_FLOAT_BADC: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_FLOAT) { + mb_set_float_badc((val_32_arr *)pdest, *(float *)psrc); + ESP_LOGV(TAG, "Convert float BADC[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_I32_DCBA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I32) { + mb_set_int32_dcba((val_32_arr *)pdest, *(int32_t *)psrc); + ESP_LOGV(TAG, "Convert int32 DCBA[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_U32_DCBA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U32) { + mb_set_uint32_dcba((val_32_arr *)pdest, *(uint32_t *)psrc); + ESP_LOGV(TAG, "Convert uint32 DCBA[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_FLOAT_DCBA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_FLOAT) { + mb_set_float_dcba((val_32_arr *)pdest, *(float *)psrc); + ESP_LOGV(TAG, "Convert float DCBA[%d] 0x%04" PRIx32 " = 0x%04" PRIx32, i, *(uint32_t *)psrc, *(uint32_t *)pdest); + } + break; + + case PARAM_TYPE_I64_ABCDEFGH: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I64) { + mb_set_int64_abcdefgh((val_64_arr *)pdest, *(int64_t *)psrc); + ESP_LOGV(TAG, "Convert int64 ABCDEFGH[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_U64_ABCDEFGH: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U64) { + mb_set_uint64_abcdefgh((val_64_arr *)pdest, *(uint64_t *)psrc); + ESP_LOGV(TAG, "Convert double ABCDEFGH[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_DOUBLE_ABCDEFGH: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_DOUBLE) { + mb_set_double_abcdefgh((val_64_arr *)pdest, *(double *)psrc); + ESP_LOGV(TAG, "Convert double ABCDEFGH[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_I64_HGFEDCBA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I64) { + mb_set_int64_hgfedcba((val_64_arr *)pdest, *(int64_t *)psrc); + ESP_LOGV(TAG, "Convert int64 HGFEDCBA[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_U64_HGFEDCBA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U64) { + mb_set_uint64_hgfedcba((val_64_arr *)pdest, *(uint64_t *)psrc); + ESP_LOGV(TAG, "Convert double HGFEDCBA[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_DOUBLE_HGFEDCBA: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_DOUBLE) { + mb_set_double_hgfedcba((val_64_arr *)pdest, *(double *)psrc); + ESP_LOGV(TAG, "Convert double HGFEDCBA[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_I64_GHEFCDAB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I64) { + mb_set_int64_ghefcdab((val_64_arr *)pdest, *(int64_t *)psrc); + ESP_LOGV(TAG, "Convert int64 GHEFCDAB[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_U64_GHEFCDAB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U64) { + mb_set_uint64_ghefcdab((val_64_arr *)pdest, *(uint64_t *)psrc); + ESP_LOGV(TAG, "Convert uint64 GHEFCDAB[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_DOUBLE_GHEFCDAB: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_DOUBLE) { + mb_set_double_ghefcdab((val_64_arr *)pdest, *(double *)psrc); + ESP_LOGV(TAG, "Convert double GHEFCDAB[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_I64_BADCFEHG: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_I64) { + mb_set_int64_badcfehg((val_64_arr *)pdest, *(int64_t *)psrc); + ESP_LOGV(TAG, "Convert int64 BADCFEHG[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_U64_BADCFEHG: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_U64) { + mb_set_uint64_badcfehg((val_64_arr *)pdest, *(uint64_t *)psrc); + ESP_LOGV(TAG, "Convert uint64 BADCFEHG[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + + case PARAM_TYPE_DOUBLE_BADCFEHG: + for MB_EACH_ELEM(psrc, pdest, param_size, PARAM_SIZE_DOUBLE) { + mb_set_double_badcfehg((val_64_arr *)pdest, *(double *)psrc); + ESP_LOGV(TAG, "Convert double BADCFEHG[%d] 0x%" PRIx64 " = 0x%" PRIx64, i, *(uint64_t *)psrc, *(uint64_t *)pdest); + } + break; + +#endif + default: + ESP_LOGE(TAG, "%s: Incorrect param type (%u).", + __FUNCTION__, (unsigned)param_type); + err = ESP_ERR_NOT_SUPPORTED; + break; + } + return err; +} diff --git a/modbus/mb_controller/common/esp_modbus_master_serial.c b/modbus/mb_controller/common/esp_modbus_master_serial.c new file mode 100644 index 0000000..967c4ba --- /dev/null +++ b/modbus/mb_controller/common/esp_modbus_master_serial.c @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_err.h" // for esp_err_t +#include "mbc_master.h" // for master interface define +#include "esp_modbus_master.h" // for public slave defines +#include "mbc_serial_master.h" // for public interface defines + +#include "sdkconfig.h" // for KConfig defines + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +/** + * Initialization of Modbus master serial + */ +esp_err_t mbc_master_create_serial(mb_communication_info_t *config, void **handler) +{ + void *ctx = NULL; + esp_err_t error = ESP_ERR_NOT_SUPPORTED; + switch(config->mode) { + case MB_RTU: + case MB_ASCII: + error = mbc_serial_master_create(config, &ctx); + break; + default: + return ESP_ERR_NOT_SUPPORTED; + } + if ((ctx) && (error == ESP_OK)) { + //mbc_master_init_iface(ctx); + *handler = ctx; + } + return error; +} + +#endif \ No newline at end of file diff --git a/modbus/mb_controller/common/esp_modbus_master_tcp.c b/modbus/mb_controller/common/esp_modbus_master_tcp.c new file mode 100644 index 0000000..9a7a5f6 --- /dev/null +++ b/modbus/mb_controller/common/esp_modbus_master_tcp.c @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_err.h" // for esp_err_t +#include "esp_modbus_master.h" // for public interface defines +#include "mbc_tcp_master.h" // for public interface defines +#include "sdkconfig.h" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +/** + * Initialization of Modbus TCP Master controller interface + */ +esp_err_t mbc_master_create_tcp(mb_communication_info_t *config, void **handler) +{ + void *ctx = NULL; + esp_err_t error = mbc_tcp_master_create(config, &ctx); + + if ((ctx) && (error == ESP_OK)) { + *handler = ctx; + } + return error; +} + +#endif \ No newline at end of file diff --git a/modbus/mb_controller/common/esp_modbus_slave.c b/modbus/mb_controller/common/esp_modbus_slave.c new file mode 100644 index 0000000..8195e52 --- /dev/null +++ b/modbus/mb_controller/common/esp_modbus_slave.c @@ -0,0 +1,517 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_err.h" // for esp_err_t +#include "esp_timer.h" // for esp_timer_get_time() +#include "sdkconfig.h" // for KConfig defines + +#include "mbc_slave.h" // for slave private type definitions +#include "esp_modbus_common.h" // for common defines +#include "esp_modbus_slave.h" // for public slave defines + +#include "mb_utils.h" // for stack bit setting utilities + +#ifdef CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT + +#define MB_ID_BYTE0(id) ((uint8_t)(id)) +#define MB_ID_BYTE1(id) ((uint8_t)(((uint16_t)(id) >> 8) & 0xFF)) +#define MB_ID_BYTE2(id) ((uint8_t)(((uint32_t)(id) >> 16) & 0xFF)) +#define MB_ID_BYTE3(id) ((uint8_t)(((uint32_t)(id) >> 24) & 0xFF)) + +#define MB_CONTROLLER_SLAVE_ID (CONFIG_FMB_CONTROLLER_SLAVE_ID) +#define MB_SLAVE_ID_SHORT (MB_ID_BYTE3(MB_CONTROLLER_SLAVE_ID)) + +// Slave ID constant +static uint8_t mb_slave_id[] = { MB_ID_BYTE0(MB_CONTROLLER_SLAVE_ID), + MB_ID_BYTE1(MB_CONTROLLER_SLAVE_ID), + MB_ID_BYTE2(MB_CONTROLLER_SLAVE_ID) }; + +#endif + +static const char TAG[] __attribute__((unused)) = "MB_CONTROLLER_SLAVE"; + +// Searches the register in the area specified by type, returns descriptor if found, else NULL +static mb_descr_entry_t *mbc_slave_find_reg_descriptor(void *ctx, mb_param_type_t type, uint16_t addr, size_t regs) +{ + mb_descr_entry_t *it; + uint16_t reg_size = 0; + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + + if (LIST_EMPTY(&mbs_opts->area_descriptors[type])) { + return NULL; + } + // search for the register in each area + for (it = LIST_FIRST(&mbs_opts->area_descriptors[type]); it != NULL; it = LIST_NEXT(it, entries)) { + reg_size = REG_SIZE(type, it->size); + if ((addr >= it->start_offset) + && (it->p_data) + && (regs >= 1) + && ((addr + regs) <= (it->start_offset + reg_size)) + && (reg_size >= 1)) { + return it; + } + } + return NULL; +} + +static void mbc_slave_free_descriptors(void *ctx) +{ + mb_descr_entry_t *it; + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + + for (int descr_type = 0; descr_type < MB_PARAM_COUNT; descr_type++) { + while ((it = LIST_FIRST(&mbs_opts->area_descriptors[descr_type]))) { + LIST_REMOVE(it, entries); + free(it); + } + } +} + +void mbc_slave_init_iface(void *ctx) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + + // Initialize list head for register areas + LIST_INIT(&mbs_opts->area_descriptors[MB_PARAM_INPUT]); + LIST_INIT(&mbs_opts->area_descriptors[MB_PARAM_HOLDING]); + LIST_INIT(&mbs_opts->area_descriptors[MB_PARAM_COIL]); + LIST_INIT(&mbs_opts->area_descriptors[MB_PARAM_DISCRETE]); +} + +/** + * Modbus controller delete function + */ +esp_err_t mbc_slave_delete(void *ctx) +{ + esp_err_t error = ESP_OK; + // Is initialization done? + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + + // Check if interface has been initialized + MB_RETURN_ON_FALSE(mbs_controller->delete, + ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly configured."); + // Call the slave controller destroy function + error = mbs_controller->delete(ctx); + if (error != ESP_OK) { + ESP_LOGE(TAG, "Slave delete failure error=(0x%x).", (uint16_t)error); + } + // Destroy all opened descriptors + mbc_slave_free_descriptors(ctx); + free(mbs_controller); + mbs_controller = NULL; + return error; +} + +/** + * Critical section lock function + */ +esp_err_t mbc_slave_lock(void *ctx) +{ + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + mb_base_t *pmb_obj = (mb_base_t *)mbs_controller->mb_base; + MB_RETURN_ON_FALSE((pmb_obj && pmb_obj->lock), ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + CRITICAL_SECTION_LOCK(pmb_obj->lock); + return ESP_OK; +} + +/** + * Critical section unlock function + */ +esp_err_t mbc_slave_unlock(void *ctx) +{ + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + mb_base_t *pmb_obj = (mb_base_t *)mbs_controller->mb_base; + MB_RETURN_ON_FALSE((pmb_obj && pmb_obj->lock), ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + CRITICAL_SECTION_UNLOCK(pmb_obj->lock); + return ESP_OK; +} + +/** + * Start Modbus controller start function + */ +esp_err_t mbc_slave_start(void *ctx) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + MB_RETURN_ON_FALSE(mbs_controller->start, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly configured."); +#ifdef CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT + // Set the slave ID if the KConfig option is selected + mb_err_enum_t status = mb_set_slv_id(mbs_controller->mb_base, MB_SLAVE_ID_SHORT, true, (uint8_t *)mb_slave_id, sizeof(mb_slave_id)); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, "mb stack set slave ID failure."); +#endif + error = mbs_controller->start(ctx); + MB_RETURN_ON_FALSE((error == ESP_OK), ESP_ERR_INVALID_STATE, TAG, + "Slave start failure error=(0x%x).", (uint16_t)error); + mbs_controller->is_active = true; + return error; +} + +/** + * Start Modbus controller stop function + */ +esp_err_t mbc_slave_stop(void *ctx) +{ + esp_err_t error = ESP_OK; + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + MB_RETURN_ON_FALSE(mbs_controller->stop, + ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly configured."); + error = mbs_controller->stop(ctx); + MB_RETURN_ON_FALSE((error == ESP_OK), ESP_ERR_INVALID_STATE, TAG, + "Slave stop failure error=(0x%x).", (uint16_t)error); + mbs_controller->is_active = false; + return error; +} + +/** + * Blocking function to get event on parameter group change for application task + */ +mb_event_group_t mbc_slave_check_event(void *ctx, mb_event_group_t group) +{ + MB_RETURN_ON_FALSE(ctx, MB_EVENT_NO_EVENTS, TAG, + "Slave interface is not correctly initialized."); + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbs_controller->check_event && mbs_controller->is_active), + MB_EVENT_NO_EVENTS, TAG, + "Slave interface is not correctly configured."); + mb_event_group_t event = mbs_controller->check_event(ctx, group); + return event; +} + +/** + * Function to get notification about parameter change from application task + */ +esp_err_t mbc_slave_get_param_info(void *ctx, mb_param_info_t *reg_info, uint32_t timeout) +{ + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((mbs_controller->get_param_info && mbs_controller->is_active), + ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly configured."); + return mbs_controller->get_param_info(ctx, reg_info, timeout); +} + +/** + * Function to set area descriptors for modbus parameters + */ +esp_err_t mbc_slave_set_descriptor(void *ctx, mb_register_area_descriptor_t descr_data) +{ + MB_RETURN_ON_FALSE((ctx), ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + esp_err_t error = ESP_OK; + mbs_controller_iface_t *mbs_controller = MB_SLAVE_GET_IFACE(ctx); + if (mbs_controller->set_descriptor) { + error = mbs_controller->set_descriptor(ctx, descr_data); + MB_RETURN_ON_FALSE((error == ESP_OK), + ESP_ERR_INVALID_STATE, TAG, + "Slave set descriptor failure error=(0x%x).", + (uint16_t)error); + } else { + mb_slave_options_t *mbs_opts = &mbs_controller->opts; + + MB_RETURN_ON_FALSE((descr_data.size < MB_INST_MAX_SIZE) && (descr_data.size >= MB_INST_MIN_SIZE), + ESP_ERR_INVALID_ARG, TAG, "mb area size is incorrect."); + uint16_t reg_size = REG_SIZE(descr_data.type, descr_data.size); + + // Check if the address is already in the descriptor list + mb_descr_entry_t *it = mbc_slave_find_reg_descriptor(ctx, descr_data.type, descr_data.start_offset, reg_size); + if (!it) { + // Start register exists in any area? + it = mbc_slave_find_reg_descriptor(ctx, descr_data.type, descr_data.start_offset, 1); + } + + MB_RETURN_ON_FALSE((it == NULL), ESP_ERR_INVALID_ARG, TAG, "mb incorrect descriptor or already defined."); + + mb_descr_entry_t *new_descr = (mb_descr_entry_t*) heap_caps_malloc(sizeof(mb_descr_entry_t), + MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT); + MB_RETURN_ON_FALSE(new_descr, ESP_ERR_NO_MEM, TAG, "mb can not allocate memory for descriptor."); + new_descr->start_offset = descr_data.start_offset; + new_descr->type = descr_data.type; + new_descr->p_data = descr_data.address; + new_descr->size = descr_data.size; + new_descr->access = descr_data.access; + LIST_INSERT_HEAD(&mbs_opts->area_descriptors[descr_data.type], new_descr, entries); + error = ESP_OK; + } + return error; +} + +// The helper function to get time stamp in microseconds +static uint64_t mbc_slave_get_time_stamp(void) +{ + uint64_t time_stamp = esp_timer_get_time(); + return time_stamp; +} + +// Helper function to send parameter information to application task +static esp_err_t mbc_slave_send_param_info(void *ctx, mb_event_group_t par_type, uint16_t mb_offset, + uint8_t *par_address, uint16_t par_size) +{ + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + esp_err_t error = ESP_FAIL; + mb_param_info_t par_info; + // Check if queue is not full the send parameter information + par_info.type = par_type; + par_info.size = par_size; + par_info.address = par_address; + par_info.time_stamp = mbc_slave_get_time_stamp(); + par_info.mb_offset = mb_offset; + BaseType_t status = xQueueSend(mbs_opts->notification_queue_handle, &par_info, MB_PAR_INFO_TOUT); + if (pdTRUE == status) { + ESP_LOGD(TAG, "Queue send parameter info (type, address, size): %d, 0x%" PRIx32 ", %d", + (int)par_type, (uint32_t)par_address, (int)par_size); + error = ESP_OK; + } else if (errQUEUE_FULL == status) { + ESP_LOGD(TAG, "Parameter queue is overflowed."); + } + return error; +} + +// Helper function to send notification +static esp_err_t mbc_slave_send_param_access_notification(void *ctx, mb_event_group_t event) +{ + MB_RETURN_ON_FALSE(ctx, ESP_ERR_INVALID_STATE, TAG, + "Slave interface is not correctly initialized."); + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + esp_err_t err = ESP_FAIL; + mb_event_group_t bits = (mb_event_group_t)xEventGroupSetBits(mbs_opts->event_group_handle, (EventBits_t)event); + if (bits & event) { + ESP_LOGD(TAG, "The MB_REG_CHANGE_EVENT = 0x%.2x is set.", (int)event); + err = ESP_OK; + } + return err; +} + +/* + * Below are the common slave read/write register callback functions + * The concrete slave port can override them using interface function pointers + */ + +// Callback function for reading of MB Input Registers +mb_err_enum_t mbc_reg_input_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_regs) +{ + void *ctx = (void *)MB_SLAVE_GET_IFACE_FROM_BASE(inst); + MB_RETURN_ON_FALSE(reg_buffer, MB_EINVAL, TAG, "Slave stack call failed."); + mb_err_enum_t status = MB_ENOERR; + address--; // address of register is already +1 + mb_descr_entry_t *it = mbc_slave_find_reg_descriptor(ctx, MB_PARAM_INPUT, address, n_regs); + if (it) { + uint16_t input_reg_start = (uint16_t)it->start_offset; // Get Modbus start address + uint8_t *input_buffer = (uint8_t *)it->p_data; // Get instance address + uint16_t regs = n_regs; + uint16_t reg_index; + // If input or configuration parameters are incorrect then return an error to stack layer + reg_index = (uint16_t)(address - input_reg_start); + reg_index <<= 1; // register Address to byte address + input_buffer += reg_index; + uint8_t *buffer_start = input_buffer; + CRITICAL_SECTION(inst->lock) + { + while (regs > 0) { + _XFER_2_RD(reg_buffer, input_buffer); + reg_index += 2; + regs -= 1; + } + } + // Send access notification + (void)mbc_slave_send_param_access_notification(ctx, MB_EVENT_INPUT_REG_RD); + // Send parameter info to application task + (void)mbc_slave_send_param_info(ctx, MB_EVENT_INPUT_REG_RD, address, + (uint8_t *)buffer_start, n_regs); + } else { + status = MB_ENOREG; + } + return status; +} + +// Callback function for reading of MB Holding Registers +// Executed by stack when request to read/write holding registers is received +mb_err_enum_t mbc_reg_holding_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_regs, mb_reg_mode_enum_t mode) +{ + void *ctx = (void *)MB_SLAVE_GET_IFACE_FROM_BASE(inst); + MB_RETURN_ON_FALSE(reg_buffer, MB_EINVAL, TAG, "Slave stack call failed."); + mb_err_enum_t status = MB_ENOERR; + uint16_t reg_index; + address--; // address of register is already +1 + mb_descr_entry_t *it = mbc_slave_find_reg_descriptor(ctx, MB_PARAM_HOLDING, address, n_regs); + if (it) { + uint16_t reg_holding_start = (uint16_t)it->start_offset; // Get Modbus start address + uint8_t *holding_buffer = (uint8_t *)it->p_data; // Get instance address + uint16_t regs = n_regs; + reg_index = (uint16_t) (address - reg_holding_start); + reg_index <<= 1; // register Address to byte address + holding_buffer += reg_index; + uint8_t *buffer_start = holding_buffer; + switch (mode) { + case MB_REG_READ: + if (it->access != MB_ACCESS_WO) { + CRITICAL_SECTION(inst->lock) + { + while (regs > 0) { + _XFER_2_RD(reg_buffer, holding_buffer); + reg_index += 2; + regs -= 1; + }; + } + // Send access notification + (void)mbc_slave_send_param_access_notification(ctx, MB_EVENT_HOLDING_REG_RD); + // Send parameter info + (void)mbc_slave_send_param_info(ctx, MB_EVENT_HOLDING_REG_RD, address, + (uint8_t *)buffer_start, n_regs); + } else { + status = MB_EINVAL; + } + break; + case MB_REG_WRITE: + if (it->access != MB_ACCESS_RO) { + CRITICAL_SECTION(inst->lock) + { + while (regs > 0) { + _XFER_2_WR(holding_buffer, reg_buffer); + holding_buffer += 2; + reg_index += 2; + regs -= 1; + }; + } + // Send access notification + (void)mbc_slave_send_param_access_notification(ctx, MB_EVENT_HOLDING_REG_WR); + // Send parameter info + (void)mbc_slave_send_param_info(ctx, MB_EVENT_HOLDING_REG_WR, (uint16_t)address, + (uint8_t *)buffer_start, (uint16_t)n_regs); + } else { + status = MB_EINVAL; + } + break; + } + } else { + status = MB_ENOREG; + } + return status; +} + +// Callback function for reading of MB Coils Registers +mb_err_enum_t mbc_reg_coils_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_coils, mb_reg_mode_enum_t mode) +{ + void *ctx =(void *)MB_SLAVE_GET_IFACE_FROM_BASE(inst); + MB_RETURN_ON_FALSE(ctx, MB_EILLSTATE, TAG, "Slave stack uninitialized."); + MB_RETURN_ON_FALSE(reg_buffer, MB_EINVAL, TAG, "Slave stack call failed."); + mb_err_enum_t status = MB_ENOERR; + uint16_t reg_index; + uint16_t coils = n_coils; + address--; // The address is already +1 + mb_descr_entry_t *it = mbc_slave_find_reg_descriptor(ctx, MB_PARAM_COIL, address, n_coils); + if (it) { + uint16_t reg_coils_start = (uint16_t)it->start_offset; // MB offset of coils + uint8_t *reg_coils_buf = (uint8_t *)it->p_data; + reg_index = (uint16_t) (address - it->start_offset); + char *coils_data_buf = (char *)(reg_coils_buf + (reg_index >> 3)); + switch (mode) { + case MB_REG_READ: + if (it->access != MB_ACCESS_WO) { + CRITICAL_SECTION(inst->lock) + { + while (coils > 0) { + uint8_t result = mb_util_get_bits((uint8_t *)reg_coils_buf, reg_index, 1); + mb_util_set_bits(reg_buffer, reg_index - (address - reg_coils_start), 1, result); + reg_index++; + coils--; + } + } + // Send an event to notify application task about event + (void)mbc_slave_send_param_access_notification(ctx, MB_EVENT_COILS_RD); + (void)mbc_slave_send_param_info(ctx, MB_EVENT_COILS_RD, address, + (uint8_t *)(coils_data_buf), n_coils); + } else { + status = MB_EINVAL; + } + break; + case MB_REG_WRITE: + if (it->access != MB_ACCESS_RO) { + CRITICAL_SECTION(inst->lock) + { + while (coils > 0) { + uint8_t result = mb_util_get_bits(reg_buffer, + reg_index - (address - reg_coils_start), 1); + mb_util_set_bits((uint8_t *)reg_coils_buf, reg_index, 1, result); + reg_index++; + coils--; + } + } + // Send an event to notify application task about event + (void)mbc_slave_send_param_access_notification(ctx, MB_EVENT_COILS_WR); + (void)mbc_slave_send_param_info(ctx, MB_EVENT_COILS_WR, address, + (uint8_t *)coils_data_buf, n_coils); + } else { + status = MB_EINVAL; + } + break; + } // switch ( mode ) + } else { + // If the configuration or input parameters are incorrect then return error to stack + status = MB_ENOREG; + } + return status; +} + +// Callback function for reading of MB Discrete Input Registers +mb_err_enum_t mbc_reg_discrete_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_discrete) +{ + void *ctx = (void *)MB_SLAVE_GET_IFACE_FROM_BASE(inst); + MB_RETURN_ON_FALSE(reg_buffer, MB_EINVAL, TAG, "Slave stack call failed."); + mb_err_enum_t status = MB_ENOERR; + uint16_t reg_index; + uint16_t reg_bit_index; + uint16_t n_reg; + uint8_t *discrete_input_buf; + // It already plus one in modbus function method. + address--; + mb_descr_entry_t *it = mbc_slave_find_reg_descriptor(ctx, MB_PARAM_DISCRETE, address, n_discrete); + if (it) { + uint16_t reg_discrete_start = (uint16_t)it->start_offset; // MB offset of registers + n_reg = (n_discrete >> 3) + 1; + discrete_input_buf = (uint8_t *)it->p_data; // the storage address + reg_index = (uint16_t) (address - reg_discrete_start) / 8; // Get register index in the buffer for bit number + reg_bit_index = (uint16_t)(address - reg_discrete_start) % 8; // Get bit index + uint8_t *temp_buf = &discrete_input_buf[reg_index]; + CRITICAL_SECTION(inst->lock) + { + while (n_reg > 0) { + *reg_buffer++ = mb_util_get_bits(&discrete_input_buf[reg_index++], reg_bit_index, 8); + n_reg--; + } + } + reg_buffer--; + // Last discrete + n_discrete = n_discrete % 8; + // Filling zero to high bit + *reg_buffer = *reg_buffer << (8 - n_discrete); + *reg_buffer = *reg_buffer >> (8 - n_discrete); + // Send an event to notify application task about event + (void)mbc_slave_send_param_access_notification(ctx, MB_EVENT_DISCRETE_RD); + (void)mbc_slave_send_param_info(ctx, MB_EVENT_DISCRETE_RD, address, + (uint8_t *)temp_buf, n_discrete); + } else { + status = MB_ENOREG; + } + return status; +} diff --git a/modbus/mb_controller/common/esp_modbus_slave_serial.c b/modbus/mb_controller/common/esp_modbus_slave_serial.c new file mode 100644 index 0000000..b28cc26 --- /dev/null +++ b/modbus/mb_controller/common/esp_modbus_slave_serial.c @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_err.h" // for esp_err_t +#include "sdkconfig.h" // for KConfig defines +#include "mbc_slave.h" // for slave interface define +#include "esp_modbus_slave.h" // for public slave defines +#include "mbc_serial_slave.h" // for public interface defines + +#include "mb_port_types.h" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +/** + * Initialization of Modbus Serial slave controller + */ +esp_err_t mbc_slave_create_serial(mb_communication_info_t *config, void **handler) +{ + void *ctx = NULL; + esp_err_t error = ESP_ERR_NOT_SUPPORTED; + switch(config->mode) +{ + case MB_RTU: + case MB_ASCII: + // Call constructor function of actual port implementation + error = mbc_serial_slave_create(config, &ctx); + break; + default: + return ESP_ERR_NOT_SUPPORTED; + } + if ((ctx) && (error == ESP_OK)) { + mbc_slave_init_iface(ctx); + *handler = ctx; + } + return error; +} + +#endif \ No newline at end of file diff --git a/modbus/mb_controller/common/esp_modbus_slave_tcp.c b/modbus/mb_controller/common/esp_modbus_slave_tcp.c new file mode 100644 index 0000000..9a1945a --- /dev/null +++ b/modbus/mb_controller/common/esp_modbus_slave_tcp.c @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_err.h" // for esp_err_t +#include "esp_modbus_slave.h" // for public slave defines +#include "mbc_tcp_slave.h" // for public interface defines + +#include "sdkconfig.h" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +/** + * Initialization of Modbus TCP Slave controller + */ +esp_err_t mbc_slave_create_tcp(mb_communication_info_t *config, void **handler) +{ + void *ctx = NULL; + esp_err_t error = mbc_tcp_slave_create(config, &ctx); + + if ((ctx) && (error == ESP_OK)) { + mbc_slave_init_iface(ctx); + *handler = ctx; + } + return error; +} + +#endif diff --git a/modbus/mb_controller/common/include/esp_modbus_common.h b/modbus/mb_controller/common/include/esp_modbus_common.h new file mode 100644 index 0000000..8375185 --- /dev/null +++ b/modbus/mb_controller/common/include/esp_modbus_common.h @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "driver/uart.h" // for UART types +#include "sdkconfig.h" + +#if CONFIG_FMB_EXT_TYPE_SUPPORT +#include "mb_endianness_utils.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#include "port_common.h" + +#if __has_include("esp_check.h") +#include "esp_check.h" +#include "esp_log.h" + +#include + +#include "mb_port_types.h" + +#define MB_RETURN_ON_FALSE(a, err_code, tag, format, ...) ESP_RETURN_ON_FALSE(a, err_code, tag, format __VA_OPT__(,) __VA_ARGS__) + +#else + +// if cannot include esp_check then use custom check macro + +#define MB_RETURN_ON_FALSE(a, err_code, tag, format, ...) do { \ + if (!(a)) { \ + ESP_LOGE(tag, "%s(%d): " format, __FUNCTION__, __LINE__ __VA_OPT__(,) __VA_ARGS__); \ + return err_code; \ + } \ +} while(0) + +#endif + +#define MB_SLAVE_ADDR_PLACEHOLDER (0xFF) +#define MB_CONTROLLER_STACK_SIZE (CONFIG_FMB_CONTROLLER_STACK_SIZE) // Stack size for Modbus controller +#define MB_CONTROLLER_PRIORITY (CONFIG_FMB_PORT_TASK_PRIO - 1) // priority of MB controller task +#define MB_PORT_TASK_AFFINITY (CONFIG_FMB_PORT_TASK_AFFINITY) + +// Default port defines +#define MB_PAR_INFO_TOUT (10) // Timeout for get parameter info +#define MB_PARITY_NONE (UART_PARITY_DISABLE) +#define MB_SECTION(lock) CRITICAL_SECTION(lock) {} + +// The Macros below handle the endianness while transfer N byte data into buffer +#define _XFER_4_RD(dst, src) { \ + *(uint8_t *)(dst)++ = *(uint8_t *)(src + 1); \ + *(uint8_t *)(dst)++ = *(uint8_t *)(src + 0); \ + *(uint8_t *)(dst)++ = *(uint8_t *)(src + 3); \ + *(uint8_t *)(dst)++ = *(uint8_t *)(src + 2); \ + (src) += 4; \ +} + +#define _XFER_2_RD(dst, src) { \ + *(uint8_t *)(dst)++ = *(uint8_t *)(src + 1); \ + *(uint8_t *)(dst)++ = *(uint8_t *)(src + 0); \ + (src) += 2; \ +} + +#define _XFER_4_WR(dst, src) { \ + *(uint8_t *)(dst + 1) = *(uint8_t *)(src)++; \ + *(uint8_t *)(dst + 0) = *(uint8_t *)(src)++; \ + *(uint8_t *)(dst + 3) = *(uint8_t *)(src)++; \ + *(uint8_t *)(dst + 2) = *(uint8_t *)(src)++ ; \ +} + +#define _XFER_2_WR(dst, src) { \ + *(uint8_t *)(dst + 1) = *(uint8_t *)(src)++; \ + *(uint8_t *)(dst + 0) = *(uint8_t *)(src)++; \ +} + +/** + * @brief Types of actual Modbus implementation + */ +typedef enum +{ + MB_PORT_SERIAL_MASTER = 0x00, /*!< Modbus port type serial master. */ + MB_PORT_SERIAL_SLAVE, /*!< Modbus port type serial slave. */ + MB_PORT_TCP_MASTER, /*!< Modbus port type TCP master. */ + MB_PORT_TCP_SLAVE, /*!< Modbus port type TCP slave. */ + MB_PORT_COUNT, /*!< Modbus port count. */ + MB_PORT_INACTIVE = 0xFF +} mb_port_type_t; + +/** + * @brief Event group for parameters notification + */ +typedef enum +{ + MB_EVENT_NO_EVENTS = 0x00, + MB_EVENT_HOLDING_REG_WR = BIT0, /*!< Modbus Event Write Holding registers. */ + MB_EVENT_HOLDING_REG_RD = BIT1, /*!< Modbus Event Read Holding registers. */ + MB_EVENT_INPUT_REG_RD = BIT3, /*!< Modbus Event Read Input registers. */ + MB_EVENT_COILS_WR = BIT4, /*!< Modbus Event Write Coils. */ + MB_EVENT_COILS_RD = BIT5, /*!< Modbus Event Read Coils. */ + MB_EVENT_DISCRETE_RD = BIT6, /*!< Modbus Event Read Discrete bits. */ + MB_EVENT_STACK_STARTED = BIT7, /*!< Modbus Event Stack started */ + MB_EVENT_STACK_CONNECTED = BIT8 /*!< Modbus Event Stack started */ +} mb_event_group_t; + +/** + * @brief Type of Modbus parameter + */ +typedef enum { + MB_PARAM_HOLDING = 0x00, /*!< Modbus Holding register. */ + MB_PARAM_INPUT, /*!< Modbus Input register. */ + MB_PARAM_COIL, /*!< Modbus Coils. */ + MB_PARAM_DISCRETE, /*!< Modbus Discrete bits. */ + MB_PARAM_COUNT, + MB_PARAM_UNKNOWN = 0xFF +} mb_param_type_t; + +typedef enum _mb_comm_mode mb_mode_type_t; + +typedef struct mb_base_t mb_base_t; + +/*! + * \brief Modbus TCP type of address for communication. + */ +typedef enum _addr_type_enum mb_tcp_addr_type_t; + +/*! + * \brief Modbus TCP communication options structure. + */ +typedef struct _port_tcp_opts mb_tcp_opts_t; + +/*! + * \brief Modbus serial communication options structure. + */ +typedef struct _port_serial_opts mb_serial_opts_t; + +/*! + * \brief Modbus common communication options structure. + */ +typedef struct _port_common_opts mb_common_opts_t; + +/** + * @brief Device communication structure to setup Modbus controller + */ +typedef union +{ + mb_comm_mode_t mode; /*!< mode option to check the communication object type*/ + mb_common_opts_t common_opts; /*!< Common options for communication object. */ +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + mb_tcp_opts_t tcp_opts; /*!< tcp options for communication object */ +#endif +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + mb_serial_opts_t ser_opts; /*!< serial options for communication object */ +#endif +} mb_communication_info_t; + +/** + * common interface method types + */ +typedef esp_err_t (*iface_create_fp)(mb_communication_info_t*, void **); /*!< Interface method create */ +typedef esp_err_t (*iface_method_default_fp)(void *ctx); /*!< Interface method default prototype */ + +#ifdef __cplusplus +} +#endif + diff --git a/modbus/mb_controller/common/include/esp_modbus_master.h b/modbus/mb_controller/common/include/esp_modbus_master.h new file mode 100644 index 0000000..cfe86f0 --- /dev/null +++ b/modbus/mb_controller/common/include/esp_modbus_master.h @@ -0,0 +1,472 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include // for standard int types definition +#include // for NULL and std defines +#include "soc/soc.h" // for BITN definitions +#include "esp_modbus_common.h" // for common types + +#ifdef __cplusplus +extern "C" { +#endif + +#define MB_MASTER_ASSERT(con) do { \ + if (!(con)) { ESP_LOGE(TAG, "assert errno:%d, errno_str: !(%s)", errno, strerror(errno)); assert(0 && #con); } \ + } while (0) + +#define MB_MASTER_GET_IFACE(pctx) (__extension__( \ +{ \ + MB_MASTER_ASSERT((pctx)); \ + ((mbm_controller_iface_t*)pctx); \ +} \ +)) + +#define MB_MASTER_GET_OPTS(pctx) (&MB_MASTER_GET_IFACE(pctx)->opts) + +#define MB_MASTER_IS_ACTIVE(pctx) ((bool)(MB_MASTER_GET_IFACE(pctx)->is_active)) + +#define MB_MASTER_GET_IFACE_FROM_BASE(pinst) (__extension__( \ +{ \ + MB_MASTER_ASSERT(pinst); \ + mb_base_t *pbase = (mb_base_t *)pinst; \ + MB_RETURN_ON_FALSE(pbase->descr.parent, MB_EILLSTATE, TAG, "Master interface is not correctly initialized."); \ + ((mbm_controller_iface_t*)pbase->descr.parent); \ +} \ +)) + +/*! + * \brief The macro to access arrays of elements for type conversion. + */ +#define MB_EACH_ELEM(psrc, pdest, arr_size, elem_size) \ +(int i = 0; (i < (arr_size / elem_size)); i++, pdest += elem_size, psrc += elem_size) + +/*! + * \brief Modbus descriptor table parameter type defines. + */ +typedef enum { + PARAM_TYPE_U8 = 0x00, /*!< Unsigned 8 */ + PARAM_TYPE_U16 = 0x01, /*!< Unsigned 16 */ + PARAM_TYPE_U32 = 0x02, /*!< Unsigned 32 */ + PARAM_TYPE_FLOAT = 0x03, /*!< Float type */ + PARAM_TYPE_ASCII = 0x04, /*!< ASCII type */ + PARAM_TYPE_BIN = 0x07, /*!< BIN type */ + PARAM_TYPE_I8_A = 0x0A, /*!< I8 signed integer in high byte of register */ + PARAM_TYPE_I8_B = 0x0B, /*!< I8 signed integer in low byte of register */ + PARAM_TYPE_U8_A = 0x0C, /*!< U8 unsigned integer written to hi byte of register */ + PARAM_TYPE_U8_B = 0x0D, /*!< U8 unsigned integer written to low byte of register */ + PARAM_TYPE_I16_AB = 0x0E, /*!< I16 signed integer, big endian */ + PARAM_TYPE_I16_BA = 0x0F, /*!< I16 signed integer, little endian */ + PARAM_TYPE_U16_AB = 0x10, /*!< U16 unsigned integer, big endian*/ + PARAM_TYPE_U16_BA = 0x11, /*!< U16 unsigned integer, little endian */ + PARAM_TYPE_I32_ABCD = 0x12, /*!< I32 ABCD signed integer, big endian */ + PARAM_TYPE_I32_CDAB = 0x13, /*!< I32 CDAB signed integer, big endian, reversed register order */ + PARAM_TYPE_I32_BADC = 0x14, /*!< I32 BADC signed integer, little endian, reversed register order */ + PARAM_TYPE_I32_DCBA = 0x15, /*!< I32 DCBA signed integer, little endian */ + PARAM_TYPE_U32_ABCD = 0x16, /*!< U32 ABCD unsigned integer, big endian */ + PARAM_TYPE_U32_CDAB = 0x17, /*!< U32 CDAB unsigned integer, big endian, reversed register order */ + PARAM_TYPE_U32_BADC = 0x18, /*!< U32 BADC unsigned integer, little endian, reversed register order */ + PARAM_TYPE_U32_DCBA = 0x19, /*!< U32 DCBA unsigned integer, little endian */ + PARAM_TYPE_FLOAT_ABCD = 0x1A, /*!< Float ABCD floating point, big endian */ + PARAM_TYPE_FLOAT_CDAB = 0x1B, /*!< Float CDAB floating point big endian, reversed register order */ + PARAM_TYPE_FLOAT_BADC = 0x1C, /*!< Float BADC floating point, little endian, reversed register order */ + PARAM_TYPE_FLOAT_DCBA = 0x1D, /*!< Float DCBA floating point, little endian */ + PARAM_TYPE_I64_ABCDEFGH = 0x1E, /*!< I64, ABCDEFGH signed integer, big endian */ + PARAM_TYPE_I64_HGFEDCBA = 0x1F, /*!< I64, HGFEDCBA signed integer, little endian */ + PARAM_TYPE_I64_GHEFCDAB = 0x20, /*!< I64, GHEFCDAB signed integer, big endian, reversed register order */ + PARAM_TYPE_I64_BADCFEHG = 0x21, /*!< I64, BADCFEHG signed integer, little endian, reversed register order */ + PARAM_TYPE_U64_ABCDEFGH = 0x22, /*!< U64, ABCDEFGH unsigned integer, big endian */ + PARAM_TYPE_U64_HGFEDCBA = 0x23, /*!< U64, HGFEDCBA unsigned integer, little endian */ + PARAM_TYPE_U64_GHEFCDAB = 0x24, /*!< U64, GHEFCDAB unsigned integer, big endian, reversed register order */ + PARAM_TYPE_U64_BADCFEHG = 0x25, /*!< U64, BADCFEHG unsigned integer, little endian, reversed register order */ + PARAM_TYPE_DOUBLE_ABCDEFGH = 0x26, /*!< Double ABCDEFGH floating point, big endian*/ + PARAM_TYPE_DOUBLE_HGFEDCBA = 0x27, /*!< Double HGFEDCBA floating point, little endian*/ + PARAM_TYPE_DOUBLE_GHEFCDAB = 0x28, /*!< Double GHEFCDAB floating point, big endian, reversed register order */ + PARAM_TYPE_DOUBLE_BADCFEHG = 0x29 /*!< Double BADCFEHG floating point, little endian, reversed register order */ +} mb_descr_type_t; + +/*! + * \brief Modbus descriptor table parameter size in bytes. + */ +typedef enum { + PARAM_SIZE_U8 = 0x01, /*!< Unsigned 8 */ + PARAM_SIZE_U8_REG = 0x02, /*!< Unsigned 8, register value */ + PARAM_SIZE_I8_REG = 0x02, /*!< Signed 8, register value */ + PARAM_SIZE_I16 = 0x02, /*!< Unsigned 16 */ + PARAM_SIZE_U16 = 0x02, /*!< Unsigned 16 */ + PARAM_SIZE_I32 = 0x04, /*!< Signed 32 */ + PARAM_SIZE_U32 = 0x04, /*!< Unsigned 32 */ + PARAM_SIZE_FLOAT = 0x04, /*!< Float 32 size */ + PARAM_SIZE_ASCII = 0x08, /*!< ASCII size default*/ + PARAM_SIZE_ASCII24 = 0x18, /*!< ASCII24 size */ + PARAM_SIZE_I64 = 0x08, /*!< Signed integer 64 size */ + PARAM_SIZE_U64 = 0x08, /*!< Unsigned integer 64 size */ + PARAM_SIZE_DOUBLE = 0x08, /*!< Double 64 size */ + PARAM_MAX_SIZE +} mb_descr_size_t; + +/*! + * \brief Modbus parameter options for description table (associated with the characteristic). + * and can be used in user application to process data. + */ +typedef union { + struct { + int opt1; /*!< Parameter option1 */ + int opt2; /*!< Parameter option2 */ + int opt3; /*!< Parameter option3 */ + }; /*!< Parameter options version 1 */ + struct { + int min; /*!< Parameter minimum value */ + int max; /*!< Parameter maximum value */ + int step; /*!< Step of parameter change tracking */ + }; /*!< Parameter options version 2 */ +} mb_parameter_opt_t; + +/** + * @brief Permissions for the characteristics + */ +typedef enum { + PAR_PERMS_READ = 1 << BIT0, /**< the characteristic of the device are readable */ + PAR_PERMS_WRITE = 1 << BIT1, /**< the characteristic of the device are writable*/ + PAR_PERMS_TRIGGER = 1 << BIT2, /**< the characteristic of the device are triggerable */ + PAR_PERMS_READ_WRITE = PAR_PERMS_READ | PAR_PERMS_WRITE, /**< the characteristic of the device are readable & writable */ + PAR_PERMS_READ_TRIGGER = PAR_PERMS_READ | PAR_PERMS_TRIGGER, /**< the characteristic of the device are readable & triggerable */ + PAR_PERMS_WRITE_TRIGGER = PAR_PERMS_WRITE | PAR_PERMS_TRIGGER, /**< the characteristic of the device are writable & triggerable */ + PAR_PERMS_READ_WRITE_TRIGGER = PAR_PERMS_READ_WRITE | PAR_PERMS_TRIGGER, /**< the characteristic of the device are readable & writable & triggerable */ +} mb_param_perms_t; + +/** + * @brief Characteristics descriptor type is used to describe characteristic and + * link it with Modbus parameters that reflect its data. + */ +typedef struct { + uint16_t cid; /*!< Characteristic cid */ + const char * param_key; /*!< The key (name) of the parameter */ + const char * param_units; /*!< The physical units of the parameter */ + uint8_t mb_slave_addr; /*!< Slave address of device in the Modbus segment */ + mb_param_type_t mb_param_type; /*!< Type of modbus parameter */ + uint16_t mb_reg_start; /*!< This is the Modbus register address. This is the 0 based value. */ + uint16_t mb_size; /*!< Size of mb parameter in registers */ + uint32_t param_offset; /*!< Parameter name (OFFSET in the parameter structure or address of instance) */ + mb_descr_type_t param_type; /*!< Float, U8, U16, U32, ASCII, etc. */ + mb_descr_size_t param_size; /*!< Number of bytes in the parameter. */ + mb_parameter_opt_t param_opts; /*!< Parameter options used to check limits and etc. */ + mb_param_perms_t access; /*!< Access permissions based on mode */ +} mb_parameter_descriptor_t; + +/** + * @brief Modbus register request type structure + */ +typedef struct { + uint8_t slave_addr; /*!< Modbus slave address */ + uint8_t command; /*!< Modbus command to send */ + uint16_t reg_start; /*!< Modbus start register */ + uint16_t reg_size; /*!< Modbus number of registers */ +} mb_param_request_t; + +/** + * @brief Initialize Modbus controller and stack for TCP port + * + * @param[out] ctx pointer to master interface structure + * @param[in] config - the pointer to stack configuration structure + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + * - ESP_ERR_NOT_SUPPORTED Port type not supported + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_master_create_tcp(mb_communication_info_t *config, void ** ctx); + +/** + * @brief Initialize Modbus Master controller and stack for Serial port + * + * @param[out] ctx pointer to master interface structure + * @param[in] config the pointer to configuration structure + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + * - ESP_ERR_NOT_SUPPORTED Port type not supported + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_master_create_serial(mb_communication_info_t *config, void ** ctx); + +/** + * @brief Deletes Modbus controller and stack engine + * + * @param[in] ctx context pointer of the initialized modbus interface + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_STATE Parameter error + */ +esp_err_t mbc_master_delete(void *ctx); + +/** + * @brief Critical section lock function for parameter access + * + * @param[in] ctx pointer to master interface structure + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_master_lock(void *ctx); + +/** + * @brief Critical section unlock function for parameter access + * + * @param[in] ctx pointer to master interface structure + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_master_unlock(void *ctx); + +/** + * @brief Starts Modbus communication stack + * + * @param[in] ctx context pointer of the initialized modbus master interface structure + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Modbus stack start error + */ +esp_err_t mbc_master_start(void *ctx); + +/** + * @brief Stops Modbus communication stack + * + * @param[in] ctx context pointer of the initialized modbus interface + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Modbus stack stop error + */ +esp_err_t mbc_master_stop(void *ctx); + +/***************************** Specific interface functions ******************************************** + * Interface functions below provide basic methods to read/write access to slave devices in Modbus + * segment as well as API to read specific supported characteristics linked to Modbus parameters + * of devices in Modbus network. +*******************************************************************************************************/ + +/** + * @brief Assign parameter description table for Modbus controller interface. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param[in] descriptor pointer to parameter description table + * @param num_elements number of elements in the table + * + * @return + * - esp_err_t ESP_OK - set descriptor successfully + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument in function call + */ +esp_err_t mbc_master_set_descriptor(void *ctx, const mb_parameter_descriptor_t *descriptor, const uint16_t num_elements); + +/** + * @brief Send data request as defined in parameter request, waits response + * from slave and returns status of command execution. This function provides standard way + * for read/write access to Modbus devices in the network. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param[in] request pointer to request structure of type mb_param_request_t + * @param[in] data_ptr pointer to data buffer to send or received data (dependent of command field in request) + * + * @return + * - esp_err_t ESP_OK - request was successful + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument of function + * - esp_err_t ESP_ERR_INVALID_RESPONSE - an invalid response from slave + * - esp_err_t ESP_ERR_TIMEOUT - operation timeout or no response from slave + * - esp_err_t ESP_ERR_NOT_SUPPORTED - the request command is not supported by slave + * - esp_err_t ESP_FAIL - slave returned an exception or other failure + */ +esp_err_t mbc_master_send_request(void *ctx, mb_param_request_t *request, void *data_ptr); + +/** + * @brief Get information about supported characteristic defined as cid. Uses parameter description table to get + * this information. The function will check if characteristic defined as a cid parameter is supported + * and returns its description in param_info. Returns ESP_ERR_NOT_FOUND if characteristic is not supported. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param[in] cid characteristic id + * @param param_info pointer to pointer of characteristic data. + * + * @return + * - esp_err_t ESP_OK - request was successful and buffer contains the supported characteristic name + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument of function + * - esp_err_t ESP_ERR_NOT_FOUND - the characteristic (cid) not found + * - esp_err_t ESP_FAIL - unknown error during lookup table processing +*/ +esp_err_t mbc_master_get_cid_info(void *ctx, uint16_t cid, const mb_parameter_descriptor_t** param_info); + +/** + * @brief Read parameter from modbus slave device whose name is defined by name and has cid. + * The additional data for request is taken from parameter description (lookup) table. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param[in] cid id of the characteristic for parameter + * @param[out] value pointer to data buffer of parameter + * @param[out] type parameter type associated with the name returned from parameter description table. + * + * @return + * - esp_err_t ESP_OK - request was successful and value buffer contains + * representation of actual parameter data from slave + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument of function or parameter descriptor + * - esp_err_t ESP_ERR_INVALID_RESPONSE - an invalid response from slave + * - esp_err_t ESP_ERR_INVALID_STATE - invalid state during data processing or allocation failure + * - esp_err_t ESP_ERR_NOT_FOUND - the requested slave is not found (not connected or not configured) + * - esp_err_t ESP_ERR_TIMEOUT - operation timed out and no response from slave + * - esp_err_t ESP_ERR_NOT_SUPPORTED - the request command is not supported by slave + * - esp_err_t ESP_ERR_NOT_FOUND - the parameter is not found in the parameter description table + * - esp_err_t ESP_FAIL - slave returned an exception or other failure +*/ +esp_err_t mbc_master_get_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type); + +/** + * @brief Read parameter from modbus slave device whose name is defined by name and has cid. + * The additional data for request is taken from parameter description (lookup) table. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param[in] cid id of the characteristic for parameter + * @param[in] uid unit identificator of the slave to set parameter + * @param[out] value pointer to data buffer of parameter + * @param[out] type parameter type associated with the name returned from parameter description table. + * + * @return + * - esp_err_t ESP_OK - request was successful and value buffer contains + * representation of actual parameter data from slave + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument of function or parameter descriptor + * - esp_err_t ESP_ERR_INVALID_RESPONSE - an invalid response from slave + * - esp_err_t ESP_ERR_INVALID_STATE - invalid state during data processing or allocation failure + * - esp_err_t ESP_ERR_NOT_FOUND - the requested slave is not found (not connected or not configured) + * - esp_err_t ESP_ERR_TIMEOUT - operation timed out and no response from slave + * - esp_err_t ESP_ERR_NOT_SUPPORTED - the request command is not supported by slave + * - esp_err_t ESP_ERR_NOT_FOUND - the parameter is not found in the parameter description table + * - esp_err_t ESP_FAIL - slave returned an exception or other failure +*/ +esp_err_t mbc_master_get_parameter_with(void *ctx, uint16_t cid, uint8_t uid, uint8_t *value, uint8_t *type); + +/** + * @brief Set characteristic's value defined as a name and cid parameter. + * The additional data for cid parameter request is taken from master parameter lookup table. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param[in] cid id of the characteristic for parameter + * @param[out] value pointer to data buffer of parameter (actual representation of json value field in binary form) + * @param[out] type pointer to parameter type associated with the name returned from parameter lookup table. + * + * @return + * - esp_err_t ESP_OK - request was successful and value was saved in the slave device registers + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument of function or parameter descriptor + * - esp_err_t ESP_ERR_INVALID_RESPONSE - an invalid response from slave during processing of parameter + * - esp_err_t ESP_ERR_INVALID_STATE - invalid state during data processing or allocation failure + * - esp_err_t ESP_ERR_NOT_FOUND - the requested slave is not found (not connected or not configured) + * - esp_err_t ESP_ERR_TIMEOUT - operation timed out and no response from slave + * - esp_err_t ESP_ERR_NOT_SUPPORTED - the request command is not supported by slave + * - esp_err_t ESP_FAIL - slave returned an exception or other failure +*/ +esp_err_t mbc_master_set_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type); + +/** + * @brief Set characteristic's value defined as a name and cid parameter. + * The additional data for cid parameter request is taken from master parameter lookup table. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param[in] cid id of the characteristic for parameter + * @param[in] uid unit identificator of the slave to set parameter + * @param[out] value pointer to data buffer of parameter (actual representation of json value field in binary form) + * @param[out] type pointer to parameter type associated with the name returned from parameter lookup table. + * + * @return + * - esp_err_t ESP_OK - request was successful and value was saved in the slave device registers + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument of function or parameter descriptor + * - esp_err_t ESP_ERR_INVALID_RESPONSE - an invalid response from slave during processing of parameter + * - esp_err_t ESP_ERR_INVALID_STATE - invalid state during data processing or allocation failure + * - esp_err_t ESP_ERR_NOT_FOUND - the requested slave is not found (not connected or not configured) + * - esp_err_t ESP_ERR_TIMEOUT - operation timed out and no response from slave + * - esp_err_t ESP_ERR_NOT_SUPPORTED - the request command is not supported by slave + * - esp_err_t ESP_FAIL - slave returned an exception or other failure +*/ +esp_err_t mbc_master_set_parameter_with(void *ctx, uint16_t cid, uint8_t uid, uint8_t *value, uint8_t *type); + +/** + * @brief Holding register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] mode - parameter access mode (MB_REG_READ, MB_REG_WRITE) + * @param[in] n_regs - number of registers + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_holding_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_regs, mb_reg_mode_enum_t mode) __attribute__ ((weak)); + +/** + * @brief Input register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] n_regs - number of registers + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_input_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_regs) __attribute__ ((weak)); + +/** + * @brief Discrete register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] n_discrete - number of discrete registers + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_discrete_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_discrete) __attribute__ ((weak)); + +/** + * @brief Coil register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] n_coils - number of coil registers + * @param[in] mode - parameter access mode (MB_REG_READ, MB_REG_WRITE) + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_coils_master_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_coils, mb_reg_mode_enum_t mode) __attribute__ ((weak)); + +/** + * @brief The helper function to set data of parameters according to its type + * + * @param[in] dest the destination address of the parameter + * @param[in] src the source address of the parameter + * @param[out] param_type type of parameter from data dictionary + * @param[out] param_size the storage size of the characteristic (in bytes). + * Describes the size of data to keep into data instance during mapping. + * + * @return + * - esp_err_t ESP_OK - request was successful and value was saved in the slave device registers + * - esp_err_t ESP_ERR_INVALID_ARG - invalid argument of function or parameter descriptor + * - esp_err_t ESP_ERR_NOT_SUPPORTED - the request command is not supported by slave +*/ +esp_err_t mbc_master_set_param_data(void* dest, void* src, mb_descr_type_t param_type, size_t param_size); + +#ifdef __cplusplus +} +#endif + diff --git a/modbus/mb_controller/common/include/esp_modbus_slave.h b/modbus/mb_controller/common/include/esp_modbus_slave.h new file mode 100644 index 0000000..01c5472 --- /dev/null +++ b/modbus/mb_controller/common/include/esp_modbus_slave.h @@ -0,0 +1,264 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +// Public interface header for slave +#include // for standard int types definition +#include // for NULL and std defines +#include "soc/soc.h" // for BITN definitions +#include "freertos/FreeRTOS.h" // for task creation and queues access +#include "freertos/event_groups.h" // for event groups +#include "esp_modbus_common.h" // for common types + +#ifdef __cplusplus +extern "C" { +#endif + +#define REG_SIZE(type, nregs) ((type == MB_PARAM_INPUT) || (type == MB_PARAM_HOLDING)) ? (nregs >> 1) : (nregs << 3) + +#define MB_SLAVE_ASSERT(con) do { \ + if (!(con)) { ESP_LOGE(TAG, "assert errno:%d, errno_str: !(%s)", errno, strerror(errno)); assert(0 && #con); } \ + } while (0) + +#define MB_SLAVE_GET_IFACE(pctx) (__extension__( \ +{ \ + MB_SLAVE_ASSERT((pctx)); \ + ((mbs_controller_iface_t*)pctx); \ +} \ +)) + +#define MB_SLAVE_GET_OPTS(pctx) (&MB_SLAVE_GET_IFACE(pctx)->opts) + +#define MB_SLAVE_IS_ACTIVE(pctx) ((bool)(MB_SLAVE_GET_IFACE(pctx)->is_active)) + +#define MB_SLAVE_GET_IFACE_FROM_BASE(pinst) (__extension__( \ +{ \ + MB_SLAVE_ASSERT(pinst); \ + mb_base_t *pbase = (mb_base_t*)pinst; \ + MB_RETURN_ON_FALSE(pbase->descr.parent, MB_EILLSTATE, TAG, "Slave interface is not correctly initialized."); \ + ((mbs_controller_iface_t*)pbase->descr.parent); \ +} \ +)) + +/** + * @brief Parameter access event information type + */ +typedef struct { + uint32_t time_stamp; /*!< Timestamp of Modbus Event (uS)*/ + uint16_t mb_offset; /*!< Modbus register offset */ + mb_event_group_t type; /*!< Modbus event type */ + uint8_t *address; /*!< Modbus data storage address */ + size_t size; /*!< Modbus event register size (number of registers)*/ +} mb_param_info_t; + +/** + * @brief Area access type modificator + */ +typedef enum { + MB_ACCESS_RW = 0x0000, + MB_ACCESS_RO = 0x0001, + MB_ACCESS_WO = 0x0002 +} mb_param_access_t; + +/** + * @brief Parameter storage area descriptor + */ +typedef struct { + uint16_t start_offset; /*!< Modbus start address for area descriptor */ + mb_param_type_t type; /*!< Type of storage area descriptor */ + mb_param_access_t access; /*!< Area access type */ + void *address; /*!< Instance address for storage area descriptor */ + size_t size; /*!< Instance size for area descriptor (bytes) */ +} mb_register_area_descriptor_t; + +/** + * @brief Initialize Modbus Slave controller and stack for TCP port + * + * @param[out] ctx context pointer of the initialized modbus interface + * @param[in] config - pointer to configuration structure for the slave + * + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + * - ESP_ERR_NOT_SUPPORTED Port type not supported + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_slave_create_tcp(mb_communication_info_t *config, void **ctx); + +/** + * @brief Initialize Modbus Slave controller and stack for Serial port + * + * @param[out] ctx context pointer of the initialized modbus interface + * @param[in] config - pointer to configuration structure for the slave + * + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + * - ESP_ERR_NOT_SUPPORTED Port type not supported + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_slave_create_serial(mb_communication_info_t *config, void **ctx); + +/** + * @brief Initialize Modbus Slave controller interface handle + * + * @param[in] ctx - pointer to slave interface data structure + */ +void mbc_slave_init_iface(void *ctx); + +/** + * @brief Deletes Modbus controller and stack engine + * + * @param[in] ctx context pointer of the initialized modbus interface + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_STATE Parameter error + */ +esp_err_t mbc_slave_delete(void *ctx); + +/** + * @brief Critical section lock function for parameter access + * + * @param[in] ctx pointer to slave handle (modbus interface) + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_slave_lock(void *ctx); + +/** + * @brief Critical section unlock for parameter access + * + * @param[in] ctx pointer to slave handle (modbus interface) + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_STATE Initialization failure + */ +esp_err_t mbc_slave_unlock(void *ctx); + +/** + * @brief Start of Modbus communication stack + * + * @param[in] ctx context pointer of the initialized modbus interface + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Modbus stack start error + */ +esp_err_t mbc_slave_start(void *ctx); + +/** + * @brief Stop of Modbus communication stack + * + * @param[in] ctx context pointer of the initialized modbus interface + * + * @return + * - ESP_OK Success + * - ESP_ERR_INVALID_ARG Modbus stack stop error + */ +esp_err_t mbc_slave_stop(void *ctx); + +/** + * @brief Wait for specific event on parameter change. + * + * @param[in] ctx context pointer of the initialized modbus interface + * @param group Group event bit mask to wait for change + * + * @return + * - mb_event_group_t event bits triggered + */ +mb_event_group_t mbc_slave_check_event(void *ctx, mb_event_group_t group); + +/** + * @brief Get parameter information + * + * @param[in] ctx context pointer of the initialized modbus interface * + * @param[out] reg_info parameter info structure + * @param[in] timeout Timeout in milliseconds to read information from + * parameter queue + * + * @return + * - ESP_OK Success + * - ESP_ERR_TIMEOUT Can not get data from parameter queue + * or queue overflow + */ +esp_err_t mbc_slave_get_param_info(void *ctx, mb_param_info_t *reg_info, uint32_t timeout); + +/** + * @brief Set Modbus area descriptor + * + * @param[in] ctx context pointer of the initialized modbus interface * + * @param descr_data Modbus registers area descriptor structure + * + * @return + * - ESP_OK: The appropriate descriptor is set + * - ESP_ERR_INVALID_ARG: The argument is incorrect + */ +esp_err_t mbc_slave_set_descriptor(void *ctx, mb_register_area_descriptor_t descr_data); + +/** + * @brief Holding register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] n_regs - number of registers + * @param[in] mode - parameter access mode (MB_REG_READ, MB_REG_WRITE) + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_holding_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_regs, mb_reg_mode_enum_t mode) __attribute__ ((weak)); + +/** + * @brief Input register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] n_regs - number of registers + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_input_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_regs) __attribute__ ((weak)); + +/** + * @brief Discrete register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] n_discrete - number of discrete registers + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_discrete_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_discrete) __attribute__ ((weak)); + +/** + * @brief Coil register read/write callback function + * + * @param[in] inst the pointer of the initialized modbus base + * @param[in] reg_buffer input buffer of registers + * @param[in] address - start address of register + * @param[in] n_coils - number of discrete registers + * @param[in] mode - parameter access mode (MB_REG_READ, MB_REG_WRITE) + * + * @return + * - MB_ENOERR: Read write is successful + * - MB_ENOREG: The argument is incorrect + */ +mb_err_enum_t mbc_reg_coils_slave_cb(mb_base_t *inst, uint8_t *reg_buffer, uint16_t address, uint16_t n_coils, mb_reg_mode_enum_t mode) __attribute__ ((weak)); + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_controller/common/include/mb_endianness_utils.h b/modbus/mb_controller/common/include/mb_endianness_utils.h new file mode 100644 index 0000000..1a1c731 --- /dev/null +++ b/modbus/mb_controller/common/include/mb_endianness_utils.h @@ -0,0 +1,556 @@ +/* + * SPDX-FileCopyrightText: 2016-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +/** + * @brief Defines the constant values based on native compiler byte ordering. + */ +#define MB_BO16_0 0 +#define MB_BO16_1 1 + +#define MB_BO32_0 0 +#define MB_BO32_1 1 +#define MB_BO32_2 2 +#define MB_BO32_3 3 + +#define MB_BO64_0 0 +#define MB_BO64_1 1 +#define MB_BO64_2 2 +#define MB_BO64_3 3 +#define MB_BO64_4 4 +#define MB_BO64_5 5 +#define MB_BO64_6 6 +#define MB_BO64_7 7 + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The sized array types used for mapping of extended values + */ +typedef uint8_t val_16_arr[2]; +typedef uint8_t val_32_arr[4]; +typedef uint8_t val_64_arr[8]; + +/** + * @brief Get int8_t (low byte) value represenatation from register + * + * @return + * - int8_t value of converted from register value + */ +int8_t mb_get_int8_a(val_16_arr *pi16); + +/** + * @brief Set i8 value to the register value pointed by pi16 + * + * @return + * - uint16_t value which represents the actual hex value of the register + */ +uint16_t mb_set_int8_a(val_16_arr *pi16, int8_t i8); + +/** + * @brief Get int8_t (high byte) value from the register value pointed by pi16 + * + * @return + * - uint16_t value which represents the actual hex value of the register + */ +int8_t mb_get_int8_b(val_16_arr *pi16); + +/** + * @brief Set i8 (high byte) value from the register value pointed by pi16 + * + * @return + * - uint16_t value which represents the actual hex value of the register + */ +uint16_t mb_set_int8_b(val_16_arr *pi16, int8_t i8); + +/** + * @brief Get uint8_t (low byte) value represenatation from register poined by pu16 + * + * @return + * - uint8_t the value of converted from register value + */ +uint8_t mb_get_uint8_a(val_16_arr *pu16); + +/** + * @brief Set u8 (low byte) value into the register value pointed by pu16 + * + * @return + * - uint16_t the value which represents the actual hex value of the register + */ +uint16_t mb_set_uint8_a(val_16_arr *pu16, uint8_t u8); + +/** + * @brief Get uint8_t (high byte) value from the register value pointed by pu16 + * + * @return + * - uint16_t the value which represents the actual hex value of the register + */ +uint8_t mb_get_uint8_b(val_16_arr *pu16); + +/** + * @brief Set u8 (high byte) value into the register value pointed by pu16 + * + * @return + * - uint16_t the value which represents the actual hex value of the register + */ +uint16_t mb_set_uint8_b(val_16_arr *pu16, uint8_t u8); + +/** + * @brief Get int16_t value from the register value pointed by pu16 with ab endianness + * + * @return + * - int16_t the value which represents the converted value from register + */ +int16_t mb_get_int16_ab(val_16_arr *pi16); + +/** + * @brief Set i16 value to the register pointed by pi16 with ab endianness + * + * @return + * - int16_t the value which represents the converted value from register + */ +uint16_t mb_set_int16_ab(val_16_arr *pi16, int16_t i16); + +/** + * @brief Get uint16_t value from the register value pointed by pu16 with ab endianness + * + * @return + * - uint16_t value which represents the converted register value + */ +uint16_t mb_get_uint16_ab(val_16_arr *pu16); + +/** + * @brief Set u16 value to the register pointed by pu16 with ab endianness + * + * @return + * - uint16_t value which represents the converted value from register + */ +uint16_t mb_set_uint16_ab(val_16_arr *pu16, uint16_t u16); + +/** + * @brief Get int16_t value from the register value pointed by pu16 with ba endianness + * + * @return + * - int16_t value which represents the converted register value + */ +int16_t mb_get_int16_ba(val_16_arr *pi16); + +/** + * @brief Set i16 value to the register pointed by pi16 with ba endianness + * + * @return + * - uint16_t value which represents the converted value from register + */ +uint16_t mb_set_int16_ba(val_16_arr *pi16, int16_t i16); + +/** + * @brief Get uint16_t value from the register value pointed by pu16 with ba endianness + * + * @return + * - uint16_t value which represents the converted register value + */ +uint16_t mb_get_uint16_ba(val_16_arr *pu16); + +/** + * @brief Set u16 value to the register pointed by pu16 with ba endianness + * + * @return + * - uint16_t value which represents the converted value from register + */ +uint16_t mb_set_uint16_ba(val_16_arr *pu16, uint16_t u16); + +/** + * @brief Get int32_t value from the register value pointed by pi32 with abcd endianness + * + * @return + * - int32_t value which represents the converted register value + */ +int32_t mb_get_int32_abcd(val_32_arr *pi32); + +/** + * @brief Set i32 value to the register pointed by pi32 with abcd endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_int32_abcd(val_32_arr *pi32, int32_t i32); + +/** + * @brief Get uint32_t value from the register value pointed by pu32 with abcd endianness + * + * @return + * - uint32_t value which represents the converted register value + */ +uint32_t mb_get_uint32_abcd(val_32_arr *pu32); + +/** + * @brief Set u32 value to the register pointed by pu32 with abcd endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_uint32_abcd(val_32_arr *pu32, uint32_t u32); + +/** + * @brief Get int32_t value from the register value pointed by pi32 with badc endianness + * + * @return + * - int32_t value which represents the converted register value + */ +int32_t mb_get_int32_badc(val_32_arr *pi32); + +/** + * @brief Set i32 value to the register pointed by pi32 with badc endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_int32_badc(val_32_arr *pi32, int32_t i32); + +/** + * @brief Get uint32_t value from the register value pointed by pu32 with badc endianness + * + * @return + * - unt32_t value which represents the converted register value + */ +uint32_t mb_get_uint32_badc(val_32_arr *pu32); + +/** + * @brief Set u32 value to the register pointed by pu32 with badc endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_uint32_badc(val_32_arr *pu32, uint32_t u32); + +/** + * @brief Get int32_t value from the register value pointed by pi32 with cdab endianness + * + * @return + * - int32_t value which represents the converted register value + */ +int32_t mb_get_int32_cdab(val_32_arr *pi32); + +/** + * @brief Set i32 value to the register pointed by pi32 with cdab endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_int32_cdab(val_32_arr *pi32, int32_t i32); + +/** + * @brief Get uint32_t value from the register value pointed by pu32 with cdab endianness + * + * @return + * - int32_t value which represents the converted register value + */ +uint32_t mb_get_uint32_cdab(val_32_arr *pu32); + +/** + * @brief Set u32 value to the register pointed by pu32 with cdab endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_uint32_cdab(val_32_arr *pu32, uint32_t u32); + +/** + * @brief Get int32_t value from the register value pointed by pi32 with dcba endianness + * + * @return + * - int32_t value which represents the converted register value + */ +int32_t mb_get_int32_dcba(val_32_arr *pi32); + +/** + * @brief Set i32 value to the register pointed by pi32 with dcba endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_int32_dcba(val_32_arr *pi32, int32_t i32); + +/** + * @brief Get uint32_t value from the register value pointed by pu32 with dcba endianness + * + * @return + * - uint32_t value which represents the converted register value + */ +uint32_t mb_get_uint32_dcba(val_32_arr *pu32); + +/** + * @brief Set u32 value to the register pointed by pu32 with dcba endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_uint32_dcba(val_32_arr *pu32, uint32_t u32); + +/** + * @brief Get float value from the register pointed by pf with abcd endianness + * + * @return + * - float value which represents the converted register value + */ +float mb_get_float_abcd(val_32_arr *pf); + +/** + * @brief Set f value to the register pointed by pf with abcd endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_float_abcd(val_32_arr *pf, float f); + +/** + * @brief Get float value from the register pointed by pf with badc endianness + * + * @return + * - float value which represents the converted register value + */ +float mb_get_float_badc(val_32_arr *pf); + +/** + * @brief Set f value to the register pointed by pf with badc endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_float_badc(val_32_arr *pf, float f); + +/** + * @brief Get float value from the register pointed by pf with cdab endianness + * + * @return + * - float value which represents the converted register value + */ +float mb_get_float_cdab(val_32_arr *pf); + +/** + * @brief Set f value to the register pointed by pf with cdab endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_float_cdab(val_32_arr *pf, float f); + +/** + * @brief Get float value from the register pointed by pf with dcba endianness + * + * @return + * - float value which represents the converted register value + */ +float mb_get_float_dcba(val_32_arr *pf); + +/** + * @brief Set f value to the register pointed by pf with dcba endianness + * + * @return + * - uint32_t value which represents the converted value from register + */ +uint32_t mb_set_float_dcba(val_32_arr *pf, float f); + +/** + * @brief Get double value from the register pointed by pd with abcdefgh endianness + * + * @return + * - double value which represents the converted register value + */ +double mb_get_double_abcdefgh(val_64_arr *pd); + +/** + * @brief Set d value to the register pointed by pd with abcdefgh endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_double_abcdefgh(val_64_arr *pd, double d); + +/** + * @brief Get double value from the register pointed by pd with hgfedcba endianness + * + * @return + * - double value which represents the converted register value + */ +double mb_get_double_hgfedcba(val_64_arr *pd); + +/** + * @brief Set d value to the register pointed by pd with hgfedcba endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_double_hgfedcba(val_64_arr *pd, double d); + +/** + * @brief Get double value from the register pointed by pd with ghefcdab endianness + * + * @return + * - double value which represents the converted register value + */ +double mb_get_double_ghefcdab(val_64_arr *pd); + +/** + * @brief Set d value to the register pointed by pd with ghefcdab endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_double_ghefcdab(val_64_arr *pd, double d); + +/** + * @brief Get double value from the register pointed by pd with badcfehg endianness + * + * @return + * - double value which represents the converted register value + */ +double mb_get_double_badcfehg(val_64_arr *pd); + +/** + * @brief Set d value to the register pointed by pd with badcfehg endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_double_badcfehg(val_64_arr *pd, double d); + +/** + * @brief Get int64_t value from the register pointed by pi64 with abcdefgh endianness + * + * @return + * - int64_t value which represents the converted register value + */ +int64_t mb_get_int64_abcdefgh(val_64_arr *pi64); + +/** + * @brief Set i value to the register pointed by pi with abcdefgh endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_int64_abcdefgh(val_64_arr *pi, int64_t i); + +/** + * @brief Get int64_t value from the register pointed by pi64 with ghefcdab endianness + * + * @return + * - int64_t value which represents the converted register value + */ +int64_t mb_get_int64_ghefcdab(val_64_arr *pi64); + +/** + * @brief Set i value to the register pointed by pi with ghefcdab endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_int64_ghefcdab(val_64_arr *pi, int64_t i); + +/** + * @brief Get int64_t value from the register pointed by pi64 with hgfedcba endianness + * + * @return + * - int64_t value which represents the converted register value + */ +int64_t mb_get_int64_hgfedcba(val_64_arr *pi64); + +/** + * @brief Set i value to the register pointed by pi with hgfedcba endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_int64_hgfedcba(val_64_arr *pi, int64_t i); + +/** + * @brief Get int64_t value from the register pointed by pi64 with badcfehg endianness + * + * @return + * - int64_t value which represents the converted register value + */ +int64_t mb_get_int64_badcfehg(val_64_arr *pi64); + +/** + * @brief Set i value to the register pointed by pi with badcfehg endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_int64_badcfehg(val_64_arr *pi, int64_t i); + +/** + * @brief Get uint64_t value from the register pointed by pui with abcdefgh endianness + * + * @return + * - uint64_t value which represents the converted register value + */ +uint64_t mb_get_uint64_abcdefgh(val_64_arr *pui); + +/** + * @brief Set ui value to the register pointed by pi with abcdefgh endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_uint64_abcdefgh(val_64_arr *pui, uint64_t ui); + +/** + * @brief Get uint64_t value from the register pointed by pui with hgfedcba endianness + * + * @return + * - uint64_t value which represents the converted register value + */ +uint64_t mb_get_uint64_hgfedcba(val_64_arr *pui); + +/** + * @brief Set ui value to the register pointed by pui with hgfedcba endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_uint64_hgfedcba(val_64_arr *pui, uint64_t ui); + +/** + * @brief Get uint64_t value from the register pointed by pui with ghefcdab endianness + * + * @return + * - uint64_t value which represents the converted register value + */ +uint64_t mb_get_uint64_ghefcdab(val_64_arr *pui); + +/** + * @brief Set ui value to the register pointed by pui with ghefcdab endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_uint64_ghefcdab(val_64_arr *pui, uint64_t ui); + +/** + * @brief Get uint64_t value from the register pointed by pui with badcfehg endianness + * + * @return + * - uint64_t value which represents the converted register value + */ +uint64_t mb_get_uint64_badcfehg(val_64_arr *pui); + +/** + * @brief Set ui value to the register pointed by pui with badcfehg endianness + * + * @return + * - uint64_t value which represents the converted value from register + */ +uint64_t mb_set_uint64_badcfehg(val_64_arr *pui, uint64_t ui); + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_controller/common/include/mbcontroller.h b/modbus/mb_controller/common/include/mbcontroller.h new file mode 100644 index 0000000..c873713 --- /dev/null +++ b/modbus/mb_controller/common/include/mbcontroller.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include // for standard int types definition +#include // for NULL and std defines +#include "string.h" // for strerror() +#include "errno.h" // for errno +#include "esp_err.h" // for error handling +#include "driver/uart.h" // for uart port number defines +#include "sdkconfig.h" // for KConfig options + +#include "esp_modbus_master.h" +#include "esp_modbus_slave.h" + + diff --git a/modbus/mb_controller/common/mb_endianness_utils.c b/modbus/mb_controller/common/mb_endianness_utils.c new file mode 100644 index 0000000..f758954 --- /dev/null +++ b/modbus/mb_controller/common/mb_endianness_utils.c @@ -0,0 +1,683 @@ +/* + * SPDX-FileCopyrightText: 2016-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include "mb_endianness_utils.h" + +#define INLINE inline __attribute__((always_inline)) + +static INLINE int16_t mb_get_int16_generic(int n0, int n1, val_16_arr *psrc) +{ + val_16_arr *pv = psrc; + union { + val_16_arr arr; + int16_t value; + } bov; + bov.arr[n0] = (*pv)[MB_BO16_0]; + bov.arr[n1] = (*pv)[MB_BO16_1]; + return (bov.value); +} + +static INLINE uint16_t mb_get_uint16_generic(int n0, int n1, val_16_arr *psrc) +{ + val_16_arr *pv = psrc; + union { + val_16_arr arr; + uint16_t value; + } bov; + bov.arr[n0] = (*pv)[MB_BO16_0]; + bov.arr[n1] = (*pv)[MB_BO16_1]; + return (bov.value); +} + +static INLINE uint16_t mb_set_uint16_generic(int n0, int n1, val_16_arr *pdest, uint16_t val) +{ + val_16_arr *pv = pdest; + union { + val_16_arr arr; + uint16_t value; + } bov; + bov.value = val; + (*pv)[MB_BO16_0] = bov.arr[n0]; + (*pv)[MB_BO16_1] = bov.arr[n1]; + return (*((uint16_t *)pv)); +} + +static INLINE int16_t mb_set_int16_generic(int n0, int n1, val_16_arr *pdest, int16_t val) +{ + val_16_arr *pv = pdest; + union { + val_16_arr arr; + int16_t value; + } bov; + bov.value = val; + (*pv)[MB_BO16_0] = bov.arr[n0]; + (*pv)[MB_BO16_1] = bov.arr[n1]; + return (*((uint16_t *)pv)); +} + +static INLINE uint32_t mb_get_uint32_generic(int n0, int n1, int n2, int n3, val_32_arr *psrc) +{ + val_32_arr *pv = psrc; + union { + val_32_arr arr; + uint32_t value; + } bov; + bov.arr[n0] = (*pv)[MB_BO32_0]; + bov.arr[n1] = (*pv)[MB_BO32_1]; + bov.arr[n2] = (*pv)[MB_BO32_2]; + bov.arr[n3] = (*pv)[MB_BO32_3]; + return (bov.value); +} + +static INLINE int32_t mb_get_int32_generic(int n0, int n1, int n2, int n3, val_32_arr *psrc) +{ + val_32_arr *pv = psrc; + union { + val_32_arr arr; + int32_t value; + } bov; + bov.arr[n0] = (*pv)[MB_BO32_0]; + bov.arr[n1] = (*pv)[MB_BO32_1]; + bov.arr[n2] = (*pv)[MB_BO32_2]; + bov.arr[n3] = (*pv)[MB_BO32_3]; + return (bov.value); +} + +static INLINE float mb_get_float_generic(int n0, int n1, int n2, int n3, val_32_arr *psrc) +{ + val_32_arr *pv = psrc; + union { + val_32_arr arr; + float value; + } bov; + bov.arr[n0] = (*pv)[MB_BO32_0]; + bov.arr[n1] = (*pv)[MB_BO32_1]; + bov.arr[n2] = (*pv)[MB_BO32_2]; + bov.arr[n3] = (*pv)[MB_BO32_3]; + return (bov.value); +} + +static INLINE uint32_t mb_set_int32_generic(int n0, int n1, int n2, int n3, val_32_arr *pdest, int32_t val) +{ + val_32_arr *pv = pdest; + union { + val_32_arr arr; + int32_t value; + } bov; + bov.value = val; + (*pv)[MB_BO32_0] = bov.arr[n0]; + (*pv)[MB_BO32_1] = bov.arr[n1]; + (*pv)[MB_BO32_2] = bov.arr[n2]; + (*pv)[MB_BO32_3] = bov.arr[n3]; + return (*((uint32_t *)pv)); +} + +static INLINE uint32_t mb_set_uint32_generic(int n0, int n1, int n2, int n3, val_32_arr *pdest, uint32_t val) +{ + val_32_arr *pv = pdest; + union { + val_32_arr arr; + uint32_t value; + } bov; + bov.value = val; + (*pv)[MB_BO32_0] = bov.arr[n0]; + (*pv)[MB_BO32_1] = bov.arr[n1]; + (*pv)[MB_BO32_2] = bov.arr[n2]; + (*pv)[MB_BO32_3] = bov.arr[n3]; + return (*((uint32_t *)pv)); +} + +static INLINE uint32_t mb_set_float_generic(int n0, int n1, int n2, int n3, val_32_arr *pdest, float val) +{ + val_32_arr *pv = pdest; + union { + val_32_arr arr; + float value; + } bov; + bov.value = val; + (*pv)[MB_BO32_0] = bov.arr[n0]; + (*pv)[MB_BO32_1] = bov.arr[n1]; + (*pv)[MB_BO32_2] = bov.arr[n2]; + (*pv)[MB_BO32_3] = bov.arr[n3]; + return (*((uint32_t *)pv)); +} + +static INLINE int64_t mb_get_int64_generic(int n0, int n1, int n2, int n3, int n4, int n5, int n6, int n7, val_64_arr *psrc) +{ + val_64_arr *pv64 = psrc; + union { + val_64_arr arr; + int64_t value; + } bo64; + bo64.arr[n0] = (*pv64)[MB_BO64_0]; + bo64.arr[n1] = (*pv64)[MB_BO64_1]; + bo64.arr[n2] = (*pv64)[MB_BO64_2]; + bo64.arr[n3] = (*pv64)[MB_BO64_3]; + bo64.arr[n4] = (*pv64)[MB_BO64_4]; + bo64.arr[n5] = (*pv64)[MB_BO64_5]; + bo64.arr[n6] = (*pv64)[MB_BO64_6]; + bo64.arr[n7] = (*pv64)[MB_BO64_7]; + return (bo64.value); +} + +static INLINE uint64_t mb_get_uint64_generic(int n0, int n1, int n2, int n3, int n4, int n5, int n6, int n7, val_64_arr *psrc) +{ + val_64_arr *pv64 = psrc; + union { + val_64_arr arr; + uint64_t value; + } bo64; + bo64.arr[n0] = (*pv64)[MB_BO64_0]; + bo64.arr[n1] = (*pv64)[MB_BO64_1]; + bo64.arr[n2] = (*pv64)[MB_BO64_2]; + bo64.arr[n3] = (*pv64)[MB_BO64_3]; + bo64.arr[n4] = (*pv64)[MB_BO64_4]; + bo64.arr[n5] = (*pv64)[MB_BO64_5]; + bo64.arr[n6] = (*pv64)[MB_BO64_6]; + bo64.arr[n7] = (*pv64)[MB_BO64_7]; + return (bo64.value); +} + +static INLINE double mb_get_double_generic(int n0, int n1, int n2, int n3, int n4, int n5, int n6, int n7, val_64_arr *psrc) +{ + val_64_arr *pv64 = psrc; + union { + val_64_arr arr; + double value; + } bo64; + bo64.arr[n0] = (*pv64)[MB_BO64_0]; + bo64.arr[n1] = (*pv64)[MB_BO64_1]; + bo64.arr[n2] = (*pv64)[MB_BO64_2]; + bo64.arr[n3] = (*pv64)[MB_BO64_3]; + bo64.arr[n4] = (*pv64)[MB_BO64_4]; + bo64.arr[n5] = (*pv64)[MB_BO64_5]; + bo64.arr[n6] = (*pv64)[MB_BO64_6]; + bo64.arr[n7] = (*pv64)[MB_BO64_7]; + return (bo64.value); +} + +static INLINE uint64_t mb_set_int64_generic(int n0, int n1, int n2, int n3, int n4, int n5, int n6, int n7, val_64_arr *pdest, int64_t val) +{ + val_64_arr *pv = pdest; + union { + val_64_arr arr; + int64_t value; + } bo64; + bo64.value = val; + (*pv)[MB_BO64_0] = bo64.arr[n0]; + (*pv)[MB_BO64_1] = bo64.arr[n1]; + (*pv)[MB_BO64_2] = bo64.arr[n2]; + (*pv)[MB_BO64_3] = bo64.arr[n3]; + (*pv)[MB_BO64_4] = bo64.arr[n4]; + (*pv)[MB_BO64_5] = bo64.arr[n5]; + (*pv)[MB_BO64_6] = bo64.arr[n6]; + (*pv)[MB_BO64_7] = bo64.arr[n7]; + return (*((uint64_t *)pv)); +} + +static INLINE uint64_t mb_set_uint64_generic(int n0, int n1, int n2, int n3, int n4, int n5, int n6, int n7, val_64_arr *pdest, uint64_t val) +{ + val_64_arr *pv = pdest; + union { + val_64_arr arr; + uint64_t value; + } bo64; + bo64.value = val; + (*pv)[MB_BO64_0] = bo64.arr[n0]; + (*pv)[MB_BO64_1] = bo64.arr[n1]; + (*pv)[MB_BO64_2] = bo64.arr[n2]; + (*pv)[MB_BO64_3] = bo64.arr[n3]; + (*pv)[MB_BO64_4] = bo64.arr[n4]; + (*pv)[MB_BO64_5] = bo64.arr[n5]; + (*pv)[MB_BO64_6] = bo64.arr[n6]; + (*pv)[MB_BO64_7] = bo64.arr[n7]; + return (*((uint64_t *)pv)); +} + +static INLINE uint64_t mb_set_double_generic(int n0, int n1, int n2, int n3, int n4, int n5, int n6, int n7, val_64_arr *pdest, double val) +{ + val_64_arr *pv = pdest; + union { + val_64_arr arr; + double value; + } bo64; + bo64.value = val; + (*pv)[MB_BO64_0] = bo64.arr[n0]; + (*pv)[MB_BO64_1] = bo64.arr[n1]; + (*pv)[MB_BO64_2] = bo64.arr[n2]; + (*pv)[MB_BO64_3] = bo64.arr[n3]; + (*pv)[MB_BO64_4] = bo64.arr[n4]; + (*pv)[MB_BO64_5] = bo64.arr[n5]; + (*pv)[MB_BO64_6] = bo64.arr[n6]; + (*pv)[MB_BO64_7] = bo64.arr[n7]; + return (*((uint64_t *)pv)); +} + +int8_t mb_get_int8_a(pi16) +val_16_arr *pi16; +{ + return((int8_t)(*pi16)[MB_BO16_0]); +} + +uint16_t mb_set_int8_a(pi16, i8) +val_16_arr *pi16; +int8_t i8; +{ + (*pi16)[MB_BO16_0] = (char)i8; + (*pi16)[MB_BO16_1] = 0; + return (*((uint16_t *)pi16)); +} + +int8_t mb_get_int8_b(pi16) +val_16_arr *pi16; +{ + return((int8_t)(*pi16)[MB_BO16_1]); +} + +uint16_t mb_set_int8_b(pi16, i8) +val_16_arr *pi16; +int8_t i8; +{ + (*pi16)[MB_BO16_0] = 0; + (*pi16)[MB_BO16_1] = (char)i8; + return (*((uint16_t *)pi16)); +} + +uint8_t mb_get_uint8_a(pu16) +val_16_arr *pu16; +{ + return((uint8_t)(*pu16)[MB_BO16_0]); +} + +uint16_t mb_set_uint8_a(pu16, u8) +val_16_arr *pu16; +uint8_t u8; +{ + (*pu16)[MB_BO16_0] = (char)u8; + (*pu16)[MB_BO16_1] = 0; + return (*((uint16_t *)pu16)); +} + +uint8_t mb_get_uint8_b(pu16) +val_16_arr *pu16; +{ + return((uint8_t)(*pu16)[MB_BO16_1]); +} + +uint16_t mb_set_uint8_b(pu16, u8) +val_16_arr *pu16; +uint8_t u8; +{ + (*pu16)[MB_BO16_0] = 0; + (*pu16)[MB_BO16_1] = (char)u8; + return (*((uint16_t *)pu16)); +} + +int16_t mb_get_int16_ab(pi16) +val_16_arr *pi16; +{ + return mb_get_int16_generic(0, 1, pi16); +} + +uint16_t mb_set_int16_ab(pi16, i16) +val_16_arr *pi16; +int16_t i16; +{ + return mb_set_int16_generic(0, 1, pi16, i16); +} + +uint16_t mb_get_uint16_ab(pu16) +val_16_arr *pu16; +{ + return mb_get_uint16_generic(0, 1, pu16); +} + +uint16_t mb_set_uint16_ab(pu16, u16) +val_16_arr *pu16; +uint16_t u16; +{ + return mb_set_uint16_generic(0, 1, pu16, u16); +} + +int16_t mb_get_int16_ba(pi16) +val_16_arr *pi16; +{ + return mb_get_int16_generic(1, 0, pi16); +} + +uint16_t mb_set_int16_ba(pi16, i16) +val_16_arr *pi16; +int16_t i16; +{ + return mb_set_int16_generic(1, 0, pi16, i16); +} + +uint16_t mb_get_uint16_ba(pu16) +val_16_arr *pu16; +{ + return mb_get_int16_generic(1, 0, pu16); +} + +uint16_t mb_set_uint16_ba(pu16, u16) +val_16_arr *pu16; +uint16_t u16; +{ + return mb_set_int16_generic(1, 0, pu16, u16); +} + +int32_t mb_get_int32_abcd(pi32) +val_32_arr *pi32; +{ + return mb_get_int32_generic(0, 1, 2, 3, pi32); +} + +uint32_t mb_set_int32_abcd(pi32, i32) +val_32_arr *pi32; +int32_t i32; +{ + return mb_set_int32_generic(0, 1, 2, 3, pi32, i32); +} + +uint32_t mb_get_uint32_abcd(pu32) +val_32_arr *pu32; +{ + return mb_get_uint32_generic(0, 1, 2, 3, pu32); +} + +uint32_t mb_set_uint32_abcd(pu32, u32) +val_32_arr *pu32; +uint32_t u32; +{ + return mb_set_uint32_generic(0, 1, 2, 3, pu32, u32); +} + +int32_t mb_get_int32_badc(pi32) +val_32_arr *pi32; +{ + return mb_get_int32_generic(1, 0, 3, 2, pi32); +} + +uint32_t mb_set_int32_badc(pi32, i32) +val_32_arr *pi32; +int32_t i32; +{ + return mb_set_int32_generic(1, 0, 3, 2, pi32, i32); +} + +uint32_t mb_get_uint32_badc(pu32) +val_32_arr *pu32; +{ + return mb_get_uint32_generic(1, 0, 3, 2, pu32); +} + +uint32_t mb_set_uint32_badc(pu32, u32) +val_32_arr *pu32; +uint32_t u32; +{ + return mb_set_uint32_generic(1, 0, 3, 2, pu32, u32); +} + +int32_t mb_get_int32_cdab(pi32) +val_32_arr *pi32; +{ + return mb_get_int32_generic(2, 3, 0, 1, pi32); +} + +uint32_t mb_set_int32_cdab(pi32, i32) +val_32_arr *pi32; +int32_t i32; +{ + return mb_set_int32_generic(2, 3, 0, 1, pi32, i32); +} + +uint32_t mb_get_uint32_cdab(pu32) +val_32_arr *pu32; +{ + return mb_get_uint32_generic(2, 3, 0, 1, pu32); +} + +uint32_t mb_set_uint32_cdab(pu32, u32) +val_32_arr *pu32; +uint32_t u32; +{ + return mb_set_uint32_generic(2, 3, 0, 1, pu32, u32); +} + +int32_t mb_get_int32_dcba(pi32) +val_32_arr *pi32; +{ + return mb_get_int32_generic(3, 2, 1, 0, pi32); +} + +uint32_t mb_set_int32_dcba(pi32, i32) +val_32_arr *pi32; +int32_t i32; +{ + return mb_set_int32_generic(3, 2, 1, 0, pi32, i32); +} + +uint32_t mb_get_uint32_dcba(pu32) +val_32_arr *pu32; +{ + return mb_get_uint32_generic(3, 2, 1, 0, pu32); +} + +uint32_t mb_set_uint32_dcba(pu32, u32) +val_32_arr *pu32; +uint32_t u32; +{ + return mb_set_uint32_generic(3, 2, 1, 0, pu32, u32); +} + +float mb_get_float_abcd(pf) +val_32_arr *pf; +{ + return mb_get_float_generic(0, 1, 2, 3, pf); +} + +uint32_t mb_set_float_abcd(pf, f) +val_32_arr *pf; +float f; +{ + return mb_set_float_generic(0, 1, 2, 3, pf, f); +} + +float mb_get_float_badc(pf) +val_32_arr *pf; +{ + return mb_get_float_generic(1, 0, 3, 2, pf); +} + +uint32_t mb_set_float_badc(pf, f) +val_32_arr *pf; +float f; +{ + return mb_set_float_generic(1, 0, 3, 2, pf, f); +} + +float mb_get_float_cdab(pf) +val_32_arr *pf; +{ + return mb_get_float_generic(2, 3, 0, 1, pf); +} + +uint32_t mb_set_float_cdab(pf, f) +val_32_arr *pf; +float f; +{ + return mb_set_float_generic(2, 3, 0, 1, pf, f); +} + +float mb_get_float_dcba(pf) +val_32_arr *pf; +{ + return mb_get_float_generic(3, 2, 1, 0, pf); +} + +uint32_t mb_set_float_dcba(pf, f) +val_32_arr *pf; +float f; +{ + return mb_set_float_generic(3, 2, 1, 0, pf, f); +} + +double mb_get_double_abcdefgh(pd) +val_64_arr *pd; +{ + return mb_get_double_generic(0, 1, 2, 3, 4, 5, 6, 7, pd); +} + +uint64_t mb_set_double_abcdefgh(pd, d) +val_64_arr *pd; +double d; +{ + return mb_set_double_generic(0, 1, 2, 3, 4, 5, 6, 7, pd, d); +} + +double mb_get_double_hgfedcba(pd) +val_64_arr *pd; +{ + return mb_get_double_generic(7, 6, 5, 4, 3, 2, 1, 0, pd); +} + +uint64_t mb_set_double_hgfedcba(pd, d) +val_64_arr *pd; +double d; +{ + return mb_set_double_generic(7, 6, 5, 4, 3, 2, 1, 0, pd, d); +} + +double mb_get_double_ghefcdab(pd) +val_64_arr *pd; +{ + return mb_get_double_generic(6, 7, 4, 5, 2, 3, 0, 1, pd); +} + +uint64_t mb_set_double_ghefcdab(pd, d) +val_64_arr *pd; +double d; +{ + return mb_set_double_generic(6, 7, 4, 5, 2, 3, 0, 1, pd, d); +} + +double mb_get_double_badcfehg(pd) +val_64_arr *pd; +{ + return mb_get_double_generic(1, 0, 3, 2, 5, 4, 7, 6, pd); +} + +uint64_t mb_set_double_badcfehg(pd, d) +val_64_arr *pd; +double d; +{ + return mb_set_double_generic(1, 0, 3, 2, 5, 4, 7, 6, pd, d); +} + +int64_t mb_get_int64_abcdefgh(pi64) +val_64_arr *pi64; +{ + return mb_get_int64_generic(0, 1, 2, 3, 4, 5, 6, 7, pi64); +} + +uint64_t mb_set_int64_abcdefgh(pi, i) +val_64_arr *pi; +int64_t i; +{ + return mb_set_int64_generic(0, 1, 2, 3, 4, 5, 6, 7, pi, i); +} + +int64_t mb_get_int64_hgfedcba(pi64) +val_64_arr *pi64; +{ + return mb_get_int64_generic(7, 6, 5, 4, 3, 2, 1, 0, pi64); +} + +uint64_t mb_set_int64_hgfedcba(pi, i) +val_64_arr *pi; +int64_t i; +{ + return mb_set_int64_generic(7, 6, 5, 4, 3, 2, 1, 0, pi, i); +} + +int64_t mb_get_int64_ghefcdab(pi64) +val_64_arr *pi64; +{ + return mb_get_int64_generic(6, 7, 4, 5, 2, 3, 0, 1, pi64); +} + +uint64_t mb_set_int64_ghefcdab(pi, i) +val_64_arr *pi; +int64_t i; +{ + return mb_set_int64_generic(6, 7, 4, 5, 2, 3, 0, 1, pi, i); +} + +int64_t mb_get_int64_badcfehg(pi64) +val_64_arr *pi64; +{ + return mb_get_int64_generic(1, 0, 3, 2, 5, 4, 7, 6, pi64); +} + +uint64_t mb_set_int64_badcfehg(pi, i) +val_64_arr *pi; +int64_t i; +{ + return mb_set_int64_generic(1, 0, 3, 2, 5, 4, 7, 6, pi, i); +} + +uint64_t mb_get_uint64_abcdefgh(pui) +val_64_arr *pui; +{ + return mb_get_uint64_generic(0, 1, 2, 3, 4, 5, 6, 7, pui); +} + +uint64_t mb_set_uint64_abcdefgh(pui, ui) +val_64_arr *pui; +uint64_t ui; +{ + return mb_set_uint64_generic(0, 1, 2, 3, 4, 5, 6, 7, pui, ui); +} + +uint64_t mb_get_uint64_hgfedcba(pui) +val_64_arr *pui; +{ + return mb_get_uint64_generic(7, 6, 5, 4, 3, 2, 1, 0, pui); +} + +uint64_t mb_set_uint64_hgfedcba(pui, ui) +val_64_arr *pui; +uint64_t ui; +{ + return mb_set_uint64_generic(7, 6, 5, 4, 3, 2, 1, 0, pui, ui); +} + +uint64_t mb_get_uint64_ghefcdab(pui) +val_64_arr *pui; +{ + return mb_get_uint64_generic(6, 7, 4, 5, 2, 3, 0, 1, pui); +} + +uint64_t mb_set_uint64_ghefcdab(pui, ui) +val_64_arr *pui; +uint64_t ui; +{ + return mb_set_uint64_generic(6, 7, 4, 5, 2, 3, 0, 1, pui, ui); +} + +uint64_t mb_get_uint64_badcfehg(pui) +val_64_arr *pui; +{ + return mb_get_int64_generic(1, 0, 3, 2, 5, 4, 7, 6, pui); +} + +uint64_t mb_set_uint64_badcfehg(pui, ui) +val_64_arr *pui; +uint64_t ui; +{ + return mb_set_uint64_generic(1, 0, 3, 2, 5, 4, 7, 6, pui, ui); +} diff --git a/modbus/mb_controller/common/mbc_master.h b/modbus/mb_controller/common/mbc_master.h new file mode 100644 index 0000000..c0e1900 --- /dev/null +++ b/modbus/mb_controller/common/mbc_master.h @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include // for list +#include "freertos/FreeRTOS.h" // for task creation and queue access +#include "freertos/task.h" // for task api access +#include "freertos/event_groups.h" // for event groups +#include "driver/uart.h" // for UART types +#include "errno.h" // for errno +#include "esp_log.h" // for log write +#include "string.h" // for strerror() +#include "esp_modbus_common.h" // for common types +#include "esp_modbus_master.h" // for public master types + +#include "mb_common.h" // for mb_base_t +#include "mb_utils.h" +#include "mb_master.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ----------------------- Defines ------------------------------------------*/ + +// Set the maximum resource waiting time, the actual time of resouce release +// will be dependent on response time set by timer + convertion time if the command is received +#define MB_MAX_RESP_DELAY_MS (3000) + +/** + * @brief Request mode for parameter to use in data dictionary + */ +typedef enum { + MB_PARAM_READ, /*!< Read parameter values. */ + MB_PARAM_WRITE /*!< Write parameter values. */ +} mb_param_mode_t; + +/** + * @brief Modbus controller handler structure + */ +typedef struct { + mb_port_type_t port_type; /*!< Modbus port type */ + mb_communication_info_t comm_opts; /*!< Modbus communication info */ + uint8_t *reg_buffer_ptr; /*!< Modbus data buffer pointer */ + uint16_t reg_buffer_size; /*!< Modbus data buffer size */ + TaskHandle_t task_handle; /*!< Modbus task handle */ + EventGroupHandle_t event_group_handle; /*!< Modbus controller event group */ + const mb_parameter_descriptor_t *param_descriptor_table; /*!< Modbus controller parameter description table */ + size_t mbm_param_descriptor_size; /*!< Modbus controller parameter description table size */ +} mb_master_options_t; + +typedef esp_err_t (*iface_get_cid_info_fp)(void *, uint16_t, const mb_parameter_descriptor_t **); /*!< Interface get_cid_info method */ +typedef esp_err_t (*iface_get_parameter_fp)(void *, uint16_t, uint8_t *, uint8_t *); /*!< Interface get_parameter method */ +typedef esp_err_t (*iface_get_parameter_with_fp)(void *, uint16_t, uint8_t, uint8_t *, uint8_t *); /*!< Interface get_parameter_with method */ +typedef esp_err_t (*iface_send_request_fp)(void *, mb_param_request_t*, void *); /*!< Interface send_request method */ +typedef esp_err_t (*iface_mbm_set_descriptor_fp)(void *, const mb_parameter_descriptor_t*, const uint16_t); /*!< Interface set_descriptor method */ +typedef esp_err_t (*iface_set_parameter_fp)(void *, uint16_t, uint8_t *, uint8_t *); /*!< Interface set_parameter method */ +typedef esp_err_t (*iface_set_parameter_with_fp)(void *, uint16_t, uint8_t, uint8_t *, uint8_t *); /*!< Interface set_parameter_with method */ + +/** + * @brief Modbus controller interface structure + */ +typedef struct { + mb_base_t *mb_base; + // Master object interface options + mb_master_options_t opts; + bool is_active; /*!< Interface is active */ + + // Public interface methods + iface_create_fp create; /*!< Interface constructor */ + iface_method_default_fp delete; /*!< Interface method delete */ + iface_method_default_fp start; /*!< Interface method start */ + iface_method_default_fp stop; /*!< Interface method stop */ + iface_get_cid_info_fp get_cid_info; /*!< Interface get_cid_info method */ + iface_get_parameter_fp get_parameter; /*!< Interface get_parameter method */ + iface_get_parameter_with_fp get_parameter_with; /*!< Interface get_parameter_with method */ + iface_send_request_fp send_request; /*!< Interface send_request method */ + iface_mbm_set_descriptor_fp set_descriptor; /*!< Interface set_descriptor method */ + iface_set_parameter_fp set_parameter; /*!< Interface set_parameter method */ + iface_set_parameter_with_fp set_parameter_with; /*!< Interface set_parameter_with method */ +} mbm_controller_iface_t; + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_controller/common/mbc_slave.h b/modbus/mb_controller/common/mbc_slave.h new file mode 100644 index 0000000..3e6a0bb --- /dev/null +++ b/modbus/mb_controller/common/mbc_slave.h @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "driver/uart.h" // for uart defines +#include "errno.h" // for errno +#include "sys/queue.h" // for list +#include "esp_log.h" // for log write +#include "string.h" // for strerror() + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mb_common.h" // for mb_base_t +#include "esp_modbus_slave.h" // for public type defines + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_INST_MIN_SIZE (1) // The minimal size of Modbus registers area in bytes +#define MB_INST_MAX_SIZE (65535 * 2) // The maximum size of Modbus area in bytes + +#define MB_CONTROLLER_NOTIFY_QUEUE_SIZE (CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE) // Number of messages in parameter notification queue +#define MB_CONTROLLER_NOTIFY_TIMEOUT (pdMS_TO_TICKS(CONFIG_FMB_CONTROLLER_NOTIFY_TIMEOUT)) // notification timeout + +/** + * @brief Modbus area descriptor list item + */ +typedef struct mb_descr_entry_s { + uint16_t start_offset; /*!< Modbus start address for area descriptor */ + mb_param_type_t type; /*!< Type of storage area descriptor */ + mb_param_access_t access; /*!< Area access type */ + void *p_data; /*!< Instance address for storage area descriptor */ + size_t size; /*!< Instance size for area descriptor (bytes) */ + LIST_ENTRY(mb_descr_entry_s) entries; /*!< The Modbus area descriptor entry */ +} mb_descr_entry_t; + +/** + * @brief Modbus controller handler structure + */ +typedef struct { + mb_port_type_t port_type; /*!< port type */ + mb_communication_info_t comm_opts; /*!< communication info */ + TaskHandle_t task_handle; /*!< task handle */ + EventGroupHandle_t event_group_handle; /*!< controller event group */ + QueueHandle_t notification_queue_handle; /*!< controller notification queue */ + LIST_HEAD(mbs_area_descriptors_, mb_descr_entry_s) area_descriptors[MB_PARAM_COUNT]; /*!< register area descriptors */ +} mb_slave_options_t; + +typedef mb_event_group_t (*iface_check_event_fp)(void *, mb_event_group_t); /*!< Interface method check_event */ +typedef esp_err_t (*iface_get_param_info_fp)(void *, mb_param_info_t*, uint32_t); /*!< Interface method get_param_info */ +typedef esp_err_t (*iface_mbs_set_descriptor_fp)(void *, mb_register_area_descriptor_t); /*!< Interface method set_descriptor */ + +/** + * @brief Request mode for parameter to use in data dictionary + */ +typedef struct +{ + mb_base_t *mb_base; + mb_slave_options_t opts; /*!< Modbus slave options */ + bool is_active; /*!< Modbus controller interface is active */ + + // Functional pointers to internal static functions of the implementation (public interface methods) + iface_create_fp create; /*!< Interface factory method */ + iface_method_default_fp delete; /*!< Interface method delete */ + iface_method_default_fp start; /*!< Interface method start */ + iface_method_default_fp stop; /*!< Interface method start */ + iface_check_event_fp check_event; /*!< Interface method check_event */ + iface_get_param_info_fp get_param_info; /*!< Interface method get_param_info */ + iface_mbs_set_descriptor_fp set_descriptor; /*!< Interface method set_descriptor */ +} mbs_controller_iface_t; + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_controller/serial/mbc_serial_master.c b/modbus/mb_controller/serial/mbc_serial_master.c new file mode 100644 index 0000000..bee0afa --- /dev/null +++ b/modbus/mb_controller/serial/mbc_serial_master.c @@ -0,0 +1,732 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// mbc_serial_master.c +// Serial master implementation of the Modbus controller + +#include // for calculation of time stamp in milliseconds +#include "esp_log.h" // for log_write +#include // for memcpy +#include "freertos/FreeRTOS.h" // for task creation +#include "freertos/task.h" // for task api access +#include "freertos/event_groups.h" // for event groups + +#include "sdkconfig.h" // for KConfig values +#include "esp_modbus_common.h" // for common types +#include "esp_modbus_master.h" // for public master types +#include "mbc_master.h" // for private master types +#include "mbc_serial_master.h" // for serial master create function and types + +#include "mb_common.h" // for mb types definition +#include "mb_config.h" +#include "mb_proto.h" + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +/*-----------------------Master mode use these variables----------------------*/ + +static const char *TAG = "mbc_serial.master"; + +// Modbus event processing task +static void mbc_ser_master_task(void *param) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(param); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(param); + + // Main Modbus stack processing cycle + for (;;) + { + // Wait for poll events + BaseType_t status = xEventGroupWaitBits(mbm_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, + pdFALSE, + portMAX_DELAY); + // Check if stack started then poll for data + if (status & MB_EVENT_STACK_STARTED) + { + (void)mbm_iface->mb_base->poll(mbm_iface->mb_base); + } + } +} + +// Modbus controller stack start function +static esp_err_t mbc_serial_master_start(void *ctx) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(ctx); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mb_err_enum_t status = MB_EIO; + mbm_iface->mb_base->descr.parent = ctx; + MB_RETURN_ON_FALSE((mbm_opts->mbm_param_descriptor_size >= 1), + ESP_ERR_INVALID_ARG, TAG, "mb descriptor table size is incorrect."); + status = mbm_iface->mb_base->enable(mbm_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack start fail, returned (0x%x).", (int)status); + // Set the mbcontroller start flag + EventBits_t flag = xEventGroupSetBits(mbm_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_STARTED); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_STARTED), + ESP_ERR_INVALID_STATE, TAG, "mb stack start event set error."); + mbm_iface->is_active = true; + return ESP_OK; +} + +// Modbus controller stack stop function +static esp_err_t mbc_serial_master_stop(void *ctx) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(ctx); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mb_err_enum_t status = MB_EIO; + mbm_iface->mb_base->descr.parent = ctx; + + // Set the mbcontroller start flag + EventBits_t flag = xEventGroupClearBits(mbm_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_STARTED); + status = mbm_iface->mb_base->disable(mbm_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack disable fail, returned (0x%x).", (int)status); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_STARTED), + ESP_ERR_INVALID_STATE, TAG, "mb stack stop event set error."); + mbm_iface->is_active = false; + return ESP_OK; +} + +// Modbus controller destroy function +static esp_err_t mbc_serial_master_delete(void *ctx) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(ctx); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mb_err_enum_t mb_error = MB_ENOERR; + + // Check the stack started bit + BaseType_t status = xEventGroupWaitBits(mbm_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, + pdFALSE, + pdMS_TO_TICKS(MB_MASTER_TIMEOUT_MS_RESPOND)); + if (mbm_iface->is_active || (status & MB_EVENT_STACK_STARTED)) + { + ESP_LOGV(TAG, "mb stack is active, try to disable."); + MB_RETURN_ON_FALSE((mbc_serial_master_stop(ctx) == ESP_OK), + ESP_ERR_INVALID_STATE, TAG, "mb stack stop failure."); + } + + mbm_iface->is_active = false; + vTaskDelete(mbm_opts->task_handle); + mbm_opts->task_handle = NULL; + vEventGroupDelete(mbm_opts->event_group_handle); + mbm_opts->event_group_handle = NULL; + mb_error = mbm_iface->mb_base->delete(mbm_iface->mb_base); + MB_RETURN_ON_FALSE((mb_error == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack delete failure, returned (0x%x).", (int)mb_error); + mbm_iface->mb_base = NULL; + free(mbm_iface); // free the memory allocated + return ESP_OK; +} + +// Set Modbus parameter description table +static esp_err_t mbc_serial_master_set_descriptor(void *ctx, const mb_parameter_descriptor_t *descriptor, const uint16_t num_elements) +{ + MB_RETURN_ON_FALSE((descriptor), ESP_ERR_INVALID_ARG, TAG, "mb incorrect descriptor."); + MB_RETURN_ON_FALSE((num_elements >= 1), ESP_ERR_INVALID_ARG, TAG, "mb table size is incorrect."); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + const mb_parameter_descriptor_t *reg_ptr = descriptor; + // Go through all items in the table to check all Modbus registers + for (uint16_t counter = 0; counter < (num_elements); counter++, reg_ptr++) + { + // Below is the code to check consistency of the table format and required fields. + MB_RETURN_ON_FALSE((reg_ptr->cid == counter), + ESP_ERR_INVALID_ARG, TAG, "mb descriptor cid field is incorrect."); + MB_RETURN_ON_FALSE((reg_ptr->param_key), + ESP_ERR_INVALID_ARG, TAG, "mb descriptor param key is incorrect."); + MB_RETURN_ON_FALSE((reg_ptr->mb_size > 0), + ESP_ERR_INVALID_ARG, TAG, "mb descriptor param size is incorrect."); + } + mbm_opts->param_descriptor_table = descriptor; + mbm_opts->mbm_param_descriptor_size = num_elements; + return ESP_OK; +} + +// Send custom Modbus request defined as mb_param_request_t structure +static esp_err_t mbc_serial_master_send_request(void *ctx, mb_param_request_t *request, void *data_ptr) +{ + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mbm_controller_iface_t *mbm_controller_iface = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((request), ESP_ERR_INVALID_ARG, TAG, "mb request structure."); + MB_RETURN_ON_FALSE((data_ptr), ESP_ERR_INVALID_ARG, TAG, "mb incorrect data pointer."); + + mb_err_enum_t mb_error = MB_EBUSY; + esp_err_t error = ESP_FAIL; + + if (mb_port_event_res_take(mbm_controller_iface->mb_base->port_obj, pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS))) + { + uint8_t mb_slave_addr = request->slave_addr; + uint8_t mb_command = request->command; + uint16_t mb_offset = request->reg_start; + uint16_t mb_size = request->reg_size; + + // Set the buffer for callback function processing of received data + mbm_opts->reg_buffer_ptr = (uint8_t *)data_ptr; + mbm_opts->reg_buffer_size = mb_size; + + mb_port_event_res_release(mbm_controller_iface->mb_base->port_obj); + + // Calls appropriate request function to send request and waits response + switch (mb_command) + { + +#if MB_FUNC_READ_COILS_ENABLED + case MB_FUNC_READ_COILS: + mb_error = mbm_rq_read_coils(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_COIL_ENABLED + case MB_FUNC_WRITE_SINGLE_COIL: + mb_error = mbm_rq_write_coil(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + *(uint16_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED + case MB_FUNC_WRITE_MULTIPLE_COILS: + mb_error = mbm_rq_write_multi_coils(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, (uint8_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED + case MB_FUNC_READ_DISCRETE_INPUTS: + mb_error = mbm_rq_read_discrete_inputs(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READ_HOLDING_ENABLED + case MB_FUNC_READ_HOLDING_REGISTER: + mb_error = mbm_rq_read_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_HOLDING_ENABLED + case MB_FUNC_WRITE_REGISTER: + mb_error = mbm_rq_write_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + *(uint16_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED + case MB_FUNC_WRITE_MULTIPLE_REGISTERS: + mb_error = mbm_rq_write_multi_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, + (uint16_t)mb_offset, (uint16_t)mb_size, + (uint16_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READWRITE_HOLDING_ENABLED + case MB_FUNC_READWRITE_MULTIPLE_REGISTERS: + mb_error = mbm_rq_rw_multi_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, (uint16_t *)data_ptr, + (uint16_t)mb_offset, (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READ_INPUT_ENABLED + case MB_FUNC_READ_INPUT_REGISTER: + mb_error = mbm_rq_read_inp_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + default: + ESP_LOGE(TAG, "%s: Incorrect or unsupported function in request (%u) ", + __FUNCTION__, mb_command); + mb_error = MB_ENOREG; + break; + } + } + + // Propagate the Modbus errors to higher level + switch (mb_error) + { + case MB_ENOERR: + error = ESP_OK; + break; + + case MB_ENOREG: + error = ESP_ERR_NOT_SUPPORTED; // Invalid register request + break; + + case MB_ETIMEDOUT: + error = ESP_ERR_TIMEOUT; // Slave did not send response + break; + + case MB_EILLFUNC: + case MB_ERECVDATA: + error = ESP_ERR_INVALID_RESPONSE; // Invalid response from slave + break; + + case MB_EBUSY: + error = ESP_ERR_INVALID_STATE; // Master is busy (previous request is pending) + break; + + default: + ESP_LOGE(TAG, "%s: Incorrect return code (%x) ", __FUNCTION__, (uint16_t)mb_error); + error = ESP_FAIL; + break; + } + + return error; +} + +static esp_err_t mbc_serial_master_get_cid_info(void *ctx, uint16_t cid, const mb_parameter_descriptor_t **param_buffer) +{ + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + + MB_RETURN_ON_FALSE((param_buffer), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect data buffer pointer."); + MB_RETURN_ON_FALSE((mbm_opts->param_descriptor_table), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect descriptor table or not set."); + MB_RETURN_ON_FALSE((cid < mbm_opts->mbm_param_descriptor_size), + ESP_ERR_NOT_FOUND, TAG, "mb incorrect cid of characteristic."); + + // It is assumed that characteristics cid increased in the table + const mb_parameter_descriptor_t *reg_info = &mbm_opts->param_descriptor_table[cid]; + + MB_RETURN_ON_FALSE((reg_info->param_key), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect characteristic key."); + *param_buffer = reg_info; + return ESP_OK; +} + +// Helper function to get modbus command for each type of Modbus register area +static uint8_t mbc_serial_master_get_command(mb_param_type_t param_type, mb_param_mode_t mode) +{ + uint8_t command = 0; + switch (param_type) + { + case MB_PARAM_HOLDING: + command = (mode == MB_PARAM_WRITE) ? MB_FUNC_WRITE_MULTIPLE_REGISTERS : MB_FUNC_READ_HOLDING_REGISTER; + break; + case MB_PARAM_INPUT: + command = MB_FUNC_READ_INPUT_REGISTER; + break; + case MB_PARAM_COIL: + command = (mode == MB_PARAM_WRITE) ? MB_FUNC_WRITE_MULTIPLE_COILS : MB_FUNC_READ_COILS; + break; + case MB_PARAM_DISCRETE: + if (mode != MB_PARAM_WRITE) + { + command = MB_FUNC_READ_DISCRETE_INPUTS; + } + else + { + ESP_LOGE(TAG, "%s: Incorrect mode (%u)", + __FUNCTION__, (unsigned)mode); + } + break; + default: + ESP_LOGE(TAG, "%s: Incorrect param type (%u)", + __FUNCTION__, (unsigned)param_type); + break; + } + return command; +} + +// Helper to search parameter by name in the parameter description table +// and fills Modbus request fields accordingly +static esp_err_t mbc_serial_master_set_request(void *ctx, uint16_t cid, mb_param_mode_t mode, + mb_param_request_t *request, + mb_parameter_descriptor_t *reg_data) +{ + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + esp_err_t error = ESP_ERR_NOT_FOUND; + MB_RETURN_ON_FALSE((request), ESP_ERR_INVALID_ARG, TAG, "mb incorrect request parameter."); + MB_RETURN_ON_FALSE((mode <= MB_PARAM_WRITE), ESP_ERR_INVALID_ARG, TAG, "mb incorrect mode."); + MB_RETURN_ON_FALSE((cid < mbm_opts->mbm_param_descriptor_size), ESP_ERR_INVALID_ARG, TAG, "mb incorrect cid parameter."); + MB_RETURN_ON_FALSE((mbm_opts->param_descriptor_table), ESP_ERR_INVALID_ARG, TAG, "mb data dictionary is incorrect."); + const mb_parameter_descriptor_t *reg_ptr = mbm_opts->param_descriptor_table; + reg_ptr += cid; + if (reg_ptr->cid == cid) + { + request->slave_addr = reg_ptr->mb_slave_addr; + request->reg_start = reg_ptr->mb_reg_start; + request->reg_size = reg_ptr->mb_size; + request->command = mbc_serial_master_get_command(reg_ptr->mb_param_type, mode); + MB_RETURN_ON_FALSE((request->command > 0), ESP_ERR_INVALID_ARG, TAG, "mb incorrect command or parameter type."); + if (reg_data) + { + *reg_data = *reg_ptr; // Set the cid registered parameter data + } + error = ESP_OK; + } + return error; +} + +// Get parameter data for corresponding characteristic +static esp_err_t mbc_serial_master_get_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type) +{ + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + MB_RETURN_ON_FALSE((value), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request ; + mb_parameter_descriptor_t reg_info = { 0 }; + uint8_t *pdata = NULL; + + error = mbc_serial_master_set_request(ctx, cid, MB_PARAM_READ, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid) && (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER)) { + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + // alloc buffer to store parameter data + pdata = calloc(1, (reg_info.mb_size << 1)); + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + error = mbc_serial_master_send_request(ctx, &request, pdata); + if (error == ESP_OK) { + // If data pointer is NULL then we don't need to set value (it is still in the cache of cid) + if (value) { + error = mbc_master_set_param_data((void *)value, (void *)pdata, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + error = ESP_ERR_INVALID_STATE; + } else { + ESP_LOGD(TAG, "%s: Good response for get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + } + } else { + ESP_LOGD(TAG, "%s: Bad response to get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } else { + ESP_LOGE(TAG, "%s: The cid(%u) not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} +// Get parameter data for corresponding characteristic +static esp_err_t mbc_serial_master_get_parameter_with(void *ctx, uint16_t cid, uint8_t uid, + uint8_t *value_ptr, uint8_t *type) +{ + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + MB_RETURN_ON_FALSE((value_ptr), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request; + mb_parameter_descriptor_t reg_info = {0}; + uint8_t *pdata = NULL; + + error = mbc_serial_master_set_request(ctx, cid, MB_PARAM_READ, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid)) + { + if (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER) + { + ESP_LOGD(TAG, "%s: override uid %d = %d for cid(%u)", + __FUNCTION__, (int)request.slave_addr, (int)uid, (unsigned)reg_info.cid); + } + request.slave_addr = uid; // override the UID + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + // alloc buffer to store parameter data + pdata = calloc(1, (reg_info.mb_size << 1)); + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + // Send request to read characteristic data + error = mbc_serial_master_send_request(ctx, &request, pdata); + if (error == ESP_OK) + { + // If data pointer is NULL then we don't need to set value (it is still in the cache of cid) + if (value_ptr) { + error = mbc_master_set_param_data((void *)value_ptr, (void *)pdata, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + error = ESP_ERR_INVALID_STATE; + } else { + ESP_LOGD(TAG, "%s: Good response for get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + } + } + else + { + ESP_LOGD(TAG, "%s: Bad response to get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } + else + { + ESP_LOGE(TAG, "%s: The cid(%u) not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} + +// Set parameter value for characteristic selected by cid +static esp_err_t mbc_serial_master_set_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type) +{ + MB_RETURN_ON_FALSE((value), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request ; + mb_parameter_descriptor_t reg_info = { 0 }; + uint8_t *pdata = NULL; + + error = mbc_serial_master_set_request(ctx, cid, MB_PARAM_WRITE, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid) && (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER)) { + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + pdata = calloc(1, (reg_info.mb_size << 1)); // alloc parameter buffer + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + // Transfer value of characteristic into parameter buffer + error = mbc_master_set_param_data((void *)pdata, (void *)value, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + free(pdata); + return ESP_ERR_INVALID_STATE; + } + // Send request to write characteristic data + error = mbc_serial_master_send_request(ctx, &request, pdata); + if (error == ESP_OK) { + ESP_LOGD(TAG, "%s: Good response for set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } else { + ESP_LOGD(TAG, "%s: Bad response to set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } else { + ESP_LOGE(TAG, "%s: The requested cid(%u) not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} + +// Set parameter value for characteristic selected by name and cid +static esp_err_t mbc_serial_master_set_parameter_with(void *ctx, uint16_t cid, uint8_t uid, + uint8_t *value_ptr, uint8_t *type) +{ + MB_RETURN_ON_FALSE((value_ptr), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request; + mb_parameter_descriptor_t reg_info = {0}; + uint8_t *pdata = NULL; + error = mbc_serial_master_set_request(ctx, cid, MB_PARAM_WRITE, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid)) + { + if (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER) + { + ESP_LOGD(TAG, "%s: override uid %d = %d for cid(%u)", + __FUNCTION__, (int)request.slave_addr, (int)uid, (unsigned)reg_info.cid); + } + request.slave_addr = uid; // override the UID + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + pdata = calloc(1, (reg_info.mb_size << 1)); // alloc parameter buffer + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + // Transfer value of characteristic into parameter buffer + error = mbc_master_set_param_data((void *)pdata, (void *)value_ptr, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + free(pdata); + return ESP_ERR_INVALID_STATE; + } + // Send request to write characteristic data + error = mbc_serial_master_send_request(ctx, &request, value_ptr); + if (error == ESP_OK) + { + ESP_LOGD(TAG, "%s: Good response for set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + else + { + ESP_LOGD(TAG, "%s: Bad response to set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } + else + { + ESP_LOGE(TAG, "%s: The requested cid(%u) not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} + +static void mbc_serial_master_iface_free(void *ctx) +{ + mbm_controller_iface_t *mbm_iface = (mbm_controller_iface_t *)(ctx); + if (mbm_iface) + { + if (mbm_iface->opts.task_handle) + { + vTaskDelete(mbm_iface->opts.task_handle); + mbm_iface->opts.task_handle = NULL; + } + if (mbm_iface->opts.event_group_handle) + { + vEventGroupDelete(mbm_iface->opts.event_group_handle); + mbm_iface->opts.event_group_handle = NULL; + } + free(mbm_iface); // free the memory allocated for interface + } +} + +static esp_err_t mbc_serial_master_controller_create(void **ctx) +{ + MB_RETURN_ON_FALSE((ctx), ESP_ERR_INVALID_STATE, TAG, "mb stack init interface fail."); + mbm_controller_iface_t *mbm_controller_iface = NULL; + + esp_err_t ret = ESP_ERR_INVALID_STATE; + BaseType_t status = 0; + + // Allocate space for controller + mbm_controller_iface = malloc(sizeof(mbm_controller_iface_t)); + MB_GOTO_ON_FALSE((mbm_controller_iface), ESP_ERR_INVALID_STATE, error, + TAG, "mb stack memory allocation fail."); + + // Initialize interface properties + mb_master_options_t *mbm_opts = &mbm_controller_iface->opts; + + // Initialization of active context of the modbus controller + mbm_opts->event_group_handle = xEventGroupCreate(); + MB_GOTO_ON_FALSE((mbm_opts->event_group_handle), ESP_ERR_INVALID_STATE, error, TAG, "mb event group error."); + // Create modbus controller task + status = xTaskCreatePinnedToCore((void *)&mbc_ser_master_task, + "mbc_ser_master", + MB_CONTROLLER_STACK_SIZE, + mbm_controller_iface, + MB_CONTROLLER_PRIORITY, + &mbm_opts->task_handle, + MB_PORT_TASK_AFFINITY); + MB_GOTO_ON_FALSE((status == pdPASS), ESP_ERR_INVALID_STATE, error, TAG, + "mb controller task creation error"); + MB_MASTER_ASSERT(mbm_opts->task_handle); // The task is created but handle is incorrect + + // Initialize public interface methods of the interface + mbm_controller_iface->create = mbc_serial_master_create; + mbm_controller_iface->delete = mbc_serial_master_delete; + mbm_controller_iface->start = mbc_serial_master_start; + mbm_controller_iface->stop = mbc_serial_master_stop; + mbm_controller_iface->get_cid_info = mbc_serial_master_get_cid_info; + mbm_controller_iface->get_parameter = mbc_serial_master_get_parameter; + mbm_controller_iface->get_parameter_with = mbc_serial_master_get_parameter_with; + mbm_controller_iface->send_request = mbc_serial_master_send_request; + mbm_controller_iface->set_descriptor = mbc_serial_master_set_descriptor; + mbm_controller_iface->set_parameter = mbc_serial_master_set_parameter; + mbm_controller_iface->set_parameter_with = mbc_serial_master_set_parameter_with; + mbm_controller_iface->mb_base = NULL; + *ctx = mbm_controller_iface; + return ESP_OK; + +error: + mbc_serial_master_iface_free((void *)mbm_controller_iface); + return ret; +} + +// Initialization of resources for Modbus serial master controller +esp_err_t mbc_serial_master_create(mb_communication_info_t *config, void **ctx) +{ + mbm_controller_iface_t *mbm_controller_iface = NULL; + MB_RETURN_ON_FALSE((ctx && config), ESP_ERR_INVALID_STATE, TAG, + "mb stack init interface fail."); + MB_RETURN_ON_FALSE((!*ctx), ESP_ERR_INVALID_STATE, TAG, "mb stack is not destroyed?"); + + mb_serial_opts_t *pcomm_info = &config->ser_opts; + + // Check communication options + MB_RETURN_ON_FALSE(((pcomm_info->mode == MB_RTU) || (pcomm_info->mode == MB_ASCII)), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect mode = (%u).", (unsigned)pcomm_info->mode); + MB_RETURN_ON_FALSE((pcomm_info->port <= UART_NUM_MAX), ESP_ERR_INVALID_ARG, TAG, + "mb wrong port to set = (%u).", (unsigned)pcomm_info->port); + MB_RETURN_ON_FALSE((pcomm_info->parity <= UART_PARITY_ODD), ESP_ERR_INVALID_ARG, TAG, + "mb wrong parity option = (%u).", (unsigned)pcomm_info->parity); + + esp_err_t ret = mbc_serial_master_controller_create((void *)&mbm_controller_iface); + MB_GOTO_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE, error, TAG, "mbc create returns (0x%x).", (int)ret); + + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(mbm_controller_iface); + mbm_opts->comm_opts = *config; + mbm_opts->port_type = MB_PORT_SERIAL_MASTER; + + // Keep the response time setting + if (!pcomm_info->response_tout_ms) + { + mbm_opts->comm_opts.ser_opts.response_tout_ms = CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND; + } + + // Initialize Modbus stack using mbcontroller parameters + mb_err_enum_t err = MB_EILLSTATE; + void *pinst = (void *)mbm_controller_iface; + + if (pcomm_info->mode == MB_RTU) + { + err = mbm_rtu_create(pcomm_info, &pinst); + } + else if (pcomm_info->mode == MB_ASCII) + { + err = mbm_ascii_create(pcomm_info, &pinst); + } + MB_GOTO_ON_FALSE((err == MB_ENOERR), ESP_ERR_INVALID_STATE, error, TAG, + "mb object create returns (0x%x).", (int)err); + mbm_controller_iface->mb_base = (mb_base_t *)pinst; + + const mb_rw_callbacks_t rw_cbs = { + .reg_input_cb = mbc_reg_input_master_cb, + .reg_holding_cb = mbc_reg_holding_master_cb, + .reg_coils_cb = mbc_reg_coils_master_cb, + .reg_discrete_cb = mbc_reg_discrete_master_cb + }; + + mbm_controller_iface->mb_base->rw_cbs = rw_cbs; + mbm_controller_iface->is_active = false; + *ctx = mbm_controller_iface; + return ESP_OK; + +error: + if (mbm_controller_iface) + { + if (mbm_controller_iface->mb_base) + { + mbm_controller_iface->mb_base->delete(mbm_controller_iface->mb_base); + mbm_controller_iface->mb_base = NULL; + } + mbc_serial_master_iface_free((void *)mbm_controller_iface); + *ctx = NULL; + } + return ret; +} + +#endif diff --git a/modbus/mb_controller/serial/mbc_serial_master.h b/modbus/mb_controller/serial/mbc_serial_master.h new file mode 100644 index 0000000..c51ddad --- /dev/null +++ b/modbus/mb_controller/serial/mbc_serial_master.h @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// mbc_serial_master.h Modbus controller serial master implementation header file + +#pragma once + +#include // for standard int types definition +#include // for NULL and std defines +#include "soc/soc.h" // for BITN definitions +#include "esp_err.h" // for esp_err_t +#include "esp_modbus_common.h" // for common defines +#include "sdkconfig.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +/** + * @brief Initialize Modbus controller and stack + * + * @param[out] ctx - pointer to pointer of interface structure + * @param[in] config - pointer to configuration structure + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + */ +esp_err_t mbc_serial_master_create(mb_communication_info_t *config, void **ctx); + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_controller/serial/mbc_serial_slave.c b/modbus/mb_controller/serial/mbc_serial_slave.c new file mode 100644 index 0000000..6e317b2 --- /dev/null +++ b/modbus/mb_controller/serial/mbc_serial_slave.c @@ -0,0 +1,293 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +// mbc_serial_slave.c +// Implementation of the Modbus controller serial slave + +#include // for calculation of time stamp in milliseconds +#include "esp_log.h" // for log_write + +#include "esp_modbus_common.h" // for common defines +#include "esp_modbus_slave.h" // for public slave interface types +#include "mbc_slave.h" // for private slave interface types +#include "mbc_serial_slave.h" // for serial slave implementation definitions + +#include "mb_common.h" // for mb object types definition + +#include "sdkconfig.h" // for KConfig values + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +static const char *TAG = "mbc_serial.slave"; + +// Modbus task function +static void mbc_ser_slave_task(void *param) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(param); + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(param); + + // Main Modbus stack processing cycle + for (;;) + { + BaseType_t status = xEventGroupWaitBits(mbs_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, // do not clear bits + pdFALSE, + portMAX_DELAY); + // Check if stack started then poll for data + if (status & MB_EVENT_STACK_STARTED) + { + (void)mbs_iface->mb_base->poll(mbs_iface->mb_base); + } + // esp_task_wdt_reset(); + } +} + +// Start Modbus controller start function +static esp_err_t mbc_serial_slave_start(void *ctx) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(ctx); + mb_err_enum_t status = MB_EIO; + + status = mbs_iface->mb_base->enable(mbs_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack enable fail, returned (0x%x).", (int)status); + // Set the mbcontroller start flag + EventBits_t flag = xEventGroupSetBits(mbs_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_STARTED); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_STARTED), + ESP_ERR_INVALID_STATE, TAG, "mb stack start event set error."); + mbs_iface->mb_base->descr.parent = ctx; + mbs_iface->is_active = true; + return ESP_OK; +} + +// Start Modbus controller stop function +static esp_err_t mbc_serial_slave_stop(void *ctx) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(ctx); + mb_err_enum_t status = MB_EIO; + // Clear the mbcontroller start flag + EventBits_t flag = xEventGroupClearBits(mbs_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_STARTED); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_STARTED), + ESP_ERR_INVALID_STATE, TAG, "mb stack start event set error."); + + status = mbs_iface->mb_base->disable(mbs_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack disable fail, returned (0x%x).", (int)status); + mbs_iface->mb_base->descr.parent = NULL; + mbs_iface->is_active = false; + return ESP_OK; +} + +// Blocking function to get event on parameter group change for application task +static mb_event_group_t mbc_serial_slave_check_event(void *ctx, mb_event_group_t group) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + MB_SLAVE_ASSERT(mbs_opts->event_group_handle); + BaseType_t status = xEventGroupWaitBits(mbs_opts->event_group_handle, (BaseType_t)group, + pdTRUE, pdFALSE, portMAX_DELAY); + return (mb_event_group_t)status; +} + +// Function to get notification about parameter change from application task +static esp_err_t mbc_serial_slave_get_param_info(void *ctx, mb_param_info_t *reg_info, uint32_t timeout) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + esp_err_t err = ESP_ERR_TIMEOUT; + MB_RETURN_ON_FALSE((mbs_opts->notification_queue_handle), + ESP_ERR_INVALID_ARG, TAG, "mb queue handle is invalid."); + MB_RETURN_ON_FALSE((reg_info), ESP_ERR_INVALID_ARG, TAG, "mb register information is invalid."); + BaseType_t status = xQueueReceive(mbs_opts->notification_queue_handle, + reg_info, pdMS_TO_TICKS(timeout)); + if (status == pdTRUE) + { + err = ESP_OK; + } + return err; +} + +// Modbus controller delete function +static esp_err_t mbc_serial_slave_delete(void *ctx) +{ + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(ctx); + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + mb_err_enum_t mb_error = MB_ENOERR; + + // Check the stack started bit + BaseType_t status = xEventGroupWaitBits(mbs_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, + pdFALSE, + MB_CONTROLLER_NOTIFY_TIMEOUT); + if (mbs_iface->is_active || (status & MB_EVENT_STACK_STARTED)) + { + ESP_LOGV(TAG, "mb stack is active, try to disable."); + if (mbc_serial_slave_stop(ctx) != ESP_OK) { + ESP_LOGE(TAG, "mb stack stop failure."); + } + } + + mbs_iface->is_active = false; + vTaskDelete(mbs_opts->task_handle); + vEventGroupDelete(mbs_opts->event_group_handle); + vQueueDelete(mbs_opts->notification_queue_handle); + mbs_opts->notification_queue_handle = NULL; + mbs_opts->event_group_handle = NULL; + mbs_opts->task_handle = NULL; + mb_error = mbs_iface->mb_base->delete(mbs_iface->mb_base); + MB_RETURN_ON_FALSE((mb_error == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack close failure returned (0x%x).", (int)mb_error); + // free the controller will be performed in common slave object + return ESP_OK; +} + +static void mbc_serial_slave_iface_free(void *ctx) +{ + mbs_controller_iface_t *mbs_iface = (mbs_controller_iface_t *)(ctx); + if (mbs_iface) + { + if (mbs_iface->opts.task_handle) + { + vTaskDelete(mbs_iface->opts.task_handle); + mbs_iface->opts.task_handle = NULL; + } + if (mbs_iface->opts.event_group_handle) + { + vEventGroupDelete(mbs_iface->opts.event_group_handle); + mbs_iface->opts.event_group_handle = NULL; + } + if (mbs_iface->opts.notification_queue_handle) + { + vQueueDelete(mbs_iface->opts.notification_queue_handle); + } + free(mbs_iface); // free the memory allocated for interface + } +} + +static esp_err_t mbc_serial_slave_controller_create(void **ctx) +{ + MB_RETURN_ON_FALSE((ctx), ESP_ERR_INVALID_STATE, TAG, + "mb stack init interface fail."); + esp_err_t ret = ESP_ERR_INVALID_STATE; + mbs_controller_iface_t *mbs_controller_iface = malloc(sizeof(mbs_controller_iface_t)); + MB_GOTO_ON_FALSE((mbs_controller_iface), ESP_ERR_NO_MEM, error, + TAG, "mb stack memory allocation fail."); + + mb_slave_options_t *mbs_opts = &mbs_controller_iface->opts; + mbs_opts->port_type = MB_PORT_SERIAL_SLAVE; // set interface port type + + // Initialization of active context of the Modbus controller + BaseType_t status = 0; + // Parameter change notification queue + mbs_opts->event_group_handle = xEventGroupCreate(); + MB_GOTO_ON_FALSE((mbs_opts->event_group_handle), ESP_ERR_NO_MEM, error, + TAG, "mb event group error."); + // Parameter change notification queue + mbs_opts->notification_queue_handle = xQueueCreate(MB_CONTROLLER_NOTIFY_QUEUE_SIZE, sizeof(mb_param_info_t)); + MB_GOTO_ON_FALSE((mbs_opts->notification_queue_handle), ESP_ERR_NO_MEM, error, + TAG, "mb notify queue creation error."); + // Create Modbus controller task + status = xTaskCreatePinnedToCore((void *)&mbc_ser_slave_task, + "mbc_ser_slave", + MB_CONTROLLER_STACK_SIZE, + mbs_controller_iface, + MB_CONTROLLER_PRIORITY, + &mbs_opts->task_handle, + MB_PORT_TASK_AFFINITY); + MB_GOTO_ON_FALSE((status == pdPASS), ESP_ERR_INVALID_STATE, error, TAG, + "mb controller task creation error"); + MB_SLAVE_ASSERT(mbs_opts->task_handle); // The task is created but handle is incorrect + + // Initialize interface function pointers + mbs_controller_iface->create = mbc_serial_slave_create; + mbs_controller_iface->delete = mbc_serial_slave_delete; + mbs_controller_iface->check_event = mbc_serial_slave_check_event; + mbs_controller_iface->get_param_info = mbc_serial_slave_get_param_info; + mbs_controller_iface->set_descriptor = NULL; // Use common set descriptor function + mbs_controller_iface->start = mbc_serial_slave_start; + mbs_controller_iface->stop = mbc_serial_slave_stop; + mbs_controller_iface->mb_base = NULL; + *ctx = mbs_controller_iface; + return ESP_OK; + +error: + mbc_serial_slave_iface_free((void *)mbs_controller_iface); + return ret; +} + +// Initialization of Modbus controller +esp_err_t mbc_serial_slave_create(mb_communication_info_t *config, void **ctx) +{ + mbs_controller_iface_t *mbs_controller_iface = NULL; + MB_RETURN_ON_FALSE((ctx && config), ESP_ERR_INVALID_STATE, TAG, + "mb stack init interface fail."); + MB_RETURN_ON_FALSE((!*ctx), ESP_ERR_INVALID_STATE, TAG, "mb stack is not destroyed?"); + mb_serial_opts_t *pcomm_info = &config->ser_opts; + + // Check communication options + MB_RETURN_ON_FALSE(((pcomm_info->mode == MB_RTU) || (pcomm_info->mode == MB_ASCII)), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect mode = (%u).", + (unsigned)pcomm_info->mode); + MB_RETURN_ON_FALSE((pcomm_info->port <= UART_NUM_MAX), ESP_ERR_INVALID_ARG, TAG, + "mb wrong port to set = (%u).", (unsigned)pcomm_info->port); + MB_RETURN_ON_FALSE((pcomm_info->parity <= UART_PARITY_ODD), ESP_ERR_INVALID_ARG, TAG, + "mb wrong parity option = (%u).", (unsigned)pcomm_info->parity); + MB_RETURN_ON_FALSE((pcomm_info->uid <= MB_ADDRESS_MAX), + ESP_ERR_INVALID_ARG, TAG, "mb wrong slave address = (0x%u).", + (unsigned)pcomm_info->uid); + + esp_err_t ret = mbc_serial_slave_controller_create((void *)&mbs_controller_iface); + MB_GOTO_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE, error, TAG, + "mbc create returns (0x%x).", (int)ret); + + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(mbs_controller_iface); + mbs_opts->port_type = MB_PORT_SERIAL_SLAVE; + mbs_opts->comm_opts = *config; + mb_err_enum_t err = MB_ENOERR; + void *pinst = (void *)mbs_controller_iface; + + // Initialize Modbus stack using mbcontroller parameters + if (pcomm_info->mode == MB_RTU) + { + err = mbs_rtu_create(pcomm_info, &pinst); + } + else if (pcomm_info->mode == MB_ASCII) + { + err = mbs_ascii_create(pcomm_info, &pinst); + } + MB_GOTO_ON_FALSE((err == MB_ENOERR), ESP_ERR_INVALID_STATE, error, TAG, + "mbs create returns (0x%x).", (int)err); + mbs_controller_iface->mb_base = (mb_base_t *)pinst; + + // Configure Modbus read/write callbacks for the base modbus object + const mb_rw_callbacks_t rw_cbs = { + .reg_input_cb = mbc_reg_input_slave_cb, + .reg_holding_cb = mbc_reg_holding_slave_cb, + .reg_coils_cb = mbc_reg_coils_slave_cb, + .reg_discrete_cb = mbc_reg_discrete_slave_cb + }; + + mbs_controller_iface->mb_base->rw_cbs = rw_cbs; + mbs_controller_iface->is_active = false; + *ctx = (void *)mbs_controller_iface; + return ESP_OK; + +error: + if (mbs_controller_iface) { + if (mbs_controller_iface->mb_base) { + mbs_controller_iface->mb_base->delete (mbs_controller_iface->mb_base); + mbs_controller_iface->mb_base = NULL; + } + mbc_serial_slave_iface_free((void *)mbs_controller_iface); + *ctx = NULL; + } + return ret; +} + +#endif \ No newline at end of file diff --git a/modbus/mb_controller/serial/mbc_serial_slave.h b/modbus/mb_controller/serial/mbc_serial_slave.h new file mode 100644 index 0000000..9d439dc --- /dev/null +++ b/modbus/mb_controller/serial/mbc_serial_slave.h @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// mbc_serial_slave.h Modbus controller serial slave implementation header file + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include // for standard int types definition +#include // for NULL and std defines +#include "esp_modbus_common.h" // for common defines +#include "sdkconfig.h" + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_CONTROLLER_NOTIFY_QUEUE_SIZE (CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE) // Number of messages in parameter notification queue +#define MB_CONTROLLER_NOTIFY_TIMEOUT (pdMS_TO_TICKS(CONFIG_FMB_CONTROLLER_NOTIFY_TIMEOUT)) // notification timeout + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +/* + * @brief Initialize Modbus controller and stack + * + * @param[out] ctx - pointer to pointer of interface structure + * @param[in] config - pointer to configuration structure + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + */ +esp_err_t mbc_serial_slave_create(mb_communication_info_t *config, void **ctx); + +#endif + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_controller/tcp/mbc_tcp_master.c b/modbus/mb_controller/tcp/mbc_tcp_master.c new file mode 100644 index 0000000..e966a6e --- /dev/null +++ b/modbus/mb_controller/tcp/mbc_tcp_master.c @@ -0,0 +1,768 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// mbc_tcp_master.c +// TCP master implementation of the Modbus controller + +#include // for calculation of time stamp in milliseconds +#include "esp_log.h" // for log_write +#include // for memcpy +#include // for list +#include "freertos/FreeRTOS.h" // for task creation and queue access +#include "freertos/task.h" // for task api access +#include "freertos/event_groups.h" // for event groups +#include "freertos/queue.h" // for queue api access + +#include "sdkconfig.h" // for KConfig values +#include "esp_modbus_common.h" // for common types +#include "esp_modbus_master.h" // for public master types +#include "mbc_master.h" // for private master types +#include "mbc_tcp_master.h" // for tcp master create function and types +#include "port_tcp_common.h" +#include "port_tcp_master.h" + +#include "mb_common.h" // for mb types definition +#include "mb_config.h" +#include "mb_proto.h" +#include "mb_port_types.h" + +#if MB_MASTER_TCP_ENABLED + +/*-----------------------Master mode use these variables----------------------*/ +static const char *TAG = "mbc_tcp.master"; + +#define MB_TCP_CONNECTION_TOUT pdMS_TO_TICKS(CONFIG_FMB_TCP_CONNECTION_TOUT_SEC * 1000) + +//typedef enum _mb_sock_state mb_sock_state_t; + +// Modbus event processing task +static void modbus_tcp_master_task(void *param) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(param); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(param); + + // Main Modbus stack processing cycle + for (;;) { + // Wait for poll events + BaseType_t status = xEventGroupWaitBits(mbm_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, + pdFALSE, + portMAX_DELAY); + // Check if stack started then poll for data + if (status & MB_EVENT_STACK_STARTED) { + (void)mbm_iface->mb_base->poll(mbm_iface->mb_base); + } + } +} + +static void mbc_tcp_master_conn_done_cb(void *ctx) +{ + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + + ESP_LOGI(TAG, "mb controller connection done."); + EventBits_t flag = xEventGroupSetBits(mbm_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_CONNECTED); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_CONNECTED), + ;, TAG, "mb stack connected event set error."); +} + +// Modbus controller stack start function +static esp_err_t mbc_tcp_master_start(void *ctx) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(ctx); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mb_err_enum_t status = MB_EIO; + mbm_iface->mb_base->descr.parent = ctx; + + MB_RETURN_ON_FALSE((mbm_opts->mbm_param_descriptor_size >= 1), + ESP_ERR_INVALID_ARG, TAG,"mb descriptor table size is incorrect."); + + status = mbm_iface->mb_base->enable(mbm_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack start fail, returned (0x%x).", (uint16_t)status); + // Wait the connection esteblished before start polling according to the option + if (!mbm_opts->comm_opts.tcp_opts.start_disconnected) { + BaseType_t start = xEventGroupWaitBits(mbm_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_CONNECTED), + pdFALSE, + pdFALSE, + MB_TCP_CONNECTION_TOUT); + MB_RETURN_ON_FALSE((start), ESP_ERR_INVALID_STATE, TAG, + "mb stack could not connect to slaves for %u seconds.", + CONFIG_FMB_TCP_CONNECTION_TOUT_SEC); + } + mbm_iface->is_active = true; + + xEventGroupSetBits(mbm_opts->event_group_handle, (EventBits_t)MB_EVENT_STACK_STARTED); + + return ESP_OK; +} + +// Modbus controller stack stop function +static esp_err_t mbc_tcp_master_stop(void *ctx) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(ctx); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mb_err_enum_t status = MB_EIO; + mbm_iface->mb_base->descr.parent = ctx; + + // Set the mbcontroller start flag + EventBits_t flag = xEventGroupClearBits(mbm_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_STARTED); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_STARTED), + ESP_ERR_INVALID_STATE, TAG, "mb stack stop event set error."); + + status = mbm_iface->mb_base->disable(mbm_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack disable fail, returned (0x%x).", (uint16_t)status); + mbm_iface->is_active = false; + return ESP_OK; +} + +// Set Modbus parameter description table +static esp_err_t mbc_tcp_master_set_descriptor(void *ctx, const mb_parameter_descriptor_t *descriptor, const uint16_t num_elements) +{ + MB_RETURN_ON_FALSE((descriptor), ESP_ERR_INVALID_ARG, TAG, "mb incorrect descriptor."); + MB_RETURN_ON_FALSE((num_elements >= 1), ESP_ERR_INVALID_ARG, TAG, "mb table size is incorrect."); + mbm_controller_iface_t *mbm_controller_iface = MB_MASTER_GET_IFACE(ctx); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + + const char **comm_ip_table = (const char **)mbm_opts->comm_opts.tcp_opts.ip_addr_table; + MB_RETURN_ON_FALSE((comm_ip_table), ESP_ERR_INVALID_ARG, TAG, "mb ip table address is incorrect."); + + const mb_parameter_descriptor_t *reg_ptr = descriptor; + mb_uid_info_t *paddr_info = NULL; + + // Go through all items in the table to check all Modbus registers + for (int idx = 0; idx < (num_elements); idx++, reg_ptr++) { + // Check consistency of the table format and required fields. + MB_RETURN_ON_FALSE((reg_ptr->cid == idx), ESP_ERR_INVALID_ARG, TAG, "mb descriptor cid field is incorrect."); + MB_RETURN_ON_FALSE((reg_ptr->param_key), ESP_ERR_INVALID_ARG, TAG, "mb descriptor param key is incorrect."); + MB_RETURN_ON_FALSE((reg_ptr->mb_size > 0), ESP_ERR_INVALID_ARG, TAG, "mb descriptor param size is incorrect."); + + if (reg_ptr->mb_slave_addr == MB_SLAVE_ADDR_PLACEHOLDER) { + continue; // skip not defined uid in the data dictionary + } + + // Is the slave with the UID already in the list? + paddr_info = mbm_port_tcp_get_slave_info(mbm_controller_iface->mb_base->port_obj, reg_ptr->mb_slave_addr, MB_SOCK_STATE_OPENED); + MB_RETURN_ON_FALSE((paddr_info), ESP_ERR_INVALID_ARG, TAG, + "mb missing IP address configuration for cid #%u, uid=%d.", (unsigned)reg_ptr->cid, (int)reg_ptr->mb_slave_addr); + ESP_LOGI(TAG, "mb found config for cid #%d, uid=%d.", (int)reg_ptr->cid, (int)reg_ptr->mb_slave_addr); + } + mbm_opts->param_descriptor_table = descriptor; + mbm_opts->mbm_param_descriptor_size = num_elements; + return ESP_OK; +} + +// Send custom Modbus request defined as mb_param_request_t structure +static esp_err_t mbc_tcp_master_send_request(void *ctx, mb_param_request_t *request, void *data_ptr) +{ + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mbm_controller_iface_t *mbm_controller_iface = MB_MASTER_GET_IFACE(ctx); + MB_RETURN_ON_FALSE((request), ESP_ERR_INVALID_ARG, TAG, "mb request structure."); + MB_RETURN_ON_FALSE((data_ptr), ESP_ERR_INVALID_ARG, TAG, "mb incorrect data pointer."); + + mb_err_enum_t mb_error = MB_EBUSY; + esp_err_t error = ESP_FAIL; + + if (mb_port_event_res_take(mbm_controller_iface->mb_base->port_obj, pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS))) { + uint8_t mb_slave_addr = request->slave_addr; + uint8_t mb_command = request->command; + uint16_t mb_offset = request->reg_start; + uint16_t mb_size = request->reg_size; + + // Set the buffer for callback function processing of received data + mbm_opts->reg_buffer_ptr = (uint8_t *)data_ptr; + mbm_opts->reg_buffer_size = mb_size; + + mb_port_event_res_release(mbm_controller_iface->mb_base->port_obj); + + // Calls appropriate request function to send request and waits response + switch(mb_command) { +#if MB_FUNC_READ_COILS_ENABLED + case MB_FUNC_READ_COILS: + mb_error = mbm_rq_read_coils(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size , + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_COIL_ENABLED + case MB_FUNC_WRITE_SINGLE_COIL: + mb_error = mbm_rq_write_coil(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + *(uint16_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED + case MB_FUNC_WRITE_MULTIPLE_COILS: + mb_error = mbm_rq_write_multi_coils(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, (uint8_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED + case MB_FUNC_READ_DISCRETE_INPUTS: + mb_error = mbm_rq_read_discrete_inputs(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READ_HOLDING_ENABLED + case MB_FUNC_READ_HOLDING_REGISTER: + mb_error = mbm_rq_read_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_HOLDING_ENABLED + case MB_FUNC_WRITE_REGISTER: + mb_error = mbm_rq_write_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + *(uint16_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED + case MB_FUNC_WRITE_MULTIPLE_REGISTERS: + mb_error = mbm_rq_write_multi_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, + (uint16_t)mb_offset, (uint16_t)mb_size, + (uint16_t *)data_ptr, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READWRITE_HOLDING_ENABLED + case MB_FUNC_READWRITE_MULTIPLE_REGISTERS: + mb_error = mbm_rq_rw_multi_holding_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, (uint16_t *)data_ptr, + (uint16_t)mb_offset, (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + +#if MB_FUNC_READ_INPUT_ENABLED + case MB_FUNC_READ_INPUT_REGISTER: + mb_error = mbm_rq_read_inp_reg(mbm_controller_iface->mb_base, (uint8_t)mb_slave_addr, (uint16_t)mb_offset, + (uint16_t)mb_size, + pdMS_TO_TICKS(MB_MAX_RESP_DELAY_MS)); + break; +#endif + default: + ESP_LOGE(TAG, "%s: Incorrect or unsupported function in request (%u) ", + __FUNCTION__, (unsigned)mb_command); + mb_error = MB_ENOREG; + break; + } + } + + // Propagate the Modbus errors to higher level + switch(mb_error) { + case MB_ENOERR: + error = ESP_OK; + break; + + case MB_ENOREG: + error = ESP_ERR_NOT_SUPPORTED; // Invalid register request + break; + + case MB_ETIMEDOUT: + error = ESP_ERR_TIMEOUT; // Slave did not send response + break; + + case MB_EILLFUNC: + case MB_ERECVDATA: + error = ESP_ERR_INVALID_RESPONSE; // Invalid response from slave + break; + + case MB_EBUSY: + error = ESP_ERR_INVALID_STATE; // Master is busy (previous request is pending) + break; + + default: + ESP_LOGE(TAG, "%s: Incorrect return code (%x) ", __FUNCTION__, (int)mb_error); + error = ESP_FAIL; + break; + } + + return error; +} + +static esp_err_t mbc_tcp_master_get_cid_info(void *ctx, uint16_t cid, const mb_parameter_descriptor_t** param_buffer) +{ + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + + MB_RETURN_ON_FALSE((param_buffer), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect data buffer pointer."); + MB_RETURN_ON_FALSE((mbm_opts->param_descriptor_table), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect descriptor table or not set."); + MB_RETURN_ON_FALSE((cid < mbm_opts->mbm_param_descriptor_size), + ESP_ERR_NOT_FOUND, TAG, "mb incorrect cid of characteristic."); + + // It is assumed that characteristics cid increased in the table + const mb_parameter_descriptor_t *reg_info = &mbm_opts->param_descriptor_table[cid]; + + MB_RETURN_ON_FALSE((reg_info->param_key), + ESP_ERR_INVALID_ARG, TAG, "mb incorrect characteristic key."); + *param_buffer = reg_info; + return ESP_OK; +} + +// Helper function to get modbus command for each type of Modbus register area +static uint8_t mbc_tcp_master_get_command(mb_param_type_t param_type, mb_param_mode_t mode) +{ + uint8_t command = 0; + switch(param_type){ // Check commands + case MB_PARAM_HOLDING: + command = (mode == MB_PARAM_WRITE) ? + MB_FUNC_WRITE_MULTIPLE_REGISTERS : + MB_FUNC_READ_HOLDING_REGISTER; + break; + case MB_PARAM_INPUT: + command = MB_FUNC_READ_INPUT_REGISTER; + break; + case MB_PARAM_COIL: + command = (mode == MB_PARAM_WRITE) ? + MB_FUNC_WRITE_MULTIPLE_COILS : + MB_FUNC_READ_COILS; + break; + case MB_PARAM_DISCRETE: + if (mode != MB_PARAM_WRITE) { + command = MB_FUNC_READ_DISCRETE_INPUTS; + } else { + ESP_LOGE(TAG, "%s: Incorrect mode (%u)", + __FUNCTION__, (unsigned)mode); + } + break; + default: + ESP_LOGE(TAG, "%s: Incorrect param type (%u)", + __FUNCTION__, (unsigned)param_type); + break; + } + return command; +} + + +// Helper to search parameter in the parameter description table and fills Modbus request fields accordingly +static esp_err_t mbc_tcp_master_set_request(void *ctx, uint16_t cid, mb_param_mode_t mode, mb_param_request_t *request, + mb_parameter_descriptor_t *reg_data) +{ + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + esp_err_t error = ESP_ERR_NOT_FOUND; + MB_RETURN_ON_FALSE((request), ESP_ERR_INVALID_ARG, TAG, "mb incorrect request parameter."); + MB_RETURN_ON_FALSE((mode <= MB_PARAM_WRITE), ESP_ERR_INVALID_ARG, TAG, "mb incorrect mode."); + MB_RETURN_ON_FALSE((cid < mbm_opts->mbm_param_descriptor_size), ESP_ERR_INVALID_ARG, TAG, "mb incorrect cid parameter."); + MB_RETURN_ON_FALSE((mbm_opts->param_descriptor_table), ESP_ERR_INVALID_ARG, TAG, "mb data dictionary is incorrect."); + const mb_parameter_descriptor_t *reg_ptr = mbm_opts->param_descriptor_table; + reg_ptr += cid; + if (reg_ptr->cid == cid) { + request->slave_addr = reg_ptr->mb_slave_addr; + request->reg_start = reg_ptr->mb_reg_start; + request->reg_size = reg_ptr->mb_size; + request->command = mbc_tcp_master_get_command(reg_ptr->mb_param_type, mode); + MB_RETURN_ON_FALSE((request->command > 0), ESP_ERR_INVALID_ARG, TAG, "mb incorrect command or parameter type."); + if (reg_data) { + *reg_data = *reg_ptr; // Set the cid registered parameter data + } + error = ESP_OK; + } + return error; +} + +// Get parameter data for corresponding characteristic +static esp_err_t mbc_tcp_master_get_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type) +{ + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + MB_RETURN_ON_FALSE((value), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + mbm_controller_iface_t *mbm_controller_iface = MB_MASTER_GET_IFACE(ctx); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request ; + mb_parameter_descriptor_t reg_info = { 0 }; + uint8_t *pdata = NULL; + + error = mbc_tcp_master_set_request(ctx, cid, MB_PARAM_READ, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid) && (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER)) { + mb_uid_info_t *paddr_info = mbm_port_tcp_get_slave_info(mbm_controller_iface->mb_base->port_obj, + request.slave_addr, MB_SOCK_STATE_CONNECTED); + MB_RETURN_ON_FALSE((paddr_info), ESP_ERR_NOT_FOUND, TAG, + "mb can not send request for cid #%u with uid = %d.", + (unsigned)reg_info.cid, (int)request.slave_addr); + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + // alloc buffer to store parameter data + pdata = calloc(1, (reg_info.mb_size << 1)); + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + error = mbc_tcp_master_send_request(ctx, &request, pdata); + if (error == ESP_OK) { + // If data pointer is NULL then we don't need to set value (it is still in the cache of cid) + if (value) { + error = mbc_master_set_param_data((void *)value, (void *)pdata, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + error = ESP_ERR_INVALID_STATE; + } else { + ESP_LOGD(TAG, "%s: Good response for get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + } + } else { + ESP_LOGD(TAG, "%s: Bad response to get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } else { + ESP_LOGE(TAG, "%s: The cid(%u) not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} + +// Get parameter data for corresponding characteristic +static esp_err_t mbc_tcp_master_get_parameter_with(void *ctx, uint16_t cid, uint8_t uid, uint8_t *value, uint8_t *type) +{ + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + MB_RETURN_ON_FALSE((value), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + mbm_controller_iface_t *mbm_controller_iface = MB_MASTER_GET_IFACE(ctx); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request; + mb_parameter_descriptor_t reg_info = { 0 }; + uint8_t *pdata = NULL; + + error = mbc_tcp_master_set_request(ctx, cid, MB_PARAM_READ, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid)) { + // check that the requested uid is connected (call to port iface) + mb_uid_info_t *paddr_info = mbm_port_tcp_get_slave_info(mbm_controller_iface->mb_base->port_obj, + uid, MB_SOCK_STATE_CONNECTED); + MB_RETURN_ON_FALSE((paddr_info), ESP_ERR_NOT_FOUND, TAG, + "mb can not send request for cid #%u with uid=%d.", (unsigned)reg_info.cid, (int)uid); + if (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER) { + ESP_LOGD(TAG, "%s: override uid %d = %d for cid(%u)", + __FUNCTION__, (int)request.slave_addr, (int)uid, (unsigned)reg_info.cid); + } + request.slave_addr = uid; // override the UID + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + // alloc buffer to store parameter data + pdata = calloc(1, (reg_info.mb_size << 1)); + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + error = mbc_tcp_master_send_request(ctx, &request, pdata); + if (error == ESP_OK) { + // If data pointer is NULL then we don't need to set value (it is still in the cache of cid) + if (value) { + error = mbc_master_set_param_data((void *)value, (void *)pdata, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + error = ESP_ERR_INVALID_STATE; + } else { + ESP_LOGD(TAG, "%s: Good response for get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + } + } else { + ESP_LOGD(TAG, "%s: Bad response to get cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } else { + ESP_LOGE(TAG, "%s: The cid(%u) address information is not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} + +// Set parameter value for characteristic selected by name and cid +static esp_err_t mbc_tcp_master_set_parameter(void *ctx, uint16_t cid, uint8_t *value, uint8_t *type) +{ + MB_RETURN_ON_FALSE((value), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + mbm_controller_iface_t *mbm_controller_iface = MB_MASTER_GET_IFACE(ctx); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request ; + mb_parameter_descriptor_t reg_info = { 0 }; + uint8_t *pdata = NULL; + + error = mbc_tcp_master_set_request(ctx, cid, MB_PARAM_WRITE, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid) && (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER)) { + mb_uid_info_t *paddr_info = mbm_port_tcp_get_slave_info(mbm_controller_iface->mb_base->port_obj, + request.slave_addr, MB_SOCK_STATE_CONNECTED); + MB_RETURN_ON_FALSE((paddr_info), ESP_ERR_NOT_FOUND, TAG, + "mb can not send request for cid #%u with uid=%d.", + (unsigned)reg_info.cid, (int)request.slave_addr); + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + pdata = calloc(1, (reg_info.mb_size << 1)); // alloc parameter buffer + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + // Transfer value of characteristic into parameter buffer + error = mbc_master_set_param_data((void *)pdata, (void *)value, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + free(pdata); + return ESP_ERR_INVALID_STATE; + } + // Send request to write characteristic data + error = mbc_tcp_master_send_request(ctx, &request, pdata); + if (error == ESP_OK) { + ESP_LOGD(TAG, "%s: Good response for set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } else { + ESP_LOGD(TAG, "%s: Bad response to set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } else { + ESP_LOGE(TAG, "%s: The requested cid(%u) not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} + +// Set parameter value for characteristic selected by name and cid +static esp_err_t mbc_tcp_master_set_parameter_with(void *ctx, uint16_t cid, uint8_t uid, uint8_t *value, uint8_t *type) +{ + MB_RETURN_ON_FALSE((value), ESP_ERR_INVALID_ARG, TAG, "value pointer is incorrect."); + MB_RETURN_ON_FALSE((type), ESP_ERR_INVALID_ARG, TAG, "type pointer is incorrect."); + mbm_controller_iface_t *mbm_controller_iface = MB_MASTER_GET_IFACE(ctx); + esp_err_t error = ESP_ERR_INVALID_RESPONSE; + mb_param_request_t request ; + mb_parameter_descriptor_t reg_info = { 0 }; + uint8_t *pdata = NULL; + + error = mbc_tcp_master_set_request(ctx, cid, MB_PARAM_WRITE, &request, ®_info); + if ((error == ESP_OK) && (cid == reg_info.cid)) { + // check that the requested uid is connected (call to port iface) + mb_uid_info_t *paddr_info = mbm_port_tcp_get_slave_info(mbm_controller_iface->mb_base->port_obj, + uid, MB_SOCK_STATE_CONNECTED); + MB_RETURN_ON_FALSE((paddr_info), ESP_ERR_NOT_FOUND, TAG, + "mb can not send request for cid #%d with uid=%d.", + (unsigned)reg_info.cid, (int)uid); + if (request.slave_addr != MB_SLAVE_ADDR_PLACEHOLDER) { + ESP_LOGD(TAG, "%s: override uid %d = %d for cid(%u)", + __FUNCTION__, (int)request.slave_addr, (int)uid, (unsigned)reg_info.cid); + } + request.slave_addr = uid; // override the UID + MB_MASTER_ASSERT(xPortGetFreeHeapSize() > (reg_info.mb_size << 1)); + + pdata = calloc(1, (reg_info.mb_size << 1)); // alloc parameter buffer + if (!pdata) { + return ESP_ERR_INVALID_STATE; + } + // Transfer value of characteristic into parameter buffer + error = mbc_master_set_param_data((void *)pdata, (void *)value, + reg_info.param_type, reg_info.param_size); + if (error != ESP_OK) { + ESP_LOGE(TAG, "fail to set parameter data."); + free(pdata); + return ESP_ERR_INVALID_STATE; + } + // Send request to write characteristic data + error = mbc_tcp_master_send_request(ctx, &request, pdata); + if (error == ESP_OK) { + ESP_LOGD(TAG, "%s: Good response for set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } else { + ESP_LOGD(TAG, "%s: Bad response to set cid(%u) = %s", + __FUNCTION__, (unsigned)reg_info.cid, (char *)esp_err_to_name(error)); + } + free(pdata); + // Set the type of parameter found in the table + *type = reg_info.param_type; + } else { + ESP_LOGE(TAG, "%s: The requested cid(%u) not found in the data dictionary.", + __FUNCTION__, (unsigned)reg_info.cid); + error = ESP_ERR_INVALID_ARG; + } + return error; +} + +// Modbus controller delete function +static esp_err_t mbc_tcp_master_delete(void *ctx) +{ + mbm_controller_iface_t *mbm_iface = MB_MASTER_GET_IFACE(ctx); + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(ctx); + mb_err_enum_t mb_error = MB_ENOERR; + + // Check the stack started bit + BaseType_t status = xEventGroupWaitBits(mbm_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, + pdFALSE, + pdMS_TO_TICKS(mbm_opts->comm_opts.tcp_opts.response_tout_ms)); + if (mbm_iface->is_active || (status & MB_EVENT_STACK_STARTED)) { + ESP_LOGD(TAG, "mb stack is active, try to disable."); + MB_RETURN_ON_FALSE((mbc_tcp_master_stop(ctx) == ESP_OK), + ESP_ERR_INVALID_STATE, TAG, "mb stack stop failure."); + } + + mbm_iface->is_active = false; + vTaskDelete(mbm_opts->task_handle); + mbm_opts->task_handle = NULL; + vEventGroupDelete(mbm_opts->event_group_handle); + mbm_opts->event_group_handle = NULL; + + mb_error = mbm_iface->mb_base->delete(mbm_iface->mb_base); + MB_RETURN_ON_FALSE((mb_error == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack delete failure, returned (0x%x).", (unsigned)mb_error); + free(mbm_iface); // free the memory allocated + ctx = NULL; + return ESP_OK; +} + +// Initialization of resources for Modbus TCP master controller +esp_err_t mbc_tcp_master_controller_create(void ** ctx) +{ + mbm_controller_iface_t *mbm_controller_iface = (mbm_controller_iface_t *)*ctx; + MB_RETURN_ON_FALSE((mbm_controller_iface == NULL), ESP_ERR_INVALID_STATE, TAG, + "mb stack is not destroyed."); + esp_err_t ret = ESP_ERR_INVALID_STATE; + mbm_controller_iface = malloc(sizeof(mbm_controller_iface_t)); + MB_GOTO_ON_FALSE((mbm_controller_iface), ESP_ERR_INVALID_STATE, error, + TAG, "mb stack memory allocation fail."); + + // Initialize interface properties + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(mbm_controller_iface); + + // Initialization of active context of the modbus controller + BaseType_t status = 0; + // Parameter change notification queue + mbm_opts->event_group_handle = xEventGroupCreate(); + MB_GOTO_ON_FALSE((mbm_opts->event_group_handle), ESP_ERR_INVALID_STATE, error, TAG, "mb event group error."); + // Create modbus controller task + status = xTaskCreatePinnedToCore((void *)&modbus_tcp_master_task, + "mbm_ctrl_tcp_task", + MB_CONTROLLER_STACK_SIZE, + mbm_controller_iface, + MB_CONTROLLER_PRIORITY, + &mbm_opts->task_handle, + MB_PORT_TASK_AFFINITY); + MB_GOTO_ON_FALSE((status == pdPASS), ESP_ERR_INVALID_STATE, error, TAG, + "mb controller task creation error, xTaskCreate() returns (0x%x).", (unsigned)status); + MB_MASTER_ASSERT(mbm_opts->task_handle); // The task is created but handle is incorrect + + // Initialize public interface methods of the interface + mbm_controller_iface->create = mbc_tcp_master_create; + mbm_controller_iface->delete = mbc_tcp_master_delete; + mbm_controller_iface->start = mbc_tcp_master_start; + mbm_controller_iface->stop = mbc_tcp_master_stop; + mbm_controller_iface->get_cid_info = mbc_tcp_master_get_cid_info; + mbm_controller_iface->get_parameter = mbc_tcp_master_get_parameter; + mbm_controller_iface->get_parameter_with = mbc_tcp_master_get_parameter_with; + mbm_controller_iface->send_request = mbc_tcp_master_send_request; + mbm_controller_iface->set_descriptor = mbc_tcp_master_set_descriptor; + mbm_controller_iface->set_parameter = mbc_tcp_master_set_parameter; + mbm_controller_iface->set_parameter_with = mbc_tcp_master_set_parameter_with; + + *ctx = mbm_controller_iface; + return ESP_OK; + +error: + if (mbm_controller_iface) { + if (mbm_controller_iface->opts.task_handle) { + vTaskDelete(mbm_controller_iface->opts.task_handle); + mbm_controller_iface->opts.task_handle = NULL; + } + if (mbm_controller_iface->opts.event_group_handle) { + vEventGroupDelete(mbm_controller_iface->opts.event_group_handle); + mbm_controller_iface->opts.event_group_handle = NULL; + } + } + free(mbm_controller_iface); // free the memory allocated + ctx = NULL; + return ret; +} + +// Initialization of resources for Modbus serial master controller +esp_err_t mbc_tcp_master_create(mb_communication_info_t *config, void **ctx) +{ + MB_RETURN_ON_FALSE((ctx && config), ESP_ERR_INVALID_STATE, TAG, + "mb stack init interface fail."); + mbm_controller_iface_t *mbm_controller_iface = (mbm_controller_iface_t *)*ctx; + MB_RETURN_ON_FALSE((!mbm_controller_iface), ESP_ERR_INVALID_STATE, TAG, + "mb stack is not destroyed."); + // Check communication options + mb_tcp_opts_t tcp_opts = (mb_tcp_opts_t)config->tcp_opts; + MB_RETURN_ON_FALSE((tcp_opts.ip_addr_table), ESP_ERR_INVALID_ARG, TAG, "mb ip table address is incorrect."); + MB_RETURN_ON_FALSE((tcp_opts.mode == MB_TCP), + ESP_ERR_INVALID_ARG, TAG, "mb transport protocol is incorrect."); + MB_RETURN_ON_FALSE(((tcp_opts.addr_type == MB_IPV6) || (tcp_opts.addr_type == MB_IPV4)), + ESP_ERR_INVALID_ARG, TAG, "mb ip address type is incorrect."); + MB_RETURN_ON_FALSE((tcp_opts.port), ESP_ERR_INVALID_ARG, TAG, "mb port is not defined."); + + esp_err_t ret = mbc_tcp_master_controller_create((void *)&mbm_controller_iface); + MB_GOTO_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE, error, TAG, + "mbc create returns (0x%x).", (int)ret); + + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(mbm_controller_iface); + // keep the communication options to be able to restart port driver + mbm_opts->comm_opts = *config; + + mbm_opts->port_type = MB_PORT_TCP_MASTER; + tcp_opts.mode = MB_TCP; // Override mode, UDP mode is not supported + mbm_opts->comm_opts.tcp_opts = tcp_opts; + + // Keep the response time setting + if (!tcp_opts.response_tout_ms) { + mbm_opts->comm_opts.tcp_opts.response_tout_ms = CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND; + } + + mb_err_enum_t err = MB_EILLSTATE; + void *pinst = (void *)mbm_controller_iface; // set as descr.parent object + + // Initialize Modbus stack using mbcontroller parameters + if (tcp_opts.mode == MB_TCP) { + err = mbm_tcp_create(&tcp_opts, &pinst); + } + MB_GOTO_ON_FALSE((err == MB_ENOERR), ESP_ERR_INVALID_STATE, error, TAG, + "mbm create returns (0x%x).", (int)ret); + + mbm_controller_iface->mb_base = (mb_base_t *)pinst; + + const mb_rw_callbacks_t rw_cbs = { + .reg_input_cb = mbc_reg_input_master_cb, + .reg_holding_cb = mbc_reg_holding_master_cb, + .reg_coils_cb = mbc_reg_coils_master_cb, + .reg_discrete_cb = mbc_reg_discrete_master_cb + }; + + mbm_controller_iface->mb_base->rw_cbs = rw_cbs; + if (!mbm_opts->comm_opts.tcp_opts.start_disconnected) { + mbm_port_tcp_set_conn_cb(mbm_controller_iface->mb_base->port_obj, + &mbc_tcp_master_conn_done_cb, + (void *)mbm_controller_iface); + } + mbm_controller_iface->is_active = false; + *ctx = mbm_controller_iface; + return ESP_OK; + +error: + if (mbm_controller_iface->mb_base) { + mbm_controller_iface->mb_base->delete(mbm_controller_iface->mb_base); + mbm_controller_iface->mb_base = NULL; + } + return ret; +} + +#endif diff --git a/modbus/mb_controller/tcp/mbc_tcp_master.h b/modbus/mb_controller/tcp/mbc_tcp_master.h new file mode 100644 index 0000000..876b851 --- /dev/null +++ b/modbus/mb_controller/tcp/mbc_tcp_master.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// mbc_tcp_master.h Modbus controller TCP master implementation header file + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include // for standard int types definition +#include // for NULL and std defines +#include "esp_modbus_common.h" // for common defines + +/* ----------------------- Defines ------------------------------------------*/ + +/** + * @brief Create Modbus Master controller and stack for TCP port + * + * @param[out] ctx - pointer to pointer of interface structure + * @param[in] config - pointer to configuration structure + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + */ +esp_err_t mbc_tcp_master_create(mb_communication_info_t *config, void **ctx); + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_controller/tcp/mbc_tcp_slave.c b/modbus/mb_controller/tcp/mbc_tcp_slave.c new file mode 100644 index 0000000..c936712 --- /dev/null +++ b/modbus/mb_controller/tcp/mbc_tcp_slave.c @@ -0,0 +1,274 @@ +/* + * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// mbc_tcp_slave.c +// Implementation of the Modbus controller TCP slave + +#include // for calculation of time stamp in milliseconds +#include "esp_log.h" // for log_write +#include "sdkconfig.h" // for KConfig values +#include "esp_modbus_common.h" // for common defines +#include "esp_modbus_slave.h" // for public slave interface types +#include "mbc_slave.h" // for private slave interface types +#include "mbc_tcp_slave.h" // for tcp slave mb controller defines +#include "port_tcp_common.h" + +#include "mb_common.h" // for mb types definition + +#if MB_TCP_ENABLED + +static const char *TAG = "mbc_tcp.slave"; + +// Modbus task function +static void modbus_tcp_slave_task(void *param) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(param); + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(param); + + // Main Modbus stack processing cycle + for (;;) { + BaseType_t status = xEventGroupWaitBits(mbs_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, // do not clear bits + pdFALSE, + portMAX_DELAY); + // Check if stack started then poll for data + if (status & MB_EVENT_STACK_STARTED) { + (void)mbs_iface->mb_base->poll(mbs_iface->mb_base); + } + } +} + +// Start Modbus controller start function +static esp_err_t mbc_tcp_slave_start(void *ctx) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(ctx); + mb_err_enum_t status = MB_EIO; + + status = mbs_iface->mb_base->enable(mbs_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack enable fail, returned (0x%x).", (uint16_t)status); + // Set the mbcontroller start flag + EventBits_t flag = xEventGroupSetBits(mbs_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_STARTED); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_STARTED), + ESP_ERR_INVALID_STATE, TAG, "mb stack start event set error."); + mbs_iface->mb_base->descr.parent = ctx; + mbs_iface->is_active = true; + return ESP_OK; +} + +// Start Modbus controller stop function +static esp_err_t mbc_tcp_slave_stop(void *ctx) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(ctx); + mb_err_enum_t status = MB_EIO; + + status = mbs_iface->mb_base->disable(mbs_iface->mb_base); + MB_RETURN_ON_FALSE((status == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack disable fail, returned (0x%x).", (uint16_t)status); + // Clear the mbcontroller start flag + EventBits_t flag = xEventGroupClearBits(mbs_opts->event_group_handle, + (EventBits_t)MB_EVENT_STACK_STARTED); + MB_RETURN_ON_FALSE((flag & MB_EVENT_STACK_STARTED), + ESP_ERR_INVALID_STATE, TAG, "mb stack start event set error."); + mbs_iface->mb_base->descr.parent = NULL; + mbs_iface->is_active = false; + return ESP_OK; +} + +// Blocking function to get event on parameter group change for application task +static mb_event_group_t mbc_tcp_slave_check_event(void *ctx, mb_event_group_t group) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + MB_SLAVE_ASSERT(mbs_opts->event_group_handle); + BaseType_t status = xEventGroupWaitBits(mbs_opts->event_group_handle, (BaseType_t)group, + pdTRUE , pdFALSE, portMAX_DELAY); + return (mb_event_group_t)status; +} + +// Function to get notification about parameter change from application task +static esp_err_t mbc_tcp_slave_get_param_info(void *ctx, mb_param_info_t *reg_info, uint32_t timeout) +{ + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + esp_err_t err = ESP_ERR_TIMEOUT; + MB_RETURN_ON_FALSE((mbs_opts->notification_queue_handle), + ESP_ERR_INVALID_ARG, TAG, "mb queue handle is invalid."); + MB_RETURN_ON_FALSE(reg_info, ESP_ERR_INVALID_ARG, TAG, "mb register information is invalid."); + BaseType_t status = xQueueReceive(mbs_opts->notification_queue_handle, + reg_info, pdMS_TO_TICKS(timeout)); + if (status == pdTRUE) { + err = ESP_OK; + } + return err; +} + +// Modbus controller delete function +static esp_err_t mbc_tcp_slave_delete(void *ctx) +{ + mbs_controller_iface_t *mbs_iface = MB_SLAVE_GET_IFACE(ctx); + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(ctx); + mb_err_enum_t mb_error = MB_ENOERR; + + // Check the stack started bit + BaseType_t status = xEventGroupWaitBits(mbs_opts->event_group_handle, + (BaseType_t)(MB_EVENT_STACK_STARTED), + pdFALSE, + pdFALSE, + MB_CONTROLLER_NOTIFY_TIMEOUT); + if (mbs_iface->is_active || (status & MB_EVENT_STACK_STARTED)) { + ESP_LOGV(TAG, "mb stack is active, try to disable."); + MB_RETURN_ON_FALSE((mbc_tcp_slave_stop(ctx) == ESP_OK), + ESP_ERR_INVALID_STATE, TAG, "mb stack stop failure."); + } + + mbs_iface->is_active = false; + vTaskDelete(mbs_opts->task_handle); + vEventGroupDelete(mbs_opts->event_group_handle); + vQueueDelete(mbs_opts->notification_queue_handle); + mb_error = mbs_iface->mb_base->delete(mbs_iface->mb_base); + MB_RETURN_ON_FALSE((mb_error == MB_ENOERR), ESP_ERR_INVALID_STATE, TAG, + "mb stack close failure returned (0x%x).", (int)mb_error); + // free the controller will be performed in common object + return ESP_OK; +} + +esp_err_t mbc_tcp_slave_controller_create(void ** ctx) +{ + MB_RETURN_ON_FALSE((ctx), ESP_ERR_INVALID_STATE, TAG, + "mb stack init interface fail."); + mbs_controller_iface_t *mbs_controller_iface = *ctx; + esp_err_t ret = ESP_ERR_INVALID_STATE; + MB_RETURN_ON_FALSE((mbs_controller_iface == NULL), ESP_ERR_INVALID_STATE, TAG, + "mb stack is not destroyed."); + + mbs_controller_iface = malloc(sizeof(mbs_controller_iface_t)); + MB_GOTO_ON_FALSE((mbs_controller_iface), ESP_ERR_NO_MEM, error, + TAG, "mb stack memory allocation fail."); + + mb_slave_options_t *mbs_opts = &mbs_controller_iface->opts; + mbs_opts->port_type = MB_PORT_TCP_SLAVE; // set interface port type + + // Initialization of active context of the Modbus controller + BaseType_t status = 0; + // Parameter change notification queue + mbs_opts->event_group_handle = xEventGroupCreate(); + MB_GOTO_ON_FALSE((mbs_opts->event_group_handle), ESP_ERR_NO_MEM, error, + TAG, "mb event group error."); + // Parameter change notification queue + mbs_opts->notification_queue_handle = xQueueCreate( + MB_CONTROLLER_NOTIFY_QUEUE_SIZE, + sizeof(mb_param_info_t)); + MB_GOTO_ON_FALSE((mbs_opts->notification_queue_handle), ESP_ERR_NO_MEM, error, + TAG, "mb notify queue creation error."); + // Create Modbus controller task + status = xTaskCreatePinnedToCore((void *)&modbus_tcp_slave_task, + "mbc_tcp_slave", + MB_CONTROLLER_STACK_SIZE, + mbs_controller_iface, + MB_CONTROLLER_PRIORITY, + &mbs_opts->task_handle, + MB_PORT_TASK_AFFINITY); + MB_GOTO_ON_FALSE((status == pdPASS), ESP_ERR_INVALID_STATE, error, TAG, + "mb controller task creation error, xTaskCreate() returns (0x%x).", (uint16_t)status); + // The task is created but handle is incorrect + MB_SLAVE_ASSERT(mbs_opts->task_handle); + + // Initialization of interface pointers + mbs_controller_iface->create = mbc_tcp_slave_create; + mbs_controller_iface->delete = mbc_tcp_slave_delete; + mbs_controller_iface->start = mbc_tcp_slave_start; + mbs_controller_iface->check_event = mbc_tcp_slave_check_event; + mbs_controller_iface->get_param_info = mbc_tcp_slave_get_param_info; + mbs_controller_iface->set_descriptor = NULL; // Use common descriptor setter + *ctx = mbs_controller_iface; + return ESP_OK; + +error: + if (mbs_controller_iface) { + if (mbs_controller_iface->opts.task_handle) { + vTaskDelete(mbs_controller_iface->opts.task_handle); + mbs_controller_iface->opts.task_handle = NULL; + } + if (mbs_controller_iface->opts.event_group_handle) { + vEventGroupDelete(mbs_controller_iface->opts.event_group_handle); + mbs_controller_iface->opts.event_group_handle = NULL; + } + } + free(mbs_controller_iface); // free the memory allocated + ctx = NULL; + return ret; +} + +// Initialization of Modbus controller +esp_err_t mbc_tcp_slave_create(mb_communication_info_t *config, void **ctx) +{ + MB_RETURN_ON_FALSE((ctx && config), ESP_ERR_INVALID_STATE, TAG, + "mb stack init interface fail."); + mbs_controller_iface_t *mbs_controller_iface = (mbs_controller_iface_t *)*ctx; + MB_RETURN_ON_FALSE((!mbs_controller_iface), ESP_ERR_INVALID_STATE, TAG, + "mb stack is not destroyed."); + // Check communication options + mb_tcp_opts_t tcp_opts = (mb_tcp_opts_t)config->tcp_opts; + MB_RETURN_ON_FALSE(((tcp_opts.mode == MB_TCP) || (tcp_opts.mode == MB_UDP)), + ESP_ERR_INVALID_ARG, TAG, "mb transport protocol is incorrect."); + MB_RETURN_ON_FALSE(((tcp_opts.addr_type == MB_IPV6) || (tcp_opts.addr_type == MB_IPV4)), + ESP_ERR_INVALID_ARG, TAG, "mb ip address type is incorrect."); + MB_RETURN_ON_FALSE((tcp_opts.port), ESP_ERR_INVALID_ARG, TAG, "mb port is not defined."); + + esp_err_t ret = mbc_tcp_slave_controller_create((void *)&mbs_controller_iface); + MB_GOTO_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE, error, TAG, + "mbc create returns (0x%x).", (uint16_t)ret); + + mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(mbs_controller_iface); + // keep the communication options to be able to restart port driver + mbs_opts->comm_opts = *config; + + mbs_opts->port_type = MB_PORT_TCP_SLAVE; + tcp_opts.mode = MB_TCP; // Override mode, UDP mode is not supported + // Keep the response time setting + if (!tcp_opts.response_tout_ms) { + tcp_opts.response_tout_ms = CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND; + } + // Set default values of communication options + if (!tcp_opts.port) { + mbs_opts->comm_opts.tcp_opts.port = MB_TCP_DEFAULT_PORT; + } + + mbs_opts->comm_opts.tcp_opts = tcp_opts; + mb_err_enum_t err = MB_ENOERR; + void *pinst = (void *)mbs_controller_iface; + + // Initialize Modbus stack using mbcontroller parameters + err = mbs_tcp_create(&tcp_opts, &pinst); + MB_GOTO_ON_FALSE((err == MB_ENOERR), ESP_ERR_INVALID_STATE, error, TAG, + "mbscreate returns (0x%x).", (uint16_t)err); + mbs_controller_iface->mb_base = (mb_base_t *)pinst; + mbs_controller_iface->mb_base->descr.is_master = false; + + // Configure Modbus read/write callbacks for the base modbus object + const mb_rw_callbacks_t rw_cbs = { + .reg_input_cb = mbc_reg_input_slave_cb, + .reg_holding_cb = mbc_reg_holding_slave_cb, + .reg_coils_cb = mbc_reg_coils_slave_cb, + .reg_discrete_cb = mbc_reg_discrete_slave_cb + }; + mbs_controller_iface->mb_base->rw_cbs = rw_cbs; + mbs_controller_iface->is_active = false; + *ctx = (void *)mbs_controller_iface; + return ESP_OK; + +error: + if (mbs_controller_iface->mb_base) { + mbs_controller_iface->mb_base->delete(mbs_controller_iface->mb_base); + mbs_controller_iface->mb_base = NULL; + } + return ret; +} + +#endif //#if MB_TCP_ENABLED diff --git a/modbus/mb_controller/tcp/mbc_tcp_slave.h b/modbus/mb_controller/tcp/mbc_tcp_slave.h new file mode 100644 index 0000000..14c0b12 --- /dev/null +++ b/modbus/mb_controller/tcp/mbc_tcp_slave.h @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2016-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// mbc_tcp_slave.h Modbus controller TCP slave implementation header file + +#pragma once + +#include // for standard int types definition +#include // for NULL and std defines +#include "esp_modbus_common.h" // for common defines + +/* ----------------------- Defines ------------------------------------------*/ + +#define MB_CONTROLLER_NOTIFY_QUEUE_SIZE (CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE) // Number of messages in parameter notification queue +#define MB_CONTROLLER_NOTIFY_TIMEOUT (pdMS_TO_TICKS(CONFIG_FMB_CONTROLLER_NOTIFY_TIMEOUT)) // notification timeout + +/** + * @brief Initialize Modbus controller and stack for TCP slave + * + * @param[out] ctx - pointer to pointer of interface structure + * @param[in] config - pointer to configuration structure + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Parameter error + */ +esp_err_t mbc_tcp_slave_create(mb_communication_info_t *config, void **ctx); diff --git a/modbus/mb_objects/functions/mbfunccoils.c b/modbus/mb_objects/functions/mbfunccoils.c new file mode 100644 index 0000000..156ce7f --- /dev/null +++ b/modbus/mb_objects/functions/mbfunccoils.c @@ -0,0 +1,224 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfunccoils.c, v 1.8 2007/02/18 23:47:16 wolti Exp $ + */ +#include +#include +#include "mb_slave.h" +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_FUNC_READ_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_FUNC_READ_COILCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_READ_SIZE (4) +#define MB_PDU_FUNC_READ_COILCNT_MAX (0x07D0) + +#define MB_PDU_FUNC_WRITE_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_FUNC_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_SIZE (4) + +#define MB_PDU_FUNC_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_FUNC_WRITE_MUL_COILCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_MUL_BYTECNT_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_FUNC_WRITE_MUL_VALUES_OFF (MB_PDU_DATA_OFF + 5) +#define MB_PDU_FUNC_WRITE_MUL_SIZE_MIN (5) +#define MB_PDU_FUNC_WRITE_MUL_COILCNT_MAX (0x07B0) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ + +#if MB_FUNC_READ_COILS_ENABLED > 0 + +mb_exception_t mbs_fn_read_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + uint16_t coil_cnt; + uint8_t byte_num; + uint8_t *frame_cur; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf == (MB_PDU_FUNC_READ_SIZE + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF + 1]); + reg_addr++; + + coil_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_COILCNT_OFF] << 8); + coil_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_COILCNT_OFF + 1]); + + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((coil_cnt >= 1) && + (coil_cnt < MB_PDU_FUNC_READ_COILCNT_MAX)) { + /* Set the current PDU data pointer to the beginning. */ + frame_cur = &frame_ptr[MB_PDU_FUNC_OFF]; + *len_buf = MB_PDU_FUNC_OFF; + + /* First byte contains the function code. */ + *frame_cur++ = MB_FUNC_READ_COILS; + *len_buf += 1; + + /* Test if the quantity of coils is a multiple of 8. If not last + * byte is only partially field with unused coils set to zero. */ + if ((coil_cnt & 0x0007) != 0) { + byte_num = (uint8_t)(coil_cnt / 8 + 1); + } else { + byte_num = (uint8_t)(coil_cnt / 8); + } + + *frame_cur++ = byte_num; + *len_buf += 1; + + if (inst->rw_cbs.reg_coils_cb) { + reg_status = inst->rw_cbs.reg_coils_cb(inst, frame_cur, reg_addr, coil_cnt, MB_REG_READ); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } else { + /* The response contains the function code, the starting address + * and the quantity of registers. We reuse the old values in the + * buffer because they are still valid. */ + *len_buf += byte_num;; + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid read coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#if MB_FUNC_WRITE_COIL_ENABLED > 0 +mb_exception_t mbs_fn_write_coil(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + uint8_t buf[2]; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf == (MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF + 1]); + reg_addr++; + + if ((frame_ptr[MB_PDU_FUNC_WRITE_VALUE_OFF + 1] == 0x00) && + ((frame_ptr[MB_PDU_FUNC_WRITE_VALUE_OFF] == 0xFF) || + (frame_ptr[MB_PDU_FUNC_WRITE_VALUE_OFF] == 0x00))) { + buf[1] = 0; + if (frame_ptr[MB_PDU_FUNC_WRITE_VALUE_OFF] == 0xFF) { + buf[0] = 1; + } else { + buf[0] = 0; + } + + if (inst->rw_cbs.reg_coils_cb) { + reg_status = inst->rw_cbs.reg_coils_cb(inst, &buf[0], reg_addr, 1, MB_REG_WRITE); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid write coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif + +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED > 0 +mb_exception_t mbs_fn_write_multi_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + uint16_t coil_cnt; + uint8_t byte_cnt; + uint8_t byte_cnt_verify; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf > (MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF + 1]); + reg_addr++; + + coil_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_COILCNT_OFF] << 8); + coil_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_COILCNT_OFF + 1]); + + byte_cnt = frame_ptr[MB_PDU_FUNC_WRITE_MUL_BYTECNT_OFF]; + + /* Compute the number of expected bytes in the request. */ + if ((coil_cnt & 0x0007) != 0) { + byte_cnt_verify = (uint8_t)(coil_cnt / 8 + 1); + } else { + byte_cnt_verify = (uint8_t)(coil_cnt / 8); + } + + if ((coil_cnt >= 1) && + (coil_cnt <= MB_PDU_FUNC_WRITE_MUL_COILCNT_MAX) && + (byte_cnt_verify == byte_cnt)) { + if (inst->rw_cbs.reg_coils_cb) { + reg_status = inst->rw_cbs.reg_coils_cb(inst, &frame_ptr[MB_PDU_FUNC_WRITE_MUL_VALUES_OFF], reg_addr, coil_cnt, MB_REG_WRITE); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } else { + /* The response contains the function code, the starting address + * and the quantity of registers. We reuse the old values in the + * buffer because they are still valid. */ + *len_buf = MB_PDU_FUNC_WRITE_MUL_BYTECNT_OFF; + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid write coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} +#endif +#endif diff --git a/modbus/mb_objects/functions/mbfunccoils_master.c b/modbus/mb_objects/functions/mbfunccoils_master.c new file mode 100644 index 0000000..ffb8030 --- /dev/null +++ b/modbus/mb_objects/functions/mbfunccoils_master.c @@ -0,0 +1,335 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (C) 2013 Armink + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfunccoils_m.c, v 1.60 2013/10/12 15:10:12 Armink Add Master Functions + */ +#include "mb_common.h" +#include "mb_proto.h" +#include "transport_common.h" +#include "mb_master.h" + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_COILCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_COILCNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +#define MB_PDU_REQ_WRITE_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_REQ_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_SIZE (4) +#define MB_PDU_FUNC_WRITE_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_FUNC_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_SIZE (4) + +#define MB_PDU_REQ_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_REQ_WRITE_MUL_COILCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_REQ_WRITE_MUL_VALUES_OFF (MB_PDU_DATA_OFF + 5) +#define MB_PDU_REQ_WRITE_MUL_SIZE_MIN (5) +#define MB_PDU_REQ_WRITE_MUL_COILCNT_MAX (0x07B0) +#define MB_PDU_FUNC_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_FUNC_WRITE_MUL_COILCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_MUL_SIZE (5) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ +#if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED + +#if MB_FUNC_READ_COILS_ENABLED + +/** + * This function will request read coil. + * + * @param snd_addr salve address + * @param coil_addr coil start address + * @param coil_num coil total number + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + */ +mb_err_enum_t mbm_rq_read_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + if (snd_addr > MB_ADDRESS_MAX) { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_READ_COILS; + + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] = coil_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1] = coil_addr; + + mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF] = coil_num >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF + 1] = coil_num; + + inst->set_send_len(inst, MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START) ); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_read_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint8_t byte_cnt; + uint8_t *mb_frame_ptr; + uint16_t reg_addr; + uint16_t coil_cnt; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + /* If this request is broadcast, and it's read mode. This request don't need execute. */ + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + status = MB_EX_NONE; + } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) { + inst->get_send_buf(inst, &mb_frame_ptr); + + reg_addr = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] << 8 ); + reg_addr |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1] ); + reg_addr++; + + coil_cnt = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF] << 8 ); + coil_cnt |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF + 1] ); + + /* Test if the quantity of coils is a multiple of 8. If not last + * byte is only partially field with unused coils set to zero. */ + if ((coil_cnt & 0x0007) != 0) { + byte_cnt = (uint8_t)(coil_cnt / 8 + 1); + } else { + byte_cnt = (uint8_t)(coil_cnt / 8); + } + + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((coil_cnt >= 1) && (byte_cnt == frame_ptr[MB_PDU_FUNC_READ_COILCNT_OFF])) { + /* Make callback to fill the buffer. */ + if (inst->rw_cbs.reg_coils_cb) { + reg_status = inst->rw_cbs.reg_coils_cb(inst, &frame_ptr[MB_PDU_FUNC_READ_VALUES_OFF], reg_addr, coil_cnt, MB_REG_READ); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid read coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} +#endif + +#if MB_FUNC_WRITE_COIL_ENABLED + +/** + * This function will request write one coil. + * + * @param snd_addr salve address + * @param coil_addr coil start address + * @param coil_data data to be written + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + * + * @see mbm_rq_write_multi_coils + */ +mb_err_enum_t mbm_rq_write_coil(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_data, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + + if (!inst || (snd_addr > MB_ADDRESS_MAX)) { + return MB_EINVAL; + } + if ((coil_data != 0xFF00) && (coil_data != 0x0000)) { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_WRITE_SINGLE_COIL; + + mb_frame_ptr[MB_PDU_REQ_WRITE_ADDR_OFF] = coil_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_ADDR_OFF + 1] = coil_addr; + + mb_frame_ptr[MB_PDU_REQ_WRITE_VALUE_OFF] = coil_data >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_VALUE_OFF + 1] = coil_data; + + inst->set_send_len(inst, (MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_SIZE)); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START) ); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_write_coil(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + mb_exception_t status = MB_EX_NONE; + (void)inst; + (void)frame_ptr; + + if (*len_buf == (MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN)) { + status = MB_EX_NONE; + } else { + /* Can't be a valid write coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif + +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED > 0 + +/** + * This function will request write multiple coils. + * + * @param snd_addr salve address + * @param coil_addr coil start address + * @param coil_num coil total number + * @param coil_data data to be written + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + * + * @see mbm_rq_write_coil + */ +mb_err_enum_t mbm_rq_write_multi_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint8_t *data_ptr, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + uint8_t *mb_data_ptr; + uint16_t reg_idx; + uint8_t byte_cnt; + + if (snd_addr > MB_ADDRESS_MAX) { + return MB_EINVAL; + } + if (coil_num > MB_PDU_REQ_WRITE_MUL_COILCNT_MAX) { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_WRITE_MULTIPLE_COILS; + + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF] = coil_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1] = coil_addr; + + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_COILCNT_OFF] = coil_num >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_COILCNT_OFF + 1] = coil_num; + + if ((coil_num & 0x0007) != 0) { + byte_cnt = (uint8_t)(coil_num / 8 + 1); + } else { + byte_cnt = (uint8_t)(coil_num / 8); + } + + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF] = byte_cnt; + + mb_data_ptr = mb_frame_ptr + MB_PDU_REQ_WRITE_MUL_VALUES_OFF; + for (reg_idx = 0; reg_idx < byte_cnt; reg_idx++) { + mb_data_ptr[reg_idx] = data_ptr[reg_idx]; + } + + inst->set_send_len(inst, (MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_MUL_SIZE_MIN + byte_cnt)); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START) ); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_write_multi_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + mb_exception_t status = MB_EX_NONE; + uint8_t *mb_frame_ptr; + uint16_t reg_address; + uint16_t coil_cnt; + uint8_t byte_cnt; + uint8_t byte_cnt_verify; + mb_err_enum_t reg_status = MB_EILLFUNC; + + /* If this request is broadcast, the *len_buf is not need check. */ + if ((*len_buf == MB_PDU_FUNC_WRITE_MUL_SIZE) || inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + inst->get_send_buf(inst, &mb_frame_ptr); + reg_address = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF] << 8); + reg_address |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF + 1]); + reg_address++; + + coil_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_COILCNT_OFF] << 8); + coil_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_COILCNT_OFF + 1]); + + byte_cnt = mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF]; + + /* Compute the number of expected bytes in the request. */ + if ((coil_cnt & 0x0007) != 0) { + byte_cnt_verify = (uint8_t)(coil_cnt / 8 + 1); + } else { + byte_cnt_verify = (uint8_t)(coil_cnt / 8); + } + + if ((coil_cnt >= 1) && (byte_cnt_verify == byte_cnt)) { + if (inst->rw_cbs.reg_coils_cb) { + reg_status = inst->rw_cbs.reg_coils_cb(inst, &mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF], + reg_address, coil_cnt, MB_REG_WRITE); + } + /* If an error occured, convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid write coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif // #if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED > 0 +#endif // #if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED diff --git a/modbus/mb_objects/functions/mbfuncdiag.c b/modbus/mb_objects/functions/mbfuncdiag.c new file mode 100644 index 0000000..9ca1d77 --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncdiag.c @@ -0,0 +1,29 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfuncdiag.c, v 1.3 2006/12/07 22:10:34 wolti Exp $ + */ diff --git a/modbus/mb_objects/functions/mbfuncdisc.c b/modbus/mb_objects/functions/mbfuncdisc.c new file mode 100644 index 0000000..1f3ed98 --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncdisc.c @@ -0,0 +1,105 @@ + /* + * FreeRTOS Modbus Libary: A Modbus serial implementation for FreeRTOS + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (C) 2006 Christian Walter + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +#include +#include +#include "mb_slave.h" +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_FUNC_READ_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_FUNC_READ_DISCCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_READ_SIZE (4) +#define MB_PDU_FUNC_READ_DISCCNT_MAX (0x07D0) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ + +#if MB_FUNC_READ_COILS_ENABLED > 0 + +mb_exception_t +mbs_fn_read_discrete_inp(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + uint16_t discrete_cnt; + uint8_t byte_num; + uint8_t *frame_cur; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + (void)inst; + + if (*len_buf == (MB_PDU_FUNC_READ_SIZE + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF + 1]); + reg_addr++; + + discrete_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_DISCCNT_OFF] << 8); + discrete_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_DISCCNT_OFF + 1]); + + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((discrete_cnt >= 1) && + (discrete_cnt < MB_PDU_FUNC_READ_DISCCNT_MAX)) { + /* Set the current PDU data pointer to the beginning. */ + frame_cur = &frame_ptr[MB_PDU_FUNC_OFF]; + *len_buf = MB_PDU_FUNC_OFF; + + /* First byte contains the function code. */ + *frame_cur++ = MB_FUNC_READ_DISCRETE_INPUTS; + *len_buf += 1; + + /* Test if the quantity of coils is a multiple of 8. If not last + * byte is only partially field with unused coils set to zero. */ + if ((discrete_cnt & 0x0007) != 0) { + byte_num = (uint8_t) (discrete_cnt / 8 + 1); + } else { + byte_num = (uint8_t) (discrete_cnt / 8); + } + *frame_cur++ = byte_num; + *len_buf += 1; + + /* Call r/w function if defined*/ + if (inst->rw_cbs.reg_discrete_cb) { + reg_status = inst->rw_cbs.reg_discrete_cb(inst, frame_cur, reg_addr, discrete_cnt); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } else { + /* The response contains the function code, the starting address + * and the quantity of registers. We reuse the old values in the + * buffer because they are still valid. */ + *len_buf += byte_num;; + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid read coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif diff --git a/modbus/mb_objects/functions/mbfuncdisc_master.c b/modbus/mb_objects/functions/mbfuncdisc_master.c new file mode 100644 index 0000000..842df00 --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncdisc_master.c @@ -0,0 +1,141 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (C) 2013 Armink + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfuncdisc_m.c, v 1.60 2013/10/15 8:48:20 Armink Add Master Functions Exp $ + */ +#include "mb_common.h" +#include "mb_proto.h" +#include "transport_common.h" +#include "mb_master.h" +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_DISCCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_DISCCNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ +#if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED + +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED + +/** + * This function will request read discrete inputs. + * + * @param snd_addr salve address + * @param discrete_addr discrete start address + * @param discrete_num discrete total number + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + */ +mb_err_enum_t mbm_rq_read_discrete_inputs(mb_base_t *inst, uint8_t snd_addr, uint16_t discrete_addr, uint16_t discrete_num, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + if (!inst || (snd_addr > MB_ADDRESS_MAX)) { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_READ_DISCRETE_INPUTS; + + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] = discrete_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1] = discrete_addr; + + mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF ] = discrete_num >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF + 1] = discrete_num; + + inst->set_send_len(inst, MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START) ); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_read_discrete_inputs(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint8_t byte_num; + uint8_t *mb_frame_ptr; + uint16_t discrete_cnt; + uint16_t reg_address; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + status = MB_EX_NONE; + } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) { + inst->get_send_buf(inst, &mb_frame_ptr); + reg_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] << 8); + reg_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1]); + reg_address++; + + discrete_cnt = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF] << 8); + discrete_cnt |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF + 1]); + + /* Test if the quantity of coils is a multiple of 8. If not last + * byte is only partially field with unused coils set to zero. */ + if ((discrete_cnt & 0x0007) != 0) { + byte_num = (uint8_t)(discrete_cnt / 8 + 1); + } else { + byte_num = (uint8_t)(discrete_cnt / 8); + } + + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((discrete_cnt >= 1) && byte_num == frame_ptr[MB_PDU_FUNC_READ_DISCCNT_OFF]) { + /* Make callback to fill the buffer. */ + if (inst->rw_cbs.reg_discrete_cb) { + reg_status = inst->rw_cbs.reg_discrete_cb(inst, &frame_ptr[MB_PDU_FUNC_READ_VALUES_OFF], + reg_address, discrete_cnt); + } + + /* If an error occured convert it into a Modbus exception. */ + if(reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid read coil register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif +#endif // #if MB_SERIAL_MASTER_RTU_ENABLED || MB_SERIAL_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED diff --git a/modbus/mb_objects/functions/mbfuncholding.c b/modbus/mb_objects/functions/mbfuncholding.c new file mode 100644 index 0000000..d874cde --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncholding.c @@ -0,0 +1,271 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfuncholding.c, v 1.12 2007/02/18 23:48:22 wolti Exp $ + */ +#include +#include +#include "mb_slave.h" +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_FUNC_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_READ_SIZE (4) +#define MB_PDU_FUNC_READ_REGCNT_MAX (0x007D) + +#define MB_PDU_FUNC_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_SIZE (4) + +#define MB_PDU_FUNC_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_MUL_BYTECNT_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_FUNC_WRITE_MUL_VALUES_OFF (MB_PDU_DATA_OFF + 5) +#define MB_PDU_FUNC_WRITE_MUL_SIZE_MIN (5) +#define MB_PDU_FUNC_WRITE_MUL_REGCNT_MAX (0x0078) + +#define MB_PDU_FUNC_READWRITE_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READWRITE_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_READWRITE_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_FUNC_READWRITE_WRITE_REGCNT_OFF (MB_PDU_DATA_OFF + 6) +#define MB_PDU_FUNC_READWRITE_BYTECNT_OFF (MB_PDU_DATA_OFF + 8) +#define MB_PDU_FUNC_READWRITE_WRITE_VALUES_OFF (MB_PDU_DATA_OFF + 9) +#define MB_PDU_FUNC_READWRITE_SIZE_MIN (9) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ + +#if MB_FUNC_WRITE_HOLDING_ENABLED > 0 + +mb_exception_t mbs_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf == (MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF + 1]); + reg_addr++; + + /* Make callback to update the value. */ + if (inst->rw_cbs.reg_holding_cb) { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &frame_ptr[MB_PDU_FUNC_WRITE_VALUE_OFF], reg_addr, 1, MB_REG_WRITE); + } + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } + } else { + /* Can't be a valid request because the length is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} +#endif + +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED > 0 +mb_exception_t mbs_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + uint16_t reg_cnt; + uint8_t reg_byte_cnt; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf >= (MB_PDU_FUNC_WRITE_MUL_SIZE_MIN + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_ADDR_OFF + 1]); + reg_addr++; + + reg_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF] << 8); + reg_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF + 1]); + + reg_byte_cnt = frame_ptr[MB_PDU_FUNC_WRITE_MUL_BYTECNT_OFF]; + + if ((reg_cnt >= 1) && + (reg_cnt <= MB_PDU_FUNC_WRITE_MUL_REGCNT_MAX) && + (reg_byte_cnt == (uint8_t) (2 * reg_cnt))) { + /* Make callback to update the register values. */ + if (inst->rw_cbs.reg_holding_cb) { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &frame_ptr[MB_PDU_FUNC_WRITE_MUL_VALUES_OFF], reg_addr, reg_cnt, MB_REG_WRITE); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } else { + /* The response contains the function code, the starting + * address and the quantity of registers. We reuse the + * old values in the buffer because they are still valid. + */ + *len_buf = MB_PDU_FUNC_WRITE_MUL_BYTECNT_OFF; + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid request because the length is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} +#endif + +#if MB_FUNC_READ_HOLDING_ENABLED > 0 + +mb_exception_t mbs_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + uint16_t reg_cnt; + uint8_t *frame_cur; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf == (MB_PDU_FUNC_READ_SIZE + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF + 1]); + reg_addr++; + + reg_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_REGCNT_OFF] << 8); + reg_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_REGCNT_OFF + 1]); + + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((reg_cnt >= 1) && (reg_cnt <= MB_PDU_FUNC_READ_REGCNT_MAX)) { + /* Set the current PDU data pointer to the beginning. */ + frame_cur = &frame_ptr[MB_PDU_FUNC_OFF]; + *len_buf = MB_PDU_FUNC_OFF; + + /* First byte contains the function code. */ + *frame_cur++ = MB_FUNC_READ_HOLDING_REGISTER; + *len_buf += 1; + + /* Second byte in the response contain the number of bytes. */ + *frame_cur++ = (uint8_t) (reg_cnt * 2); + *len_buf += 1; + + /* Make callback to fill the buffer. */ + if (inst->rw_cbs.reg_holding_cb) { + reg_status = inst->rw_cbs.reg_holding_cb(inst, frame_cur, reg_addr, reg_cnt, MB_REG_READ); + } + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } else { + *len_buf += reg_cnt * 2; + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid request because the length is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif + +#if MB_FUNC_READWRITE_HOLDING_ENABLED > 0 + +mb_exception_t mbs_fn_rw_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_rd_addr; + uint16_t reg_rd_cnt; + uint16_t reg_wr_addr; + uint16_t reg_wr_cnt; + uint8_t reg_wr_byte_cnt; + uint8_t *frame_cur; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + (void)inst; + + if (*len_buf >= (MB_PDU_FUNC_READWRITE_SIZE_MIN + MB_PDU_SIZE_MIN)) { + reg_rd_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_READ_ADDR_OFF] << 8U); + reg_rd_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_READ_ADDR_OFF + 1]); + reg_rd_addr++; + + reg_rd_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_READ_REGCNT_OFF] << 8U); + reg_rd_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_READ_REGCNT_OFF + 1]); + + reg_wr_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_WRITE_ADDR_OFF] << 8U); + reg_wr_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_WRITE_ADDR_OFF + 1]); + reg_wr_addr++; // increase by one + + reg_wr_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_WRITE_REGCNT_OFF] << 8U); + reg_wr_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READWRITE_WRITE_REGCNT_OFF + 1]); + + reg_wr_byte_cnt = frame_ptr[MB_PDU_FUNC_READWRITE_BYTECNT_OFF]; + + if ((reg_rd_cnt >= 1) && (reg_rd_cnt <= MB_PDU_FUNC_READ_REGCNT_MAX) && + (reg_wr_cnt >= 1) && (reg_wr_cnt <= MB_PDU_FUNC_WRITE_MUL_REGCNT_MAX) && + ((2 * reg_wr_cnt) == reg_wr_byte_cnt)) { + /* Make callback to update the register values. */ + if (inst->rw_cbs.reg_holding_cb) { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &frame_ptr[MB_PDU_FUNC_READWRITE_WRITE_VALUES_OFF], reg_wr_addr, reg_wr_cnt, MB_REG_WRITE); + } + + if (reg_status == MB_ENOERR) { + /* Set the current PDU data pointer to the beginning. */ + frame_cur = &frame_ptr[MB_PDU_FUNC_OFF]; + *len_buf = MB_PDU_FUNC_OFF; + + /* First byte contains the function code. */ + *frame_cur++ = MB_FUNC_READWRITE_MULTIPLE_REGISTERS; + *len_buf += 1; + + /* Second byte in the response contain the number of bytes. */ + *frame_cur++ = (uint8_t) (reg_rd_cnt * 2); + *len_buf += 1; + + /* Make the read callback. */ + if (inst->rw_cbs.reg_holding_cb) { + reg_status = inst->rw_cbs.reg_holding_cb(inst, frame_cur, reg_rd_addr, reg_rd_cnt, MB_REG_READ); + } + if (reg_status == MB_ENOERR) { + *len_buf += 2 * reg_rd_cnt; + } + } + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } + return status; +} + +#endif diff --git a/modbus/mb_objects/functions/mbfuncholding_master.c b/modbus/mb_objects/functions/mbfuncholding_master.c new file mode 100644 index 0000000..e091568 --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncholding_master.c @@ -0,0 +1,477 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (C) 2013 Armink + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfuncholding_m.c, v 1.60 2013/09/02 14:13:40 Armink Add Master Functions Exp $ + */ +#include "mb_common.h" +#include "mb_proto.h" +#include "transport_common.h" +#include "mb_master.h" +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_REGCNT_MAX (0x007D) +#define MB_PDU_FUNC_READ_BYTECNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +#define MB_PDU_REQ_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_SIZE (4) +#define MB_PDU_FUNC_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_SIZE (4) + +#define MB_PDU_REQ_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_WRITE_MUL_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_REQ_WRITE_MUL_VALUES_OFF (MB_PDU_DATA_OFF + 5) +#define MB_PDU_REQ_WRITE_MUL_SIZE_MIN (5) +#define MB_PDU_REQ_WRITE_MUL_REGCNT_MAX (0x0078) +#define MB_PDU_FUNC_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_MUL_SIZE (4) + +#define MB_PDU_REQ_READWRITE_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READWRITE_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READWRITE_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_REQ_READWRITE_WRITE_REGCNT_OFF (MB_PDU_DATA_OFF + 6) +#define MB_PDU_REQ_READWRITE_WRITE_BYTECNT_OFF (MB_PDU_DATA_OFF + 8) +#define MB_PDU_REQ_READWRITE_WRITE_VALUES_OFF (MB_PDU_DATA_OFF + 9) +#define MB_PDU_REQ_READWRITE_SIZE_MIN (9) +#define MB_PDU_FUNC_READWRITE_READ_BYTECNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READWRITE_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READWRITE_SIZE_MIN (1) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ +#if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED + +#if MB_FUNC_WRITE_HOLDING_ENABLED + +/** + * This function will request write holding register. + * + * @param snd_addr salve address + * @param reg_addr register start address + * @param reg_data register data to be written + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + */ +mb_err_enum_t mbm_rq_write_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_data, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + if (snd_addr > MB_ADDRESS_MAX) + { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) + { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_WRITE_REGISTER; + + mb_frame_ptr[MB_PDU_REQ_WRITE_ADDR_OFF] = reg_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_ADDR_OFF + 1] = reg_addr; + + mb_frame_ptr[MB_PDU_REQ_WRITE_VALUE_OFF] = reg_data >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_VALUE_OFF + 1] = reg_data; + + inst->set_send_len(inst, MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START)); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf == (MB_PDU_SIZE_MIN + MB_PDU_FUNC_WRITE_SIZE)) + { + uint16_t reg_address; + reg_address = (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF] << 8); + reg_address |= (uint16_t)(frame_ptr[MB_PDU_FUNC_WRITE_ADDR_OFF + 1]); + reg_address++; + + /* Make callback to update the value. */ + if (inst->rw_cbs.reg_holding_cb) + { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &frame_ptr[MB_PDU_FUNC_WRITE_VALUE_OFF], reg_address, 1, MB_REG_WRITE); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) + { + status = mb_error_to_exception(reg_status); + } + } + else + { + /* Can't be a valid request because the length is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} +#endif + +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED + +/** + * This function will request write multiple holding register. + * + * @param snd_addr salve address + * @param reg_addr register start address + * @param reg_num register total number + * @param data_ptr data to be written + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + */ +mb_err_enum_t mbm_rq_write_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint16_t *data_ptr, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + uint16_t reg_idx = 0; + + if (!data_ptr || !inst || (snd_addr > MB_ADDRESS_MAX)) + { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) + { + return MB_EBUSY; + } + + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_WRITE_MULTIPLE_REGISTERS; + + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF] = reg_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1] = reg_addr; + + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF] = reg_num >> 8; + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF + 1] = reg_num; + + mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF] = reg_num * 2; + + mb_frame_ptr += MB_PDU_REQ_WRITE_MUL_VALUES_OFF; + + while (reg_num > reg_idx) + { + *mb_frame_ptr++ = data_ptr[reg_idx] >> 8; + *mb_frame_ptr++ = data_ptr[reg_idx++]; + } + + inst->set_send_len(inst, (MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_MUL_SIZE_MIN + 2 * reg_num)); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START)); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + mb_exception_t status = MB_EX_NONE; + uint8_t *mb_frame_ptr; + uint16_t reg_address; + uint16_t reg_count; + uint16_t byte_count; + mb_err_enum_t reg_status = MB_EILLFUNC; + + /* If this request is broadcast, the *len_buf is not need check. */ + if ((*len_buf == MB_PDU_SIZE_MIN + MB_PDU_FUNC_WRITE_MUL_SIZE) || inst->transp_obj->frm_is_bcast(inst->transp_obj)) + { + inst->get_send_buf(inst, &mb_frame_ptr); + reg_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF] << 8); + reg_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1]); + reg_address++; + + reg_count = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF] << 8); + reg_count |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF + 1]); + + byte_count = mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF]; + + if (byte_count == 2 * reg_count) + { + /* Make callback to update the register values. */ + if (inst->rw_cbs.reg_holding_cb) + { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF], + reg_address, reg_count, MB_REG_WRITE); + } + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) + { + status = mb_error_to_exception(reg_status); + } + } + else + { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } + else + { + /* Can't be a valid request because the length is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} +#endif + +#if MB_FUNC_READ_HOLDING_ENABLED + +/** + * This function will request read holding register. + * + * @param snd_addr salve address + * @param reg_addr register start address + * @param reg_num register total number + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + */ +mb_err_enum_t mbm_rq_read_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + + if (!inst || (snd_addr > MB_ADDRESS_MAX)) + { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) + { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_READ_HOLDING_REGISTER; + + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] = reg_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1] = reg_addr; + + mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF] = reg_num >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1] = reg_num; + + inst->set_send_len(inst, (MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE)); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START)); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + mb_exception_t status = MB_EX_NONE; + uint8_t *mb_frame_ptr; + uint16_t reg_address; + uint16_t reg_count; + mb_err_enum_t reg_status = MB_EILLFUNC; + + /* If this request is broadcast, and it's read mode. This request don't need execute. */ + bool is_broadcast = false; + + is_broadcast = inst->transp_obj->frm_is_bcast(inst->transp_obj); + + if (is_broadcast == true) + { + status = MB_EX_NONE; + } + else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) + { + inst->get_send_buf(inst, &mb_frame_ptr); + reg_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] << 8); + reg_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1]); + reg_address++; + + reg_count = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF] << 8); + reg_count |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1]); + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((reg_count >= 1) && (2 * reg_count == frame_ptr[MB_PDU_FUNC_READ_BYTECNT_OFF])) + { + /* Make callback to update the register values. */ + if (inst->rw_cbs.reg_holding_cb) + { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &frame_ptr[MB_PDU_FUNC_READ_VALUES_OFF], + reg_address, reg_count, MB_REG_READ); + } + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) + { + status = mb_error_to_exception(reg_status); + } + } + else + { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } + else + { + /* Can't be a valid request because the length is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif + +#if MB_FUNC_READWRITE_HOLDING_ENABLED + +/** + * This function will request read and write holding register. + * + * @param snd_addr salve address + * @param rd_reg_addr read register start address + * @param rd_reg_num read register total number + * @param data_ptr data to be written + * @param wr_reg_addr write register start address + * @param wr_reg_num write register total number + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + */ +mb_err_enum_t mbm_rq_rw_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t rd_reg_addr, + uint16_t rd_reg_num, uint16_t *data_ptr, uint16_t wr_reg_addr, uint16_t wr_reg_num, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + uint16_t reg_idx = 0; + + if (!data_ptr || !inst || (snd_addr > MB_ADDRESS_MAX)) + { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) + { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_READWRITE_MULTIPLE_REGISTERS; + + mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_ADDR_OFF] = rd_reg_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_ADDR_OFF + 1] = rd_reg_addr; + + mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_REGCNT_OFF] = rd_reg_num >> 8; + mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_REGCNT_OFF + 1] = rd_reg_num; + + mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_ADDR_OFF] = wr_reg_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_ADDR_OFF + 1] = wr_reg_addr; + + mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_REGCNT_OFF] = wr_reg_num >> 8; + mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_REGCNT_OFF + 1] = wr_reg_num; + mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_BYTECNT_OFF] = wr_reg_num * 2; + + mb_frame_ptr += MB_PDU_REQ_READWRITE_WRITE_VALUES_OFF; + + while (wr_reg_num > reg_idx) + { + *mb_frame_ptr++ = data_ptr[reg_idx] >> 8; + *mb_frame_ptr++ = data_ptr[reg_idx++]; + } + + inst->set_send_len(inst, (MB_PDU_SIZE_MIN + MB_PDU_REQ_READWRITE_SIZE_MIN + 2 * wr_reg_num)); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START)); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_rw_multi_holding_regs(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + uint16_t reg_rd_cnt, reg_wr_cnt; + uint16_t reg_rd_address, reg_wr_address; + uint8_t *mb_frame_ptr; + + /* If this request is broadcast, and it's read mode. This request don't need execute. */ + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) + { + status = MB_EX_NONE; + } + else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READWRITE_SIZE_MIN) + { + inst->get_send_buf(inst, &mb_frame_ptr); + reg_rd_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_ADDR_OFF] << 8); + reg_rd_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_ADDR_OFF + 1]); + reg_rd_address++; + + reg_rd_cnt = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_REGCNT_OFF] << 8); + reg_rd_cnt |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_READ_REGCNT_OFF + 1]); + + reg_wr_address = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_ADDR_OFF] << 8U); + reg_wr_address |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_ADDR_OFF + 1]); + reg_wr_address++; + + reg_wr_cnt = (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_REGCNT_OFF] << 8U); + reg_wr_cnt |= (uint16_t)(mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_REGCNT_OFF + 1]); + + if ((2 * reg_rd_cnt) == frame_ptr[MB_PDU_FUNC_READWRITE_READ_BYTECNT_OFF]) + { + /* Make callback to update the register values. */ + if (inst->rw_cbs.reg_holding_cb) + { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &mb_frame_ptr[MB_PDU_REQ_READWRITE_WRITE_VALUES_OFF], + reg_wr_address, reg_wr_cnt, MB_REG_WRITE); + } + + if (reg_status == MB_ENOERR) + { + /* Make the read callback. */ + if (inst->rw_cbs.reg_holding_cb) + { + reg_status = inst->rw_cbs.reg_holding_cb(inst, &frame_ptr[MB_PDU_FUNC_READWRITE_READ_VALUES_OFF], + reg_rd_address, reg_rd_cnt, MB_REG_READ); + } + } + if (reg_status != MB_ENOERR) + { + status = mb_error_to_exception(reg_status); + } + } + else + { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } + return status; +} + +#endif +#endif // #if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED diff --git a/modbus/mb_objects/functions/mbfuncinput.c b/modbus/mb_objects/functions/mbfuncinput.c new file mode 100644 index 0000000..5a6b643 --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncinput.c @@ -0,0 +1,100 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfuncinput.c, v 1.10 2007/09/12 10:15:56 wolti Exp $ + */ +#include +#include +#include "mb_slave.h" +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_FUNC_READ_ADDR_OFF (MB_PDU_DATA_OFF) +#define MB_PDU_FUNC_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_READ_SIZE (4) +#define MB_PDU_FUNC_READ_REGCNT_MAX (0x007D) + +#define MB_PDU_FUNC_READ_RSP_BYTECNT_OFF (MB_PDU_DATA_OFF) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ +#if MB_FUNC_READ_INPUT_ENABLED > 0 +mb_exception_t mbs_fn_read_input_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + uint16_t reg_addr; + uint16_t reg_cnt; + uint8_t *frame_cur; + + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + + if (*len_buf == (MB_PDU_FUNC_READ_SIZE + MB_PDU_SIZE_MIN)) { + reg_addr = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF] << 8); + reg_addr |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_ADDR_OFF + 1]); + reg_addr++; + + reg_cnt = (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_REGCNT_OFF] << 8); + reg_cnt |= (uint16_t)(frame_ptr[MB_PDU_FUNC_READ_REGCNT_OFF + 1]); + + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((reg_cnt >= 1) + && (reg_cnt < MB_PDU_FUNC_READ_REGCNT_MAX)) { + /* Set the current PDU data pointer to the beginning. */ + frame_cur = &frame_ptr[MB_PDU_FUNC_OFF]; + *len_buf = MB_PDU_FUNC_OFF; + + /* First byte contains the function code. */ + *frame_cur++ = MB_FUNC_READ_INPUT_REGISTER; + *len_buf += 1; + + /* Second byte in the response contain the number of bytes. */ + *frame_cur++ = (uint8_t)(reg_cnt * 2); + *len_buf += 1; + if (inst->rw_cbs.reg_input_cb) { + reg_status = inst->rw_cbs.reg_input_cb(inst, frame_cur, reg_addr, reg_cnt); + } + + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } else { + *len_buf += reg_cnt * 2; + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid read input register request because the length + * is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} +#endif diff --git a/modbus/mb_objects/functions/mbfuncinput_master.c b/modbus/mb_objects/functions/mbfuncinput_master.c new file mode 100644 index 0000000..266ba48 --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncinput_master.c @@ -0,0 +1,134 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (C) 2013 Armink + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfuncinput_m.c, v 1.60 2013/10/12 14:23:40 Armink Add Master Functions Exp $ + */ +#include "mb_common.h" +#include "mb_proto.h" +#include "transport_common.h" +#include "mb_master.h" +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_BYTECNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +#define MB_PDU_FUNC_READ_RSP_BYTECNT_OFF (MB_PDU_DATA_OFF) + +/* ----------------------- Static functions ---------------------------------*/ +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code); + +/* ----------------------- Start implementation -----------------------------*/ +#if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED + +#if MB_FUNC_WRITE_HOLDING_ENABLED + +/** + * This function will request read input register. + * + * @param snd_addr salve address + * @param reg_addr register start address + * @param reg_num register total number + * @param timeout timeout (-1 will waiting forever) + * + * @return error code + */ +mb_err_enum_t mbm_rq_read_inp_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout) +{ + uint8_t *mb_frame_ptr; + + if (!inst || (snd_addr > MB_ADDRESS_MAX)) { + return MB_EINVAL; + } + if (!mb_port_event_res_take(inst->port_obj, tout)) { + return MB_EBUSY; + } + inst->get_send_buf(inst, &mb_frame_ptr); + inst->set_dest_addr(inst, snd_addr); + + mb_frame_ptr[MB_PDU_FUNC_OFF] = MB_FUNC_READ_INPUT_REGISTER; + + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] = reg_addr >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1] = reg_addr; + + mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF] = reg_num >> 8; + mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1] = reg_num; + + inst->set_send_len(inst, MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE); + + (void)mb_port_event_post(inst->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START) ); + return mb_port_event_wait_req_finish(inst->port_obj); +} + +mb_exception_t mbm_fn_read_inp_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + mb_exception_t status = MB_EX_NONE; + mb_err_enum_t reg_status = MB_EILLFUNC; + uint16_t reg_cnt; + uint16_t reg_address; + uint8_t *mb_frame_ptr; + + /* If this request is broadcast, and it's read mode. This request don't need execute. */ + if (inst->transp_obj->frm_is_bcast(inst->transp_obj)) { + status = MB_EX_NONE; + } else if (*len_buf >= MB_PDU_SIZE_MIN + MB_PDU_FUNC_READ_SIZE_MIN) { + inst->get_send_buf(inst, &mb_frame_ptr); + + reg_address = (uint16_t)( mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF] << 8 ); + reg_address |= (uint16_t)( mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1] ); + reg_address++; + + reg_cnt = (uint16_t)( mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF] << 8 ); + reg_cnt |= (uint16_t)( mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1] ); + + /* Check if the number of registers to read is valid. If not + * return Modbus illegal data value exception. + */ + if ((reg_cnt >= 1) && (2 * reg_cnt == frame_ptr[MB_PDU_FUNC_READ_BYTECNT_OFF])) { + /* Make callback to fill the buffer. */ + if (inst->rw_cbs.reg_input_cb) { + reg_status = inst->rw_cbs.reg_input_cb(inst, &frame_ptr[MB_PDU_FUNC_READ_VALUES_OFF], reg_address, reg_cnt); + } + /* If an error occured convert it into a Modbus exception. */ + if (reg_status != MB_ENOERR) { + status = mb_error_to_exception(reg_status); + } + } else { + status = MB_EX_ILLEGAL_DATA_VALUE; + } + } else { + /* Can't be a valid request because the length is incorrect. */ + status = MB_EX_ILLEGAL_DATA_VALUE; + } + return status; +} + +#endif +#endif // #if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED diff --git a/modbus/mb_objects/functions/mbfuncother.c b/modbus/mb_objects/functions/mbfuncother.c new file mode 100644 index 0000000..8c02c60 --- /dev/null +++ b/modbus/mb_objects/functions/mbfuncother.c @@ -0,0 +1,68 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbfuncother.c, v 1.8 2006/12/07 22:10:34 wolti Exp $ + */ + +#include +#include +#include "mb_slave.h" + +#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED + +/* ----------------------- Start implementation -----------------------------*/ +mb_err_enum_t mb_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len) +{ + mb_err_enum_t status = MB_ENOERR; + + /* the first byte and second byte in the buffer is reserved for + * the parameter slv_id and the running flag. The rest of + * the buffer is available for additional data. */ + if (slv_idstr_len + 2 < MB_FUNC_OTHER_REP_SLAVEID_BUF) { + inst->slave_id_len = 0; + inst->slave_id[inst->slave_id_len++] = slv_id; + inst->slave_id[inst->slave_id_len++] = (uint8_t)(is_running ? 0xFF : 0x00); + if (slv_idstr_len > 0) { + memcpy(&inst->slave_id[inst->slave_id_len], slv_idstr, + (size_t)slv_idstr_len); + inst->slave_id_len += slv_idstr_len; + } + } else { + status = MB_ENORES; + } + return status; +} + +mb_exception_t mb_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf) +{ + memcpy(&frame_ptr[MB_PDU_DATA_OFF], &inst->slave_id[0], (size_t)inst->slave_id_len); + *len_buf = (uint16_t)(MB_PDU_DATA_OFF + inst->slave_id_len); + return MB_EX_NONE; +} + +#endif diff --git a/modbus/mb_objects/functions/mbutils.c b/modbus/mb_objects/functions/mbutils.c new file mode 100644 index 0000000..3fa7683 --- /dev/null +++ b/modbus/mb_objects/functions/mbutils.c @@ -0,0 +1,128 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2016, 2017 Nucleron R&D LLC + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbutils.c, v 1.6 2007/02/18 23:49:07 wolti Exp $ + */ +#include +#include +/* ----------------------- Defines ------------------------------------------*/ +#define BITS_uint8_t 8U + +/* ----------------------- Start implementation -----------------------------*/ +void mb_util_set_bits(uint8_t *byte_buf, uint16_t bit_offset, uint8_t but_num, uint8_t value) +{ + uint16_t word_buf; + uint16_t msk; + uint16_t byte_offset; + uint16_t pre_bits_num; + uint16_t us_val = value; + + assert(but_num <= 8); + assert((size_t)BITS_uint8_t == sizeof(uint8_t) * 8); + + /* Calculate byte offset for first byte containing the bit values starting + * at bit_offset. */ + byte_offset = (uint16_t)((bit_offset) / BITS_uint8_t); + + /* How many bits precede our bits to set. */ + pre_bits_num = (uint16_t)(bit_offset - byte_offset * BITS_uint8_t); + + /* Move bit field into position over bits to set */ + us_val <<= pre_bits_num; + + /* Prepare a mask for setting the new bits. */ + msk = (uint16_t)((1 << (uint16_t) but_num) - 1); + msk <<= bit_offset - byte_offset * BITS_uint8_t; + + /* copy bits into temporary storage. */ + word_buf = byte_buf[byte_offset]; + word_buf |= byte_buf[byte_offset + 1] << BITS_uint8_t; + + /* Zero out bit field bits and then or value bits into them. */ + word_buf = (uint16_t)((word_buf & (~msk)) | us_val); + + /* move bits back into storage */ + byte_buf[byte_offset] = (uint8_t)(word_buf & 0xFF); + byte_buf[byte_offset + 1] = (uint8_t)(word_buf >> BITS_uint8_t); +} + +uint8_t mb_util_get_bits(uint8_t *byte_buf, uint16_t bit_offset, uint8_t but_num) +{ + uint16_t word_buf; + uint16_t msk; + uint16_t byte_offset; + uint16_t pre_bits_num; + + /* Calculate byte offset for first byte containing the bit values starting + * at bit_offset. */ + byte_offset = (uint16_t)((bit_offset) / BITS_uint8_t); + + /* How many bits precede our bits to set. */ + pre_bits_num = (uint16_t)(bit_offset - byte_offset * BITS_uint8_t); + + /* Prepare a mask for setting the new bits. */ + msk = (uint16_t)((1 << (uint16_t) but_num) - 1); + + /* copy bits into temporary storage. */ + word_buf = byte_buf[byte_offset]; + word_buf |= byte_buf[byte_offset + 1] << BITS_uint8_t; + + /* throw away unneeded bits. */ + word_buf >>= pre_bits_num; + + /* mask away bits above the requested bitfield. */ + word_buf &= msk; + + return (uint8_t) word_buf; +} + +mb_exception_t mb_error_to_exception(mb_err_enum_t error_code) +{ + mb_exception_t status; + + switch (error_code) { + case MB_ENOERR: + status = MB_EX_NONE; + break; + + case MB_ENOREG: + status = MB_EX_ILLEGAL_DATA_ADDRESS; + break; + + case MB_ETIMEDOUT: + status = MB_EX_SLAVE_BUSY; + break; + + default: + status = MB_EX_SLAVE_DEVICE_FAILURE; + break; + } + + return status; +} + diff --git a/modbus/mb_objects/include/mb_callbacks.h b/modbus/mb_objects/include/mb_callbacks.h new file mode 100644 index 0000000..f76debf --- /dev/null +++ b/modbus/mb_objects/include/mb_callbacks.h @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "mb_common.h" +#include "mb_proto.h" +#include "mb_func.h" + +typedef mb_err_enum_t (*reg_input_cb_fp)(mb_base_t *inst, uint8_t *reg_buff, uint16_t reg_addr, uint16_t reg_num); +typedef mb_err_enum_t (*reg_holding_cb_fp)(mb_base_t *inst, uint8_t *reg_buff, uint16_t reg_addr, uint16_t reg_num, mb_reg_mode_enum_t mode); +typedef mb_err_enum_t (*reg_coils_cb_fp)(mb_base_t *inst, uint8_t *reg_buff, uint16_t reg_addr, uint16_t coil_num, mb_reg_mode_enum_t mode); +typedef mb_err_enum_t (*reg_discrete_cb_fp)(mb_base_t *inst, uint8_t *reg_buff, uint16_t reg_addr, uint16_t disc_num); + +typedef struct _mb_rw_callbacks { + reg_input_cb_fp reg_input_cb; + reg_holding_cb_fp reg_holding_cb; + reg_coils_cb_fp reg_coils_cb; + reg_discrete_cb_fp reg_discrete_cb; +} mb_rw_callbacks_t; + +#ifdef __cplusplus +} +#endif + diff --git a/modbus/mb_objects/include/mb_common.h b/modbus/mb_objects/include/mb_common.h new file mode 100644 index 0000000..ef1d859 --- /dev/null +++ b/modbus/mb_objects/include/mb_common.h @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include "mb_config.h" +#include "mb_frame.h" +#include "mb_types.h" +#include "port_common.h" +#include "mb_callbacks.h" +#include "mb_port_types.h" + +#include "esp_log.h" + +#include "sdkconfig.h" + +/* Common definitions */ + +#ifdef __cplusplus +extern "C" { +#endif + +#if __has_include("esp_check.h") +#include "esp_check.h" + +#define MB_RETURN_ON_FALSE(a, err_code, tag, format, ...) ESP_RETURN_ON_FALSE(a, err_code, tag, format __VA_OPT__(,) __VA_ARGS__) +#define MB_GOTO_ON_ERROR(x, goto_tag, log_tag, format, ...) ESP_GOTO_ON_ERROR(x, goto_tag, log_tag, format __VA_OPT__(,) __VA_ARGS__) +#define MB_GOTO_ON_FALSE(a, err_code, goto_tag, log_tag, format, ...) ESP_GOTO_ON_FALSE(a, err_code, goto_tag, log_tag, format __VA_OPT__(,) __VA_ARGS__) + +#else + +// if cannot include esp_check then use custom check macro + +#define MB_RETURN_ON_FALSE(a, err_code, tag, format, ...) do { \ + if (!(a)) { \ + ESP_LOGE(tag, "%s(%d): " format, __FUNCTION__, __LINE__ __VA_OPT__(,) __VA_ARGS__); \ + return err_code; \ + } \ +} while(0) + +#define MB_GOTO_ON_ERROR(x, goto_tag, log_tag, format, ...) do { \ + esp_err_t err_rc_ = (x); \ + if (err_rc_ != ESP_OK) { \ + ESP_LOGE(log_tag, "%s(%d): " format, __FUNCTION__, __LINE__ __VA_OPT__(,) __VA_ARGS__); \ + ret = err_rc_; \ + goto goto_tag; \ + } \ + } while(0) + +#define MB_GOTO_ON_FALSE(a, err_code, goto_tag, log_tag, format, ...) do { \ + (void)log_tag; \ + if (!(a)) { \ + ESP_LOGE(log_tag, "%s(%d): " format, __FUNCTION__, __LINE__ __VA_OPT__(,) __VA_ARGS__); \ + ret = (err_code); \ + goto goto_tag; \ + } \ + } while (0) + +#endif + +#define MB_CAT_BUF_SIZE (100) + +#define MB_STR_CAT(pref, message) (__extension__( \ +{ \ + char buf##__FUNCTION__##__LINE__[MB_CAT_BUF_SIZE]; \ + strncpy(&(buf##__FUNCTION__##__LINE__)[0], pref, (MB_CAT_BUF_SIZE - 1)); \ + strncat((buf##__FUNCTION__##__LINE__), message, (MB_CAT_BUF_SIZE - 1)); \ + (&((buf##__FUNCTION__##__LINE__)[0])); \ +} \ +)) + +#define MB_OBJ_FMT "%p" + +#define MB_GET_OBJ_CTX(pinst, type, base) (__extension__( \ +{ \ + assert(pinst); \ + ((type *)__containerof(pinst, type, base)); \ +} \ +)) + +#define MB_OBJ(pinst) (__extension__( \ +{ \ + assert(pinst); \ + ((typeof(pinst))(pinst)); \ +} \ +)) + +#define MB_OBJ_PARENT(pinst) (__extension__( \ +{ \ + assert(pinst); \ + (((obj_descr_t*)(pinst))->parent); \ +} \ +)) + +#define MB_BASE2PORT(pinst) (__extension__( \ +{ \ + assert(pinst); \ + assert(((mb_base_t *)pinst)->port_obj); \ + (((mb_base_t *)pinst)->port_obj); \ +} \ +)) + +typedef struct mb_base_t mb_base_t; +typedef struct mb_trans_base_t mb_trans_base_t; +typedef struct mb_port_base_t mb_port_base_t; +typedef struct _obj_descr obj_descr_t; + +typedef mb_err_enum_t (*mb_delete_fp)(mb_base_t *inst); +typedef mb_err_enum_t (*mb_enable_fp)(mb_base_t *inst); +typedef mb_err_enum_t (*mb_disable_fp)(mb_base_t *inst); +typedef mb_err_enum_t (*mb_poll_fp)(mb_base_t *inst); +typedef void (*mb_set_addr_fp)(mb_base_t *inst, uint8_t dest_addr); +typedef uint8_t (*mb_get_addr_fp)(mb_base_t *inst); +typedef void (*mb_set_send_len_fp)(mb_base_t *inst, uint16_t len); +typedef uint16_t (*mb_get_send_len_fp)(mb_base_t *inst); +typedef void (*mb_get_send_buf_fp)(mb_base_t *inst, uint8_t **pbuf); + +typedef enum +{ + STATE_ENABLED, + STATE_DISABLED, + STATE_NOT_INITIALIZED +} mb_state_enum_t; + +struct mb_base_t +{ + obj_descr_t descr; + _lock_t lock; // base object lock + mb_trans_base_t *transp_obj; + mb_port_base_t *port_obj; + + uint8_t slave_id[MB_FUNC_OTHER_REP_SLAVEID_BUF]; + uint16_t slave_id_len; + + mb_delete_fp delete; + mb_enable_fp enable; + mb_disable_fp disable; + mb_poll_fp poll; + mb_set_addr_fp set_dest_addr; + mb_get_addr_fp get_dest_addr; + mb_set_send_len_fp set_send_len; + mb_get_send_len_fp get_send_len; + mb_get_send_buf_fp get_send_buf; + + mb_rw_callbacks_t rw_cbs; +}; + +typedef struct _port_tcp_opts mb_tcp_opts_t; + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mbs_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbs_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +#endif + +mb_err_enum_t mbs_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +mb_err_enum_t mbs_delete(mb_base_t *inst); +mb_err_enum_t mbs_enable(mb_base_t *inst); +mb_err_enum_t mbs_disable(mb_base_t *inst); +mb_err_enum_t mbs_poll(mb_base_t *inst); +mb_err_enum_t mbs_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len); + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +mb_err_enum_t mbm_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbm_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +#endif + +mb_err_enum_t mbm_delete(mb_base_t *inst); +mb_err_enum_t mbm_enable(mb_base_t *inst); +mb_err_enum_t mbm_disable(mb_base_t *inst); +mb_err_enum_t mbm_poll(mb_base_t *inst); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_objects/include/mb_config.h b/modbus/mb_objects/include/mb_config.h new file mode 100644 index 0000000..0e67c79 --- /dev/null +++ b/modbus/mb_objects/include/mb_config.h @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "sdkconfig.h" // for KConfig options + +#ifdef __cplusplus +extern "C" { +#endif + +#if __has_include("esp_idf_version.h") +#include "esp_idf_version.h" +#endif + +#include + +/* ----------------------- Defines ------------------------------------------*/ +/*! \defgroup modbus_cfg Modbus Configuration + * + * Most modules in the protocol stack are completly optional and can be + * excluded. This is specially important if target resources are very small + * and program memory space should be saved.
+ * + * All of these settings are available in the file mbconfig.h + */ +/*! \addtogroup modbus_cfg + * @{ + */ +/*! \brief If Modbus Master ASCII support is enabled. */ +#define MB_MASTER_ASCII_ENABLED (CONFIG_FMB_COMM_MODE_ASCII_EN) +/*! \brief If Modbus Master RTU support is enabled. */ +#define MB_MASTER_RTU_ENABLED (CONFIG_FMB_COMM_MODE_RTU_EN) +/*! \brief If Modbus Master TCP support is enabled. */ +#define MB_MASTER_TCP_ENABLED (CONFIG_FMB_COMM_MODE_TCP_EN) +/*! \brief If Modbus Slave ASCII support is enabled. */ +#define MB_SLAVE_ASCII_ENABLED (CONFIG_FMB_COMM_MODE_ASCII_EN) +/*! \brief If Modbus Slave RTU support is enabled. */ +#define MB_SLAVE_RTU_ENABLED (CONFIG_FMB_COMM_MODE_RTU_EN) +/*! \brief If Modbus Slave TCP support is enabled. */ +#define MB_TCP_ENABLED (CONFIG_FMB_COMM_MODE_TCP_EN) + +#if (!CONFIG_FMB_COMM_MODE_ASCII_EN && !CONFIG_FMB_COMM_MODE_RTU_EN && !MB_MASTER_TCP_ENABLED && !MB_TCP_ENABLED) +#error "None of Modbus communication mode is enabled. Please enable one of (ASCII, RTU, TCP) mode in Kconfig." +#endif + +#ifdef ESP_IDF_VERSION + +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) +// Features supported from 4.4 +#define MB_TIMER_SUPPORTS_ISR_DISPATCH_METHOD 1 +#endif + +#endif + +#define MB_TIMER_USE_ISR_DISPATCH_METHOD (CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD) + +/*! \brief The option is required for correct UART initialization to place handler into IRAM. + */ +#if CONFIG_UART_ISR_IN_IRAM +#define MB_PORT_SERIAL_ISR_FLAG (ESP_INTR_FLAG_IRAM) +#else +#define MB_PORT_SERIAL_ISR_FLAG (ESP_INTR_FLAG_LOWMED) +#endif + +/*! \brief The option represents the serial buffer size for RTU and ASCI. + */ +#define MB_BUFFER_SIZE (CONFIG_FMB_BUFFER_SIZE) + +/*! \brief The option is required for support of RTU over TCP. + */ +#define MB_TCP_UID_ENABLED (CONFIG_FMB_TCP_UID_ENABLED) + +/*! \brief The option defines the queue size for event queue. + */ +#define MB_EVENT_QUEUE_SIZE (CONFIG_FMB_QUEUE_LENGTH) + +/*! \brief This option defines the number of data bits per ASCII character. + * + * A parity bit is added before the stop bit which keeps the actual byte size at 10 bits. + */ +#if CONFIG_FMB_SERIAL_ASCII_BITS_PER_SYMB +#define MB_ASCII_BITS_PER_SYMB (CONFIG_FMB_SERIAL_ASCII_BITS_PER_SYMB) +#endif + +/*! \brief The character timeout value for Modbus ASCII. + * + * The character timeout value is not fixed for Modbus ASCII and is therefore + * a configuration option. It should be set to the maximum expected delay + * time of the network. + */ +#if CONFIG_FMB_SERIAL_ASCII_TIMEOUT_RESPOND_MS +#define MB_ASCII_TIMEOUT_MS (CONFIG_FMB_SERIAL_ASCII_TIMEOUT_RESPOND_MS) +#else +#define MB_ASCII_TIMEOUT_MS 1000 +#endif + +/*! \brief Timeout to wait in ASCII prior to enabling transmitter. + * + * If defined the function calls vMBPortSerialDelay with the argument + * MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS to allow for a delay before + * the serial transmitter is enabled. This is required because some + * targets are so fast that there is no time between receiving and + * transmitting the frame. If the master is to slow with enabling its + * receiver then he will not receive the response correctly. + */ +#ifndef MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS +#define MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS (0) +#endif + +/*! \brief Maximum number of Modbus functions codes the protocol stack + * should support. + * + * The maximum number of supported Modbus functions must be greater than + * the sum of all enabled functions in this file and custom function + * handlers. If set to small adding more functions will fail. + */ +#define MB_FUNC_HANDLERS_MAX (16) + +/*! \brief Number of bytes which should be allocated for the Report Slave ID + * command. + * + * This number limits the maximum size of the additional segment in the + * report slave id function. See eMBSetSlaveID( ) for more information on + * how to set this value. It is only used if MB_FUNC_OTHER_REP_SLAVEID_ENABLED + * is set to 1. + */ +#define MB_FUNC_OTHER_REP_SLAVEID_BUF (32) + +/*! \brief If the Report Slave ID function should be enabled. */ +#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED (CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT) + +/*! \brief If the Read Input Registers function should be enabled. */ +#define MB_FUNC_READ_INPUT_ENABLED (1) + +/*! \brief If the Read Holding Registers function should be enabled. */ +#define MB_FUNC_READ_HOLDING_ENABLED (1) + +/*! \brief If the Write Single Register function should be enabled. */ +#define MB_FUNC_WRITE_HOLDING_ENABLED (1) + +/*! \brief If the Write Multiple registers function should be enabled. */ +#define MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED (1) + +/*! \brief If the Read Coils function should be enabled. */ +#define MB_FUNC_READ_COILS_ENABLED (1) + +/*! \brief If the Write Coils function should be enabled. */ +#define MB_FUNC_WRITE_COIL_ENABLED (1) + +/*! \brief If the Write Multiple Coils function should be enabled. */ +#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED (1) + +/*! \brief If the Read Discrete Inputs function should be enabled. */ +#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED (1) + +/*! \brief If the Read/Write Multiple Registers function should be enabled. */ +#define MB_FUNC_READWRITE_HOLDING_ENABLED (1) + +/*! @} */ + + +#if MB_MASTER_RTU_ENABLED || MB_MASTER_ASCII_ENABLED || MB_MASTER_TCP_ENABLED +/*! \brief If master send a broadcast frame, the master will wait time of convert to delay, + * then master can send other frame */ +#define MB_MASTER_DELAY_MS_CONVERT (CONFIG_FMB_MASTER_DELAY_MS_CONVERT) +/*! \brief If master send a frame which is not broadcast,the master will wait sometime for slave. + * And if slave is not respond in this time,the master will process this timeout error. + * Then master can send other frame */ +#define MB_MASTER_TIMEOUT_MS_RESPOND (CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND) +/*! \brief The total slaves in Modbus Master system. + * \note : The slave ID must be continuous from 1.*/ +#define MB_MASTER_TOTAL_SLAVE_NUM (247) +#define MB_MASTER_MIN_TIMEOUT_MS_RESPOND (50) + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_objects/include/mb_frame.h b/modbus/mb_objects/include/mb_frame.h new file mode 100644 index 0000000..9298bf2 --- /dev/null +++ b/modbus/mb_objects/include/mb_frame.h @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "mb_config.h" + +#ifdef __cplusplus +extern "C" { +#endif +/*! + * Constants which defines the format of a modbus frame. The example is + * shown for a Modbus RTU/ASCII frame. Note that the Modbus PDU is not + * dependent on the underlying transport. + * + * + * <------------------------ MODBUS SERIAL LINE PDU (1) -------------------> + * <----------- MODBUS PDU (1') ----------------> + * +-----------+---------------+----------------------------+-------------+ + * | Address | Function Code | Data | CRC/LRC | + * +-----------+---------------+----------------------------+-------------+ + * | | | | + * (2) (3/2') (3') (4) + * + * (1) ... MB_SER_PDU_SIZE_MAX = 256 + * (2) ... MB_SER_PDU_ADDR_OFF = 0 + * (3) ... MB_SER_PDU_PDU_OFF = 1 + * (4) ... MB_SER_PDU_SIZE_CRC = 2 + * + * (1') ... MB_PDU_SIZE_MAX = 253 + * (2') ... MB_PDU_FUNC_OFF = 0 + * (3') ... MB_PDU_DATA_OFF = 1 + * + */ + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_PDU_SIZE_MAX 253 /*!< Maximum size of a PDU. */ +#define MB_PDU_SIZE_MIN 1 /*!< Function Code */ +#define MB_PDU_FUNC_OFF 0 /*!< Offset of function code in PDU. */ +#define MB_PDU_DATA_OFF 1 /*!< Offset for response data in PDU. */ + +#define MB_SER_PDU_SIZE_MAX MB_BUFFER_SIZE /*!< Maximum size of a Modbus frame. */ +#define MB_SER_PDU_SIZE_LRC 1 /*!< Size of LRC field in PDU. */ +#define MB_SER_PDU_ADDR_OFF 0 /*!< Offset of slave address in Ser-PDU. */ +#define MB_SER_PDU_PDU_OFF 1 /*!< Offset of Modbus-PDU in Ser-PDU. */ +#define MB_SER_PDU_SIZE_CRC 2 /*!< Size of CRC field in PDU. */ + +#define MB_TCP_TID 0 +#define MB_TCP_PID 2 +#define MB_TCP_LEN 4 +#define MB_TCP_UID 6 +#define MB_TCP_FUNC 7 + +#if MB_MASTER_TCP_ENABLED +#define MB_SEND_BUF_PDU_OFF MB_TCP_FUNC +#else +#define MB_SEND_BUF_PDU_OFF MB_SER_PDU_PDU_OFF +#endif + +#define MB_TCP_BUFF_MAX_SIZE MB_TCP_FUNC + MB_PDU_SIZE_MAX + +#define MB_TCP_PSEUDO_ADDRESS (255) +#define MB_TCP_PROTOCOL_ID (0) /* 0 = Modbus Protocol */ + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_objects/include/mb_func.h b/modbus/mb_objects/include/mb_func.h new file mode 100644 index 0000000..5877769 --- /dev/null +++ b/modbus/mb_objects/include/mb_func.h @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "mb_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mb_base_t mb_base_t; + +#if MB_FUNC_OTHER_REP_SLAVEID_BUF +mb_exception_t mb_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +mb_exception_t mbm_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_INPUT_ENABLED +mb_exception_t mbs_fn_read_input_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +mb_exception_t mbm_fn_read_inp_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_HOLDING_ENABLED +mb_exception_t mbs_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +mb_exception_t mbm_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_HOLDING_ENABLED +mb_exception_t mbs_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +mb_exception_t mbm_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED +mb_exception_t mbs_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +mb_exception_t mbm_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_COILS_ENABLED +mb_exception_t mbs_fn_read_coils(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +mb_exception_t mbm_fn_read_coils(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_COIL_ENABLED +mb_exception_t mbs_fn_write_coil(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +mb_exception_t mbm_fn_write_coil(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED +mb_exception_t mbs_fn_write_multi_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +mb_exception_t mbm_fn_write_multi_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED +mb_exception_t mbs_fn_read_discrete_inp(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +mb_exception_t mbm_fn_read_discrete_inputs(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READWRITE_HOLDING_ENABLED +mb_exception_t mbs_fn_rw_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +mb_exception_t mbm_fn_rw_multi_holding_regs(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_objects/include/mb_master.h b/modbus/mb_objects/include/mb_master.h new file mode 100644 index 0000000..4b7ff6e --- /dev/null +++ b/modbus/mb_objects/include/mb_master.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once +#include "mb_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mb_base_t mb_base_t; /*!< Type of moddus object */ + +mb_err_enum_t mbm_rq_read_inp_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_write_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_data, uint32_t tout); +mb_err_enum_t mbm_rq_write_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_wr_addr, uint16_t *data_ptr, uint32_t tout); +mb_err_enum_t mbm_rq_read_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_rw_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t rd_reg_addr, + uint16_t rd_reg_num, uint16_t *data_ptr, uint16_t wr_reg_addr, uint16_t wr_reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_read_discrete_inputs(mb_base_t *inst, uint8_t snd_addr, uint16_t discrete_addr, uint16_t discrete_num, uint32_t tout); +mb_err_enum_t mbm_rq_read_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint32_t tout); +mb_err_enum_t mbm_rq_write_coil(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_data, uint32_t tout); +mb_err_enum_t mbm_rq_write_multi_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint8_t *data_ptr, uint32_t tout); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_objects/include/mb_port_types.h b/modbus/mb_objects/include/mb_port_types.h new file mode 100644 index 0000000..aad49e7 --- /dev/null +++ b/modbus/mb_objects/include/mb_port_types.h @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "stdatomic.h" +#include "mb_config.h" +#include "mb_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MB_ATTR_WEAK __attribute__ ((weak)) + +typedef enum _mb_comm_mode mb_mode_type_t; + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +#include "driver/uart.h" + +__attribute__((__packed__)) +struct _port_serial_opts { + mb_mode_type_t mode; /*!< Modbus communication mode */ + uart_port_t port; /*!< Modbus communication port (UART) number */ + uint8_t uid; /*!< Modbus slave address field (dummy for master) */ + uint32_t response_tout_ms; /*!< Modbus slave response timeout */ + uint64_t test_tout_us; /*!< Modbus test timeout (reserved) */ + uint32_t baudrate; /*!< Modbus baudrate */ + uart_word_length_t data_bits; /*!< Modbus number of data bits */ + uart_stop_bits_t stop_bits; /*!< Modbus number of stop bits */ + uart_parity_t parity; /*!< Modbus UART parity settings */ +}; + +typedef struct _port_serial_opts mb_serial_opts_t; + +#endif + +typedef enum _addr_type_enum { + MB_NOIP = 0, + MB_IPV4 = 1, /*!< TCP IPV4 addressing */ + MB_IPV6 = 2 /*!< TCP IPV6 addressing */ +} mb_addr_type_t; + +__attribute__((__packed__)) +struct _port_common_opts { + mb_mode_type_t mode; /*!< Modbus communication mode */ + uint16_t port; /*!< Modbus communication port (UART) number */ + uint8_t uid; /*!< Modbus slave address field (dummy for master) */ + uint32_t response_tout_ms; /*!< Modbus slave response timeout */ + uint64_t test_tout_us; /*!< Modbus test timeout (reserved) */ +}; + +__attribute__((__packed__)) +struct _port_tcp_opts { + mb_mode_type_t mode; /*!< Modbus communication mode */ + uint16_t port; /*!< Modbus communication port (UART) number */ + uint8_t uid; /*!< Modbus slave address field (dummy for master) */ + uint32_t response_tout_ms; /*!< Modbus slave response timeout */ + uint64_t test_tout_us; /*!< Modbus test timeout (reserved) */ + mb_addr_type_t addr_type; /*!< Modbus address type */ + void *ip_addr_table; /*!< Modbus address or table for connection */ + void *ip_netif_ptr; /*!< Modbus network interface */ + char *dns_name; /*!< Modbus node DNS name */ + bool start_disconnected; /*!< (Master only option) do not wait for connection to all nodes before polling */ +}; + +typedef struct _port_tcp_opts mb_tcp_opts_t; + +// The common object descriptor struture (common for mb, transport, port objects) +struct _obj_descr { + char *parent_name; /*!< Name of the parent (base) object */ + char *obj_name; /*!< Name of the object */ + void *parent; /*!< Pointer to the parent (base) object */ + uint32_t inst_index; /*!< The consicutive index of the object instance */ + bool is_master; /*!< The current object is master or slave (false) */ +}; + +typedef struct _obj_descr obj_descr_t; + +typedef enum _mb_sock_state { + MB_SOCK_STATE_UNDEF = 0x0000, /*!< Default init state */ + MB_SOCK_STATE_CLOSED, /*!< Node is closed */ + MB_SOCK_STATE_READY, /*!< Node is ready for communication */ + MB_SOCK_STATE_OPENED, /*!< Node is opened */ + MB_SOCK_STATE_RESOLVED, /*!< Node address is resolved */ + MB_SOCK_STATE_CONNECTING, /*!< Node connection is in progress */ + MB_SOCK_STATE_CONNECTED, /*!< Node is connected */ + MB_SOCK_STATE_ACCEPTED /*!< Slave node accepted the connection */ +} mb_sock_state_t; + +typedef struct _uid_info { + uint16_t index; /*!< index of the address info */ + int fd; /*!< node global FD for VFS (reserved) */ + char *node_name_str; /*!< node name string (host name of node to resolve) */ + char *ip_addr_str; /*!< represents the IP address of the node */ + mb_addr_type_t addr_type; /*!< type of IP address */ + uint16_t uid; /*!< node unit ID (UID) field for MBAP frame */ + uint16_t port; /*!< node port number */ + mb_comm_mode_t proto; /*!< protocol type */ + _Atomic mb_sock_state_t state; /*!< node state */ + void *inst; /*!< pointer to linked instance */ +} mb_uid_info_t; + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_objects/include/mb_proto.h b/modbus/mb_objects/include/mb_proto.h new file mode 100644 index 0000000..7f710f3 --- /dev/null +++ b/modbus/mb_objects/include/mb_proto.h @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_ADDRESS_BROADCAST ( 0 ) /*! Modbus broadcast address. */ +#define MB_ADDRESS_MIN ( 1 ) /*! Smallest possible slave address. */ +#define MB_ADDRESS_MAX ( 247 ) /*! Biggest possible slave address. */ + +typedef enum _mb_commands_enum +{ + MB_FUNC_NONE = ( 0 ), + MB_FUNC_READ_COILS = ( 1 ), + MB_FUNC_READ_DISCRETE_INPUTS = ( 2 ), + MB_FUNC_WRITE_SINGLE_COIL = ( 5 ), + MB_FUNC_WRITE_MULTIPLE_COILS = ( 15 ), + MB_FUNC_READ_HOLDING_REGISTER = ( 3 ), + MB_FUNC_READ_INPUT_REGISTER = ( 4 ), + MB_FUNC_WRITE_REGISTER = ( 6 ), + MB_FUNC_WRITE_MULTIPLE_REGISTERS = ( 16 ), + MB_FUNC_READWRITE_MULTIPLE_REGISTERS= ( 23 ), + MB_FUNC_DIAG_READ_EXCEPTION = ( 7 ), + MB_FUNC_DIAG_DIAGNOSTIC = ( 8 ), + MB_FUNC_DIAG_GET_COM_EVENT_CNT = ( 11 ), + MB_FUNC_DIAG_GET_COM_EVENT_LOG = ( 12 ), + MB_FUNC_OTHER_REPORT_SLAVEID = ( 17 ), + MB_FUNC_ERROR = ( 128U ), +} mb_commands_t; + +/* ----------------------- Type definitions ---------------------------------*/ + +typedef enum +{ + MB_EX_NONE = 0x00, + MB_EX_ILLEGAL_FUNCTION = 0x01, + MB_EX_ILLEGAL_DATA_ADDRESS = 0x02, + MB_EX_ILLEGAL_DATA_VALUE = 0x03, + MB_EX_SLAVE_DEVICE_FAILURE = 0x04, + MB_EX_ACKNOWLEDGE = 0x05, + MB_EX_SLAVE_BUSY = 0x06, + MB_EX_MEMORY_PARITY_ERROR = 0x08, + MB_EX_GATEWAY_PATH_FAILED = 0x0A, + MB_EX_GATEWAY_TGT_FAILED = 0x0B +} mb_exception_t; + +typedef mb_exception_t (*mb_fn_handler_fp)(void *, uint8_t *frame_ptr, uint16_t *len_buf); + +typedef struct +{ + uint8_t func_code; + mb_fn_handler_fp handler; +} mb_fn_handler_t; + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_objects/include/mb_slave.h b/modbus/mb_objects/include/mb_slave.h new file mode 100644 index 0000000..d16c800 --- /dev/null +++ b/modbus/mb_objects/include/mb_slave.h @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "mb_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +mb_err_enum_t mb_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len); + + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_objects/include/mb_types.h b/modbus/mb_objects/include/mb_types.h new file mode 100644 index 0000000..11ae9c4 --- /dev/null +++ b/modbus/mb_objects/include/mb_types.h @@ -0,0 +1,141 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#pragma once + +#include "stdbool.h" +#include "stdint.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ----------------------- Type definitions ---------------------------------*/ + +/*! \ingroup modbus + * \brief Modbus serial transmission modes (RTU/ASCII/TCP/UDP). + * + * Modbus serial supports two transmission modes. Either ASCII or RTU. RTU + * is faster but has more hardware requirements and requires a network with + * a low jitter. ASCII is slower and more reliable on slower links (E.g. modems) + * The TCP or UDP mode is used for communication over ethernet. + */ +typedef enum _mb_comm_mode +{ + MB_RTU, /*!< RTU transmission mode. */ + MB_ASCII, /*!< ASCII transmission mode. */ + MB_TCP, /*!< TCP mode. */ + MB_UDP /*!< UDP mode. */ +} mb_comm_mode_t; + +/*! \ingroup modbus + * \brief If register should be written or read. + * + * This value is passed to the callback functions which support either + * reading or writing register values. Writing means that the application + * registers should be updated and reading means that the modbus protocol + * stack needs to know the current register values. + * + * \see mbs_reg_holding_cb(), mbs_reg_coils_cb(), mbs_reg_holding_cb() and + * mbs_reg_input_cb(). + */ +typedef enum +{ + MB_REG_READ = 0x0001, /*!< Read register values and pass to protocol stack. */ + MB_REG_WRITE = 0x0002, /*!< Update register values. */ +} mb_reg_mode_enum_t; + +/*! \ingroup modbus + * \brief Event types used by all function in the protocol stack. + */ +typedef enum _mb_event_enum { + EV_TRANS_START = 0x0001, /*!< Start of transaction. */ + EV_READY = 0x0002, /*!< Startup finished. */ + EV_FRAME_RECEIVED = 0x0004, /*!< Frame received. */ + EV_EXECUTE = 0x0008, /*!< Execute function. */ + EV_FRAME_TRANSMIT = 0x0010, /*!< Transmission started . */ + EV_FRAME_SENT = 0x0020, /*!< Frame sent. */ + EV_ERROR_PROCESS = 0x0040, /*!< Error process state. */ + EV_MASTER_ERROR_RESPOND_TIMEOUT = 0x0080, /*!< Request respond timeout. */ + EV_MASTER_ERROR_RECEIVE_DATA = 0x0100, /*!< Request receive data error. */ + EV_MASTER_ERROR_EXECUTE_FUNCTION = 0x0200, /*!< Request execute function error. */ + EV_MASTER_PROCESS_SUCCESS = 0x0400 /*!< Master error process. */ +} mb_event_enum_t; + +/*! \ingroup modbus + * \brief Error event type + */ +typedef enum _mb_err_event_enum { + EV_ERROR_INIT, /*!< No error, initial state. */ + EV_ERROR_RESPOND_TIMEOUT, /*!< Slave respond timeout. */ + EV_ERROR_RECEIVE_DATA, /*!< Receive frame data error. */ + EV_ERROR_EXECUTE_FUNCTION, /*!< Execute function error. */ + EV_ERROR_OK /*!< No error, processing completed. */ +} mb_err_event_t; + +typedef struct _mb_event_t { + mb_event_enum_t event; /*!< event itself. */ + uint64_t trans_id; /*!< unique transaction id */ + uint16_t length; /*!< length of data accociated with the event */ + void *pdata; /*!< data accociated with the event */ + mb_err_event_t type; /*!< error type accociated with the event */ + uint64_t post_ts; /*!< timestamp of event posted */ + uint64_t get_ts; /*!< timestamp of event receved */ +} mb_event_t; + +/*! \ingroup modbus + * \brief Errorcodes used by all function in the protocol stack. + */ +typedef enum +{ + MB_ENOERR, /*!< no error. */ + MB_ENOREG, /*!< illegal register address. */ + MB_EINVAL, /*!< illegal argument. */ + MB_EPORTERR, /*!< porting layer error. */ + MB_ENORES, /*!< insufficient resources. */ + MB_EIO, /*!< I/O error. */ + MB_EILLSTATE, /*!< protocol stack in illegal state. */ + MB_ERECVDATA, /*!< receive data error. */ + MB_ETIMEDOUT, /*!< timeout error occurred. */ + MB_EILLFUNC, /*!< illegal MB function. */ + MB_EBUSY, /*!< master is busy now. */ + MB_ENOCONN /*!< peer is not connected. */ +} mb_err_enum_t; + +/*! \ingroup modbus + * \brief TimerMode is Master 3 kind of Timer modes. + */ +typedef enum +{ + MB_TMODE_T35, /*!< Master receive frame T3.5 timeout. */ + MB_TMODE_RESPOND_TIMEOUT, /*!< Master wait respond for slave. */ + MB_TMODE_CONVERT_DELAY /*!< Master sent broadcast , then delay sometime.*/ +} mb_timer_mode_enum_t; + +#ifdef __cplusplus +} +#endif + diff --git a/modbus/mb_objects/include/mb_utils.h b/modbus/mb_objects/include/mb_utils.h new file mode 100644 index 0000000..0ccbe0f --- /dev/null +++ b/modbus/mb_objects/include/mb_utils.h @@ -0,0 +1,123 @@ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbutils.h, v 1.5 2006/12/07 22:10:34 wolti Exp $ + */ + +#pragma once + +#include "stdint.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/*! \defgroup modbus_utils Utilities + * + * This module contains some utility functions which can be used by + * the application. It includes some special functions for working with + * bitfields backed by a character array buffer. + * + */ +/*! \addtogroup modbus_utils + * @{ + */ +/*! \brief Function to set bits in a byte buffer. + * + * This function allows the efficient use of an array to implement bitfields. + * The array used for storing the bits must always be a multiple of two + * bytes. Up to eight bits can be set or cleared in one operation. + * + * \param byte_buf A buffer where the bit values are stored. Must be a + * multiple of 2 bytes. No length checking is performed and if + * bit_offset / 8 is greater than the size of the buffer memory contents + * is overwritten. + * \param bit_offset The starting address of the bits to set. The first + * bit has the offset 0. + * \param but_num Number of bits to modify. The value must always be smaller + * than 8. + * \param values Thew new values for the bits. The value for the first bit + * starting at bit_offset is the LSB of the value + * values + * + * \code + * ucBits[2] = {0, 0}; + * + * // Set bit 4 to 1 (read: set 1 bit starting at bit offset 4 to value 1) + * mb_util_set_bits(ucBits, 4, 1, 1); + * + * // Set bit 7 to 1 and bit 8 to 0. + * mb_util_set_bits(ucBits, 7, 2, 0x01); + * + * // Set bits 8 - 11 to 0x05 and bits 12 - 15 to 0x0A; + * mb_util_set_bits(ucBits, 8, 8, 0x5A); + * \endcode + */ +void mb_util_set_bits(uint8_t *byte_buf, uint16_t bit_offset, uint8_t but_num, uint8_t values); + +/*! \brief Function to read bits in a byte buffer. + * + * This function is used to extract up bit values from an array. Up to eight + * bit values can be extracted in one step. + * + * \param byte_buf A buffer where the bit values are stored. + * \param bit_offset The starting address of the bits to set. The first + * bit has the offset 0. + * \param but_num Number of bits to modify. The value must always be smaller + * than 8. + * + * \code + * uint8_t ucBits[2] = {0, 0}; + * uint8_t ucResult; + * + * // Extract the bits 3 - 10. + * ucResult = mb_util_get_bits(ucBits, 3, 8); + * \endcode + */ +uint8_t mb_util_get_bits(uint8_t *byte_buf, uint16_t bit_offset, uint8_t but_num); + + +/*! \brief Standard function to set slave ID in the modbus object. + * + * This function is used to set the Slave ID array for modbus object. + * This ID can then be read over Modbus. + * + * \param inst - instance pointer to base modbus object + * \param slv_id - slave short address. + * \param is_running - true, if the slave is running, false otherwise + * \param slv_idstr - the pointer to slave ID array to set in the modbus object + * \param slv_idstr_len - slave ID array length + * + * returns the modbus error code = MB_ENOERR, if set correctly, MB_ENOREG, otherwise + * \endcode + */ +mb_err_enum_t mb_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len); + + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_objects/mb_master.c b/modbus/mb_objects/mb_master.c new file mode 100644 index 0000000..c813a2d --- /dev/null +++ b/modbus/mb_objects/mb_master.c @@ -0,0 +1,562 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "mb_config.h" +#include "mb_common.h" +#include "mb_proto.h" +#include "mb_func.h" +#include "mb_master.h" +#include "transport_common.h" +#include "port_common.h" +#include "ascii_transport.h" +#include "rtu_transport.h" +#include "tcp_transport.h" + +static const char *TAG = "mb_object.master"; + +#if (MB_MASTER_ASCII_ENABLED || MB_MASTER_RTU_ENABLED || MB_MASTER_TCP_ENABLED) + +static const mb_fn_handler_t master_handlers[MB_FUNC_HANDLERS_MAX] = +{ +#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED + {MB_FUNC_OTHER_REPORT_SLAVEID, (void *)mb_fn_report_slv_id}, +#endif +#if MB_FUNC_READ_INPUT_ENABLED + {MB_FUNC_READ_INPUT_REGISTER, (void *)mbm_fn_read_inp_reg}, +#endif +#if MB_FUNC_READ_HOLDING_ENABLED + {MB_FUNC_READ_HOLDING_REGISTER, (void *)mbm_fn_read_holding_reg}, +#endif +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED + {MB_FUNC_WRITE_MULTIPLE_REGISTERS, (void *)mbm_fn_write_multi_holding_reg}, +#endif +#if MB_FUNC_WRITE_HOLDING_ENABLED + {MB_FUNC_WRITE_REGISTER, (void *)mbm_fn_write_holding_reg}, +#endif +#if MB_FUNC_READWRITE_HOLDING_ENABLED + {MB_FUNC_READWRITE_MULTIPLE_REGISTERS, (void *)mbm_fn_rw_multi_holding_regs}, +#endif +#if MB_FUNC_READ_COILS_ENABLED + {MB_FUNC_READ_COILS, (void *)mbm_fn_read_coils}, +#endif +#if MB_FUNC_WRITE_COIL_ENABLED + {MB_FUNC_WRITE_SINGLE_COIL, (void *)mbm_fn_write_coil}, +#endif +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED + {MB_FUNC_WRITE_MULTIPLE_COILS, (void *)mbm_fn_write_multi_coils}, +#endif +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED + {MB_FUNC_READ_DISCRETE_INPUTS, (void *)mbm_fn_read_discrete_inputs}, +#endif +}; + +typedef struct +{ + mb_base_t base; + mb_comm_mode_t cur_mode; + mb_state_enum_t cur_state; + const mb_fn_handler_t *func_handlers; + uint8_t *rcv_frame; + uint8_t *snd_frame; + uint16_t pdu_snd_len; + uint8_t rcv_addr; + uint16_t pdu_rcv_len; + uint8_t func_code; + mb_exception_t exception; + uint8_t master_dst_addr; + uint64_t curr_trans_id; +} mbm_object_t; + +mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +mb_err_enum_t mbm_delete(mb_base_t *inst); +mb_err_enum_t mbm_enable(mb_base_t *inst); +mb_err_enum_t mbm_disable(mb_base_t *inst); +mb_err_enum_t mbm_poll(mb_base_t *inst); + +static void mbm_set_pdu_send_length(mb_base_t *inst, uint16_t length); +static uint16_t mbm_get_pdu_send_length(mb_base_t *inst); +static void mbm_set_dest_addr(mb_base_t *inst, uint8_t dest_addr); +static uint8_t mbm_get_dest_addr(mb_base_t *inst); +static void mbm_get_pdu_send_buf(mb_base_t *inst, uint8_t **pbuf); + +#if (MB_MASTER_ASCII_ENABLED || MB_MASTER_RTU_ENABLED) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mbm_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_obj), MB_EINVAL, TAG, "invalid options for the instance."); + MB_RETURN_ON_FALSE((ser_opts->mode == MB_RTU), MB_EILLSTATE, TAG, "incorrect mode != RTU."); + mb_err_enum_t ret = MB_ENOERR; + mbm_object_t *mbm_obj = NULL; + mbm_obj = (mbm_object_t *)calloc(1, sizeof(mbm_object_t)); + MB_GOTO_ON_FALSE((mbm_obj), MB_EILLSTATE, error, TAG, "no mem for mb master instance."); + CRITICAL_SECTION_INIT(mbm_obj->base.lock); + mbm_obj->cur_state = STATE_NOT_INITIALIZED; + mbm_obj->base.delete = mbm_delete; + mbm_obj->base.enable = mbm_enable; + mbm_obj->base.disable = mbm_disable; + mbm_obj->base.poll = mbm_poll; + mbm_obj->base.set_dest_addr = mbm_set_dest_addr; + mbm_obj->base.get_dest_addr = mbm_get_dest_addr; + mbm_obj->base.set_send_len = mbm_set_pdu_send_length; + mbm_obj->base.get_send_len = mbm_get_pdu_send_length; + mbm_obj->base.get_send_buf = mbm_get_pdu_send_buf; + mbm_obj->base.descr.parent = *in_out_obj; + mbm_obj->base.descr.is_master = true; + mbm_obj->base.descr.obj_name = (char *)TAG; + mbm_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + int res = asprintf(&mbm_obj->base.descr.parent_name, "mbm_rtu@%p", mbm_obj->base.descr.parent); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "name alloc fail, err: %d", (int)res); + mb_trans_base_t *transp_obj = (mb_trans_base_t *)mbm_obj; + ret = mbm_rtu_transp_create(ser_opts, (void **)&transp_obj); + MB_GOTO_ON_FALSE((transp_obj && (ret == MB_ENOERR)), MB_EILLSTATE, error, + TAG, "transport creation, err: %d", (int)ret); + mbm_obj->func_handlers = master_handlers; + mbm_obj->cur_mode = ser_opts->mode; + mbm_obj->cur_state = STATE_DISABLED; + transp_obj->get_tx_frm(transp_obj, (uint8_t **)&mbm_obj->snd_frame); + transp_obj->get_rx_frm(transp_obj, (uint8_t **)&mbm_obj->rcv_frame); + mbm_obj->curr_trans_id = 0; + mbm_obj->base.port_obj = transp_obj->port_obj; + mbm_obj->base.transp_obj = transp_obj; + *in_out_obj = (void *)&(mbm_obj->base); + ESP_LOGD(TAG, "created object %s", mbm_obj->base.descr.parent_name); + return MB_ENOERR; + +error: + if (transp_obj) { + mbm_rtu_transp_delete(transp_obj); + } + free(mbm_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(mbm_obj->base.lock); + free(mbm_obj); + mb_port_get_inst_counter_dec(); + return ret; +} + +mb_err_enum_t mbm_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_obj), MB_EINVAL, TAG, "invalid options for the instance."); + MB_RETURN_ON_FALSE((ser_opts->mode == MB_ASCII), MB_EILLSTATE, TAG, "incorrect option mode != ASCII."); + mb_err_enum_t ret = MB_ENOERR; + mbm_object_t *mbm_obj = NULL; + mbm_obj = (mbm_object_t *)calloc(1, sizeof(mbm_object_t)); + MB_GOTO_ON_FALSE((mbm_obj), MB_EILLSTATE, error, TAG, "no mem for mb master instance."); + CRITICAL_SECTION_INIT(mbm_obj->base.lock); + mbm_obj->cur_state = STATE_NOT_INITIALIZED; + mbm_obj->base.delete = mbm_delete; + mbm_obj->base.enable = mbm_enable; + mbm_obj->base.disable = mbm_disable; + mbm_obj->base.poll = mbm_poll; + mbm_obj->base.set_dest_addr = mbm_set_dest_addr; + mbm_obj->base.get_dest_addr = mbm_get_dest_addr; + mbm_obj->base.set_send_len = mbm_set_pdu_send_length; + mbm_obj->base.get_send_len = mbm_get_pdu_send_length; + mbm_obj->base.get_send_buf = mbm_get_pdu_send_buf; + mbm_obj->base.descr.parent = *in_out_obj; + mbm_obj->base.descr.is_master = true; + mbm_obj->base.descr.obj_name = (char *)TAG; + mbm_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + int res = asprintf(&mbm_obj->base.descr.parent_name, "mbm_ascii@%p", mbm_obj->base.descr.parent); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "name alloc fail, err: %d", (int)res); + mb_trans_base_t *transp_obj = (mb_trans_base_t *)mbm_obj; + ret = mbm_ascii_transp_create(ser_opts, (void **)&transp_obj); + MB_GOTO_ON_FALSE((transp_obj && (ret == MB_ENOERR)), MB_EILLSTATE, error, + TAG, "transport creation, err: %d", (int)ret); + mbm_obj->func_handlers = master_handlers; + mbm_obj->cur_mode = ser_opts->mode; + mbm_obj->cur_state = STATE_DISABLED; + transp_obj->get_tx_frm(transp_obj, (uint8_t **)&mbm_obj->snd_frame); + transp_obj->get_rx_frm(transp_obj, (uint8_t **)&mbm_obj->rcv_frame); + mbm_obj->base.port_obj = transp_obj->port_obj; // binding of the modbus object with port object + mbm_obj->base.transp_obj = transp_obj; + *in_out_obj = (void *)&(mbm_obj->base); + ESP_LOGD(TAG, "created object %s", mbm_obj->base.descr.parent_name); + return MB_ENOERR; + +error: + if (transp_obj) + { + mbm_ascii_transp_delete(transp_obj); + } + free(mbm_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(mbm_obj->base.lock); + free(mbm_obj); + mb_port_get_inst_counter_dec(); + return ret; +} + +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj) +{ + MB_RETURN_ON_FALSE((tcp_opts && in_out_obj), MB_EINVAL, TAG, "invalid options for the instance."); + MB_RETURN_ON_FALSE((tcp_opts->mode == MB_TCP), MB_EILLSTATE, TAG, "incorrect option mode != TCP."); + mb_err_enum_t ret = MB_ENOERR; + mbm_object_t *mbm_obj = NULL; + mbm_obj = (mbm_object_t *)calloc(1, sizeof(mbm_object_t)); + MB_RETURN_ON_FALSE(mbm_obj, MB_EILLSTATE, TAG, "no mem for mb master instance."); + CRITICAL_SECTION_INIT(mbm_obj->base.lock); + mbm_obj->cur_state = STATE_NOT_INITIALIZED; + mbm_obj->base.delete = mbm_delete; + mbm_obj->base.enable = mbm_enable; + mbm_obj->base.disable = mbm_disable; + mbm_obj->base.poll = mbm_poll; + mbm_obj->base.set_dest_addr = mbm_set_dest_addr; + mbm_obj->base.get_dest_addr = mbm_get_dest_addr; + mbm_obj->base.set_send_len = mbm_set_pdu_send_length; + mbm_obj->base.get_send_len = mbm_get_pdu_send_length; + mbm_obj->base.get_send_buf = mbm_get_pdu_send_buf; + mbm_obj->base.descr.parent = *in_out_obj; + mbm_obj->base.descr.is_master = true; + mbm_obj->base.descr.obj_name = (char *)TAG; + mbm_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + int res = asprintf(&mbm_obj->base.descr.parent_name, "mbm_tcp#%p", mbm_obj->base.descr.parent); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "name alloc fail, err: %d", (int)res); + mb_trans_base_t *transp_obj = (mb_trans_base_t *)mbm_obj; + ret = mbm_tcp_transp_create(tcp_opts, (void **)&transp_obj); + MB_GOTO_ON_FALSE((transp_obj && (ret == MB_ENOERR)), MB_EILLSTATE, error, + TAG, "transport creation, err: %d", (int)ret); + mbm_obj->func_handlers = master_handlers; + mbm_obj->cur_mode = tcp_opts->mode; + mbm_obj->cur_state = STATE_DISABLED; + transp_obj->get_tx_frm(transp_obj, (uint8_t **)&mbm_obj->snd_frame); + transp_obj->get_rx_frm(transp_obj, (uint8_t **)&mbm_obj->rcv_frame); + mbm_obj->base.port_obj = transp_obj->port_obj; // binding of the modbus object with port object + mbm_obj->base.transp_obj = transp_obj; + *in_out_obj = (void *)&(mbm_obj->base); + ESP_LOGD(TAG, "created object %s", mbm_obj->base.descr.parent_name); + return MB_ENOERR; + +error: + if (transp_obj) { + mbm_tcp_transp_delete(transp_obj); + } + free(mbm_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(mbm_obj->base.lock); + free(mbm_obj); + mb_port_get_inst_counter_dec(); + return ret; +} + +#endif + +mb_err_enum_t mbm_delete(mb_base_t *inst) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + mb_err_enum_t status = MB_ENOERR; + if (mbm_obj->cur_state == STATE_DISABLED) { + if (MB_OBJ(mbm_obj->base.transp_obj)->frm_delete) { + // call destructor of the transport object + mbm_obj->base.transp_obj->frm_delete(inst->transp_obj); + } + // delete the modbus instance + free(mbm_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(inst->lock); + status = MB_ENOERR; + free(inst); + } else { + ESP_LOGD(TAG, "disable the instance %p first.", mbm_obj); + status = MB_EILLSTATE; + } + mb_port_get_inst_counter_dec(); + return status; +} + +mb_err_enum_t mbm_enable(mb_base_t *inst) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + mb_err_enum_t status = MB_ENOERR; + CRITICAL_SECTION(inst->lock) + { + if (mbm_obj->cur_state == STATE_DISABLED) { + /* Activate the protocol stack. */ + MB_OBJ(mbm_obj->base.transp_obj)->frm_start(mbm_obj->base.transp_obj); + mbm_obj->cur_state = STATE_ENABLED; + status = MB_ENOERR; + } else { + status = MB_EILLSTATE; + } + } + return status; +} + +mb_err_enum_t mbm_disable(mb_base_t *inst) +{ + mb_err_enum_t status = MB_ENOERR; + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + CRITICAL_SECTION(inst->lock) + { + if (mbm_obj->cur_state == STATE_ENABLED) { + MB_OBJ(mbm_obj->base.transp_obj)->frm_stop(mbm_obj->base.transp_obj); + mbm_obj->cur_state = STATE_DISABLED; + status = MB_ENOERR; + } else if (mbm_obj->cur_state == STATE_DISABLED) { + status = MB_ENOERR; + } else { + status = MB_EILLSTATE; + } + } + return status; +} + +static void mbm_get_pdu_send_buf(mb_base_t *inst, uint8_t **pbuf) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + MB_OBJ(mbm_obj->base.transp_obj)->get_tx_frm(mbm_obj->base.transp_obj, pbuf); +} + +__attribute__((unused)) +static void mbm_get_pdu_recv_buf(mb_base_t *inst, uint8_t **pbuf) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + MB_OBJ(mbm_obj->base.transp_obj)->get_rx_frm(mbm_obj->base.transp_obj, pbuf); +} + +static void mbm_set_pdu_send_length(mb_base_t *inst, uint16_t length) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + CRITICAL_SECTION(inst->lock) + { + mbm_obj->pdu_snd_len = length; + } +} + +static uint16_t mbm_get_pdu_send_length(mb_base_t *inst) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + return mbm_obj->pdu_snd_len; +} + +static void mbm_set_dest_addr(mb_base_t *inst, uint8_t dest_addr) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + CRITICAL_SECTION(inst->lock) + { + mbm_obj->master_dst_addr = dest_addr; + } +} + +static uint8_t mbm_get_dest_addr(mb_base_t *inst) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + return mbm_obj->master_dst_addr; +} + +void mbm_error_cb_respond_timeout(mb_base_t *inst, uint8_t dest_addr, const uint8_t *pdu_data, uint16_t pdu_length) +{ + mb_port_event_set_resp_flag(MB_BASE2PORT(inst), EV_MASTER_ERROR_RESPOND_TIMEOUT); + ESP_LOG_BUFFER_HEX_LEVEL(__func__, (void *)pdu_data, pdu_length, ESP_LOG_DEBUG); +} + +void mbm_error_cb_receive_data(mb_base_t *inst, uint8_t dest_addr, const uint8_t *pdu_data, uint16_t pdu_length) +{ + mb_port_event_set_resp_flag(MB_BASE2PORT(inst), EV_MASTER_ERROR_RECEIVE_DATA); + ESP_LOG_BUFFER_HEX_LEVEL(__func__, (void *)pdu_data, pdu_length, ESP_LOG_DEBUG); +} + +void mbm_error_cb_execute_function(mb_base_t *inst, uint8_t dest_address, const uint8_t *pdu_data, uint16_t pdu_length) +{ + mb_port_event_set_resp_flag(MB_BASE2PORT(inst), EV_MASTER_ERROR_EXECUTE_FUNCTION); + ESP_LOG_BUFFER_HEX_LEVEL(__func__, (void *)pdu_data, pdu_length, ESP_LOG_DEBUG); +} + +void mbm_error_cb_request_success(mb_base_t *inst, uint8_t dest_address, const uint8_t *pdu_data, uint16_t pdu_length) +{ + mb_port_event_set_resp_flag(MB_BASE2PORT(inst), EV_MASTER_PROCESS_SUCCESS); + ESP_LOG_BUFFER_HEX_LEVEL(__func__, (void *)pdu_data, pdu_length, ESP_LOG_DEBUG); +} + +mb_err_enum_t mbm_poll(mb_base_t *inst) +{ + mbm_object_t *mbm_obj = MB_GET_OBJ_CTX(inst, mbm_object_t, base);; + + uint16_t length; + mb_exception_t exception; + mb_err_enum_t status = MB_ENOERR; + mb_event_t event; + mb_err_event_t error_type; + + /* Check if the protocol stack is ready. */ + if (mbm_obj->cur_state != STATE_ENABLED) { + return MB_EILLSTATE; + } + + /* Check if there is a event available. If not return control to caller. + * Otherwise we will handle the event. */ + if (mb_port_event_get(MB_OBJ(mbm_obj->base.port_obj), &event)) { + switch (event.event) { + case EV_READY: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_READY", MB_OBJ_PARENT(inst)); + mb_port_event_res_release(MB_OBJ(inst->port_obj)); + break; + + case EV_FRAME_TRANSMIT: + mbm_get_pdu_send_buf(inst, &mbm_obj->snd_frame); + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":MB_TRANSMIT"), + (void *)mbm_obj->snd_frame, mbm_obj->pdu_snd_len, ESP_LOG_DEBUG); + status = MB_OBJ(inst->transp_obj)->frm_send(inst->transp_obj, mbm_obj->master_dst_addr, + mbm_obj->snd_frame, mbm_obj->pdu_snd_len); + if (status != MB_ENOERR) { + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_RECEIVE_DATA); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_ERROR_PROCESS)); + ESP_LOGE(TAG, MB_OBJ_FMT", frame send error. %d", MB_OBJ_PARENT(inst), (int)status); + } + // Initialize modbus transaction + mbm_obj->curr_trans_id = event.trans_id; + break; + + case EV_FRAME_SENT: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_FRAME_SENT", MB_OBJ_PARENT(inst)); + break; + + case EV_FRAME_RECEIVED: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_FRAME_RECEIVED", MB_OBJ_PARENT(inst)); + mbm_obj->pdu_rcv_len = event.length; + status = MB_OBJ(inst->transp_obj)->frm_rcv(inst->transp_obj, &mbm_obj->rcv_addr, + &mbm_obj->rcv_frame, &mbm_obj->pdu_rcv_len); + MB_RETURN_ON_FALSE(mbm_obj->snd_frame, MB_EILLSTATE, TAG, "Send buffer initialization fail."); + if (event.trans_id == mbm_obj->curr_trans_id) { + // Check if the frame is for us. If not ,send an error process event. + if ((status == MB_ENOERR) && ((mbm_obj->rcv_addr == mbm_obj->master_dst_addr) + || (mbm_obj->rcv_addr == MB_TCP_PSEUDO_ADDRESS))) { + if ((mbm_obj->rcv_frame[MB_PDU_FUNC_OFF] & ~MB_FUNC_ERROR) == (mbm_obj->snd_frame[MB_PDU_FUNC_OFF])) { + ESP_LOGD(TAG, MB_OBJ_FMT", frame data received successfully, (%d).", MB_OBJ_PARENT(inst), (int)status); + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":MB_RECV"), (void *)mbm_obj->rcv_frame, + (uint16_t)mbm_obj->pdu_rcv_len, ESP_LOG_DEBUG); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_EXECUTE)); + } else { + ESP_LOGE(TAG, MB_OBJ_FMT", drop incorrect frame, receive_func(%u) != send_func(%u)", + MB_OBJ_PARENT(inst), (mbm_obj->rcv_frame[MB_PDU_FUNC_OFF] & ~MB_FUNC_ERROR), + mbm_obj->snd_frame[MB_PDU_FUNC_OFF]); + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_RECEIVE_DATA); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_ERROR_PROCESS)); + } + } else { + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_RECEIVE_DATA); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_ERROR_PROCESS)); + ESP_LOGD(TAG, MB_OBJ_FMT", packet data receive failed (addr=%u)(%u).", + MB_OBJ_PARENT(inst), (unsigned)mbm_obj->rcv_addr, (unsigned)status); + } + } else { + // Ignore the `EV_FRAME_RECEIVED` event because the respond timeout occurred + // and this is likely respond to previous transaction + ESP_LOGE(TAG, MB_OBJ_FMT", drop data received outside of transaction.", MB_OBJ_PARENT(inst)); + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_RESPOND_TIMEOUT); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_ERROR_PROCESS)); + } + break; + + case EV_EXECUTE: + if (event.trans_id == mbm_obj->curr_trans_id) { + if (MB_OBJ(inst->transp_obj)->frm_is_bcast(inst->transp_obj) + && ((mbm_obj->cur_mode == MB_RTU) || (mbm_obj->cur_mode == MB_ASCII))) { + mbm_obj->rcv_frame = mbm_obj->snd_frame; + } + MB_RETURN_ON_FALSE(mbm_obj->rcv_frame, MB_EILLSTATE, TAG, + MB_OBJ_FMT", receive buffer initialization fail.", MB_OBJ_PARENT(inst)); + ESP_LOGD(TAG, MB_OBJ_FMT":EV_EXECUTE", MB_OBJ_PARENT(inst)); + mbm_obj->func_code = mbm_obj->rcv_frame[MB_PDU_FUNC_OFF]; + exception = MB_EX_ILLEGAL_FUNCTION; + /* If receive frame has exception. The receive function code highest bit is 1.*/ + if (mbm_obj->func_code & MB_FUNC_ERROR) { + exception = (mb_exception_t)mbm_obj->rcv_frame[MB_PDU_DATA_OFF]; + } else { + for (int i = 0; i < MB_FUNC_HANDLERS_MAX; i++) { + /* No more function handlers registered. Abort. */ + if (mbm_obj->func_handlers[i].func_code == 0) { + break; + } + if (mbm_obj->func_handlers[i].func_code == mbm_obj->func_code) { + /* If master request is broadcast, + * the master need execute function for all slave. + */ + if (MB_OBJ(inst->transp_obj)->frm_is_bcast(inst->transp_obj)) { + length = mbm_obj->pdu_snd_len; + for (int j = 1; j <= MB_MASTER_TOTAL_SLAVE_NUM; j++) { + mbm_set_dest_addr(inst, j); + exception = mbm_obj->func_handlers[i].handler(inst, mbm_obj->rcv_frame, &length); + } + } else { + exception = mbm_obj->func_handlers[i].handler(inst, mbm_obj->rcv_frame, &mbm_obj->pdu_rcv_len); + } + break; + } + } + } + /* If master has exception, will send error process event. Otherwise the master is idle.*/ + if (exception != MB_EX_NONE) { + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_EXECUTE_FUNCTION); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_ERROR_PROCESS)); + } else { + error_type = mb_port_event_get_err_type(MB_OBJ(inst->port_obj)); + if (error_type == EV_ERROR_INIT) { + ESP_LOGD(TAG, MB_OBJ_FMT", set event EV_ERROR_OK", MB_OBJ_PARENT(inst)); + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_OK); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_ERROR_PROCESS)); + } + } + } else { + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_EXECUTE_FUNCTION); + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_ERROR_PROCESS)); + ESP_LOGE(TAG, MB_OBJ_FMT", execution is expired.", MB_OBJ_PARENT(inst)); + } + break; + + case EV_ERROR_PROCESS: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_ERROR_PROCESS", MB_OBJ_PARENT(inst)); + // stop timer and execute specified error process callback function. + mb_port_timer_disable(MB_OBJ(inst->port_obj)); + error_type = mb_port_event_get_err_type(MB_OBJ(inst->port_obj)); + mbm_get_pdu_send_buf(inst, &mbm_obj->snd_frame); + switch (error_type) + { + case EV_ERROR_RESPOND_TIMEOUT: + mbm_error_cb_respond_timeout(inst, mbm_obj->master_dst_addr, + mbm_obj->snd_frame, mbm_obj->pdu_snd_len); + break; + case EV_ERROR_RECEIVE_DATA: + mbm_error_cb_receive_data(inst, mbm_obj->master_dst_addr, + mbm_obj->snd_frame, mbm_obj->pdu_snd_len); + break; + case EV_ERROR_EXECUTE_FUNCTION: + mbm_error_cb_execute_function(inst, mbm_obj->master_dst_addr, + mbm_obj->snd_frame, mbm_obj->pdu_snd_len); + break; + case EV_ERROR_OK: + mbm_error_cb_request_success(inst, mbm_obj->master_dst_addr, + mbm_obj->snd_frame, mbm_obj->pdu_snd_len); + break; + default: + ESP_LOGE(TAG, MB_OBJ_FMT", incorrect error type = %d.", MB_OBJ_PARENT(inst), (int)error_type); + break; + } + mb_port_event_set_err_type(MB_OBJ(inst->port_obj), EV_ERROR_INIT); + uint64_t time_div_us = mbm_obj->curr_trans_id ? (event.get_ts - mbm_obj->curr_trans_id) : 0; + mbm_obj->curr_trans_id = 0; + ESP_LOGD(TAG, MB_OBJ_FMT", transaction processing time(us) = %" PRId64, MB_OBJ_PARENT(inst), time_div_us); + mb_port_event_res_release(MB_OBJ(inst->port_obj)); + break; + + default: + ESP_LOGE(TAG, MB_OBJ_FMT", unexpected event triggered 0x%02x.", MB_OBJ_PARENT(inst), (int)event.event); + break; + } + } else { + // Something went wrong and task unblocked but there are no any correct events set + ESP_LOGE(TAG, MB_OBJ_FMT", unexpected event triggered 0x%02x.", MB_OBJ_PARENT(inst), (int)event.event); + status = MB_EILLSTATE; + } + return status; +} + +#endif diff --git a/modbus/mb_objects/mb_slave.c b/modbus/mb_objects/mb_slave.c new file mode 100644 index 0000000..40b09da --- /dev/null +++ b/modbus/mb_objects/mb_slave.c @@ -0,0 +1,383 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "mb_common.h" +#include "mb_proto.h" +#include "mb_func.h" +#include "mb_slave.h" +#include "transport_common.h" +#include "port_common.h" +#include "ascii_transport.h" +#include "rtu_transport.h" +#include "tcp_transport.h" + +static const char *TAG = "mb_object.slave"; + +static mb_fn_handler_t slave_handlers[MB_FUNC_HANDLERS_MAX] = + { +#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED + {MB_FUNC_OTHER_REPORT_SLAVEID, (void *)mb_fn_report_slv_id}, +#endif +#if MB_FUNC_READ_INPUT_ENABLED + {MB_FUNC_READ_INPUT_REGISTER, (void *)mbs_fn_read_input_reg}, +#endif +#if MB_FUNC_READ_HOLDING_ENABLED + {MB_FUNC_READ_HOLDING_REGISTER, (void *)mbs_fn_read_holding_reg}, +#endif +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED + {MB_FUNC_WRITE_MULTIPLE_REGISTERS, (void *)mbs_fn_write_multi_holding_reg}, +#endif +#if MB_FUNC_WRITE_HOLDING_ENABLED + {MB_FUNC_WRITE_REGISTER, (void *)mbs_fn_write_holding_reg}, +#endif +#if MB_FUNC_READWRITE_HOLDING_ENABLED + {MB_FUNC_READWRITE_MULTIPLE_REGISTERS, (void *)mbs_fn_rw_multi_holding_reg}, +#endif +#if MB_FUNC_READ_COILS_ENABLED + {MB_FUNC_READ_COILS, (void *)mbs_fn_read_coils}, +#endif +#if MB_FUNC_WRITE_COIL_ENABLED + {MB_FUNC_WRITE_SINGLE_COIL, (void *)mbs_fn_write_coil}, +#endif +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED + {MB_FUNC_WRITE_MULTIPLE_COILS, (void *)mbs_fn_write_multi_coils}, +#endif +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED + {MB_FUNC_READ_DISCRETE_INPUTS, (void *)mbs_fn_read_discrete_inp}, +#endif +}; + +typedef struct +{ + mb_base_t base; + // here are slave object properties and methods + uint8_t mb_address; + mb_comm_mode_t cur_mode; + mb_state_enum_t cur_state; + mb_fn_handler_t *func_handlers; + uint8_t *frame; + uint16_t length; + uint8_t func_code; + uint8_t rcv_addr; + uint64_t curr_trans_id; + volatile uint16_t *pdu_snd_len; +} mbs_object_t; + +mb_err_enum_t mbs_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +mb_err_enum_t mbs_delete(mb_base_t *inst); +mb_err_enum_t mbs_enable(mb_base_t *inst); +mb_err_enum_t mbs_disable(mb_base_t *inst); +mb_err_enum_t mbs_poll(mb_base_t *inst); +mb_err_enum_t mbs_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len); + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mbs_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj) +{ + mb_err_enum_t ret = MB_ENOERR; + MB_RETURN_ON_FALSE(ser_opts, MB_EINVAL, TAG, "invalid options for the instance."); + MB_RETURN_ON_FALSE((ser_opts->mode == MB_RTU), MB_EILLSTATE, TAG, "incorrect mode != RTU."); + mbs_object_t *mbs_obj = NULL; + mbs_obj = (mbs_object_t*)calloc(1, sizeof(mbs_object_t)); + MB_GOTO_ON_FALSE((mbs_obj), MB_EILLSTATE, error, TAG, "no mem for mb slave instance."); + CRITICAL_SECTION_INIT(mbs_obj->base.lock); + mbs_obj->cur_state = STATE_NOT_INITIALIZED; + mbs_obj->base.delete = mbs_delete; + mbs_obj->base.enable = mbs_enable; + mbs_obj->base.disable = mbs_disable; + mbs_obj->base.poll = mbs_poll; + mbs_obj->base.descr.parent = *in_out_obj; + mbs_obj->base.descr.is_master = false; + mbs_obj->base.descr.obj_name = (char *)TAG; + mbs_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + int res = asprintf(&mbs_obj->base.descr.parent_name, "mbs_rtu@%p", *in_out_obj); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "name alloc fail, err: %d", (int)res); + mb_trans_base_t *transp_obj = (mb_trans_base_t *)mbs_obj; + ret = mbs_rtu_transp_create(ser_opts, (void **)&transp_obj); + MB_GOTO_ON_FALSE((transp_obj && (ret == MB_ENOERR)), MB_EILLSTATE, error, + TAG, "transport creation, err: %d", (int)ret); + mbs_obj->func_handlers = slave_handlers; + mbs_obj->cur_mode = ser_opts->mode; + mbs_obj->mb_address = ser_opts->uid; + mbs_obj->cur_state = STATE_DISABLED; + transp_obj->get_tx_frm(transp_obj, (uint8_t **)&mbs_obj->frame); + mbs_obj->base.port_obj = transp_obj->port_obj; + mbs_obj->base.transp_obj = transp_obj; + *in_out_obj = (void *)&(mbs_obj->base); + ESP_LOGD(TAG, "created object %s", mbs_obj->base.descr.parent_name); + return MB_ENOERR; + +error: + if (transp_obj) { + mbs_rtu_transp_delete(transp_obj); + } + free(mbs_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(mbs_obj->base.lock); + free(mbs_obj); + mb_port_get_inst_counter_dec(); + return ret; +} + +mb_err_enum_t mbs_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj) +{ + mb_err_enum_t ret = MB_ENOERR; + MB_RETURN_ON_FALSE(ser_opts, MB_EINVAL, TAG, "invalid options for %s instance.", TAG); + MB_RETURN_ON_FALSE((ser_opts->mode == MB_ASCII), MB_EILLSTATE, TAG, "incorrect mode != ASCII."); + mbs_object_t *mbs_obj = NULL; + mbs_obj = (mbs_object_t*)calloc(1, sizeof(mbs_object_t)); + MB_GOTO_ON_FALSE((mbs_obj), MB_EILLSTATE, error, TAG, "no mem for mb slave instance."); + CRITICAL_SECTION_INIT(mbs_obj->base.lock); + mbs_obj->base.delete = mbs_delete; + mbs_obj->base.enable = mbs_enable; + mbs_obj->base.disable = mbs_disable; + mbs_obj->base.poll = mbs_poll; + mbs_obj->base.descr.parent = *in_out_obj; + mbs_obj->base.descr.is_master = false; + mbs_obj->base.descr.obj_name = (char *)TAG; + mbs_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + int res = asprintf(&mbs_obj->base.descr.parent_name, "mbs_ascii@%p", *in_out_obj); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "name alloc fail, err: %d", (int)res); + mb_trans_base_t *transp_obj = (mb_trans_base_t *)mbs_obj; + ret = mbs_ascii_transp_create(ser_opts, (void **)&transp_obj); + MB_GOTO_ON_FALSE((transp_obj && (ret == MB_ENOERR)), MB_EILLSTATE, error, + TAG, "transport creation, err: %d", (int)ret); + mbs_obj->func_handlers = slave_handlers; + mbs_obj->cur_mode = ser_opts->mode; + mbs_obj->mb_address = ser_opts->uid; + mbs_obj->cur_state = STATE_DISABLED; + transp_obj->get_tx_frm(transp_obj, (uint8_t **)&mbs_obj->frame); + mbs_obj->base.port_obj = transp_obj->port_obj; + mbs_obj->base.transp_obj = transp_obj; + *in_out_obj = (void *)&(mbs_obj->base); + ESP_LOGD(TAG, "created object %s", mbs_obj->base.descr.parent_name); + return MB_ENOERR; + +error: + if (transp_obj) { + mbs_ascii_transp_delete(transp_obj); + } + free(mbs_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(mbs_obj->base.lock); + free(mbs_obj); + mb_port_get_inst_counter_dec(); + return ret; +} + +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +mb_err_enum_t mbs_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj) +{ + mb_err_enum_t ret = MB_ENOERR; + MB_RETURN_ON_FALSE(tcp_opts, MB_EINVAL, TAG, "invalid options for the instance."); + MB_RETURN_ON_FALSE((tcp_opts->mode == MB_TCP), MB_EILLSTATE, TAG, "incorrect mode != TCP."); + mbs_object_t *mbs_obj = NULL; + mbs_obj = (mbs_object_t*)calloc(1, sizeof(mbs_object_t)); + MB_GOTO_ON_FALSE((mbs_obj), MB_EILLSTATE, error, TAG, "no mem for mb slave instance."); + CRITICAL_SECTION_INIT(mbs_obj->base.lock); + mbs_obj->cur_state = STATE_NOT_INITIALIZED; + mbs_obj->base.delete = mbs_delete; + mbs_obj->base.enable = mbs_enable; + mbs_obj->base.disable = mbs_disable; + mbs_obj->base.poll = mbs_poll; + mbs_obj->base.descr.parent = *in_out_obj; + mbs_obj->base.descr.is_master = false; + mbs_obj->base.descr.obj_name = (char *)TAG; + mbs_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + int res = asprintf(&mbs_obj->base.descr.parent_name, "mbs_tcp@%p", *in_out_obj); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "name alloc fail, err: %d", (int)res); + mb_trans_base_t *transp_obj = (mb_trans_base_t *)mbs_obj; + ret = mbs_tcp_transp_create(tcp_opts, (void **)&transp_obj); + MB_GOTO_ON_FALSE((transp_obj && (ret == MB_ENOERR)), MB_EILLSTATE, error, + TAG, "transport creation, err: %d", (int)ret); + mbs_obj->func_handlers = slave_handlers; + mbs_obj->cur_mode = tcp_opts->mode; + mbs_obj->mb_address = tcp_opts->uid; + mbs_obj->cur_state = STATE_DISABLED; + transp_obj->get_tx_frm(transp_obj, (uint8_t **)&mbs_obj->frame); + mbs_obj->base.port_obj = transp_obj->port_obj; + mbs_obj->base.transp_obj = transp_obj; + *in_out_obj = (void *)&(mbs_obj->base); + ESP_LOGD(TAG, "created object %s", mbs_obj->base.descr.parent_name); + return MB_ENOERR; + +error: + if (transp_obj) { + mbs_tcp_transp_delete(transp_obj); + } + free(mbs_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(mbs_obj->base.lock); + free(mbs_obj); + mb_port_get_inst_counter_dec(); + return ret; +} + +#endif + +mb_err_enum_t mbs_delete(mb_base_t *inst) +{ + mbs_object_t *mbs_obj = MB_GET_OBJ_CTX(inst, mbs_object_t, base); + mb_err_enum_t status = MB_ENOERR; + if (mbs_obj->cur_state == STATE_DISABLED) { + if (MB_OBJ(mbs_obj->base.transp_obj)->frm_delete) { + // call destructor of the transport object + MB_OBJ(mbs_obj->base.transp_obj)->frm_delete(inst->transp_obj); + } + // delete the modbus instance + free(mbs_obj->base.descr.parent_name); + CRITICAL_SECTION_CLOSE(inst->lock); + free(inst); + status = MB_ENOERR; + } else { + ESP_LOGD(TAG, " need to disable %p object first.", (void *)mbs_obj); + status = MB_EILLSTATE; + } + mb_port_get_inst_counter_dec(); + return status; +} + +mb_err_enum_t mbs_enable(mb_base_t *inst) +{ + mbs_object_t *mbs_obj = MB_GET_OBJ_CTX(inst, mbs_object_t, base); + mb_err_enum_t status = MB_ENOERR; + CRITICAL_SECTION(inst->lock) { + if (mbs_obj->cur_state == STATE_DISABLED) { + /* Activate the protocol stack. */ + MB_OBJ(mbs_obj->base.transp_obj)->frm_start(mbs_obj->base.transp_obj); + mbs_obj->cur_state = STATE_ENABLED; + status = MB_ENOERR; + } else { + status = MB_EILLSTATE; + } + } + if (!mbs_obj->mb_address) { + ESP_LOGD(TAG, "incorrect slave address in %p object.", (void *)mbs_obj); + status = MB_EINVAL; + } + return status; +} + +mb_err_enum_t mbs_disable(mb_base_t *inst) +{ + mb_err_enum_t status = MB_ENOERR; + mbs_object_t *mbs_obj = MB_GET_OBJ_CTX(inst, mbs_object_t, base);; + CRITICAL_SECTION(inst->lock) { + if (mbs_obj->cur_state == STATE_ENABLED) { + MB_OBJ(mbs_obj->base.transp_obj)->frm_stop(mbs_obj->base.transp_obj); + mbs_obj->cur_state = STATE_DISABLED; + status = MB_ENOERR; + } else if (mbs_obj->cur_state == STATE_DISABLED) { + status = MB_ENOERR; + } else { + status = MB_EILLSTATE; + } + } + return status; +} + +mb_err_enum_t mbs_poll(mb_base_t *inst) +{ + mbs_object_t *mbs_obj = MB_GET_OBJ_CTX(inst, mbs_object_t, base);; + + mb_exception_t exception; + mb_err_enum_t status = MB_ENOERR; + mb_event_t event; + + /* Check if the protocol stack is ready. */ + if (mbs_obj->cur_state != STATE_ENABLED) { + return MB_EILLSTATE; + } + + /* Check if there is a event available. If not, return control to caller. Otherwise we will handle the event. */ + if (mb_port_event_get(MB_OBJ(mbs_obj->base.port_obj), &event)) { + switch(event.event) { + case EV_READY: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_READY", MB_OBJ_PARENT(inst)); + mb_port_event_res_release(MB_OBJ(inst->port_obj)); + break; + + case EV_FRAME_RECEIVED: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_FRAME_RECEIVED", MB_OBJ_PARENT(inst)); + mbs_obj->length = event.length; + status = MB_OBJ(inst->transp_obj)->frm_rcv(inst->transp_obj, &mbs_obj->rcv_addr, &mbs_obj->frame, &mbs_obj->length); + // Check if the frame is for us. If not ,send an error process event. + if (status == MB_ENOERR) { + // Check if the frame is for us. If not ignore the frame. + if((mbs_obj->rcv_addr == mbs_obj->mb_address) || (mbs_obj->rcv_addr == MB_ADDRESS_BROADCAST) + || (mbs_obj->rcv_addr == MB_TCP_PSEUDO_ADDRESS)) { + mbs_obj->curr_trans_id = event.get_ts; + (void)mb_port_event_post(MB_OBJ(inst->port_obj), EVENT(EV_EXECUTE | EV_TRANS_START)); + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":MB_RECV"), &mbs_obj->frame[MB_PDU_FUNC_OFF], + (uint16_t)mbs_obj->length, ESP_LOG_DEBUG); + } + } + break; + + case EV_EXECUTE: + MB_RETURN_ON_FALSE(mbs_obj->frame, MB_EILLSTATE, TAG, "receive buffer fail."); + ESP_LOGD(TAG, MB_OBJ_FMT":EV_EXECUTE", MB_OBJ_PARENT(inst)); + mbs_obj->func_code = mbs_obj->frame[MB_PDU_FUNC_OFF]; + exception = MB_EX_ILLEGAL_FUNCTION; + // If receive frame has exception. The receive function code highest bit is 1. + for (int i = 0; (i < MB_FUNC_HANDLERS_MAX); i++) { + // No more function handlers registered. Abort. + if (mbs_obj->func_handlers[i].func_code == 0) { + ESP_LOGD(TAG, MB_OBJ_FMT": function (0x%x), handler is not found.", MB_OBJ_PARENT(inst), (int)mbs_obj->func_code); + break; + } + if ((mbs_obj->func_handlers[i].func_code) == mbs_obj->func_code) { + ESP_LOGD(TAG, MB_OBJ_FMT": function (0x%x), start handler.", MB_OBJ_PARENT(inst), (int)mbs_obj->func_code); + exception = mbs_obj->func_handlers[i].handler(inst, mbs_obj->frame, &mbs_obj->length); + break; + } + } + // If the request was not sent to the broadcast address, return a reply. + if ((mbs_obj->rcv_addr != MB_ADDRESS_BROADCAST) || (mbs_obj->cur_mode == MB_TCP)) { + if (exception != MB_EX_NONE) { + // An exception occurred. Build an error frame. + mbs_obj->length = 0; + mbs_obj->frame[mbs_obj->length++] = (uint8_t)(mbs_obj->func_code | MB_FUNC_ERROR); + mbs_obj->frame[mbs_obj->length++] = exception; + } + if ((mbs_obj->cur_mode == MB_ASCII) && MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS) { + mb_port_timer_delay(MB_OBJ(inst->port_obj), MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS); + } + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":MB_SEND"), (void *)mbs_obj->frame, mbs_obj->length, ESP_LOG_DEBUG); + status = MB_OBJ(inst->transp_obj)->frm_send(inst->transp_obj, mbs_obj->rcv_addr, mbs_obj->frame, mbs_obj->length); + if (status != MB_ENOERR) { + ESP_LOGE(TAG, MB_OBJ_FMT":frame send error. %d", MB_OBJ_PARENT(inst), (int)status); + } + } + break; + + case EV_FRAME_TRANSMIT: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_FRAME_TRANSMIT", MB_OBJ_PARENT(inst)); + break; + + case EV_FRAME_SENT: + ESP_LOGD(TAG, MB_OBJ_FMT":EV_MASTER_FRAME_SENT", MB_OBJ_PARENT(inst)); + uint64_t time_div_us = mbs_obj->curr_trans_id ? (event.get_ts - mbs_obj->curr_trans_id) : 0; + mbs_obj->curr_trans_id = 0; + ESP_LOGD(TAG, MB_OBJ_FMT", transaction processing time(us) = %" PRId64, MB_OBJ_PARENT(inst), time_div_us); + break; + + default: + ESP_LOGD(TAG, MB_OBJ_FMT": Unexpected event triggered 0x%02x.", MB_OBJ_PARENT(inst), (int)event.event); + break; + } + } else { + // Something went wrong and task unblocked but there are no any correct events set + ESP_LOGD(TAG, MB_OBJ_FMT": Unexpected event triggered 0x%02x, timeout?", MB_OBJ_PARENT(inst), (int)event.event); + status = MB_EILLSTATE; + } + return status; +} diff --git a/modbus/mb_ports/common/mb_transaction.c b/modbus/mb_ports/common/mb_transaction.c new file mode 100644 index 0000000..1c60799 --- /dev/null +++ b/modbus/mb_ports/common/mb_transaction.c @@ -0,0 +1,278 @@ +#include +#include +#include +#include +#include "sys/queue.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "mb_transaction.h" + +static const char *TAG = "mb_transaction"; + +/** + * @brief transaction list item + */ +typedef struct transaction_item { + uint8_t *buffer; + uint16_t len; + int node_id; + int msg_id; + void *pnode; + transaction_tick_t tick; + _Atomic pending_state_t state; + STAILQ_ENTRY(transaction_item) next; +} transaction_item_t; + +STAILQ_HEAD(transaction_list_t, transaction_item); + +struct transaction_t { + _lock_t lock; + uint64_t size; + struct transaction_list_t *list; +}; + +transaction_handle_t transaction_init(void) +{ + transaction_handle_t transaction = calloc(1, sizeof(struct transaction_t)); + ESP_MEM_CHECK(TAG, transaction, return NULL); + transaction->list = calloc(1, sizeof(struct transaction_list_t)); + ESP_MEM_CHECK(TAG, transaction->list, {free(transaction); return NULL;}); + transaction->size = 0; + CRITICAL_SECTION_INIT(transaction->lock); + STAILQ_INIT(transaction->list); + return transaction; +} + +transaction_item_handle_t transaction_enqueue(transaction_handle_t transaction, transaction_message_handle_t message, transaction_tick_t tick) +{ + transaction_item_handle_t item = calloc(1, sizeof(transaction_item_t)); + ESP_MEM_CHECK(TAG, item, { + return NULL; + }); + CRITICAL_SECTION_LOCK(transaction->lock); + item->tick = tick; + item->node_id = message->node_id; + item->pnode = message->pnode; + item->msg_id = message->msg_id; + item->len = message->len; + item->state = QUEUED; + if (!message->buffer) { + item->buffer = heap_caps_malloc(message->len, TRANSACTION_MEMORY); + memcpy(item->buffer, message->buffer, message->len); + } else { + item->buffer = message->buffer; + } + ESP_MEM_CHECK(TAG, item->buffer, { + free(item); + CRITICAL_SECTION_UNLOCK(transaction->lock); + return NULL; + }); + STAILQ_INSERT_TAIL(transaction->list, item, next); + transaction->size += item->len; + CRITICAL_SECTION_UNLOCK(transaction->lock); + ESP_LOGD(TAG, "ENQUEUE msgid=%x, len=%d, size=%"PRIu64, message->msg_id, message->len, transaction_get_size(transaction)); + return item; +} + +transaction_item_handle_t transaction_get(transaction_handle_t transaction, int msg_id) +{ + transaction_item_handle_t item; + CRITICAL_SECTION_LOCK(transaction->lock); + STAILQ_FOREACH(item, transaction->list, next) { + if (item->msg_id == msg_id) { + CRITICAL_SECTION_UNLOCK(transaction->lock); + return item; + } + } + CRITICAL_SECTION_UNLOCK(transaction->lock); + return NULL; +} + +transaction_item_handle_t transaction_get_first(transaction_handle_t transaction) +{ + transaction_item_handle_t item; + if (STAILQ_EMPTY(transaction->list)) { + return NULL; + } + + item = STAILQ_FIRST(transaction->list); + if (item) + { + return item; + } + return NULL; +} + +transaction_item_handle_t transaction_dequeue(transaction_handle_t transaction, pending_state_t state, transaction_tick_t *tick) +{ + transaction_item_handle_t item; + CRITICAL_SECTION_LOCK(transaction->lock); + STAILQ_FOREACH(item, transaction->list, next) { + if (atomic_load(&(item->state)) == state) { + if (tick) { + *tick = item->tick; + } + CRITICAL_SECTION_UNLOCK(transaction->lock); + return item; + } + } + CRITICAL_SECTION_UNLOCK(transaction->lock); + return NULL; +} + +esp_err_t transaction_delete_item(transaction_handle_t transaction, transaction_item_handle_t item_to_delete) +{ + transaction_item_handle_t item; + CRITICAL_SECTION_LOCK(transaction->lock); + STAILQ_FOREACH(item, transaction->list, next) { + if (item == item_to_delete) { + STAILQ_REMOVE(transaction->list, item, transaction_item, next); + transaction->size -= item->len; + free(item->buffer); + free(item); + CRITICAL_SECTION_UNLOCK(transaction->lock); + return ESP_OK; + } + } + CRITICAL_SECTION_UNLOCK(transaction->lock); + return ESP_FAIL; +} + +uint8_t *transaction_item_get_data(transaction_item_handle_t item, size_t *len, uint16_t *msg_id, int *node_id) +{ + if (item) { + if (len) { + *len = item->len; + } + if (msg_id) { + *msg_id = item->msg_id; + } + if (node_id) { + *node_id = item->node_id; + } + return (uint8_t *)item->buffer; + } + return NULL; +} + +esp_err_t transaction_delete(transaction_handle_t transaction, int msg_id) +{ + transaction_item_handle_t item, tmp; + CRITICAL_SECTION_LOCK(transaction->lock); + STAILQ_FOREACH_SAFE(item, transaction->list, next, tmp) { + if (item->msg_id == msg_id) { + STAILQ_REMOVE(transaction->list, item, transaction_item, next); + transaction->size -= item->len; + free(item->buffer); + free(item); + CRITICAL_SECTION_UNLOCK(transaction->lock); + ESP_LOGD(TAG, "DELETED msgid=%x, remain size=%"PRIu64, msg_id, transaction_get_size(transaction)); + return ESP_OK; + } + } + CRITICAL_SECTION_UNLOCK(transaction->lock); + return ESP_FAIL; +} + +esp_err_t transaction_set_state(transaction_handle_t transaction, int msg_id, pending_state_t state) +{ + transaction_item_handle_t item = transaction_get(transaction, msg_id); + if (item) { + atomic_store(&(item->state), state); + return ESP_OK; + } + return ESP_FAIL; +} + +pending_state_t transaction_item_get_state(transaction_item_handle_t item) +{ + if (item) { + return atomic_load(&(item->state)); + } + return INIT; +} + +esp_err_t transaction_item_set_state(transaction_item_handle_t item, pending_state_t state) +{ + if (item) { + atomic_store(&(item->state), state); + return ESP_OK; + } + return ESP_FAIL; +} + +esp_err_t transaction_set_tick(transaction_handle_t transaction, int msg_id, transaction_tick_t tick) +{ + transaction_item_handle_t item = transaction_get(transaction, msg_id); + if (item) { + item->tick = tick; + return ESP_OK; + } + return ESP_FAIL; +} + +int transaction_delete_single_expired(transaction_handle_t transaction, transaction_tick_t current_tick, transaction_tick_t timeout) +{ + int msg_id = -1; + transaction_item_handle_t item; + CRITICAL_SECTION_LOCK(transaction->lock); + STAILQ_FOREACH(item, transaction->list, next) { + if (current_tick - item->tick > timeout) { + STAILQ_REMOVE(transaction->list, item, transaction_item, next); + free(item->buffer); + transaction->size -= item->len; + msg_id = item->msg_id; + free(item); + CRITICAL_SECTION_UNLOCK(transaction->lock); + return msg_id; + } + + } + CRITICAL_SECTION_UNLOCK(transaction->lock); + return msg_id; +} + +int transaction_delete_expired(transaction_handle_t transaction, transaction_tick_t current_tick, transaction_tick_t timeout) +{ + int deleted_items = 0; + transaction_item_handle_t item, tmp; + CRITICAL_SECTION_LOCK(transaction->lock); + STAILQ_FOREACH_SAFE(item, transaction->list, next, tmp) { + if (current_tick - item->tick > timeout) { + STAILQ_REMOVE(transaction->list, item, transaction_item, next); + free(item->buffer); + transaction->size -= item->len; + free(item); + deleted_items ++; + } + } + CRITICAL_SECTION_UNLOCK(transaction->lock); + return deleted_items; +} + +uint64_t transaction_get_size(transaction_handle_t transaction) +{ + return transaction->size; +} + +void transaction_delete_all_items(transaction_handle_t transaction) +{ + transaction_item_handle_t item, tmp; + CRITICAL_SECTION_LOCK(transaction->lock); + STAILQ_FOREACH_SAFE(item, transaction->list, next, tmp) { + STAILQ_REMOVE(transaction->list, item, transaction_item, next); + transaction->size -= item->len; + free(item->buffer); + free(item); + } + CRITICAL_SECTION_UNLOCK(transaction->lock); +} + +void transaction_destroy(transaction_handle_t transaction) +{ + transaction_delete_all_items(transaction); + CRITICAL_SECTION_CLOSE(transaction->lock); + free(transaction->list); + free(transaction); +} + diff --git a/modbus/mb_ports/common/mb_transaction.h b/modbus/mb_ports/common/mb_transaction.h new file mode 100644 index 0000000..d189ac3 --- /dev/null +++ b/modbus/mb_ports/common/mb_transaction.h @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_err.h" +#include "port_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define TRANSACTION_MEMORY MALLOC_CAP_DEFAULT + +#define ESP_MEM_CHECK(TAG, a, action) if (!(a)) { \ + ESP_LOGE(TAG,"%s(%d): %s", __FUNCTION__, __LINE__, "Memory exhausted"); \ + action; \ + } + +struct transaction_item; + +typedef struct transaction_t *transaction_handle_t; +typedef struct transaction_item *transaction_item_handle_t; + +typedef struct transaction_message { + uint8_t *buffer; + uint16_t len; + int msg_id; + int node_id; + void *pnode; +} transaction_message_t; + +typedef struct transaction_message *transaction_message_handle_t; +typedef long long transaction_tick_t; + +typedef enum pending_state { + INIT, + QUEUED, + ACKNOWLEDGED, + CONFIRMED, + REPLIED, + RECEIVED, + TRANSMITTED, + EXPIRED +} pending_state_t; + +transaction_handle_t transaction_init(void); +transaction_item_handle_t transaction_enqueue(transaction_handle_t transaction, transaction_message_handle_t message, transaction_tick_t tick); +transaction_item_handle_t transaction_dequeue(transaction_handle_t transaction, pending_state_t pending, transaction_tick_t *tick); +transaction_item_handle_t transaction_get(transaction_handle_t transaction, int msg_id); +transaction_item_handle_t transaction_get_first(transaction_handle_t transaction); +uint8_t *transaction_item_get_data(transaction_item_handle_t item, size_t *len, uint16_t *msg_id, int *node_id); +esp_err_t transaction_delete(transaction_handle_t transaction, int msg_id); +esp_err_t transaction_delete_item(transaction_handle_t transaction, transaction_item_handle_t item); +int transaction_delete_expired(transaction_handle_t transaction, transaction_tick_t current_tick, transaction_tick_t timeout); + +/** + * @brief Deletes single expired message returning it's message id + * + * @return msg id of the deleted message, -1 if no expired message in the transaction + */ +int transaction_delete_single_expired(transaction_handle_t transaction, transaction_tick_t current_tick, transaction_tick_t timeout); +esp_err_t transaction_set_state(transaction_handle_t transaction, int msg_id, pending_state_t pending); +pending_state_t transaction_item_get_state(transaction_item_handle_t item); +esp_err_t transaction_item_set_state(transaction_item_handle_t item, pending_state_t state); +esp_err_t transaction_set_tick(transaction_handle_t transaction, int msg_id, transaction_tick_t tick); +uint64_t transaction_get_size(transaction_handle_t transaction); +void transaction_destroy(transaction_handle_t transaction); +void transaction_delete_all_items(transaction_handle_t transaction); + +#ifdef __cplusplus +} +#endif + diff --git a/modbus/mb_ports/common/port_common.h b/modbus/mb_ports/common/port_common.h new file mode 100644 index 0000000..63d370a --- /dev/null +++ b/modbus/mb_ports/common/port_common.h @@ -0,0 +1,228 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +/*----------------------- Platform includes --------------------------------*/ +#include "spinlock.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/event_groups.h" +#include "freertos/semphr.h" +#include "freertos/portmacro.h" + +#include "mb_port_types.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +#define MB_SER_PDU_SIZE_MIN (4) +#define MB_TIMER_TICS_PER_MS (20UL) // Define number of timer reloads per 1 mS +#define MB_TIMER_TICK_TIME_US (1000 / MB_TIMER_TICS_PER_MS) // 50uS = one discreet for timer +#define MB_EVENT_QUEUE_TIMEOUT_MAX_MS (3000) +#define MB_EVENT_QUEUE_TIMEOUT (pdMS_TO_TICKS(CONFIG_FMB_EVENT_QUEUE_TIMEOUT)) +#define MB_EVENT_QUEUE_TIMEOUT_MAX (pdMS_TO_TICKS(MB_EVENT_QUEUE_TIMEOUT_MAX_MS)) + +int lock_obj(_lock_t *plock); +void unlock_obj(_lock_t *plock); + +#define CRITICAL_SECTION_INIT(lock) \ + do \ + { \ + _lock_init((_lock_t *)&lock); \ + } while (0) + +#define CRITICAL_SECTION_CLOSE(lock) \ + do \ + { \ + _lock_close((_lock_t *)&lock); \ + } while (0) + +#define CRITICAL_SECTION_LOCK(lock) \ + do \ + { \ + lock_obj((_lock_t *)&lock); \ + } while (0) + +#define CRITICAL_SECTION_UNLOCK(lock) \ + do \ + { \ + unlock_obj((_lock_t *)&lock); \ + } while (0) + +#define CRITICAL_SECTION(lock) for (int st = lock_obj((_lock_t *)&lock); (st > 0); unlock_obj((_lock_t *)&lock), st = -1) + +#define SPIN_LOCK_INIT(lock) \ + do \ + { \ + spinlock_initialize(&lock); \ + } while (0) + + +#define CRITICAL_STORE(LOCK, PTR, VAL) \ +__extension__ \ +({ \ + __auto_type __atomic_ptr = (PTR); \ + __typeof__ ((void)0, *__atomic_ptr) __atomic_tmp = (VAL); \ + _lock_acquire((_lock_t *)&LOCK); \ + *__atomic_ptr = __atomic_tmp; \ + _lock_release((_lock_t *)&LOCK); \ + (__atomic_tmp); \ +}) + +#define CRITICAL_LOAD(LOCK, PTR) \ +__extension__ \ +({ \ + __auto_type __atomic_ptr = (PTR); \ + __typeof__ ((void)0, *__atomic_ptr) __atomic_tmp; \ + _lock_acquire((_lock_t *)&LOCK); \ + __atomic_tmp = (*__atomic_ptr); \ + _lock_release((_lock_t *)&LOCK); \ + (__atomic_tmp); \ +}) + +#define SPIN_LOCK_ENTER(lock) \ + do \ + { \ + spinlock_acquire(&lock, SPINLOCK_WAIT_FOREVER); \ + } while (0) + +#define SPIN_LOCK_EXIT(lock) \ + do \ + { \ + spinlock_release(&lock); \ + } while (0) + +#define MB_EVENT_REQ_MASK (EventBits_t)(EV_MASTER_PROCESS_SUCCESS | \ + EV_MASTER_ERROR_RESPOND_TIMEOUT | \ + EV_MASTER_ERROR_RECEIVE_DATA | \ + EV_MASTER_ERROR_EXECUTE_FUNCTION) + +#define MB_PORT_CHECK_EVENT(event, mask) (event & mask) +#define MB_PORT_CLEAR_EVENT(event, mask) \ + do \ + { \ + event &= ~mask; \ + } while (0) + +// concatenation of the two arguments +#define PP_CAT2(_1, _2) PP_CAT_(_1, _2) +#define PP_CAT_(_1, _2) _1##_2 + +#define PP_VA_NUM_ARGS(...) PP_VA_NUM_ARGS_(__VA_ARGS__, 4, 3, 2, 1) +#define PP_VA_NUM_ARGS_(_1, _2, _3, _4, N, ...) N + +// Initialization of event structure using variadic parameters +#define EVENT(...) PP_CAT2(EVENT_, PP_VA_NUM_ARGS(__VA_ARGS__))(__VA_ARGS__) + +#define EVENT_1(_1) \ + (mb_event_t) { .event = _1 } +#define EVENT_2(_1, _2) \ + (mb_event_t) { .event = _1, .length = _2 } +#define EVENT_3(_1, _2, _3) \ + (mb_event_t) { .event = _1, .length = _2, .pdata = _3 } +#define EVENT_4(_1, _2, _3, _4) \ + (mb_event_t) { .event = _1, .length = _2, .pdata = _3, .type = _4 } + +typedef struct mb_port_base_t mb_port_base_t; + +typedef struct +{ + mb_port_base_t *mb_base; +} mb_common_iface_t; + +//((mb_port_base_t *)(((mb_common_iface_t *)pctx)->mb_base)->lock); + +#define MB_OBJ_GET_LOCK(pctx) (__extension__( \ +{ \ + assert((pctx)); \ + mb_common_iface_t *iface = (mb_common_iface_t *)pctx; \ + ((_lock_t)((mb_port_base_t *)(iface->mb_base))->lock); \ +})) + +typedef bool (*mb_port_cb_fp)(void *arg); + +//!< port callback table for interrupts +typedef struct +{ + mb_port_cb_fp byte_rcvd; + mb_port_cb_fp tx_empty; + mb_port_cb_fp tmr_expired; +} mb_port_cb_t; + +typedef struct mb_port_event_t mb_port_event_t; +typedef struct mb_port_timer_t mb_port_timer_t; +typedef struct _obj_descr obj_descr_t; + +typedef struct _frame_queue_entry +{ + uint16_t tid; /*!< Transaction identifier (TID) for slave */ + uint16_t pid; /*!< Protocol ID field of MBAP frame */ + uint16_t uid; /*!< Slave unit ID (UID) field for MBAP frame */ + uint8_t *pbuf; /*!< Points to the buffer for the frame */ + uint16_t len; /*!< Length of the frame in the buffer */ + bool check; /*!< Checked flag */ +} frame_entry_t; + +struct mb_port_base_t +{ + obj_descr_t descr; + _lock_t lock; + mb_port_cb_t cb; //!< Port callbacks. + void *arg; //!< CB arg pointer. + + mb_port_event_t *event_obj; + mb_port_timer_t *timer_obj; +}; + +// Port event functions +mb_err_enum_t mb_port_event_create(mb_port_base_t *port_obj); +bool mb_port_event_post(mb_port_base_t *inst, mb_event_t event); +bool mb_port_event_get(mb_port_base_t *inst, mb_event_t *event); +bool mb_port_event_res_take(mb_port_base_t *inst, uint32_t timeout); +void mb_port_event_res_release(mb_port_base_t *inst); +void mb_port_event_set_resp_flag(mb_port_base_t *inst, mb_event_enum_t event_mask); +void mb_port_event_set_err_type(mb_port_base_t *inst, mb_err_event_t event); +mb_err_event_t mb_port_event_get_err_type(mb_port_base_t *inst); +void mb_port_event_delete(mb_port_base_t *inst); +mb_err_enum_t mb_port_event_wait_req_finish(mb_port_base_t *inst); +uint64_t mb_port_get_trans_id(mb_port_base_t *inst); + +// Port timer functions +mb_err_enum_t mb_port_timer_create(mb_port_base_t *inst, uint16_t t35_timer_ticks); +void mb_port_timer_disable(mb_port_base_t *inst); +void mb_port_timer_enable(mb_port_base_t *inst); +void mb_port_timer_respond_timeout_enable(mb_port_base_t *inst); +void mb_port_timer_convert_delay_enable(mb_port_base_t *inst); +void mb_port_set_cur_timer_mode(mb_port_base_t *inst, mb_timer_mode_enum_t tmr_mode); +mb_timer_mode_enum_t mb_port_get_cur_timer_mode(mb_port_base_t *inst); +void mb_port_timer_set_response_time(mb_port_base_t *inst, uint32_t resp_time_ms); +uint32_t mb_port_timer_get_response_time_ms(mb_port_base_t *inst); +void mb_port_timer_delay(mb_port_base_t *inst, uint16_t timeout_ms); +void mb_port_timer_delete(mb_port_base_t *inst); + +// Common functions to track instance descriptors +void mb_port_set_inst_counter(uint32_t inst_counter); +uint32_t mb_port_get_inst_counter(); +uint32_t mb_port_get_inst_counter_inc(); +uint32_t mb_port_get_inst_counter_dec(); + +// Common queue functions +QueueHandle_t queue_create(int queue_size); +void queue_delete(QueueHandle_t queue); +void queue_flush(QueueHandle_t queue); +bool queue_is_empty(QueueHandle_t queue); +esp_err_t queue_push(QueueHandle_t queue, void *pbuf, size_t len, frame_entry_t *pframe); +ssize_t queue_pop(QueueHandle_t queue, void *pbuf, size_t len, frame_entry_t *pframe); + + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_ports/common/port_event.c b/modbus/mb_ports/common/port_event.c new file mode 100644 index 0000000..8662404 --- /dev/null +++ b/modbus/mb_ports/common/port_event.c @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "esp_attr.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "sdkconfig.h" + +#include "port_common.h" +#include "mb_common.h" + +static const char *TAG = "mb_port.event"; + +struct mb_port_event_t +{ + _Atomic mb_err_event_t curr_err_type; + SemaphoreHandle_t resource_hdl; + EventGroupHandle_t event_group_hdl; + QueueHandle_t event_hdl; + _Atomic uint64_t curr_trans_id; +}; + +mb_err_enum_t mb_port_event_create(mb_port_base_t *inst) +{ + mb_port_event_t *pevent = NULL; + mb_err_enum_t ret = MB_EILLSTATE; + MB_RETURN_ON_FALSE((inst), MB_EILLSTATE, TAG, "mb event creation error."); + pevent = (mb_port_event_t *)calloc(1, sizeof(mb_port_event_t)); + MB_RETURN_ON_FALSE((pevent), MB_EILLSTATE, TAG, "mb event creation error."); + // Create modbus semaphore (mb resource). + pevent->resource_hdl = xSemaphoreCreateBinary(); + MB_GOTO_ON_FALSE((pevent->resource_hdl), MB_EILLSTATE, error, TAG, + "%s, mb resource create failure.", inst->descr.parent_name); + pevent->event_group_hdl = xEventGroupCreate(); + MB_GOTO_ON_FALSE((pevent->event_group_hdl), MB_EILLSTATE, error, TAG, + "%s, event group create error.", inst->descr.parent_name); + pevent->event_hdl = xQueueCreate(MB_EVENT_QUEUE_SIZE, sizeof(mb_event_t)); + MB_GOTO_ON_FALSE((pevent->event_hdl), MB_EILLSTATE, error, TAG, "%s, event queue create error.", inst->descr.parent_name); + vQueueAddToRegistry(pevent->event_hdl, TAG); + inst->event_obj = pevent; + atomic_init(&pevent->curr_err_type, EV_ERROR_INIT); + ESP_LOGD(TAG, "initialized object @%p", pevent); + return MB_ENOERR; + +error: + if(pevent->event_hdl) { + vQueueDelete(pevent->event_hdl); + pevent->event_hdl = NULL; + } + if (pevent->event_group_hdl) { + vEventGroupDelete(pevent->event_group_hdl); + pevent->event_group_hdl = NULL; + } + if (pevent->resource_hdl) { + vSemaphoreDelete(pevent->resource_hdl); + pevent->resource_hdl = NULL; + } + free(inst->event_obj); + inst->event_obj = NULL; + return ret; +} + +inline void mb_port_event_set_err_type(mb_port_base_t *inst, mb_err_event_t event) +{ + MB_RETURN_ON_FALSE((inst && inst->event_obj), ;, TAG, "incorrect object handle."); + atomic_store(&(inst->event_obj->curr_err_type), event); +} + +inline mb_err_event_t mb_port_event_get_err_type(mb_port_base_t *inst) +{ + MB_RETURN_ON_FALSE((inst && inst->event_obj), EV_ERROR_INIT, TAG, "incorrect object handle."); + return atomic_load(&inst->event_obj->curr_err_type); +} + +uint64_t mb_port_get_trans_id(mb_port_base_t *inst) +{ + MB_RETURN_ON_FALSE((inst && inst->event_obj), 0, TAG, "incorrect object handle."); + return atomic_load(&(inst->event_obj->curr_trans_id)); +} + +bool mb_port_event_post(mb_port_base_t *inst, mb_event_t event) +{ + MB_RETURN_ON_FALSE((inst), false, TAG, "incorrect object handle for transaction %" PRIu64, event.trans_id); + MB_RETURN_ON_FALSE((inst->event_obj && inst->event_obj->event_hdl), false, TAG, + "Wrong event handle for transaction: %" PRIu64" %d, %p, %s.", + event.trans_id, (int)(event.event), inst, inst->descr.parent_name); + BaseType_t result = pdFALSE; + mb_event_t temp_event; + temp_event = event; + temp_event.post_ts = esp_timer_get_time(); + + if (event.event & EV_TRANS_START) { + atomic_store(&(inst->event_obj->curr_trans_id), temp_event.post_ts); + } + temp_event.event = (event.event & ~EV_TRANS_START); + + if (xPortInIsrContext()) { + BaseType_t high_prio_task_woken = pdFALSE; + result = xQueueSendFromISR(inst->event_obj->event_hdl, + (const void*)&temp_event, &high_prio_task_woken); + // Was the message posted successfully? + if (result == pdPASS) { + // If high_prio_task_woken is now set to pdTRUE + // then a context switch should be requested. + if (high_prio_task_woken) { + portYIELD_FROM_ISR(); + } + return true; + } else { + ESP_EARLY_LOGV(TAG, "%s, post message %x failure .", inst->descr.parent_name, temp_event.event); + return false; + } + if (high_prio_task_woken) { + portYIELD_FROM_ISR(); + } + } else { + result = xQueueSend(inst->event_obj->event_hdl, (const void*)&temp_event, MB_EVENT_QUEUE_TIMEOUT_MAX); + if (result != pdTRUE) { + xQueueReset(inst->event_obj->event_hdl); + ESP_LOGE(TAG, "%s, post message failure.", inst->descr.parent_name); + return false; + } + } + return true; +} + +bool mb_port_event_get(mb_port_base_t *inst, mb_event_t *pevent) +{ + MB_RETURN_ON_FALSE((inst && pevent && inst->event_obj && inst->event_obj->event_hdl), false, TAG, + "incorrect object handle."); + bool event_happened = false; + + if (xQueueReceive(inst->event_obj->event_hdl, pevent, MB_EVENT_QUEUE_TIMEOUT_MAX) == pdTRUE) { + pevent->trans_id = atomic_load(&inst->event_obj->curr_trans_id); + pevent->get_ts = esp_timer_get_time(); + event_happened = true; + } else { + ESP_LOGD(TAG, "%s, get event timeout.", inst->descr.parent_name); + } + return event_happened; +} + +bool mb_port_event_res_take(mb_port_base_t *inst, uint32_t timeout) +{ + MB_RETURN_ON_FALSE((inst && inst->event_obj && inst->event_obj->resource_hdl), false, TAG, + "incorrect object handle."); + BaseType_t status = pdTRUE; + status = xSemaphoreTake(inst->event_obj->resource_hdl, timeout); + ESP_LOGD(TAG, "%s, mb take resource, (%" PRIu32 " ticks).", inst->descr.parent_name, timeout); + return status; +} + +void mb_port_event_res_release(mb_port_base_t *inst) +{ + MB_RETURN_ON_FALSE((inst && inst->event_obj && inst->event_obj->resource_hdl), ;, TAG, + "incorrect object handle."); + BaseType_t status = pdFALSE; + status = xSemaphoreGive(inst->event_obj->resource_hdl); + if (status != pdTRUE) { + ESP_LOGD(TAG, "%s, mb resource release.", inst->descr.parent_name); + } +} + +void mb_port_event_set_resp_flag(mb_port_base_t *inst, mb_event_enum_t event_mask) +{ + MB_RETURN_ON_FALSE((inst), ;, TAG, "incorrect object handle."); + (void)xEventGroupSetBits(inst->event_obj->event_group_hdl, event_mask); +} + +mb_err_enum_t mb_port_event_wait_req_finish(mb_port_base_t *inst) +{ + MB_RETURN_ON_FALSE((inst), MB_EINVAL, TAG, + "incorrect object handle."); + mb_err_enum_t err_status = MB_ENOERR; + mb_event_enum_t rcv_event; + EventBits_t bits = xEventGroupWaitBits(inst->event_obj->event_group_hdl, // The event group being tested. + MB_EVENT_REQ_MASK, // The bits within the event group to wait for. + pdTRUE, // Masked bits should be cleared before returning. + pdFALSE, // Don't wait for both bits, either bit will do. + MB_EVENT_QUEUE_TIMEOUT_MAX); // Wait forever for either bit to be set. + rcv_event = (mb_event_enum_t)(bits); + if (rcv_event) { + ESP_LOGD(TAG, "%s, %s: returned event = 0x%x", inst->descr.parent_name, __func__, (int)rcv_event); + if (!(rcv_event & MB_EVENT_REQ_MASK)) { + // if we wait for certain event bits but get from poll subset + ESP_LOGE(TAG, "%s, %s: incorrect event set = 0x%x", inst->descr.parent_name, __func__, (int)rcv_event); + } + if (MB_PORT_CHECK_EVENT(rcv_event, EV_MASTER_PROCESS_SUCCESS)) { + err_status = MB_ENOERR; + } else if (MB_PORT_CHECK_EVENT(rcv_event, EV_MASTER_ERROR_RESPOND_TIMEOUT)) { + err_status = MB_ETIMEDOUT; + } else if (MB_PORT_CHECK_EVENT(rcv_event, EV_MASTER_ERROR_RECEIVE_DATA)) { + err_status = MB_ERECVDATA; + } else if (MB_PORT_CHECK_EVENT(rcv_event, EV_MASTER_ERROR_EXECUTE_FUNCTION)) { + err_status = MB_EILLFUNC; + } + } else { + ESP_LOGE(TAG, "%s, %s: incorrect event or timeout, rcv_event = 0x%x", inst->descr.parent_name, __func__, (int)bits); + err_status = MB_ETIMEDOUT; + } + return err_status; +} + +void mb_port_event_delete(mb_port_base_t *inst) +{ + MB_RETURN_ON_FALSE((inst), ;, TAG, + "incorrect event object handle."); + if (inst->event_obj->resource_hdl) { + vSemaphoreDelete(inst->event_obj->resource_hdl); + } + if (inst->event_obj->event_group_hdl) { + vEventGroupDelete(inst->event_obj->event_group_hdl); + } + if(inst->event_obj->event_hdl) { + vQueueDelete(inst->event_obj->event_hdl); + inst->event_obj->event_hdl = NULL; + } + free(inst->event_obj); + inst->event_obj = NULL; +} diff --git a/modbus/mb_ports/common/port_other.c b/modbus/mb_ports/common/port_other.c new file mode 100644 index 0000000..ea43576 --- /dev/null +++ b/modbus/mb_ports/common/port_other.c @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "sys/lock.h" + +#include "port_common.h" + +/* ----------------------- Variables ----------------------------------------*/ +static _Atomic uint32_t inst_counter = 0; + +/* ----------------------- Start implementation -----------------------------*/ +int lock_obj(_lock_t *plock) +{ + _lock_acquire(plock); + return 1; +} + +void unlock_obj(_lock_t *plock) +{ + _lock_release(plock); +} + +__attribute__((unused)) +void mb_port_set_inst_counter(uint32_t counter) +{ + atomic_store(&inst_counter, counter); +} + +__attribute__((unused)) +uint32_t mb_port_get_inst_counter() +{ + return atomic_load(&inst_counter); +} + +uint32_t mb_port_get_inst_counter_inc() +{ + uint32_t counter = atomic_load(&inst_counter); + atomic_store(&inst_counter, (counter + 1)); + return counter; +} + +uint32_t mb_port_get_inst_counter_dec() +{ + uint32_t counter = atomic_load(&inst_counter); + atomic_store(&inst_counter, (counter - 1)); + return counter; +} + +QueueHandle_t queue_create(int queue_size) +{ + return xQueueCreate(queue_size, sizeof(frame_entry_t)); +} + +void queue_delete(QueueHandle_t queue) +{ + queue_flush(queue); + vQueueDelete(queue); +} + +esp_err_t queue_push(QueueHandle_t queue, void *pbuf, size_t len, frame_entry_t *pframe) +{ + frame_entry_t frame_info = {0}; + + if (!queue) { // || !pbuf || (len <= 0) + return -1; + } + + if (pframe) { + frame_info = *pframe; + } + + if (pbuf && (len > 0)) { + if (!frame_info.pbuf) { + frame_info.pbuf = calloc(1, len); + } + if (!frame_info.pbuf) { + return ESP_ERR_NO_MEM; + } + frame_info.len = len; + memcpy(frame_info.pbuf, pbuf, len); + } + + // try send to queue and check if the queue is full + if (xQueueSend(queue, &frame_info, portMAX_DELAY) != pdTRUE) { + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +ssize_t queue_pop(QueueHandle_t queue, void *pbuf, size_t len, frame_entry_t *pframe) +{ + TickType_t timeout = portMAX_DELAY; + + frame_entry_t frame_info = {0}; + + if (xQueueReceive(queue, &frame_info, timeout) == pdTRUE) { + if (pframe) { + *pframe = frame_info; + } + if (len > frame_info.len) { + len = frame_info.len; + } + // if the input buffer pointer is defined copy the data and free queued buffer, + // otherwise just return the frame entry + if (frame_info.pbuf && pbuf) { + memcpy(pbuf, frame_info.pbuf, len); + if (!pframe) { + free(frame_info.pbuf); // must free the buffer manually! + } + } + } else { + goto err; + } + return len; +err: + return -1; +} + +bool queue_is_empty(QueueHandle_t queue) +{ + return (uxQueueMessagesWaiting(queue) == 0); +} + +void queue_flush(QueueHandle_t queue) +{ + frame_entry_t frame_info; + while (xQueueReceive(queue, &frame_info, 0) == pdTRUE) { + if ((frame_info.len > 0) && frame_info.pbuf) { + free(frame_info.pbuf); + } + } +} diff --git a/modbus/mb_ports/common/port_timer.c b/modbus/mb_ports/common/port_timer.c new file mode 100644 index 0000000..836184c --- /dev/null +++ b/modbus/mb_ports/common/port_timer.c @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/*----------------------- Platform includes --------------------------------*/ +#include +#include "esp_idf_version.h" +#include "esp_attr.h" + +#if __has_include("driver/gptimer.h") +#include "driver/gptimer.h" +#else +#include "driver/timer.h" +#endif + +#include "esp_timer.h" +#include "esp_log.h" + +#include "port_common.h" +#include "mb_types.h" +#include "mb_config.h" +#include "mb_common.h" + +/* ----------------------- Defines ----------------------------------------*/ +struct mb_port_timer_t +{ + //spinlock_t spin_lock; + esp_timer_handle_t timer_handle; + uint16_t t35_ticks; + _Atomic uint32_t response_time_ms; + _Atomic bool timer_state; + _Atomic uint16_t timer_mode; +}; + +/* ----------------------- Static variables ---------------------------------*/ +static const char *TAG = "mb_port.timer"; + +/* ----------------------- Start implementation -----------------------------*/ +mb_timer_mode_enum_t mb_port_get_cur_timer_mode(mb_port_base_t *inst); + +static void IRAM_ATTR timer_alarm_cb(void *param) +{ + mb_port_base_t *inst = (mb_port_base_t *)param; + if (inst->cb.tmr_expired && inst->arg) { + inst->cb.tmr_expired(inst->arg); // Timer expired callback function + } + atomic_store(&(inst->timer_obj->timer_state), true); + ESP_EARLY_LOGD(TAG, "timer mode: (%d) triggered", mb_port_get_cur_timer_mode(inst)); +} + +mb_err_enum_t mb_port_timer_create(mb_port_base_t *inst, uint16_t t35_timer_ticks) +{ + MB_RETURN_ON_FALSE((t35_timer_ticks > 0), MB_EILLSTATE, TAG, + "modbus timeout discreet is incorrect."); + // MB_RETURN_ON_FALSE((inst && !inst->timer_obj), MB_EILLSTATE, TAG, + // "modbus timer is already created."); + mb_err_enum_t ret = MB_EILLSTATE; + inst->timer_obj = (mb_port_timer_t *)calloc(1, sizeof(mb_port_timer_t)); + MB_GOTO_ON_FALSE((inst && inst->timer_obj), MB_EILLSTATE, error, TAG, "mb timer allocation error."); + inst->timer_obj->timer_handle = NULL; + atomic_init(&(inst->timer_obj->timer_mode), MB_TMODE_T35); + atomic_init(&(inst->timer_obj->timer_state), false); + // Set default response time according to kconfig + atomic_init(&(inst->timer_obj->response_time_ms), MB_MASTER_TIMEOUT_MS_RESPOND); + // Save timer reload value for Modbus T35 period + inst->timer_obj->t35_ticks = t35_timer_ticks; + esp_timer_create_args_t timer_conf = { + .callback = timer_alarm_cb, + .arg = inst, +#if (MB_TIMER_SUPPORTS_ISR_DISPATCH_METHOD && MB_TIMER_USE_ISR_DISPATCH_METHOD) + .dispatch_method = ESP_TIMER_ISR, +#else + .dispatch_method = ESP_TIMER_TASK, +#endif + .name = "MB_T35timer" + }; + // Create Modbus timer + esp_err_t err = esp_timer_create(&timer_conf, &(inst->timer_obj->timer_handle)); + MB_GOTO_ON_FALSE((err == ESP_OK), MB_EILLSTATE, error, TAG, "mb timer creation error."); + ESP_LOGD(TAG, "initialized %s object @%p", TAG, inst->timer_obj); + return MB_ENOERR; + +error: + if (inst && inst->timer_obj && inst->timer_obj->timer_handle) + { + esp_timer_delete(inst->timer_obj->timer_handle); + } + free(inst->timer_obj); + inst->timer_obj = NULL; + return ret; +} + +void mb_port_timer_delete(mb_port_base_t *inst) +{ + // Delete active timer + if (inst->timer_obj) + { + if (inst->timer_obj->timer_handle) + { + esp_timer_stop(inst->timer_obj->timer_handle); + esp_timer_delete(inst->timer_obj->timer_handle); + } + free(inst->timer_obj); + inst->timer_obj = NULL; + } +} + +void mb_port_timer_us(mb_port_base_t *inst, uint64_t timeout_us) +{ + MB_RETURN_ON_FALSE((inst && inst->timer_obj->timer_handle), ;, TAG, "timer is not initialized."); + MB_RETURN_ON_FALSE((timeout_us > 0), ;, TAG, + "%s, incorrect tick value for timer = (%" PRId64 ").", inst->descr.parent_name, timeout_us); + esp_timer_stop(inst->timer_obj->timer_handle); + esp_timer_start_once(inst->timer_obj->timer_handle, timeout_us); + atomic_store(&(inst->timer_obj->timer_state), false); +} + + +inline void mb_port_set_cur_timer_mode(mb_port_base_t *inst, mb_timer_mode_enum_t tmr_mode) +{ + atomic_store(&(inst->timer_obj->timer_mode), tmr_mode); +} + +inline mb_timer_mode_enum_t mb_port_get_cur_timer_mode(mb_port_base_t *inst) +{ + return atomic_load(&(inst->timer_obj->timer_mode)); +} + +void mb_port_timer_enable(mb_port_base_t *inst) +{ + uint64_t tout_us = (inst->timer_obj->t35_ticks * MB_TIMER_TICK_TIME_US); + + // Set current timer mode, don't change it. + mb_port_set_cur_timer_mode(inst, MB_TMODE_T35); + // Set timer alarm + mb_port_timer_us(inst, tout_us); + ESP_LOGD(TAG, "%s, start timer (%" PRIu64 ").", inst->descr.parent_name, tout_us); +} + +void mb_port_timer_convert_delay_enable(mb_port_base_t *inst) +{ + // Covert time in milliseconds into ticks + uint64_t tout_us = (MB_MASTER_DELAY_MS_CONVERT * 1000); + + // Set current timer mode + mb_port_set_cur_timer_mode(inst, MB_TMODE_CONVERT_DELAY); + ESP_LOGD(TAG, "%s, convert delay enable.", inst->descr.parent_name); + mb_port_timer_us(inst, tout_us); +} + +void mb_port_timer_respond_timeout_enable(mb_port_base_t *inst) +{ + uint64_t tout_us = (inst->timer_obj->response_time_ms * 1000); + + mb_port_set_cur_timer_mode(inst, MB_TMODE_RESPOND_TIMEOUT); + ESP_LOGD(TAG, "%s, respond enable timeout (%u).", + inst->descr.parent_name, (unsigned)mb_port_timer_get_response_time_ms(inst)); + mb_port_timer_us(inst, tout_us); +} + +void mb_port_timer_delay(mb_port_base_t *inst, uint16_t timeout_ms) +{ + uint64_t tout_us = (timeout_ms * 1000); + mb_port_timer_us(inst, tout_us); +} + +void mb_port_timer_disable(mb_port_base_t *inst) +{ + // Disable timer alarm + esp_err_t err = esp_timer_stop(inst->timer_obj->timer_handle); + if (err != ESP_OK) + { + if (!esp_timer_is_active(inst->timer_obj->timer_handle)) + { + ESP_EARLY_LOGD(TAG, "%s, timer stop, returns %d.", inst->descr.parent_name, (int)err); + } + } +} + +void mb_port_timer_set_response_time(mb_port_base_t *inst, uint32_t resp_time_ms) +{ + atomic_store(&(inst->timer_obj->response_time_ms), resp_time_ms); +} + +uint32_t mb_port_timer_get_response_time_ms(mb_port_base_t *inst) +{ + return atomic_load(&(inst->timer_obj->response_time_ms)); +} diff --git a/modbus/mb_ports/serial/port_serial.c b/modbus/mb_ports/serial/port_serial.c new file mode 100644 index 0000000..c6be567 --- /dev/null +++ b/modbus/mb_ports/serial/port_serial.c @@ -0,0 +1,349 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "esp_timer.h" +#include "mb_common.h" +#include "port_common.h" +#include "mb_config.h" +#include "port_serial_common.h" + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_SERIAL_RX_SEMA_TOUT_MS (1000) +#define MB_SERIAL_RX_SEMA_TOUT (pdMS_TO_TICKS(MB_SERIAL_RX_SEMA_TOUT_MS)) +#define MB_SERIAL_RX_FLUSH_RETRY (2) +#define MB_QUEUE_LENGTH (20) +#define MB_SERIAL_TOUT (3) +#define MB_SERIAL_TX_TOUT_TICKS (pdMS_TO_TICKS(400)) +#define MB_SERIAL_TASK_STACK_SIZE (CONFIG_FMB_PORT_TASK_STACK_SIZE) +#define MB_SERIAL_RX_TOUT_TICKS (pdMS_TO_TICKS(100)) + +#define MB_SERIAL_MIN_PATTERN_INTERVAL (9) +#define MB_SERIAL_MIN_POST_IDLE (0) +#define MB_SERIAL_MIN_PRE_IDLE (0) + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +typedef struct +{ + mb_port_base_t base; + // serial communication properties + mb_serial_opts_t ser_opts; + bool rx_state_en; + bool tx_state_en; + uint16_t recv_length; + uint64_t send_time_stamp; + uint64_t recv_time_stamp; + uint32_t flags; + bool enabled; + QueueHandle_t uart_queue; // A queue to handle UART event. + TaskHandle_t task_handle; // UART task to handle UART event. + SemaphoreHandle_t bus_sema_handle; // Rx blocking semaphore handle +} mb_ser_port_t; + +/* ----------------------- Static variables & functions ----------------------*/ +static const char *TAG = "mb_port.serial"; + +static bool mb_port_ser_bus_sema_init(mb_port_base_t *inst) +{ + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + port_obj->bus_sema_handle = xSemaphoreCreateBinary(); + MB_RETURN_ON_FALSE((port_obj->bus_sema_handle), false , TAG, + "%s: RX semaphore create failure.", inst->descr.parent_name); + return true; +} + +static void mb_port_ser_bus_sema_close(mb_port_base_t *inst) +{ + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + if (port_obj->bus_sema_handle) { + vSemaphoreDelete(port_obj->bus_sema_handle); + port_obj->bus_sema_handle = NULL; + } +} + +static bool mb_port_ser_bus_sema_take(mb_port_base_t *inst, uint32_t tm_ticks) +{ + BaseType_t status = pdTRUE; + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + status = xSemaphoreTake(port_obj->bus_sema_handle, tm_ticks ); + MB_RETURN_ON_FALSE((status == pdTRUE), false , TAG, + "%s, rx semaphore take failure.", inst->descr.parent_name); + ESP_LOGV(TAG, "%s: take RX semaphore (%" PRIu32" ticks).", inst->descr.parent_name, tm_ticks); + return true; +} + +static void mb_port_ser_bus_sema_release(mb_port_base_t *inst) +{ + BaseType_t status = pdFALSE; + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + status = xSemaphoreGive(port_obj->bus_sema_handle); + if (status != pdTRUE) { + ESP_LOGD(TAG, "%s, rx semaphore is free.", inst->descr.parent_name); + } +} + +static bool mb_port_ser_bus_sema_is_busy(mb_port_base_t *inst) +{ + BaseType_t status = pdFALSE; + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + status = (uxSemaphoreGetCount(port_obj->bus_sema_handle) == 0) ? true : false; + return status; +} + +static void mb_port_ser_rx_flush(mb_port_base_t *inst) +{ + size_t size = 1; + esp_err_t err = ESP_OK; + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + for (int cnt = 0; (cnt < MB_SERIAL_RX_FLUSH_RETRY) && size; cnt++) { + err = uart_get_buffered_data_len(port_obj->ser_opts.port, &size); + MB_RETURN_ON_FALSE((err == ESP_OK), ; , TAG, + "%s, mb flush serial fail, error = 0x%x.", inst->descr.parent_name, (int)err); + BaseType_t status = xQueueReset(port_obj->uart_queue); + if (status) { + err = uart_flush_input(port_obj->ser_opts.port); + MB_RETURN_ON_FALSE((err == ESP_OK), ; , TAG, + "%s, mb flush serial fail, error = 0x%x.", inst->descr.parent_name, (int)err); + } + } +} + +void mb_port_ser_enable(mb_port_base_t *inst) +{ + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + CRITICAL_SECTION (port_obj->base.lock) { + atomic_store(&(port_obj->enabled), true); + mb_port_ser_bus_sema_release(inst); + ESP_LOGD(TAG, "%s, resume port.", port_obj->base.descr.parent_name); + // Resume receiver task from known position + vTaskResume(port_obj->task_handle); + } +} + +void mb_port_ser_disable(mb_port_base_t *inst) +{ + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + CRITICAL_SECTION (port_obj->base.lock) { + // Suspend port task by itself + atomic_store(&(port_obj->enabled), false); + ESP_LOGD(TAG, "%s, suspend port.", port_obj->base.descr.parent_name); + } +} + +// UART receive event task +static void mb_port_ser_task(void *p_args) +{ + mb_ser_port_t *port_obj = __containerof(p_args, mb_ser_port_t, base); + uart_event_t event; + MB_RETURN_ON_FALSE(port_obj, ;, TAG, "%s, get serial instance fail.", port_obj->base.descr.parent_name); + (void)mb_port_ser_rx_flush(&port_obj->base); + while(1) { + // Workaround to suspend task from known place to avoid dead lock when resume + if (!atomic_load(&(port_obj->enabled))) { + ESP_LOGI(TAG, "%s, suspend port from task.", port_obj->base.descr.parent_name); + vTaskSuspend(NULL); + } + if (xQueueReceive(port_obj->uart_queue, (void *)&event, MB_SERIAL_RX_TOUT_TICKS)) { + ESP_LOGD(TAG, "%s, UART[%d] event:", port_obj->base.descr.parent_name, port_obj->ser_opts.port); + switch(event.type) { + case UART_DATA: + ESP_LOGD(TAG, "%s, data event, len: %d.", port_obj->base.descr.parent_name, (int)event.size); + // This flag set in the event means that no more + // data received during configured timeout and UART TOUT feature is triggered + if (event.timeout_flag) { + // If bus is busy or fragmented data is received, then flush buffer + if (mb_port_ser_bus_sema_is_busy(&port_obj->base)) { + mb_port_ser_rx_flush(&port_obj->base); + break; + } + uart_get_buffered_data_len(port_obj->ser_opts.port, (unsigned int*)&event.size); + port_obj->recv_length = (event.size < MB_BUFFER_SIZE) ? event.size : MB_BUFFER_SIZE; + if (event.size <= MB_SER_PDU_SIZE_MIN) { + ESP_LOGD(TAG, "%s, drop short packet %d byte(s)", port_obj->base.descr.parent_name, (int)event.size); + (void)mb_port_ser_rx_flush(&port_obj->base); + break; + } + // New frame is received, send an event to main FSM to read it into receiver buffer + mb_port_event_post(&port_obj->base, EVENT(EV_FRAME_RECEIVED, port_obj->recv_length, NULL, 0)); + ESP_LOGD(TAG, "%s, frame %d bytes is ready.", port_obj->base.descr.parent_name, (int)port_obj->recv_length); + } + break; + //Event of HW FIFO overflow detected + case UART_FIFO_OVF: + ESP_LOGD(TAG, "%s, hw fifo overflow.", port_obj->base.descr.parent_name); + xQueueReset(port_obj->uart_queue); + break; + //Event of UART ring buffer full + case UART_BUFFER_FULL: + ESP_LOGD(TAG, "%s, ring buffer full.", port_obj->base.descr.parent_name); + (void)mb_port_ser_rx_flush(&port_obj->base); + break; + //Event of UART RX break detected + case UART_BREAK: + ESP_LOGD(TAG, "%s, uart rx break.", port_obj->base.descr.parent_name); + break; + //Event of UART parity check error + case UART_PARITY_ERR: + ESP_LOGD(TAG, "%s, uart parity error.", port_obj->base.descr.parent_name); + (void)mb_port_ser_rx_flush(&port_obj->base); + break; + //Event of UART frame error + case UART_FRAME_ERR: + ESP_LOGD(TAG, "%s, uart frame error.", port_obj->base.descr.parent_name); + (void)mb_port_ser_rx_flush(&port_obj->base); + break; + default: + ESP_LOGD(TAG, "%s, uart event type: %d.", port_obj->base.descr.parent_name, (int)event.type); + break; + } + } + } + vTaskDelete(NULL); +} + +mb_err_enum_t mb_port_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj) +{ + mb_ser_port_t *pserial = NULL; + esp_err_t err = ESP_OK; + __attribute__((unused)) mb_err_enum_t ret = MB_EILLSTATE; + pserial = (mb_ser_port_t*)calloc(1, sizeof(mb_ser_port_t)); + MB_RETURN_ON_FALSE((pserial && in_out_obj), MB_EILLSTATE, TAG, "mb serial port creation error."); + CRITICAL_SECTION_INIT(pserial->base.lock); + pserial->base.descr = ((mb_port_base_t*)*in_out_obj)->descr; + ser_opts->data_bits = ((ser_opts->data_bits > UART_DATA_5_BITS) + && (ser_opts->data_bits < UART_DATA_BITS_MAX)) + ? ser_opts->data_bits : UART_DATA_8_BITS; + // Keep the UART communication options + pserial->ser_opts = *ser_opts; + // Configure serial communication parameters + uart_config_t uart_cfg = { + .baud_rate = ser_opts->baudrate, + .data_bits = ser_opts->data_bits, + .parity = ser_opts->parity, + .stop_bits = ser_opts->stop_bits, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 2, +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) + .source_clk = UART_SCLK_DEFAULT, +#else + .source_clk = UART_SCLK_APB, +#endif + }; + // Set UART config + err = uart_param_config(pserial->ser_opts.port, &uart_cfg); + MB_GOTO_ON_FALSE((err == ESP_OK), MB_EILLSTATE, error, TAG, + "%s, mb config failure, uart_param_config() returned (0x%x).", pserial->base.descr.parent_name, (int)err); + // Install UART driver, and get the queue. + err = uart_driver_install(pserial->ser_opts.port, MB_BUFFER_SIZE, MB_BUFFER_SIZE, + MB_QUEUE_LENGTH, &pserial->uart_queue, MB_PORT_SERIAL_ISR_FLAG); + MB_GOTO_ON_FALSE((err == ESP_OK), MB_EILLSTATE, error, TAG, + "%s, mb serial driver failure, retuned (0x%x).", pserial->base.descr.parent_name, (int)err); + err = uart_set_rx_timeout(pserial->ser_opts.port, MB_SERIAL_TOUT); + MB_GOTO_ON_FALSE((err == ESP_OK), MB_EILLSTATE, error, TAG, + "%s, mb serial set rx timeout failure, returned (0x%x).", pserial->base.descr.parent_name, (int)err); + // Set always timeout flag to trigger timeout interrupt even after rx fifo full + uart_set_always_rx_timeout(pserial->ser_opts.port, true); + MB_GOTO_ON_FALSE((mb_port_ser_bus_sema_init(&pserial->base)), MB_EILLSTATE, error, TAG, + "%s, mb serial bus semaphore create fail.", pserial->base.descr.parent_name); + // Suspend task on start and then resume when initialization is completed + atomic_store(&(pserial->enabled), false); + // Create a task to handle UART events + BaseType_t status = xTaskCreatePinnedToCore(mb_port_ser_task, "port_serial_task", + MB_SERIAL_TASK_STACK_SIZE, + &pserial->base, CONFIG_FMB_PORT_TASK_PRIO, + &pserial->task_handle, CONFIG_FMB_PORT_TASK_AFFINITY); + // Force exit from function with failure + MB_GOTO_ON_FALSE((status == pdPASS), MB_EILLSTATE, error, TAG, + "%s, mb stack serial task creation error, returned (0x%x).", + pserial->base.descr.parent_name, (int)status); + *in_out_obj = &(pserial->base); + ESP_LOGD(TAG, "created object @%p", pserial); + return MB_ENOERR; + +error: + if (pserial) { + if (pserial->task_handle) { + vTaskDelete(pserial->task_handle); + } + uart_driver_delete(pserial->ser_opts.port); + CRITICAL_SECTION_CLOSE(pserial->base.lock); + mb_port_ser_bus_sema_close(&pserial->base); + } + free(pserial); + return MB_EILLSTATE; +} + +void mb_port_ser_delete(mb_port_base_t *inst) +{ + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + vTaskDelete(port_obj->task_handle); + ESP_ERROR_CHECK(uart_driver_delete(port_obj->ser_opts.port)); + mb_port_ser_bus_sema_close(inst); + CRITICAL_SECTION_CLOSE(inst->lock); + free(port_obj); +} + +bool mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **pp_ser_frame, uint16_t *p_ser_length) +{ + MB_RETURN_ON_FALSE((pp_ser_frame && p_ser_length), false, TAG, "mb serial get buffer failure."); + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + uint16_t counter = *p_ser_length ? *p_ser_length : port_obj->recv_length; + bool status = false; + + status = mb_port_ser_bus_sema_take(inst, pdMS_TO_TICKS(mb_port_timer_get_response_time_ms(inst))); + if (status && counter && *pp_ser_frame && atomic_load(&(port_obj->enabled))) { + // Read frame data from the ringbuffer of receiver + counter = uart_read_bytes(port_obj->ser_opts.port, (uint8_t *)*pp_ser_frame, + counter, MB_SERIAL_RX_TOUT_TICKS); + // Stop timer because the new data is received + mb_port_timer_disable(inst); + // Store the timestamp of received frame + port_obj->recv_time_stamp = esp_timer_get_time(); + ESP_LOGD(TAG, "%s, received data: %d bytes.", inst->descr.parent_name, (int)counter); + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":PORT_RECV"), (void *)*pp_ser_frame, counter, ESP_LOG_DEBUG); + int64_t time_delta = (port_obj->recv_time_stamp > port_obj->send_time_stamp) ? + (port_obj->recv_time_stamp - port_obj->send_time_stamp) : + (port_obj->send_time_stamp - port_obj->recv_time_stamp); + ESP_LOGD(TAG, "%s, serial processing time[us] = %" PRId64, inst->descr.parent_name, time_delta); + status = true; + *p_ser_length = counter; + } else { + ESP_LOGE(TAG, "%s: junk data (%d bytes) received. ", inst->descr.parent_name, (int)counter); + } + *p_ser_length = counter; + mb_port_ser_bus_sema_release(inst); + return status; +} + +bool mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *p_ser_frame, uint16_t ser_length) +{ + bool res = false; + int count = 0; + mb_ser_port_t *port_obj = __containerof(inst, mb_ser_port_t, base); + + res = mb_port_ser_bus_sema_take(inst, pdMS_TO_TICKS(mb_port_timer_get_response_time_ms(inst))); + if (res && p_ser_frame && ser_length && atomic_load(&(port_obj->enabled))) { + // Flush buffer received from previous transaction + mb_port_ser_rx_flush(inst); + count = uart_write_bytes(port_obj->ser_opts.port, p_ser_frame, ser_length); + // Waits while UART sending the packet + esp_err_t status = uart_wait_tx_done(port_obj->ser_opts.port, MB_SERIAL_TX_TOUT_TICKS); + (void)mb_port_event_post(inst, EVENT(EV_FRAME_SENT)); + ESP_LOGD(TAG, "%s, tx buffer sent: (%d) bytes.", inst->descr.parent_name, (int)count); + MB_RETURN_ON_FALSE((status == ESP_OK), false, TAG, "%s, mb serial sent buffer failure.", + inst->descr.parent_name); + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":PORT_SEND"), + (void *)p_ser_frame, ser_length, ESP_LOG_DEBUG); + port_obj->send_time_stamp = esp_timer_get_time(); + } else { + ESP_LOGE(TAG, "%s, send fail state:%d, %p, %u. ", inst->descr.parent_name, (int)port_obj->tx_state_en, p_ser_frame, (unsigned)ser_length); + } + mb_port_ser_bus_sema_release(inst); + return res; +} + +#endif + diff --git a/modbus/mb_ports/serial/port_serial_common.h b/modbus/mb_ports/serial/port_serial_common.h new file mode 100644 index 0000000..db02a0f --- /dev/null +++ b/modbus/mb_ports/serial/port_serial_common.h @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#include "mb_config.h" +#include "mb_types.h" +#include "mb_frame.h" +#include "mb_port_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +typedef enum _mb_comm_mode mb_mode_type_t; +typedef struct mb_port_base_t mb_port_base_t; + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +mb_err_enum_t mb_port_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **port_obj); +bool mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **pp_ser_frame, uint16_t *p_ser_length); +bool mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *p_ser_frame, uint16_t ser_length); +void mb_port_ser_enable(mb_port_base_t *inst); +void mb_port_ser_disable(mb_port_base_t *inst); +void mb_port_ser_delete(mb_port_base_t *inst); + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_ports/tcp/port_tcp_common.h b/modbus/mb_ports/tcp/port_tcp_common.h new file mode 100644 index 0000000..36b525b --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_common.h @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include +#include "mb_config.h" +#include "mb_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +#define MB_TCP_PORT_MAX_CONN (CONFIG_FMB_TCP_PORT_MAX_CONN) +#define MB_TCP_DEFAULT_PORT (CONFIG_FMB_TCP_PORT_DEFAULT) +#define MB_FRAME_QUEUE_SZ (20) +#define MB_TCP_CONNECTION_TIMEOUT_MS (20) // connection timeout in mS +#define MB_TCP_RECONNECT_TIMEOUT (5000000) // reconnection timeout in uS + +#define MB_EVENT_SEND_RCV_TOUT_MS (500) + +#define MB_TCP_MBAP_GET_FIELD(buffer, field) ((uint16_t)((buffer[field] << 8U) | buffer[field + 1])) +#define MB_TCP_MBAP_SET_FIELD(buffer, field, val) { \ + buffer[(field)] = (uint8_t)((val) >> 8U); \ + buffer[(field) + 1] = (uint8_t)((val) & 0xFF); \ +} + +#define MB_NODE_FMT(fmt) "node #%d, socket(#%d)(%s)" fmt + +mb_err_enum_t mbm_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **port_obj); +void mbm_port_tcp_delete(mb_port_base_t *inst); +void mbm_port_tcp_enable(mb_port_base_t *inst); +void mbm_port_tcp_disable(mb_port_base_t *inst); +bool mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length); +bool mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +bool mbm_port_tcp_add_slave_info(mb_port_base_t *inst, const uint16_t index, const char *ip_str, uint8_t uid); + +mb_err_enum_t mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **port_obj); +void mbs_port_tcp_delete(mb_port_base_t *inst); +void mbs_port_tcp_enable(mb_port_base_t *inst); +void mbs_port_tcp_disable(mb_port_base_t *inst); +bool mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length); +bool mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); + +#endif + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_ports/tcp/port_tcp_driver.c b/modbus/mb_ports/tcp/port_tcp_driver.c new file mode 100644 index 0000000..49a38fb --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_driver.c @@ -0,0 +1,797 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include +#include "errno.h" + +#include "esp_log.h" +#include "esp_check.h" +#include "esp_timer.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" + +#include "esp_netif.h" + +#include "port_common.h" +#include "esp_vfs_eventfd.h" +#include "port_tcp_driver.h" +#include "port_tcp_utils.h" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +static const char *TAG = "mb_driver"; + +static esp_event_loop_handle_t mb_drv_loop_handle = NULL; +static int mb_drv_loop_inst_counter = 0; +static char msg_buffer[100]; // The buffer for event debugging (used for all instances) + +static const event_msg_t event_msg_table[] = { + MB_EVENT_TBL_IT(MB_EVENT_READY), + MB_EVENT_TBL_IT(MB_EVENT_OPEN), + MB_EVENT_TBL_IT(MB_EVENT_RESOLVE), + MB_EVENT_TBL_IT(MB_EVENT_CONNECT), + MB_EVENT_TBL_IT(MB_EVENT_SEND_DATA), + MB_EVENT_TBL_IT(MB_EVENT_RECV_DATA), + MB_EVENT_TBL_IT(MB_EVENT_ERROR), + MB_EVENT_TBL_IT(MB_EVENT_CLOSE), + MB_EVENT_TBL_IT(MB_EVENT_TIMEOUT), +}; + +// The function to print event +const char *driver_event_to_name_r(mb_driver_event_t event) +{ + msg_buffer[0] = 0; + size_t i; + for (i = 0; i < sizeof(event_msg_table) / sizeof(event_msg_table[0]); ++i) { + if (event_msg_table[i].event & event) { + strlcat(msg_buffer, "|", sizeof(msg_buffer)); + strlcat(msg_buffer, event_msg_table[i].msg, sizeof(msg_buffer)); + } + } + return msg_buffer; +} + +static esp_err_t init_event_fd(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + if (!mb_drv_loop_inst_counter) { + esp_vfs_eventfd_config_t config = MB_EVENTFD_CONFIG(); + esp_err_t err = esp_vfs_eventfd_register(&config); + if ((err != ESP_OK) && (err != ESP_ERR_INVALID_STATE)) { + ESP_LOGE(TAG, "eventfd registration fail."); + } + } + pdrv_ctx->event_fd = eventfd(0, 0); + MB_RETURN_ON_FALSE((pdrv_ctx->event_fd > 0), ESP_ERR_INVALID_STATE, TAG, "eventfd init error."); + return (pdrv_ctx->event_fd > 0) ? ESP_OK : ESP_ERR_INVALID_STATE; +} + +static esp_err_t close_event_fd(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + if (mb_drv_loop_inst_counter) { + close(pdrv_ctx->event_fd); + } else { + ESP_LOGD(TAG, "close eventfd (%d).", (int)pdrv_ctx->event_fd); + return esp_vfs_eventfd_unregister(); + } + return ESP_OK; +} + +int32_t write_event(void *ctx, mb_event_info_t *pevent) +{ + MB_RETURN_ON_FALSE((pevent && ctx), -1, TAG, "wrong arguments."); + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t err = esp_event_post_to(mb_drv_loop_handle, + MB_EVENT_BASE(ctx), (int32_t)pevent->event_id, pevent, + sizeof(mb_event_info_t), MB_EVENT_TOUT); + if ((err != ESP_OK)) { + ESP_LOGE(TAG, "%p, event loop send fail, err = %d.", ctx, (int)err); + return -1; + } + // send eventfd to just trigger select + int32_t ret = write(pdrv_ctx->event_fd, (char *)&pevent->val, sizeof(mb_event_info_t)); + return (ret == sizeof(mb_event_info_t)) ? pevent->event_id : -1; +} + +static int32_t read_event(void *ctx, mb_event_info_t *pevent) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + MB_RETURN_ON_FALSE(pevent, ESP_ERR_INVALID_STATE, TAG, "cannot get event."); + int ret = read(pdrv_ctx->event_fd, (char *)&pevent->val, sizeof(mb_event_info_t)); + return (ret == sizeof(mb_event_info_t)) ? pevent->event_id : -1; +} + +static esp_err_t mb_drv_event_loop_init(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t err = ESP_OK; + /* Create Event loop without task (will be created separately)*/ + esp_event_loop_args_t loop_args = { + .queue_size = MB_EVENT_QUEUE_SZ, + .task_name = NULL + }; + if (!mb_drv_loop_handle && !mb_drv_loop_inst_counter) { + err = esp_event_loop_create(&loop_args, &mb_drv_loop_handle); + MB_RETURN_ON_FALSE(((err == ESP_OK) && mb_drv_loop_handle), ESP_ERR_INVALID_STATE, + TAG, "create event loop failed, err=%d.", (int)err); + } + pdrv_ctx->event_loop_hdl = mb_drv_loop_handle; + if (asprintf(&pdrv_ctx->loop_name, "loop:%p", ctx) == -1) { + abort(); + } + return err; +} + +static esp_err_t mb_drv_event_loop_deinit(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t err = ESP_OK; + // delete event loop + MB_RETURN_ON_FALSE((mb_drv_loop_handle), ESP_ERR_INVALID_STATE, + TAG, "delete event loop failed."); + if (mb_drv_loop_inst_counter) { + ESP_LOGD(TAG, "delete loop inst: %s.", pdrv_ctx->loop_name); + free(pdrv_ctx->loop_name); + pdrv_ctx->loop_name = NULL; + mb_drv_loop_inst_counter--; + } + if (!mb_drv_loop_inst_counter) { + err = esp_event_loop_delete(mb_drv_loop_handle); + ESP_LOGD(TAG, "delete event loop: %p.", mb_drv_loop_handle); + mb_drv_loop_handle = NULL; + MB_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, "delete event loop failed, error=%d.", (int)err); + } + return err; +} + +esp_err_t mb_drv_register_handler(void *ctx, mb_driver_event_t event, mb_event_handler_fp fp) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t ret = ESP_ERR_INVALID_STATE; + + ESP_LOGD(TAG, "%p, event 0x%x, register.", pdrv_ctx, (int)event); + + ret = esp_event_handler_instance_register_with(mb_drv_loop_handle, MB_EVENT_BASE(ctx), event, + fp, ctx, &pdrv_ctx->event_handler); + MB_RETURN_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE , + TAG, "%p, event handler %p, registration error.", pdrv_ctx, pdrv_ctx->event_handler); + + return ESP_OK; +} + +esp_err_t mb_drv_unregister_handler(void *ctx, mb_driver_event_t event) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t ret = ESP_ERR_INVALID_STATE; + ESP_LOGD(TAG, "%p, event handler %p, event 0x%x, unregister.", pdrv_ctx, pdrv_ctx->event_handler, (int)event); + + ret = esp_event_handler_instance_unregister_with(mb_drv_loop_handle, + MB_EVENT_BASE(ctx), (int32_t)event, pdrv_ctx->event_handler); + MB_RETURN_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE , + TAG, "%p, event handler %p, unregister error.", pdrv_ctx, pdrv_ctx->event_handler); + + return ESP_OK; +} + +static esp_err_t init_queues(mb_node_info_t *mb_node) +{ + mb_node->rx_queue = queue_create(MB_RX_QUEUE_MAX_SIZE); + MB_RETURN_ON_FALSE(mb_node->rx_queue, ESP_ERR_NO_MEM, TAG, "create rx queue failed"); + mb_node->tx_queue = queue_create(MB_TX_QUEUE_MAX_SIZE); + MB_RETURN_ON_FALSE(mb_node->tx_queue, ESP_ERR_NO_MEM, TAG, "create tx queue failed"); + return ESP_OK; +} + +static void delete_queues(mb_node_info_t *pmb_node) +{ + if (!queue_is_empty(pmb_node->rx_queue)) + { + queue_flush(pmb_node->rx_queue); + } + if (!queue_is_empty(pmb_node->tx_queue)) + { + queue_flush(pmb_node->tx_queue); + } + queue_delete(pmb_node->rx_queue); + queue_delete(pmb_node->tx_queue); + pmb_node->rx_queue = NULL; + pmb_node->tx_queue = NULL; +} + +inline void mb_drv_lock(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + CRITICAL_SECTION_LOCK(pdrv_ctx->lock); +} + +inline void mb_drv_unlock(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + CRITICAL_SECTION_UNLOCK(pdrv_ctx->lock); +} + +__attribute__((unused)) +mb_sock_state_t mb_drv_get_node_state(void *ctx, int fd) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_node_info_t *pnode = pdrv_ctx->mb_nodes[fd]; + return (pnode) ? atomic_load(&pnode->addr_info.state) : MB_SOCK_STATE_UNDEF; +} + +void mb_drv_check_suspend_shutdown(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + + if (pdrv_ctx->close_done_sema) { + mb_status_flags_t status = mb_drv_wait_status_flag(ctx, (MB_FLAG_SHUTDOWN | MB_FLAG_SUSPEND), 0); + ESP_LOGD(TAG, "%p, driver check shutdown (%d)...", ctx, (int)status); + if (status & MB_FLAG_SHUTDOWN) { + xSemaphoreGive(pdrv_ctx->close_done_sema); + ESP_LOGD(TAG, "%p, driver task shutdown...", ctx); + vTaskDelete(NULL); + } else if (status & MB_FLAG_SUSPEND) { + xSemaphoreGive(pdrv_ctx->close_done_sema); + ESP_LOGD(TAG, "%p, driver task is suspended...", ctx); + vTaskSuspend(NULL); + } + } +} + +mb_status_flags_t mb_drv_set_status_flag(void *ctx, mb_status_flags_t mask) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + return (mb_status_flags_t)xEventGroupSetBits(pdrv_ctx->status_flags_hdl, (EventBits_t)mask); +} + +mb_status_flags_t mb_drv_clear_status_flag(void *ctx, mb_status_flags_t mask) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + return (mb_status_flags_t)xEventGroupClearBits(pdrv_ctx->status_flags_hdl, (EventBits_t)mask); +} + +mb_status_flags_t mb_drv_wait_status_flag(void *ctx, mb_status_flags_t mask, TickType_t ticks) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + return (mb_status_flags_t)xEventGroupWaitBits(pdrv_ctx->status_flags_hdl, + (BaseType_t)(mask), + pdFALSE, + pdFALSE, + ticks); +} + +int mb_drv_open(void *ctx, mb_uid_info_t addr_info, int flags) +{ + int fd = UNDEF_FD; + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_node_info_t *pnode_info = NULL; + // Find free fd and initialize + for (fd = 0; fd < MB_MAX_FDS; fd++) { + pnode_info = pdrv_ctx->mb_nodes[fd]; + if (!pnode_info) { + pnode_info = calloc(1, sizeof(mb_node_info_t)); + if (!pnode_info) { + goto err; + } + ESP_LOGD(TAG, "%p, open vfd: %d, sl_addr: %02x, node: %s:%u", + ctx, fd, (int8_t)addr_info.uid, + addr_info.ip_addr_str, (unsigned)addr_info.port); + if (init_queues(pnode_info) != ESP_OK) { + goto err; + } + if (pdrv_ctx->mb_node_open_count > MB_MAX_FDS) { + goto err; + } + mb_drv_lock(ctx); + pdrv_ctx->mb_node_open_count++; + pnode_info->index = fd; + pnode_info->fd = fd; + pnode_info->sock_id = addr_info.fd; + pnode_info->error = -1; + pnode_info->recv_err = -1; + pnode_info->addr_info = addr_info; + //pnode_info->addr_info.ip_addr_str = NULL; + pnode_info->addr_info.index = fd; + pnode_info->send_time = esp_timer_get_time(); + pnode_info->recv_time = esp_timer_get_time(); + pnode_info->tid_counter = 0; + pnode_info->send_counter = 0; + pnode_info->recv_counter = 0; + pnode_info->is_blocking = ((flags & O_NONBLOCK) == 0); + pdrv_ctx->mb_nodes[fd] = pnode_info; + // mark opened node in the open set + FD_SET(fd, &pdrv_ctx->open_set); + mb_drv_unlock(ctx); + MB_SET_NODE_STATE(pnode_info, MB_SOCK_STATE_OPENED); + DRIVER_SEND_EVENT(ctx, MB_EVENT_OPEN, fd); + return fd; + } + } + +err: + free(pnode_info); + pdrv_ctx->mb_nodes[fd] = NULL; + mb_drv_unlock(ctx); + return UNDEF_FD; +} + +mb_node_info_t *mb_drv_get_node(void *ctx, int fd) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + return pdrv_ctx->mb_nodes[fd]; +} + +// writes data into tx queue +ssize_t mb_drv_write(void *ctx, int fd, const void *data, size_t size) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + ssize_t ret = -1; + + if (size == 0) { + return 0; + } + + mb_node_info_t *pnode_info = pdrv_ctx->mb_nodes[fd]; + if (!pnode_info) { + errno = EBADF; + return 0; + } + + if (MB_GET_NODE_STATE(pnode_info) >= MB_SOCK_STATE_CONNECTED) { + if (queue_push(pnode_info->tx_queue, (void *)data, size, NULL) == ESP_OK) { + ret = size; + // Inform FSM that is new frame data is ready to be send + DRIVER_SEND_EVENT(ctx, MB_EVENT_SEND_DATA, pnode_info->index); + } else { + // I/O error + errno = EIO; + } + } else { + // bad file desc + errno = EBADF; + } + return ret; +} + +// reads data from rx queue +ssize_t mb_drv_read(void *ctx, int fd, void *data, size_t size) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_node_info_t *pnode_info = pdrv_ctx->mb_nodes[fd]; + if (!pnode_info) { + errno = EBADF; + return 0; + } + + // fd might be in process of closing (close was already called but preempted) + if (MB_GET_NODE_STATE(pnode_info) < MB_SOCK_STATE_CONNECTED) { + // bad file desc + errno = EBADF; + return -1; + } + + if (size == 0) { + return 0; + } + + ssize_t actual_size = -1; + if ((actual_size = queue_pop(pnode_info->rx_queue, data, size, NULL)) < 0) { + errno = EAGAIN; + } + + return actual_size; +} + +int mb_drv_close(void *ctx, int fd) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_node_info_t *pnode_info = pdrv_ctx->mb_nodes[fd]; // get address of configuration + + if (!pnode_info) { + // not valid opened fd + errno = EBADF; + return -1; + } + + // stop socket + if (MB_GET_NODE_STATE(pnode_info) != MB_SOCK_STATE_CLOSED) { + MB_SET_NODE_STATE(pnode_info, MB_SOCK_STATE_CLOSED); + // Do we need to close connection, if the close event is not run + port_close_connection((mb_node_info_t *)pnode_info); + } + mb_drv_lock(ctx); + FD_CLR(fd, &pdrv_ctx->open_set); + delete_queues(pnode_info); + if (pnode_info->addr_info.node_name_str != pnode_info->addr_info.ip_addr_str) { + free((void *)pnode_info->addr_info.ip_addr_str); // node ip addr string shall be freed + } + free((void *)pnode_info->addr_info.node_name_str); + pnode_info->addr_info.node_name_str = NULL; + pnode_info->addr_info.ip_addr_str = NULL; + free(pnode_info); + pdrv_ctx->mb_nodes[fd] = NULL; + mb_drv_unlock(ctx); + + return 0; +} + +mb_node_info_t *mb_drv_get_next_node_from_set(void *ctx, int *pfd, fd_set *pfdset) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + if (!pfdset || !pfd) { + return NULL; + } + mb_node_info_t *pnode_info = NULL; + for (int fd = *pfd; fd < MB_MAX_FDS; fd++) { + pnode_info = pdrv_ctx->mb_nodes[fd]; + if (pnode_info && (pnode_info->sock_id > 0) + && (MB_GET_NODE_STATE(pnode_info) >= MB_SOCK_STATE_CONNECTED) + && (FD_ISSET(pnode_info->index, pfdset) || (FD_ISSET(pnode_info->sock_id, pfdset)))) { + *pfd = fd; + //FD_CLR(pnode_info->sock_id, pfdset); + return pnode_info; + } + } + return NULL; +} + +mb_node_info_t *mb_drv_get_node_info_from_addr(void *ctx, uint8_t node_addr) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_node_info_t *pnode_info = NULL; + for (int fd = 0; fd < MB_MAX_FDS; fd++) { + pnode_info = pdrv_ctx->mb_nodes[fd]; + if (pnode_info && pnode_info->addr_info.uid == node_addr) { + return pnode_info; + } + } + return NULL; +} + +static int mb_drv_register_fds(void *ctx, fd_set *pfdset) +{ + mb_node_info_t *pnode_info = NULL; + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + // Setup select waiting for eventfd && socket events + FD_ZERO(pfdset); + int max_fd = UNDEF_FD; + // Add to the set all connected slaves + for (int i = 0; i < MB_MAX_FDS; i++) { + pnode_info = pdrv_ctx->mb_nodes[i]; + if (pnode_info && pnode_info->sock_id && (MB_GET_NODE_STATE(pnode_info) >= MB_SOCK_STATE_CONNECTED)) { + MB_ADD_FD(pnode_info->sock_id, max_fd, pfdset); + } + } + // Add event fd events to the set to handle them in one select + MB_ADD_FD(pdrv_ctx->event_fd, max_fd, pfdset); + // Add listen socket to handle incoming connections (for slave only) + MB_ADD_FD(pdrv_ctx->listen_sock_fd, max_fd, pfdset); + return max_fd; +} + +// Wait socket ready event during timeout +static int mb_drv_wait_fd_events(void *ctx, fd_set *pfdset, fd_set *perrset, int time_ms) +{ + fd_set readset = *pfdset; + int ret = 0; + struct timeval tv; + + if (!ctx || !pfdset) { + return -1; + } + + tv.tv_sec = time_ms / 1000; + tv.tv_usec = (time_ms - (tv.tv_sec * 1000)) * 1000; + + // fill the readset according to the active fds + int max_fd = mb_drv_register_fds(ctx, &readset); + if (perrset) { + *perrset = readset; // initialize error set if used + } + + ret = select(max_fd + 1, &readset, NULL, perrset, &tv); + if (ret == 0) { + // No respond from node during timeout + ret = ERR_TIMEOUT; + } else if (ret < 0) { + ret = -1; + } + *pfdset = readset; + return ret; +} + +esp_err_t mb_drv_start_task(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + (void)mb_drv_clear_status_flag(ctx, MB_FLAG_SUSPEND); + ESP_LOGD(TAG, "%p, resume tcp driver task.", ctx); + vTaskResume(pdrv_ctx->mb_tcp_task_handle); + return ESP_OK; +} + +esp_err_t mb_drv_stop_task(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t err = ESP_ERR_TIMEOUT; + if (!pdrv_ctx->close_done_sema) { + pdrv_ctx->close_done_sema = xSemaphoreCreateBinary(); + } + (void)mb_drv_set_status_flag(ctx, MB_FLAG_SUSPEND); + // Check if we can safely suspend the port task (workaround for issue with deadlock in suspend) + if (!pdrv_ctx->close_done_sema + || !(mb_drv_wait_status_flag(ctx, MB_FLAG_SUSPEND, 1) & MB_FLAG_SUSPEND) + || (xSemaphoreTake(pdrv_ctx->close_done_sema, pdMS_TO_TICKS(MB_WAIT_DONE_MS)) != pdTRUE) + ) { + ESP_LOGD(TAG, "%p, could not stop driver task during timeout.", ctx); + vTaskSuspend(pdrv_ctx->mb_tcp_task_handle); + err = ESP_OK; + } + ESP_LOGD(TAG, "%p, stop tcp driver task.", ctx); + if (pdrv_ctx->close_done_sema) { + vSemaphoreDelete(pdrv_ctx->close_done_sema); + pdrv_ctx->close_done_sema = NULL; + } + return err; +} + +// Todo: remove this later +err_t mb_drv_check_node_state(void *ctx, int fd) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_node_info_t *pnode = NULL; + err_t err = ERR_ABRT; + int curr_fd = (fd >= 0) ? fd : 0; + + while(((pnode = mb_drv_get_next_node_from_set(ctx, &curr_fd, &pdrv_ctx->conn_set)) + && (curr_fd < MB_MAX_FDS))) { + if (FD_ISSET(pnode->sock_id, &pdrv_ctx->conn_set)) { + uint64_t last_read_div_us = (esp_timer_get_time() - pnode->recv_time); + if (last_read_div_us >= (uint64_t)(MB_RECONNECT_TIME_MS * 1000)) { + // ESP_LOGD(TAG, "%p, slave: %d, sock: %d, IP:%s, check connection, time = %" PRId64 ", rcv_time: %" PRId64, + // ctx, (int)pnode->index, (int)pnode->sock_id, pnode->addr_info.ip_addr_str, + // (esp_timer_get_time() / 1000), pnode->recv_time / 1000); + err = port_check_alive(pnode, 1); + if ((err < 0) && (err != ERR_INPROGRESS)) { + ESP_LOGE(TAG, "Node #%d [%s], connection error.", pnode->index, pnode->addr_info.ip_addr_str); + } else { + ESP_LOGD(TAG, "Node #%d [%s], connection is ok.", pnode->index, pnode->addr_info.ip_addr_str); + } + pnode->recv_time = esp_timer_get_time(); + curr_fd++; + } + } + } + return err; +} + +void mb_drv_tcp_task(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + ESP_LOGD(TAG, "Start of driver task."); + while (1) { + fd_set readset, errorset; + // check all active socket and fd events + int ret = mb_drv_wait_fd_events(ctx, &readset, &errorset, MB_SELECT_WAIT_MS); + if (ret == ERR_TIMEOUT) { + // timeout occured waiting for the vfds + // ESP_LOGD(TAG, "%p, task select timeout.", ctx); + DRIVER_SEND_EVENT(ctx, MB_EVENT_TIMEOUT, UNDEF_FD); + mb_drv_check_suspend_shutdown(ctx); + } else if (ret == -1) { + // error occured during waiting for vfds activation + ESP_LOGD(TAG, "%p, task select error.", ctx); + mb_drv_check_suspend_shutdown(ctx); + ESP_LOGD(TAG, "%p, socket error, fdset: %" PRIx64, ctx, *(uint64_t *)&errorset); + } else { + // Is the fd event triggered, process the event + if (pdrv_ctx->event_fd && FD_ISSET(pdrv_ctx->event_fd, &readset)) { + mb_event_info_t mb_event = {0}; + int32_t event_id = read_event(ctx, &mb_event); + ESP_LOGD(TAG, "%p, fd event get: 0x%02x:%d, %s", + ctx, (int)event_id, (int)mb_event.opt_fd, driver_event_to_name_r(event_id)); + mb_drv_check_suspend_shutdown(ctx); + // Drive the event loop + esp_err_t err = esp_event_loop_run(mb_drv_loop_handle, pdMS_TO_TICKS(MB_TCP_EVENT_LOOP_TICK_MS)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "%p, event loop run, returns fail: %x", ctx, (int)err); + } + } else if (pdrv_ctx->listen_sock_fd && FD_ISSET(pdrv_ctx->listen_sock_fd, &readset)) { + // If something happened on the listen socket, then it is an incoming connection. + ESP_LOGD(TAG, "%p, listen_sock is active.", ctx); + mb_uid_info_t node_info; + int sock_id = port_accept_connection(pdrv_ctx->listen_sock_fd, &node_info); + if (sock_id) { + int fd = mb_drv_open(pdrv_ctx, node_info, 0); + if (fd < 0) { + ESP_LOGE(TAG, "%p, unable to open node: %s", pdrv_ctx, node_info.ip_addr_str); + } else { + DRIVER_SEND_EVENT(ctx, MB_EVENT_CONNECT, fd); + } + } + } else { + // socket event is ready, process each socket event + mb_drv_check_suspend_shutdown(ctx); + int curr_fd = 0; + mb_node_info_t *pnode_info = NULL; + ESP_LOGD(TAG, "%p, socket event active: %" PRIx64, ctx, *(uint64_t *)&readset); + while(((pnode_info = mb_drv_get_next_node_from_set(ctx, &curr_fd, &readset)) + && (curr_fd < MB_MAX_FDS))) { + if (FD_ISSET(pnode_info->sock_id, &pdrv_ctx->conn_set)) { + // The data is ready in the socket, read frame and queue + FD_CLR(pnode_info->sock_id, &readset); + int ret = port_read_packet(pnode_info); + if (ret > 0) { + ESP_LOGD(TAG, "%p, "MB_NODE_FMT(", frame received."), ctx, (int)pnode_info->fd, + (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str); + mb_drv_lock(ctx); + pnode_info->recv_time = esp_timer_get_time(); + mb_drv_unlock(ctx); + DRIVER_SEND_EVENT(ctx, MB_EVENT_RECV_DATA, pnode_info->index); + } else if (ret == ERR_TIMEOUT) { + ESP_LOGD(TAG, "%p, "MB_NODE_FMT(", frame read timeout or closed connection."), ctx, (int)pnode_info->fd, + (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str); + } else if (ret == ERR_BUF) { + // After retries a response with incorrect TID received, process failure. + pdrv_ctx->event_cbs.mb_sync_event_cb(pdrv_ctx->event_cbs.port_arg, MB_SYNC_EVENT_RECV_FAIL); + ESP_LOGD(TAG, "%p, "MB_NODE_FMT(", frame error."), ctx, (int)pnode_info->fd, + (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str); + } else { + if (ret == ERR_CONN) { + ESP_LOGE(TAG, "%p, "MB_NODE_FMT(", connection lost."), ctx, (int)pnode_info->fd, + (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str); + DRIVER_SEND_EVENT(ctx, MB_EVENT_ERROR, pnode_info->index); + } else { + ESP_LOGE(TAG, "%p, "MB_NODE_FMT(", critical error=%d, errno=%u."), ctx, (int)pnode_info->fd, + (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str, (int)ret, (unsigned)errno); + } + } + } + curr_fd++; + mb_drv_check_suspend_shutdown(ctx); + } + } + } + } +} + +esp_err_t mb_drv_register(port_driver_t **ctx) +{ + port_driver_t driver_config = MB_DRIVER_CONFIG_DEFAULT; + esp_err_t ret = ESP_ERR_INVALID_STATE; + + port_driver_t *pctx = (port_driver_t *)calloc(1, sizeof(port_driver_t)); + MB_GOTO_ON_FALSE((pctx), ESP_ERR_NO_MEM, error, TAG, "%p, driver allocation fail.", pctx); + *pctx = driver_config; + + CRITICAL_SECTION_INIT(pctx->lock); + + // create and initialize modbus driver conetext structure + pctx->mb_nodes = calloc(MB_MAX_FDS, sizeof(mb_node_info_t *)); + MB_GOTO_ON_FALSE((pctx->mb_nodes), ESP_ERR_NO_MEM, error, TAG, "%p, node allocation fail.", pctx); + + for (int i = 0; i < MB_MAX_FDS; i++) { + pctx->mb_nodes[i] = NULL; + } + + ret = init_event_fd((void *)pctx); + MB_GOTO_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE , error, + TAG, "%p, vfs eventfd init error.", pctx); + + ret = mb_drv_event_loop_init((void *)pctx); + MB_GOTO_ON_FALSE((ret == ESP_OK), ESP_ERR_INVALID_STATE , error, + TAG, "%p, event loop init error.", pctx); + + pctx->status_flags_hdl = xEventGroupCreate(); + MB_GOTO_ON_FALSE((pctx->status_flags_hdl), ESP_ERR_INVALID_STATE, error, + TAG, "%p, mb event group error.", pctx); + + mb_drv_loop_inst_counter++; + + // Create task for packet processing + BaseType_t state = xTaskCreatePinnedToCore(mb_drv_tcp_task, + "mb_drv_tcp_task", + MB_TASK_STACK_SZ, + pctx, + MB_TASK_PRIO, + &pctx->mb_tcp_task_handle, + MB_PORT_TASK_AFFINITY); + MB_GOTO_ON_FALSE((state == pdTRUE), ESP_ERR_INVALID_STATE , error, + TAG, "%p, event task creation error.", pctx); + (void)mb_drv_stop_task(pctx); + + *ctx = pctx; + pctx->is_registered = true; + FD_ZERO(&pctx->open_set); + FD_ZERO(&pctx->conn_set); + return ESP_OK; + +error: + if (pctx) { + if (pctx->mb_tcp_task_handle) { + vTaskDelete(pctx->mb_tcp_task_handle); + } + if (mb_drv_loop_handle) { + (void)esp_event_loop_delete(mb_drv_loop_handle); + mb_drv_loop_handle = NULL; + free(pctx->loop_name); + pctx->loop_name = NULL; + } + if (pctx->event_fd) { + close(pctx->event_fd); + (void)esp_vfs_eventfd_unregister(); + } + if (pctx->close_done_sema) { + vSemaphoreDelete(pctx->close_done_sema); + pctx->close_done_sema = NULL; + } + free(pctx->mb_nodes); + } + free(pctx); + return ret; +} + +esp_err_t mb_drv_unregister(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + ESP_LOGD(TAG, "%p, driver unregister.", pdrv_ctx); + (void)mb_drv_set_status_flag(ctx, MB_FLAG_SHUTDOWN); + pdrv_ctx->close_done_sema = xSemaphoreCreateBinary(); + + // if no semaphore (alloc issues) or couldn't acquire it, just delete the task + if (!pdrv_ctx->close_done_sema + || !(mb_drv_wait_status_flag(ctx, MB_FLAG_SHUTDOWN, 0) & MB_FLAG_SHUTDOWN) + || (xSemaphoreTake(pdrv_ctx->close_done_sema, pdMS_TO_TICKS(MB_WAIT_DONE_MS)) != pdTRUE) + ) { + ESP_LOGD(TAG, "%p, driver tasks couldn't exit within timeout -> abruptly deleting the task.", pdrv_ctx); + vTaskDelete(pdrv_ctx->mb_tcp_task_handle); + } + + mb_drv_event_loop_deinit(ctx); + if (pdrv_ctx->close_done_sema) { + vSemaphoreDelete(pdrv_ctx->close_done_sema); + pdrv_ctx->close_done_sema = NULL; + } + + esp_err_t err = close_event_fd(ctx); + if (err != ESP_OK) { + ESP_LOGE(TAG, "could not close the eventfd handle, err = %d. Already closed?", err); + } + + for (int i = 0; i < MB_MAX_FDS; i++) { + mb_node_info_t *pnode_info = pdrv_ctx->mb_nodes[i]; + if (pnode_info) { + ESP_LOGD(TAG, "%p, close node instance #%d(%s).", ctx, i, pnode_info->addr_info.node_name_str); + mb_drv_close(ctx, i); + } + } + + free(pdrv_ctx->mb_nodes); // free the node info address array + pdrv_ctx->mb_nodes = NULL; + + vEventGroupDelete(pdrv_ctx->status_flags_hdl); + + pdrv_ctx->is_registered = false; + CRITICAL_SECTION_CLOSE(pdrv_ctx->lock); + free(pdrv_ctx); + + return ESP_OK; +} + +void mb_drv_set_cb(void *ctx, void *conn_cb, void *arg) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_drv_lock(ctx); + pdrv_ctx->event_cbs.on_conn_done_cb = conn_cb; + pdrv_ctx->event_cbs.arg = arg; + mb_drv_unlock(ctx); +} + +#endif \ No newline at end of file diff --git a/modbus/mb_ports/tcp/port_tcp_driver.h b/modbus/mb_ports/tcp/port_tcp_driver.h new file mode 100644 index 0000000..8707742 --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_driver.h @@ -0,0 +1,338 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +//#include +#include + +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "esp_event.h" // for esp event loop + +#if __has_include("mdns.h") +#include "mdns.h" +#endif + +#include "mb_frame.h" +#include "mb_config.h" + +#include "port_tcp_utils.h" +#include "mb_port_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +#define MB_PORT_DEFAULT (CONFIG_FMB_TCP_PORT_DEFAULT) +#define UNDEF_FD (-1) +#define MB_EVENT_TOUT (300 / portTICK_PERIOD_MS) +#define MB_CONN_TICK_TIMEOUT (10 / portTICK_PERIOD_MS) + +typedef void (*mb_event_handler_fp)(void *ctx, esp_event_base_t base, int32_t id, void *data); +#define MB_EVENT_HANDLER(handler_name) void (handler_name)(void *ctx, esp_event_base_t base, int32_t id, void *data) + +#define MB_TASK_STACK_SZ (CONFIG_FMB_PORT_TASK_STACK_SIZE) +#define MB_TASK_PRIO (CONFIG_FMB_PORT_TASK_PRIO) +#define MB_PORT_TASK_AFFINITY (CONFIG_FMB_PORT_TASK_AFFINITY) + +#define MB_MAX_FDS (MB_TCP_PORT_MAX_CONN) +#define MB_RETRY_MAX (2) +#define MB_RECONNECT_TIME_MS (1000) +#define MB_RX_QUEUE_MAX_SIZE (CONFIG_FMB_QUEUE_LENGTH) +#define MB_TX_QUEUE_MAX_SIZE (CONFIG_FMB_QUEUE_LENGTH) +#define MB_EVENT_QUEUE_SZ (CONFIG_FMB_QUEUE_LENGTH * MB_TCP_PORT_MAX_CONN) + +#define MB_WAIT_DONE_MS (5000) +#define MB_SELECT_WAIT_MS (200) +#define MB_TCP_SEND_TIMEOUT_MS (500) +#define MB_TCP_EVENT_LOOP_TICK_MS (50) + +#define MB_DRIVER_CONFIG_DEFAULT { \ + .spin_lock = portMUX_INITIALIZER_UNLOCKED, \ + .listen_sock_fd = UNDEF_FD, \ + .retry_cnt = MB_RETRY_MAX, \ + .mb_tcp_task_handle = NULL, \ + .mb_node_open_count = 0, \ + .curr_node_index = 0, \ + .mb_proto = MB_TCP, \ + .network_iface_ptr = NULL, \ + .dns_name = NULL, \ + .mb_nodes = NULL, \ + .mb_node_curr = NULL, \ + .close_done_sema = NULL, \ + .max_conn_sd = UNDEF_FD, \ + .node_conn_count = 0, \ + .event_fd = UNDEF_FD, \ +} + +#define MB_EVENTFD_CONFIG() (esp_vfs_eventfd_config_t) { \ + .max_fds = MB_TCP_PORT_MAX_CONN \ +}; + +typedef struct _port_driver port_driver_t; + +#define MB_CHECK_FD_RANGE(fd) ((fd < MB_TCP_PORT_MAX_CONN) && (fd >= 0)) + +#define MB_GET_DRV_PTR(ctx) (__extension__( \ +{ \ + assert(ctx); \ + ((port_driver_t *)ctx); \ +} \ +)) + +#define MB_EVENT_TBL_IT(event) {event, #event} + +#define MB_EVENT_BASE(context) (__extension__( \ +{ \ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(context); \ + (pdrv_ctx->loop_name) ? (esp_event_base_t)(pdrv_ctx->loop_name) : "UNK_BASE"; \ +} \ +)) + +#define MB_ADD_FD(fd, max_fd, pfdset) do { \ + if (fd) { \ + (max_fd = (fd > max_fd) ? fd : max_fd); \ + FD_SET(fd, pfdset); \ + } \ +} while(0) + + +// Macro for atomic operations +#define MB_ATOMIC_LOAD(ctx, addr) (__extension__( \ +{ \ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); \ + (CRITICAL_LOAD(pdrv_ctx->lock, addr)); \ +} \ +)) + +#define MB_ATOMIC_STORE(ctx, addr, val) (__extension__( \ +{ \ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); \ + CRITICAL_STORE(pdrv_ctx->lock, addr, val); \ +} \ +)) + +// Post event to event loop and unblocks the select through the eventfd to handle the event loop run, +// So, the eventfd value keeps last event and its fd. +#define DRIVER_SEND_EVENT(ctx, event, fd) (__extension__( \ +{ \ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); \ + mb_event_info_t (event_info##__FUNCTION__##__LINE__); \ + (event_info##__FUNCTION__##__LINE__).event_id = (int32_t)event; \ + (event_info##__FUNCTION__##__LINE__).opt_fd = fd; \ + ((write_event((void *)pdrv_ctx, &(event_info##__FUNCTION__##__LINE__)) > 0) \ + ? ((event_info##__FUNCTION__##__LINE__)).event_id : UNDEF_FD); \ +} \ +)) + +#define MB_GET_NODE_STATE(pnode) (atomic_load(&((mb_node_info_t *)pnode)->addr_info.state)) + +#define MB_SET_NODE_STATE(pnode, node_state) do { \ + atomic_store(&(((mb_node_info_t *)pnode)->addr_info.state), node_state); \ +} while(0) + +typedef enum _mb_driver_event { + MB_EVENT_READY = 0x0001, + MB_EVENT_OPEN = 0x0002, + MB_EVENT_RESOLVE = 0x0004, + MB_EVENT_CONNECT = 0x0008, + MB_EVENT_SEND_DATA = 0x0010, + MB_EVENT_RECV_DATA = 0x0020, + MB_EVENT_ERROR = 0x0040, + MB_EVENT_CLOSE = 0x0080, + MB_EVENT_TIMEOUT = 0x0100 +} mb_driver_event_t; + +typedef struct { + mb_driver_event_t event; + const char *msg; +} event_msg_t; + +typedef union { + struct { + int32_t event_id; /*!< an event */ + int32_t opt_fd; /*!< fd option for an event */ + }; + uint64_t val; +} mb_event_info_t; + +typedef struct _mb_node_info { + int index; /*!< slave information index */ + int fd; /*!< slave global file descriptor */ + int sock_id; /*!< socket ID of slave */ + mb_uid_info_t addr_info; /*!< slave address info structure*/ + int error; /*!< socket error */ + int recv_err; /*!< socket receive error */ + QueueHandle_t rx_queue; /*!< receive response queue */ + QueueHandle_t tx_queue; /*!< send request queue */ + int64_t send_time; /*!< send request time stamp */ + int64_t recv_time; /*!< receive response time stamp */ + uint16_t tid_counter; /*!< transaction identifier (TID) for slave */ + uint16_t send_counter; /*!< number of packets sent to slave during one session */ + uint16_t recv_counter; /*!< number of packets received from slave during one session */ + bool is_blocking; /*!< slave blocking bit state saved */ +} mb_node_info_t; + +typedef enum _mb_sync_event { + MB_SYNC_EVENT_RECV_OK = 0x0001, + MB_SYNC_EVENT_RECV_FAIL = 0x0002, + MB_SYNC_EVENT_SEND_OK = 0x0003, + MB_SYNC_EVENT_TOUT +} mb_sync_event_t; + +typedef enum _mb_status_flags { + MB_FLAG_BLANK = 0x0000, + MB_FLAG_TRANSACTION_DONE = 0x0001, + MB_FLAG_DISCONNECTED = 0x0002, + MB_FLAG_CONNECTED = 0x0004, + MB_FLAG_SUSPEND = 0x0008, + MB_FLAG_SHUTDOWN = 0x0010 +} mb_status_flags_t; + +typedef struct _driver_event_cbs { + void (*on_conn_done_cb)(void *); + void *arg; + uint64_t (*mb_sync_event_cb)(void *, mb_sync_event_t); + void *port_arg; +} mb_driver_event_cb_t; + +/** + * @brief Modbus driver context parameters + * + */ +typedef struct _port_driver { + void *parent; /*!< parent object pointer */ + char *dns_name; /*!< DNS name of the object */ + portMUX_TYPE spin_lock; /*!< spin lock */ + _lock_t lock; /*!< semaphore mutex */ + bool is_registered; /*!< driver is active flag */ + int listen_sock_fd; /*!< listen socket fd */ + int retry_cnt; /*!< retry counter for events */ + mb_comm_mode_t mb_proto; /*!< current node protocol type */ + uint16_t port; /*!< current node port number */ + uint8_t uid; /*!< unit identifier of the node */ + bool is_master; /*!< identify the type of instance (master, slave) */ + void *network_iface_ptr; /*!< netif interface pointer */ + mb_node_info_t **mb_nodes; /*!< information structures for each associated node */ + uint16_t mb_node_open_count; /*!< count of associated nodes */ + uint16_t node_conn_count; /*!< number of associated nodes */ + mb_node_info_t *mb_node_curr; /*!< current slave information */ + uint16_t curr_node_index; /*!< current processing slave index */ + fd_set open_set; /*!< file descriptor set for opened nodes */ + fd_set conn_set; /*!< file descriptor set for associated nodes */ + int max_conn_sd; /*!< max file descriptor for associated nodes */ + int event_fd; /*!< eventfd descriptor for modbus event tracking */ + SemaphoreHandle_t close_done_sema; /*!< close and done semaphore */ + EventGroupHandle_t status_flags_hdl; /*!< status bits to control nodes states */ + TaskHandle_t mb_tcp_task_handle; /*!< TCP/UDP handling task handle */ + esp_event_loop_handle_t event_loop_hdl; /*!< event loop handle */ + esp_event_handler_instance_t event_handler; /*!< event handler instance */ + char *loop_name; /*!< name for event loop used as base */ + mb_driver_event_cb_t event_cbs; + //LIST_HEAD(mb_uid_info_, mb_uid_entry_s) node_list; /*!< node address information list */ + //uint16_t node_list_count; +} port_driver_t; + +/** + * @brief Register modbus driver + * + * This function must be called prior usage of ESP-MODBUS Interface + * + * @param ctx - pointer to pointer of driver interface structure to be created. + * @param config MODBUS virtual filesystem driver configuration. Default base path /dev/net/modbus/tcp is used when this paramenter is NULL. + * @return esp_err_t + * - ESP_OK on success + */ +esp_err_t mb_drv_register(port_driver_t **config); + +/** + * @brief Unregister modbus driver + * + * @param ctx - pointer to driver interface structure + * @return esp_err_t + * - ESP_OK on success + */ +esp_err_t mb_drv_unregister(void *ctx); + +/** + * @brief Start task of modbus driver + * + * @param ctx - pointer to driver interface structure + * @return esp_err_t + * - ESP_OK on success + */ +esp_err_t mb_drv_start_task(void *ctx); + + +/** + * @brief Unregister modbus driver + * + * @param ctx - pointer to driver interface structure + * @return esp_err_t + * - ESP_OK on success + */ +esp_err_t mb_drv_stop_task(void *ctx); + +/** + * @brief get slave information structure from its short slave address + * + * This function must be called after initialization of ESP-MODBUS Interface + * + * @param uid - modbus slave address of the slave + * @return mb_node_info_t + * - Address of slave info structure on success + * - NULL, if the slave is not found + */ +mb_node_info_t *mb_drv_get_node_info_from_addr(void *ctx, uint8_t uid); + +mb_node_info_t *mb_drv_get_node(void *ctx, int fd); + +mb_sock_state_t mb_drv_get_node_state(void *ctx, int fd); + +int mb_drv_open(void *ctx, mb_uid_info_t addr_info, int flags); + +ssize_t mb_drv_write(void *ctx, int fd, const void *data, size_t size); + +ssize_t mb_drv_read(void *ctx, int fd, void *data, size_t size); + +int mb_drv_close(void *ctx, int fd); + +int32_t write_event(void *ctx, mb_event_info_t *pevent); + +const char *driver_event_to_name_r(mb_driver_event_t event); + +void mb_drv_set_cb(void *ctx, void *conn_cb, void *arg); + +mb_status_flags_t mb_drv_wait_status_flag(void *ctx, mb_status_flags_t mask, TickType_t ticks); + +esp_err_t mb_drv_register_handler(void *ctx, mb_driver_event_t event, mb_event_handler_fp fp); + +esp_err_t mb_drv_unregister_handler(void *ctx, mb_driver_event_t event); + +void mb_drv_check_suspend_shutdown(void *ctx); + +void mb_drv_lock(void *ctx); + +void mb_drv_unlock(void *ctx); + +mb_node_info_t *mb_drv_get_next_node_from_set(void *ctx, int *pfd, fd_set *pfdset); + +mb_status_flags_t mb_drv_set_status_flag(void *ctx, mb_status_flags_t mask); + +mb_status_flags_t mb_drv_clear_status_flag(void *ctx, mb_status_flags_t mask); + +err_t mb_drv_check_node_state(void *ctx, int fd); + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_ports/tcp/port_tcp_master.c b/modbus/mb_ports/tcp/port_tcp_master.c new file mode 100644 index 0000000..54c3be2 --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_master.c @@ -0,0 +1,692 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include + +#include "port_tcp_common.h" +#include "port_tcp_driver.h" +#include "port_tcp_master.h" +#include "port_tcp_utils.h" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +typedef struct +{ + mb_port_base_t base; + // TCP communication properties + mb_tcp_opts_t tcp_opts; + uint8_t ptemp_buf[MB_TCP_BUFF_MAX_SIZE]; + port_driver_t *pdriver; +} mbm_tcp_port_t; + +/* ----------------------- Static variables & functions ----------------------*/ +static const char *TAG = "mb_port.tcp.master"; +static uint64_t mbm_port_tcp_sync_event(void *inst, mb_sync_event_t sync_event); +bool mbm_port_timer_expired(void *inst); +extern int port_scan_addr_string(char *buffer, mb_uid_info_t *pslave_info); + +static esp_err_t mbm_port_tcp_register_handlers(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t ret = ESP_ERR_INVALID_STATE; + + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_READY, mbm_on_ready); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_READY); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_OPEN, mbm_on_open); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_OPEN); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_RESOLVE, mbm_on_resolve); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_RESOLVE); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_CONNECT, mbm_on_connect); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CONNECT); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_ERROR, mbm_on_error); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_ERROR); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_SEND_DATA, mbm_on_send_data); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_SEND_DATA); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_RECV_DATA, mbm_on_recv_data); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_RECV_DATA); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_CLOSE, mbm_on_close); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CLOSE); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_TIMEOUT, mbm_on_timeout); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_TIMEOUT); + return ESP_OK; +} + +static esp_err_t mbm_port_tcp_unregister_handlers(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t ret = ESP_ERR_INVALID_STATE; + ESP_LOGD(TAG, "%p, event handler %p, unregister.", pdrv_ctx, pdrv_ctx->event_handler); + + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_READY); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_READY); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_OPEN); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_OPEN); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_RESOLVE); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_RESOLVE); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_CONNECT); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CONNECT); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_ERROR); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_ERROR); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_SEND_DATA); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_SEND_DATA); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_RECV_DATA); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_RECV_DATA); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_CLOSE); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CLOSE); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_TIMEOUT); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_TIMEOUT); + return ESP_OK; +} + +mb_err_enum_t mbm_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **port_obj) +{ + MB_RETURN_ON_FALSE((port_obj && tcp_opts), MB_EINVAL, TAG, "mb tcp port invalid arguments."); + mbm_tcp_port_t *ptcp = NULL; + esp_err_t err = ESP_OK; + mb_err_enum_t ret = MB_EILLSTATE; + + ptcp = (mbm_tcp_port_t*)calloc(1, sizeof(mbm_tcp_port_t)); + MB_GOTO_ON_FALSE(ptcp, MB_EILLSTATE, error, TAG, "mb tcp port creation error."); + ptcp->pdriver = NULL; + CRITICAL_SECTION_INIT(ptcp->base.lock); + ptcp->base.descr = ((mb_port_base_t *)*port_obj)->descr; + + err = mb_drv_register(&ptcp->pdriver); + MB_GOTO_ON_FALSE(((err == ESP_OK) && ptcp->pdriver), MB_EILLSTATE, error, + TAG, "mb tcp port driver registration failed, err = (%x).", (int)err); + ptcp->pdriver->parent = ptcp; + + err = mbm_port_tcp_register_handlers(ptcp->pdriver); + MB_GOTO_ON_FALSE(((err == ESP_OK) && ptcp->pdriver), MB_EILLSTATE, error, + TAG, "mb tcp port driver event handlers registration failed, err = (%x).", (int)err); + + ptcp->pdriver->network_iface_ptr = tcp_opts->ip_netif_ptr; + ptcp->pdriver->mb_proto = tcp_opts->mode; + ptcp->pdriver->port = tcp_opts->port; + ptcp->pdriver->uid = tcp_opts->uid; + ptcp->pdriver->is_master = true; + ptcp->pdriver->dns_name = tcp_opts->dns_name; + ptcp->pdriver->event_cbs.mb_sync_event_cb = mbm_port_tcp_sync_event; + ptcp->pdriver->event_cbs.port_arg = (void *)ptcp; + + ptcp->base.cb.tmr_expired = mbm_port_timer_expired; + ptcp->base.cb.tx_empty = NULL; + ptcp->base.cb.byte_rcvd = NULL; + ptcp->base.arg = (void *)ptcp; + + char **paddr_table = tcp_opts->ip_addr_table; + MB_GOTO_ON_FALSE((paddr_table && *paddr_table), MB_EILLSTATE, error, + TAG, "mb tcp port nodes registration failed %p, %p.", paddr_table, *paddr_table); + mb_uid_info_t slave_address_info; + int fd = 0; + + while(*paddr_table) { + int res = port_scan_addr_string((char *)*paddr_table, &slave_address_info); + if (res > 0) { + ESP_LOGD(TAG, "Config: %s, IP: %s, port: %d, slave_addr: %d, ip_ver: %s", + (char *)*paddr_table, slave_address_info.ip_addr_str, slave_address_info.port, + slave_address_info.uid, (slave_address_info.addr_type == MB_IPV4 ? "IPV4" : "IPV6")); + fd = mb_drv_open(ptcp->pdriver, slave_address_info, 0); + if (fd < 0) { + ESP_LOGE(TAG, "%p, unable to open slave: %s", ptcp->pdriver, slave_address_info.ip_addr_str); + } else { + ESP_LOGD(TAG, "%p, open slave: %d, %s:%d", + ptcp->pdriver, fd, slave_address_info.ip_addr_str, slave_address_info.port); + } + } else { + ESP_LOGE(TAG, "%p, unable to open slave: %s, check configuration.", ptcp->pdriver, (char *)*paddr_table); + } + paddr_table++; + } + +#ifdef MB_MDNS_IS_INCLUDED + err = port_start_mdns_service(&ptcp->pdriver->dns_name, true, tcp_opts->uid, ptcp->pdriver->network_iface_ptr); + MB_GOTO_ON_FALSE((err == ESP_OK), MB_EILLSTATE, error, + TAG, "mb tcp port mdns service init failure."); +#endif + + *port_obj = &(ptcp->base); + ESP_LOGD(TAG, "created object @%p", ptcp); + return MB_ENOERR; + +error: +#ifdef MB_MDNS_IS_INCLUDED + port_stop_mdns_service(&ptcp->pdriver->dns_name); +#endif + if (ptcp && ptcp->pdriver) { + (void)mbm_port_tcp_unregister_handlers(ptcp->pdriver); + (void)mb_drv_unregister(ptcp->pdriver); + CRITICAL_SECTION_CLOSE(ptcp->base.lock); + // if the MDNS resolving is enabled, then free it + } + free(ptcp); + return ret; +} + +void mbm_port_tcp_delete(mb_port_base_t *inst) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + esp_err_t err = MB_EILLSTATE; + err = mbm_port_tcp_unregister_handlers(port_obj->pdriver); + MB_RETURN_ON_FALSE((err == ESP_OK), ;, TAG, "mb tcp port invalid arguments."); +#ifdef MB_MDNS_IS_INCLUDED + port_stop_mdns_service(&port_obj->pdriver->dns_name); +#endif + err = mb_drv_unregister(port_obj->pdriver); + if (err != ESP_OK) { + ESP_LOGE(TAG, "driver unregister fail, returns (0x%d).", (uint16_t)err); + } + CRITICAL_SECTION_CLOSE(inst->lock); + free(port_obj); +} + +void mbm_port_tcp_enable(mb_port_base_t *inst) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + (void)mb_drv_start_task(port_obj->pdriver); + (void)mb_drv_clear_status_flag(port_obj->pdriver, MB_FLAG_DISCONNECTED); + DRIVER_SEND_EVENT(port_obj->pdriver, MB_EVENT_RESOLVE, UNDEF_FD); +} + +void mbm_port_tcp_disable(mb_port_base_t *inst) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + // Change the state of all slaves to close + DRIVER_SEND_EVENT(port_obj->pdriver, MB_EVENT_CLOSE, UNDEF_FD); + (void)mb_drv_wait_status_flag(port_obj->pdriver, MB_FLAG_DISCONNECTED, pdMS_TO_TICKS(MB_RECONNECT_TIME_MS)); +} + +bool mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + + mb_node_info_t *pinfo = port_obj->pdriver->mb_node_curr; + MB_RETURN_ON_FALSE((pinfo), false, TAG, "incorrect current slave pointer."); + bool status = false; + + size_t sz = mb_drv_read(port_obj->pdriver, pinfo->fd, port_obj->ptemp_buf, MB_BUFFER_SIZE); + if (sz > MB_TCP_FUNC) { + uint16_t tid_counter = MB_TCP_MBAP_GET_FIELD(port_obj->ptemp_buf, MB_TCP_TID); + if (tid_counter == (pinfo->tid_counter - 1)) { + *ppframe = port_obj->ptemp_buf; + *plength = sz; + ESP_LOGD(TAG, "%p, "MB_NODE_FMT(", get packet TID: 0x%04" PRIx16 ":0x%04" PRIx16 ", %p."), + port_obj->pdriver, pinfo->index, pinfo->sock_id, pinfo->addr_info.ip_addr_str, + (unsigned)tid_counter, (unsigned)pinfo->tid_counter, *ppframe); + + uint64_t time = 0; + time = port_get_timestamp() - pinfo->send_time; + ESP_LOGD(TAG, "%p, "MB_NODE_FMT(", processing time[us] = %ju."), port_obj->pdriver, pinfo->index, + pinfo->sock_id, pinfo->addr_info.ip_addr_str, time); + status = true; + } else { + ESP_LOGE(TAG, "%p, "MB_NODE_FMT(", drop packet TID: 0x%04" PRIx16 ":0x%04" PRIx16 ", %p."), + port_obj->pdriver, pinfo->index, pinfo->sock_id, + pinfo->addr_info.ip_addr_str, (unsigned)tid_counter, (unsigned)pinfo->tid_counter, *ppframe); + } + } + return status; +} + +bool mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + + bool frame_sent = false; + // get slave descriptor from its address + mb_node_info_t *pinfo = (mb_node_info_t *)mb_drv_get_node_info_from_addr(port_obj->pdriver, address); + MB_RETURN_ON_FALSE((pinfo && (MB_GET_NODE_STATE(pinfo) >= MB_SOCK_STATE_CONNECTED)), + false, TAG, "the slave address #%d is not registered.", address); + + if (pinfo && pframe) { + // Apply TID field to the frame before send + MB_TCP_MBAP_SET_FIELD(pframe, MB_TCP_TID, pinfo->tid_counter); + pframe[MB_TCP_UID] = (uint8_t)(pinfo->addr_info.uid); + } + + ESP_LOGD(TAG, "%p, send fd: %d, sock_id: %d[%s], %p, len: %d", + port_obj->pdriver, pinfo->fd, pinfo->sock_id, pinfo->addr_info.node_name_str, pframe, length); + + // Write data to the modbus driver send queue of the slave + int write_length = mb_drv_write(port_obj->pdriver, pinfo->fd, pframe, length); + if (write_length) { + frame_sent = true; + } else { + ESP_LOGE(TAG, "mbm_write fail, returns %d.", write_length); + } + // mb_port_timer_respond_timeout_enable(inst); // the timer is set in the transport + + return frame_sent; +} + +void mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + mb_drv_set_cb(port_obj->pdriver, conn_fp, arg); +} + +// Timer handler to check timeout of socket response +bool mbm_port_timer_expired(void *inst) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + bool need_poll = false; + BaseType_t task_unblocked; + mb_event_info_t mb_event; + esp_err_t err = ESP_FAIL; + + mb_port_timer_disable(inst); + // If timer mode is respond timeout, the master event then turns EV_MASTER_EXECUTE status. + if (mb_port_get_cur_timer_mode(inst) == MB_TMODE_RESPOND_TIMEOUT) { + // It is now to check solution. + mb_event.event_id = MB_EVENT_TIMEOUT; + mb_event.opt_fd = port_obj->pdriver->curr_node_index; + err = esp_event_isr_post_to(port_obj->pdriver->event_loop_hdl, MB_EVENT_BASE(port_obj->pdriver), + (int32_t)MB_EVENT_TIMEOUT, (void *)&mb_event, sizeof(mb_event_info_t*), &task_unblocked); + if (err != ESP_OK) { + ESP_EARLY_LOGE(TAG, "Timeout event send error: %d", err); + } + need_poll = task_unblocked; + mb_port_event_set_err_type(inst, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_event_post(inst, EVENT(EV_ERROR_PROCESS)); + } + return need_poll; +} + +mb_uid_info_t *mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state) +{ + mbm_tcp_port_t *port_obj = __containerof(inst, mbm_tcp_port_t, base); + mb_uid_info_t *paddr_info = NULL; + mb_node_info_t *pinfo = mb_drv_get_node_info_from_addr(port_obj->pdriver, slave_addr); + if (pinfo && (MB_GET_NODE_STATE(pinfo) >= exp_state)) { + paddr_info = &pinfo->addr_info; + } + return paddr_info; +} + +static uint64_t mbm_port_tcp_sync_event(void *inst, mb_sync_event_t sync_event) +{ + switch(sync_event) { + case MB_SYNC_EVENT_RECV_OK: + mb_port_timer_disable(inst); + mb_port_event_set_err_type(inst, EV_ERROR_INIT); + mb_port_event_post(inst, EVENT(EV_FRAME_RECEIVED)); + break; + + case MB_SYNC_EVENT_RECV_FAIL: + mb_port_timer_disable(inst); + mb_port_event_set_err_type(inst, EV_ERROR_RECEIVE_DATA); + mb_port_event_post(inst, EVENT(EV_ERROR_PROCESS)); + break; + + case MB_SYNC_EVENT_SEND_OK: + mb_port_event_post(inst, EVENT(EV_FRAME_SENT)); + break; + default: + break; + } + return mb_port_get_trans_id(inst); +} + +MB_EVENT_HANDLER(mbm_on_ready) +{ + // The driver is registered + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); +} + +MB_EVENT_HANDLER(mbm_on_open) +{ + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); +} + +MB_EVENT_HANDLER(mbm_on_resolve) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + + if (MB_CHECK_FD_RANGE(pevent_info->opt_fd)) { + // The mdns is not used in the main app, then can use manually defined IPs + int fd = pevent_info->opt_fd; + mb_node_info_t *pslave = mb_drv_get_node(pdrv_ctx, fd); + if (pslave && (MB_GET_NODE_STATE(pslave) == MB_SOCK_STATE_OPENED) + && FD_ISSET(pslave->index, &pdrv_ctx->open_set)) { + mb_status_flags_t status = mb_drv_wait_status_flag(pdrv_ctx, MB_FLAG_DISCONNECTED, 0); + if ((status & MB_FLAG_DISCONNECTED)) { + ESP_LOGV(TAG, "%p, slave: %d, sock: %d, IP:%s, disconnected.", + ctx, (int)pslave->index, (int)pslave->sock_id, pslave->addr_info.ip_addr_str); + return; + } + // The slave IP is defined manually + if (port_check_host_addr(pslave->addr_info.node_name_str, NULL)) { + pslave->addr_info.ip_addr_str = pslave->addr_info.node_name_str; + ESP_LOGD(TAG, "%p, slave: %d, IP address [%s], added to connection list.", ctx, (int)fd, pslave->addr_info.ip_addr_str); + MB_SET_NODE_STATE(pslave, MB_SOCK_STATE_RESOLVED); + DRIVER_SEND_EVENT(ctx, MB_EVENT_CONNECT, pslave->index); + } else { +#ifdef MB_MDNS_IS_INCLUDED + int ret = port_resolve_mdns_host(pslave->addr_info.node_name_str, (char **)&pslave->addr_info.ip_addr_str); + if (ret > 0) { + ESP_LOGI(TAG, "%p, slave: %d, resolved with IP:%s.", ctx, (int)fd, pslave->addr_info.ip_addr_str); + MB_SET_NODE_STATE(pslave, MB_SOCK_STATE_RESOLVED); + DRIVER_SEND_EVENT(ctx, MB_EVENT_CONNECT, pslave->index); + } else { + // continue resolve while not resolved + DRIVER_SEND_EVENT(ctx, MB_EVENT_RESOLVE, pslave->index); + } +#else + ESP_LOGE(TAG, "%p, slave: %d, IP:%s, mdns service is not supported.", ctx, (int)fd, pslave->addr_info.node_name_str); + DRIVER_SEND_EVENT(ctx, MB_EVENT_RESOLVE, pslave->index); +#endif + } + } + } else if (pevent_info->opt_fd < 0) { + // Todo: query for services is removed from this version + // #ifdef MB_MDNS_IS_INCLUDED + // // If the mDNS feature support is enabled, use it to resolve the slave IP + // res = mb_drv_resolve_mdns_service(ctx, "_modbus", "_tcp", pdrv_ctx->addr_type); + // ESP_LOGD(TAG, "%p, use mdns to resolve slave: %d, resolved: %d devices.", ctx, (int)pevent_info->opt_fd, res); + // #else + for (int fd = 0; fd < pdrv_ctx->mb_node_open_count; fd++) { + mb_node_info_t *pslave = mb_drv_get_node(pdrv_ctx, fd); + if (pslave && (MB_GET_NODE_STATE(pslave) == MB_SOCK_STATE_OPENED) + && FD_ISSET(pslave->index, &pdrv_ctx->open_set)) { + DRIVER_SEND_EVENT(ctx, MB_EVENT_RESOLVE, pslave->index); + } + mb_drv_check_suspend_shutdown(ctx); + } + // #endif + } +} + +MB_EVENT_HANDLER(mbm_on_connect) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_node_info_t *pnode_info = NULL; + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + err_t err = ERR_CONN; + if (MB_CHECK_FD_RANGE(pevent_info->opt_fd)) { + pnode_info = mb_drv_get_node(pdrv_ctx, pevent_info->opt_fd); + if (pnode_info && (MB_GET_NODE_STATE(pnode_info) < MB_SOCK_STATE_CONNECTED)) { + ESP_LOGD(TAG, "%p, connection phase, slave: #%d(%d) [%s].", + ctx, (int)pevent_info->opt_fd, (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str); + if (pnode_info->sock_id != UNDEF_FD) { + port_close_connection(pnode_info); + } + err = port_connect(ctx, pnode_info); + switch (err) { + case ERR_OK: + if (!FD_ISSET(pnode_info->sock_id, &pdrv_ctx->conn_set)) { + FD_SET(pnode_info->sock_id, &pdrv_ctx->conn_set); + mb_drv_lock(ctx); + pdrv_ctx->node_conn_count++; + pdrv_ctx->max_conn_sd = (pnode_info->sock_id > pdrv_ctx->max_conn_sd) ? (int)pnode_info->sock_id : pdrv_ctx->max_conn_sd; + // Update time stamp for connected slaves + pnode_info->send_time = esp_timer_get_time(); + pnode_info->recv_time = esp_timer_get_time(); + mb_drv_unlock(ctx); + ESP_LOGI(TAG, "%p, slave: #%d, sock:%d, IP: %s, is connected.", + ctx, (int)pevent_info->opt_fd, (int)pnode_info->sock_id, + pnode_info->addr_info.ip_addr_str); + } + MB_SET_NODE_STATE(pnode_info, MB_SOCK_STATE_CONNECTED); + port_keep_alive(pnode_info); + break; + case ERR_INPROGRESS: + if (FD_ISSET(pnode_info->sock_id, &pdrv_ctx->conn_set)) { + FD_CLR(pnode_info->sock_id, &pdrv_ctx->conn_set); + ESP_LOGD(TAG, "%p, slave: #%d, sock:%d, IP:%s, connect fail error = %d.", + ctx, (int)pevent_info->opt_fd, (int)pnode_info->sock_id, + pnode_info->addr_info.ip_addr_str, (int)err); + mb_drv_lock(ctx); + if (pdrv_ctx->node_conn_count) { + pdrv_ctx->node_conn_count--; + } + mb_drv_unlock(ctx); + } + MB_SET_NODE_STATE(pnode_info, MB_SOCK_STATE_CONNECTING); + vTaskDelay(MB_CONN_TICK_TIMEOUT); + // try to connect to slave and check connection again if it is not connected + DRIVER_SEND_EVENT(ctx, MB_EVENT_CONNECT, pevent_info->opt_fd); + break; + case ERR_CONN: + ESP_LOGE(TAG, "Modbus connection phase, slave: %d [%s], connection error (%d).", + (int)pevent_info->opt_fd, pnode_info->addr_info.ip_addr_str, (int)err); + break; + default: + ESP_LOGE(TAG, "Invalid error state, slave: %d [%s], error = %d.", + (int)pevent_info->opt_fd, pnode_info->addr_info.ip_addr_str, (int)err); + break; + } + } + } else { + // if the event fd is UNDEF_FD (an event for all slaves), + // then perform connection phase for all resolved slaves sending the connection event + for (int node = 0; (node < MB_TCP_PORT_MAX_CONN); node++) { + pnode_info = mb_drv_get_node(pdrv_ctx, node); + if (pnode_info && (MB_GET_NODE_STATE(pnode_info) == MB_SOCK_STATE_RESOLVED)) { + if (((pnode_info->sock_id < 0) || !FD_ISSET(pnode_info->sock_id, &pdrv_ctx->conn_set)) + && FD_ISSET(node, &pdrv_ctx->open_set)) { + DRIVER_SEND_EVENT(ctx, MB_EVENT_CONNECT, pnode_info->index); + } + } + mb_drv_check_suspend_shutdown(ctx); + } + } + ESP_LOGD(TAG, "Opened/connected: %u, %u.", + (unsigned)pdrv_ctx->mb_node_open_count, (unsigned)pdrv_ctx->node_conn_count); + if (pdrv_ctx->mb_node_open_count == pdrv_ctx->node_conn_count) { + if (pdrv_ctx->event_cbs.on_conn_done_cb) { + pdrv_ctx->event_cbs.on_conn_done_cb(pdrv_ctx->event_cbs.arg); + } + ESP_LOGI(TAG, "%p, Connected: %u, %u, start polling.", + ctx, (unsigned)pdrv_ctx->mb_node_open_count, (unsigned)pdrv_ctx->node_conn_count); + } +} + +MB_EVENT_HANDLER(mbm_on_error) +{ + static int curr_fd = 0; + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + mb_node_info_t *pnode_info = NULL; + if (MB_CHECK_FD_RANGE(pevent_info->opt_fd)) { + mb_drv_check_suspend_shutdown(ctx); + mb_status_flags_t status = mb_drv_wait_status_flag(pdrv_ctx, MB_FLAG_DISCONNECTED, 0); + if ((status & MB_FLAG_DISCONNECTED)) { + ESP_LOGE(TAG, "%p, node: %d, is in disconnected state.", ctx, (int)pevent_info->opt_fd); + return; + } + curr_fd = pevent_info->opt_fd; + pnode_info = mb_drv_get_next_node_from_set(ctx, &curr_fd, &pdrv_ctx->conn_set); + if (pnode_info && (status > 0)) { + uint64_t last_read_div_us = esp_timer_get_time() - pnode_info->recv_time; + ESP_LOGD(TAG, "%p, slave: %d, sock: %d, IP:%s, check connection, time = %" PRId64 ", rcv_time: %" PRId64, + ctx, (int)pnode_info->index, (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str, + (esp_timer_get_time() / 1000), pnode_info->recv_time / 1000); + if (last_read_div_us >= (uint64_t)(MB_RECONNECT_TIME_MS * 1000)) { + mb_drv_check_suspend_shutdown(ctx); + err_t err = port_check_alive(pnode_info, MB_RECONNECT_TIME_MS); + if (err < 0) { + ESP_LOGD(TAG, "%p, slave: %d, sock: %d, inactive for %" PRId64 " [ms], reconnect...", + ctx, (int)pnode_info->index, (int)pnode_info->sock_id, + (last_read_div_us / 1000)); + MB_SET_NODE_STATE(pnode_info, MB_SOCK_STATE_OPENED); + FD_CLR(pnode_info->sock_id, &pdrv_ctx->conn_set); + port_close_connection(pnode_info); + mb_drv_lock(ctx); + pdrv_ctx->node_conn_count--; + mb_drv_unlock(ctx); + DRIVER_SEND_EVENT(ctx, MB_EVENT_CONNECT, pnode_info->index); + } else { + curr_fd++; + } + } else { + ESP_LOGD(TAG, "%p, slave: %d, sock: %d, inactive for %" PRId64 " [ms], wait reconnection...", + ctx, (int)pnode_info->index, (int)pnode_info->sock_id, + (last_read_div_us / 1000)); + } + } + } else if (pevent_info->opt_fd < 0) { + // send resolve event to all slaves + for (int fd = 0; fd < pdrv_ctx->mb_node_open_count; fd++) { + mb_drv_check_suspend_shutdown(ctx); + mb_node_info_t *pslave = mb_drv_get_node(pdrv_ctx, fd); + if (pslave && (MB_GET_NODE_STATE(pslave) == MB_SOCK_STATE_OPENED) + && FD_ISSET(pslave->index, &pdrv_ctx->open_set)) { + DRIVER_SEND_EVENT(ctx, MB_EVENT_RESOLVE, pslave->index); + } + } + } +} + +MB_EVENT_HANDLER(mbm_on_send_data) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + mb_drv_check_suspend_shutdown(ctx); + mb_node_info_t *pinfo = mb_drv_get_node(pdrv_ctx, pevent_info->opt_fd); + if (pinfo && !queue_is_empty(pinfo->tx_queue)) { + uint8_t tx_buffer[MB_TCP_BUFF_MAX_SIZE] = {0}; + ESP_LOGD(TAG, "%p, get info: %d, sock_id: %d, queue_state: %d, state: %d.", + ctx, (int)pevent_info->opt_fd, (int)pinfo->sock_id, + (int)queue_is_empty(pinfo->tx_queue), (int)MB_GET_NODE_STATE(pinfo)); + size_t sz = queue_pop(pinfo->tx_queue, tx_buffer, sizeof(tx_buffer), NULL); + if (MB_GET_NODE_STATE(pinfo) < MB_SOCK_STATE_CONNECTED) { + // if slave is not connected, drop data. + ESP_LOGE(TAG, "%p, "MB_NODE_FMT(", is invalid, drop send data."), + ctx, (int)pinfo->index, (int)pinfo->sock_id, pinfo->addr_info.ip_addr_str); + return; + } + int ret = port_write_poll(pinfo, tx_buffer, sz, MB_TCP_SEND_TIMEOUT_MS); + if (ret < 0) { + ESP_LOGE(TAG, "%p, "MB_NODE_FMT(", send data failure, err(errno) = %d(%u)."), + ctx, (int)pinfo->index, (int)pinfo->sock_id, + pinfo->addr_info.ip_addr_str, (int)ret, (unsigned)errno); + DRIVER_SEND_EVENT(ctx, MB_EVENT_ERROR, pinfo->index); + pinfo->error = ret; + } else { + ESP_LOGD(TAG, "%p, "MB_NODE_FMT(", send data successful: TID:0x%04x, %d (bytes), errno %d"), + ctx, (int)pinfo->index, (int)pinfo->sock_id, + pinfo->addr_info.ip_addr_str, (unsigned)pinfo->tid_counter, (int)ret, (unsigned)errno); + pinfo->error = 0; + // Every successful write increase TID counter + if (pinfo->tid_counter < (USHRT_MAX - 1)) { + pinfo->tid_counter++; + } else { + pinfo->tid_counter = (uint16_t)(pinfo->index << 8U); + } + } + pdrv_ctx->event_cbs.mb_sync_event_cb(pdrv_ctx->event_cbs.port_arg, MB_SYNC_EVENT_SEND_OK); + mb_drv_lock(ctx); + pdrv_ctx->mb_node_curr = pinfo; + pdrv_ctx->curr_node_index = pinfo->index; + pinfo->send_time = esp_timer_get_time(); + pinfo->send_counter = (pinfo->send_counter < (USHRT_MAX - 1)) ? (pinfo->send_counter + 1) : 0; + mb_drv_unlock(ctx); + // Get send buffer from stack + ESP_LOG_BUFFER_HEX_LEVEL("SENT", tx_buffer, sz, ESP_LOG_DEBUG); + } +} + +MB_EVENT_HANDLER(mbm_on_recv_data) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + size_t sz = 0; + uint8_t pbuf[MB_TCP_BUFF_MAX_SIZE] = {0}; + mb_drv_check_suspend_shutdown(ctx); + // Get frame from queue, check for correctness, push back correct frame and generate receive condition. + // Removes incorrect or expired frames from the queue, leave just correct one then sent sync event + mb_node_info_t *pnode_info = mb_drv_get_node(pdrv_ctx, pevent_info->opt_fd); + if (pnode_info) { + ESP_LOGD(TAG, "%p, slave #%d(%d) [%s], receive data ready.", ctx, (int)pevent_info->opt_fd, + (int)pnode_info->sock_id, pnode_info->addr_info.ip_addr_str); + while ((sz <= 0) && !queue_is_empty(pnode_info->rx_queue)) { + size_t sz = queue_pop(pnode_info->rx_queue, pbuf, MB_TCP_BUFF_MAX_SIZE, NULL); + if ((sz > MB_TCP_FUNC) && (sz < sizeof(pbuf))) { + uint16_t tid = MB_TCP_MBAP_GET_FIELD(pbuf, MB_TCP_TID); + ESP_LOGD(TAG, "%p, packet TID: 0x%04" PRIx16 " received.", ctx, tid); + if (tid == (pnode_info->tid_counter - 1)) { + queue_push(pnode_info->rx_queue, pbuf, sz, NULL); + mb_drv_lock(ctx); + pnode_info->recv_time = esp_timer_get_time(); + mb_drv_unlock(ctx); + // send receive event to modbus object + pdrv_ctx->event_cbs.mb_sync_event_cb(pdrv_ctx->event_cbs.port_arg, MB_SYNC_EVENT_RECV_OK); + break; + } + } + mb_drv_check_suspend_shutdown(ctx); + } + } +} + +MB_EVENT_HANDLER(mbm_on_close) +{ + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s, fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + // if close all sockets event is received + if (pevent_info->opt_fd < 0) { + (void)mb_drv_clear_status_flag(pdrv_ctx, MB_FLAG_DISCONNECTED); + for (int fd = 0; fd < MB_MAX_FDS; fd++) { + mb_node_info_t *pslave = mb_drv_get_node(pdrv_ctx, fd); + if (pslave && (MB_GET_NODE_STATE(pslave) >= MB_SOCK_STATE_OPENED) + && FD_ISSET(pslave->index, &pdrv_ctx->open_set)) { + mb_drv_lock(ctx); + // Check connection and unregister slave + if ((pslave->sock_id > 0) && (FD_ISSET(pslave->sock_id, &pdrv_ctx->conn_set)) ) { + FD_CLR(pslave->sock_id, &pdrv_ctx->conn_set); + if (pdrv_ctx->node_conn_count) { + pdrv_ctx->node_conn_count--; + } + } + FD_CLR(pslave->index, &pdrv_ctx->open_set); + mb_drv_unlock(ctx); + // close the socket connection, if active + (void)port_close_connection(pslave); + // change slave state immediately to release from select + MB_SET_NODE_STATE(pslave, MB_SOCK_STATE_READY); + } + } + (void)mb_drv_set_status_flag(pdrv_ctx, MB_FLAG_DISCONNECTED); + mb_drv_check_suspend_shutdown(ctx); + } +} + +MB_EVENT_HANDLER(mbm_on_timeout) +{ + // Socket read/write timeout is triggered + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + // Todo: this event can be used to check network state (kkep empty for now) + mb_drv_check_suspend_shutdown(ctx); +} + +#endif diff --git a/modbus/mb_ports/tcp/port_tcp_master.h b/modbus/mb_ports/tcp/port_tcp_master.h new file mode 100644 index 0000000..c6779c3 --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_master.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "esp_event.h" // for esp event loop + +#include "mb_common.h" +#include "mb_frame.h" + +#include "port_tcp_driver.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +typedef enum _mb_sock_state mb_sock_state_t; +typedef struct _uid_info mb_uid_info_t; + +void mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg); +mb_uid_info_t *mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t uid, mb_sock_state_t exp_state); + +MB_EVENT_HANDLER(mbm_on_ready); +MB_EVENT_HANDLER(mbm_on_open); +MB_EVENT_HANDLER(mbm_on_resolve); +MB_EVENT_HANDLER(mbm_on_connect); +MB_EVENT_HANDLER(mbm_on_send_data); +MB_EVENT_HANDLER(mbm_on_recv_data); +MB_EVENT_HANDLER(mbm_on_error); +MB_EVENT_HANDLER(mbm_on_close); +MB_EVENT_HANDLER(mbm_on_timeout); + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_ports/tcp/port_tcp_slave.c b/modbus/mb_ports/tcp/port_tcp_slave.c new file mode 100644 index 0000000..43a619b --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_slave.c @@ -0,0 +1,605 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "port_tcp_common.h" +#include "port_tcp_slave.h" +#include "port_tcp_driver.h" +#include "port_tcp_utils.h" + +#include "mb_transaction.h" + +#include "port_common.h" // use common port functions + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +typedef struct +{ + mb_port_base_t base; + // TCP communication properties + mb_tcp_opts_t tcp_opts; + mb_uid_info_t addr_info; + uint8_t ptemp_buf[MB_TCP_BUFF_MAX_SIZE]; + // The driver object for the slave + port_driver_t *pdriver; + transaction_handle_t transaction; + uint16_t trans_count; +} mbs_tcp_port_t; + +/* ----------------------- Static variables & functions ----------------------*/ +static const char *TAG = "mb_port.tcp.slave"; + +static uint64_t mbs_port_tcp_sync_event(void *inst, mb_sync_event_t sync_event); + +static esp_err_t mbs_port_tcp_register_handlers(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t ret = ESP_ERR_INVALID_STATE; + + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_READY, mbs_on_ready); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_READY); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_OPEN, mbs_on_open); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_OPEN); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_CONNECT, mbs_on_connect); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CONNECT); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_ERROR, mbs_on_error); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_ERROR); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_SEND_DATA, mbs_on_send_data); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_SEND_DATA); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_RECV_DATA, mbs_on_recv_data); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_RECV_DATA); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_CLOSE, mbs_on_close); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CLOSE); + ret = mb_drv_register_handler(pdrv_ctx, MB_EVENT_TIMEOUT, mbs_on_timeout); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_TIMEOUT); + return ESP_OK; +} + +static esp_err_t mbs_port_tcp_unregister_handlers(void *ctx) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + esp_err_t ret = ESP_ERR_INVALID_STATE; + ESP_LOGD(TAG, "%p, event handler %p, unregister.", pdrv_ctx, pdrv_ctx->event_handler); + + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_READY); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_READY); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_OPEN); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_OPEN); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_CONNECT); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CONNECT); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_SEND_DATA); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_SEND_DATA); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_RECV_DATA); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_RECV_DATA); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_CLOSE); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_CLOSE); + ret = mb_drv_unregister_handler(pdrv_ctx, MB_EVENT_TIMEOUT); + MB_RETURN_ON_FALSE((ret == ESP_OK), MB_EINVAL, TAG, + "%x, mb tcp port event registration failed.", (int)MB_EVENT_TIMEOUT); + return ESP_OK; +} + +mb_err_enum_t mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **port_obj) +{ + MB_RETURN_ON_FALSE((port_obj && tcp_opts), MB_EINVAL, TAG, "mb tcp port invalid arguments."); + mbs_tcp_port_t *ptcp = NULL; + esp_err_t err = ESP_ERR_INVALID_STATE; + ptcp = (mbs_tcp_port_t *)calloc(1, sizeof(mbs_tcp_port_t)); + MB_RETURN_ON_FALSE((ptcp && port_obj), MB_EILLSTATE, TAG, "mb tcp port creation error."); + CRITICAL_SECTION_INIT(ptcp->base.lock); + mb_err_enum_t ret = MB_EILLSTATE; + + // Copy object descriptor from parent object (is used for logging) + ptcp->base.descr = ((mb_port_base_t *)*port_obj)->descr; + ptcp->pdriver = NULL; + ptcp->transaction = transaction_init(); + MB_GOTO_ON_FALSE((ptcp->transaction), MB_EILLSTATE, error, + TAG, "mb transaction init failed."); + + ESP_MEM_CHECK(TAG, ptcp->transaction, goto error); + + err = mb_drv_register(&ptcp->pdriver); + MB_GOTO_ON_FALSE(((err == ESP_OK) && ptcp->pdriver), MB_EILLSTATE, error, + TAG, "mb tcp port driver registration failed, err = (%x).", (int)err); + + err = mbs_port_tcp_register_handlers(ptcp->pdriver); + MB_GOTO_ON_FALSE(((err == ESP_OK) && ptcp->pdriver), MB_EILLSTATE, error, + TAG, "mb tcp port driver registration failed, err = (%x).", (int)err); + + ptcp->pdriver->parent = ptcp; // just for logging purposes + ptcp->tcp_opts = *tcp_opts; + ptcp->pdriver->network_iface_ptr = tcp_opts->ip_netif_ptr; + ptcp->pdriver->mb_proto = tcp_opts->mode; + ptcp->pdriver->uid = tcp_opts->uid; + ptcp->pdriver->is_master = false; + ptcp->pdriver->event_cbs.mb_sync_event_cb = mbs_port_tcp_sync_event; + ptcp->pdriver->event_cbs.port_arg = (void *)ptcp; + +#ifdef MB_MDNS_IS_INCLUDED +err = port_start_mdns_service(&ptcp->pdriver->dns_name, false, tcp_opts->uid, ptcp->pdriver->network_iface_ptr); + MB_GOTO_ON_FALSE((err == ESP_OK), MB_EILLSTATE, error, + TAG, "mb tcp port mdns service init failure."); + ESP_LOGD(TAG, "Start mdns for @%p", ptcp); +#endif + // ptcp->base.cb.tmr_expired = mbs_port_timer_expired; + ptcp->base.cb.tx_empty = NULL; + ptcp->base.cb.byte_rcvd = NULL; + ptcp->base.arg = (void *)ptcp; + *port_obj = &(ptcp->base); + ESP_LOGD(TAG, "created object @%p", ptcp); + return MB_ENOERR; + +error: + if (ptcp && ptcp->transaction) + { + transaction_destroy(ptcp->transaction); + } +#ifdef MB_MDNS_IS_INCLUDED + port_stop_mdns_service(&ptcp->pdriver->dns_name); +#endif + if (ptcp && ptcp->pdriver) + { + if (ptcp->pdriver->event_handler) + { + mbs_port_tcp_unregister_handlers(ptcp->pdriver); + ptcp->pdriver->event_handler = NULL; + } + (void)mb_drv_unregister(ptcp->pdriver); + CRITICAL_SECTION_CLOSE(ptcp->base.lock); + } + free(ptcp); + return ret; +} + +void mbs_port_tcp_delete(mb_port_base_t *inst) +{ + mbs_tcp_port_t *port_obj = __containerof(inst, mbs_tcp_port_t, base); + if (port_obj && port_obj->transaction) + { + transaction_destroy(port_obj->transaction); + } +#ifdef MB_MDNS_IS_INCLUDED + port_stop_mdns_service(&port_obj->pdriver->dns_name); +#endif + if (port_obj && port_obj->pdriver) + { + if (port_obj->pdriver->event_handler) + { + mbs_port_tcp_unregister_handlers(port_obj->pdriver); + port_obj->pdriver->event_handler = NULL; + } + (void)mb_drv_unregister(port_obj->pdriver); + CRITICAL_SECTION_CLOSE(port_obj->base.lock); + } + CRITICAL_SECTION_CLOSE(inst->lock); + free(port_obj); +} + +void mbs_port_tcp_enable(mb_port_base_t *inst) +{ + mbs_tcp_port_t *port_obj = __containerof(inst, mbs_tcp_port_t, base); + (void)mb_drv_start_task(port_obj->pdriver); + DRIVER_SEND_EVENT(port_obj->pdriver, MB_EVENT_READY, UNDEF_FD); +} + +void mbs_port_tcp_disable(mb_port_base_t *inst) +{ + mbs_tcp_port_t *port_obj = __containerof(inst, mbs_tcp_port_t, base); + // Change the state of all slaves to close + DRIVER_SEND_EVENT(port_obj->pdriver, MB_EVENT_CLOSE, UNDEF_FD); + (void)mb_drv_wait_status_flag(port_obj->pdriver, MB_FLAG_DISCONNECTED, pdMS_TO_TICKS(MB_RECONNECT_TIME_MS)); +} + +bool mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + mbs_tcp_port_t *port_obj = __containerof(inst, mbs_tcp_port_t, base); + port_driver_t *pdrv_ctx = port_obj->pdriver; + mb_node_info_t *pnode = NULL; + bool status = false; + transaction_item_handle_t item; + + if (plength && ppframe && *ppframe) + { + mb_drv_lock(pdrv_ctx); + item = transaction_get_first(port_obj->transaction); + if (item && (transaction_item_get_state(item) == ACKNOWLEDGED)) + { + uint16_t tid = 0; + int node_id = 0; + size_t len = 0; + uint8_t *pbuf = transaction_item_get_data(item, &len, &tid, &node_id); + pnode = mb_drv_get_node(pdrv_ctx, node_id); + if (pbuf && pnode && (MB_GET_NODE_STATE(pnode) >= MB_SOCK_STATE_CONNECTED)) + { + memcpy(*ppframe, pbuf, len); + //*ppframe = pbuf; + *plength = (uint16_t)len; + status = true; + ESP_LOGD(TAG, "%p, " MB_NODE_FMT(", get packet TID: 0x%04" PRIx16 ", %p."), + port_obj, pnode->index, pnode->sock_id, + pnode->addr_info.ip_addr_str, (unsigned)pnode->tid_counter, *ppframe); + if (ESP_OK != transaction_item_set_state(item, CONFIRMED)) { + ESP_LOGE(TAG, "transaction queue set state fail."); + } + } + } else { + // Delete expired frames + int frame_cnt = transaction_delete_expired(port_obj->transaction, + port_get_timestamp(), + (1000 * MB_MASTER_TIMEOUT_MS_RESPOND)); + if (frame_cnt) { + ESP_LOGE(TAG, "Deleted %d expired frames.", frame_cnt); + } + } + mb_drv_unlock(pdrv_ctx); + } + return status; +} + +bool mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length) +{ + mbs_tcp_port_t *port_obj = __containerof(inst, mbs_tcp_port_t, base); + + MB_RETURN_ON_FALSE((pframe && (length > 0)), false, TAG, "incorrect arguments."); + bool frame_sent = false; + + uint16_t tid = MB_TCP_MBAP_GET_FIELD(pframe, MB_TCP_TID); + port_driver_t *pdrv_ctx = port_obj->pdriver; + transaction_item_handle_t item; + + mb_drv_lock(pdrv_ctx); + item = transaction_dequeue(port_obj->transaction, CONFIRMED, NULL); + if (item) { + uint16_t msg_id = 0; + int node_id = 0; + uint8_t *pbuf = transaction_item_get_data(item, NULL, &msg_id, &node_id); + if (pbuf && (tid == msg_id)) { + mb_node_info_t *pnode = mb_drv_get_node(pdrv_ctx, node_id); + int write_length = mb_drv_write(pdrv_ctx, node_id, pframe, length); + if (pnode && write_length) { + frame_sent = true; + ESP_LOGD(TAG, "%p, node: #%d, socket(#%d)[%s], send packet TID: 0x%04" PRIx16 ":0x%04" PRIx16 ", %p, len: %d, ", + pdrv_ctx, pnode->index, pnode->sock_id, + pnode->addr_info.node_name_str, (unsigned)tid, (unsigned)msg_id, pframe, length); + } else { + ESP_LOGE(TAG, "%p, node: #%d, socket(#%d)[%s], mbs_write fail, TID: 0x%04" PRIx16 ":0x%04" PRIx16 ", %p, len: %d, ", + pdrv_ctx, pnode->index, pnode->sock_id, + pnode->addr_info.node_name_str, (unsigned)tid, (unsigned)msg_id, pframe, length); + } + if (ESP_OK != transaction_item_set_state(item, REPLIED)) { + ESP_LOGE(TAG, "transaction queue set state fail."); + } + } + } else { + ESP_LOGE(TAG, "queue can not find the item to send."); + } + mb_drv_unlock(pdrv_ctx); + + if (!frame_sent) + { + ESP_LOGE(TAG, "incorrect frame to send."); + } + return frame_sent; +} + +static uint64_t mbs_port_tcp_sync_event(void *inst, mb_sync_event_t sync_event) +{ + switch (sync_event) + { + case MB_SYNC_EVENT_RECV_OK: + mb_port_timer_disable(inst); + mb_port_event_set_err_type(inst, EV_ERROR_INIT); + mb_port_event_post(inst, EVENT(EV_FRAME_RECEIVED)); + break; + + case MB_SYNC_EVENT_RECV_FAIL: + mb_port_timer_disable(inst); + mb_port_event_set_err_type(inst, EV_ERROR_RECEIVE_DATA); + mb_port_event_post(inst, EVENT(EV_ERROR_PROCESS)); + break; + + case MB_SYNC_EVENT_SEND_OK: + mb_port_event_post(inst, EVENT(EV_FRAME_SENT)); + break; + default: + break; + } + return mb_port_get_trans_id(inst); +} + +MB_EVENT_HANDLER(mbs_on_ready) +{ + // The driver is registered + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mbs_tcp_port_t *port_obj = __containerof(pdrv_ctx->parent, mbs_tcp_port_t, base); + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + ESP_LOGD(TAG, "addr_table:%p, addr_type:%d, mode:%d, port:%d", port_obj->tcp_opts.ip_addr_table, + (int)port_obj->tcp_opts.addr_type, + (int)port_obj->tcp_opts.mode, + (int)port_obj->tcp_opts.port); + + int listen_sock = port_bind_addr(port_obj->tcp_opts.ip_addr_table, + port_obj->tcp_opts.addr_type, + port_obj->tcp_opts.mode, + port_obj->tcp_opts.port); + if (listen_sock < 0) + { + mb_drv_check_suspend_shutdown(ctx); + ESP_LOGE(TAG, "%s, sock: %d, bind error", (char *)base, listen_sock); + mb_drv_lock(pdrv_ctx); + if (pdrv_ctx->retry_cnt) pdrv_ctx->retry_cnt--; + mb_drv_unlock(pdrv_ctx); + if (pdrv_ctx->retry_cnt) { + vTaskDelay(TRANSACTION_TICKS); + DRIVER_SEND_EVENT(ctx, MB_EVENT_READY, UNDEF_FD); + } else { + DRIVER_SEND_EVENT(ctx, MB_EVENT_CLOSE, UNDEF_FD); + ESP_LOGE(TAG, "%s, stop binding.", (char *)base); + // mbs_port_tcp_disable(&port_obj->base); + } + } + else + { + mb_drv_lock(ctx); + pdrv_ctx->listen_sock_fd = listen_sock; + mb_drv_unlock(ctx); + ESP_LOGI(TAG, "%s %s: fd: %d, bind is done", (char *)base, __func__, (int)pevent_info->opt_fd); + } +} + +MB_EVENT_HANDLER(mbs_on_open) +{ + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + //port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); +} + +MB_EVENT_HANDLER(mbs_on_connect) +{ + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + mb_node_info_t *pnode = mb_drv_get_node(pdrv_ctx, pevent_info->opt_fd); + if (!pnode) { + ESP_LOGD(TAG, "%s %s: fd: %d, is closed.", (char *)base, __func__, (int)pevent_info->opt_fd); + return; + } + mb_drv_lock(ctx); + MB_SET_NODE_STATE(pnode, MB_SOCK_STATE_CONNECTED); + FD_SET(pnode->sock_id, &pdrv_ctx->conn_set); + mb_drv_unlock(ctx); +} + +MB_EVENT_HANDLER(mbs_on_recv_data) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + mbs_tcp_port_t *port_obj = (mbs_tcp_port_t *)pdrv_ctx->parent; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + mb_node_info_t *pnode = mb_drv_get_node(pdrv_ctx, pevent_info->opt_fd); + transaction_item_handle_t item = NULL; + if (pnode) + { + if (!queue_is_empty(pnode->rx_queue)) + { + ESP_LOGD(TAG, "%p, node #%d(%d) [%s], receive data ready.", ctx, (int)pevent_info->opt_fd, + (int)pnode->sock_id, pnode->addr_info.ip_addr_str); + frame_entry_t frame_entry; + size_t sz = queue_pop(pnode->rx_queue, NULL, MB_BUFFER_SIZE, &frame_entry); + if (sz > MB_TCP_FUNC) + { + uint16_t tid_counter = MB_TCP_MBAP_GET_FIELD(frame_entry.pbuf, MB_TCP_TID); + ESP_LOGD(TAG, "%p, " MB_NODE_FMT(", received packet TID: 0x%04" PRIx16 ", %p."), + pdrv_ctx, pnode->index, pnode->sock_id, + pnode->addr_info.ip_addr_str, (unsigned)tid_counter, frame_entry.pbuf); + mb_drv_lock(pdrv_ctx); + transaction_message_t msg; + msg.buffer = frame_entry.pbuf; + msg.len = frame_entry.len; + msg.msg_id = frame_entry.tid; + msg.node_id = pnode->index; + msg.pnode = pnode; + item = transaction_enqueue(port_obj->transaction, &msg, port_get_timestamp()); + pnode->tid_counter = tid_counter; // assign the TID from frame to use it on send + mb_drv_unlock(pdrv_ctx); + } + } + mb_drv_lock(pdrv_ctx); + item = transaction_get_first(port_obj->transaction); + if (item) + { + if (transaction_item_get_state(item) == QUEUED) + { + // send receive event to modbus object to get the new data + uint16_t msg_id = 0; + uint64_t tick = 0; + (void)transaction_item_get_data(item, NULL, &msg_id, NULL); + tick = port_get_timestamp(); + pdrv_ctx->event_cbs.mb_sync_event_cb(pdrv_ctx->event_cbs.port_arg, MB_SYNC_EVENT_RECV_OK); + transaction_set_tick(port_obj->transaction, msg_id, (transaction_tick_t)tick); + if (ESP_OK == transaction_item_set_state(item, ACKNOWLEDGED)) { + ESP_LOGD(TAG, "%p, " MB_NODE_FMT(", acknoledged packet TID: 0x%04" PRIx16 "."), + pdrv_ctx, pnode->index, pnode->sock_id, + pnode->addr_info.ip_addr_str, (unsigned)msg_id); + } + } + else + { + if (transaction_item_get_state(item) != TRANSMITTED) { + // Todo: for test removing expired item + transaction_delete_expired(port_obj->transaction, port_get_timestamp(), 1000 * 1000); + } + if (MB_FLAG_TRANSACTION_DONE == mb_drv_wait_status_flag(port_obj->pdriver, + MB_FLAG_TRANSACTION_DONE, + TRANSACTION_TICKS)) { + (void)mb_drv_clear_status_flag(pdrv_ctx, MB_FLAG_TRANSACTION_DONE); + } + // postpone the packet processing + DRIVER_SEND_EVENT(ctx, MB_EVENT_RECV_DATA, pnode->index); + } + } else { + ESP_LOGE(TAG, "%p, no queued items found", ctx); + } + mb_drv_unlock(pdrv_ctx); + } + mb_drv_check_suspend_shutdown(ctx); +} + +MB_EVENT_HANDLER(mbs_on_send_data) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + mbs_tcp_port_t *port_obj = (mbs_tcp_port_t *)pdrv_ctx->parent; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + mb_node_info_t *pnode = mb_drv_get_node(pdrv_ctx, pevent_info->opt_fd); + if (pnode && !queue_is_empty(pnode->tx_queue)) + { + frame_entry_t frame_entry; + // pop the frame entry, keep the buffer + size_t sz = queue_pop(pnode->tx_queue, NULL, MB_BUFFER_SIZE, &frame_entry); + if (!sz || (MB_GET_NODE_STATE(pnode) < MB_SOCK_STATE_CONNECTED)) { + ESP_LOGE(TAG, "%p, "MB_NODE_FMT(", is invalid, drop data."), + ctx, (int)pnode->index, (int)pnode->sock_id, pnode->addr_info.ip_addr_str); + return; + } + uint16_t tid = MB_TCP_MBAP_GET_FIELD(frame_entry.pbuf, MB_TCP_TID); + pnode->error = 0; + int ret = port_write_poll(pnode, frame_entry.pbuf, sz, MB_TCP_SEND_TIMEOUT_MS); + if (ret < 0) + { + ESP_LOGE(TAG, "%p, " MB_NODE_FMT(", send data failure, err(errno) = %d(%u)."), + ctx, (int)pnode->index, (int)pnode->sock_id, + pnode->addr_info.ip_addr_str, (int)ret, (unsigned)errno); + DRIVER_SEND_EVENT(ctx, MB_EVENT_ERROR, pnode->index); + pnode->error = ret; + } + else + { + pnode->error = 0; + if (tid != pnode->tid_counter) + { + ESP_LOGE(TAG, "%p, " MB_NODE_FMT(", send incorrect frame TID:0x%04" PRIx16 "!= 0x%04" PRIx16 ", %d (bytes), errno %d"), + ctx, (int)pnode->index, (int)pnode->sock_id, + pnode->addr_info.ip_addr_str, pnode->tid_counter, tid, (int)ret, (unsigned)errno); + } + else + { + ESP_LOGD(TAG, "%p, " MB_NODE_FMT(", send data successful: TID:0x%04" PRIx16 ":0x%04" PRIx16 ", %d (bytes), errno %d"), + ctx, (int)pnode->index, (int)pnode->sock_id, + pnode->addr_info.ip_addr_str, pnode->tid_counter, tid, (int)ret, (unsigned)errno); + } + ESP_LOG_BUFFER_HEX_LEVEL("SENT", frame_entry.pbuf, ret, ESP_LOG_DEBUG); + } + (void)mb_drv_set_status_flag(pdrv_ctx, MB_FLAG_TRANSACTION_DONE); + pdrv_ctx->event_cbs.mb_sync_event_cb(pdrv_ctx->event_cbs.port_arg, MB_SYNC_EVENT_SEND_OK); + mb_drv_lock(pdrv_ctx); + transaction_set_state(port_obj->transaction, tid, TRANSMITTED); + if (transaction_delete(port_obj->transaction, tid) != ESP_OK) { + ESP_LOGE(TAG, "Failed to remove queued TID:0x%04" PRIx16, tid); + } else { + ESP_LOGD(TAG, "Remove the message TID:0x%04" PRIx16, tid); + } + free(frame_entry.pbuf); + pnode->send_time = esp_timer_get_time(); + pnode->send_counter = (pnode->send_counter < (USHRT_MAX - 1)) ? (pnode->send_counter + 1) : 0; + mb_drv_unlock(pdrv_ctx); + } +} + +MB_EVENT_HANDLER(mbs_on_error) +{ + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + mb_node_info_t *pnode = mb_drv_get_node(pdrv_ctx, pevent_info->opt_fd); + if (!pnode) { + ESP_LOGD(TAG, "%s %s: fd: %d, is closed.", (char *)base, __func__, (int)pevent_info->opt_fd); + return; + } + port_close_connection(pnode); + mb_drv_lock(ctx); + // Check connection and unregister slave + if ((pnode->sock_id > 0) && (FD_ISSET(pnode->sock_id, &pdrv_ctx->conn_set))) + { + FD_CLR(pnode->sock_id, &pdrv_ctx->conn_set); + if (pdrv_ctx->node_conn_count) + { + pdrv_ctx->node_conn_count--; + } + } + if (pnode->index && (FD_ISSET(pnode->index, &pdrv_ctx->open_set))) { + FD_CLR(pnode->index, &pdrv_ctx->open_set); + } + mb_drv_unlock(ctx); + mb_drv_close(pdrv_ctx, pevent_info->opt_fd); +} + +MB_EVENT_HANDLER(mbs_on_close) +{ + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + ESP_LOGD(TAG, "%s %s, fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + // if close all sockets event is received + if (pevent_info->opt_fd < 0) + { + (void)mb_drv_clear_status_flag(pdrv_ctx, MB_FLAG_DISCONNECTED); + for (int fd = 0; fd < MB_MAX_FDS; fd++) + { + mb_node_info_t *pnode = mb_drv_get_node(pdrv_ctx, fd); + if (pnode && (MB_GET_NODE_STATE(pnode) >= MB_SOCK_STATE_OPENED) + && FD_ISSET(pnode->index, &pdrv_ctx->open_set)) + { + mb_drv_lock(ctx); + // Check connection and unregister slave + if ((pnode->sock_id > 0) && (FD_ISSET(pnode->sock_id, &pdrv_ctx->conn_set))) + { + FD_CLR(pnode->sock_id, &pdrv_ctx->conn_set); + if (pdrv_ctx->node_conn_count) { + pdrv_ctx->node_conn_count--; + } + } + FD_CLR(pnode->index, &pdrv_ctx->open_set); + mb_drv_unlock(ctx); + // close the socket connection, if active + (void)port_close_connection(pnode); + // change slave state immediately to release from select + MB_SET_NODE_STATE(pnode, MB_SOCK_STATE_READY); + } + } + (void)mb_drv_set_status_flag(pdrv_ctx, MB_FLAG_DISCONNECTED); + mb_drv_check_suspend_shutdown(ctx); + } +} + +MB_EVENT_HANDLER(mbs_on_timeout) +{ + // Slave timeout triggered + mb_event_info_t *pevent_info = (mb_event_info_t *)data; + //port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + ESP_LOGD(TAG, "%s %s: fd: %d", (char *)base, __func__, (int)pevent_info->opt_fd); + // Todo: add network diagnostic here (ping)? Keep empty for now. + //mb_drv_check_node_state(pdrv_ctx, UNDEF_FD); + mb_drv_check_suspend_shutdown(ctx); +} + +#endif \ No newline at end of file diff --git a/modbus/mb_ports/tcp/port_tcp_slave.h b/modbus/mb_ports/tcp/port_tcp_slave.h new file mode 100644 index 0000000..e845bf5 --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_slave.h @@ -0,0 +1,56 @@ +#pragma once + +/* ----------------------- Platform includes --------------------------------*/ +#include "esp_log.h" + +#include +#include + +#include "esp_err.h" +#include "esp_timer.h" +#include "sys/time.h" +#include "esp_netif.h" + +/* ----------------------- lwIP includes ------------------------------------*/ +// #include "lwip/opt.h" +// #include "lwip/sys.h" +// #include "lwip/err.h" +// #include "lwip/sockets.h" +// #include "lwip/netdb.h" +// #include "net/if.h" + +#include "mb_common.h" +#include "mb_frame.h" + +#include "esp_modbus_common.h" // for common types for network options +#include "port_tcp_driver.h" +#include "sys/queue.h" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +#define TRANSACTION_TICKS pdMS_TO_TICKS(50) + +/** + * @brief Modbus slave addr list item for the master + */ +typedef struct mb_data_entry_s { + int node_id; + uint64_t token; + mb_node_info_t *pnode; + frame_entry_t frame; + bool pending; + STAILQ_ENTRY(mb_data_entry_s) entries; +} mb_data_item_t; + + +MB_EVENT_HANDLER(mbs_on_ready); +MB_EVENT_HANDLER(mbs_on_open); +MB_EVENT_HANDLER(mbs_on_resolve); +MB_EVENT_HANDLER(mbs_on_connect); +MB_EVENT_HANDLER(mbs_on_send_data); +MB_EVENT_HANDLER(mbs_on_recv_data); +MB_EVENT_HANDLER(mbs_on_error); +MB_EVENT_HANDLER(mbs_on_close); +MB_EVENT_HANDLER(mbs_on_timeout); + +#endif \ No newline at end of file diff --git a/modbus/mb_ports/tcp/port_tcp_utils.c b/modbus/mb_ports/tcp/port_tcp_utils.c new file mode 100644 index 0000000..f29e66d --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_utils.c @@ -0,0 +1,912 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#if __has_include("esp_mac.h") +#include "esp_mac.h" +#endif + +#include "port_tcp_master.h" +#include "port_tcp_utils.h" +#include "port_tcp_driver.h" +#include "sdkconfig.h" + +#define TAG "port.utils" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +// Check host name and/or fill the IP address structure +bool port_check_host_addr(const char *host_str, ip_addr_t *host_addr) +{ + MB_RETURN_ON_FALSE((host_str), false, TAG, "wrong host name or IP."); + char cstr[HOST_STR_MAX_LEN]; + char *pstr = &cstr[0]; + ip_addr_t target_addr; + struct addrinfo hint; + struct addrinfo *paddr_list; + memset(&hint, 0, sizeof(hint)); + // Do name resolution for both protocols + hint.ai_family = AF_UNSPEC; + hint.ai_flags = AI_ADDRCONFIG; // get IPV6 address if supported, otherwise IPV4 + hint.ai_flags |= AI_CANONNAME; + memset(&target_addr, 0, sizeof(target_addr)); + + // convert domain name to IP address + // Todo: check EAI_FAIL error when resolve host name + int ret = getaddrinfo(host_str, NULL, &hint, &paddr_list); + if (ret != 0) { + ESP_LOGD(TAG, "Incorrect host IP: %s", host_str); + return false; + } + if (paddr_list->ai_family == AF_INET) { + struct in_addr addr4 = ((struct sockaddr_in *)(paddr_list->ai_addr))->sin_addr; + inet_addr_to_ip4addr(ip_2_ip4(&target_addr), &addr4); + pstr = ip4addr_ntoa_r(ip_2_ip4(&target_addr), cstr, sizeof(cstr)); + } +#if CONFIG_LWIP_IPV6 + else { + struct in6_addr addr6 = ((struct sockaddr_in6 *)(paddr_list->ai_addr))->sin6_addr; + inet6_addr_to_ip6addr(ip_2_ip6(&target_addr), &addr6); + pstr = ip6addr_ntoa_r(ip_2_ip6(&target_addr), cstr, sizeof(cstr)); + } +#endif + if (host_addr) { + *host_addr = target_addr; + } + ESP_LOGD(TAG, "Check name[IP]: \"%s\"[%s]", paddr_list->ai_canonname ? paddr_list->ai_canonname : "UNK", pstr); + freeaddrinfo(paddr_list); + return true; +} + +bool port_close_connection(mb_node_info_t *pinfo) +{ + if (!pinfo) { + return false; + } + if (pinfo->sock_id <= 0) { + ESP_LOGD(TAG, "wrong socket info or disconnected socket: %d, skip.", pinfo->index); + return false; + } + uint8_t tmp_buff[MB_PDU_SIZE_MAX]; + + // Empty tcp buffer before shutdown + (void)recv(pinfo->sock_id, &tmp_buff[0], MB_PDU_SIZE_MAX, MSG_DONTWAIT); + queue_flush(pinfo->rx_queue); + queue_flush(pinfo->tx_queue); + + if (shutdown(pinfo->sock_id, SHUT_RDWR) == -1) { + ESP_LOGV(TAG, "Shutdown failed sock %d, errno=%d", pinfo->sock_id, (int)errno); + } + close(pinfo->sock_id); + MB_SET_NODE_STATE(pinfo, MB_SOCK_STATE_OPENED); + pinfo->sock_id = UNDEF_FD; + return true; +} + +// The helper function to get time stamp in microseconds +int64_t port_get_timestamp(void) +{ + int64_t time_stamp = esp_timer_get_time(); + return time_stamp; +} + +static void port_ms_to_tv(uint16_t timeout_ms, struct timeval *tv) +{ + tv->tv_sec = timeout_ms / 1000; + tv->tv_usec = (timeout_ms - (tv->tv_sec * 1000)) * 1000; +} + +int port_enqueue_packet(QueueHandle_t queue, uint8_t *pbuf, uint16_t len) +{ + frame_entry_t frame_info = {0}; + esp_err_t ret = ESP_ERR_INVALID_STATE; + + if (queue && pbuf) { + frame_info.tid = MB_TCP_MBAP_GET_FIELD(pbuf, MB_TCP_TID); + frame_info.uid = pbuf[MB_TCP_UID]; + frame_info.pid = MB_TCP_MBAP_GET_FIELD(pbuf, MB_TCP_PID); + frame_info.len = MB_TCP_MBAP_GET_FIELD(pbuf, MB_TCP_LEN) + MB_TCP_UID; + if (len != frame_info.len) { + ESP_LOGE(TAG, "Packet TID (%x), length in frame %u != %u expected.", frame_info.tid, frame_info.len, len); + } + assert(xPortGetFreeHeapSize() > frame_info.len); + + ret = queue_push(queue, pbuf, frame_info.len, &frame_info); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Packet TID (%x), data enqueue failed.", frame_info.tid); + // The packet send fail or the task which is waiting for event is already unblocked + return ERR_BUF; + } else { + ESP_LOGD(TAG, "Enqueue data, length=%d, TID=0x%" PRIx16, frame_info.len, frame_info.tid); + return (int)frame_info.len; + } + } else { + ESP_LOGE(TAG, "Enqueue data fail, %p, length=%d.", pbuf, len); + } + return ERR_BUF; +} + +int port_dequeue_packet(QueueHandle_t queue, frame_entry_t *pframe_info) +{ + frame_entry_t frame_info = {0}; + esp_err_t ret = ESP_ERR_INVALID_STATE; + + if (queue && pframe_info) { + ret = queue_pop(queue, NULL, MB_TCP_BUFF_MAX_SIZE, &frame_info); + if (ret == ESP_OK) { + if ((frame_info.pid == 0) && (frame_info.uid < MB_ADDRESS_MAX)) { + *pframe_info = frame_info; + ESP_LOGD(TAG, "Dequeue data, length=%d, TID=0x%" PRIx16, (int)pframe_info->len, (int)pframe_info->tid); + return ERR_OK; + } + } else { + ESP_LOGE(TAG, "Dequeue data, failure %d", (int)ret); + } + } + return ERR_BUF; +} + +static int port_get_buf(mb_node_info_t *pinfo, uint8_t *pdst_buf, uint16_t len, uint16_t read_tick_ms) +{ + int ret = 0; + uint8_t *pbuf = pdst_buf; + uint16_t bytes_left = len; + struct timeval time_val; + + MB_RETURN_ON_FALSE((pinfo && (pinfo->sock_id > UNDEF_FD)), -1, TAG, "Try to read incorrect socket = #%d.", pinfo->sock_id); + + // Set receive timeout for socket <= slave respond time + time_val.tv_sec = read_tick_ms / 1000; + time_val.tv_usec = (read_tick_ms % 1000) * 1000; + setsockopt(pinfo->sock_id, SOL_SOCKET, SO_RCVTIMEO, &time_val, sizeof(time_val)); + + // blocking read of data from socket + ret = recv(pinfo->sock_id, pbuf, bytes_left, 0); + if (ret < 0) { + if (errno == EINPROGRESS || errno == EAGAIN || errno == EWOULDBLOCK) { + // Read timeout occurred, check the timeout and return + return 0; + } else if (errno == ENOTCONN) { + ESP_LOGE(TAG, "socket(#%d)(%s) connection closed, ret=%d, errno=%d.", + pinfo->sock_id, pinfo->addr_info.ip_addr_str, ret, (int)errno); + // Socket connection closed + return ERR_CONN; + } else { + // Other error occurred during receiving + ESP_LOGE(TAG, "Socket(#%d)(%s) receive error, ret = %d, errno = %d(%s)", + pinfo->sock_id, pinfo->addr_info.ip_addr_str, ret, (int)errno, strerror(errno)); + return -1; + } + } + return ret; +} + +int port_read_packet(mb_node_info_t *pinfo) +{ + uint16_t temp = 0; + int ret = 0; + uint8_t ptemp_buf[MB_TCP_BUFF_MAX_SIZE] = {0}; + + // Receive data from connected client + if (pinfo) { + MB_RETURN_ON_FALSE((pinfo->sock_id > 0), -1, TAG, "try to read incorrect socket = #%d.", pinfo->sock_id); + // Read packet header + ret = port_get_buf(pinfo, ptemp_buf, MB_TCP_UID, MB_READ_TICK); + if (ret < 0) { + pinfo->recv_err = ret; + return ret; + } else if (ret != MB_TCP_UID) { + ESP_LOGD(TAG, "Socket (#%d)(%s), fail to read modbus header. ret=%d", + pinfo->sock_id, pinfo->addr_info.ip_addr_str, ret); + pinfo->recv_err = ERR_VAL; + return ERR_VAL; + } + + temp = MB_TCP_MBAP_GET_FIELD(ptemp_buf, MB_TCP_PID); + if (temp != 0) { + pinfo->recv_err = ERR_BUF; + return ERR_BUF; + } + + // If we have received the MBAP header we can analyze it and calculate + // the number of bytes left to complete the current response. + temp = MB_TCP_MBAP_GET_FIELD(ptemp_buf, MB_TCP_LEN); + if (temp > MB_TCP_BUFF_MAX_SIZE) { + ESP_LOGD("RCV", "Incorrect packet length: %d", temp); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, ptemp_buf, MB_TCP_FUNC, ESP_LOG_DEBUG); + pinfo->recv_err = ERR_BUF; + temp = MB_TCP_BUFF_MAX_SIZE; // read all remaining data from buffer + } + + ret = port_get_buf(pinfo, &ptemp_buf[MB_TCP_UID], temp, MB_READ_TICK); + if (ret < 0) { + pinfo->recv_err = ret; + return ret; + } else if (ret != temp) { + pinfo->recv_err = ERR_VAL; + return ERR_VAL; + } + + if (ptemp_buf[MB_TCP_UID] > MB_ADDRESS_MAX) { + pinfo->recv_err = ERR_BUF; + return ERR_BUF; + } + + ret = port_enqueue_packet(pinfo->rx_queue, ptemp_buf, temp + MB_TCP_UID); + if (ret < 0) { + pinfo->recv_err = ret; + return ret; + } + + pinfo->recv_counter++; + + pinfo->recv_err = ERR_OK; + return ret + MB_TCP_FUNC; + } + return -1; +} + +err_t port_set_blocking(mb_node_info_t *pinfo, bool is_blocking) +{ + if (!pinfo) { + return ERR_CONN; + } + // Set non blocking attribute for socket + uint32_t flags = fcntl(pinfo->sock_id, F_GETFL); + flags = is_blocking ? flags & ~O_NONBLOCK : flags | O_NONBLOCK; + if (fcntl(pinfo->sock_id, F_SETFL, flags) == -1) { + ESP_LOGE(TAG, "Socket(#%d)(%s), fcntl() call error=%d", + pinfo->sock_id, pinfo->addr_info.ip_addr_str, (int)errno); + return ERR_WOULDBLOCK; + } else { + pinfo->is_blocking = ((flags & O_NONBLOCK) != O_NONBLOCK); + } + return ERR_OK; +} + +void port_keep_alive(mb_node_info_t *pinfo) +{ + int optval = 1; + setsockopt(pinfo->sock_id, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)); +} + +// Check connection for timeout helper +err_t port_check_alive(mb_node_info_t *pinfo, uint32_t timeout_ms) +{ + fd_set write_set; + fd_set err_set; + err_t err = -1; + struct timeval time_val; + + if (pinfo && pinfo->sock_id != -1) { + FD_ZERO(&write_set); + FD_ZERO(&err_set); + FD_SET(pinfo->sock_id, &write_set); + FD_SET(pinfo->sock_id, &err_set); + port_ms_to_tv(timeout_ms, &time_val); + // Check if the socket is writable + err = select(pinfo->sock_id + 1, NULL, &write_set, &err_set, &time_val); + if ((err < 0) || FD_ISSET(pinfo->sock_id, &err_set)) { + if (errno == EINPROGRESS) { + err = ERR_INPROGRESS; + } else { + ESP_LOGV(TAG, MB_NODE_FMT(" connection, select write err(errno) = %d(%d)."), + pinfo->index, pinfo->sock_id, pinfo->addr_info.ip_addr_str, err, (int)errno); + err = ERR_CONN; + } + } else if (err == 0) { + ESP_LOGV(TAG, "Socket(#%d)(%s), connection timeout occurred, err(errno) = %d(%d).", + pinfo->sock_id, pinfo->addr_info.ip_addr_str, err, (int)errno); + return ERR_INPROGRESS; + } else { + int opt_err = 0; + uint32_t opt_len = sizeof(opt_err); + // Check socket error + err = getsockopt(pinfo->sock_id, SOL_SOCKET, SO_ERROR, (void *)&opt_err, (socklen_t *)&opt_len); + if (opt_err != 0) { + ESP_LOGD(TAG, "Socket(#%d)(%s), sock error occurred (%d).", + pinfo->sock_id, pinfo->addr_info.ip_addr_str, opt_err); + return ERR_CONN; + } + ESP_LOGV(TAG, "Socket(#%d)(%s), is alive.", + pinfo->sock_id, pinfo->addr_info.ip_addr_str); + return ERR_OK; + } + } else { + err = ERR_CONN; + } + return err; +} + +// Unblocking connect function +err_t port_connect(void *ctx, mb_node_info_t *pinfo) +{ + if (!pinfo) { + return ERR_CONN; + } + port_driver_t *pdrv_ctx = MB_GET_DRV_PTR(ctx); + err_t err = ERR_OK; + char str[HOST_STR_MAX_LEN]; + char *pstr = NULL; + ip_addr_t target_addr; + struct addrinfo hint; + struct addrinfo *addr_list; + struct addrinfo *pcur_addr; + + memset(&hint, 0, sizeof(hint)); + // Do name resolution for both protocols + hint.ai_flags = AI_ADDRCONFIG | AI_CANONNAME; // get IPV6 address if supported, otherwise IPV4 + hint.ai_family = (pinfo->addr_info.addr_type == MB_IPV4) ? AF_INET : AF_INET6; + hint.ai_socktype = (pinfo->addr_info.proto == MB_UDP) ? SOCK_DGRAM : SOCK_STREAM; + hint.ai_protocol = (pinfo->addr_info.proto == MB_UDP) ? IPPROTO_UDP : IPPROTO_TCP; + memset(&target_addr, 0, sizeof(target_addr)); + + if (asprintf(&pstr, "%u", pinfo->addr_info.port) == -1) { + abort(); + } + + // convert domain name to IP address + int ret = getaddrinfo(pinfo->addr_info.ip_addr_str, pstr, &hint, &addr_list); + free(pstr); + if (ret != 0) { + ESP_LOGE(TAG, "Cannot resolve host: %s", pinfo->addr_info.ip_addr_str); + return ERR_CONN; + } + + for (pcur_addr = addr_list; pcur_addr != NULL; pcur_addr = pcur_addr->ai_next) { + if (pcur_addr->ai_family == AF_INET) { + struct in_addr addr4 = ((struct sockaddr_in *)(pcur_addr->ai_addr))->sin_addr; + inet_addr_to_ip4addr(ip_2_ip4(&target_addr), &addr4); + pstr = ip4addr_ntoa_r(ip_2_ip4(&target_addr), str, sizeof(str)); + } +#if CONFIG_LWIP_IPV6 + else if (pcur_addr->ai_family == AF_INET6) { + struct in6_addr addr6 = ((struct sockaddr_in6 *)(pcur_addr->ai_addr))->sin6_addr; + inet6_addr_to_ip6addr(ip_2_ip6(&target_addr), &addr6); + pstr = ip6addr_ntoa_r(ip_2_ip6(&target_addr), str, sizeof(str)); + // Set scope id to fix routing issues with local address + ((struct sockaddr_in6 *)(pcur_addr->ai_addr))->sin6_scope_id = + esp_netif_get_netif_impl_index(pdrv_ctx->network_iface_ptr); + } +#endif + if (pinfo->sock_id <= 0) { + pinfo->sock_id = socket(pcur_addr->ai_family, pcur_addr->ai_socktype, pcur_addr->ai_protocol); + if (pinfo->sock_id < 0) { + ESP_LOGE(TAG, "Unable to create socket: #%d, errno %d", pinfo->sock_id, (int)errno); + err = ERR_IF; + continue; + } + } else { + ESP_LOGV(TAG, "Socket (#%d)(%s) created.", pinfo->sock_id, str); + } + + // Set non blocking attribute for socket + port_set_blocking(pinfo, false); + + // Can return EINPROGRESS as an error which means + // that connection is in progress and should be checked later + err = connect(pinfo->sock_id, (struct sockaddr *)pcur_addr->ai_addr, pcur_addr->ai_addrlen); + if ((err < 0) && (errno == EINPROGRESS || errno == EALREADY)) { + // The unblocking connect is pending (check status later) or already connected + ESP_LOGV(TAG, "Socket(#%d)(%s) connection is pending, errno %d (%s).", + pinfo->sock_id, str, (int)errno, strerror(errno)); + + // Set keep alive flag in socket options + port_keep_alive(pinfo); + err = port_check_alive(pinfo, MB_TCP_CONNECTION_TIMEOUT_MS); + continue; + } else if ((err < 0) && (errno == EISCONN)) { + // Socket already connected + err = ERR_OK; + continue; + } else if (err != ERR_OK) { + // Other error occurred during connection + ESP_LOGV(TAG, "%p, "MB_NODE_FMT(" unable to connect, error=%d, errno %d (%s)"), + ctx, pinfo->index, pinfo->sock_id, str, err, (int)errno, strerror(errno)); + port_close_connection(pinfo); + err = ERR_CONN; + } else { + ESP_LOGI(TAG, "%p, "MB_NODE_FMT(", successfully connected."), + ctx, pinfo->index, pinfo->sock_id, str); + continue; + } + } + freeaddrinfo(addr_list); + port_set_blocking(pinfo, true); + return err; +} + +int port_write_poll(mb_node_info_t *pinfo, const uint8_t *pframe, uint16_t frame_len, uint32_t timeout) +{ + // Check if the socket is alive (writable and SO_ERROR == 0) + int res = (int)port_check_alive(pinfo, timeout); + if ((res < 0) && (res != ERR_INPROGRESS)) { + ESP_LOGE(TAG, MB_NODE_FMT(", is not writable, error: %d, errno %d"), + pinfo->index, pinfo->sock_id, pinfo->addr_info.ip_addr_str, res, (int)errno); + return res; + } + res = send(pinfo->sock_id, pframe, frame_len, TCP_NODELAY); + if (res < 0) { + ESP_LOGE(TAG, MB_NODE_FMT(", send data error: %d, errno %d"), + pinfo->index, pinfo->sock_id, pinfo->addr_info.ip_addr_str, res, (int)errno); + } + return res; +} + +// Scan IP address according to IPV settings +int port_scan_addr_string(char *buffer, mb_uid_info_t *pinfo) +{ + char *phost_str = NULL; + unsigned int a[8] = {0}; + int ret = 0; + uint16_t index = 0; + uint16_t port = 0; + + MB_RETURN_ON_FALSE((buffer && (strlen(buffer) < (HOST_STR_MAX_LEN - 8)) && pinfo), + -1, TAG, "check input parameters fail."); + +#if CONFIG_LWIP_IPV6 + // Configuration format: + // "12:2001:0db8:85a3:0000:0000:8a2e:0370:7334:502" + // "12:2001:0db8:85a3:0000:0000:8a2e:0370:7334" + ret = sscanf(buffer, "%" PRIu16 ";" IPV6STR ";%" PRIu16, &index, &a[0], &a[1], &a[2], &a[3], &a[4], &a[5], &a[6], &a[7], &port); + if ((ret == MB_STR_LEN_IDX_IP6) || (ret == MB_STR_LEN_IDX_IP6_PORT)) { + if (-1 == asprintf(&phost_str, IPV6STR, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])) { + abort(); + } + pinfo->node_name_str = phost_str; + pinfo->ip_addr_str = phost_str; + pinfo->uid = index; + pinfo->fd = UNDEF_FD; + pinfo->port = (ret == MB_STR_LEN_IDX_IP6_PORT) ? port : CONFIG_FMB_TCP_PORT_DEFAULT; + pinfo->addr_type = MB_IPV6; + pinfo->proto = MB_TCP; + return ret; + } + + // Configuration format: + // "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + ret = sscanf(buffer, IPV6STR, &a[0], &a[1], &a[2], &a[3], &a[4], &a[5], &a[6], &a[7]); + if (ret == MB_STR_LEN_IP6_ONLY) { + if (-1 == asprintf(&phost_str, IPV6STR, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])) { + abort(); + } + pinfo->node_name_str = phost_str; + pinfo->ip_addr_str = phost_str; + pinfo->uid = 0; + pinfo->fd = UNDEF_FD; + pinfo->port = CONFIG_FMB_TCP_PORT_DEFAULT; + pinfo->addr_type = MB_IPV6; + pinfo->proto = MB_TCP; + return ret; + } +#endif + // Configuration format: + // "192.168.1.1" + ret = sscanf(buffer, IPSTR, &a[0], &a[1], &a[2], &a[3]); + if (ret == MB_STR_LEN_IP4_ONLY) { + if (-1 == asprintf(&phost_str, IPSTR, a[0], a[1], a[2], a[3])) { + abort(); + } + pinfo->node_name_str = phost_str; + pinfo->ip_addr_str = phost_str; + pinfo->uid = 0; + pinfo->fd = UNDEF_FD; + pinfo->port = CONFIG_FMB_TCP_PORT_DEFAULT; + pinfo->addr_type = MB_IPV4; + pinfo->proto = MB_TCP; + return ret; + } + + // Configuration format: + // "1:192.168.1.1:502" + ret = sscanf(buffer, "%" PRIu16 ";"IPSTR";%" PRIu16, &index, &a[0], &a[1], &a[2], &a[3], &port); + if ((ret == MB_STR_LEN_IDX_IP4_PORT) || (ret == MB_STR_LEN_IDX_IP4)) { + if (-1 == asprintf(&phost_str, IPSTR, a[0], a[1], a[2], a[3])) { + abort(); + } + pinfo->node_name_str = phost_str; + pinfo->ip_addr_str = phost_str; + pinfo->uid = index; + pinfo->fd = UNDEF_FD; + pinfo->port = (ret == MB_STR_LEN_IDX_IP4_PORT) ? port : CONFIG_FMB_TCP_PORT_DEFAULT; + pinfo->addr_type = MB_IPV4; + pinfo->proto = MB_TCP; + return ret; + } + + // Configuration format: + // "01:mb_node_tcp_01:502" + ret = sscanf(buffer, "%" PRIu16 ";%m[a-z0-9_];%" PRIu16, (uint16_t*)&index, &phost_str, &port); + if ((ret == MB_STR_LEN_HOST) || (ret == MB_STR_LEN_IDX_HOST_PORT)) { + pinfo->node_name_str = (phost_str && strlen(phost_str)) ? phost_str : pinfo->node_name_str; + pinfo->ip_addr_str = (pinfo->node_name_str) ? pinfo->node_name_str : pinfo->ip_addr_str; + pinfo->uid = index; + pinfo->fd = UNDEF_FD; + pinfo->port = (ret == MB_STR_LEN_IDX_HOST_PORT) ? port : CONFIG_FMB_TCP_PORT_DEFAULT; + pinfo->addr_type = MB_IPV4; + pinfo->proto = MB_TCP; + return ret; + } + + // Configuration format: + // "mb_node_tcp_01" + ret = sscanf(buffer, "%m[a-z0-9_]", &phost_str); + if (ret == MB_STR_LEN_HOST) { + + pinfo->node_name_str = (phost_str && strlen(phost_str)) ? phost_str : pinfo->node_name_str; + pinfo->ip_addr_str = (pinfo->node_name_str) ? pinfo->node_name_str : pinfo->ip_addr_str; + pinfo->uid = index; + pinfo->fd = UNDEF_FD; + pinfo->port = CONFIG_FMB_TCP_PORT_DEFAULT; + pinfo->addr_type = MB_IPV4; + pinfo->proto = MB_TCP; + return ret; + } + + return -1; +} + +#ifdef MB_MDNS_IS_INCLUDED + +static int mdns_instance_count = 0; + +inline char *gen_id_str(char *service_name, char *node_id_str) +{ + sprintf(node_id_str, "%s%02X%02X%02X%02X", service_name, MB_ID2STR(MB_DEVICE_ID)); + return node_id_str; +} + +// This function has limitation of working with IP6 address only +esp_err_t port_start_mdns_service(char **ppdns_name, bool is_master, int uid, void *pnode_netif) +{ + char temp_str[HOST_STR_MAX_LEN] = {0}; + esp_ip6_addr_t ip6[LWIP_IPV6_NUM_ADDRESSES]; + int ip6_addrs_count = 0; + esp_err_t err = ESP_ERR_INVALID_STATE; + MB_RETURN_ON_FALSE((pnode_netif), + err, TAG, "Invalid parameters for dns."); + + // initialize mDNS + err = mdns_init(); // if the mdns is already initialized on higher layer, just returns + MB_RETURN_ON_FALSE(((err == ESP_OK) && pnode_netif), err, TAG, "mdns init fail, err = %d.", (int)err); + + esp_netif_t *pnetif = (esp_netif_t*)pnode_netif; + // set mDNS hostname (required if need to advertise services) + err = mdns_hostname_get(temp_str); + if (err != ESP_OK) { + uint8_t mac[6]; + MB_RETURN_ON_FALSE((esp_netif_get_mac(pnetif, mac) == ESP_OK), + err, TAG, "get MAC fail, err = %d.", (int)err); + snprintf(temp_str, HOST_STR_MAX_LEN, "%s_%02x%02x%02x", MB_MDNS_INST_NAME(is_master), mac[3], mac[4], mac[5]); + err = mdns_hostname_set(temp_str); + MB_RETURN_ON_FALSE((err == ESP_OK), + err, TAG, "could not set mdns host name, err = %d.", (int)err); + ESP_LOGI(TAG, "hostname set to: [%s]", temp_str); + err = mdns_instance_name_set(temp_str); + MB_RETURN_ON_FALSE((err == ESP_OK), + err, TAG, "mdns instance name set fail, err = %d.", (int)err); + } + // Check if the default mdns name is defined in the configuration + if (ppdns_name && *ppdns_name && (strlen(*ppdns_name) >= MB_MDNS_STR_MIN_LENGTH)) { + strncpy(temp_str, *ppdns_name, strlen(*ppdns_name)); + *ppdns_name[strlen(*ppdns_name)] = '\0'; + } else { + if (snprintf(temp_str, sizeof(temp_str), "%s_%02X", MB_MDNS_INST_NAME(is_master), uid) <= 0) { + return ESP_ERR_INVALID_STATE; + }; + } + + // Setup the real assigned dns_name instead of constant string + if (ppdns_name) { + *ppdns_name = strdup(temp_str); + } + + esp_netif_ip_info_t ip_info; + mdns_ip_addr_t node_ip; + err = esp_netif_get_ip_info(pnetif, &ip_info); + MB_RETURN_ON_FALSE((err == ESP_OK), + err, TAG, "get IP info fail, err = %d.", (int)err); + node_ip.next = NULL; + node_ip.addr.type = ESP_IPADDR_TYPE_V4; + node_ip.addr.u_addr.ip4 = ip_info.ip; + node_ip.addr.u_addr.ip6.addr[1] = 0; + node_ip.addr.u_addr.ip6.addr[2] = 0; + node_ip.addr.u_addr.ip6.addr[3] = 0; + node_ip.addr.u_addr.ip6.zone = 0; + err = mdns_delegate_hostname_add(temp_str, &node_ip); + MB_RETURN_ON_FALSE((err == ESP_OK), err, TAG, "mdns set delegate name fail, err = %d.", (int)err); + +#if CONFIG_LWIP_IPV6 + // If the IP_V6 addresses supported on current interface, delegate them as well. + ip6_addrs_count = esp_netif_get_all_ip6(pnetif, ip6); + for (int i = 0; i < ip6_addrs_count; ++i) { + ESP_LOGI(TAG, "IPv6 address: " IPV6STR, IPV62STR(ip6[i])); + node_ip.next = NULL; + node_ip.addr.type = ESP_IPADDR_TYPE_V6; + node_ip.addr.u_addr.ip6 = ip6[i]; + err = mdns_delegate_hostname_add(temp_str, &node_ip); + MB_RETURN_ON_FALSE((err == ESP_OK), err, TAG, "mdns set delegate name fail, err = %d.", (int)err); + } +#endif + + // setup dns name for the modbus segment + err = mdns_delegate_hostname_add(MB_MDNS_SEGMENT_NAME, &node_ip); + MB_RETURN_ON_FALSE((err == ESP_OK), err, TAG, "mdns set segment name fail, err = %d.", (int)err); + ESP_LOGI(TAG, "mdns delegate hostname set to: [%s]", temp_str); + ESP_LOGI(TAG, "IP: " IPSTR, IP2STR(&ip_info.ip)); + ESP_LOGI(TAG, "GW: " IPSTR, IP2STR(&ip_info.gw)); + ESP_LOGI(TAG, "NETMASK: " IPSTR, IP2STR(&ip_info.netmask)); + + mdns_instance_count++; + + return ESP_OK; +} + +void port_stop_mdns_service(char **ppdns_name) +{ + if (ppdns_name && *ppdns_name) { + mdns_delegate_hostname_remove(*ppdns_name); + free(*ppdns_name); + *ppdns_name = NULL; + if (mdns_instance_count) { + mdns_instance_count--; + } + } + if (!mdns_instance_count) { + mdns_delegate_hostname_remove(MB_MDNS_SEGMENT_NAME); + mdns_service_remove_all(); + mdns_free(); + } +} + +// char *port_get_node_ip_str(mdns_ip_addr_t *address, mb_addr_type_t addr_type) +// { +// mdns_ip_addr_t *a = address; +// char *node_ip_str = NULL; + +// while (a) { +// if ((a->addr.type == ESP_IPADDR_TYPE_V6) && (addr_type == MB_IPV6)) { +// if (-1 == asprintf(&node_ip_str, IPV6STR, IPV62STR(a->addr.u_addr.ip6))) { +// abort(); +// } +// } else if ((a->addr.type == ESP_IPADDR_TYPE_V4) && (addr_type == MB_IPV4)) { +// if (-1 == asprintf(&node_ip_str, IPSTR, IP2STR(&(a->addr.u_addr.ip4)))) { +// abort(); +// } +// } +// if (node_ip_str) { +// break; +// } +// a = a->next; +// } +// return node_ip_str; +// } + +// The helper to resolve host name based on service query (not used for now) +// esp_err_t port_resolve_slave(uint8_t short_addr, mdns_result_t *result, char **resolved_ip, +// mb_addr_type_t addr_type) +// { +// if (!short_addr || !result || !resolved_ip) { +// return ESP_ERR_INVALID_ARG; +// } +// mdns_result_t *r = result; +// int t; +// char *node_ip = NULL; +// char node_name[22] = {0}; + +// if (sprintf(node_name, "mb_node_tcp_%02X", short_addr) < 0) { +// ESP_LOGE(TAG, "Fail to create instance name for index: %d", short_addr); +// abort(); +// } +// for (; r; r = r->next) { +// if ((r->ip_protocol == MDNS_IP_PROTOCOL_V4) && (addr_type == MB_IPV6)) { +// continue; +// } else if ((r->ip_protocol == MDNS_IP_PROTOCOL_V6) && (addr_type == MB_IPV4)) { +// continue; +// } +// // Check host name for Modbus short address and +// // append it into slave ip address table +// if ((strcmp(r->instance_name, node_name) == 0) && (r->port == CONFIG_FMB_TCP_PORT_DEFAULT)) { +// printf(" PTR : %s\n", r->instance_name); +// if (r->txt_count) { +// printf(" TXT : [%u] ", r->txt_count); +// for (t = 0; t < r->txt_count; t++) { +// printf("%s=%s; ", r->txt[t].key, r->txt[t].value ? r->txt[t].value : "NULL"); +// } +// printf("\n"); +// } +// node_ip = port_get_node_ip_str(r->addr, addr_type); +// if (node_ip) { +// ESP_LOGI(TAG, "Resolved slave %s[%s]:%u", r->hostname, node_ip, r->port); +// *resolved_ip = node_ip; +// return ESP_OK; +// } +// } +// } +// *resolved_ip = NULL; +// ESP_LOGD(TAG, "Fail to resolve slave: %s", node_name); +// return ESP_ERR_NOT_FOUND; +// } + +int port_resolve_mdns_host(const char *host_name, char **paddr_str) +{ + ESP_LOGD(TAG, "Query A: %s.local", host_name); + + esp_ip_addr_t addr; + char *pstr = NULL; + bzero(&addr, sizeof(esp_ip_addr_t)); + + // Try to send query to obtain the IPv4 address + esp_err_t err = mdns_query_a(host_name, MB_MDNS_QUERY_TIME_MS, &addr.u_addr.ip4); + if (err) { + if (err == ESP_ERR_NOT_FOUND){ + // Try to resolve using AAAA query + err = mdns_query_aaaa(host_name, MB_MDNS_QUERY_TIME_MS, &addr.u_addr.ip6); + if (err == ESP_ERR_NOT_FOUND) { + ESP_LOGE(TAG, "Host: %s, was not resolved!", host_name); + return -1; + } + addr.type = ESP_IPADDR_TYPE_V6; + if (asprintf(&pstr, IPV6STR, IPV62STR(addr.u_addr.ip6) == -1)) { + abort(); + } + } else { + return -1; + } + } else { + addr.type = ESP_IPADDR_TYPE_V4; + if (asprintf(&pstr, IPSTR, IP2STR(&addr.u_addr.ip4)) == -1) { + abort(); + } + } + if (paddr_str) { + ESP_LOGD(TAG, "Host: %s, was resolved with IP: %s", host_name, pstr); + *paddr_str = pstr; + } + return strlen(pstr); +} + +#endif // #ifdef MB_MDNS_IS_INCLUDED + +// Create a listening socket on pbind_ip, with ip address_type, protocol type and port +int port_bind_addr(const char *pbind_ip, mb_addr_type_t addr_type, mb_comm_mode_t proto, uint16_t port) +{ + int temp_par, ret; + int listen_sock_fd = -1; + struct addrinfo hint; + struct addrinfo *paddr_list; + struct addrinfo *pcur_addr; + char* pstr = NULL; + + memset(&hint, 0, sizeof(hint)); + + // Bind to IPv6 and/or IPv4, but only in the desired protocol + // Todo: Find a reason why AF_UNSPEC does not work for IPv6 + hint.ai_family = AF_UNSPEC; + //hint.ai_family = (addr_type == MB_IPV4) ? AF_INET : AF_INET6; + hint.ai_socktype = (proto == MB_UDP) ? SOCK_DGRAM : SOCK_STREAM; + // The LWIP has an issue when connection to IPv6 socket + hint.ai_protocol = (proto == MB_UDP) ? IPPROTO_UDP : IPPROTO_TCP; + hint.ai_flags = AI_NUMERICSERV | AI_PASSIVE | AI_CANONNAME; + + if (asprintf(&pstr, "%u", port) == -1) { + abort(); + } + + ret = getaddrinfo(pbind_ip, pstr, &hint, &paddr_list); + free(pstr); + + if ((ret != 0) ) { + return -1; + } + + // Try the sockaddr until a binding succeeds + for (pcur_addr = paddr_list; pcur_addr != NULL; pcur_addr = pcur_addr->ai_next) + { + listen_sock_fd = (int)socket(pcur_addr->ai_family, pcur_addr->ai_socktype, + pcur_addr->ai_protocol); + if (listen_sock_fd < 0) + { + continue; + } + + temp_par = 1; + // Allow multi client connections + if (setsockopt(listen_sock_fd, SOL_SOCKET, SO_REUSEADDR, + (const char*)&temp_par, sizeof(temp_par)) != 0) + { + close(listen_sock_fd); + listen_sock_fd = UNDEF_FD; + continue; + } + + if (bind(listen_sock_fd, (struct sockaddr *)pcur_addr->ai_addr, + (socklen_t)pcur_addr->ai_addrlen) != 0 ) + { + close(listen_sock_fd); + listen_sock_fd = UNDEF_FD; + continue; + } + + // Listen only makes sense for TCP + if (proto == MB_TCP) + { + if (listen(listen_sock_fd, MB_TCP_NET_LISTEN_BACKLOG) != 0) + { + ESP_LOGE(TAG, "Error occurred during listen: errno=%u", (unsigned)errno); + close(listen_sock_fd); + listen_sock_fd = UNDEF_FD; + continue; + } + } + // Bind was successful + pstr = (pcur_addr->ai_canonname == NULL) ? (char *)"\0" : pcur_addr->ai_canonname; + ESP_LOGI(TAG, "Socket (#%d), listener %s on port: %u, errno=%u", + (int)listen_sock_fd, pstr, (unsigned)port, (unsigned)errno); + break; + } + + freeaddrinfo(paddr_list); + return(listen_sock_fd); +} + +int port_accept_connection(int listen_sock_id, mb_uid_info_t *pinfo) +{ + MB_RETURN_ON_FALSE((pinfo), -1, TAG, "Wrong parameter pointer."); + MB_RETURN_ON_FALSE((listen_sock_id > 0), -1, TAG, "Incorrect listen socket ID."); + + // Address structure large enough for both IPv4 or IPv6 address + struct sockaddr_storage src_addr; + char addr_str[128]; + int sock_id = UNDEF_FD; + char *paddr = NULL; + socklen_t addr_size = sizeof(struct sockaddr_storage); + bzero(&src_addr, sizeof(struct sockaddr_storage)); + + // Accept new socket connection if not active + sock_id = accept(listen_sock_id, (struct sockaddr *)&src_addr, &addr_size); + if (sock_id < 0) { + ESP_LOGE(TAG, "Unable to accept connection: errno=%u", (unsigned)errno); + close(sock_id); + } else { + // Get the sender's ip address as string + if (src_addr.ss_family == PF_INET) { + inet_ntoa_r(((struct sockaddr_in *)&src_addr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1); + pinfo->port = ntohs(((struct sockaddr_in *)&src_addr)->sin_port); + pinfo->addr_type = MB_IPV4; + } +#if CONFIG_LWIP_IPV6 + else if (src_addr.ss_family == PF_INET6) { + inet6_ntoa_r(((struct sockaddr_in6 *)&src_addr)->sin6_addr, addr_str, sizeof(addr_str) - 1); + pinfo->port = ntohs(((struct sockaddr_in6 *)&src_addr)->sin6_port); + pinfo->addr_type = MB_IPV6; + } +#endif + else { + // Make sure ss_family is valid + abort(); + } + ESP_LOGI(TAG, "Socket (#%d), accept client connection from address[port]: %s[%d]", (int)sock_id, addr_str, pinfo->port); + paddr = strdup(addr_str); + if (paddr) { + pinfo->fd = sock_id; + pinfo->ip_addr_str = paddr; + pinfo->node_name_str = paddr; + pinfo->proto = MB_TCP; + pinfo->uid = 0; + } + } + return sock_id; +} + +#endif + +#ifdef __cplusplus +} +#endif diff --git a/modbus/mb_ports/tcp/port_tcp_utils.h b/modbus/mb_ports/tcp/port_tcp_utils.h new file mode 100644 index 0000000..09ce892 --- /dev/null +++ b/modbus/mb_ports/tcp/port_tcp_utils.h @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "lwip/err.h" +#include "lwip/sockets.h" +#include "lwip/netdb.h" +#include "esp_netif.h" +#include "net/if.h" + +#include "port_tcp_common.h" + +#if __has_include("esp_timer.h") +#include "esp_timer.h" +#endif + +#if __has_include("esp_mac.h") +#include "esp_mac.h" +#endif + +#if __has_include("mdns.h") +#include "mdns.h" +#endif + +// Workaround for MDNS_NAME_BUF_LEN being defined in private header +#ifndef MDNS_NAME_BUF_LEN +#define MDNS_NAME_BUF_LEN 64 +#endif + +#define HOST_STR_MAX_LEN (MDNS_NAME_BUF_LEN) +#define MB_TCP_NET_LISTEN_BACKLOG (SOMAXCONN) + +#if MB_MDNS_IS_INCLUDED + +#define MB_ID_BYTE0(id) ((uint8_t)(id)) +#define MB_ID_BYTE1(id) ((uint8_t)(((uint16_t)(id) >> 8) & 0xFF)) +#define MB_ID_BYTE2(id) ((uint8_t)(((uint32_t)(id) >> 16) & 0xFF)) +#define MB_ID_BYTE3(id) ((uint8_t)(((uint32_t)(id) >> 24) & 0xFF)) + +#define MB_ID2STR(id) MB_ID_BYTE0(id), MB_ID_BYTE1(id), MB_ID_BYTE2(id), MB_ID_BYTE3(id) + +#if CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT +#define MB_DEVICE_ID (uint32_t)CONFIG_FMB_CONTROLLER_SLAVE_ID +#endif + +// #define MB_SLAVE_ADDR (CONFIG_MB_SLAVE_ADDR) + +#endif + +#define MB_MDNS_PORT (CONFIG_FMB_TCP_PORT_DEFAULT) +#define MB_READ_TICK (500) +#define MB_MDNS_QUERY_TIME_MS (2000) + +#define MB_STR_LEN_HOST 1 // "mb_node_tcp_01" +#define MB_STR_LEN_IDX_HOST 2 // "12:mb_node_tcp_01" +#define MB_STR_LEN_IDX_HOST_PORT 3 // "01:mb_node_tcp_01:1502" +#define MB_STR_LEN_IP4_ONLY 4 // "192.168.1.1" +#define MB_STR_LEN_IDX_IP4 5 // "1:192.168.1.1" +#define MB_STR_LEN_IDX_IP4_PORT 6 // "1:192.168.1.1:502" +#define MB_STR_LEN_IP6_ONLY 8 // "2001:0db8:85a3:0000:0000:8a2e:0370:7334" +#define MB_STR_LEN_IDX_IP6 9 // "12:2001:0db8:85a3:0000:0000:8a2e:0370:7334" +#define MB_STR_LEN_IDX_IP6_PORT 10 // "12:2001:0db8:85a3:0000:0000:8a2e:0370:7334:502" + +#define MB_MDNS_STR_MIN_LENGTH 10 // "mb_node_01" +#define MB_MDNS_SEGMENT_NAME "mb_tcp_segment" // "mb_node_01" + +#define MB_MDNS_INST_NAME(is_master) (__extension__( \ +{ \ + ((is_master) ? "mb_master_tcp" : "mb_slave_tcp"); \ +} \ +)) + +typedef struct _frame_queue_entry frame_entry_t; +typedef struct _mb_node_info mb_node_info_t; +typedef enum _addr_type_enum mb_tcp_addr_type_t; + +bool port_check_host_addr(const char *host_str, ip_addr_t* host_addr); +mb_node_info_t* port_get_current_info(void *ctx); +void port_check_shutdown(void *ctx); +int64_t port_get_resp_time_left(mb_node_info_t* pinfo); +int port_enqueue_packet(QueueHandle_t queue, uint8_t *pbuf, uint16_t len); +int port_dequeue_packet(QueueHandle_t queue, frame_entry_t* pframe_info); +int port_read_packet(mb_node_info_t* pinfo); +err_t port_set_blocking(mb_node_info_t* pinfo, bool is_blocking); +void port_keep_alive(mb_node_info_t* pinfo); +err_t port_check_alive(mb_node_info_t* pinfo, uint32_t timeout_ms); +err_t port_connect(void *ctx, mb_node_info_t* pinfo); +bool port_close_connection(mb_node_info_t* pinfo); +int port_write_poll(mb_node_info_t* pinfo, const uint8_t *pframe, uint16_t frame_len, uint32_t timeout); +int64_t port_get_timestamp(void); + +typedef struct _uid_info mb_uid_info_t; + +int port_scan_addr_string(char *buffer, mb_uid_info_t *pnode_info); + +#if MB_MDNS_IS_INCLUDED + +// Init mdns service +esp_err_t port_start_mdns_service(char **ppdns_name, bool is_master, int uid, void *pnode_netif); +void port_stop_mdns_service(char **ppdns_name); + +typedef struct mdns_ip_addr_s mdns_ip_addr_t; +typedef struct mdns_result_s mdns_result_t; + +char *port_get_node_ip_str(mdns_ip_addr_t *address, mb_addr_type_t addr_type); +esp_err_t port_resolve_slave(uint8_t short_addr, mdns_result_t *result, char **resolved_ip, mb_addr_type_t addr_type); +int port_resolve_mdns_host(const char *host_name, char **paddr_str); + +#endif + +// Modbus slave utility functions + +int port_bind_addr(const char *pbind_ip, mb_addr_type_t addr_type, mb_comm_mode_t proto, uint16_t port); +int port_accept_connection(int listen_sock_id, mb_uid_info_t *pnode_info); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_transports/ascii/ascii_lrc.c b/modbus/mb_transports/ascii/ascii_lrc.c new file mode 100644 index 0000000..078ef4c --- /dev/null +++ b/modbus/mb_transports/ascii/ascii_lrc.c @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "ascii_lrc.h" + +/* ----------------------- functions ---------------------------------*/ +uint8_t mb_char2bin(uint8_t char_val) +{ + if ((char_val >= '0') && (char_val <= '9')) { + return (uint8_t)(char_val - '0'); + } else if ((char_val >= 'A') && (char_val <= 'F')) { + return (uint8_t)(char_val - 'A' + 0x0A); + } else { + return 0xFF; + } +} + +uint8_t mb_bin2char(uint8_t byte_val) +{ + if (byte_val <= 0x09) { + return (uint8_t)('0' + byte_val); + } else if ((byte_val >= 0x0A) && (byte_val <= 0x0F)) { + return (uint8_t)(byte_val - 0x0A + 'A'); + } else { + /* Programming error. */ + assert(0); + } + return '0'; +} + +uint8_t __attribute__ ((unused)) mb_lrc(uint8_t *pframe, uint16_t length) +{ + uint8_t lrc = 0; /* LRC char initialized */ + + while (length--) { + lrc += *pframe++; /* Add buffer byte without carry */ + } + + /* Return twos complement */ + lrc = (uint8_t)(-((char)lrc)); + return lrc; +} + +// The helper function to fill ASCII frame buffer +int mb_ascii_set_buf(const uint8_t *pdata, uint8_t *pbuf, int bin_length) +{ + int bin_idx = 0; + int frm_idx = 0; + uint8_t lrc = 0; + + assert(pdata && pbuf); + + pbuf[0] = MB_ASCII_START; + for (frm_idx = 1; (bin_idx < bin_length); bin_idx++) { + pbuf[frm_idx++] = mb_bin2char((uint8_t)(pdata[bin_idx] >> 4)); // High nibble + pbuf[frm_idx++] = mb_bin2char((uint8_t)(pdata[bin_idx] & 0X0F)); // Low nibble + lrc += pdata[bin_idx]; + } + lrc = (uint8_t)(-((char)lrc)); + pbuf[frm_idx++] = mb_bin2char((uint8_t)(lrc >> 4)); + pbuf[frm_idx++] = mb_bin2char((uint8_t)(lrc & 0X0F)); + pbuf[frm_idx++] = MB_ASCII_CR; + pbuf[frm_idx++] = MB_ASCII_LF; + + return frm_idx; +} + +int mb_ascii_get_binary_buf(uint8_t *pdata, int length) +{ + int bin_idx = 0; + uint8_t lrc = 0; + + assert(pdata); + + if ((pdata[0] == ':') && (pdata[length - 1] == '\n') && (pdata[length - 2] == '\r')) { + for (int str_idx = 1; (str_idx < length) && (pdata[str_idx] > ' '); str_idx += 2) { + pdata[bin_idx] = (mb_char2bin(pdata[str_idx]) << 4); // High nibble + pdata[bin_idx] |= mb_char2bin(pdata[str_idx + 1]); // Low nibble + lrc += pdata[bin_idx++]; + } + } + + lrc = (uint8_t)(-((char)lrc)); + bin_idx = ((lrc == 0) && (bin_idx == ((length - 3) >> 1))) ? bin_idx : -1; + return bin_idx; +} + diff --git a/modbus/mb_transports/ascii/ascii_lrc.h b/modbus/mb_transports/ascii/ascii_lrc.h new file mode 100644 index 0000000..b0e1563 --- /dev/null +++ b/modbus/mb_transports/ascii/ascii_lrc.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#define MB_ASCII_CR '\r' /*!< Default CR character for Modbus ASCII. */ +#define MB_ASCII_LF '\n' /*!< Default LF character for Modbus ASCII. */ +#define MB_ASCII_START ':' /*!< Start of frame for Modbus ASCII. */ + +/* ----------------------- Static functions ---------------------------------*/ +uint8_t mb_char2bin(uint8_t char_val); +uint8_t mb_bin2char(uint8_t byte_val); +uint8_t mb_lrc(uint8_t *frame_ptr, uint16_t len_buf); +int mb_ascii_get_binary_buf(uint8_t *pdata, int length); +int mb_ascii_set_buf(const uint8_t *pdata, uint8_t *pbuf, int bin_length); \ No newline at end of file diff --git a/modbus/mb_transports/ascii/ascii_master.c b/modbus/mb_transports/ascii/ascii_master.c new file mode 100644 index 0000000..4861710 --- /dev/null +++ b/modbus/mb_transports/ascii/ascii_master.c @@ -0,0 +1,287 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "ascii_transport.h" +#include "port_serial_common.h" +#include "port_common.h" + +#include "mb_config.h" + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN) + +static const char *TAG = "mb_transp.ascii_master"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + uint8_t snd_buf[MB_ASCII_SER_PDU_SIZE_MAX]; + uint8_t rcv_buf[MB_ASCII_SER_PDU_SIZE_MAX]; + uint8_t *pascii_puf; + uint16_t snd_pdu_len; + uint8_t *snd_buf_cur; + uint16_t snd_buf_cnt; + uint16_t rcv_buf_pos; + bool frame_is_broadcast; + volatile mb_timer_mode_enum_t cur_timer_mode; +} mbm_ascii_trasp_t; + +static mb_err_enum_t mbm_ascii_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +static mb_err_enum_t mbm_ascii_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +static void mbm_ascii_transp_start(mb_trans_base_t *inst); +static void mbm_ascii_transp_stop(mb_trans_base_t *inst); +static void mbm_ascii_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static void mbm_ascii_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static bool mbm_ascii_transp_timer_expired(void *inst); +static bool mbm_ascii_transp_rq_is_bcast(mb_trans_base_t *inst); + +mb_err_enum_t mbm_ascii_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_inst), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mbm_ascii_trasp_t *transp = NULL; + transp = (mbm_ascii_trasp_t *)calloc(1, sizeof(mbm_ascii_trasp_t)); + transp->pascii_puf = calloc(1, MB_ASCII_SER_PDU_SIZE_MAX); + MB_RETURN_ON_FALSE((transp && transp->pascii_puf), MB_EILLSTATE, TAG, "no mem for ascii master transport instance."); + CRITICAL_SECTION_INIT(transp->base.lock); + CRITICAL_SECTION_LOCK(transp->base.lock); + transp->base.frm_rcv = mbm_ascii_transp_receive; + transp->base.frm_send = mbm_ascii_transp_send; + transp->base.frm_start = mbm_ascii_transp_start; + transp->base.frm_stop = mbm_ascii_transp_stop; + transp->base.get_rx_frm = mbm_ascii_transp_get_rcv_buf; + transp->base.get_tx_frm = mbm_ascii_transp_get_snd_buf; + transp->base.frm_delete = mbm_ascii_transp_delete; + transp->base.frm_is_bcast = mbm_ascii_transp_rq_is_bcast; + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mb_port_ser_create(ser_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "serial port creation, err: %d", ret); + ret = mb_port_timer_create(port_obj, (MB_ASCII_TIMEOUT_MS * MB_TIMER_TICS_PER_MS)); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "timer port creation, err: %d", ret); + // Override default response time if defined + if (ser_opts->response_tout_ms) { + mb_port_timer_set_response_time(port_obj, ser_opts->response_tout_ms); + } + ret = mb_port_event_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "event port creation, err: %d", ret); + transp->base.port_obj = port_obj; + // Set callback function pointer for the timer + port_obj->cb.tmr_expired = mbm_ascii_transp_timer_expired; + port_obj->cb.tx_empty = NULL; + port_obj->cb.byte_rcvd = NULL; + port_obj->arg = (void *)transp; + transp->port_obj = port_obj; + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + return MB_ENOERR; + +error: + free((void *)transp->pascii_puf); + transp->pascii_puf = NULL; + if (port_obj) { + free(port_obj->event_obj); + free(port_obj->timer_obj); + } + free(port_obj); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + +bool mbm_ascii_transp_delete(mb_trans_base_t *inst) +{ + mbm_ascii_trasp_t *trans = __containerof(inst, mbm_ascii_trasp_t, base); + mb_port_timer_delete(trans->base.port_obj); + mb_port_event_delete(trans->base.port_obj); + mb_port_ser_delete(trans->base.port_obj); + free((void *)trans->pascii_puf); + CRITICAL_SECTION_CLOSE(inst->lock); + free(trans); + return true; +} + +static void mbm_ascii_transp_start(mb_trans_base_t *inst) +{ + mbm_ascii_trasp_t *transp = __containerof(inst, mbm_ascii_trasp_t, base); + + CRITICAL_SECTION(inst->lock) { + mb_port_ser_enable(inst->port_obj); + mb_port_timer_enable(inst->port_obj); + }; + + /* No special startup required for ASCII. */ + (void)mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); +} + +static void mbm_ascii_transp_stop(mb_trans_base_t *inst) +{ + CRITICAL_SECTION(inst->lock) { + mb_port_ser_disable(inst->port_obj); + mb_port_timer_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbm_ascii_transp_receive(mb_trans_base_t *inst, uint8_t *prcv_addr, uint8_t **ppframe_buf, uint16_t *pbuf_len) +{ + mbm_ascii_trasp_t *transp = __containerof(inst, mbm_ascii_trasp_t, base); + mb_err_enum_t status = MB_ENOERR; + + if (!pbuf_len) { + return MB_EIO; + } + + uint8_t *pbuf = (uint8_t *)transp->rcv_buf; + uint16_t length = *pbuf_len; + + if (mb_port_ser_recv_data(inst->port_obj, &pbuf, &length) == false) + { + return MB_EPORTERR; + } + + assert(length < MB_ASCII_SER_PDU_SIZE_MAX); + + // Convert the received ascii frame buffer to the binary representation + int ret = mb_ascii_get_binary_buf(pbuf, length); + + /* Check length and LRC checksum */ + if (ret >= MB_ASCII_SER_PDU_SIZE_MIN) { + /* Save the address field. All frames are passed to the upper layed + * and the decision if a frame is used is done there. + */ + *prcv_addr = pbuf[MB_SER_PDU_ADDR_OFF]; + + /* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus + * size of address field and LRC checksum. + */ + *pbuf_len = (uint16_t)(ret - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_LRC); + transp->rcv_buf_pos = ret; + + /* Return the start of the Modbus PDU to the caller. */ + *ppframe_buf = (uint8_t *)&pbuf[MB_SER_PDU_PDU_OFF]; + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbm_ascii_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t frame_len) +{ + mbm_ascii_trasp_t *transp = __containerof(inst, mbm_ascii_trasp_t, base); + mb_err_enum_t status = MB_ENOERR; + + if (slv_addr > MB_MASTER_TOTAL_SLAVE_NUM) { + return MB_EINVAL; + } + + if (frame_ptr && frame_len) { + /* First byte before the Modbus-PDU is the slave address. */ + transp->snd_buf_cur = (uint8_t *)frame_ptr - 1; + transp->snd_buf_cnt = 1; + + /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */ + transp->snd_buf_cur[MB_SER_PDU_ADDR_OFF] = slv_addr; + transp->snd_buf_cnt += frame_len; + + /* Prepare the ASCII buffer and send it to port */ + int ascii_len = mb_ascii_set_buf(transp->snd_buf_cur, (uint8_t *)transp->pascii_puf, transp->snd_buf_cnt); + if (ascii_len > MB_ASCII_SER_PDU_SIZE_MIN) { + bool ret = mb_port_ser_send_data(inst->port_obj, (uint8_t *)transp->pascii_puf, ascii_len); + if (!ret) { + return MB_EPORTERR; + } + transp->frame_is_broadcast = (slv_addr == MB_ADDRESS_BROADCAST) ? true : false; + // If the frame is broadcast, master will enable timer of convert delay, + // else master will enable timer of respond timeout. */ + if (transp->frame_is_broadcast) { + mb_port_timer_convert_delay_enable(transp->base.port_obj); + } else { + mb_port_timer_respond_timeout_enable(transp->base.port_obj); + } + } else { + status = MB_EIO; + } + } else { + status = MB_EIO; + } + return status; +} + +// The receive fsm function (not implemented for this transport) +__attribute__((unused)) +static bool mbm_ascii_transp_rcv_fsm(mb_trans_base_t *inst) +{ + return false; +} + +// The send fsm function (not implemented for this transport) +__attribute__((unused)) +static bool mbm_ascii_transp_snd_fsm(mb_trans_base_t *inst) +{ + return false; +} + +// The timer expired function +static bool mbm_ascii_transp_timer_expired(void *inst) +{ + mbm_ascii_trasp_t *transp = __containerof(inst, mbm_ascii_trasp_t, base); + + bool need_poll = false; + mb_timer_mode_enum_t timer_mode = mb_port_get_cur_timer_mode(transp->base.port_obj); + + mb_port_timer_disable(transp->base.port_obj); + + switch(timer_mode) { + case MB_TMODE_T35: + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + ESP_EARLY_LOGD(TAG, "%p:EV_READY", transp->base.descr.parent); + break; + + case MB_TMODE_RESPOND_TIMEOUT: + mb_port_event_set_err_type(transp->base.port_obj, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_ERROR_PROCESS)); + ESP_EARLY_LOGD(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", transp->base.descr.parent); + break; + + case MB_TMODE_CONVERT_DELAY: + /* If timer mode is convert delay, the master event then turns EV_MASTER_EXECUTE status. */ + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_EXECUTE)); + ESP_EARLY_LOGD(TAG, "%p:MB_TMODE_CONVERT_DELAY", transp->base.descr.parent); + break; + + default: + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + break; + } + + return need_poll; +} + +static void mbm_ascii_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_ascii_trasp_t *transp = __containerof(inst, mbm_ascii_trasp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->rcv_buf[MB_PDU_FUNC_OFF]; + } +} + +static void mbm_ascii_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_ascii_trasp_t *transp = __containerof(inst, mbm_ascii_trasp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->snd_buf[MB_ASCII_SER_PDU_PDU_OFF]; + } +} + +static bool mbm_ascii_transp_rq_is_bcast(mb_trans_base_t *inst) +{ + mbm_ascii_trasp_t *transp = __containerof(inst, mbm_ascii_trasp_t, base); + return transp->frame_is_broadcast; +} + +#endif \ No newline at end of file diff --git a/modbus/mb_transports/ascii/ascii_slave.c b/modbus/mb_transports/ascii/ascii_slave.c new file mode 100644 index 0000000..812d430 --- /dev/null +++ b/modbus/mb_transports/ascii/ascii_slave.c @@ -0,0 +1,238 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "ascii_transport.h" +#include "port_serial_common.h" + +#include "sdkconfig.h" + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN) + +static const char *TAG = "mb_transp.ascii_slave"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + + // private properties + volatile uint8_t pdu_buf[MB_ASCII_SER_PDU_SIZE_MAX]; + uint8_t *rcv_buf; + uint8_t *pascii_puf; + uint16_t snd_pdu_len; + uint8_t *snd_buf_cur; + uint16_t snd_buf_cnt; + uint16_t rcv_buf_pos; + volatile mb_timer_mode_enum_t cur_timer_mode; +} mbs_ascii_trasp_t; + +mb_err_enum_t mbs_ascii_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +static void mbs_ascii_transp_start(mb_trans_base_t *inst); +static void mbs_ascii_transp_stop(mb_trans_base_t *inst); +static mb_err_enum_t mbs_ascii_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +static mb_err_enum_t mbs_ascii_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +static bool mbs_ascii_transp_rcv_fsm(mb_trans_base_t *inst); +static bool mbs_ascii_transp_snd_fsm(mb_trans_base_t *inst); +static bool mbs_ascii_transp_timer_expired(void *inst); +void mbs_ascii_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static void mbs_ascii_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); + +mb_err_enum_t mbs_ascii_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_inst), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mbs_ascii_trasp_t *transp = NULL; + transp = (mbs_ascii_trasp_t *)calloc(1, sizeof(mbs_ascii_trasp_t)); + MB_RETURN_ON_FALSE(transp, MB_EILLSTATE, TAG, "no mem for the %s instance.", TAG); + transp->pascii_puf = calloc(1, MB_ASCII_SER_PDU_SIZE_MAX); + MB_RETURN_ON_FALSE((transp && transp->pascii_puf), MB_EILLSTATE, TAG, "no mem for the %s instance.", TAG); + CRITICAL_SECTION_INIT(transp->base.lock); + transp->base.frm_rcv = mbs_ascii_transp_receive; + transp->base.frm_send = mbs_ascii_transp_send; + transp->base.frm_start = mbs_ascii_transp_start; + transp->base.frm_stop = mbs_ascii_transp_stop; + transp->base.get_rx_frm = mbs_ascii_transp_get_rcv_buf; + transp->base.get_tx_frm = mbs_ascii_transp_get_snd_buf; + transp->base.frm_delete = mbs_ascii_transp_delete; + transp->base.frm_is_bcast = NULL; + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mb_port_ser_create(ser_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "serial port creation, err: %d", ret); + ret = mb_port_timer_create(port_obj, (MB_ASCII_TIMEOUT_MS * MB_TIMER_TICS_PER_MS)); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "timer port creation, err: %d", ret); + ret = mb_port_event_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "event port creation, err: %d", ret); + transp->base.port_obj = port_obj; + transp->rcv_buf = (uint8_t *)&transp->pdu_buf[0]; + // Set callback function pointer for the timer + port_obj->cb.tmr_expired = mbs_ascii_transp_timer_expired; + port_obj->cb.tx_empty = NULL; + port_obj->cb.byte_rcvd = NULL; + port_obj->arg = (void *)transp; + transp->port_obj = port_obj; + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + return MB_ENOERR; + +error: + free(transp->pascii_puf); + transp->pascii_puf = NULL; + if (port_obj) { + free(port_obj->event_obj); + free(port_obj->timer_obj); + } + free(port_obj); + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + +bool mbs_ascii_transp_delete(mb_trans_base_t *inst) +{ + mbs_ascii_trasp_t *transp = __containerof(inst, mbs_ascii_trasp_t, base); + mb_port_timer_delete(transp->base.port_obj); + mb_port_event_delete(transp->base.port_obj); + mb_port_ser_delete(transp->base.port_obj); + free(transp->pascii_puf); + CRITICAL_SECTION_CLOSE(inst->lock); + free(transp); + return true; +} + +static void mbs_ascii_transp_start(mb_trans_base_t *inst) +{ + mbs_ascii_trasp_t *transp = __containerof(inst, mbs_ascii_trasp_t, base); + CRITICAL_SECTION(inst->lock) { + mb_port_ser_enable(inst->port_obj); + mb_port_timer_enable(inst->port_obj); + }; + + /* No special startup required for ASCII. */ + (void)mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); +} + +static void mbs_ascii_transp_stop(mb_trans_base_t *inst) +{ + CRITICAL_SECTION(inst->lock) { + mb_port_ser_disable(inst->port_obj); + mb_port_timer_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbs_ascii_transp_receive(mb_trans_base_t *inst, uint8_t *prcv_addr, uint8_t **ppframe_buf, uint16_t *pbuf_len) +{ + mbs_ascii_trasp_t *transp = __containerof(inst, mbs_ascii_trasp_t, base); + mb_err_enum_t status = MB_ENOERR; + + assert(transp->rcv_buf); + uint8_t *pbuf = (uint8_t *)transp->rcv_buf; + uint16_t length = *pbuf_len; + + if (mb_port_ser_recv_data(inst->port_obj, &pbuf, &length) == false) { + return MB_EPORTERR; + } + + assert(length < MB_ASCII_SER_PDU_SIZE_MAX); + + // Convert the received ascii frame buffer to the binary representation + int ret = mb_ascii_get_binary_buf(pbuf, length); + + /* Check length and CRC checksum */ + if (ret >= MB_ASCII_SER_PDU_SIZE_MIN) { + /* Save the address field. All frames are passed to the upper layed + * and the decision if a frame is used is done there. + */ + *prcv_addr = pbuf[MB_SER_PDU_ADDR_OFF]; + + /* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus + * size of address field and LRC checksum. + */ + *pbuf_len = (uint16_t)(ret - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_LRC); + transp->rcv_buf_pos = ret; + + /* Return the start of the Modbus PDU to the caller. */ + *ppframe_buf = (uint8_t *)&pbuf[MB_SER_PDU_PDU_OFF]; + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbs_ascii_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t frame_len) +{ + mbs_ascii_trasp_t *transp = __containerof(inst, mbs_ascii_trasp_t, base); + mb_err_enum_t status = MB_ENOERR; + + if (slv_addr > MB_MASTER_TOTAL_SLAVE_NUM) { + return MB_EINVAL; + } + + if (frame_ptr && frame_len) { + /* First byte before the Modbus-PDU is the slave address. */ + transp->snd_buf_cur = (uint8_t *)frame_ptr - 1; + transp->snd_buf_cnt = 1; + + /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */ + transp->snd_buf_cur[MB_SER_PDU_ADDR_OFF] = slv_addr; + transp->snd_buf_cnt += frame_len; + + /* Prepare the ASCII buffer and send it to port */ + int ascii_len = mb_ascii_set_buf(transp->snd_buf_cur, (uint8_t *)transp->pascii_puf, transp->snd_buf_cnt); + if (ascii_len > MB_ASCII_SER_PDU_SIZE_MIN) { + bool ret = mb_port_ser_send_data(inst->port_obj, (uint8_t *)transp->pascii_puf, ascii_len); + if (!ret) { + return MB_EPORTERR; + } + } else { + status = MB_EIO; + } + } else { + status = MB_EIO; + } + return status; +} + +__attribute__((unused)) +static bool mbs_ascii_transp_rcv_fsm(mb_trans_base_t *inst) +{ + return false; +} + +__attribute__((unused)) +static bool mbs_ascii_transp_snd_fsm(mb_trans_base_t *inst) +{ + return false; +} + +static bool mbs_ascii_transp_timer_expired(void *inst) +{ + mbs_ascii_trasp_t *transp = __containerof(inst, mbs_ascii_trasp_t, base); + + mb_port_timer_disable(transp->base.port_obj); + return false; +} + +void mbs_ascii_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbs_ascii_trasp_t *transp = __containerof(inst, mbs_ascii_trasp_t, base); + + assert(transp->rcv_buf); + + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->rcv_buf[MB_PDU_FUNC_OFF]; + } +} + +static void mbs_ascii_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbs_ascii_trasp_t *transp = __containerof(inst, mbs_ascii_trasp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->pdu_buf[MB_PDU_FUNC_OFF]; + } +} + +#endif \ No newline at end of file diff --git a/modbus/mb_transports/ascii/ascii_transport.h b/modbus/mb_transports/ascii/ascii_transport.h new file mode 100644 index 0000000..128cf76 --- /dev/null +++ b/modbus/mb_transports/ascii/ascii_transport.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "mb_config.h" +#include "mb_common.h" +#include "mb_types.h" +#include "mb_frame.h" +#include "mb_proto.h" +#include "transport_common.h" +#include "port_common.h" +#include "ascii_lrc.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN) + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_ASCII_SER_PDU_SIZE_MIN 5 /*!< Minimum size of a Modbus ASCII frame. */ +#define MB_ASCII_SER_PDU_SIZE_MAX MB_SER_PDU_SIZE_MAX * 2 /*!< Maximum size of a Modbus ASCII frame. */ +#define MB_ASCII_SER_PDU_SIZE_LRC 1 /*!< Size of LRC field in PDU. */ +#define MB_ASCII_SER_PDU_ADDR_OFF 0 /*!< Offset of slave address in Ser-PDU. */ +#define MB_ASCII_SER_PDU_PDU_OFF 1 /*!< Offset of Modbus-PDU in Ser-PDU. */ + +typedef struct _port_serial_opts mb_serial_opts_t; +typedef struct mb_trans_base_t mb_trans_base_t; + +mb_err_enum_t mbm_ascii_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +mb_err_enum_t mbs_ascii_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +bool mbs_ascii_transp_delete(mb_trans_base_t *inst); +bool mbm_ascii_transp_delete(mb_trans_base_t *inst); + +#endif + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_transports/rtu/mbcrc.c b/modbus/mb_transports/rtu/mbcrc.c new file mode 100644 index 0000000..28bded4 --- /dev/null +++ b/modbus/mb_transports/rtu/mbcrc.c @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2010 Christian Walter + * + * SPDX-License-Identifier: BSD-3-Clause + * + * SPDX-FileContributor: 2016-2021 Espressif Systems (Shanghai) CO LTD + */ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbcrc.c, v 1.7 2007/02/18 23:50:27 wolti Exp $ + */ +/* ----------------------- Platform includes --------------------------------*/ +#include "mb_common.h" + +static const uint8_t crc_hi_tab[] = { + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40 +}; + +static const uint8_t crc_lo_tab[] = { + 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, + 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, + 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, + 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, + 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, + 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, + 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, + 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, + 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, + 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, + 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, + 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, + 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, + 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, + 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, + 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, + 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, + 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, + 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, + 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, + 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, + 0x41, 0x81, 0x80, 0x40 +}; + +uint16_t +mb_crc16(uint8_t *frame_ptr, uint16_t len_buf) +{ + uint8_t crc_hi = 0xFF; + uint8_t crc_lo = 0xFF; + int idx; + + while (len_buf--) { + idx = crc_lo ^ *(frame_ptr++); + crc_lo = (uint8_t)(crc_hi ^ crc_hi_tab[idx]); + crc_hi = crc_lo_tab[idx]; + } + return (uint16_t)(crc_hi << 8 | crc_lo); +} diff --git a/modbus/mb_transports/rtu/mbcrc.h b/modbus/mb_transports/rtu/mbcrc.h new file mode 100644 index 0000000..95396fd --- /dev/null +++ b/modbus/mb_transports/rtu/mbcrc.h @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2010 Christian Walter + * + * SPDX-License-Identifier: BSD-3-Clause + * + * SPDX-FileContributor: 2016-2021 Espressif Systems (Shanghai) CO LTD + */ +/* + * FreeModbus Libary: A portable Modbus implementation for Modbus ASCII/RTU. + * Copyright (c) 2006 Christian Walter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * File: $Id: mbcrc.h, v 1.5 2006/12/07 22:10:34 wolti Exp $ + */ + +#pragma once + +uint16_t mb_crc16(uint8_t *frame_ptr, uint16_t len_buf); + diff --git a/modbus/mb_transports/rtu/rtu_master.c b/modbus/mb_transports/rtu/rtu_master.c new file mode 100644 index 0000000..a19eb26 --- /dev/null +++ b/modbus/mb_transports/rtu/rtu_master.c @@ -0,0 +1,299 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "rtu_transport.h" +#include "port_serial_common.h" +#include "port_common.h" + +#include "mb_config.h" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN) + +static const char *TAG = "mb_transp.rtu_master"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + uint8_t snd_buf[MB_RTU_SER_PDU_SIZE_MAX]; + uint8_t rcv_buf[MB_RTU_SER_PDU_SIZE_MAX]; + uint16_t snd_pdu_len; + uint8_t *snd_buf_cur; + uint16_t snd_buf_cnt; + uint16_t rcv_buf_pos; + bool frame_is_broadcast; + volatile mb_timer_mode_enum_t cur_timer_mode; + mb_rtu_state_enum_t state; +} mbm_rtu_transp_t; + +mb_err_enum_t mbm_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +static void mbm_rtu_transp_start(mb_trans_base_t *inst); +static void mbm_rtu_transp_stop(mb_trans_base_t *inst); +static mb_err_enum_t mbm_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +static mb_err_enum_t mbm_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +static bool mbm_rtu_transp_rcv_fsm(mb_trans_base_t *inst); +static bool mbm_rtu_transp_snd_fsm(mb_trans_base_t *inst); +static bool mbm_rtu_transp_timer_expired(void *inst); +static void mbm_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static void mbm_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +// static uint16_t mbm_rtu_transp_get_snd_len(mb_trans_base_t *inst); +static void mbm_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len); +static bool mbm_rtu_transp_rq_is_bcast(mb_trans_base_t *inst); +bool mbm_rtu_transp_delete(mb_trans_base_t *inst); + +mb_err_enum_t mbm_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_inst), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mbm_rtu_transp_t *transp = NULL; + transp = (mbm_rtu_transp_t *)calloc(1, sizeof(mbm_rtu_transp_t)); + MB_RETURN_ON_FALSE(transp, MB_EILLSTATE, TAG, "no mem for %s instance.", TAG); + CRITICAL_SECTION_INIT(transp->base.lock); + CRITICAL_SECTION_LOCK(transp->base.lock); + transp->base.frm_rcv = mbm_rtu_transp_receive; + transp->base.frm_send = mbm_rtu_transp_send; + transp->base.frm_start = mbm_rtu_transp_start; + transp->base.frm_stop = mbm_rtu_transp_stop; + transp->base.get_rx_frm = mbm_rtu_transp_get_rcv_buf; + transp->base.get_tx_frm = mbm_rtu_transp_get_snd_buf; + transp->base.frm_delete = mbm_rtu_transp_delete; + transp->base.frm_is_bcast = mbm_rtu_transp_rq_is_bcast; + // Copy parent object descriptor + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mb_port_ser_create(ser_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "serial port creation, err: %d", ret); + ret = mb_port_timer_create(port_obj, MB_RTU_GET_T35_VAL(ser_opts->baudrate)); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "timer port creation, err: %d", ret); + // Override default response time if defined + if (ser_opts->response_tout_ms) { + mb_port_timer_set_response_time(port_obj, ser_opts->response_tout_ms); + } + ret = mb_port_event_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "event port creation, err: %d", ret); + transp->base.port_obj = port_obj; + // Set callback function pointer for the timer + port_obj->cb.tmr_expired = mbm_rtu_transp_timer_expired; + port_obj->cb.tx_empty = NULL; + port_obj->cb.byte_rcvd = NULL; + port_obj->arg = (void *)transp; + transp->port_obj = port_obj; // register the created port object + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + return MB_ENOERR; + +error: + if (port_obj->timer_obj) { + mb_port_timer_delete(port_obj); + } + if (port_obj->event_obj) { + mb_port_event_delete(port_obj); + } + if (port_obj) { + mb_port_ser_delete(port_obj); + } + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + +bool mbm_rtu_transp_delete(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + mb_port_ser_delete(transp->base.port_obj); + mb_port_timer_delete(transp->base.port_obj); + mb_port_event_delete(transp->base.port_obj); + CRITICAL_SECTION_CLOSE(inst->lock); + free(transp); + return true; +} + +static void mbm_rtu_transp_start(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + transp->state = MB_RTU_STATE_INIT; + CRITICAL_SECTION(inst->lock) { + mb_port_ser_enable(inst->port_obj); + mb_port_timer_enable(inst->port_obj); + }; + /* No special startup required for RTU. */ + (void)mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); +} + +static void mbm_rtu_transp_stop(mb_trans_base_t *inst) +{ + CRITICAL_SECTION(inst->lock) { + mb_port_ser_disable(inst->port_obj); + mb_port_timer_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbm_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *prcv_addr, uint8_t **ppframe_buf, uint16_t *pbuf_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + + if (!pbuf_len || !prcv_addr || !ppframe_buf || !pbuf_len) { + return MB_EIO; + } + + mb_err_enum_t status = MB_ENOERR; + + uint8_t *pbuf = (uint8_t *)transp->rcv_buf; + uint16_t length = *pbuf_len; + + if (mb_port_ser_recv_data(inst->port_obj, &pbuf, &length) == false) { + *pbuf_len = 0; + return MB_EPORTERR; + } + + assert(length < MB_RTU_SER_PDU_SIZE_MAX); + assert(pbuf); + + /* Check length and CRC checksum */ + if ((length >= MB_RTU_SER_PDU_SIZE_MIN) + && (mb_crc16((uint8_t *)pbuf, length) == 0)) { + /* Save the address field. All frames are passed to the upper layed + * and the decision if a frame is used is done there. + */ + *prcv_addr = pbuf[MB_SER_PDU_ADDR_OFF]; + + /* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus + * size of address field and CRC checksum. + */ + *pbuf_len = (uint16_t)(length - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC); + transp->rcv_buf_pos = length; + + /* Return the start of the Modbus PDU to the caller. */ + *ppframe_buf = (uint8_t *)&pbuf[MB_SER_PDU_PDU_OFF]; + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbm_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t frame_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + mb_err_enum_t status = MB_ENOERR; + uint16_t crc16 = 0; + + if (slv_addr > MB_MASTER_TOTAL_SLAVE_NUM) { + return MB_EINVAL; + } + + if (frame_ptr && frame_len) { + /* First byte before the Modbus-PDU is the slave address. */ + transp->snd_buf_cur = (uint8_t *)frame_ptr - 1; + transp->snd_buf_cnt = 1; + + /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */ + transp->snd_buf_cur[MB_SER_PDU_ADDR_OFF] = slv_addr; + transp->snd_buf_cnt += frame_len; + /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */ + crc16 = mb_crc16((uint8_t *) transp->snd_buf_cur, transp->snd_buf_cnt); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 & 0xFF); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 >> 8); + + bool ret = mb_port_ser_send_data(inst->port_obj, (uint8_t *)transp->snd_buf_cur, transp->snd_buf_cnt); + if (!ret) { + return MB_EPORTERR; + } + transp->frame_is_broadcast = (slv_addr == MB_ADDRESS_BROADCAST) ? true : false; + // If the frame is broadcast, master will enable timer of convert delay, + // else master will enable timer of respond timeout. */ + if (transp->frame_is_broadcast) { + mb_port_timer_convert_delay_enable(transp->base.port_obj); + } else { + mb_port_timer_respond_timeout_enable(transp->base.port_obj); + } + + } else { + status = MB_EIO; + } + return status; +} + +__attribute__((unused)) +static bool mbm_rtu_transp_rcv_fsm(mb_trans_base_t *inst) +{ + return false; +} + +__attribute__((unused)) +static bool mbm_rtu_transp_snd_fsm(mb_trans_base_t *inst) +{ + return false; +} + + +static bool mbm_rtu_transp_timer_expired(void *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + + bool need_poll = false; + mb_timer_mode_enum_t timer_mode = mb_port_get_cur_timer_mode(transp->base.port_obj); + + mb_port_timer_disable(transp->base.port_obj); + + switch(timer_mode) { + case MB_TMODE_T35: + //need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + //ESP_EARLY_LOGD(TAG, "%p:EV_READY", transp->base.descr.parent); + break; + + case MB_TMODE_RESPOND_TIMEOUT: + mb_port_event_set_err_type(transp->base.port_obj, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_ERROR_PROCESS)); + ESP_EARLY_LOGW(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", transp->base.descr.parent); + break; + + case MB_TMODE_CONVERT_DELAY: + /* If timer mode is convert delay, the master event then turns EV_MASTER_EXECUTE status. */ + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_EXECUTE)); + ESP_EARLY_LOGD(TAG, "%p:MB_TMODE_CONVERT_DELAY", transp->base.descr.parent); + break; + + default: + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + break; + } + + return need_poll; +} + +static void mbm_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->rcv_buf[MB_PDU_FUNC_OFF]; + } +} + +static void mbm_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->snd_buf[MB_RTU_SER_PDU_PDU_OFF]; + } +} + +__attribute__((unused)) +static void mbm_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + transp->snd_buf_cnt = snd_pdu_len; + } +} + +static bool mbm_rtu_transp_rq_is_bcast(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + return transp->frame_is_broadcast; +} + +#endif diff --git a/modbus/mb_transports/rtu/rtu_slave.c b/modbus/mb_transports/rtu/rtu_slave.c new file mode 100644 index 0000000..2dd7835 --- /dev/null +++ b/modbus/mb_transports/rtu/rtu_slave.c @@ -0,0 +1,254 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "rtu_transport.h" +#include "port_serial_common.h" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN) + +static const char *TAG = "mb_transp.rtu_slave"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + + uint8_t snd_buf[MB_RTU_SER_PDU_SIZE_MAX]; // pdu_buf + uint8_t rcv_buf[MB_RTU_SER_PDU_SIZE_MAX]; + uint16_t snd_pdu_len; + uint8_t *snd_buf_cur; + uint16_t snd_buf_cnt; + uint16_t rcv_buf_pos; + volatile mb_timer_mode_enum_t cur_timer_mode; + mb_rtu_state_enum_t state; +} mbs_rtu_transp_t; + +mb_err_enum_t mbs_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +static void mbs_rtu_transp_start(mb_trans_base_t *inst); +static void mbs_rtu_transp_stop(mb_trans_base_t *inst); +static mb_err_enum_t mbs_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +static mb_err_enum_t mbs_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +static bool mbs_rtu_transp_rcv_fsm(mb_trans_base_t *inst); +static bool mbs_rtu_transp_snd_fsm(mb_trans_base_t *inst); +static bool mbs_rtu_transp_timer_expired(void *inst); +static void mbs_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +void mbs_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static uint16_t mbs_rtu_transp_get_snd_len(mb_trans_base_t *inst); +static void mbs_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len); +bool mbs_rtu_transp_delete(mb_trans_base_t *inst); + +mb_err_enum_t mbs_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_inst), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mbs_rtu_transp_t *transp = NULL; + transp = (mbs_rtu_transp_t *)calloc(1, sizeof(mbs_rtu_transp_t)); + MB_RETURN_ON_FALSE(transp, MB_EILLSTATE, TAG, "no mem for rtu slave transport instance."); + CRITICAL_SECTION_INIT(transp->base.lock); + CRITICAL_SECTION_LOCK(transp->base.lock); + transp->base.frm_rcv = mbs_rtu_transp_receive; + transp->base.frm_send = mbs_rtu_transp_send; + transp->base.frm_start = mbs_rtu_transp_start; + transp->base.frm_stop = mbs_rtu_transp_stop; + transp->base.get_rx_frm = mbs_rtu_transp_get_rcv_buf; + transp->base.get_tx_frm = mbs_rtu_transp_get_snd_buf; + transp->base.frm_delete = mbs_rtu_transp_delete; + transp->base.frm_is_bcast = NULL; + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mb_port_ser_create(ser_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "serial port creation, err: %d", ret); + ret = mb_port_timer_create(port_obj, MB_RTU_GET_T35_VAL(ser_opts->baudrate)); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "timer port creation, err: %d", ret); + ret = mb_port_event_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "event port creation, err: %d", ret); + transp->base.port_obj = port_obj; + // Set callback function pointer for the timer + port_obj->cb.tmr_expired = mbs_rtu_transp_timer_expired; + port_obj->cb.tx_empty = NULL; + port_obj->cb.byte_rcvd = NULL; + port_obj->arg = (void *)transp; + transp->port_obj = port_obj; + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + return MB_ENOERR; + +error: + if (port_obj) { + free(port_obj->event_obj); + free(port_obj->timer_obj); + } + free(port_obj); + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + + +bool mbs_rtu_transp_delete(mb_trans_base_t *inst) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + mb_port_ser_delete(transp->base.port_obj); + mb_port_timer_delete(transp->base.port_obj); + mb_port_event_delete(transp->base.port_obj); + } + CRITICAL_SECTION_CLOSE(inst->lock); + free(transp); + return true; +} + +static void mbs_rtu_transp_start(mb_trans_base_t *inst) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + transp->state = MB_RTU_STATE_INIT; + CRITICAL_SECTION(inst->lock) { + mb_port_ser_enable(inst->port_obj); + //mb_port_timer_enable(inst->port_obj); + }; + (void)mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); +} + +static void mbs_rtu_transp_stop(mb_trans_base_t *inst) +{ + CRITICAL_SECTION(inst->lock) { + mb_port_ser_disable(inst->port_obj); + mb_port_timer_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbs_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *prcv_addr, uint8_t **ppframe_buf, uint16_t *pbuf_len) +{ + if (!pbuf_len || !prcv_addr || !ppframe_buf || !pbuf_len) { + return MB_EIO; + } + + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + mb_err_enum_t status = MB_ENOERR; + + uint8_t *pbuf = (uint8_t *)transp->rcv_buf; + uint16_t length = *pbuf_len; + + if (mb_port_ser_recv_data(inst->port_obj, &pbuf, &length) == false){ + *pbuf_len = 0; + return MB_EPORTERR; + } + + assert(length < MB_RTU_SER_PDU_SIZE_MAX); + assert(pbuf); + + /* Check length and CRC checksum */ + if ((length >= MB_RTU_SER_PDU_SIZE_MIN) + && (mb_crc16((uint8_t *)pbuf, length) == 0)) { + /* Save the address field. All frames are passed to the upper layed + * and the decision if a frame is used is done there. + */ + *prcv_addr = pbuf[MB_SER_PDU_ADDR_OFF]; + + /* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus + * size of address field and CRC checksum. + */ + *pbuf_len = (uint16_t)(length - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC); + transp->rcv_buf_pos = length; + + /* Return the start of the Modbus PDU to the caller. */ + *ppframe_buf = (uint8_t *)&pbuf[MB_SER_PDU_PDU_OFF]; + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbs_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t frame_len) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + mb_err_enum_t status = MB_ENOERR; + uint16_t crc16 = 0; + + if (slv_addr > MB_MASTER_TOTAL_SLAVE_NUM) { + return MB_EINVAL; + } + + if (frame_ptr && frame_len) { + /* First byte before the Modbus-PDU is the slave address. */ + transp->snd_buf_cur = (uint8_t *)frame_ptr - 1; + transp->snd_buf_cnt = 1; + + /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */ + transp->snd_buf_cur[MB_SER_PDU_ADDR_OFF] = slv_addr; + transp->snd_buf_cnt += frame_len; + /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */ + crc16 = mb_crc16((uint8_t *) transp->snd_buf_cur, transp->snd_buf_cnt); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 & 0xFF); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 >> 8); + + bool ret = mb_port_ser_send_data(inst->port_obj, (uint8_t *)transp->snd_buf_cur, transp->snd_buf_cnt); + if (!ret) { + return MB_EPORTERR; + } + } else { + status = MB_EIO; + } + return status; +} + +__attribute__((unused)) +static bool mbs_rtu_transp_rcv_fsm(mb_trans_base_t *inst) +{ + return false; +} + +__attribute__((unused)) +static bool mbs_rtu_transp_snd_fsm(mb_trans_base_t *inst) +{ + return false; +} + +static bool mbs_rtu_transp_timer_expired(void *inst) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + bool need_poll = false; + //mb_timer_mode_enum_t timer_mode = mb_port_get_cur_timer_mode(transp->base.port_obj); + + mb_port_timer_disable(transp->base.port_obj); + + return need_poll; +} + +static void mbs_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->snd_buf[MB_RTU_SER_PDU_PDU_OFF]; + } +} + +void mbs_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->rcv_buf[MB_PDU_FUNC_OFF]; + } +} + +__attribute__((unused)) +static uint16_t mbs_rtu_transp_get_snd_len(mb_trans_base_t *inst) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + return transp->snd_buf_cnt; +} + +__attribute__((unused)) +static void mbs_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len) +{ + mbs_rtu_transp_t *transp = __containerof(inst, mbs_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + transp->snd_buf_cnt = snd_pdu_len; + } +} + +#endif \ No newline at end of file diff --git a/modbus/mb_transports/rtu/rtu_transport.h b/modbus/mb_transports/rtu/rtu_transport.h new file mode 100644 index 0000000..24ffacb --- /dev/null +++ b/modbus/mb_transports/rtu/rtu_transport.h @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "mb_config.h" +#include "mb_common.h" +#include "mb_types.h" +#include "mb_frame.h" +#include "mb_proto.h" +#include "mbcrc.h" +#include "transport_common.h" +#include "port_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_RTU_EN) + +/* If baudrate > 19200 then we should use the fixed timer values + * t35 = 1750us. Otherwise t35 must be 3.5 times the character time. + * The timer reload value for a character is given by: + * + * ChTimeValue = Ticks_per_1s / (Baudrate / 11) + * = 11 * Ticks_per_1s / Baudrate + * = 220000 / Baudrate + * The reload for t3.5 is 1.5 times this value and similary + * for t3.5. + */ +#define MB_RTU_GET_T35_VAL(baudrate) (__extension__( \ +{ \ + uint16_t tmr_35_50us = (baudrate > 19200) ? \ + 35 : ((7UL * 220000UL) / (2UL * baudrate)); \ + tmr_35_50us; \ +} \ +)) + +/* ----------------------- Defines ------------------------------------------*/ +#define MB_RTU_SER_PDU_SIZE_MIN 4 /*!< Minimum size of a Modbus RTU frame. */ +#define MB_RTU_SER_PDU_SIZE_MAX MB_BUFFER_SIZE /*!< Maximum size of a Modbus RTU frame. */ +#define MB_RTU_SER_PDU_SIZE_CRC 2 /*!< Size of CRC field in PDU. */ +#define MB_RTU_SER_PDU_ADDR_OFF 0 /*!< Offset of slave address in Ser-PDU. */ +#define MB_RTU_SER_PDU_PDU_OFF 1 /*!< Offset of Modbus-PDU in Ser-PDU. */ + +typedef enum +{ + MB_RTU_STATE_INIT, /*!< Receiver is in initial state. */ + MB_RTU_STATE_ACTIVE, /*!< Receiver is in active state. */ + MB_RTU_STATE_ERROR /*!< If the frame is invalid. */ +} mb_rtu_state_enum_t; + +typedef struct _port_serial_opts mb_serial_opts_t; +typedef struct mb_trans_base_t mb_trans_base_t; + +mb_err_enum_t mbm_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +mb_err_enum_t mbs_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +bool mbm_rtu_transp_delete(mb_trans_base_t *inst); +bool mbs_rtu_transp_delete(mb_trans_base_t *inst); + +#endif + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_transports/tcp/tcp_master.c b/modbus/mb_transports/tcp/tcp_master.c new file mode 100644 index 0000000..f70b957 --- /dev/null +++ b/modbus/mb_transports/tcp/tcp_master.c @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tcp_transport.h" +#include "port_tcp_common.h" +#include "port_tcp_master.h" // for port tout function + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +static const char *TAG = "mb_transp.tcp_master"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + uint8_t recv_buf[MB_TCP_BUF_SIZE]; + uint8_t send_buf[MB_TCP_BUF_SIZE]; + mb_tcp_state_enum_t state; + uint16_t snd_pdu_len; +} mbm_tcp_transp_t; + +/* ----------------------- Defines ------------------------------------------*/ + +/* ----------------------- Function prototypes ------------------------------*/ +mb_err_enum_t mbm_tcp_transp_create(mb_tcp_opts_t *tcp_opts, void **in_out_inst); +static void mbm_tcp_transp_start(mb_trans_base_t *inst); +static void mbm_tcp_transp_stop(mb_trans_base_t *inst); +static mb_err_enum_t mbm_tcp_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *pbuf_len); +static mb_err_enum_t mbm_tcp_transp_send(mb_trans_base_t *inst, uint8_t _unused, const uint8_t *pframe, uint16_t len); +static void mbm_tcp_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static void mbm_tcp_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +bool mbm_tcp_transp_delete(mb_trans_base_t *inst); +static bool mbm_tcp_transp_rq_is_bcast(mb_trans_base_t *inst); + +mb_err_enum_t mbm_tcp_transp_create(mb_tcp_opts_t *tcp_opts, void **in_out_inst) +{ + mb_err_enum_t ret = MB_ENOERR; + mbm_tcp_transp_t *transp = NULL; + transp = (mbm_tcp_transp_t *)calloc(1, sizeof(mbm_tcp_transp_t)); + MB_RETURN_ON_FALSE(transp, MB_EILLSTATE, TAG, "no mem for instance."); + CRITICAL_SECTION_INIT(transp->base.lock); + CRITICAL_SECTION_LOCK(transp->base.lock); + transp->base.frm_rcv = mbm_tcp_transp_receive; + transp->base.frm_send = mbm_tcp_transp_send; + transp->base.frm_start = mbm_tcp_transp_start; + transp->base.frm_stop = mbm_tcp_transp_stop; + transp->base.get_rx_frm = mbm_tcp_transp_get_rcv_buf; + transp->base.get_tx_frm = mbm_tcp_transp_get_snd_buf; + transp->base.frm_delete = mbm_tcp_transp_delete; + transp->base.frm_is_bcast = mbm_tcp_transp_rq_is_bcast; + // Copy parent object descriptor + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mbm_port_tcp_create(tcp_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "port creation, err: %d", ret); + ret = mb_port_timer_create(port_obj, MB_TCP_TIMEOUT_MS * MB_TIMER_TICS_PER_MS); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "timer port creation, err: %d", ret); + // Override default response time if defined + if (tcp_opts->response_tout_ms) { + mb_port_timer_set_response_time(port_obj, tcp_opts->response_tout_ms); + } + ret = mb_port_event_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "event port creation, err: %d", ret); + // Set callback function pointer for the timer + // port_obj->cb.tmr_expired = mbm_tcp_transp_timer_expired; + // port_obj->cb.tx_empty = NULL; + // port_obj->cb.byte_rcvd = NULL; + // port_obj->arg = (void *)transp; + transp->base.port_obj = port_obj; + transp->port_obj = port_obj; + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + return MB_ENOERR; +error: + if (port_obj) { + free(port_obj->event_obj); + free(port_obj->timer_obj); + } + free(port_obj); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + +bool mbm_tcp_transp_delete(mb_trans_base_t *inst) +{ + mbm_tcp_transp_t *transp = __containerof(inst, mbm_tcp_transp_t, base); + // destroy method of port tcp master is here + CRITICAL_SECTION(inst->lock) { + mbm_port_tcp_delete(inst->port_obj); + mb_port_timer_delete(inst->port_obj); + mb_port_event_delete(inst->port_obj); + } + CRITICAL_SECTION_CLOSE(inst->lock); + free(transp); + return true; +} + +static void mbm_tcp_transp_start(mb_trans_base_t *inst) +{ + mbm_tcp_transp_t *transp = __containerof(inst, mbm_tcp_transp_t, base); + transp->state = MB_TCP_STATE_INIT; + CRITICAL_SECTION(inst->lock) { + mbm_port_tcp_enable(inst->port_obj); + mb_port_timer_enable(inst->port_obj); + }; + /* No special startup required for TCP. */ + (void)mb_port_event_post(inst->port_obj, EVENT(EV_READY)); +} + +static void mbm_tcp_transp_stop(mb_trans_base_t *inst) +{ + /* Make sure that no more clients are connected. */ + CRITICAL_SECTION(inst->lock) { + mbm_port_tcp_disable(inst->port_obj); + mb_port_timer_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbm_tcp_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr, uint8_t **frame_ptr_buf, uint16_t *pbuf_len) +{ + mb_err_enum_t status = MB_EIO; + uint8_t *frame_ptr; + uint16_t len = *pbuf_len; + uint16_t pid; + + if (mbm_port_tcp_recv_data(inst->port_obj, &frame_ptr, &len) != false) { + pid = frame_ptr[MB_TCP_PID] << 8U; + pid |= frame_ptr[MB_TCP_PID + 1]; + + if (pid == MB_TCP_PROTOCOL_ID) { + *frame_ptr_buf = &frame_ptr[MB_TCP_FUNC]; + *pbuf_len = len - MB_TCP_FUNC; + status = MB_ENOERR; + + /* Get MBAP UID field if its support is enabled. + * Otherwise just ignore this field. + */ +#if MB_TCP_UID_ENABLED + *rcv_addr = frame_ptr[MB_TCP_UID]; +#else + *rcv_addr = MB_TCP_PSEUDO_ADDRESS; +#endif + } + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbm_tcp_transp_send(mb_trans_base_t *inst, uint8_t address, const uint8_t *pframe, uint16_t len) +{ + mb_err_enum_t status = MB_ENOERR; + uint8_t *frame_ptr = (uint8_t *)pframe - MB_TCP_FUNC; + uint16_t tcp_len = len + MB_TCP_FUNC; + + /* The MBAP header is already initialized because the caller calls this + * function with the buffer returned by the previous call. Therefore we + * only have to update the length in the header. Note that the length + * header includes the size of the Modbus PDU and the UID Byte. Therefore + * the length is len plus one. + */ + frame_ptr[MB_TCP_LEN] = (len + 1) >> 8U; + frame_ptr[MB_TCP_LEN + 1] = (len + 1) & 0xFF; + + /* Set UID field in the MBAP if it is supported. + * If the RTU over TCP is not supported, the UID = 0 or 0xFF. + */ +#if MB_TCP_UID_ENABLED + frame_ptr[MB_TCP_UID] = address; +#else + frame_ptr[MB_TCP_UID] = 0x00; +#endif + + if (mbm_port_tcp_send_data(inst->port_obj, address, frame_ptr, tcp_len) == false) { + status = MB_EIO; + } + mb_port_timer_respond_timeout_enable(inst->port_obj); + return status; +} + +static void mbm_tcp_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_tcp_transp_t *transp = __containerof(inst, mbm_tcp_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = transp->recv_buf + MB_TCP_FUNC; + } +} + +static void mbm_tcp_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_tcp_transp_t *transp = __containerof(inst, mbm_tcp_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = transp->send_buf + MB_TCP_FUNC; + } +} + +static bool mbm_tcp_transp_rq_is_bcast(mb_trans_base_t *inst) +{ + return false; //no broadcast packets on tcp +} + +#endif + diff --git a/modbus/mb_transports/tcp/tcp_slave.c b/modbus/mb_transports/tcp/tcp_slave.c new file mode 100644 index 0000000..235388d --- /dev/null +++ b/modbus/mb_transports/tcp/tcp_slave.c @@ -0,0 +1,231 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tcp_transport.h" +#include "port_tcp_common.h" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +static const char *TAG = "mb_transp.tcp_slave"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + uint8_t recv_buf[MB_TCP_BUF_SIZE]; + uint8_t send_buf[MB_TCP_BUF_SIZE]; + mb_tcp_state_enum_t state; + uint16_t snd_pdu_len; +} mbs_tcp_transp_t; + +/* ----------------------- Defines ------------------------------------------*/ + +/* ----------------------- Function prototypes ------------------------------*/ +static void mbs_tcp_transp_start(mb_trans_base_t *inst); +static void mbs_tcp_transp_stop(mb_trans_base_t *inst); +static mb_err_enum_t mbs_tcp_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr, uint8_t **frame_ptr_buf, uint16_t *pbuf_len); +static mb_err_enum_t mbs_tcp_transp_send(mb_trans_base_t *inst, uint8_t _unused, const uint8_t *frame_ptr, uint16_t len); +static void mbs_tcp_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static void mbs_tcp_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +bool mbs_tcp_transp_delete(mb_trans_base_t *inst); +static bool mbs_tcp_transp_timer_expired(void *inst); + +mb_err_enum_t mbs_tcp_transp_create(mb_tcp_opts_t *tcp_opts, void **in_out_inst) +{ + mb_err_enum_t ret = MB_ENOERR; + mbs_tcp_transp_t *transp = NULL; + transp = (mbs_tcp_transp_t *)calloc(1, sizeof(mbs_tcp_transp_t)); + MB_RETURN_ON_FALSE(transp, MB_EILLSTATE, TAG, "no mem for instance."); + CRITICAL_SECTION_INIT(transp->base.lock); + CRITICAL_SECTION_LOCK(transp->base.lock); + transp->base.frm_rcv = mbs_tcp_transp_receive; + transp->base.frm_send = mbs_tcp_transp_send; + transp->base.frm_start = mbs_tcp_transp_start; + transp->base.frm_stop = mbs_tcp_transp_stop; + transp->base.get_rx_frm = mbs_tcp_transp_get_rcv_buf; + transp->base.get_tx_frm = mbs_tcp_transp_get_snd_buf; + transp->base.frm_delete = mbs_tcp_transp_delete; + transp->base.frm_is_bcast = NULL; + // Copy parent object descriptor + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mbs_port_tcp_create(tcp_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "tcp port creation, err: %d", ret); + ret = mb_port_timer_create(port_obj, MB_TCP_TIMEOUT_MS * MB_TIMER_TICS_PER_MS); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "timer port creation, err: %d", ret); + // Override default response time if defined + if (tcp_opts->response_tout_ms) { + mb_port_timer_set_response_time(port_obj, tcp_opts->response_tout_ms); + } + ret = mb_port_event_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EPORTERR, error, TAG, "event port creation, err: %d", ret); + transp->base.port_obj = port_obj; + // Set callback function pointer for the timer + port_obj->cb.tmr_expired = mbs_tcp_transp_timer_expired; + port_obj->cb.tx_empty = NULL; + port_obj->cb.byte_rcvd = NULL; + port_obj->arg = (void *)transp; + transp->port_obj = port_obj; + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + return MB_ENOERR; +error: + if (port_obj) { + free(port_obj->event_obj); + free(port_obj->timer_obj); + } + free(port_obj); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + +bool mbs_tcp_transp_delete(mb_trans_base_t *inst) +{ + mbs_tcp_transp_t *transp = __containerof(inst, mbs_tcp_transp_t, base); + // destroy method of port tcp slave is here + CRITICAL_SECTION(inst->lock) { + mbs_port_tcp_delete(inst->port_obj); + mb_port_timer_delete(inst->port_obj); + mb_port_event_delete(inst->port_obj); + } + CRITICAL_SECTION_CLOSE(inst->lock); + free(transp); + return true; +} + +static void mbs_tcp_transp_start(mb_trans_base_t *inst) +{ + CRITICAL_SECTION(inst->lock) { + mbs_port_tcp_enable(inst->port_obj); + mb_port_timer_enable(inst->port_obj); + }; + /* No special startup required for TCP. */ + (void)mb_port_event_post(inst->port_obj, EVENT(EV_READY)); +} + +static void mbs_tcp_transp_stop(mb_trans_base_t *inst) +{ + /* Make sure that no more clients are connected. */ + CRITICAL_SECTION(inst->lock) { + mbs_port_tcp_disable(inst->port_obj); + mb_port_timer_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbs_tcp_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr, uint8_t **frame_ptr_buf, uint16_t *pbuf_len) +{ + if (!pbuf_len || !frame_ptr_buf || !pbuf_len) { + return MB_EIO; + } + + mbs_tcp_transp_t *transp = __containerof(inst, mbs_tcp_transp_t, base); + + uint8_t *frame_ptr = (uint8_t *)transp->recv_buf; + uint16_t length = *pbuf_len; + mb_err_enum_t status = MB_EIO; + uint16_t pid; + + if (mbs_port_tcp_recv_data(inst->port_obj, &frame_ptr, &length) != false) { + pid = frame_ptr[MB_TCP_PID] << 8U; + pid |= frame_ptr[MB_TCP_PID + 1]; + + if (pid == MB_TCP_PROTOCOL_ID) { + *frame_ptr_buf = &frame_ptr[MB_TCP_FUNC]; + *pbuf_len = length - MB_TCP_FUNC; + status = MB_ENOERR; + + /* Get MBAP UID field if its support is enabled. + * Otherwise just ignore this field. + */ +#if MB_TCP_UID_ENABLED + *rcv_addr = frame_ptr[MB_TCP_UID]; +#else + *rcv_addr = MB_TCP_PSEUDO_ADDRESS; +#endif + } + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbs_tcp_transp_send(mb_trans_base_t *inst, uint8_t _unused, const uint8_t *pframe, uint16_t len) +{ + mb_err_enum_t status = MB_ENOERR; + uint8_t *frame_ptr = (uint8_t *)pframe - MB_TCP_FUNC; + uint16_t tcp_len = len + MB_TCP_FUNC; + + /* The MBAP header is already initialized because the caller calls this + * function with the buffer returned by the previous call. Therefore we + * only have to update the length in the header. Note that the length + * header includes the size of the Modbus PDU and the UID Byte. Therefore + * the length is len plus one. + */ + frame_ptr[MB_TCP_LEN] = (len + 1) >> 8U; + frame_ptr[MB_TCP_LEN + 1] = (len + 1) & 0xFF; + + if (mbs_port_tcp_send_data(inst->port_obj, frame_ptr, tcp_len) == false) { + status = MB_EIO; + } + return status; +} + +static bool mbs_tcp_transp_timer_expired(void *inst) +{ + mbs_tcp_transp_t *transp = __containerof(inst, mbs_tcp_transp_t, base); + + bool need_poll = false; + mb_timer_mode_enum_t timer_mode = mb_port_get_cur_timer_mode(transp->base.port_obj); + + mb_port_timer_disable(transp->base.port_obj); + + switch(timer_mode) { + case MB_TMODE_T35: + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + ESP_EARLY_LOGD(TAG, "EV_READY"); + break; + + case MB_TMODE_RESPOND_TIMEOUT: + mb_port_event_set_err_type(transp->base.port_obj, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_ERROR_PROCESS)); + ESP_EARLY_LOGD(TAG, "EV_ERROR_RESPOND_TIMEOUT"); + break; + + case MB_TMODE_CONVERT_DELAY: + /* If timer mode is convert delay, the master event then turns EV_MASTER_EXECUTE status. */ + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_EXECUTE)); + ESP_EARLY_LOGD(TAG, "MB_TMODE_CONVERT_DELAY"); + break; + + default: + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + break; + } + + return need_poll; +} + +static void mbs_tcp_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbs_tcp_transp_t *transp = __containerof(inst, mbs_tcp_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = transp->recv_buf + MB_TCP_FUNC; + } +} + +static void mbs_tcp_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbs_tcp_transp_t *transp = __containerof(inst, mbs_tcp_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = transp->send_buf + MB_TCP_FUNC; + } +} + +#endif \ No newline at end of file diff --git a/modbus/mb_transports/tcp/tcp_transport.h b/modbus/mb_transports/tcp/tcp_transport.h new file mode 100644 index 0000000..6669be1 --- /dev/null +++ b/modbus/mb_transports/tcp/tcp_transport.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "sdkconfig.h" +#include "mb_common.h" +#include "mb_types.h" +#include "transport_common.h" +#include "port_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +/* ----------------------- Defines ------------------------------------------*/ + +// Common definitions for TCP port +#define MB_TCP_BUF_SIZE (256 + 7) // Must hold a complete Modbus TCP frame. + +#define MB_TCP_TIMEOUT_MS (1000) + +typedef enum +{ + MB_TCP_STATE_INIT, /*!< Receiver is in initial state. */ + MB_TCP_STATE_ACTIVE, /*!< Receiver is in active state. */ + MB_TCP_STATE_ERROR /*!< If the frame is invalid. */ +} mb_tcp_state_enum_t; + +typedef struct mb_trans_base_t mb_trans_base_t; + +mb_err_enum_t mbm_tcp_transp_create(mb_tcp_opts_t *tcp_opts, void **in_out_inst); +mb_err_enum_t mbs_tcp_transp_create(mb_tcp_opts_t *tcp_opts, void **in_out_inst); +bool mbs_tcp_transp_delete(mb_trans_base_t *inst); +bool mbm_tcp_transp_delete(mb_trans_base_t *inst); + +#endif + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/modbus/mb_transports/transport_common.h b/modbus/mb_transports/transport_common.h new file mode 100644 index 0000000..02d7f61 --- /dev/null +++ b/modbus/mb_transports/transport_common.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "mb_types.h" +#include "port_common.h" +#include "mb_port_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mb_trans_base_t mb_trans_base_t; /*!< Type of moddus transport object */ +typedef struct _obj_descr obj_descr_t; + +typedef void (*mb_frm_start_fp)(mb_trans_base_t *transport); +typedef void (*mb_frm_stop_fp)(mb_trans_base_t *transport); +typedef mb_err_enum_t (*mb_frm_rcv_fp)(mb_trans_base_t *transport, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +typedef mb_err_enum_t (*mb_frm_snd_fp)(mb_trans_base_t *transport, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +typedef void (*mb_get_rx_frm_fp) (mb_trans_base_t *transport, uint8_t **frame_ptr_buf); +typedef void (*mb_get_tx_frm_fp) (mb_trans_base_t *transport, uint8_t **frame_ptr_buf); +typedef bool (*mb_get_fp)(mb_trans_base_t *inst); + +struct mb_trans_base_t +{ + obj_descr_t descr; + + _lock_t lock; + mb_port_base_t *port_obj; + + mb_frm_start_fp frm_start; + mb_frm_stop_fp frm_stop; + mb_get_fp frm_delete; + mb_frm_snd_fp frm_send; + mb_frm_rcv_fp frm_rcv; + mb_get_rx_frm_fp get_rx_frm; + mb_get_rx_frm_fp get_tx_frm; + mb_get_fp frm_is_bcast; +}; //!< Transport methods + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..11266f0 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,56 @@ +[pytest] +# only the files with prefix `pytest_` would be recognized as pytest test scripts. +python_files = pytest_*.py +norecursedirs = managed_components* espressif__mdns* + +# ignore PytestExperimentalApiWarning for record_xml_attribute +# set traceback to "short" to prevent the overwhelming tracebacks +addopts = + -s + --embedded-services esp,idf + --tb short + --skip-check-coredump y + --ignore-glob=pytest_*mdns*.py + +# ignore DeprecationWarning +filterwarnings = + ignore::DeprecationWarning:matplotlib.*: + ignore::DeprecationWarning:google.protobuf.*: + ignore::_pytest.warning_types.PytestExperimentalApiWarning + +markers = + # target markers + esp32: support esp32 target + esp32s2: support esp32s2 target + esp32s3: support esp32s3 target + esp32c3: support esp32c3 target + esp32c2: support esp32c2 target + esp32p4: support esp32p4 target + esp32c5: support esp32c5 target + + # special markers + temp_skip_ci: temp skip tests for specified targets only in ci + + # env markers + generic: tests should be run on generic runners + + # multi-dut markers + multi_dut_generic: tests should be run on generic runners, at least have two duts connected. + multi_dut_modbus_generic: generic Modbus dut + multi_dut_modbus_tcp: Modbus TCP runners with two duts connected + multi_dut_modbus_rs485: Modbus RTU/ASCII runners with two duts connected + multi_dut_modbus_serial: Alias for Modbus RTU/ASCII runners with two duts connected + generic_multi_device: generic multi device runners + +# log related +log_cli = True +log_cli_level = INFO +log_cli_format = %(asctime)s %(levelname)s %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S + +# junit related +junit_family = xunit1 + +## log all to `system-out` when case fail +junit_logging = stdout +junit_log_passing_tests = False diff --git a/test_apps/.build-test-rules.yml b/test_apps/.build-test-rules.yml new file mode 100644 index 0000000..acee310 --- /dev/null +++ b/test_apps/.build-test-rules.yml @@ -0,0 +1,16 @@ +physical_tests: + disable_test: + - if: IDF_TARGET != "esp32" + reason: only manual test is performed for other targets + disable: + - if: CONFIG_NAME == "wifi" and SOC_WIFI_SUPPORTED != 1 and IDF_TARGET != "esp32" + +unit_tests: + disable_test: + - if: IDF_TARGET != "esp32" + reason: only manual test is performed for other targets + +adapter_tests: + disable_test: + - if: IDF_TARGET != "esp32" + reason: only manual test is performed for other targets \ No newline at end of file diff --git a/test_apps/adapter_tests/.gitignore b/test_apps/adapter_tests/.gitignore new file mode 100644 index 0000000..a348e50 --- /dev/null +++ b/test_apps/adapter_tests/.gitignore @@ -0,0 +1 @@ +/__pycache__/ diff --git a/test_apps/adapter_tests/CMakeLists.txt b/test_apps/adapter_tests/CMakeLists.txt new file mode 100644 index 0000000..391abd1 --- /dev/null +++ b/test_apps/adapter_tests/CMakeLists.txt @@ -0,0 +1,8 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../test_common") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_comm_adapter) diff --git a/test_apps/adapter_tests/README.md b/test_apps/adapter_tests/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/adapter_tests/README.md @@ -0,0 +1,4 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | + +This test app is used to test modbus interface. diff --git a/test_apps/adapter_tests/main/CMakeLists.txt b/test_apps/adapter_tests/main/CMakeLists.txt new file mode 100644 index 0000000..04de324 --- /dev/null +++ b/test_apps/adapter_tests/main/CMakeLists.txt @@ -0,0 +1,12 @@ +set(srcs "test_app_main.c" + "test_modbus_adapter_serial.c" + "test_modbus_adapter_tcp.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES cmock test_common unity test_utils + ) # + +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_adapter_impl_serial") +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_adapter_impl_tcp") diff --git a/test_apps/adapter_tests/main/Kconfig.projbuild b/test_apps/adapter_tests/main/Kconfig.projbuild new file mode 100644 index 0000000..5a837ad --- /dev/null +++ b/test_apps/adapter_tests/main/Kconfig.projbuild @@ -0,0 +1,45 @@ +menu "Modbus Test Configuration" + + config MB_PORT_ADAPTER_EN + bool "Enable Modbus port adapter to substitute hardware layer for test." + default y + help + When option is enabled the port communication layer is substituted by + port adapter layer to allow testing of higher layers without access to physical layer. + + config MB_TEST_SLAVE_TASK_PRIO + int "Modbus master test task priority" + range 4 23 + default 4 + help + Modbus master task priority for the test. + + config MB_TEST_MASTER_TASK_PRIO + int "Modbus slave test task priority" + range 4 23 + default 4 + help + Modbus slave task priority for the test. + + config MB_TEST_COMM_CYCLE_COUNTER + int "Modbus communication cycle counter" + range 10 1000 + default 10 + help + Modbus communication cycle counter for test. + + config MB_TEST_LEAK_WARN_LEVEL + int "Modbus test leak warning level" + range 4 256 + default 32 + help + Modbus test leak warning level. + + config MB_TEST_LEAK_CRITICAL_LEVEL + int "Modbus test leak critical level" + range 4 1024 + default 64 + help + Modbus test leak critical level. + +endmenu diff --git a/test_apps/adapter_tests/main/component.mk b/test_apps/adapter_tests/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/adapter_tests/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/adapter_tests/main/idf_component.yml b/test_apps/adapter_tests/main/idf_component.yml new file mode 100644 index 0000000..77cf471 --- /dev/null +++ b/test_apps/adapter_tests/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../" + diff --git a/test_apps/adapter_tests/main/test_app_main.c b/test_apps/adapter_tests/main/test_app_main.c new file mode 100644 index 0000000..c891750 --- /dev/null +++ b/test_apps/adapter_tests/main/test_app_main.c @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_fixture.h" + +#include "sdkconfig.h" + +static void run_all_tests(void) +{ +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + RUN_TEST_GROUP(modbus_adapter_serial); +#endif +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + RUN_TEST_GROUP(modbus_adapter_tcp); +#endif +} + +void app_main(void) +{ + UNITY_MAIN_FUNC(run_all_tests); +} diff --git a/test_apps/adapter_tests/main/test_modbus_adapter_serial.c b/test_apps/adapter_tests/main/test_modbus_adapter_serial.c new file mode 100644 index 0000000..808fe91 --- /dev/null +++ b/test_apps/adapter_tests/main/test_modbus_adapter_serial.c @@ -0,0 +1,257 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity_fixture.h" + +#include "test_utils.h" + +#if __has_include("unity_test_utils.h") +#define UNITY_TEST_UTILS_INCLUDED +// unity test utils are used +#include "unity_test_utils.h" +#include "unity_test_utils_memory.h" +#else +// Unit_test_app utils from test_utils ("test_utils.h"), v4.4 +#define unity_utils_task_delete test_utils_task_delete +#endif + +#include "sdkconfig.h" +#include "test_common.h" + +#define TEST_SER_PORT_NUM1 (1) +#define TEST_SER_PORT_NUM2 (2) +#define TEST_TASK_TIMEOUT_MS (120000) +#define TEST_LEAK_WARN (32) +#define TEST_LEAK_CRITICAL (64) +#define TEST_SLAVE_SEND_TOUT_US (5000) +#define TEST_MASTER_SEND_TOUT_US (5000) + +#define TEST_MASTER_RESPOND_TOUT_MS (CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND) + +#define TAG "MODBUS_SERIAL_TEST" + +// The workaround to statically link whole test library +__attribute__((unused)) bool mb_test_include_adapter_impl_serial = true; + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 0, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1, STR("MB_hold_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG2, STR("MB_hold_reg-2"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG3, STR("MB_hold_reg-3"), STR("Data"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, 3, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG_COUNT, STR("CYCLE_COUNTER"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER} +}; + +// Calculate number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +TEST_GROUP(modbus_adapter_serial); + +TEST_SETUP(modbus_adapter_serial) +{ + test_common_start(); +} + +TEST_TEAR_DOWN(modbus_adapter_serial) +{ + int task_count = test_common_task_start_all(1); + TEST_ASSERT_TRUE(task_count > 0); + TEST_ASSERT_EQUAL(task_count, test_common_task_wait_done_delete_all(TEST_TASK_TIMEOUT_MS)); + test_common_stop(); + ESP_LOGI(TAG, "%s, done successfully.", __func__); +} + +TEST(modbus_adapter_serial, test_modbus_adapter_rtu) +{ + mb_communication_info_t slave_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config1, 0)); + + mb_communication_info_t slave_config2 = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR2, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config2, 0)); + + // Initialize and start Modbus controller + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_RTU, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors)); +} + +TEST(modbus_adapter_serial, test_modbus_adapter_ascii) +{ + mb_communication_info_t slave_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_ASCII, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config1, 0)); + + mb_communication_info_t slave_config2 = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_ASCII, + .ser_opts.uid = MB_DEVICE_ADDR2, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config2, 0)); + + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_ASCII, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors)); +} + +// ignore test for now (temporary workaround for the issue) +IGNORE_TEST(modbus_adapter_serial, test_modbus_adapter_rtu_two_ports) +{ + mb_communication_info_t slave_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_ASCII, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config1, 0)); + + mb_communication_info_t slave_config2 = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_ASCII, + .ser_opts.uid = MB_DEVICE_ADDR2, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config2, 0)); + + mb_communication_info_t slave_config3 = { + .ser_opts.port = TEST_SER_PORT_NUM2, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config3, 0)); + + mb_communication_info_t slave_config4 = { + .ser_opts.port = TEST_SER_PORT_NUM2, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR2, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_serial_create(&slave_config4, 0)); + + mb_communication_info_t master_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM1, + .ser_opts.mode = MB_ASCII, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_master_serial_create(&master_config1, 0, &descriptors[0], num_descriptors)); + + mb_communication_info_t master_config2 = { + .ser_opts.port = TEST_SER_PORT_NUM2, + .ser_opts.mode = MB_RTU, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_master_serial_create(&master_config2, 0, &descriptors[0], num_descriptors)); +} + +TEST_GROUP_RUNNER(modbus_adapter_serial) +{ + RUN_TEST_CASE(modbus_adapter_serial, test_modbus_adapter_rtu); + RUN_TEST_CASE(modbus_adapter_serial, test_modbus_adapter_ascii); + RUN_TEST_CASE(modbus_adapter_serial, test_modbus_adapter_rtu_two_ports); +} + +#endif + + diff --git a/test_apps/adapter_tests/main/test_modbus_adapter_tcp.c b/test_apps/adapter_tests/main/test_modbus_adapter_tcp.c new file mode 100644 index 0000000..44dfb22 --- /dev/null +++ b/test_apps/adapter_tests/main/test_modbus_adapter_tcp.c @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity_fixture.h" + +#include "test_utils.h" + +#if __has_include("unity_test_utils.h") +#define UNITY_TEST_UTILS_INCLUDED +// unity test utils are used +#include "unity_test_utils.h" +#include "unity_test_utils_memory.h" +#else +// Unit_test_app utils from test_utils ("test_utils.h"), v4.4 +#define unity_utils_task_delete test_utils_task_delete +#endif + +#include "sdkconfig.h" +#include "test_common.h" + +#define TEST_TCP_PORT_NUM (1502) +#define TEST_TASK_TIMEOUT_MS (120000) +#define TEST_LEAK_WARN (32) +#define TEST_LEAK_CRITICAL (64) +#define TEST_SLAVE_SEND_TOUT_US (50) +#define TEST_MASTER_SEND_TOUT_US (50) + +#define TEST_MASTER_RESPOND_TOUT_MS (CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND) + +#define TAG "MODBUS_TCP_TEST" + +// The workaround to statically link whole test library +__attribute__((unused)) bool mb_test_include_adapter_impl_tcp = true; + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 0, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1, STR("MB_hold_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG2, STR("MB_hold_reg-2"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG3, STR("MB_hold_reg-3"), STR("Data"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, 3, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG_COUNT, STR("CYCLE_COUNTER"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER} +}; + +// Calculate number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +TEST_GROUP(modbus_adapter_tcp); + +TEST_SETUP(modbus_adapter_tcp) +{ + test_common_start(); +} + +TEST_TEAR_DOWN(modbus_adapter_tcp) +{ + int task_count = test_common_task_start_all(1); + TEST_ASSERT_TRUE(task_count > 0); + TEST_ASSERT_EQUAL(task_count, test_common_task_wait_done_delete_all(TEST_TASK_TIMEOUT_MS)); + test_common_stop(); + ESP_LOGI(TAG, "%s, done successfully.", __func__); +} + +const char *slave_tcp_addr_table[] = { + "01;mb_slave_tcp_01;1502", // Corresponds to characteristic MB_DEVICE_ADDR1 "mb_slave_tcp_01" + "200;mb_slave_tcp_c8;1502", // Corresponds to characteristic MB_DEVICE_ADDR2 "mb_slave_tcp_C8" + NULL // End of table condition (must be included) +}; + +TEST(modbus_adapter_tcp, test_modbus_adapter_tcp) +{ + mb_communication_info_t tcp_slave_cfg_1 = { + .tcp_opts.port = TEST_TCP_PORT_NUM, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = NULL, + .tcp_opts.uid = MB_DEVICE_ADDR1, + .tcp_opts.start_disconnected = true, + .tcp_opts.response_tout_ms = 1, + .tcp_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_tcp_create(&tcp_slave_cfg_1, 0)); + + mb_communication_info_t tcp_slave_cfg_2 = { + .tcp_opts.port = TEST_TCP_PORT_NUM, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = NULL, + .tcp_opts.uid = MB_DEVICE_ADDR2, + .tcp_opts.start_disconnected = true, + .tcp_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .tcp_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_tcp_create(&tcp_slave_cfg_2, 0)); + + // Initialize and start Modbus controller + mb_communication_info_t tcp_master_cfg_1 = { + .tcp_opts.port = TEST_TCP_PORT_NUM, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = (void *)slave_tcp_addr_table, + .tcp_opts.uid = 0, + .tcp_opts.start_disconnected = false, + .tcp_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .tcp_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + TEST_ASSERT_NOT_NULL(test_common_master_tcp_create(&tcp_master_cfg_1, 0, &descriptors[0], num_descriptors)); +} + +TEST_GROUP_RUNNER(modbus_adapter_tcp) +{ + RUN_TEST_CASE(modbus_adapter_tcp, test_modbus_adapter_tcp); +} + +#endif + + diff --git a/test_apps/adapter_tests/pytest_mb_comm_adapter.py b/test_apps/adapter_tests/pytest_mb_comm_adapter.py new file mode 100644 index 0000000..76c9ddc --- /dev/null +++ b/test_apps/adapter_tests/pytest_mb_comm_adapter.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +CONFIGS = [ + pytest.param('serial', marks=[pytest.mark.esp32, pytest.mark.esp32p4]), + pytest.param('tcp', marks=[pytest.mark.esp32, pytest.mark.esp32p4]), +] + +@pytest.mark.temp_skip_ci(targets=['esp32p4'], reason='no multi-dev runner') +@pytest.mark.multi_dut_modbus_generic +@pytest.mark.parametrize('config', CONFIGS, indirect=True) +def test_modbus_comm_adapter(dut: Dut) -> None: + dut.expect_unity_test_output() diff --git a/test_apps/adapter_tests/sdkconfig.ci.serial b/test_apps/adapter_tests/sdkconfig.ci.serial new file mode 100644 index 0000000..d869129 --- /dev/null +++ b/test_apps/adapter_tests/sdkconfig.ci.serial @@ -0,0 +1,23 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/adapter_tests/sdkconfig.ci.tcp b/test_apps/adapter_tests/sdkconfig.ci.tcp new file mode 100644 index 0000000..ea87e56 --- /dev/null +++ b/test_apps/adapter_tests/sdkconfig.ci.tcp @@ -0,0 +1,23 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/adapter_tests/sdkconfig.defaults b/test_apps/adapter_tests/sdkconfig.defaults new file mode 100644 index 0000000..4bafbd1 --- /dev/null +++ b/test_apps/adapter_tests/sdkconfig.defaults @@ -0,0 +1,21 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_APP_BUILD_USE_FLASH_SECTIONS=n +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 diff --git a/test_apps/conftest.py b/test_apps/conftest.py new file mode 100644 index 0000000..24edeff --- /dev/null +++ b/test_apps/conftest.py @@ -0,0 +1,115 @@ +# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +# pylint: disable=W0621 # redefined-outer-name + +import logging +import os +import sys +from datetime import datetime +from enum import Enum +from typing import Any, Callable, Dict, Match, Optional, TextIO, Tuple + +import pexpect +import pytest +from pytest_embedded_idf import CaseTester +from _pytest.config import Config +from _pytest.fixtures import FixtureRequest +from _pytest.monkeypatch import MonkeyPatch +from pytest_embedded.plugin import multi_dut_argument, multi_dut_fixture +from pytest_embedded_idf.app import IdfApp +from pytest_embedded_idf.dut import IdfDut +from pytest_embedded_idf.serial import IdfSerial + +DEFAULT_SDKCONFIG = 'default' + + +############ +# Fixtures # +############ + + +@pytest.fixture +def case_tester(dut: IdfDut, **kwargs): # type: ignore + yield CaseTester(dut, **kwargs) + + +@pytest.fixture(scope='session', autouse=True) +def session_tempdir() -> str: + + _tmpdir = os.path.join( + os.path.dirname(__file__), + 'pytest_embedded_log', + datetime.now().strftime('%Y-%m-%d_%H-%M-%S'), + ) + os.makedirs(_tmpdir, exist_ok=True) + return _tmpdir + + +@pytest.fixture(autouse=True) +@multi_dut_fixture +def junit_properties( + test_case_name: str, record_xml_attribute: Callable[[str, object], None] +) -> None: + """ + This fixture is autoused and will modify the junit report test case name to .. + """ + record_xml_attribute('name', test_case_name) + + +@pytest.fixture(scope='module') +def monkeypatch_module(request: FixtureRequest) -> MonkeyPatch: + mp = MonkeyPatch() + request.addfinalizer(mp.undo) + return mp + + +@pytest.fixture +@multi_dut_argument +def config(request: FixtureRequest) -> str: + return getattr(request, 'param', None) or DEFAULT_SDKCONFIG + + +@pytest.fixture +@multi_dut_fixture +def build_dir(app_path: str, target: Optional[str], config: Optional[str]) -> str: + """ + Check local build dir with the following priority: + + 1. build__ + 2. build_ + 3. build_ + 4. build + + Args: + app_path: app path + target: target + config: config + + Returns: + valid build directory + """ + check_dirs = [] + if target is not None and config is not None: + check_dirs.append(f'build_{target}_{config}') + if target is not None: + check_dirs.append(f'build_{target}') + if config is not None: + check_dirs.append(f'build_{config}') + check_dirs.append('build') + + for check_dir in check_dirs: + binary_path = os.path.join(app_path, check_dir) + if os.path.isdir(binary_path): + logging.info(f'find valid binary path: {binary_path}') + return check_dir + + logging.warning( + 'checking binary path: %s... missing... try another place', binary_path + ) + + recommend_place = check_dirs[0] + logging.error( + f'no build dir valid. Please build the binary via "idf.py -B {recommend_place} build" and run pytest again' + ) + sys.exit(1) diff --git a/test_apps/physical_tests/.gitignore b/test_apps/physical_tests/.gitignore new file mode 100644 index 0000000..5e16a9c --- /dev/null +++ b/test_apps/physical_tests/.gitignore @@ -0,0 +1,2 @@ +/pytest_embedded_log/ +/__pycache__/ diff --git a/test_apps/physical_tests/CMakeLists.txt b/test_apps/physical_tests/CMakeLists.txt new file mode 100644 index 0000000..5f208ff --- /dev/null +++ b/test_apps/physical_tests/CMakeLists.txt @@ -0,0 +1,8 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../test_common") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_modbus_comm_multi_dev) diff --git a/test_apps/physical_tests/README.md b/test_apps/physical_tests/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/physical_tests/README.md @@ -0,0 +1,4 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | + +This test app is used to test modbus interface. diff --git a/test_apps/physical_tests/main/CMakeLists.txt b/test_apps/physical_tests/main/CMakeLists.txt new file mode 100644 index 0000000..fa9a219 --- /dev/null +++ b/test_apps/physical_tests/main/CMakeLists.txt @@ -0,0 +1,14 @@ +set(srcs "test_app_main.c" + "test_modbus_rs485_comm_master_slave.c" + "test_modbus_tcp_comm_master_slave.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES cmock test_utils test_common unity nvs_flash esp_event esp_eth + ) + +# The workaround for WHOLE_ARCHIVE which is absent in v4.4 +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_phys_impl_tcp") +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_phys_impl_serial") + diff --git a/test_apps/physical_tests/main/Kconfig.projbuild b/test_apps/physical_tests/main/Kconfig.projbuild new file mode 100644 index 0000000..4b8402c --- /dev/null +++ b/test_apps/physical_tests/main/Kconfig.projbuild @@ -0,0 +1,45 @@ +menu "Modbus Test Configuration" + + config MB_PORT_ADAPTER_EN + bool "Enable Modbus port adapter to substitute hardware layer for test." + default y + help + When option is enabled the port communication layer is substituted by + port adapter layer to allow testing of higher layers without access to physical layer. + + config MB_TEST_SLAVE_TASK_PRIO + int "Modbus master test task priority" + range 4 23 + default 4 + help + Modbus master task priority for the test. + + config MB_TEST_MASTER_TASK_PRIO + int "Modbus slave test task priority" + range 4 23 + default 4 + help + Modbus slave task priority for the test. + + config MB_TEST_COMM_CYCLE_COUNTER + int "Modbus test communication cycle counter" + range 10 1000 + default 10 + help + Modbus communication cycle counter for test. + + config MB_TEST_LEAK_WARN_LEVEL + int "Modbus test leak warning level" + range 4 256 + default 32 + help + Modbus test leak warning level. + + config MB_TEST_LEAK_CRITICAL_LEVEL + int "Modbus test leak critical level" + range 4 1024 + default 64 + help + Modbus test leak critical level. + +endmenu diff --git a/test_apps/physical_tests/main/component.mk b/test_apps/physical_tests/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/physical_tests/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/physical_tests/main/idf_component.yml b/test_apps/physical_tests/main/idf_component.yml new file mode 100644 index 0000000..a3c27db --- /dev/null +++ b/test_apps/physical_tests/main/idf_component.yml @@ -0,0 +1,10 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../" + espressif/mdns: + version: "^1.0.0" + protocol_examples_common: + path: ${IDF_PATH}/examples/common_components/protocol_examples_common + diff --git a/test_apps/physical_tests/main/test_app_main.c b/test_apps/physical_tests/main/test_app_main.c new file mode 100644 index 0000000..25ee4ca --- /dev/null +++ b/test_apps/physical_tests/main/test_app_main.c @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "test_common.h" + +void app_main(void) +{ + printf("Modbus RS485 multi-device test cases/n"); + unity_run_menu(); +} diff --git a/test_apps/physical_tests/main/test_modbus_rs485_comm_master_slave.c b/test_apps/physical_tests/main/test_modbus_rs485_comm_master_slave.c new file mode 100644 index 0000000..7bbc0cb --- /dev/null +++ b/test_apps/physical_tests/main/test_modbus_rs485_comm_master_slave.c @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity.h" + +#include "sdkconfig.h" +#include "test_common.h" +#include "test_utils.h" + +#if __has_include("unity_test_utils.h") +// unity test utils are used +#include "unity_test_utils.h" +#else +// Unit_test_app utils from test_utils ("test_utils.h"), v4.4 +#define unity_utils_task_delete test_utils_task_delete +#endif + +#define TEST_SER_PORT_NUM (1) +#define TEST_TASK_TIMEOUT_MS (160000) +#define TEST_SEND_TOUT_US (30000) +#define TEST_RESP_TOUT_MS (1000) +#define TEST_BAUD_RATE (115200) + +#if CONFIG_IDF_TARGET_ESP32 +#define TEST_SER_PIN_RX (22) +#define TEST_SER_PIN_TX (23) +// RTS for RS485 Half-Duplex Mode manages DE/~RE +#define TEST_SER_PIN_RTS (18) +#else +#define TEST_SER_PIN_RX (4) +#define TEST_SER_PIN_TX (5) +#define TEST_SER_PIN_RTS (10) +#endif + +#define TEST_MASTER_RESPOND_TOUT_MS (CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND) + +// The workaround to statically link the whole test library +__attribute__((unused)) bool mb_test_include_phys_impl_serial = true; + +#define TAG "MODBUS_SERIAL_COMM_TEST" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 0, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1, STR("MB_hold_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG2, STR("MB_hold_reg-2"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG3, STR("MB_hold_reg-3"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 3, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG_COUNT, STR("CYCLE_COUNTER"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER} +}; + +// The number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +static void test_modbus_rs485_rtu_slave(void) +{ + mb_communication_info_t slave_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TEST_ESP_OK(uart_set_pin(slave_config1.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + + TaskHandle_t slave_task_handle = test_common_slave_serial_create(&slave_config1, 0); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(slave_config1.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + + ESP_LOGI(TAG, "Slave RTU is started. (%s).", __func__); + + unity_send_signal("Slave_ready"); + unity_wait_for_signal("Master_started"); + + test_common_task_start(slave_task_handle, 1); + TEST_ASSERT_TRUE(test_common_task_wait_done(slave_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +} + +static void test_modbus_rs485_rtu_master(void) +{ + ESP_LOGI(TAG, "Master RTU is started (%s).", __func__); + unity_wait_for_signal("Slave_ready"); + unity_send_signal("Master_started"); + + // Initialize and start Modbus controller + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TaskHandle_t master_task_handle = test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(master_config.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + TEST_ESP_OK(uart_set_pin(master_config.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + + test_common_task_start(master_task_handle, 1); + TEST_ASSERT_TRUE(test_common_task_wait_done(master_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +} + +/* + * Modbus RS485 RTU multi device test case + */ +TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 RTU multi device master - slave case.", "[modbus][test_env=multi_dut_modbus_serial]", test_modbus_rs485_rtu_slave, test_modbus_rs485_rtu_master); + +static void test_modbus_rs485_ascii_slave(void) +{ + mb_communication_info_t slave_config1 = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_ASCII, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TEST_ESP_OK(uart_set_pin(slave_config1.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + + TaskHandle_t slave_task_handle = test_common_slave_serial_create(&slave_config1, 0); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(slave_config1.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + + ESP_LOGI(TAG, "Slave ASCII is started. (%s).", __func__); + + unity_send_signal("Slave_ready"); + unity_wait_for_signal("Master_started"); + + test_common_task_start(slave_task_handle, 1); + TEST_ASSERT_TRUE(test_common_task_wait_done(slave_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +}; + +static void test_modbus_rs485_ascii_master(void) +{ + ESP_LOGI(TAG, "Master ASCII is started (%s).", __func__); + unity_wait_for_signal("Slave_ready"); + + // Initialize and start Modbus controller + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_ASCII, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = TEST_BAUD_RATE, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_SEND_TOUT_US + }; + + TaskHandle_t master_task_handle = test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors); + + // Set driver mode to Half Duplex + TEST_ESP_OK(uart_set_mode(master_config.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX)); + TEST_ESP_OK(uart_set_pin(master_config.ser_opts.port, TEST_SER_PIN_TX, + TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE)); + unity_send_signal("Master_started"); + + test_common_task_start(master_task_handle, 1); + TEST_ASSERT_TRUE(test_common_task_wait_done(master_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS))); +} + +/* + * Modbus RS485 ASCII multi device test case + */ +TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 ASCII multi device master - slave case.", "[modbus][test_env=multi_dut_modbus_serial]", test_modbus_rs485_ascii_slave, test_modbus_rs485_ascii_master); + + +#endif \ No newline at end of file diff --git a/test_apps/physical_tests/main/test_modbus_tcp_comm_master_slave.c b/test_apps/physical_tests/main/test_modbus_tcp_comm_master_slave.c new file mode 100644 index 0000000..c3df360 --- /dev/null +++ b/test_apps/physical_tests/main/test_modbus_tcp_comm_master_slave.c @@ -0,0 +1,241 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity.h" + +#include "sdkconfig.h" +#include "test_common.h" +#include "test_utils.h" + +#include "nvs_flash.h" + +#if MB_MDNS_IS_INCLUDED +#include "mdns.h" +#endif + +#include "protocol_examples_common.h" +#include "esp_event.h" + +#if __has_include("unity_test_utils.h") +// unity test utils are used +#include "unity_test_utils.h" +#else +// Unit_test_app utils from test_utils ("test_utils.h"), v4.4 +#define unity_utils_task_delete test_utils_task_delete +#endif + +#define TEST_TCP_PORT_NUM1 (1502) +#define TEST_TCP_PORT_NUM2 (502) +#define TEST_TCP_TASK_TIMEOUT_MS (160000) +#define TEST_TCP_SLAVE_SEND_TOUT_US (500) +#define TEST_TCP_MASTER_SEND_TOUT_US (500) + +#define TEST_MASTER_RESPOND_TOUT_MS (CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND) + +// The workaround to statically link the whole test library +__attribute__((unused)) bool mb_test_include_phys_impl_tcp = true; + +#define TAG "MODBUS_TCP_COMM_TEST" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 0, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1, STR("MB_hold_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG2, STR("MB_hold_reg-2"), STR("Data"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG3, STR("MB_hold_reg-3"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 3, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG_COUNT, STR("CYCLE_COUNTER"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER} +}; + +// The number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +const char *slave_tcp_addr_table[] = { + "01;mb_slave_tcp_01;1502", // Corresponds to characteristic MB_DEVICE_ADDR1 + "200;mb_slave_tcp_c8;502", // Corresponds to characteristic MB_DEVICE_ADDR2 + NULL // End of table condition (must be included) +}; + +static esp_err_t test_tcp_services_init(void **pnetif) +{ + esp_err_t result = nvs_flash_init(); + if ((result == ESP_ERR_NVS_NO_FREE_PAGES) || (result == ESP_ERR_NVS_NEW_VERSION_FOUND)) { + ESP_ERROR_CHECK(nvs_flash_erase()); + result = nvs_flash_init(); + } + ESP_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "nvs_flash_init fail, returns(0x%x).", + (int)result); + result = esp_netif_init(); + ESP_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_netif_init fail, returns(0x%x).", + (int)result); + result = esp_event_loop_create_default(); + ESP_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_event_loop_create_default fail, returns(0x%x).", + (int)result); +#if MB_MDNS_IS_INCLUDED + // Start mdns service and register device + if (mdns_init() != ESP_OK) { + ESP_LOGE(TAG, "initialization of mdns fail."); + }; +#endif + // This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + // Read "Establishing Wi-Fi or Ethernet Connection" section in + // examples/protocols/README.md for more information about this function. + result = example_connect(); + ESP_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "example_connect fail, returns(0x%x).", + (int)result); +#if CONFIG_EXAMPLE_CONNECT_WIFI + // result = esp_wifi_set_ps(WIFI_PS_NONE); + // ESP_RETURN_ON_FALSE((result == ESP_OK), ESP_ERR_INVALID_STATE, + // TAG, + // "esp_wifi_set_ps fail, returns(0x%x).", + // (int)result); +#endif + if (pnetif) { + *pnetif = get_example_netif(); + } + return ESP_OK; +} + +static esp_err_t test_tcp_services_destroy(void) +{ + esp_err_t err = ESP_OK; + + err = example_disconnect(); + ESP_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "example_disconnect fail, returns(0x%x).", + (int)err); + err = esp_event_loop_delete_default(); + ESP_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "esp_event_loop_delete_default fail, returns(0x%x).", + (int)err); + err = esp_netif_deinit(); + ESP_RETURN_ON_FALSE(((err == ESP_OK) || (err == ESP_ERR_NOT_SUPPORTED)), + ESP_ERR_INVALID_STATE, + TAG, + "esp_netif_deinit fail, returns(0x%x).", + (int)err); + err = nvs_flash_deinit(); + ESP_RETURN_ON_FALSE((err == ESP_OK), ESP_ERR_INVALID_STATE, + TAG, + "nvs_flash_deinit fail, returns(0x%x).", + (int)err); +#if MB_MDNS_IS_INCLUDED + // Stop mdns service and register device + mdns_free(); +#endif + return err; +} + +static void test_modbus_tcp_slave(void) +{ + void *pnetif = NULL; + TEST_ASSERT_TRUE(test_tcp_services_init(&pnetif) == ESP_OK); + TEST_ASSERT_NOT_NULL(pnetif); + test_common_start(); + + mb_communication_info_t tcp_slave_cfg_1 = { + .tcp_opts.port = TEST_TCP_PORT_NUM1, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = NULL, + .tcp_opts.uid = MB_DEVICE_ADDR1, + .tcp_opts.start_disconnected = true, + .tcp_opts.response_tout_ms = 1, + .tcp_opts.test_tout_us = TEST_TCP_SLAVE_SEND_TOUT_US, + .tcp_opts.ip_netif_ptr = pnetif + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_tcp_create(&tcp_slave_cfg_1, 0)); + + ESP_LOGI(TAG, "Slave TCP #1 is started. (%s).", __func__); + + mb_communication_info_t tcp_slave_cfg_2 = { + .tcp_opts.port = TEST_TCP_PORT_NUM2, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = NULL, + .tcp_opts.uid = MB_DEVICE_ADDR2, + .tcp_opts.start_disconnected = true, + .tcp_opts.response_tout_ms = 1, + .tcp_opts.test_tout_us = TEST_TCP_SLAVE_SEND_TOUT_US, + .tcp_opts.ip_netif_ptr = pnetif + }; + + TEST_ASSERT_NOT_NULL(test_common_slave_tcp_create(&tcp_slave_cfg_2, 0)); + + ESP_LOGI(TAG, "Slave TCP #2 is started. (%s).", __func__); + + unity_send_signal("Slave_ready"); + unity_wait_for_signal("Master_started"); + + TEST_ASSERT_EQUAL(test_common_task_start_all(1), + test_common_task_wait_done_delete_all(TEST_TCP_TASK_TIMEOUT_MS)); + + ESP_LOGI(TAG, "Slave TCP test is complited. (%s).", __func__); + + test_common_stop(); + + test_tcp_services_destroy(); +} + +static void test_modbus_tcp_master(void) +{ + void *pnetif = NULL; + TEST_ASSERT_TRUE(test_tcp_services_init(&pnetif) == ESP_OK); + TEST_ASSERT_NOT_NULL(pnetif); + test_common_start(); + + ESP_LOGI(TAG, "Master TCP is started (%s).", __func__); + unity_wait_for_signal("Slave_ready"); + + // Initialize and start Modbus controller + mb_communication_info_t tcp_master_cfg_1 = { + .tcp_opts.port = TEST_TCP_PORT_NUM1, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = (void *)slave_tcp_addr_table, + .tcp_opts.uid = 0, + .tcp_opts.start_disconnected = false, + .tcp_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .tcp_opts.test_tout_us = TEST_TCP_MASTER_SEND_TOUT_US, + .tcp_opts.ip_netif_ptr = pnetif + }; + + TEST_ASSERT_NOT_NULL(test_common_master_tcp_create(&tcp_master_cfg_1, 0, &descriptors[0], num_descriptors)); + + unity_send_signal("Master_started"); + + TEST_ASSERT_EQUAL(test_common_task_start_all(1), + test_common_task_wait_done_delete_all(TEST_TCP_TASK_TIMEOUT_MS)); + + test_common_stop(); + + test_tcp_services_destroy(); + ESP_LOGI(TAG, "Master TCP is complited. (%s).", __func__); +} + +/* + * Modbus TCP multi device test case + */ +TEST_CASE_MULTIPLE_DEVICES("Modbus TCP multi device master - slave case.", "[modbus][test_env=multi_dut_modbus_tcp]", + test_modbus_tcp_slave, test_modbus_tcp_master); + +#endif \ No newline at end of file diff --git a/test_apps/physical_tests/pytest_mb_comm_multi_dev.py b/test_apps/physical_tests/pytest_mb_comm_multi_dev.py new file mode 100644 index 0000000..d712f14 --- /dev/null +++ b/test_apps/physical_tests/pytest_mb_comm_multi_dev.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded_idf import CaseTester + +@pytest.mark.esp32 +@pytest.mark.multi_dut_modbus_serial +@pytest.mark.parametrize('count, config', [(2, 'serial')], indirect=True) +def test_modbus_comm_multi_dev_serial(case_tester) -> None: # type: ignore + for case in case_tester.test_menu: + if case.attributes.get('test_env', 'multi_dut_modbus_serial') == 'multi_dut_modbus_serial': + print(case.name) + case_tester.run_multi_dev_case(case=case, reset=True) + +@pytest.mark.esp32 +@pytest.mark.multi_dut_modbus_tcp +@pytest.mark.parametrize('count, config', [(2, 'wifi'), (2, 'ethernet')], indirect=True) +def test_modbus_comm_multi_dev_tcp(case_tester) -> None: # type: ignore + for case in case_tester.test_menu: + if case.attributes.get('test_env', 'multi_dut_modbus_tcp') == 'multi_dut_modbus_tcp': + print(case.name) + case_tester.run_multi_dev_case(case=case, reset=True) \ No newline at end of file diff --git a/test_apps/physical_tests/sdkconfig.ci.ethernet b/test_apps/physical_tests/sdkconfig.ci.ethernet new file mode 100644 index 0000000..2dd5b78 --- /dev/null +++ b/test_apps/physical_tests/sdkconfig.ci.ethernet @@ -0,0 +1,34 @@ +# +# Modbus configuration +# +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=1502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_FMB_EXT_TYPE_SUPPORT=y + +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_CONNECT_ETHERNET=y +CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y +CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_MDC_GPIO=23 +CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 +CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 +CONFIG_EXAMPLE_ETH_PHY_ADDR=1 +CONFIG_EXAMPLE_ETHERNET_EMAC_TASK_STACK_SIZE=4096 + +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y +CONFIG_ETH_USE_SPI_ETHERNET=n + +# Enable debug logging +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y \ No newline at end of file diff --git a/test_apps/physical_tests/sdkconfig.ci.serial b/test_apps/physical_tests/sdkconfig.ci.serial new file mode 100644 index 0000000..4fbe485 --- /dev/null +++ b/test_apps/physical_tests/sdkconfig.ci.serial @@ -0,0 +1,30 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_TEST_SLAVE_TASK_PRIO=5 +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + +# Avoid CI issues "Warning: The smallest app partition is nearly full (5% free space left)!" +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# Enable debug logging +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y + diff --git a/test_apps/physical_tests/sdkconfig.ci.wifi b/test_apps/physical_tests/sdkconfig.ci.wifi new file mode 100644 index 0000000..2cd6de6 --- /dev/null +++ b/test_apps/physical_tests/sdkconfig.ci.wifi @@ -0,0 +1,24 @@ +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_PORT_DEFAULT=502 +CONFIG_FMB_TCP_CONNECTION_TOUT_SEC=20 +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=n +CONFIG_FMB_COMM_MODE_ASCII_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=3000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TCP_UID_ENABLED=n +CONFIG_EXAMPLE_CONNECT_IPV6=n +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_EXAMPLE_CONNECT_ETHERNET=n +CONFIG_EXAMPLE_CONNECT_WIFI=y +CONFIG_EXAMPLE_WIFI_SSID="${CI_WIFI_SSID}" +CONFIG_EXAMPLE_WIFI_PASSWORD="${CI_WIFI_PASSW}" + + +# Avoid CI issues "Warning: The smallest app partition is nearly full (5% free space left)!" +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# Enable debug logging +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y diff --git a/test_apps/physical_tests/sdkconfig.defaults b/test_apps/physical_tests/sdkconfig.defaults new file mode 100644 index 0000000..a0107e8 --- /dev/null +++ b/test_apps/physical_tests/sdkconfig.defaults @@ -0,0 +1,27 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 +CONFIG_MB_TEST_MASTER_TASK_PRIO=5 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 + +# Avoid CI issues "Warning: The smallest app partition is nearly full (5% free space left)!" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y + +# Enable debug logging +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y diff --git a/test_apps/test_common/CMakeLists.txt b/test_apps/test_common/CMakeLists.txt new file mode 100644 index 0000000..bc347b1 --- /dev/null +++ b/test_apps/test_common/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) +idf_component_register(SRCS "test_common.c" + INCLUDE_DIRS "include" + REQUIRES unity) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") + +add_subdirectory(mb_utest_lib) +target_link_libraries(${COMPONENT_LIB} PUBLIC mb_ut_lib) diff --git a/test_apps/test_common/README.md b/test_apps/test_common/README.md new file mode 100644 index 0000000..bc45836 --- /dev/null +++ b/test_apps/test_common/README.md @@ -0,0 +1,3 @@ +# Test common + +This directory contains component that is common for Modbus master and slave tests. diff --git a/test_apps/test_common/include/test_common.h b/test_apps/test_common/include/test_common.h new file mode 100644 index 0000000..c0d99f6 --- /dev/null +++ b/test_apps/test_common/include/test_common.h @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include + +#include "unity.h" +#include "unity_test_runner.h" + +#include "mbcontroller.h" // for common Modbus defines +#include "esp_log.h" + +#include "sdkconfig.h" + +#define STR(fieldname) ((const char *)(fieldname)) +#define OPTS(min_val, max_val, step_val) \ +{ \ + .opt1 = min_val, .opt2 = max_val, .opt3 = step_val \ +} + +// Enumeration of modbus slave addresses accessed by master device +enum +{ + MB_DEVICE_ADDR1 = 1, + MB_DEVICE_ADDR2 = 200 +}; + +// Enumeration of all supported CIDs for device (used in parameter definition table) +enum { + CID_DEV_REG0 = 0, + CID_DEV_REG1, + CID_DEV_REG2, + CID_DEV_REG3, + CID_DEV_REG_COUNT +}; + +// Enumeration of predefined test values +enum { + TEST_REG_VAL1 = 0x1111, + TEST_REG_VAL2 = 0x2222, + TEST_REG_VAL3 = 0x3333, + TEST_REG_VAL4 = 0x4444 +}; + +typedef struct task_entry_s { + TaskHandle_t task_handle; + SemaphoreHandle_t task_sema_handle; + void *inst_handle; + LIST_ENTRY(task_entry_s) entries; +} task_entry_t; + +/** + * @brief Start, stop helpers for common test module + * + */ +void test_common_start(); +void test_common_stop(); + +/** + * @brief Helper test functions for multi instance modbus master - slave test + * + */ +// TaskHandle_t test_common_slave_serial_create(mb_communication_info_t *pconfig); + +TaskHandle_t test_common_slave_serial_create(mb_communication_info_t *pconfig, uint32_t priority); +TaskHandle_t test_common_master_serial_create(mb_communication_info_t *pconfig,uint32_t priority, const mb_parameter_descriptor_t *pdescr, uint16_t descr_size); +TaskHandle_t test_common_slave_tcp_create(mb_communication_info_t *pconfig, uint32_t priority); +TaskHandle_t test_common_master_tcp_create(mb_communication_info_t *pconfig, uint32_t priority, const mb_parameter_descriptor_t *pdescr, uint16_t descr_size); +TaskHandle_t test_common_start_busy_task(uint32_t priority); + +/** + * @brief The test helper function to check memory leak + * + */ +void test_common_check_leak(size_t before_free, size_t after_free, const char *type, size_t warn_threshold, size_t critical_threshold); + +// Slave setup register area helper +void test_common_slave_setup_start(void *mbs_handle); +// Helper function to read characteristic from slave +esp_err_t test_common_read_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data); +// Helper function to write characteristic into slave +esp_err_t test_common_write_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data); + +/** + * @brief The test helper functions to work with test tasks + * + */ +void test_common_task_notify_start(TaskHandle_t task_handle, uint32_t value); +void test_common_task_start(TaskHandle_t task_handle, uint32_t value); +int test_common_task_start_all(uint32_t value); +bool test_common_task_wait_done(TaskHandle_t task_handle, TickType_t timeout_ticks); +bool test_common_task_wait_done_delete(TaskHandle_t task_handle, TickType_t task_timeout_ticks); +int test_common_task_wait_done_delete_all(TickType_t task_timeout_tick); +void test_common_task_delete(TaskHandle_t task_handle); +void test_common_task_delete_all(); +void *test_common_task_get_instance(TaskHandle_t task_handle); + + diff --git a/test_apps/test_common/mb_utest_lib/CMakeLists.txt b/test_apps/test_common/mb_utest_lib/CMakeLists.txt new file mode 100644 index 0000000..b396969 --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/CMakeLists.txt @@ -0,0 +1,75 @@ +message(STATUS "mb_ut_lib: ${CMAKE_CURRENT_LIST_DIR}, ${CONFIG_MB_UTEST}") + +add_library(mb_ut_lib "${CMAKE_CURRENT_LIST_DIR}/port_adapter.c" + "${CMAKE_CURRENT_LIST_DIR}/port_stubs.c") + +idf_component_get_property(dir esp-modbus COMPONENT_DIR) +target_include_directories(mb_ut_lib PUBLIC + "${CMAKE_CURRENT_LIST_DIR}" + "${CMAKE_CURRENT_LIST_DIR}/include" + "${dir}/modbus/mb_controller/common" + "${dir}/modbus/mb_controller/common/include" + "${dir}/modbus/mb_controller/serial" + "${dir}/modbus/mb_controller/tcp" + "${dir}/modbus/mb_objects/include" + "${dir}/modbus/mb_ports/common" + "${dir}/modbus/mb_ports/serial" + "${dir}/modbus/mb_ports/tcp" + "${dir}/modbus/mb_transports" + "${dir}/modbus/mb_transports/rtu" + "${dir}/modbus/mb_transports/ascii" + "${dir}/modbus/mb_transports/tcp" + ) + +idf_component_get_property(driver_lib driver COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${driver_lib}) +idf_component_get_property(timer_lib esp_timer COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${timer_lib}) +idf_component_get_property(netif_lib esp_netif COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${netif_lib}) +idf_component_get_property(test_utils_lib test_utils COMPONENT_LIB) +target_link_libraries(mb_ut_lib PUBLIC ${test_utils_lib}) + + +# Wrap port functions to substitute port with port_adapter object +if(CONFIG_MB_PORT_ADAPTER_EN) + + set(WRAP_FUNCTIONS + mbm_port_tcp_create + mbm_port_tcp_set_conn_cb + mbm_port_tcp_get_slave_info + mbm_port_tcp_send_data + mbm_port_tcp_recv_data + mbm_port_tcp_enable + mbm_port_tcp_disable + mbm_port_tcp_delete + mbs_port_tcp_create + mbs_port_tcp_enable + mbs_port_tcp_disable + mbs_port_tcp_send_data + mbs_port_tcp_recv_data + mbs_port_tcp_delete + mb_port_event_post + mb_port_event_get + mb_port_ser_create + mb_port_ser_recv_data + mb_port_ser_send_data + mb_port_ser_enable + mb_port_ser_disable + mb_port_ser_delete + ) + +foreach(wrap ${WRAP_FUNCTIONS}) + target_link_libraries(mb_ut_lib PUBLIC "-Wl,--undefined=${wrap}") + target_link_libraries(mb_ut_lib PUBLIC "-Wl,--wrap=${wrap}") + #target_link_libraries(mb_ut_lib INTERFACE "-u __wrap_${wrap}") +endforeach() + +endif() + +# allow multiple symbol definitions +target_link_libraries(mb_ut_lib PUBLIC "-Wl,--allow-multiple-definition") + +if(CONFIG_MB_PORT_ADAPTER_EN) + set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_stub_impl") +endif() \ No newline at end of file diff --git a/test_apps/test_common/mb_utest_lib/port_adapter.c b/test_apps/test_common/mb_utest_lib/port_adapter.c new file mode 100644 index 0000000..2057bfc --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_adapter.c @@ -0,0 +1,672 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + +#include "esp_timer.h" +#include "sdkconfig.h" +#include "esp_log.h" +#include "esp_err.h" + +#include "mb_common.h" +#include "esp_modbus_common.h" +#include "mbc_slave.h" + +#include "mb_common.h" +#include "port_common.h" +#include "mb_config.h" +#include "port_serial_common.h" +#include "port_adapter.h" +#include "mb_port_types.h" +#include "port_stubs.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* ----------------------- Defines ------------------------------------------*/ + +#define MB_ADAPTER_TASK_STACK_SIZE (CONFIG_FMB_PORT_TASK_STACK_SIZE) +#define MB_ADAPTER_MAX_PORTS (8) +#define MB_ADAPTER_RX_QUEUE_MAX_SIZE (CONFIG_FMB_QUEUE_LENGTH * MB_ADAPTER_MAX_PORTS) +#define MB_ADAPTER_TX_QUEUE_MAX_SIZE (CONFIG_FMB_QUEUE_LENGTH * MB_ADAPTER_MAX_PORTS) +#define MB_ADAPTER_QUEUE_TIMEOUT (200 / portTICK_PERIOD_MS) +#define MB_ADAPTER_QUEUE_SET_MAX_LEN ((sizeof(frame_entry_t) + sizeof(mb_uid_info_t)) * MB_ADAPTER_MAX_PORTS) // +#define MB_ADAPTER_CONN_TIMEOUT (200 / portTICK_PERIOD_MS) + +typedef struct _mb_adapter_port_entry +{ + mb_port_base_t base; + uint8_t rx_buffer[CONFIG_FMB_BUFFER_SIZE]; + uint16_t recv_length; + uint64_t send_time_stamp; + uint64_t recv_time_stamp; + _Atomic uint64_t test_timeout_us; + uint32_t flags; + mb_uid_info_t addr_info; + QueueHandle_t rx_queue; + QueueHandle_t tx_queue; + QueueHandle_t conn_queue; /*!< conection queue handle */ + SemaphoreHandle_t conn_sema_handle; /*!< connection blocking semaphore handle */ + esp_timer_handle_t timer_handle; + EventGroupHandle_t event_group_handle; + LIST_ENTRY(_mb_adapter_port_entry) entries; +} mb_port_adapter_t; + +/* ----------------------- Static variables & functions ----------------------*/ +static const char *TAG = "mb_port.test_adapter"; + +static LIST_HEAD(mb_port_inst, _mb_adapter_port_entry) s_port_list = LIST_HEAD_INITIALIZER(s_port_list); +static uint32_t s_port_list_counter = 0; /*!< port registered instance counter */ + +// The queue set for the receive task +static QueueSetHandle_t queue_set = NULL; +static TaskHandle_t adapter_task_handle; /*!< receive task handle */ + +IRAM_ATTR +static bool mb_port_adapter_timer_expired(void *inst) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + + bool need_poll = false; + mb_timer_mode_enum_t timer_mode = mb_port_get_cur_timer_mode(&port_obj->base); + + mb_port_timer_disable(&port_obj->base); + + switch (timer_mode) + { + case MB_TMODE_T35: + need_poll = mb_port_event_post(&port_obj->base, EVENT(EV_READY)); + ESP_EARLY_LOGD(TAG, "%p:EV_READY", port_obj->base.descr.parent); + break; + + case MB_TMODE_RESPOND_TIMEOUT: + mb_port_event_set_err_type(&port_obj->base, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_event_post(&port_obj->base, EVENT(EV_ERROR_PROCESS)); + + ESP_EARLY_LOGW(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", port_obj->base.descr.parent); + break; + + case MB_TMODE_CONVERT_DELAY: + /* If timer mode is convert delay, the master event then turns EV_MASTER_EXECUTE status. */ + need_poll = mb_port_event_post(&port_obj->base, EVENT(EV_EXECUTE)); + ESP_EARLY_LOGD(TAG, "%p:MB_TMODE_CONVERT_DELAY", port_obj->base.descr.parent); + break; + + default: + need_poll = mb_port_event_post(&port_obj->base, EVENT(EV_READY)); + break; + } + + return need_poll; +} + +void mb_port_adapter_set_response_time(mb_port_base_t *inst, uint64_t resp_time) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + atomic_store(&(port_obj->test_timeout_us), resp_time); +} + +int mb_port_adapter_get_rx_buffer(mb_port_base_t *inst, uint8_t **ppfame, int *plen) +{ + MB_RETURN_ON_FALSE((ppfame && plen), -1, TAG, "mb serial get buffer failure."); + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + int sz = port_obj->recv_length; + if (*ppfame && *plen >= port_obj->recv_length) + { + CRITICAL_SECTION(inst->lock) + { + memcpy(*ppfame, port_obj->rx_buffer, sz); + } + } + else + { + *ppfame = port_obj->rx_buffer; + *plen = sz; + } + return sz; +} + +int mb_port_adapter_get_tx_buffer(mb_port_base_t *inst, uint8_t **ppfame, int *plen) +{ + MB_RETURN_ON_FALSE((ppfame && plen), -1, TAG, "mb serial get buffer failure."); + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + int sz = port_obj->recv_length; + if (*ppfame && *plen >= port_obj->recv_length) + { + CRITICAL_SECTION(inst->lock) + { + memcpy(*ppfame, port_obj->rx_buffer, sz); + } + } + else + { + *ppfame = port_obj->rx_buffer; + *plen = sz; + } + return sz; +} + +void mb_port_adapter_set_flag(mb_port_base_t *inst, mb_queue_flags_t mask) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + EventBits_t bits = xEventGroupSetBits(port_obj->event_group_handle, (EventBits_t)mask); + ESP_LOGV(TAG, "%s: set flag (0x%x).", inst->descr.parent_name, (int)bits); +} + +void mb_port_adapter_clear_flag(mb_port_base_t *inst, mb_queue_flags_t mask) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + EventBits_t bits = xEventGroupClearBits(port_obj->event_group_handle, (EventBits_t)mask); + ESP_LOGV(TAG, "%s: clear flag (0x%x).", inst->descr.parent_name, (int)bits); +} + +uint16_t mb_port_adapter_wait_flag(mb_port_base_t *inst, uint16_t mask, uint32_t timeout) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + EventBits_t bits = xEventGroupWaitBits(port_obj->event_group_handle, // The event group being tested. + (EventBits_t)mask, // The bits within the event group to wait for. + pdTRUE, // Masked bits should be cleared before returning. + pdFALSE, // Don't wait for both bits, either bit will do. + (TickType_t)timeout); // Wait during timeout for either bit to be set. + ESP_LOGV(TAG, "%s: get flag (0x%x).", inst->descr.parent_name, (int)bits); + return (uint16_t)bits; +} + +// Timer task to send notification on timeout expiration +IRAM_ATTR +static void mb_port_adapter_timer_cb(void *param) +{ + mb_port_adapter_t *port_obj = __containerof(param, mb_port_adapter_t, base); + uint8_t temp_buffer[CONFIG_FMB_BUFFER_SIZE] = {0}; + mb_port_adapter_t *it; + + if (!LIST_EMPTY(&s_port_list)) + { + // send the queued frame to all registered ports with the same port number + int sz = queue_pop(port_obj->tx_queue, (void *)&temp_buffer[0], CONFIG_FMB_BUFFER_SIZE, NULL); + LIST_FOREACH(it, &s_port_list, entries) + { + if (it && (it != port_obj) && + (port_obj->addr_info.port == it->addr_info.port) && (sz != -1) + && (port_obj->addr_info.proto == it->addr_info.proto) + && (!port_obj->addr_info.uid || !it->addr_info.uid)) + { + // Send the data to all ports with the same communication port setting except itself + queue_push(it->rx_queue, (void *)&temp_buffer[0], sz, NULL); + mb_port_adapter_set_flag(&port_obj->base, MB_QUEUE_FLAG_SENT); + ESP_LOGD(TAG, "Send (%d bytes) from %s to %s. ", (int)sz, port_obj->base.descr.parent_name, it->base.descr.parent_name); + } + } + } +} + +bool mb_port_adapter_is_connected(void *inst) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + if (queue_is_empty(port_obj->conn_queue) + && port_obj->base.descr.is_master) { + return true; + } + return false; +} + +static void mb_port_adapter_conn_logic(void *inst, mb_uid_info_t *paddr_info) +{ + bool slave_found = false; + mb_port_adapter_t *slave = NULL; + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + + if (port_obj->base.descr.is_master) { // master object + LIST_FOREACH(slave, &s_port_list, entries) { + if ((paddr_info->uid == slave->addr_info.uid) + && !slave->base.descr.is_master + && (paddr_info->port == slave->addr_info.port)) { + // Register each slave object + ESP_LOGD(TAG, "Check connection state of object #%d(%s), uid: %d, port: %d, %s", + paddr_info->index, paddr_info->node_name_str, + paddr_info->uid, paddr_info->port, + (paddr_info->state == MB_SOCK_STATE_CONNECTED) ? "CONNECTED" : "DISCONNECTED"); + if ((paddr_info->state != MB_SOCK_STATE_CONNECTED) || (paddr_info->inst != inst)) { + (void)xQueueSend(slave->conn_queue, &port_obj->addr_info, MB_ADAPTER_QUEUE_TIMEOUT); + } else { + mb_port_adapter_set_flag(inst, MB_QUEUE_FLAG_CONNECTED); + } + slave_found = true; + break; + } + } + if (!slave_found) { + // reactivate the connection set + ESP_LOGE(TAG, "Slave #%d(%s), uid: %d, port: %d is not found, reconnect.", + paddr_info->index, paddr_info->node_name_str, paddr_info->uid, paddr_info->port); + (void)xQueueSend(port_obj->conn_queue, paddr_info, MB_ADAPTER_QUEUE_TIMEOUT); + vTaskDelay(MB_ADAPTER_CONN_TIMEOUT); + } + } else { // slave connection logic + ESP_LOGD(TAG, "Register connection in adapter object #%d(%s), uid: %d, port: %d, to master %s", + port_obj->addr_info.index, port_obj->addr_info.node_name_str, + port_obj->addr_info.uid, port_obj->addr_info.port, paddr_info->node_name_str); + // Mimic connection logic for each slave here + //mb_port_adapter_slave_connect(it); + port_obj->addr_info.state = MB_SOCK_STATE_CONNECTED; + mb_port_adapter_t *master = (mb_port_adapter_t *)(paddr_info->inst); + port_obj->addr_info.inst = paddr_info->inst; // link slave with master + (void)xQueueSend(master->conn_queue, &port_obj->addr_info, MB_ADAPTER_QUEUE_TIMEOUT); + } +} + +// UART receive event task +static void mb_port_adapter_task(void *p_args) +{ + QueueSetMemberHandle_t active_queue = NULL; + mb_port_adapter_t *it = NULL; + mb_uid_info_t addr_info; + frame_entry_t frame_entry; + + while (1) + { + if (!LIST_EMPTY(&s_port_list)) + { + active_queue = xQueueSelectFromSet(queue_set, MB_ADAPTER_QUEUE_TIMEOUT); + LIST_FOREACH(it, &s_port_list, entries) + { + if (it && active_queue && (it->rx_queue == active_queue)) { + if (xQueuePeek(it->rx_queue, &frame_entry, 0) == pdTRUE) { + it->recv_length = frame_entry.len; + mb_port_event_post(&it->base, EVENT(EV_FRAME_RECEIVED, frame_entry.len, NULL, 0)); + ESP_LOGD(TAG, "%s, frame %d bytes is ready.", (it->base.descr.parent_name), (int)frame_entry.len); + } + } else if (it && (it->conn_queue == active_queue)) { + if (xQueueReceive(it->conn_queue, &addr_info, MB_ADAPTER_QUEUE_TIMEOUT) == pdTRUE) { + mb_port_adapter_conn_logic(it, &addr_info); + } + } + } + } + else + { + vTaskDelay(1); + } + } + vTaskDelete(NULL); +} + +static mb_err_enum_t mb_port_adapter_connect(mb_tcp_opts_t *tcp_opts, void *pobject) +{ + char **paddr_table = tcp_opts->ip_addr_table; + mb_uid_info_t uid_info; + mb_port_adapter_t *port_obj = __containerof(pobject, mb_port_adapter_t, base); + + MB_RETURN_ON_FALSE((paddr_table && *paddr_table && (tcp_opts->mode == MB_TCP)), + MB_EINVAL, TAG, + "%s, invalid address table.", port_obj->base.descr.parent_name); + int count = 0; + while (*paddr_table) + { + int res = port_scan_addr_string((char *)*paddr_table, &uid_info); + if (res > 0) + { + ESP_LOGD(TAG, "Config: %s, IP: %s, port: %d, slave_addr: %d, ip_ver: %s", + (char *)*paddr_table, uid_info.ip_addr_str, uid_info.port, + uid_info.uid, (uid_info.addr_type == MB_IPV4 ? "IPV4" : "IPV6")); + uid_info.index = count++; + free(uid_info.ip_addr_str); + uid_info.ip_addr_str = (char *)*paddr_table; + uid_info.node_name_str = uid_info.ip_addr_str; + if (xQueueSend(port_obj->conn_queue, &uid_info, MB_EVENT_QUEUE_TIMEOUT_MAX) != pdTRUE) + { + ESP_LOGE(TAG, "can not send info to connection queue."); + }; + // Mimic connection event + if (!tcp_opts->start_disconnected) { + uint16_t event = mb_port_adapter_wait_flag(pobject, MB_QUEUE_FLAG_CONNECTED, MB_ADAPTER_CONN_TIMEOUT); + if (!event) { + ESP_LOGE(TAG, "Could not connect to slave %s during timeout.", (char *)*paddr_table); + } + } + } + else + { + ESP_LOGE(TAG, "unable to open slave: %s, check configuration.", (char *)*paddr_table); + } + paddr_table++; + } + ESP_LOGD(TAG, "parsed and added %d slave configurations.", count); + return count ? MB_ENOERR : MB_EINVAL; +} + +mb_err_enum_t mb_port_adapter_create(mb_uid_info_t *paddr_info, mb_port_base_t **in_out_obj) +{ + mb_port_adapter_t *padapter = NULL; + mb_err_enum_t ret = MB_EILLSTATE; + padapter = (mb_port_adapter_t *)calloc(1, sizeof(mb_port_adapter_t)); + + MB_GOTO_ON_FALSE((padapter && paddr_info && in_out_obj), MB_EILLSTATE, error, TAG, "mb serial port creation error."); + + CRITICAL_SECTION_INIT(padapter->base.lock); + padapter->base.descr = ((mb_port_base_t *)*in_out_obj)->descr; + padapter->addr_info = *paddr_info; + + esp_timer_create_args_t timer_conf = { + .callback = mb_port_adapter_timer_cb, + .arg = padapter, + .dispatch_method = ESP_TIMER_TASK, + .name = padapter->base.descr.parent_name + }; + // Create Modbus timer handlers for streams + MB_GOTO_ON_ERROR(esp_timer_create(&timer_conf, &padapter->timer_handle), + error, TAG, "create input stream timer failed."); + + padapter->rx_queue = queue_create(MB_ADAPTER_RX_QUEUE_MAX_SIZE); + MB_GOTO_ON_FALSE(padapter->rx_queue, MB_EILLSTATE, error, TAG, "create rx queue failed"); + padapter->tx_queue = queue_create(MB_ADAPTER_TX_QUEUE_MAX_SIZE); + MB_GOTO_ON_FALSE(padapter->tx_queue, MB_EILLSTATE, error, TAG, "create tx queue failed"); + padapter->event_group_handle = xEventGroupCreate(); + MB_GOTO_ON_FALSE((padapter->event_group_handle), MB_EILLSTATE, error, TAG, + "%p, event group create error.", *in_out_obj); + + if (!s_port_list_counter) + { + // Create a task to handle UART events + BaseType_t status = xTaskCreatePinnedToCore(mb_port_adapter_task, "adapt_rx_task", + MB_ADAPTER_TASK_STACK_SIZE, + &padapter->base, CONFIG_FMB_PORT_TASK_PRIO, + &adapter_task_handle, CONFIG_FMB_PORT_TASK_AFFINITY); + // Force exit from function with failure + MB_GOTO_ON_FALSE((status == pdPASS), MB_EILLSTATE, error, TAG, + "serial task creation error, returned (0x%x).", (int)status); + // Create the queue set to handle clients + queue_set = xQueueCreateSet(MB_ADAPTER_QUEUE_SET_MAX_LEN); + MB_GOTO_ON_FALSE((queue_set), MB_EILLSTATE, error, TAG, "can not create queue set."); + } + // Add connection set for master object only + padapter->conn_queue = xQueueCreate(MB_ADAPTER_MAX_PORTS, sizeof(mb_uid_info_t)); + MB_GOTO_ON_FALSE(padapter->conn_queue, MB_EILLSTATE, error, TAG, "create conn queue failed"); + MB_GOTO_ON_FALSE((queue_set && xQueueAddToSet(padapter->conn_queue, queue_set)), + MB_EILLSTATE, error, TAG, "can not add conn queue to queue set."); + // Add rx queue to set + MB_GOTO_ON_FALSE((queue_set && xQueueAddToSet(padapter->rx_queue, queue_set)), + MB_EILLSTATE, error, TAG, "can not add rx queue to queue set."); + + MB_GOTO_ON_FALSE((s_port_list_counter <= MB_ADAPTER_MAX_PORTS), MB_EILLSTATE, error, + TAG, "adapter exceeded maximum number of ports = %d", MB_ADAPTER_MAX_PORTS); + + // register new port instance in the list + LIST_INSERT_HEAD(&s_port_list, padapter, entries); + s_port_list_counter++; + char *pstr; + int res = asprintf(&pstr, "%d;%s;%u", (unsigned)paddr_info->uid, + padapter->base.descr.parent_name, (unsigned)paddr_info->port); + MB_GOTO_ON_FALSE((res), MB_EILLSTATE, error, + TAG, "object adress info alloc fail, err: %d", (int)res); + padapter->base.cb.tmr_expired = mb_port_adapter_timer_expired; + padapter->base.cb.tx_empty = NULL; + padapter->base.cb.byte_rcvd = NULL; + padapter->base.arg = (void *)padapter; + + padapter->addr_info.state = MB_SOCK_STATE_CONNECTING; + padapter->addr_info.inst = padapter; + padapter->addr_info.node_name_str = pstr; + padapter->addr_info.ip_addr_str = pstr; + *in_out_obj = &(padapter->base); + ESP_LOGD(TAG, "created object @%p, from parent %p", padapter, padapter->base.descr.parent); + return MB_ENOERR; + +error: + if (padapter) { + mb_port_adapter_delete(&padapter->base); + } + return ret; +} + +mb_err_enum_t mb_port_adapter_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj) +{ + mb_uid_info_t addr_info = { + .ip_addr_str = NULL, + .index = s_port_list_counter, + .addr_type = MB_IPV4, + .uid = tcp_opts->uid, + .port = tcp_opts->port, + .proto = MB_TCP, + .state = MB_SOCK_STATE_UNDEF + }; + + mb_port_base_t *pobj = *in_out_obj; + mb_err_enum_t ret = mb_port_adapter_create(&addr_info, &pobj); + + if ((ret == MB_ENOERR) && pobj) { + // Parse master config and register dependent objects + if (pobj->descr.is_master) { + ESP_LOGI(TAG, "Parsing of config for %s", pobj->descr.parent_name); + ret |= mb_port_adapter_connect(tcp_opts, pobj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, + "%s, could not parse config, err=%x.", pobj->descr.parent_name, (int)ret); + } + ESP_LOGD(TAG, "%s, set test time to %" PRIu64, pobj->descr.parent_name, tcp_opts->test_tout_us); + mb_port_adapter_set_response_time(pobj, (tcp_opts->test_tout_us)); + } + *in_out_obj = pobj; + return ret; + +error: + if (pobj) { + mb_port_adapter_delete(pobj); + } + return ret; +} + + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +mb_err_enum_t mb_port_adapter_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj) +{ + mb_uid_info_t addr_info = { + .ip_addr_str = NULL, // unknown + .index = s_port_list_counter, + .addr_type = MB_NOIP, + .uid = ser_opts->uid, + .port = ser_opts->port, + .proto = ser_opts->mode, + .state = MB_SOCK_STATE_UNDEF + }; + + mb_port_base_t *pobj = *in_out_obj; + mb_err_enum_t ret = mb_port_adapter_create(&addr_info, &pobj); + if ((ret == MB_ENOERR) && pobj) { + ESP_LOGD(TAG, "%s, set test time to %d", pobj->descr.parent_name, (int)(ser_opts->test_tout_us)); + mb_port_adapter_set_response_time(pobj, (ser_opts->test_tout_us)); + *in_out_obj = pobj; + } + return ret; +} +#endif + +void mb_port_adapter_delete(mb_port_base_t *inst) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + + if (port_obj->rx_queue && !queue_is_empty(port_obj->rx_queue)) + { + queue_flush(port_obj->rx_queue); + } + if (port_obj->tx_queue && !queue_is_empty(port_obj->tx_queue)) + { + queue_flush(port_obj->tx_queue); + } + if (port_obj && port_obj->event_group_handle) + { + vEventGroupDelete(port_obj->event_group_handle); + port_obj->event_group_handle = NULL; + } + if (port_obj && port_obj->timer_handle) + { + esp_timer_stop(port_obj->timer_handle); + esp_timer_delete(port_obj->timer_handle); + port_obj->timer_handle = NULL; + } + CRITICAL_SECTION_CLOSE(inst->lock); + LIST_REMOVE(port_obj, entries); + if (s_port_list_counter) + { + atomic_store(&(s_port_list_counter), (s_port_list_counter - 1)); + if (queue_set && port_obj && port_obj->rx_queue) + { + xQueueRemoveFromSet(port_obj->rx_queue, queue_set); + } + if (port_obj && port_obj->conn_queue && queue_set) + { + xQueueRemoveFromSet(port_obj->conn_queue, queue_set); + } + } + if (!s_port_list_counter) + { + if (port_obj && adapter_task_handle) { + vTaskDelete(adapter_task_handle); + adapter_task_handle = NULL; + } + vQueueDelete(queue_set); + queue_set = NULL; + } + if (port_obj && port_obj->rx_queue && port_obj->tx_queue) + { + queue_delete(port_obj->rx_queue); + queue_delete(port_obj->tx_queue); + port_obj->rx_queue = NULL; + port_obj->tx_queue = NULL; + } + if (port_obj && port_obj->conn_queue) + { + vQueueDelete(port_obj->conn_queue); + port_obj->conn_queue = NULL; + } + if (port_obj && port_obj->addr_info.node_name_str) + { + free(port_obj->addr_info.node_name_str); + port_obj->addr_info.node_name_str = NULL; + port_obj->addr_info.ip_addr_str = NULL; + } + free(port_obj); +} + +static esp_err_t mb_port_adapter_set_timer(mb_port_base_t *inst, uint64_t time_diff_us) +{ + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + esp_timer_stop(port_obj->timer_handle); + esp_err_t ret = esp_timer_start_once(port_obj->timer_handle, time_diff_us); + MB_RETURN_ON_FALSE((ret == ESP_OK), + ESP_ERR_INVALID_STATE, TAG, + "%s, could not start timer, err=%x.", inst->descr.parent_name, (int)ret); + return ESP_OK; +} + +bool mb_port_adapter_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + MB_RETURN_ON_FALSE((ppframe && plength), false, TAG, "mb serial get buffer failure."); + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + int length = *plength ? *plength : port_obj->recv_length; + + if (length) + { + CRITICAL_SECTION_LOCK(inst->lock); + int length = queue_pop(port_obj->rx_queue, &port_obj->rx_buffer[0], CONFIG_FMB_BUFFER_SIZE, NULL); + if (length) + { + mb_port_timer_disable(inst); + ESP_LOGD(TAG, "%s, received data: %d bytes.", inst->descr.parent_name, length); + // Stop timer because the new data is received + // Store the timestamp of received frame + port_obj->recv_time_stamp = esp_timer_get_time(); + *ppframe = &port_obj->rx_buffer[0]; + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":PORT_RECV"), + (void *)&port_obj->rx_buffer[0], (uint16_t)length, ESP_LOG_DEBUG); + } + CRITICAL_SECTION_UNLOCK(inst->lock); + } + else + { + ESP_LOGE(TAG, "%s: junk data (%d bytes) received. ", inst->descr.parent_name, length); + } + *plength = length; + return true; +} + +bool mb_port_adapter_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length) +{ + bool res = false; + mb_port_adapter_t *port_obj = __containerof(inst, mb_port_adapter_t, base); + uint64_t time_diff = atomic_load(&port_obj->test_timeout_us); + + if (pframe && length) + { + CRITICAL_SECTION_LOCK(inst->lock); + esp_err_t err = queue_push(port_obj->tx_queue, (void *)pframe, length, NULL); + CRITICAL_SECTION_UNLOCK(inst->lock); + MB_RETURN_ON_FALSE((err == ESP_OK), + false, TAG, "%s, could not send the data into queue.", inst->descr.parent_name); + MB_RETURN_ON_FALSE((mb_port_adapter_set_timer(inst, time_diff) == ESP_OK), + false, TAG, "%s, could not set output timer.", inst->descr.parent_name); + // Wait for send buffer complition + uint16_t flags = mb_port_adapter_wait_flag(inst, MB_QUEUE_FLAG_SENT, MB_EVENT_QUEUE_TIMEOUT_MAX); + port_obj->send_time_stamp = esp_timer_get_time(); + // Print sent packet, the tag used is more clear to see + ESP_LOG_BUFFER_HEX_LEVEL(MB_STR_CAT(inst->descr.parent_name, ":PORT_SEND"), + (void *)pframe, length, ESP_LOG_DEBUG); + (void)mb_port_event_post(inst, EVENT(EV_FRAME_SENT)); + ESP_LOGD(TAG, "%s, tx completed, flags = 0x%04x.", inst->descr.parent_name, (int)flags); + res = true; + + } + else + { + ESP_LOGE(TAG, "send callback %p, %u. ", pframe, (unsigned)length); + } + return res; +} + +void mb_port_adapter_enable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter tcp enable port."); +} + +void mb_port_adapter_disable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter tcp disable port."); +} + +mb_uid_info_t *mb_port_adapter_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state) +{ + mb_port_adapter_t *it = NULL; + + if (!LIST_EMPTY(&s_port_list)) + { + LIST_FOREACH(it, &s_port_list, entries) + { + if ((it->addr_info.uid == slave_addr) && (it->base.descr.is_master == false)) + { + return (&it->addr_info); + } + } + } + return NULL; +} + +void mb_port_adapter_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg) +{ + void (*on_conn_done_cb)(void *) = conn_fp; + + if (mb_port_adapter_is_connected(inst)) { + if (on_conn_done_cb && arg) { + on_conn_done_cb(arg); + } + } +} + +#ifdef __cplusplus +} +#endif diff --git a/test_apps/test_common/mb_utest_lib/port_adapter.h b/test_apps/test_common/mb_utest_lib/port_adapter.h new file mode 100644 index 0000000..e1a7c33 --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_adapter.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_log.h" +#include "mb_common.h" +#include + +#include "port_tcp_utils.h" +#include "port_common.h" + +typedef enum +{ + MB_QUEUE_FLAG_EMPTY = 0x0000, + MB_QUEUE_FLAG_SENT = 0x0001, + MB_QUEUE_FLAG_RECV = 0x0002, + MB_QUEUE_FLAG_CONNECTED = 0x0004 +} mb_queue_flags_t; + +#define MB_QUEUE_FLAGS (MB_QUEUE_FLAG_SENT | MB_QUEUE_FLAG_RECV | MB_QUEUE_FLAG_CONNECTED) + +typedef struct _uid_info mb_uid_info_t; + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) +mb_err_enum_t mb_port_adapter_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj); +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) +mb_err_enum_t mb_port_adapter_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +#endif + +void mb_port_adapter_delete(mb_port_base_t *inst); +void mb_port_adapter_set_response_time(mb_port_base_t *inst, uint64_t resp_time); +int mb_port_adapter_get_rx_buffer(mb_port_base_t *inst, uint8_t **ppfame, int *plength); + +bool mb_port_adapter_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length); +bool mb_port_adapter_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +void mb_port_adapter_enable(mb_port_base_t *inst); +void mb_port_adapter_disable(mb_port_base_t *inst); +void mb_port_adapter_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg); +void mb_port_adapter_tcp_set_conn_time(mb_port_base_t *inst, void *conn_fp, void *arg); +mb_uid_info_t *mb_port_adapter_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state); diff --git a/test_apps/test_common/mb_utest_lib/port_stubs.c b/test_apps/test_common/mb_utest_lib/port_stubs.c new file mode 100644 index 0000000..9e11570 --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_stubs.c @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" + +#include "esp_timer.h" +#include "esp_log.h" +#include "esp_err.h" + +#include "mb_common.h" +#include "esp_modbus_common.h" +#include "mbc_slave.h" + +#include "mb_common.h" +#include "port_common.h" +#include "mb_config.h" +#include "port_serial_common.h" +#include "port_tcp_common.h" +#include "port_adapter.h" +#include "port_stubs.h" + +#include "sdkconfig.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static __attribute__((unused)) const char *TAG = "port_stub"; + +#if (CONFIG_MB_PORT_ADAPTER_EN) + +// The workaround to statically link whole test library +__attribute__((unused)) bool mb_test_include_stub_impl = true; + +// Below are function wrappers to substitute actual port object with the adapter object for test purpose + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +mb_err_enum_t __wrap_mb_port_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj) +{ + return mb_port_adapter_ser_create(ser_opts, in_out_obj); +} + +void __wrap_mb_port_ser_delete(mb_port_base_t *inst) +{ + mb_port_adapter_delete(inst); +} + +bool __wrap_mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + return mb_port_adapter_recv_data(inst, ppframe, plength); +} + +bool __wrap_mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length) +{ + return mb_port_adapter_send_data(inst, 0, pframe, length); +} + +void __wrap_mb_port_ser_enable(mb_port_base_t *inst) +{ + mb_port_adapter_enable(inst); +} + +void __wrap_mb_port_ser_disable(mb_port_base_t *inst) +{ + mb_port_adapter_disable(inst); +} + +#endif + +IRAM_ATTR +bool __wrap_mb_port_event_get(mb_port_base_t *inst, mb_event_t *pevent) +{ + bool result = __real_mb_port_event_get(inst, pevent); + ESP_LOGD(TAG, "%s, get event:%x.", inst->descr.parent_name, pevent->event); + return result; +} + +IRAM_ATTR +bool __wrap_mb_port_event_post(mb_port_base_t *inst, mb_event_t event) +{ + bool result = __real_mb_port_event_post(inst, event); + return result; +} + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +// Below are the TCP port function wrappers to exchange the port layer to TCP adapter +mb_err_enum_t __wrap_mbm_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj) +{ + ESP_LOGD(TAG, "master tcp adapter installed."); + return mb_port_adapter_tcp_create(tcp_opts, in_out_obj); +} + +bool __wrap_mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + return mb_port_adapter_recv_data(inst, ppframe, plength); +} + +bool __wrap_mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length) +{ + return mb_port_adapter_send_data(inst, address, pframe, length); +} + +void __wrap_mbm_port_tcp_delete(mb_port_base_t *inst) +{ + mb_port_adapter_delete(inst); +} + +void __wrap_mbm_port_tcp_enable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter master tcp enable port."); +} + +void __wrap_mbm_port_tcp_disable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter master tcp disable port."); +} + +void __wrap_mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg) +{ + ESP_LOGD(TAG, "adapter set connection callback."); + mb_port_adapter_tcp_set_conn_cb(inst, conn_fp, arg); +} + +mb_uid_info_t *__wrap_mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state) +{ + ESP_LOGD(TAG, "adapter get slave #%d info.", slave_addr); + return mb_port_adapter_get_slave_info(inst, slave_addr, exp_state); +} + +// Wrappers for modbus slave tcp + +mb_err_enum_t __wrap_mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj) +{ + ESP_LOGD(TAG, "install slave tcp adapter."); + return mb_port_adapter_tcp_create(tcp_opts, in_out_obj); +} + +bool __wrap_mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength) +{ + return mb_port_adapter_recv_data(inst, ppframe, plength); +} + +bool __wrap_mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length) +{ + return mb_port_adapter_send_data(inst, 0, pframe, length); +} + +void __wrap_mbs_port_tcp_delete(mb_port_base_t *inst) +{ + mb_port_adapter_delete(inst); +} + +void __wrap_mbs_port_tcp_enable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter slave tcp enable port."); +} + +void __wrap_mbs_port_tcp_disable(mb_port_base_t *inst) +{ + ESP_LOGD(TAG, "adapter slave tcp disable port."); +} + +#endif + +#endif // CONFIG_MB_PORT_ADAPTER_EN + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/test_common/mb_utest_lib/port_stubs.h b/test_apps/test_common/mb_utest_lib/port_stubs.h new file mode 100644 index 0000000..d0638df --- /dev/null +++ b/test_apps/test_common/mb_utest_lib/port_stubs.h @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_log.h" +#include "mb_common.h" +#include "mb_port_types.h" +#include "sdkconfig.h" + +// Serial port function wrappers + +bool __wrap_mb_port_event_get(mb_port_base_t *inst, mb_event_t *pevent); +bool __wrap_mb_port_event_post(mb_port_base_t *inst, mb_event_t event); +extern bool __real_mb_port_event_get(mb_port_base_t *inst, mb_event_t *pevent); +extern bool __real_mb_port_event_post(mb_port_base_t *inst, mb_event_t event); + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +extern void __real_mb_port_ser_enable(mb_port_base_t *inst); +extern void __real_mb_port_ser_disable(mb_port_base_t *inst); +extern bool __real_mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *p_ser_frame, uint16_t ser_length); +extern bool __real_mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **pp_ser_frame, uint16_t *p_ser_length); +extern void __real_mb_port_ser_delete(mb_port_base_t *inst); + +mb_err_enum_t __wrap_mb_port_ser_create(mb_serial_opts_t *ser_opts, mb_port_base_t **in_out_obj); +void __wrap_mb_port_ser_enable(mb_port_base_t *inst); +void __wrap_mb_port_ser_disable(mb_port_base_t *inst); +bool __wrap_mb_port_ser_send_data(mb_port_base_t *inst, uint8_t *p_ser_frame, uint16_t ser_length); +bool __wrap_mb_port_ser_recv_data(mb_port_base_t *inst, uint8_t **pp_ser_frame, uint16_t *p_ser_length); +void __wrap_mb_port_ser_delete(mb_port_base_t *inst); + +#endif + +// TCP port function wrappers + +mb_err_enum_t __wrap_mbm_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +void __wrap_mbm_port_tcp_delete(mb_port_base_t *inst); + +bool __wrap_mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length); +bool __wrap_mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +void __wrap_mbm_port_tcp_enable(mb_port_base_t *inst); +void __wrap_mbm_port_tcp_disable(mb_port_base_t *inst); +void __wrap_mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg); +mb_uid_info_t *__wrap_mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t uid, mb_sock_state_t exp_state); +//bool __wrap_mbm_port__expired(void *inst); + +extern void __real_mbm_port_tcp_enable(mb_port_base_t *inst); +extern void __real_mbm_port_tcp_disable(mb_port_base_t *inst); +extern void __real_mbm_port_tcp_set_conn_cb(mb_port_base_t *inst, void *conn_fp, void *arg); +extern mb_uid_info_t *__real_mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t uid, mb_sock_state_t exp_state); +extern bool __real_mbm_port_tcp_send_data(mb_port_base_t *inst, uint8_t address, uint8_t *pframe, uint16_t length); +extern bool __real_mbm_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); + +mb_err_enum_t __wrap_mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +void __wrap_mbs_port_tcp_delete(mb_port_base_t *inst); +bool __wrap_mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length); +bool __wrap_mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +void __wrap_mbs_port_tcp_enable(mb_port_base_t *inst); +void __wrap_mbs_port_tcp_disable(mb_port_base_t *inst); + +mb_err_enum_t __real_mbs_port_tcp_create(mb_tcp_opts_t *tcp_opts, mb_port_base_t **in_out_obj); +extern void __real_mbs_port_tcp_delete(mb_port_base_t *inst); +extern bool __real_mbs_port_tcp_send_data(mb_port_base_t *inst, uint8_t *pframe, uint16_t length); +extern bool __real_mbs_port_tcp_recv_data(mb_port_base_t *inst, uint8_t **ppframe, uint16_t *plength); +extern void __real_mbs_port_tcp_enable(mb_port_base_t *inst); +extern void __real_mbs_port_tcp_disable(mb_port_base_t *inst); \ No newline at end of file diff --git a/test_apps/test_common/sdkconfig.defaults b/test_apps/test_common/sdkconfig.defaults new file mode 100644 index 0000000..241fb10 --- /dev/null +++ b/test_apps/test_common/sdkconfig.defaults @@ -0,0 +1,21 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 + diff --git a/test_apps/test_common/test_common.c b/test_apps/test_common/test_common.c new file mode 100644 index 0000000..23646ff --- /dev/null +++ b/test_apps/test_common/test_common.c @@ -0,0 +1,685 @@ +/* + * SPDX-FileCopyrightText: 2018-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/portmacro.h" +#include "freertos/queue.h" + +#include "port_adapter.h" +#include "mb_common.h" +#include "mbc_slave.h" +#include "mbc_master.h" + +#include "test_common.h" +#include "esp_heap_caps.h" + +#include "sdkconfig.h" + +#ifdef CONFIG_HEAP_TRACING +#include "esp_heap_trace.h" +#endif + +#define TEST_TASK_PRIO_MASTER (CONFIG_MB_TEST_MASTER_TASK_PRIO) +#define TEST_TASK_PRIO_SLAVE (CONFIG_MB_TEST_SLAVE_TASK_PRIO) +#define TEST_TASK_STACK_SIZE (5120) +#define TEST_TASK_CYCLE_COUNTER (CONFIG_MB_TEST_COMM_CYCLE_COUNTER) +#define TEST_BUSY_TASK_PRIO (20) + +#define TEST_REG_START_AREA0 (0x0000) +#define TEST_READ_MASK (MB_EVENT_HOLDING_REG_RD |\ + MB_EVENT_INPUT_REG_RD |\ + MB_EVENT_DISCRETE_RD |\ + MB_EVENT_COILS_RD) +#define TEST_WRITE_MASK (MB_EVENT_HOLDING_REG_WR | MB_EVENT_COILS_WR) +#define TEST_READ_WRITE_MASK (TEST_WRITE_MASK | TEST_READ_MASK) +#define TEST_BUSY_COUNT (150000) +#define TEST_PAR_INFO_GET_TOUT (10) +#define TEST_SEND_TIMEOUT (200 / portTICK_PERIOD_MS) +#define TEST_TASK_START_TIMEOUT (10000 / portTICK_PERIOD_MS) +#define TEST_NOTIF_SEND_TOUT (400 / portTICK_PERIOD_MS) +#define TEST_NOTIF_SIZE (20) +#define TEST_ALLOW_PROC_FAIL (5) // percentage of allowed failures due to desynchronization +#define TEST_TASK_TICK_TIME (50 / portTICK_PERIOD_MS) + +#define TAG "TEST_COMMON" + +typedef enum { + RT_HOLDING_RD, + RT_HOLDING_WR +} mb_access_t; + +static uint16_t holding_registers[16] = {0}; +static uint16_t input_registers[8] = {0}; +static uint16_t coil_registers[10] = {0}; + +const uint16_t holding_registers_counter = (sizeof(holding_registers) / sizeof(holding_registers[0])); +const uint16_t input_registers_counter = (sizeof(input_registers) / sizeof(input_registers[0])); +const uint16_t coil_registers_counter = (sizeof(coil_registers) / sizeof(coil_registers[0])); + +static int test_error_counter = 0; +static int test_good_counter = 0; + +static size_t before_free_8bit = 0; +static size_t before_free_32bit = 0; + +// Heap memory leak traicing +#ifdef CONFIG_HEAP_TRACING +#define NUM_RECORDS 500 +static heap_trace_record_t trace_record[NUM_RECORDS]; +#endif + +#define CHECK_PAR_VALUE(par, err, value, expected) \ + do \ + { \ + if ((err != ESP_OK) || (((uint16_t)value) != ((uint16_t)expected))) \ + { \ + ESP_LOGE(TAG, "CHAR #%u, value: 0x%" PRIx16 ", expected: 0x%" PRIx16 ", error = %d.", \ + (unsigned)par, ((uint16_t)value), ((uint16_t)expected), (int)err); \ + TEST_ASSERT((++test_error_counter * 100 / (TEST_TASK_CYCLE_COUNTER * CID_DEV_REG_COUNT)) <= TEST_ALLOW_PROC_FAIL); \ + } \ + else \ + { \ + ESP_LOGI(TAG, "CHAR #%u, value is ok.", (unsigned)par); \ + test_good_counter++; \ + } \ + } while (0) + +// The linked list of test tasks instances +LIST_HEAD(task_entry, task_entry_s) s_task_list; + +static portMUX_TYPE s_list_spinlock = portMUX_INITIALIZER_UNLOCKED; + +static void task_entry_remove(task_entry_t *task_entry) +{ + portENTER_CRITICAL(&s_list_spinlock); + LIST_REMOVE(task_entry, entries); + portEXIT_CRITICAL(&s_list_spinlock); + ESP_LOGD(TAG, "Delete task 0x%" PRIx32, (uint32_t)task_entry->task_handle); + vTaskDelete(task_entry->task_handle); + vSemaphoreDelete(task_entry->task_sema_handle); + free(task_entry); +} + +static bool task_wait_done_and_remove(task_entry_t *task_entry, TickType_t tout_ticks) +{ + bool is_done = false; + if (task_entry && task_entry->task_handle && task_entry->task_sema_handle) { + if ((xSemaphoreTake(task_entry->task_sema_handle, tout_ticks) == pdTRUE)) { + ESP_LOGI(TAG, "Test task 0x%" PRIx32 ", done successfully.", (uint32_t)task_entry->task_handle); + is_done = true; + } else { + ESP_LOGE(TAG, "Could not complete task 0x%" PRIx32 " after timeout, force kill the task.", + (uint32_t)task_entry->task_handle); + is_done = false; + } + vTaskDelay(1); // Let the lower priority task to suspend or delete itself + task_entry_remove(task_entry); + } + return (is_done); +} + +static void test_task_add_entry(TaskHandle_t task_handle, void *pinst) +{ + TEST_ASSERT_TRUE(task_handle); + task_entry_t *new_entry = (task_entry_t*) calloc(1, sizeof(task_entry_t)); + TEST_ASSERT_TRUE(new_entry); + portENTER_CRITICAL(&s_list_spinlock); + new_entry->task_handle = task_handle; + new_entry->task_sema_handle = xSemaphoreCreateBinary(); + new_entry->inst_handle = pinst; + LIST_INSERT_HEAD(&s_task_list, new_entry, entries); + portEXIT_CRITICAL(&s_list_spinlock); + xSemaphoreTake(new_entry->task_sema_handle, 1); +} + +static task_entry_t *test_task_find_entry(TaskHandle_t task_handle) +{ + TEST_ASSERT_NOT_NULL(task_handle); + + task_entry_t *it, *pfound = NULL; + if (LIST_EMPTY(&s_task_list)) { + return NULL; + } + + portENTER_CRITICAL(&s_list_spinlock); + LIST_FOREACH(it, &s_task_list, entries) { + if (it->task_handle == task_handle) { + pfound = it; + break; + } + } + portEXIT_CRITICAL(&s_list_spinlock); + return pfound; +} + +static void test_common_task_notify_done(TaskHandle_t task_handle) +{ + task_entry_t *it = test_task_find_entry(task_handle); + if (it) { + xSemaphoreGive(it->task_sema_handle); + } +} + +static void test_busy_task(void *phandle) +{ + spinlock_t spin_lock; + SPIN_LOCK_INIT(spin_lock); + ESP_EARLY_LOGW(TAG, "test task"); + while(1) { + SPIN_LOCK_ENTER(spin_lock); + for (int i = 0; i < TEST_BUSY_COUNT; i++){ + ; + } + SPIN_LOCK_EXIT(spin_lock); + vTaskDelay(1); + } +} + +void test_common_task_start(TaskHandle_t task_handle, uint32_t value) +{ + // Directly notify the task waiting to start loop + test_common_task_notify_start(task_handle, value); +} + +int test_common_task_start_all(uint32_t value) +{ + task_entry_t *it = NULL; + if (LIST_EMPTY(&s_task_list)) { + return 0; + } + int task_count = 0; + LIST_FOREACH(it, &s_task_list, entries) { + test_common_task_notify_start(it->task_handle, value); + task_count++; + } + return task_count; +} + +bool test_common_task_wait_done(TaskHandle_t task_handle, TickType_t timeout_ticks) +{ + task_entry_t *it = test_task_find_entry(task_handle); + if (it && (xSemaphoreTake(it->task_sema_handle, timeout_ticks) == pdTRUE)) { + return true; + } + return false; +} + +bool test_common_task_wait_done_delete(TaskHandle_t task_handle, TickType_t task_timeout_ticks) +{ + task_entry_t *it = test_task_find_entry(task_handle); + return task_wait_done_and_remove(it, task_timeout_ticks); +} + +int test_common_task_wait_done_delete_all(TickType_t task_timeout_tick) +{ + task_entry_t *it, *ptmp = NULL; + int task_count = 0; + if (LIST_EMPTY(&s_task_list)) { + return 0; + } + LIST_FOREACH_SAFE(it, &s_task_list, entries, ptmp) { + task_wait_done_and_remove(it, task_timeout_tick); + task_count++; + } + return task_count; +} + +void test_common_task_delete(TaskHandle_t task_handle) +{ + task_entry_t *it = test_task_find_entry(task_handle); + if (it) { + task_entry_remove(it); + } +} + +void test_common_task_delete_all() +{ + task_entry_t *it = NULL; + while ((it = LIST_FIRST(&s_task_list))) { + task_entry_remove(it); + } +} + +void *test_common_task_get_instance(TaskHandle_t task_handle) +{ + task_entry_t *it = test_task_find_entry(task_handle); + if (it) { + return it->inst_handle; + } + return NULL; +} + +// Start the high priority task to mimic the case when the modbus +// tasks do not get time quota from RTOS. +TaskHandle_t test_common_start_busy_task(uint32_t priority) +{ + TaskHandle_t busy_task_handle = NULL; + if (!priority) { + priority = TEST_BUSY_TASK_PRIO; + } + + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_busy_task, "busy_task", + TEST_TASK_STACK_SIZE, + NULL, priority, + &busy_task_handle, MB_PORT_TASK_AFFINITY)); + test_task_add_entry(busy_task_handle, NULL); + return busy_task_handle; +} + +void test_common_start() +{ +#ifdef CONFIG_HEAP_TRACING + ESP_ERROR_CHECK( heap_trace_init_standalone(trace_record, NUM_RECORDS) ); +#endif + + before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + +#ifdef CONFIG_HEAP_TRACING + ESP_ERROR_CHECK( heap_trace_start(HEAP_TRACE_LEAKS) ); +#endif + LIST_INIT(&s_task_list); +} + +void test_common_stop() +{ + //vQueueDelete(tasks_done_queue); + holding_registers[CID_DEV_REG_COUNT] = 0; + test_error_counter = 0; + test_good_counter = 0; + + /* check if unit test has caused heap corruption in any heap */ + TEST_ASSERT_MESSAGE(heap_caps_check_integrity(MALLOC_CAP_INVALID, true), "The test has corrupted the heap"); + +#ifdef CONFIG_HEAP_TRACING + ESP_ERROR_CHECK( heap_trace_stop() ); + heap_trace_dump(); +#endif + size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + test_common_check_leak(before_free_8bit, after_free_8bit, "8BIT", + CONFIG_MB_TEST_LEAK_WARN_LEVEL, CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL); + test_common_check_leak(before_free_32bit, after_free_32bit, "32BIT", + CONFIG_MB_TEST_LEAK_WARN_LEVEL, CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL); +} + +static uint32_t test_common_task_wait_start(TickType_t timeout_ticks) +{ + static uint32_t notify_value = 0; + + if (xTaskNotifyWait(0, 0, ¬ify_value, timeout_ticks) == pdTRUE) { + ESP_LOGD(TAG, "Task: 0x%" PRIx32 ", get notify value = %u", + (uint32_t)xTaskGetCurrentTaskHandle(), (unsigned)notify_value); + return pdTRUE; + } + return 0; +} + +void test_common_task_notify_start(TaskHandle_t task_handle, uint32_t value) +{ + ESP_LOGD(TAG, "Notify task start 0x%" PRIx32, (uint32_t)task_handle); + TEST_ASSERT_EQUAL_INT(xTaskNotify(task_handle, value, eSetValueWithOverwrite), pdTRUE); +} + +void test_common_check_leak(size_t before_free, + size_t after_free, + const char *type, + size_t warn_threshold, + size_t critical_threshold) +{ + int free_delta = (int)after_free - (int)before_free; + printf("MALLOC_CAP_%s usage: Free memory delta: %d Leak threshold: -%u \n", + type, + free_delta, + critical_threshold); + + if (free_delta > 0) { + return; // free memory went up somehow + } + + size_t leaked = (size_t)(free_delta * -1); + if (leaked <= warn_threshold) { + return; + } + + printf("MALLOC_CAP_%s %s leak: Before %u bytes free, After %u bytes free (delta %u)\n", + type, + leaked <= critical_threshold ? "potential" : "critical", + before_free, after_free, leaked); + fflush(stdout); + TEST_ASSERT_MESSAGE(leaked <= critical_threshold, "The test leaked too much memory"); +} + +// Helper function to read one characteristic from slave +esp_err_t test_common_read_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data) +{ + const mb_parameter_descriptor_t *param_descriptor = NULL; + + esp_err_t err = mbc_master_get_cid_info(handle, cid, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) + { + uint8_t type = 0; + err = mbc_master_get_parameter(handle, cid, (uint8_t *)par_data, &type); + if (err == ESP_OK) + { + ESP_LOGI(TAG, "%p, CHAR #%u %s (%s) value = (0x%04x) parameter read successful.", + handle, + param_descriptor->cid, + param_descriptor->param_key, + param_descriptor->param_units, + *(uint16_t *)par_data); + } + else + { + ESP_LOGE(TAG, "%p, CHAR #%u (%s) read fail, err = 0x%x (%s).", + handle, + param_descriptor->cid, + param_descriptor->param_key, + (int)err, + (char *)esp_err_to_name(err)); + } + } + return err; +} + +// Helper function to write one characteristic to slave +esp_err_t write_modbus_parameter(void *handle, uint16_t cid, uint16_t *par_data) +{ + const mb_parameter_descriptor_t *param_descriptor = NULL; + + esp_err_t err = mbc_master_get_cid_info(handle, cid, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) + { + uint8_t type = 0; // type of parameter from dictionary + err = mbc_master_set_parameter(handle, cid, (uint8_t *)par_data, &type); + if (err == ESP_OK) + { + ESP_LOGI(TAG, "%p, CHAR #%u %s (%s) value = (0x%04x), write successful.", + handle, + param_descriptor->cid, + param_descriptor->param_key, + param_descriptor->param_units, + *(uint16_t *)par_data); + } + else + { + ESP_LOGE(TAG, "%p, CHAR #%u (%s) write fail, err = 0x%x (%s).", + handle, + param_descriptor->cid, + param_descriptor->param_key, + (int)err, + (char *)esp_err_to_name(err)); + } + } + return err; +} + +// This is user function to read and write modbus holding registers +static void test_master_task(void *arg) +{ + void *mbm_handle = arg; + //mbm_controller_iface_t *pctrl_obj = ((mbm_controller_iface_t *)mbm_handle); + + static mb_access_t req_type = RT_HOLDING_RD; + esp_err_t err = ESP_FAIL; + uint16_t cycle_counter = 0; + + // Wait task start notification during timeout + test_common_task_wait_start(TEST_TASK_START_TIMEOUT); + + holding_registers[CID_DEV_REG0] = TEST_REG_VAL1; + holding_registers[CID_DEV_REG1] = TEST_REG_VAL2; + holding_registers[CID_DEV_REG2] = TEST_REG_VAL3; + holding_registers[CID_DEV_REG3] = TEST_REG_VAL4; + holding_registers[CID_DEV_REG_COUNT] = cycle_counter; + write_modbus_parameter(mbm_handle, CID_DEV_REG0, &holding_registers[CID_DEV_REG0]); + write_modbus_parameter(mbm_handle, CID_DEV_REG1, &holding_registers[CID_DEV_REG1]); + write_modbus_parameter(mbm_handle, CID_DEV_REG2, &holding_registers[CID_DEV_REG2]); + write_modbus_parameter(mbm_handle, CID_DEV_REG3, &holding_registers[CID_DEV_REG3]); + for (cycle_counter = 0; cycle_counter <= TEST_TASK_CYCLE_COUNTER; cycle_counter++) + { + switch (req_type) + { + case RT_HOLDING_RD: + err = test_common_read_modbus_parameter(mbm_handle, CID_DEV_REG0, &holding_registers[CID_DEV_REG0]); + CHECK_PAR_VALUE(CID_DEV_REG0, err, holding_registers[CID_DEV_REG0], TEST_REG_VAL1); + + err = test_common_read_modbus_parameter(mbm_handle, CID_DEV_REG1, &holding_registers[CID_DEV_REG1]); + CHECK_PAR_VALUE(CID_DEV_REG1, err, holding_registers[CID_DEV_REG1], TEST_REG_VAL2); + + err = test_common_read_modbus_parameter(mbm_handle, CID_DEV_REG2, &holding_registers[CID_DEV_REG2]); + CHECK_PAR_VALUE(CID_DEV_REG2, err, holding_registers[CID_DEV_REG2], TEST_REG_VAL3); + + err = test_common_read_modbus_parameter(mbm_handle, CID_DEV_REG3, &holding_registers[CID_DEV_REG3]); + CHECK_PAR_VALUE(CID_DEV_REG3, err, holding_registers[CID_DEV_REG3], TEST_REG_VAL4); + req_type = RT_HOLDING_WR; + break; + + case RT_HOLDING_WR: + err = write_modbus_parameter(mbm_handle, CID_DEV_REG0, &holding_registers[CID_DEV_REG0]); + CHECK_PAR_VALUE(CID_DEV_REG0, err, holding_registers[CID_DEV_REG0], TEST_REG_VAL1); + + err = write_modbus_parameter(mbm_handle, CID_DEV_REG1, &holding_registers[CID_DEV_REG1]); + CHECK_PAR_VALUE(CID_DEV_REG1, err, holding_registers[CID_DEV_REG1], TEST_REG_VAL2); + + err = write_modbus_parameter(mbm_handle, CID_DEV_REG2, &holding_registers[CID_DEV_REG2]); + CHECK_PAR_VALUE(CID_DEV_REG2, err, holding_registers[CID_DEV_REG2], TEST_REG_VAL3); + + err = write_modbus_parameter(mbm_handle, CID_DEV_REG3, &holding_registers[CID_DEV_REG3]); + CHECK_PAR_VALUE(CID_DEV_REG3, err, holding_registers[CID_DEV_REG3], TEST_REG_VAL4); + req_type = RT_HOLDING_RD; + break; + + default: + break; + } + if (holding_registers[CID_DEV_REG_COUNT] >= TEST_TASK_CYCLE_COUNTER) { + ESP_LOGI(TAG, "Stop master: %p.", mbm_handle); + break; + } else { + write_modbus_parameter(mbm_handle, CID_DEV_REG_COUNT, &cycle_counter); + vTaskDelay(TEST_TASK_TICK_TIME); // Let the IDLE task to trigger + } + } + ESP_LOGI(TAG, "Destroy master, inst: %p.", mbm_handle); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + test_common_task_notify_done(xTaskGetCurrentTaskHandle()); + vTaskSuspend(NULL); +} + +static void test_slave_task(void *arg) +{ + void *mbs_handle = arg; + mbs_controller_iface_t *pctrl_obj = ((mbs_controller_iface_t *)mbs_handle); + mb_param_info_t reg_info; // keeps the Modbus registers access information + + test_common_task_wait_start(TEST_TASK_START_TIMEOUT); + + while(1) { + // Get parameter information from parameter queue + esp_err_t err = mbc_slave_get_param_info(mbs_handle, ®_info, TEST_PAR_INFO_GET_TOUT); + const char *rw_str = (reg_info.type & TEST_READ_MASK) ? "READ" : "WRITE"; + + // Filter events and process them accordingly + if ((err != ESP_ERR_TIMEOUT) && (reg_info.type & TEST_READ_WRITE_MASK)) + { + // Get parameter information from parameter queue + ESP_LOGI("SLAVE", "OBJ %p, %s (%" PRIu32 " us), SL: %u, REG:%u, TYPE:%u, INST_ADDR:0x%" PRIx32 "(0x%" PRIx16 "), SIZE:%u", + (void *)pctrl_obj->mb_base->descr.parent, + rw_str, + (uint32_t)reg_info.time_stamp, + (unsigned)pctrl_obj->opts.comm_opts.common_opts.uid, + (unsigned)reg_info.mb_offset, + (unsigned)reg_info.type, + (uint32_t)reg_info.address, + *(uint16_t *)reg_info.address, + (unsigned)reg_info.size); + } + vTaskDelay(TEST_TASK_TICK_TIME); // Let IDLE task to trigger + if (holding_registers[CID_DEV_REG_COUNT] >= TEST_TASK_CYCLE_COUNTER) { + ESP_LOGD(TAG, "Stop slave: %p.", mbs_handle); + vTaskDelay(TEST_TASK_TICK_TIME); // Let master to get response from slave prior to close + break; + } + } + ESP_LOGI(TAG, "Destroy slave, inst: %p.", mbs_handle); + TEST_ESP_OK(mbc_slave_delete(mbs_handle)); + ESP_LOGD(TAG, "Notify task done, inst: %p.", xTaskGetCurrentTaskHandle()); + test_common_task_notify_done(xTaskGetCurrentTaskHandle()); + vTaskDelay(10); + vTaskSuspend(NULL); +} + +void test_common_slave_setup_start(void *mbs_handle) +{ + TEST_ASSERT_TRUE(mbs_handle); + mb_register_area_descriptor_t reg_area; + + reg_area.type = MB_PARAM_HOLDING; + reg_area.start_offset = TEST_REG_START_AREA0; + reg_area.address = (void *)&holding_registers[CID_DEV_REG0]; + reg_area.size = holding_registers_counter << 1; + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = TEST_REG_START_AREA0; + reg_area.address = (void *)&input_registers[CID_DEV_REG0]; + reg_area.size = input_registers_counter << 1; + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + + reg_area.type = MB_PARAM_COIL; + reg_area.start_offset = TEST_REG_START_AREA0; + reg_area.address = (void *)&coil_registers[CID_DEV_REG0]; + reg_area.size = coil_registers_counter; + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + TEST_ESP_OK(mbc_slave_start(mbs_handle)); +} + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +TaskHandle_t test_common_master_serial_create(mb_communication_info_t *pconfig, + uint32_t priority, + const mb_parameter_descriptor_t *pdescr, + uint16_t descr_size) +{ + if (!pconfig || !pdescr) { + ESP_LOGI(TAG, "invalid master configuration."); + } + + void *mbm_handle = NULL; + TaskHandle_t master_task_handle = NULL; + + TEST_ESP_OK(mbc_master_create_serial(pconfig, &mbm_handle)); + mbm_controller_iface_t *pbase = mbm_handle; + + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, pdescr, descr_size)); + ESP_LOGI(TAG, "%p, modbus master stack is initialized", mbm_handle); + + TEST_ESP_OK(mbc_master_start(mbm_handle)); + ESP_LOGI(TAG, "%p, modbus master start...", mbm_handle) ; + + if (priority) { + priority = TEST_TASK_PRIO_MASTER; + } + + char* port_name = pbase->mb_base->descr.parent_name; + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_master_task, port_name, + TEST_TASK_STACK_SIZE, + mbm_handle, priority, + &master_task_handle, MB_PORT_TASK_AFFINITY)); + test_task_add_entry(master_task_handle, mbm_handle); + return master_task_handle; +} + +TaskHandle_t test_common_slave_serial_create(mb_communication_info_t *pconfig, uint32_t priority) +{ + if (!pconfig) { + ESP_LOGI(TAG, "invalid slave configuration."); + } + + void *mbs_handle = NULL; + TaskHandle_t slave_task_handle = NULL; + + TEST_ESP_OK(mbc_slave_create_serial(pconfig, &mbs_handle)); + + mbs_controller_iface_t *pbase = mbs_handle; + + test_common_slave_setup_start(mbs_handle); + + if (priority) { + priority = TEST_TASK_PRIO_SLAVE; + } + + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_slave_task, pbase->mb_base->descr.parent_name, + TEST_TASK_STACK_SIZE, + mbs_handle, priority, + &slave_task_handle, MB_PORT_TASK_AFFINITY)); + test_task_add_entry(slave_task_handle, mbs_handle); + return slave_task_handle; +} + +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +TaskHandle_t test_common_master_tcp_create(mb_communication_info_t *pconfig, uint32_t priority, const mb_parameter_descriptor_t *pdescr, uint16_t descr_size) +{ + if (!pconfig || !pdescr) { + ESP_LOGI(TAG, "invalid master configuration."); + } + + void *mbm_handle = NULL; + TaskHandle_t master_task_handle = NULL; + + TEST_ESP_OK(mbc_master_create_tcp(pconfig, &mbm_handle)); + mbm_controller_iface_t *pbase = mbm_handle; + + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, pdescr, descr_size)); + ESP_LOGI(TAG, "%p, modbus master stack is initialized", mbm_handle); + + TEST_ESP_OK(mbc_master_start(mbm_handle)); + ESP_LOGI(TAG, "%p, modbus master start...", mbm_handle) ; + + if (priority) { + priority = TEST_TASK_PRIO_MASTER; + } + + char *port_name = pbase->mb_base->descr.parent_name; + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_master_task, port_name, + TEST_TASK_STACK_SIZE, + mbm_handle, priority, + &master_task_handle, MB_PORT_TASK_AFFINITY)); + + test_task_add_entry(master_task_handle, mbm_handle); + return master_task_handle; +} + +TaskHandle_t test_common_slave_tcp_create(mb_communication_info_t *pconfig, uint32_t priority) +{ + if (!pconfig) { + ESP_LOGI(TAG, "invalid slave configuration."); + } + + void *mbs_handle = NULL; + TaskHandle_t slave_task_handle = NULL; + + TEST_ESP_OK(mbc_slave_create_tcp(pconfig, &mbs_handle)); + + mbs_controller_iface_t *pbase = mbs_handle; + test_common_slave_setup_start(mbs_handle); + + if (priority) { + priority = TEST_TASK_PRIO_SLAVE; + } + + TEST_ASSERT_TRUE(xTaskCreatePinnedToCore(test_slave_task, pbase->mb_base->descr.parent_name, + TEST_TASK_STACK_SIZE, + mbs_handle, priority, + &slave_task_handle, MB_PORT_TASK_AFFINITY)); + test_task_add_entry(slave_task_handle, mbs_handle); + return slave_task_handle; +} + +#endif diff --git a/test_apps/unit_tests/mb_controller_common/CMakeLists.txt b/test_apps/unit_tests/mb_controller_common/CMakeLists.txt new file mode 100644 index 0000000..79c2793 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/CMakeLists.txt @@ -0,0 +1,9 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../../test_common") +list(APPEND EXTRA_COMPONENT_DIRS "../test_stubs") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_mb_controller_common_unit) diff --git a/test_apps/unit_tests/mb_controller_common/README.md b/test_apps/unit_tests/mb_controller_common/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/README.md @@ -0,0 +1,4 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | + +This test app is used to test modbus interface. diff --git a/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/CMakeLists.txt b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/CMakeLists.txt new file mode 100644 index 0000000..aa13055 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/CMakeLists.txt @@ -0,0 +1,9 @@ +# NOTE: This kind of mocking currently works on Linux targets only. +# On Espressif chips, too many dependencies are missing at the moment. + +idf_component_mock(INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} + REQUIRES test_common cmock + MOCK_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/test_mbm_object.h) + + + diff --git a/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/mock/mock_config.yaml b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/mock/mock_config.yaml new file mode 100644 index 0000000..596255b --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/mock/mock_config.yaml @@ -0,0 +1,9 @@ + :cmock: + :plugins: + - expect + - expect_any_args + - return_thru_ptr + - array + - ignore + - ignore_arg + - callback diff --git a/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/test_mbm_object.h b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/test_mbm_object.h new file mode 100644 index 0000000..43d40b5 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/components/mocked_esp_modbus/test_mbm_object.h @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include "mb_config.h" +#include "mb_common.h" +#include "mb_port_types.h" + +#include "sdkconfig.h" + +/* Common definitions */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mb_base_t mb_base_t; /*!< Type of modbus object */ +typedef struct mb_port_base_t mb_port_base_t; + +bool mb_port_event_get(mb_port_base_t *inst, mb_event_t *pevent); +bool mb_port_event_post(mb_port_base_t *inst, mb_event_t event); +bool mb_port_event_res_take(mb_port_base_t *inst, uint32_t timeout); +void mb_port_event_res_release(mb_port_base_t *inst); +mb_err_enum_t mb_port_event_wait_req_finish(mb_port_base_t *inst); + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mbs_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbs_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +mb_err_enum_t mbm_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbm_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +mb_err_enum_t mbm_rq_read_inp_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_write_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_data, uint32_t tout); +mb_err_enum_t mbm_rq_write_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_wr_addr, uint16_t *data_ptr, uint32_t tout); +mb_err_enum_t mbm_rq_read_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t reg_addr, uint16_t reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_rw_multi_holding_reg(mb_base_t *inst, uint8_t snd_addr, uint16_t rd_reg_addr, + uint16_t rd_reg_num, uint16_t *data_ptr, uint16_t wr_reg_addr, uint16_t wr_reg_num, uint32_t tout); +mb_err_enum_t mbm_rq_read_discrete_inputs(mb_base_t *inst, uint8_t snd_addr, uint16_t discrete_addr, uint16_t discrete_num, uint32_t tout); +mb_err_enum_t mbm_rq_read_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint32_t tout); +mb_err_enum_t mbm_rq_write_coil(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_data, uint32_t tout); +mb_err_enum_t mbm_rq_write_multi_coils(mb_base_t *inst, uint8_t snd_addr, uint16_t coil_addr, uint16_t coil_num, uint8_t *data_ptr, uint32_t tout); + +#if MB_FUNC_OTHER_REP_SLAVEID_BUF +mb_exception_t mb_fn_report_slv_id(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_INPUT_ENABLED +mb_exception_t mbs_fn_read_input_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_HOLDING_ENABLED +mb_exception_t mbs_fn_read_holding_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_HOLDING_ENABLED +mb_exception_t mbs_fn_write_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED +mb_exception_t mbs_fn_write_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_COILS_ENABLED +mb_exception_t mbs_fn_read_coils(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_COIL_ENABLED +mb_exception_t mbs_fn_write_coil(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED +mb_exception_t mbs_fn_write_multi_coils(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED +mb_exception_t mbs_fn_read_discrete_inp(mb_base_t *inst, uint8_t *frame_ptr, uint16_t *len_buf); +#endif + +#if MB_FUNC_READWRITE_HOLDING_ENABLED +mb_exception_t mbs_fn_rw_multi_holding_reg(mb_base_t *inst, uint8_t *frame_ptr,uint16_t *len_buf); +#endif + +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +typedef struct _port_tcp_opts mb_tcp_opts_t; + +mb_err_enum_t mbs_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +mb_err_enum_t mbs_delete(mb_base_t *inst); +mb_err_enum_t mbs_enable(mb_base_t *inst); +mb_err_enum_t mbs_disable(mb_base_t *inst); +mb_err_enum_t mbs_poll(mb_base_t *inst); +mb_err_enum_t mbs_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len); + +mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); +mb_uid_info_t *mbm_port_tcp_get_slave_info(mb_port_base_t *inst, uint8_t slave_addr, mb_sock_state_t exp_state); + +#endif + +mb_err_enum_t mbm_delete(mb_base_t *inst); +mb_err_enum_t mbm_enable(mb_base_t *inst); +mb_err_enum_t mbm_disable(mb_base_t *inst); +mb_err_enum_t mbm_poll(mb_base_t *inst); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/unit_tests/mb_controller_common/main/CMakeLists.txt b/test_apps/unit_tests/mb_controller_common/main/CMakeLists.txt new file mode 100644 index 0000000..d1bb8b3 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/CMakeLists.txt @@ -0,0 +1,11 @@ +set(srcs "test_app_main.c" + "test_mb_controller_common.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES test_stubs mocked_esp_modbus test_common cmock test_utils unity) + +# The workaround for WHOLE_ARCHIVE is absent in v4.4 +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_impl") +#target_compile_options(${COMPONENT_LIB} PUBLIC -fsanitize=address = -lasan) diff --git a/test_apps/unit_tests/mb_controller_common/main/Kconfig.projbuild b/test_apps/unit_tests/mb_controller_common/main/Kconfig.projbuild new file mode 100644 index 0000000..ce29a07 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/Kconfig.projbuild @@ -0,0 +1,45 @@ +menu "Modbus Test Configuration" + + config MB_PORT_ADAPTER_EN + bool "Enable Modbus port adapter to substitute hardware layer for test." + default y + help + When option is enabled the port communication layer is substituted by + port adapter layer to allow testing of higher layers without access to physical layer. + + config MB_TEST_SLAVE_TASK_PRIO + int "Modbus master test task priority" + range 4 23 + default 4 + help + Modbus master task priority for the test. + + config MB_TEST_MASTER_TASK_PRIO + int "Modbus slave test task priority" + range 4 23 + default 4 + help + Modbus slave task priority for the test. + + config MB_TEST_COMM_CYCLE_COUNTER + int "Modbus communication cycle counter for test" + range 10 1000 + default 10 + help + Modbus communication cycle counter for test. + + config MB_TEST_LEAK_WARN_LEVEL + int "Modbus test leak warning level" + range 4 256 + default 32 + help + Modbus test leak warning level. + + config MB_TEST_LEAK_CRITICAL_LEVEL + int "Modbus test leak critical level" + range 4 1024 + default 64 + help + Modbus test leak critical level. + +endmenu diff --git a/test_apps/unit_tests/mb_controller_common/main/component.mk b/test_apps/unit_tests/mb_controller_common/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/unit_tests/mb_controller_common/main/idf_component.yml b/test_apps/unit_tests/mb_controller_common/main/idf_component.yml new file mode 100644 index 0000000..e6964ae --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../../" + diff --git a/test_apps/unit_tests/mb_controller_common/main/test_app_main.c b/test_apps/unit_tests/mb_controller_common/main/test_app_main.c new file mode 100644 index 0000000..f1b9310 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/test_app_main.c @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_fixture.h" + +static void run_all_tests(void) +{ + RUN_TEST_GROUP(unit_test_controller); +} + +void app_main(void) +{ + UNITY_MAIN_FUNC(run_all_tests); +} diff --git a/test_apps/unit_tests/mb_controller_common/main/test_mb_controller_common.c b/test_apps/unit_tests/mb_controller_common/main/test_mb_controller_common.c new file mode 100644 index 0000000..1da8aaa --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/main/test_mb_controller_common.c @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity_fixture.h" + +#include "sdkconfig.h" +#include "test_common.h" +#include "mbc_master.h" +#include "mbc_slave.h" + +#include "Mocktest_mbm_object.h" +#include "mb_object_stub.h" + +#define TEST_SER_PORT_NUM 1 +#define TEST_TCP_PORT_NUM 1502 +#define TEST_TASKS_NUM 3 +#define TEST_TASK_TIMEOUT_MS 30000 +#define TEST_ALLOWED_LEAK 32 +#define TEST_SLAVE_SEND_TOUT_US 30000 +#define TEST_MASTER_SEND_TOUT_US 30000 + +#define TEST_MASTER_RESPOND_TOUT_MS CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND + +#define TAG "MODBUS_CONTROLLER_COMMON_TEST" + +// The workaround to statically link whole test library +__attribute__((unused)) bool mb_test_include_impl = true; + +enum { + CID_DEV_REG0_INPUT, + CID_DEV_REG0_HOLD, + CID_DEV_REG1_INPUT, + CID_DEV_REG0_COIL, + CID_DEV_REG0_DISCRITE, + CID_DEV_REG_CNT, + CID_ITEMS_CNT +}; + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0_INPUT, STR("MB_input_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 0, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_REG0_HOLD, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1_INPUT, STR("MB_input_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_COIL, STR("MB_coil_reg-0"), STR("bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 3, 8, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_DISCRITE, STR("MB_discr_reg-0"), STR("bit"), MB_DEVICE_ADDR1, MB_PARAM_DISCRETE, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG_CNT, STR("CYCLE_COUNTER"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 4, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, +}; + +// Calculate number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +TEST_GROUP(unit_test_controller); + +TEST_SETUP(unit_test_controller) +{ + test_common_start(); +} + +TEST_TEAR_DOWN(unit_test_controller) +{ + test_common_stop(); +} + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +TEST(unit_test_controller, test_setup_destroy_master_tcp) +{ + mb_communication_info_t master_config = { + .tcp_opts.port = TEST_TCP_PORT_NUM, + .tcp_opts.mode = MB_TCP, + .tcp_opts.addr_type = MB_IPV4, + .tcp_opts.ip_addr_table = (void *)(0x44332211), + .tcp_opts.uid = MB_DEVICE_ADDR1, + .tcp_opts.start_disconnected = true, + .tcp_opts.response_tout_ms = 1, + .tcp_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US, + .tcp_opts.ip_netif_ptr = (void *)(0x11223344) + }; + + ESP_LOGI(TAG, "TEST: Verify master create-destroy sequence TCP."); + + void *mbm_handle = NULL; + mb_base_t *pmb_base = NULL; + TEST_ESP_ERR(MB_ENOERR, mb_stub_tcp_create(&master_config.tcp_opts, (void *)&pmb_base)); + + mbm_tcp_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_tcp_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + mbm_port_tcp_get_slave_info_IgnoreAndReturn((void *)(0x11223344)); + TEST_ESP_OK(mbc_master_create_tcp(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + ESP_LOGI(TAG, "Test passed successfully."); +} + +#endif + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + +TEST(unit_test_controller, test_setup_destroy_master_serial) +{ + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + + ESP_LOGI(TAG, "TEST: Verify master create-destroy sequence."); + + void *mbm_handle = NULL; + mb_base_t *pmb_base = NULL; + TEST_ESP_ERR(MB_ENOERR, mb_stub_serial_create(&master_config.ser_opts, (void *)&pmb_base)); + + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + + master_config.ser_opts.mode = MB_ASCII; + mbm_handle = NULL; + pmb_base = NULL; + + mbm_ascii_create_ExpectAnyArgsAndReturn(MB_EINVAL); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_master_delete(mbm_handle)); + ESP_LOGI(TAG, "Test passed successfully."); +} + +TEST(unit_test_controller, test_setup_destroy_slave_serial) +{ + // Initialize and start Modbus controller + mb_communication_info_t slave_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS, + .ser_opts.test_tout_us = TEST_MASTER_SEND_TOUT_US + }; + + ESP_LOGI(TAG, "TEST: Verify slave create-destroy sequence."); + void *mbs_handle = NULL; + mb_base_t *pmb_base = mbs_handle; + TEST_ESP_ERR(MB_ENOERR, mb_stub_serial_create(&slave_config.ser_opts, (void *)&pmb_base)); + mbs_rtu_create_ExpectAndReturn(&slave_config.ser_opts, (void *)pmb_base, MB_ENOERR); + mbs_rtu_create_IgnoreArg_in_out_obj(); + mbs_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_slave_create_serial(&slave_config, &mbs_handle)); + TEST_ESP_OK(mbc_slave_delete(mbs_handle)); + + slave_config.ser_opts.mode = MB_ASCII; + mbs_handle = NULL; + mbs_ascii_create_ExpectAnyArgsAndReturn(MB_EILLSTATE); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_slave_create_serial(&slave_config, &mbs_handle)); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mbc_slave_delete(mbs_handle)); + ESP_LOGI(TAG, "Test passed successfully."); +} + +esp_err_t test_master_registers(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US + }; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_serial_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mb_port_event_res_take_ExpectAnyArgsAndReturn(true); + mb_port_event_res_release_ExpectAnyArgs(); + TEST_ESP_OK(mbc_master_start(mbm_handle)); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + esp_err_t err = mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) + { + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t type = 0; // type of parameter from dictionary + uint8_t *pdata = (uint8_t *)calloc(1, param_descriptor->mb_size + 1); + ESP_LOGI(TAG, "Test CID #%d, %s, %s", param_descriptor->cid, param_descriptor->param_key, param_descriptor->param_units); + // This is to check the request function is called with appropriate params. + switch(param_descriptor->mb_param_type) { \ + case MB_PARAM_INPUT: \ + mbm_rq_read_inp_reg_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_inp_reg_IgnoreArg_tout(); \ + break; \ + case MB_PARAM_HOLDING: + mbm_rq_read_holding_reg_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_holding_reg_IgnoreArg_tout(); \ + break; \ + case MB_PARAM_COIL: \ + mbm_rq_read_coils_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_coils_IgnoreArg_tout(); \ + break; \ + case MB_PARAM_DISCRETE: \ + mbm_rq_read_discrete_inputs_ExpectAndReturn(pmb_base, \ + param_descriptor->mb_slave_addr, \ + param_descriptor->mb_reg_start, \ + param_descriptor->mb_size, \ + 1, \ + mb_err); \ + mbm_rq_read_discrete_inputs_IgnoreArg_tout(); \ + break; \ + default: + TEST_FAIL(); \ + break; \ + } + err = mbc_master_get_parameter(mbm_handle, par_index, pdata, &type); \ + free(pdata); + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +// Check if modbus controller object forms correct modbus request from data dictionary +// and is able to transfer data using mb_object. Check possible errors returned back from +// mb_object and make sure the modbus controller handles them correctly. +TEST(unit_test_controller, test_master_send_request_serial) +{ + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_INPUT, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_INPUT, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_HOLD, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_COIL, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_registers(CID_DEV_REG0_DISCRITE, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_registers(CID_DEV_REG0_DISCRITE, MB_ETIMEDOUT)); +} + +#endif + +TEST_GROUP_RUNNER(unit_test_controller) +{ + +#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN) + RUN_TEST_CASE(unit_test_controller, test_setup_destroy_master_serial); + RUN_TEST_CASE(unit_test_controller, test_setup_destroy_slave_serial); + RUN_TEST_CASE(unit_test_controller, test_master_send_request_serial); +#endif + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + RUN_TEST_CASE(unit_test_controller, test_setup_destroy_master_tcp); +#endif + +} diff --git a/test_apps/unit_tests/mb_controller_common/pytest_mb_controller_common.py b/test_apps/unit_tests/mb_controller_common/pytest_mb_controller_common.py new file mode 100644 index 0000000..a299069 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/pytest_mb_controller_common.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +CONFIGS = [ + pytest.param('generic', marks=[pytest.mark.esp32, pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32c3]), +] + + +@pytest.mark.multi_dut_modbus_generic +@pytest.mark.parametrize('config', CONFIGS, indirect=True) +def test_modbus_controller_common(dut: Dut) -> None: + dut.expect_unity_test_output() diff --git a/test_apps/unit_tests/mb_controller_common/sdkconfig.ci.generic b/test_apps/unit_tests/mb_controller_common/sdkconfig.ci.generic new file mode 100644 index 0000000..15e1fef --- /dev/null +++ b/test_apps/unit_tests/mb_controller_common/sdkconfig.ci.generic @@ -0,0 +1,23 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=y +CONFIG_FMB_TCP_UID_ENABLED=y +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=n +CONFIG_MB_TEST_SLAVE_TASK_PRIO=4 +CONFIG_MB_TEST_MASTER_TASK_PRIO=4 +CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10 +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/unit_tests/mb_controller_mapping/CMakeLists.txt b/test_apps/unit_tests/mb_controller_mapping/CMakeLists.txt new file mode 100644 index 0000000..1272dd2 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/CMakeLists.txt @@ -0,0 +1,17 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +list(APPEND EXTRA_COMPONENT_DIRS "../../test_common") +list(APPEND EXTRA_COMPONENT_DIRS "../test_stubs") + +#set(COMPONENTS driver esp_timer esp_event esp_netif main) + +# list(APPEND EXTRA_COMPONENT_DIRS +# "$ENV{IDF_PATH}/tools/mocks/lwip/" +# "$ENV{IDF_PATH}/tools/mocks/freertos/" +# "$ENV{IDF_PATH}/tools/mocks/esp_timer/" +# ) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(test_mb_controller_mapping_unit) diff --git a/test_apps/unit_tests/mb_controller_mapping/README.md b/test_apps/unit_tests/mb_controller_mapping/README.md new file mode 100644 index 0000000..a1a8536 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/README.md @@ -0,0 +1,4 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | + +This test app is used to test modbus interface. diff --git a/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/CMakeLists.txt b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/CMakeLists.txt new file mode 100644 index 0000000..0b50b5c --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/CMakeLists.txt @@ -0,0 +1,9 @@ +# NOTE: This kind of mocking currently works on Linux targets only. +# On Espressif chips, too many dependencies are missing at the moment. + +idf_component_mock(INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} + REQUIRES cmock test_common + MOCK_HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/test_mbm_object.h) + + + diff --git a/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/mock/mock_config.yaml b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/mock/mock_config.yaml new file mode 100644 index 0000000..596255b --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/mock/mock_config.yaml @@ -0,0 +1,9 @@ + :cmock: + :plugins: + - expect + - expect_any_args + - return_thru_ptr + - array + - ignore + - ignore_arg + - callback diff --git a/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/test_mbm_object.h b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/test_mbm_object.h new file mode 100644 index 0000000..83e370d --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/components/mocked_esp_modbus/test_mbm_object.h @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#include "mb_config.h" +#include "mb_common.h" +#include "mb_port_types.h" + +#include "sdkconfig.h" + +/* Common definitions */ + +#ifdef __cplusplus +extern "C" { +#endif + +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_REGCNT_MAX (0x007D) +#define MB_PDU_FUNC_READ_BYTECNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +#define MB_PDU_REQ_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_SIZE (4) +#define MB_PDU_FUNC_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_VALUE_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_SIZE (4) + +#define MB_PDU_REQ_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_WRITE_MUL_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_REQ_WRITE_MUL_VALUES_OFF (MB_PDU_DATA_OFF + 5) +#define MB_PDU_REQ_WRITE_MUL_SIZE_MIN (5) +#define MB_PDU_REQ_WRITE_MUL_REGCNT_MAX (0x0078) +#define MB_PDU_FUNC_WRITE_MUL_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_WRITE_MUL_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_FUNC_WRITE_MUL_SIZE (4) + +#define MB_PDU_REQ_READWRITE_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READWRITE_READ_REGCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READWRITE_WRITE_ADDR_OFF (MB_PDU_DATA_OFF + 4) +#define MB_PDU_REQ_READWRITE_WRITE_REGCNT_OFF (MB_PDU_DATA_OFF + 6) +#define MB_PDU_REQ_READWRITE_WRITE_BYTECNT_OFF (MB_PDU_DATA_OFF + 8) +#define MB_PDU_REQ_READWRITE_WRITE_VALUES_OFF (MB_PDU_DATA_OFF + 9) +#define MB_PDU_REQ_READWRITE_SIZE_MIN (9) +#define MB_PDU_FUNC_READWRITE_READ_BYTECNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READWRITE_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READWRITE_SIZE_MIN (1) + +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_COILCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_COILCNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) + +#define MB_PDU_REQ_READ_ADDR_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_REQ_READ_DISCCNT_OFF (MB_PDU_DATA_OFF + 2) +#define MB_PDU_REQ_READ_SIZE (4) +#define MB_PDU_FUNC_READ_DISCCNT_OFF (MB_PDU_DATA_OFF + 0) +#define MB_PDU_FUNC_READ_VALUES_OFF (MB_PDU_DATA_OFF + 1) +#define MB_PDU_FUNC_READ_SIZE_MIN (1) +#define MB_PDU_REQ_WRITE_MUL_COILCNT_OFF (MB_PDU_DATA_OFF + 2) + +typedef struct mb_base_t mb_base_t; /*!< Type of modbus object */ +typedef struct mb_port_base_t mb_port_base_t; + +bool mb_port_event_get(mb_port_base_t *inst, mb_event_t *pevent); +bool mb_port_event_post(mb_port_base_t *inst, mb_event_t event); +bool mb_port_event_res_take(mb_port_base_t *inst, uint32_t timeout); +void mb_port_event_res_release(mb_port_base_t *inst); +mb_err_enum_t mb_port_event_wait_req_finish(mb_port_base_t *inst); + +#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mbs_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbs_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + +mb_err_enum_t mbm_rtu_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +mb_err_enum_t mbm_ascii_create(mb_serial_opts_t *ser_opts, void **in_out_obj); + + +#endif + +mb_err_enum_t mbs_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +mb_err_enum_t mbs_delete(mb_base_t *inst); +mb_err_enum_t mbs_enable(mb_base_t *inst); +mb_err_enum_t mbs_disable(mb_base_t *inst); +mb_err_enum_t mbs_poll(mb_base_t *inst); +mb_err_enum_t mbs_set_slv_id(mb_base_t *inst, uint8_t slv_id, bool is_running, uint8_t const *slv_idstr, uint16_t slv_idstr_len); + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +typedef struct _port_tcp_opts mb_tcp_opts_t; + +mb_err_enum_t mbm_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +#endif + +mb_err_enum_t mbm_delete(mb_base_t *inst); +mb_err_enum_t mbm_enable(mb_base_t *inst); +mb_err_enum_t mbm_disable(mb_base_t *inst); +mb_err_enum_t mbm_poll(mb_base_t *inst); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/unit_tests/mb_controller_mapping/main/CMakeLists.txt b/test_apps/unit_tests/mb_controller_mapping/main/CMakeLists.txt new file mode 100644 index 0000000..7e929e3 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/CMakeLists.txt @@ -0,0 +1,10 @@ +set(srcs "test_app_main.c" + "test_mb_controller_unit.c" +) + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +idf_component_register(SRCS ${srcs} + PRIV_REQUIRES cmock unity test_stubs test_utils mocked_esp_modbus ) # test_common + +# The workaround for WHOLE_ARCHIVE, which is absent in v4.4 +set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_impl") \ No newline at end of file diff --git a/test_apps/unit_tests/mb_controller_mapping/main/Kconfig.projbuild b/test_apps/unit_tests/mb_controller_mapping/main/Kconfig.projbuild new file mode 100644 index 0000000..ce29a07 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/Kconfig.projbuild @@ -0,0 +1,45 @@ +menu "Modbus Test Configuration" + + config MB_PORT_ADAPTER_EN + bool "Enable Modbus port adapter to substitute hardware layer for test." + default y + help + When option is enabled the port communication layer is substituted by + port adapter layer to allow testing of higher layers without access to physical layer. + + config MB_TEST_SLAVE_TASK_PRIO + int "Modbus master test task priority" + range 4 23 + default 4 + help + Modbus master task priority for the test. + + config MB_TEST_MASTER_TASK_PRIO + int "Modbus slave test task priority" + range 4 23 + default 4 + help + Modbus slave task priority for the test. + + config MB_TEST_COMM_CYCLE_COUNTER + int "Modbus communication cycle counter for test" + range 10 1000 + default 10 + help + Modbus communication cycle counter for test. + + config MB_TEST_LEAK_WARN_LEVEL + int "Modbus test leak warning level" + range 4 256 + default 32 + help + Modbus test leak warning level. + + config MB_TEST_LEAK_CRITICAL_LEVEL + int "Modbus test leak critical level" + range 4 1024 + default 64 + help + Modbus test leak critical level. + +endmenu diff --git a/test_apps/unit_tests/mb_controller_mapping/main/component.mk b/test_apps/unit_tests/mb_controller_mapping/main/component.mk new file mode 100644 index 0000000..a98f634 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/component.mk @@ -0,0 +1,4 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) diff --git a/test_apps/unit_tests/mb_controller_mapping/main/idf_component.yml b/test_apps/unit_tests/mb_controller_mapping/main/idf_component.yml new file mode 100644 index 0000000..e6964ae --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../../" + diff --git a/test_apps/unit_tests/mb_controller_mapping/main/test_app_main.c b/test_apps/unit_tests/mb_controller_mapping/main/test_app_main.c new file mode 100644 index 0000000..7e23d41 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/test_app_main.c @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_fixture.h" + +static void run_all_tests(void) +{ + RUN_TEST_GROUP(unit_test_controller); +} + +void app_main(void) +{ + UNITY_MAIN_FUNC(run_all_tests); +} diff --git a/test_apps/unit_tests/mb_controller_mapping/main/test_mb_controller_unit.c b/test_apps/unit_tests/mb_controller_mapping/main/test_mb_controller_unit.c new file mode 100644 index 0000000..8ef4808 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/main/test_mb_controller_unit.c @@ -0,0 +1,487 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "unity_fixture.h" + +#include "sdkconfig.h" +#include "test_common.h" +#include "mbc_master.h" +#include "mbc_slave.h" + +#include "Mocktest_mbm_object.h" +#include "mb_object_stub.h" + +#define TEST_SER_PORT_NUM 1 +#define TEST_TCP_PORT_NUM 1502 +#define TEST_TASKS_NUM 3 +#define TEST_TASK_TIMEOUT_MS 30000 +#define TEST_ALLOWED_LEAK 32 +#define TEST_SLAVE_SEND_TOUT_US 30000 +#define TEST_MASTER_SEND_TOUT_US 30000 + +#define TEST_MASTER_RESPOND_TOUT_MS CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND + +#define TAG "MB_CONTROLLER_TEST" + +// The workaround to statically link whole test library +__attribute__((unused)) bool mb_test_include_impl = true; + +enum +{ + CID_DEV_REG0_INPUT, + CID_DEV_REG0_HOLD, + CID_DEV_REG1_INPUT, + CID_DEV_REG0_COIL, + CID_DEV_REG0_DISCRITE, + CID_DEV_INPUT_AREA, + CID_DEV_HOLD_AREA, + CID_DEV_COIL_AREA, + CID_DEV_DISCR_AREA, +}; + +#define TEST_AREA0_REG_OFFS 2 +#define TEST_HOLD_AREA0_REG_SZ 10 +#define TEST_COIL_AREA0_REG_SZ 10 +#define TEST_INPUT_AREA0_REG_SZ 10 +#define TEST_DISCR_AREA0_REG_SZ 10 + +static uint16_t input_registers[TEST_INPUT_AREA0_REG_SZ + 1] = {0}; +static uint16_t hold_registers[TEST_HOLD_AREA0_REG_SZ + 1] = {0}; +static uint16_t coil_registers[TEST_COIL_AREA0_REG_SZ + 1] = {0}; +static uint16_t discr_registers[TEST_COIL_AREA0_REG_SZ + 1] = {0}; + +// Example Data (Object) Dictionary for Modbus parameters +static const mb_parameter_descriptor_t descriptors[] = { + {CID_DEV_REG0_INPUT, STR("MB_input_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 0, 2, + (uint32_t)&input_registers[0], PARAM_TYPE_U32, 4, OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_REG0_HOLD, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 1, 2, + (uint32_t)&hold_registers[1], PARAM_TYPE_U32, 4, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG1_INPUT, STR("MB_input_reg-1"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_COIL, STR("MB_coil_reg-0"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, 3, TEST_COIL_AREA0_REG_SZ, + 0, PARAM_TYPE_U16, 2, OPTS(0x03ff, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_REG0_DISCRITE, STR("MB_discr_reg-0"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_DISCRETE, 4, TEST_DISCR_AREA0_REG_SZ, + 0, PARAM_TYPE_U16, 2, OPTS(0x03fe, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER}, + {CID_DEV_INPUT_AREA, STR("MB_input_area"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, TEST_AREA0_REG_OFFS, TEST_INPUT_AREA0_REG_SZ, + (uint32_t)&input_registers[1], PARAM_TYPE_ASCII, (TEST_INPUT_AREA0_REG_SZ * 2), OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_HOLD_AREA, STR("MB_holding_area"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, TEST_AREA0_REG_OFFS, TEST_HOLD_AREA0_REG_SZ, + (uint32_t)&hold_registers[1], PARAM_TYPE_ASCII, (TEST_HOLD_AREA0_REG_SZ * 2), OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_COIL_AREA, STR("MB_coil_area"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, TEST_AREA0_REG_OFFS, TEST_COIL_AREA0_REG_SZ, + (uint32_t)&coil_registers[1], PARAM_TYPE_U16, ((TEST_COIL_AREA0_REG_SZ >> 3) + 1), OPTS(0, 0, 0), PAR_PERMS_READ}, + {CID_DEV_DISCR_AREA, STR("MB_discr_area"), STR("Bit"), MB_DEVICE_ADDR1, MB_PARAM_COIL, TEST_AREA0_REG_OFFS, TEST_DISCR_AREA0_REG_SZ, + (uint32_t)&discr_registers[1], PARAM_TYPE_U16, ((TEST_DISCR_AREA0_REG_SZ >> 3) + 1), OPTS(0, 0, 0), PAR_PERMS_READ}, +}; + +// Calculate number of parameters in the table +const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0])); + +TEST_GROUP(unit_test_controller); + +TEST_SETUP(unit_test_controller) +{ + test_common_start(); +} + +TEST_TEAR_DOWN(unit_test_controller) +{ + test_common_stop(); +} + +static void test_slave_check_descriptor(int par_index) +{ + mb_communication_info_t slave_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_1, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 0, + .ser_opts.test_tout_us = 0 + }; + + void *mbs_handle = NULL; + mb_base_t *pmb_base = NULL; // fake mb_base handle + + TEST_ESP_ERR(MB_ENOERR, mb_stub_serial_create(&slave_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbs_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbs_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + + TEST_ESP_OK(mbc_slave_create_serial(&slave_config, &mbs_handle)); + TEST_ASSERT(mbs_handle); + + mbs_controller_iface_t *mbs_iface = (mbs_controller_iface_t *)mbs_handle; + //mb_slave_options_t *mbs_opts = MB_SLAVE_GET_OPTS(mbs_iface); + TEST_ASSERT_EQUAL_HEX32(mbs_iface->mb_base, pmb_base); + + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_input_cb, mbc_reg_input_slave_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_holding_cb, mbc_reg_holding_slave_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_coils_cb, mbc_reg_coils_slave_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_discrete_cb, mbc_reg_discrete_slave_cb); + + mb_parameter_descriptor_t *pdescr = (mb_parameter_descriptor_t *)&descriptors[par_index]; + mb_register_area_descriptor_t reg_area; + ESP_LOGI(TAG, "Test CID #%d, %s, %s", pdescr->cid, pdescr->param_key, pdescr->param_units); + + uint16_t n_bytes = ((pdescr->mb_param_type == MB_PARAM_INPUT) || (pdescr->mb_param_type == MB_PARAM_HOLDING)) + ? (pdescr->mb_size << 1) : ((pdescr->mb_size >> 3) + 1); + + // First define the correct area + reg_area.type = pdescr->mb_param_type; + reg_area.start_offset = pdescr->mb_reg_start; + reg_area.address = (void *)pdescr->param_offset; + reg_area.size = n_bytes; + ESP_LOGI(TAG, "Area (type, reg_start, address, size): %d, %u, 0x%" PRIx32 ", %d, is defined.", + (int)reg_area.type, (unsigned)reg_area.start_offset, (uint32_t)reg_area.address, (int)reg_area.size); + TEST_ESP_OK(mbc_slave_set_descriptor(mbs_handle, reg_area)); + + // Check additional area overlapped + reg_area.start_offset = (pdescr->mb_reg_start + pdescr->mb_size - 2); + reg_area.size = 2; + ESP_LOGI(TAG, "Area overlapped (type, reg_start, address, size): %d, %u, 0x%" PRIx32 ", %d.", + (int)reg_area.type, (unsigned)reg_area.start_offset, (uint32_t)reg_area.address, (int)reg_area.size); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, mbc_slave_set_descriptor(mbs_handle, reg_area)); + + reg_area.start_offset = pdescr->mb_reg_start; + reg_area.size = n_bytes; + reg_area.address = (void *)pdescr->param_offset - 2; + ESP_LOGI(TAG, "Area redefine (type, reg_start, address, size): %d, %u, 0x%" PRIx32 ", %d.", + (int)reg_area.type, (unsigned)reg_area.start_offset, (uint32_t)reg_area.address, (int)reg_area.size); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, mbc_slave_set_descriptor(mbs_handle, reg_area)); + + TEST_ESP_OK(mbc_slave_delete(mbs_handle)); // the destructor of mb controller destroys the fake mb_object as well + TEST_ASSERT_EQUAL_HEX(mb_port_get_inst_counter(), 0); + ESP_LOGI(TAG, "Test passed successfully."); +} + +static esp_err_t test_master_read_req(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US}; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_serial_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mb_port_event_post_ExpectAndReturn(pmb_base->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START), true); + TEST_ESP_OK(mbc_master_start(mbm_handle)); + mb_port_event_wait_req_finish_ExpectAndReturn(pmb_base->port_obj, mb_err); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + TEST_ESP_OK(mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor)); + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t type = 0; // type of parameter from dictionary + uint8_t pdata[100] = {0}; + ESP_LOGI(TAG, "Test CID #%d, %s, %s", param_descriptor->cid, param_descriptor->param_key, param_descriptor->param_units); + mb_port_event_res_take_ExpectAnyArgsAndReturn(true); + mb_port_event_res_release_ExpectAnyArgs(); + mb_port_event_res_take_ExpectAnyArgsAndReturn(true); + mb_port_event_res_release_ExpectAnyArgs(); + + // Call the read method of modbus controller + esp_err_t err = mbc_master_get_parameter(mbm_handle, par_index, pdata, &type); + uint8_t *mb_frame_ptr = NULL; + // get send buffer back using the fake mb_object + pmb_base->get_send_buf(pmb_base, &mb_frame_ptr); + TEST_ASSERT_EQUAL_HEX8(pmb_base->get_dest_addr(pmb_base), param_descriptor->mb_slave_addr); + uint8_t send_len = pmb_base->get_send_len(pmb_base); + TEST_ASSERT_EQUAL_HEX8(send_len, (MB_PDU_SIZE_MIN + MB_PDU_REQ_READ_SIZE)); + // Check that request function forms correct buffer + switch (param_descriptor->mb_param_type) + { + case MB_PARAM_INPUT: + // TEST_CHECK_EQ + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_INPUT_REGISTER); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + case MB_PARAM_HOLDING: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_HOLDING_REGISTER); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_REGCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + case MB_PARAM_COIL: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_COILS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_COILCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + case MB_PARAM_DISCRETE: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_READ_DISCRETE_INPUTS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_READ_DISCCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + default: + TEST_FAIL(); + break; + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); // the destructor of mb controller destroys the fake mb_object as well + TEST_ASSERT_EQUAL_HEX(mb_port_get_inst_counter(), 0); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +static esp_err_t test_master_write_req(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US}; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_serial_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mb_port_event_post_ExpectAndReturn(pmb_base->port_obj, EVENT(EV_FRAME_TRANSMIT | EV_TRANS_START), true); + mb_port_event_wait_req_finish_ExpectAndReturn(pmb_base->port_obj, mb_err); + TEST_ESP_OK(mbc_master_start(mbm_handle)); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + TEST_ESP_OK(mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor)); + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t type = 0; // type of parameter from dictionary + uint8_t reg_data[] = {0x11, 0x22, 0x33, 0x44}; + ESP_LOGI(TAG, "Test CID #%d, %s, %s", param_descriptor->cid, param_descriptor->param_key, param_descriptor->param_units); + mb_port_event_res_take_ExpectAnyArgsAndReturn(true); + mb_port_event_res_release_ExpectAnyArgs(); + mb_port_event_res_take_ExpectAnyArgsAndReturn(true); + mb_port_event_res_release_ExpectAnyArgs(); + + // Call the read method of modbus controller + esp_err_t err = mbc_master_set_parameter(mbm_handle, par_index, reg_data, &type); + uint8_t *mb_frame_ptr = NULL; + // get send buffer back using the fake mb_object + pmb_base->get_send_buf(pmb_base, &mb_frame_ptr); + TEST_ASSERT_EQUAL_HEX8(pmb_base->get_dest_addr(pmb_base), param_descriptor->mb_slave_addr); + uint8_t send_len = pmb_base->get_send_len(pmb_base); + // Check that request function forms correct buffer + switch (param_descriptor->mb_param_type) + { + case MB_PARAM_HOLDING: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_WRITE_MULTIPLE_REGISTERS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_REGCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF], (param_descriptor->mb_size << 1)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF], sizeof(reg_data)); + TEST_ASSERT_EQUAL_HEX8(send_len, ((MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_MUL_SIZE_MIN + 2 * param_descriptor->mb_size))); + for (int i = 0; (i < param_descriptor->mb_size); i++) + { + TEST_ASSERT_EQUAL_HEX8(reg_data[0], mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF + 1]); + TEST_ASSERT_EQUAL_HEX8(reg_data[1], mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF]); + } + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + // TEST_ESP_ERR(MB_ENOERR, mbs_fn_write_holding_reg(pmb_base, mb_frame_ptr, &send_len)); + break; + case MB_PARAM_COIL: + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_FUNC_OFF], MB_FUNC_WRITE_MULTIPLE_COILS); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF], (param_descriptor->mb_reg_start >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_ADDR_OFF + 1], (param_descriptor->mb_reg_start & 0x00FF)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_COILCNT_OFF], (param_descriptor->mb_size >> 8)); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_COILCNT_OFF + 1], (param_descriptor->mb_size & 0x00FF)); + uint8_t byte_cnt = (param_descriptor->mb_size & 0x0007) ? ((param_descriptor->mb_size >> 3) + 1) : (param_descriptor->mb_size >> 3); + TEST_ASSERT_EQUAL_HEX8(mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_BYTECNT_OFF], byte_cnt); + TEST_ASSERT_EQUAL_HEX8(send_len, (MB_PDU_SIZE_MIN + MB_PDU_REQ_WRITE_MUL_SIZE_MIN + byte_cnt)); + TEST_ASSERT_EQUAL_HEX8(reg_data[0], mb_frame_ptr[MB_PDU_REQ_WRITE_MUL_VALUES_OFF]); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, (void *)mb_frame_ptr, send_len, ESP_LOG_INFO); + break; + default: + TEST_FAIL(); + break; + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + TEST_ASSERT_EQUAL_HEX(mb_port_get_inst_counter(), 0); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +static esp_err_t test_master_check_callback(int par_index, mb_err_enum_t mb_err) +{ + mb_communication_info_t master_config = { + .ser_opts.port = TEST_SER_PORT_NUM, + .ser_opts.mode = MB_RTU, + .ser_opts.uid = MB_DEVICE_ADDR1, + .ser_opts.data_bits = UART_DATA_8_BITS, + .ser_opts.stop_bits = UART_STOP_BITS_2, + .ser_opts.baudrate = 115200, + .ser_opts.parity = UART_PARITY_DISABLE, + .ser_opts.response_tout_ms = 1, + .ser_opts.test_tout_us = TEST_SLAVE_SEND_TOUT_US}; + mb_base_t *pmb_base = NULL; // fake mb_base handle + void *mbm_handle = NULL; + + TEST_ESP_ERR(MB_ENOERR, mb_stub_serial_create(&master_config.ser_opts, (void *)&pmb_base)); + pmb_base->port_obj = (mb_port_base_t *)0x44556677; + mbm_rtu_create_ExpectAnyArgsAndReturn(MB_ENOERR); + mbm_rtu_create_ReturnThruPtr_in_out_obj((void **)&pmb_base); + TEST_ESP_OK(mbc_master_create_serial(&master_config, &mbm_handle)); + TEST_ESP_OK(mbc_master_set_descriptor(mbm_handle, &descriptors[0], num_descriptors)); + mbm_controller_iface_t *mbm_controller_iface = (mbm_controller_iface_t *)mbm_handle; + mb_master_options_t *mbm_opts = MB_MASTER_GET_OPTS(mbm_controller_iface); + TEST_ASSERT_EQUAL_HEX32(mbm_controller_iface->mb_base, pmb_base); + + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_input_cb, mbc_reg_input_master_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_holding_cb, mbc_reg_holding_master_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_coils_cb, mbc_reg_coils_master_cb); + TEST_ASSERT_EQUAL_HEX32(pmb_base->rw_cbs.reg_discrete_cb, mbc_reg_discrete_master_cb); + + TEST_ESP_OK(mbc_master_start(mbm_handle)); + + const mb_parameter_descriptor_t *param_descriptor = NULL; + TEST_ESP_OK(mbc_master_get_cid_info(mbm_handle, par_index, ¶m_descriptor)); + TEST_ASSERT_EQUAL_HEX32(&descriptors[par_index], param_descriptor); + uint8_t reg_data_in[] = {0x11, 0x22, 0x33, 0x44}; + uint8_t reg_data_out[4] = {0}; + mbm_opts->reg_buffer_size = param_descriptor->mb_size; + mbm_opts->reg_buffer_ptr = ®_data_out[0]; + esp_err_t err = ESP_FAIL; + uint8_t byte_cnt = 0; + // Check that request function forms correct buffer + switch (param_descriptor->mb_param_type) + { + case MB_PARAM_HOLDING: + err = mbc_reg_holding_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, + param_descriptor->mb_size, MB_REG_READ); + for (int i = 0; (i < param_descriptor->mb_size); i++) + { + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1)], reg_data_out[(i << 1) + 1]); + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1) + 1], reg_data_out[(i << 1)]); + } + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + break; + + case MB_PARAM_INPUT: + err = mbc_reg_input_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, + param_descriptor->mb_size); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + for (int i = 0; (i < param_descriptor->mb_size); i++) + { + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1)], reg_data_out[(i << 1) + 1]); + TEST_ASSERT_EQUAL_HEX8(reg_data_in[(i << 1) + 1], reg_data_out[(i << 1)]); + } + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, (param_descriptor->mb_size << 1), ESP_LOG_INFO); + break; + + case MB_PARAM_COIL: + reg_data_in[0] = 0xFF; + reg_data_in[1] = 0xFF; + err = mbc_reg_coils_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, param_descriptor->mb_size, MB_REG_READ); + byte_cnt = (param_descriptor->mb_size & 0x0007) ? ((param_descriptor->mb_size >> 3) + 1) : (param_descriptor->mb_size >> 3); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, byte_cnt, ESP_LOG_INFO); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[0] & param_descriptor->param_opts.opt1), param_descriptor->param_opts.opt1); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[1] & ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)), ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, byte_cnt, ESP_LOG_INFO); + break; + + case MB_PARAM_DISCRETE: + reg_data_in[0] = 0xFF; + reg_data_in[1] = 0xFF; + err = mbc_reg_discrete_master_cb(pmb_base, reg_data_in, param_descriptor->mb_reg_start, param_descriptor->mb_size); + byte_cnt = (param_descriptor->mb_size & 0x0007) ? ((param_descriptor->mb_size >> 3) + 1) : (param_descriptor->mb_size >> 3); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", INPUT_BUFF", (void *)reg_data_in, byte_cnt, ESP_LOG_INFO); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[0] & param_descriptor->param_opts.opt1), param_descriptor->param_opts.opt1); + TEST_ASSERT_EQUAL_HEX8((reg_data_out[1] & ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)), ((param_descriptor->param_opts.opt1 >> 8) & 0xFF)); + ESP_LOG_BUFFER_HEX_LEVEL(TAG ", OUTPUT_BUFF", (void *)reg_data_out, byte_cnt, ESP_LOG_INFO); + break; + + default: + break; + } + TEST_ESP_OK(mbc_master_stop(mbm_handle)); + TEST_ESP_OK(mbc_master_delete(mbm_handle)); + ESP_LOGI(TAG, "Test passed successfully."); + return err; +} + +// Check if modbus controller object forms correct modbus request from data dictionary +// and is able to transfer data using mb_object. Check possible errors returned back from +// mb_object and make sure the modbus controller handles them correctly. +TEST(unit_test_controller, test_master_send_read_request) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller handles read requests correctly."); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_INPUT, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_read_req(CID_DEV_REG0_INPUT, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_ERR_INVALID_RESPONSE, test_master_read_req(CID_DEV_REG0_INPUT, MB_ERECVDATA)); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_INVALID_RESPONSE, test_master_read_req(CID_DEV_REG0_HOLD, MB_ERECVDATA)); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_read_req(CID_DEV_REG0_COIL, MB_ETIMEDOUT)); + TEST_ESP_ERR(ESP_OK, test_master_read_req(CID_DEV_REG0_DISCRITE, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_read_req(CID_DEV_REG0_DISCRITE, MB_ETIMEDOUT)); +} + +TEST(unit_test_controller, test_master_send_write_request) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller handles write requests correctly."); + TEST_ESP_ERR(ESP_OK, test_master_write_req(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_INVALID_RESPONSE, test_master_write_req(CID_DEV_REG0_HOLD, MB_ERECVDATA)); + TEST_ESP_ERR(ESP_OK, test_master_write_req(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_ERR_TIMEOUT, test_master_write_req(CID_DEV_REG0_COIL, MB_ETIMEDOUT)); +} + +TEST(unit_test_controller, test_slave_check_area_descriptor) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller defines the area descriptors correctly."); + test_slave_check_descriptor(CID_DEV_INPUT_AREA); + test_slave_check_descriptor(CID_DEV_HOLD_AREA); + test_slave_check_descriptor(CID_DEV_COIL_AREA); + test_slave_check_descriptor(CID_DEV_DISCR_AREA); +} + +TEST(unit_test_controller, test_master_register_callbacks) +{ + ESP_LOGI(TAG, "TEST: Check the modbus master controller handles mapping callback functions correctly."); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_HOLD, MB_ENOERR)); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_INPUT, MB_ENOERR)); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_COIL, MB_ENOERR)); + TEST_ESP_ERR(ESP_OK, test_master_check_callback(CID_DEV_REG0_DISCRITE, MB_ENOERR)); +} + +TEST_GROUP_RUNNER(unit_test_controller) +{ + RUN_TEST_CASE(unit_test_controller, test_master_send_read_request); + RUN_TEST_CASE(unit_test_controller, test_master_send_write_request); + RUN_TEST_CASE(unit_test_controller, test_master_register_callbacks); + RUN_TEST_CASE(unit_test_controller, test_slave_check_area_descriptor); +} diff --git a/test_apps/unit_tests/mb_controller_mapping/pytest_mb_controller_mapping.py b/test_apps/unit_tests/mb_controller_mapping/pytest_mb_controller_mapping.py new file mode 100644 index 0000000..0095389 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/pytest_mb_controller_mapping.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +CONFIGS = [ + pytest.param('serial', marks=[pytest.mark.esp32, pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32c3]), +] + + +@pytest.mark.multi_dut_modbus_generic +@pytest.mark.parametrize('config', CONFIGS, indirect=True) +def test_modbus_controller_mapping(dut: Dut) -> None: + dut.expect_unity_test_output() diff --git a/test_apps/unit_tests/mb_controller_mapping/sdkconfig.ci.serial b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.ci.serial new file mode 100644 index 0000000..927e15a --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.ci.serial @@ -0,0 +1,19 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y +CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128 +CONFIG_MB_TEST_LEAK_WARN_LEVEL=128 + diff --git a/test_apps/unit_tests/mb_controller_mapping/sdkconfig.default b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.default new file mode 100644 index 0000000..61feae0 --- /dev/null +++ b/test_apps/unit_tests/mb_controller_mapping/sdkconfig.default @@ -0,0 +1,17 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +# +# Modbus configuration +# +CONFIG_UNITY_ENABLE_FIXTURE=y +CONFIG_FMB_PORT_TASK_STACK_SIZE=4096 +CONFIG_FMB_PORT_TASK_PRIO=10 +CONFIG_FMB_COMM_MODE_RTU_EN=y +CONFIG_FMB_COMM_MODE_ASCII_EN=y +CONFIG_FMB_COMM_MODE_TCP_EN=n +CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000 +CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300 +CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y +CONFIG_MB_PORT_ADAPTER_EN=y + diff --git a/test_apps/unit_tests/mb_ext_types/CMakeLists.txt b/test_apps/unit_tests/mb_ext_types/CMakeLists.txt new file mode 100644 index 0000000..e880dea --- /dev/null +++ b/test_apps/unit_tests/mb_ext_types/CMakeLists.txt @@ -0,0 +1,8 @@ +#This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components") +set(COMPONENTS main) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(mb_endianness_utils) diff --git a/test_apps/unit_tests/mb_ext_types/README.md b/test_apps/unit_tests/mb_ext_types/README.md new file mode 100644 index 0000000..c75201f --- /dev/null +++ b/test_apps/unit_tests/mb_ext_types/README.md @@ -0,0 +1,3 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | + diff --git a/test_apps/unit_tests/mb_ext_types/main/CMakeLists.txt b/test_apps/unit_tests/mb_ext_types/main/CMakeLists.txt new file mode 100644 index 0000000..9266b51 --- /dev/null +++ b/test_apps/unit_tests/mb_ext_types/main/CMakeLists.txt @@ -0,0 +1,7 @@ +set(srcs "test_mb_endianness_utils.c") + +idf_component_register(SRCS ${srcs} + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES esp-modbus test_utils unity) + + diff --git a/test_apps/unit_tests/mb_ext_types/main/idf_component.yml b/test_apps/unit_tests/mb_ext_types/main/idf_component.yml new file mode 100644 index 0000000..e6964ae --- /dev/null +++ b/test_apps/unit_tests/mb_ext_types/main/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: ">=5.0" + espressif/esp-modbus: + version: "^2.0.0" + override_path: "../../../../" + diff --git a/test_apps/unit_tests/mb_ext_types/main/test_mb_endianness_utils.c b/test_apps/unit_tests/mb_ext_types/main/test_mb_endianness_utils.c new file mode 100644 index 0000000..62379c5 --- /dev/null +++ b/test_apps/unit_tests/mb_ext_types/main/test_mb_endianness_utils.c @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include "unity.h" +#include "test_utils.h" + +#include "sdkconfig.h" +#include "mb_endianness_utils.h" + +#define TAG "MB_ENDIANNESS_TEST" + +// The below is the data used for endianness conversion test + +const uint16_t TEST_INT8_A = 0x00F6; +const uint16_t TEST_INT8_B = 0xF600; + +const uint16_t TEST_UINT8_A = 0x0037; +const uint16_t TEST_UINT8_B = 0x3700; + +const uint16_t TEST_UINT16_AB = 0x3039; +const uint16_t TEST_UINT16_BA = 0x3930; +const uint16_t TEST_INT16_AB = 0x3039; +const uint16_t TEST_INT16_BA = 0x3930; + +const uint32_t TEST_FLOAT_ABCD = 0x4640e400; +const uint32_t TEST_FLOAT_DCBA = 0x00e44046; +const uint32_t TEST_FLOAT_BADC = 0x404600e4; +const uint32_t TEST_FLOAT_CDAB = 0xe4004640; + +const uint32_t TEST_UINT32_ABCD = 0x11223344; +const uint32_t TEST_UINT32_DCBA = 0x44332211; +const uint32_t TEST_UINT32_BADC = 0x22114433; +const uint32_t TEST_UINT32_CDAB = 0x33441122; + +const uint64_t TEST_DOUBLE_ABCDEFGH = 0x40c81c8000000000; +const uint64_t TEST_DOUBLE_HGFEDCBA = 0x00000000801cc840; +const uint64_t TEST_DOUBLE_GHEFCDAB = 0x000000001c8040c8; +const uint64_t TEST_DOUBLE_BADCFEHG = 0xc840801c00000000; + +const uint64_t TEST_INT64_ABCDEFGH = 0xffffffffffffcfc7; +const uint64_t TEST_INT64_HGFEDCBA = 0xc7cfffffffffffff; +const uint64_t TEST_INT64_GHEFCDAB = 0xcfc7ffffffffffff; +const uint64_t TEST_INT64_BADCFEHG = 0xffffffffffffc7cf; + +const uint64_t TEST_UINT64_ABCDEFGH = 0x1122334455667788; +const uint64_t TEST_UINT64_HGFEDCBA = 0x8877665544332211; +const uint64_t TEST_UINT64_GHEFCDAB = 0x7788556633441122; +const uint64_t TEST_UINT64_BADCFEHG = 0x2211443366558877; + +TEST_CASE("Test endianness conversion for all extended Modbus types.", "[MB_ENDIANNESS]") +{ + val_16_arr arr_16 = {0}; + val_32_arr arr_32 = {0}; + val_64_arr arr_64 = {0}; + + TEST_ASSERT(mb_set_uint8_a(&arr_16, (uint8_t)55) == TEST_UINT8_A); + TEST_ASSERT(mb_get_uint8_a(&arr_16) == (uint8_t)55); + TEST_ASSERT(mb_set_int8_a(&arr_16, (int8_t)-10) == TEST_INT8_A); + TEST_ASSERT(mb_get_int8_a(&arr_16) == (int8_t)-10); + + TEST_ASSERT(mb_set_uint8_b(&arr_16, (uint8_t)55) == TEST_UINT8_B); + TEST_ASSERT(mb_get_uint8_b(&arr_16) == (uint8_t)55); + TEST_ASSERT(mb_set_int8_b(&arr_16, (int8_t)-10) == TEST_INT8_B); + TEST_ASSERT(mb_get_int8_b(&arr_16) == (int8_t)-10); + + TEST_ASSERT(mb_set_uint16_ab(&arr_16, (uint16_t)12345) == TEST_UINT16_AB); + TEST_ASSERT(mb_get_uint16_ab(&arr_16) == (uint16_t)12345); + TEST_ASSERT(mb_set_int16_ab(&arr_16, (int16_t)12345) == TEST_INT16_AB); + TEST_ASSERT(mb_get_int16_ab(&arr_16) == (int16_t)12345); + + TEST_ASSERT(mb_set_uint16_ba(&arr_16, (uint16_t)12345) == TEST_UINT16_BA); + TEST_ASSERT(mb_get_uint16_ba(&arr_16) == (uint16_t)12345); + TEST_ASSERT(mb_set_int16_ba(&arr_16, (int16_t)12345) == TEST_INT16_BA); + TEST_ASSERT(mb_get_int16_ba(&arr_16) == (int16_t)12345); + + TEST_ASSERT(mb_set_uint16_ab(&arr_16, (uint16_t)12345) == TEST_UINT16_AB); + TEST_ASSERT(mb_get_uint16_ab(&arr_16) == (uint16_t)12345); + TEST_ASSERT(mb_set_int16_ab(&arr_16, (int16_t)12345) == TEST_INT16_AB); + TEST_ASSERT(mb_get_int16_ab(&arr_16) == (int16_t)12345); + + TEST_ASSERT(mb_set_float_abcd(&arr_32, (float)12345.0) == TEST_FLOAT_ABCD); + TEST_ASSERT(mb_get_float_abcd(&arr_32) == (float)12345.0); + + TEST_ASSERT(mb_set_float_badc(&arr_32, (float)12345.0) == TEST_FLOAT_BADC); + TEST_ASSERT(mb_get_float_badc(&arr_32) == (float)12345.0); + + TEST_ASSERT(mb_set_float_cdab(&arr_32, (float)12345.0) == TEST_FLOAT_CDAB); + TEST_ASSERT(mb_get_float_cdab(&arr_32) == (float)12345.0); + + TEST_ASSERT(mb_set_float_dcba(&arr_32, (float)12345.0) == TEST_FLOAT_DCBA); + TEST_ASSERT(mb_get_float_dcba(&arr_32) == (float)12345.0); + + TEST_ASSERT(mb_set_uint32_abcd(&arr_32, (uint32_t)0x11223344) == TEST_UINT32_ABCD); + TEST_ASSERT(mb_get_uint32_abcd(&arr_32) == (uint32_t)0x11223344); + TEST_ASSERT(mb_set_int32_abcd(&arr_32, (int32_t)0x11223344) == TEST_UINT32_ABCD); + TEST_ASSERT(mb_get_int32_abcd(&arr_32) == (int32_t)0x11223344); + + TEST_ASSERT(mb_set_uint32_badc(&arr_32, (uint32_t)0x11223344) == TEST_UINT32_BADC); + TEST_ASSERT(mb_get_uint32_badc(&arr_32) == (uint32_t)0x11223344); + TEST_ASSERT(mb_set_int32_badc(&arr_32, (int32_t)0x11223344) == TEST_UINT32_BADC); + TEST_ASSERT(mb_get_int32_badc(&arr_32) == (int32_t)0x11223344); + + TEST_ASSERT(mb_set_uint32_cdab(&arr_32, (uint32_t)0x11223344) == TEST_UINT32_CDAB); + TEST_ASSERT(mb_get_uint32_cdab(&arr_32) == (uint32_t)0x11223344); + TEST_ASSERT(mb_set_int32_cdab(&arr_32, (int32_t)0x11223344) == TEST_UINT32_CDAB); + TEST_ASSERT(mb_get_int32_cdab(&arr_32) == (int32_t)0x11223344); + + TEST_ASSERT(mb_set_uint32_dcba(&arr_32, (uint32_t)0x11223344) == TEST_UINT32_DCBA); + TEST_ASSERT(mb_get_uint32_dcba(&arr_32) == (uint32_t)0x11223344); + TEST_ASSERT(mb_set_int32_dcba(&arr_32, (int32_t)0x11223344) == TEST_UINT32_DCBA); + TEST_ASSERT(mb_get_int32_dcba(&arr_32) == (int32_t)0x11223344); + + TEST_ASSERT(mb_set_double_abcdefgh(&arr_64, (double)12345.0) == TEST_DOUBLE_ABCDEFGH); + TEST_ASSERT(mb_get_double_abcdefgh(&arr_64) == (double)12345.0); + TEST_ASSERT(mb_set_uint64_abcdefgh(&arr_64, (uint64_t)0x1122334455667788) == TEST_UINT64_ABCDEFGH); + TEST_ASSERT(mb_get_uint64_abcdefgh(&arr_64) == (uint64_t)0x1122334455667788); + TEST_ASSERT(mb_set_int64_abcdefgh(&arr_64, (int64_t)-12345) == TEST_INT64_ABCDEFGH); + TEST_ASSERT(mb_get_int64_abcdefgh(&arr_64) == (int64_t)-12345); + + TEST_ASSERT(mb_set_double_hgfedcba(&arr_64, (double)12345.0) == TEST_DOUBLE_HGFEDCBA); + TEST_ASSERT(mb_get_double_hgfedcba(&arr_64) == (double)12345.0); + TEST_ASSERT(mb_set_uint64_hgfedcba(&arr_64, (uint64_t)0x1122334455667788) == TEST_UINT64_HGFEDCBA); + TEST_ASSERT(mb_get_uint64_hgfedcba(&arr_64) == (uint64_t)0x1122334455667788); + TEST_ASSERT(mb_set_int64_hgfedcba(&arr_64, (int64_t)-12345) == TEST_INT64_HGFEDCBA); + TEST_ASSERT(mb_get_int64_hgfedcba(&arr_64) == (int64_t)-12345); + + TEST_ASSERT(mb_set_double_ghefcdab(&arr_64, (double)12345.0) == TEST_DOUBLE_GHEFCDAB); + TEST_ASSERT(mb_get_double_ghefcdab(&arr_64) == (double)12345.0); + TEST_ASSERT(mb_set_uint64_ghefcdab(&arr_64, (uint64_t)0x1122334455667788) == TEST_UINT64_GHEFCDAB); + TEST_ASSERT(mb_get_uint64_ghefcdab(&arr_64) == (uint64_t)0x1122334455667788); + TEST_ASSERT(mb_set_int64_ghefcdab(&arr_64, (int64_t)-12345) == TEST_INT64_GHEFCDAB); + TEST_ASSERT(mb_get_int64_ghefcdab(&arr_64) == (int64_t)-12345); + + TEST_ASSERT(mb_set_double_badcfehg(&arr_64, (double)12345.0) == TEST_DOUBLE_BADCFEHG); + TEST_ASSERT(mb_get_double_badcfehg(&arr_64) == (double)12345.0); + TEST_ASSERT(mb_set_uint64_badcfehg(&arr_64, (uint64_t)0x1122334455667788) == TEST_UINT64_BADCFEHG); + TEST_ASSERT(mb_get_uint64_badcfehg(&arr_64) == (uint64_t)0x1122334455667788); + TEST_ASSERT(mb_set_int64_badcfehg(&arr_64, (int64_t)-12345) == TEST_INT64_BADCFEHG); + TEST_ASSERT(mb_get_int64_badcfehg(&arr_64) == (int64_t)-12345); +} + +void app_main(void) +{ + unity_run_menu(); +} diff --git a/test_apps/unit_tests/mb_ext_types/pytest_mb_endianness_utils.py b/test_apps/unit_tests/mb_ext_types/pytest_mb_endianness_utils.py new file mode 100644 index 0000000..80f69b3 --- /dev/null +++ b/test_apps/unit_tests/mb_ext_types/pytest_mb_endianness_utils.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +#@pytest.mark.supported_targets +@pytest.mark.esp32 # test on esp32 for now +@pytest.mark.multi_dut_modbus_generic +def test_mb_endianness_utils(dut: Dut) -> None: + dut.run_all_single_board_cases() diff --git a/test_apps/unit_tests/mb_ext_types/sdkconfig.defaults b/test_apps/unit_tests/mb_ext_types/sdkconfig.defaults new file mode 100644 index 0000000..7b32825 --- /dev/null +++ b/test_apps/unit_tests/mb_ext_types/sdkconfig.defaults @@ -0,0 +1,2 @@ +# General options for test +CONFIG_FMB_EXT_TYPE_SUPPORT=y diff --git a/test_apps/unit_tests/test_stubs/CMakeLists.txt b/test_apps/unit_tests/test_stubs/CMakeLists.txt new file mode 100644 index 0000000..cf9fd3d --- /dev/null +++ b/test_apps/unit_tests/test_stubs/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) +set(srcs "src/mb_object_stub.c") + +idf_component_register( SRCS ${srcs} + INCLUDE_DIRS "include" + REQUIRES test_common unity) diff --git a/test_apps/unit_tests/test_stubs/include/mb_object_stub.h b/test_apps/unit_tests/test_stubs/include/mb_object_stub.h new file mode 100644 index 0000000..65ee727 --- /dev/null +++ b/test_apps/unit_tests/test_stubs/include/mb_object_stub.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_log.h" +#include "mb_common.h" +#include "mb_port_types.h" + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) +mb_err_enum_t mb_stub_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); +#endif + +#if (MB_MASTER_ASCII_ENABLED || MB_MASTER_RTU_ENABLED) +mb_err_enum_t mb_stub_serial_create(mb_serial_opts_t *ser_opts, void **in_out_obj); +#endif diff --git a/test_apps/unit_tests/test_stubs/include/transport_common.h b/test_apps/unit_tests/test_stubs/include/transport_common.h new file mode 100644 index 0000000..02d7f61 --- /dev/null +++ b/test_apps/unit_tests/test_stubs/include/transport_common.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "mb_types.h" +#include "port_common.h" +#include "mb_port_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mb_trans_base_t mb_trans_base_t; /*!< Type of moddus transport object */ +typedef struct _obj_descr obj_descr_t; + +typedef void (*mb_frm_start_fp)(mb_trans_base_t *transport); +typedef void (*mb_frm_stop_fp)(mb_trans_base_t *transport); +typedef mb_err_enum_t (*mb_frm_rcv_fp)(mb_trans_base_t *transport, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +typedef mb_err_enum_t (*mb_frm_snd_fp)(mb_trans_base_t *transport, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +typedef void (*mb_get_rx_frm_fp) (mb_trans_base_t *transport, uint8_t **frame_ptr_buf); +typedef void (*mb_get_tx_frm_fp) (mb_trans_base_t *transport, uint8_t **frame_ptr_buf); +typedef bool (*mb_get_fp)(mb_trans_base_t *inst); + +struct mb_trans_base_t +{ + obj_descr_t descr; + + _lock_t lock; + mb_port_base_t *port_obj; + + mb_frm_start_fp frm_start; + mb_frm_stop_fp frm_stop; + mb_get_fp frm_delete; + mb_frm_snd_fp frm_send; + mb_frm_rcv_fp frm_rcv; + mb_get_rx_frm_fp get_rx_frm; + mb_get_rx_frm_fp get_tx_frm; + mb_get_fp frm_is_bcast; +}; //!< Transport methods + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/test_apps/unit_tests/test_stubs/src/mb_object_stub.c b/test_apps/unit_tests/test_stubs/src/mb_object_stub.c new file mode 100644 index 0000000..818f609 --- /dev/null +++ b/test_apps/unit_tests/test_stubs/src/mb_object_stub.c @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "mb_config.h" +#include "mb_common.h" +#include "mb_proto.h" +#include "mb_func.h" +#include "mb_master.h" +#include "transport_common.h" +#include "port_common.h" +#include "ascii_transport.h" +#include "rtu_transport.h" +#include "tcp_transport.h" + +static const char *TAG = "mb_object.master.stub"; + +#if (MB_MASTER_ASCII_ENABLED || MB_MASTER_RTU_ENABLED || MB_MASTER_TCP_ENABLED) + +typedef struct +{ + mb_base_t base; + uint16_t pdu_snd_len; + uint8_t dst_addr; + uint8_t snd_buf[MB_BUFFER_SIZE]; +} mb_object_t; + + +mb_err_enum_t mb_delete(mb_base_t *inst); +mb_err_enum_t mb_enable(mb_base_t *inst); +mb_err_enum_t mb_disable(mb_base_t *inst); +mb_err_enum_t mb_poll(mb_base_t *inst); + +static void mb_set_pdu_send_length(mb_base_t *inst, uint16_t length); +static uint16_t mb_get_pdu_send_length(mb_base_t *inst); +static void mb_set_dest_addr(mb_base_t *inst, uint8_t dest_addr); +static uint8_t mb_get_dest_addr(mb_base_t *inst); +static void mb_get_pdu_send_buf(mb_base_t *inst, uint8_t **pbuf); + +//mb_err_enum_t mb_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj); + +#if (CONFIG_FMB_COMM_MODE_TCP_EN) + +mb_err_enum_t mb_stub_tcp_create(mb_tcp_opts_t *tcp_opts, void **in_out_obj) +{ + MB_RETURN_ON_FALSE((tcp_opts && in_out_obj), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mb_object_t *mb_obj = NULL; + mb_obj = (mb_object_t *)calloc(1, sizeof(mb_object_t)); + MB_GOTO_ON_FALSE((mb_obj), MB_EILLSTATE, error, TAG, "no mem for mb master instance."); + CRITICAL_SECTION_INIT(mb_obj->base.lock); + ESP_LOGW(TAG, "Create fake mb_base object."); + mb_obj->base.delete = mb_delete; + mb_obj->base.enable = mb_enable; + mb_obj->base.disable = mb_disable; + mb_obj->base.poll = mb_poll; + mb_obj->base.set_dest_addr = mb_set_dest_addr; + mb_obj->base.get_dest_addr = mb_get_dest_addr; + mb_obj->base.set_send_len = mb_set_pdu_send_length; + mb_obj->base.get_send_len = mb_get_pdu_send_length; + mb_obj->base.get_send_buf = mb_get_pdu_send_buf; + mb_obj->base.descr.parent = *in_out_obj; + mb_obj->base.descr.is_master = true; + mb_obj->base.descr.obj_name = (char *)TAG; + mb_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + *in_out_obj = (void *)mb_obj; + return MB_ENOERR; + +error: + CRITICAL_SECTION_CLOSE(mb_obj->base.lock); + free(mb_obj); + mb_port_get_inst_counter_dec(); + ESP_LOGW(TAG, "Delete fake mb_base object."); + return ret; +} +#endif + +#if (MB_MASTER_ASCII_ENABLED || MB_MASTER_RTU_ENABLED) + +typedef struct _port_serial_opts mb_serial_opts_t; + +mb_err_enum_t mb_stub_serial_create(mb_serial_opts_t *ser_opts, void **in_out_obj) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_obj), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mb_object_t *mb_obj = NULL; + mb_obj = (mb_object_t *)calloc(1, sizeof(mb_object_t)); + MB_GOTO_ON_FALSE((mb_obj), MB_EILLSTATE, error, TAG, "no mem for mb master instance."); + CRITICAL_SECTION_INIT(mb_obj->base.lock); + ESP_LOGW(TAG, "Create fake mb_base object."); + mb_obj->base.delete = mb_delete; + mb_obj->base.enable = mb_enable; + mb_obj->base.disable = mb_disable; + mb_obj->base.poll = mb_poll; + mb_obj->base.set_dest_addr = mb_set_dest_addr; + mb_obj->base.get_dest_addr = mb_get_dest_addr; + mb_obj->base.set_send_len = mb_set_pdu_send_length; + mb_obj->base.get_send_len = mb_get_pdu_send_length; + mb_obj->base.get_send_buf = mb_get_pdu_send_buf; + mb_obj->base.descr.parent = *in_out_obj; + mb_obj->base.descr.is_master = true; + mb_obj->base.descr.obj_name = (char *)TAG; + mb_obj->base.descr.inst_index = mb_port_get_inst_counter_inc(); + *in_out_obj = (void *)mb_obj; + return MB_ENOERR; + +error: + CRITICAL_SECTION_CLOSE(mb_obj->base.lock); + free(mb_obj); + mb_port_get_inst_counter_dec(); + ESP_LOGW(TAG, "Delete fake mb_base object."); + return ret; +} + +#endif + +mb_err_enum_t mb_delete(mb_base_t *inst) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + CRITICAL_SECTION_CLOSE(mb_obj->base.lock); + free(mb_obj); + mb_port_get_inst_counter_dec(); + ESP_LOGW(TAG, "Delete fake mb_base object."); + return MB_ENOERR; +} + +mb_err_enum_t mb_enable(mb_base_t *inst) +{ + ESP_LOGW(TAG, "Enable fake mb_base object."); + mb_err_enum_t status = MB_ENOERR; + return status; +} + +mb_err_enum_t mb_disable(mb_base_t *inst) +{ + mb_err_enum_t status = MB_ENOERR; + ESP_LOGW(TAG, "Disable fake mb_base object."); + return status; +} + +static void mb_get_pdu_send_buf(mb_base_t *inst, uint8_t **pbuf) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + if (pbuf) { + *pbuf = mb_obj->snd_buf; + } +} + +// __attribute__((unused)) +// static void mb_get_pdu_recv_buf(mb_base_t *inst, uint8_t **pbuf) +// { +// //mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); +// } + +static void mb_set_pdu_send_length(mb_base_t *inst, uint16_t length) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + mb_obj->pdu_snd_len = length; +} + +__attribute__((unused)) +static uint16_t mb_get_pdu_send_length(mb_base_t *inst) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + return mb_obj->pdu_snd_len; +} + +static void mb_set_dest_addr(mb_base_t *inst, uint8_t dest_addr) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + mb_obj->dst_addr = dest_addr; +} + +static uint8_t mb_get_dest_addr(mb_base_t *inst) +{ + mb_object_t *mb_obj = __containerof(inst, mb_object_t, base); + return mb_obj->dst_addr; +} + +mb_err_enum_t mb_poll(mb_base_t *inst) +{ + mb_err_enum_t status = MB_ENOERR; + ESP_LOGW(TAG, "Poll function called of fake mb_base object."); + vTaskDelay(1); + return status; +} + +#endif diff --git a/test_apps/unit_tests/test_stubs/src/mb_transport_stub.c b/test_apps/unit_tests/test_stubs/src/mb_transport_stub.c new file mode 100644 index 0000000..e96f573 --- /dev/null +++ b/test_apps/unit_tests/test_stubs/src/mb_transport_stub.c @@ -0,0 +1,300 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "rtu_transport.h" +#include "port_serial_common.h" +#include "port_common.h" + +#include "mb_config.h" + +#if (CONFIG_FMB_COMM_MODE_RTU_EN) + +static const char *TAG = "mb_transp.rtu_master"; + +typedef struct +{ + mb_trans_base_t base; + mb_port_base_t *port_obj; + uint8_t snd_buf[MB_RTU_SER_PDU_SIZE_MAX]; + uint8_t rcv_buf[MB_RTU_SER_PDU_SIZE_MAX]; + uint16_t snd_pdu_len; + uint8_t *snd_buf_cur; + uint16_t snd_buf_cnt; + uint16_t rcv_buf_pos; + bool frame_is_broadcast; + volatile mb_timer_mode_enum_t cur_timer_mode; + mb_rtu_state_enum_t state; +} mbm_rtu_transp_t; + +mb_err_enum_t mbm_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst); +static void mbm_rtu_transp_start(mb_trans_base_t *inst); +static void mbm_rtu_transp_stop(mb_trans_base_t *inst); +static mb_err_enum_t mbm_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *rcv_addr_buf, uint8_t **frame_ptr_buf, uint16_t *len_buf); +static mb_err_enum_t mbm_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t len); +static bool mbm_rtu_transp_rcv_fsm(mb_trans_base_t *inst); +static bool mbm_rtu_transp_snd_fsm(mb_trans_base_t *inst); +static bool mbm_rtu_transp_timer_expired(void *inst); +static void mbm_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +static void mbm_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf); +// static uint16_t mbm_rtu_transp_get_snd_len(mb_trans_base_t *inst); +static void mbm_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len); +static bool mbm_rtu_transp_rq_is_bcast(mb_trans_base_t *inst); +bool mbm_rtu_transp_delete(mb_trans_base_t *inst); + +mb_err_enum_t mbm_rtu_transp_create(mb_serial_opts_t *ser_opts, void **in_out_inst) +{ + MB_RETURN_ON_FALSE((ser_opts && in_out_inst), MB_EINVAL, TAG, "invalid options for the instance."); + mb_err_enum_t ret = MB_ENOERR; + mbm_rtu_transp_t *transp = NULL; + transp = (mbm_rtu_transp_t *)calloc(1, sizeof(mbm_rtu_transp_t)); + MB_RETURN_ON_FALSE(transp, MB_EILLSTATE, TAG, "no mem for %s instance.", TAG); + CRITICAL_SECTION_INIT(transp->base.lock); + CRITICAL_SECTION_LOCK(transp->base.lock); + transp->base.frm_rcv = mbm_rtu_transp_receive; + transp->base.frm_send = mbm_rtu_transp_send; + transp->base.frm_start = mbm_rtu_transp_start; + transp->base.frm_stop = mbm_rtu_transp_stop; + transp->base.get_rx_frm = mbm_rtu_transp_get_rcv_buf; + transp->base.get_tx_frm = mbm_rtu_transp_get_snd_buf; + transp->base.frm_delete = mbm_rtu_transp_delete; + transp->base.frm_is_bcast = mbm_rtu_transp_rq_is_bcast; + // Copy parent object descriptor + transp->base.descr = ((mb_port_base_t *)*in_out_inst)->descr; + transp->base.descr.obj_name = (char *)TAG; + mb_port_base_t *port_obj = (mb_port_base_t *)*in_out_inst; + ret = mb_port_ser_create(ser_opts, &port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "serial port creation, err: %d", ret); + ret = mb_port_timer_create(port_obj, MB_RTU_GET_T35_VAL(ser_opts->baudrate)); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "timer port creation, err: %d", ret); + // Override default response time if defined + if (ser_opts->response_tout_ms) { + mb_port_timer_set_response_time(port_obj, ser_opts->response_tout_ms); + } + ret = mb_port_event_create(port_obj); + MB_GOTO_ON_FALSE((ret == MB_ENOERR), MB_EILLSTATE, error, TAG, "event port creation, err: %d", ret); + transp->base.port_obj = port_obj; + // Set callback function pointer for the timer + port_obj->cb.tmr_expired = mbm_rtu_transp_timer_expired; + port_obj->cb.tx_empty = NULL; + port_obj->cb.byte_rcvd = NULL; + port_obj->arg = (void *)transp; + transp->port_obj = port_obj; // register the created port object + *in_out_inst = &(transp->base); + ESP_LOGD(TAG, "created %s object @%p", TAG, transp); + CRITICAL_SECTION_UNLOCK(transp->base.lock); + return MB_ENOERR; + +error: + if (port_obj->timer_obj) { + mb_port_timer_delete(port_obj); + } + if (port_obj->event_obj) { + mb_port_event_delete(port_obj); + } + if (port_obj) { + mb_port_ser_delete(port_obj); + } + CRITICAL_SECTION_CLOSE(transp->base.lock); + free(transp); + return ret; +} + +bool mbm_rtu_transp_delete(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + mb_port_ser_delete(transp->base.port_obj); + mb_port_timer_delete(transp->base.port_obj); + mb_port_event_delete(transp->base.port_obj); + CRITICAL_SECTION_CLOSE(inst->lock); + free(transp); + return true; +} + +static void mbm_rtu_transp_start(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + transp->state = MB_RTU_STATE_INIT; + CRITICAL_SECTION(inst->lock) { + mb_port_ser_enable(inst->port_obj); + mb_port_timer_enable(inst->port_obj); + }; + /* No special startup required for RTU. */ + (void)mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); +} + +static void mbm_rtu_transp_stop(mb_trans_base_t *inst) +{ + CRITICAL_SECTION(inst->lock) { + mb_port_ser_disable(inst->port_obj); + mb_port_timer_disable(inst->port_obj); + }; +} + +static mb_err_enum_t mbm_rtu_transp_receive(mb_trans_base_t *inst, uint8_t *prcv_addr, uint8_t **ppframe_buf, uint16_t *pbuf_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + + if (!pbuf_len || !prcv_addr || !ppframe_buf || !pbuf_len) { + return MB_EIO; + } + + mb_err_enum_t status = MB_ENOERR; + + uint8_t *pbuf = (uint8_t *)transp->rcv_buf; + uint16_t length = *pbuf_len; + + if (mb_port_ser_recv_data(inst->port_obj, &pbuf, &length) == false) { + *pbuf_len = 0; + return MB_EPORTERR; + } + + assert(length < MB_RTU_SER_PDU_SIZE_MAX); + assert(pbuf); + + /* Check length and CRC checksum */ + if ((length >= MB_RTU_SER_PDU_SIZE_MIN) + && (mb_crc16((uint8_t *)pbuf, length) == 0)) { + /* Save the address field. All frames are passed to the upper layed + * and the decision if a frame is used is done there. + */ + *prcv_addr = pbuf[MB_SER_PDU_ADDR_OFF]; + + /* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus + * size of address field and CRC checksum. + */ + *pbuf_len = (uint16_t)(length - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC); + transp->rcv_buf_pos = length; + + /* Return the start of the Modbus PDU to the caller. */ + *ppframe_buf = (uint8_t *)&pbuf[MB_SER_PDU_PDU_OFF]; + } else { + status = MB_EIO; + } + return status; +} + +static mb_err_enum_t mbm_rtu_transp_send(mb_trans_base_t *inst, uint8_t slv_addr, const uint8_t *frame_ptr, uint16_t frame_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + mb_err_enum_t status = MB_ENOERR; + uint16_t crc16 = 0; + + if (slv_addr > MB_MASTER_TOTAL_SLAVE_NUM) { + return MB_EINVAL; + } + + if (frame_ptr && frame_len) { + /* First byte before the Modbus-PDU is the slave address. */ + transp->snd_buf_cur = (uint8_t *)frame_ptr - 1; + transp->snd_buf_cnt = 1; + + /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */ + transp->snd_buf_cur[MB_SER_PDU_ADDR_OFF] = slv_addr; + transp->snd_buf_cnt += frame_len; + /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */ + crc16 = mb_crc16((uint8_t *) transp->snd_buf_cur, transp->snd_buf_cnt); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 & 0xFF); + transp->snd_buf_cur[transp->snd_buf_cnt++] = (uint8_t)(crc16 >> 8); + + bool ret = mb_port_ser_send_data(inst->port_obj, (uint8_t *)transp->snd_buf_cur, transp->snd_buf_cnt); + if (!ret) { + return MB_EPORTERR; + } + transp->frame_is_broadcast = (slv_addr == MB_ADDRESS_BROADCAST) ? true : false; + // If the frame is broadcast, master will enable timer of convert delay, + // else master will enable timer of respond timeout. */ + if (transp->frame_is_broadcast) { + mb_port_timer_convert_delay_enable(transp->base.port_obj); + } else { + mb_port_timer_respond_timeout_enable(transp->base.port_obj); + } + + } else { + status = MB_EIO; + } + return status; +} + +__attribute__((unused)) +static bool mbm_rtu_transp_rcv_fsm(mb_trans_base_t *inst) +{ + return false; +} + +__attribute__((unused)) +static bool mbm_rtu_transp_snd_fsm(mb_trans_base_t *inst) +{ + return false; +} + + +static bool mbm_rtu_transp_timer_expired(void *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + + bool need_poll = false; + mb_timer_mode_enum_t timer_mode = mb_port_get_cur_timer_mode(transp->base.port_obj); + + mb_port_timer_disable(transp->base.port_obj); + + switch(timer_mode) { + case MB_TMODE_T35: + //need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + //ESP_EARLY_LOGD(TAG, "%p:EV_READY", transp->base.descr.parent); + break; + + case MB_TMODE_RESPOND_TIMEOUT: + mb_port_event_set_err_type(transp->base.port_obj, EV_ERROR_RESPOND_TIMEOUT); + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_ERROR_PROCESS)); + ESP_EARLY_LOGW(TAG, "%p:EV_ERROR_RESPOND_TIMEOUT", transp->base.descr.parent); + break; + + case MB_TMODE_CONVERT_DELAY: + /* If timer mode is convert delay, the master event then turns EV_MASTER_EXECUTE status. */ + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_EXECUTE)); + ESP_EARLY_LOGD(TAG, "%p:MB_TMODE_CONVERT_DELAY", transp->base.descr.parent); + break; + + default: + need_poll = mb_port_event_post(transp->base.port_obj, EVENT(EV_READY)); + break; + } + + return need_poll; +} + +static void mbm_rtu_transp_get_rcv_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->rcv_buf[MB_PDU_FUNC_OFF]; + } +} + +static void mbm_rtu_transp_get_snd_buf(mb_trans_base_t *inst, uint8_t **frame_ptr_buf) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + *frame_ptr_buf = (uint8_t *)&transp->snd_buf[MB_RTU_SER_PDU_PDU_OFF]; + } +} + +__attribute__((unused)) +static void mbm_rtu_transp_set_snd_len(mb_trans_base_t *inst, uint16_t snd_pdu_len) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + CRITICAL_SECTION(inst->lock) { + transp->snd_buf_cnt = snd_pdu_len; + } +} + +static bool mbm_rtu_transp_rq_is_bcast(mb_trans_base_t *inst) +{ + mbm_rtu_transp_t *transp = __containerof(inst, mbm_rtu_transp_t, base); + return transp->frame_is_broadcast; +} + +#endif diff --git a/tools/ignore_build_warnings.txt b/tools/ignore_build_warnings.txt new file mode 100644 index 0000000..fe3f6c1 --- /dev/null +++ b/tools/ignore_build_warnings.txt @@ -0,0 +1,16 @@ +library/error\.o +/.*error\S*\.o +.*error.*\.c\.obj +.*error.*\.c +.*error.*\.cpp\.obj +.*error.*\.cxx\.obj +.*error.*\.cc\.obj +-Werror +error\.d +/.*error\S*.d +reassigning to symbol +changes choice state +crosstool_version_check\.cmake +CryptographyDeprecationWarning +Warning: \d+/\d+ app partitions are too small for binary +CMake Deprecation Warning at main/lib/tinyxml2/CMakeLists\.txt:11 \(cmake_policy\) diff --git a/tools/robot/ModbusSupport.py b/tools/robot/ModbusSupport.py new file mode 100644 index 0000000..4be31f0 --- /dev/null +++ b/tools/robot/ModbusSupport.py @@ -0,0 +1,442 @@ +import struct +from scapy.packet import Packet, Raw +from scapy.fields import ShortField, XShortField, ByteField, XByteField, StrLenField, \ + FieldListField, ByteEnumField, BitFieldLenField, ConditionalField + +# The below classes override the functionality of original scapy modbus module +# to workaround some dissection issues with modbus packets and do explicit dissection of PDA +# based on function code from payload for request, exception and response frames. + +modbus_exceptions = { 0: "Undefined", + 1: "Illegal function", + 2: "Illegal data address", + 3: "Illegal data value", + 4: "Slave device failure", + 5: "Acknowledge", + 6: "Slave device busy", + 8: "Memory parity error", + 10: "Gateway path unavailable", + 11: "Gateway target device failed to respond"} + +# The common CRC16 checksum calculation method for Modbus Serial RTU frames +def mb_crc(frame:Raw, length) -> int: + crc = 0xFFFF + for n in range(length): + crc ^= (frame[n]) + for i in range(8): + if crc & 1: + crc >>= 1 + crc ^= 0xA001 + else: + crc >>= 1 + return crc + +# Modbus MBAP basic class +class ModbusMBAP(Packet): + name = "Modbus TCP" + fields_desc = [ ShortField("transId", 0), + ShortField("protoId", 0), + ShortField("len", 6), + XByteField("UnitId", 247), + ] + +# Can be used to replace all Modbus read +class ModbusPDU_Read_Generic(Packet): + name = "Read Generic" + fields_desc = [ XByteField("funcCode", 0x01), + XShortField("startAddr", 0x0000), + XShortField("quantity", 0x0001)] + +# 0x01 - Read Coils +class ModbusPDU01_Read_Coils(Packet): + name = "Read Coils Request" + fields_desc = [ XByteField("funcCode", 0x01), + # 0x0000 to 0xFFFF + XShortField("startAddr", 0x0000), + XShortField("quantity", 0x0001)] + +class ModbusPDU01_Read_Coils_Answer(Packet): + name = "Read Coils Answer" + fields_desc = [ XByteField("funcCode", 0x01), + BitFieldLenField("byteCount", None, 8, count_of="coilStatus"), + FieldListField("coilStatus", [0x00], ByteField("",0x00), count_from = lambda pkt: pkt.byteCount) ] + +class ModbusPDU01_Read_Coils_Exception(Packet): + name = "Read Coils Exception" + fields_desc = [ XByteField("funcCode", 0x81), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +# 0x02 - Read Discrete Inputs +class ModbusPDU02_Read_Discrete_Inputs(Packet): + name = "Read Discrete Inputs" + fields_desc = [ XByteField("funcCode", 0x02), + XShortField("startAddr", 0x0000), + XShortField("quantity", 0x0001)] + +class ModbusPDU02_Read_Discrete_Inputs_Answer(Packet): + name = "Read Discrete Inputs Answer" + fields_desc = [ XByteField("funcCode", 0x02), + BitFieldLenField("byteCount", None, 8, count_of="inputStatus"), + FieldListField("inputStatus", [0x00], ByteField("",0x00), count_from = lambda pkt: pkt.byteCount) ] + +class ModbusPDU02_Read_Discrete_Inputs_Exception(Packet): + name = "Read Discrete Inputs Exception" + fields_desc = [ XByteField("funcCode", 0x82), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +# 0x03 - Read Holding Registers +class ModbusPDU03_Read_Holding_Registers(Packet): + name = "Read Holding Registers" + fields_desc = [ XByteField("funcCode", 0x03), + XShortField("startAddr", 0x0001), + XShortField("quantity", 0x0002)] + +class ModbusPDU03_Read_Holding_Registers_Answer(Packet): + name = "Read Holding Registers Answer" + fields_desc = [ XByteField("funcCode", 0x03), + BitFieldLenField("byteCount", None, 8, count_of="registerVal"), + FieldListField("registerVal", [0x0000], ShortField("",0x0000), count_from = lambda pkt: pkt.byteCount)] + +class ModbusPDU03_Read_Holding_Registers_Exception(Packet): + name = "Read Holding Registers Exception" + fields_desc = [ XByteField("funcCode", 0x83), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +# 0x04 - Read Input Registers +class ModbusPDU04_Read_Input_Registers(Packet): + name = "Read Input Registers" + fields_desc = [ XByteField("funcCode", 0x04), + XShortField("startAddr", 0x0000), + XShortField("quantity", 0x0001)] + +class ModbusPDU04_Read_Input_Registers_Answer(Packet): + name = "Read Input Registers Response" + fields_desc = [XByteField("funcCode", 0x04), + BitFieldLenField("byteCount", None, 8, + count_of="registerVal", + adjust=lambda pkt, x: x * 2), + FieldListField("registerVal", [0x0000], + ShortField("", 0x0000), + count_from=lambda pkt: pkt.byteCount)] + +class ModbusPDU04_Read_Input_Registers_Exception(Packet): + name = "Read Input Registers Exception" + fields_desc = [ XByteField("funcCode", 0x84), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +# 0x05 - Write Single Coil +class ModbusPDU05_Write_Single_Coil(Packet): + name = "Write Single Coil" + fields_desc = [ XByteField("funcCode", 0x05), + XShortField("outputAddr", 0x0000), + XShortField("outputValue", 0x0000)] + +class ModbusPDU05_Write_Single_Coil_Answer(Packet): + name = "Write Single Coil" + fields_desc = [ XByteField("funcCode", 0x05), + XShortField("outputAddr", 0x0000), + XShortField("outputValue", 0x0000)] + +class ModbusPDU05_Write_Single_Coil_Exception(Packet): + name = "Write Single Coil Exception" + fields_desc = [ XByteField("funcCode", 0x85), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +# 0x06 - Write Single Register +class ModbusPDU06_Write_Single_Register(Packet): + name = "Write Single Register" + fields_desc = [ XByteField("funcCode", 0x06), + XShortField("registerAddr", 0x0000), + XShortField("registerValue", 0x0000)] + +class ModbusPDU06_Write_Single_Register_Answer(Packet): + name = "Write Single Register Answer" + fields_desc = [ XByteField("funcCode", 0x06), + XShortField("registerAddr", 0x0000), + XShortField("registerValue", 0x0000)] + +class ModbusPDU06_Write_Single_Register_Exception(Packet): + name = "Write Single Register Exception" + fields_desc = [ XByteField("funcCode", 0x86), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +# 0x07 - Read Exception Status (Serial Line Only) +class ModbusPDU07_Read_Exception_Status(Packet): + name = "Read Exception Status" + fields_desc = [ XByteField("funcCode", 0x07)] + +class ModbusPDU07_Read_Exception_Status_Answer(Packet): + name = "Read Exception Status Answer" + fields_desc = [ XByteField("funcCode", 0x07), + XByteField("startAddr", 0x00)] + +class ModbusPDU07_Read_Exception_Status_Exception(Packet): + name = "Read Exception Status Exception" + fields_desc = [ XByteField("funcCode", 0x87), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +# 0x0F - Write Multiple Coils +class ModbusPDU0F_Write_Multiple_Coils(Packet): + name = "Write Multiple Coils" + fields_desc = [ XByteField("funcCode", 0x0F), + XShortField("startAddr", 0x0000), + XShortField("quantityOutput", 0x0001), + BitFieldLenField("byteCount", None, 8, count_of="outputsValue", adjust=lambda pkt,x:x), + FieldListField("outputsValue", [0x00], XByteField("", 0x00), count_from = lambda pkt: pkt.byteCount)] + +class ModbusPDU0F_Write_Multiple_Coils_Answer(Packet): + name = "Write Multiple Coils Answer" + fields_desc = [ XByteField("funcCode", 0x0F), + XShortField("startAddr", 0x0000), + XShortField("quantityOutput", 0x0001)] + +class ModbusPDU0F_Write_Multiple_Coils_Exception(Packet): + name = "Write Multiple Coils Exception" + fields_desc = [ XByteField("funcCode", 0x8F), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +class ModbusPDU10_Write_Multiple_Registers(Packet): + name = "Write Multiple Registers" + fields_desc = [XByteField("funcCode", 0x10), + XShortField("startAddr", 0x0000), + BitFieldLenField("quantityRegisters", None, 16, + count_of="outputsValue"), + BitFieldLenField("byteCount", None, 8, + count_of="outputsValue", + adjust=lambda pkt, x: x * 2), + FieldListField("outputsValue", [0x0000], + XShortField("", 0x0000), + count_from=lambda pkt: pkt.byteCount)] + +class ModbusPDU10_Write_Multiple_Registers_Serial(ModbusPDU10_Write_Multiple_Registers): + name = "Write Multiple Registers Serial" + _crc: int = 0 + + def get_crc(self) -> int: + return self._crc + + def post_build(self, p, pay): + self._crc = 0 + if self.outputsValue is not None and len(self.outputsValue) > 0: + self._crc = mb_crc(p, len(p)) + p = p + struct.pack("= 2: + if mb_crc(payload, len(payload)) == 0: + #if self._crc == mb_crc(payload[:-2], len(payload)-2): + self._crc = struct.unpack("0), + ConditionalField(XByteField("runIdicatorStatus", 0x00), lambda pkt: pkt.byteCount>0)] + +class ModbusPDU11_Report_Slave_Id_Exception(Packet): + name = "Report Slave Id Exception" + fields_desc = [ XByteField("funcCode", 0x91), + ByteEnumField("exceptCode", 1, modbus_exceptions)] + +class ModbusADU_Request(ModbusMBAP): + name = "ModbusADU Request" + _mb_exception: modbus_exceptions = 0 + fields_desc = [ + XShortField("transId", 0x0000), # needs to be unique + XShortField("protoId", 0x0000), # needs to be zero (Modbus) + XShortField("len", None), # is calculated with payload + XByteField("unitId", 0x00)] # 0xFF or 0x00 should be used for Modbus over TCP/IP + + def mb_get_last_exception(self): + return _mb_exception + + # Dissects packets + def guess_payload_class(self, payload): + funcCode = int(payload[0]) + #print(f'Request guess payload class func: {funcCode}') + self._mb_exception = 0 + + if funcCode == 0x01: + return ModbusPDU01_Read_Coils + elif funcCode == 0x81: + self._mb_exception = int(payload[1]) + return ModbusPDU01_Read_Coils_Exception + + elif funcCode == 0x02: + return ModbusPDU02_Read_Discrete_Inputs + elif funcCode == 0x82: + self._mb_exception = int(payload[1]) + return ModbusPDU02_Read_Discrete_Inputs_Exception + + elif funcCode == 0x03: + return ModbusPDU03_Read_Holding_Registers + elif funcCode == 0x83: + self._mb_exception = int(payload[1]) + return ModbusPDU03_Read_Holding_Registers_Exception + + elif funcCode == 0x04: + return ModbusPDU04_Read_Input_Registers + elif funcCode == 0x84: + self._mb_exception = int(payload[1]) + return ModbusPDU04_Read_Input_Registers_Exception + + elif funcCode == 0x05: + return ModbusPDU05_Write_Single_Coil + elif funcCode == 0x85: + self._mb_exception = int(payload[1]) + return ModbusPDU05_Write_Single_Coil_Exception + + elif funcCode == 0x06: + return ModbusPDU06_Write_Single_Register + elif funcCode == 0x86: + self._mb_exception = int(payload[1]) + return ModbusPDU06_Write_Single_Register_Exception + + elif funcCode == 0x07: + return ModbusPDU07_Read_Exception_Status + elif funcCode == 0x87: + self._mb_exception = int(payload[1]) + return ModbusPDU07_Read_Exception_Status_Exception + + elif funcCode == 0x0F: + return ModbusPDU0F_Write_Multiple_Coils + elif funcCode == 0x8F: + self._mb_exception = int(payload[1]) + return ModbusPDU0F_Write_Multiple_Coils_Exception + + elif funcCode == 0x11: + return ModbusPDU11_Report_Slave_Id + elif funcCode == 0x91: + self._mb_exception = int(payload[1]) + return ModbusPDU11_Report_Slave_Id_Exception + + elif funcCode == 0x10: + #return ModbusPDU10_Write_Multiple_Registers + return ModbusPDU10_Write_Multiple_Registers_Serial.guess_payload_class(self, payload) + elif funcCode == 0x90: + self._mb_exception = int(payload[1]) + return ModbusPDU10_Write_Multiple_Registers_Exception + + else: + return Packet.guess_payload_class(self, payload) + + def post_build(self, p, pay): + if self.len is None: + l = len(pay) + 1 #+len(p) + p = p[:4]+struct.pack("!H", l) + p[6:] + return p+pay + + def my_show(self, p): + for f in p.fields_desc: + fvalue = p.getfieldval(f.name) + reprval = f.i2repr(p,fvalue) + return "%s" % (reprval) + +# If we know the packet is an Modbus answer, we can dissect it with +# ModbusADU_Response(str(pkt)) +# Scapy will dissect it on it's own if the TCP stream is available +class ModbusADU_Response(ModbusMBAP): + name = "ModbusADU Response" + _mb_exception: modbus_exceptions = 0 + + fields_desc = [ + XShortField("transId", 0x0000), # needs to be unique + XShortField("protoId", 0x0000), # needs to be zero (Modbus) + XShortField("len", None), # is calculated with payload + XByteField("unitId", 0x01)] # 0xFF or 0x00 should be used for Modbus over TCP/IP + + def mb_get_last_exception(self): + return _mb_exception + + # Dissects packets + def guess_payload_class(self, payload): + funcCode = int(payload[0]) + + self._mb_exception = 0 + + if funcCode == 0x01: + return ModbusPDU01_Read_Coils_Answer + elif funcCode == 0x81: + self._mb_exception = int(payload[1]) + return ModbusPDU01_Read_Coils_Exception + + elif funcCode == 0x02: + return ModbusPDU02_Read_Discrete_Inputs_Answer + elif funcCode == 0x82: + self._mb_exception = int(payload[1]) + return ModbusPDU02_Read_Discrete_Inputs_Exception + + elif funcCode == 0x03: + return ModbusPDU03_Read_Holding_Registers_Answer + elif funcCode == 0x83: + self._mb_exception = int(payload[1]) + return ModbusPDU03_Read_Holding_Registers_Exception + + elif funcCode == 0x04: + return ModbusPDU04_Read_Input_Registers_Answer + elif funcCode == 0x84: + self._mb_exception = int(payload[1]) + return ModbusPDU04_Read_Input_Registers_Exception + + elif funcCode == 0x05: + return ModbusPDU05_Write_Single_Coil_Answer + elif funcCode == 0x85: + self._mb_exception = int(payload[1]) + return ModbusPDU05_Write_Single_Coil_Exception + + elif funcCode == 0x06: + return ModbusPDU06_Write_Single_Register_Answer + elif funcCode == 0x86: + self._mb_exception = int(payload[1]) + return ModbusPDU06_Write_Single_Register_Exception + + elif funcCode == 0x07: + return ModbusPDU07_Read_Exception_Status_Answer + elif funcCode == 0x87: + self._mb_exception = int(payload[1]) + return ModbusPDU07_Read_Exception_Status_Exception + + elif funcCode == 0x0F: + return ModbusPDU0F_Write_Multiple_Coils_Answer + elif funcCode == 0x8F: + self._mb_exception = int(payload[1]) + return ModbusPDU0F_Write_Multiple_Coils_Exception + + elif funcCode == 0x10: + return ModbusPDU10_Write_Multiple_Registers_Answer + elif funcCode == 0x90: + self._mb_exception = int(payload[1]) + return ModbusPDU10_Write_Multiple_Registers_Exception + + elif funcCode == 0x11: + return ModbusPDU11_Report_Slave_Id_Answer + elif funcCode == 0x91: + self._mb_exception = int(payload[1]) + return ModbusPDU11_Report_Slave_Id_Exception + + else: + return Packet.guess_payload_class(self, payload) \ No newline at end of file diff --git a/tools/robot/ModbusTestLib.py b/tools/robot/ModbusTestLib.py new file mode 100644 index 0000000..2675080 --- /dev/null +++ b/tools/robot/ModbusTestLib.py @@ -0,0 +1,482 @@ +#!/usr/bin/python + +import socket +import random +import binascii +import os +from typing import Optional, Any, Tuple + +from scapy.utils import wrpcap +from scapy.packet import Packet, Raw +from scapy.supersocket import StreamSocket +from scapy.layers.inet import Ether, IP, TCP +from scapy.config import conf +from scapy.error import Scapy_Exception + +from robot.api.deco import keyword, library +from robot.api.logger import info, debug, trace, console + +from ModbusSupport import modbus_exceptions, ModbusADU_Request, ModbusADU_Response, ModbusPDU03_Read_Holding_Registers, ModbusPDU10_Write_Multiple_Registers, \ + ModbusPDU04_Read_Input_Registers, ModbusPDU01_Read_Coils, ModbusPDU0F_Write_Multiple_Coils, ModbusPDU02_Read_Discrete_Inputs, ModbusPDU06_Write_Single_Register + +# Disable debugging of dissector, and set default padding for scapy configuration class +# to workaround issues under robot framework +conf.debug_dissector = False +conf.padding = 1 + +# The default values for self test of the library +MB_DEF_SERVER_IP = '127.0.0.1' +MB_DEF_PORT = 1502 +MB_DEF_TRANS_ID = 0x0000 +MB_DEF_FUNC_HOLDING_READ = 0x03 +MB_DEF_FUNC_HOLDING_WRITE = 0x10 +MB_DEF_FUNC_INPUT_READ = 0x04 +MB_DEF_FUNC_COILS_READ = 0x01 +MB_DEF_FUNC_COILS_WRITE = 0x0F +MB_DEF_QUANTITY = 1 +MB_DEF_START_OFFS = 0x0001 +MB_DEF_REQ_TOUT = 5.0 + +MB_LOGGING_PATH = '.' + +# The constructed packets for self testing +TEST_PACKET_HOLDING_READ = 'ModbusADU_Request(transId=MB_DEF_TRANS_ID, unitId=0x01, protoId=0, len=6)/\ + ModbusPDU03_Read_Holding_Registers(funcCode=MB_DEF_FUNC_HOLDING_READ, startAddr=MB_DEF_START_OFFS, quantity=MB_DEF_QUANTITY)' +TEST_PACKET_HOLDING_WRITE = 'ModbusADU_Request(transId=MB_DEF_TRANS_ID, unitId=0x01, protoId=0)/\ + ModbusPDU10_Write_Multiple_Registers(funcCode=MB_DEF_FUNC_HOLDING_WRITE, startAddr=MB_DEF_START_OFFS, quantityRegisters=2, outputsValue=[0x1122, 0x3344])' +TEST_PACKET_INPUT_READ = 'ModbusADU_Request(transId=MB_DEF_TRANS_ID, unitId=0x01, protoId=0, len=6)/\ + ModbusPDU04_Read_Input_Registers(funcCode=MB_DEF_FUNC_INPUT_READ, startAddr=MB_DEF_START_OFFS, quantity=MB_DEF_QUANTITY)' +TEST_PACKET_COILS_READ = 'ModbusADU_Request(unitId=0x01, protoId=0)/\ + ModbusPDU01_Read_Coils(funcCode=MB_DEF_FUNC_COILS_READ, startAddr=MB_DEF_START_OFFS, quantity=MB_DEF_QUANTITY)' +TEST_PACKET_COILS_WRITE = 'ModbusADU_Request(unitId=0x01, protoId=0)/\ + ModbusPDU0F_Write_Multiple_Coils(funcCode=MB_DEF_FUNC_COILS_WRITE, startAddr=MB_DEF_START_OFFS, quantityOutput=MB_DEF_QUANTITY, outputsValue=[0xFF])' + +# The simplified version of custom Modbus Library to check robot framework +@library(scope='GLOBAL', version='2.0.0') +class ModbusTestLib: + ''' + ModbusTestLib class is the custom Modbus library for robot framework. + The test class for Modbus includes common functionality to receive and parse Modbus frames. + ''' + MB_EXCEPTION_MASK = 0x0080 + MB_EXCEPTION_FUNC_MASK = 0x007F + + def __init__(self, ip_address = MB_DEF_SERVER_IP, port = MB_DEF_PORT, timeout = MB_DEF_REQ_TOUT) -> None: + self._connection: StreamSocket = None + self.class_id = random.randint(0,100) # is to track of created instance number + self.node_port = port + self.node_address = ip_address + self.trans_id = 0x0001 + self.socket = None + self.host_ip = None + self.exception_message = None + self.exception = None + self.in_adu = None + self.in_pdu = None + self.resp_timeout = timeout + self.pcap_file_name = "{path}/{file}_{id}.{ext}".format(path=MB_LOGGING_PATH, file='mb_frames', ext='pcap', id=str(self.class_id)) + if os.path.isfile(self.pcap_file_name): + os.remove(self.pcap_file_name) + + @property + def connection(self) -> Optional[StreamSocket]: + # type: () -> Optional[Any] + if _connection is not None: + raise SystemError('No Connection established! Connect to server first!') + return self._connection + + def get_slave_ip(self) -> int: + # type: () -> int + if self.dut_slave_ip_address is None: + raise SystemError('Transaction is not initialized!') + return self.trans_id + + def get_host_ip(self): + # type: () -> Optional[Any] + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + s.connect(('', 12345)) # use random port + return s.getsockname()[0] + + def get_trans_id(self) -> int: + # type: () -> int + if self.trans_id is None: + raise SystemError('Transaction is not initialized!') + return self.trans_id + + def inc_trans_id(self) -> int: + # type: () -> int + if self.trans_id is None: + raise SystemError('Transaction field is incorrect!') + self.trans_id = self.trans_id + 1 + if self.trans_id > 65535: + self.trans_id = 1 + return self.trans_id + + def get_last_adu(self) -> Optional[Packet]: + # type: () -> Optional[Packet] + return self.in_adu + + def get_last_pdu(self) -> Any: + return self.in_pdu + + def mb_match_packet(self, pkt) -> bool: + # type: (Packet, int, bool) -> Optional[Packet] + return True if pkt.haslayer(ModbusADU_Response) else False + + # This function implements workaround for sr1 function which does not work reliable in some versions of scapy + def _req_send_recv(self, pkt, timeout=0, verbose=True) -> Optional[Packet]: + # type: (Packet, int, bool) -> Optional[Packet] + if self._connection is None: + raise ValueError("The connection is not active.") + packet = pkt.build() + try: + self._connection.send(packet) + ans = self._connection.sniff(filter=f"tcp and dst host {self.node_address} and dst port {self.node_port}", + prn=lambda x:x.sprintf("{IP:%IP.src% -> %IP.dst%\n}{Raw:%Raw.load%\n}"), + count=1, timeout=timeout) + return ans[0] if ans else None + + except Exception as exception: + raise Scapy_Exception(f"Send fail: {exception}") + + @keyword("Get Class Id") + def get_class_id(self) -> int: + # type: (Any) -> Optional[Packet] + """ + Return unique class ID for robot suit debugging. + Args: + None + Returns: + Class instance ID + """ + return self.class_id + + # Validation of the created packet + def _validate_packet(self, packet: Packet) -> None: + # type: (Packet) -> None + if not packet.haslayer(ModbusADU_Request) or packet[ModbusADU_Request].protoId != 0: + raise ValueError("Only Modbus TCP requests are allowed!") + if not packet[ModbusADU_Request].transId: + packet.transId = self.get_trans_id() + + @keyword("Create Request") + def create_request(self, packet_str) -> Packet: + # type: (Any, int) -> Optional[Packet] + """ + Create a Modbus packet based on the given string representation. + Args: + packet_str (str): A string representing the Modbus packet. + Returns: + ModbusADU_Request: The created Modbus packet. + Raises: + ValueError: If the packet creation fails. + """ + try: + packet: ModbusADU_Request = eval(packet_str) + self._validate_packet(packet) + print("Packet created: %s" % str(packet.summary())) + return packet + + except Exception as exception: + raise ValueError(f"Failed to create packet: {str(exception)}") + + # Connects to a target via TCP socket + @keyword("Connect") + def connect(self, ip_addr=MB_DEF_SERVER_IP, port=MB_DEF_PORT) -> StreamSocket: + # type: (Any, int) -> Optional[Any] + """ + Create a Modbus connection to target over socket stream. + Args: + ip_addr (str): A string representing the Modbus server address. + port: A server port to connect + Returns: + StreamSocket: The created Modbus socket. + Raises: + Scapy_Exception: If the packet creation fails. + """ + if (ip_addr is None and self.node_address is None): + print("Connection is not esteblished.") + raise ValueError('No parameters defined!') + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + print(f"Connect to server: {ip_addr}:{port}") + s.connect((ip_addr, port)) + self._connection = StreamSocket(s, basecls=ModbusADU_Response) + self.host_ip = self.get_host_ip() + print(f'Host IP address: {self.host_ip}') + self.node_address = ip_addr + self.node_port = port + self.socket = s + except Exception as exception: + self.node_address = None + self.node_port = None + self.socket + raise Scapy_Exception(f"Could not connect to socket: {exception}") + return self._connection + + @keyword("Disconnect") + def disconnect(self) -> None: + # type: () -> None + """ + The disconnect from socket method to work as robot framework keyword. + Args: + None + Returns: + None + Raises: + Scapy_Exception: If the connection close fail. + """ + if self._connection is not None: + info(f"Disconnect from server.") + try: + self._connection.close() + self._connection = None + self.socket = None + + except Exception as exception: + raise Scapy_Exception(f"Connection close fail, exception occurred: ({exception})") + + @keyword("Send Packet") + def send_packet_and_get_response(self, pkt, timeout=2, verbose=True) -> Optional[bytes]: + # type: (Packet, int, bool) -> Optional[bytes] + """ + Wrapped send and receive function used as the robot framework keyword. + Args: + pkt: A Modbus packet. + timeout: timeout to send the data + verbose: logging information + Returns: + bytes: The created Modbus socket as Raw bytes. + Raises: + Scapy_Exception: If the packet send or receive fail. + """ + try: + request: Packet = pkt + if self._connection is None: + print("Connection is not established") + self.connect(ip_addr=self.node_address, port=self.node_port) + response: Packet = self._req_send_recv(request[ModbusADU_Request], timeout=timeout, verbose=verbose) + #assert response is not None, "No respond from slave" + if request is None or response is None: + print("No response from slave.") + return None + print(f"Packet sent: {request[ModbusADU_Request].show(dump=True)}") + print(f"Packet get: {response[ModbusADU_Response].show(dump=True)}") + print(f"Answer bin: {bytes(response)}") + print(f"Answer hex: {binascii.hexlify(bytes(response)).decode('ascii')}") + # Mimic the whole Modbus frames correctly in the pcap + dport = 502 # self.node_port, override to default port to show the packets correctly under wireshark + if not request.haslayer(Ether): + pcap_out = Ether() + if not request.haslayer(IP): + pcap_out /= IP(dst=self.node_address, src=self.host_ip) + if not request.haslayer(TCP): + pcap_out /= TCP(dport=dport, sport=random.randint(37000,39000)) + pcap_out /= request + if not request.haslayer(Ether): + pcap_in = Ether() + if not request.haslayer(IP): + pcap_in /= IP(src=self.node_address, dst=self.host_ip) + if not request.haslayer(TCP): + pcap_in /= TCP(dport=dport, sport=random.randint(37000,39000)) + pcap_in /= response + # record the packets sent/received + wrpcap(self.pcap_file_name, bytes(pcap_out), append=True) + wrpcap(self.pcap_file_name, bytes(pcap_in), append=True) + self.inc_trans_id() + return bytes(response) if response else None + + except Exception as exception: + raise Scapy_Exception(f"Send data fail, exception occurred: ({exception})") + + @keyword("Translate Response") + def translate_response(self, pkt) -> Any: + # type: (bytes) -> Optional[Packet] + """ + Translates response received from server. Does dissection of the received packet. + Args: + pkt: A Modbus packet. + Returns: + bytes: The created Modbus socket as Raw bytes. + Raises: + Scapy_Exception: If the packet send or receive fail. + """ + try: + packet: Packet = ModbusADU_Response(pkt) + if packet is None: #or not packet.haslayer(ModbusADU_Response) + raise ValueError("Only Modbus TCP responses are allowed!") + print(f"Packet received: {packet.show(dump=True)}") + self.in_pdu, __ = packet.extract_padding(packet) + print(f"Test received: pdu: {type(self.in_pdu)} {self.in_pdu}, {bytes(self.in_pdu)}") + # workaround to use dissected packets under robot framework + # issue with scapy dissector on incomplete packets + if packet.haslayer(Raw): + print(f"Test workaround PDU: {bytes(packet[Raw].load)}") + self.in_pdu = packet.guess_payload_class(packet[Raw].load) + if (packet.funcCode & self.MB_EXCEPTION_MASK) and \ + hasattr(self.in_pdu, 'exceptCode') and \ + (self.in_pdu.exceptCode in modbus_exceptions): + self.exception = packet.exceptCode + self.exception_message = modbus_exceptions[self.exception] + self.in_adu = packet[ModbusADU_Response] + print(f'PDU: {self.in_pdu}') + return self.in_pdu + + except Exception as exception: + self.in_adu = None + self.in_pdu = None + raise Scapy_Exception(f"Parsing of response : ({exception})") + + def get_int(self, val) -> int: + # type: (Any) -> int + if isinstance(val, str): + return int(val) + elif isinstance(val, int): + return val + else: + raise ValueError("Invalid value type") + + @keyword("Check Response") + def check_response(self, pdu, expected_func) -> Tuple[int, str]: + # type: (Packet, str) -> Tuple[int, str] + """ + Check PDU frame from response. Check exception code + Args: + pdu: A Modbus PDU frame. + expected_func: timeout to send the data + Returns: + exception: The exception code from Modbus frame + exception_message: exception message + Raises: + ValueError: If the packet send or receive fail. + """ + assert pdu is not None and isinstance(pdu, Packet), "Incorrect pdu provided." + func_code: int = 0 + if isinstance(pdu, ModbusADU_Response): + print(f"PDU is ModbusADU_Response, funcCode: {pdu.funcCode}") + func_code = pdu.getfieldval('funcCode') + elif hasattr(pdu, 'funcCode') and isinstance(pdu.funcCode, XByteField): + print(f"Test PDU: type:{type(pdu)}, PDU:{pdu}, Func:{type(pdu.funcCode.i2repr(pdu, pdu.funcCode))}, {pdu.funcCode.i2repr(pdu, pdu.funcCode)}") + func_code = pdu.getfieldval(pdu, 'funcCode') + print(f"PDU has funcCode attribute, value: {pdu.funcCode} {func_code}") + else: + raise ValueError(f"Invalid PDU type or missing function code: {type(pdu)}, {type(pdu.funcCode)}") + print(f"func code: {type(func_code)} {func_code}") + if ((func_code & self.MB_EXCEPTION_MASK) and hasattr(pdu, 'exceptCode') and pdu.exceptCode): + self.exception = pdu.exceptCode + else: + self.exception = 0 + exp_func = self.get_int(expected_func) if isinstance(expected_func, str) else expected_func + assert ((func_code & self.MB_EXCEPTION_FUNC_MASK) == exp_func), f"Unexpected function code: {func_code & self.MB_EXCEPTION_FUNC_MASK}, {exp_func}" + self.exception_message = modbus_exceptions[self.exception] + print(f"MB exception: {self.exception}, {self.exception_message}") + return self.exception, self.exception_message + + @keyword("Check ADU") + def check_adu(self, adu_out, adu_in): + # type: (Packet, Packet) -> Optional[int] + """ + Check ADU frame fields. + Args: + adu_out: A Modbus ADU frame request. + adu_in: A Modbus ADU frame response. + Returns: + transId: the transaction ID, if the frames are correct, otherwise returns None + Raises: + ValueError: If the packet send or receive fail. + """ + assert adu_out is not None and adu_in is not None, "Incorrect adu frame provided." + try: + if hasattr(adu_out, 'TransId') and \ + hasattr(adu_in, 'TransId') and \ + hasattr(adu_out, 'protoId') and \ + hasattr(adu_in, 'protoId') and \ + adu_out.protoId == adu_in.protoId and \ + adu_out.TransId == adu_in.TransId: + return adu_out.TransId + return None + + except Exception as exception: + raise Scapy_Exception(f"Send data fail, exception occurred: ({exception})") + + @keyword("Get Bits From PDU") + def get_bits_from_pdu(self, pdu): + # type: (Packet) -> List[bool] + """ + Check PDU frame, extract bits (coils or discrete) from PDU. + Args: + pdu: A Modbus PDU frame. + Returns: + bits: The list of bits of boolean type. + Raises: + ValueError: If the packet send or receive fail. + """ + assert pdu is not None and isinstance(pdu, Packet) and \ + (hasattr(pdu, 'coilStatus') or hasattr(pdu, 'inputStatus')), "Incorrect pdu provided." + bits = [] + if hasattr(pdu, 'byteCount') and (pdu.byteCount >= 1): + data_bytes = bytes(pdu.coilStatus) if hasattr(pdu, 'coilStatus') else bytes(pdu.inputStatus) + bits = [(data_bytes[i//8] & 1 << i%8 != 0) for i in range(len(data_bytes) * 8)] + return bits + + # Self test function to debug the supported methods. Todo: remove later + def self_test(self) -> None: + # type: () -> None + self.connect(ip_addr=MB_DEF_SERVER_IP, port=MB_DEF_PORT) + packet = self.create_request(TEST_PACKET_HOLDING_READ) + print(f"Test: Packet created: {packet}") + response = self.send_packet_and_get_response(packet, timeout=1, verbose=0) + assert response and len(response) > 1, "No response from slave" + print(f"Test: received: {bytes(response)}") + pdu = self.translate_response(response) + if pdu is not None: + print(f"Register values: {pdu}, len:{len(pdu.registerVal)}") + print(f"PDU Exception: {self.check_response(pdu, packet.funcCode)}") + packet = self.create_request(TEST_PACKET_HOLDING_WRITE) + response = self.send_packet_and_get_response(packet, timeout=1, verbose=0) + assert response and len(response) > 1, "No response from slave" + print(f"Write response ack: {response} ") + if response is not None: + print(f"Test: received: {bytes(response)}") + pdu = self.translate_response(response) + exception = self.check_response(pdu, packet.funcCode) + if pdu is not None and not exception: + print(f"PDU Exception: {self.check_response(pdu, packet.funcCode)}") + packet = self.create_request(TEST_PACKET_INPUT_READ) + print(f"Test: input read packet: {packet}") + response = self.send_packet_and_get_response(packet, timeout=1, verbose=0) + assert response and len(response) > 1, "incorrect response" + print(f"Test: received: {bytes(response)}") + pdu = self.translate_response(response) + exception = self.check_response(pdu, packet.funcCode) + if pdu is not None and not exception: + print(f"Register values: {pdu}, len:{len(pdu.registerVal)}") + packet = self.create_request(TEST_PACKET_COILS_READ) + print(f"Test: read coils request: {packet}") + response = self.send_packet_and_get_response(packet, timeout=1, verbose=0) + assert response and len(response) > 1, "Incorrect coil read response" + print(f"Test: received: {bytes(response)}") + pdu = self.translate_response(response) + exception = self.check_response(pdu, packet.funcCode) + if pdu is not None and not exception: + print(f"Register values: {pdu}, len:{len(pdu.coilStatus)}") + packet = self.create_request(TEST_PACKET_COILS_WRITE) + print(f"Test: write coils request: {packet}") + response = self.send_packet_and_get_response(packet, timeout=1, verbose=0) + assert response and len(response) > 1, "Incorrect coil read response" + print(f"Test: received: {bytes(response)}") + pdu = self.translate_response(response) + exception = self.check_response(pdu, packet.funcCode) + if pdu is not None and not exception: + print(f"Register values: {pdu}, len: {pdu.quantityOutput}") + self.disconnect() + + + #################################################################### + #banner = "\nRobot custom Modbus library based on scapy framework\n" + +if __name__ == "__main__": + # interact(mydict=globals(), mybanner=banner) + test_lib = ModbusTestLib() + test_lib.self_test() diff --git a/tools/robot/ModbusTestSuite.resource b/tools/robot/ModbusTestSuite.resource new file mode 100644 index 0000000..1814c3e --- /dev/null +++ b/tools/robot/ModbusTestSuite.resource @@ -0,0 +1,242 @@ +*** Variables *** +${MODBUS_DEF_SERVER_IP} 127.0.0.1 +${MODBUS_DEF_PORT} 1502 +${PAR1} 1 +${PAR2} 2 + +${FUNC_WRITE_HOLDING_REGISTERS} ${0x10} +${FUNC_WRITE_HOLDING_REGISTER} ${0x06} +${FUNC_READ_HOLDING_REGISTERS} ${0x03} +${FUNC_READ_INPUT_REGISTERS} ${0x04} +${FUNC_READ_COILS} ${0x01} +${FUNC_WRITE_COILS} ${0x0F} +${FUNC_READ_DISCRETE_INPUTS} ${0x02} + +*** Settings *** +Library Collections +Library ModbusTestLib.py WITH NAME ModbusTestLib + +*** Keywords *** +Create Input Read Registers Request + [Arguments] ${uid} ${startAddr} ${quantity} + ${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0, len=6)/ModbusPDU04_Read_Input_Registers(funcCode=${FUNC_READ_INPUT_REGISTERS}, startAddr=${startAddr}, quantity=${quantity}) + RETURN ${packet} + +Create Holding Read Registers Request + [Arguments] ${uid} ${startAddr} ${quantity} + ${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0, len=6)/ModbusPDU03_Read_Holding_Registers(funcCode=${FUNC_READ_HOLDING_REGISTERS}, startAddr=${startAddr}, quantity=${quantity}) + RETURN ${packet} + +Create Holding Write Registers Request + [Arguments] ${uid} ${startAddr} ${quantity} ${data} + ${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0)/ModbusPDU10_Write_Multiple_Registers(funcCode=${FUNC_WRITE_HOLDING_REGISTERS}, startAddr=${startAddr}, quantityRegisters=${quantity}, outputsValue=${data}) + Log Packet: ${packet} + RETURN ${packet} + +Create Holding Write Register Request + [Arguments] ${uid} ${startAddr} ${data} + ${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0)/ModbusPDU06_Write_Single_Register(funcCode=${FUNC_WRITE_HOLDING_REGISTER}, registerAddr=${startAddr}, registerValue=${data}) + Log Packet: ${packet} + RETURN ${packet} + +Create Coils Read Request + [Arguments] ${uid} ${startAddr} ${quantity} + ${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0)/ModbusPDU01_Read_Coils(funcCode=${FUNC_READ_COILS}, startAddr=${startAddr}, quantity=${quantity}) + Log Packet: ${packet} + RETURN ${packet} + +Create Coils Write Request + [Arguments] ${uid} ${startAddr} ${quantity} ${coil_data} + ${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0)/ModbusPDU0F_Write_Multiple_Coils(funcCode=${FUNC_WRITE_COILS}, startAddr=${startAddr}, quantityOutput=${quantity}, outputsValue=${coil_data}) + Log Packet: ${packet} + RETURN ${packet} + +Create Discrete Read Request + [Arguments] ${uid} ${startAddr} ${quantity} + ${packet} = Create Request ModbusADU_Request(unitId=${uid}, protoId=0)/ModbusPDU02_Read_Discrete_Inputs(funcCode=${FUNC_READ_DISCRETE_INPUTS}, startAddr=${startAddr}, quantity=${quantity}) + Log Packet: ${packet} + RETURN ${packet} + +Read Input Registers + [Arguments] ${uid} ${start_addr} ${quantity} ${exception_expected} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + Log Read Input Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity} + ${req} = Create Input Read Registers Request ${uid} ${start_addr} ${quantity} + #Create Connection ${server} ${port} + ${response_frame} = Send Packet And Get Response ${req} + Should Not Be Empty ${response_frame} + ${packet} = Translate Response ${response_frame} + Should Be Equal As Integers ${req.transId} ${packet.transId} + ${exception} ${exp_message} = Check Response ${packet} ${req.funcCode} + Should Be Equal As Integers ${exception} ${exception_expected} + Log exception: (${exception}: ${exp_message}), expected: ${exception_expected} + IF ${exception} == ${0} + ${vallist} = Convert To List ${packet.registerVal} + Should Not Be Empty ${vallist} + Log Modbus register values:${vallist} + FOR ${item} IN @{vallist} + Log Modbus register value:${item} + #Append To List ${} ${item} + END + ${length} = Get length ${vallist} + Log Items count is: ${length} + Should Be Equal As Integers ${length} ${quantity} + ELSE + Log "Exception is evaluated correctly (${exception}: ${exp_message}) == ${exception_expected}" + END + +Read Holding Registers + [Arguments] ${uid} ${start_addr} ${quantity} ${exception_expected} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + Log Read Holding Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity} + ${req} = Create Holding Read Registers Request ${uid} ${start_addr} ${quantity} + #Create Connection ${server} ${port} + ${response_frame} = Send Packet And Get Response ${req} + Should Not Be Empty ${response_frame} + ${packet} = Translate Response ${response_frame} + Should Be Equal As Integers ${req.transId} ${packet.transId} + ${exception} ${exp_message} = Check Response ${packet} ${req.funcCode} + Log exception: (${exception}: ${exp_message}), expected: ${exception_expected} + Should Be Equal As Integers ${exception} ${exception_expected} + IF ${exception} == ${0} + ${vallist} = Convert To List ${packet.registerVal} + Should Not Be Empty ${vallist} + Log Modbus register values:${vallist} + FOR ${item} IN @{vallist} + Log Modbus register value:${item} + #Append To List ${} ${item} + END + ${length} = Get length ${vallist} + Log Items count is: ${length} + Should Be Equal As Integers ${length} ${quantity} + ELSE + Log "Exception is evaluated correctly ${exception} == ${exception_expected}" + END + +Write Holding Registers + [Arguments] ${uid} ${start_addr} ${quantity} ${data} ${exception_expected} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + Log Write Hold Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity}, data:${data} + ${req} = Create Holding Write Registers Request ${uid} ${start_addr} ${quantity} ${data} + #Create Connection ${server} ${port} + ${response_frame} = Send Packet And Get Response ${req} + Should Not Be Empty ${response_frame} + ${packet} = Translate Response ${response_frame} + Should Be Equal As Integers ${req.transId} ${packet.transId} + Should Not Be Empty ${packet} + Log Response is: ${packet.show(dump=True)} + ${exception} ${exp_message} = Check Response ${packet} ${req.funcCode} + Log exception: (${exception}: ${exp_message}), expected: ${exception_expected} + Should Be Equal As Integers ${exception} ${exception_expected} + Run Keyword If ${exception} == ${0} Should Be Equal As Integers ${${packet.quantityRegisters}} ${quantity} + ... ELSE Log "Exception is evaluated correctly ${exception} == ${exception_expected}" + +Write Single Holding Register + [Arguments] ${uid} ${start_addr} ${data} ${exception_expected} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + Log Write Single Holding Register with parameters UID:${uid}, offs:${start_addr} + ${req} = Create Holding Write Register Request ${uid} ${start_addr} ${data} + #Create Connection ${server} ${port} + ${response_frame} = Send Packet And Get Response ${req} + Should Not Be Empty ${response_frame} + ${packet} = Translate Response ${response_frame} + Should Not Be Empty ${packet} + Log Response is: ${packet.show(dump=True)} + Should Be Equal As Integers ${req.transId} ${packet.transId} + ${exception} ${exp_message} = Check Response ${packet} ${req.funcCode} + Log exception: (${exception}: ${exp_message}), expected: ${exception_expected} + Should Be Equal As Integers ${exception} ${exception_expected} + Run Keyword If ${exception} == ${0} Should Be Equal As Integers ${${packet.registerValue}} ${data} + ... ELSE Log "Exception is evaluated correctly ${exception} == ${exception_expected}" + +Read Coil Registers + [Arguments] ${uid} ${start_addr} ${quantity} ${exception_expected} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + Log Read Coil Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity} + ${req} = Create Coils Read Request ${uid} ${start_addr} ${quantity} + ${response_frame} = Send Packet And Get Response ${req} + Should Not Be Empty ${response_frame} + ${packet} = Translate Response ${response_frame} + Should Not Be Empty ${packet} + Log Response is: ${packet.show(dump=True)} + Should Be Equal As Integers ${req.transId} ${packet.transId} + ${exception} ${exp_message} = Check Response ${packet} ${req.funcCode} + Log exception: (${exception}: ${exp_message}), expected: ${exception_expected} + Should Be Equal As Integers ${exception} ${exception_expected} + IF ${exception} == ${0} + ${coils} = Get Bits From PDU ${packet} + Should Not Be Empty ${coils} + Should Be Equal As Integers ${${coils.__len__()}} ${${packet.byteCount} * 8} + Log Returned modbus coils: ${coils} + ELSE + Log "Exception is evaluated correctly ${exception} == ${exception_expected}" + END + +Write Coil Registers + [Arguments] ${uid} ${start_addr} ${quantity} ${coil_data} ${exception_expected} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + Log Write Coil Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity}, coil_data:${coil_data} + ${req} = Create Coils Write Request ${uid} ${start_addr} ${quantity} ${coil_data} + ${response_frame} = Send Packet And Get Response ${req} + Should Not Be Empty ${response_frame} + ${packet} = Translate Response ${response_frame} + Should Not Be Empty ${packet} + Log Response is: ${packet.show(dump=True)} + Should Be Equal As Integers ${req.transId} ${packet.transId} + ${exception} ${exp_message} = Check Response ${packet} ${req.funcCode} + Log exception: (${exception}: ${exp_message}), expected: ${exception_expected} + Should Be Equal As Integers ${exception} ${exception_expected} + IF ${exception} == ${0} + Log ${${packet.quantityOutput}} + Should Be Equal As Integers ${${packet.quantityOutput}} ${quantity} + ELSE + Log "Exception is evaluated correctly ${exception} == ${exception_expected}" + END + +Read Discrete Input Registers + [Arguments] ${uid} ${start_addr} ${quantity} ${exception_expected} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + Log Read Discrete Input Registers with parameters UID:${uid}, offs:${start_addr}, quantity:${quantity} + ${req} = Create Discrete Read Request ${uid} ${start_addr} ${quantity} + ${response_frame} = Send Packet And Get Response ${req} + Should Not Be Empty ${response_frame} + ${packet} = Translate Response ${response_frame} + Should Not Be Empty ${packet} + Log Response is: ${packet.show(dump=True)} + Should Be Equal As Integers ${req.transId} ${packet.transId} + ${exception} ${exp_message} = Check Response ${packet} ${req.funcCode} + Log exception: (${exception}: ${exp_message}), expected: ${exception_expected} + Should Be Equal As Integers ${exception} ${exception_expected} + IF ${exception} == ${0} + ${dicretes} = Get Bits From PDU ${packet} + Should Not Be Empty ${dicretes} + Should Be Equal As Integers ${${dicretes.__len__()}} ${${packet.byteCount} * 8} + Log Returned modbus dicretes: ${dicretes} + ELSE + Log "Exception is evaluated correctly ${exception} == ${exception_expected}" + END + +Send Packet And Get Response + [Arguments] ${packet} + ${response} = Send Packet ${packet} timeout=3 verbose=1 + Log Got response ${response} + RETURN ${response} + +Create Connection + [Arguments] ${host} ${port} + ${classId} = Get Class Id + Log Library ClassId: ${classId} + ${connection} = Connect ${host} ${port} + IF ${connection} + Log Connection to host: ${host}:${port} established. + ELSE + Log Connection to host: ${host}:${port} failed. + END + RETURN ${connection} diff --git a/tools/robot/ModbusTestSuite.robot b/tools/robot/ModbusTestSuite.robot new file mode 100644 index 0000000..3d108ca --- /dev/null +++ b/tools/robot/ModbusTestSuite.robot @@ -0,0 +1,64 @@ +*** Settings *** +Documentation A test suite for Modbus commands. +... +... Keywords are imported from the resource file +Resource ModbusTestSuite.resource +Default Tags multi_dut_modbus_generic +Suite Setup Create Connection ${MODBUS_DEF_SERVER_IP} ${MODBUS_DEF_PORT} +Suite Teardown Disconnect + +*** Variables *** +${suiteConnection} None + +*** Test Cases *** +Test Read Holding Registers With Different Addresses And Quantities + [Documentation] Test reading holding registers from different addresses with different quantities + [Template] Read Holding Registers + 0x01 0x0001 2 0 + 0x01 0x0002 3 0 + +Test Write Holding Registers With Different Addresses And Quantities + [Documentation] Test write holding registers for different addresses with different quantities + [Template] Write Holding Registers + 0x01 0x0003 2 [0x1122, 0x3344] 0 + 0x01 0x0004 3 [0x1122, 0x3344, 0x5566] 0 + +Test Read Input Registers With Different Addresses And Quantities + [Documentation] Test read input registers for different addresses with different quantities + [Template] Read Input Registers + 0x01 0x0003 2 0 + 0x01 0x0004 3 0 + 0x01 0x0001 200 3 + 0x01 0x2344 3 2 + +Test Write Single Holding Register + [Documentation] Test write one single holding register + [Template] Write Single Holding Register + 0x01 0x0001 0x1122 0 + 0x01 0x2344 0x1122 2 + 0x01 0x0010 0x3344 0 + +Test Read Coils With Different Addresses And Quantities + [Documentation] Test read coil registers for different addresses with different quantities + [Template] Read Coil Registers + 0x01 0x0001 0 3 + 0x01 0x0001 16 0 + 0x01 0x0010 20 2 + 0x01 0x0002 300 2 + 0x01 0x0008 30 2 + +Test Read Discrete Inputs With Different Addresses And Quantities + [Documentation] Test read discrete registers for different addresses with different quantities + [Template] Read Discrete Input Registers + 0x01 0x0001 0 3 + 0x01 0x0001 16 0 + 0x01 0x0010 20 2 + 0x01 0x0002 300 2 + 0x01 0x0008 30 2 + +Test Write Coils With Different Addresses And Quantities + [Documentation] Test write coil registers for different addresses with different quantities + [Template] Write Coil Registers + 0x01 0x0000 8 [0xFF] 0 + 0x01 0x0005 300 [0xFF] 3 + 0x01 0x0008 16 [0xFF, 0x55] 0 diff --git a/tools/test_requirements.txt b/tools/test_requirements.txt new file mode 100644 index 0000000..a3f2fee --- /dev/null +++ b/tools/test_requirements.txt @@ -0,0 +1,7 @@ +pytest +pytest-embedded +pytest-embedded-idf +pytest-embedded-serial +pytest-embedded-serial-esp +robotframework +scapy>=2.6.0