001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.common.hardware;
019
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.IOException;
023import java.nio.file.Files;
024import java.nio.file.Paths;
025import java.util.function.Supplier;
026import org.photonvision.jni.CombinedRuntimeLoader;
027
028@SuppressWarnings({"unused", "doclint"})
029public enum Platform {
030    // WPILib Supported (JNI)
031    WINDOWS_64("Windows x64", Platform::getUnknownModel, false, OSType.WINDOWS, true),
032    LINUX_32("Linux x86", Platform::getUnknownModel, false, OSType.LINUX, true),
033    LINUX_64("Linux x64", Platform::getUnknownModel, false, OSType.LINUX, true),
034    LINUX_RASPBIAN32(
035            "Linux Raspbian 32-bit",
036            Platform::getLinuxDeviceTreeModel,
037            true,
038            OSType.LINUX,
039            true), // Raspberry Pi 3/4 with a 32-bit image
040    LINUX_RASPBIAN64(
041            "Linux Raspbian 64-bit",
042            Platform::getLinuxDeviceTreeModel,
043            true,
044            OSType.LINUX,
045            true), // Raspberry Pi 3/4 with a 64-bit image
046    LINUX_RK3588_64(
047            "Linux AARCH 64-bit with RK3588",
048            Platform::getLinuxDeviceTreeModel,
049            false,
050            OSType.LINUX,
051            true),
052    LINUX_QCS6490(
053            "Linux AARCH 64-bit with QCS6490",
054            Platform::getLinuxDeviceTreeModel,
055            false,
056            OSType.LINUX,
057            true), // QCS6490 SBCs
058    LINUX_AARCH64(
059            "Linux AARCH64",
060            Platform::getLinuxDeviceTreeModel,
061            false,
062            OSType.LINUX,
063            true), // Jetson Nano, Jetson TX2
064
065    // PhotonVision Supported (Manual build/install)
066    LINUX_ARM64(
067            "Linux ARM64", Platform::getLinuxDeviceTreeModel, false, OSType.LINUX, true), // ODROID C2, N2
068
069    // Completely unsupported
070    WINDOWS_32("Windows x86", Platform::getUnknownModel, false, OSType.WINDOWS, false),
071    MACOS("Mac OS", Platform::getUnknownModel, false, OSType.MACOS, false),
072    LINUX_ARM32(
073            "Linux ARM32", Platform::getUnknownModel, false, OSType.LINUX, false), // ODROID XU4, C1+
074    UNKNOWN("Unsupported Platform", Platform::getUnknownModel, false, OSType.UNKNOWN, false);
075
076    public enum OSType {
077        WINDOWS,
078        LINUX,
079        MACOS,
080        UNKNOWN
081    }
082
083    public final String description;
084    public final String hardwareModel;
085    public final boolean isPi;
086    public final OSType osType;
087    public final boolean isSupported;
088
089    // Set once at init, shouldn't be needed after.
090    private static Platform currentPlatform = getCurrentPlatform();
091    private static boolean override = false;
092
093    Platform(
094            String description,
095            Supplier<String> getHardwareModel,
096            boolean isPi,
097            OSType osType,
098            boolean isSupported) {
099        this.description = description;
100        this.hardwareModel = getHardwareModel.get();
101        this.isPi = isPi;
102        this.osType = osType;
103        this.isSupported = isSupported;
104    }
105
106    public static void overridePlatform(Platform platform) {
107        currentPlatform = platform;
108        override = true;
109    }
110
111    //////////////////////////////////////////////////////
112    // Public API
113
114    // Checks specifically if unix shell and API are supported
115    public static boolean isLinux() {
116        return currentPlatform.osType == OSType.LINUX;
117    }
118
119    public static boolean isRK3588() {
120        return currentPlatform == LINUX_RK3588_64
121                || Platform.isOrangePi()
122                || Platform.isCoolPi4b()
123                || Platform.isRock5C()
124                || fileHasText("/proc/device-tree/compatible", "rk3588");
125    }
126
127    public static boolean isQCS6490() {
128        return currentPlatform == LINUX_QCS6490 || Platform.isRubik();
129    }
130
131    public static boolean isRaspberryPi() {
132        return currentPlatform.isPi;
133    }
134
135    public static String getPlatformName() {
136        if (currentPlatform.equals(UNKNOWN)) {
137            return UnknownPlatformString;
138        } else {
139            return currentPlatform.description;
140        }
141    }
142
143    /**
144     * This function serves to map between formats used in the CombinedRuntimeLoader and the platform
145     * names used in the wpilib-tools-plugin. This is typically used for native libraries.
146     *
147     * @return String representing the platform in the format used by wpilib-tools-plugin, or an empty
148     *     string if the platform is not recognized.
149     */
150    public static String getNativePlatform() {
151        String platPath = CombinedRuntimeLoader.getPlatformPath();
152
153        if (platPath == "/linux/x86-64/") {
154            return "linuxx64";
155        } else if (platPath == "/windows/x86-64/") {
156            return "winx64";
157        } else if (platPath == "/linux/arm64/") {
158            return "linuxarm64";
159        } else {
160            return "";
161        }
162    }
163
164    public static String getHardwareModel() {
165        return currentPlatform.hardwareModel;
166    }
167
168    public static boolean isSupported() {
169        return currentPlatform.isSupported;
170    }
171
172    public static boolean isAthena() {
173        File runRobotFile = new File("/usr/local/frc/bin/frcRunRobot.sh");
174        return runRobotFile.exists();
175    }
176
177    public static boolean isWindows() {
178        var p = getCurrentPlatform();
179        return (p == WINDOWS_32 || p == WINDOWS_64);
180    }
181
182    //////////////////////////////////////////////////////
183
184    // Debug info related to unknown platforms for debug help
185    private static final String OS_NAME = System.getProperty("os.name");
186    private static final String OS_ARCH = System.getProperty("os.arch");
187    private static final String UnknownPlatformString =
188            String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
189    private static final String UnknownDeviceModelString = "Unknown";
190
191    public static Platform getCurrentPlatform() {
192        if (override) {
193            return currentPlatform;
194        }
195
196        String OS_NAME;
197        if (Platform.OS_NAME != null) {
198            OS_NAME = Platform.OS_NAME;
199        } else {
200            OS_NAME = System.getProperty("os.name");
201        }
202
203        String OS_ARCH;
204        if (Platform.OS_ARCH != null) {
205            OS_ARCH = Platform.OS_ARCH;
206        } else {
207            OS_ARCH = System.getProperty("os.arch");
208        }
209
210        if (OS_NAME.startsWith("Windows")) {
211            if (OS_ARCH.equals("x86") || OS_ARCH.equals("i386")) {
212                return WINDOWS_32;
213            } else if (OS_ARCH.equals("amd64") || OS_ARCH.equals("x86_64")) {
214                return WINDOWS_64;
215            } else {
216                // please don't try this
217                return UNKNOWN;
218            }
219        }
220
221        if (OS_NAME.startsWith("Mac")) {
222            // TODO - once we have real support, this might have to be more granular
223            return MACOS;
224        }
225
226        if (OS_NAME.startsWith("Linux")) {
227            if (isPiSBC()) {
228                if (OS_ARCH.equals("arm") || OS_ARCH.equals("arm32")) {
229                    return LINUX_RASPBIAN32;
230                } else if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
231                    return LINUX_RASPBIAN64;
232                } else {
233                    // Unknown/exotic installation
234                    return UNKNOWN;
235                }
236            } else if (isJetsonSBC()) {
237                if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
238                    // TODO - do we need to check OS version?
239                    return LINUX_AARCH64;
240                } else {
241                    // Unknown/exotic installation
242                    return UNKNOWN;
243                }
244            } else if (OS_ARCH.equals("amd64") || OS_ARCH.equals("x86_64")) {
245                return LINUX_64;
246            } else if (OS_ARCH.equals("x86") || OS_ARCH.equals("i386")) {
247                return LINUX_32;
248            } else if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
249                // TODO - os detection needed?
250                if (isRK3588()) {
251                    return LINUX_RK3588_64;
252                } else if (isQCS6490()) {
253                    return LINUX_QCS6490;
254
255                } else {
256                    return LINUX_AARCH64;
257                }
258            } else if (OS_ARCH.equals("arm") || OS_ARCH.equals("arm32")) {
259                return LINUX_ARM32;
260            } else {
261                // Unknown or otherwise unsupported platform
262                return Platform.UNKNOWN;
263            }
264        }
265
266        // If we fall through all the way to here,
267        return Platform.UNKNOWN;
268    }
269
270    // Check for various known SBC types
271    private static boolean isPiSBC() {
272        return fileHasText("/proc/cpuinfo", "Raspberry Pi");
273    }
274
275    private static boolean isOrangePi() {
276        return fileHasText("/proc/device-tree/model", "Orange Pi 5");
277    }
278
279    private static boolean isRock5C() {
280        return fileHasText("/proc/device-tree/model", "ROCK 5C");
281    }
282
283    private static boolean isCoolPi4b() {
284        return fileHasText("/proc/device-tree/model", "CoolPi 4B");
285    }
286
287    private static boolean isJetsonSBC() {
288        // https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
289        return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");
290    }
291
292    private static boolean isRubik() {
293        return fileHasText("/proc/device-tree/model", "RUBIK");
294    }
295
296    static String getLinuxDeviceTreeModel() {
297        var deviceTreeModelPath = Paths.get("/proc/device-tree/model");
298        try {
299            if (Files.exists(deviceTreeModelPath)) {
300                return Files.readString(deviceTreeModelPath).trim();
301            }
302        } catch (Exception ex) {
303            return UnknownDeviceModelString;
304        }
305        return UnknownDeviceModelString;
306    }
307
308    static String getUnknownModel() {
309        return UnknownDeviceModelString;
310    }
311
312    // Checks for various names of linux OS
313    private static boolean isStretch() {
314        // TODO - this is a total guess
315        return fileHasText("/etc/os-release", "Stretch");
316    }
317
318    private static boolean isBuster() {
319        // TODO - this is a total guess
320        return fileHasText("/etc/os-release", "Buster");
321    }
322
323    private static boolean fileHasText(String filename, String text) {
324        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
325            while (true) {
326                String value = reader.readLine();
327                if (value == null) {
328                    return false;
329
330                } else if (value.contains(text)) {
331                    return true;
332                } // else, next line
333            }
334        } catch (IOException ex) {
335            return false;
336        }
337    }
338}