diff --git a/README.md b/README.md new file mode 100644 index 0000000..c510e63 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# UDPLog +This project is a simple UDP server host that can run multiple instances of a UDP server on different ports. When data is sent to it, it writes the data to a log file. + +You can configure it using a simple `settings.ini` file. + +### Intent +The original purpose of this project was to provide a way for microcontrollers such as the Arduino line or the `Raspberry Pi Pico W` etc. to be able to send data to a computer for ongoing data logging. Doing it over the network is MUCH EASIER than trying to do it via bluetooth over serial. + +The program will work for anything that can send data via UDP to an IP address or network host, it is not limited to microcontroller projects. + +## Installation +Look in the releases section and download the zip file for your operating system, then unzip it. It has been compiled on MacOS, Windows and Ubuntu Linux. They are native binaries and do not require the Java virtual machine to run. When you download the executable, you run it and after first launch, it will create a file in the folder it was launched from called `settings.ini`. + +## Usage +Open the settings file and change the options to suit your needs. The format of the settings file is: + +``` +[Server1] +port=4444 +bufferSize=512 +logFile=/Users/user/logs/logFile1.txt + +[Server2] +port=4445 +bufferSize=512 +logFile=/Users/user/logs/logFile2.txt +``` + +Here is the breakdown + +- Section Heading + - The section headings must be enclosed in brackets and the first word must be `Server`. Each section will create a new UDP server instance, so you can have as many as you like. You're only limited by the amount of RAM you have in your computer and each instance doesn't take much ram at all (maybe a hundred bytes or less). + +- ***port*** + - This is the UDP port number that you need to send your data to (1 - 65535) see [this link](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers) for port numbers you should not use. +- ***bufferSize*** + - This is the size of the UDP packet you desire to send. In Arduino environments, make this number larger than the buffer size you use to build your UDP packet. If that size is unknown then simply set it to be larger than the largest size your project might send. The data frame in a UDP packet can only be 65,535 bytes (64k), but there is overhead in those packets so your size should be smaller than that. Common practice is to not exceed 1,500 for your packet size because of fragmentation potential etc. But on a local network, you can safely go much larger than that since you most likely won't be traversing multiple routers processing heavy amounts of traffic. You don't need to be precise here ... going with a size that is larger than you anticipate is a good idea (so lets say you wont ever send a packet larger than 500 bytes, set this value to 1,000 and you'll be fine) +- ***logFile*** + - This is the FULL PATH to the log file that you want that specific server instance to write the data to. (A Windows path would need to be in standard Windows path format - `C:\Users\user\logs\logFile.txt` for example) + +### Program Arguments +```aiignore +UDPLog test +UDPLog version +``` + +Passing in the word `test` will give you feedback as the program runs which lets you perform data send tests and the program will let you know if your data was received etc. + +## Example Arduino Code +This is how a typical Arduino sketch might look like that would use this program: +```C++ +#include +#include + +WiFiUDP udpSend; + +const auto ssid = "ssid"; +const auto password = "ssidPassword"; +const IPAddress logIPAddress(192,168,1,15); // IP address to send data to +constexpr uint32_t logPort = 60379; // UDP port to send data on + +void sendUDP(const String &msg, const int udpPort = logPort) { + if (WiFi.status() == WL_CONNECTED) { + int len = msg.length(); + char buffer[len + 1]; + snprintf(buffer, sizeof(buffer), "%s", msg.c_str()); + udpSend.beginPacket(logIPAddress, udpPort); + udpSend.write(buffer); + udpSend.endPacket(); + } +} + +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); +} + +void loop() { + if(logData) { + sendUDP(myLogData, logPort); + } +} +``` + +## Service / Daemon +You can use the program as a Windows service or a linux daemon, but you'll have to Google how to do that if you're interested in it. + +### Assistance +If you have any questions, comments or problems, simply create an Issue in this repo and I'll reply as soon as I see it. + +# Release Notes +- 1.0.0 - Initial release diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..958874b --- /dev/null +++ b/pom.xml @@ -0,0 +1,163 @@ + + + 4.0.0 + + com.simtechdata + UDPLog + 1.0.0 + jar + + UDPLog + Creates Server threads that listen for data on UDP ports then logs data into log files + + + 22 + 22 + UTF-8 + 2.18.0 + 3.5.0 + 3.7.1 + 0.10.4 + com.simtechdata.App + + + + + + + + + commons-io + commons-io + 2.18.0 + + + + + + + src/main/resources + true + + version.properties + + + + + + + org.codehaus.mojo + versions-maven-plugin + ${versions-maven-plugin} + + + + display-dependency-updates + display-plugin-updates + property-updates-report + dependency-updates-report + plugin-updates-report + update-properties + use-latest-versions + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin} + + + enforce-maven + + enforce + + + + + 4.0.0-beta-3 + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin} + + + + ${mainClass} + + + + jar-with-dependencies + + ${name} + + + + make-assembly + package + + single + + + + + + + + + maven_central + Maven Central + https://repo.maven.apache.org/maven2/ + + + + + + native + + + + + org.graalvm.buildtools + native-maven-plugin + ${native-maven-plugin} + true + + + build-native + + + + ${java.version} + ${project.name} + ${mainClass} + true + false + + -Djava.awt.headless=true + --no-fallback + --verbose + --enable-preview + --enable-http + --enable-https + --initialize-at-build-time=org.sqlite.util.ProcessRunner + -H:+UnlockExperimentalVMOptions + -H:+ReportExceptionStackTraces + -H:Name=${project.name} + + + + + + + + diff --git a/src/main/java/com/simtechdata/App.java b/src/main/java/com/simtechdata/App.java new file mode 100644 index 0000000..5b7f75f --- /dev/null +++ b/src/main/java/com/simtechdata/App.java @@ -0,0 +1,158 @@ +package com.simtechdata; + +import com.simtechdata.network.ServerManager; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +public class App { + + public static boolean graal = false; + public static boolean test = false; + + public static void main(String[] args) throws Exception { + for(String a : args) { + String arg = a.toLowerCase(); + switch(arg) { + case "version", "--version", "/version" -> { + showVersion(); + System.exit(0); + } + case "graal", "graalvm" -> { + graal = true; + graal(); + } + case "test" -> { + test = true; + } + default -> { + System.out.println("Unknown argument: " + a); + System.exit(0); + } + } + } + if(!graal) { + startServers(); + } + } + + private static void startServers() { + Path path = Paths.get(System.getProperty("user.dir")); + File settingsFile = path.resolve("settings.ini").toFile(); + try { + if(settingsFile.exists()) { + String settingsContents = FileUtils.readFileToString(settingsFile); + String[] settings = settingsContents.split("\n"); + StringBuilder sb = new StringBuilder(); + boolean getSettings = false; + int countValid = 0; + for(int x=0; x udpPort = Integer.parseInt(value); + case "buffersize" -> bufferSize = Integer.parseInt(value); + case "logfile" -> logFilePath = value; + default ->{} + } + } + } + if(!logFilePath.isEmpty() && udpPort > 0 && bufferSize > 0) { + ServerManager serverManager = new ServerManager(logFilePath, udpPort, bufferSize); + new Thread(serverManager).start(); + } + else { + System.err.println("\nSection " + header + " seems to not contain correct settings."); + System.err.println("Check settings.ini and try again\n"); + } + } + + private static String settingsFile() { + return """ + [Server1] + port=4444 + bufferSize=512 + logFile=/Users/user/logs/logFile1.txt + + [Server2] + port=4445 + bufferSize=512 + logFile=/Users/user/logs/logFile2.txt + """; + } + + private static void showVersion() { + Properties prop = new Properties(); + try (InputStream input = App.class.getClassLoader().getResourceAsStream("version.properties")) { + if (input == null) { + System.out.println("Could not determine current version"); + } + else { + prop.load(input); + System.out.println(prop.getProperty("version")); + } + } + catch (IOException e) { + System.out.println(Arrays.toString(e.getStackTrace())); + throw new RuntimeException(e); + } + } + + private static void graal() { + Path logFilePath = Paths.get(System.getProperty("user.home"),"temp","logfileUDPLog.txt"); + int udpPort = 54321; + int bufferSize = 512; + ServerManager serverManager = new ServerManager(logFilePath.toString(), udpPort, bufferSize); + new Thread(serverManager).start(); + System.out.println("Use an app to send some data to this machine on UDP port " + udpPort); + } +} diff --git a/src/main/java/com/simtechdata/network/ServerManager.java b/src/main/java/com/simtechdata/network/ServerManager.java new file mode 100644 index 0000000..a0a4a55 --- /dev/null +++ b/src/main/java/com/simtechdata/network/ServerManager.java @@ -0,0 +1,64 @@ +package com.simtechdata.network; + +import com.simtechdata.App; + +import java.net.DatagramSocket; +import java.net.SocketException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ServerManager implements Runnable { + + + private final ScheduledExecutorService SCHEDULER = Executors.newSingleThreadScheduledExecutor(); + + public ServerManager(String logFilePath, int udpPort, int bufferSize) { + this.logFilePath = logFilePath; + this.udpPort = udpPort; + this.bufferSize = bufferSize; + } + + private final String logFilePath; + private final int udpPort; + private final int bufferSize; + private UDPServer udpServer; + private Thread thread; + + @Override + public void run() { + if(udpPortAvailable(udpPort)) { + SCHEDULER.scheduleAtFixedRate(checkServer(), 0, 5, TimeUnit.SECONDS); + } + } + + private void startServer() { + udpServer = new UDPServer(logFilePath, udpPort, bufferSize); + thread = new Thread(udpServer); + thread.start(); + if(App.test) { + System.out.println("Started server on port: " + udpPort); + } + } + + private Runnable checkServer() { + return () -> { + if (udpServer == null || !udpServer.isRunning() || thread == null || !thread.isAlive()) { + startServer(); + } + }; + } + + public boolean udpPortAvailable(int port) { + try (DatagramSocket socket = new DatagramSocket(port)) { + socket.setReuseAddress(true); + return true; + } catch (SocketException e) { + System.err.println("The selected UDP port: " + udpPort + " Is already in use on this machine."); + System.err.println("Please set a different port and try again."); + System.exit(0); + } + return false; + } + +} diff --git a/src/main/java/com/simtechdata/network/UDPServer.java b/src/main/java/com/simtechdata/network/UDPServer.java new file mode 100644 index 0000000..8a7448c --- /dev/null +++ b/src/main/java/com/simtechdata/network/UDPServer.java @@ -0,0 +1,94 @@ +package com.simtechdata.network; + +import com.simtechdata.App; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; + +public class UDPServer implements Runnable { + + public UDPServer(String logFilePath, int udpPort, int bufferSize) { + this.logFilePath = logFilePath; + this.udpPort = udpPort; + this.bufferSize = bufferSize; + } + + private final String logFilePath; + private final int udpPort; + private final int bufferSize; + private DatagramSocket socket; + private boolean running = true; + @Override + public void run() { + try { + socket = new DatagramSocket(udpPort); + socket.setReuseAddress(true); + while (running) { + DatagramPacket inPacket = getInPacket(); + if (socket != null && !socket.isClosed()) { + socket.receive(inPacket); + new Thread(logData(inPacket)).start(); + } + else { + running = false; + return; + } + } + } + catch(SocketException ignored) { + System.out.println("UDP Socket Closed"); + } + catch (IOException e) { + e.printStackTrace(); + } + running = false; + } + + + private DatagramPacket getInPacket() { + byte[] buffer = new byte[bufferSize]; + return new DatagramPacket(buffer, buffer.length); + } + + + private Runnable logData(DatagramPacket inPacket) { + return () -> { + String data = new String(inPacket.getData(), 0, inPacket.getLength()); + File file = new File(logFilePath); + if(file.exists()) { + try { + String fileContents = FileUtils.readFileToString(file); + fileContents += data + "\n"; + FileUtils.writeStringToFile(file, fileContents); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + else { + try { + FileUtils.createParentDirectories(file); + FileUtils.writeStringToFile(file, data + "\n"); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + if(App.test) { + System.out.println("Wrote:\n" + data + "\n\nTo log file: " + file.getAbsolutePath()); + } + if(App.graal) { + System.out.println("Data received and logged into file: " + file.getAbsolutePath()); + System.exit(0); + } + }; + } + + public boolean isRunning() { + return running; + } +} diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties new file mode 100644 index 0000000..defbd48 --- /dev/null +++ b/src/main/resources/version.properties @@ -0,0 +1 @@ +version=${project.version}