diff --git a/.gitignore b/.gitignore index ca6b1b7..cb8c953 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ extra/tools/*/bin !extra/tools/*/build extra/tools/*/build/stage .task/ +.vscode # Prerequisites *.d diff --git a/README.md b/README.md index 8937966..17df9d8 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,14 @@ task: Available tasks for this project: * generate-fmimcl: Generate an FMI MCL from an existing FMU. * generate-fmimodelc: Generate a FMI ModelC FMU from an existing (DSE/ModelC) Simer simulation. +# FMI Gateway with the generate-fmigateway command: +$ task generate-fmigateway \ + FMI_VERSION=2 \ + SIGNAL_GROUPS="extra/tools/fmi/test/testdata/fmigateway/SG1.yaml,extra/tools/fmi/test/testdata/fmigateway/SG2.yaml" +Running FMI Toolset command: gen-gateway +Adding SignalGroup Model_1 to out/fmu.yaml +Adding SignalGroup Model_2 to out/fmu.yaml +Creating Model YAML: gateway (out/model.yaml) # FMI MCL with the generate-fmimcl command: $ task generate-fmimcl \ diff --git a/Taskfile.yml b/Taskfile.yml index 0f360d8..f61cc7d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -44,6 +44,14 @@ tasks: cmds: - task: gen-fmu + generate-fmigateway: + desc: Generate a FMI Gateway FMU from existing Signalgroups. + run: always + dir: '{{.USER_WORKING_DIR}}' + label: dse:fmi:generate-fmigateway + cmds: + - task: gen-gateway + gen-mcl: internal: true run: always @@ -112,6 +120,32 @@ tasks: requires: vars: [SIM, FMU_NAME] + gen-gateway: + internal: true + run: always + dir: '{{.USER_WORKING_DIR}}' + label: dse:fmi:gen-gateway + vars: + SIGNAL_GROUPS: '{{.SIGNAL_GROUPS | default "SIGNAL_GROUPS_NOT_SPECIFIED"}}' + OUT_DIR: '{{.OUT_DIR | default "out"}}' + SESSION: "{{if .SESSION}}--session '{{.SESSION}}'{{else}}{{end}}" + VERSION: "{{if .VERSION}}--version '{{.VERSION}}'{{else}}{{end}}" + FMI_VERSION: "{{if .FMIVERSION}}--fmiVersion '{{.FMIVERSION}}'{{else}}{{end}}" + UUID: "{{if .UUID}}--uuid '{{.UUID}}'{{else}}{{end}}" + cmds: + - docker run --rm + -v {{.ENTRYDIR}}:/sim + {{.FMI_IMAGE}}:{{.FMI_TAG}} + gen-gateway + --signalgroups {{.SIGNAL_GROUPS}} + --outdir {{.OUT_DIR}} + {{.SESSION}} + {{.VERSION}} + {{.FMI_VERSION}} + {{.UUID}} + requires: + vars: [SIGNAL_GROUPS] + patch-signalgroup: run: always dir: '{{.USER_WORKING_DIR}}' diff --git a/doc/Makefile b/doc/Makefile index 1ad1eb0..36d1ee5 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -45,8 +45,15 @@ DOC_OUTPUT_fmu := doc/content/apis/fmi/fmu/index.md DOC_LINKTITLE_fmu := "FMU" DOC_TITLE_fmu := "FMU API Reference" +# Module "fmu" +DOC_INPUT_fmigateway := dse/fmigateway/fmigateway.h +DOC_CDIR_fmigateway := dse/fmigateway/fmigateway.c,dse/fmigateway/parser.c,dse/fmigateway/index.c,dse/fmigateway/session.c,dse/fmigateway/session_win32.c +DOC_OUTPUT_fmigateway := doc/content/apis/fmi/fmigateway/index.md +DOC_LINKTITLE_fmigateway := "Gateway FMU" +DOC_TITLE_fmigateway := "FMI Gateway FMU API Reference" + # Targets -DOC_C_MODULES := fmimcl fmimodelc fmu +DOC_C_MODULES := fmimcl fmimodelc fmu fmigateway .PHONY: examples diff --git a/doc/content/apis/fmi/fmigateway/fmigateway-component.png b/doc/content/apis/fmi/fmigateway/fmigateway-component.png new file mode 100644 index 0000000..299838e Binary files /dev/null and b/doc/content/apis/fmi/fmigateway/fmigateway-component.png differ diff --git a/doc/content/apis/fmi/fmigateway/index.md b/doc/content/apis/fmi/fmigateway/index.md new file mode 100644 index 0000000..42402f5 --- /dev/null +++ b/doc/content/apis/fmi/fmigateway/index.md @@ -0,0 +1,262 @@ +--- +title: FMI Gateway FMU API Reference +linkTitle: Gateway FMU +--- +## fmu_signals_reset + + +Resets the binary signals of the gateway to a length of 0, if the signals have +not been reseted yet. + +> Required by FMU. + +### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + +## fmu_signals_setup + + +Placeholder to signal the FMU to not use the default signal allocation. + +### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + +## fmu_signals_remove + + +This method frees the allocated binary signal indexes. + +### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + +## FMI ModelC Gateway + + + +### Component Diagram + + + +![](fmigateway-component.png) + + +### Example + + + + +## fmu_create + + +This method allocates the necessary gateway models. The location of the required +yaml files is set and allocated. + +> Required by FMU. + +### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +### Returns + +0 (int32_t) +: The FMU was created correctly. + + + +## fmu_init + + +In this method the required yaml files are parsed and the session is configured, +if required. The gateway is set up and connected to the simbus. After a +sucessfull connection has been established, the fmu variables are indexed to +their corresponding simbus signals. + +> Required by FMU. + +### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +### Returns + +0 (int32_t) +: The FMU was created correctly. + + + +## fmu_step + + +This method executes one step of the gateway model and signals are exchanged +with the other simulation participants. + +> Required by FMU. + +### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +communication_point (double) +: The current model time of the FMU in seconds. +step_size (double) +: The step size of the FMU in seconds. + +### Returns + +0 (int32_t) +: The FMU step was performed correctly. + + + + +## fmu_destroy + + +Releases memory and system resources allocated by gateway. + +> Required by FMU. + +### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +### Returns + +0 (int32_t) +: The FMU data was released correctly. + + + +## Typedefs + +### FmiGateway + +```c +typedef struct FmiGateway { + int * model; + struct (anonymous struct at dse/fmigateway/fmigateway.h:75:5) settings; + int binary_signals_reset; +} +``` + +### FmiGatewaySession + +```c +typedef struct FmiGatewaySession { + WindowsModel * w_models; + struct (anonymous struct at dse/fmigateway/fmigateway.h:61:5) init; + struct (anonymous struct at dse/fmigateway/fmigateway.h:65:5) shutdown; + double last_step; +} +``` + +### WindowsModel + +```c +typedef struct WindowsModel { + const char * path; + const char * file; + int show_process; + const char * name; + double step_size; + double end_time; + int log_level; + const char * yaml; + double current_step; + double timeout; + void * w_process; +} +``` + +## Functions + +### fmigateway_parse + +This method loads the required yaml files from the resource location of the fmu. +The loaded yaml files are parsed into the fmu descriptor object. + +#### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + +### fmigateway_session_configure + +If session parameters were parsed from the model description, this method +configures and starts the additional models, or executes the given command. + +#### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + +### fmigateway_session_end + +If session parameters were parsed from the model description, this method +shuts down the additional models, or executes the given command. + +#### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + +### fmigateway_session_windows_end + +Termiantes all previously started windows processes. +After sending the termination signals, one additionally +step is made by the gateway to close the simulation. + +#### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + +### fmigateway_session_windows_start + +Creates windows processes based on the parameters +configured in a yaml file. Process informations are +stored for later termination. + +#### Parameters + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + + + diff --git a/doc/content/docs/user/fmi/fmigateway/index.md b/doc/content/docs/user/fmi/fmigateway/index.md index 52b8a86..ce49ddc 100644 --- a/doc/content/docs/user/fmi/fmigateway/index.md +++ b/doc/content/docs/user/fmi/fmigateway/index.md @@ -32,4 +32,86 @@ FMI Gateway FMU ## Operation -> DOC: Operation. +### Session + +A session handling can be defined for the gatway using the following yaml file in the toolchain. +It is then automatically placed as part of the annoations in the model.yaml. + +|Name|Type|Required|Description| +|---|---|---|---| +|session|object|true|none| +|» initialization|object|false|Script/ File that is run in the initialization phase of the FMU| +|»» path|string|true|Path to the file| +|»» file|string|true|File to be executed| +|» shutdown|object|false|Script/ File that is run in the shutdown phase of the FMU| +|»» path|string|true|Path to the file| +|»» file|string|true|File to be executed| +|» windows|object|false|List of ModelC models to be started/ stopped on windows| +|»» model|string|true|Name of the model| +|»» annotation|object|true|| +|»»» stepsize|float|true|Step size of this model| +|»»» endtime|float|false|End time for this model. If not set, model runs until gateway shutdown| +|»»» loglevel|int|true|Log level between 0 and 6 (0 for most logs)| +|»»» timeout|false|true|Model timeout| +|»»» path|string|true|Path to the model executable| +|»»» file|string|true|Model file| +|»»» yaml|string|true|List of yaml files, separated by ";"| +|»»» show|boolean|true|Terminal toggle for on/ off| + +```text +--- +annotations: + session: + initialization: + path: 'foo' + file: 'bar' + shutdown: + path: 'foo_shutdown' + file: 'bar_shutdown' + windows: + - model: 'Model_1' + annotations: + stepsize: 0.1 + endtime: 0.2 + loglevel: 6 + timeout: 60.0 + path: 'foo' + file: 'bar' + yaml: 'foo;bar' + show: true + - model: 'Model_2' + annotations: + stepsize: 0.15 + endtime: 0.25 + loglevel: 5 + timeout: 61.0 + path: 'foo_2' + file: 'bar_2' + yaml: 'foo_2;bar_2' + show: false +``` + +### Toolchains + +The required files for the FMI gateway can be generated using a toolchain included +in the FMI GO tools. This operation with generate the required modelDescription.xml, +model.yaml and fmu.yaml. + +|Name|Required|Description| +|---|---|---| +|VERSION|false|Version of the Gateway FMU| +|FMI_VERSION|false|FMI Version of the Gateway FMU (Default 2)| +|SESSION|false|Yaml file containing the session handling| +|SIGNAL_GROUPS|true|List of yaml files to generate the required files for the Gateway FMU| +|OUT_DIR|false|Path to the generated files| +|UUID|false|UUID of the Gateway FMU| + +```bash +$ task generate-fmigateway \ + VERSION=0.01 \ + FMI_VERSION=2 \ + SESSION=session.yaml \ + SIGNAL_GROUPS="extra/tools/fmi/test/testdata/fmigateway/SG1.yaml,extra/tools/fmi/test/testdata/fmigateway/SG2.yaml" \ + OUT_DIR=./out \ + UUID=2f9f2b62-0718-4e66-8f40-6735e03d4c08 +``` diff --git a/dse/CMakeLists.txt b/dse/CMakeLists.txt index 28a6a77..5b74fff 100644 --- a/dse/CMakeLists.txt +++ b/dse/CMakeLists.txt @@ -142,6 +142,7 @@ endif() # =========== add_subdirectory(fmimcl) add_subdirectory(fmimodelc) +add_subdirectory(fmigateway) add_subdirectory(fmu) add_subdirectory(importer) add_subdirectory(examples) diff --git a/dse/examples/CMakeLists.txt b/dse/examples/CMakeLists.txt index 6bbd5e2..476ec9f 100644 --- a/dse/examples/CMakeLists.txt +++ b/dse/examples/CMakeLists.txt @@ -5,4 +5,5 @@ if($ENV{PACKAGE_ARCH} STREQUAL "linux-amd64") add_subdirectory(fmu/counter) add_subdirectory(fmu/linear) +add_subdirectory(gateway) endif() diff --git a/dse/examples/gateway/CMakeLists.txt b/dse/examples/gateway/CMakeLists.txt new file mode 100644 index 0000000..4c3e5e4 --- /dev/null +++ b/dse/examples/gateway/CMakeLists.txt @@ -0,0 +1,224 @@ +# Copyright 2024 Robert Bosch GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.21) + +# set(CMAKE_VERBOSE_MAKEFILE ON) + +set(VERSION "$ENV{PACKAGE_VERSION}") +set(MODULE "Gateway") +set(MODULE_LC "gateway") + +project(fmigateway VERSION ${VERSION}) + +include(FetchContent) + +# set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED TRUE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_BUILD_TYPE Debug) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ggdb -DLIBXML_STATIC") +list(APPEND C_CXX_WARNING_FLAGS + -Wall + -W + -Wwrite-strings + -Wno-missing-field-initializers + -Wno-misleading-indentation +) +add_compile_options(${C_CXX_WARNING_FLAGS}) + + +# External Project - DSE C Lib +# ---------------------------- +FetchContent_Declare(dse_clib + URL https://github.com/boschglobal/dse.clib/archive/refs/tags/v$ENV{DSE_CLIB_VERSION}.zip +) +FetchContent_MakeAvailable(dse_clib) +set(DSE_CLIB_SOURCE_DIR ${dse_clib_SOURCE_DIR}/dse) +set(DSE_CLIB_INCLUDE_DIR "${DSE_CLIB_SOURCE_DIR}/..") +set(FMI2_INCLUDE_DIR "${DSE_CLIB_SOURCE_DIR}/clib/fmi/fmi2/headers") +set(FMI3_INCLUDE_DIR "${DSE_CLIB_SOURCE_DIR}/clib/fmi/fmi3/headers") + + +# External Project - DSE ModelC LIB +# --------------------------------- +FetchContent_Declare(dse_modelc_lib + URL https://github.com/boschglobal/dse.modelc/releases/download/v$ENV{DSE_MODELC_VERSION}/ModelC-$ENV{DSE_MODELC_VERSION}-$ENV{PACKAGE_ARCH}.zip +) +FetchContent_MakeAvailable(dse_modelc_lib) +set(DSE_MODELC_INCLUDE_DIR "${dse_modelc_lib_SOURCE_DIR}/include") + + +set(FMU_SOURCE_DIR "../../fmu") +set(FMU_INCLUDE_DIR "../../..") +set(FMI_2_COMMON_FILES + ${FMU_SOURCE_DIR}/ascii85.c + ${FMU_SOURCE_DIR}/fmi2fmu.c + ${FMU_SOURCE_DIR}/signal.c +) +set(FMI_3_COMMON_FILES + ${FMU_SOURCE_DIR}/ascii85.c + ${FMU_SOURCE_DIR}/fmi3fmu.c + ${FMU_SOURCE_DIR}/signal.c +) +set(FMIGATEWAY_FILES "../../fmigateway") +set(FMIGATEWAY_FILES + ${FMIGATEWAY_FILES}/fmigateway.c + ${FMIGATEWAY_FILES}/index.c + ${FMIGATEWAY_FILES}/session.c + ${FMIGATEWAY_FILES}/parser.c + $<$:${FMIGATEWAY_FILES}/session_win32.c> +) + +# Targets +# ======= + +# Simulation (single Model) +# ---------- +add_library(gatewaytarget SHARED + model.c +) +target_include_directories(gatewaytarget + PRIVATE + ${DSE_MODELC_INCLUDE_DIR} + ${FMU_INCLUDE_DIR} +) +target_link_directories(gatewaytarget + PRIVATE + $<$:${dse_modelc_lib_SOURCE_DIR}/bin> +) +target_link_libraries(gatewaytarget + PRIVATE + $<$:modelc> +) +install(TARGETS gatewaytarget + LIBRARY DESTINATION + examples/gateway/model +) + +# FMI2 Gateway FMU +# --------------- +add_library(gatewayfmi2fmu SHARED + ${FMI_2_COMMON_FILES} + ${FMIGATEWAY_FILES} +) +target_include_directories(gatewayfmi2fmu + PRIVATE + ${DSE_CLIB_INCLUDE_DIR} + ${DSE_MODELC_INCLUDE_DIR} + ${FMI2_INCLUDE_DIR} + ${FMU_INCLUDE_DIR} +) +target_link_directories(gatewayfmi2fmu + PRIVATE + $<$:${dse_modelc_lib_SOURCE_DIR}/lib> + $<$:${dse_modelc_lib_SOURCE_DIR}/bin> +) +target_compile_definitions(gatewayfmi2fmu + PRIVATE + PLATFORM_OS="${CDEF_PLATFORM_OS}" + PLATFORM_ARCH="${CDEF_PLATFORM_ARCH}" +) +target_link_libraries(gatewayfmi2fmu + PRIVATE + yaml + xml + $<$:bcrypt> + $<$:-Wl,--whole-archive,--export-dynamic> + $<$:modelc_bundled> + $<$:-Wl,--no-whole-archive> + $<$:rt> + $<$:modelc> + $<$:dl> + +) +set_target_properties(gatewayfmi2fmu + PROPERTIES + OUTPUT_NAME + fmi2gateway +) +install( + TARGETS + gatewayfmi2fmu + LIBRARY DESTINATION + examples/gateway/fmi2/binaries/linux64 +) +install( + FILES + fmi2/modelDescription.xml + DESTINATION + examples/gateway/fmi2 +) +install( + FILES + data/fmu.yaml + data/model.yaml + data/stack.yaml + DESTINATION + examples/gateway/fmi2/resources +) + +# FMI3 Gateway FMU +# --------------- +add_library(gatewayfmi3fmu SHARED + ${FMI_3_COMMON_FILES} + ${FMIGATEWAY_FILES} +) +target_include_directories(gatewayfmi3fmu + PRIVATE + ${DSE_CLIB_INCLUDE_DIR} + ${DSE_MODELC_INCLUDE_DIR} + ${FMI3_INCLUDE_DIR} + ${FMU_INCLUDE_DIR} +) +target_link_directories(gatewayfmi3fmu + PRIVATE + $<$:${dse_modelc_lib_SOURCE_DIR}/lib> + $<$:${dse_modelc_lib_SOURCE_DIR}/bin> +) +target_compile_definitions(gatewayfmi3fmu + PRIVATE + PLATFORM_OS="${CDEF_PLATFORM_OS}" + PLATFORM_ARCH="${CDEF_PLATFORM_ARCH}" +) +target_link_libraries(gatewayfmi3fmu + PRIVATE + yaml + xml + $<$:bcrypt> + $<$:-Wl,--whole-archive,--export-dynamic> + $<$:modelc_bundled> + $<$:-Wl,--no-whole-archive> + $<$:rt> + $<$:modelc> + $<$:dl> + +) +set_target_properties(gatewayfmi3fmu + PROPERTIES + OUTPUT_NAME + fmi3gateway +) +install( + TARGETS + gatewayfmi3fmu + LIBRARY DESTINATION + examples/gateway/fmi3/binaries/x86_64-linux +) +install( + FILES + fmi3/modelDescription.xml + DESTINATION + examples/gateway/fmi3 +) +install( + FILES + data/fmu.yaml + data/model.yaml + data/stack.yaml + DESTINATION + examples/gateway/fmi3/resources +) diff --git a/dse/examples/gateway/data/fmu.yaml b/dse/examples/gateway/data/fmu.yaml new file mode 100644 index 0000000..64848d7 --- /dev/null +++ b/dse/examples/gateway/data/fmu.yaml @@ -0,0 +1,90 @@ +--- +kind: SignalGroup +metadata: + name: scalar_vector + labels: + channel: scalar + annotations: + vector_type: scalar +spec: + signals: + - signal: scalar_in + annotations: + fmi_variable_vref: 1001 + fmi_variable_name: scalar_in + fmi_variable_type: Real + fmi_variable_causality: input + - signal: scalar_out + annotations: + fmi_variable_vref: 1002 + fmi_variable_name: scalar_out + fmi_variable_type: Real + fmi_variable_causality: output +# - signal: scalar_param +# annotations: +# fmi_variable_vref: 1003 +# fmi_variable_name: scalar_param +# fmi_variable_type: Real +# fmi_variable_causality: parameter +--- +kind: SignalGroup +metadata: + name: scalar_vector2 + labels: + channel: scalar + annotations: + vector_type: scalar +spec: + signals: + - signal: scalar_in2 + annotations: + fmi_variable_vref: 1004 + fmi_variable_name: scalar_in2 + fmi_variable_type: Real + fmi_variable_causality: input + - signal: scalar_out2 + annotations: + fmi_variable_vref: 1005 + fmi_variable_name: scalar_out2 + fmi_variable_type: Real + fmi_variable_causality: output +# - signal: scalar_param +# annotations: +# fmi_variable_vref: 1003 +# fmi_variable_name: scalar_param +# fmi_variable_type: Real +# fmi_variable_causality: parameter +--- +kind: SignalGroup +metadata: + name: binary_vector + labels: + channel: binary + type: binary + annotations: + vector_type: binary + vector_name: network_vector +spec: + signals: + - signal: can + annotations: + mime_type: 'application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; node_id=2; interface_id=0' + # + # FMI Value Reference + # ------------------- + fmi_variable_vref: 4 + fmi_variable_vref_input: 4 + fmi_variable_vref_output: 5 + # + # FMI LS Text Encoding (applied to FMI String Variables): + # -------------------- + dse.standards.fmi-ls-binary-to-text.encoding: ascii85 + # (supporting annotations to avoid XML parsing) + dse.standards.fmi-ls-binary-to-text.vref: [2, 3, 4, 5, 6, 7, 8, 9] + # + # FMI LS Bus Topology (applied to FMI String/Binary Variables): + # ------------------- + dse.standards.fmi-ls-bus-topology.bus_id: 1 + # (supporting annotations to avoid XML parsing) + dse.standards.fmi-ls-bus-topology.rx_vref: [2, 4, 6, 8] + dse.standards.fmi-ls-bus-topology.tx_vref: [3, 5, 7, 9] diff --git a/dse/examples/gateway/data/model.yaml b/dse/examples/gateway/data/model.yaml new file mode 100644 index 0000000..d3c1111 --- /dev/null +++ b/dse/examples/gateway/data/model.yaml @@ -0,0 +1,52 @@ +--- +kind: Model +metadata: + name: Gateway + annotations: + adapter: 'fmi' + fmi_version: '2.0' + fmi_model_cosim: true + fmi_model_version: '1.48' + fmi_stepsize: '0.0005' + fmi_endtime: '0.002' + fmi_guid: '{11111111-2222-3333-4444-555555555555}' + fmi_resource_dir: 'data' + set_init_values: true + loglevel: 4 +spec: + runtime: + gateway: {} + channels: + - alias: scalar_vector + selectors: + channel: scalar + - alias: binary_vector + selectors: + channel: binary +--- +kind: Model +metadata: + name: Target +spec: + runtime: + dynlib: + - os: linux + arch: amd64 + path: model/libgatewaytarget.so + - os: linux + arch: x86 + path: model/libgatewaytarget.so + - os: windows + arch: x64 + path: model/libgatewaytarget.dll + - os: windows + arch: x86 + path: model/libgatewaytarget.dll + channels: + - alias: scalar_vector + selectors: + side: scalar_vector + - alias: binary_vector + selectors: + channel: binary +--- diff --git a/dse/examples/gateway/data/stack.yaml b/dse/examples/gateway/data/stack.yaml new file mode 100644 index 0000000..f2736f1 --- /dev/null +++ b/dse/examples/gateway/data/stack.yaml @@ -0,0 +1,48 @@ +--- +kind: Stack +metadata: + name: stack +spec: + connection: + transport: + redispubsub: + uri: redis://localhost:6379 + timeout: 60 + models: + - name: simbus + model: + name: simbus + channels: + - name: scalar + expectedModelCount: 2 + - name: binary + expectedModelCount: 2 + - name: gateway + uid: 42 + model: + name: Gateway + channels: + - name: scalar + alias: scalar_vector + selectors: + channel: scalar + - name: binary + alias: binary_vector + selectors: + channel: binary + - name: target_inst + uid: 43 + model: + name: Target + runtime: + files: + - fmu/resources/fmu.yaml + channels: + - name: scalar + alias: scalar_vector + selectors: + channel: scalar + - name: binary + alias: binary_vector + selectors: + channel: binary diff --git a/dse/examples/gateway/fmi2/modelDescription.xml b/dse/examples/gateway/fmi2/modelDescription.xml new file mode 100644 index 0000000..4f9e89b --- /dev/null +++ b/dse/examples/gateway/fmi2/modelDescription.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; interface_id=3 + + + ascii85 + + + 1 + + + + + + + + ascii85 + + + 1 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dse/examples/gateway/fmi3/modelDescription.xml b/dse/examples/gateway/fmi3/modelDescription.xml new file mode 100644 index 0000000..46910a8 --- /dev/null +++ b/dse/examples/gateway/fmi3/modelDescription.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; interface_id=3 + + + ascii85 + + + 1 + + + + + + + + ascii85 + + + 1 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dse/examples/gateway/model.c b/dse/examples/gateway/model.c new file mode 100644 index 0000000..8607c3a --- /dev/null +++ b/dse/examples/gateway/model.c @@ -0,0 +1,110 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include + + +typedef struct { + ModelDesc model; + /* Network Codec reference. */ + NCODEC* nc; + /* Model Instance values. */ + char* node_id; + uint8_t counter; +} ExtendedModelDesc; + + +static inline NCODEC* _index(ExtendedModelDesc* m, const char* v, const char* s) +{ + ModelSignalIndex idx = signal_index((ModelDesc*)m, v, s); + if (idx.binary == NULL) log_fatal("Signal not found (%s:%s)", v, s); + + NCODEC* nc = signal_codec(idx.sv, idx.signal); + if (nc == NULL) log_fatal("NCodec object not available (%s:%s)", v, s); + + return nc; +} + + +static void _adjust_node_id(NCODEC* nc, const char* node_id) +{ + ncodec_config(nc, (struct NCodecConfigItem){ + .name = "node_id", + .value = node_id, + }); +} + + +ModelDesc* model_create(ModelDesc* model) +{ + /* Extend the ModelDesc object (using a shallow copy). */ + ExtendedModelDesc* m = calloc(1, sizeof(ExtendedModelDesc)); + memcpy(m, model, sizeof(ModelDesc)); + + /* Index the Network Codec. */ + m->nc = _index(m, "binary_vector", "can"); + + /* Configure the Network Codec. */ + for (int i = 0; i >= 0; i++) { + NCodecConfigItem nci = ncodec_stat(m->nc, &i); + if (strcmp(nci.name, "node_id") == 0) { + m->node_id = strdup(nci.value); + break; + } + } + if (m->node_id == NULL) log_fatal("NCodec node_id not configured!"); + + /* Return the extended object. */ + return (ModelDesc*)m; +} + + +int model_step(ModelDesc* model, double* model_time, double stop_time) +{ + ExtendedModelDesc* m = (ExtendedModelDesc*)model; + + /* Scalar signals. */ + ModelSignalIndex counter = + m->model.index((ModelDesc*)m, "scalar_vector", "scalar_out"); + if (counter.scalar == NULL) return -EINVAL; + *(counter.scalar) += 1; + + /* Message RX - spoof the node_id to avoid RX filtering. */ + _adjust_node_id(m->nc, "49"); + while (1) { + NCodecCanMessage msg = {}; + int len = ncodec_read(m->nc, &msg); + if (len < 0) break; + log_notice("Model: RX[%04x] %s", msg.frame_id, msg.buffer); + } + _adjust_node_id(m->nc, m->node_id); + + /* Message TX. */ + char msg[100] = "Hello World!"; + snprintf(msg + strlen(msg), sizeof(msg), " from node_id=%s", m->node_id); + struct NCodecCanMessage tx_msg = { + .frame_id = ++m->counter + 1000, /* Simple frame_id: 1001, 1002 ... */ + .frame_type = CAN_EXTENDED_FRAME, + .buffer = (uint8_t*)msg, + .len = strlen(msg) + 1, /* Capture the NULL terminator. */ + }; + ncodec_truncate(m->nc); + ncodec_write(m->nc, &tx_msg); + ncodec_flush(m->nc); + + *model_time = stop_time; + return 0; +} + + +void model_destroy(ModelDesc* model) +{ + ExtendedModelDesc* m = (ExtendedModelDesc*)model; + free(m->node_id); +} diff --git a/dse/fmigateway/CMakeLists.txt b/dse/fmigateway/CMakeLists.txt new file mode 100644 index 0000000..bc40870 --- /dev/null +++ b/dse/fmigateway/CMakeLists.txt @@ -0,0 +1,170 @@ +# Copyright 2024 Robert Bosch GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.21) + +# set(CMAKE_VERBOSE_MAKEFILE ON) + +set(VERSION "$ENV{PACKAGE_VERSION}") +set(MODULE "FmiGateway") +set(MODULE_LC "fmigateway") + +project(${MODULE} + VERSION ${VERSION} + DESCRIPTION "DSE FMI ModelC Gateway" + HOMEPAGE_URL "$ENV{PROJECT_URL}" +) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED TRUE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_BUILD_TYPE Debug) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -ggdb -DLIBXML_STATIC") +list(APPEND C_CXX_WARNING_FLAGS + -Wall + -W + -Wwrite-strings + -Wno-missing-field-initializers + -Wno-misleading-indentation +) +add_compile_options(${C_CXX_WARNING_FLAGS}) + + +set(FMI2_INCLUDE_DIR "${DSE_CLIB_SOURCE_DIR}/clib/fmi/fmi2/headers") +set(FMI3_INCLUDE_DIR "${DSE_CLIB_SOURCE_DIR}/clib/fmi/fmi3/headers") + + +set(FMU_SOURCE_DIR "../fmu") +set(FMU_INCLUDE_DIR "../..") +set(FMI_2_COMMON_FILES + ${FMU_SOURCE_DIR}/ascii85.c + ${FMU_SOURCE_DIR}/fmi2fmu.c + ${FMU_SOURCE_DIR}/signal.c +) +set(FMI_3_COMMON_FILES + ${FMU_SOURCE_DIR}/ascii85.c + ${FMU_SOURCE_DIR}/fmi3fmu.c + ${FMU_SOURCE_DIR}/signal.c +) + + +# Targets +# ======= + +# FMI2 Gateway FMU +# --------------- +add_library(fmi2gateway SHARED + ${FMI_2_COMMON_FILES} + fmigateway.c + index.c + session.c + parser.c + $<$:session_win32.c> +) +target_include_directories(fmi2gateway + PRIVATE + ${DSE_CLIB_INCLUDE_DIR} + ${DSE_MODELC_INCLUDE_DIR} + ${DSE_MODELC_SOURCE_INCLUDE_DIR} + ${FMI2_INCLUDE_DIR} + ${FMU_INCLUDE_DIR} +) +target_link_directories(fmi2gateway + PRIVATE + $<$:${dse_modelc_lib_SOURCE_DIR}/lib> + $<$:${dse_modelc_lib_SOURCE_DIR}/bin> +) +target_compile_definitions(fmi2gateway + PRIVATE + PLATFORM_OS="${CDEF_PLATFORM_OS}" + PLATFORM_ARCH="${CDEF_PLATFORM_ARCH}" +) +target_link_libraries(fmi2gateway + PRIVATE + yaml + xml + $<$:bcrypt> + $<$:-Wl,--whole-archive,--export-dynamic> + $<$:modelc_bundled> + $<$:-Wl,--no-whole-archive> + $<$:rt> + $<$:modelc> + $<$:dl> + +) +install( + TARGETS + fmi2gateway + LIBRARY DESTINATION + ${MODULE_LC}/lib +) + +# FMI3 Gateway FMU +# --------------- +add_library(fmi3gateway SHARED + ${FMI_3_COMMON_FILES} + fmigateway.c + index.c + session.c + parser.c + $<$:session_win32.c> +) +target_include_directories(fmi3gateway + PRIVATE + ${DSE_CLIB_INCLUDE_DIR} + ${DSE_MODELC_INCLUDE_DIR} + ${DSE_MODELC_SOURCE_INCLUDE_DIR} + ${FMI3_INCLUDE_DIR} + ${FMU_INCLUDE_DIR} +) +target_link_directories(fmi3gateway + PRIVATE + $<$:${dse_modelc_lib_SOURCE_DIR}/lib> + $<$:${dse_modelc_lib_SOURCE_DIR}/bin> +) +target_compile_definitions(fmi3gateway + PRIVATE + PLATFORM_OS="${CDEF_PLATFORM_OS}" + PLATFORM_ARCH="${CDEF_PLATFORM_ARCH}" +) +target_link_libraries(fmi3gateway + PRIVATE + yaml + xml + $<$:bcrypt> + $<$:-Wl,--whole-archive,--export-dynamic> + $<$:modelc_bundled> + $<$:-Wl,--no-whole-archive> + $<$:rt> + $<$:modelc> + $<$:dl> + +) +install( + TARGETS + fmi3gateway + LIBRARY DESTINATION + ${MODULE_LC}/lib +) + +# ModelC DLL (windows only) +# ------------------------- +install( + FILES + $<$:${dse_modelc_lib_SOURCE_DIR}/bin/libmodelc.dll> + DESTINATION + ${MODULE_LC}/bin +) + + +# Package +# ======= +set(CPACK_SYSTEM_NAME $ENV{PACKAGE_ARCH}) +set(CPACK_PACKAGE_VENDOR "Robert Bosch GmbH") +set(CPACK_PACKAGE_DESCRIPTION "DSE FMI Gateway") +set(CPACK_OUTPUT_FILE_PREFIX _dist) +set(CPACK_GENERATOR "ZIP") +set(CPACK_PACKAGE_CHECKSUM MD5) +set(CPACK_MONOLITHIC_INSTALL TRUE) +include(CPack) diff --git a/dse/fmigateway/fmigateway.c b/dse/fmigateway/fmigateway.c new file mode 100644 index 0000000..04c13a0 --- /dev/null +++ b/dse/fmigateway/fmigateway.c @@ -0,0 +1,277 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + + +#define UNUSED(x) ((void)x) + + +/** +fmu_create +========== + +This method allocates the necessary gateway models. The location of the required +yaml files is set and allocated. + +> Required by FMU. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +Returns +------- +0 (int32_t) +: The FMU was created correctly. +*/ +int32_t fmu_create(FmuInstanceData* fmu) +{ + assert(fmu); + /* Allocate the ModelGatewayDesc object. */ + FmiGateway* fmi_gw = calloc(1, sizeof(FmiGateway)); + *fmi_gw = (FmiGateway){ + .model = calloc(1, sizeof(ModelGatewayDesc)), + .settings.yaml_files = calloc(4, sizeof(char*)), + }; + + /* Allocate a NTL for the required files. */ + fmi_gw->settings.yaml_files[0] = + dse_path_cat(fmu->instance.resource_location, "model.yaml"); + fmi_gw->settings.yaml_files[1] = + dse_path_cat(fmu->instance.resource_location, "fmu.yaml"); + fmi_gw->settings.yaml_files[2] = + dse_path_cat(fmu->instance.resource_location, "stack.yaml"); + + fmu->data = (void*)fmi_gw; + + return 0; +} + + +/** +fmu_init +======== + +In this method the required yaml files are parsed and the session is configured, +if required. The gateway is set up and connected to the simbus. After a +sucessfull connection has been established, the fmu variables are indexed to +their corresponding simbus signals. + +> Required by FMU. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +Returns +------- +0 (int32_t) +: The FMU was created correctly. +*/ +int32_t fmu_init(FmuInstanceData* fmu) +{ + assert(fmu); + FmiGateway* fmi_gw = fmu->data; + assert(fmi_gw); + ModelGatewayDesc* gw = fmi_gw->model; + assert(gw); + + /* Parse the yaml files. */ + fmigateway_parse(fmu); + + fmigateway_session_configure(fmu); + + /* Setup the Model Gateway object. */ + fmu_log(fmu, 0, "Debug", "Setting up the Simbus connection..."); + model_gw_setup(gw, "gateway", fmi_gw->settings.yaml_files, + fmi_gw->settings.log_level, fmi_gw->settings.step_size, + fmi_gw->settings.end_time); + fmu_log(fmu, 0, "Debug", "Connected to the Simbus..."); + + fmigateway_index_scalar_signals( + fmu, gw, &fmu->variables.scalar.input, &fmu->variables.scalar.output); + fmigateway_index_binary_signals( + fmu, gw, &fmu->variables.binary.rx, &fmu->variables.binary.tx); + fmigateway_index_text_encoding(fmu, gw, &fmu->variables.binary.encode_func, + &fmu->variables.binary.decode_func); + + return 0; +} + + +/** +fmu_step +======== + +This method executes one step of the gateway model and signals are exchanged +with the other simulation participants. + +> Required by FMU. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +communication_point (double) +: The current model time of the FMU in seconds. +step_size (double) +: The step size of the FMU in seconds. + +Returns +------- +0 (int32_t) +: The FMU step was performed correctly. + +*/ +int32_t fmu_step( + FmuInstanceData* fmu, double communication_point, double step_size) +{ + assert(fmu); + FmiGateway* fmi_gw = fmu->data; + assert(fmi_gw); + ModelGatewayDesc* gw = fmi_gw->model; + assert(gw); + + /* Step the model. */ + int rc = model_gw_sync(gw, communication_point); + if (rc == E_GATEWAYBEHIND) { + return 0; + } + + /* Save current step for shutdown process. */ + if (fmi_gw->settings.session) { + fmi_gw->settings.session->last_step = communication_point; + fmi_gw->settings.step_size = step_size; + } + + return 0; +} + + +/** +fmu_destroy +=========== + +Releases memory and system resources allocated by gateway. + +> Required by FMU. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +Returns +------- +0 (int32_t) +: The FMU data was released correctly. +*/ +int32_t fmu_destroy(FmuInstanceData* fmu) +{ + assert(fmu); + FmiGateway* fmi_gw = fmu->data; + assert(fmi_gw); + ModelGatewayDesc* gw = fmi_gw->model; + assert(gw); + + fmigateway_session_end(fmu); + + /* Disconnect from the simbus. */ + model_gw_exit(gw); + + /* Cleanup */ + dse_yaml_destroy_doc_list(fmi_gw->settings.doc_list); + for (size_t i = 0; fmi_gw->settings.yaml_files[i]; i++) { + free((char*)fmi_gw->settings.yaml_files[i]); + } + free(fmi_gw->settings.yaml_files); + free(gw); + free(fmi_gw); + + return 0; +} + + +/** +fmu_signals_reset +================= + +Resets the binary signals of the gateway to a length of 0, if the signals have +not been reseted yet. + +> Required by FMU. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +void fmu_signals_reset(FmuInstanceData* fmu) +{ + assert(fmu); + FmiGateway* fmi_gw = fmu->data; + assert(fmi_gw); + + if (fmi_gw->binary_signals_reset) return; + + for (SignalVector* sv = fmi_gw->model->sv; sv && sv->name; sv++) { + if (sv->is_binary == false) continue; + sv->length[0] = 0; + } + fmi_gw->binary_signals_reset = true; +} + + +/** +fmu_signals_setup +================= + +Placeholder to signal the FMU to not use the default signal allocation. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +void fmu_signals_setup(FmuInstanceData* fmu) +{ + UNUSED(fmu); +} + + +static inline int _free_fmu_idx(void* map_item, void* additional_data) +{ + UNUSED(additional_data); + FmuSignalVectorIndex* fmu_idx = map_item; + if (fmu_idx) { + free(fmu_idx->sv); + } + return 0; +} + + +/** +fmu_signals_remove +================== + +This method frees the allocated binary signal indexes. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +void fmu_signals_remove(FmuInstanceData* fmu) +{ + assert(fmu); + fmu_log(fmu, 0, "Debug", "Removing additional signal data..."); + hashmap_iterator(&fmu->variables.binary.rx, _free_fmu_idx, false, NULL); + hashmap_iterator(&fmu->variables.binary.tx, _free_fmu_idx, false, NULL); +} diff --git a/dse/fmigateway/fmigateway.h b/dse/fmigateway/fmigateway.h new file mode 100644 index 0000000..b13c513 --- /dev/null +++ b/dse/fmigateway/fmigateway.h @@ -0,0 +1,106 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#ifndef DSE_FMIGATEWAY_FMIGATEWAY_H_ +#define DSE_FMIGATEWAY_FMIGATEWAY_H_ + +#include +#include +#include + + +/** +FMI ModelC Gateway +================== + + +Component Diagram +----------------- + + +![](fmigateway-component.png) + + +Example +------- +*/ + +typedef struct WindowsModel { + /* process Information */ + const char* path; + const char* file; + bool show_process; + /* Model information. */ + const char* name; + double step_size; + double end_time; + int log_level; + const char* yaml; + double current_step; + double timeout; + /* Windows Information. */ + void* w_process; +} WindowsModel; + +typedef struct FmiGatewaySession { + WindowsModel* w_models; + struct { + const char* path; + const char* file; + } init; + struct { + const char* path; + const char* file; + } shutdown; + /* Additional information. */ + double last_step; +} FmiGatewaySession; + +typedef struct FmiGateway { + ModelGatewayDesc* model; + struct { + YamlDocList* doc_list; + const char** yaml_files; + double step_size; + double end_time; + int log_level; + const char* log_location; + FmiGatewaySession* session; + } settings; + bool binary_signals_reset; +} FmiGateway; + +/* index.c */ +DLL_PRIVATE void fmigateway_index_scalar_signals( + FmuInstanceData* fmu, ModelGatewayDesc* m, HashMap* input, HashMap* output); +DLL_PRIVATE void fmigateway_index_binary_signals( + FmuInstanceData* fmu, ModelGatewayDesc* m, HashMap* rx, HashMap* tx); +DLL_PRIVATE void fmigateway_index_text_encoding(FmuInstanceData* fmu, + ModelGatewayDesc* m, HashMap* encode_func, HashMap* decode_func); + +/* parser.c */ +DLL_PRIVATE void fmigateway_parse(FmuInstanceData* fmu); + +/* session.c */ +DLL_PRIVATE void fmigateway_session_configure(FmuInstanceData* fmu); +DLL_PRIVATE void fmigateway_session_end(FmuInstanceData* fmu); + +/* session_win32.c */ +DLL_PRIVATE void fmigateway_session_windows_start(FmuInstanceData* fmu); +DLL_PRIVATE void fmigateway_session_windows_end(FmuInstanceData* fmu); + + +#endif // DSE_FMIGATEWAY_FMIGATEWAY_H_ diff --git a/dse/fmigateway/index.c b/dse/fmigateway/index.c new file mode 100644 index 0000000..f49e2ea --- /dev/null +++ b/dse/fmigateway/index.c @@ -0,0 +1,244 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include + + +static SchemaSignalObject* __signal_match; +static const char* __signal_match_name; + + +ChannelSpec* _model_build_channel_spec( + ModelInstanceSpec* model_instance, const char* channel_name) +{ + const char* selectors[] = { "name" }; + const char* values[] = { channel_name }; + YamlNode* c_node = dse_yaml_find_node_in_seq( + model_instance->spec, "channels", selectors, values, 1); + /* No channel was found with matching name. */ + if (c_node == NULL) { + const char* selectors[] = { "alias" }; + const char* values[] = { channel_name }; + c_node = dse_yaml_find_node_in_seq( + model_instance->spec, "channels", selectors, values, 1); + } + /* No channel was found with matching alias. */ + if (c_node == NULL) { + return NULL; + } + + ChannelSpec* channel_spec = calloc(1, sizeof(ChannelSpec)); + channel_spec->name = channel_name; + channel_spec->private = c_node; + YamlNode* n_node = dse_yaml_find_node(c_node, "name"); + if (n_node && n_node->scalar) channel_spec->name = n_node->scalar; + YamlNode* a_node = dse_yaml_find_node(c_node, "alias"); + if (a_node && a_node->scalar) channel_spec->alias = a_node->scalar; + + return channel_spec; /* Caller to free. */ +} + + +static int _signal_group_match_handler( + ModelInstanceSpec* model_instance, SchemaObject* object) +{ + uint32_t index = 0; + + /* Enumerate over the signals. */ + SchemaSignalObject* so; + do { + so = schema_object_enumerator(model_instance, object, "spec/signals", + &index, schema_signal_object_generator); + if (so == NULL) break; + if (strcmp(so->signal, __signal_match_name) == 0) { + __signal_match = so; /* Caller to free. */ + return 0; + } + free(so); + } while (1); + + return 0; +} + + +static const char** _signal_annotation_list(ModelInstanceSpec* mi, + SignalVector* sv, const char* signal, const char* name) +{ + /* Set the search vars. */ + __signal_match = NULL; + __signal_match_name = signal; + + /* Select and handle the schema objects. */ + ChannelSpec* cs = _model_build_channel_spec(mi, sv->name); + SchemaObjectSelector* selector; + selector = schema_build_channel_selector(mi, cs, "SignalGroup"); + if (selector) { + schema_object_search(mi, selector, _signal_group_match_handler); + } + schema_release_selector(selector); + free(cs); + /* Look for the annotation. */ + if (__signal_match) { + const char** v = NULL; + YamlNode* a_node = + dse_yaml_find_node(__signal_match->data, "annotations"); + if (a_node) { + v = dse_yaml_get_array(a_node, name, NULL); + } + free(__signal_match); + __signal_match = NULL; + return v; + } + + return NULL; +} + + +void fmigateway_index_scalar_signals( + FmuInstanceData* fmu, ModelGatewayDesc* m, HashMap* input, HashMap* output) +{ + for (SignalVector* sv = m->sv; sv && sv->name; sv++) { + if (sv->is_binary) continue; + + for (uint32_t i = 0; i < sv->count; i++) { + /* Value Reference. */ + const char* vref = + signal_annotation(sv, i, "fmi_variable_vref", NULL); + if (vref == NULL) continue; + + /* Locate the variable. */ + ModelSignalIndex idx = signal_index( + (ModelDesc*)m->mi->model_desc, sv->alias, sv->signal[i]); + if (idx.scalar == NULL) continue; + + /* Index based on causality. */ + const char* causality = + signal_annotation(sv, i, "fmi_variable_causality", NULL); + if (strcmp(causality, "output") == 0) { + hashmap_set(output, vref, idx.scalar); + } else if (strcmp(causality, "input") == 0) { + hashmap_set(input, vref, idx.scalar); + } + } + } + + fmu_log(fmu, 0, "Debug", " Scalar: input=%u, output=%u", input->used_nodes, + output->used_nodes); +} + + +static inline void _set_binary_variable(ModelDesc* m, SignalVector* sv, + uint32_t index, HashMap* map, const char* vref) +{ + /* Locate the variable. */ + ModelSignalIndex idx = signal_index(m, sv->alias, sv->signal[index]); + if (idx.binary == NULL) return; + + FmuSignalVectorIndex* fmu_idx = calloc(1, sizeof(FmuSignalVectorIndex)); + fmu_idx->sv = calloc(1, sizeof(FmuSignalVector)); + fmu_idx->sv->binary = idx.binary; + fmu_idx->sv->signal = (char**)idx.sv->signal; + fmu_idx->sv->length = idx.sv->length; + fmu_idx->sv->buffer_size = idx.sv->buffer_size; + fmu_idx->vi = idx.signal; + hashmap_set_alt(map, vref, fmu_idx); +} + + +void fmigateway_index_binary_signals( + FmuInstanceData* fmu, ModelGatewayDesc* m, HashMap* rx, HashMap* tx) +{ + for (ModelInstanceSpec* mi = m->sim->instance_list; mi && mi->name; mi++) { + for (SignalVector* sv = mi->model_desc->sv; sv && sv->name; sv++) { + if (sv->is_binary == false) continue; + + for (uint32_t i = 0; i < sv->count; i++) { + /* Value Reference. */ + const char* vref = + signal_annotation(sv, i, "fmi_variable_vref", NULL); + if (vref == NULL) continue; + + /* FMU binary input variables. */ + /* Index according to bus topology. */ + const char** rx_list = _signal_annotation_list(sv->mi, sv, + sv->signal[i], "dse.standards.fmi-ls-bus-topology.rx_vref"); + if (rx_list) { + for (size_t j = 0; rx_list[j]; j++) { + /* Value Reference for the RX variable. */ + const char* rx_vref = rx_list[j]; + + /* Create a FmuSignalVector and add it to the map. */ + _set_binary_variable( + m->mi->model_desc, sv, i, rx, rx_vref); + } + free(rx_list); + } + + /* FMU binary output variables. */ + /* Index according to bus topology. */ + const char** tx_list = _signal_annotation_list(sv->mi, sv, + sv->signal[i], "dse.standards.fmi-ls-bus-topology.tx_vref"); + if (tx_list) { + for (size_t j = 0; tx_list[j]; j++) { + /* Value Reference for the TX variable. */ + const char* tx_vref = tx_list[j]; + + /* Create a FmuSignalVector and add it to the map. */ + _set_binary_variable( + m->mi->model_desc, sv, i, tx, tx_vref); + } + free(tx_list); + } + } + } + } + + fmu_log(fmu, 0, "Debug", " Binary: rx=%u, tx=%u", rx->used_nodes, + tx->used_nodes); +} + + +void fmigateway_index_text_encoding(FmuInstanceData* fmu, ModelGatewayDesc* m, + HashMap* encode_func, HashMap* decode_func) +{ + for (ModelInstanceSpec* mi = m->sim->instance_list; mi && mi->name; mi++) { + for (SignalVector* sv = mi->model_desc->sv; sv && sv->name; sv++) { + if (sv->is_binary == false) continue; + + for (uint32_t i = 0; i < sv->count; i++) { + /* Value Reference. */ + const char* _ = + signal_annotation(sv, i, "fmi_variable_vref", NULL); + if (_ == NULL) continue; + + /* Encoding. */ + const char* encoding = signal_annotation(sv, i, + "dse.standards.fmi-ls-binary-to-text.encoding", NULL); + if (strcmp(encoding, "ascii85") != 0) continue; + + /* Index, all with same encoding (for now). */ + // dse.standards.fmi-ls-binary-to-text.vref: [[2,3,4,5,6,7,8,9] + const char** vref_list = _signal_annotation_list(sv->mi, sv, + sv->signal[i], "dse.standards.fmi-ls-binary-to-text.vref"); + if (vref_list) { + for (size_t j = 0; vref_list[j]; j++) { + /* Value Reference for the RX variable. */ + const char* vref = vref_list[j]; + + /* Encoding. */ + hashmap_set(encode_func, vref, ascii85_encode); + hashmap_set(decode_func, vref, ascii85_decode); + } + free(vref_list); + } + } + } + } + fmu_log(fmu, 0, "Debug", " Encoding: enc=%u, dec=%u", + encode_func->used_nodes, decode_func->used_nodes); +} diff --git a/dse/fmigateway/parser.c b/dse/fmigateway/parser.c new file mode 100644 index 0000000..770e7b1 --- /dev/null +++ b/dse/fmigateway/parser.c @@ -0,0 +1,183 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include + + +#define UNUSED(x) ((void)x) +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) +#define MODEL_MAX_TIME 60 * 60 * 10 // 10 minutes +#define STEP_SIZE 0.0005 + + +static inline void* _gwfmu_model_generator(ModelInstanceSpec* mi, void* data) +{ + UNUSED(mi); + YamlNode* n = dse_yaml_find_node((YamlNode*)data, "model"); + if (n && n->scalar) { + WindowsModel* w_model = calloc(1, sizeof(WindowsModel)); + + w_model->name = n->scalar; + if (dse_yaml_get_double( + data, "annotations/stepsize", &w_model->step_size)) { + w_model->step_size = STEP_SIZE; + }; + if (dse_yaml_get_double( + data, "annotations/endtime", &w_model->end_time)) { + w_model->end_time = MODEL_MAX_TIME; + }; + if (dse_yaml_get_int( + data, "annotations/loglevel", &w_model->log_level)) { + w_model->log_level = -1; + }; + if (dse_yaml_get_double( + data, "annotations/timeout", &w_model->timeout)) { + w_model->timeout = 60.0; + }; + dse_yaml_get_string(data, "annotations/path", &w_model->path); + dse_yaml_get_string(data, "annotations/file", &w_model->file); + dse_yaml_get_string(data, "annotations/yaml", &w_model->yaml); + dse_yaml_get_bool(data, "annotations/show", &w_model->show_process); + + log_notice("%s", w_model->name); + log_notice(" Path: %s", w_model->path); + log_notice(" File: %s", w_model->file); + log_notice(" Yaml: %s", w_model->yaml); + log_notice(" Stepsize: %lf", w_model->step_size); + log_notice(" Endtime: %lf", w_model->end_time); + log_notice(" Timout: %lf", w_model->timeout); + log_notice(" Loglevel: %d", w_model->log_level); + log_notice(" Show: %d", w_model->show_process); + return w_model; + } + return NULL; +} + + +static inline void _parse_session_script(FmuInstanceData* fmu, YamlNode* node, + const char* node_path, const char** path, const char** file) +{ + YamlNode* _node = dse_yaml_find_node(node, node_path); + if (_node) { + if (dse_yaml_get_string(_node, "path", path)) { + *path = fmu->instance.resource_location; + } + dse_yaml_get_string(_node, "file", file); + } +} + + +static inline void _gwfmu_parse_session(ModelInstanceSpec* mi, SchemaObject* o, + FmuInstanceData* fmu, YamlNode* s_node) +{ + FmiGateway* fmi_gw = fmu->data; + assert(fmi_gw); + + fmi_gw->settings.session = calloc(1, sizeof(FmiGatewaySession)); + FmiGatewaySession* session = fmi_gw->settings.session; + + _parse_session_script(fmu, s_node, "initialization", &session->init.path, + &session->init.file); + _parse_session_script(fmu, s_node, "shutdown", &session->shutdown.path, + &session->shutdown.file); + + YamlNode* w_node = dse_yaml_find_node(s_node, "windows"); + if (w_node) { + /* Enumerate over the models. */ + uint32_t index = 0; + WindowsModel* w_model; + HashList m_list; + hashlist_init(&m_list, 100); + do { + w_model = schema_object_enumerator(mi, o, + "metadata/annotations/session/windows", &index, + _gwfmu_model_generator); + if (w_model == NULL) break; + if (w_model->name) { + hashlist_append(&m_list, w_model); + } else { + free(w_model); + } + } while (1); + /* Convert to a NTL. */ + session->w_models = hashlist_ntl(&m_list, sizeof(WindowsModel), true); + } +} + + +static inline int _model_match_handler(ModelInstanceSpec* mi, SchemaObject* o) +{ + FmuInstanceData* fmu = o->data; + FmiGateway* fmi_gw = fmu->data; + assert(fmi_gw); + + /* Get and set runtime parameters. */ + if (dse_yaml_get_double(o->doc, "metadata/annotations/fmi_stepsize", + &(fmi_gw->settings.step_size))) { + fmi_gw->settings.step_size = STEP_SIZE; + } + if (dse_yaml_get_double(o->doc, "metadata/annotations/fmi_endtime", + &(fmi_gw->settings.end_time))) { + fmi_gw->settings.end_time = MODEL_MAX_TIME; + } + if (dse_yaml_get_int(o->doc, "metadata/annotations/loglevel", + &(fmi_gw->settings.log_level))) { + fmi_gw->settings.log_level = 6; + } + if (dse_yaml_get_string(o->doc, "metadata/annotations/loglocation", + &(fmi_gw->settings.log_location))) { + fmi_gw->settings.log_location = "."; + } + + + /* Create Session if specified in the doc. */ + YamlNode* session_node = + dse_yaml_find_node(o->doc, "metadata/annotations/session"); + if (session_node) { + _gwfmu_parse_session(mi, o, fmu, session_node); + } + + return 0; +} + + +/** +fmigateway_parse +================ + +This method loads the required yaml files from the resource location of the fmu. +The loaded yaml files are parsed into the fmu descriptor object. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +void fmigateway_parse(FmuInstanceData* fmu) +{ + FmiGateway* fmi_gw = fmu->data; + assert(fmi_gw); + + /* Load yaml files. */ + for (size_t i = 0; fmi_gw->settings.yaml_files[i]; i++) { + fmi_gw->settings.doc_list = dse_yaml_load_file( + fmi_gw->settings.yaml_files[i], fmi_gw->settings.doc_list); + } + + /* Parse the FMU Model. */ + SchemaObjectSelector m_sel = { + .kind = "Model", + .name = "Gateway", + .data = fmu, + }; + ModelInstanceSpec mi = { .yaml_doc_list = fmi_gw->settings.doc_list }; + schema_object_search(&mi, &m_sel, _model_match_handler); +} diff --git a/dse/fmigateway/session.c b/dse/fmigateway/session.c new file mode 100644 index 0000000..a6d127b --- /dev/null +++ b/dse/fmigateway/session.c @@ -0,0 +1,86 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + + +#define UNUSED(x) ((void)x) + + +/** +fmigateway_session_configure +============================ + +If session parameters were parsed from the model description, this method +configures and starts the additional models, or executes the given command. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +void fmigateway_session_configure(FmuInstanceData* fmu) +{ + FmiGateway* fmi_gw = fmu->data; + + FmiGatewaySession* session = fmi_gw->settings.session; + if (session == NULL) return; + + if (session->init.file) { + char buf[256]; + snprintf( + buf, sizeof(buf), "%s%s", session->init.path, session->init.file); + system(buf); + } + + if (session->w_models && strcmp(PLATFORM_OS, "windows") == 0) { + fmigateway_session_windows_start(fmu); + } +} + + +/** +fmigateway_session_end +====================== + +If session parameters were parsed from the model description, this method +shuts down the additional models, or executes the given command. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +void fmigateway_session_end(FmuInstanceData* fmu) +{ + FmiGateway* fmi_gw = fmu->data; + + FmiGatewaySession* session = fmi_gw->settings.session; + if (session == NULL) return; + + if (session->shutdown.file) { + char buf[256]; + snprintf(buf, sizeof(buf), "%s%s", session->shutdown.path, + session->shutdown.file); + system(buf); + } + + if (session->w_models && strcmp(PLATFORM_OS, "windows") == 0) { + fmigateway_session_windows_end(fmu); + } +} + + +__attribute__((weak)) void fmigateway_session_windows_start( + FmuInstanceData* fmu) +{ + UNUSED(fmu); +} + + +__attribute__((weak)) void fmigateway_session_windows_end(FmuInstanceData* fmu) +{ + UNUSED(fmu); +} diff --git a/dse/fmigateway/session_win32.c b/dse/fmigateway/session_win32.c new file mode 100644 index 0000000..1df906e --- /dev/null +++ b/dse/fmigateway/session_win32.c @@ -0,0 +1,392 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#define MODEL_MAX_TIME 60 * 60 * 10 // 10 minutes + + +typedef struct WindowsProcess { + STARTUPINFO s_info; + PROCESS_INFORMATION p_info; +} WindowsProcess; + + +/* +_build_cmd +========== + +Build the command for the modelC process from the yaml parameters. +Endtime and stepsize are optional. + +Parameters +---------- +w_model (WindowsModel) +: Model Descriptor containing parameter information. + +Returns +------- +string +: string containing the cmd to start a windows model +*/ +static inline char* _build_cmd(WindowsModel* w_model, const char* path) +{ + char cmd[1024]; + size_t max_len = sizeof cmd; + int offset = + snprintf(cmd, max_len, "cmd /C cd %s && %s", path, w_model->file); + offset += snprintf(cmd + offset, max_len, " --name %s", w_model->name); + offset += + snprintf(cmd + offset, max_len, " --endtime %lf", w_model->end_time); + offset += + snprintf(cmd + offset, max_len, " --stepsize %lf", w_model->step_size); + offset += + snprintf(cmd + offset, max_len, " --logger %d", w_model->log_level); + offset += + snprintf(cmd + offset, max_len, " --timeout %lf", w_model->timeout); + if (w_model->yaml) { + offset += snprintf(cmd + offset, max_len, " %s", w_model->yaml); + } + return strdup(cmd); +} + + +/* +_create_file +============ + +Creates a file on a windows operating system. + +Parameters +---------- +name (char*) +: name of the file to be created. + +Returns +------- +HANDLE +: open handle to the specified file +*/ +static inline HANDLE _create_file(char* name) +{ + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = NULL; + sa.bInheritHandle = TRUE; + + return CreateFile(name, FILE_WRITE_DATA, FILE_SHARE_WRITE | FILE_SHARE_READ, + &sa, // default security + CREATE_ALWAYS, // create new file always + FILE_ATTRIBUTE_NORMAL, // normal file + NULL // attribute template + ); +} + + +/* +_terminate_process +================== + +Terminate a windows process. + +Parameters +---------- +w_model (WindowsModel) +: Model Descriptor containing parameter information. +*/ +static inline void _terminate_process(WindowsModel* w_model) +{ + WindowsProcess* w_process = w_model->w_process; + + TerminateProcess(w_process->p_info.hProcess, 0); + CloseHandle(w_process->p_info.hProcess); + CloseHandle(w_process->p_info.hThread); + free(w_process); +} + + +/* +_gracefully_termiante_process +============================= + +Gracefully terminate a windows process by sending a Ctrl C, Sigint +Signal to the process. + +Parameters +---------- +w_model (WindowsModel) +: Model Descriptor containing parameter information. +*/ +static inline void _gracefully_termiante_process(WindowsModel* w_model) +{ + WindowsProcess* w_process = w_model->w_process; + if (w_model->end_time != MODEL_MAX_TIME) return; + + /* Sequence of functions to attach to the running models and sent + a SIGINT */ + FreeConsole(); + AttachConsole(w_process->p_info.dwProcessId); + SetConsoleCtrlHandler(NULL, true); + GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, w_process->p_info.dwProcessId); + FreeConsole(); + + /* wait for the processes to process the SIGINT and reattach to + host. */ + Sleep(1000); + SetConsoleCtrlHandler(NULL, false); + AttachConsole(-1); +} + + +/* +_start_redis +============ + +Create and start a new redis process. + +Parameters +---------- +w_model (WindowsModel) +: Model Descriptor containing parameter information. + +w_process (WindowsProcess) +: Process Descriptor, references various data. + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +static inline void _start_redis( + WindowsModel* w_model, WindowsProcess* w_process, FmuInstanceData* fmu) +{ + /* Redis Server. */ + char* file_path = + dse_path_cat(fmu->instance.resource_location, w_model->file); + + if (!CreateProcess(file_path, NULL, NULL, NULL, FALSE, + CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP, NULL, NULL, + &(w_process->s_info), &(w_process->p_info))) { + log_fatal("Could not start Redis-Server.exe"); + } + free(file_path); +} + + +/* +_start_model +============ + +Create and start a new modelC process. + +Parameters +---------- +w_model (WindowsModel) +: Model Descriptor containing parameter information. + +w_process (WindowsProcess) +: Process Descriptor, references various data. + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +static inline void _start_model( + WindowsModel* w_model, WindowsProcess* w_process, FmuInstanceData* fmu) +{ + FmiGateway* fmi_gw = fmu->data; + char* cmd = _build_cmd(w_model, fmu->instance.resource_location); + + HANDLE _log; + if (w_model->log_level >= 0) { + /* Create logfile for modelC models. */ + char log[128]; + snprintf(log, sizeof(log), "%s/%s_log.txt", + fmi_gw->settings.log_location, w_model->name); + _log = _create_file(log); + if (_log) { + w_process->s_info.hStdInput = NULL; + w_process->s_info.hStdError = _log; + w_process->s_info.hStdOutput = _log; + } + } + + /* ModelC models. */ + if (!CreateProcess(NULL, cmd, NULL, NULL, ((_log) ? TRUE : FALSE), + CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP, NULL, NULL, + &(w_process->s_info), &(w_process->p_info))) { + log_fatal( + "Could not start %s (error %d)", w_model->name, GetLastError()); + } + free(cmd); +} + + +/* +_start_process +============== + +Start a modelC windows process based on information from +a yaml parameter configuration. + +Parameters +---------- +w_model (WindowsModel) +: Model Descriptor containing parameter information. + +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. +*/ +static inline void _start_process(WindowsModel* w_model, FmuInstanceData* fmu) +{ + WindowsProcess* w_process = w_model->w_process; + + log_notice("Starting process: %s", w_model->name); + + /* Initialize the process handles and information for + the windows processes. */ + ZeroMemory(&(w_process->s_info), sizeof(STARTUPINFO)); + ZeroMemory(&(w_process->p_info), sizeof(PROCESS_INFORMATION)); + + w_process->s_info.cb = sizeof(STARTUPINFO); + w_process->s_info.dwFlags = (STARTF_USESTDHANDLES); + + /* Display a terminal window. */ + if (w_model->show_process) { + w_process->s_info.lpTitle = (char*)w_model->name; + } else { + w_process->s_info.dwFlags = + (STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW); + w_process->s_info.wShowWindow = SW_HIDE; + } + + /* Redis requires a different startup. */ + if (strcasecmp(w_model->name, "redis") == 0) { + _start_redis(w_model, w_process, fmu); + return; + } + _start_model(w_model, w_process, fmu); +} + + +/* +_check_shutdown +=============== + +Observe a process and check if it is terminated. + +Parameters +---------- +w_model (WindowsModel) +: Model Descriptor containing parameter information. + +sec (integer) +: Time in seconds. +*/ +inline static int32_t _check_shutdown(WindowsModel* w_model, int sec) +{ + WindowsProcess* w_process = w_model->w_process; + DWORD result = WaitForSingleObject(w_process->p_info.hProcess, sec * 1000); + if (result == WAIT_OBJECT_0) { + log_notice("%s is shut down.", w_model->name); + } else { + log_error("%s is still active.", w_model->name); + return -1; + } + /* Close the model handle. */ + CloseHandle(w_process->p_info.hProcess); + CloseHandle(w_process->p_info.hThread); + free(w_model->w_process); + return 0; +} + + +/** +fmigateway_session_windows_start +================================ + +Creates windows processes based on the parameters +configured in a yaml file. Process informations are +stored for later termination. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + */ +void fmigateway_session_windows_start(FmuInstanceData* fmu) +{ + FmiGateway* fmi_gw = fmu->data; + FmiGatewaySession* session = fmi_gw->settings.session; + + for (WindowsModel* w_m = session->w_models; w_m && w_m->name; w_m++) { + w_m->w_process = calloc(1, sizeof(WindowsProcess)); + _start_process(w_m, fmu); + } +} + + +/** +fmigateway_session_windows_end +============================== + +Termiantes all previously started windows processes. +After sending the termination signals, one additionally +step is made by the gateway to close the simulation. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + */ +void fmigateway_session_windows_end(FmuInstanceData* fmu) +{ + FmiGateway* fmi_gw = fmu->data; + FmiGatewaySession* session = fmi_gw->settings.session; + ModelGatewayDesc* gw = fmi_gw->model; + + WindowsModel* redis = NULL; + WindowsModel* simbus = NULL; + + for (WindowsModel* w_m = session->w_models; w_m && w_m->name; w_m++) { + if (strcasecmp(w_m->name, "simbus") == 0) { + simbus = w_m; + continue; + } else if (strcasecmp(w_m->name, "redis") == 0) { + redis = w_m; + continue; + } + _gracefully_termiante_process(w_m); + } + + model_gw_sync( + gw, session->last_step + (fmi_gw->settings.step_size * 1.001)); + fmu_log(fmu, 0, "Debug", "Extra step for shutting down models finished..."); + + model_gw_exit(gw); + fmu_log(fmu, 0, "Debug", "Gateway exited..."); + + /* Loop through processes and confirm that all are closed. */ + for (WindowsModel* w_m = session->w_models; w_m && w_m->name; w_m++) { + if (strcasecmp(w_m->name, "redis") == 0 || + strcasecmp(w_m->name, "simbus") == 0) { + continue; + } + _check_shutdown(w_m, 10); + } + + if (simbus) { + if (_check_shutdown(simbus, 10) < 0) { + _gracefully_termiante_process(simbus); + } + } + if (redis) _terminate_process(redis); +} diff --git a/dse/fmimodelc/examples/network_fmu/fmi2importer.c b/dse/fmimodelc/examples/network_fmu/fmi2importer.c index 9b75968..78a1572 100644 --- a/dse/fmimodelc/examples/network_fmu/fmi2importer.c +++ b/dse/fmimodelc/examples/network_fmu/fmi2importer.c @@ -93,7 +93,7 @@ int main(int argc, char** argv) fmi2Instantiate instantiate = dlsym(handle, "fmi2Instantiate"); if (instantiate == NULL) return EINVAL; fmu = instantiate("network_fmu", fmi2CoSimulation, "guid", "resources", - NULL, true, false); + NULL, true, true); if (fmu == NULL) return EINVAL; /* fmi2ExitInitializationMode */ diff --git a/dse/fmimodelc/examples/network_fmu/modelDescription.xml b/dse/fmimodelc/examples/network_fmu/modelDescription.xml index 74d2a44..5404c21 100644 --- a/dse/fmimodelc/examples/network_fmu/modelDescription.xml +++ b/dse/fmimodelc/examples/network_fmu/modelDescription.xml @@ -1,36 +1,54 @@ - - - - - ... - ... - - - - - application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; interface_id=3 - - - ascii85 - - - 1 - - - - - - - - ascii85 - - - 1 - - - - ... - ... - ... - ... - + + + + + + + + + + + + + + + + + + + application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; interface_id=3 + + + ascii85 + + + 1 + + + + + + + + ascii85 + + + 1 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dse/fmu/fmi2fmu.c b/dse/fmu/fmi2fmu.c index 155152e..2c61825 100644 --- a/dse/fmu/fmi2fmu.c +++ b/dse/fmu/fmi2fmu.c @@ -20,45 +20,60 @@ #define VREF_KEY_LEN (10 + 1) +/** +default_log +=========== + +Default logging function in case the FMU caller does not provide any logger. +*/ void default_log(fmi2ComponentEnvironment componentEnvironment, fmi2String instanceName, fmi2Status status, fmi2String category, fmi2String message, ...) { UNUSED(componentEnvironment); + UNUSED(instanceName); static const char* statusString[] = { "OK", "Warning", "Discard", "Error", "Fatal", "Pending" }; - printf("[%s] %s: ", category, instanceName); - va_list args; - va_start(args, message); - vprintf(message, args); - va_end(args); - printf(" (%s)\n", statusString[status]); + printf("[%s:%s] %s\n", category, statusString[status], message); fflush(stdout); } -static void _log(const char* format, ...) + +void fmu_log(FmuInstanceData* fmu, const int status, const char* category, + const char* message, ...) { - printf("FMU: "); + if (fmu->instance.log_enabled == fmi2False) return; + va_list args; - va_start(args, format); - vprintf(format, args); + va_start(args, message); + char format[1024]; + vsnprintf(format, sizeof(format), message, args); va_end(args); - printf("\n"); - fflush(stdout); + + ((void (*)())fmu->instance.logger)( + NULL, fmu->instance.name, status, category, format); } -static void _log_binary_signal(FmuSignalVectorIndex* idx) + + +static void _log_binary_signal(FmuInstanceData* fmu, FmuSignalVectorIndex* idx) { if (idx == NULL || idx->sv->binary == NULL) return; uint32_t index = idx->vi; - _log(" - name : %s", idx->sv->signal[index]); - _log(" length : %d", idx->sv->length[index]); - _log(" buffer len : %d", idx->sv->buffer_size[index]); + + fmu_log(fmu, fmi2OK, "Debug", + "\n - name : %s" + "\n length : %d" + "\n buffer len : %d", + idx->sv->signal[index], idx->sv->length[index], + idx->sv->buffer_size[index]); + uint8_t* buffer = *idx->sv->binary; for (uint32_t j = 0; j + 16 < idx->sv->length[index]; j += 16) { - _log(" %02x %02x %02x %02x %02x %02x %02x %02x " - "%02x %02x %02x %02x %02x %02x %02x %02x", + fmu_log(fmu, fmi2OK, "Debug", + "\n %02x %02x %02x %02x %02x %02x %02x %02x " + "%02x %02x %02x %02x %02x %02x %02x %02x", buffer[j + 0], buffer[j + 1], buffer[j + 2], buffer[j + 3], buffer[j + 4], buffer[j + 5], buffer[j + 6], buffer[j + 7], buffer[j + 8], buffer[j + 9], buffer[j + 10], buffer[j + 11], @@ -91,7 +106,6 @@ fmi2Component fmi2Instantiate(fmi2String instance_name, fmi2Type fmu_type, UNUSED(visible); /* Create the FMU Model Instance Data. */ - _log("Create the FMU Model Instance Data"); FmuInstanceData* fmu = calloc(1, sizeof(FmuInstanceData)); fmu->instance.name = strdup(instance_name); fmu->instance.type = fmu_type; @@ -111,8 +125,7 @@ fmi2Component fmi2Instantiate(fmi2String instance_name, fmi2Type fmu_type, } else { fmu->instance.logger = default_log; } - ((void (*)())fmu->instance.logger)( - NULL, fmu->instance.name, fmi2OK, "Debug", "FMU Model instantiated"); + fmu_log(fmu, fmi2OK, "Debug", "FMU Model instantiated"); /** * Calculate the offset needed to trim/correct the resource location. @@ -130,11 +143,11 @@ fmi2Component fmi2Instantiate(fmi2String instance_name, fmi2Type fmu_type, resource_path_offset = strlen(FILE_URI_SHORT_SCHEME); } fmu->instance.resource_location += resource_path_offset; - ((void (*)())fmu->instance.logger)(NULL, fmu->instance.name, fmi2OK, - "Debug", "Resource location: %s", fmu->instance.resource_location); - ((void (*)())fmu->instance.logger)( - NULL, fmu->instance.name, fmi2OK, "Debug", "Build indexes..."); + fmu_log(fmu, fmi2OK, "Debug", "Resource location: %s", + fmu->instance.resource_location); + + fmu_log(fmu, fmi2OK, "Debug", "Build indexes..."); hashmap_init(&fmu->variables.scalar.input); hashmap_init(&fmu->variables.scalar.output); hashmap_init(&fmu->variables.binary.rx); @@ -150,8 +163,9 @@ fmi2Component fmi2Instantiate(fmi2String instance_name, fmi2Type fmu_type, hashlist_init(&fmu->variables.binary.free_list, 1024); /* Specialised Model. */ - fmu_create(fmu); - + if (fmu_create(fmu) != fmi2OK) { + fmu_log(fmu, fmi2Error, "Error", "The FMU was not created correctly!"); + } /* Return the created instance object. */ return (fmi2Component)fmu; } @@ -182,7 +196,7 @@ fmi2Status fmi2ExitInitializationMode(fmi2Component c) assert(c); FmuInstanceData* fmu = (FmuInstanceData*)c; - int rc = fmu_init(fmu); + int32_t rc = fmu_init(fmu); return (rc == 0 ? fmi2OK : fmi2Error); } @@ -282,7 +296,7 @@ fmi2Status fmi2GetString(fmi2Component c, const fmi2ValueReference vr[], if (data == NULL || data_len == 0) continue; /* Write the requested string, encode if configured. */ - _log_binary_signal(idx); + _log_binary_signal(fmu, idx); EncodeFunc ef = hashmap_get(&fmu->variables.binary.encode_func, vr_idx); if (ef) { value[i] = ef((char*)data, data_len); @@ -406,7 +420,7 @@ fmi2Status fmi2SetString(fmi2Component c, const fmi2ValueReference vr[], /* Append the binary string to the Binary Signal. */ dse_buffer_append(&idx->sv->binary[idx->vi], &idx->sv->length[idx->vi], &idx->sv->buffer_size[idx->vi], (void*)data, data_len); - _log_binary_signal(idx); + _log_binary_signal(fmu, idx); /* Release the decode string/memory. Caller owns value[]. */ if (data != value[i]) free(data); @@ -461,7 +475,8 @@ fmi2Status fmi2DoStep(fmi2Component c, fmi2Real currentCommunicationPoint, if (fmu->variables.vtable.reset) fmu->variables.vtable.reset(fmu); /* Step the model. */ - int rc = fmu_step(fmu, currentCommunicationPoint, communicationStepSize); + int32_t rc = + fmu_step(fmu, currentCommunicationPoint, communicationStepSize); /* Reset the binary signal reset mechanism. */ fmu->variables.signals_reset = false; @@ -487,15 +502,13 @@ void fmi2FreeInstance(fmi2Component c) assert(c); FmuInstanceData* fmu = (FmuInstanceData*)c; - if (fmu_destroy(fmu) < 0) { - ((void (*)())fmu->instance.logger)(NULL, fmu->instance.name, fmi2OK, - "Error", "Could not release model"); + if (fmu_destroy(fmu) < fmi2OK) { + fmu_log(fmu, fmi2Error, "Error", "Could not release model"); } if (fmu->variables.vtable.remove) fmu->variables.vtable.remove(fmu); - ((void (*)())fmu->instance.logger)( - NULL, fmu->instance.name, fmi2OK, "Debug", "Destroy the index"); + fmu_log(fmu, fmi2OK, "Debug", "Destroy the index"); hashmap_destroy(&fmu->variables.scalar.input); hashmap_destroy(&fmu->variables.scalar.output); hashmap_destroy(&fmu->variables.binary.rx); @@ -504,8 +517,7 @@ void fmi2FreeInstance(fmi2Component c) hashmap_destroy(&fmu->variables.binary.decode_func); hashlist_destroy(&fmu->variables.binary.free_list); - ((void (*)())fmu->instance.logger)(NULL, fmu->instance.name, fmi2OK, - "Debug", "Release FMI instance resources"); + fmu_log(fmu, fmi2OK, "Debug", "Release FMI instance resources"); free(fmu->instance.name); free(fmu->instance.guid); free(fmu->instance.save_resource_location); diff --git a/dse/fmu/fmi3fmu.c b/dse/fmu/fmi3fmu.c index 6952d81..9385056 100644 --- a/dse/fmu/fmi3fmu.c +++ b/dse/fmu/fmi3fmu.c @@ -1,4 +1,4 @@ -// Copyright 2023 Robert Bosch GmbH +// Copyright 2024 Robert Bosch GmbH // // SPDX-License-Identifier: Apache-2.0 @@ -7,10 +7,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -20,17 +20,12 @@ #define VREF_KEY_LEN (10 + 1) -static void _log(const char* format, ...) -{ - printf("FmiGateway: "); - va_list args; - va_start(args, format); - vprintf(format, args); - va_end(args); - printf("\n"); - fflush(stdout); -} +/** +default_log +=========== +Default logging function in case the FMU caller does not provide any logger. +*/ void default_log(fmi3InstanceEnvironment instanceEnvironment, fmi3Status status, fmi3String category, fmi3String message) { @@ -44,6 +39,45 @@ void default_log(fmi3InstanceEnvironment instanceEnvironment, fmi3Status status, } +void fmu_log(FmuInstanceData* fmu, const int status, const char* category, + const char* message, ...) +{ + if (fmu->instance.log_enabled == fmi3False) return; + + va_list args; + va_start(args, message); + char format[1024]; + vsnprintf(format, sizeof(format), message, args); + va_end(args); + + ((void (*)())fmu->instance.logger)(NULL, status, category, format); +} + + +static void _log_binary_signal(FmuInstanceData* fmu, FmuSignalVectorIndex* idx) +{ + if (idx == NULL || idx->sv->binary == NULL) return; + uint32_t index = idx->vi; + + fmu_log(fmu, fmi3OK, "Debug", + "\n - name : %s" + "\n length : %d" + "\n buffer len : %d", + idx->sv->signal[index], idx->sv->length[index], + idx->sv->buffer_size[index]); + + uint8_t* buffer = *idx->sv->binary; + for (uint32_t j = 0; j + 16 < idx->sv->length[index]; j += 16) { + fmu_log(fmu, fmi3OK, "Debug", + "\n %02x %02x %02x %02x %02x %02x %02x %02x " + "%02x %02x %02x %02x %02x %02x %02x %02x", + buffer[j + 0], buffer[j + 1], buffer[j + 2], buffer[j + 3], + buffer[j + 4], buffer[j + 5], buffer[j + 6], buffer[j + 7], + buffer[j + 8], buffer[j + 9], buffer[j + 10], buffer[j + 11], + buffer[j + 12], buffer[j + 13], buffer[j + 14], buffer[j + 15]); + } +} + /* Inquire version numbers and setting logging status */ const char* fmi3GetVersion() @@ -111,8 +145,7 @@ fmi3Instance fmi3InstantiateCoSimulation(fmi3String instanceName, } else { fmu->instance.logger = default_log; } - ((void (*)())fmu->instance.logger)( - NULL, fmi3OK, "fmi3Instantiate", "FMU Model instantiated"); + fmu_log(fmu, fmi3OK, "Debug", "FMU Model instantiated"); /** * Calculate the offset needed to trim/correct the resource location. @@ -130,8 +163,11 @@ fmi3Instance fmi3InstantiateCoSimulation(fmi3String instanceName, resource_path_offset = strlen(FILE_URI_SHORT_SCHEME); } fmu->instance.resource_location += resource_path_offset; - _log("Resource location: %s", fmu->instance.resource_location); + fmu_log(fmu, fmi3OK, "Debug", "Resource location: %s", + fmu->instance.resource_location); + + fmu_log(fmu, fmi3OK, "Debug", "Build indexes..."); hashmap_init(&fmu->variables.scalar.input); hashmap_init(&fmu->variables.scalar.output); hashmap_init(&fmu->variables.binary.rx); @@ -147,7 +183,9 @@ fmi3Instance fmi3InstantiateCoSimulation(fmi3String instanceName, hashlist_init(&fmu->variables.binary.free_list, 1024); /* Specialised Model. */ - fmu_create(fmu); + if (fmu_create(fmu) != fmi3OK) { + fmu_log(fmu, fmi3Error, "Error", "The FMU was not created correctly!"); + } return (fmi3Instance)fmu; } @@ -178,13 +216,14 @@ void fmi3FreeInstance(fmi3Instance instance) assert(instance); FmuInstanceData* fmu = (FmuInstanceData*)instance; - if (fmu_destroy(fmu) < 0) { - _log("Error while releasing the allocated specialised model."); + if (fmu_destroy(fmu) < fmi3OK) { + fmu_log(fmu, fmi3Error, "Error", + "Error while releasing the allocated specialised model."); } if (fmu->variables.vtable.remove) fmu->variables.vtable.remove(fmu); - _log("Destroy the index"); + fmu_log(fmu, fmi3OK, "Ok", "Destroy the index"); hashmap_destroy(&fmu->variables.scalar.input); hashmap_destroy(&fmu->variables.scalar.output); hashmap_destroy(&fmu->variables.binary.rx); @@ -193,7 +232,7 @@ void fmi3FreeInstance(fmi3Instance instance) hashmap_destroy(&fmu->variables.binary.decode_func); hashlist_destroy(&fmu->variables.binary.free_list); - _log("Release FMI instance resources"); + fmu_log(fmu, fmi3OK, "Ok", "Release FMI instance resources"); free(fmu->instance.name); free(fmu->instance.guid); free(fmu->instance.save_resource_location); @@ -221,7 +260,7 @@ fmi3Status fmi3ExitInitializationMode(fmi3Instance instance) assert(instance); FmuInstanceData* fmu = (FmuInstanceData*)instance; - int rc = fmu_init(fmu); + int32_t rc = fmu_init(fmu); return (rc == 0 ? fmi3OK : fmi3Error); } @@ -449,6 +488,7 @@ fmi3Status fmi3GetBinary(fmi3Instance instance, if (data == NULL || data_len == 0) continue; /* Write the requested string, encode if configured. */ + _log_binary_signal(fmu, idx); EncodeFunc ef = hashmap_get(&fmu->variables.binary.encode_func, vr_idx); if (ef) { values[i] = (fmi3Binary)ef((char*)data, data_len); @@ -673,6 +713,7 @@ fmi3Status fmi3SetBinary(fmi3Instance instance, /* Append the binary string to the Binary Signal. */ dse_buffer_append(&idx->sv->binary[idx->vi], &idx->sv->length[idx->vi], &idx->sv->buffer_size[idx->vi], (void*)data, valueSizes[i]); + _log_binary_signal(fmu, idx); /* Release the decode string/memory. Caller owns value[]. */ if (data != values[i]) free((uint8_t*)data); @@ -1103,7 +1144,8 @@ fmi3Status fmi3DoStep(fmi3Instance instance, if (fmu->variables.vtable.reset) fmu->variables.vtable.reset(fmu); /* Step the model. */ - int rc = fmu_step(fmu, currentCommunicationPoint, communicationStepSize); + int32_t rc = + fmu_step(fmu, currentCommunicationPoint, communicationStepSize); /* Reset the binary signal reset mechanism. */ fmu->variables.signals_reset = false; diff --git a/dse/fmu/fmu.c b/dse/fmu/fmu.c index 1ba446a..1880287 100644 --- a/dse/fmu/fmu.c +++ b/dse/fmu/fmu.c @@ -104,8 +104,8 @@ It is called in the `FreeInstance()` Method of the FMU. Parameters ---------- -model (ModelDesc*) -: Model descriptor object. +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. Returns ------- @@ -117,3 +117,27 @@ int32_t fmu_destroy(FmuInstanceData* fmu) UNUSED(fmu); return 0; } + + +/** +fmu_log +======= + +Write a log message to the logger defined by the FMU. + +Parameters +---------- +fmu (FmuInstanceData*) +: The FMU Descriptor object representing an instance of the FMU Model. + +status (const int) +: The status of the message to be logged. + +category (const char*) +: The category the message belongs to. + +message (const char*) +: The message to be logged by the FMU. +*/ +extern void fmu_log(FmuInstanceData* fmu, const int status, + const char* category, const char* message, ...); diff --git a/dse/fmu/fmu.h b/dse/fmu/fmu.h index a370a90..58d9eeb 100644 --- a/dse/fmu/fmu.h +++ b/dse/fmu/fmu.h @@ -223,6 +223,8 @@ DLL_PRIVATE int32_t fmu_init(FmuInstanceData* fmu); DLL_PRIVATE int32_t fmu_step( FmuInstanceData* fmu, double communication_point, double step_size); DLL_PRIVATE int32_t fmu_destroy(FmuInstanceData* fmu); +DLL_PRIVATE void fmu_log(FmuInstanceData* fmu, const int status, + const char* category, const char* message, ...); /* FMU Signal Interface (optional) */ DLL_PUBLIC void fmu_signals_reset(FmuInstanceData* fmu); diff --git a/dse/importer/importer.c b/dse/importer/importer.c index 3092708..ec84416 100644 --- a/dse/importer/importer.c +++ b/dse/importer/importer.c @@ -61,7 +61,7 @@ static struct option long_options[] = { { "help", no_argument, NULL, 'h' }, { "version", required_argument, NULL, 'V' }, { "fmu", required_argument, NULL, 'F' }, - { "stepSize", required_argument, NULL, 'S' }, + { "step_size", required_argument, NULL, 'S' }, { "steps", required_argument, NULL, 'N' }, }; @@ -97,7 +97,7 @@ static int _run_fmu2_cosim( return EINVAL; } fmu = instantiate( - "fmu", fmi2CoSimulation, "guid", "resources", NULL, true, false); + "fmu", fmi2CoSimulation, "guid", "resources", NULL, true, true); if (fmu == NULL) return EINVAL; /* fmi2ExitInitializationMode */ @@ -391,8 +391,10 @@ int main(int argc, char** argv) } if (desc->binary.vr_tx_binary) free(desc->binary.vr_tx_binary); if (desc->binary.val_tx_binary) free(desc->binary.val_tx_binary); + if (desc->binary.val_size_tx_binary) free(desc->binary.val_size_tx_binary); if (desc->binary.vr_rx_binary) free(desc->binary.vr_rx_binary); if (desc->binary.val_rx_binary) free(desc->binary.val_rx_binary); + if (desc->binary.val_size_rx_binary) free(desc->binary.val_size_rx_binary); if (desc->real.vr_tx_real) free(desc->real.vr_tx_real); if (desc->real.val_tx_real) free(desc->real.val_tx_real); if (desc->real.vr_rx_real) free(desc->real.vr_rx_real); diff --git a/dse/importer/xml.c b/dse/importer/xml.c index bb072ad..a1b1d86 100644 --- a/dse/importer/xml.c +++ b/dse/importer/xml.c @@ -188,12 +188,12 @@ void _parse_fmi2_model_desc(HashMap* vr_rx_real, HashMap* vr_tx_real, static inline void _parse_fmi3_scalar( - xmlNodePtr child, xmlChar* vr, xmlChar* causality, xmlChar* start, - HashMap* vr_rx_real, HashMap* vr_tx_real) + xmlNodePtr child, xmlChar* vr, xmlChar* causality, HashMap* vr_rx_real, + HashMap* vr_tx_real) { if (xmlStrcmp(child->name, (xmlChar*)"Float64")) return; - start = xmlGetProp(child, (xmlChar*)"start"); + xmlChar* start = xmlGetProp(child, (xmlChar*)"start"); double _start = 0.0; if (start) _start = atof((char*)start); @@ -202,15 +202,19 @@ static inline void _parse_fmi3_scalar( } else if (xmlStrcmp(causality, (xmlChar*)"output") == 0) { hashmap_set_double(vr_tx_real, (char*)vr, _start); } + + /* Cleanup. */ + if (start) xmlFree(start); } static inline void _parse_fmi3_binary( - xmlNodePtr child, xmlChar* vr, xmlChar* causality, xmlChar* start, - HashMap* vr_rx_binary, HashMap* vr_tx_binary) + xmlNodePtr child, xmlChar* vr, xmlChar* causality, HashMap* vr_rx_binary, + HashMap* vr_tx_binary) { if (xmlStrcmp(child->name, (xmlChar*)"Binary")) return; + xmlChar* start = NULL; for (xmlNodePtr _child = child->children; _child; _child = _child->next) { if (_child->type != XML_ELEMENT_NODE) continue; @@ -220,10 +224,14 @@ static inline void _parse_fmi3_binary( } if (strcmp((char*)causality, "input") == 0) { - hashmap_set_string(vr_rx_binary, (char*)vr, (char*)start); + hashmap_set_string( + vr_rx_binary, (char*)vr, start ? (char*)start : (char*)""); } else if (strcmp((char*)causality, "output") == 0) { hashmap_set_string(vr_tx_binary, (char*)vr, (char*)""); } + + /* Cleanup. */ + if (start) xmlFree(start); } @@ -240,20 +248,18 @@ static inline void _parse_fmi3_model_desc(HashMap* vr_rx_real, HashMap* vr_tx_re xmlChar* vr = xmlGetProp(child, (xmlChar*)"valueReference"); xmlChar* causality = xmlGetProp(child, (xmlChar*)"causality"); - xmlChar* start = NULL; if (vr == NULL || causality == NULL) goto next; _parse_fmi3_scalar( - child, vr, causality, start, vr_rx_real, vr_tx_real); + child, vr, causality, vr_rx_real, vr_tx_real); _parse_fmi3_binary( - child, vr, causality, start, vr_rx_binary, vr_tx_binary); + child, vr, causality, vr_rx_binary, vr_tx_binary); /* Cleanup. */ next: if (vr) xmlFree(vr); if (causality) xmlFree(causality); - if (start) xmlFree(start); } } xmlXPathFreeObject(xml_sv_obj); diff --git a/extra/external/CMakeLists.txt b/extra/external/CMakeLists.txt index f7aaae8..3024ff1 100644 --- a/extra/external/CMakeLists.txt +++ b/extra/external/CMakeLists.txt @@ -116,6 +116,20 @@ set(XML_INSTALL_DIR "${INSTALL_DIR}") file(MAKE_DIRECTORY "${XML_SOURCE_DIR}/include") file(MAKE_DIRECTORY "${XML_INSTALL_DIR}/include/libxml2") +# External Project - DSE Network Codec (unit test only) +# ----------------- +ExternalProject_Add(dse_ncodec + URL ${ExternalProject__DSE_NCODEC__URL} + HTTP_USERNAME ${ExternalProject__DSE_NCODEC__USERNAME} + HTTP_PASSWORD ${ExternalProject__DSE_NCODEC__PASSWORD} + SOURCE_DIR "$ENV{EXTERNAL_BUILD_DIR}/dse_ncodec" + SOURCE_SUBDIR "dse/ncodec/libs/automotive-bus" + BINARY_DIR "$ENV{EXTERNAL_BUILD_DIR}/dse_ncodec" + CMAKE_ARGS + -DCMAKE_C_FLAGS=${CMAKE_C_FLAGS} + INSTALL_COMMAND "" +) + # dlfcn-win32 # ----------- if(NOT WIN32) diff --git a/extra/external/oss_repos.cmake b/extra/external/oss_repos.cmake index 57e9e9b..6ca4b3e 100644 --- a/extra/external/oss_repos.cmake +++ b/extra/external/oss_repos.cmake @@ -10,7 +10,7 @@ set(ExternalProject__MODELC__URL https://github.com/boschglobal/dse.model set(ExternalProject__MODELC_LIB__URL https://github.com/boschglobal/dse.modelc/releases/download/v$ENV{DSE_MODELC_VERSION}/ModelC-$ENV{DSE_MODELC_VERSION}-$ENV{PACKAGE_ARCH}.zip) set(ExternalProject__DLFCNWIN32__URL https://github.com/dlfcn-win32/dlfcn-win32/archive/refs/tags/v1.3.0.tar.gz) set(ExternalProject__XML2__URL https://github.com/GNOME/libxml2/archive/refs/tags/v2.13.5.zip) - +set(ExternalProject__DSE_NCODEC__URL https://github.com/boschglobal/dse.standards/archive/refs/tags/v1.1.0.tar.gz) # Used by unit testing only (provided by ModelC for released targets). set(ExternalProject__YAML__URL https://github.com/yaml/libyaml/archive/0.2.5.tar.gz) diff --git a/extra/tools/fmi/README.md b/extra/tools/fmi/README.md index 78095c7..bed0772 100644 --- a/extra/tools/fmi/README.md +++ b/extra/tools/fmi/README.md @@ -85,6 +85,25 @@ Appending file: out/model/signalgroup.yaml $ ls out/model model.yaml* signalgroup.yaml* + +$ task generate-fmigateway \ + SIGNAL_GROUPS="extra/tools/fmi/test/testdata/fmigateway/SG1.yaml,extra/tools/fmi/test/testdata/fmigateway/SG2.yaml" +Running FMI Toolset command: gen-gateway +Options: + fmiVersion : 2 + libroot : /usr/local + log : 4 + outdir : out + session : + signalgroups : extra/tools/fmi/test/testdata/fmigateway/SG1.yaml,extra/tools/fmi/test/testdata/fmigateway/SG2.yaml + uuid : + version : 0.0.1 +Adding SignalGroup Model_1 to out/fmu.yaml +Adding SignalGroup Model_2 to out/fmu.yaml +Creating Model YAML: gateway (out/model.yaml) + +$ ls out/ +fmu.yaml modelDescription.xml model.yaml ``` diff --git a/extra/tools/fmi/cmd/fmi/main.go b/extra/tools/fmi/cmd/fmi/main.go index 9be5e9b..d899c48 100644 --- a/extra/tools/fmi/cmd/fmi/main.go +++ b/extra/tools/fmi/cmd/fmi/main.go @@ -20,6 +20,7 @@ var ( generate.NewFmiMclCommand("gen-mcl"), generate.NewGenSignalGroupCommand("gen-signalgroup"), generate.NewGenFmuAnnotationCommand("gen-annotations"), + generate.NewFmiGatewayCommand("gen-gateway"), patch.NewPatchSignalGroupCommand("patch-signalgroup"), } ) diff --git a/extra/tools/fmi/internal/app/generate/fmigateway.go b/extra/tools/fmi/internal/app/generate/fmigateway.go new file mode 100644 index 0000000..51c938f --- /dev/null +++ b/extra/tools/fmi/internal/app/generate/fmigateway.go @@ -0,0 +1,327 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package generate + +import ( + "flag" + "fmt" + "log/slog" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/boschdevcloud.com/dse.fmi/extra/tools/fmi/pkg/file/handler" + "github.com/boschdevcloud.com/dse.fmi/extra/tools/fmi/pkg/file/handler/kind" + "github.com/boschdevcloud.com/dse.fmi/extra/tools/fmi/pkg/file/index" + "github.com/boschdevcloud.com/dse.fmi/extra/tools/fmi/pkg/fmi/fmi2" + "github.com/boschdevcloud.com/dse.fmi/extra/tools/fmi/pkg/log" + schema_kind "github.com/boschglobal/dse.schemas/code/go/dse/kind" + "github.com/google/uuid" + "gopkg.in/yaml.v3" +) + +type GenFmiGatewayCommand struct { + commandName string + fs *flag.FlagSet + + logLevel int + signalGroups string + uuid string + version string + fmiVersion string + outdir string + session string + + libRootPath string +} + +func NewFmiGatewayCommand(name string) *GenFmiGatewayCommand { + c := &GenFmiGatewayCommand{commandName: name, fs: flag.NewFlagSet(name, flag.ExitOnError)} + + c.fs.IntVar(&c.logLevel, "log", 4, "Loglevel") + c.fs.StringVar(&c.signalGroups, "signalgroups", "", "A list of signalgroups separated by comma") + c.fs.StringVar(&c.uuid, "uuid", "", "UUID to assign to the FMU, set to '' to generate a new UUID") + c.fs.StringVar(&c.version, "version", "0.0.1", "Version to assign to the FMU") + c.fs.StringVar(&c.fmiVersion, "fmiVersion", "2", "Modelica FMI Version") + c.fs.StringVar(&c.outdir, "outdir", "out", "Output directory for the FMU file") + c.fs.StringVar(&c.session, "session", "", "Yaml file containing the session annotation") + + // Supports unit testing. + c.fs.StringVar(&c.libRootPath, "libroot", "/usr/local", "System lib root path (where lib & lib32 directory are found)") + + return c +} + +func (c GenFmiGatewayCommand) Name() string { + return c.commandName +} + +func (c GenFmiGatewayCommand) FlagSet() *flag.FlagSet { + return c.fs +} + +func (c *GenFmiGatewayCommand) Parse(args []string) error { + return c.fs.Parse(args) +} + +func (c *GenFmiGatewayCommand) Run() error { + slog.SetDefault(log.NewLogger(c.logLevel)) + if len(c.uuid) == 0 { + c.uuid = uuid.New().String() + } + + if err := os.MkdirAll(c.outdir, os.ModePerm); err != nil { + return err + } + if err := c.mergeSignalGroups(c.signalGroups, c.outdir); err != nil { + return err + } + fmuXml, channels, err := c.createFmuXml() + if err != nil { + return err + } + if err := c.generateModel(*fmuXml, channels); err != nil { + return err + } + fmuModelDescriptionFilename := filepath.Join(c.outdir, "modelDescription.xml") + if err := writeXml(fmuXml, fmuModelDescriptionFilename); err != nil { + return fmt.Errorf("could not generate the FMU Model Description (%v)", err) + } + return nil +} + +func (c *GenFmiGatewayCommand) patchSignal(vr *int, sg_type string, signals []schema_kind.Signal, used_signals *[]string) ([]schema_kind.Signal, error) { + var signal_list []schema_kind.Signal + if sg_type == "string" { + return signals, nil + } + for _, s := range signals { + if slices.Contains(*used_signals, s.Signal) { + continue + } + *used_signals = append(*used_signals, s.Signal) + slog.Info(fmt.Sprintf("Patch Signals: %s", s.Signal)) + causality := "" + if (*s.Annotations)["softecu_direction"] != nil { + switch (*s.Annotations)["softecu_direction"] { + case "M2E": + { + causality = "input" + break + } + case "E2M": + { + causality = "output" + break + } + default: + break + } + } + if (*s.Annotations)["direction"] != nil { + causality = ((*s.Annotations)["direction"]).(string) + } + if (*s.Annotations)["fmi_variable_causality"] != nil { + causality = ((*s.Annotations)["fmi_variable_causality"]).(string) + } + + annotations := schema_kind.Annotations{ + "fmi_variable_causality": causality, + "fmi_variable_vref": *vr, + "fmi_variable_type": sg_type, + "fmi_variable_name": s.Signal, + } + *s.Annotations = annotations + signal_list = append(signal_list, s) + *vr++ + } + return signal_list, nil +} + +func (c *GenFmiGatewayCommand) mergeSignalGroups(signalGroups string, outDir string) error { + signalGroupList := []string{} + if len(signalGroups) > 0 { + signalGroupList = strings.Split(signalGroups, ",") + } + vr := 0 + var signal_list []string + for _, signalGroup := range signalGroupList { + + // Load the Signal Group. + inputYaml, err := os.ReadFile(signalGroup) + if err != nil { + return fmt.Errorf("unable to read input file: %s (%w)", signalGroup, err) + } + sg := schema_kind.SignalGroup{} + if err := yaml.Unmarshal(inputYaml, &sg); err != nil { + return fmt.Errorf("unable to unmarshal signalgroup yaml: %s (%w)", signalGroup, err) + } + sg_type := "real" + if sg.Metadata.Annotations != nil { + if (*sg.Metadata.Annotations)["vector_type"] != nil { + if (*sg.Metadata.Annotations)["vector_type"] == "binary" { + sg_type = "string" + } + } + } + + signals := sg.Spec.Signals + + sg.Spec.Signals, err = c.patchSignal(&vr, sg_type, signals, &signal_list) + if err != nil { + return err + } + + if err := writeYaml(&sg, filepath.Join(outDir, "fmu.yaml"), true); err != nil { + return err + } + } + return nil +} + +func (c *GenFmiGatewayCommand) setGeneralXmlFields(fmuXml *fmi2.FmiModelDescription) { + // Basic FMU Information. + fmuXml.ModelName = "gateway" + fmuXml.FmiVersion = c.fmiVersion + fmuXml.Guid = fmt.Sprintf("{%s}", c.uuid) + fmuXml.GenerationTool = "DSE FMI - FMU Gateway" + fmuXml.GenerationDateAndTime = time.Now().String() + fmuXml.Author = "Robert Bosch GmbH" + fmuXml.Version = c.version + fmuXml.Description = "Gateway to connect to the DSE Simbus" + + // Runtime Information. + fmuXml.DefaultExperiment.StartTime = "0.0" + fmuXml.DefaultExperiment.StopTime = "9999" + fmuXml.DefaultExperiment.StepSize = "0.0005" + + // Model Information. + fmuXml.CoSimulation.ModelIdentifier = fmt.Sprintf("libfmi%sgateway", c.fmiVersion) // This is the packaged SO/DLL file. + fmuXml.CoSimulation.CanHandleVariableCommunicationStepSize = "true" + fmuXml.CoSimulation.CanInterpolateInputs = "true" +} + +func (c *GenFmiGatewayCommand) buildXmlSignals(fmuXml *fmi2.FmiModelDescription, index *index.YamlFileIndex) ([]string, error) { + var channels []string + if signalGroupDocs, ok := index.DocMap["SignalGroup"]; ok { + for _, doc := range signalGroupDocs { + fmt.Fprintf(flag.CommandLine.Output(), "Adding SignalGroup %s to %s\n", doc.Metadata.Name, doc.File) + signalGroupSpec := doc.Spec.(*schema_kind.SignalGroupSpec) + if doc.Metadata.Annotations["vector_type"] == "binary" { + if err := fmi2.BinarySignal(*signalGroupSpec, fmuXml); err != nil { + slog.Warn(fmt.Sprintf("Skipped BinarySignal (Error: %s)", err.Error())) + } + } else { + if err := fmi2.ScalarSignal(*signalGroupSpec, fmuXml); err != nil { + slog.Debug(fmt.Sprintf("Skipped Scalarsignal (Error: %s)", err.Error())) + } + } + if !slices.Contains(channels, doc.Metadata.Labels["channel"]) { + channels = append(channels, doc.Metadata.Labels["channel"]) + slog.Debug(fmt.Sprintf("added %s", doc.Metadata.Labels["channel"])) + } + } + } + return channels, nil +} + +func (c *GenFmiGatewayCommand) createFmuXml() (*fmi2.FmiModelDescription, []string, error) { + fmuXml := fmi2.FmiModelDescription{} + c.setGeneralXmlFields(&fmuXml) + + index := index.NewYamlFileIndex() + _, docs, err := handler.ParseFile(filepath.Join(c.outdir, "fmu.yaml")) + if err != nil { + return nil, nil, fmt.Errorf("parse failed (%s) on file: %s", err.Error(), "fmu.yaml") + } + for _, doc := range docs.([]kind.KindDoc) { + slog.Info(fmt.Sprintf("kind: %s; name=%s (%s)", doc.Kind, doc.Metadata.Name, doc.File)) + if _, ok := index.DocMap[doc.Kind]; !ok { + index.DocMap[doc.Kind] = []kind.KindDoc{} + } + index.DocMap[doc.Kind] = append(index.DocMap[doc.Kind], doc) + } + channels, err := c.buildXmlSignals(&fmuXml, index) + if err != nil { + return nil, nil, err + } + return &fmuXml, channels, nil +} + +func (c *GenFmiGatewayCommand) generateChannels(channel_map []string) ([]schema_kind.Channel, error) { + channels := []schema_kind.Channel{} + for i := range channel_map { + channel := schema_kind.Channel{ + Alias: stringPtr(channel_map[i]), + Selectors: &schema_kind.Labels{ + "channel": channel_map[i], + }, + } + channels = append(channels, channel) + } + return channels, nil +} + +func (c *GenFmiGatewayCommand) generateModel(fmiMD fmi2.FmiModelDescription, channel_map []string) error { + + // Build the model.xml + nodeExists := func(Node any) string { + if Node != nil { + return "true" + } + return "false" + } + + model := schema_kind.Model{ + Kind: "Model", + Metadata: &schema_kind.ObjectMetadata{ + Name: &fmiMD.ModelName, + Annotations: &schema_kind.Annotations{ + "fmi_version": fmiMD.FmiVersion, + "fmi_model_cosim": nodeExists(fmiMD.CoSimulation), + "fmi_stepsize": fmiMD.DefaultExperiment.StepSize, + "fmi_endtime": fmiMD.DefaultExperiment.StopTime, + "fmi_guid": fmiMD.Guid, + "fmi_resource_dir": "resources", + "loglevel": 4, + }, + }, + } + + if len(c.session) != 0 { + inputYaml, err := os.ReadFile(c.session) + if err != nil { + return fmt.Errorf("unable to read input file: %s (%w)", "session.yaml", err) + } + ann := schema_kind.ObjectMetadata{} + if err := yaml.Unmarshal(inputYaml, &ann); err != nil { + return fmt.Errorf("unable to unmarshal signalgroup yaml: %s (%w)", "session.yaml", err) + } + (*model.Metadata.Annotations)["session"] = (*ann.Annotations)["session"] + } + + channels, err := c.generateChannels(channel_map) + if err != nil { + return fmt.Errorf("could not generate channels: (%v)", err) + } + spec := schema_kind.ModelSpec{ + Channels: &channels, + } + model.Spec = spec + + // Write the Model + if err := os.MkdirAll(c.outdir, 0755); err != nil { + return fmt.Errorf("error Creating Directory for Yaml output: %v", err) + } + modelYamlPath := filepath.Join(c.outdir, "model.yaml") + fmt.Fprintf(flag.CommandLine.Output(), "Creating Model YAML: %s (%s)\n", *model.Metadata.Name, modelYamlPath) + if err := writeYaml(&model, modelYamlPath, false); err != nil { + return fmt.Errorf("error writing YAML: %v", err) + } + + return nil +} diff --git a/extra/tools/fmi/internal/app/generate/fmimcl.go b/extra/tools/fmi/internal/app/generate/fmimcl.go index f5437f1..185737b 100644 --- a/extra/tools/fmi/internal/app/generate/fmimcl.go +++ b/extra/tools/fmi/internal/app/generate/fmimcl.go @@ -26,12 +26,6 @@ type FmiMclCommand struct { logLevel int } -type channel []struct { - Alias string `json:"alias"` - Name string `json:"name"` - Selector map[string]string `json:"selector"` -} - func NewFmiMclCommand(name string) *FmiMclCommand { c := &FmiMclCommand{commandName: name, fs: flag.NewFlagSet(name, flag.ExitOnError)} c.fs.StringVar(&c.fmupath, "fmu", "", "Path to FMU") diff --git a/extra/tools/fmi/internal/app/generate/fmimodelc_test.go b/extra/tools/fmi/internal/app/generate/fmimodelc_test.go index 8546280..4b0f929 100644 --- a/extra/tools/fmi/internal/app/generate/fmimodelc_test.go +++ b/extra/tools/fmi/internal/app/generate/fmimodelc_test.go @@ -24,26 +24,24 @@ func TestFmiModelc_ModelDescription(t *testing.T) { assert.Equal(t, "{11111111-2222-3333-4444-555555555555}", fmiMD.Guid, "Guid should match") assert.Equal(t, "fmi2modelcfmu", fmiMD.CoSimulation.ModelIdentifier, "ModelIdentifier should match") - ann := &[]fmi2.Annotations{ - { - Tool: []fmi2.Tool{ - { - Name: "dse.standards.fmi-ls-binary-codec", - Annotation: &[]fmi2.Annotation{ - {Name: "mimetype", Text: "application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; node_id=2; interface_id=0"}, - }, + ann := &fmi2.Annotations{ + Tool: []fmi2.Tool{ + { + Name: "dse.standards.fmi-ls-binary-codec", + Annotation: []fmi2.Annotation{ + {Name: "mimetype", Text: "application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; node_id=2; interface_id=0"}, }, - { - Name: "dse.standards.fmi-ls-binary-to-text", - Annotation: &[]fmi2.Annotation{ - {Name: "encoding", Text: "ascii85"}, - }, + }, + { + Name: "dse.standards.fmi-ls-binary-to-text", + Annotation: []fmi2.Annotation{ + {Name: "encoding", Text: "ascii85"}, }, - { - Name: "dse.standards.fmi-ls-bus-topology", - Annotation: &[]fmi2.Annotation{ - {Name: "bus_id", Text: "1"}, - }, + }, + { + Name: "dse.standards.fmi-ls-bus-topology", + Annotation: []fmi2.Annotation{ + {Name: "bus_id", Text: "1"}, }, }, }, diff --git a/extra/tools/fmi/internal/app/generate/signalgroup.go b/extra/tools/fmi/internal/app/generate/signalgroup.go index 5473cb0..7197dda 100644 --- a/extra/tools/fmi/internal/app/generate/signalgroup.go +++ b/extra/tools/fmi/internal/app/generate/signalgroup.go @@ -127,7 +127,10 @@ func (c *GenSignalGroupCommand) generateSignalVector(fmiMD fmi2.FmiModelDescript }, } signalVector.Spec.Signals = scalarSignals - writeYaml(&signalVector, c.outputFile, true) + if err := writeYaml(&signalVector, c.outputFile, true); err != nil { + return err + } + if len(binarySignals) > 0 { networkVector := kind.SignalGroup{ Kind: "SignalGroup", @@ -143,7 +146,9 @@ func (c *GenSignalGroupCommand) generateSignalVector(fmiMD fmi2.FmiModelDescript }, } networkVector.Spec.Signals = binarySignals - writeYaml(&networkVector, c.outputFile, true) + if err := writeYaml(&networkVector, c.outputFile, true); err != nil { + return err + } } return nil diff --git a/extra/tools/fmi/pkg/fmi/fmi2/xml.go b/extra/tools/fmi/pkg/fmi/fmi2/xml.go index 5bd9970..754a2c0 100644 --- a/extra/tools/fmi/pkg/fmi/fmi2/xml.go +++ b/extra/tools/fmi/pkg/fmi/fmi2/xml.go @@ -230,10 +230,10 @@ func BinarySignal(signalGroupSpec schema_kind.SignalGroupSpec, FmiXml *FmiModelD for _, signal := range signalGroupSpec.Signals { var rx_vref []interface{} var tx_vref []interface{} - if rx_vref := (*signal.Annotations)["dse.standards.fmi-ls-bus-topology.rx_vref"]; rx_vref == nil { + if rx_vref = (*signal.Annotations)["dse.standards.fmi-ls-bus-topology.rx_vref"].([]interface{}); rx_vref == nil { return fmt.Errorf("Could not get rx_vref for signal %s", signal.Signal) } - if tx_vref := (*signal.Annotations)["dse.standards.fmi-ls-bus-topology.tx_vref"]; tx_vref == nil { + if tx_vref = (*signal.Annotations)["dse.standards.fmi-ls-bus-topology.tx_vref"].([]interface{}); tx_vref == nil { return fmt.Errorf("Could not get tx_vref for signal %s", signal.Signal) } if len(rx_vref) != len(tx_vref) { diff --git a/extra/tools/fmi/pkg/patch/signalgroup.go b/extra/tools/fmi/pkg/patch/signalgroup.go index e5fcdd9..ee6144e 100644 --- a/extra/tools/fmi/pkg/patch/signalgroup.go +++ b/extra/tools/fmi/pkg/patch/signalgroup.go @@ -20,9 +20,9 @@ type PatchSignalGroupCommand struct { name string fs *flag.FlagSet - signalGroupFile string - signalPatchFile string - removeUnknown bool + signalGroupFile string + signalPatchFile string + removeUnknown bool } func NewPatchSignalGroupCommand(name string) *PatchSignalGroupCommand { @@ -45,7 +45,6 @@ func (c *PatchSignalGroupCommand) Parse(args []string) error { return c.fs.Parse(args) } - func (c *PatchSignalGroupCommand) Run() error { inputYaml, err := os.ReadFile(c.signalGroupFile) if err != nil { @@ -74,7 +73,6 @@ func (c *PatchSignalGroupCommand) Run() error { return nil } - func patchSignals(sigGroup *kind.SignalGroup, patchFile string, removeUnknown bool) error { // Load the CSV patch file. f, err := os.Open(patchFile) diff --git a/extra/tools/fmi/test/testdata/fmigateway/SG1.yaml b/extra/tools/fmi/test/testdata/fmigateway/SG1.yaml new file mode 100644 index 0000000..21d1c1c --- /dev/null +++ b/extra/tools/fmi/test/testdata/fmigateway/SG1.yaml @@ -0,0 +1,29 @@ +--- +kind: SignalGroup +metadata: + labels: + channel: E2M_M2E + model: Model_1 + name: Model_1 +spec: + signals: + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: M2E + softecu_name: softecu_name_1 + signal: softecu_name_1_changed + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: E2M + softecu_name: softecu_name_2 + signal: softecu_name_2_changed + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: M2E + softecu_name: softecu_name_3 + signal: softecu_name_3_changed + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: E2M + softecu_name: softecu_name_4 + signal: softecu_name_4_changed diff --git a/extra/tools/fmi/test/testdata/fmigateway/SG2.yaml b/extra/tools/fmi/test/testdata/fmigateway/SG2.yaml new file mode 100644 index 0000000..4cd9ede --- /dev/null +++ b/extra/tools/fmi/test/testdata/fmigateway/SG2.yaml @@ -0,0 +1,29 @@ +--- +kind: SignalGroup +metadata: + labels: + channel: E2M_M2E + model: Model_2 + name: Model_2 +spec: + signals: + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: M2E + softecu_name: softecu_name_5 + signal: softecu_name_5_changed + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: E2M + softecu_name: softecu_name_6 + signal: softecu_name_6_changed + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: M2E + softecu_name: softecu_name_1 + signal: softecu_name_1_changed + - annotations: + softecu_access_function: E2M+M2E + softecu_direction: E2M + softecu_name: softecu_name_2 + signal: softecu_name_2_changed diff --git a/extra/tools/fmi/test/testdata/fmigateway/SG3.yaml b/extra/tools/fmi/test/testdata/fmigateway/SG3.yaml new file mode 100644 index 0000000..4ffee9c --- /dev/null +++ b/extra/tools/fmi/test/testdata/fmigateway/SG3.yaml @@ -0,0 +1,25 @@ +--- +kind: SignalGroup +metadata: + labels: + channel: E2M_M2E + model: Model_3 + name: Model_3 +spec: + signals: + - annotations: + direction: input + mcl_fmi_variable_id: '1342177280' + mcl_fmi_variable_name: fmu_signal_1 + mcl_fmi_variable_start: '0.0' + mcl_fmi_variable_type: Real + org-fmu-name: fmu_signal_1 + signal: fmu_signal_1_changed + - annotations: + direction: output + mcl_fmi_variable_id: '1342177280' + mcl_fmi_variable_name: fmu_signal_2 + mcl_fmi_variable_start: '0.0' + mcl_fmi_variable_type: Real + org-fmu-name: fmu_signal_2 + signal: fmu_signal_2_changed diff --git a/extra/tools/fmi/test/testdata/fmigateway/SG4.yaml b/extra/tools/fmi/test/testdata/fmigateway/SG4.yaml new file mode 100644 index 0000000..fbe1b6d --- /dev/null +++ b/extra/tools/fmi/test/testdata/fmigateway/SG4.yaml @@ -0,0 +1,15 @@ +--- +kind: SignalGroup +metadata: + labels: + channel: E2M_M2E + model: Model_4 + name: Model_4 +spec: + signals: + - signal: signal_1 + annotations: + direction: input + - signal: signal_2 + annotations: + direction: output diff --git a/extra/tools/fmi/test/testdata/fmigateway/SG5.yaml b/extra/tools/fmi/test/testdata/fmigateway/SG5.yaml new file mode 100644 index 0000000..b061223 --- /dev/null +++ b/extra/tools/fmi/test/testdata/fmigateway/SG5.yaml @@ -0,0 +1,20 @@ +--- +kind: SignalGroup +metadata: + labels: + channel: com_phys + name: Model_5 +spec: + signals: + - annotations: + direction: input + signal: signal_1 + - annotations: + direction: output + signal: signal_2 + - annotations: + direction: input + signal: signal_3 + - annotations: + direction: output + signal: signal_4 diff --git a/extra/tools/fmi/test/testdata/fmigateway/SG6.yaml b/extra/tools/fmi/test/testdata/fmigateway/SG6.yaml new file mode 100644 index 0000000..d91333d --- /dev/null +++ b/extra/tools/fmi/test/testdata/fmigateway/SG6.yaml @@ -0,0 +1,32 @@ +--- +kind: SignalGroup +metadata: + name: network_vector + labels: + channel: network_vector + annotations: + vector_type: binary +spec: + signals: + - signal: can + annotations: + mime_type: 'application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; node_id=2; interface_id=0' + # + # FMI Value Reference + # ------------------- + fmi_variable_vref: 4 + fmi_variable_vref_input: 4 + fmi_variable_vref_output: 5 + # + # FMI LS Text Encoding (applied to FMI String Variables): + # -------------------- + dse.standards.fmi-ls-binary-to-text.encoding: ascii85 + # (supporting annotations to avoid XML parsing) + dse.standards.fmi-ls-binary-to-text.vref: [2, 3, 4, 5, 6, 7, 8, 9] + # + # FMI LS Bus Topology (applied to FMI String/Binary Variables): + # ------------------- + dse.standards.fmi-ls-bus-topology.bus_id: 1 + # (supporting annotations to avoid XML parsing) + dse.standards.fmi-ls-bus-topology.rx_vref: [2, 4, 6, 8] + dse.standards.fmi-ls-bus-topology.tx_vref: [3, 5, 7, 9] diff --git a/extra/tools/fmi/test/testdata/fmigateway/session.yaml b/extra/tools/fmi/test/testdata/fmigateway/session.yaml new file mode 100644 index 0000000..2952464 --- /dev/null +++ b/extra/tools/fmi/test/testdata/fmigateway/session.yaml @@ -0,0 +1,30 @@ +--- +annotations: + session: + initialization: + path: 'foo' + file: 'bar' + shutdown: + path: 'foo_shutdown' + file: 'bar_shutdown' + windows: + - model: 'Model_1' + annotations: + stepsize: 0.1 + endtime: 0.2 + loglevel: 6 + timeout: 60.0 + path: 'foo' + file: 'bar' + yaml: 'foo;bar' + show: true + - model: 'Model_2' + annotations: + stepsize: 0.15 + endtime: 0.25 + loglevel: 5 + timeout: 61.0 + path: 'foo_2' + file: 'bar_2' + yaml: 'foo_2;bar_2' + show: false diff --git a/tests/cmocka/CMakeLists.txt b/tests/cmocka/CMakeLists.txt index 504f4c7..02f5128 100644 --- a/tests/cmocka/CMakeLists.txt +++ b/tests/cmocka/CMakeLists.txt @@ -61,6 +61,7 @@ FetchContent_MakeAvailable(dse_clib) set(DSE_CLIB_SOURCE_DIR ${dse_clib_SOURCE_DIR}/dse/clib) set(DSE_CLIB_SOURCE_FILES ${DSE_CLIB_SOURCE_DIR}/util/binary.c + ${DSE_CLIB_SOURCE_DIR}/util/strings.c ${DSE_CLIB_SOURCE_DIR}/util/yaml.c ${DSE_CLIB_SOURCE_DIR}/collections/hashmap.c ${DSE_CLIB_SOURCE_DIR}/collections/set.c @@ -225,3 +226,4 @@ install( add_subdirectory(fmimodelc) add_subdirectory(fmu) +add_subdirectory(fmigateway) diff --git a/tests/cmocka/Makefile b/tests/cmocka/Makefile index 03e63f9..56ec729 100644 --- a/tests/cmocka/Makefile +++ b/tests/cmocka/Makefile @@ -27,6 +27,7 @@ run: cd build/_out; $(GDB_CMD) bin/test_mstep cd build/_out; $(GDB_CMD) bin/test_fmimodelc cd build/_out; $(GDB_CMD) bin/test_fmu + cd build/_out; $(GDB_CMD) bin/test_fmigateway clean: rm -rf build diff --git a/tests/cmocka/fmigateway/CMakeLists.txt b/tests/cmocka/fmigateway/CMakeLists.txt new file mode 100644 index 0000000..f1db1cc --- /dev/null +++ b/tests/cmocka/fmigateway/CMakeLists.txt @@ -0,0 +1,121 @@ +# Copyright 2024 Robert Bosch GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.21) + +# set(CMAKE_VERBOSE_MAKEFILE ON) + +project(test_fmigateway) + + + +# FMI ModelC Code +# =============== +set(DSE_FMIGATEWAY_SOURCE_DIR ../../../dse/fmigateway) +set(DSE_FMI2FMU_SOURCE_DIR ../../../dse/fmu) +set(DSE_FMIGATEWAY_SOURCE_FILES + ${DSE_FMI2FMU_SOURCE_DIR}/fmi2fmu.c + ${DSE_FMI2FMU_SOURCE_DIR}/ascii85.c + ${DSE_FMI2FMU_SOURCE_DIR}/signal.c + ${DSE_FMIGATEWAY_SOURCE_DIR}/fmigateway.c + ${DSE_FMIGATEWAY_SOURCE_DIR}/index.c + ${DSE_FMIGATEWAY_SOURCE_DIR}/session.c + ${DSE_FMIGATEWAY_SOURCE_DIR}/parser.c +) +set(DSE_FMIGATEWAY_INCLUDE_DIR "${DSE_FMIGATEWAY_SOURCE_DIR}/../..") + +set(FMI2_INCLUDE_DIR "${DSE_CLIB_SOURCE_DIR}/fmi/fmi2/headers") + +# External Project - DSE Network Codec +# ------------------------------------ +set(DSE_NCODEC_SOURCE_DIR "$ENV{EXTERNAL_BUILD_DIR}/dse_ncodec") +set(DSE_NCODEC_BINARY_DIR "$ENV{EXTERNAL_BUILD_DIR}/dse_ncodec") +find_library(DSE_NCODEC_LIB + NAMES + libautomotive-bus-codec.a + PATHS + ${DSE_NCODEC_BINARY_DIR} + REQUIRED + NO_DEFAULT_PATH +) +add_library(dse_ncodec STATIC IMPORTED GLOBAL) +set_target_properties(dse_ncodec + PROPERTIES + IMPORTED_LOCATION "${DSE_NCODEC_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${DSE_NCODEC_SOURCE_DIR}" +) +set(DSE_NCODEC_SOURCE_FILES + ${DSE_NCODEC_SOURCE_DIR}/dse/ncodec/codec.c +) +set(DSE_NCODEC_INCLUDE_DIR "${DSE_NCODEC_SOURCE_DIR}") +set(DSE_MODELC_SOURCE_FILES + ${DSE_MODELC_SOURCE_DIR}/model/gateway.c + ${DSE_MODELC_SOURCE_DIR}/model/model.c + ${DSE_MODELC_SOURCE_DIR}/model/ncodec.c + ${DSE_MODELC_SOURCE_DIR}/model/schema.c + ${DSE_MODELC_SOURCE_DIR}/model/signal.c + ${DSE_MODELC_SOURCE_DIR}/model/trace.c + + ${DSE_MODELC_SOURCE_DIR}/controller/controller_stub.c + ${DSE_MODELC_SOURCE_DIR}/controller/model_function.c + ${DSE_MODELC_SOURCE_DIR}/controller/loader.c + ${DSE_MODELC_SOURCE_DIR}/controller/modelc.c + ${DSE_MODELC_SOURCE_DIR}/controller/modelc_debug.c + ${DSE_MODELC_SOURCE_DIR}/controller/modelc_args.c + ${DSE_MODELC_SOURCE_DIR}/controller/step.c + ${DSE_MODELC_SOURCE_DIR}/controller/model_runtime.c + + ${DSE_MODELC_SOURCE_DIR}/adapter/adapter_loopb.c + ${DSE_MODELC_SOURCE_DIR}/adapter/index.c +) + +# Targets +# ======= + +# Target - FMI Gateway +# ------------------- +add_executable(test_fmigateway + __test__.c + test_parser.c + test_fmi2.c + ${DSE_NCODEC_SOURCE_FILES} + ${DSE_FMIGATEWAY_SOURCE_FILES} + ${DSE_MODELC_SOURCE_FILES} + ${DSE_CLIB_SOURCE_FILES} +) +target_include_directories(test_fmigateway + PRIVATE + ${DSE_NCODEC_INCLUDE_DIR} + ${DSE_FMIGATEWAY_INCLUDE_DIR} + ${DSE_MODELC_INCLUDE_DIR} + ${DSE_CLIB_INCLUDE_DIR} + ${FMI2_INCLUDE_DIR} + ${YAML_SOURCE_DIR}/include + ./ +) +target_compile_definitions(test_fmigateway + PUBLIC + CMOCKA_TESTING + PRIVATE + PLATFORM_OS="${CDEF_PLATFORM_OS}" + PLATFORM_ARCH="${CDEF_PLATFORM_ARCH}" + MODELC_VERSION="${DSE_MODELC_VERSION}" +) +target_link_libraries(test_fmigateway + PUBLIC + PRIVATE + xml + dse_ncodec + yaml + cmocka + dl + m +) +install(TARGETS test_fmigateway) +install( + FILES + DESTINATION + data/fmigateway +) + diff --git a/tests/cmocka/fmigateway/__test__.c b/tests/cmocka/fmigateway/__test__.c new file mode 100644 index 0000000..6f2485d --- /dev/null +++ b/tests/cmocka/fmigateway/__test__.c @@ -0,0 +1,24 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + + +uint8_t __log_level__; /* LOG_ERROR LOG_INFO LOG_DEBUG LOG_TRACE */ + + +extern int run_fmigateway__parser_tests(void); +extern int run_fmigateway__fmi2_tests(void); + + +int main() +{ + __log_level__ = LOG_QUIET; // LOG_DEBUG;//LOG_QUIET; + + int rc = 0; + rc |= run_fmigateway__parser_tests(); + rc |= run_fmigateway__fmi2_tests(); + return rc; +} diff --git a/tests/cmocka/fmigateway/data/fmu.yaml b/tests/cmocka/fmigateway/data/fmu.yaml new file mode 100644 index 0000000..64848d7 --- /dev/null +++ b/tests/cmocka/fmigateway/data/fmu.yaml @@ -0,0 +1,90 @@ +--- +kind: SignalGroup +metadata: + name: scalar_vector + labels: + channel: scalar + annotations: + vector_type: scalar +spec: + signals: + - signal: scalar_in + annotations: + fmi_variable_vref: 1001 + fmi_variable_name: scalar_in + fmi_variable_type: Real + fmi_variable_causality: input + - signal: scalar_out + annotations: + fmi_variable_vref: 1002 + fmi_variable_name: scalar_out + fmi_variable_type: Real + fmi_variable_causality: output +# - signal: scalar_param +# annotations: +# fmi_variable_vref: 1003 +# fmi_variable_name: scalar_param +# fmi_variable_type: Real +# fmi_variable_causality: parameter +--- +kind: SignalGroup +metadata: + name: scalar_vector2 + labels: + channel: scalar + annotations: + vector_type: scalar +spec: + signals: + - signal: scalar_in2 + annotations: + fmi_variable_vref: 1004 + fmi_variable_name: scalar_in2 + fmi_variable_type: Real + fmi_variable_causality: input + - signal: scalar_out2 + annotations: + fmi_variable_vref: 1005 + fmi_variable_name: scalar_out2 + fmi_variable_type: Real + fmi_variable_causality: output +# - signal: scalar_param +# annotations: +# fmi_variable_vref: 1003 +# fmi_variable_name: scalar_param +# fmi_variable_type: Real +# fmi_variable_causality: parameter +--- +kind: SignalGroup +metadata: + name: binary_vector + labels: + channel: binary + type: binary + annotations: + vector_type: binary + vector_name: network_vector +spec: + signals: + - signal: can + annotations: + mime_type: 'application/x-automotive-bus; interface=stream; type=frame; bus=can; schema=fbs; bus_id=1; node_id=2; interface_id=0' + # + # FMI Value Reference + # ------------------- + fmi_variable_vref: 4 + fmi_variable_vref_input: 4 + fmi_variable_vref_output: 5 + # + # FMI LS Text Encoding (applied to FMI String Variables): + # -------------------- + dse.standards.fmi-ls-binary-to-text.encoding: ascii85 + # (supporting annotations to avoid XML parsing) + dse.standards.fmi-ls-binary-to-text.vref: [2, 3, 4, 5, 6, 7, 8, 9] + # + # FMI LS Bus Topology (applied to FMI String/Binary Variables): + # ------------------- + dse.standards.fmi-ls-bus-topology.bus_id: 1 + # (supporting annotations to avoid XML parsing) + dse.standards.fmi-ls-bus-topology.rx_vref: [2, 4, 6, 8] + dse.standards.fmi-ls-bus-topology.tx_vref: [3, 5, 7, 9] diff --git a/tests/cmocka/fmigateway/data/model.yaml b/tests/cmocka/fmigateway/data/model.yaml new file mode 100644 index 0000000..6643be5 --- /dev/null +++ b/tests/cmocka/fmigateway/data/model.yaml @@ -0,0 +1,52 @@ +--- +kind: Model +metadata: + name: Gateway + annotations: + adapter: 'fmi' + fmi_version: '2.0' + fmi_model_cosim: true + fmi_model_version: '1.48' + fmi_stepsize: '0.0005' + fmi_endtime: '0.002' + fmi_guid: '{11111111-2222-3333-4444-555555555555}' + fmi_resource_dir: 'data' + set_init_values: true + loglevel: 6 +spec: + runtime: + gateway: {} + channels: + - alias: scalar_vector + selectors: + channel: scalar + - alias: binary_vector + selectors: + channel: binary +--- +kind: Model +metadata: + name: Target +spec: + runtime: + dynlib: + - os: linux + arch: amd64 + path: model/libgatewaytarget.so + - os: linux + arch: x86 + path: model/libgatewaytarget.so + - os: windows + arch: x64 + path: model/libgatewaytarget.dll + - os: windows + arch: x86 + path: model/libgatewaytarget.dll + channels: + - alias: scalar_vector + selectors: + side: scalar_vector + - alias: binary_vector + selectors: + channel: binary +--- diff --git a/tests/cmocka/fmigateway/data/model_parser.yaml b/tests/cmocka/fmigateway/data/model_parser.yaml new file mode 100644 index 0000000..ba44239 --- /dev/null +++ b/tests/cmocka/fmigateway/data/model_parser.yaml @@ -0,0 +1,58 @@ +--- +kind: Model +metadata: + name: Gateway + annotations: + adapter: 'fmi' + fmi_version: '2.0' + fmi_model_cosim: true + fmi_model_version: '1.48' + fmi_stepsize: '0.005' + fmi_endtime: '0.02' + fmi_guid: '{11111111-2222-3333-4444-555555555555}' + fmi_resource_dir: 'data' + set_init_values: true + loglevel: 6 + loglocation: 'foo/bar' + session: + initialization: + path: 'foo' + file: 'bar' + shutdown: + path: 'foo_shutdown' + file: 'bar_shutdown' + windows: + - model: 'Model_1' + annotations: + stepsize: 0.1 + endtime: 0.2 + loglevel: 6 + timeout: 60.0 + path: 'foo' + file: 'bar' + yaml: 'foo;bar' + show: true + - model: 'Model_2' + annotations: + stepsize: 0.15 + endtime: 0.25 + loglevel: 5 + timeout: 61.0 + path: 'foo_2' + file: 'bar_2' + yaml: 'foo_2;bar_2' + show: false + linux: + local: + path: 'foo' + file: 'bar' +spec: + runtime: + gateway: {} + channels: + - alias: scalar_vector + selectors: + channel: scalar + - alias: binary_vector + selectors: + channel: binary diff --git a/tests/cmocka/fmigateway/data/stack.yaml b/tests/cmocka/fmigateway/data/stack.yaml new file mode 100644 index 0000000..f2736f1 --- /dev/null +++ b/tests/cmocka/fmigateway/data/stack.yaml @@ -0,0 +1,48 @@ +--- +kind: Stack +metadata: + name: stack +spec: + connection: + transport: + redispubsub: + uri: redis://localhost:6379 + timeout: 60 + models: + - name: simbus + model: + name: simbus + channels: + - name: scalar + expectedModelCount: 2 + - name: binary + expectedModelCount: 2 + - name: gateway + uid: 42 + model: + name: Gateway + channels: + - name: scalar + alias: scalar_vector + selectors: + channel: scalar + - name: binary + alias: binary_vector + selectors: + channel: binary + - name: target_inst + uid: 43 + model: + name: Target + runtime: + files: + - fmu/resources/fmu.yaml + channels: + - name: scalar + alias: scalar_vector + selectors: + channel: scalar + - name: binary + alias: binary_vector + selectors: + channel: binary diff --git a/tests/cmocka/fmigateway/test_fmi2.c b/tests/cmocka/fmigateway/test_fmi2.c new file mode 100644 index 0000000..e70cb00 --- /dev/null +++ b/tests/cmocka/fmigateway/test_fmi2.c @@ -0,0 +1,279 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#define UNUSED(x) ((void)x) +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + + +extern uint8_t __log_level__; + + +typedef struct fmi2_setup { + fmi2String instance_name; + fmi2Type fmu_type; + fmi2String fmu_guid; + fmi2String fmu_resource_location; + fmi2CallbackFunctions* functions; + fmi2Boolean visible; + fmi2Boolean logging_on; + Controller* controller; + Endpoint* endpoint; +} fmi2_setup; + +void stub_setup_objects(Controller* c, Endpoint* e); +void stub_release_objects(Controller* c, Endpoint* e); + + +void _fmi2_unit_test_logger(fmi2ComponentEnvironment componentEnvironment, + fmi2String instanceName, fmi2Status status, fmi2String category, + fmi2String message, ...) +{ + UNUSED(componentEnvironment); + UNUSED(instanceName); + + static const char* statusString[] = { "OK", "Warning", "Discard", "Error", + "Fatal", "Pending" }; + + if ((strcmp(category, "Debug") == 0) && ( __log_level__ > LOG_DEBUG)) { + return; + } + if ((strcmp(category, "Simbus") == 0) && ( __log_level__ > LOG_SIMBUS)) { + return; + } + if ((strcmp(category, "Info") == 0) && ( __log_level__ > LOG_INFO)) { + return; + } + if ((strcmp(category, "Notice") == 0) && ( __log_level__ > LOG_NOTICE)) { + return; + } + if ((strcmp(category, "Quiet") == 0) && ( __log_level__ > LOG_QUIET)) { + return; + } + + printf("[%s:%s] %s\n", category, statusString[status], message); + fflush(stdout); +} + + +int test_fmigateway__fmi2_setup(void** state) +{ + fmi2_setup* setup = calloc(1, sizeof(fmi2_setup)); + /* commit with log level in model.yaml set to 6. */ + setup->logging_on = fmi2True; + setup->fmu_guid = "{1-22-333-4444-55555-666666-7777777}"; + setup->fmu_type = fmi2CoSimulation; + setup->instance_name = "test_inst"; + setup->visible = fmi2True; + setup->fmu_resource_location = "../../../../tests/cmocka/fmigateway/data"; + setup->functions = calloc(1, sizeof(fmi2CallbackFunctions)); + setup->functions->logger = _fmi2_unit_test_logger; + + setup->controller = calloc(1, sizeof(Controller)); + setup->controller->adapter = calloc(1, sizeof(Adapter)); + setup->endpoint = calloc(1, sizeof(Endpoint)); + stub_setup_objects(setup->controller, setup->endpoint); + + *state = setup; + + return 0; +} + + +int test_fmigateway__fmi2_teardown(void** state) +{ + fmi2_setup* setup = *state; + free(setup->functions); + + if (setup && setup->controller) { + if (setup->controller->adapter) free(setup->controller->adapter); + free(setup->controller); + } + if (setup && setup->endpoint) free(setup->endpoint); + + stub_release_objects(setup->controller, setup->endpoint); + free(setup); + + return 0; +} + +void test_fmigateway__fmi2_instantiate(void** state) +{ + fmi2_setup* setup = *state; + + FmuInstanceData* inst = fmi2Instantiate(setup->instance_name, + setup->fmu_type, setup->fmu_guid, setup->fmu_resource_location, + setup->functions, setup->visible, setup->logging_on); + + /* Check instance data. */ + assert_non_null(inst->instance.guid); + assert_string_equal(inst->instance.guid, setup->fmu_guid); + assert_non_null(inst->instance.name); + assert_string_equal(inst->instance.name, setup->instance_name); + assert_non_null(inst->instance.resource_location); + assert_string_equal( + inst->instance.resource_location, setup->fmu_resource_location); + assert_non_null(inst->instance.type); + assert_int_equal(inst->instance.type, setup->fmu_type); + + /* Check gateway data. */ + assert_non_null(inst->data); + + fmi2FreeInstance(inst); +} + +void test_fmigateway__fmi2_ExitInitializationMode(void** state) +{ + fmi2_setup* setup = *state; + + FmuInstanceData* inst = fmi2Instantiate(setup->instance_name, + setup->fmu_type, setup->fmu_guid, setup->fmu_resource_location, + setup->functions, setup->visible, setup->logging_on); + + fmi2ExitInitializationMode(inst); + FmiGateway* fmi_gw = inst->data; + + assert_string_equal(fmi_gw->model->mi->name, "gateway"); + assert_double_equal(fmi_gw->settings.step_size, 0.0005, 0.0); + assert_double_equal(fmi_gw->settings.end_time, 0.002, 0.0); + + assert_int_equal(inst->variables.scalar.input.used_nodes, 2); + assert_int_equal(inst->variables.scalar.output.used_nodes, 2); + for (SignalVector* sv = fmi_gw->model->sv; sv && sv->name; sv++) { + if (sv->is_binary) continue; + + /* Check if SignalVector <-> Hashmap is set up correctly. */ + assert_memory_equal( + (hashmap_get(&inst->variables.scalar.input, "1001")), + &sv->scalar[0], sizeof(double)); + assert_memory_equal( + (hashmap_get(&inst->variables.scalar.input, "1004")), + &sv->scalar[2], sizeof(double)); + + assert_memory_equal( + (hashmap_get(&inst->variables.scalar.output, "1002")), + &sv->scalar[1], sizeof(double)); + assert_memory_equal( + (hashmap_get(&inst->variables.scalar.output, "1005")), + &sv->scalar[3], sizeof(double)); + } + + assert_int_equal(inst->variables.binary.rx.used_nodes, 4); + assert_int_equal(inst->variables.binary.tx.used_nodes, 4); + + fmi2FreeInstance(inst); +} + +void test_fmigateway__fmi2_DOUBLE(void** state) +{ + fmi2_setup* setup = *state; + + FmuInstanceData* inst = fmi2Instantiate(setup->instance_name, + setup->fmu_type, setup->fmu_guid, setup->fmu_resource_location, + setup->functions, setup->visible, setup->logging_on); + + fmi2ExitInitializationMode(inst); + FmiGateway* fmi_gw = inst->data; + + const unsigned int VR_DBL_IN[] = { 1001, 1004 }; + const unsigned int VR_DBL_OUT[] = { 1002, 1005 }; + double VALUE[] = { 1.0, 2.0 }; + fmi2SetReal(inst, VR_DBL_IN, 2, VALUE); + + for (SignalVector* sv = fmi_gw->model->sv; sv && sv->name; sv++) { + if (sv->is_binary) continue; + assert_double_equal(VALUE[0], sv->scalar[0], 0.0); + assert_double_equal(VALUE[1], sv->scalar[2], 0.0); + + sv->scalar[1] = 3.0; + sv->scalar[3] = 4.0; + } + + fmi2DoStep(inst, 0.0, 0.0005, 0); + + fmi2GetReal(inst, VR_DBL_OUT, 2, VALUE); + for (SignalVector* sv = fmi_gw->model->sv; sv && sv->name; sv++) { + if (sv->is_binary) continue; + assert_double_equal(VALUE[0], sv->scalar[1], 0.0); + assert_double_equal(VALUE[1], sv->scalar[3], 0.0); + } + fmi2FreeInstance(inst); +} + +void test_fmigateway__fmi2_BINARY(void** state) +{ + fmi2_setup* setup = *state; + + FmuInstanceData* inst = fmi2Instantiate(setup->instance_name, + setup->fmu_type, setup->fmu_guid, setup->fmu_resource_location, + setup->functions, setup->visible, setup->logging_on); + + fmi2ExitInitializationMode(inst); + FmiGateway* fmi_gw = inst->data; + + const unsigned int VR_BIN_IN[] = { 2, 4, 6, 8 }; + const unsigned int VR_BIN_OUT[] = { 3, 5, 7, 9 }; + const char* VALUE[] = { "6:jpZ0`", "6:jpZ1&", "6:jpZ1B", "6:jpZ1]" }; + + fmi2SetString(inst, VR_BIN_IN, 4, VALUE); + + for (SignalVector* sv = fmi_gw->model->sv; sv && sv->name; sv++) { + if (sv->is_binary == false) continue; + int len = strlen("BIN_1BIN_2BIN_3BIN_4"); + assert_memory_equal("BIN_1BIN_2BIN_3BIN_4", sv->binary[0], len); + assert_int_equal(sv->length[0], len); + assert_int_equal(sv->buffer_size[0], len); + } + + fmi2DoStep(inst, 0.0, 0.0005, 0); + + for (SignalVector* sv = fmi_gw->model->sv; sv && sv->name; sv++) { + if (sv->is_binary == false) continue; + sv->length[0] = 0; + + dse_buffer_append(&sv->binary[0], &sv->length[0], &sv->buffer_size[0], + (void*)"REMOTE_1", strlen("REMOTE_1")); + } + + fmi2GetString(inst, VR_BIN_OUT, 4, VALUE); + for (SignalVector* sv = fmi_gw->model->sv; sv && sv->name; sv++) { + if (sv->is_binary == false) continue; + assert_memory_equal(";FO;U<(1.K", VALUE[0], 8); + assert_memory_equal(";FO;U<(1.K", VALUE[1], 8); + assert_memory_equal(";FO;U<(1.K", VALUE[2], 8); + assert_memory_equal(";FO;U<(1.K", VALUE[3], 8); + } + + fmi2FreeInstance(inst); +} + + +int run_fmigateway__fmi2_tests(void) +{ + void* s = test_fmigateway__fmi2_setup; + void* t = test_fmigateway__fmi2_teardown; + + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_fmigateway__fmi2_instantiate, s, t), + cmocka_unit_test_setup_teardown( + test_fmigateway__fmi2_ExitInitializationMode, s, t), + cmocka_unit_test_setup_teardown(test_fmigateway__fmi2_DOUBLE, s, t), + cmocka_unit_test_setup_teardown(test_fmigateway__fmi2_BINARY, s, t), + }; + + return cmocka_run_group_tests_name("engine", tests, NULL, NULL); +} diff --git a/tests/cmocka/fmigateway/test_parser.c b/tests/cmocka/fmigateway/test_parser.c new file mode 100644 index 0000000..f7b469f --- /dev/null +++ b/tests/cmocka/fmigateway/test_parser.c @@ -0,0 +1,140 @@ +// Copyright 2024 Robert Bosch GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include + + +#define UNUSED(x) ((void)x) +#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + + +int test_fmigateway__parser_setup(void** state) +{ + FmuInstanceData* fmu = calloc(1, sizeof(FmuInstanceData)); + + FmiGateway* fmi_gw = calloc(1, sizeof(FmiGateway)); + + fmi_gw->model = calloc(1, sizeof(ModelGatewayDesc)); + + fmu->data = (void*)fmi_gw; + + fmu->instance.resource_location = + strdup("../../../../tests/cmocka/fmigateway/data"); + + fmi_gw->settings.yaml_files = calloc(2, sizeof(char*)); + fmi_gw->settings.yaml_files[0] = + dse_path_cat(fmu->instance.resource_location, "model_parser.yaml"); + + *state = fmu; + return 0; +} + +int test_fmigateway__parser_teardown(void** state) +{ + FmuInstanceData* fmu = *state; + FmiGateway* fmi_gw = fmu->data; + + if (fmu) { + dse_yaml_destroy_doc_list(fmi_gw->settings.doc_list); + /* cleanup. */ + for (const char** _ = fmi_gw->settings.yaml_files; *_; _++) { + free((char*)*_); + }; + free(fmu->instance.resource_location); + + if (fmu->data) { + free(fmi_gw->settings.yaml_files); + free(fmi_gw->settings.session->w_models); + free(fmi_gw->settings.session); + free(fmi_gw->model); + free(fmi_gw); + } + } + free(fmu); + + return 0; +} + +void test_fmigateway__parser(void** state) +{ + FmuInstanceData* fmu = *state; + FmiGateway* fmi_gw = fmu->data; + + // Test conditions. + WindowsModel pi[] = { + { + .name = "Model_1", + .step_size = 0.1, + .end_time = 0.2, + .log_level = 6, + .timeout = 60.0, + .path = "foo", + .file = "bar", + .yaml = "foo;bar", + .show_process = true, + }, + { + .name = "Model_2", + .step_size = 0.15, + .end_time = 0.25, + .log_level = 5, + .timeout = 61.0, + .path = "foo_2", + .file = "bar_2", + .yaml = "foo_2;bar_2", + .show_process = false, + }, + }; + + assert_null(fmi_gw->settings.end_time); + assert_null(fmi_gw->settings.log_level); + assert_null(fmi_gw->settings.session); + + fmigateway_parse(fmu); + + assert_non_null(fmi_gw->settings.doc_list); + assert_double_equal(fmi_gw->settings.end_time, 0.02, 0.0); + assert_double_equal(fmi_gw->settings.step_size, 0.005, 0.0); + assert_int_equal(fmi_gw->settings.log_level, 6); + assert_string_equal(fmi_gw->settings.log_location, "foo/bar"); + assert_non_null(fmi_gw->settings.session); + assert_string_equal(fmi_gw->settings.session->init.path, "foo"); + assert_string_equal(fmi_gw->settings.session->init.file, "bar"); + assert_string_equal( + fmi_gw->settings.session->shutdown.path, "foo_shutdown"); + assert_string_equal( + fmi_gw->settings.session->shutdown.file, "bar_shutdown"); + + assert_non_null(fmi_gw->settings.session->w_models); + for (size_t i = 0; i < ARRAY_SIZE(pi); i++) { + WindowsModel* w_model = &fmi_gw->settings.session->w_models[i]; + WindowsModel* TC_model = &pi[i]; + assert_string_equal(TC_model->name, w_model->name); + assert_string_equal(TC_model->path, w_model->path); + assert_string_equal(TC_model->file, w_model->file); + assert_string_equal(TC_model->yaml, w_model->yaml); + assert_double_equal(TC_model->step_size, w_model->step_size, 0.0); + assert_double_equal(TC_model->end_time, w_model->end_time, 0.0); + assert_double_equal(TC_model->timeout, w_model->timeout, 0.0); + assert_int_equal(TC_model->log_level, w_model->log_level); + assert_int_equal(TC_model->show_process, w_model->show_process); + } +} + + +int run_fmigateway__parser_tests(void) +{ + void* s = test_fmigateway__parser_setup; + void* t = test_fmigateway__parser_teardown; + + const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown( + test_fmigateway__parser, s, t) }; + + return cmocka_run_group_tests_name("PARSER", tests, NULL, NULL); +} diff --git a/tests/testscript/e2e/fmigateway_fmi2.txtar b/tests/testscript/e2e/fmigateway_fmi2.txtar new file mode 100644 index 0000000..45c9317 --- /dev/null +++ b/tests/testscript/e2e/fmigateway_fmi2.txtar @@ -0,0 +1,62 @@ +env NAME=fmigateway + +# Operate from the WORKDIR (allocated by caller, i.e. docker command). +cd $WORKDIR +env SIM=sim + +env EXAMPLE=$REPODIR/dse/build/_out/examples/gateway +env FMU_IMPORTER=$REPODIR/dse/build/_out/importer/fmuImporter + + +# Create the simulation folder (Simer layout) +exec ls -R $WORK +mkdir $SIM/bin +cp $FMU_IMPORTER $SIM/bin/fmuImporter + +exec cp -R $EXAMPLE/fmi2 $SIM/fmu +exec cp -R $EXAMPLE/model $SIM/model + +# TEST: FMI Gateway with FMI2 +exec sh -e $WORK/test.sh + +stdout 'Importer: Cwd: /sim/fmu' +stdout 'Importer: Loading FMU: binaries/linux64/libfmi2gateway.so' +stdout 'FMU Model instantiated' +stdout 'Resource location: resources' +stdout 'Setting up the Simbus connection...' +stdout 'Connected to the Simbus...' +stdout 'Configure Channel: scalar_vector' +stdout 'Configure Channel: binary_vector' +stdout 'Build indexes...' +stdout 'Scalar: input=2, output=2' +stdout 'Binary: rx=4, tx=4' +stdout 'Encoding: enc=8, dec=8' + +stdout 'name : can' +stdout 'length : 480' +stdout 'buffer len : 480' + +stdout 'Importer: \[1002\] 9' +stdout 'Importer: \[3\] F8u:' +stdout 'Importer: \[5\] F8u:' +stdout 'Importer: \[7\] F8u:' +stdout 'Importer: \[9\] F8u:' + +stdout 'Model: RX\[03f1\] Hello World! from node_id=2' + +-- test.sh -- +SIMER="${SIMER:-ghcr.io/boschglobal/dse-simer:latest}" + +docker run --name gateway -i --rm -v $ENTRYWORKDIR/$SIM:/sim \ + --env SIMBUS_LOGLEVEL=4 \ + --net="host" \ + --entrypoint="" $SIMER \ + bash -c "cd fmu; valgrind -q --leak-check=yes --error-exitcode=808 \ + ../bin/fmuImporter -F binaries/linux64/libfmi2gateway.so" & + +docker run --name simer -i --rm -v $ENTRYWORKDIR/$SIM:/sim \ + --env SIMBUS_LOGLEVEL=4 \ + --env target_inst:SIMBUS_LOGGER=4 \ + --net="host" \ + $SIMER \ + -stepsize 0.0005 -endtime 0.005 diff --git a/tests/testscript/e2e/fmigateway_fmi3.txtar b/tests/testscript/e2e/fmigateway_fmi3.txtar new file mode 100644 index 0000000..c1f696d --- /dev/null +++ b/tests/testscript/e2e/fmigateway_fmi3.txtar @@ -0,0 +1,57 @@ +env NAME=fmigateway + +# Operate from the WORKDIR (allocated by caller, i.e. docker command). +cd $WORKDIR +env SIM=sim + +env EXAMPLE=$REPODIR/dse/build/_out/examples/gateway +env FMU_IMPORTER=$REPODIR/dse/build/_out/importer/fmuImporter + + +# Create the simulation folder (Simer layout) +exec ls -R $WORK +mkdir $SIM/bin +cp $FMU_IMPORTER $SIM/bin/fmuImporter + +exec cp -R $EXAMPLE/fmi3 $SIM/fmu +exec cp -R $EXAMPLE/model $SIM/model + +# TEST: FMI Gateway with FMI3 +exec sh -e $WORK/test.sh + +stdout 'Importer: Cwd: /sim/fmu' +stdout 'Importer: Loading FMU: binaries/x86_64-linux/libfmi3gateway.so' +stdout 'FMU Model instantiated' +stdout 'Resource location: resources' +stdout 'Setting up the Simbus connection...' +stdout 'Connected to the Simbus...' +stdout 'Configure Channel: scalar_vector' +stdout 'Configure Channel: binary_vector' +stdout 'Scalar: input=2, output=2' +stdout 'Binary: rx=4, tx=4' +stdout 'Encoding: enc=8, dec=8' + +stdout 'Importer: \[1002\] 9' +stdout 'Importer: \[3\] F8u:' +stdout 'Importer: \[5\] F8u:' +stdout 'Importer: \[7\] F8u:' +stdout 'Importer: \[9\] F8u:' + +stdout 'Model: RX\[03f1\] Hello World! from node_id=2' + +-- test.sh -- +SIMER="${SIMER:-ghcr.io/boschglobal/dse-simer:latest}" + +docker run --name gateway -i --rm -v $ENTRYWORKDIR/$SIM:/sim \ + --env SIMBUS_LOGLEVEL=4 \ + --net="host" \ + --entrypoint="" $SIMER \ + bash -c "cd fmu; valgrind -q --leak-check=yes --error-exitcode=808 \ + ../bin/fmuImporter -V 3 -F binaries/x86_64-linux/libfmi3gateway.so" & + +docker run --name simer -i --rm -v $ENTRYWORKDIR/$SIM:/sim \ + --env SIMBUS_LOGLEVEL=4 \ + --env target_inst:SIMBUS_LOGGER=4 \ + --net="host" \ + $SIMER \ + -stepsize 0.0005 -endtime 0.005