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 | 
2 |
3 | [Deutsche Beschreibung ist hier.](README_DE.md)
4 |
5 | # Pi4J Applications with JavaFX based GUI
6 |
7 | [](https://github.com/Pi4J/pi4j-template-javafx/graphs/contributors)
8 | [](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 | 
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 | 
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 | 
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 | 
3 |
4 | # Pi4J Applikationen mit JavaFX-basiertem GUI
5 |
6 | [](https://github.com/DieterHolz/RaspPiFX-Template-Project/graphs/contributors)
7 | [](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 | 
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 | 
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 | 
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 | *
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 super V> 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 super P> 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 super V> 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 | *