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        switch (platPath) {
154            case "/linux/x86-64/":
155                return "linuxx86-64";
156            case "/windows/x86-64/":
157                return "winx86-64";
158            case "/linux/arm64/":
159                return "linuxarm64";
160            default:
161                return "";
162        }
163    }
164
165    public static String getHardwareModel() {
166        return currentPlatform.hardwareModel;
167    }
168
169    public static boolean isSupported() {
170        return currentPlatform.isSupported;
171    }
172
173    public static boolean isAthena() {
174        File runRobotFile = new File("/usr/local/frc/bin/frcRunRobot.sh");
175        return runRobotFile.exists();
176    }
177
178    public static boolean isWindows() {
179        var p = getCurrentPlatform();
180        return (p == WINDOWS_32 || p == WINDOWS_64);
181    }
182
183    //////////////////////////////////////////////////////
184
185    // Debug info related to unknown platforms for debug help
186    private static final String OS_NAME = System.getProperty("os.name");
187    private static final String OS_ARCH = System.getProperty("os.arch");
188    private static final String UnknownPlatformString =
189            String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
190    private static final String UnknownDeviceModelString = "Unknown";
191
192    public static Platform getCurrentPlatform() {
193        if (override) {
194            return currentPlatform;
195        }
196
197        String OS_NAME;
198        if (Platform.OS_NAME != null) {
199            OS_NAME = Platform.OS_NAME;
200        } else {
201            OS_NAME = System.getProperty("os.name");
202        }
203
204        String OS_ARCH;
205        if (Platform.OS_ARCH != null) {
206            OS_ARCH = Platform.OS_ARCH;
207        } else {
208            OS_ARCH = System.getProperty("os.arch");
209        }
210
211        if (OS_NAME.startsWith("Windows")) {
212            if (OS_ARCH.equals("x86") || OS_ARCH.equals("i386")) {
213                return WINDOWS_32;
214            } else if (OS_ARCH.equals("amd64") || OS_ARCH.equals("x86_64")) {
215                return WINDOWS_64;
216            } else {
217                // please don't try this
218                return UNKNOWN;
219            }
220        }
221
222        if (OS_NAME.startsWith("Mac")) {
223            // TODO - once we have real support, this might have to be more granular
224            return MACOS;
225        }
226
227        if (OS_NAME.startsWith("Linux")) {
228            if (isPiSBC()) {
229                if (OS_ARCH.equals("arm") || OS_ARCH.equals("arm32")) {
230                    return LINUX_RASPBIAN32;
231                } else if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
232                    return LINUX_RASPBIAN64;
233                } else {
234                    // Unknown/exotic installation
235                    return UNKNOWN;
236                }
237            } else if (isJetsonSBC()) {
238                if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
239                    // TODO - do we need to check OS version?
240                    return LINUX_AARCH64;
241                } else {
242                    // Unknown/exotic installation
243                    return UNKNOWN;
244                }
245            } else if (OS_ARCH.equals("amd64") || OS_ARCH.equals("x86_64")) {
246                return LINUX_64;
247            } else if (OS_ARCH.equals("x86") || OS_ARCH.equals("i386")) {
248                return LINUX_32;
249            } else if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
250                // TODO - os detection needed?
251                if (isRK3588()) {
252                    return LINUX_RK3588_64;
253                } else if (isQCS6490()) {
254                    return LINUX_QCS6490;
255
256                } else {
257                    return LINUX_AARCH64;
258                }
259            } else if (OS_ARCH.equals("arm") || OS_ARCH.equals("arm32")) {
260                return LINUX_ARM32;
261            } else {
262                // Unknown or otherwise unsupported platform
263                return Platform.UNKNOWN;
264            }
265        }
266
267        // If we fall through all the way to here,
268        return Platform.UNKNOWN;
269    }
270
271    // Check for various known SBC types
272    private static boolean isPiSBC() {
273        return fileHasText("/proc/cpuinfo", "Raspberry Pi");
274    }
275
276    private static boolean isOrangePi() {
277        return fileHasText("/proc/device-tree/model", "Orange Pi 5");
278    }
279
280    private static boolean isRock5C() {
281        return fileHasText("/proc/device-tree/model", "ROCK 5C");
282    }
283
284    private static boolean isCoolPi4b() {
285        return fileHasText("/proc/device-tree/model", "CoolPi 4B");
286    }
287
288    private static boolean isJetsonSBC() {
289        // https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
290        return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");
291    }
292
293    private static boolean isRubik() {
294        return fileHasText("/proc/device-tree/model", "RUBIK");
295    }
296
297    static String getLinuxDeviceTreeModel() {
298        var deviceTreeModelPath = Paths.get("/proc/device-tree/model");
299        try {
300            if (Files.exists(deviceTreeModelPath)) {
301                return Files.readString(deviceTreeModelPath).trim();
302            }
303        } catch (Exception ex) {
304            return UnknownDeviceModelString;
305        }
306        return UnknownDeviceModelString;
307    }
308
309    static String getUnknownModel() {
310        return UnknownDeviceModelString;
311    }
312
313    // Checks for various names of linux OS
314    private static boolean isStretch() {
315        // TODO - this is a total guess
316        return fileHasText("/etc/os-release", "Stretch");
317    }
318
319    private static boolean isBuster() {
320        // TODO - this is a total guess
321        return fileHasText("/etc/os-release", "Buster");
322    }
323
324    private static boolean fileHasText(String filename, String text) {
325        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
326            while (true) {
327                String value = reader.readLine();
328                if (value == null) {
329                    return false;
330
331                } else if (value.contains(text)) {
332                    return true;
333                } // else, next line
334            }
335        } catch (IOException ex) {
336            return false;
337        }
338    }
339}