From c28a59cde5403f2968fac58b36a70a79c5ee8c98 Mon Sep 17 00:00:00 2001 From: pjbosco Date: Sat, 27 Jun 2026 21:35:49 -0400 Subject: [PATCH 1/4] feat: add RevisionCode parser and structured board identification Introduce RevisionCode, a parser for the Raspberry Pi 32-bit revision code format (NOQuuuWuFMMMCCCCPPPPTTTTTTTTRRRR). Both new-style codes (identified by bit 23) and old-style codes are supported. New-style codes are decoded by extracting structured bit fields for memory size, processor, and board type. Old-style codes are matched against known revision code tables. Update BoardModel.getByBoardCode() to attempt structured identification via RevisionCode before falling back to legacy string matching. Add BoardModel.getByRevisionCode(RevisionCode) which switches on the decoded board type and, where necessary, disambiguates on PCB revision or memory size. This aligns board identification with the Raspberry Pi foundation's recommended best practices for revision code usage, ensuring that new board revisions of existing types are identified correctly without requiring a code change. - Add RevisionCode with nested MemorySize, Processor, and Type enums - Add RevisionCodeTest with full coverage of new-style and old-style parsing, field extraction, string input handling, and edge cases - Update BoardModel.getByBoardCode() to delegate to RevisionCode - Add BoardModel.getByRevisionCode(RevisionCode) - Add BoardModelTest cases for getByRevisionCode() --- .../pi4j/boardinfo/definition/BoardModel.java | 86 ++++ .../boardinfo/definition/RevisionCode.java | 411 ++++++++++++++++++ .../boardinfo/definition/BoardModelTest.java | 88 ++++ .../definition/RevisionCodeTest.java | 149 +++++++ 4 files changed, 734 insertions(+) create mode 100644 pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java create mode 100644 pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java index f0890eef4..ea1436c05 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java @@ -444,14 +444,100 @@ public enum BoardModel { this.usesRP1 = usesRP1; } + /** + * Returns the {@link BoardModel} corresponding to the given {@link RevisionCode} by matching on the board type and, + * where necessary, the PCB revision or memory size. + * + * @param revisionCode the parsed revision code to identify + * @return the matching {@code BoardModel}, or {@code UNKNOWN} if the type is not recognized + */ + public static BoardModel getByRevisionCode(RevisionCode revisionCode) { + switch (revisionCode.type()) { + case RPI_A: + return BoardModel.MODEL_1_A; + case RPI_B: + case RPI_ALPHA: + return BoardModel.MODEL_1_B; + case RPI_A_PLUS: + return BoardModel.MODEL_1_A_PLUS; + case RPI_B_PLUS: + return BoardModel.MODEL_1_B_PLUS; + case RPI_2B: + if (revisionCode.revision() < 2) { + return BoardModel.MODEL_2_B; + } else { + return BoardModel.MODEL_2_B_V1_2; + } + case RPI_CM1: + return BoardModel.COMPUTE_1; + case RPI_3B: + return BoardModel.MODEL_3_B; + case RPI_ZERO: + if (revisionCode.revision() < 3) { + return BoardModel.ZERO_PCB_1_2; + } else { + return BoardModel.ZERO_PCB_1_3; + } + case RPI_CM3: + return BoardModel.COMPUTE_3; + case RPI_ZERO_W: + return BoardModel.ZERO_W; + case RPI_3B_PLUS: + return BoardModel.MODEL_3_B_PLUS; + case RPI_3A_PLUS: + return BoardModel.MODEL_3_A_PLUS; + case RPI_CM3_PLUS: + return BoardModel.COMPUTE_3_PLUS; + case RPI_4B: + return BoardModel.MODEL_4_B; + case RPI_ZERO_2_W: + case RPI_CM0: + return BoardModel.ZERO_V2; + case RPI_400: + return BoardModel.MODEL_400; + case RPI_CM4: + return BoardModel.COMPUTE_4; + case RPI_CM4S: + return BoardModel.COMPUTE_4_SODIMM; + case RPI_5: + return BoardModel.MODEL_5_B; + case RPI_CM5: + case RPI_CM5_LITE: + return BoardModel.COMPUTE_5; + case RPI_500_500_PLUS: + if (RevisionCode.MemorySize.GB_16.equals(revisionCode.memorySize())) { + return BoardModel.MODEL_500_PLUS; + } else { + return BoardModel.MODEL_500; + } + default: + return UNKNOWN; + } + } + /** * Retrieves the board model corresponding to the given board code. * + *

Where possible, identification is performed by decoding the structured fields of the revision code per the + * Raspberry Pi revision code specification. If the revision code cannot be parsed, falls back to direct string + * matching against known board codes.

+ * * @param boardCode the board code to look up * @return the matching {@code BoardModel} or {@code UNKNOWN} if no match is found * @throws Exception if multiple matches are found */ public static BoardModel getByBoardCode(String boardCode) throws Exception { + try { + RevisionCode revisionCode = RevisionCode.of(boardCode); + BoardModel result = getByRevisionCode(revisionCode); + if (result != UNKNOWN) + { + return result; + } + } catch (IllegalArgumentException e) { + // Not a valid revision code, fall through to legacy lookup. + } + var matches = Arrays.stream(BoardModel.values()) .filter(bm -> bm.boardCodes.contains(boardCode)) .collect(Collectors.toList()); diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java new file mode 100644 index 000000000..9b31ae9fa --- /dev/null +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java @@ -0,0 +1,411 @@ +package com.pi4j.boardinfo.definition; + +/*- + * #%L + * ********************************************************************** + * ORGANIZATION : Pi4J + * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) + * FILENAME : BoardInfoHelper.java + * + * This file is part of the Pi4J project. More information about + * this project can be found here: https://pi4j.com/ + * ********************************************************************** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.util.Objects; + +/** + * Represents a parsed Raspberry Pi 32-bit revision code in the format: + *
NOQuuuWuFMMMCCCCPPPPTTTTTTTTRRRR
+ * where: + * + * + *

Both old-style and new-style revision codes are supported. New-style codes are identified + * by bit 23 being set. Old-style codes are matched against known revision code tables.

+ * + *

Instances are obtained via the factory methods {@link #of(int)} and {@link #of(String)}:

+ *
+ *     RevisionCode code = RevisionCode.of("a03111");
+ *     RevisionCode code = RevisionCode.of(0xa03111);
+ * 
+ * + * @see + * Raspberry Pi New-Style Revision Codes + */ +public final class RevisionCode { + + private final MemorySize memorySize; + private final Processor processor; + private final Type type; + private final int revision; + + /** + * Private constructor. Use {@link #of(int)} or {@link #of(String)} to obtain instances. + * + * @param memorySize the memory size decoded from the revision code + * @param processor the processor decoded from the revision code + * @param type the board type decoded from the revision code + * @param revision the PCB revision number decoded from the revision code + */ + private RevisionCode(MemorySize memorySize, Processor processor, Type type, int revision) { + this.memorySize = memorySize; + this.processor = processor; + this.type = type; + this.revision = revision; + } + + /** + * Returns the memory size of the board. + * + * @return the {@link MemorySize} decoded from the revision code + */ + public MemorySize memorySize() { + return memorySize; + } + + /** + * Returns the processor of the board. + * + * @return the {@link Processor} decoded from the revision code + */ + public Processor processor() { + return processor; + } + + /** + * Returns the board type. + * + * @return the {@link Type} decoded from the revision code + */ + public Type type() { + return type; + } + + /** + * Returns the PCB revision number. + * For old-style revision codes, this always returns {@code 1} since old-style boards + * do not have revision-specific {@code BoardModel} instances. + * + * @return the PCB revision number + */ + public int revision() { + return revision; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RevisionCode that)) return false; + return memorySize == that.memorySize + && processor == that.processor + && type == that.type + && revision == that.revision; + } + + @Override + public int hashCode() { + return Objects.hash( + memorySize, + processor, + type, + revision); + } + + @Override + public String toString() { + return "RevisionCode{" + + "memorySize=" + memorySize + + ", processor=" + processor + + ", type=" + type + + ", revision=" + revision + + '}'; + } + + /** + * Creates a {@code RevisionCode} by parsing a 32-bit integer revision code. + * + * @param revisionCode the raw 32-bit revision code + * @return a {@code RevisionCode} instance representing the parsed fields + */ + public static RevisionCode of(int revisionCode) { + return new RevisionCode( + MemorySize.fromRevisionCode(revisionCode), + Processor.fromRevisionCode(revisionCode), + Type.fromRevisionCode(revisionCode), + extractRevision(revisionCode) + ); + } + + /** + * Creates a {@code RevisionCode} by parsing a hexadecimal string revision code. + * The string may optionally include a {@code 0x} or {@code 0X} prefix and surrounding + * whitespace, both of which are stripped before parsing. + * + * @param revisionCode the revision code as a hex string (e.g. {@code "a03111"} or {@code "0xa03111"}) + * @return a {@code RevisionCode} instance representing the parsed fields + * @throws IllegalArgumentException if the string is {@code null}, blank, or not valid hexadecimal + */ + public static RevisionCode of(String revisionCode) { + if (revisionCode == null || revisionCode.isBlank()) { + throw new IllegalArgumentException(("Revision code cannot be null or empty.")); + } + final String clean = revisionCode.trim().toUpperCase().replace("0X", "").replace(" ", ""); + try { + int value = (int) Long.parseLong(clean, 16); + return of(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Revision code is not valid hex: " + revisionCode, e); + } + } + + /** + * Returns {@code true} if the given revision code uses the new-style format, + * identified by bit 23 being set. + * + * @param revisionCode the raw 32-bit revision code + * @return {@code true} if new-style, {@code false} if old-style + */ + private static boolean isNewStyleRevisionCode(int revisionCode) { + return (revisionCode & 0x800000) != 0; + } + + /** + * Extracts the PCB revision number (bits 3:0) from a new-style revision code. + * For old-style revision codes, returns {@code 1} since old-style boards do not + * have revision-specific {@code BoardModel} instances. + * + * @param revisionCode the raw 32-bit revision code + * @return the PCB revision number, or {@code 1} for old-style codes + */ + private static int extractRevision(int revisionCode) { + return isNewStyleRevisionCode(revisionCode) ? revisionCode & 0xF : 1; + } + + /** + * Represents the memory size field (bits 22:20) of a new-style Raspberry Pi revision code. + * + *

Old-style revision codes are matched against known values. New-style codes are + * identified by the 3-bit MMM field. If the value is unrecognized, {@link #UNKNOWN} is returned.

+ * + * @see + * Raspberry Pi New-Style Revision Codes + */ + public enum MemorySize { + MB_256(0, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x0012), + MB_512(1, 0x000d, 0x000e, 0x000f, 0x0010, 0x0011, 0x0013, 0x0014), + GB_1(2), + GB_2(3), + GB_4(4), + GB_8(5), + GB_16(6), + OTHER(7, 0x0015), + UNKNOWN(8); + + /** A list of old-style revision codes for the enum instance. */ + private final int[] oldStyleRevisionCodes; + + /** The value of the enum instance. */ + private final int value; + + MemorySize(int value) { + this(value, new int[0]); + } + + MemorySize(int value, int ... oldStyleRevisionCodes) { + this.oldStyleRevisionCodes = oldStyleRevisionCodes; + this.value = value; + } + + /** + * Returns the {@code MemorySize} corresponding to the given revision code. + * Returns {@link #UNKNOWN} if no match is found. + * + * @param revisionCode the raw 32-bit revision code + * @return the matching {@code MemorySize}, or {@link #UNKNOWN} + */ + static MemorySize fromRevisionCode(int revisionCode) { + if (isNewStyleRevisionCode(revisionCode)) { + final int value = (revisionCode >> 20) & 0x7; + for (var s : values()) { + if (s.value == value) { + return s; + } + } + } else { + for (var s : values()) { + for (var r : s.oldStyleRevisionCodes) { + if (r == revisionCode) { + return s; + } + } + } + } + return UNKNOWN; + } + } + + /** + * Represents the processor field (bits 15:12) of a new-style Raspberry Pi revision code. + * + *

All boards using old-style revision codes are assumed to use the {@link #BCM2835}. + * If the processor value in a new-style code is unrecognized, {@link #UNKNOWN} is returned.

+ * + * @see + * Raspberry Pi New-Style Revision Codes + */ + public enum Processor { + BCM2835(0, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000d, 0x000e, 0x000f, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015), + BCM2836(1), + BCM2837(2), + BCM2711(3), + BCM2712(4), + UNKNOWN(16); + + /** A list of old-style revision codes for the enum instance. */ + private final int[] oldStyleRevisionCodes; + + /** The value of the enum instance. */ + private final int value; + + Processor(int value) { + this(value, new int[0]); + } + + Processor(int value, int ... oldStyleRevisionCodes) { + this.oldStyleRevisionCodes = oldStyleRevisionCodes; + this.value = value; + } + + /** + * Returns the {@code Processor} corresponding to the given revision code. + * Old-style revision codes always return {@link #BCM2835}. + * Returns {@link #UNKNOWN} if no match is found in a new-style code. + * + * @param revisionCode the raw 32-bit revision code + * @return the matching {@code Processor}, or {@link #UNKNOWN} + */ + static Processor fromRevisionCode(int revisionCode) { + if (isNewStyleRevisionCode(revisionCode)) { + final int value = (revisionCode >> 12) & 0xF; + for (var s : values()) { + if (s.value == value) { + return s; + } + } + } else { + for (var s : values()) { + for (var r : s.oldStyleRevisionCodes) { + if (r == revisionCode) { + return s; + } + } + } + } + return UNKNOWN; + } + } + + /** + * Represents the board type field (bits 11:4) of a new-style Raspberry Pi revision code. + * + *

Old-style revision codes are matched against known values per type. New-style codes + * are identified by the 8-bit TTTTTTTT field. If the value is unrecognized, + * {@link #UNKNOWN} is returned.

+ * + * @see + * Raspberry Pi New-Style Revision Codes + */ + public enum Type { + RPI_A(0, 0x0007, 0x0008, 0x0009), + RPI_B(1, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x000d, 0x000e, 0x000f), + RPI_A_PLUS(2, 0x0012, 0x0015), + RPI_B_PLUS(3, 0x0010, 0x0013), + RPI_2B(4), + RPI_ALPHA(5), + RPI_CM1(6, 0x0011, 0x0014), + RPI_3B(8), + RPI_ZERO(9), + RPI_CM3(10), + RPI_ZERO_W(12), + RPI_3B_PLUS(13), + RPI_3A_PLUS(14), + RPI_CM3_PLUS(16), + RPI_4B(17), + RPI_ZERO_2_W(18), + RPI_400(19), + RPI_CM4(20), + RPI_CM4S(21), + RPI_5(23), + RPI_CM5(24), + RPI_500_500_PLUS(25), + RPI_CM5_LITE(26), + RPI_CM0(27), + UNKNOWN(256); + + /** A list of old-style revision codes for the enum instance. */ + private final int[] oldStyleRevisionCodes; + + /** The value of the enum instance. */ + private final int value; + + Type(int value) { + this(value, new int[0]); + } + + Type(int value, int ... oldStyleRevisionCodes) { + this.oldStyleRevisionCodes = oldStyleRevisionCodes; + this.value = value; + } + + /** + * Returns the {@code Type} corresponding to the given revision code. + * Returns {@link #UNKNOWN} if no match is found. + * + * @param revisionCode the raw 32-bit revision code + * @return the matching {@code Type}, or {@link #UNKNOWN} + */ + static Type fromRevisionCode(int revisionCode) { + if (isNewStyleRevisionCode(revisionCode)) { + final int value = (revisionCode >> 4) & 0xFF; + for (var s : values()) { + if (s.value == value) { + return s; + } + } + } else { + for (var s : values()) { + for (var r : s.oldStyleRevisionCodes) { + if (r == revisionCode) { + return s; + } + } + } + } + return UNKNOWN; + } + } +} diff --git a/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/BoardModelTest.java b/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/BoardModelTest.java index 898a2a505..c34644769 100644 --- a/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/BoardModelTest.java +++ b/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/BoardModelTest.java @@ -9,6 +9,94 @@ class BoardModelTest { + @Test + void getBoardModelByRevisionCode() { + assertAll( + () -> assertEquals(BoardModel.MODEL_1_A_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("900021"))), + () -> assertEquals(BoardModel.MODEL_1_B_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("900032"))), + () -> assertEquals(BoardModel.MODEL_2_B, BoardModel.getByRevisionCode(RevisionCode.of("a01040"))), + () -> assertEquals(BoardModel.MODEL_2_B, BoardModel.getByRevisionCode(RevisionCode.of("a01041"))), + () -> assertEquals(BoardModel.MODEL_2_B, BoardModel.getByRevisionCode(RevisionCode.of("a21041"))), + () -> assertEquals(BoardModel.MODEL_2_B_V1_2, BoardModel.getByRevisionCode(RevisionCode.of("a02042"))), + () -> assertEquals(BoardModel.MODEL_2_B_V1_2, BoardModel.getByRevisionCode(RevisionCode.of("a22042"))), + () -> assertEquals(BoardModel.COMPUTE_1, BoardModel.getByRevisionCode(RevisionCode.of("900061"))), + () -> assertEquals(BoardModel.MODEL_3_B, BoardModel.getByRevisionCode(RevisionCode.of("a02082"))), + () -> assertEquals(BoardModel.MODEL_3_B, BoardModel.getByRevisionCode(RevisionCode.of("a22082"))), + () -> assertEquals(BoardModel.MODEL_3_B, BoardModel.getByRevisionCode(RevisionCode.of("a32082"))), + () -> assertEquals(BoardModel.MODEL_3_B, BoardModel.getByRevisionCode(RevisionCode.of("a52082"))), + () -> assertEquals(BoardModel.MODEL_3_B, BoardModel.getByRevisionCode(RevisionCode.of("a22083"))), + () -> assertEquals(BoardModel.ZERO_PCB_1_2, BoardModel.getByRevisionCode(RevisionCode.of("900092"))), + () -> assertEquals(BoardModel.ZERO_PCB_1_2, BoardModel.getByRevisionCode(RevisionCode.of("920092"))), + () -> assertEquals(BoardModel.ZERO_PCB_1_3, BoardModel.getByRevisionCode(RevisionCode.of("900093"))), + () -> assertEquals(BoardModel.ZERO_PCB_1_3, BoardModel.getByRevisionCode(RevisionCode.of("920093"))), + () -> assertEquals(BoardModel.COMPUTE_3, BoardModel.getByRevisionCode(RevisionCode.of("a020a0"))), + () -> assertEquals(BoardModel.COMPUTE_3, BoardModel.getByRevisionCode(RevisionCode.of("a220a0"))), + () -> assertEquals(BoardModel.ZERO_W, BoardModel.getByRevisionCode(RevisionCode.of("9000c1"))), + () -> assertEquals(BoardModel.MODEL_3_B_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("a020d3"))), + () -> assertEquals(BoardModel.MODEL_3_B_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("a020d4"))), + () -> assertEquals(BoardModel.MODEL_3_A_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("9020e0"))), + () -> assertEquals(BoardModel.MODEL_3_A_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("9020e1"))), + () -> assertEquals(BoardModel.COMPUTE_3_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("a02100"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("a03111"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("b03111"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("c03111"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("b03112"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("c03112"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("b03114"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("c03114"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("d03114"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("b03115"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("c03115"))), + () -> assertEquals(BoardModel.MODEL_4_B, BoardModel.getByRevisionCode(RevisionCode.of("d03115"))), + () -> assertEquals(BoardModel.ZERO_V2, BoardModel.getByRevisionCode(RevisionCode.of("902120"))), + () -> assertEquals(BoardModel.MODEL_400, BoardModel.getByRevisionCode(RevisionCode.of("c03130"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("a03140"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("b03140"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("c03140"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("d03140"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("a03141"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("b03141"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("c03141"))), + () -> assertEquals(BoardModel.COMPUTE_4, BoardModel.getByRevisionCode(RevisionCode.of("d03141"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("b04170"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("c04170"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("d04170"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("a04171"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("b04171"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("c04171"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("d04171"))), + () -> assertEquals(BoardModel.MODEL_5_B, BoardModel.getByRevisionCode(RevisionCode.of("e04171"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("b04180"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("c04180"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("d04180"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("e04180"))), + () -> assertEquals(BoardModel.MODEL_500, BoardModel.getByRevisionCode(RevisionCode.of("d04190"))), + () -> assertEquals(BoardModel.MODEL_500_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("e04190"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("b041a0"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("c041a0"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("d041a0"))), + () -> assertEquals(BoardModel.COMPUTE_5, BoardModel.getByRevisionCode(RevisionCode.of("e041a0"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("0002"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("0003"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("0004"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("0005"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("0006"))), + () -> assertEquals(BoardModel.MODEL_1_A, BoardModel.getByRevisionCode(RevisionCode.of("0007"))), + () -> assertEquals(BoardModel.MODEL_1_A, BoardModel.getByRevisionCode(RevisionCode.of("0008"))), + () -> assertEquals(BoardModel.MODEL_1_A, BoardModel.getByRevisionCode(RevisionCode.of("0009"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("000d"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("000e"))), + () -> assertEquals(BoardModel.MODEL_1_B, BoardModel.getByRevisionCode(RevisionCode.of("000f"))), + () -> assertEquals(BoardModel.MODEL_1_B_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("0010"))), + () -> assertEquals(BoardModel.COMPUTE_1, BoardModel.getByRevisionCode(RevisionCode.of("0011"))), + () -> assertEquals(BoardModel.MODEL_1_A_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("0012"))), + () -> assertEquals(BoardModel.MODEL_1_B_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("0013"))), + () -> assertEquals(BoardModel.COMPUTE_1, BoardModel.getByRevisionCode(RevisionCode.of("0014"))), + () -> assertEquals(BoardModel.MODEL_1_A_PLUS, BoardModel.getByRevisionCode(RevisionCode.of("0015"))), + () -> assertEquals(BoardModel.UNKNOWN, BoardModel.getByRevisionCode(RevisionCode.of("ffffffff"))) + ); + } + @Test void getBoardModelByBoardCode() { assertAll( diff --git a/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java b/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java new file mode 100644 index 000000000..785c8f67c --- /dev/null +++ b/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java @@ -0,0 +1,149 @@ +package com.pi4j.boardinfo.definition; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RevisionCodeTest { + + @Test + void getInvalidRevisionCode() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of(null)), + () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of("")), + () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of("invalid hex")) + ); + } + + @Test + void getNewStyleRevisionCode() { + int revisionCode = 0x800000; + revisionCode |= (0x4 << 20); // Set MemorySize GB_4 + revisionCode |= (0x4 << 12); // Set Processor: BCM2712 + revisionCode |= (0x17 << 4); // Set Type: RPI_5 + revisionCode |= 1; // Set Revision: 1 + RevisionCode actual = RevisionCode.of(revisionCode); + assertAll( + () -> assertEquals(RevisionCode.MemorySize.GB_4, actual.memorySize()), + () -> assertEquals(RevisionCode.Processor.BCM2712, actual.processor()), + () -> assertEquals(RevisionCode.Type.RPI_5, actual.type()), + () -> assertEquals(1, actual.revision()) + ); + } + + @Test + void getOldStyleRevisionCode() { + String revisionCode = "0x0002"; + RevisionCode actual = RevisionCode.of(revisionCode); + assertAll( + () -> assertEquals(RevisionCode.MemorySize.MB_256, actual.memorySize()), + () -> assertEquals(RevisionCode.Processor.BCM2835, actual.processor()), + () -> assertEquals(RevisionCode.Type.RPI_B, actual.type()), + () -> assertEquals(1, actual.revision()) + ); + } + + @Test + void getUnknownRevisionCode() { + String revisionCode = "0x0001"; + RevisionCode actual = RevisionCode.of(revisionCode); + assertAll( + () -> assertEquals(RevisionCode.MemorySize.UNKNOWN, actual.memorySize()), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, actual.processor()), + () -> assertEquals(RevisionCode.Type.UNKNOWN, actual.type()), + () -> assertEquals(1, actual.revision()) + ); + } + + @Test + void getMemorySizeFromRevisionCode() { + assertAll( + // New-style + () -> assertEquals(RevisionCode.MemorySize.MB_256, RevisionCode.MemorySize.fromRevisionCode(0x800000)), + () -> assertEquals(RevisionCode.MemorySize.MB_512, RevisionCode.MemorySize.fromRevisionCode(0x800000 | (0x1 << 20))), + () -> assertEquals(RevisionCode.MemorySize.GB_1, RevisionCode.MemorySize.fromRevisionCode(0x800000 | (0x2 << 20))), + () -> assertEquals(RevisionCode.MemorySize.GB_2, RevisionCode.MemorySize.fromRevisionCode(0x800000 | (0x3 << 20))), + () -> assertEquals(RevisionCode.MemorySize.GB_4, RevisionCode.MemorySize.fromRevisionCode(0x800000 | (0x4 << 20))), + () -> assertEquals(RevisionCode.MemorySize.GB_8, RevisionCode.MemorySize.fromRevisionCode(0x800000 | (0x5 << 20))), + () -> assertEquals(RevisionCode.MemorySize.GB_16, RevisionCode.MemorySize.fromRevisionCode(0x800000 | (0x6 << 20))), + () -> assertEquals(RevisionCode.MemorySize.OTHER, RevisionCode.MemorySize.fromRevisionCode(0x800000 | (0x7 << 20))), + // Old-style + () -> assertEquals(RevisionCode.MemorySize.MB_256, RevisionCode.MemorySize.fromRevisionCode(2)), + () -> assertEquals(RevisionCode.MemorySize.MB_512, RevisionCode.MemorySize.fromRevisionCode(16)), + () -> assertEquals(RevisionCode.MemorySize.OTHER, RevisionCode.MemorySize.fromRevisionCode(21)), + () -> assertEquals(RevisionCode.MemorySize.UNKNOWN, RevisionCode.MemorySize.fromRevisionCode(0)) + ); + } + + @Test + void getTypeFromRevisionCode() { + assertAll( + // New-style + () -> assertEquals(RevisionCode.Type.RPI_A, RevisionCode.Type.fromRevisionCode(0x800000)), + () -> assertEquals(RevisionCode.Type.RPI_B, RevisionCode.Type.fromRevisionCode(0x800000 | (0x01 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_A_PLUS, RevisionCode.Type.fromRevisionCode(0x800000 | (0x02 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_B_PLUS, RevisionCode.Type.fromRevisionCode(0x800000 | (0x03 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_2B, RevisionCode.Type.fromRevisionCode(0x800000 | (0x04 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_ALPHA, RevisionCode.Type.fromRevisionCode(0x800000 | (0x05 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM1, RevisionCode.Type.fromRevisionCode(0x800000 | (0x06 << 4))), + () -> assertEquals(RevisionCode.Type.UNKNOWN, RevisionCode.Type.fromRevisionCode(0x800000 | (0x07 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_3B, RevisionCode.Type.fromRevisionCode(0x800000 | (0x08 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_ZERO, RevisionCode.Type.fromRevisionCode(0x800000 | (0x09 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM3, RevisionCode.Type.fromRevisionCode(0x800000 | (0x0a << 4))), + () -> assertEquals(RevisionCode.Type.UNKNOWN, RevisionCode.Type.fromRevisionCode(0x800000 | (0x0b << 4))), + () -> assertEquals(RevisionCode.Type.RPI_ZERO_W, RevisionCode.Type.fromRevisionCode(0x800000 | (0x0c << 4))), + () -> assertEquals(RevisionCode.Type.RPI_3B_PLUS, RevisionCode.Type.fromRevisionCode(0x800000 | (0x0d << 4))), + () -> assertEquals(RevisionCode.Type.RPI_3A_PLUS, RevisionCode.Type.fromRevisionCode(0x800000 | (0x0e << 4))), + () -> assertEquals(RevisionCode.Type.UNKNOWN, RevisionCode.Type.fromRevisionCode(0x800000 | (0x0f << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM3_PLUS, RevisionCode.Type.fromRevisionCode(0x800000 | (0x10 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_4B, RevisionCode.Type.fromRevisionCode(0x800000 | (0x11 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_ZERO_2_W, RevisionCode.Type.fromRevisionCode(0x800000 | (0x12 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_400, RevisionCode.Type.fromRevisionCode(0x800000 | (0x13 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM4, RevisionCode.Type.fromRevisionCode(0x800000 | (0x14 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM4S, RevisionCode.Type.fromRevisionCode(0x800000 | (0x15 << 4))), + () -> assertEquals(RevisionCode.Type.UNKNOWN, RevisionCode.Type.fromRevisionCode(0x800000 | (0x16 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_5, RevisionCode.Type.fromRevisionCode(0x800000 | (0x17 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM5, RevisionCode.Type.fromRevisionCode(0x800000 | (0x18 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_500_500_PLUS, RevisionCode.Type.fromRevisionCode(0x800000 | (0x19 << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM5_LITE, RevisionCode.Type.fromRevisionCode(0x800000 | (0x1a << 4))), + () -> assertEquals(RevisionCode.Type.RPI_CM0, RevisionCode.Type.fromRevisionCode(0x800000 | (0x1b << 4))), + // Old-style + () -> assertEquals(RevisionCode.Type.RPI_A, RevisionCode.Type.fromRevisionCode(0x7)), + () -> assertEquals(RevisionCode.Type.RPI_B, RevisionCode.Type.fromRevisionCode(0x3)), + () -> assertEquals(RevisionCode.Type.RPI_A_PLUS, RevisionCode.Type.fromRevisionCode(0x12)), + () -> assertEquals(RevisionCode.Type.RPI_B_PLUS, RevisionCode.Type.fromRevisionCode(0x13)), + () -> assertEquals(RevisionCode.Type.RPI_CM1, RevisionCode.Type.fromRevisionCode(0x11)), + () -> assertEquals(RevisionCode.Type.UNKNOWN, RevisionCode.Type.fromRevisionCode(0x1a)) + ); + } + + @Test + void getProcessorFromRevisionCode() { + assertAll( + // New-style + () -> assertEquals(RevisionCode.Processor.BCM2835, RevisionCode.Processor.fromRevisionCode(0x800000)), + () -> assertEquals(RevisionCode.Processor.BCM2836, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x1 << 12))), + () -> assertEquals(RevisionCode.Processor.BCM2837, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x2 << 12))), + () -> assertEquals(RevisionCode.Processor.BCM2711, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x3 << 12))), + () -> assertEquals(RevisionCode.Processor.BCM2712, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x4 << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x5 << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x6 << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x7 << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x8 << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0x9 << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0xa << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0xb << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0xc << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0xd << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0xe << 12))), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x800000 | (0xf << 12))), + // Old-style + () -> assertEquals(RevisionCode.Processor.BCM2835, RevisionCode.Processor.fromRevisionCode(0x2)), + () -> assertEquals(RevisionCode.Processor.BCM2835, RevisionCode.Processor.fromRevisionCode(0x3)), + () -> assertEquals(RevisionCode.Processor.BCM2835, RevisionCode.Processor.fromRevisionCode(0x4)), + () -> assertEquals(RevisionCode.Processor.BCM2835, RevisionCode.Processor.fromRevisionCode(0x5)), + () -> assertEquals(RevisionCode.Processor.BCM2835, RevisionCode.Processor.fromRevisionCode(0x6)), + () -> assertEquals(RevisionCode.Processor.UNKNOWN, RevisionCode.Processor.fromRevisionCode(0x1a)) + ); + } +} From 21d95b07993f52d62cdf7f5526a2b3a4a0bd41d3 Mon Sep 17 00:00:00 2001 From: pjbosco Date: Sat, 27 Jun 2026 22:01:42 -0400 Subject: [PATCH 2/4] Fixed incorrect filename in header. --- .../main/java/com/pi4j/boardinfo/definition/RevisionCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java index 9b31ae9fa..590a95f70 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java @@ -5,7 +5,7 @@ * ********************************************************************** * ORGANIZATION : Pi4J * PROJECT : Pi4J :: LIBRARY :: Java Library (CORE) - * FILENAME : BoardInfoHelper.java + * FILENAME : RevisionCode.java * * This file is part of the Pi4J project. More information about * this project can be found here: https://pi4j.com/ From d77b0823ae6cd37c34313942f3f16f311e502b1f Mon Sep 17 00:00:00 2001 From: pjbosco Date: Mon, 29 Jun 2026 14:52:21 -0400 Subject: [PATCH 3/4] Refactored RevisionCode slightly to reduce cognitive complexity by: - extracting a common interface, RevisionCodeEnum`, for internal enumerations of RevisionCode - creating a new fromNewStyleCode() method that generically returns an enum constant whose bit field value matches the extracted field of a new-style revision code based on a given shift and mask - creating a new fromOldStyleCode() method that generically returns an enum constant whose old-style revision code list contains the given revision code --- .../boardinfo/definition/RevisionCode.java | 173 ++++++++++++------ 1 file changed, 121 insertions(+), 52 deletions(-) diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java index 590a95f70..a9d9d13ba 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java @@ -26,6 +26,7 @@ */ import java.util.Objects; +import java.util.function.Supplier; /** * Represents a parsed Raspberry Pi 32-bit revision code in the format: @@ -205,6 +206,32 @@ private static int extractRevision(int revisionCode) { return isNewStyleRevisionCode(revisionCode) ? revisionCode & 0xF : 1; } + /** + * A common interface for the internal enumerations of {@code RevisionCode}. + * + *

Provides access to the numeric field value used in new-style revision codes + * and the list of old-style revision codes associated with each constant, allowing + * shared lookup logic to operate generically across {@link RevisionCode.MemorySize}, + * {@link RevisionCode.Processor}, and {@link RevisionCode.Type}.

+ */ + private interface RevisionCodeEnum { + /** + * Returns the numeric value of this constant as encoded in the + * new-style revision code bit field. + * + * @return the bit field value + */ + int getValue(); + + /** + * Returns the old-style revision codes associated with this constant, + * or an empty array if this constant has no old-style codes. + * + * @return the old-style revision codes + */ + int[] getOldStyleRevisionCodes(); + } + /** * Represents the memory size field (bits 22:20) of a new-style Raspberry Pi revision code. * @@ -214,7 +241,7 @@ private static int extractRevision(int revisionCode) { * @see * Raspberry Pi New-Style Revision Codes */ - public enum MemorySize { + public enum MemorySize implements RevisionCodeEnum { MB_256(0, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x0012), MB_512(1, 0x000d, 0x000e, 0x000f, 0x0010, 0x0011, 0x0013, 0x0014), GB_1(2), @@ -248,23 +275,19 @@ public enum MemorySize { * @return the matching {@code MemorySize}, or {@link #UNKNOWN} */ static MemorySize fromRevisionCode(int revisionCode) { - if (isNewStyleRevisionCode(revisionCode)) { - final int value = (revisionCode >> 20) & 0x7; - for (var s : values()) { - if (s.value == value) { - return s; - } - } - } else { - for (var s : values()) { - for (var r : s.oldStyleRevisionCodes) { - if (r == revisionCode) { - return s; - } - } - } - } - return UNKNOWN; + return isNewStyleRevisionCode(revisionCode) ? + fromNewStyleCode(revisionCode, 20, 0x7, MemorySize::values, MemorySize.UNKNOWN) : + fromOldStyleCode(revisionCode, MemorySize::values, MemorySize.UNKNOWN); + } + + @Override + public int getValue() { + return value; + } + + @Override + public int[] getOldStyleRevisionCodes() { + return oldStyleRevisionCodes; } } @@ -277,7 +300,7 @@ static MemorySize fromRevisionCode(int revisionCode) { * @see * Raspberry Pi New-Style Revision Codes */ - public enum Processor { + public enum Processor implements RevisionCodeEnum { BCM2835(0, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x0009, 0x000d, 0x000e, 0x000f, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015), BCM2836(1), BCM2837(2), @@ -309,23 +332,19 @@ public enum Processor { * @return the matching {@code Processor}, or {@link #UNKNOWN} */ static Processor fromRevisionCode(int revisionCode) { - if (isNewStyleRevisionCode(revisionCode)) { - final int value = (revisionCode >> 12) & 0xF; - for (var s : values()) { - if (s.value == value) { - return s; - } - } - } else { - for (var s : values()) { - for (var r : s.oldStyleRevisionCodes) { - if (r == revisionCode) { - return s; - } - } - } - } - return UNKNOWN; + return isNewStyleRevisionCode(revisionCode) ? + fromNewStyleCode(revisionCode, 12, 0xF, Processor::values, Processor.UNKNOWN) : + fromOldStyleCode(revisionCode, Processor::values, Processor.UNKNOWN); + } + + @Override + public int getValue() { + return value; + } + + @Override + public int[] getOldStyleRevisionCodes() { + return oldStyleRevisionCodes; } } @@ -339,7 +358,7 @@ static Processor fromRevisionCode(int revisionCode) { * @see * Raspberry Pi New-Style Revision Codes */ - public enum Type { + public enum Type implements RevisionCodeEnum { RPI_A(0, 0x0007, 0x0008, 0x0009), RPI_B(1, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x000d, 0x000e, 0x000f), RPI_A_PLUS(2, 0x0012, 0x0015), @@ -389,23 +408,73 @@ public enum Type { * @return the matching {@code Type}, or {@link #UNKNOWN} */ static Type fromRevisionCode(int revisionCode) { - if (isNewStyleRevisionCode(revisionCode)) { - final int value = (revisionCode >> 4) & 0xFF; - for (var s : values()) { - if (s.value == value) { - return s; - } - } - } else { - for (var s : values()) { - for (var r : s.oldStyleRevisionCodes) { - if (r == revisionCode) { - return s; - } - } + return isNewStyleRevisionCode(revisionCode) ? + fromNewStyleCode(revisionCode, 4, 0xFF, Type::values, Type.UNKNOWN) : + fromOldStyleCode(revisionCode, Type::values, Type.UNKNOWN); + } + + @Override + public int getValue() { + return value; + } + + @Override + public int[] getOldStyleRevisionCodes() { + return oldStyleRevisionCodes; + } + } + + /** + * Returns the enum constant of type {@code T} whose bit field value matches the + * extracted field from the given new-style revision code. + * + *

The field value is extracted by shifting the revision code right by {@code shift} + * bits and masking with {@code mask}. The result is compared against + * {@link RevisionCodeEnum#getValue()} for each constant.

+ * + * @param the enum type, which must implement {@link RevisionCodeEnum} + * @param revisionCode the raw 32-bit new-style revision code + * @param shift the number of bits to right-shift before applying the mask + * @param mask the bitmask to apply after shifting + * @param values a supplier of the enum constants to search (e.g. {@code MemorySize::values}) + * @param unknown the sentinel value to return if no match is found + * @return the matching enum constant, or {@code unknown} if no match is found + */ + private static T fromNewStyleCode( + int revisionCode, int shift, int mask, + Supplier values, T unknown) { + final int value = (revisionCode >> shift) & mask; + for (var s : values.get()) { + if (s.getValue() == value) { + return s; + } + } + return unknown; + } + + /** + * Returns the enum constant of type {@code T} whose old-style revision code list + * contains the given revision code. + * + *

Each constant is checked in declaration order. The first constant whose + * {@link RevisionCodeEnum#getOldStyleRevisionCodes()} array contains the given + * revision code is returned.

+ * + * @param the enum type, which must implement {@link RevisionCodeEnum} + * @param revisionCode the raw 32-bit old-style revision code + * @param values a supplier of the enum constants to search (e.g. {@code Type::values}) + * @param unknown the sentinel value to return if no match is found + * @return the matching enum constant, or {@code unknown} if no match is found + */ + private static T fromOldStyleCode( + int revisionCode, Supplier values, T unknown) { + for (var s : values.get()) { + for (var r : s.getOldStyleRevisionCodes()) { + if (r == revisionCode) { + return s; } } - return UNKNOWN; } + return unknown; } } From e035a5598c9a92263facd5e18aa7297130fb0946 Mon Sep 17 00:00:00 2001 From: pjbosco Date: Mon, 29 Jun 2026 21:45:02 -0400 Subject: [PATCH 4/4] Implement fixes for the issues found by copilot: - Refactor RevisionCode.of(String) to do more initial checking on the given String. - Update MemorySize.getOldStyleRevisionCodes(), Type.getOldStyleRevisionCodes(), and Processor.getOldStyleRevisionCodes() methods to return a copy of the internal array of each respective enum rather than a reference to the internal array itself. - Fix formatting in BoardModel.getByBoardCode(). - Update RevisionCodeTest to more thoroughly test the RevisionCode class, especially around input validation for the of() method. Also added a test for the equals(), hashCode(), and toString() methods. --- .../pi4j/boardinfo/definition/BoardModel.java | 3 +-- .../boardinfo/definition/RevisionCode.java | 21 +++++++++------- .../definition/RevisionCodeTest.java | 25 ++++++++++++++++++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java index ea1436c05..4475dfbfd 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/BoardModel.java @@ -530,8 +530,7 @@ public static BoardModel getByBoardCode(String boardCode) throws Exception { try { RevisionCode revisionCode = RevisionCode.of(boardCode); BoardModel result = getByRevisionCode(revisionCode); - if (result != UNKNOWN) - { + if (result != UNKNOWN) { return result; } } catch (IllegalArgumentException e) { diff --git a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java index a9d9d13ba..94e93de2b 100644 --- a/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java +++ b/pi4j-core/src/main/java/com/pi4j/boardinfo/definition/RevisionCode.java @@ -25,6 +25,7 @@ * #L% */ +import java.util.Arrays; import java.util.Objects; import java.util.function.Supplier; @@ -174,13 +175,15 @@ public static RevisionCode of(String revisionCode) { if (revisionCode == null || revisionCode.isBlank()) { throw new IllegalArgumentException(("Revision code cannot be null or empty.")); } - final String clean = revisionCode.trim().toUpperCase().replace("0X", "").replace(" ", ""); - try { - int value = (int) Long.parseLong(clean, 16); - return of(value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Revision code is not valid hex: " + revisionCode, e); + String clean = revisionCode.trim(); + if (clean.startsWith("0X") || clean.startsWith("0x")) { + clean = clean.substring(2); } + + if (clean.isEmpty() || clean.length() > 8 || !clean.matches("[0-9a-fA-F]+")) { + throw new IllegalArgumentException("Revision code is not valid 32-bit hex value: " + revisionCode); + } + return of((int) Long.parseLong(clean, 16)); } /** @@ -287,7 +290,7 @@ public int getValue() { @Override public int[] getOldStyleRevisionCodes() { - return oldStyleRevisionCodes; + return Arrays.copyOf(oldStyleRevisionCodes, oldStyleRevisionCodes.length); } } @@ -344,7 +347,7 @@ public int getValue() { @Override public int[] getOldStyleRevisionCodes() { - return oldStyleRevisionCodes; + return Arrays.copyOf(oldStyleRevisionCodes, oldStyleRevisionCodes.length); } } @@ -420,7 +423,7 @@ public int getValue() { @Override public int[] getOldStyleRevisionCodes() { - return oldStyleRevisionCodes; + return Arrays.copyOf(oldStyleRevisionCodes, oldStyleRevisionCodes.length); } } diff --git a/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java b/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java index 785c8f67c..6733ad5e4 100644 --- a/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java +++ b/pi4j-core/src/test/java/com/pi4j/boardinfo/definition/RevisionCodeTest.java @@ -11,7 +11,30 @@ void getInvalidRevisionCode() { assertAll( () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of(null)), () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of("")), - () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of("invalid hex")) + () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of("0X")), + () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of("0xBadHex")), + () -> assertThrows(IllegalArgumentException.class, () -> RevisionCode.of("FFFFFFFFF")) + ); + } + + @Test + void testHashCodeEqualsToString() { + final Object obj = new Object(); + final String rc1Str = "e04180"; + final String rc2Str = "0XE04180"; + final int rcInt = 0xe04180; + final RevisionCode rc1 = RevisionCode.of(rc1Str); + final RevisionCode rc2 = RevisionCode.of(rc2Str); + assertAll( + () -> assertNotEquals(rc1Str, rc2Str), + () -> assertNotEquals(rc1, obj), + () -> assertEquals(rc1, rc2), + () -> assertEquals(rc1.hashCode(), rc2.hashCode()), + () -> assertEquals(rc1.toString(), rc2.toString()), + () -> assertNotEquals(rc1, RevisionCode.of(rcInt ^ (0x1 << 21))), // Flip a memory size bit. + () -> assertNotEquals(rc1, RevisionCode.of(rcInt ^ (0x1 << 5))), // Flip a type bit. + () -> assertNotEquals(rc1, RevisionCode.of(rcInt ^ (0x1 << 14))), // Flip a processor bit. + () -> assertNotEquals(rc1, RevisionCode.of(rcInt ^ 0x1)) // Flip a revision bit. ); }