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();