From 38b19ca18173d8b6ca49c6348c2730b2e565e395 Mon Sep 17 00:00:00 2001 From: Dariusz Zbyrad <dariusz.zbyrad@allegro.pl> Date: Mon, 25 Nov 2024 11:10:44 +0100 Subject: [PATCH 1/4] Refactor BoardInfoHelper class and belonging classes --- .../boardinfo/datareader/BoardCodeReader.java | 92 +++++ .../boardinfo/datareader/CpuInfoReader.java | 93 +++++ .../boardinfo/datareader/MemInfoReader.java | 93 +++++ .../pi4j/boardinfo/definition/HeaderPins.java | 2 +- .../com/pi4j/boardinfo/model/JvmMemory.java | 2 +- .../pi4j/boardinfo/util/BoardInfoHelper.java | 324 +++++++++++------- .../boardinfo/util/CommandProperties.java | 64 ++++ .../pi4j/boardinfo/util/SystemProperties.java | 109 ++++++ .../util/command/CommandExecutor.java | 126 +++++++ .../boardinfo/util/command/CommandResult.java | 136 ++++++++ pi4j-core/src/main/java/module-info.java | 4 + .../datareader/BoardCodeReaderTest.java | 100 ++++++ .../datareader/CpuInfoReaderTest.java | 99 ++++++ .../datareader/MemInfoReaderTest.java | 127 +++++++ .../util/BoardModelDetectionTest.java | 141 +++++++- 15 files changed, 1382 insertions(+), 130 deletions(-) create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/util/CommandProperties.java create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/util/SystemProperties.java create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java create mode 100644 pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/BoardCodeReaderTest.java create mode 100644 pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/CpuInfoReaderTest.java create mode 100644 pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/MemInfoReaderTest.java diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java new file mode 100644 index 00000000..5ec6bc68 --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java @@ -0,0 +1,92 @@ +package com.pi4j.boardinfo.datareader; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : BoardCodeReader.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.pi4j.boardinfo.util.command.CommandResult; +import com.pi4j.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +import static com.pi4j.boardinfo.util.command.CommandResult.failure; +import static com.pi4j.boardinfo.util.command.CommandResult.success; + +/** + * This class reads the board model code from the file system, specifically + * from the `/proc/device-tree/model` file on Raspberry Pi systems. + */ +public class BoardCodeReader { + + private static final Logger logger = LoggerFactory.getLogger(BoardCodeReader.class); + private static String modelFilePath = "/proc/device-tree/model"; + + /** + * Sets the file path for testing purposes. + * + * @param path The file path to be used. + */ + public static void setModelFilePath(String path) { + modelFilePath = path; + } + + /** + * Reads the board model code from the file system. + * + * @return A {@link CommandResult} containing: + * - {@code success}: true if the file was read successfully, false otherwise. + * - {@code outputMessage}: the content of the model file (trimmed). + * - {@code errorMessage}: any error message encountered during the process. + */ + public static CommandResult getBoardCode() { + String outputMessage = StringUtil.EMPTY; + String errorMessage = StringUtil.EMPTY; + + try (BufferedReader reader = new BufferedReader(new FileReader(modelFilePath))) { + StringBuilder content = new StringBuilder(); + String line; + + // Read the entire content of the file line by line. + while ((line = reader.readLine()) != null) { + content.append(line); + } + + outputMessage = content.toString().trim(); // Remove unnecessary whitespace. + } catch (IOException ex) { + errorMessage = "IOException: " + ex.getMessage(); + logger.error("Failed to read the board model from '{}': {}", modelFilePath, errorMessage); + } + + // Return CommandResult based on success or failure + if (!errorMessage.isEmpty()) { + return failure(errorMessage); + } + + return success(outputMessage); + } +} diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java new file mode 100644 index 00000000..ad756aba --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java @@ -0,0 +1,93 @@ +package com.pi4j.boardinfo.datareader; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : CpuInfoReader.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +import com.pi4j.boardinfo.util.command.CommandResult; +import com.pi4j.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +import static com.pi4j.boardinfo.util.command.CommandResult.failure; +import static com.pi4j.boardinfo.util.command.CommandResult.success; + +/** + * This class reads CPU information from the file system, specifically + * the `/proc/cpuinfo` file on Linux systems, to extract the CPU revision. + */ +public class CpuInfoReader { + + private static final Logger logger = LoggerFactory.getLogger(CpuInfoReader.class); + private static String cpuInfoFilePath = "/proc/cpuinfo"; + + /** + * Sets the CPU info file path for testing purposes. + * + * @param path The file path to be used. + */ + public static void setCpuInfoFilePath(String path) { + cpuInfoFilePath = path; + } + + /** + * Reads the CPU revision from the `/proc/cpuinfo` file. + * + * @return A {@link CommandResult} containing: + * - {@code success}: true if the revision was successfully extracted, false otherwise. + * - {@code outputMessage}: the CPU revision value. + * - {@code errorMessage}: any error message encountered during the process. + */ + public static CommandResult getCpuRevision() { + String outputMessage = StringUtil.EMPTY; + String errorMessage = StringUtil.EMPTY; + + try (BufferedReader reader = new BufferedReader(new FileReader(cpuInfoFilePath))) { + String line; + // Read file line by line to locate the "Revision" entry. + while ((line = reader.readLine()) != null) { + if (line.startsWith("Revision")) { + String[] parts = line.split(":"); + if (parts.length > 1) { + outputMessage = parts[1].trim(); // Extract and trim the revision value. + } + break; // No need to process further once "Revision" is found. + } + } + } catch (IOException ex) { + errorMessage = "IOException: " + ex.getMessage(); + logger.error("Failed to read the CPU revision from '{}': {}", cpuInfoFilePath, errorMessage); + } + + // Return CommandResult based on success or failure + if (!errorMessage.isEmpty() || outputMessage.isEmpty()) { + return failure(errorMessage.isEmpty() ? "CPU revision not found in file" : errorMessage); + } + + return success(outputMessage); + } +} diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java new file mode 100644 index 00000000..fd9ba551 --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java @@ -0,0 +1,93 @@ +package com.pi4j.boardinfo.datareader; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : MemInfoReader.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.pi4j.boardinfo.util.command.CommandResult; +import com.pi4j.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +import static com.pi4j.boardinfo.util.command.CommandResult.failure; +import static com.pi4j.boardinfo.util.command.CommandResult.success; + +/** + * This class reads memory information from the file system, specifically + * the `/proc/meminfo` file on Linux systems, to extract the total memory. + */ +public class MemInfoReader { + + private static final Logger logger = LoggerFactory.getLogger(MemInfoReader.class); + private static String memInfoFilePath = "/proc/meminfo"; + + /** + * Sets the memory info file path for testing purposes. + * + * @param path The file path to be used. + */ + public static void setMemInfoFilePath(String path) { + memInfoFilePath = path; + } + + /** + * Reads the memory information file and extracts the "MemTotal" entry. + * + * @return A {@link CommandResult} containing: + * - {@code success}: true if the "MemTotal" entry is found and valid. + * - {@code outputMessage}: the value of the "MemTotal" entry (trimmed). + * - {@code errorMessage}: an error message if the entry is not found or if reading fails. + */ + public static CommandResult getMemTotal() { + String errorMessage = StringUtil.EMPTY; + String memTotalLine = StringUtil.EMPTY; + + try (BufferedReader reader = new BufferedReader(new FileReader(memInfoFilePath))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("MemTotal:")) { + memTotalLine = line.trim(); + break; + } + } + } catch (IOException ex) { + errorMessage = "IOException: " + ex.getMessage(); + logger.error("Failed to read memory information from '{}': {}", memInfoFilePath, errorMessage); + } + + // Return CommandResult based on whether "MemTotal" was found + if (!errorMessage.isEmpty()) { + return failure(errorMessage); + } + if (memTotalLine.isEmpty()) { + return failure("MemTotal entry not found in memory information file."); + } + + return success(memTotalLine); + } +} \ No newline at end of file diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/HeaderPins.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/HeaderPins.java index 571dbc7a..c304eaad 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/HeaderPins.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/HeaderPins.java @@ -17,7 +17,7 @@ public enum HeaderPins { COMPUTE_J6("Compute J6", getComputeJ6()); private final String label; - private List<HeaderPin> pins; + private final List<HeaderPin> pins; HeaderPins(String label, List<HeaderPin> pins) { this.label = label; diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/model/JvmMemory.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/model/JvmMemory.java index bd84e2ec..f19b91b9 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/model/JvmMemory.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/model/JvmMemory.java @@ -2,7 +2,7 @@ public class JvmMemory { - private static double mb = 1024.0 * 1024.0; + private static final double mb = 1024.0 * 1024.0; private final long total; private final long free; diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java index c6335c4d..f9d98096 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java @@ -1,88 +1,203 @@ package com.pi4j.boardinfo.util; +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : BoardInfoHelper.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import com.pi4j.boardinfo.datareader.BoardCodeReader; +import com.pi4j.boardinfo.datareader.CpuInfoReader; +import com.pi4j.boardinfo.datareader.MemInfoReader; import com.pi4j.boardinfo.definition.BoardModel; -import com.pi4j.boardinfo.model.*; +import com.pi4j.boardinfo.model.BoardInfo; +import com.pi4j.boardinfo.model.BoardReading; +import com.pi4j.boardinfo.model.JavaInfo; +import com.pi4j.boardinfo.model.JvmMemory; +import com.pi4j.boardinfo.model.OperatingSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; -import java.util.concurrent.TimeUnit; - +import static com.pi4j.boardinfo.util.CommandProperties.CORE_VOLTAGE_COMMAND; +import static com.pi4j.boardinfo.util.CommandProperties.TEMPERATURE_COMMAND; +import static com.pi4j.boardinfo.util.CommandProperties.UPTIME_COMMAND; +import static com.pi4j.boardinfo.util.SystemProperties.ARCHITECTURE_DATA_MODEL; +import static com.pi4j.boardinfo.util.SystemProperties.JAVA_RUNTIME_VERSION; +import static com.pi4j.boardinfo.util.SystemProperties.JAVA_VENDOR; +import static com.pi4j.boardinfo.util.SystemProperties.JAVA_VENDOR_VERSION; +import static com.pi4j.boardinfo.util.SystemProperties.JAVA_VERSION; +import static com.pi4j.boardinfo.util.SystemProperties.OS_ARCH; +import static com.pi4j.boardinfo.util.SystemProperties.OS_NAME; +import static com.pi4j.boardinfo.util.SystemProperties.OS_VERSION; +import static com.pi4j.boardinfo.util.command.CommandExecutor.execute; + +/** + * The {@code BoardInfoHelper} class provides utility methods for detecting system and board information. + * It retrieves detailed information about the operating system, Java runtime, board model, memory, temperature, + * voltage, and other relevant hardware details of the Raspberry Pi or other compatible devices. + * + * <p>This class uses a singleton pattern to ensure that board information is detected only once and can be reused + * throughout the application. The detected board information is stored in the {@link BoardInfo} object and can + * be accessed via the {@link #current()} method.</p> + */ public class BoardInfoHelper { private static final Logger logger = LoggerFactory.getLogger(BoardInfoHelper.class); - private static final BoardInfoHelper instance; - private BoardInfo boardInfo; - - static { - instance = new BoardInfoHelper(); - } + private final BoardInfo boardInfo; + /** + * Private constructor to initialize the board information by detecting the operating system, Java version, + * and board details. + * This method will try to detect the board info using both the board code and name. + */ private BoardInfoHelper() { - var os = new OperatingSystem(System.getProperty("os.name"), System.getProperty("os.version"), - System.getProperty("os.arch")); + OperatingSystem os = new OperatingSystem( + System.getProperty(OS_NAME), + System.getProperty(OS_VERSION), + System.getProperty(OS_ARCH) + ); logger.info("Detected OS: {}", os); - var java = new JavaInfo(System.getProperty("java.version"), System.getProperty("java.runtime.version"), - System.getProperty("java.vendor"), System.getProperty("java.vendor.version")); + JavaInfo java = new JavaInfo( + System.getProperty(JAVA_VERSION), + System.getProperty(JAVA_RUNTIME_VERSION), + System.getProperty(JAVA_VENDOR), + System.getProperty(JAVA_VENDOR_VERSION) + ); logger.info("Detected Java: {}", java); - // Example output: c03111 - var boardVersionCode = getBoardVersionCode(); + this.boardInfo = detectBoardInfo(os, java); + } + + /** + * Returns the current instance of {@code BoardInfoHelper}, which contains board information. + * + * @return the current {@link BoardInfo} instance + */ + public static BoardInfo current() { + return SingletonHelper.INSTANCE.boardInfo; + } + + /** + * Reinitializes the singleton instance of {@code BoardInfoHelper}. + * This method is useful for testing or reloading board information in controlled scenarios. + * + * <p>Note: Frequent usage of this method is not recommended as it resets the global state.</p> + */ + public static synchronized void reinitialize() { + logger.info("Reinitializing BoardInfoHelper singleton instance."); + SingletonHelper.resetInstance(); + } + + /** + * Inner static class responsible for holding the singleton instance of {@code BoardInfoHelper}. + * Implements the Bill Pugh Singleton Design, ensuring thread-safety and lazy initialization. + */ + private static class SingletonHelper { + private static volatile BoardInfoHelper INSTANCE = new BoardInfoHelper(); + + /** + * Resets the singleton instance for reinitialization. + */ + private static synchronized void resetInstance() { + INSTANCE = new BoardInfoHelper(); + } + } + + /** + * Detects the board information by attempting to retrieve the board version code or name. + * This method prioritizes detection by board version code and falls back to board name if necessary. + * + * @param os the detected operating system information + * @param java the detected Java runtime information + * @return a {@link BoardInfo} object containing the detected board information + */ + private BoardInfo detectBoardInfo(OperatingSystem os, JavaInfo java) { + String boardVersionCode = getBoardVersionCode(); try { - var boardModelByBoardCode = BoardModel.getByBoardCode(boardVersionCode); + BoardModel boardModelByBoardCode = BoardModel.getByBoardCode(boardVersionCode); if (boardModelByBoardCode != BoardModel.UNKNOWN) { logger.info("Detected board type {} by code: {}", boardModelByBoardCode.name(), boardVersionCode); - this.boardInfo = new BoardInfo(boardModelByBoardCode, os, java); - return; + return new BoardInfo(boardModelByBoardCode, os, java); } } catch (Exception e) { logger.warn("Could not detect the board type for code {}: {}", boardVersionCode, e.getMessage()); } - // Example output: Raspberry Pi 4 Model B Rev 1.1 - var boardName = getBoardName(); - var boardModelByBoardName = BoardModel.getByBoardName(boardName); + String boardName = getBoardName(); + BoardModel boardModelByBoardName = BoardModel.getByBoardName(boardName); if (boardModelByBoardName != BoardModel.UNKNOWN) { logger.info("Detected board type {} by name: {}", boardModelByBoardName.name(), boardName); - this.boardInfo = new BoardInfo(boardModelByBoardName, os, java); - return; + return new BoardInfo(boardModelByBoardName, os, java); } - // Maybe there are other ways how a board can be detected? - // If so, this method can be further extended... logger.warn("Sorry, could not detect the board type"); - this.boardInfo = new BoardInfo(BoardModel.UNKNOWN, os, java); - } - - public static BoardInfo current() { - return instance.boardInfo; + return new BoardInfo(BoardModel.UNKNOWN, os, java); } /** - * Flag indicating that the board is using the RP1 chip for GPIO. - * <a href="https://www.raspberrypi.com/documentation/microcontrollers/rp1.html">https://www.raspberrypi.com/documentation/microcontrollers/rp1.html</a> - * @return + * Checks if the device uses the RP1 processor. + * + * @return {@code true} if the board is a Raspberry Pi Model 5B, otherwise {@code false}. */ public static boolean usesRP1() { - return instance.boardInfo.getBoardModel() == BoardModel.MODEL_5_B; + return current().getBoardModel() == BoardModel.MODEL_5_B; } + /** + * Checks if the application is running on a Raspberry Pi device. + * + * @return {@code true} if the board model is not {@link BoardModel#UNKNOWN}, otherwise {@code false}. + */ public static boolean runningOnRaspberryPi() { - return instance.boardInfo.getBoardModel() != BoardModel.UNKNOWN; + return current().getBoardModel() != BoardModel.UNKNOWN; } + /** + * Checks if the system architecture is 32-bit. + * + * @return {@code true} if the system is 32-bit, otherwise {@code false}. + */ public static boolean is32bit() { return !is64bit(); } + /** + * Checks if the system architecture is 64-bit. + * + * @return {@code true} if the system is 64-bit, otherwise {@code false}. + */ public static boolean is64bit() { - return System.getProperty("sun.arch.data.model").equals("64"); + return System.getProperty(ARCHITECTURE_DATA_MODEL).equals("64"); } + /** + * Retrieves the board version code by reading CPU revision information. + * + * @return the board version code, or an empty string if not found + */ public static String getBoardVersionCode() { - var output = getCommandOutput("cat /proc/cpuinfo | grep 'Revision' | awk '{print $3}'"); + var output = CpuInfoReader.getCpuRevision(); if (output.isSuccess()) { return output.getOutputMessage(); } @@ -90,8 +205,13 @@ public static String getBoardVersionCode() { return ""; } + /** + * Retrieves the board name by reading device tree information. + * + * @return the board name, or an empty string if not found + */ public static String getBoardName() { - var output = getCommandOutput("cat /proc/device-tree/model"); + var output = BoardCodeReader.getBoardCode(); if (output.isSuccess()) { return output.getOutputMessage(); } @@ -99,98 +219,66 @@ public static String getBoardName() { return ""; } + /** + * Retrieves information about the JVM memory usage. + * + * @return a {@link JvmMemory} object containing memory details + */ public static JvmMemory getJvmMemory() { return new JvmMemory(Runtime.getRuntime()); } + /** + * Retrieves a collection of readings about the board, including name, version code, + * temperature, uptime, core voltage, and memory total. + * + * @return a {@link BoardReading} object containing the board readings + */ public static BoardReading getBoardReading() { return new BoardReading( - getCommandOutput("cat /proc/device-tree/model").getOutputMessage(), - // https://raspberry-projects.com/pi/command-line/detect-rpi-hardware-version - getCommandOutput("cat /proc/cpuinfo | grep 'Revision' | awk '{print $3}'").getOutputMessage(), - // https://linuxhint.com/commands-for-hardware-information-raspberry-pi/ - getCommandOutput("vcgencmd measure_temp").getOutputMessage(), - getCommandOutput("uptime").getOutputMessage(), - // https://linuxhint.com/find-hardware-information-raspberry-pi/ - getCommandOutput("vcgencmd measure_volts").getOutputMessage(), - // https://www.baeldung.com/linux/total-physical-memory - getCommandOutput("cat /proc/meminfo | head -n 1").getOutputMessage() + getBoardName(), + getBoardVersionCode(), + getTemperature(), + getUptime(), + getCoreVoltage(), + getMemTotal() ); } - private static class CommandResult { - private final boolean success; - private final String outputMessage; - private final String errorMessage; - - public CommandResult(boolean success, String outputMessage, String errorMessage) { - this.success = success; - this.outputMessage = outputMessage; - this.errorMessage = errorMessage; - } - - public boolean isSuccess() { - return success; - } - - public String getOutputMessage() { - return outputMessage; - } - - public String getErrorMessage() { - return errorMessage; - } + /** + * Retrieves the total memory information from the system. + * + * @return a string representing the total memory + */ + private static String getMemTotal() { + return MemInfoReader.getMemTotal().getOutputMessage(); } - private static CommandResult getCommandOutput(String command) { - boolean finished = false; - String outputMessage = ""; - String errorMessage = ""; - - ProcessBuilder builder = new ProcessBuilder(); - builder.command("sh", "-c", command); - - try { - Process process = builder.start(); - - OutputStream outputStream = process.getOutputStream(); - InputStream inputStream = process.getInputStream(); - InputStream errorStream = process.getErrorStream(); - - outputMessage = readStream(inputStream); - errorMessage = readStream(errorStream); - - finished = process.waitFor(30, TimeUnit.SECONDS); - outputStream.flush(); - outputStream.close(); - - if (!finished) { - process.destroyForcibly(); - } - } catch (IOException ex) { - errorMessage = "IOException: " + ex.getMessage(); - } catch (InterruptedException ex) { - errorMessage = "InterruptedException: " + ex.getMessage(); - } - - if (!finished || !errorMessage.isEmpty()) { - logger.error("Could not execute '{}' to detect the board model: {}", command, errorMessage); - return new CommandResult(false, outputMessage, errorMessage); - } + /** + * Retrieves the core voltage reading of the board. + * + * @return a string representing the core voltage + */ + private static String getCoreVoltage() { + return execute(CORE_VOLTAGE_COMMAND).getOutputMessage(); + } - return new CommandResult(true, outputMessage, errorMessage); + /** + * Retrieves the system uptime. + * + * @return a string representing the uptime + */ + private static String getUptime() { + return execute(UPTIME_COMMAND).getOutputMessage(); } - private static String readStream(InputStream inputStream) { - StringBuilder rt = new StringBuilder(); - try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { - String line; - while ((line = bufferedReader.readLine()) != null) { - rt.append(line); - } - } catch (Exception ex) { - rt.append("ERROR: ").append(ex.getMessage()); - } - return rt.toString(); + /** + * Retrieves the system temperature reading. + * + * @return a string representing the temperature + */ + private static String getTemperature() { + return execute(TEMPERATURE_COMMAND).getOutputMessage(); } } + diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/CommandProperties.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/CommandProperties.java new file mode 100644 index 00000000..d462c3e0 --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/CommandProperties.java @@ -0,0 +1,64 @@ +package com.pi4j.boardinfo.util; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : CommandProperties.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +/** + * Interface that defines constants for common system commands. + * These commands are typically used to gather system-related information + * such as voltage, uptime, and temperature on Raspberry Pi or similar systems. + */ +public interface CommandProperties { + + /** + * Command to measure the core voltage of the system. + * <p> + * This command uses the `vcgencmd` tool to query the system's core voltage, + * which is useful for monitoring the power supply to the system's central processing unit (CPU). + * The result is usually in the format of a voltage value (e.g., "1.20V"). + * </p> + */ + String CORE_VOLTAGE_COMMAND = "vcgencmd measure_volts"; + + /** + * Command to retrieve the system's uptime. + * <p> + * This command uses the `uptime` utility to get the amount of time the system has been running since + * its last boot. The output typically includes the system's uptime in days, hours, and minutes. + * This can be useful for monitoring the system's stability or for detecting if the system has been restarted recently. + * </p> + */ + String UPTIME_COMMAND = "uptime"; + + /** + * Command to measure the temperature of the system's CPU. + * <p> + * This command uses the `vcgencmd` tool to query the CPU temperature of the system, which is crucial + * for thermal management and ensuring that the system is not overheating. The output typically includes + * the temperature value in Celsius (e.g., "48.3'C"). + * </p> + */ + String TEMPERATURE_COMMAND = "vcgencmd measure_temp"; +} diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/SystemProperties.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/SystemProperties.java new file mode 100644 index 00000000..718e9794 --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/SystemProperties.java @@ -0,0 +1,109 @@ +package com.pi4j.boardinfo.util; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : SystemProperties.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +/** + * Interface that defines constants for common system properties. + * These properties provide information about the operating system, + * architecture, and Java runtime environment. + */ +public interface SystemProperties { + + /** + * The name of the operating system. + * <p> + * This property represents the name of the operating system, such as + * "Windows 10", "Linux", or "macOS". + * </p> + */ + String OS_NAME = "os.name"; + + /** + * The version of the operating system. + * <p> + * This property provides the version of the operating system, for example, + * "10.0" for Windows 10 or "5.4.0" for a specific Linux kernel version. + * </p> + */ + String OS_VERSION = "os.version"; + + /** + * The architecture of the operating system. + * <p> + * This property indicates the CPU architecture, such as "x86", "x86_64", or "arm". + * It gives a hint of the underlying hardware architecture on which the operating system is running. + * </p> + */ + String OS_ARCH = "os.arch"; + + /** + * The architecture's data model (either 32-bit or 64-bit). + * <p> + * This property indicates the data model of the JVM, either "32" or "64", + * based on whether the underlying architecture is 32-bit or 64-bit. + * </p> + * For example, on a 64-bit system, it would return "64", whereas on a 32-bit system, + * it would return "32". This property can help determine the system's memory addressing capacity. + */ + String ARCHITECTURE_DATA_MODEL = "sun.arch.data.model"; + + /** + * The version of the Java Runtime Environment (JRE). + * <p> + * This property provides the version number of the Java runtime, such as "1.8.0_291" + * or "17.0.1", depending on the version of the JRE installed on the system. + * </p> + */ + String JAVA_VERSION = "java.version"; + + /** + * The version of the Java Runtime Environment (JRE) vendor. + * <p> + * This property provides the specific version of the Java runtime being used, + * which may include additional information about vendor-specific patches or modifications. + * For example, "Oracle Corporation" or "OpenJDK" may appear as the vendor name. + * </p> + */ + String JAVA_RUNTIME_VERSION = "java.runtime.version"; + + /** + * The name of the Java vendor. + * <p> + * This property returns the name of the company or organization that provides the Java runtime. + * Examples include "Oracle Corporation", "OpenJDK", or "Amazon Corretto". + * </p> + */ + String JAVA_VENDOR = "java.vendor"; + + /** + * The version of the Java vendor's implementation of the JRE. + * <p> + * This property provides version information for the Java vendor's specific implementation. + * It may include a specific release or patch version, such as "1.8.0_291" or "17.0.1". + * </p> + */ + String JAVA_VENDOR_VERSION = "java.vendor.version"; +} diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java new file mode 100644 index 00000000..67834550 --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java @@ -0,0 +1,126 @@ +package com.pi4j.boardinfo.util.command; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : CommandExecutor.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.concurrent.TimeUnit; + +import static com.pi4j.boardinfo.util.command.CommandResult.failure; +import static com.pi4j.boardinfo.util.command.CommandResult.success; + +/** + * A utility class for executing system commands and capturing their output. + * + * <p>Please be careful when using this method. Since we decided to remove the + * 'sh -c' part, this method no longer supports shell-specific features such as pipes, + * redirection, or complex shell commands. It is intended to execute simple commands directly.</p> + */ +public class CommandExecutor { + private static final Logger logger = LoggerFactory.getLogger(CommandExecutor.class); + private static final int COMMAND_TIMEOUT_SECONDS = 30; + + /** + * Executes a given command and captures its output and error streams. + * + * <p>Please be careful when using this method. Since we decided to remove the + * 'sh -c' part, this method no longer supports pipes, redirection, or other + * shell-specific features. Only simple, direct commands should be executed.</p> + * + * @param command The command to execute (must be simple, direct command without shell features). + * @return A {@link CommandResult} containing: + * - {@code success}: true if the command executed successfully within the timeout. + * - {@code outputMessage}: the standard output from the command. + * - {@code errorMessage}: the error output or any exception message. + */ + public static CommandResult execute(String command) { + boolean finished = false; + String outputMessage = ""; + String errorMessage = ""; + + // Configure the process builder with the command (no shell involved). + ProcessBuilder builder = new ProcessBuilder(command.split(" ")); + + try { + // Start the process + Process process = builder.start(); + + // Read output and error streams + outputMessage = readStream(process.getInputStream()); + errorMessage = readStream(process.getErrorStream()); + + // Wait for the process to complete or timeout + finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + // Ensure the process is terminated if not finished + if (!finished) { + process.destroyForcibly(); + errorMessage = "Process timeout after " + COMMAND_TIMEOUT_SECONDS + " seconds."; + } + + } catch (IOException ex) { + errorMessage = "IOException while executing command: " + ex.getMessage(); + logger.error("IOException during command execution '{}': {}", command, ex.getMessage(), ex); + } catch (InterruptedException ex) { + errorMessage = "InterruptedException during command execution: " + ex.getMessage(); + Thread.currentThread().interrupt(); // Restore the interrupted status. + logger.error("InterruptedException during command execution '{}': {}", command, ex.getMessage(), ex); + } + + // Log error and return failure result if necessary + if (!finished || !errorMessage.isEmpty()) { + logger.error("Failed to execute command '{}': {}", command, errorMessage); + return failure(errorMessage); + } + + return success(outputMessage); + } + + /** + * Reads the content of an InputStream and returns it as a string. + * + * @param inputStream The InputStream to read. + * @return The content of the InputStream as a string. + */ + private static String readStream(InputStream inputStream) { + StringBuilder content = new StringBuilder(); + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + content.append(line).append(System.lineSeparator()); + } + } catch (IOException ex) { + content.append("ERROR: ").append(ex.getMessage()); + logger.error("Error reading stream: {}", ex.getMessage(), ex); + } + return content.toString().trim(); // Trim trailing line separator. + } +} diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java new file mode 100644 index 00000000..4bd70ea1 --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java @@ -0,0 +1,136 @@ +package com.pi4j.boardinfo.util.command; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : CommandResult.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.util.Objects; +import java.util.Optional; + +/** + * Represents the result of executing a command. + */ +public class CommandResult { + private final boolean success; + private final String outputMessage; + private final String errorMessage; + + /** + * Constructor to create a new CommandResult. + * + * @param success Whether the command execution was successful. + * @param outputMessage The standard output of the command. + * @param errorMessage The error message if the command failed. + */ + private CommandResult(boolean success, String outputMessage, String errorMessage) { + this.success = success; + this.outputMessage = Optional.ofNullable(outputMessage).orElse(""); + this.errorMessage = Optional.ofNullable(errorMessage).orElse(""); + } + + /** + * Static method to create a successful CommandResult. + * + * @param outputMessage The standard output of the successful command. + * @return A CommandResult representing a successful command execution. + */ + public static CommandResult success(String outputMessage) { + return new CommandResult(true, outputMessage, null); + } + + /** + * Static method to create a failed CommandResult. + * + * @param errorMessage The error message from the failed command execution. + * @return A CommandResult representing a failed command execution. + */ + public static CommandResult failure(String errorMessage) { + return new CommandResult(false, null, errorMessage); + } + + /** + * Returns whether the command execution was successful. + * + * @return {@code true} if successful, {@code false} otherwise. + */ + public boolean isSuccess() { + return success; + } + + /** + * Returns the standard output message from the command. + * + * @return The standard output message. + */ + public String getOutputMessage() { + return outputMessage; + } + + /** + * Returns the error message from the command execution. + * + * @return The error message. + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * Provides a string representation of the CommandResult object. + * Useful for logging and debugging. + * + * @return A string describing the CommandResult. + */ + @Override + public String toString() { + return String.format("CommandResult{success=%b, outputMessage='%s', errorMessage='%s'}", + success, outputMessage, errorMessage); + } + + /** + * Compares this CommandResult to another object for equality. + * + * @param o The object to compare to. + * @return {@code true} if the objects are equal, {@code false} otherwise. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CommandResult that = (CommandResult) o; + return success == that.success && + Objects.equals(outputMessage, that.outputMessage) && + Objects.equals(errorMessage, that.errorMessage); + } + + /** + * Returns a hash code value for this CommandResult. + * + * @return The hash code for this object. + */ + @Override + public int hashCode() { + return Objects.hash(success, outputMessage, errorMessage); + } +} diff --git a/pi4j-core/src/main/java/module-info.java b/pi4j-core/src/main/java/module-info.java index 9b66aeb4..d1f75028 100644 --- a/pi4j-core/src/main/java/module-info.java +++ b/pi4j-core/src/main/java/module-info.java @@ -61,6 +61,10 @@ exports com.pi4j.provider.exception; exports com.pi4j.registry; exports com.pi4j.util; + exports com.pi4j.boardinfo.datareader; + opens com.pi4j.boardinfo.datareader; + exports com.pi4j.boardinfo.util.command; + opens com.pi4j.boardinfo.util.command; // extensibility service interfaces uses com.pi4j.extension.Plugin; diff --git a/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/BoardCodeReaderTest.java b/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/BoardCodeReaderTest.java new file mode 100644 index 00000000..48fc164f --- /dev/null +++ b/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/BoardCodeReaderTest.java @@ -0,0 +1,100 @@ +package com.pi4j.boardinfo.datareader; + +import com.pi4j.boardinfo.util.command.CommandResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +class BoardCodeReaderTest { + + private File tempModelFile; + + @BeforeEach + void setUp() throws IOException { + // Create a temporary file for testing + tempModelFile = File.createTempFile("model", ".tmp"); + BoardCodeReader.setModelFilePath(tempModelFile.getAbsolutePath()); + } + + @AfterEach + void tearDown() { + // Delete the temporary file after each test + if (tempModelFile.exists()) { + tempModelFile.delete(); + } + } + + @Test + void testGetBoardCode_Success() throws IOException { + // Write valid content to the temporary model file + try (FileWriter writer = new FileWriter(tempModelFile)) { + writer.write("Raspberry Pi 4 Model B Rev 1.1\n"); + } + + // Execute the method + CommandResult result = BoardCodeReader.getBoardCode(); + + // Verify the result + assertTrue(result.isSuccess()); + assertEquals("Raspberry Pi 4 Model B Rev 1.1", result.getOutputMessage()); + assertEquals("", result.getErrorMessage()); + } + + @Test + void testGetBoardCode_EmptyFile() throws IOException { + // Create an empty model file + try (FileWriter writer = new FileWriter(tempModelFile)) { + // No content written + } + + // Execute the method + CommandResult result = BoardCodeReader.getBoardCode(); + + // Verify the result + assertTrue(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("", result.getErrorMessage()); + } + + @Test + void testGetBoardCode_FileNotReadable() throws IOException { + // Write valid content and then make the file unreadable + try (FileWriter writer = new FileWriter(tempModelFile)) { + writer.write("Raspberry Pi 4 Model B Rev 1.1\n"); + } + tempModelFile.setReadable(false); + + // Execute the method + CommandResult result = BoardCodeReader.getBoardCode(); + + // Verify the result + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertTrue(result.getErrorMessage().contains("IOException")); + + // Restore file permissions for cleanup + tempModelFile.setReadable(true); + } + + @Test + void testGetBoardCode_InvalidContent() throws IOException { + // Write invalid content (whitespace only) to the temporary file + try (FileWriter writer = new FileWriter(tempModelFile)) { + writer.write("\n\n "); // Just whitespace + } + + // Execute the method + CommandResult result = BoardCodeReader.getBoardCode(); + + // Verify the result + assertTrue(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("", result.getErrorMessage()); + } +} diff --git a/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/CpuInfoReaderTest.java b/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/CpuInfoReaderTest.java new file mode 100644 index 00000000..2e690ecd --- /dev/null +++ b/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/CpuInfoReaderTest.java @@ -0,0 +1,99 @@ +package com.pi4j.boardinfo.datareader; + +import com.pi4j.boardinfo.util.command.CommandResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +class CpuInfoReaderTest { + + private File tempCpuInfoFile; + + @BeforeEach + void setUp() throws IOException { + tempCpuInfoFile = File.createTempFile("cpuinfo", ".tmp"); + CpuInfoReader.setCpuInfoFilePath(tempCpuInfoFile.getAbsolutePath()); + } + + @AfterEach + void tearDown() { + if (tempCpuInfoFile.exists()) { + tempCpuInfoFile.delete(); + } + } + + @Test + void testGetCpuRevision_Success() throws IOException { + try (FileWriter writer = new FileWriter(tempCpuInfoFile)) { + writer.write("Processor\t: ARMv7 Processor\n"); + writer.write("Revision\t: 000e\n"); + } + + CommandResult result = CpuInfoReader.getCpuRevision(); + + assertTrue(result.isSuccess()); + assertEquals("000e", result.getOutputMessage()); + assertEquals("", result.getErrorMessage()); + } + + @Test + void testGetCpuRevision_NoRevision() throws IOException { + try (FileWriter writer = new FileWriter(tempCpuInfoFile)) { + writer.write("Processor\t: ARMv7 Processor\n"); + } + + CommandResult result = CpuInfoReader.getCpuRevision(); + + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("CPU revision not found in file", result.getErrorMessage()); + } + + @Test + void testGetCpuRevision_FileNotReadable() throws IOException { + try (FileWriter writer = new FileWriter(tempCpuInfoFile)) { + writer.write("Revision\t: 000e\n"); + } + tempCpuInfoFile.setReadable(false); + + CommandResult result = CpuInfoReader.getCpuRevision(); + + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertTrue(result.getErrorMessage().contains("IOException")); + + tempCpuInfoFile.setReadable(true); + } + + @Test + void testGetCpuRevision_EmptyFile() throws IOException { + try (FileWriter writer = new FileWriter(tempCpuInfoFile)) { + // Empty file + } + + CommandResult result = CpuInfoReader.getCpuRevision(); + + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("CPU revision not found in file", result.getErrorMessage()); + } + + @Test + void testGetCpuRevision_CorruptedFile() throws IOException { + try (FileWriter writer = new FileWriter(tempCpuInfoFile)) { + writer.write("This is not valid CPU info\n"); + } + + CommandResult result = CpuInfoReader.getCpuRevision(); + + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("CPU revision not found in file", result.getErrorMessage()); + } +} diff --git a/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/MemInfoReaderTest.java b/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/MemInfoReaderTest.java new file mode 100644 index 00000000..723c8de6 --- /dev/null +++ b/pi4j-core/src/test/java/com/pi4j/boardinfo/datareader/MemInfoReaderTest.java @@ -0,0 +1,127 @@ +package com.pi4j.boardinfo.datareader; + +import com.pi4j.boardinfo.util.command.CommandResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +class MemInfoReaderTest { + + private File tempMemInfoFile; + + @BeforeEach + void setUp() throws IOException { + // Create a unique temporary file for testing + tempMemInfoFile = File.createTempFile("meminfo", ".tmp"); + MemInfoReader.setMemInfoFilePath(tempMemInfoFile.getAbsolutePath()); + } + + @AfterEach + void tearDown() { + // Delete the temporary file after each test + if (tempMemInfoFile.exists()) { + tempMemInfoFile.delete(); + } + } + + @Test + void testGetMemTotal_Success() throws IOException { + // Write valid content to the temporary memory info file + try (FileWriter writer = new FileWriter(tempMemInfoFile)) { + writer.write("MemTotal: 8192 kB\n"); + } + + // Execute the method + CommandResult result = MemInfoReader.getMemTotal(); + + // Verify the result + assertTrue(result.isSuccess()); + assertEquals("MemTotal: 8192 kB", result.getOutputMessage()); + assertEquals("", result.getErrorMessage()); + } + + @Test + void testGetMemTotal_EmptyFile() throws IOException { + // Create an empty memory info file + try (FileWriter writer = new FileWriter(tempMemInfoFile)) { + // Leave file empty + } + + // Execute the method + CommandResult result = MemInfoReader.getMemTotal(); + + // Verify the result + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("MemTotal entry not found in memory information file.", result.getErrorMessage()); + } + + @Test + void testGetMemTotal_FileNotReadable() { + // Simulate a file that cannot be read by deleting it + tempMemInfoFile.delete(); + + // Execute the method + CommandResult result = MemInfoReader.getMemTotal(); + + // Verify the result + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertTrue(result.getErrorMessage().contains("IOException")); + } + + @Test + void testGetMemTotal_InvalidContent() throws IOException { + // Write invalid content (whitespace only) to the temporary file + try (FileWriter writer = new FileWriter(tempMemInfoFile)) { + writer.write("\n\n "); // Just whitespace + } + + // Execute the method + CommandResult result = MemInfoReader.getMemTotal(); + + // Verify the result + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("MemTotal entry not found in memory information file.", result.getErrorMessage()); + } + + @Test + void testGetMemTotal_CorruptedContent() throws IOException { + // Write corrupted content to the temporary memory info file + try (FileWriter writer = new FileWriter(tempMemInfoFile)) { + writer.write("!@#$%^&*() Invalid Data\n"); + } + + // Execute the method + CommandResult result = MemInfoReader.getMemTotal(); + + // Verify the result + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("MemTotal entry not found in memory information file.", result.getErrorMessage()); + } + + @Test + void testGetMemTotal_NoMemTotalEntry() throws IOException { + // Write content without a "MemTotal" entry + try (FileWriter writer = new FileWriter(tempMemInfoFile)) { + writer.write("MemFree: 4096 kB\n"); + writer.write("Buffers: 1024 kB\n"); + } + + // Execute the method + CommandResult result = MemInfoReader.getMemTotal(); + + // Verify the result + assertFalse(result.isSuccess()); + assertEquals("", result.getOutputMessage()); + assertEquals("MemTotal entry not found in memory information file.", result.getErrorMessage()); + } +} diff --git a/pi4j-core/src/test/java/com/pi4j/boardinfo/util/BoardModelDetectionTest.java b/pi4j-core/src/test/java/com/pi4j/boardinfo/util/BoardModelDetectionTest.java index 2c1be2ed..f38080ed 100644 --- a/pi4j-core/src/test/java/com/pi4j/boardinfo/util/BoardModelDetectionTest.java +++ b/pi4j-core/src/test/java/com/pi4j/boardinfo/util/BoardModelDetectionTest.java @@ -1,28 +1,149 @@ package com.pi4j.boardinfo.util; +import com.pi4j.boardinfo.datareader.BoardCodeReader; +import com.pi4j.boardinfo.datareader.CpuInfoReader; +import com.pi4j.boardinfo.definition.PiModel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; class BoardModelDetectionTest { + private File tempCpuInfoFile; + private File tempDeviceTreeModelFile; + + @BeforeEach + void setUp() throws IOException { + tempCpuInfoFile = File.createTempFile("cpuinfo", ".tmp"); + tempDeviceTreeModelFile = File.createTempFile("devicetree_model", ".tmp"); + + CpuInfoReader.setCpuInfoFilePath(tempCpuInfoFile.getAbsolutePath()); + BoardCodeReader.setModelFilePath(tempDeviceTreeModelFile.getAbsolutePath()); + } + + @AfterEach + void tearDown() { + deleteFile(tempCpuInfoFile); + deleteFile(tempDeviceTreeModelFile); + + // Reset paths to default + CpuInfoReader.setCpuInfoFilePath("/proc/cpuinfo"); + BoardCodeReader.setModelFilePath("/proc/device-tree/model"); + } + @Test - void testGetDetectedBoard() { + void testGetOperatingSystem() { + BoardInfoHelper.reinitialize(); var detectedBoard = BoardInfoHelper.current(); assertAll( - () -> assertEquals(detectedBoard.getOperatingSystem().getName(), System.getProperty("os.name")), - () -> assertEquals(detectedBoard.getOperatingSystem().getVersion(), System.getProperty("os.version")), - () -> assertEquals(detectedBoard.getOperatingSystem().getArchitecture(), System.getProperty("os.arch")), + () -> assertEquals(System.getProperty("os.name"), detectedBoard.getOperatingSystem().getName()), + () -> assertEquals(System.getProperty("os.version"), detectedBoard.getOperatingSystem().getVersion()), + () -> assertEquals(System.getProperty("os.arch"), detectedBoard.getOperatingSystem().getArchitecture()) + ); + } - () -> assertEquals(detectedBoard.getJavaInfo().getVersion(), System.getProperty("java.version")), - () -> assertEquals(detectedBoard.getJavaInfo().getRuntime(), System.getProperty("java.runtime.version")), - () -> assertEquals(detectedBoard.getJavaInfo().getVendor(), System.getProperty("java.vendor")), - () -> assertEquals(detectedBoard.getJavaInfo().getVendorVersion(), System.getProperty("java.vendor.version")) + @Test + void testGetJavaInfo() { + BoardInfoHelper.reinitialize(); + var detectedBoard = BoardInfoHelper.current(); - // Cannot test detectedBoard.getBoardModel().getModel()) as the output is different on - // Raspberry Pi versus PC, macOS or build server + assertAll( + () -> assertEquals(System.getProperty("java.version"), detectedBoard.getJavaInfo().getVersion()), + () -> assertEquals(System.getProperty("java.runtime.version"), detectedBoard.getJavaInfo().getRuntime()), + () -> assertEquals(System.getProperty("java.vendor"), detectedBoard.getJavaInfo().getVendor()), + () -> assertEquals(System.getProperty("java.vendor.version"), detectedBoard.getJavaInfo().getVendorVersion()) ); } + + @Test + void testGetBoardModelUsingCpuinfo_Success() throws IOException { + mockCpuInfoResponse("Processor: ARMv7 Processor\n" + + "Revision: a03111\n"); + + BoardInfoHelper.reinitialize(); + var detectedBoard = BoardInfoHelper.current(); + + assertEquals(PiModel.MODEL_B, detectedBoard.getBoardModel().getModel()); + } + + @Test + void testGetBoardModelUsingDeviceTreeModel_Success() throws IOException { + mockCpuInfoResponse("INVALID CONTENT"); + mockDeviceTreeModelResponse("Raspberry Pi 4 Model B Rev 1.1"); + + BoardInfoHelper.reinitialize(); + var detectedBoard = BoardInfoHelper.current(); + + assertEquals(PiModel.MODEL_B, detectedBoard.getBoardModel().getModel()); + } + + @Test + void testConflictingData_DeviceTreeTakesPriority() throws IOException { + mockCpuInfoResponse("Processor: ARMv7 Processor\n" + + "Revision: a020d3\n"); // Pi 3 Model B+ + mockDeviceTreeModelResponse("Raspberry Pi 4 Model B Rev 1.1"); + + BoardInfoHelper.reinitialize(); + var detectedBoard = BoardInfoHelper.current(); + + // Device tree should take priority + assertEquals(PiModel.MODEL_B, detectedBoard.getBoardModel().getModel()); + } + + @Test + void testBothSourcesInvalid() throws IOException { + mockCpuInfoResponse("INVALID DATA"); + mockDeviceTreeModelResponse("UNKNOWN DEVICE"); + + BoardInfoHelper.reinitialize(); + var detectedBoard = BoardInfoHelper.current(); + + assertEquals(PiModel.UNKNOWN, detectedBoard.getBoardModel().getModel()); + } + + @Test + void testPartialCpuinfoData() throws IOException { + mockCpuInfoResponse("Processor: ARMv7 Processor\n"); + + BoardInfoHelper.reinitialize(); + var detectedBoard = BoardInfoHelper.current(); + + assertEquals(PiModel.UNKNOWN, detectedBoard.getBoardModel().getModel()); + } + + @Test + void testPartialDeviceTreeData() throws IOException { + mockDeviceTreeModelResponse("Raspberry"); + + BoardInfoHelper.reinitialize(); + var detectedBoard = BoardInfoHelper.current(); + + assertEquals(PiModel.UNKNOWN, detectedBoard.getBoardModel().getModel()); + } + + private void mockCpuInfoResponse(String content) throws IOException { + try (FileWriter writer = new FileWriter(tempCpuInfoFile)) { + writer.write(content); + } + } + + private void mockDeviceTreeModelResponse(String content) throws IOException { + try (FileWriter writer = new FileWriter(tempDeviceTreeModelFile)) { + writer.write(content); + } + } + + private void deleteFile(File file) { + if (file != null && file.exists() && !file.delete()) { + System.err.println("Warning: Failed to delete temporary file: " + file.getAbsolutePath()); + } + } } From 467d77c21ebd64bc9df76204d6485b612b7d090b Mon Sep 17 00:00:00 2001 From: Dariusz Zbyrad <dariusz.zbyrad@allegro.pl> Date: Mon, 25 Nov 2024 13:52:57 +0100 Subject: [PATCH 2/4] Refactor BoardInfoHelper class and belonging classes --- .../boardinfo/datareader/BoardCodeReader.java | 4 +--- .../boardinfo/datareader/CpuInfoReader.java | 1 - .../boardinfo/datareader/MemInfoReader.java | 1 - .../pi4j/boardinfo/util/BoardInfoHelper.java | 5 +++-- .../util/command/CommandExecutor.java | 5 ----- .../boardinfo/util/command/CommandResult.java | 18 ++++++++++-------- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java index 5ec6bc68..88455dcf 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/BoardCodeReader.java @@ -71,18 +71,16 @@ public static CommandResult getBoardCode() { StringBuilder content = new StringBuilder(); String line; - // Read the entire content of the file line by line. while ((line = reader.readLine()) != null) { content.append(line); } - outputMessage = content.toString().trim(); // Remove unnecessary whitespace. + outputMessage = content.toString().trim(); } catch (IOException ex) { errorMessage = "IOException: " + ex.getMessage(); logger.error("Failed to read the board model from '{}': {}", modelFilePath, errorMessage); } - // Return CommandResult based on success or failure if (!errorMessage.isEmpty()) { return failure(errorMessage); } diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java index ad756aba..08600123 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java @@ -83,7 +83,6 @@ public static CommandResult getCpuRevision() { logger.error("Failed to read the CPU revision from '{}': {}", cpuInfoFilePath, errorMessage); } - // Return CommandResult based on success or failure if (!errorMessage.isEmpty() || outputMessage.isEmpty()) { return failure(errorMessage.isEmpty() ? "CPU revision not found in file" : errorMessage); } diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java index fd9ba551..3f46b3d2 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java @@ -80,7 +80,6 @@ public static CommandResult getMemTotal() { logger.error("Failed to read memory information from '{}': {}", memInfoFilePath, errorMessage); } - // Return CommandResult based on whether "MemTotal" was found if (!errorMessage.isEmpty()) { return failure(errorMessage); } diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java index f9d98096..6d8c2a79 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java @@ -34,6 +34,7 @@ import com.pi4j.boardinfo.model.JavaInfo; import com.pi4j.boardinfo.model.JvmMemory; import com.pi4j.boardinfo.model.OperatingSystem; +import com.pi4j.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -156,7 +157,7 @@ private BoardInfo detectBoardInfo(OperatingSystem os, JavaInfo java) { } /** - * Checks if the device uses the RP1 processor. + * Checks if the device uses the RP1 chip. * * @return {@code true} if the board is a Raspberry Pi Model 5B, otherwise {@code false}. */ @@ -202,7 +203,7 @@ public static String getBoardVersionCode() { return output.getOutputMessage(); } logger.error("Could not get the board version code: {}", output.getErrorMessage()); - return ""; + return StringUtil.EMPTY; } /** diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java index 67834550..8e50f3af 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandExecutor.java @@ -70,17 +70,13 @@ public static CommandResult execute(String command) { ProcessBuilder builder = new ProcessBuilder(command.split(" ")); try { - // Start the process Process process = builder.start(); - // Read output and error streams outputMessage = readStream(process.getInputStream()); errorMessage = readStream(process.getErrorStream()); - // Wait for the process to complete or timeout finished = process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS); - // Ensure the process is terminated if not finished if (!finished) { process.destroyForcibly(); errorMessage = "Process timeout after " + COMMAND_TIMEOUT_SECONDS + " seconds."; @@ -95,7 +91,6 @@ public static CommandResult execute(String command) { logger.error("InterruptedException during command execution '{}': {}", command, ex.getMessage(), ex); } - // Log error and return failure result if necessary if (!finished || !errorMessage.isEmpty()) { logger.error("Failed to execute command '{}': {}", command, errorMessage); return failure(errorMessage); diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java index 4bd70ea1..0efcf26a 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/command/CommandResult.java @@ -25,6 +25,8 @@ * #L% */ +import com.pi4j.util.StringUtil; + import java.util.Objects; import java.util.Optional; @@ -39,14 +41,14 @@ public class CommandResult { /** * Constructor to create a new CommandResult. * - * @param success Whether the command execution was successful. - * @param outputMessage The standard output of the command. - * @param errorMessage The error message if the command failed. + * @param success Whether the command execution was successful. + * @param outputMessage The standard output of the command. + * @param errorMessage The error message if the command failed. */ private CommandResult(boolean success, String outputMessage, String errorMessage) { this.success = success; - this.outputMessage = Optional.ofNullable(outputMessage).orElse(""); - this.errorMessage = Optional.ofNullable(errorMessage).orElse(""); + this.outputMessage = Optional.ofNullable(outputMessage).orElse(StringUtil.EMPTY); + this.errorMessage = Optional.ofNullable(errorMessage).orElse(StringUtil.EMPTY); } /** @@ -105,7 +107,7 @@ public String getErrorMessage() { @Override public String toString() { return String.format("CommandResult{success=%b, outputMessage='%s', errorMessage='%s'}", - success, outputMessage, errorMessage); + success, outputMessage, errorMessage); } /** @@ -120,8 +122,8 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; CommandResult that = (CommandResult) o; return success == that.success && - Objects.equals(outputMessage, that.outputMessage) && - Objects.equals(errorMessage, that.errorMessage); + Objects.equals(outputMessage, that.outputMessage) && + Objects.equals(errorMessage, that.errorMessage); } /** From 3108ebe12a1267b8a64f072099a332034170695d Mon Sep 17 00:00:00 2001 From: Dariusz Zbyrad <dariusz.zbyrad@allegro.pl> Date: Mon, 25 Nov 2024 14:06:30 +0100 Subject: [PATCH 3/4] Refactor BoardInfoHelper class and belonging classes --- .../main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java | 6 +++--- .../boardinfo/util/{CommandProperties.java => Command.java} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename pi4j-core/src/main/java/com/pi4j/boardinfo/util/{CommandProperties.java => Command.java} (98%) diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java index 6d8c2a79..188e6d2c 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/BoardInfoHelper.java @@ -38,9 +38,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.pi4j.boardinfo.util.CommandProperties.CORE_VOLTAGE_COMMAND; -import static com.pi4j.boardinfo.util.CommandProperties.TEMPERATURE_COMMAND; -import static com.pi4j.boardinfo.util.CommandProperties.UPTIME_COMMAND; +import static com.pi4j.boardinfo.util.Command.CORE_VOLTAGE_COMMAND; +import static com.pi4j.boardinfo.util.Command.TEMPERATURE_COMMAND; +import static com.pi4j.boardinfo.util.Command.UPTIME_COMMAND; import static com.pi4j.boardinfo.util.SystemProperties.ARCHITECTURE_DATA_MODEL; import static com.pi4j.boardinfo.util.SystemProperties.JAVA_RUNTIME_VERSION; import static com.pi4j.boardinfo.util.SystemProperties.JAVA_VENDOR; diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/CommandProperties.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/Command.java similarity index 98% rename from pi4j-core/src/main/java/com/pi4j/boardinfo/util/CommandProperties.java rename to pi4j-core/src/main/java/com/pi4j/boardinfo/util/Command.java index d462c3e0..a7e5a06a 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/util/CommandProperties.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/util/Command.java @@ -30,7 +30,7 @@ * These commands are typically used to gather system-related information * such as voltage, uptime, and temperature on Raspberry Pi or similar systems. */ -public interface CommandProperties { +public interface Command { /** * Command to measure the core voltage of the system. From eeeb8bccbf4b331a4846b33d6ff53df0e9043274 Mon Sep 17 00:00:00 2001 From: Dariusz Zbyrad <dariusz.zbyrad@allegro.pl> Date: Mon, 25 Nov 2024 15:33:52 +0100 Subject: [PATCH 4/4] Simplifies the loop by using an early continue to skip irrelevant lines, reducing nesting. --- .../pi4j/boardinfo/datareader/CpuInfoReader.java | 13 +++++++------ .../pi4j/boardinfo/datareader/MemInfoReader.java | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java index 08600123..fbc367f8 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/CpuInfoReader.java @@ -70,13 +70,14 @@ public static CommandResult getCpuRevision() { String line; // Read file line by line to locate the "Revision" entry. while ((line = reader.readLine()) != null) { - if (line.startsWith("Revision")) { - String[] parts = line.split(":"); - if (parts.length > 1) { - outputMessage = parts[1].trim(); // Extract and trim the revision value. - } - break; // No need to process further once "Revision" is found. + if (!line.startsWith("Revision")) { + continue; // Skip lines that do not start with "Revision" } + String[] parts = line.split(":"); + if (parts.length > 1) { + outputMessage = parts[1].trim(); // Extract and trim the revision value. + } + break; // No need to process further once "Revision" is found. } } catch (IOException ex) { errorMessage = "IOException: " + ex.getMessage(); diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java index 3f46b3d2..a479aced 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/datareader/MemInfoReader.java @@ -70,10 +70,11 @@ public static CommandResult getMemTotal() { try (BufferedReader reader = new BufferedReader(new FileReader(memInfoFilePath))) { String line; while ((line = reader.readLine()) != null) { - if (line.startsWith("MemTotal:")) { - memTotalLine = line.trim(); - break; + if (!line.startsWith("MemTotal:")) { + continue; // Skip lines that don't start with "MemTotal:" } + memTotalLine = line.trim(); + break; // No need to process further once "MemTotal:" is found } } catch (IOException ex) { errorMessage = "IOException: " + ex.getMessage();