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