├── .gitignore ├── .run ├── Attach to Pi Debugger.run.xml ├── Debug on Pi.run.xml ├── Restart on Pi.run.xml ├── Run Local.run.xml └── Run on Pi.run.xml ├── LICENSE ├── README.md ├── README_DE.md ├── assets ├── FHNW.png ├── mvc-concept.png ├── mvc-interaction.png ├── wiring.fzz └── wiring_bb.png ├── pom.xml └── src ├── assembly ├── assembly.xml ├── download_openjfx.sh ├── restart.sh ├── start.sh └── startInDebugMode.sh ├── main ├── java │ ├── com │ │ └── pi4j │ │ │ ├── catalog │ │ │ └── components │ │ │ │ ├── SimpleButton.java │ │ │ │ ├── SimpleLed.java │ │ │ │ └── base │ │ │ │ ├── Component.java │ │ │ │ ├── DigitalActuator.java │ │ │ │ ├── DigitalSensor.java │ │ │ │ ├── I2CDevice.java │ │ │ │ ├── PIN.java │ │ │ │ ├── PwmActuator.java │ │ │ │ ├── SerialDevice.java │ │ │ │ └── SpiDevice.java │ │ │ ├── mvc │ │ │ ├── multicontrollerapp │ │ │ │ ├── AppStarter.java │ │ │ │ ├── controller │ │ │ │ │ ├── ApplicationController.java │ │ │ │ │ ├── CounterController.java │ │ │ │ │ └── LEDController.java │ │ │ │ ├── model │ │ │ │ │ └── ExampleModel.java │ │ │ │ └── view │ │ │ │ │ ├── gui │ │ │ │ │ ├── ExampleGUI.java │ │ │ │ │ └── ExamplePuiEmulator.java │ │ │ │ │ └── pui │ │ │ │ │ └── ExamplePUI.java │ │ │ ├── templateapp │ │ │ │ ├── AppStarter.java │ │ │ │ ├── controller │ │ │ │ │ └── SomeController.java │ │ │ │ ├── model │ │ │ │ │ └── SomeModel.java │ │ │ │ └── view │ │ │ │ │ ├── gui │ │ │ │ │ ├── SomeGUI.java │ │ │ │ │ └── SomePuiEmulator.java │ │ │ │ │ └── pui │ │ │ │ │ └── SomePUI.java │ │ │ ├── templatepuiapp │ │ │ │ ├── AppStarter.java │ │ │ │ ├── controller │ │ │ │ │ └── SomeController.java │ │ │ │ ├── model │ │ │ │ │ └── SomeModel.java │ │ │ │ └── view │ │ │ │ │ └── SomePUI.java │ │ │ └── util │ │ │ │ └── mvcbase │ │ │ │ ├── ConcurrentTaskQueue.java │ │ │ │ ├── ControllerBase.java │ │ │ │ ├── MvcLogger.java │ │ │ │ ├── ObservableArray.java │ │ │ │ ├── ObservableValue.java │ │ │ │ ├── Projector.java │ │ │ │ ├── PuiBase.java │ │ │ │ └── ViewMixin.java │ │ │ └── setup │ │ │ └── HelloFX.java │ └── module-info.java └── resources │ ├── fonts │ ├── Lato │ │ ├── Lato-Bla.ttf │ │ ├── Lato-Bol.ttf │ │ ├── Lato-Hai.ttf │ │ ├── Lato-Lig.ttf │ │ └── Lato-Reg.ttf │ └── fontawesome-webfont.ttf │ ├── mvc │ ├── multicontrollerapp │ │ └── style.css │ └── templateapp │ │ └── style.css │ └── setup │ ├── openduke.png │ └── style.css └── test └── java └── com └── pi4j ├── catalog ├── ComponentTest.java └── components │ ├── SimpleButtonTest.java │ └── SimpleLedTest.java └── mvc ├── multicontrollerapp ├── controller │ └── ApplicationControllerTest.java ├── model │ └── ExampleModelTest.java └── view │ └── pui │ └── ExamplePUITest.java ├── templateapp ├── controller │ └── SomeControllerTest.java ├── model │ └── SomeModelTest.java └── view │ └── pui │ └── SomePUITest.java ├── templatepuiapp └── controller │ └── SomeControllerTest.java └── util └── mvcbase ├── ConcurrentTaskQueueTest.java ├── ControllerBaseTest.java ├── ObservableArrayTest.java └── ObservableValueTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven 2 | /target/ 3 | 4 | # Eclipse 5 | .classpath 6 | .factorypath 7 | .project 8 | .settings 9 | 10 | # IntelliJ 11 | /.idea/ 12 | *.iml 13 | 14 | # FXGL 15 | 16 | /system/ 17 | /logs/ 18 | -------------------------------------------------------------------------------- /.run/Attach to Pi Debugger.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /.run/Debug on Pi.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.run/Restart on Pi.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.run/Run Local.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.run/Run on Pi.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![FHNW](assets/FHNW.png) 2 | 3 | [Deutsche Beschreibung ist hier.](README_DE.md) 4 | 5 | # Pi4J Applications with JavaFX based GUI 6 | 7 | [![Contributors](https://img.shields.io/github/contributors/Pi4J/pi4j-template-javafx)](https://github.com/Pi4J/pi4j-template-javafx/graphs/contributors) 8 | [![License](https://img.shields.io/github/license/Pi4J/pi4j-template-javafx)](https://github.com/Pi4J/pi4j-template-javafx/blob/master/LICENSE) 9 | 10 | This template project contains descriptions and example code how to combine a [JavaFX](https://openjfx.io)-based Graphical-User-Interface (GUI) with sensors and actuators that are attached to the Raspberry Pi by using the [Pi4J-Library](https://www.pi4j.com). 11 | 12 | This repository should not be cloned directly. It is a template project and one should create their own project by using the `Use this template` Button. 13 | 14 | ## Prepare Raspberry Pi and Developer Laptop 15 | 16 | Please make sure that your Raspberry Pi and your developer laptop are prepared as described in the [Hello Pi5 Projekt](https://gitlab.fhnw.ch/ip_12_preparation/hellopi5.git). 17 | 18 | ## Development process 19 | The recommended development process is also described in the [Hello Pi5 Projekt](https://gitlab.fhnw.ch/ip_12_preparation/hellopi5.git). 20 | 21 | Please read the chapters _Entwicklungsprozess_ and _Applikation im Debugger starten_. 22 | 23 | ## The example applications 24 | 25 | #### HelloFX 26 | 27 | Used only to test if the [JavaFX](https://openjfx.io) libraries are installed correctly. Should not be used as a template for one's own JavaFX applications. 28 | 29 | To start: 30 | 31 | - Set `launcher.class` in `pom.xml`: 32 | - `com.pi4j.setup.HelloFX` 33 | - With `Run Local` starts locally on the developer computer 34 | - With `Run on Pi` starts remotely on the Raspberry Pi 35 | 36 | Once the JavaFX setup has been tested, `HelloFX` can be deleted. 37 | 38 | #### Wiring 39 | 40 | The other example applications use a LED and a button. These must be wired as is shown in the following diagram: 41 | 42 | ![Wiring](assets/wiring_bb.png) 43 | 44 | 45 | #### TemplateApp 46 | 47 | This application shows the interaction between a [JavaFX](https://openjfx.io) based Graphical User Interface (GUI) and the Raspberry Pi connected sensors and actuators, the Physical User Interface (PUI). 48 | 49 | This application is to be used as a template for one's own applications. This includes the existing test cases. 50 | 51 | You should first get to know and understand the example. For your own applications, you should then copy the TemplateApp and modify it for your project, however, without violating the rules of the MVC concept, which is described below. 52 | 53 | To start: 54 | 55 | - Set `launcher.class` in `pom.xml`: 56 | - `com.pi4j.mvc.templateapp.AppStarter` 57 | - With `Run Local` (or directly from the IDE) starts locally on the development computer. Useful for GUI development. The PUI is not available on the local computer. The GUI can largely be developed without the need for a Raspberry Pi. 58 | - in `AppStarter` a simple `PuiEmulator` can be started, so that the interaction between GUI and PUI can also be tested on the local development computer. 59 | - With `Run on Pi` starts remotely on the Raspberry Pi (now including the PUI) 60 | 61 | #### TemplatePUIApp 62 | 63 | The MVC concept should also be used for applications without a GUI. 64 | 65 | When developing PUI only applications, or when adding the GUI later, then one should use the `TemplatePUIApp` as template. 66 | 67 | To start: 68 | 69 | - Set `launcher.class` in `pom.xml`: 70 | - `com.pi4j.mvc.templatepuiapp.AppStarter` 71 | - `Run Local` makes no sense for PUI only applications 72 | - With `Run on Pi` starts remotely on the Raspberry Pi 73 | 74 | 75 | ## The MVC concept 76 | 77 | The classic Model-View-Controller concept contains in addition to the starter class at least 3 more classes. The interaction is clearly defined: 78 | 79 | ![MVC Concept](assets/mvc-concept.png) 80 | 81 | - _Model classes_ 82 | 83 | - contain the complete state which is to be visualized, thus these classes are called _Presentation-Model_ 84 | - are completely separate to the Controller and View classes, i.e. they may not interact with those classes 85 | - _Controller classes_ 86 | 87 | - define the entire functionality, i.e. the so-called actions, in the form of methods 88 | - manage the model classes by definition of the business logic 89 | - have no access to the view classes 90 | - _View classes_ 91 | 92 | - only calls methods on the controller, i.e. triggering actions 93 | - are notified of the model of state changes 94 | - observes the state of the model 95 | - never change the model directly 96 | - _Starter class._ 97 | 98 | - Is a subclass of `javafx.application.Application`. Instantiates the three other classes and starts the application. 99 | 100 | In our case at least two view classes exist: 101 | 102 | - _GUI class._ The Graphical-User-Interface. [JavaFX](https://openjfx.io) based implementation of the visualization of the UI on the screen. 103 | - _PUI class._ The Physical-User-Interface. Pi4J based implementation of the sensors and actors. Uses `Component` classes, as is used in [Pi4J Example Components](https://github.com/Pi4J/pi4j-example-components.git). 104 | 105 | GUI and PUI are completely separated from each other, i.e., a GUI button to turn an LED on has no direct access to the LED component of the PUI. Instead, the GUI button triggers a corresponding action in the controller which then sets the `on` state property in the model. The PUI listening on this state then turns the actual LED on or off. 106 | 107 | GUI and PUI work with the same identical controller and thus also the same identical model. 108 | 109 | It is important that one understands this concept and then apply the concepts to one's own project. Should you have questions, contact the Pi4j team. 110 | 111 | In the MVC concept, every user interaction traverses the exact same cycle: 112 | 113 | ![MVC Concept](assets/mvc-interaction.png) 114 | 115 | #### Projector Pattern 116 | 117 | The view classes, i.e. GUI and PUI, implement the Projector-Pattern published by Prof. Dierk König [Projector Pattern](https://dierk.github.io/Home/projectorPattern/ProjectorPattern.html). 118 | 119 | The basic tasks of the GUI and PUI are the same. When looking at the code, this is visible: 120 | they implement the common interface `Projector` and can thus be used in the same way. 121 | 122 | Consequences of this design: 123 | 124 | - Additional UIs can be added, without having to change existing classes, except for the starter class 125 | - An example for this is the `PuiEmulator`, which can be started when necessary. 126 | - This architecture is also useful for 127 | - GUI only applications and 128 | - PUI only applications (see `TemplatePUIApp`). 129 | 130 | ### Implementing the MVC concept 131 | 132 | The base classes, required by the MVC concept, are in the package `com.pi4j.mvc.util.mvcbase`. The classes have extensive documentation. 133 | 134 | ## MultiControllerApp 135 | 136 | A more advanced example is the `MultiControllerApp`. It shows the usage and relevancy of multiple controllers in an application. 137 | 138 | For any controller, the following is imperative: 139 | 140 | - each action is asynchronous and follows the sequence of actions explicitly 141 | - for this each controller uses its own `ConcurrentTaskQueue` 142 | - the UI is thus never blocked by an action 143 | - if a UI triggers additional actions when an action is in execution, there this action is stored in the `ConcurrentTaskQueue` and executed after the current action has completed. 144 | 145 | For simple applications, a single controller will suffice. 146 | 147 | There are situations where actions are to be executed in parallel. 148 | 149 | The `MultiControllerApp` shows such an example. It should be possible to change the counter, _while an LED is blinking_. 150 | 151 | - With a single controller, this would not be accomplishable. The controller would execute the `Decrease-Action` only after the `Blink-Action` is complete. 152 | - With two controllers this is simple: `LedController` and `CounterController` each have a `ConcurrentTaskQueue`. Actions which concern the LED are thus executed independent of actions which modify the counter. 153 | - An `ApplicationController` is implemented to coordinate the other controllers, thus giving the UI only a singly visible API. 154 | 155 | To start: 156 | 157 | - Set `launcher.class` in `pom.xml`: 158 | - `com.pi4j.mvc.multicontrollerapp.AppStarter` 159 | - With `Run Local` (or directly from the IDE) starts locally on the development computer 160 | - A rudimentary `PuiEmulator` can be started in `AppStarter`, to test the interaction of the GUI and PUI. 161 | - With `Run on Pi` starts remotely on the Raspberry Pi 162 | 163 | ## JUnit Tests 164 | 165 | Through the clear separation in model, view and controller, testing of large parts of the application can be automated. These tests are usually executed on the local development computer, i.e. not on the Raspberry Pi. 166 | 167 | #### Controller Tests 168 | 169 | The controller implements the entirety of the base functionality. It should be validated with extensive test cases. 170 | 171 | It should be pointed out, that all changes to the model are performed asynchronously, thus validation can only be done after the asynchronous Tasks are completed. 172 | 173 | An example can be seen in `SomeControllerTest`. 174 | 175 | #### Presentation-Model Tests 176 | 177 | The model is simply a collection of `ObservableValues` and doesn't offer any additional functionality, thus it does not require any additional test cases. 178 | 179 | #### Tests for individual PUI-Components 180 | 181 | The individual PUI-components can be tested easily using the Pi4J integrated `MockPlatform`. These tests can be executed locally on the development computer. A Raspberry Pi is not needed. 182 | 183 | 184 | #### PUI Tests 185 | 186 | The PUI can also be tested quite well with JUnit tests. 187 | 188 | It should be pointed out that the actions are again executed asynchronously. 189 | 190 | An example is the `SomePUITest`. 191 | 192 | ## LICENSE 193 | 194 | This repository is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the 195 | License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, 198 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and 199 | limitations under the License. 200 | -------------------------------------------------------------------------------- /README_DE.md: -------------------------------------------------------------------------------- 1 | 2 | ![FHNW](assets/FHNW.png) 3 | 4 | # Pi4J Applikationen mit JavaFX-basiertem GUI 5 | 6 | [![Contributors](https://img.shields.io/github/contributors/DieterHolz/RaspPiFX-Template-Project)](https://github.com/DieterHolz/RaspPiFX-Template-Project/graphs/contributors) 7 | [![License](https://img.shields.io/github/license/DieterHolz/RaspPiFX-Template-Project)](https://github.com/DieterHolz/RaspPiFX-Template-Project/blob/master/LICENSE) 8 | 9 | 10 | Dieses Template Projekt wird für die Programmierausbildung in den IP12-Projekten an der [Fachhochschule Nordwestschweiz](https://www.fhnw.ch/en/degree-programmes/engineering/icompetence) (FHNW) eingesetzt. 11 | 12 | Es enthält Beschreibungen und Beispiele wie ein [JavaFX](https://openjfx.io)-basiertes Graphical-User-Interface (GUI) mit mittels der [Pi4J-Library](https://www.pi4j.com) an den Raspberry Pi angeschlossenen Aktoren und Sensoren kombiniert werden können. 13 | 14 | Insbesondere sind Template-Projekte enthalten, die als Startpunkt für eigene Projekte dienen. 15 | 16 | 17 | ## Setup von Raspberry Pi und Entwickler-Laptop 18 | 19 | Bitte stellen Sie sicher, dass ihr Laptop und der von Ihnen verwendete Raspberry Pi 5 wie im [Hello Pi5 Projekt](https://gitlab.fhnw.ch/ip_12_preparation/hellopi5.git) beschrieben vorbereitet ist. 20 | 21 | 22 | ## Entwicklungsprozess 23 | 24 | Unser Entwicklungsprozess für die IP12-Projekte ist ebenfalls im Projekt [Hello Pi5 Projekt](https://gitlab.fhnw.ch/ip_12_preparation/hellopi5.git) beschrieben. 25 | 26 | Lesen Sie insbesondere die Kapitel _Entwicklungsprozess_ und _Applikation im Debugger starten_. 27 | 28 | 29 | ## Die enthaltenen Beispiel-Programme 30 | 31 | #### HelloFX 32 | Dient ausschliesslich der Überprüfung der [JavaFX](https://openjfx.io)-Basis-Installation. Auf keinen Fall als Vorlage für die eigenen [JavaFX](https://openjfx.io)-Applikationen verwenden. 33 | 34 | Zum Starten: 35 | - `launcher.class` im `pom.xml` auswählen 36 | - `com.pi4j.setup.HelloFX` 37 | - mit `Run Local` auf dem Laptop starten 38 | - mit `Run on Pi` auf dem RaspPi starten 39 | 40 | Sobald der JavaFX-Setup überprüft ist, kann `HelloFX` gelöscht werden. 41 | 42 | #### Wiring 43 | Die anderen Beispielprogramme verwenden eine LED und einen Button. Diese müssen folgendermassen verdrahtet werden: 44 | 45 | ![Wiring](assets/wiring_bb.png) 46 | 47 | 48 | #### TemplateApp 49 | 50 | `TemplateApp` zeigt das Zusammenspiel eines [JavaFX](https://openjfx.io)-basierten Graphical-User-Interfaces (GUI) mit an den RaspPi angeschlossenen Sensoren und Aktuatoren, dem Physical-User-Interface (PUI). 51 | 52 | Es dient als Vorlage für Ihre eigene Applikation. Das umfasst auch die enthaltenen TestCases. 53 | 54 | Sie sollten zunächst das Beispiel kennenlernen und verstehen. Für Ihre eigene Applikation sollten Sie anschliessend die `TemplateApp` kopieren und entsprechend abändern, ohne dabei die Grundregeln des MVC-Konzepts zu verletzen (s.u.). 55 | 56 | Zum Starten: 57 | - `launcher.class` im `pom.xml` auswählen 58 | - `com.pi4j.mvc.templateapp.AppStarter` 59 | - mit `Run Local` (oder direkt aus der IDE heraus) auf dem Laptop starten. Sinnvoll für die GUI-Entwicklung. Das PUI steht auf dem Laptop nicht zur Verfügung. Das GUI kann jedoch weitgehend ohne Einsatz des RaspPis entwickelt werden 60 | - in `AppStarter` kann zusätzlich noch ein rudimentärer PuiEmulator gestartet werden, so dass das Zusammenspiel zwischen GUI und PUI auch auf dem Laptop überprüft werden kann. 61 | - mit `Run on Pi` auf dem RaspPi starten (jetzt natürlich inklusive "echtem" PUI) 62 | 63 | 64 | #### TemplatePUIApp 65 | 66 | Das MVC-Konzept sollte auch für Applikationen ohne GUI verwendet werden. 67 | 68 | Falls Sie eine reine PUI-Applikation entwickeln oder erst später ein GUI hinzufügen wollen, sollten Sie die `TemplatePUIApp` als Vorlage nehmen. 69 | 70 | Zum Starten: 71 | - `launcher.class` im `pom.xml` auswählen 72 | - `com.pi4j.mvc.templatepuiapp.AppStarter` 73 | - `Run Local` ist bei reinen PUI-Applikationen nicht sinnvoll 74 | - mit `Run on Pi` auf dem RaspPi starten 75 | 76 | 77 | ## Das MVC-Konzept 78 | 79 | Beim klassischen Model-View-Controller-Konzept sind neben der Starter-Klasse mindestens 3 Klassen beteiligt. Das Zusammenspiel dieser Klassen ist klar geregelt: 80 | 81 | ![MVC Concept](assets/mvc-concept.png) 82 | 83 | - _Model Klassen_ 84 | - enthalten den gesamten zu visualisierenden Zustand. Wir nennen diese Klassen daher _Presentation-Model_ 85 | - sind komplett unabhängig von Controller und View 86 | 87 | - _Controller Klassen_ 88 | - stellen die gesamte Funktionalität, die sogenannten Actions, in Form von Methoden zur Verfügung 89 | - verwalten die Model-Klassen gemäss der zugrundeliegenden Business-Logik 90 | - haben keinen Zugriff auf die View-Klassen 91 | 92 | - _View Klassen_ 93 | - rufen ausschliesslich Methoden auf dem Controller auf, sie "triggern Actions" 94 | - werden vom Model über Zustandsänderungen notifiziert 95 | - observieren den Status des Models 96 | - ändern das Model nie direkt 97 | 98 | - _Starter Klasse._ Ist eine Subklasse von `javafx.application.Application`. Instanziiert die drei anderen Klassen und startet die Applikation. 99 | 100 | In unserem Fall gibt es mindestens zwei View-Klassen 101 | 102 | - _GUI Klasse._ Das Graphical-User-Interface. [JavaFX](https://openjfx.io)-basierte Implementierung des auf dem Bildschirm angezeigten UIs. 103 | - _PUI Klasse._ Das Physical-User-Interface. Pi4J-basierte Implementierung der Sensoren und Aktuatoren. Verwendet Component-Klassen, wie Sie sie aus dem sogenannten [Pi4J Component Catalogue](https://github.com/Pi4J/pi4j-example-components.git) kennen. 104 | 105 | GUI und PUI sind komplett voneinander getrennt, z.B. hat der GUI-Button zum Anschalten der LED keinen direkten Zugriff auf die LED-Component des PUIs. Stattdessen triggert der GUI-Button lediglich eine entsprechende Action im Controller, der wiederum die on-Property im Model auf den neuen Wert setzt. In einem separaten Schritt reagiert die LED-Component des PUIs auf diese Wertänderung und schaltet die LED an bzw. aus. 106 | 107 | GUI und PUI arbeiten mit dem identischen Controller und damit auch mit dem identischen Model. 108 | 109 | Es ist wichtig, dass Sie dieses Konzept verstehen und für Ihr Projekt anwenden können. Gehen Sie bei Fragen auf die Fachcoaches oder OOP-Dozierenden zu. 110 | 111 | Jede Benutzer-Interaktion durchläuft im MVC-Konzept den immer gleichen Kreislauf: 112 | 113 | ![MVC Concept](assets/mvc-interaction.png) 114 | 115 | #### Projector Pattern 116 | Unsere View-Klassen, also GUI und PUI, setzen das von Prof. Dierk König veröffentlichte [Projector Pattern](https://dierk.github.io/Home/projectorPattern/ProjectorPattern.html) um. 117 | 118 | Die grundlegenden Aufgaben von GUI und PUI sind gleich. Auf Code-Ebene ist dies erkennbar: 119 | sie implementieren das gemeinsames Interface `Projector`, können also auf die gleiche Weise verwendet werden. 120 | 121 | Weitere Konsequenzen 122 | - Es können weitere UIs hinzugefügt werden, ohne dass es Code-Änderungen bei den bestehenden Klassen (ausser der Starter-Klasse) nach sich zieht. 123 | - Ein Beispiel dafür ist der `PuiEmulator`, der bei Bedarf zusätzlich gestartet werden kann. 124 | - Diese Architektur ist auch geeignet für 125 | - reine GUI-Applikationen und 126 | - reine PUI-Applikationen (siehe `TemplatePUIApp`). 127 | 128 | 129 | ### Implementierung des MVC-Konzepts 130 | 131 | Die Basis-Klassen, die für die Implementierung des MVC-Konzepts notwendig sind, sind im Package `com.pi4j.mvc.util.mvcbase`. Die Klassen sind im Code ausführlich dokumentiert. 132 | 133 | ## MultiControllerApp 134 | Ein etwas fortgeschritteneres Beispiel ist die `MultiControllerApp`. Sie zeigt den Einsatz und die Notwendigkeit von mehreren Controllern in einer Applikation. 135 | 136 | Für einen einzelnen Controller gilt: 137 | - jede Action wird asynchron und reihenfolgetreu ausgeführt 138 | - dafür hat jeder Controller eine eigene `ConcurrentTaskQueue` integriert 139 | - das UI wird dadurch während der Ausführung einer Action _nicht_ blockiert 140 | - werden vom UI weitere Actions getriggert während eine Action gerade in Bearbeitung ist, werden diese in der `ConcurrentTaskQueue` aufgesammelt und ausgeführt, sobald die vorherigen Actions abgearbeitet sind. 141 | 142 | Für einfache Applikationen reicht ein einzelner Controller meist aus. 143 | 144 | Es gibt aber Situationen, bei denen Actions ausgeführt werden sollen, während eine andere Action noch läuft. 145 | 146 | Die `MultiControllerApp` zeigt so ein Beispiel. Es soll möglich sein, den Counter zu verändern _während die LED blinkt_ . 147 | - Mit einem einzigen Controller ist das nicht umsetzbar. Der Controller würde beispielsweise die 'Decrease-Action' erst ausführen, nachdem die 'Blink-Action' abgeschlossen ist. 148 | - Bei zwei Controllern ist es jedoch einfach: `LedController` und `CounterController` haben jeder eine `ConcurrentTaskQueue`. Actions, die die LED betreffen, werden also unabhängig von den Actions, die den Counter verändern, ausgeführt. 149 | - Es sollte zusätzlich ein `ApplicationController` implementiert werden, der die anderen Controller koordiniert und das für das UI sichtbare API zur Verfügung stellt. 150 | 151 | Zum Starten: 152 | - `launcher.class` im `pom.xml` auswählen 153 | - `com.pi4j.mvc.multicontrollerapp.AppStarter` 154 | - mit `Run local` (oder direkt aus der IDE heraus) auf dem Laptop starten. 155 | - in `AppStarter` kann zusätzlich noch ein rudimentärer PuiEmulator gestartet werden, so dass das Zusammenspiel zwischen GUI und PUI auch auf dem Laptop überprüft werden kann. 156 | - mit `Run on Pi` auf dem RaspPi starten 157 | 158 | 159 | ## JUnit Tests 160 | 161 | Durch die klare Trennung in Model, View und Controller können grosse Teile der Applikation mittels einfachen JUnit-Tests automatisiert getestet werden. Diese Tests werden in der Regel auf dem Laptop, also nicht auf dem RaspPi, ausgeführt. 162 | 163 | #### Controller Tests 164 | 165 | Der Controller implementiert die gesamte zur Verfügung stehende Grund-Funktionalität. Er sollte mit ausführlichen TestCases automatisch überprüft werden. 166 | 167 | Dabei gilt es zu beachten, dass der Controller alle Veränderungen auf dem Model asynchron ausführt. Eine Überprüfung der Resultate ist also erst möglich, wenn die asynchrone Task beendet ist. 168 | 169 | Ein Beispiel sehen Sie in `SomeControllerTest`. 170 | 171 | #### Presentation-Model Tests 172 | 173 | Das Model ist lediglich eine Ansammlung von `ObservableValues` und bietet darüber hinaus keine weitere Funktionalität. Daher sind normalerweise auch keine weiteren TestCases notwendig. 174 | 175 | #### Tests für einzelne PUI-Components 176 | 177 | Die einzelnen PUI-Components können sehr gut via der in Pi4J integrierten `MockPlatform` getestet werden. Diese Tests werden auf dem Laptop ausgeführt. Ein RaspPi ist nicht notwendig. 178 | 179 | 180 | #### PUI Tests 181 | Das PUI ihrer Applikation kann ebenfalls gut mittels JUnit getestet werden. 182 | 183 | Auch hier müssen die Tests berücksichtigen, dass die Actions asynchron ausgeführt werden. 184 | 185 | Ein Beispiel ist `SomePUITest`. 186 | 187 | 188 | ## LICENSE 189 | 190 | This repository is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the 191 | License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 192 | 193 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, 194 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and 195 | limitations under the License. 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /assets/FHNW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/assets/FHNW.png -------------------------------------------------------------------------------- /assets/mvc-concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/assets/mvc-concept.png -------------------------------------------------------------------------------- /assets/mvc-interaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/assets/mvc-interaction.png -------------------------------------------------------------------------------- /assets/wiring.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/assets/wiring.fzz -------------------------------------------------------------------------------- /assets/wiring_bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/assets/wiring_bb.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.pi4j.mvc 8 | Raspifx-template 9 | RaspPiFX 10 | Starter and example project how to integrate JavaFX and Pi4J in a single application. 11 | 0.0.1 12 | 13 | 14 | 15 | oss.sonatype.org-snapshot 16 | https://oss.sonatype.org/content/repositories/snapshots 17 | 18 | false 19 | 20 | 21 | true 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | com.pi4j.mvc.templateapp.AppStarter 32 | 33 | 34 | 35 | JFXApp 36 | 37 | 38 | pi4j 39 | 10.175.62.110 40 | 41 | 22 42 | pi 43 | pi4j 44 | /home/pi/deploy 45 | start.sh 46 | restart.sh 47 | startInDebugMode.sh 48 | download_openjfx.sh 49 | 50 | 51 | 24 52 | 53 | 54 | 24 55 | 3.0.1 56 | 2.11.0 57 | 5.12.1 58 | 5.16.1 59 | 60 | 61 | 0.0.8 62 | 63 | 1.10.15 64 | 65 | 66 | 3.4.0 67 | 3.13.0 68 | 3.8.0 69 | 3.4.1 70 | 3.1.3 71 | 3.4.2 72 | 3.10.0 73 | 3.3.1 74 | 3.5.0 75 | 3.1.0 76 | 77 | 78 | UTF-8 79 | ${java.version} 80 | ${java.version} 81 | 82 | 83 | 84 | 85 | 86 | org.openjfx 87 | javafx-graphics 88 | ${javafx.version} 89 | 90 | 91 | org.openjfx 92 | javafx-controls 93 | ${javafx.version} 94 | 95 | 96 | 97 | 98 | org.slf4j 99 | slf4j-api 100 | 2.0.12 101 | 102 | 103 | org.slf4j 104 | slf4j-simple 105 | 2.0.12 106 | 107 | 108 | 109 | com.pi4j 110 | pi4j-core 111 | ${pi4j.version} 112 | 113 | 114 | com.pi4j 115 | pi4j-plugin-gpiod 116 | ${pi4j.version} 117 | 118 | 119 | com.pi4j 120 | pi4j-plugin-linuxfs 121 | ${pi4j.version} 122 | 123 | 124 | 125 | 126 | com.fazecast 127 | jSerialComm 128 | ${jSerialComm.version} 129 | 130 | 131 | 132 | 133 | com.pi4j 134 | pi4j-plugin-mock 135 | ${pi4j.version} 136 | 137 | 138 | 139 | 140 | 141 | org.junit.jupiter 142 | junit-jupiter-engine 143 | ${junit.version} 144 | test 145 | 146 | 147 | org.junit.jupiter 148 | junit-jupiter-params 149 | ${junit.version} 150 | test 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-install-plugin 160 | 161 | 162 | true 163 | 164 | 165 | 166 | org.apache.maven.plugins 167 | maven-compiler-plugin 168 | 169 | ${java.version} 170 | ${java.version} 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-clean-plugin 180 | ${maven-clean-plugin.version} 181 | 182 | 183 | org.apache.maven.plugins 184 | maven-antrun-plugin 185 | ${maven-antrun-plugin.version} 186 | 187 | 188 | org.apache.maven.plugins 189 | maven-compiler-plugin 190 | ${maven-compiler-plugin.version} 191 | 192 | 193 | org.apache.maven.plugins 194 | maven-dependency-plugin 195 | ${maven-dependency-plugin.version} 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-jar-plugin 200 | ${maven-jar-plugin.version} 201 | 202 | 203 | org.apache.maven.plugins 204 | maven-install-plugin 205 | ${maven-install-plugin.version} 206 | 207 | 208 | org.apache.maven.plugins 209 | maven-javadoc-plugin 210 | ${maven-javadoc-plugin.version} 211 | 212 | 213 | org.apache.maven.plugins 214 | maven-resources-plugin 215 | ${maven-resources-plugin.version} 216 | 217 | 218 | org.apache.maven.plugins 219 | maven-surefire-plugin 220 | ${maven-surefire-plugin.version} 221 | 222 | 223 | org.codehaus.mojo 224 | exec-maven-plugin 225 | ${exec-maven-plugin.version} 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | release 234 | 235 | ${jar.name} 236 | 237 | 238 | org.apache.maven.plugins 239 | maven-dependency-plugin 240 | 241 | 242 | copy-dependencies 243 | prepare-package 244 | 245 | copy-dependencies 246 | 247 | 248 | 249 | ${project.build.directory}/libs 250 | 251 | runtime 252 | false 253 | false 254 | true 255 | true 256 | 257 | 258 | 259 | 260 | 261 | org.apache.maven.plugins 262 | maven-jar-plugin 263 | 264 | 265 | 266 | true 267 | libs/ 268 | ${launcher.class} 269 | false 270 | 271 | 272 | 273 | 274 | 275 | maven-assembly-plugin 276 | 277 | false 278 | 279 | src/assembly/assembly.xml 280 | 281 | 282 | 283 | 284 | all 285 | package 286 | 287 | single 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | run-local 299 | 300 | 301 | 302 | org.codehaus.mojo 303 | exec-maven-plugin 304 | 305 | 306 | verify 307 | 308 | exec 309 | 310 | 311 | 312 | 313 | java 314 | --module-path "${project.build.directory}/libs" --add-modules javafx.controls -jar "${project.build.directory}/${jar.name}.jar" 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | run-on-Pi 324 | 325 | 326 | 327 | org.apache.maven.plugins 328 | maven-antrun-plugin 329 | 330 | 331 | transfer 332 | install 333 | 334 | run 335 | 336 | 337 | 338 | 339 | 342 | 343 | 344 | 348 | 349 | 350 | 354 | 355 | 356 | 360 | 364 | 368 | 372 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | org.apache.ant 383 | ant-jsch 384 | ${ant-jsch.version} 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | restart-on-Pi 395 | 396 | 397 | 398 | org.apache.maven.plugins 399 | maven-antrun-plugin 400 | 401 | 402 | transfer 403 | validate 404 | 405 | run 406 | 407 | 408 | 409 | 410 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | org.apache.ant 421 | ant-jsch 422 | ${ant-jsch.version} 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | debug 435 | 436 | startInDebugMode.sh 437 | 438 | 439 | 440 | 441 | -------------------------------------------------------------------------------- /src/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 3 | bin 4 | 5 | zip 6 | 7 | 8 | 9 | 10 | ${project.build.directory}/libs 11 | libs 12 | false 13 | 14 | javafx*.jar 15 | 16 | 17 | 18 | 19 | ${project.build.directory} 20 | . 21 | false 22 | 23 | ${build.finalName}.jar 24 | 25 | 26 | 27 | 28 | src/assembly 29 | . 30 | unix 31 | 32 | *.sh 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/assembly/download_openjfx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Variables 4 | URL="https://download2.gluonhq.com/openjfx/24/openjfx-24_linux-aarch64_bin-sdk.zip" 5 | SHA_URL="https://download2.gluonhq.com/openjfx/24/openjfx-24_linux-aarch64_bin-sdk.zip.sha256" 6 | TARGET_DIR="$HOME/openjfx" 7 | SUBDIR="extracted_files" 8 | TEMP_DIR=$(mktemp -d) 9 | 10 | # Function to calculate SHA256 sum 11 | calculate_sha256() { 12 | sha256sum "$1" | awk '{print $1}' 13 | } 14 | 15 | # Create target directory if it doesn't exist 16 | rm -rf "${TARGET_DIR:?}/$SUBDIR" 17 | mkdir -p "$TARGET_DIR/$SUBDIR" 18 | 19 | # Download the file if not already downloaded 20 | if [ ! -f "$TARGET_DIR/$(basename "$URL")" ]; then 21 | echo "Downloading $URL..." 22 | wget -q --show-progress -O "$TARGET_DIR/$(basename "$URL")" "$URL" 23 | else 24 | echo "File already exists. Skipping download." 25 | fi 26 | 27 | # Download the SHA256 file if not already downloaded 28 | if [ ! -f "$TARGET_DIR/$(basename "$SHA_URL")" ]; then 29 | echo "Downloading SHA256 checksum file..." 30 | wget -q -O "$TARGET_DIR/$(basename "$SHA_URL")" "$SHA_URL" 31 | fi 32 | 33 | # Verify the SHA256 sum 34 | echo "Verifying SHA256 sum..." 35 | if [ "$(calculate_sha256 "$TARGET_DIR/$(basename "$URL")")" == "$(cut -d ' ' -f1 "$TARGET_DIR/$(basename "$SHA_URL")")" ]; then 36 | echo "SHA256 sum verified successfully." 37 | else 38 | echo "SHA256 sum verification failed." 39 | rm -rf "$TEMP_DIR" 40 | exit 1 41 | fi 42 | 43 | # Extract the zip file 44 | echo "Extracting the zip file..." 45 | unzip -q "$TARGET_DIR/$(basename "$URL")" -d "$TARGET_DIR/$SUBDIR" 46 | 47 | # Find the folder and rename it 48 | folder=$(find "$TARGET_DIR/$SUBDIR" -maxdepth 1 -mindepth 1 -type d -exec echo {} \;) 49 | if [ -n "$folder" ]; then 50 | mv "$folder" "$TARGET_DIR/$SUBDIR/openjfx" 51 | fi 52 | 53 | echo "Extraction completed. Files are located in: $TARGET_DIR/$SUBDIR" 54 | -------------------------------------------------------------------------------- /src/assembly/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$1" 3 | pkill java 4 | DISPLAY=:0 XAUTHORITY=/home/pi/.Xauthority java --module-path "$HOME/openjfx/extracted_files/openjfx/lib" --add-modules javafx.controls -Dsun.java2d.opengl=True -XX:+UseZGC -Xmx1G -jar "$2".jar 5 | exit 0 6 | -------------------------------------------------------------------------------- /src/assembly/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$1" 3 | ./download_openjfx.sh 4 | pkill java 5 | DISPLAY=:0 XAUTHORITY=/home/pi/.Xauthority java --module-path "$HOME/openjfx/extracted_files/openjfx/lib" --add-modules javafx.controls -Dsun.java2d.opengl=True -XX:+UseZGC -Xmx1G -jar "$2".jar 6 | exit 0 7 | -------------------------------------------------------------------------------- /src/assembly/startInDebugMode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$1" 3 | pkill java 4 | DISPLAY=:0 XAUTHORITY=/home/pi/.Xauthority java --module-path "$HOME/openjfx/extracted_files/openjfx/lib" --add-modules javafx.controls -XX:+UseZGC -Xmx1G -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005 -jar "$2".jar 5 | exit 0 6 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/SimpleButton.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components; 2 | 3 | import java.time.Duration; 4 | import java.util.concurrent.ExecutorService; 5 | import java.util.concurrent.Executors; 6 | 7 | import com.pi4j.context.Context; 8 | import com.pi4j.io.gpio.digital.DigitalInput; 9 | import com.pi4j.io.gpio.digital.DigitalState; 10 | import com.pi4j.io.gpio.digital.PullResistance; 11 | 12 | import com.pi4j.catalog.components.base.DigitalSensor; 13 | import com.pi4j.catalog.components.base.PIN; 14 | 15 | import static com.pi4j.io.gpio.digital.DigitalInput.DEFAULT_DEBOUNCE; 16 | 17 | public class SimpleButton extends DigitalSensor { 18 | /** 19 | * Specifies if button state is inverted, e.g., HIGH = depressed, LOW = pressed 20 | * This will also automatically switch the pull resistance to PULL_UP 21 | */ 22 | private final boolean inverted; 23 | /** 24 | * Runnable Code when button is depressed 25 | */ 26 | private Runnable onUp; 27 | /** 28 | * Runnable Code when button is pressed 29 | */ 30 | private Runnable onDown; 31 | /** 32 | * Handler while button is pressed 33 | */ 34 | private Runnable whileDown; 35 | /** 36 | * Timer while button is pressed 37 | */ 38 | private Duration whilePressedDelay; 39 | 40 | /** 41 | * what needs to be done while the button is pressed (and whilePressed is != null) 42 | */ 43 | private final Runnable whileDownWorker = () -> { 44 | while (isDown()) { 45 | delay(whilePressedDelay); 46 | if (isDown() && whileDown != null) { 47 | logDebug("whileDown triggered"); 48 | whileDown.run(); 49 | } 50 | } 51 | }; 52 | 53 | private ExecutorService executor; 54 | 55 | /** 56 | * Creates a new button component 57 | * 58 | * @param pi4j Pi4J context 59 | */ 60 | public SimpleButton(Context pi4j, PIN address, boolean inverted) { 61 | this(pi4j, address, inverted, DEFAULT_DEBOUNCE); 62 | } 63 | 64 | /** 65 | * Creates a new button component with custom GPIO address and debounce time. 66 | * 67 | * @param pi4j Pi4J context 68 | * @param address GPIO address of button 69 | * @param inverted Specify if button state is inverted 70 | * @param debounce Debounce time in microseconds 71 | */ 72 | public SimpleButton(Context pi4j, PIN address, boolean inverted, long debounce) { 73 | super(pi4j, 74 | DigitalInput.newConfigBuilder(pi4j) 75 | .id("BCM" + address) 76 | .name("Button #" + address) 77 | .address(address.getPin()) 78 | .debounce(debounce) 79 | .pull(inverted ? PullResistance.PULL_UP : PullResistance.PULL_DOWN) 80 | .build()); 81 | 82 | this.inverted = inverted; 83 | 84 | /* 85 | * Gets a DigitalStateChangeEvent directly from the Provider, as this 86 | * Class is a listener. This runs in a different Thread than main. 87 | * Calls the methods onUp, onDown and whilePressed. WhilePressed gets 88 | * executed in an own Thread, as to not block other resources. 89 | */ 90 | digitalInput.addListener(stateChangeEvent -> { 91 | DigitalState state = getState(); 92 | 93 | logDebug("Button switched to '%s'", state); 94 | 95 | switch (state) { 96 | case HIGH -> { 97 | if (onDown != null) { 98 | logDebug("onDown triggered"); 99 | onDown.run(); 100 | } 101 | if (whileDown != null) { 102 | executor.submit(whileDownWorker); 103 | } 104 | } 105 | case LOW -> { 106 | if (onUp != null) { 107 | logDebug("onUp triggered"); 108 | onUp.run(); 109 | } 110 | } 111 | case UNKNOWN -> logError("Button is in State UNKNOWN"); 112 | } 113 | }); 114 | } 115 | 116 | /** 117 | * Checks if button is currently pressed. 118 | *

119 | * For a not-inverted button this means: if the button is pressed, then full voltage is present 120 | * at the GPIO-Pin. Therefore, the DigitalState is HIGH 121 | * 122 | * @return true if button is pressed 123 | */ 124 | public boolean isDown() { 125 | return getState() == DigitalState.HIGH; 126 | } 127 | 128 | /** 129 | * Checks if button is currently depressed (= NOT pressed) 130 | *

131 | * For a not-inverted button this means: if the button is depressed, then no voltage is present 132 | * at the GPIO-Pin. Therefore, the DigitalState is LOW 133 | * 134 | * @return true if button is depressed 135 | */ 136 | public boolean isUp() { 137 | return getState() == DigitalState.LOW; 138 | } 139 | 140 | 141 | /** 142 | * Sets or disables the handler for the onDown event. 143 | *

144 | * This event gets triggered whenever the button is pressed. 145 | * Only a single event handler can be registered at once. 146 | * 147 | * @param task Event handler to call or null to disable 148 | */ 149 | public void onDown(Runnable task) { 150 | onDown = task; 151 | } 152 | 153 | /** 154 | * Sets or disables the handler for the onUp event. 155 | *

156 | * This event gets triggered whenever the button is no longer pressed. 157 | * Only a single event handler can be registered at once. 158 | * 159 | * @param task Event handler to call or null to disable 160 | */ 161 | public void onUp(Runnable task) { 162 | onUp = task; 163 | } 164 | 165 | /** 166 | * Sets or disables the handler for the whilePressed event. 167 | *

168 | * This event gets triggered whenever the button is pressed. 169 | * Only a single event handler can be registered at once. 170 | * 171 | * @param task Event handler to call or null to disable 172 | * @param delay delay between two executions of task 173 | */ 174 | public void whilePressed(Runnable task, Duration delay) { 175 | whileDown = task; 176 | whilePressedDelay = delay; 177 | if(executor != null){ 178 | executor.shutdownNow(); 179 | } 180 | if(task != null){ 181 | executor = Executors.newSingleThreadExecutor(); 182 | } 183 | } 184 | 185 | public boolean isInInitialState(){ 186 | return onDown == null && onUp == null && whileDown == null && executor == null; 187 | } 188 | 189 | /** 190 | * disables all the handlers for the onUp, onDown and WhileDown Events 191 | */ 192 | @Override 193 | public void reset() { 194 | onDown = null; 195 | onUp = null; 196 | whileDown = null; 197 | if(executor != null){ 198 | executor.shutdown(); 199 | } 200 | executor = null; 201 | } 202 | 203 | /** 204 | * Returns the current state of the Digital State 205 | * 206 | * @return Current DigitalInput state (Can be HIGH, LOW or UNKNOWN) 207 | */ 208 | private DigitalState getState() { 209 | return switch (digitalInput.state()) { 210 | case HIGH -> inverted ? DigitalState.LOW : DigitalState.HIGH; 211 | case LOW -> inverted ? DigitalState.HIGH : DigitalState.LOW; 212 | default -> DigitalState.UNKNOWN; 213 | }; 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/SimpleLed.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components; 2 | 3 | import com.pi4j.context.Context; 4 | import com.pi4j.io.gpio.digital.DigitalOutput; 5 | 6 | import com.pi4j.catalog.components.base.DigitalActuator; 7 | import com.pi4j.catalog.components.base.PIN; 8 | 9 | public class SimpleLed extends DigitalActuator { 10 | 11 | /** 12 | * Creates a new SimpleLed component with a custom BCM pin. 13 | * 14 | * @param pi4j Pi4J context 15 | * @param address Custom BCM pin address 16 | */ 17 | public SimpleLed(Context pi4j, PIN address) { 18 | super(pi4j, 19 | DigitalOutput.newConfigBuilder(pi4j) 20 | .id("BCM" + address) 21 | .name("LED #" + address) 22 | .address(address.getPin()) 23 | .build()); 24 | logDebug("Created new SimpleLed component"); 25 | digitalOutput.off(); 26 | } 27 | 28 | /** 29 | * Sets LED to on. 30 | */ 31 | public void on() { 32 | if(!isOn()){ 33 | logDebug("LED turned ON"); 34 | digitalOutput.on(); 35 | } 36 | } 37 | 38 | public boolean isOn(){ 39 | return digitalOutput.isOn(); 40 | } 41 | 42 | /** 43 | * Sets LED to off 44 | */ 45 | public void off() { 46 | if(isOn()){ 47 | logDebug("LED turned OFF"); 48 | digitalOutput.off(); 49 | } 50 | } 51 | 52 | /** 53 | * Toggle the LED state depending on its current state. 54 | * 55 | * @return Return true or false according to the new state of the relay. 56 | */ 57 | public boolean toggle() { 58 | digitalOutput.toggle(); 59 | logDebug("LED toggled, now it is %s", digitalOutput.isOff() ? "OFF" : "ON"); 60 | 61 | return digitalOutput.isOff(); 62 | } 63 | 64 | @Override 65 | public void reset() { 66 | off(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/Component.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | import java.time.Duration; 4 | import java.util.logging.ConsoleHandler; 5 | import java.util.logging.Level; 6 | import java.util.logging.Logger; 7 | 8 | public abstract class Component { 9 | /** 10 | * Logger instance 11 | */ 12 | private static final Logger logger = Logger.getLogger("Pi4J Components"); 13 | 14 | static { 15 | Level appropriateLevel = Level.INFO; 16 | //Level appropriateLevel = Level.FINE; //use if 'debug' 17 | 18 | System.setProperty("java.util.logging.SimpleFormatter.format", 19 | "%4$s: %5$s [%1$tl:%1$tM:%1$tS %1$Tp]%n"); 20 | 21 | logger.setLevel(appropriateLevel); 22 | logger.setUseParentHandlers(false); 23 | ConsoleHandler handler = new ConsoleHandler(); 24 | handler.setLevel(appropriateLevel); 25 | logger.addHandler(handler); 26 | } 27 | 28 | protected Component(){ 29 | } 30 | 31 | /** 32 | * Override this method to clean up all used resources 33 | */ 34 | public void reset(){ 35 | //nothing to do by default 36 | } 37 | 38 | protected void logInfo(String msg, Object... args) { 39 | logger.info(() -> String.format(msg, args)); 40 | } 41 | 42 | protected void logError(String msg, Object... args) { 43 | logger.severe(() -> String.format(msg, args)); 44 | } 45 | 46 | protected void logDebug(String msg, Object... args) { 47 | logger.fine(() -> String.format(msg, args)); 48 | } 49 | 50 | protected void logException(String msg, Throwable exception){ 51 | logger.log(Level.SEVERE, msg, exception); 52 | } 53 | 54 | /** 55 | * Utility function to sleep for the specified amount of milliseconds. 56 | * An {@link InterruptedException} will be caught and ignored while setting the interrupt flag again. 57 | * 58 | * @param duration Time to sleep 59 | */ 60 | protected void delay(Duration duration) { 61 | try { 62 | long nanos = duration.toNanos(); 63 | long millis = nanos / 1_000_000; 64 | int remainingNanos = (int) (nanos % 1_000_000); 65 | Thread.sleep(millis, remainingNanos); 66 | } catch (InterruptedException e) { 67 | Thread.currentThread().interrupt(); 68 | } 69 | } 70 | 71 | protected T asMock(Class type, Object instance) { 72 | return type.cast(instance); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/DigitalActuator.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.pi4j.context.Context; 6 | import com.pi4j.io.gpio.digital.DigitalOutput; 7 | import com.pi4j.io.gpio.digital.DigitalOutputConfig; 8 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalOutput; 9 | 10 | public abstract class DigitalActuator extends Component { 11 | /** 12 | * Pi4J digital output instance used by this component 13 | */ 14 | protected final DigitalOutput digitalOutput; 15 | 16 | protected DigitalActuator(Context pi4j, DigitalOutputConfig config) { 17 | digitalOutput = pi4j.create(config); 18 | } 19 | 20 | public int pinNumber(){ 21 | return digitalOutput.address().intValue(); 22 | } 23 | 24 | 25 | // --------------- for testing -------------------- 26 | 27 | public MockDigitalOutput mock() { 28 | return asMock(MockDigitalOutput.class, digitalOutput); 29 | } 30 | 31 | public MockDigitalOutput[] mock(DigitalOutput[] digitalOutputs) { 32 | return Arrays.stream(digitalOutputs) 33 | .map(d -> asMock(MockDigitalOutput.class, digitalOutput)) 34 | .toArray(MockDigitalOutput[]::new); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/DigitalSensor.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | import com.pi4j.context.Context; 4 | import com.pi4j.io.gpio.digital.DigitalInput; 5 | import com.pi4j.io.gpio.digital.DigitalInputConfig; 6 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalInput; 7 | 8 | public abstract class DigitalSensor extends Component { 9 | /** 10 | * Pi4J digital input instance used by this component (that's the low-level Pi4J Class) 11 | */ 12 | protected final DigitalInput digitalInput; 13 | 14 | protected DigitalSensor(Context pi4j, DigitalInputConfig config){ 15 | digitalInput = pi4j.create(config); 16 | } 17 | 18 | public int pinNumber(){ 19 | return digitalInput.address().intValue(); 20 | } 21 | 22 | 23 | // --------------- for testing -------------------- 24 | 25 | public MockDigitalInput mock() { 26 | return asMock(MockDigitalInput.class, digitalInput); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/I2CDevice.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | import java.time.Duration; 4 | 5 | import com.pi4j.context.Context; 6 | import com.pi4j.io.i2c.I2C; 7 | import com.pi4j.plugin.mock.provider.i2c.MockI2C; 8 | 9 | public abstract class I2CDevice extends Component { 10 | 11 | /** 12 | * The Default BUS and Device Address. 13 | * On the PI, you can look it up with the Command 'sudo i2cdetect -y 1' 14 | */ 15 | protected static final int DEFAULT_BUS = 0x01; 16 | 17 | /** 18 | * The PI4J I2C component 19 | */ 20 | private final I2C i2c; 21 | 22 | 23 | protected I2CDevice(Context pi4j, int device, String name){ 24 | i2c = pi4j.create(I2C.newConfigBuilder(pi4j) 25 | .id("I2C-" + DEFAULT_BUS + "@" + device) 26 | .name(name+ "@" + device) 27 | .bus(DEFAULT_BUS) 28 | .device(device) 29 | .build()); 30 | init(i2c); 31 | logDebug("I2C device %s initialized", name); 32 | } 33 | 34 | 35 | /** 36 | * send a single command to device 37 | */ 38 | protected void sendCommand(byte cmd) { 39 | i2c.write(cmd); 40 | delay(Duration.ofNanos(100_000)); 41 | } 42 | 43 | protected int readRegister(int register) { 44 | return i2c.readRegisterWord(register); 45 | } 46 | 47 | /** 48 | * send custom configuration to device 49 | * 50 | * @param config custom configuration 51 | */ 52 | protected void writeRegister(int register, int config) { 53 | i2c.writeRegisterWord(register, config); 54 | } 55 | 56 | /** 57 | * send some data to device 58 | * 59 | * @param data 60 | */ 61 | protected void write(byte data){ 62 | i2c.write(data); 63 | } 64 | 65 | /** 66 | * Execute Display commands 67 | * 68 | * @param command Select the LCD Command 69 | * @param data Setup command data 70 | */ 71 | protected void sendCommand(byte command, byte data) { 72 | sendCommand((byte) (command | data)); 73 | } 74 | 75 | protected abstract void init(I2C i2c); 76 | 77 | // --------------- for testing -------------------- 78 | 79 | public MockI2C mock() { 80 | return asMock(MockI2C.class, i2c); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/PIN.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | /** 4 | * Helper Class, used as Raspberry-Pi pin-numbering. Is helpful to see, which pin can act as what I/O provider 5 | */ 6 | public enum PIN { 7 | SDA1(2), 8 | SCL1(2), 9 | TXD(14), 10 | RXD(15), 11 | D4(4), 12 | D5(5), 13 | D6(6), 14 | D11(11), 15 | D12(12), 16 | D13(13), 17 | D16(16), 18 | D17(17), 19 | D20(20), 20 | D21(21), 21 | D22(22), 22 | D23(23), 23 | D24(24), 24 | D25(25), 25 | D26(26), 26 | D27(27), 27 | MOSI(10), 28 | MISO(9), 29 | CEO(8), 30 | CE1(7), 31 | PWM18(18), 32 | PWM19(19); 33 | 34 | private final int pin; 35 | 36 | PIN(int pin) { 37 | this.pin = pin; 38 | } 39 | 40 | public int getPin() { 41 | return pin; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/PwmActuator.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | import java.util.List; 4 | 5 | import com.pi4j.context.Context; 6 | import com.pi4j.io.pwm.Pwm; 7 | import com.pi4j.io.pwm.PwmConfig; 8 | import com.pi4j.plugin.mock.provider.pwm.MockPwm; 9 | 10 | public class PwmActuator extends Component { 11 | protected static final List AVAILABLE_PWM_PINS = List.of(PIN.PWM18, PIN.PWM19); 12 | 13 | protected final Pwm pwm; 14 | 15 | protected PwmActuator(Context pi4J, PwmConfig config) { 16 | if(AVAILABLE_PWM_PINS.stream().noneMatch(p -> p.getPin() == config.address())){ 17 | throw new IllegalArgumentException("Pin " + config.address() + " is not a PWM Pin"); 18 | } 19 | pwm = pi4J.create(config); 20 | } 21 | 22 | @Override 23 | public void reset() { 24 | pwm.off(); 25 | } 26 | 27 | // --------------- for testing -------------------- 28 | 29 | public MockPwm mock() { 30 | return asMock(MockPwm.class, pwm); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/SerialDevice.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.InputStreamReader; 5 | import java.time.Duration; 6 | import java.util.function.Consumer; 7 | 8 | import com.pi4j.context.Context; 9 | import com.pi4j.io.serial.FlowControl; 10 | import com.pi4j.io.serial.Parity; 11 | import com.pi4j.io.serial.Serial; 12 | import com.pi4j.io.serial.StopBits; 13 | 14 | /** 15 | * 16 | */ 17 | public class SerialDevice extends Component { 18 | /** 19 | * The PI4J Serial 20 | */ 21 | private final Serial serial; 22 | 23 | private final Consumer onNewData; 24 | 25 | private boolean continueReading = false; 26 | 27 | private Thread serialReaderThread; 28 | 29 | public SerialDevice(Context pi4j, Consumer onNewData){ 30 | serial = pi4j.create(Serial.newConfigBuilder(pi4j) 31 | .use_9600_N81() 32 | .dataBits_8() 33 | .parity(Parity.NONE) 34 | .stopBits(StopBits._1) 35 | .flowControl(FlowControl.NONE) 36 | .id("my-serial") 37 | .device("/dev/ttyS0") 38 | .build()); 39 | this.onNewData = onNewData; 40 | //todo: Check if this is really necessary 41 | serial.open(); 42 | // Wait till the serial port is open 43 | while (!serial.isOpen()) { 44 | delay(Duration.ofMillis(250)); 45 | } 46 | } 47 | 48 | @Override 49 | public void reset() { 50 | stopReading(); 51 | serial.close(); 52 | 53 | super.reset(); 54 | } 55 | 56 | public void stopReading() { 57 | continueReading = false; 58 | serialReaderThread = null; 59 | } 60 | 61 | public void startReading(){ 62 | if(continueReading){ 63 | return; 64 | } 65 | continueReading = true; 66 | serialReaderThread = new Thread(() -> listenToSerialPort(), "SerialReader"); 67 | serialReaderThread.setDaemon(true); 68 | serialReaderThread.start(); 69 | } 70 | 71 | private void listenToSerialPort() { 72 | // We use a buffered reader to handle the data received from the serial port 73 | BufferedReader br = new BufferedReader(new InputStreamReader(serial.getInputStream())); 74 | 75 | try { 76 | // Data from the GPS is received in lines 77 | StringBuilder line = new StringBuilder(); 78 | 79 | // Read data until the flag is false 80 | while (continueReading) { 81 | // First we need to check if there is data available to read. 82 | // The read() command for pi-gpio-serial is a NON-BLOCKING call, in contrast to typical java input streams. 83 | var available = serial.available(); 84 | if (available > 0) { 85 | for (int i = 0; i < available; i++) { 86 | byte b = (byte) br.read(); 87 | if (b < 32) { 88 | // All non-string bytes are handled as line breaks 89 | if (line.length() > 0) { 90 | // Here we should add code to parse the data to a GPS data object 91 | onNewData.accept(line.toString()); 92 | line = new StringBuilder(); 93 | } 94 | } else { 95 | line.append((char) b); 96 | } 97 | } 98 | } else { 99 | Thread.sleep(100); 100 | } 101 | } 102 | } catch (Exception e) { 103 | logException("Error reading data from serial: ", e); 104 | e.printStackTrace(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/catalog/components/base/SpiDevice.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components.base; 2 | 3 | import com.pi4j.context.Context; 4 | import com.pi4j.io.spi.Spi; 5 | import com.pi4j.io.spi.SpiConfig; 6 | import com.pi4j.plugin.mock.provider.spi.MockSpi; 7 | 8 | public class SpiDevice extends Component { 9 | /** 10 | * The PI4J SPI 11 | */ 12 | private final Spi spi; 13 | private final Context pi4j; 14 | 15 | protected SpiDevice(Context pi4j, SpiConfig config){ 16 | this.pi4j = pi4j; 17 | spi = pi4j.create(config); 18 | logDebug("SPI is open"); 19 | } 20 | 21 | protected void sendToSerialDevice(byte[] data) { 22 | spi.write(data); 23 | } 24 | 25 | @Override 26 | public void reset() { 27 | super.reset(); 28 | spi.close(); 29 | spi.shutdown(pi4j); 30 | logDebug("SPI closed"); 31 | } 32 | 33 | // --------------- for testing -------------------- 34 | 35 | public MockSpi mock() { 36 | return asMock(MockSpi.class, spi); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/AppStarter.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp; 2 | 3 | import com.pi4j.mvc.multicontrollerapp.controller.ApplicationController; 4 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 5 | import com.pi4j.mvc.multicontrollerapp.view.gui.ExampleGUI; 6 | import com.pi4j.mvc.multicontrollerapp.view.pui.ExamplePUI; 7 | 8 | import javafx.application.Application; 9 | import javafx.scene.Parent; 10 | import javafx.scene.Scene; 11 | import javafx.scene.layout.Pane; 12 | import javafx.stage.Stage; 13 | 14 | public class AppStarter extends Application { 15 | 16 | private ApplicationController controller; 17 | private ExamplePUI pui; 18 | 19 | @Override 20 | public void start(Stage primaryStage) { 21 | // that's your 'information hub'. 22 | ExampleModel model = new ExampleModel(); 23 | 24 | controller = new ApplicationController(model); 25 | 26 | //both gui and pui are working on the same controller 27 | pui = new ExamplePUI(controller); 28 | 29 | Pane gui = new ExampleGUI(controller); 30 | 31 | Scene scene = new Scene(gui); 32 | 33 | primaryStage.setTitle("GUI of a Pi4J App"); 34 | primaryStage.setScene(scene); 35 | 36 | primaryStage.show(); 37 | 38 | // on desktop, it's convenient to have a very basic emulator for the PUI to test the interaction between GUI and PUI 39 | //startPUIEmulator(new ExamplePuiEmulator(controller)); 40 | } 41 | 42 | @Override 43 | public void stop() { 44 | controller.shutdown(); 45 | pui.shutdown(); 46 | } 47 | 48 | private void startPUIEmulator(Parent puiEmulator){ 49 | Scene emulatorScene = new Scene(puiEmulator); 50 | Stage secondaryStage = new Stage(); 51 | secondaryStage.setTitle("PUI Emulator"); 52 | secondaryStage.setScene(emulatorScene); 53 | secondaryStage.show(); 54 | } 55 | 56 | public static void main(String[] args) { 57 | launch(args); //start the whole application 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/controller/ApplicationController.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.controller; 2 | 3 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 4 | import com.pi4j.mvc.util.mvcbase.ControllerBase; 5 | 6 | /** 7 | * Provides all the available actions to the UI. 8 | *

9 | * Usually all the methods just delegate the call to the appropriate (Sub-)Controller. 10 | * 11 | */ 12 | public class ApplicationController extends ControllerBase { 13 | 14 | private final LEDController ledController; 15 | private final CounterController counterController; 16 | 17 | public ApplicationController(ExampleModel model) { 18 | super(model); 19 | ledController = new LEDController(model); 20 | counterController = new CounterController(model); 21 | } 22 | 23 | @Override 24 | public void awaitCompletion() { 25 | super.awaitCompletion(); 26 | ledController.awaitCompletion(); 27 | counterController.awaitCompletion(); 28 | } 29 | 30 | // the actions we need in our application 31 | // these methods are public and can be called from GUI and PUI (and nothing else) 32 | 33 | @Override 34 | public void shutdown() { 35 | super.shutdown(); 36 | ledController.shutdown(); 37 | counterController.shutdown(); 38 | } 39 | 40 | public void increaseCounter() { 41 | counterController.increaseCounter(); 42 | } 43 | 44 | public void decreaseCounter() { 45 | counterController.decreaseCounter(); 46 | } 47 | 48 | public void setLedGlows(boolean glows){ 49 | ledController.setIsActive(glows); 50 | } 51 | 52 | public void blink(){ 53 | ledController.blink(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/controller/CounterController.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.controller; 2 | 3 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 4 | import com.pi4j.mvc.util.mvcbase.ControllerBase; 5 | 6 | /** 7 | * Handles all the functionality needed to manage the 'counter'. 8 | *

9 | * All methods are intentionally 'package private'. Only 'ApplicationController' can access them. 10 | */ 11 | class CounterController extends ControllerBase { 12 | 13 | CounterController(ExampleModel model) { 14 | super(model); 15 | } 16 | 17 | // the logic we need for managing the counter 18 | 19 | void increaseCounter() { 20 | increaseValue(model.counter); 21 | } 22 | 23 | void decreaseCounter() { 24 | decreaseValue(model.counter); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/controller/LEDController.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.controller; 2 | 3 | import java.time.Duration; 4 | 5 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 6 | import com.pi4j.mvc.util.mvcbase.ControllerBase; 7 | 8 | /** 9 | * Handles all the functionality needed to manage the 'LED'. 10 | *

11 | * All methods are intentionally 'package private'. Only 'ApplicationController' can access them. 12 | */ 13 | class LEDController extends ControllerBase { 14 | 15 | LEDController(ExampleModel model) { 16 | super(model); 17 | } 18 | 19 | void setIsActive(boolean glows) { 20 | setValue(model.isActive, glows); 21 | } 22 | 23 | /** 24 | * In this example Controller even controls the blinking behaviour 25 | */ 26 | void blink() { 27 | final Duration pause = Duration.ofMillis(500); 28 | setIsActive(false); 29 | for (int i = 0; i < 4; i++) { 30 | setIsActive(true); 31 | pauseExecution(pause); 32 | setIsActive(false); 33 | pauseExecution(pause); 34 | } 35 | } 36 | 37 | /** 38 | * Example for triggering some built-in action in PUI instead of implement it in Controller. 39 | *

40 | * Controller can't call PUI-component methods directly. Use a trigger instead. 41 | * 42 | */ 43 | // public void blinkViaBuiltInAction() { 44 | // toggle(model.blinkingTrigger); 45 | // } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/model/ExampleModel.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.model; 2 | 3 | import com.pi4j.mvc.util.mvcbase.ObservableValue; 4 | 5 | /** 6 | * In MVC the 'Model' mainly consists of 'ObservableValues'. 7 | *

8 | * There should be no need for additional methods. 9 | *

10 | * All the application logic is handled by the 'Controller' 11 | */ 12 | public class ExampleModel { 13 | public final ObservableValue systemInfo = new ObservableValue<>("JavaFX " + System.getProperty("javafx.version") + ", running on Java " + System.getProperty("java.version") + "."); 14 | public final ObservableValue counter = new ObservableValue<>(73); 15 | public final ObservableValue isActive = new ObservableValue<>(false); 16 | 17 | // if you want to use the LED's built-in blinking feature (instead of implementing blinking in Controller), you need an additional state 18 | // public final ObservableValue blinkingTrigger = new ObservableValue<>(false); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/view/gui/ExampleGUI.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.view.gui; 2 | 3 | import com.pi4j.mvc.multicontrollerapp.controller.ApplicationController; 4 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 5 | import com.pi4j.mvc.util.mvcbase.ViewMixin; 6 | 7 | import javafx.geometry.Insets; 8 | import javafx.geometry.Pos; 9 | import javafx.scene.control.Button; 10 | import javafx.scene.control.Label; 11 | import javafx.scene.layout.BorderPane; 12 | import javafx.scene.layout.HBox; 13 | import javafx.scene.layout.Priority; 14 | import javafx.scene.layout.Region; 15 | import javafx.scene.layout.VBox; 16 | 17 | public class ExampleGUI extends BorderPane implements ViewMixin { 18 | private static final String LIGHT_BULB = "\uf0eb"; // the Unicode of the lightbulb-icon in fontawesome font 19 | private static final String HEARTBEAT = "\uf21e"; // the Unicode of the heartbeat-icon in fontawesome font 20 | 21 | // declare all the UI elements you need 22 | private Button ledButton; 23 | private Button blinkButton; 24 | private Button increaseButton; 25 | private Label counterLabel; 26 | private Label infoLabel; 27 | 28 | public ExampleGUI(ApplicationController controller) { 29 | init(controller); //remember to call init 30 | } 31 | 32 | @Override 33 | public void initializeSelf() { 34 | //load all fonts you need 35 | loadFonts("/fonts/Lato/Lato-Lig.ttf", "/fonts/fontawesome-webfont.ttf"); 36 | 37 | //apply your style 38 | addStylesheetFiles("/mvc/multicontrollerapp/style.css"); 39 | 40 | getStyleClass().add("root-pane"); 41 | } 42 | 43 | @Override 44 | public void initializeParts() { 45 | ledButton = new Button(LIGHT_BULB); 46 | ledButton.getStyleClass().add("icon-button"); 47 | 48 | blinkButton = new Button(HEARTBEAT); 49 | blinkButton.getStyleClass().add("icon-button"); 50 | 51 | increaseButton = new Button("+"); 52 | 53 | counterLabel = new Label(); 54 | counterLabel.getStyleClass().add("counter-label"); 55 | 56 | infoLabel = new Label(); 57 | infoLabel.getStyleClass().add("info-label"); 58 | } 59 | 60 | @Override 61 | public void layoutParts() { 62 | // consider to use GridPane instead 63 | Region spacer = new Region(); 64 | HBox.setHgrow(spacer, Priority.ALWAYS); 65 | 66 | HBox topBox = new HBox(ledButton, spacer, blinkButton); 67 | topBox.setAlignment(Pos.CENTER); 68 | 69 | VBox centerBox = new VBox(counterLabel, increaseButton); 70 | centerBox.setAlignment(Pos.CENTER); 71 | centerBox.setFillWidth(true); 72 | centerBox.setPadding(new Insets(30)); 73 | 74 | setTop(topBox); 75 | setCenter(centerBox); 76 | setBottom(infoLabel); 77 | } 78 | 79 | @Override 80 | public void setupUiToActionBindings(ApplicationController controller) { 81 | // look at that: all EventHandlers just trigger an action on Controller 82 | // by calling a single method 83 | 84 | increaseButton.setOnAction (event -> controller.increaseCounter()); 85 | ledButton.setOnMousePressed (event -> controller.setLedGlows(true)); 86 | ledButton.setOnMouseReleased(event -> controller.setLedGlows(false)); 87 | blinkButton.setOnAction (event -> controller.blink()); 88 | } 89 | 90 | @Override 91 | public void setupModelToUiBindings(ExampleModel model) { 92 | onChangeOf(model.systemInfo) // the value we need to observe, in this case that's an ObservableValue, no need to convert it 93 | .update(infoLabel.textProperty()); // keeps textProperty and systemInfo in sync 94 | 95 | onChangeOf(model.counter) // the value we need to observe, in this case, that's an ObservableValue 96 | .convertedBy(String::valueOf) // we have to convert the Integer to a String 97 | .update(counterLabel.textProperty()); // keeps textProperty and counter in sync 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/view/gui/ExamplePuiEmulator.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.view.gui; 2 | 3 | import com.pi4j.mvc.multicontrollerapp.controller.ApplicationController; 4 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 5 | import com.pi4j.mvc.util.mvcbase.ViewMixin; 6 | 7 | import javafx.geometry.Insets; 8 | import javafx.geometry.Pos; 9 | import javafx.scene.control.Button; 10 | import javafx.scene.control.Label; 11 | import javafx.scene.layout.VBox; 12 | 13 | 14 | public class ExamplePuiEmulator extends VBox implements ViewMixin { 15 | 16 | // for each PUI component, declare a corresponding JavaFX control 17 | private Label led; 18 | private Button decreaseButton; 19 | 20 | public ExamplePuiEmulator(ApplicationController controller){ 21 | init(controller); 22 | } 23 | 24 | @Override 25 | public void initializeSelf() { 26 | setPrefWidth(250); 27 | } 28 | 29 | @Override 30 | public void initializeParts() { 31 | led = new Label(); 32 | decreaseButton = new Button("Decrease"); 33 | } 34 | 35 | @Override 36 | public void layoutParts() { 37 | setPadding(new Insets(20)); 38 | setSpacing(20); 39 | setAlignment(Pos.CENTER); 40 | getChildren().addAll(led, decreaseButton); 41 | } 42 | 43 | @Override 44 | public void setupUiToActionBindings(ApplicationController controller) { 45 | //trigger the same actions as the real PUI 46 | decreaseButton.setOnAction(event -> controller.decreaseCounter()); 47 | } 48 | 49 | @Override 50 | public void setupModelToUiBindings(ExampleModel model) { 51 | //observe the same values as the real PUI 52 | onChangeOf(model.isActive) 53 | .convertedBy(glows -> glows ? "on" : "off") 54 | .update(led.textProperty()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/multicontrollerapp/view/pui/ExamplePUI.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.view.pui; 2 | 3 | import com.pi4j.mvc.multicontrollerapp.controller.ApplicationController; 4 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 5 | import com.pi4j.mvc.util.mvcbase.PuiBase; 6 | 7 | import com.pi4j.catalog.components.base.PIN; 8 | import com.pi4j.catalog.components.SimpleButton; 9 | import com.pi4j.catalog.components.SimpleLed; 10 | 11 | public class ExamplePUI extends PuiBase { 12 | //declare all hardware components attached to RaspPi 13 | //these are protected to give unit tests access to them 14 | protected SimpleLed led; 15 | protected SimpleButton button; 16 | 17 | public ExamplePUI(ApplicationController controller) { 18 | super(controller); 19 | } 20 | 21 | @Override 22 | public void initializeParts() { 23 | led = new SimpleLed(pi4J, PIN.D22); 24 | button = new SimpleButton(pi4J, PIN.D24, false); 25 | } 26 | 27 | @Override 28 | public void setupUiToActionBindings(ApplicationController controller) { 29 | button.onUp(controller::decreaseCounter); 30 | } 31 | 32 | @Override 33 | public void setupModelToUiBindings(ExampleModel model) { 34 | onChangeOf(model.isActive) 35 | .execute((oldValue, newValue) -> { 36 | if (newValue) { 37 | led.on(); 38 | } else { 39 | led.off(); 40 | } 41 | }); 42 | 43 | // if you want to use the built-in blinking feature (instead of implementing blinking in Controller): 44 | // onChangeOf(model.blinkingTrigger) 45 | // .execute((oldValue, newValue) -> led.blink(4, Duration.ofMillis(500))); 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templateapp/AppStarter.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp; 2 | 3 | import com.pi4j.mvc.templateapp.controller.SomeController; 4 | import com.pi4j.mvc.templateapp.model.SomeModel; 5 | import com.pi4j.mvc.templateapp.view.gui.SomeGUI; 6 | import com.pi4j.mvc.templateapp.view.pui.SomePUI; 7 | 8 | import javafx.application.Application; 9 | import javafx.scene.Parent; 10 | import javafx.scene.Scene; 11 | import javafx.scene.layout.Pane; 12 | import javafx.stage.Stage; 13 | 14 | public class AppStarter extends Application { 15 | 16 | private SomeController controller; 17 | private SomePUI pui; 18 | 19 | @Override 20 | public void start(Stage primaryStage) { 21 | // that's your 'information hub'. 22 | SomeModel model = new SomeModel(); 23 | 24 | controller = new SomeController(model); 25 | 26 | //both gui and pui are working on the same controller 27 | pui = new SomePUI(controller); 28 | 29 | Pane gui = new SomeGUI(controller); 30 | 31 | Scene scene = new Scene(gui); 32 | 33 | primaryStage.setTitle("GUI of a Pi4J App"); 34 | primaryStage.setScene(scene); 35 | 36 | primaryStage.show(); 37 | 38 | // on desktop, it's convenient to have a very basic emulator for the PUI to test the interaction between GUI and PUI 39 | //startPUIEmulator(new SomePuiEmulator(controller)); 40 | } 41 | 42 | @Override 43 | public void stop() { 44 | controller.shutdown(); 45 | pui.shutdown(); 46 | } 47 | 48 | private void startPUIEmulator(Parent puiEmulator) { 49 | Scene emulatorScene = new Scene(puiEmulator); 50 | Stage secondaryStage = new Stage(); 51 | secondaryStage.setTitle("PUI Emulator"); 52 | secondaryStage.setScene(emulatorScene); 53 | secondaryStage.show(); 54 | } 55 | 56 | public static void main(String[] args) { 57 | launch(args); //start the whole application 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templateapp/controller/SomeController.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.controller; 2 | 3 | import com.pi4j.mvc.templateapp.model.SomeModel; 4 | import com.pi4j.mvc.util.mvcbase.ControllerBase; 5 | 6 | 7 | public class SomeController extends ControllerBase { 8 | 9 | public SomeController(SomeModel model) { 10 | super(model); 11 | } 12 | 13 | // the logic we need in our application 14 | // these methods can be called from GUI and PUI (and from nowhere else) 15 | 16 | public void increaseCounter() { 17 | increaseValue(model.counter); 18 | } 19 | 20 | public void decreaseCounter() { 21 | //use updateModel if several values need an update in this action 22 | updateModel(decrease(model.counter), 23 | set(model.isActive, false)); 24 | } 25 | 26 | public void setIsActive(boolean is){ 27 | //use setValue if a single value needs to be updated in this action 28 | setValue(model.isActive, is); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templateapp/model/SomeModel.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.model; 2 | 3 | import com.pi4j.mvc.util.mvcbase.ObservableValue; 4 | 5 | /** 6 | * In MVC the 'Model' mainly consists of 'ObservableValues'. 7 | *

8 | * There should be no need for additional methods. 9 | *

10 | * All the application logic is handled by the 'Controller' 11 | */ 12 | public class SomeModel { 13 | public final ObservableValue systemInfo = new ObservableValue<>("JavaFX " + System.getProperty("javafx.version") + ", running on Java " + System.getProperty("java.version") + "."); 14 | public final ObservableValue counter = new ObservableValue<>(73); 15 | public final ObservableValue isActive = new ObservableValue<>(false); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templateapp/view/gui/SomeGUI.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.view.gui; 2 | 3 | import com.pi4j.mvc.templateapp.controller.SomeController; 4 | import com.pi4j.mvc.templateapp.model.SomeModel; 5 | import com.pi4j.mvc.util.mvcbase.ViewMixin; 6 | 7 | import javafx.geometry.Insets; 8 | import javafx.geometry.Pos; 9 | import javafx.scene.control.Button; 10 | import javafx.scene.control.Label; 11 | import javafx.scene.layout.BorderPane; 12 | import javafx.scene.layout.HBox; 13 | import javafx.scene.layout.VBox; 14 | 15 | public class SomeGUI extends BorderPane implements ViewMixin { //all GUI-elements have to implement ViewMixin 16 | 17 | private static final String LIGHT_BULB = "\uf0eb"; // the Unicode of the lightbulb-icon in fontawesome font 18 | 19 | // declare all the UI elements you need 20 | private Button ledButton; 21 | private Button increaseButton; 22 | private Label counterLabel; 23 | private Label infoLabel; 24 | 25 | public SomeGUI(SomeController controller) { 26 | init(controller); //remember to call 'init' 27 | } 28 | 29 | @Override 30 | public void initializeSelf() { 31 | //load all fonts you need 32 | loadFonts("/fonts/Lato/Lato-Lig.ttf", "/fonts/fontawesome-webfont.ttf"); 33 | 34 | //apply your style 35 | addStylesheetFiles("/mvc/templateapp/style.css"); 36 | 37 | getStyleClass().add("root-pane"); 38 | } 39 | 40 | @Override 41 | public void initializeParts() { 42 | ledButton = new Button(LIGHT_BULB); 43 | ledButton.getStyleClass().add("icon-button"); 44 | 45 | increaseButton = new Button("+"); 46 | 47 | counterLabel = new Label(); 48 | counterLabel.getStyleClass().add("counter-label"); 49 | 50 | infoLabel = new Label(); 51 | infoLabel.getStyleClass().add("info-label"); 52 | } 53 | 54 | @Override 55 | public void layoutParts() { 56 | HBox topBox = new HBox(ledButton); 57 | topBox.setAlignment(Pos.CENTER); 58 | 59 | VBox centerBox = new VBox(counterLabel, increaseButton); 60 | centerBox.setAlignment(Pos.CENTER); 61 | centerBox.setFillWidth(true); 62 | centerBox.setPadding(new Insets(30)); 63 | 64 | setTop(topBox); 65 | setCenter(centerBox); 66 | setBottom(infoLabel); 67 | } 68 | 69 | @Override 70 | public void setupUiToActionBindings(SomeController controller) { 71 | // look at that: all EventHandlers just trigger an action on 'controller' by calling a single method 72 | increaseButton.setOnAction (event -> controller.increaseCounter()); 73 | ledButton.setOnMousePressed (event -> controller.setIsActive(true)); 74 | ledButton.setOnMouseReleased(event -> controller.setIsActive(false)); 75 | } 76 | 77 | @Override 78 | public void setupModelToUiBindings(SomeModel model) { 79 | onChangeOf(model.systemInfo) // the value we need to observe, in this case that's an ObservableValue, no need to convert it 80 | .update(infoLabel.textProperty()); // keeps textProperty and systemInfo in sync 81 | 82 | onChangeOf(model.counter) // the value we need to observe, in this case, that's an ObservableValue 83 | .convertedBy(String::valueOf) // we have to convert the Integer to a String 84 | .update(counterLabel.textProperty()); // keeps textProperty and counter in sync 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templateapp/view/gui/SomePuiEmulator.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.view.gui; 2 | 3 | import com.pi4j.mvc.templateapp.controller.SomeController; 4 | import com.pi4j.mvc.templateapp.model.SomeModel; 5 | import com.pi4j.mvc.util.mvcbase.ViewMixin; 6 | 7 | import javafx.geometry.Insets; 8 | import javafx.geometry.Pos; 9 | import javafx.scene.control.Button; 10 | import javafx.scene.control.Label; 11 | import javafx.scene.layout.VBox; 12 | 13 | public class SomePuiEmulator extends VBox implements ViewMixin { 14 | 15 | // for each PUI component, declare a corresponding JavaFX-control 16 | private Label led; 17 | private Button decreaseButton; 18 | 19 | public SomePuiEmulator(SomeController controller){ 20 | init(controller); 21 | } 22 | 23 | @Override 24 | public void initializeSelf() { 25 | setPrefWidth(250); 26 | } 27 | 28 | @Override 29 | public void initializeParts() { 30 | led = new Label(); 31 | decreaseButton = new Button("Decrease"); 32 | } 33 | 34 | @Override 35 | public void layoutParts() { 36 | setPadding(new Insets(20)); 37 | setSpacing(20); 38 | setAlignment(Pos.CENTER); 39 | getChildren().addAll(led, decreaseButton); 40 | } 41 | 42 | @Override 43 | public void setupUiToActionBindings(SomeController controller) { 44 | //trigger the same actions as the real PUI 45 | 46 | decreaseButton.setOnAction(event -> controller.decreaseCounter()); 47 | } 48 | 49 | @Override 50 | public void setupModelToUiBindings(SomeModel model) { 51 | //observe the same values as the real PUI 52 | 53 | onChangeOf(model.isActive) 54 | .convertedBy(active -> active ? "on" : "off") 55 | .update(led.textProperty()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templateapp/view/pui/SomePUI.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.view.pui; 2 | 3 | import com.pi4j.mvc.templateapp.controller.SomeController; 4 | import com.pi4j.mvc.templateapp.model.SomeModel; 5 | import com.pi4j.mvc.util.mvcbase.PuiBase; 6 | 7 | import com.pi4j.catalog.components.base.PIN; 8 | import com.pi4j.catalog.components.SimpleButton; 9 | import com.pi4j.catalog.components.SimpleLed; 10 | 11 | public class SomePUI extends PuiBase { 12 | //declare all hardware components attached to RaspPi 13 | //these are protected to give unit tests access to them 14 | protected SimpleLed led; 15 | protected SimpleButton button; 16 | 17 | public SomePUI(SomeController controller) { 18 | super(controller); 19 | } 20 | 21 | @Override 22 | public void initializeParts() { 23 | led = new SimpleLed(pi4J, PIN.D22); 24 | button = new SimpleButton(pi4J, PIN.D24, false); 25 | } 26 | 27 | @Override 28 | public void setupUiToActionBindings(SomeController controller) { 29 | button.onDown(() -> controller.setIsActive(true)); 30 | button.onUp (() -> controller.decreaseCounter()); 31 | } 32 | 33 | @Override 34 | public void setupModelToUiBindings(SomeModel model) { 35 | onChangeOf(model.isActive) 36 | .execute((oldValue, newValue) -> { 37 | if (newValue) { 38 | led.on(); 39 | } else { 40 | led.off(); 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templatepuiapp/AppStarter.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templatepuiapp; 2 | 3 | import com.pi4j.mvc.templatepuiapp.controller.SomeController; 4 | import com.pi4j.mvc.templatepuiapp.model.SomeModel; 5 | import com.pi4j.mvc.templatepuiapp.view.SomePUI; 6 | 7 | import static com.pi4j.mvc.util.mvcbase.MvcLogger.LOGGER; 8 | 9 | public class AppStarter { 10 | 11 | public static void main(String[] args) { 12 | SomeController controller = new SomeController(new SomeModel()); 13 | SomePUI pui = new SomePUI(controller); 14 | 15 | LOGGER.logInfo("App started"); 16 | 17 | // This will ensure Pi4J is properly finished. All I/O instances are 18 | // released by the system and shutdown in the appropriate 19 | // manner. It will also ensure that any background 20 | // threads/processes are cleanly shutdown and any used memory 21 | // is returned to the system. 22 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 23 | controller.shutdown(); 24 | pui.shutdown(); 25 | })); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templatepuiapp/controller/SomeController.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templatepuiapp.controller; 2 | 3 | import com.pi4j.mvc.templatepuiapp.model.SomeModel; 4 | import com.pi4j.mvc.util.mvcbase.ControllerBase; 5 | 6 | import static com.pi4j.mvc.util.mvcbase.MvcLogger.LOGGER; 7 | 8 | public class SomeController extends ControllerBase { 9 | 10 | protected final int terminationCount = 10; 11 | 12 | public SomeController(SomeModel model) { 13 | super(model); 14 | } 15 | 16 | public void activate(){ 17 | setValue(model.busy, true); 18 | } 19 | 20 | public void deactivate(){ 21 | // use 'updateModel' if you need to set multiple values 22 | updateModel(set(model.busy, false), 23 | increase(model.counter)); 24 | 25 | //using 'runLater' assures that new value is set on model 26 | runLater(m -> { 27 | LOGGER.logInfo("Number of activations: %d", m.counter.getValue()); 28 | if (m.counter.getValue() > terminationCount) { 29 | terminate(); 30 | } 31 | }); 32 | } 33 | 34 | protected void terminate() { 35 | LOGGER.logInfo("Goodbye!"); 36 | System.exit(0); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templatepuiapp/model/SomeModel.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templatepuiapp.model; 2 | 3 | import com.pi4j.mvc.util.mvcbase.ObservableValue; 4 | 5 | public class SomeModel { 6 | public final ObservableValue counter = new ObservableValue<>(0); 7 | public final ObservableValue busy = new ObservableValue<>(false); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/templatepuiapp/view/SomePUI.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templatepuiapp.view; 2 | 3 | import com.pi4j.mvc.templatepuiapp.controller.SomeController; 4 | import com.pi4j.mvc.templatepuiapp.model.SomeModel; 5 | import com.pi4j.mvc.util.mvcbase.PuiBase; 6 | 7 | import com.pi4j.catalog.components.base.PIN; 8 | import com.pi4j.catalog.components.SimpleButton; 9 | import com.pi4j.catalog.components.SimpleLed; 10 | 11 | 12 | public class SomePUI extends PuiBase { 13 | //declare all hardware components attached to RaspPi 14 | //these are protected to give unit tests access to them 15 | protected SimpleLed led; 16 | protected SimpleButton button; 17 | 18 | public SomePUI(SomeController controller) { 19 | super(controller); 20 | } 21 | 22 | @Override 23 | public void initializeParts() { 24 | led = new SimpleLed(pi4J, PIN.D22); 25 | button = new SimpleButton(pi4J, PIN.D24, false); 26 | } 27 | 28 | @Override 29 | public void setupUiToActionBindings(SomeController controller) { 30 | //if the user interacts with one of the parts, always trigger a Controller action 31 | button.onDown(controller::activate); 32 | 33 | //don't call 'led.off()' here. You will miss the Controller logic (increase the terminationCounter and terminate) 34 | button.onUp(controller::deactivate); 35 | } 36 | 37 | @Override 38 | public void setupModelToUiBindings(SomeModel model) { 39 | onChangeOf(model.busy) 40 | .execute((oldValue, newValue) -> { 41 | if (newValue) { 42 | led.on(); 43 | } else { 44 | led.off(); 45 | } 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/ConcurrentTaskQueue.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.time.Duration; 4 | import java.util.concurrent.ConcurrentLinkedQueue; 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.Future; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.function.Consumer; 10 | import java.util.function.Supplier; 11 | 12 | /** 13 | * A device where tasks can be submitted for execution. 14 | *

15 | * Execution is asynchronous - possibly in a different thread - but the sequence is kept stable, such that 16 | * for all tasks A and B: if B is submitted after A, B will only be executed after A is finished. 17 | *

18 | * New tasks can be submitted while tasks are running. 19 | *

20 | * Task submission itself is supposed to be thread-confined, 21 | * i.e., creation of the ConcurrentTaskQueue and task submission is expected to run in the same thread, 22 | * most likely the JavaFX UI Application Thread. 23 | * 24 | * @author Dierk Koenig 25 | */ 26 | 27 | public final class ConcurrentTaskQueue { 28 | private final ExecutorService executor; 29 | private final ConcurrentLinkedQueue> buffer; 30 | private final Duration maxToDoTime; 31 | 32 | private boolean running = false; // for non-thread-confined submissions, we might need an AtomicBoolean 33 | 34 | public ConcurrentTaskQueue() { 35 | this(Duration.ofSeconds(5)); 36 | } 37 | 38 | public ConcurrentTaskQueue(Duration maxToDoTime) { 39 | this.maxToDoTime = maxToDoTime; 40 | this.executor = Executors.newFixedThreadPool(1); // use 2 for overlapping onDone with next to-do 41 | this.buffer = new ConcurrentLinkedQueue<>(); 42 | } 43 | 44 | public void shutdown() { 45 | executor.shutdown(); 46 | } 47 | 48 | public void submit(Supplier todo) { 49 | submit(todo, r -> { 50 | }); 51 | } 52 | 53 | public void submit(Supplier todo, Consumer onDone) { 54 | buffer.add(new Task<>(todo, onDone)); 55 | execute(); 56 | } 57 | 58 | private void execute() { 59 | if (running) { 60 | return; 61 | } 62 | 63 | final Task task = buffer.poll(); 64 | 65 | if(task == null){ 66 | return; 67 | } 68 | 69 | running = true; 70 | 71 | final Future todoFuture = executor.submit(task.todo::get); 72 | 73 | Runnable onDoneRunnable = () -> { 74 | try { 75 | final R r = todoFuture.get(maxToDoTime.getSeconds(), TimeUnit.SECONDS); 76 | task.onDone.accept(r); 77 | } catch (Exception e) { 78 | e.printStackTrace(); // todo: think about better exception handling 79 | } finally { 80 | running = false; 81 | execute(); 82 | } 83 | }; 84 | executor.submit(onDoneRunnable); 85 | } 86 | 87 | private static class Task { 88 | private final Supplier todo; // the return type of to-do .. 89 | private final Consumer onDone; // .. must match the input type of onDone 90 | 91 | public Task(Supplier todo, Consumer onDone) { 92 | this.todo = todo; 93 | this.onDone = onDone; 94 | } 95 | } 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/ControllerBase.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.time.Duration; 4 | import java.util.Objects; 5 | import java.util.concurrent.CountDownLatch; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.function.Consumer; 8 | import java.util.function.Supplier; 9 | 10 | /** 11 | * Base class for all Controllers. 12 | *

13 | * The whole application logic is located in controller classes. 14 | *

15 | * Controller classes work on and manage the Model. Models encapsulate the whole application state. 16 | *

17 | * Controllers provide the whole core functionality of the application, so called 'Actions' 18 | *

19 | * Execution of Actions is asynchronous. The sequence is kept stable, such that 20 | * for all actions A and B: if B is submitted after A, B will only be executed after A is finished. 21 | */ 22 | public abstract class ControllerBase { 23 | 24 | private ConcurrentTaskQueue actionQueue; 25 | 26 | // the model managed by this Controller. Only subclasses have direct access 27 | protected final M model; 28 | 29 | /** 30 | * The Controller needs a Model. 31 | * 32 | * @param model Model managed by this Controller 33 | */ 34 | protected ControllerBase(M model){ 35 | Objects.requireNonNull(model); 36 | 37 | this.model = model; 38 | } 39 | 40 | public void shutdown(){ 41 | if(null != actionQueue){ 42 | actionQueue.shutdown(); 43 | actionQueue = null; 44 | } 45 | } 46 | 47 | /** 48 | * If anything needs to be run once at startup from the controller 49 | */ 50 | public void startUp(){} 51 | 52 | /** 53 | * Schedule the given action for execution in strict order in external thread, asynchronously. 54 | *

55 | * onDone is called as soon as action is finished 56 | */ 57 | protected void async(Supplier action, Consumer onDone) { 58 | if(null == actionQueue){ 59 | actionQueue = new ConcurrentTaskQueue<>(); 60 | } 61 | actionQueue.submit(action, onDone); 62 | } 63 | 64 | 65 | /** 66 | * Schedule the given action for execution in strict order in external thread, asynchronously. 67 | * 68 | */ 69 | protected void async(Runnable todo){ 70 | async(() -> { 71 | todo.run(); 72 | return model; 73 | }, 74 | m -> {}); 75 | } 76 | 77 | /** 78 | * Schedule the given action after all the actions already scheduled have finished. 79 | * 80 | */ 81 | public void runLater(Consumer action) { 82 | async(() -> model, action); 83 | } 84 | 85 | /** 86 | * Intermediate solution for TestCase support. 87 | *

88 | * The best solution would be that 'action' of 'runLater' is executed on calling thread. 89 | *

90 | * Waits until all current actions in actionQueue are completed. 91 | *

92 | * In most cases it's wrong to call this method from within an application. 93 | */ 94 | public void awaitCompletion(){ 95 | if(actionQueue == null){ 96 | return; 97 | } 98 | 99 | CountDownLatch latch = new CountDownLatch(1); 100 | actionQueue.submit( () -> { 101 | latch.countDown(); 102 | return null; 103 | }); 104 | try { 105 | //noinspection ResultOfMethodCallIgnored 106 | latch.await(5, TimeUnit.SECONDS); 107 | } catch (InterruptedException e) { 108 | throw new IllegalStateException("CountDownLatch was interrupted"); 109 | } 110 | } 111 | 112 | /** 113 | * Only the other base classes 'ViewMixin' and 'PUI_Base' need access, therefore it's 'package private' 114 | */ 115 | M getModel() { 116 | return model; 117 | } 118 | 119 | /** 120 | * Even for setting a value the controller is responsible. 121 | *

122 | * No application-specific class can access ObservableValue.setValue 123 | *

124 | * The value is set asynchronously. 125 | */ 126 | protected void setValue(ObservableValue observableValue, V newValue){ 127 | async(() -> observableValue.setValue(newValue)); 128 | } 129 | 130 | /** 131 | * Even for setting values in the array, the controller is responsible. 132 | *

133 | * No application-specific class can access ObservableValue.setValues 134 | *

135 | * Values are set asynchronously. 136 | */ 137 | protected void setValues(ObservableArray observableArray, V[] newValues){ 138 | async(() -> observableArray.setValues(newValues)); 139 | } 140 | 141 | /** 142 | * Even for setting a value in the array, the controller is responsible. 143 | *

144 | * No application-specific class can access ObservableValue.setValue 145 | *

146 | * The value is set asynchronously. 147 | */ 148 | protected void setValue(ObservableArray observableArray, int position, V newValue){ 149 | async(() -> observableArray.setValue(position, newValue)); 150 | } 151 | 152 | protected V get(ObservableValue observableValue){ 153 | return observableValue.getValue(); 154 | } 155 | 156 | protected V[] get(ObservableArray observableArray){ 157 | return observableArray.getValues(); 158 | } 159 | 160 | protected V get(ObservableArray observableArray, int position){ 161 | return observableArray.getValue(position); 162 | } 163 | 164 | /** 165 | * Convenience method to toggle an ObservableValue 166 | */ 167 | protected void toggleValue(ObservableValue observableValue){ 168 | async(() -> observableValue.setValue(!observableValue.getValue())); 169 | } 170 | 171 | /** 172 | * Convenience method to toggle an ObservableArray at position x 173 | */ 174 | protected void toggle(ObservableArray observableArray, int position){ 175 | async(() -> observableArray.setValue(position, !observableArray.getValue(position))); 176 | } 177 | 178 | /** 179 | * Convenience method to increase a~ ObservableValue by 1 180 | */ 181 | protected void increaseValue(ObservableValue observableValue){ 182 | async(() -> observableValue.setValue(observableValue.getValue() + 1)); 183 | } 184 | 185 | /** 186 | * Convenience method to increase an ObservableArray by 1 at position x 187 | */ 188 | protected void increase(ObservableArray observableArray, int position){ 189 | async(() -> observableArray.setValue(position, observableArray.getValue(position) + 1)); 190 | } 191 | 192 | /** 193 | * Convenience method to decrease a~ ObservableValue by 1 194 | */ 195 | protected void decreaseValue(ObservableValue observableValue){ 196 | async(() -> observableValue.setValue(observableValue.getValue() - 1)); 197 | } 198 | 199 | /** 200 | * Convenience method to decrease a~ ObservableArray by 1 at position x 201 | */ 202 | protected void decrease(ObservableArray observableArray, int position){ 203 | async(() -> observableArray.setValue(position, observableArray.getValue(position) - 1)); 204 | } 205 | 206 | /** 207 | * Utility function to pause execution of actions for the specified amount of time. 208 | *

209 | * An {@link InterruptedException} will be catched and ignored while setting the interrupt flag again. 210 | * 211 | * @param duration time to sleep 212 | */ 213 | protected void pauseExecution(Duration duration) { 214 | async(() -> { 215 | try { 216 | Thread.sleep(duration.toMillis()); 217 | } catch (InterruptedException e) { 218 | Thread.currentThread().interrupt(); 219 | } 220 | }); 221 | } 222 | 223 | /** 224 | * Use this if you need to update several ObservableValues in one async call. 225 | *

226 | * Use 'set', 'increase', 'decrease' or 'toggle' to get an appropriate Setter 227 | */ 228 | protected void updateModel(Setter... setters){ 229 | async(() -> { 230 | for (Setter setter : setters) { 231 | setter.setValue(); 232 | } 233 | }); 234 | } 235 | 236 | protected Setter set(ObservableValue observableValue, V value){ 237 | return new Setter(observableValue, () -> value); 238 | } 239 | 240 | protected Setter increase(ObservableValue observableValue){ 241 | return new Setter<>(observableValue, () -> get(observableValue) + 1); 242 | } 243 | 244 | protected Setter decrease(ObservableValue observableValue){ 245 | return new Setter<>(observableValue, () -> get(observableValue) - 1); 246 | } 247 | 248 | protected Setter toggle(ObservableValue observableValue){ 249 | return new Setter<>(observableValue, () -> !get(observableValue)); 250 | } 251 | 252 | protected ArraySetter set(ObservableArray observableArray, V[] values){ 253 | return new ArraySetter<>(observableArray, values); 254 | } 255 | 256 | protected static class Setter { 257 | private final ObservableValue observableValue; 258 | 259 | // supplier is used here to get the value at execution time and not at registration time 260 | private final Supplier valueSupplier; 261 | 262 | private Setter(ObservableValue observableValue, Supplier valueSupplier) { 263 | this.observableValue = observableValue; 264 | this.valueSupplier = valueSupplier; 265 | } 266 | 267 | void setValue() { 268 | observableValue.setValue(valueSupplier.get()); 269 | } 270 | } 271 | 272 | protected static class ArraySetter { 273 | private final ObservableArray observableArray; 274 | private final V[] values; 275 | 276 | private ArraySetter(ObservableArray observableArray, V[] values) { 277 | this.observableArray = observableArray; 278 | this.values = values; 279 | } 280 | 281 | void setValue() { 282 | observableArray.setValues(values); 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/MvcLogger.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.logging.ConsoleHandler; 4 | import java.util.logging.Level; 5 | import java.util.logging.Logger; 6 | 7 | public final class MvcLogger { 8 | private final Logger logger = Logger.getLogger("Pi4J Template Project");; 9 | 10 | public static final MvcLogger LOGGER = new MvcLogger(); 11 | 12 | private MvcLogger(){ 13 | Level appropriateLevel = Level.INFO; 14 | //Level appropriateLevel = Level.FINE; //use if 'debug' 15 | 16 | System.setProperty("java.util.logging.SimpleFormatter.format", 17 | "%4$s: %5$s [%1$tl:%1$tM:%1$tS %1$Tp]%n"); 18 | 19 | logger.setLevel(appropriateLevel); 20 | logger.setUseParentHandlers(false); 21 | ConsoleHandler handler = new ConsoleHandler(); 22 | handler.setLevel(appropriateLevel); 23 | logger.addHandler(handler); 24 | } 25 | 26 | public void logInfo(String msg, Object... args) { 27 | logger.info(() -> String.format(msg, args)); 28 | } 29 | 30 | public void logError(String msg, Object... args) { 31 | logger.severe(() -> String.format(msg, args)); 32 | } 33 | 34 | public void logConfig(String msg, Object... args) { 35 | logger.config(() -> String.format(msg, args)); 36 | } 37 | 38 | public void logDebug(String msg, Object... args) { 39 | logger.fine(() -> String.format(msg, args)); 40 | } 41 | 42 | public void logException(String msg, Throwable exception){ 43 | logger.log(Level.SEVERE, msg, exception); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/ObservableArray.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.Objects; 6 | import java.util.Set; 7 | 8 | public final class ObservableArray { 9 | /** all these listeners will get notified whenever the value changes */ 10 | private final Set> listeners = new HashSet<>(); 11 | 12 | /** 13 | * The actual values of the array 14 | */ 15 | private volatile V[] values; 16 | 17 | /** 18 | * Default Constructor 19 | * @param initialValues the initial array 20 | */ 21 | public ObservableArray(V[] initialValues) { 22 | values = initialValues; 23 | } 24 | 25 | /** 26 | * Registers a new observer (aka 'listener') 27 | * 28 | * @param listener specifies what needs to be done whenever the value is changed 29 | */ 30 | public void onChange(ValueChangeListener listener) { 31 | listeners.add(listener); 32 | listener.update(values, values); // listener is notified immediately 33 | } 34 | 35 | /** 36 | * That's the core functionality of an 'ObservableValue'. 37 | *

38 | * Every time the value changes, all the listeners will be notified. 39 | *

40 | * This is method is 'package private', only 'ControllerBase' is allowed to set a new value. 41 | *

42 | * For the UIs setValue is not accessible 43 | * 44 | * @param newValues the new value 45 | */ 46 | void setValues(V[] newValues) { 47 | if (Arrays.equals(values, newValues)) { // no notification if value hasn't changed 48 | return; 49 | } 50 | V[] oldValues = values.clone(); 51 | values = newValues; 52 | 53 | listeners.forEach(listener -> { 54 | if (Arrays.equals(values, newValues)) { // pre-ordered listeners might have changed this and thus the callback no longer applies 55 | listener.update(oldValues, newValues); 56 | } 57 | }); 58 | } 59 | 60 | /** 61 | * That's the core functionality of an 'ObservableValue'. 62 | *

63 | * Every time the value changes, all the listeners will be notified. 64 | *

65 | * This is method is 'package private', only 'ControllerBase' is allowed to set a new value. 66 | *

67 | * For the UIs setValue is not accessible 68 | * 69 | * @param newValue the new value 70 | */ 71 | void setValue(int position, V newValue) { 72 | if (Objects.equals(values[position], newValue)) { // no notification if value hasn't changed 73 | return; 74 | } 75 | V[] oldValues = values.clone(); 76 | values[position] = newValue; 77 | 78 | listeners.forEach(listener -> listener.update(oldValues, values)); 79 | } 80 | 81 | /** 82 | * It's ok to make this public. 83 | * 84 | * @return the value managed by this ObservableValues 85 | */ 86 | public V[] getValues() { 87 | return values; 88 | } 89 | 90 | /** 91 | * It's ok to make this public. 92 | * 93 | * @return the value managed by this ObservableValue 94 | */ 95 | public V getValue(int position) { 96 | return values[position]; 97 | } 98 | 99 | /** 100 | * Giving the array out as a String 101 | * @return String with the values of the array 102 | */ 103 | @Override 104 | public String toString() { 105 | return Arrays.toString(values); 106 | } 107 | 108 | @FunctionalInterface 109 | public interface ValueChangeListener { 110 | void update(V[] oldValue, V[] newValue); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/ObservableValue.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.HashSet; 4 | import java.util.Objects; 5 | import java.util.Set; 6 | 7 | /** 8 | * A basic implementation of the Observable-Pattern. 9 | *

10 | * Be prepared to enhance this according to your requirements. 11 | */ 12 | public final class ObservableValue { 13 | // all these listeners will get notified whenever the value changes 14 | private final Set> listeners = new HashSet<>(); 15 | 16 | private volatile V value; 17 | 18 | public ObservableValue(V initialValue) { 19 | value = initialValue; 20 | } 21 | 22 | /** 23 | * Registers a new observer (aka 'listener') 24 | * 25 | * @param listener specifies what needs to be done whenever the value is changed 26 | */ 27 | public void onChange(ValueChangeListener listener) { 28 | listeners.add(listener); 29 | listener.update(value, value); // listener is notified immediately 30 | } 31 | 32 | /** 33 | * That's the core functionality of an 'ObservableValue'. 34 | *

35 | * Every time the value changes, all the listeners will be notified. 36 | *

37 | * This is method is 'package private', only 'ControllerBase' is allowed to set a new value. 38 | *

39 | * For the UIs setValue is not accessible 40 | * 41 | * @param newValue the new value 42 | */ 43 | void setValue(V newValue) { 44 | if (Objects.equals(value, newValue)) { // no notification if value hasn't changed 45 | return; 46 | } 47 | V oldValue = value; 48 | value = newValue; 49 | 50 | listeners.forEach(listener -> { 51 | if (value.equals(newValue)) { // pre-ordered listeners might have changed this and thus the callback no longer applies 52 | listener.update(oldValue, newValue); 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * It's ok to make this public. 59 | * 60 | * @return the value managed by this ObservableValue 61 | */ 62 | public V getValue() { 63 | return value; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return value.toString(); 69 | } 70 | 71 | @FunctionalInterface 72 | public interface ValueChangeListener { 73 | void update(V oldValue, V newValue); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/Projector.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Projector is the common interface for both, GUI and PUI. 7 | *

8 | * See Prof. Dierk Koenig's conference talk 9 | */ 10 | interface Projector> { 11 | 12 | /** 13 | * needs to be called inside the constructor of your UI-part 14 | */ 15 | default void init(C controller) { 16 | Objects.requireNonNull(controller); 17 | initializeSelf(); 18 | initializeParts(); 19 | setupUiToActionBindings(controller); 20 | setupModelToUiBindings(controller.getModel()); 21 | } 22 | 23 | /** 24 | * Everything that needs to be done to initialize the UI-part itself. 25 | *

26 | * For GUIs loading stylesheet-files or additional fonts are typical examples. 27 | */ 28 | default void initializeSelf(){ 29 | } 30 | 31 | /** 32 | * completely initialize all necessary UI-elements (like buttons, text-fields, etc. on GUI or distance sensors on PUI ) 33 | */ 34 | void initializeParts(); 35 | 36 | 37 | /** 38 | * Triggering some action on Controller if the user interacts with the UI. 39 | *

40 | * There's no need to have access to model for this task. 41 | *

42 | * All EventHandlers will call a single method on the Controller. 43 | *

44 | * If you are about to call more than one method, you should introduce a new method on Controller. 45 | */ 46 | default void setupUiToActionBindings(C controller) { 47 | } 48 | 49 | /** 50 | * Whenever an 'ObservableValue' in 'model' changes, the UI must be updated. 51 | *

52 | * There's no need to have access to controller for this task. 53 | *

54 | * Register all necessary observers here. 55 | */ 56 | default void setupModelToUiBindings(M model) { 57 | } 58 | 59 | /** 60 | * At the Startup, this method gets called. 61 | *

62 | * Perfect, if a function in the controller or in the pui needs to be run exactly once. 63 | */ 64 | default void startUp(C controller) { 65 | controller.startUp(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/PuiBase.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.ExecutorService; 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.function.Consumer; 8 | import java.util.function.Supplier; 9 | 10 | import com.pi4j.Pi4J; 11 | import com.pi4j.context.Context; 12 | 13 | /** 14 | * Base class for all PUIs. 15 | *

16 | * In our scenario, we also have a GUI. 17 | *

18 | * We have to avoid that one of the UIs is blocked because the other UI has to perform a long-running task. 19 | *

20 | * Therefore, we need an additional "worker-thread" in both UIs. 21 | *

22 | * For JavaFX-based GUIs that's already available (the JavaFX Application Thread). 23 | *

24 | * For PUIs, we need to do that ourselves. It's implemented as a provider/consumer-pattern (see {@link ConcurrentTaskQueue}). 25 | */ 26 | public abstract class PuiBase> implements Projector{ 27 | 28 | // all PUI actions should be done asynchronously (to avoid UI freezing) 29 | private final ConcurrentTaskQueue queue = new ConcurrentTaskQueue<>(); 30 | 31 | protected final Context pi4J; 32 | 33 | public PuiBase(C controller) { 34 | Objects.requireNonNull(controller); 35 | 36 | pi4J = Pi4J.newAutoContext(); 37 | 38 | init(controller); 39 | } 40 | 41 | public void shutdown(){ 42 | pi4J.shutdown(); 43 | queue.shutdown(); 44 | } 45 | 46 | protected void async(Supplier todo, Consumer onDone) { 47 | queue.submit(todo, onDone); 48 | } 49 | 50 | public void runLater(Consumer todo) { 51 | async(() -> null, todo); 52 | } 53 | 54 | /** 55 | * Intermediate solution for TestCase support. 56 | *

57 | * The best solution would be that 'action' of 'runLater' is executed on calling thread. 58 | *

59 | * Waits until all current actions in actionQueue are completed. 60 | * 61 | */ 62 | public void awaitCompletion(){ 63 | final ExecutorService waitForFinishedService = Executors.newFixedThreadPool(1); 64 | // would be nice if this could just be a method reference 65 | async(() -> { 66 | waitForFinishedService.shutdown(); 67 | return null; 68 | }, 69 | unused -> { 70 | }); 71 | try { 72 | //noinspection ResultOfMethodCallIgnored 73 | waitForFinishedService.awaitTermination(5, TimeUnit.SECONDS); 74 | } catch (InterruptedException e) { 75 | throw new IllegalThreadStateException(); // very unlikely to happen 76 | } 77 | } 78 | 79 | /** 80 | * First step to register an observer. 81 | * 82 | * @param observableValue the value that should trigger some PUI-updates 83 | * @return an Updater to specify what needs to be done whenever observableValue changes 84 | */ 85 | protected Updater onChangeOf(ObservableValue observableValue) { 86 | return new Updater<>(observableValue); 87 | } 88 | 89 | /** 90 | * First step to register an observer. 91 | * 92 | * @param observableArray the value that should trigger some PUI-updates 93 | * @return an Updater to specify what needs to be done whenever observableValue changes 94 | */ 95 | protected ArrayUpdater onChangeOf(ObservableArray observableArray) { 96 | return new ArrayUpdater<>(observableArray); 97 | } 98 | 99 | /** 100 | * Second step to specify an observer. 101 | *

102 | * Use 'triggerPUIAction' to specify what needs to be done whenever the observed value changes 103 | */ 104 | public class Updater { 105 | private final ObservableValue observableValue; 106 | 107 | Updater(ObservableValue observableValue) { 108 | this.observableValue = observableValue; 109 | } 110 | 111 | public void execute(ObservableValue.ValueChangeListener action) { 112 | observableValue.onChange((oldValue, newValue) -> queue.submit(() -> { 113 | action.update(oldValue, newValue); 114 | return null; 115 | })); 116 | } 117 | 118 | public void execute(Consumer action){ 119 | observableValue.onChange((oldValue, newValue) -> 120 | queue.submit(() -> { 121 | action.accept(newValue); 122 | return null; 123 | })); 124 | } 125 | } 126 | 127 | /** 128 | * Second step to specify an observer. 129 | *

130 | * Use 'triggerPUIAction' to specify what needs to be done whenever the observed array changes 131 | */ 132 | public class ArrayUpdater { 133 | private final ObservableArray observableArray; 134 | 135 | ArrayUpdater(ObservableArray observableArray) { this.observableArray = observableArray; } 136 | 137 | public void execute(ObservableArray.ValueChangeListener action) { 138 | observableArray.onChange((oldValue, newValue) -> queue.submit(() -> { 139 | action.update(oldValue, newValue); 140 | return null; 141 | })); 142 | } 143 | } 144 | } 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/mvc/util/mvcbase/ViewMixin.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | 8 | import javafx.application.Platform; 9 | import javafx.beans.property.DoubleProperty; 10 | import javafx.beans.property.IntegerProperty; 11 | import javafx.beans.property.Property; 12 | import javafx.scene.text.Font; 13 | 14 | /** 15 | * Use this interface for all of your GUI-parts to assure implementation consistency. 16 | *

17 | * It provides the basic functionality to make MVC run. 18 | */ 19 | public interface ViewMixin> extends Projector { 20 | 21 | @Override 22 | default void init(C controller) { 23 | Projector.super.init(controller); 24 | layoutParts(); 25 | } 26 | 27 | /** 28 | * the method name says it all 29 | */ 30 | void layoutParts(); 31 | 32 | /** 33 | * just a convenience method to load stylesheet files 34 | * 35 | * @param stylesheetFiles name of the stylesheet file 36 | */ 37 | default void addStylesheetFiles(String... stylesheetFiles){ 38 | for(String file : stylesheetFiles){ 39 | String stylesheet = Objects.requireNonNull(getClass().getResource(file)).toExternalForm(); 40 | getStylesheets().add(stylesheet); 41 | } 42 | } 43 | 44 | /** 45 | * just a convenience method to load additional fonts 46 | */ 47 | default void loadFonts(String... fonts){ 48 | for(String f : fonts){ 49 | Font.loadFont(getClass().getResourceAsStream(f), 0); 50 | } 51 | } 52 | 53 | List getStylesheets(); 54 | 55 | /** 56 | * Starting point for registering an observer. 57 | * 58 | * @param observableValue the value that needs to be observed 59 | * 60 | * @return a 'Converter' to specify a function converting the type of 'ObservableValue' into the type of the 'Property' 61 | */ 62 | default Converter onChangeOf(ObservableValue observableValue){ 63 | return new Converter<>(observableValue); 64 | } 65 | 66 | /** 67 | * 68 | */ 69 | record Converter(ObservableValue observableValue) { 70 | 71 | /** 72 | * Second (optional) step for registering an observer to specify a converter-function 73 | * 74 | * @param converter the function converting the type of 'ObservableValue' into the type of the 'Property' 75 | * @return an Updater to specify the 'GUI-Property' that needs to be updated if 'ObservableValue' changes 76 | */ 77 | public Updater convertedBy(Function converter) { 78 | return new Updater<>(observableValue, converter); 79 | } 80 | 81 | /** 82 | * Registers an observer without any type conversion that will keep property-value and observableValue in sync. 83 | * 84 | * @param property GUI-Property that will be updated when observableValue changes 85 | */ 86 | public void update(Property property) { 87 | execute((oldValue, newValue) -> property.setValue(newValue)); 88 | } 89 | 90 | /** 91 | * Registers an observer. 92 | * 93 | * @param listener whatever needs to be done on GUI when observableValue changes 94 | */ 95 | public void execute(ObservableValue.ValueChangeListener listener) { 96 | observableValue.onChange((oldValue, newValue) -> Platform.runLater(() -> listener.update(oldValue, newValue))); 97 | } 98 | } 99 | 100 | record Updater(ObservableValue observableValue, Function converter) { 101 | 102 | /** 103 | * Registers an observer that will keep observableValue and GUI-Property in sync by applying the specified converter. 104 | * 105 | * @param property GUI-Property that will be updated when observableValue changes 106 | */ 107 | public void update(Property property) { 108 | observableValue.onChange((oldValue, newValue) -> { 109 | P convertedValue = converter.apply(newValue); 110 | Platform.runLater(() -> property.setValue(convertedValue)); 111 | }); 112 | } 113 | } 114 | 115 | default ActionTrigger onChangeOf(Property property){ 116 | return new ActionTrigger<>(property); 117 | } 118 | 119 | default ActionTrigger onChangeOf(DoubleProperty property){ 120 | return new ActionTrigger<>(property); 121 | } 122 | 123 | default ActionTrigger onChangeOf(IntegerProperty property){ 124 | return new ActionTrigger<>(property); 125 | } 126 | 127 | record ActionTrigger(Property property) { 128 | 129 | public void triggerAction(Consumer action) { 130 | property.addListener((observableValue, oldValue, newValue) -> action.accept((V) newValue)); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/pi4j/setup/HelloFX.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.setup; 2 | 3 | import java.util.Objects; 4 | 5 | import javafx.application.Application; 6 | import javafx.geometry.Pos; 7 | import javafx.scene.Scene; 8 | import javafx.scene.control.Button; 9 | import javafx.scene.control.Label; 10 | import javafx.scene.image.Image; 11 | import javafx.scene.image.ImageView; 12 | import javafx.scene.layout.VBox; 13 | import javafx.stage.Stage; 14 | 15 | /** 16 | * A tiny tool to check the basic setup of JavaFX. 17 | *

18 | * It's a long way from here to a real application. 19 | *

20 | * But as long as 'HelloFX' isn't working properly, it's useless to go any further. 21 | *

22 | * It's not meant to be any kind of template to start your development. 23 | *

24 | * Initially copied from https://github.com/openjfx/samples/blob/master/CommandLine/Modular/CLI/hellofx/src/hellofx/HelloFX.java 25 | * 26 | */ 27 | public class HelloFX extends Application { 28 | 29 | @Override 30 | public void start(Stage stage) { 31 | String javaVersion = System.getProperty("java.version"); 32 | String javafxVersion = System.getProperty("javafx.version"); 33 | 34 | Label lbl = new Label("JavaFX " + javafxVersion + ", running on Java " + javaVersion + "."); 35 | 36 | Button btn = new Button("Say Hello"); 37 | btn.setOnAction(event -> lbl.setText("Hello")); 38 | 39 | ImageView imgView = new ImageView(new Image(Objects.requireNonNull(HelloFX.class.getResourceAsStream("/setup/openduke.png")))); 40 | imgView.setFitHeight(200); 41 | imgView.setPreserveRatio(true); 42 | 43 | VBox rootPane = new VBox(50, imgView, lbl, btn); 44 | rootPane.setAlignment(Pos.CENTER); 45 | rootPane.getStylesheets().add(Objects.requireNonNull(HelloFX.class.getResource("/setup/style.css")).toExternalForm()); 46 | 47 | Scene scene = new Scene(rootPane, 640, 480); 48 | stage.setTitle("Plain JavaFX App"); 49 | 50 | stage.setScene(scene); 51 | stage.show(); 52 | } 53 | 54 | public static void main(String[] args) { 55 | launch(); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | open module com.pi4j.mvc { 2 | // Pi4J Modules 3 | requires com.pi4j; 4 | requires com.pi4j.library.linuxfs; 5 | requires com.pi4j.plugin.mock; 6 | requires com.pi4j.plugin.linuxfs; 7 | uses com.pi4j.extension.Extension; 8 | uses com.pi4j.provider.Provider; 9 | 10 | // for logging 11 | requires java.logging; 12 | 13 | // JavaFX 14 | requires javafx.base; 15 | requires javafx.controls; 16 | 17 | 18 | // Module Exports 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/fonts/Lato/Lato-Bla.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/src/main/resources/fonts/Lato/Lato-Bla.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/Lato/Lato-Bol.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/src/main/resources/fonts/Lato/Lato-Bol.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/Lato/Lato-Hai.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/src/main/resources/fonts/Lato/Lato-Hai.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/Lato/Lato-Lig.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/src/main/resources/fonts/Lato/Lato-Lig.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/Lato/Lato-Reg.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/src/main/resources/fonts/Lato/Lato-Reg.ttf -------------------------------------------------------------------------------- /src/main/resources/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/src/main/resources/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/resources/mvc/multicontrollerapp/style.css: -------------------------------------------------------------------------------- 1 | .root-pane { 2 | -fx-padding: 10; 3 | -fx-font-family: 'Lato Light'; 4 | -fx-font-size: 16; 5 | -fx-pref-width: 350; 6 | -fx-pref-height: 400; 7 | -fx-background-color: white; 8 | } 9 | 10 | .root-pane .info-label { 11 | -fx-font-size: 10; 12 | } 13 | 14 | .root-pane .counter-label { 15 | -fx-font-size: 152; 16 | } 17 | 18 | .root-pane .icon-button { 19 | -fx-font-family: 'FontAwesome'; 20 | -fx-font-size: 28; 21 | -fx-min-width: 60; 22 | -fx-min-height: 60; 23 | -fx-background-radius: 30; 24 | -fx-content-display: text-only; 25 | -fx-cursor: hand; 26 | -fx-text-fill: #f3a83b; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/mvc/templateapp/style.css: -------------------------------------------------------------------------------- 1 | .root-pane { 2 | -fx-padding: 10; 3 | -fx-font-family: 'Lato Light'; 4 | -fx-font-size: 16; 5 | -fx-pref-width: 350; 6 | -fx-pref-height: 400; 7 | -fx-background-color: white; 8 | } 9 | 10 | .root-pane .info-label { 11 | -fx-font-size: 10; 12 | } 13 | 14 | .root-pane .counter-label { 15 | -fx-font-size: 152; 16 | } 17 | 18 | .root-pane .icon-button { 19 | -fx-font-family: 'FontAwesome'; 20 | -fx-font-size: 28; 21 | -fx-min-width: 60; 22 | -fx-min-height: 60; 23 | -fx-background-radius: 30; 24 | -fx-content-display: text-only; 25 | -fx-cursor: hand; 26 | -fx-text-fill: #f3a83b; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/setup/openduke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pi4J/pi4j-template-javafx/de36a9e6b089bf9dbe71ecbd7b9a618b0917d847/src/main/resources/setup/openduke.png -------------------------------------------------------------------------------- /src/main/resources/setup/style.css: -------------------------------------------------------------------------------- 1 | .label { 2 | -fx-text-fill: blue; 3 | -fx-font-size: 24; 4 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/catalog/ComponentTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.BeforeEach; 5 | 6 | import com.pi4j.Pi4J; 7 | import com.pi4j.context.Context; 8 | import com.pi4j.plugin.mock.platform.MockPlatform; 9 | import com.pi4j.plugin.mock.provider.gpio.analog.MockAnalogInputProvider; 10 | import com.pi4j.plugin.mock.provider.gpio.analog.MockAnalogOutputProvider; 11 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalInputProvider; 12 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalOutputProvider; 13 | import com.pi4j.plugin.mock.provider.i2c.MockI2CProvider; 14 | import com.pi4j.plugin.mock.provider.pwm.MockPwmProvider; 15 | import com.pi4j.plugin.mock.provider.serial.MockSerialProvider; 16 | import com.pi4j.plugin.mock.provider.spi.MockSpiProvider; 17 | 18 | public abstract class ComponentTest { 19 | protected Context pi4j; 20 | 21 | @BeforeEach 22 | public final void setUpPi4J() { 23 | pi4j = Pi4J.newContextBuilder() 24 | .add(new MockPlatform()) 25 | .add( 26 | MockAnalogInputProvider.newInstance(), 27 | MockAnalogOutputProvider.newInstance(), 28 | MockSpiProvider.newInstance(), 29 | MockPwmProvider.newInstance(), 30 | MockSerialProvider.newInstance(), 31 | MockI2CProvider.newInstance(), 32 | MockDigitalInputProvider.newInstance(), 33 | MockDigitalOutputProvider.newInstance() 34 | ) 35 | .build(); 36 | } 37 | 38 | @AfterEach 39 | public void tearDownPi4J() { 40 | pi4j.shutdown(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/pi4j/catalog/components/SimpleButtonTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.Duration; 7 | 8 | import com.pi4j.io.gpio.digital.DigitalState; 9 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalInput; 10 | 11 | import com.pi4j.catalog.ComponentTest; 12 | import com.pi4j.catalog.components.base.PIN; 13 | 14 | import static java.lang.Thread.sleep; 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | public class SimpleButtonTest extends ComponentTest { 18 | 19 | private SimpleButton button; 20 | private MockDigitalInput digitalInput; 21 | private final PIN pinNumber = PIN.D26; 22 | 23 | @BeforeEach 24 | public void setUp() { 25 | button = new SimpleButton(pi4j, pinNumber, false); 26 | digitalInput = button.mock(); 27 | } 28 | 29 | @Test 30 | public void testButtonState() { 31 | //when 32 | digitalInput.mockState(DigitalState.HIGH); 33 | 34 | //then 35 | assertTrue(button.isDown()); 36 | assertFalse(button.isUp()); 37 | 38 | //when 39 | digitalInput.mockState(DigitalState.LOW); 40 | 41 | //then 42 | assertTrue(button.isUp()); 43 | assertFalse(button.isDown()); 44 | } 45 | 46 | @Test 47 | public void testButtonStateOfInvertedButton(){ 48 | //given 49 | SimpleButton invertedButton = new SimpleButton(pi4j, PIN.D21, true); 50 | digitalInput = invertedButton.mock(); 51 | 52 | //when 53 | digitalInput.mockState(DigitalState.LOW); 54 | 55 | //then 56 | assertTrue(invertedButton.isDown()); 57 | assertFalse(invertedButton.isUp()); 58 | 59 | //when 60 | digitalInput.mockState(DigitalState.HIGH); 61 | 62 | //then 63 | assertFalse(invertedButton.isDown()); 64 | assertTrue(invertedButton.isUp()); 65 | } 66 | 67 | @Test 68 | public void testPinNumber() { 69 | assertEquals(pinNumber.getPin(), button.pinNumber()); 70 | } 71 | 72 | @Test 73 | public void testOnDown() { 74 | //given 75 | Counter counter = new Counter(); 76 | 77 | digitalInput.mockState(DigitalState.LOW); 78 | 79 | button.onDown(() -> counter.increase()); 80 | 81 | //when 82 | digitalInput.mockState(DigitalState.HIGH); 83 | 84 | //then 85 | assertEquals(1, counter.count); 86 | 87 | //when 88 | digitalInput.mockState(DigitalState.HIGH); 89 | 90 | //then 91 | assertEquals(1, counter.count); 92 | 93 | //when 94 | digitalInput.mockState(DigitalState.LOW); 95 | 96 | //then 97 | assertEquals(1, counter.count); 98 | 99 | //when 100 | digitalInput.mockState(DigitalState.HIGH); 101 | 102 | //then 103 | assertEquals(2, counter.count); 104 | 105 | //when 106 | button.reset(); 107 | 108 | //then 109 | assertTrue(button.isInInitialState()); 110 | } 111 | 112 | @Test 113 | public void testOnUp() { 114 | //given 115 | Counter counter = new Counter(); 116 | 117 | digitalInput.mockState(DigitalState.LOW); 118 | 119 | // counter should be increased whenever button gets depressed 120 | // or, in other words, whenever the state of GPIO-Pin switches from HIGH to LOW 121 | 122 | button.onUp(() -> counter.increase()); 123 | 124 | //when 125 | digitalInput.mockState(DigitalState.HIGH); 126 | 127 | //then 128 | assertEquals(0, counter.count); 129 | 130 | //when 131 | digitalInput.mockState(DigitalState.LOW); 132 | 133 | //then 134 | assertEquals(1, counter.count); 135 | 136 | //when 137 | digitalInput.mockState(DigitalState.HIGH); 138 | 139 | //then 140 | assertEquals(1, counter.count); 141 | 142 | //when 143 | digitalInput.mockState(DigitalState.LOW); 144 | 145 | //then 146 | assertEquals(2, counter.count); 147 | } 148 | 149 | @Test 150 | public void testWhilePressed() throws InterruptedException { 151 | //given 152 | Duration samplingTime = Duration.ofMillis(100); 153 | 154 | Counter counter = new Counter(); 155 | 156 | button.whilePressed(counter::increase, samplingTime); 157 | 158 | //when 159 | digitalInput.mockState(DigitalState.HIGH); 160 | 161 | //then 162 | assertEquals(0, counter.count); 163 | 164 | //when 165 | sleep(2 * samplingTime.toMillis()); 166 | 167 | //stop whilePressed 168 | digitalInput.mockState(DigitalState.LOW); 169 | 170 | //then 171 | int currentCount = counter.count; 172 | assertTrue(currentCount <= 2); 173 | 174 | //when 175 | sleep(2 * samplingTime.toMillis()); 176 | 177 | //then 178 | assertEquals(currentCount, counter.count); 179 | } 180 | 181 | @Test 182 | public void testDeRegisterAll(){ 183 | //given 184 | Runnable task = () -> System.out.println("not important for test"); 185 | button.onUp(task); 186 | button.onDown(task); 187 | button.whilePressed(task, Duration.ofMillis(10)); 188 | 189 | //when 190 | button.reset(); 191 | 192 | //then 193 | assertTrue(button.isInInitialState()); 194 | } 195 | 196 | private class Counter { 197 | int count; 198 | 199 | void increase(){ 200 | count++; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/test/java/com/pi4j/catalog/components/SimpleLedTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.catalog.components; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import com.pi4j.io.gpio.digital.DigitalState; 7 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalOutput; 8 | 9 | import com.pi4j.catalog.ComponentTest; 10 | import com.pi4j.catalog.components.base.PIN; 11 | 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | 15 | public class SimpleLedTest extends ComponentTest { 16 | 17 | private SimpleLed led; 18 | private MockDigitalOutput digitalOutput; 19 | 20 | @BeforeEach 21 | public void setUp() { 22 | led = new SimpleLed(pi4j, PIN.D26); 23 | digitalOutput = led.mock(); 24 | } 25 | 26 | @Test 27 | public void testInitialization() { 28 | assertEquals(26, led.pinNumber()); 29 | } 30 | 31 | @Test 32 | public void testOn() { 33 | //when 34 | led.on(); 35 | 36 | //then 37 | assertEquals(DigitalState.HIGH, digitalOutput.state()); 38 | assertTrue(led.isOn()); 39 | } 40 | 41 | @Test 42 | public void testOff() { 43 | //when 44 | led.off(); 45 | 46 | //then 47 | assertEquals(DigitalState.LOW, digitalOutput.state()); 48 | assertFalse(led.isOn()); 49 | } 50 | 51 | 52 | @Test 53 | public void testToggle() { 54 | //given 55 | led.off(); 56 | 57 | //when 58 | led.toggle(); 59 | 60 | //then 61 | assertEquals(DigitalState.HIGH, digitalOutput.state()); 62 | 63 | //when 64 | led.toggle(); 65 | 66 | //then 67 | assertEquals(DigitalState.LOW, digitalOutput.state()); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/multicontrollerapp/controller/ApplicationControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.controller; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | 6 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertFalse; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ApplicationControllerTest { 15 | 16 | @Test 17 | void testCounter() { 18 | //given 19 | ExampleModel model = new ExampleModel(); 20 | int initialCount = model.counter.getValue(); 21 | 22 | ApplicationController controller = new ApplicationController(model); 23 | 24 | //when 25 | controller.increaseCounter(); 26 | controller.awaitCompletion(); 27 | 28 | //then 29 | assertEquals(initialCount + 1, model.counter.getValue()); 30 | 31 | //when 32 | controller.decreaseCounter(); 33 | controller.awaitCompletion(); 34 | 35 | //then 36 | assertEquals(initialCount, model.counter.getValue()); 37 | } 38 | 39 | @Test 40 | void testLED() { 41 | //given 42 | ExampleModel model = new ExampleModel(); 43 | ApplicationController controller = new ApplicationController(model); 44 | 45 | //when 46 | controller.setLedGlows(true); 47 | controller.awaitCompletion(); 48 | 49 | //then 50 | assertTrue(model.isActive.getValue()); 51 | 52 | //when 53 | controller.setLedGlows(false); 54 | controller.awaitCompletion(); 55 | 56 | //then 57 | assertFalse(model.isActive.getValue()); 58 | } 59 | 60 | @Test 61 | void testBlink(){ 62 | //given 63 | ExampleModel model = new ExampleModel(); 64 | ApplicationController controller = new ApplicationController(model); 65 | 66 | AtomicInteger counter = new AtomicInteger(-1); 67 | model.isActive.onChange((oldValue, newValue) -> counter.getAndIncrement()); 68 | 69 | //when 70 | controller.blink(); 71 | controller.awaitCompletion(); 72 | 73 | //then 74 | assertEquals(8, counter.get()); 75 | assertFalse(model.isActive.getValue()); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/multicontrollerapp/model/ExampleModelTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.model; 2 | 3 | class ExampleModelTest { 4 | 5 | // ExampleModel consists of a list of ObservableValues. No additional logic. Therefore, no need to test anything here. 6 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/multicontrollerapp/view/pui/ExamplePUITest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.multicontrollerapp.view.pui; 2 | 3 | import com.pi4j.mvc.multicontrollerapp.controller.ApplicationController; 4 | import com.pi4j.mvc.multicontrollerapp.model.ExampleModel; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import com.pi4j.io.gpio.digital.DigitalState; 9 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalInput; 10 | 11 | import com.pi4j.catalog.ComponentTest; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertFalse; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | public class ExamplePUITest extends ComponentTest { 18 | 19 | @Test 20 | void testLED() { 21 | //given 22 | ExampleModel model = new ExampleModel(); 23 | ApplicationController controller = new ApplicationController(model); 24 | ExamplePUI pui = new ExamplePUI(controller); 25 | 26 | //when 27 | controller.setLedGlows(true); 28 | controller.awaitCompletion(); 29 | pui.awaitCompletion(); 30 | 31 | //then 32 | assertTrue(pui.led.isOn()); 33 | 34 | //when 35 | controller.setLedGlows(false); 36 | controller.awaitCompletion(); 37 | pui.awaitCompletion(); 38 | 39 | //then 40 | assertFalse(pui.led.isOn()); 41 | } 42 | 43 | @Test 44 | void testButton() { 45 | //given 46 | ExampleModel model = new ExampleModel(); 47 | ApplicationController controller = new ApplicationController(model); 48 | ExamplePUI pui = new ExamplePUI(controller); 49 | 50 | int initialCounter = model.counter.getValue(); 51 | 52 | MockDigitalInput digitalInput = pui.button.mock(); 53 | digitalInput.mockState(DigitalState.HIGH); 54 | 55 | //when 56 | digitalInput.mockState(DigitalState.LOW); 57 | controller.awaitCompletion(); 58 | 59 | //then 60 | assertEquals(initialCounter - 1, model.counter.getValue()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/templateapp/controller/SomeControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.controller; 2 | 3 | import com.pi4j.mvc.templateapp.model.SomeModel; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertFalse; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | 12 | class SomeControllerTest { 13 | @Test 14 | void testCounter() { 15 | //given 16 | SomeModel model = new SomeModel(); 17 | int initialCount = model.counter.getValue(); 18 | 19 | SomeController controller = new SomeController(model); 20 | 21 | //when 22 | controller.increaseCounter(); 23 | controller.awaitCompletion(); 24 | 25 | //then 26 | assertEquals(initialCount + 1, model.counter.getValue()); 27 | 28 | //when 29 | controller.decreaseCounter(); 30 | controller.awaitCompletion(); 31 | 32 | //then 33 | assertEquals(initialCount, model.counter.getValue()); 34 | } 35 | 36 | @Test 37 | void testLED() { 38 | //given 39 | SomeModel model = new SomeModel(); 40 | 41 | SomeController controller = new SomeController(model); 42 | 43 | //when 44 | controller.setIsActive(true); 45 | controller.awaitCompletion(); 46 | 47 | //then 48 | assertTrue(model.isActive.getValue()); 49 | 50 | //when 51 | controller.setIsActive(false); 52 | controller.awaitCompletion(); 53 | 54 | //then 55 | assertFalse(model.isActive.getValue()); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/templateapp/model/SomeModelTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.model; 2 | 3 | class SomeModelTest { 4 | 5 | // 'SomeModel' consists of a list of ObservableValues. No additional logic. Therefore, no need to test anything here. 6 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/templateapp/view/pui/SomePUITest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templateapp.view.pui; 2 | 3 | import com.pi4j.mvc.templateapp.controller.SomeController; 4 | import com.pi4j.mvc.templateapp.model.SomeModel; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import com.pi4j.io.gpio.digital.DigitalState; 9 | import com.pi4j.plugin.mock.provider.gpio.digital.MockDigitalInput; 10 | 11 | import com.pi4j.catalog.ComponentTest; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertFalse; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | 18 | class SomePUITest extends ComponentTest { 19 | @Test 20 | void testLED() { 21 | //given 22 | SomeModel model = new SomeModel(); 23 | SomeController controller = new SomeController(model); 24 | SomePUI pui = new SomePUI(controller); 25 | 26 | //when 27 | controller.setIsActive(true); 28 | controller.awaitCompletion(); 29 | pui.awaitCompletion(); 30 | 31 | //then 32 | assertTrue(pui.led.isOn()); 33 | 34 | //when 35 | controller.setIsActive(false); 36 | controller.awaitCompletion(); 37 | pui.awaitCompletion(); 38 | 39 | //then 40 | assertFalse(pui.led.isOn()); 41 | } 42 | 43 | @Test 44 | void testButton() { 45 | //given 46 | SomeModel model = new SomeModel(); 47 | SomeController controller = new SomeController(model); 48 | SomePUI pui = new SomePUI(controller); 49 | 50 | int initialCounter = model.counter.getValue(); 51 | 52 | MockDigitalInput digitalInput = pui.button.mock(); 53 | digitalInput.mockState(DigitalState.HIGH); 54 | 55 | //when 56 | digitalInput.mockState(DigitalState.LOW); 57 | pui.awaitCompletion(); 58 | controller.awaitCompletion(); 59 | 60 | //then 61 | assertEquals(initialCounter - 1, model.counter.getValue()); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/templatepuiapp/controller/SomeControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.templatepuiapp.controller; 2 | 3 | import com.pi4j.mvc.templatepuiapp.model.SomeModel; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertFalse; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | class SomeControllerTest { 12 | 13 | @Test 14 | void testActivate() { 15 | //given 16 | SomeModel model = new SomeModel(); 17 | SomeController controller = new SomeController(model); 18 | 19 | //when 20 | controller.activate(); 21 | controller.awaitCompletion(); 22 | 23 | //then 24 | assertTrue(model.busy.getValue()); 25 | assertEquals(0, model.counter.getValue()); //counter is increased on 'deactivate' 26 | } 27 | 28 | @Test 29 | void testDeactivate() { 30 | //given 31 | SomeModel model = new SomeModel(); 32 | SomeController controller = new SomeController(model); 33 | 34 | //when 35 | controller.deactivate(); 36 | controller.awaitCompletion(); 37 | 38 | //then 39 | assertFalse(model.busy.getValue()); //only busy on'activate' 40 | assertEquals(1, model.counter.getValue()); 41 | } 42 | 43 | @Test 44 | void testIsNotBusyAndNotTerminated() { 45 | //given 46 | SomeModel model = new SomeModel(); 47 | SomeController controller = new SomeController(model); 48 | 49 | int count = controller.terminationCount; 50 | 51 | //when 52 | for (int i = 0; i < count; i++) { 53 | controller.deactivate(); 54 | } 55 | controller.awaitCompletion(); 56 | 57 | //then 58 | assertFalse(model.busy.getValue()); 59 | assertEquals(count, model.counter.getValue()); 60 | } 61 | 62 | @Test 63 | void testTermination() { 64 | //given 65 | SomeModel model = new SomeModel(); 66 | TestController controller = new TestController(model); 67 | 68 | //when 69 | for (int i = 0; i < controller.terminationCount; i++) { 70 | controller.deactivate(); 71 | } 72 | controller.awaitCompletion(); 73 | 74 | //then 75 | assertFalse(controller.terminateCalled); 76 | 77 | //when 78 | controller.deactivate(); 79 | controller.awaitCompletion(); 80 | 81 | //then 82 | assertTrue(controller.terminateCalled); 83 | } 84 | 85 | private static class TestController extends SomeController { 86 | private boolean terminateCalled = false; 87 | 88 | public TestController(SomeModel model) { 89 | super(model); 90 | } 91 | 92 | @Override 93 | protected void terminate() { 94 | terminateCalled = true; 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/util/mvcbase/ConcurrentTaskQueueTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.concurrent.ConcurrentLinkedQueue; 4 | import java.util.concurrent.CountDownLatch; 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ConcurrentTaskQueueTest { 15 | 16 | @Test 17 | void testSequenceGuarantees() throws InterruptedException { 18 | // given 19 | final ConcurrentTaskQueue taskQueue = new ConcurrentTaskQueue<>(); 20 | final ConcurrentLinkedQueue collector = new ConcurrentLinkedQueue<>(); 21 | 22 | // when we concurrently produce and consume some numbers 23 | for (int i = 0; i < 10; i++) { 24 | int finalI = i; 25 | taskQueue.submit( 26 | () -> { 27 | try { 28 | Thread.sleep(10); // force some thread switching to make the test more realistic 29 | } catch (InterruptedException e) { 30 | e.printStackTrace(); 31 | } 32 | return finalI; 33 | }, 34 | collector::add 35 | ); 36 | } 37 | 38 | // special in the test case: wait until all concurrent tasks are finished 39 | // such that we can synchronously assert the outcome. 40 | // The general idea is to submit a last task to the CTQ that concurrently sets a state that we can 41 | // synchronously wait for. 42 | CountDownLatch latch = new CountDownLatch(1); 43 | taskQueue.submit( () -> { 44 | latch.countDown(); 45 | return null; 46 | }); 47 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 48 | 49 | // then no number is missing and the sequence is retained 50 | Integer[] expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 51 | assertArrayEquals( expected, collector.toArray() ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/util/mvcbase/ControllerBaseTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertFalse; 8 | import static org.junit.jupiter.api.Assertions.assertSame; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | class ControllerBaseTest { 12 | private TestModel model; 13 | private ControllerBase controller; 14 | 15 | @BeforeEach 16 | void setup() { 17 | model = new TestModel(); 18 | controller = new ControllerBase<>(model) { 19 | }; 20 | } 21 | 22 | @Test 23 | void testInitialization() { 24 | //then 25 | assertSame(model, controller.getModel()); 26 | } 27 | 28 | @Test 29 | void testSetValue() { 30 | //given 31 | int newInt = 42; 32 | boolean newBool = true; 33 | 34 | //when 35 | controller.setValue(model.someInt, newInt); 36 | controller.setValue(model.someBoolean, newBool); 37 | 38 | controller.awaitCompletion(); 39 | 40 | //then 41 | assertEquals(newInt, model.someInt.getValue()); 42 | assertEquals(newBool, model.someBoolean.getValue()); 43 | } 44 | 45 | @Test 46 | void testToggle() { 47 | //given 48 | model.someBoolean.setValue(true); 49 | 50 | //when 51 | controller.toggleValue(model.someBoolean); 52 | controller.awaitCompletion(); 53 | 54 | //then 55 | assertFalse(model.someBoolean.getValue()); 56 | 57 | //when 58 | controller.toggleValue(model.someBoolean); 59 | controller.awaitCompletion(); 60 | 61 | //then 62 | assertTrue(model.someBoolean.getValue()); 63 | } 64 | 65 | private static class TestModel { 66 | final ObservableValue someInt = new ObservableValue<>(73); 67 | final ObservableValue someBoolean = new ObservableValue<>(false); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/util/mvcbase/ObservableArrayTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | 11 | class ObservableArrayTest { 12 | 13 | @Test 14 | void testInitialization() { 15 | //when 16 | ObservableArray v = new ObservableArray<>(new Boolean[]{false, false, false, false}); 17 | 18 | //then 19 | assertFalse(v.getValue(0)); 20 | 21 | //when 22 | v = new ObservableArray<>(new Boolean[]{true, false, false, false}); 23 | 24 | //then 25 | assertTrue(v.getValue(0)); 26 | } 27 | 28 | @Test 29 | void testSetValue() { 30 | //given 31 | ObservableArray observableValue = new ObservableArray<>(new Boolean[]{false, false, false, false}); 32 | 33 | //when 34 | observableValue.setValue(0, true); 35 | 36 | //then 37 | assertTrue(observableValue.getValue(0)); 38 | 39 | //when 40 | observableValue.setValue(0, false); 41 | 42 | //then 43 | assertFalse(observableValue.getValue(0)); 44 | } 45 | 46 | @Test 47 | void testSetListener(){ 48 | //given 49 | String initialValue = "initial Value"; 50 | String firstValue = "first value"; 51 | ObservableArray observableArray = new ObservableArray<>(new String[]{initialValue}); 52 | 53 | AtomicInteger counter = new AtomicInteger(0); 54 | AtomicReference foundOld = new AtomicReference<>(); 55 | AtomicReference foundNew = new AtomicReference<>(); 56 | 57 | //when 58 | observableArray.onChange((oldValue, newValue) -> { 59 | counter.getAndIncrement(); 60 | foundOld.set(oldValue[0]); 61 | foundNew.set(newValue[0]); 62 | }); 63 | 64 | //then 65 | assertEquals(1, counter.get()); // listener is called on registration 66 | assertEquals(initialValue, foundOld.get()); // initial value of oldValue is current value 67 | assertEquals(initialValue, foundNew.get()); // current value 68 | 69 | //when 70 | observableArray.setValue(0, initialValue); 71 | 72 | //then 73 | assertEquals(1, counter.get()); // value stays the same; listener is not called 74 | 75 | //when 76 | observableArray.setValue(0, firstValue); 77 | 78 | //then 79 | assertEquals(2, counter.get()); // value has changed; listener is called 80 | assertEquals(initialValue, foundOld.get()); 81 | assertEquals(firstValue, foundNew.get()); 82 | } 83 | } -------------------------------------------------------------------------------- /src/test/java/com/pi4j/mvc/util/mvcbase/ObservableValueTest.java: -------------------------------------------------------------------------------- 1 | package com.pi4j.mvc.util.mvcbase; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | import org.junit.jupiter.api.Disabled; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertFalse; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | 16 | 17 | class ObservableValueTest { 18 | 19 | @Test 20 | void testInitialization() { 21 | //when 22 | ObservableValue v = new ObservableValue<>(false); 23 | 24 | //then 25 | assertFalse(v.getValue()); 26 | 27 | //when 28 | v = new ObservableValue<>(true); 29 | 30 | //then 31 | assertTrue(v.getValue()); 32 | } 33 | 34 | @Test 35 | void testSetValue() { 36 | //given 37 | ObservableValue observableValue = new ObservableValue<>(false); 38 | 39 | //when 40 | observableValue.setValue(true); 41 | 42 | //then 43 | assertTrue(observableValue.getValue()); 44 | 45 | //when 46 | observableValue.setValue(false); 47 | 48 | //then 49 | assertFalse(observableValue.getValue()); 50 | } 51 | 52 | @Test 53 | void testSetListener(){ 54 | //given 55 | String initialValue = "initial Value"; 56 | String firstValue = "first value"; 57 | ObservableValue observableValue = new ObservableValue<>(initialValue); 58 | 59 | AtomicInteger counter = new AtomicInteger(0); 60 | AtomicReference foundOld = new AtomicReference<>(); 61 | AtomicReference foundNew = new AtomicReference<>(); 62 | 63 | //when 64 | observableValue.onChange((oldValue, newValue) -> { 65 | counter.getAndIncrement(); 66 | foundOld.set(oldValue); 67 | foundNew.set(newValue); 68 | }); 69 | 70 | //then 71 | assertEquals(1, counter.get()); // listener is called on registration 72 | assertEquals(initialValue, foundOld.get()); // initial value of oldValue is current value 73 | assertEquals(initialValue, foundNew.get()); // current value 74 | 75 | //when 76 | observableValue.setValue(initialValue); 77 | 78 | //then 79 | assertEquals(1, counter.get()); // value stays the same; listener is not called 80 | 81 | //when 82 | observableValue.setValue(firstValue); 83 | 84 | //then 85 | assertEquals(2, counter.get()); // value has changed; listener is called 86 | assertEquals(initialValue, foundOld.get()); 87 | assertEquals(firstValue, foundNew.get()); 88 | } 89 | 90 | @Disabled("This test sometimes fails, most probably because testcase is wrong, not implementation") 91 | @Test 92 | void testEdgeCase(){ 93 | //given 94 | ObservableValue observableValue = new ObservableValue<>("start"); 95 | 96 | List log1 = new ArrayList<>(); 97 | List log2 = new ArrayList<>(); 98 | 99 | //when 100 | observableValue.onChange((oldValue, newValue) -> { 101 | log1.add(oldValue); 102 | log1.add(newValue); 103 | if(newValue.equals("second")){ 104 | observableValue.setValue("third"); 105 | } 106 | }); 107 | 108 | observableValue.onChange((oldValue, newValue) -> { 109 | log2.add(oldValue); 110 | log2.add(newValue); 111 | }); 112 | 113 | //then 114 | assertArrayEquals(new String[]{"start", "start"}, log1.toArray(new String[0])); 115 | assertArrayEquals(new String[]{"start", "start"}, log2.toArray(new String[0])); 116 | 117 | //when 118 | observableValue.setValue("second"); 119 | 120 | //then 121 | // first observer has seen all value changes 122 | assertArrayEquals(new String[]{"start", "start", "start", "second", "second", "third"}, log1.toArray(new String[0])); 123 | 124 | // the second observer might _not_ have seen all value changes but he sees 125 | // at least the last proper value change !!! 126 | assertArrayEquals(new String[]{"start", "start", "second", "third"}, log2.toArray(new String[0])); 127 | } 128 | 129 | } --------------------------------------------------------------------------------