├── Source └── UEFMI │ ├── ThirdParty │ └── fmikit │ │ └── src │ │ ├── FMU.cpp │ │ ├── FMU1.cpp │ │ └── FMU2.cpp │ ├── Public │ ├── UEFMI.h │ ├── A_Test.h │ └── A_FMU.h │ ├── Private │ ├── A_Test.cpp │ ├── UEFMI.cpp │ └── A_FMU.cpp │ └── UEFMI.Build.cs ├── resources └── Icon128.png ├── Content ├── Maps │ ├── Level_0.umap │ └── Level_1.umap └── BluePrints │ └── BP_FMU.uasset ├── PluginAssets ├── fmus │ └── test.fmu ├── images │ └── fmuUEBP.PNG └── modelica │ └── test.mo ├── .gitmodules ├── setup.bat ├── setup.sh ├── UEFMI.uplugin ├── Config └── DefaultUEFMI.ini ├── .gitignore └── README.md /Source/UEFMI/ThirdParty/fmikit/src/FMU.cpp: -------------------------------------------------------------------------------- 1 | ../../../../../ThirdParty/fmikit/src/FMU.cpp -------------------------------------------------------------------------------- /Source/UEFMI/ThirdParty/fmikit/src/FMU1.cpp: -------------------------------------------------------------------------------- 1 | ../../../../../ThirdParty/fmikit/src/FMU1.cpp -------------------------------------------------------------------------------- /Source/UEFMI/ThirdParty/fmikit/src/FMU2.cpp: -------------------------------------------------------------------------------- 1 | ../../../../../ThirdParty/fmikit/src/FMU2.cpp -------------------------------------------------------------------------------- /resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORNL-Modelica/UnrealEngine-FMIPlugin/HEAD/resources/Icon128.png -------------------------------------------------------------------------------- /Content/Maps/Level_0.umap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORNL-Modelica/UnrealEngine-FMIPlugin/HEAD/Content/Maps/Level_0.umap -------------------------------------------------------------------------------- /Content/Maps/Level_1.umap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORNL-Modelica/UnrealEngine-FMIPlugin/HEAD/Content/Maps/Level_1.umap -------------------------------------------------------------------------------- /PluginAssets/fmus/test.fmu: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORNL-Modelica/UnrealEngine-FMIPlugin/HEAD/PluginAssets/fmus/test.fmu -------------------------------------------------------------------------------- /Content/BluePrints/BP_FMU.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORNL-Modelica/UnrealEngine-FMIPlugin/HEAD/Content/BluePrints/BP_FMU.uasset -------------------------------------------------------------------------------- /PluginAssets/images/fmuUEBP.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ORNL-Modelica/UnrealEngine-FMIPlugin/HEAD/PluginAssets/images/fmuUEBP.PNG -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "FMIKit-Simulink"] 2 | path = ThirdParty/fmikit 3 | url = https://github.com/ORNL-Modelica/FMIKit-Simulink.git 4 | branch = unreal 5 | -------------------------------------------------------------------------------- /setup.bat: -------------------------------------------------------------------------------- 1 | set PROJECTPATH=%~dp0 2 | set SOURCEDIR=%PROJECTPATH%ThirdParty\fmikit\src\ 3 | set DESTDIR=%PROJECTPATH%Source\UEFMI\ThirdParty\fmikit\src\ 4 | xcopy "%SOURCEDIR%" "%DESTDIR%" /y 5 | pause -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | SCRIPT_PATH="${BASH_SOURCE:-$0}" 2 | ABS_SCRIPT_PATH="$(realpath "${SCRIPT_PATH}")" 3 | ABS_DIRECTORY="$(dirname "${ABS_SCRIPT_PATH}")" 4 | 5 | SOURCEDIR="${ABS_DIRECTORY}""/ThirdParty/fmikit/src/." 6 | DESTDIR="${ABS_DIRECTORY}""/Source/UEFMI/ThirdParty/fmikit/src/" 7 | cp -a "${SOURCEDIR}" "${DESTDIR}" 8 | -------------------------------------------------------------------------------- /UEFMI.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "UE-FMI Plugin", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "UEFMI", 20 | "Type": "Runtime", 21 | "LoadingPhase": "Default" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /Source/UEFMI/Public/UEFMI.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | class FUEFMIModule : public IModuleInterface 9 | { 10 | public: 11 | 12 | /** IModuleInterface implementation */ 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | 16 | private: 17 | bool SearchForDllPath(FString _searchBase, FString _dllName); 18 | 19 | int handle = 0; 20 | void *DLLHandle; 21 | void *DLLHandle2; 22 | }; 23 | -------------------------------------------------------------------------------- /Config/DefaultUEFMI.ini: -------------------------------------------------------------------------------- 1 | [CoreRedirects] 2 | ; Plugin Redirector 3 | ;+PackageRedirects=(OldName="/FMIKit/", NewName="/UEFMI/", MatchSubstring=true) 4 | 5 | ; Class Redirectors 6 | ;+ClassRedirects=(OldName="/Script/FMIKit.",NewName="/Script/UEFMI.", MatchSubstring=true) 7 | 8 | ; Struct Redirectors 9 | ;+StructRedirects=(OldName="/Script/{OldGenericPlugin.GenericStruct}",NewName="/Script/{NewGenericPlugin.GenericStruct}") 10 | 11 | ; Enum Redirectors 12 | ;+EnumRedirects=(OldName="/Script/{OldGenericPlugin.EGenericStruct}",NewName="/Script/{NewGenericPlugin.EGenericStruct}") -------------------------------------------------------------------------------- /PluginAssets/modelica/test.mo: -------------------------------------------------------------------------------- 1 | within ; 2 | model test 3 | parameter Real sigma=10; 4 | parameter Real rho=28; 5 | parameter Real beta=8/3; 6 | parameter Real x_start = 1 "Initial x-coordinate" annotation(Dialog(tab="Initialization")); 7 | parameter Real y_start = 1 "Initial y-coordinate" annotation(Dialog(tab="Initialization")); 8 | parameter Real z_start = 1 "Initial z-coordinate" annotation(Dialog(tab="Initialization")); 9 | Real x "x-coordinate"; 10 | Real y "y-coordinate"; 11 | Real z "z-coordinate"; 12 | initial equation 13 | x = x_start; 14 | y = y_start; 15 | z = z_start; 16 | equation 17 | der(x) = sigma*(y-x); 18 | der(y) = rho*x - y - x*z; 19 | der(z) = x*y - beta*z; 20 | annotation (experiment(StopTime=100, __Dymola_Algorithm="Dassl")); 21 | end test; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | *.ipa 33 | 34 | # These project files can be generated by the engine 35 | *.xcodeproj 36 | *.xcworkspace 37 | *.sln 38 | *.suo 39 | *.opensdf 40 | *.sdf 41 | *.VC.db 42 | *.VC.opendb 43 | 44 | # Precompiled Assets 45 | SourceArt/**/*.png 46 | SourceArt/**/*.tga 47 | 48 | # Binary Files 49 | **/Binaries 50 | 51 | # Builds 52 | **/Build 53 | 54 | # Whitelist PakBlacklist-.txt files 55 | !Build/*/ 56 | Build/*/** 57 | !Build/*/PakBlacklist*.txt 58 | 59 | # Don't ignore icon files in Build 60 | !Build/**/*.ico 61 | 62 | # Built data for maps 63 | *_BuiltData.uasset 64 | 65 | # Configuration files generated by the Editor 66 | **/Saved 67 | 68 | # Compiled source files for the engine to use 69 | **/Intermediate 70 | 71 | # Cache files for the editor to use 72 | **/DerivedDataCache -------------------------------------------------------------------------------- /Source/UEFMI/Private/A_Test.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021, UT-Battelle, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "A_Test.h" 18 | 19 | // Sets default values 20 | AA_Test::AA_Test() 21 | { 22 | // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. 23 | PrimaryActorTick.bCanEverTick = true; 24 | } 25 | 26 | #if WITH_EDITOR 27 | void AA_Test::PostEditChangeProperty(struct FPropertyChangedEvent& e) 28 | { 29 | //Super::PostEditChangeProperty(e); 30 | 31 | if (e.MemberProperty->GetFName().ToString() == TEXT("mPath")) 32 | { 33 | mUnzipDir = "hello"; 34 | } 35 | } 36 | #endif 37 | 38 | // Called when the game starts or when spawned 39 | void AA_Test::BeginPlay() 40 | { 41 | //SetActorTickInterval(1.f); 42 | Super::BeginPlay(); 43 | 44 | FString test = mUnzipDir; 45 | FString cat = mUnzipDir; 46 | } -------------------------------------------------------------------------------- /Source/UEFMI/Public/A_Test.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021, UT-Battelle, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include "CoreMinimal.h" 20 | #include "GameFramework/Actor.h" 21 | #include "A_Test.generated.h" 22 | 23 | UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) 24 | class UEFMI_API AA_Test : public AActor 25 | { 26 | GENERATED_BODY() 27 | 28 | public: 29 | // Sets default values for this actor's properties 30 | AA_Test(); 31 | 32 | protected: 33 | // Called when the game starts or when spawned 34 | virtual void BeginPlay() override; 35 | 36 | #if WITH_EDITOR 37 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 38 | #endif 39 | 40 | public: 41 | 42 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 43 | FFilePath mPath; 44 | 45 | private: 46 | FString mUnzipDir; 47 | }; 48 | -------------------------------------------------------------------------------- /Source/UEFMI/Private/UEFMI.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "UEFMI.h" 4 | #include "Misc/LocalTimestampDirectoryVisitor.h" 5 | 6 | #define LOCTEXT_NAMESPACE "FFMIKitModule" 7 | 8 | void FUEFMIModule::StartupModule() 9 | { 10 | 11 | } 12 | 13 | void FUEFMIModule::ShutdownModule() 14 | { 15 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 16 | // we call this function before unloading the module. 17 | FPlatformProcess::FreeDllHandle(DLLHandle); 18 | } 19 | 20 | bool FUEFMIModule::SearchForDllPath(FString _searchBase, FString _dllName) 21 | { 22 | //Search Plugins folder for an instance of Dll.dll, and add to platform search path 23 | TArray directoriesToSkip; 24 | IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); 25 | FLocalTimestampDirectoryVisitor Visitor(PlatformFile, directoriesToSkip, directoriesToSkip, false); 26 | PlatformFile.IterateDirectory(*_searchBase, Visitor); 27 | 28 | for (TMap::TIterator TimestampIt(Visitor.FileTimes); TimestampIt; ++TimestampIt) 29 | { 30 | const FString file = TimestampIt.Key(); 31 | const FString filePath = FPaths::GetPath(file); 32 | const FString fileName = FPaths::GetCleanFilename(file); 33 | if (fileName.Compare(_dllName) == 0) 34 | { 35 | FPlatformProcess::AddDllDirectory(*filePath); // only load dll when needed for use. Broken with 4.11. 36 | switch (handle) { 37 | case 0: 38 | DLLHandle = FPlatformProcess::GetDllHandle(*file); 39 | break; 40 | case 1: 41 | DLLHandle2 = FPlatformProcess::GetDllHandle(*file); 42 | break; 43 | default: 44 | //DLLHandle3 = FPlatformProcess::GetDllHandle(*file); 45 | break; 46 | } 47 | handle++; 48 | return true; 49 | } 50 | } 51 | return false; 52 | } 53 | 54 | #undef LOCTEXT_NAMESPACE 55 | 56 | IMPLEMENT_MODULE(FUEFMIModule, UEFMI) -------------------------------------------------------------------------------- /Source/UEFMI/UEFMI.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using System; 4 | using System.IO; 5 | using System.Runtime.Intrinsics.X86; 6 | using UnrealBuildTool; 7 | //using static System.Net.Mime.MediaTypeNames; 8 | 9 | public class UEFMI : ModuleRules 10 | { 11 | private string ThirdPartyPath 12 | { 13 | get { return Path.GetFullPath(Path.Combine(ModuleDirectory, "../../ThirdParty/")); } 14 | } 15 | 16 | private string FMIKitPath 17 | { 18 | get { return Path.GetFullPath(Path.Combine(ThirdPartyPath, "fmikit/")); } 19 | } 20 | //private string FMILIBPath 21 | //{ 22 | // get { return Path.GetFullPath(Path.Combine(ThirdPartyPath, "fmilib/")); } 23 | //} 24 | public UEFMI(ReadOnlyTargetRules Target) : base(Target) 25 | { 26 | bEnableExceptions = true; 27 | 28 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 29 | 30 | PublicIncludePaths.AddRange( 31 | new string[] { 32 | // ... add public include paths required here ... 33 | } 34 | ); 35 | 36 | PrivateIncludePaths.AddRange( 37 | new string[] { 38 | // ... add other private include paths required here ... 39 | } 40 | ); 41 | 42 | PublicDependencyModuleNames.AddRange( 43 | new string[] 44 | { 45 | "Core", 46 | // ... add other public dependencies that you statically link with here ... 47 | } 48 | ); 49 | 50 | 51 | PrivateDependencyModuleNames.AddRange( 52 | new string[] 53 | { 54 | "Core", 55 | "CoreUObject", 56 | "Engine", 57 | "InputCore", 58 | "Slate", 59 | "SlateCore", 60 | "XmlParser", 61 | // ... add private dependencies that you statically link with here ... 62 | } 63 | ); 64 | 65 | 66 | DynamicallyLoadedModuleNames.AddRange( 67 | new string[] 68 | { 69 | // ... add any modules that your module loads dynamically here ... 70 | } 71 | ); 72 | 73 | LoadFMIKIT(Target); 74 | //LoadFMILIB(Target); 75 | } 76 | 77 | public bool LoadFMIKIT(ReadOnlyTargetRules Target) 78 | { 79 | bool isLibrarySupported = false; 80 | 81 | if ((Target.Platform == UnrealTargetPlatform.Win64)) 82 | { 83 | isLibrarySupported = true; 84 | 85 | PublicIncludePaths.Add(Path.Combine(FMIKitPath, "Include")); 86 | } 87 | 88 | return isLibrarySupported; 89 | } 90 | 91 | //public bool LoadFMILIB(ReadOnlyTargetRules Target) 92 | //{ 93 | // bool isLibrarySupported = false; 94 | 95 | // if ((Target.Platform == UnrealTargetPlatform.Win64)) 96 | // { 97 | // isLibrarySupported = true; 98 | 99 | // PublicDelayLoadDLLs.Add("fmilib_shared.dll"); 100 | // PublicAdditionalLibraries.Add(Path.Combine(FMILIBPath, "lib/fmilib_shared.lib")); 101 | // PublicIncludePaths.Add(Path.Combine(FMILIBPath, "include")); 102 | // } 103 | 104 | // return isLibrarySupported; 105 | //} 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unreal Engine - FMI Plugin (UEFMI) 2 | 3 | [![FMU_BP_example](./PluginAssets/images/fmuUEBP.PNG)](https://youtu.be/r3NeJKJt4Z8) 4 | 5 | # Background 6 | - The UEFMI plugin allows for the use of dynamic simulation models via Functional Mockup Units (FMUs) directly in an Unreal Engine project. 7 | - An FMU is code that has been packaged according to the [Functional Mockup Interface](https://fmi-standard.org/), a free and open-source standard for exchanging dynamic (i.e., time-dependent) simulation models. 8 | - Common examples of models that get packaged into FMUs are: 9 | - Physics based vehicle simulators (e.g., drive train, HVAC, water vehicles, etc.) 10 | - Energy systems (e.g., power plants, buildings) 11 | - Space systems (e.g., solar sail mechanics, propulsion systems) 12 | - And many many more... 13 | 14 | # Prerequisites 15 | 16 | This work was tested using the following. It may work on something else but no guarantees. 17 | - Windows 10 18 | - Unreal Engine 5.1+ 19 | - Visual Studio 2022 20 | - **An unzip utility on path, currently supported are unzip, 7z, tar** 21 | 22 | # `A_FMU` -> The Workhorse 23 | 24 | `A_FMU` under [`UEFMI C++ Classes/UEFMI/Public`](./Source/UEFMI/Private/A_FMU.cpp) contains the magic to make the FMU run. Users are highly encouraged to look at `A_FMU.cpp` if they need to understand more intimately the implementation. A couple important notes are: 25 | - `PathFMU` is the location of the FMU and supports relative or absolute paths. 26 | - `mResults` returns the results requested from the variables added to `mStoredVariables`. 27 | - `mResults` only returns values when `mAutoSimulateTick` = True. Else it is empty and variables must be be retrieved using the `GetReal()` function. 28 | - `mModelVariables` are the names of all availble variables found in the model which could be added to `mStoredVariables`. 29 | 30 | # Installation 31 | 32 | > As a plugin, UEFMI is intended to be used inside an existing Unreal Engine project! 33 | 34 | The process below will go through the steps to adding the plugin to a new Unreal Engine project. 35 | 36 | 1. Create an Unreal Engine project. Fror illustration purposes let's call it `MYPROJECT`. 37 | - Go to the project `MYPROJECT` folder and create a `Plugins` folder if it doesn't exist. 38 | ``` 39 | cd MYPROJECT 40 | mkdir Plugins 41 | ``` 42 | 43 | 1. Clone the repository and submodules. To do that, in the ``MYPROJECT/Plugins` folder: 44 | ``` 45 | cd Plugins 46 | git clone https://github.com/ORNL-Modelica/UnrealEngine-FMIPlugin.git UEFMI 47 | cd UEFMI 48 | git submodule init 49 | git submodule update 50 | ``` 51 | - Note: 52 | - `git submodule update --remote` to grab the latest commits instead of specific commit 53 | 1. Copy the files from the submodule to the `UEFMI` plugin source folder (choose one option). In `MYPROJECT/Plugins/UEFMI` folder: 54 | - **Auto**: 55 | - Run `setup.bat` (Windows) or `setup.sh` (Linux) 56 | - **Manual**: 57 | - Copy files:
`FMU.cpp`, `FMU1.cpp`, `FMU2.cpp` 58 | - From:
`ThirdParty/fmikit/src` 59 | - To:
`Source/UEFMI/ThirdParty/fmikit/src` 60 | 1. Run the Unreal Engine project 61 | - Double click `MYPROJECT.uproject` and rebuild the plugin if prompted. 62 | - Or right-click `MYPROJECT.uproject` and select `Generate Visual Studio project files` and then open `MYPROJECT.sln` 63 | 64 | ## Test Installation 65 | 66 | These examples use the `test.fmu` included in the repo. The FMU provided will be extracted to a temporary folder called `fmus` at the top level of the UE project folder. 67 | 68 | > `test.fmu` is a [Lorenz System model](https://en.wikipedia.org/wiki/Lorenz_system) model created from the Modelica source code [test.mo](./src/test.mo) 69 | 70 | - `Level_0` 71 | - This level provides example blueprints (`BP_FMU`) demonstrating the automatic and manual options for simulating an FMU. 72 | - `BP_FMU` implements the `A_FMU` class. 73 | - `Level_1` 74 | - Simple use of `A_FMU` and printing a variable to the screen via the level blueprint. 75 | 76 | # Known Issues 77 | 78 | - Currently only floats and booleans (i.e., 0/1) are supported variables in `A_FMU`. 79 | - Has not yet been tested on non-Windows OS. 80 | 81 | # License 82 | 83 | Copyright 2019 UT-Battelle. The code is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). -------------------------------------------------------------------------------- /Source/UEFMI/Public/A_FMU.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021, UT-Battelle, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include "Containers/Map.h" 20 | #include "Engine/DataTable.h" 21 | #include "FMU2.h" 22 | #include "CoreMinimal.h" 23 | #include "GameFramework/Actor.h" 24 | #include "A_FMU.generated.h" 25 | 26 | USTRUCT() 27 | struct FModelVariables : public FTableRowBase 28 | { 29 | GENERATED_USTRUCT_BODY() 30 | 31 | public: 32 | UPROPERTY(VisibleAnywhere, BlueprintReadWrite) 33 | int ValueReference; 34 | }; 35 | 36 | UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) 37 | class UEFMI_API AA_FMU : public AActor 38 | { 39 | GENERATED_BODY() 40 | 41 | public: 42 | // Sets default values for this actor's properties 43 | AA_FMU(); 44 | 45 | protected: 46 | // Called when the game starts or when spawned 47 | virtual void BeginPlay() override; 48 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 49 | 50 | // Called when actor is created or any updates are made to it 51 | virtual void OnConstruction(const FTransform& Transform) override; 52 | virtual void PostInitProperties() override; 53 | virtual void PostLoad() override; 54 | 55 | #if WITH_EDITOR 56 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 57 | #endif 58 | 59 | public: 60 | // Called every frame 61 | virtual void Tick(float DeltaTime) override; 62 | 63 | void ExtractFMU(); 64 | void ParseXML(); 65 | void SetInitialValues(); 66 | 67 | // Function for instantiating the FMU (can be used to reset the FMU simulation as well). 68 | UFUNCTION(BlueprintCallable) 69 | void Initialize(); 70 | // Return the real variable of the specified name. 71 | UFUNCTION(BlueprintCallable) 72 | float GetReal(FString Name); 73 | // Function to solver the FMU to the specified time step 74 | UFUNCTION(BlueprintCallable) 75 | void DoStep(float StepSize); 76 | // Control logic to sync real time and fmu time together. Typically called before DoStep(). 77 | UFUNCTION(BlueprintCallable) 78 | bool ControlStep(float DeltaTime); 79 | // Change an FMU variable value. 80 | UFUNCTION(BlueprintCallable) 81 | void SetReal(FString Name, float Value); 82 | 83 | // Use to specify if the internal logic is desired to run the FMU. Else control FMU solution manually. 84 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 85 | bool mAutoSimulateTick = false; 86 | // Full file path of the *.fmu file 87 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 88 | FFilePath PathFMU; 89 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 90 | FFilePath mPath; 91 | // Change the simulation speed of the FMU relative to real time. 92 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 93 | float mSpeedMultiplier = 1.0f; 94 | // Variables available in the XML for inputs/outputs 95 | UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "FMU Settings") 96 | TMap mModelVariables; 97 | // Specify the variables to store for access. See mModelVariables for available variables. 98 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 99 | TArray mStoredVariables; 100 | // Value of stored variables. Indexed in the same order as mStoredVariables. 101 | UPROPERTY(BlueprintReadWrite, Category = "FMU Settings") 102 | TMap mResults; 103 | // Specify the initial values of variables. See mModelVariables for potentially available variables. 104 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 105 | TMap mInitialValues; 106 | 107 | // Toggle to overwrite the editor specified settings with those found in FMU model description XML. 108 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 109 | bool bUseXMLExperimentSettings = false; 110 | // Start time of the FMU simulation 111 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 112 | float mStartTime = 0.f; 113 | // Stop time of the FMU simulation 114 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 115 | float mStopTime = 1.f; 116 | // Time step (s) of the FMU simulation 117 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 118 | float mStepSize = 0.1f; 119 | // FMU solver tolerance 120 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 121 | float mTolerance = 1e-4; 122 | // Variable to pause the FMU simulation 123 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 124 | bool mPause = false; 125 | // Toggle to have the simulation reset and restart when the mStopTime is reached 126 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 127 | bool mLoop = false; 128 | //UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "FMU Settings") 129 | //bool mbPrintFMUTime = false; 130 | 131 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 132 | FString mUnzipDir; 133 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 134 | FString mGuid; 135 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 136 | FString mModelIdentifier; 137 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 138 | FString mFMIVersion; 139 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 140 | FString mInstanceName = "instance"; 141 | 142 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 143 | float mFMUTime; 144 | UPROPERTY(BlueprintReadOnly, Category = "FMU Settings") 145 | bool mbLoaded = false; 146 | private: 147 | std::unique_ptr mFmu = nullptr; 148 | 149 | fmi2Real mTimeLast; 150 | fmi2Real mTimeNow; 151 | 152 | }; 153 | -------------------------------------------------------------------------------- /Source/UEFMI/Private/A_FMU.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021, UT-Battelle, LLC] 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | #include "A_FMU.h" 18 | 19 | #include "XmlFile.h" 20 | #include "..\Public\A_FMU.h" 21 | 22 | #ifdef WIN32 23 | #include "Windows.h" 24 | #define cmd(a) WinExec(a, SW_HIDE) 25 | #define comparison < 32 26 | 27 | #include 28 | #define mkdir _mkdir 29 | #else 30 | #define cmd(a) system(a) 31 | #define comparison != 0 32 | #endif 33 | 34 | // Sets default values 35 | AA_FMU::AA_FMU() 36 | { 37 | // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. 38 | PrimaryActorTick.bCanEverTick = true; 39 | } 40 | 41 | // Called when actor is created or any updates are made to it 42 | void AA_FMU::OnConstruction(const FTransform& Transform) 43 | { 44 | 45 | } 46 | 47 | #if WITH_EDITOR 48 | void AA_FMU::PostEditChangeProperty(struct FPropertyChangedEvent& e) 49 | { 50 | Super::PostEditChangeProperty(e); 51 | 52 | if (e.MemberProperty->GetFName().ToString() == TEXT("PathFMU")) 53 | { 54 | ExtractFMU(); 55 | mResults.Empty(); 56 | } 57 | if (mAutoSimulateTick && e.MemberProperty->GetFName().ToString() == TEXT("mStoreVariables")) 58 | { 59 | mResults.Empty(); 60 | } 61 | if (e.MemberProperty->GetFName().ToString() == TEXT("bUseXMLExperimentSettings")) 62 | { 63 | if (bUseXMLExperimentSettings) 64 | { 65 | ParseXML(); 66 | } 67 | } 68 | } 69 | #endif 70 | 71 | void AA_FMU::PostInitProperties() 72 | { 73 | Super::PostInitProperties(); 74 | } 75 | 76 | void AA_FMU::PostLoad() 77 | { 78 | Super::PostLoad(); 79 | 80 | ExtractFMU(); 81 | mResults.Empty(); 82 | } 83 | 84 | // Called when the game starts or when spawned 85 | void AA_FMU::BeginPlay() 86 | { 87 | 88 | // Prevent crashing due to missing extracted fmu 89 | bool bFMUExtractedExists = FPaths::FileExists(mUnzipDir + "/modelDescription.xml"); 90 | if (!bFMUExtractedExists) 91 | { 92 | UE_LOG(LogTemp, Error, TEXT("Initialization of FMU failed. Try checking that the .fmu exists. Currently looking for %s"), *mUnzipDir); 93 | return; 94 | } 95 | 96 | //SetActorTickInterval(1.f); 97 | Super::BeginPlay(); 98 | 99 | Initialize(); 100 | } 101 | 102 | void AA_FMU::Initialize() 103 | { 104 | mbLoaded = false; 105 | mTimeNow = 0.0; 106 | mTimeLast = 0.0; 107 | mFMUTime = mStartTime; 108 | 109 | // Believe this is no longer needed 110 | //AA_FMU::DestroyFMU(); 111 | 112 | // if(!mFmu) // This line with the new fmikit.... prevents (we think) issues with locked dll file. However, it messes up autolooping... need to find a better solution 113 | mFmu = std::make_unique(TCHAR_TO_UTF8(*mGuid), TCHAR_TO_UTF8(*mModelIdentifier), TCHAR_TO_UTF8(*mUnzipDir), TCHAR_TO_UTF8(*mInstanceName)); 114 | mFmu->instantiate(true); 115 | 116 | // Initial values seem to be required to be set at multiple places 117 | // Line 1180 - under "if initialize:" https://github.com/CATIA-Systems/FMPy/blob/c01eb7b45a8c0542986d6692458f8c81df425587/fmpy/simulation.py#L1163 118 | //SetInitialValues(); 119 | mFmu->setupExperiment(true, mTolerance, mStartTime, true, mStopTime); 120 | SetInitialValues(); 121 | mFmu->enterInitializationMode(); 122 | SetInitialValues(); 123 | mFmu->exitInitializationMode(); 124 | 125 | mbLoaded = true; 126 | UE_LOG(LogTemp, Display, TEXT("Initialization of FMU complete: %s"), *mPath.FilePath); 127 | } 128 | 129 | void AA_FMU::EndPlay(const EEndPlayReason::Type EndPlayReason) 130 | { 131 | Super::EndPlay(EndPlayReason); 132 | } 133 | 134 | // Called every frame 135 | void AA_FMU::Tick(float DeltaTime) 136 | { 137 | Super::Tick(DeltaTime); 138 | 139 | // This is an option to let the user have an auto simulated model instead of blueprint control. Having issues 140 | if (mAutoSimulateTick && !mPause) 141 | { 142 | try 143 | { 144 | if (mLoop && (mFMUTime > mStopTime)) 145 | { 146 | Initialize(); 147 | } 148 | 149 | if (ControlStep(DeltaTime)) 150 | { 151 | try { 152 | mFmu->doStep(mStepSize); 153 | } 154 | catch (std::exception e) 155 | { 156 | FString errorMsg(e.what()); 157 | UE_LOG(LogTemp, Error, TEXT("%s"), *errorMsg); 158 | return; 159 | } 160 | } 161 | else 162 | { 163 | return; 164 | } 165 | 166 | for (FString Key : mStoredVariables) 167 | { 168 | if (mResults.Contains(Key)) 169 | { 170 | mResults[Key] = mFmu->getReal(mModelVariables[FName(Key)].ValueReference); 171 | } 172 | else if (mModelVariables.Contains(FName(Key))) 173 | { 174 | mResults.Add(Key, mFmu->getReal(mModelVariables[FName(Key)].ValueReference)); 175 | } 176 | } 177 | } 178 | catch(std::exception e) 179 | { 180 | FString errorMsg(e.what()); 181 | UE_LOG(LogTemp, Error, TEXT("%s"), *errorMsg); 182 | return; 183 | } 184 | } 185 | } 186 | 187 | void AA_FMU::ExtractFMU() 188 | { 189 | mPath = PathFMU; 190 | if (mPath.FilePath.IsEmpty()) 191 | { 192 | UE_LOG(LogTemp, Warning, TEXT("mPath to .fmu is empty.")); 193 | return; 194 | } 195 | if (FPaths::GetExtension(*mPath.FilePath, false) != TEXT("fmu")) 196 | { 197 | UE_LOG(LogTemp, Warning, TEXT("Invalid mPath. It does not contain a `.fmu` extension.")); 198 | return; 199 | } 200 | 201 | if (FPaths::IsRelative(*PathFMU.FilePath)) 202 | { 203 | mPath.FilePath = FPaths::Combine(FPaths::ProjectContentDir(), PathFMU.FilePath); 204 | mPath.FilePath = FPaths::ConvertRelativePathToFull(mPath.FilePath); 205 | } 206 | 207 | if (!FPaths::FileExists(*mPath.FilePath)) 208 | { 209 | UE_LOG(LogTemp, Warning, TEXT("Invalid mPath. %s not found."), *mPath.FilePath); 210 | return; 211 | } 212 | 213 | // Gather parts of FMU path 214 | FString pathPart, filename, extension; 215 | FPaths::Split(mPath.FilePath, pathPart, filename, extension); 216 | std::string sPath = TCHAR_TO_UTF8(*mPath.FilePath); 217 | 218 | // Define path for extracted the FMU 219 | FString projPath = FPaths::ProjectDir(); 220 | FString exDir = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*projPath) + "fmus/"; // Directory to extract FMU 221 | 222 | if (!(FPaths::DirectoryExists(exDir))) { 223 | std::string tempString = TCHAR_TO_UTF8(*exDir); 224 | mkdir(tempString.c_str()); 225 | } 226 | FString actorName = GetName(); // added in case of multiple FMUs 227 | mUnzipDir = exDir + filename + actorName; 228 | 229 | // Extract the FMU: attempt to use unzip, then 7z, then tar 230 | std::string dir = TCHAR_TO_UTF8(*mUnzipDir); 231 | std::string exe = "unzip " + sPath + " -d " + dir; 232 | bool success = true; 233 | if (cmd(exe.c_str()) comparison) { 234 | exe = "7z x \"" + sPath + "\" -aoa -o\"" + dir + "\""; 235 | if (cmd(exe.c_str()) comparison) { 236 | mkdir(dir.c_str()); 237 | exe = "tar -xf \"" + sPath + "\" -C \"" + dir + "\""; 238 | success = !(cmd(exe.c_str()) comparison); 239 | } 240 | } 241 | 242 | // TODO: Dual maintenance with FmuActorComponent.cpp 243 | // TODO: Add jar, minizip or other as unzip option? Use CreateProcess() instead of WinExec()? 244 | FString extracted = success ? "Extracted" : "Failed to extract"; 245 | FString msg(exe.c_str()); 246 | UE_LOG(LogTemp, Display, TEXT("%s fmu using command: %s"), *extracted, *msg); 247 | 248 | // there seems to be a race condition for unzipping where the above messages and ParseXML sometimes occur before the fmu is done unzipping... 249 | ParseXML(); 250 | } 251 | 252 | void AA_FMU::ParseXML() 253 | { 254 | FString xmlFile = mUnzipDir + "/modelDescription.xml"; 255 | 256 | // Add loop to avoid race conditions where the unzip process has not completed 257 | int counter = 0; 258 | while (!FPaths::FileExists(*xmlFile) && counter < 5) { 259 | UE_LOG(LogTemp, Error, TEXT("Attempting to parse XML"), *xmlFile); 260 | Sleep(100); 261 | counter++; 262 | } 263 | 264 | if (!FPaths::FileExists(*xmlFile)) 265 | { 266 | UE_LOG(LogTemp, Error, TEXT("Invalid mPath. %s not found. Make sure a supported unzip tool is on the PATH"), *xmlFile); 267 | return; 268 | } 269 | 270 | // fmiModelDescription (root) 271 | FXmlFile model(xmlFile, EConstructMethod::ConstructFromFile); 272 | FXmlNode* root = model.GetRootNode(); 273 | mFMIVersion = *root->GetAttribute("fmiVersion"); 274 | mModelIdentifier = *root->GetAttribute("modelName"); 275 | mGuid = *root->GetAttribute("guid"); 276 | 277 | // CoSimulation 278 | // - 279 | 280 | // DefaultExperiment 281 | if (bUseXMLExperimentSettings) 282 | { 283 | FXmlNode* defaultExperiment = root->FindChildNode("DefaultExperiment"); 284 | mStartTime = FCString::Atof(*defaultExperiment->GetAttribute("startTime")); 285 | mStopTime = FCString::Atof(*defaultExperiment->GetAttribute("stopTime")); 286 | mStepSize = FCString::Atof(*defaultExperiment->GetAttribute("stepSize")); 287 | mTolerance = FCString::Atof(*defaultExperiment->GetAttribute("tolerance")); 288 | 289 | // Check to ensure defaults exists that make sense. 0 is returned if the attribute is not found in the xml. 290 | if (mStopTime == 0) 291 | { 292 | mStopTime = 1.0; 293 | } 294 | if (mStepSize == 0) 295 | { 296 | mStepSize = 0.1; 297 | } 298 | if (mTolerance == 0) 299 | { 300 | mTolerance = 1e-4; 301 | } 302 | } 303 | 304 | // ModelVariables 305 | FXmlNode* modelVariables = root->FindChildNode("ModelVariables"); 306 | TArray nodes = modelVariables->GetChildrenNodes(); 307 | 308 | // Clear existing values in TMap 309 | mModelVariables.Empty(); 310 | 311 | // Populate TMap 312 | for (FXmlNode* node : nodes) 313 | { 314 | struct FModelVariables ModelVariables; 315 | ModelVariables.ValueReference = FCString::Atoi(*node->GetAttribute("valueReference")); 316 | FString key = node->GetAttribute("name"); 317 | mModelVariables.Add(FName(key), ModelVariables); 318 | // may need to add logic to handle non integer values if needeed in the future. Ignore them or assign them in some other way. 319 | } 320 | 321 | // ModelStructure 322 | // - 323 | 324 | UE_LOG(LogTemp, Display, TEXT("XML parsing complete for: %s"), *mModelIdentifier); 325 | //GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("XML parsing complete for: %s"), *tests); // Does not work in VS2019 326 | } 327 | 328 | float AA_FMU::GetReal(FString Name) 329 | { 330 | if (!mModelVariables.Contains(FName(Name))) 331 | return std::numeric_limits::lowest(); 332 | if (!mbLoaded) 333 | return std::numeric_limits::lowest(); 334 | 335 | try { 336 | 337 | float y = mFmu->getReal(mModelVariables[FName(Name)].ValueReference); 338 | return y; 339 | } 340 | catch (std::exception e) 341 | { 342 | FString errorMsg(e.what()); 343 | UE_LOG(LogTemp, Error, TEXT("%s"), *errorMsg); 344 | return 0; 345 | } 346 | } 347 | 348 | void AA_FMU::DoStep(float StepSize) 349 | { 350 | if (!mbLoaded) 351 | return; 352 | 353 | try { 354 | mFmu->doStep(StepSize); 355 | } 356 | catch (std::exception e) 357 | { 358 | FString errorMsg(e.what()); 359 | UE_LOG(LogTemp, Error, TEXT("%s"), *errorMsg); 360 | } 361 | catch (...) { 362 | UE_LOG(LogTemp, Error, TEXT("An unknown error occurred during doStep.")); 363 | } 364 | } 365 | 366 | void AA_FMU::SetReal(FString Name, float Value) 367 | { 368 | if (!mModelVariables.Contains(FName(Name))) 369 | return; 370 | if (!mbLoaded) 371 | return; 372 | try { 373 | mFmu->setReal(mModelVariables[FName(Name)].ValueReference, Value); 374 | } 375 | catch (std::exception e) 376 | { 377 | FString errorMsg(e.what()); 378 | UE_LOG(LogTemp, Error, TEXT("%s"), *errorMsg); 379 | } 380 | } 381 | 382 | bool AA_FMU::ControlStep(float DeltaTime) 383 | { 384 | if (!mbLoaded) 385 | return false; 386 | 387 | mTimeNow += DeltaTime; 388 | if (!(mTimeNow > mTimeLast + mStepSize / mSpeedMultiplier)) 389 | return false; 390 | 391 | if (mTimeLast >= mStopTime / mSpeedMultiplier) 392 | return false; 393 | 394 | mTimeLast += mStepSize / mSpeedMultiplier; 395 | mFMUTime += mStepSize; 396 | 397 | //if (mbPrintFMUTime) 398 | //{ 399 | // UE_LOG(LogTemp, Warning, TEXT("XML parsing complete for: %f"), mFMUTime); 400 | //} 401 | return true; 402 | } 403 | 404 | void AA_FMU::SetInitialValues() 405 | { 406 | if (!mbLoaded) 407 | return; 408 | for (const TPair& pair : mInitialValues) 409 | { 410 | AA_FMU::SetReal(pair.Key, pair.Value); 411 | } 412 | } --------------------------------------------------------------------------------