From d87f1ed1f85364145a33fc599faf9f6bbbc00aba Mon Sep 17 00:00:00 2001 From: dave Date: Sat, 9 Nov 2024 11:08:20 +0000 Subject: [PATCH] Initial public version --- README.md | 13 +++- java/embedControlJavaFx/README.md | 22 ++++++- java/javaApiExamples/README.md | 6 +- ... APIClientThatAcceptsForRemoteDevice.java} | 2 +- ...va => APIConnectSocketToRemoteDevice.java} | 6 +- .../{ => client}/StandaloneRs232Test.java | 10 ++- ...eviceSocketServerAcceptingConnections.java | 61 +++++++++++++++++++ .../DeviceWithClientConnectionExample.java | 42 +++---------- .../server/MakeEncryptionKeyExample.java | 2 +- java/javaApiWebsocketServer/README.md | 8 ++- .../websocket/SimpleWebSocketExample.java | 13 ++++ 11 files changed, 138 insertions(+), 47 deletions(-) rename java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/{ClientThatAcceptsForRemoteExample.java => APIClientThatAcceptsForRemoteDevice.java} (99%) rename java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/{ConnectToRemoteDeviceServerExample.java => APIConnectSocketToRemoteDevice.java} (96%) rename java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/{ => client}/StandaloneRs232Test.java (89%) create mode 100644 java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceSocketServerAcceptingConnections.java diff --git a/README.md b/README.md index da9aea5..3b4b1b6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,18 @@ ## Java examples -All the examples are set up to work with the latest version of Java `OpenJDK` and `maven` build tool. You can use more or less any OpenJDK distribution, but we tend to use Adoptium at the moment. +### Setting up a Java build + +For development we recommend IntelliJ, either edition of it should be fine to get started with. To build on the command line, ensure that you have the most recent OpenJDK and a recent version of maven on your system, without these it will not be possible to build. + +* All OpenJDK's that we've tested work for this, we've tried: Liberica, Adoptium, Amazon Corretto and Microsoft JDK. +* For Apache maven we recommend using [https://maven.apache.org/] + +Using git or zip download, get the contents of the tcMenu repository locally, for example: + + git clone https://github.com/TcMenu/tcmenu-examples-starters.git + +Once you've set up the above you can start with any of the following projects: * embedControlJavaFX: A starting point for deploying a desktop control and monitoring UI app that works on many platforms. * embeddedJavaDeviceUI: A starting point embedded control application for embedded boards that can run JavaFX. diff --git a/java/embedControlJavaFx/README.md b/java/embedControlJavaFx/README.md index d63837b..96b5097 100644 --- a/java/embedControlJavaFx/README.md +++ b/java/embedControlJavaFx/README.md @@ -2,10 +2,28 @@ Contained in this package is the source code for the complete user interface that connects to a remote device running tcMenu, as long as the board is using one of the supported Serial or Network based plugins. -Before releasing a package based on this we recommend that you change the icons and naming. +Before releasing a package based on this we recommend that you change the icons and naming, especially if releasing publicly. ## Building a packaged version -This is a Java application, and builds using the maven build system. +The below will produce you a package that can be used on most platforms supported by Java. It will use the [Java SDK jpackage tool](https://docs.oracle.com/en/java/javase/21/docs/specs/man/jpackage.html) to build a native image that can be installed onto a user system. +### Building the embedCONTROLFx desktop UI for Windows +Ensure you are in the embedCONTROLFx/target directory. This will produce an executable that can be run directly on Windows, depending on the options you chose, it will either generate an MSI installer, self installing EXE, or just an executable. + + cp classes/fximg/embedCONTROL.ico . + + jpackage --type app-image -n embedCONTROL -p jfx/deps --input jfx/app --resource-dir .\classes\fximg\ --icon embedCONTROL.ico --app-version 4.4.0 --verbose --java-options "-Dprism.lcdtext=false" --add-modules "jdk.crypto.cryptoki" -m com.thecoderscorner.tcmenu.embedcontrolfx/com.thecoderscorner.embedcontrol.jfxapp.EmbedControlApp + +### Building the embedCONTROLFx desktop for macOS + +Ensure you are in the embedCONTROLFx/target directory. This will produce a disk image in the regular macOS install format. You can also choose to create an install package or just an executable. + + jpackage -n embedCONTROL -p jfx/deps --input jfx/app --icon ./classes/fximg/MyIcon.icns --vendor TheCodersCorner --app-version 4.4.0 --verbose --license-file ../../LICENSE --java-options "-Dprism.lcdtext=false" --add-modules "jdk.crypto.cryptoki" -m com.thecoderscorner.tcmenu.embedcontrolfx/com.thecoderscorner.embedcontrol.jfxapp.EmbedControlApp + +### Building the embedCONTROLFx desktop for Linux + +Ensure you are in the embedCONTROLFx/target directory. This will create a native package depending on which Linux you are using. It has been tested on Ubuntu and also ArmArch64 NOMone. + + jpackage -n embedCONTROL -p jfx/deps --input jfx/app --icon ./classes/fximg/large_icon.png --verbose --license-file ../../LICENSE --linux-app-category Utility --linux-menu-group "Utility;" --java-options "-Dprism.lcdtext=false" --app-version 4.4.0 --add-modules "jdk.crypto.cryptoki" -m com.thecoderscorner.tcmenu.embedcontrolfx/com.thecoderscorner.embedcontrol.jfxapp.EmbedControlApp diff --git a/java/javaApiExamples/README.md b/java/javaApiExamples/README.md index e1ae948..2098ff1 100644 --- a/java/javaApiExamples/README.md +++ b/java/javaApiExamples/README.md @@ -2,10 +2,10 @@ Various demonstrations of how to use the Java API in your applications. Kept as ## Examples for when the API is acting as a device -These examples cover the cases where the API is acting as a device, for example an embedded Raspberry PI. +These examples cover the cases where the API is acting as a `device`, for example a Raspberry PI or other Linux system embedded into a device. -* An example showing a device that tries to connect to client connections - `DeviceWithClientConnectionExample.java` -* This code allows you to make AES encryption keys `MakeEncryptionKeyExample.java` +* An example showing a `device` that tries to connect to the `API` which is accepting. As a `device` we would be responsible for bootstrapping and hosting the menu items - `DeviceWithClientConnectionExample.java` +* This code allows you to make AES encryption keys that work with the other examples or elsewhere `MakeEncryptionKeyExample.java` ## Examples where the API is running remotely connecting to a device diff --git a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/ClientThatAcceptsForRemoteExample.java b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/APIClientThatAcceptsForRemoteDevice.java similarity index 99% rename from java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/ClientThatAcceptsForRemoteExample.java rename to java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/APIClientThatAcceptsForRemoteDevice.java index b037284..37d63c2 100644 --- a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/ClientThatAcceptsForRemoteExample.java +++ b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/APIClientThatAcceptsForRemoteDevice.java @@ -24,7 +24,7 @@ * * See DeviceWithClientConnectionExample for the other side of this example */ -public class ClientThatAcceptsForRemoteExample { +public class APIClientThatAcceptsForRemoteDevice { private final static System.Logger logger = System.getLogger("ExampleClient"); // the port on which we will accept. diff --git a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/ConnectToRemoteDeviceServerExample.java b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/APIConnectSocketToRemoteDevice.java similarity index 96% rename from java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/ConnectToRemoteDeviceServerExample.java rename to java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/APIConnectSocketToRemoteDevice.java index bed40e6..2593ee0 100644 --- a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/ConnectToRemoteDeviceServerExample.java +++ b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/APIConnectSocketToRemoteDevice.java @@ -25,7 +25,7 @@ * as an example of where to start with the Java API. This example creates a connection to a socket server, this is the * default case. */ -public class ConnectToRemoteDeviceServerExample implements RemoteControllerListener { +public class APIConnectSocketToRemoteDevice implements RemoteControllerListener { // where we want to connect, host and port private static final String MY_HOST = "192.168.0.96"; private static final int MY_PORT = 3333; @@ -44,11 +44,11 @@ public class ConnectToRemoteDeviceServerExample implements RemoteControllerListe private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(4); public static void main(String[] args) { - var server = new ConnectToRemoteDeviceServerExample(); + var server = new APIConnectSocketToRemoteDevice(); server.start(); } - public ConnectToRemoteDeviceServerExample() { + public APIConnectSocketToRemoteDevice() { // here we create the connection and controller using the builder. var builder = new SocketControllerBuilder() .withAddress(MY_HOST).withPort(MY_PORT) diff --git a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/StandaloneRs232Test.java b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/StandaloneRs232Test.java similarity index 89% rename from java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/StandaloneRs232Test.java rename to java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/StandaloneRs232Test.java index d18e5a9..41f007e 100644 --- a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/StandaloneRs232Test.java +++ b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/client/StandaloneRs232Test.java @@ -4,7 +4,7 @@ * */ -package com.thecoderscorner.menu.examples; +package com.thecoderscorner.menu.examples.client; import com.thecoderscorner.menu.domain.MenuItem; import com.thecoderscorner.menu.domain.state.MenuTree; @@ -91,6 +91,11 @@ public void start() { } } + /** + * This class implements the `RemoteControllerListener` interface and as such is made aware of events from the + * connector layer, and also told when we've managed to bootstrap successfully or when items change. The controller + * takes care of the menu tree, which it builds during the device bootstrap. + */ private class MyRemoteListener implements RemoteControllerListener { @Override public void menuItemChanged(MenuItem item, boolean valueOnly) { @@ -125,6 +130,9 @@ public void connectionState(RemoteInformation remoteInformation, AuthStatus conn @Override public void ackReceived(CorrelationId key, MenuItem item, AckStatus st) { + // This is called when an update or other event is acknowledged. It provides back the same correlation ID + // for acknowledgement that the API generated during send. Whenever you call send on the controller, you're + // provided by return with a correlation. logger.log(INFO, "Ack -" + key + " item " + item + " status " + st); } diff --git a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceSocketServerAcceptingConnections.java b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceSocketServerAcceptingConnections.java new file mode 100644 index 0000000..3039185 --- /dev/null +++ b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceSocketServerAcceptingConnections.java @@ -0,0 +1,61 @@ +package com.thecoderscorner.menu.examples.server; + +import com.thecoderscorner.menu.auth.PreDefinedAuthenticator; +import com.thecoderscorner.menu.domain.util.DomainFixtures; +import com.thecoderscorner.menu.mgr.MenuManagerServer; +import com.thecoderscorner.menu.remote.mgrclient.ClientBasedConnectionManager; +import com.thecoderscorner.menu.remote.mgrclient.SocketServerConnectionManager; +import com.thecoderscorner.menu.remote.protocol.ConfigurableProtocolConverter; + +import java.time.Clock; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * This example shows how to create a Java device server, IE where this code would reside as an application on an + * embedded device such as a Raspberry PI. This example will accept connections in TagVal format from API clients. + * It takes a port to accept on, and will hold connections open until connection failure. + */ +public class DeviceSocketServerAcceptingConnections { + // the port on which we'll listen for connections. + public final static int MY_PORT = 3333; + // the name of this device, sent during join + public static final String MY_SERVER_NAME = "ClientDevExample"; + // the UUID of this device, sent during join + public static final UUID MY_SERVER_UUID = UUID.fromString("A2BA2013-E17A-4551-8462-A0F6D15968AC"); + + public static void main(String[] args) { + // set up logging + ConsoleHandler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + handler.setLevel(Level.FINEST); + Logger.getLogger("").addHandler(handler); + Logger.getLogger("").setLevel(Level.FINEST); + + // this protocol handler is capable of handling all regular TagVal messages, and you can even add your own extras + var protocol = new ConfigurableProtocolConverter(true); + // the clock in system UTC time, you can use any implementation. + var clock = Clock.systemUTC(); + // the thread pool that we'll use to handle connections. + var executor = Executors.newScheduledThreadPool(4); + + // create the connection logic, in this case we are a device that connects to an "accepting" client. + var deviceRemote = new SocketServerConnectionManager(protocol, executor, MY_PORT, clock, 15000); + + // create a basic tree that we'll send remotely during bootstrap. + var tree = DomainFixtures.fullEspAmplifierTestTree(); + + // now we create the menu manager object, it is responsible for handling remote connections and bootstrapping + // clients that connect. + var menuManager = new MenuManagerServer(executor, tree, MY_SERVER_NAME, MY_SERVER_UUID, new PreDefinedAuthenticator(true), clock); + menuManager.addConnectionManager(deviceRemote); + // you can provide an implementation that returns the serial number of the board + menuManager.setBoardSerialProvider(() -> 1234); + + menuManager.start(); + } +} diff --git a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceWithClientConnectionExample.java b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceWithClientConnectionExample.java index 589a9e6..1bdfc39 100644 --- a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceWithClientConnectionExample.java +++ b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/DeviceWithClientConnectionExample.java @@ -2,16 +2,10 @@ import com.thecoderscorner.menu.auth.PreDefinedAuthenticator; import com.thecoderscorner.menu.domain.util.DomainFixtures; -import com.thecoderscorner.menu.domain.state.MenuTree; -import com.thecoderscorner.menu.mgr.MenuInMenu; import com.thecoderscorner.menu.mgr.MenuManagerServer; -import com.thecoderscorner.menu.remote.ConnectMode; -import com.thecoderscorner.menu.remote.LocalIdentifier; -import com.thecoderscorner.menu.remote.RemoteConnector; import com.thecoderscorner.menu.remote.encryption.AESEncryptionHandlerFactory; import com.thecoderscorner.menu.remote.mgrclient.ClientBasedConnectionManager; import com.thecoderscorner.menu.remote.protocol.ConfigurableProtocolConverter; -import com.thecoderscorner.menu.remote.socket.SocketBasedConnector; import java.time.Clock; import java.util.UUID; @@ -26,59 +20,39 @@ * See ClientThatAcceptsForRemoteExample for the other side of this example. */ public class DeviceWithClientConnectionExample { - // where we want to connect to both host and port + // where we want to connect to (both host and port) public static final String MY_HOST = "localhost"; public static final int MY_PORT = 3333; // the name that we'll provide when we join public static final String MY_SERVER_NAME = "ClientDevExample"; // the UUID of this device public static final UUID MY_SERVER_UUID = UUID.fromString("A2BA2013-E17A-4551-8462-A0F6D15968AC"); + // the encryption key the API will use public static final String ENCRYPTED_AES_KEY = "A8UvLzdTzUCYeqir6DODRquIbch04kN1EuyocNqoJI4="; - private static final String MENU_IN_MENU_REMOTE = "192.168.0.222"; - private static final int MENU_IN_MENU_PORT = 3333; public static void main(String[] args) { - // First we create a few objects that are needed by both the remote connection logic and the menu manager. + // this protocol handler is capable of handling all regular TagVal messages, and you can even add your own extras var protocol = new ConfigurableProtocolConverter(true); + // the clock in system UTC time, you can use any implementation. var clock = Clock.systemUTC(); + // the thread pool that we'll use to handle connections. var executor = Executors.newScheduledThreadPool(4); + // an encryption factory, you can use `NoEncryptionFactory` too if not needed. var aesEncryptionFactory = new AESEncryptionHandlerFactory(ENCRYPTED_AES_KEY); // create the connection logic, in this case we are a device that connects to an "accepting" client. var deviceRemote = new ClientBasedConnectionManager(MY_HOST, MY_PORT, protocol, clock, executor, aesEncryptionFactory); - // create a basic menu manager, in any menu app we'd normally have one of these. + // create a basic tree that we'll send remotely during bootstrap. var tree = DomainFixtures.fullEspAmplifierTestTree(); // now we create the menu manager object, it is responsible for handling remote connections and bootstrapping // clients that connect. var menuManager = new MenuManagerServer(executor, tree, MY_SERVER_NAME, MY_SERVER_UUID, new PreDefinedAuthenticator(true), clock); menuManager.addConnectionManager(deviceRemote); + // you can provide an implementation that returns the serial number of the board menuManager.setBoardSerialProvider(() -> 1234); - // start optional menu in menu set up. - - // optionally we can now add one or more menu-in-menu structures. See here for more information: - // https://tcmenu.github.io/documentation/arduino-libraries/tc-menu/tcmenu-iot/java-menu-in-menu/ - - // first we create a remote connector that will read the source menu and receive updates as it changes. - RemoteConnector connector = new SocketBasedConnector( - new LocalIdentifier(MY_SERVER_UUID, MY_SERVER_NAME), - executor, clock, protocol, MENU_IN_MENU_REMOTE, MENU_IN_MENU_PORT, - ConnectMode.FULLY_AUTHENTICATED, null - ); - - // now we create a menu in menu item that puts all the items in the "source" menu into our menu at an offset. - var menuInMenu = new MenuInMenu(connector, menuManager, MenuTree.ROOT, - MenuInMenu.ReplicationMode.REPLICATE_SILENTLY, - 50100, 55000); - - // now we start the menu in menu configuration, it puts the "source" menu structure into our tree and keeps it - // in sync in both directions. - menuInMenu.start(); - - // end optional menu in menu setup. - // this finally spins up the example menuManager.start(); } diff --git a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/MakeEncryptionKeyExample.java b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/MakeEncryptionKeyExample.java index b2f4fc2..1b93334 100644 --- a/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/MakeEncryptionKeyExample.java +++ b/java/javaApiExamples/src/main/java/com/thecoderscorner/menu/examples/server/MakeEncryptionKeyExample.java @@ -12,7 +12,7 @@ /** * This example shows how to make an encryption key pair, use them, serialise them, and create a tcMenuApi protocol - * encryption handler from it too. + * encryption handler from it too. The encryption keys are 256 bits in length using AES format. */ public class MakeEncryptionKeyExample { public static void main(String[] args) throws Exception { diff --git a/java/javaApiWebsocketServer/README.md b/java/javaApiWebsocketServer/README.md index 17ddb35..792171b 100644 --- a/java/javaApiWebsocketServer/README.md +++ b/java/javaApiWebsocketServer/README.md @@ -1 +1,7 @@ -WebSocket Demo +## Very basic Java Websocket example + +This example shows how to use [Java-WebSocket](https://github.com/TooTallNate/Java-WebSocket) with TcMenu API to provide the "device" side of a websocket connection. Websocket connections are normally used with [EmbedControlJS](https://github.com/TcMenu/embedcontrolJS) to provide the API connection in a browser environment. + +This is for the case where the websocket is separate to the hosting, IE you for example are using Apache or other webserver to host the static application files, and want to handle the websocket connection separately. + +This is often very useful during development of the JavaScript application, as it allows you to run a simple "device" on your machine that the application can connect to. diff --git a/java/javaApiWebsocketServer/src/main/java/com/thecoderscorner/menu/example/websocket/SimpleWebSocketExample.java b/java/javaApiWebsocketServer/src/main/java/com/thecoderscorner/menu/example/websocket/SimpleWebSocketExample.java index cd9bd81..f277904 100644 --- a/java/javaApiWebsocketServer/src/main/java/com/thecoderscorner/menu/example/websocket/SimpleWebSocketExample.java +++ b/java/javaApiWebsocketServer/src/main/java/com/thecoderscorner/menu/example/websocket/SimpleWebSocketExample.java @@ -23,31 +23,44 @@ public class SimpleWebSocketExample { public static void main(String[] args) { + // enable logging onto the console. ConsoleHandler handler = new ConsoleHandler(); handler.setFormatter(new SimpleFormatter()); handler.setLevel(Level.FINEST); Logger.getLogger("").addHandler(handler); Logger.getLogger("").setLevel(Level.FINEST); + // now build a tree that the menu manager will manage. var tree = DomainFixtures.fullEspAmplifierTestTree(); + // and an executor, simple single thread one is enough for this. var executor = Executors.newSingleThreadScheduledExecutor(); + // and a clock, we choose default timezone. var clock = Clock.systemDefaultZone(); + // this protocol handler can deal with all TagVal messages, you can even add your own custom ones too. MenuCommandProtocol tagValProtocol = new ConfigurableProtocolConverter(true); + // now create the menu manager, it manages all remotes, bootstrapping API connections etc. var menuManager = new MenuManagerServer(executor, tree, "WS Test", UUID.randomUUID(), new PropertiesAuthenticator("./auth.properties", new NoDialogFacilities()), clock); + // then we add the websocket and regular endpoints to it, it can handle both regular socket connections and + // also websocket connections. menuManager.addConnectionManager(new WebSocketServerConnectionManager(tagValProtocol, 3333, clock)); menuManager.addConnectionManager(new SocketServerConnectionManager(tagValProtocol, executor, 3334, clock, 15000)); + // fire it up so we can connect menuManager.start(); + // an example of updating a menu item, in this case a list. var menuList = menuManager.getManagedMenu().getMenuById(21).orElseThrow(); menuManager.updateMenuItem(menuManager, menuList, List.of("salad", "pasta", "pizza")); + // if you want to send simulated updates when running set VM option -DsendSimulatedUpdates=true if(Boolean.getBoolean("sendSimulatedUpdates")) { + // update the list item randomly every 5 seconds. executor.scheduleAtFixedRate(() -> menuManager.updateMenuItem(menuManager, menuList, randomListData()), 5000, 5000, TimeUnit.MILLISECONDS); + // now update a few analog items randomly. executor.scheduleAtFixedRate(() -> { if (menuManager.isAnyRemoteConnection()) return; var menuVolume = (AnalogMenuItem) tree.getMenuById(1).orElseThrow();