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;
026
027@SuppressWarnings("unused")
028public enum Platform {
029    // WPILib Supported (JNI)
030    WINDOWS_64("Windows x64", Platform::getUnknownModel, "winx64", false, OSType.WINDOWS, true),
031    LINUX_32("Linux x86", Platform::getUnknownModel, "linuxx64", false, OSType.LINUX, true),
032    LINUX_64("Linux x64", Platform::getUnknownModel, "linuxx64", false, OSType.LINUX, true),
033    LINUX_RASPBIAN32(
034            "Linux Raspbian 32-bit",
035            Platform::getLinuxDeviceTreeModel,
036            "linuxarm32",
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            "linuxarm64",
044            true,
045            OSType.LINUX,
046            true), // Raspberry Pi 3/4 with a 64-bit image
047    LINUX_RK3588_64(
048            "Linux AARCH 64-bit with RK3588",
049            Platform::getLinuxDeviceTreeModel,
050            "linuxarm64",
051            false,
052            OSType.LINUX,
053            true),
054    LINUX_QCS6490(
055            "Linux AARCH 64-bit with QCS6490",
056            Platform::getLinuxDeviceTreeModel,
057            "linuxarm64",
058            false,
059            OSType.LINUX,
060            true), // QCS6490 SBCs
061    LINUX_AARCH64(
062            "Linux AARCH64",
063            Platform::getLinuxDeviceTreeModel,
064            "linuxarm64",
065            false,
066            OSType.LINUX,
067            true), // Jetson Nano, Jetson TX2
068
069    // PhotonVision Supported (Manual build/install)
070    LINUX_ARM64(
071            "Linux ARM64",
072            Platform::getLinuxDeviceTreeModel,
073            "linuxarm64",
074            false,
075            OSType.LINUX,
076            true), // ODROID C2, N2
077
078    // Completely unsupported
079    WINDOWS_32("Windows x86", Platform::getUnknownModel, "windowsx64", false, OSType.WINDOWS, false),
080    MACOS("Mac OS", Platform::getUnknownModel, "osxuniversal", false, OSType.MACOS, false),
081    LINUX_ARM32(
082            "Linux ARM32",
083            Platform::getUnknownModel,
084            "linuxarm32",
085            false,
086            OSType.LINUX,
087            false), // ODROID XU4, C1+
088    UNKNOWN("Unsupported Platform", Platform::getUnknownModel, "", false, OSType.UNKNOWN, false);
089
090    public enum OSType {
091        WINDOWS,
092        LINUX,
093        MACOS,
094        UNKNOWN
095    }
096
097    public final String description;
098    public final String hardwareModel;
099    public final String nativeLibraryFolderName;
100    public final boolean isPi;
101    public final OSType osType;
102    public final boolean isSupported;
103
104    // Set once at init, shouldn't be needed after.
105    private static final Platform currentPlatform = getCurrentPlatform();
106
107    Platform(
108            String description,
109            Supplier<String> getHardwareModel,
110            String nativeLibFolderName,
111            boolean isPi,
112            OSType osType,
113            boolean isSupported) {
114        this.description = description;
115        this.hardwareModel = getHardwareModel.get();
116        this.isPi = isPi;
117        this.osType = osType;
118        this.isSupported = isSupported;
119        this.nativeLibraryFolderName = nativeLibFolderName;
120    }
121
122    //////////////////////////////////////////////////////
123    // Public API
124
125    // Checks specifically if unix shell and API are supported
126    public static boolean isLinux() {
127        return currentPlatform.osType == OSType.LINUX;
128    }
129
130    public static boolean isRK3588() {
131        return Platform.isOrangePi() || Platform.isCoolPi4b() || Platform.isRock5C();
132    }
133
134    public static boolean isQCS6490() {
135        return isRubik();
136    }
137
138    public static boolean isRaspberryPi() {
139        return currentPlatform.isPi;
140    }
141
142    public static String getPlatformName() {
143        if (currentPlatform.equals(UNKNOWN)) {
144            return UnknownPlatformString;
145        } else {
146            return currentPlatform.description;
147        }
148    }
149
150    public static String getHardwareModel() {
151        return currentPlatform.hardwareModel;
152    }
153
154    public static String getNativeLibraryFolderName() {
155        return currentPlatform.nativeLibraryFolderName;
156    }
157
158    public static boolean isSupported() {
159        return currentPlatform.isSupported;
160    }
161
162    public static boolean isAthena() {
163        File runRobotFile = new File("/usr/local/frc/bin/frcRunRobot.sh");
164        return runRobotFile.exists();
165    }
166
167    //////////////////////////////////////////////////////
168
169    // Debug info related to unknown platforms for debug help
170    private static final String OS_NAME = System.getProperty("os.name");
171    private static final String OS_ARCH = System.getProperty("os.arch");
172    private static final String UnknownPlatformString =
173            String.format("Unknown Platform. OS: %s, Architecture: %s", OS_NAME, OS_ARCH);
174    private static final String UnknownDeviceModelString = "Unknown";
175
176    public static Platform getCurrentPlatform() {
177        String OS_NAME;
178        if (Platform.OS_NAME != null) {
179            OS_NAME = Platform.OS_NAME;
180        } else {
181            OS_NAME = System.getProperty("os.name");
182        }
183
184        String OS_ARCH;
185        if (Platform.OS_ARCH != null) {
186            OS_ARCH = Platform.OS_ARCH;
187        } else {
188            OS_ARCH = System.getProperty("os.arch");
189        }
190
191        if (OS_NAME.startsWith("Windows")) {
192            if (OS_ARCH.equals("x86") || OS_ARCH.equals("i386")) {
193                return WINDOWS_32;
194            } else if (OS_ARCH.equals("amd64") || OS_ARCH.equals("x86_64")) {
195                return WINDOWS_64;
196            } else {
197                // please don't try this
198                return UNKNOWN;
199            }
200        }
201
202        if (OS_NAME.startsWith("Mac")) {
203            // TODO - once we have real support, this might have to be more granular
204            return MACOS;
205        }
206
207        if (OS_NAME.startsWith("Linux")) {
208            if (isPiSBC()) {
209                if (OS_ARCH.equals("arm") || OS_ARCH.equals("arm32")) {
210                    return LINUX_RASPBIAN32;
211                } else if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
212                    return LINUX_RASPBIAN64;
213                } else {
214                    // Unknown/exotic installation
215                    return UNKNOWN;
216                }
217            } else if (isJetsonSBC()) {
218                if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
219                    // TODO - do we need to check OS version?
220                    return LINUX_AARCH64;
221                } else {
222                    // Unknown/exotic installation
223                    return UNKNOWN;
224                }
225            } else if (OS_ARCH.equals("amd64") || OS_ARCH.equals("x86_64")) {
226                return LINUX_64;
227            } else if (OS_ARCH.equals("x86") || OS_ARCH.equals("i386")) {
228                return LINUX_32;
229            } else if (OS_ARCH.equals("aarch64") || OS_ARCH.equals("arm64")) {
230                // TODO - os detection needed?
231                if (isRK3588()) {
232                    return LINUX_RK3588_64;
233                } else if (isQCS6490()) {
234                    return LINUX_QCS6490;
235
236                } else {
237                    return LINUX_AARCH64;
238                }
239            } else if (OS_ARCH.equals("arm") || OS_ARCH.equals("arm32")) {
240                return LINUX_ARM32;
241            } else {
242                // Unknown or otherwise unsupported platform
243                return Platform.UNKNOWN;
244            }
245        }
246
247        // If we fall through all the way to here,
248        return Platform.UNKNOWN;
249    }
250
251    // Check for various known SBC types
252    private static boolean isPiSBC() {
253        return fileHasText("/proc/cpuinfo", "Raspberry Pi");
254    }
255
256    private static boolean isOrangePi() {
257        return fileHasText("/proc/device-tree/model", "Orange Pi 5");
258    }
259
260    private static boolean isRock5C() {
261        return fileHasText("/proc/device-tree/model", "ROCK 5C");
262    }
263
264    private static boolean isCoolPi4b() {
265        return fileHasText("/proc/device-tree/model", "CoolPi 4B");
266    }
267
268    private static boolean isJetsonSBC() {
269        // https://forums.developer.nvidia.com/t/how-to-recognize-jetson-nano-device/146624
270        return fileHasText("/proc/device-tree/model", "NVIDIA Jetson");
271    }
272
273    private static boolean isRubik() {
274        return fileHasText("/proc/device-tree/model", "RUBIK");
275    }
276
277    static String getLinuxDeviceTreeModel() {
278        var deviceTreeModelPath = Paths.get("/proc/device-tree/model");
279        try {
280            if (Files.exists(deviceTreeModelPath)) {
281                return Files.readString(deviceTreeModelPath).trim();
282            }
283        } catch (Exception ex) {
284            return UnknownDeviceModelString;
285        }
286        return UnknownDeviceModelString;
287    }
288
289    static String getUnknownModel() {
290        return UnknownDeviceModelString;
291    }
292
293    // Checks for various names of linux OS
294    private static boolean isStretch() {
295        // TODO - this is a total guess
296        return fileHasText("/etc/os-release", "Stretch");
297    }
298
299    private static boolean isBuster() {
300        // TODO - this is a total guess
301        return fileHasText("/etc/os-release", "Buster");
302    }
303
304    private static boolean fileHasText(String filename, String text) {
305        try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {
306            while (true) {
307                String value = reader.readLine();
308                if (value == null) {
309                    return false;
310
311                } else if (value.contains(text)) {
312                    return true;
313                } // else, next line
314            }
315        } catch (IOException ex) {
316            return false;
317        }
318    }
319
320    public static boolean isWindows() {
321        var p = getCurrentPlatform();
322        return (p == WINDOWS_32 || p == WINDOWS_64);
323    }
324}