View Javadoc
1   /*
2    * Copyright 2016-2023 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.util;
6   
7   import java.nio.ByteBuffer;
8   import java.nio.ByteOrder;
9   import java.nio.charset.StandardCharsets;
10  import java.util.Arrays;
11  import java.util.Locale;
12  
13  import org.slf4j.Logger;
14  import org.slf4j.LoggerFactory;
15  
16  import oshi.annotation.SuppressForbidden;
17  import oshi.annotation.concurrent.ThreadSafe;
18  
19  /**
20   * EDID parsing utility.
21   */
22  @ThreadSafe
23  public final class EdidUtil {
24  
25      private static final Logger LOG = LoggerFactory.getLogger(EdidUtil.class);
26  
27      private EdidUtil() {
28      }
29  
30      /**
31       * Gets the Manufacturer ID from (up to) 3 5-bit characters in bytes 8 and 9
32       *
33       * @param edid The EDID byte array
34       * @return The manufacturer ID
35       */
36      @SuppressForbidden(reason = "customized base 2 parsing not in Util class")
37      public static String getManufacturerID(byte[] edid) {
38          // Bytes 8-9 are manufacturer ID in 3 5-bit characters.
39          String temp = String.format(Locale.ROOT, "%8s%8s", Integer.toBinaryString(edid[8] & 0xFF),
40                  Integer.toBinaryString(edid[9] & 0xFF)).replace(' ', '0');
41          LOG.debug("Manufacurer ID: {}", temp);
42          return String.format(Locale.ROOT, "%s%s%s", (char) (64 + Integer.parseInt(temp.substring(1, 6), 2)),
43                  (char) (64 + Integer.parseInt(temp.substring(7, 11), 2)),
44                  (char) (64 + Integer.parseInt(temp.substring(12, 16), 2))).replace("@", "");
45      }
46  
47      /**
48       * Gets the Product ID, bytes 10 and 11
49       *
50       * @param edid The EDID byte array
51       * @return The product ID
52       */
53      public static String getProductID(byte[] edid) {
54          // Bytes 10-11 are product ID expressed in hex characters
55          return Integer.toHexString(
56                  ByteBuffer.wrap(Arrays.copyOfRange(edid, 10, 12)).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xffff);
57      }
58  
59      /**
60       * Gets the Serial number, bytes 12-15
61       *
62       * @param edid The EDID byte array
63       * @return If all 4 bytes represent alphanumeric characters, a 4-character string, otherwise a hex string.
64       */
65      public static String getSerialNo(byte[] edid) {
66          // Bytes 12-15 are Serial number (last 4 characters)
67          if (LOG.isDebugEnabled()) {
68              LOG.debug("Serial number: {}", Arrays.toString(Arrays.copyOfRange(edid, 12, 16)));
69          }
70          return String.format(Locale.ROOT, "%s%s%s%s", getAlphaNumericOrHex(edid[15]), getAlphaNumericOrHex(edid[14]),
71                  getAlphaNumericOrHex(edid[13]), getAlphaNumericOrHex(edid[12]));
72      }
73  
74      private static String getAlphaNumericOrHex(byte b) {
75          return Character.isLetterOrDigit((char) b) ? String.format(Locale.ROOT, "%s", (char) b)
76                  : String.format(Locale.ROOT, "%02X", b);
77      }
78  
79      /**
80       * Return the week of year of manufacture
81       *
82       * @param edid The EDID byte array
83       * @return The week of year
84       */
85      public static byte getWeek(byte[] edid) {
86          // Byte 16 is manufacture week
87          return edid[16];
88      }
89  
90      /**
91       * Return the year of manufacture
92       *
93       * @param edid The EDID byte array
94       * @return The year of manufacture
95       */
96      public static int getYear(byte[] edid) {
97          // Byte 17 is manufacture year-1990
98          byte temp = edid[17];
99          LOG.debug("Year-1990: {}", temp);
100         return temp + 1990;
101     }
102 
103     /**
104      * Return the EDID version
105      *
106      * @param edid The EDID byte array
107      * @return The EDID version
108      */
109     public static String getVersion(byte[] edid) {
110         // Bytes 18-19 are EDID version
111         return edid[18] + "." + edid[19];
112     }
113 
114     /**
115      * Test if this EDID is a digital monitor based on byte 20
116      *
117      * @param edid The EDID byte array
118      * @return True if the EDID represents a digital monitor, false otherwise
119      */
120     public static boolean isDigital(byte[] edid) {
121         // Byte 20 is Video input params
122         return 1 == (edid[20] & 0xff) >> 7;
123     }
124 
125     /**
126      * Get monitor width in cm
127      *
128      * @param edid The EDID byte array
129      * @return Monitor width in cm
130      */
131     public static int getHcm(byte[] edid) {
132         // Byte 21 is horizontal size in cm
133         return edid[21];
134     }
135 
136     /**
137      * Get monitor height in cm
138      *
139      * @param edid The EDID byte array
140      * @return Monitor height in cm
141      */
142     public static int getVcm(byte[] edid) {
143         // Byte 22 is vertical size in cm
144         return edid[22];
145     }
146 
147     /**
148      * Get the VESA descriptors
149      *
150      * @param edid The EDID byte array
151      * @return A 2D array with four 18-byte elements representing VESA descriptors
152      */
153     public static byte[][] getDescriptors(byte[] edid) {
154         byte[][] desc = new byte[4][18];
155         for (int i = 0; i < desc.length; i++) {
156             System.arraycopy(edid, 54 + 18 * i, desc[i], 0, 18);
157         }
158         return desc;
159     }
160 
161     /**
162      * Get the VESA descriptor type
163      *
164      * @param desc An 18-byte VESA descriptor
165      * @return An integer representing the first four bytes of the VESA descriptor
166      */
167     public static int getDescriptorType(byte[] desc) {
168         return ByteBuffer.wrap(Arrays.copyOfRange(desc, 0, 4)).getInt();
169     }
170 
171     /**
172      * Parse a detailed timing descriptor
173      *
174      * @param desc An 18-byte VESA descriptor
175      * @return A string describing part of the detailed timing descriptor
176      */
177     public static String getTimingDescriptor(byte[] desc) {
178         int clock = ByteBuffer.wrap(Arrays.copyOfRange(desc, 0, 2)).order(ByteOrder.LITTLE_ENDIAN).getShort() / 100;
179         int hActive = (desc[2] & 0xff) + ((desc[4] & 0xf0) << 4);
180         int vActive = (desc[5] & 0xff) + ((desc[7] & 0xf0) << 4);
181         return String.format(Locale.ROOT, "Clock %dMHz, Active Pixels %dx%d ", clock, hActive, vActive);
182     }
183 
184     /**
185      * Parse descriptor range limits
186      *
187      * @param desc An 18-byte VESA descriptor
188      * @return A string describing some of the range limits
189      */
190     public static String getDescriptorRangeLimits(byte[] desc) {
191         return String.format(Locale.ROOT, "Field Rate %d-%d Hz vertical, %d-%d Hz horizontal, Max clock: %d MHz",
192                 desc[5], desc[6], desc[7], desc[8], desc[9] * 10);
193     }
194 
195     /**
196      * Parse descriptor text
197      *
198      * @param desc An 18-byte VESA descriptor
199      * @return Plain text starting at the 4th byte
200      */
201     public static String getDescriptorText(byte[] desc) {
202         return new String(Arrays.copyOfRange(desc, 4, 18), StandardCharsets.US_ASCII).trim();
203     }
204 
205     /**
206      * Parse an EDID byte array into user-readable information
207      *
208      * @param edid An EDID byte array
209      * @return User-readable text represented by the EDID
210      */
211     public static String toString(byte[] edid) {
212         StringBuilder sb = new StringBuilder();
213         sb.append("  Manuf. ID=").append(EdidUtil.getManufacturerID(edid));
214         sb.append(", Product ID=").append(EdidUtil.getProductID(edid));
215         sb.append(", ").append(EdidUtil.isDigital(edid) ? "Digital" : "Analog");
216         sb.append(", Serial=").append(EdidUtil.getSerialNo(edid));
217         sb.append(", ManufDate=").append(EdidUtil.getWeek(edid) * 12 / 52 + 1).append('/')
218                 .append(EdidUtil.getYear(edid));
219         sb.append(", EDID v").append(EdidUtil.getVersion(edid));
220         int hSize = EdidUtil.getHcm(edid);
221         int vSize = EdidUtil.getVcm(edid);
222         sb.append(String.format(Locale.ROOT, "%n  %d x %d cm (%.1f x %.1f in)", hSize, vSize, hSize / 2.54,
223                 vSize / 2.54));
224         byte[][] desc = EdidUtil.getDescriptors(edid);
225         for (byte[] b : desc) {
226             switch (EdidUtil.getDescriptorType(b)) {
227             case 0xff:
228                 sb.append("\n  Serial Number: ").append(EdidUtil.getDescriptorText(b));
229                 break;
230             case 0xfe:
231                 sb.append("\n  Unspecified Text: ").append(EdidUtil.getDescriptorText(b));
232                 break;
233             case 0xfd:
234                 sb.append("\n  Range Limits: ").append(EdidUtil.getDescriptorRangeLimits(b));
235                 break;
236             case 0xfc:
237                 sb.append("\n  Monitor Name: ").append(EdidUtil.getDescriptorText(b));
238                 break;
239             case 0xfb:
240                 sb.append("\n  White Point Data: ").append(ParseUtil.byteArrayToHexString(b));
241                 break;
242             case 0xfa:
243                 sb.append("\n  Standard Timing ID: ").append(ParseUtil.byteArrayToHexString(b));
244                 break;
245             default:
246                 if (EdidUtil.getDescriptorType(b) <= 0x0f && EdidUtil.getDescriptorType(b) >= 0x00) {
247                     sb.append("\n  Manufacturer Data: ").append(ParseUtil.byteArrayToHexString(b));
248                 } else {
249                     sb.append("\n  Preferred Timing: ").append(EdidUtil.getTimingDescriptor(b));
250                 }
251                 break;
252             }
253         }
254         return sb.toString();
255     }
256 }