├── .dockerfiles ├── Dockerfile └── sources-20.04-azure.list ├── .gitattributes ├── .github └── workflows │ └── ci-cosim-demo-app.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conanfile.txt ├── dev.cljs.edn ├── go.mod ├── go.sum ├── libcosim ├── libcosim.go ├── observers.go ├── scenario.go ├── trending.go └── utils.go ├── main.go ├── project.clj ├── resources └── public │ ├── favicon.ico │ ├── index.html │ └── static │ ├── css │ ├── assets │ │ └── fonts │ │ │ └── icons.woff2 │ ├── icon.css │ ├── semantic.css │ ├── style.css │ └── themes │ │ └── default │ │ └── assets │ │ └── fonts │ │ ├── icons.eot │ │ ├── icons.svg │ │ ├── icons.ttf │ │ ├── icons.woff │ │ └── icons.woff2 │ └── js │ └── plotly.min.js ├── run-linux ├── run-windows.cmd ├── server ├── server.go └── websockets.go ├── set-rpath ├── src └── client │ ├── components.cljs │ ├── config.cljs │ ├── controller.cljs │ ├── core.cljs │ ├── localstorage.cljs │ ├── msgpack.cljs │ ├── msgpack_format.cljs │ ├── scenario.cljs │ ├── trend.cljs │ └── view.cljs ├── structs └── structs.go └── test └── client └── core_test.cljs /.dockerfiles/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | COPY sources-20.04-azure.list /etc/apt/sources.list 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends \ 7 | build-essential \ 8 | curl \ 9 | patchelf \ 10 | git \ 11 | flex \ 12 | python3-setuptools \ 13 | python3-wheel \ 14 | python3-pip \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | # Install Go 18 | RUN curl "https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz" | tar xvz -C /usr/local; \ 19 | export PATH="/usr/local/go/bin:$PATH"; \ 20 | go version 21 | 22 | ENV GOPATH /go 23 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 24 | 25 | RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" 26 | WORKDIR $GOPATH 27 | 28 | # Install additional build tools 29 | RUN go get -v -u github.com/gobuffalo/packr/packr && go clean -cache 30 | RUN pip3 install conan cmake 31 | -------------------------------------------------------------------------------- /.dockerfiles/sources-20.04-azure.list: -------------------------------------------------------------------------------- 1 | deb http://azure.archive.ubuntu.com/ubuntu bionic main universe multiverse 2 | deb http://azure.archive.ubuntu.com/ubuntu bionic-security main 3 | deb http://azure.archive.ubuntu.com/ubuntu bionic-updates main universe multiverse 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | go.mod eol=lf 2 | go.sum eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/ci-cosim-demo-app.yml: -------------------------------------------------------------------------------- 1 | name: cosim-demo-app CI 2 | 3 | # This workflow is triggered on pushes to the repository. 4 | on: [push, workflow_dispatch] 5 | 6 | jobs: 7 | client: 8 | name: Build client 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-java@v1 14 | with: 15 | java-version: '11' 16 | - uses: DeLaGuardo/setup-clojure@3.1 17 | with: 18 | lein: latest 19 | - uses: actions/cache@v1 20 | with: 21 | path: ~/.m2/repository 22 | key: ${{ runner.os }}-m2-${{ hashFiles('**/project.clj') }} 23 | restore-keys: | 24 | ${{ runner.os }}-m2- 25 | - run: lein cljsbuild once min 26 | - run: lein cljsbuild test 27 | - uses: actions/upload-artifact@v2 28 | with: 29 | name: client 30 | path: resources/public/static/js/compiled/app.js 31 | 32 | server: 33 | name: Build Go Application with packr 34 | needs: client 35 | runs-on: ${{ matrix.os }} 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | os: [ubuntu-20.04, windows-2019] 40 | compiler_version: [9] 41 | 42 | env: 43 | CONAN_LOGIN_USERNAME_OSP: ${{ secrets.osp_artifactory_usr }} 44 | CONAN_PASSWORD_OSP: ${{ secrets.osp_artifactory_pwd }} 45 | CONAN_REVISIONS_ENABLED: 1 46 | CONAN_NON_INTERACTIVE: True 47 | 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions/setup-python@v2 51 | with: 52 | python-version: '3.x' 53 | - name: Install prerequisites 54 | run: pip install conan==1.59 55 | - name: Configure libcxx for Linux 56 | run: | 57 | conan profile new default --detect 58 | conan profile update settings.compiler.libcxx=libstdc++11 default 59 | if: runner.os == 'Linux' 60 | - name: Add Conan remote 61 | run: conan remote add osp https://osp.jfrog.io/artifactory/api/conan/conan-local --force 62 | - name: Install Conan deps 63 | run: conan install -s build_type=Release . 64 | - name: Download client 65 | uses: actions/download-artifact@v2 66 | with: 67 | name: client 68 | path: resources/public/static/js/compiled/ 69 | - uses: actions/setup-go@v2 70 | with: 71 | go-version: '1.14' 72 | - name: Install packr 73 | run: go get -v github.com/gobuffalo/packr/packr 74 | - name: packr build 75 | run: packr build -v 76 | env: 77 | CGO_LDFLAGS: '-Wl,-rpath,$ORIGIN/../lib' 78 | - name: Prepare Windows distribution 79 | run: | 80 | cp ./cosim-demo-app.exe dist/bin 81 | cp ./run-windows.cmd ./dist 82 | if: runner.os == 'Windows' 83 | - name: Prepare Linux distribution 84 | run: | 85 | mkdir -p dist/bin 86 | cp ./cosim-demo-app dist/bin 87 | cp ./run-linux ./dist 88 | sh ./set-rpath ./dist/lib 89 | if: runner.os == 'Linux' 90 | - name: Upload artifact 91 | uses: actions/upload-artifact@v2 92 | with: 93 | name: cosim-demo-app-${{ runner.os }} 94 | path: dist 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Linux binary 9 | cosim-demo-app 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Client build directory and artifact 18 | resources/public/static/js/compiled/* 19 | 20 | # Conan output 21 | include/ 22 | dist/ 23 | conan_imports_manifest.txt 24 | conanbuildinfo.txt 25 | conaninfo.txt 26 | 27 | activate_run* 28 | deactivate_run* 29 | environment_run* 30 | 31 | graph_info.json 32 | 33 | # Created by .ignore support plugin (hsz.mobi) 34 | 35 | .idea 36 | *.iml 37 | 38 | target 39 | 40 | .nrepl-port 41 | figwheel_server.log 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | All notable changes to Cosim Demo App will be documented in this file. This includes new features, bug fixes and breaking changes. 3 | For a more detailed list of all changes, click the header links for each version. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 6 | 7 | ### [v0.9.0] – 2022-04-27 8 | 9 | #### changed 10 | Using libcosimc v0.9.0 11 | 12 | #### fixed 13 | Show missing x-value for scatter plots ([PR#201](https://github.com/open-simulation-platform/cosim-demo-app/pull/201)) 14 | 15 | ### [v0.8.3] – 2021-11-29 16 | 17 | #### changed 18 | Using libcosimc v0.8.3 19 | 20 | ### [v0.8.2] – 2021-10-22 21 | 22 | #### changed 23 | - FMU-proxy replaced with proxy-fmu which is included in libcosim. No need to manage the proxy processes manually. 24 | This is now automatically handled by libcosim ([PR#197](https://github.com/open-simulation-platform/cosim-demo-app/pull/197)) 25 | - Update real time functionality ([PR#186](https://github.com/open-simulation-platform/cosim-demo-app/pull/186)) 26 | 27 | ### v0.7.0 – 2020-06-08 28 | First public release. 29 | 30 | 31 | [v0.8.2]: https://github.com/open-simulation-platform/cosim-demo-app/compare/v0.7.0...v0.8.2 32 | [v0.8.3]: https://github.com/open-simulation-platform/cosim-demo-app/compare/v0.8.2...v0.8.3 33 | [v0.9.0]: https://github.com/open-simulation-platform/cosim-demo-app/compare/v0.8.3...v0.9.0 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributor guidelines 2 | ====================== 3 | 4 | This document contains a set of rules and guidelines for everyone who wishes 5 | to contribute to the contents of this repository, hereafter referred to as 6 | "the software". 7 | 8 | 9 | General 10 | ------- 11 | All contributors implicitly agree to license their contribution under the same 12 | terms as the rest of the software. See the `LICENSE` file for details. 13 | 14 | All contributions to the software, in the form of changes, removals or 15 | additions to source code and other files under source control, shall be made 16 | via pull requests. A pull request must always be reviewed and merged by someone 17 | other than its author. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright the Open Simulation Platform contributors. 2 | 3 | Mozilla Public License Version 2.0 4 | ================================== 5 | 6 | 1. Definitions 7 | -------------- 8 | 9 | 1.1. "Contributor" 10 | means each individual or legal entity that creates, contributes to 11 | the creation of, or owns Covered Software. 12 | 13 | 1.2. "Contributor Version" 14 | means the combination of the Contributions of others (if any) used 15 | by a Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | means Covered Software of a particular Contributor. 19 | 20 | 1.4. "Covered Software" 21 | means Source Code Form to which the initial Contributor has attached 22 | the notice in Exhibit A, the Executable Form of such Source Code 23 | Form, and Modifications of such Source Code Form, in each case 24 | including portions thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | (a) that the initial Contributor has attached the notice described 30 | in Exhibit B to the Covered Software; or 31 | 32 | (b) that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the 34 | terms of a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | means any form of the work other than Source Code Form. 38 | 39 | 1.7. "Larger Work" 40 | means a work that combines Covered Software with other material, in 41 | a separate file or files, that is not Covered Software. 42 | 43 | 1.8. "License" 44 | means this document. 45 | 46 | 1.9. "Licensable" 47 | means having the right to grant, to the maximum extent possible, 48 | whether at the time of the initial grant or subsequently, any and 49 | all of the rights conveyed by this License. 50 | 51 | 1.10. "Modifications" 52 | means any of the following: 53 | 54 | (a) any file in Source Code Form that results from an addition to, 55 | deletion from, or modification of the contents of Covered 56 | Software; or 57 | 58 | (b) any new file in Source Code Form that contains any Covered 59 | Software. 60 | 61 | 1.11. "Patent Claims" of a Contributor 62 | means any patent claim(s), including without limitation, method, 63 | process, and apparatus claims, in any patent Licensable by such 64 | Contributor that would be infringed, but for the grant of the 65 | License, by the making, using, selling, offering for sale, having 66 | made, import, or transfer of either its Contributions or its 67 | Contributor Version. 68 | 69 | 1.12. "Secondary License" 70 | means either the GNU General Public License, Version 2.0, the GNU 71 | Lesser General Public License, Version 2.1, the GNU Affero General 72 | Public License, Version 3.0, or any later versions of those 73 | licenses. 74 | 75 | 1.13. "Source Code Form" 76 | means the form of the work preferred for making modifications. 77 | 78 | 1.14. "You" (or "Your") 79 | means an individual or a legal entity exercising rights under this 80 | License. For legal entities, "You" includes any entity that 81 | controls, is controlled by, or is under common control with You. For 82 | purposes of this definition, "control" means (a) the power, direct 83 | or indirect, to cause the direction or management of such entity, 84 | whether by contract or otherwise, or (b) ownership of more than 85 | fifty percent (50%) of the outstanding shares or beneficial 86 | ownership of such entity. 87 | 88 | 2. License Grants and Conditions 89 | -------------------------------- 90 | 91 | 2.1. Grants 92 | 93 | Each Contributor hereby grants You a world-wide, royalty-free, 94 | non-exclusive license: 95 | 96 | (a) under intellectual property rights (other than patent or trademark) 97 | Licensable by such Contributor to use, reproduce, make available, 98 | modify, display, perform, distribute, and otherwise exploit its 99 | Contributions, either on an unmodified basis, with Modifications, or 100 | as part of a Larger Work; and 101 | 102 | (b) under Patent Claims of such Contributor to make, use, sell, offer 103 | for sale, have made, import, and otherwise transfer either its 104 | Contributions or its Contributor Version. 105 | 106 | 2.2. Effective Date 107 | 108 | The licenses granted in Section 2.1 with respect to any Contribution 109 | become effective for each Contribution on the date the Contributor first 110 | distributes such Contribution. 111 | 112 | 2.3. Limitations on Grant Scope 113 | 114 | The licenses granted in this Section 2 are the only rights granted under 115 | this License. No additional rights or licenses will be implied from the 116 | distribution or licensing of Covered Software under this License. 117 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 118 | Contributor: 119 | 120 | (a) for any code that a Contributor has removed from Covered Software; 121 | or 122 | 123 | (b) for infringements caused by: (i) Your and any other third party's 124 | modifications of Covered Software, or (ii) the combination of its 125 | Contributions with other software (except as part of its Contributor 126 | Version); or 127 | 128 | (c) under Patent Claims infringed by Covered Software in the absence of 129 | its Contributions. 130 | 131 | This License does not grant any rights in the trademarks, service marks, 132 | or logos of any Contributor (except as may be necessary to comply with 133 | the notice requirements in Section 3.4). 134 | 135 | 2.4. Subsequent Licenses 136 | 137 | No Contributor makes additional grants as a result of Your choice to 138 | distribute the Covered Software under a subsequent version of this 139 | License (see Section 10.2) or under the terms of a Secondary License (if 140 | permitted under the terms of Section 3.3). 141 | 142 | 2.5. Representation 143 | 144 | Each Contributor represents that the Contributor believes its 145 | Contributions are its original creation(s) or it has sufficient rights 146 | to grant the rights to its Contributions conveyed by this License. 147 | 148 | 2.6. Fair Use 149 | 150 | This License is not intended to limit any rights You have under 151 | applicable copyright doctrines of fair use, fair dealing, or other 152 | equivalents. 153 | 154 | 2.7. Conditions 155 | 156 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 157 | in Section 2.1. 158 | 159 | 3. Responsibilities 160 | ------------------- 161 | 162 | 3.1. Distribution of Source Form 163 | 164 | All distribution of Covered Software in Source Code Form, including any 165 | Modifications that You create or to which You contribute, must be under 166 | the terms of this License. You must inform recipients that the Source 167 | Code Form of the Covered Software is governed by the terms of this 168 | License, and how they can obtain a copy of this License. You may not 169 | attempt to alter or restrict the recipients' rights in the Source Code 170 | Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | (a) such Covered Software must also be made available in Source Code 177 | Form, as described in Section 3.1, and You must inform recipients of 178 | the Executable Form how they can obtain a copy of such Source Code 179 | Form by reasonable means in a timely manner, at a charge no more 180 | than the cost of distribution to the recipient; and 181 | 182 | (b) You may distribute such Executable Form under the terms of this 183 | License, or sublicense it under different terms, provided that the 184 | license for the Executable Form does not attempt to limit or alter 185 | the recipients' rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for 191 | the Covered Software. If the Larger Work is a combination of Covered 192 | Software with a work governed by one or more Secondary Licenses, and the 193 | Covered Software is not Incompatible With Secondary Licenses, this 194 | License permits You to additionally distribute such Covered Software 195 | under the terms of such Secondary License(s), so that the recipient of 196 | the Larger Work may, at their option, further distribute the Covered 197 | Software under the terms of either this License or such Secondary 198 | License(s). 199 | 200 | 3.4. Notices 201 | 202 | You may not remove or alter the substance of any license notices 203 | (including copyright notices, patent notices, disclaimers of warranty, 204 | or limitations of liability) contained within the Source Code Form of 205 | the Covered Software, except that You may alter any license notices to 206 | the extent required to remedy known factual inaccuracies. 207 | 208 | 3.5. Application of Additional Terms 209 | 210 | You may choose to offer, and to charge a fee for, warranty, support, 211 | indemnity or liability obligations to one or more recipients of Covered 212 | Software. However, You may do so only on Your own behalf, and not on 213 | behalf of any Contributor. You must make it absolutely clear that any 214 | such warranty, support, indemnity, or liability obligation is offered by 215 | You alone, and You hereby agree to indemnify every Contributor for any 216 | liability incurred by such Contributor as a result of warranty, support, 217 | indemnity or liability terms You offer. You may include additional 218 | disclaimers of warranty and limitations of liability specific to any 219 | jurisdiction. 220 | 221 | 4. Inability to Comply Due to Statute or Regulation 222 | --------------------------------------------------- 223 | 224 | If it is impossible for You to comply with any of the terms of this 225 | License with respect to some or all of the Covered Software due to 226 | statute, judicial order, or regulation then You must: (a) comply with 227 | the terms of this License to the maximum extent possible; and (b) 228 | describe the limitations and the code they affect. Such description must 229 | be placed in a text file included with all distributions of the Covered 230 | Software under this License. Except to the extent prohibited by statute 231 | or regulation, such description must be sufficiently detailed for a 232 | recipient of ordinary skill to be able to understand it. 233 | 234 | 5. Termination 235 | -------------- 236 | 237 | 5.1. The rights granted under this License will terminate automatically 238 | if You fail to comply with any of its terms. However, if You become 239 | compliant, then the rights granted under this License from a particular 240 | Contributor are reinstated (a) provisionally, unless and until such 241 | Contributor explicitly and finally terminates Your grants, and (b) on an 242 | ongoing basis, if such Contributor fails to notify You of the 243 | non-compliance by some reasonable means prior to 60 days after You have 244 | come back into compliance. Moreover, Your grants from a particular 245 | Contributor are reinstated on an ongoing basis if such Contributor 246 | notifies You of the non-compliance by some reasonable means, this is the 247 | first time You have received notice of non-compliance with this License 248 | from such Contributor, and You become compliant prior to 30 days after 249 | Your receipt of the notice. 250 | 251 | 5.2. If You initiate litigation against any entity by asserting a patent 252 | infringement claim (excluding declaratory judgment actions, 253 | counter-claims, and cross-claims) alleging that a Contributor Version 254 | directly or indirectly infringes any patent, then the rights granted to 255 | You by any and all Contributors for the Covered Software under Section 256 | 2.1 of this License shall terminate. 257 | 258 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 259 | end user license agreements (excluding distributors and resellers) which 260 | have been validly granted by You or Your distributors under this License 261 | prior to termination shall survive termination. 262 | 263 | ************************************************************************ 264 | * * 265 | * 6. Disclaimer of Warranty * 266 | * ------------------------- * 267 | * * 268 | * Covered Software is provided under this License on an "as is" * 269 | * basis, without warranty of any kind, either expressed, implied, or * 270 | * statutory, including, without limitation, warranties that the * 271 | * Covered Software is free of defects, merchantable, fit for a * 272 | * particular purpose or non-infringing. The entire risk as to the * 273 | * quality and performance of the Covered Software is with You. * 274 | * Should any Covered Software prove defective in any respect, You * 275 | * (not any Contributor) assume the cost of any necessary servicing, * 276 | * repair, or correction. This disclaimer of warranty constitutes an * 277 | * essential part of this License. No use of any Covered Software is * 278 | * authorized under this License except under this disclaimer. * 279 | * * 280 | ************************************************************************ 281 | 282 | ************************************************************************ 283 | * * 284 | * 7. Limitation of Liability * 285 | * -------------------------- * 286 | * * 287 | * Under no circumstances and under no legal theory, whether tort * 288 | * (including negligence), contract, or otherwise, shall any * 289 | * Contributor, or anyone who distributes Covered Software as * 290 | * permitted above, be liable to You for any direct, indirect, * 291 | * special, incidental, or consequential damages of any character * 292 | * including, without limitation, damages for lost profits, loss of * 293 | * goodwill, work stoppage, computer failure or malfunction, or any * 294 | * and all other commercial damages or losses, even if such party * 295 | * shall have been informed of the possibility of such damages. This * 296 | * limitation of liability shall not apply to liability for death or * 297 | * personal injury resulting from such party's negligence to the * 298 | * extent applicable law prohibits such limitation. Some * 299 | * jurisdictions do not allow the exclusion or limitation of * 300 | * incidental or consequential damages, so this exclusion and * 301 | * limitation may not apply to You. * 302 | * * 303 | ************************************************************************ 304 | 305 | 8. Litigation 306 | ------------- 307 | 308 | Any litigation relating to this License may be brought only in the 309 | courts of a jurisdiction where the defendant maintains its principal 310 | place of business and such litigation shall be governed by laws of that 311 | jurisdiction, without reference to its conflict-of-law provisions. 312 | Nothing in this Section shall prevent a party's ability to bring 313 | cross-claims or counter-claims. 314 | 315 | 9. Miscellaneous 316 | ---------------- 317 | 318 | This License represents the complete agreement concerning the subject 319 | matter hereof. If any provision of this License is held to be 320 | unenforceable, such provision shall be reformed only to the extent 321 | necessary to make it enforceable. Any law or regulation which provides 322 | that the language of a contract shall be construed against the drafter 323 | shall not be used to construe this License against a Contributor. 324 | 325 | 10. Versions of the License 326 | --------------------------- 327 | 328 | 10.1. New Versions 329 | 330 | Mozilla Foundation is the license steward. Except as provided in Section 331 | 10.3, no one other than the license steward has the right to modify or 332 | publish new versions of this License. Each version will be given a 333 | distinguishing version number. 334 | 335 | 10.2. Effect of New Versions 336 | 337 | You may distribute the Covered Software under the terms of the version 338 | of the License under which You originally received the Covered Software, 339 | or under the terms of any subsequent version published by the license 340 | steward. 341 | 342 | 10.3. Modified Versions 343 | 344 | If you create software not governed by this License, and you want to 345 | create a new license for such software, you may create and use a 346 | modified version of this License if you rename the license and remove 347 | any references to the name of the license steward (except to note that 348 | such modified license differs from this License). 349 | 350 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 351 | Licenses 352 | 353 | If You choose to distribute Source Code Form that is Incompatible With 354 | Secondary Licenses under the terms of this version of the License, the 355 | notice described in Exhibit B of this License must be attached. 356 | 357 | Exhibit A - Source Code Form License Notice 358 | ------------------------------------------- 359 | 360 | This Source Code Form is subject to the terms of the Mozilla Public 361 | License, v. 2.0. If a copy of the MPL was not distributed with this 362 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 363 | 364 | If it is not possible or desirable to put the notice in a particular 365 | file, then You may include the notice in a location (such as a LICENSE 366 | file in a relevant directory) where a recipient would be likely to look 367 | for such a notice. 368 | 369 | You may add additional accurate notices of copyright ownership. 370 | 371 | Exhibit B - "Incompatible With Secondary Licenses" Notice 372 | --------------------------------------------------------- 373 | 374 | This Source Code Form is "Incompatible With Secondary Licenses", as 375 | defined by the Mozilla Public License, v. 2.0. 376 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cosim Demo Application 2 | ========================== 3 | ![cosim-demo-app CI](https://github.com/open-simulation-platform/cosim-demo-app/workflows/cosim-demo-app%20CI/badge.svg) 4 | 5 | This repository contains a server-client demo application for libcosim. 6 | The server is written in Go and the client in clojurescript 7 | 8 | Server 9 | ------------ 10 | 11 | ### Required tools 12 | * Conan v1.59 (currently v2 is not supported) 13 | * Go dev tools: [Golang](https://golang.org/dl/) >= 1.11 14 | * Compiler: [MinGW-w64](https://sourceforge.net/projects/mingw-w64/?source=typ_redirect) (Windows), GCC >= 9 (Linux) 15 | * Package managers: [Conan](https://conan.io/) and [Go Modules](https://github.com/golang/go/wiki/Modules) 16 | 17 | Throughout this guide, we will use Conan to manage C++ dependencies. However, you can also install the C++ dependencies manually. 18 | 19 | **Note: About the installation of MinGW-w64 (Windows)** 20 | 21 | An easier way to install it is to download the Mingw-w64 automated installer from 22 | [here](https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe/download) 23 | and follow the steps in the wizard. It is essential that the installation path does not contain any spaces. 24 | Install a current version and specify win32 as thread when requested. Additionally, choose the architecture x86_64. 25 | 26 | After installing it, you need to add it to the PATH environment variable (add the path where 27 | your MinGW-w64 has been installed to e.g., C:\mingw\mingw64\bin). 28 | 29 | Alternatively, MinGW can also be installed via [chocolatey](https://chocolatey.org/install). 30 | Once chocolatey is installed, simply run the following command (as admin) to install MinGW: 31 | ```ps 32 | choco install mingw 33 | ``` 34 | 35 | ### Step 1: Configure Conan 36 | 37 | First, add the OSP Conan repository as a remote and configure the username and 38 | password to access it: 39 | 40 | conan remote add osp https://osp.jfrog.io/artifactory/api/conan/conan-local 41 | 42 | ### Step 2: Build and run 43 | 44 | You can do this in two ways: 45 | 46 | #### Alternative 1: Using Conan 47 | 48 | From the cosim-demo-app source directory, get C/C++ dependencies using Conan: 49 | ```bash 50 | conan install . -u -s build_type=Release -g virtualrunenv 51 | source activate_run.sh # or run activate_run.bat in windows 52 | go build 53 | ``` 54 | 55 | To run the application on Windows: 56 | 57 | activate_run.bat (activate_run.ps1 in PowerShell) 58 | cosim-demo-app.exe 59 | deactivate_run.bat when done (deactivate_run.ps1 in PowerShell) 60 | 61 | To run the application on Linux: 62 | 63 | source activate_run.sh 64 | ./cosim-demo-app 65 | ./deactivate_run.sh when done 66 | 67 | Open a browser at http://localhost:8000/status to verify that it's running (you should see some JSON). 68 | 69 | #### Alternative 2: Manually handle libcosimc dependencies 70 | 71 | You will have to define CGO environment variables with arguments pointing to your libcosimc headers and libraries. An 72 | example for Windows can be: 73 | 74 | set CGO_CFLAGS=-IC:\dev\libcosimc\include 75 | set CGO_LDFLAGS=-LC:\dev\libcosimc\bin -lcosim -lcosimc 76 | go build 77 | 78 | To run the application on Windows you need to also update the path to point to your libraries: 79 | 80 | set PATH=C:\dev\libcosimc\bin;%PATH% 81 | cosim-demo-app.exe 82 | 83 | To run the application on Linux you need to update the LD_LIBRARY_PATH: 84 | 85 | LD_LIBRARY_PATH=~dev/libcosimc/lib:$LD_LIBRARY_PATH 86 | export LD_LIBRARY_PATH 87 | ./cosim-demo-app 88 | 89 | Open a browser at http://localhost:8000/status to verify that it's running (you should see some JSON). 90 | 91 | Client 92 | ------ 93 | Providing a web user interface. 94 | 95 | #### Development mode 96 | - Install a [JDK](https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 97 | - Install leiningen https://leiningen.org/ 98 | - Run `lein figwheel` 99 | - View it in your browser at http://localhost:3449 100 | 101 | You now have a framework running for live reloading of client code. 102 | 103 | #### Building the client 104 | - Run `lein cljsbuild once min` 105 | - The client application will be compiled to `/resources/js/compiled` 106 | 107 | 108 | ### Create distribution with built-in client 109 | 110 | To package the application with the client you can use packr. You can install packr and build distributable with: 111 | 112 | go get -u github.com/gobuffalo/packr/packr 113 | packr build 114 | -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [generators] 2 | 3 | [requires] 4 | libcosimc/0.10.2@osp/stable 5 | 6 | [options] 7 | libcosim:proxyfmu=True 8 | 9 | [imports] 10 | include, cosim.h -> ./include 11 | 12 | bin, boost_chrono*.dll -> ./dist/bin 13 | bin, boost_context*.dll -> ./dist/bin 14 | bin, boost_date_time*.dll -> ./dist/bin 15 | bin, boost_fiber*.dll -> ./dist/bin 16 | bin, boost_filesystem*.dll -> ./dist/bin 17 | bin, boost_log*.dll -> ./dist/bin 18 | bin, boost_system*.dll -> ./dist/bin 19 | bin, boost_thread*.dll -> ./dist/bin 20 | bin, cosim*.dll -> ./dist/bin 21 | bin, fmilib_shared.dll -> ./dist/bin 22 | bin, xerces-c*.dll -> ./dist/bin 23 | bin, yaml-cpp*.dll -> ./dist/bin 24 | bin, zip.dll -> ./dist/bin 25 | bin, proxyfmu* -> ./dist/bin 26 | bin, fmilibwrapper*.dll -> ./dist/bin 27 | 28 | lib, libboost_chrono.so.* -> ./dist/lib 29 | lib, libboost_context.so.* -> ./dist/lib 30 | lib, libboost_date_time.so.* -> ./dist/lib 31 | lib, libboost_fiber.so.* -> ./dist/lib 32 | lib, libboost_filesystem.so.* -> ./dist/lib 33 | lib, libboost_log.so.* -> ./dist/lib 34 | lib, libboost_system.so.* -> ./dist/lib 35 | lib, libboost_thread.so* -> ./dist/lib 36 | lib, libcosim*.so -> ./dist/lib 37 | lib, libfmilib_shared.so -> ./dist/lib 38 | lib, libxerces-c*.so -> ./dist/lib 39 | lib, libyaml-cpp*.so.* -> ./dist/lib 40 | lib, libzip.so* -> ./dist/lib 41 | lib, libfmilibwrapper*.so -> ./dist/lib 42 | lib, libproxyfmu-client*.so -> ./dist/lib 43 | 44 | ., license* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 45 | ., */license* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 46 | ., copying* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 47 | ., */copying* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 48 | ., notice* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 49 | ., */notice* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 50 | ., authors* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 51 | ., */authors* -> ./dist/doc/licenses @ folder=True, ignore_case=True, keep_path=False 52 | -------------------------------------------------------------------------------- /dev.cljs.edn: -------------------------------------------------------------------------------- 1 | ^{:watch-dirs ["test" "src"] 2 | :css-dirs ["resources/public/static/css"] 3 | :auto-testing true} 4 | {:main client.core} -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module cosim-demo-app 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gobuffalo/packr v1.30.1 7 | github.com/gorilla/mux v1.7.4 8 | github.com/gorilla/websocket v1.4.2 9 | github.com/ugorji/go/codec v1.1.7 10 | github.com/gobuffalo/envy v1.7.0 // indirect 11 | github.com/gobuffalo/packd v0.3.0 // indirect 12 | github.com/joho/godotenv v1.3.0 // indirect 13 | github.com/rogpeppe/go-internal v1.3.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 3 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 4 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 5 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 6 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 11 | github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= 12 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 13 | github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= 14 | github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= 15 | github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= 16 | github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= 17 | github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= 18 | github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= 19 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 20 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 21 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 22 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 23 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 24 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 25 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 26 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 27 | github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 28 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 29 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 30 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 31 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 32 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 33 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 34 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 35 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 36 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 40 | github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= 41 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 42 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 43 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 44 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 45 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 46 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 47 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 48 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 49 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 52 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 53 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 54 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 55 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 56 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 57 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 58 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 59 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 60 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 61 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 62 | golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 63 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 66 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 75 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 76 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 77 | -------------------------------------------------------------------------------- /libcosim/libcosim.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package libcosim 6 | 7 | /* 8 | #cgo CFLAGS: -I${SRCDIR}/../include 9 | #cgo LDFLAGS: -L${SRCDIR}/../dist/bin -L${SRCDIR}/../dist/lib -lcosimc -lstdc++ 10 | #include 11 | */ 12 | import "C" 13 | import ( 14 | "cosim-demo-app/structs" 15 | "errors" 16 | "fmt" 17 | "io/ioutil" 18 | "log" 19 | "os" 20 | "path/filepath" 21 | "strconv" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | func printLastError() { 27 | fmt.Printf("Error code %d: %s\n", int(C.cosim_last_error_code()), C.GoString(C.cosim_last_error_message())) 28 | } 29 | 30 | func lastErrorMessage() string { 31 | msg := C.cosim_last_error_message() 32 | return C.GoString(msg) 33 | } 34 | 35 | func lastErrorCode() C.cosim_errc { 36 | return C.cosim_last_error_code() 37 | } 38 | 39 | func createExecution() (execution *C.cosim_execution) { 40 | startTime := C.cosim_time_point(0.0 * 1e9) 41 | stepSize := C.cosim_duration(0.1 * 1e9) 42 | execution = C.cosim_execution_create(startTime, stepSize) 43 | return execution 44 | } 45 | 46 | func createConfigExecution(confDir string) (execution *C.cosim_execution) { 47 | startTime := C.cosim_time_point(0.0 * 1e9) 48 | execution = C.cosim_osp_config_execution_create(C.CString(confDir), false, startTime) 49 | return execution 50 | } 51 | 52 | func createSsdExecution(ssdDir string) (execution *C.cosim_execution) { 53 | startTime := C.cosim_time_point(0.0 * 1e9) 54 | execution = C.cosim_ssp_execution_create(C.CString(ssdDir), false, startTime) 55 | return execution 56 | } 57 | 58 | type executionStatus struct { 59 | time float64 60 | totalAverageRealTimeFactor float64 61 | rollingAverageRealTimeFactor float64 62 | realTimeFactorTarget float64 63 | stepsToMonitor int 64 | isRealTimeSimulation bool 65 | state string 66 | lastErrorMessage string 67 | lastErrorCode string 68 | } 69 | 70 | func translateState(state C.cosim_execution_state) string { 71 | switch state { 72 | case C.COSIM_EXECUTION_STOPPED: 73 | return "COSIM_EXECUTION_STOPPED" 74 | case C.COSIM_EXECUTION_RUNNING: 75 | return "COSIM_EXECUTION_RUNNING" 76 | case C.COSIM_EXECUTION_ERROR: 77 | return "COSIM_EXECUTION_ERROR" 78 | } 79 | return "UNKNOWN" 80 | } 81 | 82 | func translateErrorCode(code C.cosim_errc) string { 83 | switch code { 84 | case C.COSIM_ERRC_SUCCESS: 85 | return "COSIM_ERRC_SUCCESS" 86 | case C.COSIM_ERRC_UNSPECIFIED: 87 | return "COSIM_ERRC_UNSPECIFIED" 88 | case C.COSIM_ERRC_ERRNO: 89 | return "COSIM_ERRC_ERRNO" 90 | case C.COSIM_ERRC_INVALID_ARGUMENT: 91 | return "COSIM_ERRC_INVALID_ARGUMENT" 92 | case C.COSIM_ERRC_ILLEGAL_STATE: 93 | return "COSIM_ERRC_ILLEGAL_STATE" 94 | case C.COSIM_ERRC_OUT_OF_RANGE: 95 | return "COSIM_ERRC_OUT_OF_RANGE" 96 | case C.COSIM_ERRC_STEP_TOO_LONG: 97 | return "COSIM_ERRC_STEP_TOO_LONG" 98 | case C.COSIM_ERRC_BAD_FILE: 99 | return "COSIM_ERRC_BAD_FILE" 100 | case C.COSIM_ERRC_UNSUPPORTED_FEATURE: 101 | return "COSIM_ERRC_UNSUPPORTED_FEATURE" 102 | case C.COSIM_ERRC_DL_LOAD_ERROR: 103 | return "COSIM_ERRC_DL_LOAD_ERROR" 104 | case C.COSIM_ERRC_MODEL_ERROR: 105 | return "COSIM_ERRC_MODEL_ERROR" 106 | case C.COSIM_ERRC_SIMULATION_ERROR: 107 | return "COSIM_ERRC_SIMULATION_ERROR" 108 | case C.COSIM_ERRC_ZIP_ERROR: 109 | return "COSIM_ERRC_ZIP_ERROR" 110 | } 111 | return "UNKNOWN" 112 | } 113 | 114 | func getExecutionStatus(execution *C.cosim_execution) (execStatus executionStatus) { 115 | var status C.cosim_execution_status 116 | success := int(C.cosim_execution_get_status(execution, &status)) 117 | nanoTime := int64(status.current_time) 118 | execStatus.time = float64(nanoTime) * 1e-9 119 | execStatus.totalAverageRealTimeFactor = float64(status.total_average_real_time_factor) 120 | execStatus.rollingAverageRealTimeFactor = float64(status.rolling_average_real_time_factor) 121 | execStatus.realTimeFactorTarget = float64(status.real_time_factor_target) 122 | execStatus.isRealTimeSimulation = int(status.is_real_time_simulation) > 0 123 | execStatus.stepsToMonitor = int(status.steps_to_monitor) 124 | execStatus.state = translateState(status.state) 125 | if success < 0 || status.state == C.COSIM_EXECUTION_ERROR { 126 | execStatus.lastErrorMessage = lastErrorMessage() 127 | execStatus.lastErrorCode = translateErrorCode(lastErrorCode()) 128 | } 129 | return 130 | } 131 | 132 | func createLocalSlave(fmuPath string, instanceName string) *C.cosim_slave { 133 | return C.cosim_local_slave_create(C.CString(fmuPath), C.CString(instanceName)) 134 | } 135 | 136 | func createOverrideManipulator() (manipulator *C.cosim_manipulator) { 137 | manipulator = C.cosim_override_manipulator_create() 138 | return 139 | } 140 | 141 | func executionAddManipulator(execution *C.cosim_execution, manipulator *C.cosim_manipulator) { 142 | C.cosim_execution_add_manipulator(execution, manipulator) 143 | } 144 | 145 | func executionAddSlave(execution *C.cosim_execution, slave *C.cosim_slave) int { 146 | slaveIndex := C.cosim_execution_add_slave(execution, slave) 147 | if slaveIndex < 0 { 148 | printLastError() 149 | C.cosim_execution_destroy(execution) 150 | } 151 | return int(slaveIndex) 152 | } 153 | 154 | func executionStart(execution *C.cosim_execution) (bool, string) { 155 | success := C.cosim_execution_start(execution) 156 | if int(success) < 0 { 157 | return false, strCat("Unable to start simulation: ", lastErrorMessage()) 158 | } else { 159 | return true, "Simulation is running" 160 | } 161 | } 162 | 163 | func executionDestroy(execution *C.cosim_execution) { 164 | C.cosim_execution_destroy(execution) 165 | } 166 | 167 | func localSlaveDestroy(slave *C.cosim_slave) { 168 | C.cosim_local_slave_destroy(slave) 169 | } 170 | 171 | func manipulatorDestroy(manipulator *C.cosim_manipulator) { 172 | C.cosim_manipulator_destroy(manipulator) 173 | } 174 | 175 | func executionStop(execution *C.cosim_execution) (bool, string) { 176 | success := C.cosim_execution_stop(execution) 177 | if int(success) < 0 { 178 | return false, strCat("Unable to stop simulation: ", lastErrorMessage()) 179 | } else { 180 | return true, "Simulation is paused" 181 | } 182 | } 183 | 184 | func executionEnableRealTime(execution *C.cosim_execution) (bool, string) { 185 | success := C.cosim_execution_enable_real_time_simulation(execution) 186 | if int(success) < 0 { 187 | return false, strCat("Unable to enable real time: ", lastErrorMessage()) 188 | } else { 189 | return true, "Real time execution enabled" 190 | } 191 | } 192 | 193 | func executionDisableRealTime(execution *C.cosim_execution) (bool, string) { 194 | success := C.cosim_execution_disable_real_time_simulation(execution) 195 | if int(success) < 0 { 196 | return false, strCat("Unable to disable real time: ", lastErrorMessage()) 197 | } else { 198 | return true, "Real time execution disabled" 199 | } 200 | } 201 | 202 | func executionSetCustomRealTimeFactor(execution *C.cosim_execution, realTimeFactor string) (bool, string) { 203 | val, err := strconv.ParseFloat(realTimeFactor, 64) 204 | 205 | if err != nil { 206 | log.Println(err) 207 | return false, err.Error() 208 | } 209 | 210 | if val <= 0.0 { 211 | return false, "Real time factor target must be greater than 0.0" 212 | } 213 | 214 | C.cosim_execution_set_real_time_factor_target(execution, C.double(val)) 215 | 216 | return true, "Custom real time factor successfully set" 217 | } 218 | 219 | func executionSetStepsToMonitor(execution *C.cosim_execution, numSteps string) (bool, string) { 220 | val, err := strconv.Atoi(numSteps) 221 | 222 | if err != nil { 223 | log.Println(err) 224 | return false, err.Error() 225 | } 226 | 227 | C.cosim_execution_set_steps_to_monitor(execution, C.int(val)) 228 | 229 | return true, "Custom real time factor successfully set" 230 | } 231 | 232 | func setReal(manipulator *C.cosim_manipulator, slaveIndex int, valueRef int, value float64) (bool, string) { 233 | vr := make([]C.cosim_value_reference, 1) 234 | vr[0] = C.cosim_value_reference(valueRef) 235 | v := make([]C.double, 1) 236 | v[0] = C.double(value) 237 | success := C.cosim_manipulator_slave_set_real(manipulator, C.cosim_slave_index(slaveIndex), &vr[0], C.size_t(1), &v[0]) 238 | if int(success) < 0 { 239 | return false, strCat("Unable to set real variable value: ", lastErrorMessage()) 240 | } else { 241 | return true, "Successfully set real variable value" 242 | } 243 | } 244 | 245 | func setInteger(manipulator *C.cosim_manipulator, slaveIndex int, valueRef int, value int) (bool, string) { 246 | vr := make([]C.cosim_value_reference, 1) 247 | vr[0] = C.cosim_value_reference(valueRef) 248 | v := make([]C.int, 1) 249 | v[0] = C.int(value) 250 | success := C.cosim_manipulator_slave_set_integer(manipulator, C.cosim_slave_index(slaveIndex), &vr[0], C.size_t(1), &v[0]) 251 | if int(success) < 0 { 252 | return false, strCat("Unable to set integer variable value: ", lastErrorMessage()) 253 | } else { 254 | return true, "Successfully set integer variable value" 255 | } 256 | } 257 | 258 | func setBoolean(manipulator *C.cosim_manipulator, slaveIndex int, valueRef int, value bool) (bool, string) { 259 | vr := make([]C.cosim_value_reference, 1) 260 | vr[0] = C.cosim_value_reference(valueRef) 261 | v := make([]C.bool, 1) 262 | v[0] = C.bool(value) 263 | success := C.cosim_manipulator_slave_set_boolean(manipulator, C.cosim_slave_index(slaveIndex), &vr[0], C.size_t(1), &v[0]) 264 | if int(success) < 0 { 265 | return false, strCat("Unable to set boolean variable value: ", lastErrorMessage()) 266 | } else { 267 | return true, "Successfully set boolean variable value" 268 | } 269 | } 270 | 271 | func setString(manipulator *C.cosim_manipulator, slaveIndex int, valueRef int, value string) (bool, string) { 272 | vr := make([]C.cosim_value_reference, 1) 273 | vr[0] = C.cosim_value_reference(valueRef) 274 | v := make([]*C.char, 1) 275 | v[0] = C.CString(value) 276 | success := C.cosim_manipulator_slave_set_string(manipulator, C.cosim_slave_index(slaveIndex), &vr[0], C.size_t(1), &v[0]) 277 | if int(success) < 0 { 278 | return false, strCat("Unable to set boolean variable value", lastErrorMessage()) 279 | } else { 280 | return true, "Successfully set boolean variable value" 281 | } 282 | } 283 | 284 | func setVariableValue(sim *Simulation, slaveIndex string, valueType string, valueReference string, value string) (bool, string) { 285 | index, err := strconv.Atoi(slaveIndex) 286 | if err != nil { 287 | return false, strCat("Can't parse slave index as integer: ", slaveIndex) 288 | } 289 | valueRef, err := strconv.Atoi(valueReference) 290 | if err != nil { 291 | return false, strCat("Can't parse value reference as integer: ", valueReference) 292 | } 293 | switch valueType { 294 | case "Real": 295 | val, err := strconv.ParseFloat(value, 64) 296 | if err != nil { 297 | log.Println(err) 298 | return false, strCat("Can't parse value as double: ", value, ", error: ", err.Error()) 299 | } else { 300 | return setReal(sim.OverrideManipulator, index, valueRef, val) 301 | } 302 | case "Integer": 303 | val, err := strconv.Atoi(value) 304 | if err != nil { 305 | log.Println(err) 306 | return false, strCat("Can't parse value as integer: ", value, ", error: ", err.Error()) 307 | } else { 308 | return setInteger(sim.OverrideManipulator, index, valueRef, val) 309 | } 310 | case "Boolean": 311 | var val = false 312 | if "true" == value { 313 | val = true 314 | } 315 | return setBoolean(sim.OverrideManipulator, index, valueRef, val) 316 | case "String": 317 | return setString(sim.OverrideManipulator, index, valueRef, value) 318 | 319 | default: 320 | message := strCat("Can't set this value: ", value) 321 | fmt.Println(message) 322 | return false, message 323 | } 324 | } 325 | 326 | func resetVariable(manipulator *C.cosim_manipulator, slaveIndex int, variableType C.cosim_variable_type, valueRef int) (bool, string) { 327 | vr := make([]C.cosim_value_reference, 1) 328 | vr[0] = C.cosim_value_reference(valueRef) 329 | success := C.cosim_manipulator_slave_reset(manipulator, C.cosim_slave_index(slaveIndex), variableType, &vr[0], C.size_t(1)) 330 | if int(success) < 0 { 331 | return false, strCat("Unable to reset variable value: ", lastErrorMessage()) 332 | } else { 333 | return true, "Successfully reset variable value" 334 | } 335 | } 336 | 337 | func resetVariableValue(sim *Simulation, slaveIndex string, valueType string, valueReference string) (bool, string) { 338 | index, err := strconv.Atoi(slaveIndex) 339 | if err != nil { 340 | return false, strCat("Can't parse slave index as integer: ", slaveIndex) 341 | } 342 | valueRef, err := strconv.Atoi(valueReference) 343 | if err != nil { 344 | return false, strCat("Can't parse value reference as integer: ", valueReference) 345 | } 346 | switch valueType { 347 | case "Real": 348 | return resetVariable(sim.OverrideManipulator, index, C.COSIM_VARIABLE_TYPE_REAL, valueRef) 349 | case "Integer": 350 | return resetVariable(sim.OverrideManipulator, index, C.COSIM_VARIABLE_TYPE_INTEGER, valueRef) 351 | case "Boolean": 352 | return resetVariable(sim.OverrideManipulator, index, C.COSIM_VARIABLE_TYPE_BOOLEAN, valueRef) 353 | case "String": 354 | return resetVariable(sim.OverrideManipulator, index, C.COSIM_VARIABLE_TYPE_STRING, valueRef) 355 | default: 356 | message := strCat("Can't reset variable with type ", valueType, " and value reference ", valueReference, " for slave with index ", slaveIndex) 357 | log.Println(message) 358 | return false, message 359 | } 360 | } 361 | 362 | func parseCausality(causality C.cosim_variable_causality) (string, error) { 363 | switch causality { 364 | case C.COSIM_VARIABLE_CAUSALITY_INPUT: 365 | return "input", nil 366 | case C.COSIM_VARIABLE_CAUSALITY_OUTPUT: 367 | return "output", nil 368 | case C.COSIM_VARIABLE_CAUSALITY_PARAMETER: 369 | return "parameter", nil 370 | case C.COSIM_VARIABLE_CAUSALITY_CALCULATEDPARAMETER: 371 | return "calculatedParameter", nil 372 | case C.COSIM_VARIABLE_CAUSALITY_LOCAL: 373 | return "local", nil 374 | case C.COSIM_VARIABLE_CAUSALITY_INDEPENDENT: 375 | return "independent", nil 376 | } 377 | return "", errors.New("Unable to parse variable causality") 378 | } 379 | 380 | func parseVariability(variability C.cosim_variable_variability) (string, error) { 381 | switch variability { 382 | case C.COSIM_VARIABLE_VARIABILITY_CONSTANT: 383 | return "constant", nil 384 | case C.COSIM_VARIABLE_VARIABILITY_FIXED: 385 | return "fixed", nil 386 | case C.COSIM_VARIABLE_VARIABILITY_TUNABLE: 387 | return "tunable", nil 388 | case C.COSIM_VARIABLE_VARIABILITY_DISCRETE: 389 | return "discrete", nil 390 | case C.COSIM_VARIABLE_VARIABILITY_CONTINUOUS: 391 | return "continuous", nil 392 | } 393 | return "", errors.New("Unable to parse variable variability") 394 | } 395 | 396 | func parseType(valueType C.cosim_variable_type) (string, error) { 397 | switch valueType { 398 | case C.COSIM_VARIABLE_TYPE_REAL: 399 | return "Real", nil 400 | case C.COSIM_VARIABLE_TYPE_INTEGER: 401 | return "Integer", nil 402 | case C.COSIM_VARIABLE_TYPE_STRING: 403 | return "String", nil 404 | case C.COSIM_VARIABLE_TYPE_BOOLEAN: 405 | return "Boolean", nil 406 | } 407 | return "", errors.New("unable to parse variable type") 408 | } 409 | 410 | func addVariableMetadata(execution *C.cosim_execution, fmu *structs.FMU) error { 411 | nVariables := C.cosim_slave_get_num_variables(execution, C.cosim_slave_index(fmu.ExecutionIndex)) 412 | if int(nVariables) < 0 { 413 | return errors.New("invalid slave index to find variables for") 414 | } else if int(nVariables) == 0 { 415 | log.Println("No variables for ", fmu.Name) 416 | return nil 417 | } 418 | 419 | var variables = make([]C.cosim_variable_description, int(nVariables)) 420 | nVariablesRead := C.cosim_slave_get_variables(execution, C.cosim_slave_index(fmu.ExecutionIndex), &variables[0], C.size_t(nVariables)) 421 | if int(nVariablesRead) < 0 { 422 | return errors.New(strCat("Unable to get variables for slave with name ", fmu.Name)) 423 | } 424 | for _, variable := range variables[0:int(nVariablesRead)] { 425 | name := C.GoString(&variable.name[0]) 426 | ref := int(variable.reference) 427 | causality, err := parseCausality(variable.causality) 428 | if err != nil { 429 | return errors.New(strCat("Problem parsing causality for slave ", fmu.Name, ", variable ", name)) 430 | } 431 | variability, err := parseVariability(variable.variability) 432 | if err != nil { 433 | return errors.New(strCat("Problem parsing variability for slave ", fmu.Name, ", variable ", name)) 434 | } 435 | valueType, err := parseType(variable._type) 436 | if err != nil { 437 | return errors.New(strCat("Problem parsing type for slave ", fmu.Name, ", variable ", name)) 438 | } 439 | fmu.Variables = append(fmu.Variables, structs.Variable{ 440 | name, 441 | ref, 442 | causality, 443 | variability, 444 | valueType, 445 | }) 446 | } 447 | return nil 448 | } 449 | 450 | func fetchManipulatedVariables(execution *C.cosim_execution) []structs.ManipulatedVariable { 451 | nVars := int(C.cosim_get_num_modified_variables(execution)) 452 | if nVars <= 0 { 453 | return nil 454 | } 455 | 456 | var variables = make([]C.cosim_variable_id, nVars) 457 | numVars := int(C.cosim_get_modified_variables(execution, &variables[0], C.size_t(nVars))) 458 | 459 | if numVars < 0 { 460 | log.Println("Error while fetching modified variables: ", lastErrorMessage()) 461 | return nil 462 | } 463 | 464 | var varStructs = make([]structs.ManipulatedVariable, numVars) 465 | for n, variable := range variables[0:numVars] { 466 | slaveIndex := int(variable.slave_index) 467 | valueReference := int(variable.value_reference) 468 | variableType, err := parseType(variable._type) 469 | 470 | if err != nil { 471 | log.Println("Problem parsing variable type: ", variable._type) 472 | return nil 473 | } 474 | 475 | varStructs[n] = structs.ManipulatedVariable{ 476 | slaveIndex, 477 | variableType, 478 | valueReference, 479 | } 480 | } 481 | 482 | return varStructs 483 | } 484 | 485 | func simulationTeardown(sim *Simulation) (bool, string) { 486 | executionDestroy(sim.Execution) 487 | observerDestroy(sim.Observer) 488 | observerDestroy(sim.TrendObserver) 489 | if nil != sim.FileObserver { 490 | observerDestroy(sim.FileObserver) 491 | } 492 | manipulatorDestroy(sim.OverrideManipulator) 493 | manipulatorDestroy(sim.ScenarioManager) 494 | for _, slave := range sim.LocalSlaves { 495 | localSlaveDestroy(slave) 496 | } 497 | 498 | sim.Execution = nil 499 | sim.LocalSlaves = []*C.cosim_slave{} 500 | sim.Observer = nil 501 | sim.TrendObserver = nil 502 | sim.FileObserver = nil 503 | sim.OverrideManipulator = nil 504 | sim.ScenarioManager = nil 505 | sim.MetaData = &structs.MetaData{} 506 | return true, "Simulation teardown successful" 507 | } 508 | 509 | func validateConfigPath(configPath string) (bool, string) { 510 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 511 | return false, strCat(configPath, " does not exist") 512 | } 513 | return true, "" 514 | } 515 | 516 | func isDirectory(configPath string) bool { 517 | fi, _ := os.Stat(configPath) 518 | return fi.Mode().IsDir() 519 | } 520 | 521 | func validateConfigDir(fmuDir string) (bool, string) { 522 | files, err := ioutil.ReadDir(fmuDir) 523 | if err != nil { 524 | return false, err.Error() 525 | } 526 | var hasFMUs = false 527 | for _, f := range files { 528 | if strings.HasSuffix(f.Name(), ".fmu") { 529 | hasFMUs = true 530 | } 531 | } 532 | var hasSSD = false 533 | for _, f := range files { 534 | if f.Name() == "SystemStructure.ssd" { 535 | hasSSD = true 536 | } 537 | } 538 | var hasOspXml = false 539 | for _, f := range files { 540 | if f.Name() == "OspSystemStructure.xml" { 541 | hasOspXml = true 542 | } 543 | } 544 | if !hasFMUs && !hasSSD && !hasOspXml { 545 | return false, strCat(fmuDir, " does not contain any FMUs, OspSystemStructure.xml nor a SystemStructure.ssd file") 546 | } 547 | return true, "" 548 | } 549 | 550 | type configuration struct { 551 | configFile string 552 | configDir string 553 | isSsdConfig bool 554 | isOspConfig bool 555 | } 556 | 557 | func checkConfiguration(configPath string) (valid bool, message string, config configuration) { 558 | if valid, message := validateConfigPath(configPath); !valid { 559 | return false, message, config 560 | } 561 | 562 | if isDirectory(configPath) { 563 | if valid, message := validateConfigDir(configPath); !valid { 564 | return false, message, config 565 | } 566 | config.configDir = configPath 567 | if hasFile(configPath, "OspSystemStructure.xml") { 568 | config.configFile = filepath.Join(configPath, "OspSystemStructure.xml") 569 | config.isOspConfig = true 570 | } else if hasFile(configPath, "SystemStructure.ssd") { 571 | config.configFile = filepath.Join(configPath, "SystemStructure.ssd") 572 | config.isSsdConfig = true 573 | } 574 | } else { 575 | config.configFile = configPath 576 | config.configDir = filepath.Dir(configPath) 577 | if strings.HasSuffix(configPath, ".xml") { 578 | config.isOspConfig = true 579 | } else if strings.HasSuffix(configPath, ".ssd") { 580 | config.isSsdConfig = true 581 | } else { 582 | return false, "Given file path does not have a recognized format (xml, ssd): " + configPath, config 583 | } 584 | } 585 | 586 | return true, "", config 587 | } 588 | 589 | func initializeSimulation(sim *Simulation, status *structs.SimulationStatus, configPath string, logDir string) (bool, string, string) { 590 | valid, message, config := checkConfiguration(configPath) 591 | if !valid { 592 | return false, message, "" 593 | } 594 | 595 | var execution *C.cosim_execution 596 | if config.isOspConfig { 597 | execution = createConfigExecution(config.configFile) 598 | if execution == nil { 599 | return false, strCat("Could not create execution from OspSystemStructure.xml file: ", lastErrorMessage()), "" 600 | } 601 | } else if config.isSsdConfig { 602 | execution = createSsdExecution(config.configFile) 603 | if execution == nil { 604 | return false, strCat("Could not create execution from SystemStructure.ssd file: ", lastErrorMessage()), "" 605 | } 606 | } else { 607 | execution = createExecution() 608 | if execution == nil { 609 | return false, strCat("Could not create execution: ", lastErrorMessage()), "" 610 | } 611 | paths := getFmuPaths(config.configDir) 612 | for _, path := range paths { 613 | slave, err := addFmu(execution, path) 614 | if err != nil { 615 | return false, strCat("Could not add FMU to execution: ", err.Error()), "" 616 | } else { 617 | sim.LocalSlaves = append(sim.LocalSlaves, slave) 618 | } 619 | } 620 | } 621 | 622 | metaData := structs.MetaData{ 623 | FMUs: []structs.FMU{}, 624 | } 625 | err := addMetadata(execution, &metaData) 626 | if err != nil { 627 | return false, err.Error(), "" 628 | } 629 | 630 | observer := createObserver() 631 | executionAddObserver(execution, observer) 632 | 633 | trendObserver := createTrendObserver() 634 | executionAddObserver(execution, trendObserver) 635 | 636 | var fileObserver *C.cosim_observer 637 | if len(logDir) > 0 { 638 | if hasFile(config.configDir, "LogConfig.xml") { 639 | fileObserver = createFileObserverFromCfg(logDir, filepath.Join(config.configDir, "LogConfig.xml")) 640 | } else { 641 | fileObserver = createFileObserver(logDir) 642 | } 643 | executionAddObserver(execution, fileObserver) 644 | } 645 | 646 | manipulator := createOverrideManipulator() 647 | executionAddManipulator(execution, manipulator) 648 | 649 | scenarioManager := createScenarioManager() 650 | executionAddManipulator(execution, scenarioManager) 651 | 652 | sim.Execution = execution 653 | sim.Observer = observer 654 | sim.TrendObserver = trendObserver 655 | sim.FileObserver = fileObserver 656 | sim.OverrideManipulator = manipulator 657 | sim.ScenarioManager = scenarioManager 658 | sim.MetaData = &metaData 659 | 660 | setupPlotsFromConfig(sim, status, config.configDir) 661 | 662 | return true, "Simulation loaded successfully", config.configDir 663 | } 664 | 665 | func resetSimulation(sim *Simulation, status *structs.SimulationStatus, configPath string, logDir string) (bool, string, string) { 666 | var success = false 667 | var message = "" 668 | var configDir = "" 669 | 670 | success, message = executionStop(sim.Execution) 671 | log.Println(message) 672 | 673 | if success { 674 | status.Loaded = false 675 | status.Status = "stopped" 676 | status.ConfigDir = "" 677 | status.Trends = []structs.Trend{} 678 | status.Module = "" 679 | success, message = simulationTeardown(sim) 680 | log.Println(message) 681 | } 682 | 683 | success, message, configDir = initializeSimulation(sim, status, configPath, logDir) 684 | log.Println(message) 685 | 686 | return success, message, configDir 687 | } 688 | 689 | func executeCommand(cmd []string, sim *Simulation, status *structs.SimulationStatus) (shorty structs.ShortLivedData, feedback structs.CommandFeedback) { 690 | var success = false 691 | var message = "No feedback implemented for this command" 692 | switch cmd[0] { 693 | case "load": 694 | status.Loading = true 695 | var configDir string 696 | success, message, configDir = initializeSimulation(sim, status, cmd[1], cmd[2]) 697 | if success { 698 | status.Loaded = true 699 | status.ConfigDir = configDir 700 | status.Status = "pause" 701 | shorty.ModuleData = sim.MetaData 702 | scenarios := findScenarios(status) 703 | shorty.Scenarios = &scenarios 704 | } 705 | status.Loading = false 706 | case "teardown": 707 | status.Loaded = false 708 | status.Status = "stopped" 709 | status.ConfigDir = "" 710 | status.Trends = []structs.Trend{} 711 | status.Module = "" 712 | success, message = simulationTeardown(sim) 713 | shorty.ModuleData = sim.MetaData 714 | case "reset": 715 | status.Loading = true 716 | var configDir string 717 | success, message, configDir = resetSimulation(sim, status, cmd[1], cmd[2]) 718 | if success { 719 | status.Loaded = true 720 | status.ConfigDir = configDir 721 | status.Status = "pause" 722 | shorty.ModuleData = sim.MetaData 723 | scenarios := findScenarios(status) 724 | shorty.Scenarios = &scenarios 725 | } 726 | status.Loading = false 727 | case "pause": 728 | success, message = executionStop(sim.Execution) 729 | status.Status = "pause" 730 | case "play": 731 | success, message = executionStart(sim.Execution) 732 | status.Status = "play" 733 | case "enable-realtime": 734 | success, message = executionEnableRealTime(sim.Execution) 735 | case "disable-realtime": 736 | success, message = executionDisableRealTime(sim.Execution) 737 | case "set-custom-realtime-factor": 738 | success, message = executionSetCustomRealTimeFactor(sim.Execution, cmd[1]) 739 | case "set-steps-to-monitor": 740 | success, message = executionSetStepsToMonitor(sim.Execution, cmd[1]) 741 | case "newtrend": 742 | success, message = addNewTrend(status, cmd[1], cmd[2]) 743 | case "addtotrend": 744 | success, message = addToTrend(sim, status, cmd[1], cmd[2], cmd[3]) 745 | case "untrend": 746 | success, message = removeAllFromTrend(sim, status, cmd[1]) 747 | case "removetrend": 748 | success, message = removeTrend(status, cmd[1]) 749 | case "active-trend": 750 | success, message = activeTrend(status, cmd[1]) 751 | case "setlabel": 752 | success, message = setTrendLabel(status, cmd[1], cmd[2]) 753 | case "trend-zoom": 754 | idx, _ := strconv.Atoi(cmd[1]) 755 | status.Trends[idx].Spec = structs.TrendSpec{Auto: false, Begin: parseFloat(cmd[2]), End: parseFloat(cmd[3])} 756 | success = true 757 | message = strCat("Plotting values from ", cmd[2], " to ", cmd[3]) 758 | case "trend-zoom-reset": 759 | idx, _ := strconv.Atoi(cmd[1]) 760 | status.Trends[idx].Spec = structs.TrendSpec{Auto: true, Range: parseFloat(cmd[2])} 761 | success = true 762 | message = strCat("Plotting last ", cmd[2], " seconds") 763 | case "set-value": 764 | success, message = setVariableValue(sim, cmd[1], cmd[2], cmd[3], cmd[4]) 765 | case "reset-value": 766 | success, message = resetVariableValue(sim, cmd[1], cmd[2], cmd[3]) 767 | case "get-module-data": 768 | shorty.ModuleData = sim.MetaData 769 | scenarios := findScenarios(status) 770 | shorty.Scenarios = &scenarios 771 | success = true 772 | message = "Fetched metadata" 773 | case "signals": 774 | success, message = setSignalSubscriptions(status, cmd) 775 | case "load-scenario": 776 | success, message = loadScenario(sim, status, cmd[1]) 777 | case "abort-scenario": 778 | success, message = abortScenario(sim.ScenarioManager) 779 | case "parse-scenario": 780 | scenario, err := parseScenario(status, cmd[1]) 781 | if err != nil { 782 | success = false 783 | message = err.Error() 784 | } else { 785 | success = true 786 | message = "Successfully parsed scenario" 787 | shorty.Scenario = &scenario 788 | } 789 | default: 790 | message = "Unknown command, this is not good" 791 | fmt.Println(message, cmd) 792 | } 793 | return shorty, structs.CommandFeedback{Success: success, Message: message, Command: cmd[0]} 794 | } 795 | 796 | func CommandLoop(state chan structs.JsonResponse, sim *Simulation, command chan []string, status *structs.SimulationStatus) { 797 | for { 798 | select { 799 | case cmd := <-command: 800 | shorty, feedback := executeCommand(cmd, sim, status) 801 | state <- GenerateJsonResponse(status, sim, feedback, shorty) 802 | } 803 | } 804 | } 805 | 806 | func setSignalSubscriptions(status *structs.SimulationStatus, cmd []string) (bool, string) { 807 | var variables []structs.Variable 808 | var message = "Successfully set signal subscriptions" 809 | var success = true 810 | if len(cmd) > 1 { 811 | status.Module = cmd[1] 812 | for j := 2; j < (len(cmd) - 3); j += 4 { 813 | name := cmd[j] 814 | caus := cmd[j+1] 815 | typ := cmd[j+2] 816 | vr := cmd[j+3] 817 | valRef, err := strconv.Atoi(vr) 818 | if err != nil { 819 | message = strCat("Could not parse value reference from: ", vr, ", ", err.Error()) 820 | log.Println(message) 821 | success = false 822 | } else { 823 | variables = append(variables, 824 | structs.Variable{ 825 | Name: name, 826 | Causality: caus, 827 | Type: typ, 828 | ValueReference: valRef, 829 | }) 830 | } 831 | } 832 | } else { 833 | message = "Successfully reset signal subscriptions" 834 | } 835 | status.SignalSubscriptions = variables 836 | return success, message 837 | } 838 | 839 | func findFmu(metaData *structs.MetaData, moduleName string) (foundFmu structs.FMU, err error) { 840 | for _, fmu := range metaData.FMUs { 841 | if fmu.Name == moduleName { 842 | foundFmu = fmu 843 | return foundFmu, nil 844 | } 845 | } 846 | return foundFmu, errors.New("Simulator with name " + moduleName + " does not exist.") 847 | } 848 | 849 | func findVariable(fmu structs.FMU, variableName string) (foundVariable structs.Variable, err error) { 850 | for _, variable := range fmu.Variables { 851 | if variable.Name == variableName { 852 | foundVariable = variable 853 | return foundVariable, nil 854 | } 855 | } 856 | return foundVariable, errors.New("Variable with name " + variableName + " does not exist for simulator " + fmu.Name) 857 | } 858 | 859 | func findModuleData(status *structs.SimulationStatus, metaData *structs.MetaData, observer *C.cosim_observer) (module structs.Module) { 860 | if len(status.SignalSubscriptions) > 0 { 861 | 862 | slave, err := findFmu(metaData, status.Module) 863 | if err != nil { 864 | log.Println(err.Error()) 865 | return 866 | } 867 | slaveIndex := slave.ExecutionIndex 868 | realSignals := observerGetReals(observer, status.SignalSubscriptions, slaveIndex) 869 | intSignals := observerGetIntegers(observer, status.SignalSubscriptions, slaveIndex) 870 | boolSignals := observerGetBooleans(observer, status.SignalSubscriptions, slaveIndex) 871 | stringSignals := observerGetStrings(observer, status.SignalSubscriptions, slaveIndex) 872 | var signals []structs.Signal 873 | signals = append(signals, realSignals...) 874 | signals = append(signals, intSignals...) 875 | signals = append(signals, boolSignals...) 876 | signals = append(signals, stringSignals...) 877 | 878 | module.Signals = signals 879 | module.Name = status.Module 880 | } 881 | return 882 | } 883 | 884 | func GetSignalValue(module string, cardinality string, signal string) int { 885 | return 1 886 | } 887 | 888 | func GenerateJsonResponse(status *structs.SimulationStatus, sim *Simulation, feedback structs.CommandFeedback, shorty structs.ShortLivedData) structs.JsonResponse { 889 | var response = structs.JsonResponse{ 890 | Loading: status.Loading, 891 | Loaded: status.Loaded, 892 | Status: status.Status, 893 | LibVersion: status.LibVersion, 894 | } 895 | 896 | if status.Loaded { 897 | execStatus := getExecutionStatus(sim.Execution) 898 | response.ExecutionState = execStatus.state 899 | response.LastErrorCode = execStatus.lastErrorCode 900 | response.LastErrorMessage = execStatus.lastErrorMessage 901 | response.SimulationTime = execStatus.time 902 | response.TotalAverageRealTimeFactor = execStatus.totalAverageRealTimeFactor 903 | response.RollingAverageRealTimeFactor = execStatus.rollingAverageRealTimeFactor 904 | response.RealTimeFactorTarget = execStatus.realTimeFactorTarget 905 | response.IsRealTimeSimulation = execStatus.isRealTimeSimulation 906 | response.StepsToMonitor = execStatus.stepsToMonitor 907 | response.Module = findModuleData(status, sim.MetaData, sim.Observer) 908 | response.ConfigDir = status.ConfigDir 909 | generatePlotData(sim, status) 910 | response.Trends = status.Trends 911 | response.ManipulatedVariables = fetchManipulatedVariables(sim.Execution) 912 | if sim.ScenarioManager != nil && isScenarioRunning(sim.ScenarioManager) { 913 | response.RunningScenario = status.CurrentScenario 914 | } 915 | 916 | } 917 | if (structs.CommandFeedback{}) != feedback { 918 | response.Feedback = &feedback 919 | } 920 | if (structs.ShortLivedData{} != shorty) { 921 | if shorty.Scenarios != nil { 922 | response.Scenarios = shorty.Scenarios 923 | } 924 | if shorty.Scenario != nil { 925 | response.Scenario = shorty.Scenario 926 | } 927 | if shorty.ModuleData != nil { 928 | response.ModuleData = shorty.ModuleData 929 | } 930 | } 931 | return response 932 | } 933 | 934 | func StateUpdateLoop(state chan structs.JsonResponse, simulationStatus *structs.SimulationStatus, sim *Simulation) { 935 | for { 936 | state <- GenerateJsonResponse(simulationStatus, sim, structs.CommandFeedback{}, structs.ShortLivedData{}) 937 | time.Sleep(1000 * time.Millisecond) 938 | } 939 | } 940 | 941 | func addFmu(execution *C.cosim_execution, fmuPath string) (*C.cosim_slave, error) { 942 | baseName := filepath.Base(fmuPath) 943 | instanceName := strings.TrimSuffix(baseName, filepath.Ext(baseName)) 944 | fmt.Printf("Creating instance %s from %s\n", instanceName, fmuPath) 945 | localSlave := createLocalSlave(fmuPath, instanceName) 946 | if localSlave == nil { 947 | printLastError() 948 | return nil, errors.New(strCat("Unable to create slave from fmu: ", fmuPath)) 949 | } 950 | index := executionAddSlave(execution, localSlave) 951 | if index < 0 { 952 | return nil, errors.New(strCat("Unable to add slave to execution: ", fmuPath)) 953 | } 954 | return localSlave, nil 955 | } 956 | 957 | func addMetadata(execution *C.cosim_execution, metaData *structs.MetaData) error { 958 | nSlaves := C.cosim_execution_get_num_slaves(execution) 959 | var slaveInfos = make([]C.cosim_slave_info, int(nSlaves)) 960 | C.cosim_execution_get_slave_infos(execution, &slaveInfos[0], nSlaves) 961 | for _, info := range slaveInfos { 962 | name := C.GoString(&info.name[0]) 963 | index := int(info.index) 964 | fmu := structs.FMU{} 965 | fmu.Name = name 966 | fmu.ExecutionIndex = index 967 | err := addVariableMetadata(execution, &fmu) 968 | metaData.FMUs = append(metaData.FMUs, fmu) 969 | if err != nil { 970 | return err 971 | } 972 | } 973 | return nil 974 | } 975 | 976 | func getFmuPaths(loadFolder string) (paths []string) { 977 | info, e := os.Stat(loadFolder) 978 | if os.IsNotExist(e) { 979 | fmt.Println("Load folder does not exist!") 980 | return 981 | } else if !info.IsDir() { 982 | fmt.Println("Load folder is not a directory!") 983 | return 984 | } else { 985 | files, err := ioutil.ReadDir(loadFolder) 986 | if err != nil { 987 | log.Fatal(err) 988 | } 989 | for _, f := range files { 990 | if strings.HasSuffix(f.Name(), ".fmu") { 991 | paths = append(paths, filepath.Join(loadFolder, f.Name())) 992 | } 993 | } 994 | } 995 | return paths 996 | } 997 | 998 | func hasFile(folder string, fileName string) bool { 999 | info, e := os.Stat(folder) 1000 | if os.IsNotExist(e) { 1001 | fmt.Println("Folder does not exist: ", folder) 1002 | return false 1003 | } else if !info.IsDir() { 1004 | fmt.Println("Folder is not a directory: ", folder) 1005 | return false 1006 | } else { 1007 | files, err := ioutil.ReadDir(folder) 1008 | if err != nil { 1009 | log.Fatal(err) 1010 | } 1011 | for _, f := range files { 1012 | if f.Name() == fileName { 1013 | return true 1014 | } 1015 | } 1016 | } 1017 | fmt.Println("Folder does not contain file: ", fileName, folder) 1018 | return false 1019 | } 1020 | 1021 | type Simulation struct { 1022 | Execution *C.cosim_execution 1023 | Observer *C.cosim_observer 1024 | TrendObserver *C.cosim_observer 1025 | FileObserver *C.cosim_observer 1026 | OverrideManipulator *C.cosim_manipulator 1027 | ScenarioManager *C.cosim_manipulator 1028 | MetaData *structs.MetaData 1029 | LocalSlaves []*C.cosim_slave 1030 | } 1031 | 1032 | func CreateEmptySimulation() Simulation { 1033 | return Simulation{} 1034 | } 1035 | 1036 | func SetupLogging() { 1037 | success := C.cosim_log_setup_simple_console_logging() 1038 | if int(success) < 0 { 1039 | log.Println("Could not set up console logging!") 1040 | } else { 1041 | C.cosim_log_set_output_level(C.COSIM_LOG_SEVERITY_INFO) 1042 | log.Println("Console logging set up with severity: INFO") 1043 | } 1044 | } 1045 | 1046 | func Version() structs.Versions { 1047 | libVer := C.cosim_libcosim_version() 1048 | libcVer := C.cosim_libcosimc_version() 1049 | 1050 | return structs.Versions{ 1051 | LibVer: fmt.Sprintf("%d.%d.%d", int(libVer.major), int(libVer.minor), int(libVer.patch)), 1052 | LibcVer: fmt.Sprintf("%d.%d.%d", int(libcVer.major), int(libcVer.minor), int(libcVer.patch)), 1053 | } 1054 | } 1055 | -------------------------------------------------------------------------------- /libcosim/observers.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package libcosim 6 | 7 | /* 8 | #cgo CFLAGS: -I${SRCDIR}/../include 9 | #cgo LDFLAGS: -L${SRCDIR}/../dist/bin -L${SRCDIR}/../dist/lib -lcosimc -lstdc++ 10 | #include 11 | */ 12 | import "C" 13 | import ( 14 | "cosim-demo-app/structs" 15 | "errors" 16 | ) 17 | 18 | func createObserver() (observer *C.cosim_observer) { 19 | observer = C.cosim_last_value_observer_create() 20 | return 21 | } 22 | 23 | func createTrendObserver() (observer *C.cosim_observer) { 24 | observer = C.cosim_buffered_time_series_observer_create(100000) 25 | return 26 | } 27 | 28 | func createFileObserver(logPath string) (observer *C.cosim_observer) { 29 | observer = C.cosim_file_observer_create(C.CString(logPath)) 30 | return 31 | } 32 | 33 | func createFileObserverFromCfg(logPath string, cfgPath string) (observer *C.cosim_observer) { 34 | observer = C.cosim_file_observer_create_from_cfg(C.CString(logPath), C.CString(cfgPath)) 35 | return 36 | } 37 | 38 | func executionAddObserver(execution *C.cosim_execution, observer *C.cosim_observer) { 39 | C.cosim_execution_add_observer(execution, observer) 40 | } 41 | 42 | func observerDestroy(observer *C.cosim_observer) { 43 | C.cosim_observer_destroy(observer) 44 | } 45 | 46 | func observerGetReals(observer *C.cosim_observer, variables []structs.Variable, slaveIndex int) (realSignals []structs.Signal) { 47 | var realValueRefs []C.cosim_value_reference 48 | var realVariables []structs.Variable 49 | var numReals int 50 | for _, variable := range variables { 51 | if variable.Type == "Real" { 52 | ref := C.cosim_value_reference(variable.ValueReference) 53 | realValueRefs = append(realValueRefs, ref) 54 | realVariables = append(realVariables, variable) 55 | numReals++ 56 | } 57 | } 58 | 59 | if numReals > 0 { 60 | realOutVal := make([]C.double, numReals) 61 | C.cosim_observer_slave_get_real(observer, C.cosim_slave_index(slaveIndex), &realValueRefs[0], C.size_t(numReals), &realOutVal[0]) 62 | 63 | realSignals = make([]structs.Signal, numReals) 64 | for k := range realVariables { 65 | realSignals[k] = structs.Signal{ 66 | Name: realVariables[k].Name, 67 | Causality: realVariables[k].Causality, 68 | Type: realVariables[k].Type, 69 | Value: realOutVal[k], 70 | } 71 | } 72 | } 73 | return realSignals 74 | } 75 | 76 | func observerGetIntegers(observer *C.cosim_observer, variables []structs.Variable, slaveIndex int) (intSignals []structs.Signal) { 77 | var intValueRefs []C.cosim_value_reference 78 | var intVariables []structs.Variable 79 | var numIntegers int 80 | for _, variable := range variables { 81 | if variable.Type == "Integer" { 82 | ref := C.cosim_value_reference(variable.ValueReference) 83 | intValueRefs = append(intValueRefs, ref) 84 | intVariables = append(intVariables, variable) 85 | numIntegers++ 86 | } 87 | } 88 | 89 | if numIntegers > 0 { 90 | intOutVal := make([]C.int, numIntegers) 91 | C.cosim_observer_slave_get_integer(observer, C.cosim_slave_index(slaveIndex), &intValueRefs[0], C.size_t(numIntegers), &intOutVal[0]) 92 | 93 | intSignals = make([]structs.Signal, numIntegers) 94 | for k := range intVariables { 95 | intSignals[k] = structs.Signal{ 96 | Name: intVariables[k].Name, 97 | Causality: intVariables[k].Causality, 98 | Type: intVariables[k].Type, 99 | Value: int(intOutVal[k]), 100 | } 101 | } 102 | } 103 | return intSignals 104 | } 105 | 106 | func observerGetBooleans(observer *C.cosim_observer, variables []structs.Variable, slaveIndex int) (boolSignals []structs.Signal) { 107 | var boolValueRefs []C.cosim_value_reference 108 | var boolVariables []structs.Variable 109 | var numBooleans int 110 | for _, variable := range variables { 111 | if variable.Type == "Boolean" { 112 | ref := C.cosim_value_reference(variable.ValueReference) 113 | boolValueRefs = append(boolValueRefs, ref) 114 | boolVariables = append(boolVariables, variable) 115 | numBooleans++ 116 | } 117 | } 118 | 119 | if numBooleans > 0 { 120 | boolOutVal := make([]C.bool, numBooleans) 121 | C.cosim_observer_slave_get_boolean(observer, C.cosim_slave_index(slaveIndex), &boolValueRefs[0], C.size_t(numBooleans), &boolOutVal[0]) 122 | 123 | boolSignals = make([]structs.Signal, numBooleans) 124 | for k := range boolVariables { 125 | boolSignals[k] = structs.Signal{ 126 | Name: boolVariables[k].Name, 127 | Causality: boolVariables[k].Causality, 128 | Type: boolVariables[k].Type, 129 | Value: bool(boolOutVal[k]), 130 | } 131 | } 132 | } 133 | return boolSignals 134 | } 135 | 136 | func observerGetStrings(observer *C.cosim_observer, variables []structs.Variable, slaveIndex int) (stringSignals []structs.Signal) { 137 | var stringValueRefs []C.cosim_value_reference 138 | var stringVariables []structs.Variable 139 | var numStrings int 140 | for _, variable := range variables { 141 | if variable.Type == "String" { 142 | ref := C.cosim_value_reference(variable.ValueReference) 143 | stringValueRefs = append(stringValueRefs, ref) 144 | stringVariables = append(stringVariables, variable) 145 | numStrings++ 146 | } 147 | } 148 | 149 | if numStrings > 0 { 150 | stringOutVal := make([]*C.char, numStrings) 151 | C.cosim_observer_slave_get_string(observer, C.cosim_slave_index(slaveIndex), &stringValueRefs[0], C.size_t(numStrings), &stringOutVal[0]) 152 | 153 | stringSignals = make([]structs.Signal, numStrings) 154 | for k := range stringVariables { 155 | stringSignals[k] = structs.Signal{ 156 | Name: stringVariables[k].Name, 157 | Causality: stringVariables[k].Causality, 158 | Type: stringVariables[k].Type, 159 | Value: C.GoString(stringOutVal[k]), 160 | } 161 | } 162 | } 163 | return stringSignals 164 | } 165 | 166 | func observerGetRealSamples(observer *C.cosim_observer, signal *structs.TrendSignal, spec structs.TrendSpec) { 167 | slaveIndex := C.cosim_slave_index(signal.SlaveIndex) 168 | valueRef := C.cosim_value_reference(signal.ValueReference) 169 | 170 | stepNumbers := make([]C.cosim_step_number, 2) 171 | var success C.int 172 | if spec.Auto { 173 | duration := C.cosim_duration(spec.Range * 1e9) 174 | success = C.cosim_observer_get_step_numbers_for_duration(observer, slaveIndex, duration, &stepNumbers[0]) 175 | } else { 176 | tBegin := C.cosim_time_point(spec.Begin * 1e9) 177 | tEnd := C.cosim_time_point(spec.End * 1e9) 178 | success = C.cosim_observer_get_step_numbers(observer, slaveIndex, tBegin, tEnd, &stepNumbers[0]) 179 | } 180 | if int(success) < 0 { 181 | return 182 | } 183 | first := stepNumbers[0] 184 | last := stepNumbers[1] 185 | 186 | numSamples := int(last) - int(first) + 1 187 | cnSamples := C.size_t(numSamples) 188 | realOutVal := make([]C.double, numSamples) 189 | timeVal := make([]C.cosim_time_point, numSamples) 190 | timeStamps := make([]C.cosim_step_number, numSamples) 191 | actualNumSamples := C.cosim_observer_slave_get_real_samples(observer, slaveIndex, valueRef, first, cnSamples, &realOutVal[0], &timeStamps[0], &timeVal[0]) 192 | ns := int(actualNumSamples) 193 | if ns <= 0 { 194 | return 195 | } 196 | trendVals := make([]float64, ns) 197 | times := make([]float64, ns) 198 | for i := 0; i < ns; i++ { 199 | trendVals[i] = float64(realOutVal[i]) 200 | times[i] = 1e-9 * float64(timeVal[i]) 201 | } 202 | signal.TrendXValues = times 203 | signal.TrendYValues = trendVals 204 | } 205 | 206 | func observerGetRealSynchronizedSamples(observer *C.cosim_observer, signal1 *structs.TrendSignal, signal2 *structs.TrendSignal, spec structs.TrendSpec) { 207 | slaveIndex1 := C.cosim_slave_index(signal1.SlaveIndex) 208 | valueRef1 := C.cosim_value_reference(signal1.ValueReference) 209 | 210 | slaveIndex2 := C.cosim_slave_index(signal2.SlaveIndex) 211 | valueRef2 := C.cosim_value_reference(signal2.ValueReference) 212 | 213 | stepNumbers := make([]C.cosim_step_number, 2) 214 | var success C.int 215 | if spec.Auto { 216 | duration := C.cosim_duration(spec.Range * 1e9) 217 | success = C.cosim_observer_get_step_numbers_for_duration(observer, slaveIndex1, duration, &stepNumbers[0]) 218 | } else { 219 | tBegin := C.cosim_time_point(spec.Begin * 1e9) 220 | tEnd := C.cosim_time_point(spec.End * 1e9) 221 | success = C.cosim_observer_get_step_numbers(observer, slaveIndex1, tBegin, tEnd, &stepNumbers[0]) 222 | } 223 | if int(success) < 0 { 224 | return 225 | } 226 | first := stepNumbers[0] 227 | last := stepNumbers[1] 228 | 229 | numSamples := int(last) - int(first) + 1 230 | cnSamples := C.size_t(numSamples) 231 | realOutVal1 := make([]C.double, numSamples) 232 | realOutVal2 := make([]C.double, numSamples) 233 | actualNumSamples := C.cosim_observer_slave_get_real_synchronized_series(observer, slaveIndex1, valueRef1, slaveIndex2, valueRef2, first, cnSamples, &realOutVal1[0], &realOutVal2[0]) 234 | ns := int(actualNumSamples) 235 | if ns <= 0 { 236 | return 237 | } 238 | trendVals1 := make([]float64, ns) 239 | trendVals2 := make([]float64, ns) 240 | for i := 0; i < ns; i++ { 241 | trendVals1[i] = float64(realOutVal1[i]) 242 | trendVals2[i] = float64(realOutVal2[i]) 243 | } 244 | signal1.TrendXValues = trendVals1 245 | signal2.TrendYValues = trendVals2 246 | } 247 | 248 | func toVariableType(valueType string) (C.cosim_variable_type, error) { 249 | switch valueType { 250 | case "Real": 251 | return C.COSIM_VARIABLE_TYPE_REAL, nil 252 | case "Integer": 253 | return C.COSIM_VARIABLE_TYPE_INTEGER, nil 254 | case "Boolean": 255 | return C.COSIM_VARIABLE_TYPE_BOOLEAN, nil 256 | case "String": 257 | return C.COSIM_VARIABLE_TYPE_STRING, nil 258 | } 259 | return C.COSIM_VARIABLE_TYPE_REAL, errors.New(strCat("Unknown variable type:", valueType)) 260 | } 261 | 262 | func observerStartObserving(observer *C.cosim_observer, slaveIndex int, valueType string, valueRef int) error { 263 | variableType, err := toVariableType(valueType) 264 | if err != nil { 265 | return err 266 | } 267 | C.cosim_observer_start_observing(observer, C.cosim_slave_index(slaveIndex), variableType, C.cosim_value_reference(valueRef)) 268 | return nil 269 | } 270 | 271 | func observerStopObserving(observer *C.cosim_observer, slaveIndex int, valueType string, valueRef int) error { 272 | variableType, err := toVariableType(valueType) 273 | if err != nil { 274 | return err 275 | } 276 | C.cosim_observer_stop_observing(observer, C.cosim_slave_index(slaveIndex), variableType, C.cosim_value_reference(valueRef)) 277 | return nil 278 | } 279 | -------------------------------------------------------------------------------- /libcosim/scenario.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package libcosim 6 | 7 | /* 8 | #include 9 | */ 10 | import "C" 11 | import ( 12 | "cosim-demo-app/structs" 13 | "encoding/json" 14 | "fmt" 15 | "io/ioutil" 16 | "log" 17 | "os" 18 | "path/filepath" 19 | "strings" 20 | ) 21 | 22 | func createScenarioManager() (manipulator *C.cosim_manipulator) { 23 | manipulator = C.cosim_scenario_manager_create() 24 | return 25 | } 26 | 27 | func isScenarioRunning(manipulator *C.cosim_manipulator) bool { 28 | intVal := C.cosim_scenario_is_running(manipulator) 29 | return intVal > 0 30 | } 31 | 32 | func doesFileExist(path string) bool { 33 | if _, err := os.Stat(path); err == nil { 34 | return true 35 | 36 | } else if os.IsNotExist(err) { 37 | return false 38 | } else { 39 | log.Println("Well, this is awkward.") 40 | return false 41 | } 42 | } 43 | 44 | func loadScenario(sim *Simulation, status *structs.SimulationStatus, filename string) (bool, string) { 45 | pathToFile := filepath.Join(status.ConfigDir, "scenarios", filename) 46 | if !strings.HasSuffix(pathToFile, "json") { 47 | return false, "Scenario file must be of type *.json" 48 | } 49 | if !doesFileExist(pathToFile) { 50 | return false, strCat("Can't find file ", pathToFile) 51 | } 52 | success := C.cosim_execution_load_scenario(sim.Execution, sim.ScenarioManager, C.CString(pathToFile)) 53 | if success < 0 { 54 | return false, strCat("Problem loading scenario file: ", lastErrorMessage()) 55 | } 56 | status.CurrentScenario = filename 57 | return true, strCat("Successfully loaded scenario ", pathToFile) 58 | } 59 | 60 | func abortScenario(manipulator *C.cosim_manipulator) (bool, string) { 61 | intVal := C.cosim_scenario_abort(manipulator) 62 | if int(intVal) < 0 { 63 | return false, strCat("Failed to abort scenario: ", lastErrorMessage()) 64 | } else { 65 | return true, "Scenario aborted" 66 | } 67 | } 68 | 69 | func parseScenario(status *structs.SimulationStatus, filename string) (interface{}, error) { 70 | pathToFile := filepath.Join(status.ConfigDir, "scenarios", filename) 71 | jsonFile, err := os.Open(pathToFile) 72 | 73 | if err != nil { 74 | log.Println("Can't open file:", pathToFile) 75 | return "", err 76 | } 77 | 78 | defer jsonFile.Close() 79 | 80 | bytes, err := ioutil.ReadAll(jsonFile) 81 | if err != nil { 82 | return "", err 83 | } 84 | var data interface{} 85 | err = json.Unmarshal(bytes, &data) 86 | if err != nil { 87 | return "", err 88 | } 89 | return data, nil 90 | } 91 | 92 | func findScenarios(status *structs.SimulationStatus) (scenarios []string) { 93 | folder := filepath.Join(status.ConfigDir, "scenarios") 94 | info, e := os.Stat(folder) 95 | if os.IsNotExist(e) { 96 | fmt.Println("Scenario folder does not exist: ", folder) 97 | return 98 | } else if !info.IsDir() { 99 | fmt.Println("Scenario folder is not a directory: ", folder) 100 | return 101 | } else { 102 | files, err := ioutil.ReadDir(folder) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | for _, f := range files { 107 | if strings.HasSuffix(f.Name(), ".json") { 108 | scenarios = append(scenarios, f.Name()) 109 | } 110 | } 111 | } 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /libcosim/trending.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package libcosim 6 | 7 | import ( 8 | "cosim-demo-app/structs" 9 | "encoding/json" 10 | "io/ioutil" 11 | "log" 12 | "math/rand" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | ) 17 | 18 | func generateNextTrendId(status *structs.SimulationStatus) int { 19 | var maxId = 0 20 | for _, trend := range status.Trends { 21 | if trend.Id > maxId { 22 | maxId = trend.Id 23 | } 24 | } 25 | 26 | return maxId + 1 27 | } 28 | 29 | func addNewTrend(status *structs.SimulationStatus, plotType string, label string) (bool, string) { 30 | id := generateNextTrendId(status) 31 | 32 | status.Trends = append(status.Trends, structs.Trend{ 33 | Id: id, 34 | PlotType: plotType, 35 | Label: label, 36 | TrendSignals: []structs.TrendSignal{}, 37 | Spec: structs.TrendSpec{ 38 | Auto: true, 39 | Range: 10.0}}) 40 | return true, "Added new trend" 41 | } 42 | 43 | func addToTrend(sim *Simulation, status *structs.SimulationStatus, module string, signal string, plotIndex string) (bool, string) { 44 | 45 | idx, err := strconv.Atoi(plotIndex) 46 | if err != nil { 47 | message := strCat("Cannot parse plotIndex as integer", plotIndex, ", ", err.Error()) 48 | log.Println(message) 49 | return false, message 50 | } 51 | 52 | fmu, err := findFmu(sim.MetaData, module) 53 | if err != nil { 54 | message := err.Error() 55 | log.Println(message) 56 | return false, message 57 | } 58 | 59 | variable, err := findVariable(fmu, signal) 60 | if err != nil { 61 | message := err.Error() 62 | log.Println(message) 63 | return false, message 64 | } 65 | 66 | err = observerStartObserving(sim.TrendObserver, fmu.ExecutionIndex, variable.Type, variable.ValueReference) 67 | if err != nil { 68 | message := strCat("Cannot start observing variable ", lastErrorMessage()) 69 | log.Println(message) 70 | return false, message 71 | } 72 | 73 | status.Trends[idx].TrendSignals = append(status.Trends[idx].TrendSignals, structs.TrendSignal{ 74 | Module: module, 75 | SlaveIndex: fmu.ExecutionIndex, 76 | Signal: signal, 77 | Causality: variable.Causality, 78 | Type: variable.Type, 79 | ValueReference: variable.ValueReference}) 80 | 81 | return true, "Added variable to trend" 82 | } 83 | 84 | func setTrendLabel(status *structs.SimulationStatus, trendIndex string, trendLabel string) (bool, string) { 85 | idx, _ := strconv.Atoi(trendIndex) 86 | var uuid = rand.Intn(9999)*rand.Intn(9999) + rand.Intn(9999) 87 | status.Trends[idx].Label = strCat(trendLabel, " #", strconv.Itoa(uuid)) 88 | return true, "Modified trend label" 89 | } 90 | 91 | func removeAllFromTrend(sim *Simulation, status *structs.SimulationStatus, trendIndex string) (bool, string) { 92 | var success = true 93 | var message = "Removed all variables from trend" 94 | 95 | idx, _ := strconv.Atoi(trendIndex) 96 | for _, trendSignal := range status.Trends[idx].TrendSignals { 97 | err := observerStopObserving(sim.TrendObserver, trendSignal.SlaveIndex, trendSignal.Type, trendSignal.ValueReference) 98 | if err != nil { 99 | message = strCat("Cannot stop observing variable: ", lastErrorMessage()) 100 | success = false 101 | log.Println("Cannot stop observing", err) 102 | } 103 | } 104 | status.Trends[idx].TrendSignals = []structs.TrendSignal{} 105 | return success, message 106 | } 107 | 108 | func removeTrend(status *structs.SimulationStatus, trendIndex string) (bool, string) { 109 | idx, _ := strconv.Atoi(trendIndex) 110 | 111 | if len(status.Trends) > 1 { 112 | status.Trends = append(status.Trends[:idx], status.Trends[idx+1:]...) 113 | } else { 114 | status.Trends = []structs.Trend{} 115 | } 116 | 117 | return true, "Removed trend" 118 | } 119 | 120 | func activeTrend(status *structs.SimulationStatus, trendIndex string) (bool, string) { 121 | if len(trendIndex) > 0 { 122 | idx, err := strconv.Atoi(trendIndex) 123 | if err != nil { 124 | return false, strCat("Could not parse trend index: ", trendIndex, " ", err.Error()) 125 | } 126 | status.ActiveTrend = idx 127 | } else { 128 | status.ActiveTrend = -1 129 | } 130 | return true, "Changed active trend index" 131 | } 132 | 133 | func generatePlotData(sim *Simulation, status *structs.SimulationStatus) { 134 | for idx, trend := range status.Trends { 135 | if status.ActiveTrend != idx { 136 | for i, _ := range trend.TrendSignals { 137 | trend.TrendSignals[i].TrendXValues = nil 138 | trend.TrendSignals[i].TrendYValues = nil 139 | } 140 | continue 141 | } 142 | switch trend.PlotType { 143 | case "trend": 144 | if len(trend.TrendSignals) > 0 { 145 | for i, _ := range trend.TrendSignals { 146 | var signal = &trend.TrendSignals[i] 147 | switch signal.Type { 148 | case "Real": 149 | observerGetRealSamples(sim.TrendObserver, signal, trend.Spec) 150 | } 151 | } 152 | } 153 | break 154 | case "scatter": 155 | signalCount := len(trend.TrendSignals) 156 | if signalCount > 0 { 157 | for j := 0; (j + 1) < signalCount; j += 2 { 158 | var signal1 = &trend.TrendSignals[j] 159 | var signal2 = &trend.TrendSignals[j+1] 160 | if signal1.Type == "Real" && signal2.Type == "Real" { 161 | observerGetRealSynchronizedSamples(sim.TrendObserver, signal1, signal2, trend.Spec) 162 | } 163 | } 164 | } 165 | break 166 | } 167 | } 168 | } 169 | 170 | func parsePlotConfig(pathToFile string) (data structs.PlotConfig, err error) { 171 | jsonFile, err := os.Open(pathToFile) 172 | 173 | if err != nil { 174 | log.Println("Can't open file:", err.Error(), pathToFile) 175 | return data, err 176 | } 177 | 178 | defer jsonFile.Close() 179 | 180 | bytes, err := ioutil.ReadAll(jsonFile) 181 | if err != nil { 182 | log.Println("Can't read file:", err.Error(), pathToFile) 183 | return data, err 184 | } 185 | err = json.Unmarshal(bytes, &data) 186 | if err != nil { 187 | log.Println("Can't unmarshal PlotConfig json contents:", err.Error()) 188 | return data, err 189 | } 190 | return data, nil 191 | } 192 | 193 | func setupPlotsFromConfig(sim *Simulation, status *structs.SimulationStatus, configDir string) { 194 | if hasFile(configDir, "PlotConfig.json") { 195 | log.Println("We have a PlotConfig") 196 | pathToFile := filepath.Join(configDir, "PlotConfig.json") 197 | plotConfig, err := parsePlotConfig(pathToFile) 198 | 199 | if err != nil { 200 | log.Println("Can't parse PlotConfig.json:", err.Error()) 201 | return 202 | } 203 | 204 | for idx, plot := range plotConfig.Plots { 205 | success, message := addNewTrend(status, plot.PlotType, plot.Label) 206 | if !success { 207 | log.Println("Could not add new plot:", message) 208 | } else { 209 | plotIdx := strconv.Itoa(idx) 210 | for _, variable := range plot.PlotVariables { 211 | success, message := addToTrend(sim, status, variable.Simulator, variable.Variable, plotIdx) 212 | if !success { 213 | log.Println("Could not add variable to plot:", message) 214 | } 215 | } 216 | } 217 | } 218 | 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /libcosim/utils.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package libcosim 6 | 7 | import ( 8 | "log" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func parseFloat(argument string) float64 { 14 | f, err := strconv.ParseFloat(argument, 64) 15 | if err != nil { 16 | log.Fatal(err) 17 | return 0.0 18 | } 19 | return f 20 | } 21 | 22 | func strCat(strs ...string) string { 23 | var sb strings.Builder 24 | for _, str := range strs { 25 | sb.WriteString(str) 26 | } 27 | return sb.String() 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package main 6 | 7 | import ( 8 | "cosim-demo-app/libcosim" 9 | "cosim-demo-app/server" 10 | "cosim-demo-app/structs" 11 | ) 12 | 13 | func main() { 14 | libcosim.SetupLogging() 15 | sim := libcosim.CreateEmptySimulation() 16 | 17 | // Creating a command channel 18 | cmd := make(chan []string, 10) 19 | state := make(chan structs.JsonResponse, 10) 20 | 21 | simulationStatus := structs.SimulationStatus{ 22 | Loaded: false, 23 | Status: "stopped", 24 | Trends: []structs.Trend{}, 25 | LibVersion: libcosim.Version(), 26 | } 27 | 28 | // Passing the channel to the go routine 29 | go libcosim.StateUpdateLoop(state, &simulationStatus, &sim) 30 | go libcosim.CommandLoop(state, &sim, cmd, &simulationStatus) 31 | 32 | //Passing the channel to the server 33 | server.Server(cmd, state, &simulationStatus, &sim) 34 | close(cmd) 35 | } 36 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject client "1.0-SNAPSHOT" 2 | :min-lein-version "2.0.0" 3 | :dependencies [[kee-frame "0.3.3"] 4 | [jarohen/chord "0.8.1"] 5 | [com.cognitect/transit-clj "0.8.309"] 6 | [com.cognitect/transit-cljs "0.8.256"] 7 | [reagent "0.8.1"] 8 | [re-frame "0.10.6" :exclusions [reagent]] 9 | [org.clojure/clojurescript "1.10.439"] 10 | [org.clojure/clojure "1.9.0"] 11 | [org.clojure/tools.reader "1.3.0"] 12 | [fulcrologic/fulcro "2.8.0"] 13 | [fulcrologic/semantic-ui-react-wrappers "2.0.4"] 14 | [cljsjs/semantic-ui-react "0.84.0-0"] 15 | [binaryage/oops "0.7.0"]] 16 | :plugins [[lein-figwheel "0.5.18"] 17 | [figwheel-sidecar "0.5.17"] 18 | [lein-cljsbuild "1.1.7"] 19 | [lein-count "1.0.9"] 20 | [cider/cider-nrepl "0.21.1"]] 21 | 22 | :clean-targets ^{:protect false} [:target-path :compile-path "resources/public/static/js/compiled"] 23 | 24 | :cljsbuild {:builds [{:id "app" 25 | :source-paths ["src"] 26 | :figwheel true 27 | :compiler {:main client.core 28 | :asset-path "/static/js/compiled/out" 29 | :output-to "resources/public/static/js/compiled/app.js" 30 | :output-dir "resources/public/static/js/compiled/out" 31 | :source-map-timestamp true 32 | :parallel-build true 33 | :closure-defines {client.core/debug true 34 | client.view/default-load-dir "" 35 | client.view/default-log-dir "" 36 | "re_frame.trace.trace_enabled_QMARK_" true} 37 | :preloads [devtools.preload day8.re-frame-10x.preload] 38 | :external-config {:devtools/config {:features-to-install [:formatters]}} 39 | :foreign-libs [{:file "resources/public/static/js/plotly.min.js" 40 | :provides ["cljsjs.plotly"]}]}} 41 | {:id "min" 42 | :source-paths ["src"] 43 | :compiler {:output-to "resources/public/static/js/compiled/app.js" 44 | :optimizations :simple 45 | :parallel-build true 46 | :foreign-libs [{:file "resources/public/static/js/plotly.min.js" 47 | :provides ["cljsjs.plotly"]}]}}]} 48 | 49 | :figwheel {:css-dirs ["resources/public/static/css"]} 50 | 51 | :profiles {:dev [:project/dev :profiles/dev] 52 | :profiles/dev {} 53 | :project/dev {:dependencies [[binaryage/devtools "0.9.10"] 54 | [day8.re-frame/re-frame-10x "0.3.3-react16"] 55 | [cider/piggieback "0.4.0"] 56 | [figwheel-sidecar "0.5.18"]] 57 | :repl-options {:nrepl-middelware [cider.piggieback/wrap-cljs-repl]}}}) 58 | -------------------------------------------------------------------------------- /resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-simulation-platform/cosim-demo-app/c341f86c0a6c3a45fff33dea86eede80889e60b7/resources/public/favicon.ico -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Cosim Demo App loading...

5 |
6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/public/static/css/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-simulation-platform/cosim-demo-app/c341f86c0a6c3a45fff33dea86eede80889e60b7/resources/public/static/css/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /resources/public/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: relative; 3 | } 4 | 5 | #sidebar { 6 | position: fixed; 7 | top: 51.8px; 8 | left: 0; 9 | bottom: 0; 10 | width: 18%; 11 | background-color: #f5f5f5; 12 | padding: 0; 13 | overflow-y: auto; 14 | } 15 | #sidebar .ui.menu { 16 | margin: 2em 0 0; 17 | font-size: 16px; 18 | } 19 | #sidebar .ui.menu > a.item { 20 | color: #337ab7; 21 | border-radius: 0 !important; 22 | } 23 | #sidebar .ui.menu > a.item.active { 24 | background-color: #337ab7; 25 | color: white; 26 | border: none !important; 27 | } 28 | #sidebar .ui.menu > a.item:hover { 29 | background-color: #4f93ce; 30 | color: white; 31 | } 32 | 33 | #content { 34 | margin-left: 19%; 35 | width: 81%; 36 | margin-top: 3em; 37 | padding-left: 3em; 38 | float: left; 39 | } 40 | #content > .ui.grid { 41 | padding-right: 4em; 42 | padding-bottom: 3em; 43 | } 44 | #content h1 { 45 | font-size: 36px; 46 | } 47 | #content .ui.divider:not(.hidden) { 48 | margin: 0; 49 | } 50 | #content table.ui.table { 51 | border: none; 52 | } 53 | #content table.ui.table thead th { 54 | border-bottom: 2px solid #eee !important; 55 | } 56 | #content span.additional { 57 | color: #777; 58 | margin-left: 15px; 59 | font-size: 20px; 60 | } 61 | #content .plotname-edit { 62 | cursor: pointer; 63 | } 64 | #content .plotname-edit:hover { 65 | color: #aaa; 66 | } 67 | #sidebar a.itemstyle { 68 | color: rgb(0,0,0,0.5); 69 | } 70 | #sidebar a.itemstyle:hover { 71 | color: #333; 72 | } 73 | #sidebar a.itemstyle.active { 74 | color: #000; 75 | font-weight: bold; 76 | } 77 | -------------------------------------------------------------------------------- /resources/public/static/css/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-simulation-platform/cosim-demo-app/c341f86c0a6c3a45fff33dea86eede80889e60b7/resources/public/static/css/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /resources/public/static/css/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-simulation-platform/cosim-demo-app/c341f86c0a6c3a45fff33dea86eede80889e60b7/resources/public/static/css/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /resources/public/static/css/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-simulation-platform/cosim-demo-app/c341f86c0a6c3a45fff33dea86eede80889e60b7/resources/public/static/css/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /resources/public/static/css/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-simulation-platform/cosim-demo-app/c341f86c0a6c3a45fff33dea86eede80889e60b7/resources/public/static/css/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /run-linux: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | xdg-open http://localhost:8000 3 | ./bin/cosim-demo-app 4 | -------------------------------------------------------------------------------- /run-windows.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | start .\bin\cosim-demo-app.exe 3 | timeout 2 > nul 4 | start http://localhost:8000 5 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package server 6 | 7 | import ( 8 | "cosim-demo-app/libcosim" 9 | "cosim-demo-app/structs" 10 | "encoding/json" 11 | "github.com/gobuffalo/packr" 12 | "github.com/gorilla/mux" 13 | "io/ioutil" 14 | "log" 15 | "net/http" 16 | ) 17 | 18 | func Server(command chan []string, state chan structs.JsonResponse, simulationStatus *structs.SimulationStatus, sim *libcosim.Simulation) { 19 | router := mux.NewRouter() 20 | box := packr.NewBox("../resources/public") 21 | 22 | router.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { 23 | w.Header().Set("Content-Type", "application/json") 24 | json.NewEncoder(w).Encode(libcosim.GenerateJsonResponse(simulationStatus, sim, structs.CommandFeedback{}, structs.ShortLivedData{})) 25 | }) 26 | 27 | router.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { 28 | w.Header().Set("Content-Type", "application/json") 29 | json.NewEncoder(w).Encode(libcosim.Version()) 30 | }) 31 | 32 | router.HandleFunc("/modules", func(w http.ResponseWriter, r *http.Request) { 33 | w.Header().Set("Content-Type", "application/json") 34 | json.NewEncoder(w).Encode(sim.MetaData) 35 | }) 36 | 37 | router.HandleFunc("/plot-config", func(w http.ResponseWriter, r *http.Request) { 38 | w.Header().Set("Access-Control-Allow-Origin", "*") 39 | w.Header().Set("Access-Control-Allow-Methods", "POST") 40 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 41 | }).Methods("OPTIONS") 42 | 43 | router.HandleFunc("/plot-config", func(w http.ResponseWriter, r *http.Request) { 44 | w.Header().Set("Access-Control-Allow-Origin", "*") 45 | configDir := simulationStatus.ConfigDir 46 | trends := simulationStatus.Trends 47 | plots := []structs.Plot{} 48 | for i := 0; i < len(trends); i++ { 49 | trendValues := trends[i].TrendSignals 50 | variables := []structs.PlotVariable{} 51 | for j := 0; j < len(trendValues); j++ { 52 | plotVariable := structs.PlotVariable{trendValues[j].Module, trendValues[j].Signal} 53 | variables = append(variables, plotVariable) 54 | } 55 | plot := structs.Plot{trends[i].Label, trends[i].PlotType, variables} 56 | plots = append(plots, plot) 57 | } 58 | plotConfig := structs.PlotConfig{plots} 59 | plotConfigJson, _ := json.Marshal(plotConfig) 60 | err := ioutil.WriteFile(configDir+"/"+"PlotConfig.json", plotConfigJson, 0644) 61 | if err != nil { 62 | log.Println("Could not write PlotConfig to file, data: ", plotConfig, ", error was:", err) 63 | } 64 | msg := "Wrote plot configuration to " + configDir + "/" + "PlotConfig.json" 65 | log.Println(msg) 66 | json.NewEncoder(w).Encode(msg) 67 | }).Methods("POST") 68 | 69 | router.HandleFunc("/value/{module}/{cardinality}/{signal}", func(w http.ResponseWriter, r *http.Request) { 70 | w.Header().Set("Content-Type", "application/json") 71 | vars := mux.Vars(r) 72 | module := vars["module"] 73 | cardinality := vars["cardinality"] 74 | signal := vars["signal"] 75 | json.NewEncoder(w).Encode(libcosim.GetSignalValue(module, cardinality, signal)) 76 | }) 77 | 78 | router.HandleFunc("/command", func(w http.ResponseWriter, r *http.Request) { 79 | body, _ := ioutil.ReadAll(r.Body) 80 | commandRequest := []string{} 81 | json.Unmarshal(body, &commandRequest) 82 | command <- commandRequest 83 | }).Methods("PUT") 84 | 85 | router.HandleFunc("/ws", WebsocketHandler(command, state)) 86 | 87 | //Default handler 88 | router.PathPrefix("/").Handler(http.FileServer(box)) 89 | 90 | log.Fatal(http.ListenAndServe(":8000", router)) 91 | } 92 | -------------------------------------------------------------------------------- /server/websockets.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package server 6 | 7 | import ( 8 | "cosim-demo-app/structs" 9 | "github.com/gorilla/websocket" 10 | "github.com/ugorji/go/codec" 11 | "io" 12 | "log" 13 | "net/http" 14 | "reflect" 15 | ) 16 | 17 | type JsonRequest struct { 18 | Command []string `json:"command,omitempty"` 19 | } 20 | 21 | var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} 22 | 23 | func commandLoop(command chan []string, conn *websocket.Conn) { 24 | var ( 25 | mh codec.MsgpackHandle 26 | ) 27 | mh.MapType = reflect.TypeOf(map[string]interface{}(nil)) 28 | decoder := codec.NewDecoder(nil, &mh) 29 | 30 | for { 31 | data := JsonRequest{} 32 | _, r, err := conn.NextReader() 33 | if err != nil { 34 | log.Println("read error:", err) 35 | break 36 | } 37 | 38 | decoder.Reset(r) 39 | err = decoder.Decode(&data) 40 | 41 | if err == io.EOF { 42 | // One value is expected in the message. 43 | log.Println("Message was EOF:", err, data) 44 | err = io.ErrUnexpectedEOF 45 | } else if err != nil { 46 | log.Println("Could not parse message:", data, ", error was:", err) 47 | } else if data.Command != nil { 48 | command <- data.Command 49 | } 50 | } 51 | } 52 | 53 | func stateLoop(state chan structs.JsonResponse, conn *websocket.Conn) { 54 | var ( 55 | mh codec.MsgpackHandle 56 | ) 57 | mh.MapType = reflect.TypeOf(map[string]interface{}(nil)) 58 | encoder := codec.NewEncoder(nil, &mh) 59 | for { 60 | latestState := <-state 61 | 62 | w, err := conn.NextWriter(2) 63 | encoder.Reset(w) 64 | err = encoder.Encode(latestState) 65 | if err != nil { 66 | log.Println("write error:", err) 67 | log.Println("latestState:", latestState) 68 | state <- latestState 69 | break 70 | } 71 | } 72 | } 73 | 74 | func WebsocketHandler(command chan []string, state chan structs.JsonResponse) func(w http.ResponseWriter, r *http.Request) { 75 | return func(w http.ResponseWriter, r *http.Request) { 76 | conn, err := upgrader.Upgrade(w, r, nil) 77 | if err != nil { 78 | log.Print("upgrade:", err) 79 | return 80 | } 81 | //defer conn.Close() 82 | go commandLoop(command, conn) 83 | go stateLoop(state, conn) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /set-rpath: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | for file in $(pwd)/$1/* 3 | do 4 | patchelf --set-rpath '$ORIGIN' $file 5 | done 6 | -------------------------------------------------------------------------------- /src/client/components.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.components 6 | (:require [client.controller :as controller] 7 | [re-frame.core :as rf] 8 | [reagent.core :as r])) 9 | 10 | (defn variable-override-editor [index {:keys [name type value-reference]} value] 11 | (let [editing? (r/atom false) 12 | edited? (r/atom false) 13 | internal-value (r/atom value) 14 | save (fn [] 15 | (rf/dispatch [::controller/set-value (str index) type (str value-reference) @internal-value]) 16 | (reset! editing? false) 17 | (reset! edited? true)) 18 | save-if-changed (fn [value] 19 | (if (not= value @internal-value) 20 | (save) 21 | (reset! editing? false))) 22 | has-manipulator? (rf/subscribe [:has-manipulator? index type value-reference])] 23 | (fn [_ _ value] 24 | (if @editing? 25 | [:div.ui.action.input.fluid 26 | (if (= "Boolean" type) 27 | [:select.ui.dropdown 28 | {:value @internal-value 29 | :on-change #(reset! internal-value (.. % -target -value))} 30 | [:option {:value "false"} "false"] 31 | [:option {:value "true"} "true"]] 32 | [:input {:type :text 33 | :auto-focus true 34 | :id (str "input-" name) 35 | :value @internal-value 36 | :on-change #(reset! internal-value (.. % -target -value)) 37 | :on-key-press #(when (= (.-key %) "Enter") (save-if-changed value)) 38 | ;:on-blur #(save-if-changed value) 39 | }]) 40 | [:button.ui.right.icon.button 41 | {:on-click save} 42 | [:i.check.link.icon]] 43 | [:button.ui.right.icon.button 44 | {:on-click #(reset! editing? false)} 45 | [:i.times.link.icon]]] 46 | [:div 47 | (when @has-manipulator? 48 | [:span.plotname-edit 49 | {:on-click (fn [] 50 | (reset! edited? false) 51 | (rf/dispatch [::controller/reset-value (str index) type (str value-reference)])) 52 | :data-tooltip "Remove override"} 53 | [:i.eraser.icon]]) 54 | [:span.plotname-edit 55 | {:on-click (fn [] 56 | (reset! editing? true) 57 | (reset! internal-value value)) 58 | :data-tooltip "Override value"} 59 | (if @has-manipulator? 60 | [:i.edit.red.link.icon] 61 | [:i.edit.link.icon])] 62 | (if (and @edited? (= "pause" @(rf/subscribe [:status]))) 63 | @internal-value 64 | (str value))])))) 65 | 66 | (defn text-editor [value event tooltip] 67 | (let [editing? (r/atom false) 68 | internal-value (r/atom value) 69 | save (fn [] 70 | (rf/dispatch (conj event @internal-value)) 71 | (reset! editing? false)) 72 | save-if-changed (fn [value] 73 | (if (not= value @internal-value) 74 | (save) 75 | (reset! editing? false)))] 76 | (fn [value] 77 | (if @editing? 78 | [:div.ui.action.input.fluid 79 | [:input {:type :text 80 | :auto-focus true 81 | :id (str "input-" name) 82 | :value @internal-value 83 | :on-change #(reset! internal-value (.. % -target -value)) 84 | :on-key-press #(when (= (.-key %) "Enter") (save-if-changed value)) 85 | :on-blur #(save-if-changed value)}] 86 | [:button.ui.right.icon.button 87 | {:on-click save} 88 | [:i.check.link.icon]] 89 | [:button.ui.right.icon.button 90 | {:on-click #(reset! editing? false)} 91 | [:i.times.link.icon]]] 92 | [:div 93 | [:span.plotname-edit 94 | {:on-click (fn [] 95 | (reset! editing? true) 96 | (reset! internal-value value)) 97 | :data-tooltip tooltip} 98 | [:i.edit.link.icon]] 99 | value])))) 100 | -------------------------------------------------------------------------------- /src/client/config.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.config) 6 | 7 | (def socket-url "ws://localhost:8000/ws") 8 | -------------------------------------------------------------------------------- /src/client/controller.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.controller 6 | (:require-macros [cljs.core.async.macros :refer [go go-loop]]) 7 | (:require [kee-frame.core :as k] 8 | [kee-frame.websocket :as websocket] 9 | [client.config :refer [socket-url]] 10 | [ajax.core :as ajax] 11 | [re-frame.loggers :as re-frame-log] 12 | [cljs.spec.alpha :as s] 13 | [cljs.core.async :refer [ route :data :name (= :module)) 28 | (-> route :path-params))) 29 | :start [::module-enter] 30 | :stop [::module-leave]}) 31 | 32 | (k/reg-controller :trend 33 | {:params (fn [route] 34 | (when (-> route :data :name (= :trend)) 35 | (-> route :path-params))) 36 | :start [::trend-enter] 37 | :stop [::trend-leave]}) 38 | 39 | (k/reg-controller :scenario 40 | {:params (fn [route] 41 | (when (-> route :data :name (= :scenario)) 42 | (-> route :path-params))) 43 | :start [::scenario-enter] 44 | :stop [::scenario-leave]}) 45 | 46 | (k/reg-controller :websocket-controller 47 | {:params (constantly true) 48 | :start [:start-websockets]}) 49 | 50 | (defn socket-command [cmd] 51 | {:dispatch [::websocket/send socket-url {:command cmd}]}) 52 | 53 | (k/reg-event-fx :start-websockets 54 | (fn [_ _] 55 | (merge {::websocket/open {:path socket-url 56 | :dispatch ::socket-message-received 57 | :format :msgpack 58 | :wrap-message identity}} 59 | (socket-command ["get-module-data"])))) 60 | 61 | (s/def ::fmu (s/keys :req-un [::name ::index ::variables])) 62 | (s/def ::fmus (s/coll-of ::fmu)) 63 | (s/def ::module-data (s/keys :req-un [::fmus])) 64 | 65 | (k/reg-event-fx ::feedback-message 66 | (fn [{:keys [db]} [message]] 67 | (if-not (:success message) 68 | {:db (assoc db :feedback-message message)} 69 | (when (:show-success-feedback-messages db) 70 | (go 71 | (> db 98 | :state 99 | :module-data 100 | :fmus 101 | (filter #(= module (:name %))) 102 | first)) 103 | 104 | (defn variable-groups [module-meta causality vars-per-page] 105 | (->> module-meta 106 | :variables 107 | (filter #(= causality (:causality %))) 108 | (sort-by :name) 109 | (partition-all vars-per-page))) 110 | 111 | (defn filter-signals [groups page] 112 | (some-> groups 113 | seq 114 | (nth (dec page)))) 115 | 116 | (defn editable? [{:keys [type causality] :as variable}] 117 | (if (and (#{"input" "parameter" "calculatedParameter" "output"} causality) 118 | (#{"Real" "Integer" "Boolean" "String"} type)) 119 | (assoc variable :editable? true) 120 | variable)) 121 | 122 | (defn encode-variables [variables] 123 | (->> variables 124 | (map (fn [{:keys [name causality type value-reference]}] 125 | [name causality type (str value-reference)])) 126 | (flatten))) 127 | 128 | (k/reg-event-fx ::fetch-signals 129 | (fn [{:keys [db]} _] 130 | (let [{:keys [current-module active-causality page current-module-meta vars-per-page]} db 131 | groups (variable-groups current-module-meta active-causality vars-per-page) 132 | viewing (filter-signals groups page)] 133 | (merge {:db (assoc db 134 | :viewing (map editable? viewing) 135 | :page-count (count groups))} 136 | (socket-command (concat ["signals" current-module] (encode-variables viewing))))))) 137 | 138 | (k/reg-event-fx ::module-enter 139 | (fn [{:keys [db]} [{:keys [module causality]}]] 140 | (merge {:db (assoc db 141 | :current-module module 142 | :current-module-meta (module-meta db module) 143 | :active-causality causality 144 | :page 1) 145 | :dispatch [::fetch-signals]}))) 146 | 147 | (k/reg-event-fx ::module-leave 148 | (fn [{:keys [db]} _] 149 | (when (not= (:current-module db) 150 | (get-in db [:kee-frame/route :path-params :module])) 151 | (merge {:db (dissoc db :current-module :current-module-meta)} 152 | (socket-command ["signals"]))))) 153 | 154 | (k/reg-event-fx ::trend-enter 155 | (fn [{:keys [db]} [{:keys [index]}]] 156 | (merge {:db (assoc db :active-trend-index index)} 157 | (socket-command ["active-trend" (str index)])))) 158 | 159 | (k/reg-event-fx ::trend-leave 160 | (fn [{:keys [db]} _] 161 | (merge {:db (dissoc db :active-trend-index)} 162 | (socket-command ["active-trend" nil])))) 163 | 164 | (k/reg-event-db ::toggle-show-success-feedback-messages 165 | (fn [db _] 166 | (let [new-setting (not (:show-success-feedback-messages db))] 167 | (storage/set-item! "show-success-feedback-message" new-setting) 168 | (assoc db :show-success-feedback-messages new-setting)))) 169 | 170 | (k/reg-event-fx ::scenario-enter 171 | (fn [{:keys [db]} [{:keys [id]}]] 172 | (merge {:db (assoc db :scenario-id id)} 173 | (socket-command ["parse-scenario" id])))) 174 | 175 | (k/reg-event-db ::scenario-leave 176 | (fn [db _] 177 | (dissoc db :scenario-id))) 178 | 179 | (k/reg-event-fx ::load 180 | (fn [{:keys [db]} [folder log-folder]] 181 | (let [paths (distinct (conj (:prev-paths db) folder))] 182 | (storage/set-item! "cosim-paths" (pr-str paths)) 183 | (merge {:db (assoc db :prev-paths paths :plot-config-changed? false :log-dir (or log-folder ""))} 184 | (socket-command ["load" folder (or log-folder "")]))))) 185 | 186 | (k/reg-event-fx ::reset 187 | (fn [{:keys [db]} [folder log-folder]] 188 | (merge {:navigate-to [:index]} 189 | (socket-command ["reset" folder (or log-folder "")])))) 190 | 191 | (k/reg-event-fx ::delete-prev 192 | (fn [{:keys [db]} [path]] 193 | (let [paths (remove #(= path %) (:prev-paths db))] 194 | (storage/set-item! "cosim-paths" (pr-str paths)) 195 | {:db (assoc db :prev-paths paths)}))) 196 | 197 | (k/reg-event-fx ::teardown 198 | (fn [_ _] 199 | (socket-command ["teardown"]))) 200 | 201 | (k/reg-event-fx ::play 202 | (fn [_ _] 203 | (socket-command ["play"]))) 204 | 205 | (k/reg-event-fx ::pause 206 | (fn [_ _] 207 | (socket-command ["pause"]))) 208 | 209 | (k/reg-event-fx ::enable-realtime 210 | (fn [_] 211 | (socket-command ["enable-realtime"]))) 212 | 213 | (k/reg-event-fx ::set-real-time-factor-target 214 | (fn [_ [val]] 215 | (socket-command ["set-custom-realtime-factor" val]))) 216 | 217 | (k/reg-event-fx ::set-steps-to-monitor 218 | (fn [_ [val]] 219 | (socket-command ["set-steps-to-monitor" val]))) 220 | 221 | (k/reg-event-fx ::disable-realtime 222 | (fn [_ _] 223 | (socket-command ["disable-realtime"]))) 224 | 225 | (k/reg-event-fx ::untrend 226 | (fn [{:keys [db]} [id]] 227 | (merge (socket-command ["untrend" (str id)]) 228 | {:db (assoc db :plot-config-changed? true)}))) 229 | 230 | (k/reg-event-fx ::untrend-single 231 | (fn [_ [trend-id variable-id]] 232 | (print (str "Untrend " variable-id " from trend " trend-id)))) 233 | 234 | (k/reg-event-fx ::removetrend 235 | (fn [{:keys [db]} [id]] 236 | (let [route-name (:name (:data (:kee-frame/route db))) 237 | route-param-index (int (:index (:path-params (:kee-frame/route db)))) 238 | current-path-to-be-deleted (and (= :trend route-name) (= route-param-index id)) 239 | smaller-index-to-be-deleted (and (= :trend route-name) (> route-param-index id))] 240 | (merge (when (or current-path-to-be-deleted smaller-index-to-be-deleted) {:navigate-to [:index]}) 241 | (socket-command ["removetrend" (str id)]) 242 | {:db (assoc db :plot-config-changed? true)})))) 243 | 244 | (k/reg-event-fx ::new-trend 245 | (fn [{:keys [db]} [type label]] 246 | (merge (socket-command ["newtrend" type label]) 247 | {:db (assoc db :plot-config-changed? true)}))) 248 | 249 | (k/reg-event-fx ::add-to-trend 250 | (fn [{:keys [db]} [module signal plot-index]] 251 | (merge (socket-command ["addtotrend" module signal (str plot-index)]) 252 | {:db (assoc db :plot-config-changed? true)}))) 253 | 254 | (k/reg-event-fx ::set-label 255 | (fn [{:keys [db]} [label]] 256 | (merge (socket-command ["setlabel" (:active-trend-index db) label]) 257 | {:db (assoc db :plot-config-changed? true)}))) 258 | 259 | (k/reg-event-fx ::set-value 260 | (fn [_ [index type value-reference value]] 261 | (socket-command ["set-value" index type value-reference (str value)]))) 262 | 263 | (k/reg-event-fx ::reset-value 264 | (fn [_ [index type value-reference]] 265 | (socket-command ["reset-value" index type value-reference]))) 266 | 267 | (k/reg-event-fx ::trend-zoom 268 | (fn [{:keys [db]} [begin end]] 269 | (socket-command ["trend-zoom" (:active-trend-index db) (str begin) (str end)]))) 270 | 271 | (k/reg-event-fx ::trend-zoom-reset 272 | (fn [{:keys [db]} _] 273 | (let [trend-range (or (:trend-range db) 10)] 274 | (socket-command ["trend-zoom-reset" (:active-trend-index db) (str trend-range)])))) 275 | 276 | (k/reg-event-fx ::trend-range 277 | (fn [{:keys [db]} [new-range]] 278 | {:db (assoc db :trend-range new-range) 279 | :dispatch [::trend-zoom-reset]})) 280 | 281 | (k/reg-event-fx ::set-page 282 | (fn [{:keys [db]} [page]] 283 | {:db (assoc db :page page) 284 | :dispatch [::fetch-signals]})) 285 | 286 | (k/reg-event-fx ::set-vars-per-page 287 | (fn [{:keys [db]} [n]] 288 | {:db (assoc db 289 | :vars-per-page (max 1 n) 290 | :page 1) 291 | :dispatch [::fetch-signals]})) 292 | 293 | (k/reg-event-fx ::load-scenario 294 | (fn [{:keys [db]} [file-name]] 295 | (merge 296 | {:db (assoc db :scenario-start-time (-> db :state :time) 297 | :scenario-end-time (some-> db :state :scenario :end))} 298 | (socket-command ["load-scenario" file-name])))) 299 | 300 | (k/reg-event-fx ::abort-scenario 301 | (fn [{:keys [db]} [file-name]] 302 | (merge 303 | {:db (dissoc db :scenario-start-time :scenario-end-time)} 304 | (socket-command ["abort-scenario" file-name])))) 305 | 306 | (k/reg-event-db ::toggle-dismiss-error 307 | (fn [db] 308 | (update db :error-dismissed not))) 309 | 310 | (k/reg-event-db ::set-plot-height 311 | (fn [db [height]] 312 | (assoc db :plot-height height))) 313 | 314 | (k/reg-event-fx ::save-trends-failure 315 | (fn [_ [error]] 316 | {:dispatch [::feedback-message {:success false 317 | :message error 318 | :command "save-plot-config"}]})) 319 | 320 | (k/reg-event-fx ::save-trends-success 321 | (fn [{:keys [db]} [response]] 322 | {:db (assoc db :plot-config-changed? false) 323 | :dispatch [::feedback-message {:success true 324 | :message response 325 | :command "save-plot-config"}]})) 326 | 327 | (k/reg-event-fx ::save-trends-configuration 328 | (fn [_ _] 329 | {:http-xhrio {:method :post 330 | :uri "http://localhost:8000/plot-config" 331 | :format (ajax/json-request-format) 332 | :on-failure [::save-trends-failure] 333 | :on-success [::save-trends-success] 334 | :response-format (ajax/json-response-format {:keywords? true})}})) 335 | -------------------------------------------------------------------------------- /src/client/core.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.core 6 | (:require [kee-frame.core :as k] 7 | [re-frame.core :as rf] 8 | [client.view :as view] 9 | [client.controller :as controller] 10 | [client.config :refer [socket-url]] 11 | [cljs.reader :as reader] 12 | [client.localstorage :as storage] 13 | [client.config :refer [socket-url]] 14 | [clojure.string :as str])) 15 | 16 | (enable-console-print!) 17 | 18 | (goog-define debug false) 19 | 20 | (def routes 21 | [["/" :index] 22 | ["/modules/:module/:causality" :module] 23 | ["/trend/:index" :trend] 24 | ["/scenarios" :scenarios] 25 | ["/scenarios/:id" :scenario]]) 26 | 27 | (def sort-order 28 | (let [order ["input" "independent" "parameter" "calculatedParameter" "local" "internal" "output"]] 29 | (zipmap order (range (count order))))) 30 | 31 | (defn by-order [s1 s2] 32 | (compare (sort-order s1) (sort-order s2))) 33 | 34 | (defn find-module [db module] 35 | (some->> (get-in db [:state :module-data :fmus]) 36 | (filter #(= module (:name %))) 37 | first)) 38 | 39 | (defn causalities [db module] 40 | (when-let [variables (some-> db (find-module module) :variables)] 41 | (some->> variables 42 | (map :causality) 43 | distinct 44 | (sort by-order)))) 45 | 46 | (defn active-causality [db] 47 | (let [module (:current-module db)] 48 | (or ((set (causalities db module)) (:active-causality db)) 49 | (-> db 50 | (causalities module) 51 | first)))) 52 | 53 | (defn simulation-time [db] 54 | (some-> db :state :time (.toFixed 3))) 55 | 56 | (defn scenario-percent [db] 57 | (let [simulation-time (simulation-time db) 58 | scenario-start-time (:scenario-start-time db) 59 | scenario-end-time (:scenario-end-time db)] 60 | (when (and scenario-start-time scenario-end-time) 61 | (-> (/ (- simulation-time scenario-start-time) scenario-end-time) (* 100) (.toFixed 2) (min 100))))) 62 | 63 | (defn real-time-factor [db] 64 | (str 65 | (some-> db :state :rollingAverageRealTimeFactor (.toFixed 2)) 66 | "/" 67 | (some-> db :state :totalAverageRealTimeFactor (.toFixed 2)))) 68 | 69 | (defn real-time-factor-target [db] 70 | (some-> db :state :realTimeFactorTarget (.toFixed 3))) 71 | 72 | (rf/reg-sub :real-time-factor #(real-time-factor %)) 73 | 74 | (defn real-time? [db] 75 | (if (some-> db :state :isRealTime) 76 | "true" 77 | "false")) 78 | 79 | (defn status-data [db] 80 | (array-map "Algorithm type" "Fixed step" 81 | "Simulation time" (simulation-time db) 82 | "Real time factor (total average)" (some-> db :state :totalAverageRealTimeFactor (.toFixed 3)) 83 | "Real time factor (rolling average)" (some-> db :state :rollingAverageRealTimeFactor (.toFixed 3)) 84 | "Number of steps for rolling average" (some-> db :state :stepsToMonitor) 85 | "Real time factor target" (real-time-factor-target db) 86 | "Real time target" (real-time? db) 87 | "Connection status" (get-in db [:kee-frame.websocket/sockets socket-url :state]) 88 | "Path to loaded config folder" (-> db :state :configDir))) 89 | 90 | (rf/reg-sub :overview status-data) 91 | (rf/reg-sub :time simulation-time) 92 | (rf/reg-sub :loading? (comp :loading :state)) 93 | (rf/reg-sub :loaded? (comp :loaded :state)) 94 | (rf/reg-sub :loaded-dir (fn [db] 95 | (-> db :state :configDir))) 96 | (rf/reg-sub :log-dir (fn [db] 97 | (:log-dir db))) 98 | (rf/reg-sub :prev-paths (fn [db] 99 | (:prev-paths db))) 100 | (rf/reg-sub :status (comp :status :state)) 101 | (rf/reg-sub :realtime? (comp :isRealTime :state)) 102 | 103 | (rf/reg-sub :module-routes (fn [db] 104 | (let [modules (-> db :state :module-data :fmus)] 105 | (map (fn [module] 106 | {:name (:name module) 107 | :causality (first (causalities db (:name module)))}) 108 | modules)))) 109 | 110 | (rf/reg-sub :module-active? (fn [db] 111 | (= (:current-module db) (->> db :state :module :name)))) 112 | 113 | (rf/reg-sub :current-module #(:current-module %)) 114 | 115 | (rf/reg-sub :current-module-index #(-> % :current-module-meta :index)) 116 | 117 | (rf/reg-sub :scenario-percent scenario-percent) 118 | 119 | (rf/reg-sub :pages 120 | (fn [db] 121 | (let [page-count (:page-count db) 122 | all-pages (range 1 (inc page-count))] 123 | all-pages))) 124 | 125 | (rf/reg-sub :module-signals (fn [db] 126 | (:viewing db))) 127 | 128 | (rf/reg-sub :causalities (fn [db] (causalities db (:current-module db)))) 129 | (rf/reg-sub :active-causality active-causality) 130 | (rf/reg-sub :active-trend-index :active-trend-index) 131 | 132 | (rf/reg-sub :signal-value (fn [db [_ module name causality type]] 133 | (->> db 134 | :state 135 | :module 136 | :signals 137 | (filter (fn [var] 138 | (and (= (:name var) name) 139 | (= (:causality var) causality) 140 | (= (:type var) type)))) 141 | first 142 | :value))) 143 | 144 | (rf/reg-sub :trend-info (fn [db _] 145 | (map-indexed (fn [idx {:keys [trend-values] :as trend}] 146 | (-> (select-keys trend [:id :label :plot-type]) 147 | (assoc :index idx) 148 | (assoc :count (count trend-values)))) 149 | (-> db :state :trends)))) 150 | 151 | (rf/reg-sub :current-page #(:page %)) 152 | (rf/reg-sub :vars-per-page #(:vars-per-page %)) 153 | 154 | (rf/reg-sub :feedback-message #(:feedback-message %)) 155 | 156 | (rf/reg-sub :show-success-feedback-messages :show-success-feedback-messages) 157 | 158 | (defn validate-event [db event] 159 | (let [module-tree (-> db :state :module-data :fmus) 160 | model-valid? (->> module-tree (map :name) (filter #(= (:model event) %)) seq boolean) 161 | variable-valid? (some->> module-tree 162 | (filter #(= (:model event) (:name %))) 163 | first 164 | :variables 165 | (filter (fn [{:keys [name]}] 166 | (= name (:variable event)))) 167 | seq 168 | boolean)] 169 | (-> event 170 | (assoc :model-valid? model-valid? :variable-valid? variable-valid? :valid? (and model-valid? variable-valid?)) 171 | (assoc :validation-message (cond 172 | (not model-valid?) "Can't find a model with this name" 173 | (not variable-valid?) "Can't find a variable with this name"))))) 174 | 175 | (defn merge-defaults [db scenario] 176 | (let [new-events (->> scenario 177 | :events 178 | (mapv (fn [event] 179 | (merge (:defaults scenario) event))) 180 | (mapv (fn [event] 181 | (validate-event db event))) 182 | (sort-by :time))] 183 | (-> scenario 184 | (assoc :events new-events) 185 | (assoc :valid? (every? :valid? new-events))))) 186 | 187 | (rf/reg-sub :scenarios (fn [db] 188 | (let [running (-> db :state :running-scenario)] 189 | (->> (get-in db [:state :scenarios]) 190 | (map (fn [filename] 191 | {:id filename 192 | :running? (= filename running)})))))) 193 | 194 | (rf/reg-sub :scenario-running? 195 | (fn [db [_ id]] 196 | (-> db 197 | :state 198 | :running-scenario 199 | (= id)))) 200 | 201 | (rf/reg-sub :any-scenario-running? 202 | (fn [db] 203 | (-> db :state :running-scenario seq))) 204 | 205 | 206 | (rf/reg-sub :scenario-start-time 207 | (fn [db] 208 | (:scenario-start-time db))) 209 | 210 | (rf/reg-sub :scenario-current-time 211 | (fn [db] 212 | (let [scenario-start-time (:scenario-start-time db) 213 | simulation-time (-> db :state :time)] 214 | (- simulation-time scenario-start-time)))) 215 | 216 | (rf/reg-sub :scenario (fn [db] 217 | (->> db 218 | :state 219 | :scenario 220 | (merge-defaults db)))) 221 | 222 | (rf/reg-sub :get-key 223 | (fn [db [_ key]] 224 | (key db))) 225 | 226 | (rf/reg-sub :get-state-key 227 | (fn [db [_ key]] 228 | (-> db :state key))) 229 | 230 | (rf/reg-sub :scenario-id #(:scenario-id %)) 231 | 232 | (rf/reg-sub :has-manipulator? 233 | (fn [db [_ index gui-type value-reference]] 234 | (let [manip-vars (-> db :state :manipulatedVariables)] 235 | (seq (filter (fn [{:keys [slaveIndex type valueReference]}] 236 | (and (= index slaveIndex) 237 | (= gui-type type) 238 | (= value-reference valueReference))) manip-vars))))) 239 | 240 | (rf/reg-sub :error (fn [db] 241 | (when (-> db :state :executionState (= "COSIM_EXECUTION_ERROR")) 242 | {:last-error-code (-> db :state :lastErrorCode) 243 | :last-error-message (-> db :state :lastErrorMessage)}))) 244 | 245 | (rf/reg-sub :error-dismissed #(:error-dismissed %)) 246 | 247 | (rf/reg-sub :plot-height #(:plot-height %)) 248 | 249 | (rf/reg-sub :config-dir (comp :configDir :state)) 250 | 251 | (rf/reg-sub :plot-config-changed? #(:plot-config-changed? %)) 252 | 253 | (rf/reg-sub :lib-version (fn [db] 254 | (-> db :state :libVersion))) 255 | 256 | (k/start! {:routes routes 257 | :hash-routing? true 258 | :debug? (if debug {:blacklist #{::controller/socket-message-received}} false) 259 | :root-component [view/root-comp] 260 | :initial-db {:page 1 261 | :vars-per-page 20 262 | :prev-paths (reader/read-string (storage/get-item "cosim-paths")) 263 | :show-success-feedback-messages (reader/read-string (storage/get-item "show-success-feedback-message")) 264 | :plot-config-changed? false}}) 265 | -------------------------------------------------------------------------------- /src/client/localstorage.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.localstorage 6 | (:require [oops.core :refer [ocall oget]])) 7 | 8 | (def local-storage 9 | (oget js/window "localStorage")) 10 | 11 | (defn set-item! 12 | [key val] 13 | (ocall local-storage "setItem" key val)) 14 | 15 | (defn get-item 16 | [key] 17 | (ocall local-storage "getItem" key)) 18 | 19 | (defn remove-item! 20 | [key] 21 | (ocall local-storage "removeItem" key)) -------------------------------------------------------------------------------- /src/client/msgpack.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.msgpack 6 | "Code copied from https://github.com/pkcsecurity/msgpack-cljs due to lack of 7 | hosting on clojars or other repositories. License at time of copy is APACHE 8 | 2.0, see https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | One single change has been done in (defn unpack [bytes] ...), omitting the 11 | call to .-buffer on the bytes argument." 12 | (:require [goog.array] 13 | [goog.crypt] 14 | [goog.math.Long])) 15 | 16 | (def msgpack-stream-default-size 64) 17 | 18 | (defn string->bytes [s] 19 | (js/Uint8Array. (.stringToUtf8ByteArray goog.crypt s))) 20 | 21 | (defn bytes->string [bs] 22 | (.utf8ByteArrayToString goog.crypt bs)) 23 | 24 | (defn array= [a b] 25 | (.equals goog.array a b)) 26 | 27 | (defrecord Extended [type data]) 28 | 29 | (defprotocol IExtendable 30 | (extension [this])) 31 | 32 | (defprotocol IStream 33 | (inc-offset! [this new-offset]) 34 | (resize-on-demand! [this n]) 35 | (stream->uint8array [this])) 36 | 37 | (defprotocol IInputStream 38 | (write [this buffer]) 39 | (write-u8 [this u8]) 40 | (write-i8 [this i8]) 41 | (write-u16 [this u16]) 42 | (write-i16 [this i16]) 43 | (write-u32 [this u32]) 44 | (write-i32 [this i32]) 45 | (write-i64 [this i64]) 46 | (write-f64 [this f64])) 47 | 48 | (deftype MsgpackInputStream [^:unsynchronized-mutable bytes ^:unsynchronized-mutable offset] 49 | IStream 50 | (resize-on-demand! [this n] 51 | (let [base (+ offset n)] 52 | (when (> base (.-byteLength bytes)) 53 | (let [new-bytes (js/Uint8Array. (bit-or 0 (* 1.5 base))) 54 | old-bytes (js/Uint8Array. (.-buffer bytes))] 55 | (set! bytes (js/DataView. (.-buffer new-bytes))) 56 | (.set new-bytes old-bytes 0))))) 57 | (inc-offset! [this n] (set! offset (+ offset n))) 58 | (stream->uint8array [this] 59 | (js/Uint8Array. (.-buffer bytes) 0 offset)) 60 | 61 | IInputStream 62 | (write [this buffer] 63 | (resize-on-demand! this (.-byteLength buffer)) 64 | (.set (js/Uint8Array. (.-buffer bytes)) buffer offset) 65 | (inc-offset! this (.-byteLength buffer))) 66 | (write-u8 [this u8] 67 | (resize-on-demand! this 1) 68 | (.setUint8 bytes offset u8 false) 69 | (inc-offset! this 1)) 70 | (write-i8 [this i8] 71 | (resize-on-demand! this 1) 72 | (.setInt8 bytes offset i8 false) 73 | (inc-offset! this 1)) 74 | (write-u16 [this u16] 75 | (resize-on-demand! this 2) 76 | (.setUint16 bytes offset u16 false) 77 | (inc-offset! this 2)) 78 | (write-i16 [this i16] 79 | (resize-on-demand! this 2) 80 | (.setInt16 bytes offset i16 false) 81 | (inc-offset! this 2)) 82 | (write-u32 [this u32] 83 | (resize-on-demand! this 4) 84 | (.setUint32 bytes offset u32 false) 85 | (inc-offset! this 4)) 86 | (write-i32 [this i32] 87 | (resize-on-demand! this 4) 88 | (.setInt32 bytes offset i32 false) 89 | (inc-offset! this 4)) 90 | 91 | ; msgpack stores integers in big-endian 92 | (write-i64 [this u64] 93 | (let [glong (goog.math.Long/fromNumber u64)] 94 | (write-i32 this (.getHighBits glong)) 95 | (write-i32 this (.getLowBits glong)))) 96 | 97 | (write-f64 [this f64] 98 | (resize-on-demand! this 8) 99 | (.setFloat64 bytes offset f64 false) 100 | (inc-offset! this 8))) 101 | 102 | (defn input-stream [] 103 | (MsgpackInputStream. (js/DataView. (js/ArrayBuffer. msgpack-stream-default-size)) 0)) 104 | 105 | (defn pack-bytes [stream bytes] 106 | (let [n (.-byteLength bytes)] 107 | (cond 108 | (<= n 0xff) (doto stream (write-u8 0xc4) (write-u8 n) (write bytes)) 109 | (<= n 0xffff) (doto stream (write-u8 0xc5) (write-u16 n) (write bytes)) 110 | (<= n 0xffffffff) (doto stream (write-u8 0xc6) (write-u32 n) (write bytes)) 111 | :else (throw (js/Error. "bytes too large to pack"))))) 112 | 113 | ; we will support doubles only 114 | (defn pack-float [stream f] 115 | (doto stream (write-u8 0xcb) (write-f64 f))) 116 | 117 | (defn pack-int [stream i] 118 | (cond 119 | ; +fixnum 120 | (<= 0 i 127) (write-u8 stream i) 121 | ; -fixnum 122 | (<= -32 i -1) (write-i8 stream i) 123 | 124 | ; uint 8 125 | (<= 0 i 0xff) (doto stream (write-u8 0xcc) (write-u8 i)) 126 | ; uint 16 127 | (<= 0 i 0xffff) (doto stream (write-u8 0xcd) (write-u16 i)) 128 | ; uint 32 129 | (<= 0 i 0xffffffff) (doto stream (write-u8 0xce) (write-u32 i)) 130 | ; uint 64 131 | (<= 0 i 0xffffffffffffffff) (doto stream (write-u8 0xcf) (write-i64 i)) 132 | 133 | ; int 8 134 | (<= -0x80 i -1) (doto stream (write-u8 0xd0) (write-i8 i)) 135 | ; int 16 136 | (<= -0x8000 i -1) (doto stream (write-u8 0xd1) (write-i16 i)) 137 | ; int 32 138 | (<= -0x80000000 i -1) (doto stream (write-u8 0xd2) (write-i32 i)) 139 | ; int 64 140 | (<= -0x8000000000000000 i -1) (doto stream (write-u8 0xd3) (write-i64 i)) 141 | 142 | :else (throw (js/Error. (str "Integer value out of bounds: " i))))) 143 | 144 | (defn pack-number [stream n] 145 | (if-not (integer? n) 146 | (pack-float stream n) 147 | (pack-int stream n))) 148 | 149 | (defn pack-string [stream s] 150 | (let [bytes (string->bytes s) 151 | len (.-byteLength bytes)] 152 | (cond 153 | (<= len 0x1f) (doto stream (write-u8 (bit-or 2r10100000 len)) (write bytes)) 154 | (<= len 0xff) (doto stream (write-u8 0xd9) (write-u8 len) (write bytes)) 155 | (<= len 0xffff) (doto stream (write-u8 0xda) (write-u16 len) (write bytes)) 156 | (<= len 0xffffffff) (doto stream (write-u8 0xdb) (write-u32 len) (write bytes)) 157 | :else (throw (js/Error. "string too large to pack"))))) 158 | 159 | (declare pack) 160 | 161 | (defprotocol Packable 162 | "Objects that can be serialized as MessagePack types" 163 | (pack-stream [this stream])) 164 | 165 | (defn pack-coll [stream coll] 166 | (doseq [x coll] 167 | (pack-stream x stream))) 168 | 169 | (extend-protocol IExtendable 170 | PersistentHashSet 171 | (extension [this] 172 | (Extended. 0x07 (pack (vec this)))) 173 | Keyword 174 | (extension [this] 175 | (Extended. 0x03 (pack (.substring (str this) 1) (input-stream)))) 176 | cljs.core.Symbol 177 | (extension [this] 178 | (Extended. 0x04 (pack (str this))))) 179 | 180 | (defn pack-extended [s {:keys [type data]}] 181 | (let [len (.-byteLength data)] 182 | (case len 183 | 1 (write-u8 s 0xd4) 184 | 2 (write-u8 s 0xd5) 185 | 4 (write-u8 s 0xd6) 186 | 8 (write-u8 s 0xd7) 187 | 16 (write-u8 s 0xd8) 188 | (cond 189 | (<= len 0xff) (doto s (write-u8 0xc7) (write-u8 len)) 190 | (<= len 0xffff) (doto s (write-u8 0xc8) (write-u16 len)) 191 | (<= len 0xffffffff) (doto s (write-u8 0xc9) (write-u32 len)) 192 | :else (throw (js/Error. "extended type too large to pack")))) 193 | (write-u8 s type) 194 | (write s data))) 195 | 196 | (defn pack-seq [s seq] 197 | (let [len (count seq)] 198 | (cond 199 | (<= len 0xf) (doto s (write-u8 (bit-or 2r10010000 len)) (pack-coll seq)) 200 | (<= len 0xffff) (doto s (write-u8 0xdc) (write-u16 len) (pack-coll seq)) 201 | (<= len 0xffffffff) (doto s (write-u8 0xdd) (write-u32 len) (pack-coll seq)) 202 | :else (throw (js/Error. "seq type too large to pack"))))) 203 | 204 | (defn pack-map [s map] 205 | (let [len (count map) 206 | pairs (interleave (keys map) (vals map))] 207 | (cond 208 | (<= len 0xf) (doto s (write-u8 (bit-or 2r10000000 len)) (pack-coll pairs)) 209 | (<= len 0xffff) (doto s (write-u8 0xde) (write-u16 len) (pack-coll pairs)) 210 | (<= len 0xffffffff) (doto s (write-u8 0xdf) (write-u32 len) (pack-coll pairs)) 211 | :else (throw (js/Error. "map type too large to pack"))))) 212 | 213 | (extend-protocol Packable 214 | nil 215 | (pack-stream [_ s] (write-u8 s 0xc0)) 216 | 217 | boolean 218 | (pack-stream [bool s] (write-u8 s (if bool 0xc3 0xc2))) 219 | 220 | number 221 | (pack-stream [n s] (pack-number s n)) 222 | 223 | string 224 | (pack-stream [str s] (pack-string s str)) 225 | 226 | Extended 227 | (pack-stream [ext s] (pack-extended s ext)) 228 | 229 | PersistentVector 230 | (pack-stream [seq s] (pack-seq s seq)) 231 | 232 | EmptyList 233 | (pack-stream [seq s] (pack-seq s seq)) 234 | 235 | List 236 | (pack-stream [seq s] (pack-seq s seq)) 237 | 238 | LazySeq 239 | (pack-stream [seq s] (pack-seq s (vec seq))) 240 | 241 | js/Uint8Array 242 | (pack-stream [u8 s] (pack-bytes s u8)) 243 | 244 | PersistentArrayMap 245 | (pack-stream [array-map s] (pack-map s array-map)) 246 | 247 | PersistentHashMap 248 | (pack-stream [hmap s] (pack-map s hmap)) 249 | 250 | PersistentHashSet 251 | (pack-stream [hset s] (pack-stream (extension hset) s)) 252 | 253 | Keyword 254 | (pack-stream [kw s] (pack-stream (extension kw) s)) 255 | 256 | Symbol 257 | (pack-stream [sym s] (pack-stream (extension sym) s))) 258 | 259 | (declare unpack-stream) 260 | 261 | (defprotocol IOutputStream 262 | (read [this n]) 263 | (read-bytes [this n]) 264 | (read-u8 [this]) 265 | (read-i8 [this]) 266 | (read-u16 [this]) 267 | (read-i16 [this]) 268 | (read-u32 [this]) 269 | (read-i32 [this]) 270 | (read-i64 [this]) 271 | (read-f32 [this]) 272 | (read-f64 [this])) 273 | 274 | (deftype MsgpackOutputStream [bytes ^:unsynchronized-mutable offset] 275 | IStream 276 | (inc-offset! [this n] (set! offset (+ offset n))) 277 | (resize-on-demand! [this n] nil) 278 | (stream->uint8array [this] 279 | (js/Uint8Array. (.-buffer bytes))) 280 | 281 | IOutputStream 282 | (read [this n] 283 | (let [old-offset offset] 284 | (inc-offset! this n) 285 | (.slice (.-buffer bytes) old-offset offset))) 286 | (read-bytes [this n] 287 | (js/Uint8Array. (read this n))) 288 | (read-u8 [this] 289 | (let [u8 (.getUint8 bytes offset)] 290 | (inc-offset! this 1) 291 | u8)) 292 | (read-i8 [this] 293 | (let [i8 (.getInt8 bytes offset)] 294 | (inc-offset! this 1) 295 | i8)) 296 | (read-u16 [this] 297 | (let [u16 (.getUint16 bytes offset)] 298 | (inc-offset! this 2) 299 | u16)) 300 | (read-i16 [this] 301 | (let [i16 (.getInt16 bytes offset false)] 302 | (inc-offset! this 2) 303 | i16)) 304 | (read-u32 [this] 305 | (let [u32 (.getUint32 bytes offset false)] 306 | (inc-offset! this 4) 307 | u32)) 308 | (read-i32 [this] 309 | (let [i32 (.getInt32 bytes offset false)] 310 | (inc-offset! this 4) 311 | i32)) 312 | (read-i64 [this] 313 | (let [high-bits (.getInt32 bytes offset false) 314 | low-bits (.getInt32 bytes (+ offset 4) false)] 315 | (inc-offset! this 8) 316 | (.toNumber (goog.math.Long. low-bits high-bits)))) 317 | (read-f32 [this] 318 | (let [f32 (.getFloat32 bytes offset false)] 319 | (inc-offset! this 4) 320 | f32)) 321 | (read-f64 [this] 322 | (let [f64 (.getFloat64 bytes offset false)] 323 | (inc-offset! this 8) 324 | f64))) 325 | 326 | (defn output-stream [bytes] 327 | (MsgpackOutputStream. (js/DataView. bytes) 0)) 328 | 329 | (defn read-str [stream n] 330 | (bytes->string (read-bytes stream n))) 331 | 332 | (defn unpack-n [stream n] 333 | (let [v (transient [])] 334 | (dotimes [_ n] 335 | (conj! v (unpack-stream stream))) 336 | (persistent! v))) 337 | 338 | (defn unpack-map [stream n] 339 | (apply hash-map (unpack-n stream (* 2 n)))) 340 | 341 | (declare unpack-ext) 342 | 343 | (defn unpack-stream [stream] 344 | (let [byte (read-u8 stream)] 345 | (case byte 346 | 0xc0 nil 347 | 0xc2 false 348 | 0xc3 true 349 | 0xc4 (read-bytes stream (read-u8 stream)) 350 | 0xc5 (read-bytes stream (read-u16 stream)) 351 | 0xc6 (read-bytes stream (read-u32 stream)) 352 | 0xc7 (unpack-ext stream (read-u8 stream)) 353 | 0xc8 (unpack-ext stream (read-u16 stream)) 354 | 0xc9 (unpack-ext stream (read-u32 stream)) 355 | 0xca (read-f32 stream) 356 | 0xcb (read-f64 stream) 357 | 0xcc (read-u8 stream) 358 | 0xcd (read-u16 stream) 359 | 0xce (read-u32 stream) 360 | 0xcf (read-i64 stream) 361 | 0xd0 (read-i8 stream) 362 | 0xd1 (read-i16 stream) 363 | 0xd2 (read-i32 stream) 364 | 0xd3 (read-i64 stream) 365 | 0xd4 (unpack-ext stream 1) 366 | 0xd5 (unpack-ext stream 2) 367 | 0xd6 (unpack-ext stream 4) 368 | 0xd7 (unpack-ext stream 8) 369 | 0xd8 (unpack-ext stream 16) 370 | 0xd9 (read-str stream (read-u8 stream)) 371 | 0xda (read-str stream (read-u16 stream)) 372 | 0xdb (read-str stream (read-u32 stream)) 373 | 0xdc (unpack-n stream (read-u16 stream)) 374 | 0xdd (unpack-n stream (read-u32 stream)) 375 | 0xde (unpack-map stream (read-u16 stream)) 376 | 0xdf (unpack-map stream (read-u32 stream)) 377 | (cond 378 | (= (bit-and 2r11100000 byte) 2r11100000) byte 379 | (= (bit-and 2r10000000 byte) 0) byte 380 | (= (bit-and 2r11100000 byte) 2r10100000) (read-str stream (bit-and 2r11111 byte)) 381 | (= (bit-and 2r11110000 byte) 2r10010000) (unpack-n stream (bit-and 2r1111 byte)) 382 | (= (bit-and 2r11110000 byte) 2r10000000) (unpack-map stream (bit-and 2r1111 byte)) 383 | :else (throw (js/Error. "invalid msgpack stream")))))) 384 | 385 | (defn keyword-deserializer [bytes] 386 | (keyword 387 | (unpack-stream 388 | (output-stream bytes)))) 389 | 390 | (defn symbol-deserializer [bytes] 391 | (symbol 392 | (unpack-stream 393 | (output-stream bytes)))) 394 | 395 | (defn char-deserializer [bytes] 396 | (unpack-stream 397 | (output-stream bytes))) 398 | 399 | (defn ratio-deserializer [bytes] 400 | (let [[n d] (unpack-stream (output-stream bytes))] 401 | (/ n d))) 402 | 403 | (defn set-deserializer [bytes] 404 | (set (unpack-stream (output-stream bytes)))) 405 | 406 | (defn unpack-ext [stream n] 407 | (let [type (read-u8 stream)] 408 | (case type 409 | 3 (keyword-deserializer (read stream n)) 410 | 4 (symbol-deserializer (read stream n)) 411 | 5 (char-deserializer (read stream n)) 412 | 6 (ratio-deserializer (read stream n)) 413 | 7 (set-deserializer (read stream n))))) 414 | 415 | (defn unpack [bytes] 416 | (unpack-stream 417 | (output-stream bytes))) 418 | 419 | (defn pack [obj] 420 | (let [stream (input-stream)] 421 | (pack-stream obj stream) 422 | (stream->uint8array stream))) 423 | -------------------------------------------------------------------------------- /src/client/msgpack_format.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.msgpack-format 6 | (:require [chord.format :as f] 7 | [client.msgpack :as msgpack])) 8 | 9 | (defn unpack [s] 10 | (-> s 11 | msgpack/unpack 12 | clojure.walk/keywordize-keys)) 13 | 14 | (defn pack [obj] 15 | (-> obj 16 | clojure.walk/stringify-keys 17 | msgpack/pack)) 18 | 19 | (defmethod f/formatter* :msgpack [_] 20 | (reify f/ChordFormatter 21 | 22 | (freeze [_ obj] 23 | (pack obj)) 24 | 25 | (thaw [_ s] 26 | (unpack s)))) 27 | -------------------------------------------------------------------------------- /src/client/scenario.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.scenario 6 | (:require [re-frame.core :as rf] 7 | [kee-frame.core :as k] 8 | [clojure.string :as str] 9 | [client.controller :as controller])) 10 | 11 | (defn scenario-filename-to-name [file-name] 12 | (str/capitalize (first (str/split file-name ".")))) 13 | 14 | (defn cellie [text valid? message] 15 | (if valid? 16 | [:td text] 17 | [:td {:class "error" 18 | :data-tooltip message} text])) 19 | 20 | (defn run-button [scenario-id any-running? invalid?] 21 | [:button.ui.right.labeled.icon.button 22 | {:disabled (or any-running? invalid?) 23 | :on-click #(rf/dispatch [::controller/load-scenario scenario-id])} 24 | (if invalid? [:i.red.ban.icon] [:i.green.play.icon]) 25 | (if invalid? 26 | "Invalid syntax" 27 | (if any-running? "Other scenario running" "Load scenario"))]) 28 | 29 | (defn running-button [scenario-id] 30 | [:button.ui.right.labeled.icon.button 31 | {:on-click #(rf/dispatch [::controller/abort-scenario scenario-id])} 32 | "Abort scenario" 33 | [:i.red.stop.icon]]) 34 | 35 | (defn round-number 36 | [f] 37 | (/ (.round js/Math (* 100 f)) 100)) 38 | 39 | (defn one [] 40 | (let [scenario @(rf/subscribe [:scenario]) 41 | scenario-id @(rf/subscribe [:scenario-id]) 42 | scenario-percent @(rf/subscribe [:scenario-percent]) 43 | scenario-start-time @(rf/subscribe [:scenario-start-time]) 44 | scenario-current-time @(rf/subscribe [:scenario-current-time]) 45 | running? @(rf/subscribe [:scenario-running? scenario-id]) 46 | any-running? @(rf/subscribe [:any-scenario-running?])] 47 | [:div 48 | [:div.ui.header "Actions"] 49 | (if running? 50 | [:div 51 | [running-button scenario-id] 52 | (when (not (nil? scenario-percent)) [:span 53 | [:div (str "Scenario start time: " (round-number scenario-start-time) "s")] 54 | [:div (str "Scenario current time: " (round-number scenario-current-time) "s")] 55 | [:div (str "Scenario execution: " (round-number scenario-percent) "%")]])] 56 | [run-button scenario-id any-running? (not (:valid? scenario))]) 57 | [:div.ui.header "Description"] 58 | [:div (or (:description scenario) "No description available")] 59 | [:div.ui.header "Events"] 60 | [:table.ui.celled.striped.selectable.fluid.table 61 | [:thead 62 | [:tr 63 | [:th "Time"] 64 | [:th "Model"] 65 | [:th "Variable"] 66 | [:th "Action"] 67 | [:th "Value"]]] 68 | [:tbody 69 | (map-indexed (fn [index {:keys [time model variable action value model-valid? variable-valid? validation-message] :as event}] 70 | [:tr {:key (str "scenario-" index "-event") 71 | :class (if (and running? 72 | (not (nil? scenario-percent)) 73 | (< time scenario-current-time)) "positive" "")} 74 | [:td time] 75 | [cellie model model-valid? validation-message] 76 | [cellie variable variable-valid? validation-message] 77 | [:td action] 78 | [:td (str value)]]) 79 | (:events scenario))]] 80 | [:div.ui.header "End time"] 81 | [:div (or (:end scenario) 82 | (-> scenario :events last :time))]])) 83 | 84 | (defn overview [] 85 | (let [scenarios @(rf/subscribe [:scenarios])] 86 | [:div.ui.large.list 87 | (map (fn [{:keys [id running?]}] 88 | [:div.item {:key (str "scenario-" id)} 89 | [:i.file.alternate.icon {:class (when running? "green")}] 90 | [:div.content 91 | [:a {:href (k/path-for [:scenario {:id id}])} (str (scenario-filename-to-name id) " - " id)]]]) 92 | scenarios)])) 93 | -------------------------------------------------------------------------------- /src/client/trend.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.trend 6 | (:require [client.controller :as controller] 7 | [reagent.core :as r] 8 | [cljsjs.plotly] 9 | [re-frame.core :as rf] 10 | [cljs.spec.alpha :as s] 11 | [client.components :as c] 12 | [clojure.string :as str])) 13 | 14 | (def id-store (atom nil)) 15 | 16 | (def plot-heights {:collapsed "50vh" 17 | :expanded "75vh"}) 18 | 19 | (def range-configs 20 | [{:seconds 10 21 | :text "10s"} 22 | {:seconds 30 23 | :text "30s"} 24 | {:seconds 60 25 | :text "1m"} 26 | {:seconds (* 60 5) 27 | :text "5m"} 28 | {:seconds (* 60 10) 29 | :text "10m"} 30 | {:seconds (* 60 20) 31 | :text "20m"}]) 32 | 33 | (def common-layout 34 | {:autosize true 35 | :use-resize-handler true 36 | :showlegend true 37 | :uirevision true 38 | :margin {:l 0 :r 0 :t 25 :pad 5} 39 | :xaxis {:automargin true} 40 | :yaxis {:automargin true} 41 | :legend {:orientation "h" 42 | :uirevision true}}) 43 | 44 | (def trend-layout 45 | (merge common-layout 46 | {:xaxis {:automargin true 47 | :title {:text "Time [s]" 48 | :font {:size 14}}}})) 49 | 50 | (def scatter-layout 51 | (merge common-layout 52 | {:xaxis {:autorange true 53 | :autotick true 54 | :ticks "" 55 | :automargin true}})) 56 | 57 | (def options 58 | {:responsive true 59 | :toImageButtonOptions {:width 1280 :height 768}}) 60 | 61 | (defn- plotly-expand-button [plot-height plot-expanded?] 62 | (if @plot-expanded? 63 | [:button.ui.button.right.floated {:on-click (fn [] 64 | (rf/dispatch [::controller/set-plot-height (:collapsed plot-heights)]) 65 | (swap! plot-expanded? not))} 66 | [:i.compress.icon] 67 | "Compress plot"] 68 | [:button.ui.button.right.floated {:on-click (fn [] 69 | (rf/dispatch [::controller/set-plot-height (:expanded plot-heights)]) 70 | (swap! plot-expanded? not))} 71 | [:i.expand.icon] 72 | "Expand plot"])) 73 | 74 | (defn- layout-selector [plot-type] 75 | (case plot-type 76 | "trend" trend-layout 77 | "scatter" scatter-layout 78 | {})) 79 | 80 | (defn- autoscale! [] 81 | (js/Plotly.relayout 82 | "plotly" 83 | (clj->js {:xaxis {:autorange true} 84 | :yaxis {:autorange true}}))) 85 | 86 | (defn- namespaced 87 | "Takes a map and a symbol name or string and creates a new map with namespaced keys as defined by the symbol. 88 | E.g. (namespaced {:a 1} 'my-ns) -> {:my-ns/a 1}" 89 | [m ns] 90 | (into {} 91 | (map (fn [[k v]] [(keyword (str ns "/" (name k))) v]) 92 | (seq m)))) 93 | 94 | (def first-signal-ns 'first) 95 | (def second-signal-ns 'second) 96 | 97 | (defn- format-data-for-plotting 98 | "Data for time series plots (trend) are returned as is. 99 | For XY plots (scatter) pairs of trend-values are merged together to form a plot with x and y values. 100 | The metadata fields are given namespaces to avoid loosing information when merging the pairs of values." 101 | [plot-type trend-values] 102 | (case plot-type 103 | "trend" trend-values 104 | "scatter" (map (fn [[a b]] 105 | (merge 106 | (select-keys a [:xvals :yvals]) 107 | (select-keys b [:xvals :yvals]) 108 | (namespaced (dissoc a :xvals :yvals) first-signal-ns) 109 | (namespaced (dissoc b :xvals :yvals) second-signal-ns))) 110 | (partition 2 trend-values)) 111 | [])) 112 | 113 | (defn- range-selector [trend-range {:keys [text seconds]}] 114 | ^{:key text} 115 | [:button.ui.button 116 | {:on-click #(rf/dispatch [::controller/trend-range seconds]) 117 | :class (if (= trend-range seconds) "active" "")} 118 | text]) 119 | 120 | (defn plot-type-from-label [label] 121 | "Expects label to be a string on format 'Time series #a9123ddc-..'" 122 | (str/trim (first (str/split label "#")))) 123 | 124 | (defn- time-series-legend-name [{:keys [module signal causality type]}] 125 | (str/join " - " [module signal causality type])) 126 | 127 | (defn- xy-plot-legend-name [plot] 128 | (let [first-signal ((keyword (str first-signal-ns "/" 'signal)) plot) 129 | second-signal ((keyword (str second-signal-ns "/" 'signal)) plot)] 130 | (str/join " / " [first-signal second-signal]))) 131 | 132 | (defn- delete-series [dom-node] 133 | (let [num-series (-> dom-node .-data .-length)] 134 | (doseq [_ (range num-series)] 135 | (js/Plotly.deleteTraces dom-node 0)))) 136 | 137 | (defn- add-traces [dom-node plots legend-fn] 138 | (doseq [plot plots] 139 | (js/Plotly.addTraces dom-node (clj->js {:name (legend-fn plot) :x [] :y []})))) 140 | 141 | (defn- update-traces [dom-node trend-values] 142 | (let [num-series (-> dom-node .-data .-length)] 143 | (doseq [_ (range num-series)] 144 | (js/Plotly.deleteTraces dom-node 0)) 145 | (case (:plot-type @(rf/subscribe [::active-trend])) 146 | "trend" (add-traces dom-node trend-values time-series-legend-name) 147 | "scatter" (add-traces dom-node trend-values xy-plot-legend-name)))) 148 | 149 | (defn- update-chart-data [dom-node trend-values layout trend-id] 150 | (when-not (= trend-id @id-store) 151 | (reset! id-store trend-id) 152 | (delete-series dom-node) 153 | (autoscale!)) 154 | (s/assert ::trend-values trend-values) 155 | (let [init-data {:x [] :y []} 156 | data (reduce (fn [data {:keys [xvals yvals]}] 157 | (-> data 158 | (update :x conj xvals) 159 | (update :y conj yvals))) 160 | init-data trend-values)] 161 | (update-traces dom-node trend-values) 162 | (js/Plotly.update dom-node (clj->js data) (clj->js layout) (clj->js options)))) 163 | 164 | (defn- relayout-callback [js-event] 165 | (let [event (js->clj js-event) 166 | begin (get event "xaxis.range[0]") 167 | end (get event "xaxis.range[1]") 168 | auto? (get event "xaxis.autorange") 169 | active-trend @(rf/subscribe [::active-trend])] 170 | (when (= (:plot-type active-trend) "trend") 171 | (cond 172 | auto? 173 | (rf/dispatch [::controller/trend-zoom-reset]) 174 | (and begin end) 175 | (rf/dispatch [::controller/trend-zoom begin end]))))) 176 | 177 | (defn- set-dom-element-height! [dom-node height] 178 | (-> dom-node .-style .-height (set! height))) 179 | 180 | (defn- trend-inner [] 181 | (let [update-plot (fn [comp] 182 | (let [{:keys [trend-values trend-id plot-type plot-height]} (r/props comp) 183 | dom-node (r/dom-node comp) 184 | _ (set-dom-element-height! dom-node plot-height) 185 | layout (layout-selector plot-type)] 186 | (update-chart-data (r/dom-node comp) trend-values layout trend-id))) 187 | render-plot (fn [comp] 188 | (let [{:keys [plot-type plot-height]} (r/props comp) 189 | dom-node (r/dom-node comp) 190 | _ (set-dom-element-height! dom-node plot-height) 191 | layout (layout-selector plot-type)] 192 | (js/Plotly.newPlot dom-node 193 | (clj->js [{:x [] 194 | :y [] 195 | :mode "lines" 196 | :type "scatter"}]) 197 | (clj->js layout) 198 | (clj->js options))))] 199 | (r/create-class 200 | {:component-did-mount (fn [comp] 201 | (render-plot comp) 202 | (.on (r/dom-node comp) "plotly_relayout" relayout-callback)) 203 | :component-did-update update-plot 204 | :reagent-render (fn [] 205 | [:div#plotly.column])}))) 206 | 207 | (defn variable-row [] 208 | (let [untrending? (r/atom false)] 209 | (fn [module signal causality val] 210 | [:tr 211 | [:td module] 212 | [:td signal] 213 | [:td causality] 214 | [:td (when (and (some? val) (number? val)) 215 | (.toFixed val 4))] 216 | #_[:td 217 | (if @untrending? 218 | [:i.fa.fa-spinner.fa-spin] 219 | [:span {:style {:float 'right :cursor 'pointer} 220 | :data-tooltip "Remove variable from plot" 221 | :data-position "top center"} 222 | [:i.eye.slash.gray.icon {:on-click #(rf/dispatch [::controller/untrend-single trend-idx (str module "." signal)])}]])]]))) 223 | 224 | (defn last-value [xvals yvals plot-type] 225 | (let [last-x (last xvals) 226 | last-y (last yvals)] 227 | (if (= plot-type "scatter") 228 | (or last-x last-y) 229 | last-y))) 230 | 231 | (defn variables-table [trend-values plot-type] 232 | [:table.ui.single.line.striped.table 233 | [:thead 234 | [:tr 235 | [:th "Model"] 236 | [:th "Variable"] 237 | [:th "Causality"] 238 | [:th "Value"] 239 | #_[:th {:style {:text-align 'right}} "Remove"]]] 240 | [:tbody 241 | (doall 242 | (for [{:keys [module signal causality xvals yvals]} trend-values] ^{:key (str module signal (rand-int 9999))} 243 | [variable-row module signal causality (last-value xvals yvals plot-type)]))]]) 244 | 245 | (defn trend-outer [] 246 | (let [trend-range (rf/subscribe [::trend-range]) 247 | active-trend (rf/subscribe [::active-trend]) 248 | active-trend-index (rf/subscribe [:active-trend-index]) 249 | plot-height (rf/subscribe [:plot-height]) 250 | plot-expanded? (r/atom false)] 251 | (fn [] 252 | (let [{:keys [id plot-type label trend-values]} @active-trend 253 | active-trend-index (int @active-trend-index) 254 | name (plot-type-from-label label)] 255 | [:div.ui.one.column.grid 256 | 257 | [c/text-editor name [::controller/set-label] "Rename plot"] 258 | 259 | [:div.two.column.row 260 | [:div.column 261 | (doall (map (partial range-selector @trend-range) range-configs))] 262 | [:div.column 263 | [plotly-expand-button plot-height plot-expanded?] 264 | [:button.ui.button.right.floated {:on-click #(rf/dispatch [::controller/removetrend active-trend-index])} 265 | [:i.trash.gray.icon] 266 | "Remove plot"] 267 | [:button.ui.button.right.floated {:on-click #(rf/dispatch [::controller/untrend active-trend-index])} 268 | [:i.eye.slash.gray.icon] 269 | "Remove all variables from plot"]]] 270 | 271 | [:div.one.column.row 272 | [trend-inner {:trend-values (format-data-for-plotting plot-type trend-values) 273 | :plot-type plot-type 274 | :plot-height (or @plot-height (:collapsed plot-heights)) 275 | :trend-id id}]] 276 | 277 | (when (not @plot-expanded?) 278 | [variables-table trend-values plot-type])])))) 279 | 280 | (rf/reg-sub ::active-trend #(get-in % [:state :trends (-> % :active-trend-index int)])) 281 | 282 | (rf/reg-sub ::trend-range 283 | :<- [::active-trend] 284 | #(-> % :spec :range)) 285 | 286 | (defn- ascending-points? [tuples] 287 | (= tuples 288 | (sort-by :x tuples))) 289 | 290 | (s/def ::module string?) 291 | (s/def ::signal string?) 292 | (s/def ::trend-point (s/keys :req-un [::x ::y])) 293 | (s/def ::ascending-points ascending-points?) 294 | (s/def ::trend-data (s/and (s/coll-of ::trend-point :kind vector?) ::ascending-points)) 295 | (s/def ::trend-value (s/keys :req-un [::module ::signal ::trend-data])) 296 | (s/def ::trend-values (s/coll-of ::trend-value)) 297 | -------------------------------------------------------------------------------- /src/client/view.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | (ns client.view 6 | (:require [client.trend :as trend] 7 | [kee-frame.core :as k] 8 | [re-frame.core :as rf] 9 | [reagent.core :as r] 10 | [client.controller :as controller] 11 | [client.config :refer [socket-url]] 12 | [client.components :as c] 13 | [client.scenario :as scenario] 14 | [clojure.string :as str] 15 | [fulcrologic.semantic-ui.factories :as semantic] 16 | [fulcrologic.semantic-ui.icons :as icons])) 17 | 18 | (goog-define default-load-dir "") 19 | (goog-define default-log-dir "") 20 | 21 | (defn variable-display [module index {:keys [name causality type editable?] :as variable}] 22 | (let [value @(rf/subscribe [:signal-value module name causality type])] 23 | (if editable? 24 | [c/variable-override-editor index variable value] 25 | [:div (str value)]))) 26 | 27 | (defn xy-plottable? [type] 28 | (= "Real" type)) 29 | 30 | (defn plottable? [type] 31 | (#{"Real" "Integer"} type)) 32 | 33 | (defn trend-item [current-module name type {:keys [id index count label plot-type]}] 34 | (let [label-text (str (inc index) ") " (trend/plot-type-from-label label))] 35 | (case plot-type 36 | 37 | "trend" (semantic/ui-dropdown-item 38 | {:key (str "trend-item-" id) 39 | :text label-text 40 | :label "Time series" 41 | :onClick #(rf/dispatch [::controller/add-to-trend current-module name index])}) 42 | 43 | "scatter" (when (xy-plottable? type) 44 | (let [axis (if (even? count) 'X 'Y)] 45 | (semantic/ui-dropdown-item 46 | {:key (str "trend-item-" id) 47 | :text label-text 48 | :label (str "XY plot - " axis " axis") 49 | :onClick #(rf/dispatch [::controller/add-to-trend current-module name index])})))))) 50 | 51 | (defn action-dropdown [current-module name type trend-info] 52 | (when (and (seq trend-info) (plottable? type)) 53 | (semantic/ui-dropdown 54 | {:button false :text "Add to plot"} 55 | (semantic/ui-dropdown-menu 56 | {:direction 'left} 57 | (map (partial trend-item current-module name type) trend-info))))) 58 | 59 | (defn pages-menu [] 60 | (let [current-page @(rf/subscribe [:current-page]) 61 | pages @(rf/subscribe [:pages]) 62 | vars-per-page @(rf/subscribe [:vars-per-page])] 63 | [:div.right.menu 64 | [:div.item 65 | [:div.ui.transparent.input 66 | [:button.ui.icon.button 67 | {:disabled (= 1 current-page) 68 | :on-click #(when (< 1 current-page) 69 | (rf/dispatch [::controller/set-page (dec current-page)]))} 70 | [:i.chevron.left.icon]] 71 | [:input {:type :text 72 | :readOnly true 73 | :value (str "Page " current-page " of " (last pages))}] 74 | [:button.ui.icon.button 75 | {:disabled (= (last pages) current-page) 76 | :on-click #(when (< current-page (last pages)) 77 | (rf/dispatch [::controller/set-page (inc current-page)]))} 78 | [:i.chevron.right.icon]]] 79 | [:div.ui.icon.button.simple.dropdown 80 | [:i.sliders.horizontal.icon] 81 | [:div.menu 82 | [:div.header (str vars-per-page " variables per page")] 83 | [:div.item 84 | [:div.ui.icon.buttons 85 | [:button.ui.button {:on-click #(rf/dispatch [::controller/set-vars-per-page (- vars-per-page 5)])} [:i.minus.icon] 5] 86 | [:button.ui.button {:on-click #(rf/dispatch [::controller/set-vars-per-page (dec vars-per-page)])} [:i.minus.icon] 1] 87 | [:button.ui.button {:on-click #(rf/dispatch [::controller/set-vars-per-page (inc vars-per-page)])} [:i.plus.icon] 1] 88 | [:button.ui.button {:on-click #(rf/dispatch [::controller/set-vars-per-page (+ vars-per-page 5)])} [:i.plus.icon] 5]]]]]]])) 89 | 90 | (defn tab-content [tabby] 91 | (let [current-module @(rf/subscribe [:current-module]) 92 | current-module-index @(rf/subscribe [:current-module-index]) 93 | module-signals @(rf/subscribe [:module-signals]) 94 | active @(rf/subscribe [:active-causality]) 95 | trend-info @(rf/subscribe [:trend-info])] 96 | [:div.ui.bottom.attached.tab.segment {:data-tab tabby 97 | :class (when (= tabby active) "active")} 98 | [:table.ui.compact.single.line.striped.selectable.table 99 | [:thead 100 | [:tr 101 | [:th.five.wide "Name"] 102 | [:th.one.wide "Type"] 103 | [:th "Value"] 104 | [:th.one.wide "Actions"]]] 105 | [:tbody 106 | (map (fn [{:keys [name causality type] :as variable}] 107 | [:tr {:key (str current-module "-" causality "-" name)} 108 | [:td name] 109 | [:td type] 110 | [:td [variable-display current-module current-module-index variable]] 111 | [:td (action-dropdown current-module name type trend-info)]]) 112 | module-signals)]]])) 113 | 114 | (defn module-listing [] 115 | (let [causalities @(rf/subscribe [:causalities]) 116 | active @(rf/subscribe [:active-causality]) 117 | module-active? @(rf/subscribe [:module-active?]) 118 | current-module @(rf/subscribe [:current-module])] 119 | (if module-active? 120 | [:div.ui.one.column.grid 121 | [:div.one.column.row 122 | [:div.column 123 | [:div.ui.top.attached.tabular.menu 124 | (for [causality causalities] 125 | ^{:key (str "tab-" causality)} 126 | [:a.item {:data-tab causality 127 | :class (when (= causality active) "active") 128 | :href (k/path-for [:module {:module current-module :causality causality}])} 129 | causality]) 130 | [pages-menu]] 131 | (for [causality causalities] 132 | ^{:key (str "tab-content-" causality)} 133 | [tab-content causality])]]] 134 | [:div.ui.active.centered.inline.text.massive.loader 135 | {:style {:margin-top "20%"}} 136 | "Loading"]))) 137 | 138 | (defn- simulation-status-header-text [simulation-has-loaded?] 139 | (if simulation-has-loaded? "Simulation status" "Simulation setup")) 140 | 141 | (defn sidebar [] 142 | (let [module-routes (rf/subscribe [:module-routes]) 143 | route (rf/subscribe [:kee-frame/route]) 144 | loaded? (rf/subscribe [:loaded?]) 145 | trend-info (rf/subscribe [:trend-info]) 146 | active-trend-index (rf/subscribe [:active-trend-index]) 147 | scenarios (rf/subscribe [:scenarios]) 148 | plot-config-changed? (rf/subscribe [:plot-config-changed?]) 149 | scenario-percent (rf/subscribe [:scenario-percent])] 150 | (fn [] 151 | (let [module-routes (sort-by :name @module-routes) 152 | route-name (-> @route :data :name) 153 | route-module (-> @route :path-params :module)] 154 | [:div.ui.secondary.vertical.fluid.menu 155 | [:div.item 156 | [:a.header {:href (k/path-for [:index]) 157 | :class (when (= route-name :index) "active")} 158 | (simulation-status-header-text @loaded?)]] 159 | (when @loaded? 160 | [:div.item 161 | [:div.header "Plots"] 162 | [:div.menu 163 | (doall 164 | (map (fn [{:keys [index label count plot-type]}] 165 | [:div.item {:key label} 166 | [:a.itemstyle {:class (when (and (= index (int @active-trend-index)) (= route-name :trend)) "active") 167 | :href (k/path-for [:trend {:index index}])} 168 | (str (inc index) ") " (trend/plot-type-from-label label))] 169 | (let [display-number (if (= plot-type "trend") count (int (/ count 2)))] 170 | [:div.ui.teal.left.pointing.label display-number]) 171 | [:span {:style {:float 'right :cursor 'pointer :z-index 1000} 172 | :data-tooltip "Remove plot" 173 | :data-position "top center"} 174 | [:i.trash.gray.icon {:on-click #(rf/dispatch [::controller/removetrend index])}]] 175 | (if (< 0 count) 176 | [:span {:style {:float 'right :cursor 'pointer :z-index 1000} 177 | :data-tooltip "Remove all variables from plot" 178 | :data-position "top center"} 179 | [:i.eye.slash.gray.icon {:on-click #(rf/dispatch [::controller/untrend index])}]])]) 180 | @trend-info)) 181 | [:div.item 182 | [:button.ui.icon.button {:style {:margin-top 5} 183 | :on-click #(rf/dispatch [::controller/new-trend "scatter" (str "XY plot #" (random-uuid))])} 184 | [:i.plus.icon] "XY plot"] 185 | [:button.ui.icon.button {:style {:margin-top 5} 186 | :on-click #(rf/dispatch [::controller/new-trend "trend" (str "Time series #" (random-uuid))])} 187 | [:i.plus.icon] "Time series"] 188 | [:button.ui.icon.button {:style {:margin-top 5} 189 | :disabled (or (empty? @trend-info) 190 | (not @plot-config-changed?)) 191 | :on-click #(rf/dispatch [::controller/save-trends-configuration]) 192 | :title (str "Save the current plots with variables as \"PlotConfig.json\"" 193 | "\nto " @(rf/subscribe [:config-dir]) ".\n" 194 | "This will overwrite any existing PlotConfig.json file in this directory.")} 195 | [:i.download.icon] "Save plots config"]]]]) 196 | (when (and @loaded? 197 | (seq @scenarios)) 198 | [:div.item 199 | [:a.header 200 | {:href (k/path-for [:scenarios]) 201 | :class (when (= route-name :scenarios) "active")} 202 | "Scenarios"] 203 | [:div.menu 204 | (doall 205 | (map (fn [{:keys [id running?]}] 206 | [:a.item {:class (when (= (-> @route :path-params :id) id) "active") 207 | :key id 208 | :href (k/path-for [:scenario {:id id}])} (scenario/scenario-filename-to-name id) 209 | (when running? [:div {:style {:display 'inline-block :float 'right}} (str @scenario-percent "%") 210 | [:i.green.play.icon]])]) 211 | @scenarios))]]) 212 | [:div.ui.divider] 213 | (when @loaded? 214 | [:div.item 215 | [:div.header "Models"] 216 | [:div.menu 217 | (map (fn [{:keys [name causality]}] 218 | [:a.item {:class (when (= route-module name) "active") 219 | :key name 220 | :href (k/path-for [:module {:module name :causality causality}])} name]) 221 | module-routes)]])])))) 222 | 223 | (defn realtime-button [] 224 | (if @(rf/subscribe [:realtime?]) 225 | [:button.ui.button 226 | {:on-click #(rf/dispatch [::controller/disable-realtime]) 227 | :data-tooltip "Execute simulation as fast as possible"} 228 | "Disable"] 229 | [:button.ui.button 230 | {:on-click #(rf/dispatch [::controller/enable-realtime]) 231 | :data-tooltip "Execute simulation towards real time target"} 232 | "Enable"])) 233 | 234 | (defn realtime-factor-field [] 235 | [c/text-editor 236 | @(rf/subscribe [:get-state-key :realTimeFactorTarget]) 237 | [::controller/set-real-time-factor-target] 238 | "Edit real time target"]) 239 | 240 | (defn steps-to-monitor-field [] 241 | [c/text-editor 242 | @(rf/subscribe [:get-state-key :stepsToMonitor]) 243 | [::controller/set-steps-to-monitor] 244 | "Set number of steps to use in rolling average real time factor calculation. 245 | Increase this value to reduce fluctuations."]) 246 | 247 | (defn teardown-button [] 248 | [:button.ui.button {:on-click #(rf/dispatch [::controller/teardown]) 249 | :disabled (not (= @(rf/subscribe [:status]) "pause")) 250 | :data-tooltip "Tear down current simulation, allowing for a simulation restart. Simulation must be paused."} 251 | "Tear down"]) 252 | 253 | (defn command-feedback-message [] 254 | (when-let [{:keys [command message success]} @(rf/subscribe [:feedback-message])] 255 | [:div.ui.message 256 | {:class (if success "positive" "negative") 257 | :style {:position :absolute 258 | :bottom 20 259 | :right 20}} 260 | [:i.close.icon {:on-click #(rf/dispatch [::controller/close-feedback-message])}] 261 | [:div.header (if success 262 | "Command success" 263 | "Command failure")] 264 | [:p (str "Command: " command)] 265 | (when-not (str/blank? message) 266 | [:p (str "Message: " message)])])) 267 | 268 | (defn dashboard [] 269 | [:div 270 | [:table.ui.basic.table.definition 271 | [:tbody 272 | (for [[k v] @(rf/subscribe [:overview])] 273 | ^{:key k} 274 | [:tr 275 | [:td k] 276 | [:td v]])]] 277 | [:h3 "Controls"] 278 | [:table.ui.basic.table.definition 279 | [:tbody 280 | [:tr [:td "Real time target"] [:td [realtime-button]]] 281 | [:tr [:td "Real time factor target value"] [:td [realtime-factor-field]]] 282 | [:tr [:td "Number of steps for rolling average"] [:td [steps-to-monitor-field]]] 283 | [:tr [:td "Simulation execution"] [:td [teardown-button]]]]]]) 284 | 285 | (defn index-page [] 286 | (let [loading? (rf/subscribe [:loading?]) 287 | loaded? (rf/subscribe [:loaded?]) 288 | prev-paths (rf/subscribe [:prev-paths]) 289 | load-dir (r/atom default-load-dir) 290 | log-dir (r/atom default-log-dir)] 291 | (fn [] 292 | (if @loaded? 293 | [dashboard] 294 | [:div.ui.two.column.grid 295 | [:div.two.column.row 296 | [:div.column 297 | [:div.ui.fluid.right.labeled.input 298 | {:data-tooltip "Specify the path to: an OspSystemStructure.xml, a SystemStructure.ssd, a directory containing either, or a directory containing FMUs"} 299 | [:input {:style {:min-width "400px"} 300 | :type :text 301 | :placeholder "/path/to/configuration..." 302 | :value @load-dir 303 | :on-change #(reset! load-dir (-> % .-target .-value))}] 304 | [:div.ui.label "Configuration"]]]] 305 | [:div.two.column.row 306 | [:div.column 307 | [:div.ui.fluid.right.labeled.input {:data-tooltip "[Optional] Specify a directory where output log files will be stored"} 308 | [:input {:style {:min-width "400px"} 309 | :type :text 310 | :placeholder "/path/to/logs... (optional)" 311 | :value @log-dir 312 | :on-change #(reset! log-dir (-> % .-target .-value))}] 313 | [:div.ui.label "Log output"]]]] 314 | [:div.two.column.row 315 | [:div.column 316 | [:button.ui.button.right.floated {:disabled (or (empty? @load-dir) @loading?) 317 | :class (when @loading? "loading") 318 | :on-click #(rf/dispatch [::controller/load @load-dir @log-dir])} "Load simulation"]]] 319 | [:div.two.column.row 320 | [:div.column 321 | [:h3 "Previously used configurations"] 322 | [:div.ui.relaxed.divided.list 323 | (map (fn [path] 324 | [:div.item {:key path} 325 | [:i.large.folder.open.middle.aligned.icon] 326 | [:div.content 327 | [:a.header {:on-click #(reset! load-dir path)} 328 | path] 329 | [:div.description 330 | [:a.right.floated {:on-click #(rf/dispatch [::controller/delete-prev path])} 331 | [:i.delete.icon]]]]]) 332 | @prev-paths)]]]])))) 333 | 334 | (defn- scenario-header [file-name] 335 | (let [name (scenario/scenario-filename-to-name file-name)] 336 | [:div.row name 337 | [:span.additional (str "scenario data from ") file-name]])) 338 | 339 | (defn root-comp [] 340 | (let [socket-state (rf/subscribe [:kee-frame.websocket/state socket-url]) 341 | loaded? (rf/subscribe [:loaded?]) 342 | load-dir (rf/subscribe [:loaded-dir]) 343 | log-dir (rf/subscribe [:log-dir]) 344 | status (rf/subscribe [:status]) 345 | module (rf/subscribe [:current-module]) 346 | trends (rf/subscribe [:trend-info]) 347 | active-trend-index (rf/subscribe [:active-trend-index]) 348 | scenario-name (rf/subscribe [:scenario-id]) 349 | error (rf/subscribe [:error]) 350 | error-dismissed (rf/subscribe [:error-dismissed]) 351 | lib-version (rf/subscribe [:lib-version])] 352 | (fn [] 353 | [:div 354 | [:div.ui.inverted.huge.borderless.fixed.menu 355 | [:a.header.item {:href "/"} "Cosim Demo App"] 356 | [:div.right.menu 357 | (when (= :disconnected (:state @socket-state)) 358 | [:div.item 359 | [:div "Lost server connection!"]]) 360 | (when @error 361 | [:a.item {:on-click #(rf/dispatch [::controller/toggle-dismiss-error])} "Error!"]) 362 | (when @loaded? 363 | [:div.item 364 | [:div {:data-tooltip "Rolling average / total average" 365 | :data-position "bottom center"} 366 | "RTF: " @(rf/subscribe [:real-time-factor])]]) 367 | (when @loaded? 368 | [:div.item 369 | [:div "Time: " @(rf/subscribe [:time])]]) 370 | (when (and @loaded? (= @status "pause")) 371 | [:a.item {:on-click #(rf/dispatch [::controller/play])} "Play"]) 372 | (when (and @loaded? (= @status "play")) 373 | [:a.item {:on-click #(rf/dispatch [::controller/pause])} "Pause"]) 374 | (when @loaded? 375 | [:a.item {:on-click #(rf/dispatch [::controller/reset @load-dir @log-dir])} "Reset"]) 376 | [:div.ui.simple.dropdown.item 377 | [:i.question.circle.icon] 378 | [:div.menu 379 | [:a.item {:href "https://open-simulation-platform.github.io/cosim-demo-app/user-guide" :target "_blank"} [:i.file.alternate.icon] "User guide"] 380 | [:a.item {:href "https://github.com/open-simulation-platform/cosim-demo-app/issues" :target "_blank"} [:i.icon.edit] "Report an issue"] 381 | [:a.item {:href "http://open-simulation-platform.com" :target "_blank"} [:i.icon.linkify] "OSP site"] 382 | [:a.item {:on-click #(rf/dispatch [::controller/toggle-show-success-feedback-messages])} 383 | (if @(rf/subscribe [:show-success-feedback-messages]) [:i.toggle.on.icon.green] [:i.toggle.off.icon]) 384 | "Show success command feedback"] 385 | (when (:libcosim @lib-version) 386 | [:span.item [:i.icon.info.circle] (str "LibCosim version: " (:libcosim @lib-version))]) 387 | (when (:libcosimc @lib-version) 388 | [:span.item [:i.icon.info.circle] (str "LibCosimC version: " (:libcosimc @lib-version))])]]]] 389 | [:div.ui.grid 390 | [:div.row 391 | [:div#sidebar.column 392 | [sidebar]] 393 | [:div#content.column 394 | [:div.ui.grid 395 | [:div.row 396 | [:h1.ui.huge.header [k/switch-route (comp :name :data) 397 | :module (or @module "") 398 | :trend (if (and (number? (int @active-trend-index)) (not-empty @trends)) 399 | (trend/plot-type-from-label (:label (nth @trends (int @active-trend-index)))) 400 | "") 401 | :index (simulation-status-header-text @loaded?) 402 | :scenarios "Scenarios" 403 | :scenario (scenario-header @scenario-name) 404 | nil [:div "Loading..."]]]] 405 | [:div.ui.divider] 406 | [:div.row 407 | [k/switch-route (comp :name :data) 408 | :trend [trend/trend-outer] 409 | :module [module-listing] 410 | :index [index-page] 411 | :scenarios [scenario/overview] 412 | :scenario [scenario/one] 413 | nil [:div "Loading..."]]]]]]] 414 | (if (not= :connected (:state @socket-state)) 415 | [:div.ui.page.dimmer.transition.active 416 | {:style {:display :flex}} 417 | [:div.content 418 | [:div.center 419 | [:h2.ui.inverted.icon.header 420 | [:i.heartbeat.icon] 421 | "Lost server connection!"] 422 | [:div.sub.header "It looks like the server is down. Try restarting the server and hit F5"]]]] 423 | (when (and @error (not @error-dismissed)) 424 | [:div.ui.page.dimmer.transition.active 425 | {:style {:display :flex}} 426 | [:div.content 427 | [:div.center 428 | [:h2.ui.inverted.icon.header 429 | [:i.red.bug.icon] 430 | "An error occured!"] 431 | [:h3 (str "Last error code: " (:last-error-code @error))] 432 | [:h3 (str "Last error message: " (:last-error-message @error))] 433 | [:h4 "The simulation can no longer continue. Please restart the application."] 434 | [:button.ui.button.inverted {:on-click #(rf/dispatch [::controller/toggle-dismiss-error])} "Dismiss"]]]])) 435 | [command-feedback-message]]))) 436 | -------------------------------------------------------------------------------- /structs/structs.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | package structs 6 | 7 | type Signal struct { 8 | Name string `json:"name"` 9 | Causality string `json:"causality"` 10 | Type string `json:"type"` 11 | Value interface{} `json:"value"` 12 | } 13 | 14 | type Module struct { 15 | Signals []Signal `json:"signals,omitempty"` 16 | Name string `json:"name,omitempty"` 17 | } 18 | 19 | type ManipulatedVariable struct { 20 | SlaveIndex int `json:"slaveIndex"` 21 | Type string `json:"type"` 22 | ValueReference int `json:"valueReference"` 23 | } 24 | 25 | type JsonResponse struct { 26 | Loading bool `json:"loading"` 27 | Loaded bool `json:"loaded"` 28 | ExecutionState string `json:"executionState"` 29 | LastErrorCode string `json:"lastErrorCode"` 30 | LastErrorMessage string `json:"lastErrorMessage"` 31 | SimulationTime float64 `json:"time"` 32 | TotalAverageRealTimeFactor float64 `json:"totalAverageRealTimeFactor"` 33 | RollingAverageRealTimeFactor float64 `json:"rollingAverageRealTimeFactor"` 34 | RealTimeFactorTarget float64 `json:"realTimeFactorTarget"` 35 | IsRealTimeSimulation bool `json:"isRealTime"` 36 | StepsToMonitor int `json:"stepsToMonitor"` 37 | ConfigDir string `json:"configDir,omitempty"` 38 | LibVersion Versions `json:"libVersion,omitempty"` 39 | Status string `json:"status,omitempty"` 40 | Module Module `json:"module,omitempty"` 41 | Trends []Trend `json:"trends"` 42 | ModuleData *MetaData `json:"module-data,omitempty"` 43 | Feedback *CommandFeedback `json:"feedback,omitempy"` 44 | Scenarios *[]string `json:"scenarios,omitempty"` 45 | Scenario *interface{} `json:"scenario,omitempty"` 46 | RunningScenario string `json:"running-scenario"` 47 | ManipulatedVariables []ManipulatedVariable `json:"manipulatedVariables"` 48 | } 49 | 50 | type TrendSignal struct { 51 | Module string `json:"module"` 52 | SlaveIndex int `json:"slave-index"` 53 | Signal string `json:"signal"` 54 | Causality string `json:"causality"` 55 | Type string `json:"type"` 56 | ValueReference int `json:"value-reference"` 57 | TrendXValues []float64 `json:"xvals,omitempty"` 58 | TrendYValues []float64 `json:"yvals,omitempty"` 59 | } 60 | 61 | type Trend struct { 62 | Id int `json:"id"` 63 | PlotType string `json:"plot-type"` 64 | Label string `json:"label"` 65 | TrendSignals []TrendSignal `json:"trend-values"` 66 | Spec TrendSpec `json:"spec"` 67 | } 68 | 69 | type TrendSpec struct { 70 | Begin float64 `json:"begin"` 71 | End float64 `json:"end"` 72 | Range float64 `json:"range"` 73 | Auto bool `json:"auto"` 74 | } 75 | 76 | type ShortLivedData struct { 77 | Scenarios *[]string 78 | Scenario *interface{} 79 | ModuleData *MetaData 80 | } 81 | 82 | type SimulationStatus struct { 83 | Loading bool 84 | Loaded bool 85 | ConfigDir string 86 | LibVersion Versions 87 | Module string 88 | SignalSubscriptions []Variable 89 | Trends []Trend 90 | Status string 91 | CurrentScenario string 92 | ActiveTrend int 93 | } 94 | 95 | type Variable struct { 96 | Name string `json:"name"` 97 | ValueReference int `json:"value-reference"` 98 | Causality string `json:"causality"` 99 | Variability string `json:"variability"` 100 | Type string `json:"type"` 101 | } 102 | 103 | type FMU struct { 104 | Name string `json:"name"` 105 | ExecutionIndex int `json:"index"` 106 | Variables []Variable `json:"variables"` 107 | } 108 | 109 | type MetaData struct { 110 | FMUs []FMU `json:"fmus"` 111 | } 112 | 113 | type CommandFeedback struct { 114 | Success bool `json:"success"` 115 | Message string `json:"message"` 116 | Command string `json:"command"` 117 | } 118 | 119 | type PlotVariable struct { 120 | Simulator string `json:"simulator"` 121 | Variable string `json:"variable"` 122 | } 123 | 124 | type Plot struct { 125 | Label string `json:"label"` 126 | PlotType string `json:"plotType"` 127 | PlotVariables []PlotVariable `json:"variables"` 128 | } 129 | 130 | type PlotConfig struct { 131 | Plots []Plot `json:"plots"` 132 | } 133 | 134 | type Versions struct { 135 | LibVer string `json:"libcosim"` 136 | LibcVer string `json:"libcosimc"` 137 | } 138 | -------------------------------------------------------------------------------- /test/client/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns client.core-test 2 | (:require 3 | [cljs.test :refer-macros [deftest is testing]] 4 | [client.core :as core])) 5 | 6 | (deftest root-component-test 7 | (is (= :div (first (core/root-comp))))) 8 | 9 | (deftest ws-request 10 | (is (= "play" (:command (core/ws-request "play"))))) --------------------------------------------------------------------------------