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