├── go.mod ├── .gitignore ├── identifier ├── identifier.go └── utils.go ├── go.sum ├── LICENSE ├── test ├── test_utils.go └── test_utils_test.go ├── cmd └── sbcidentify │ └── main.go ├── sbcidentify.go ├── boardtype ├── boardtype.go ├── raspberrypi.go ├── nvidia │ ├── nvidia_test.go │ └── nvidia.go ├── raspberrypi │ ├── raspberrypi_test.go │ └── raspberrypi.go └── nvidia.go ├── logging.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rinzlerlabs/sbcidentify 2 | 3 | go 1.23.2 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /identifier/identifier.go: -------------------------------------------------------------------------------- 1 | package identifier 2 | 3 | import ( 4 | "log/slog" 5 | 6 | boardType "github.com/rinzlerlabs/sbcidentify/boardtype" 7 | ) 8 | 9 | type BoardIdentifier interface { 10 | Name() string 11 | GetBoardType() (boardType.SBC, error) 12 | } 13 | 14 | var identifiers []func(*slog.Logger) BoardIdentifier = make([]func(*slog.Logger) BoardIdentifier, 0) 15 | 16 | func RegisterBoardIdentifier(identifier func(*slog.Logger) BoardIdentifier) { 17 | identifiers = append(identifiers, identifier) 18 | } 19 | 20 | func BuildIdentifiers(logger *slog.Logger) []BoardIdentifier { 21 | ids := make([]BoardIdentifier, 0) 22 | for _, id := range identifiers { 23 | ids = append(ids, id(logger)) 24 | } 25 | return ids 26 | } 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pete Garafano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test_utils.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/rinzlerlabs/sbcidentify" 8 | "github.com/rinzlerlabs/sbcidentify/boardtype" 9 | ) 10 | 11 | type test struct { 12 | requiresRoot bool 13 | requiredBoardType *boardtype.SBC 14 | requiresSbc bool 15 | } 16 | 17 | func Test() *test { 18 | return &test{} 19 | } 20 | 21 | func (t *test) RequiresRoot() *test { 22 | t.requiresRoot = true 23 | return t 24 | } 25 | 26 | func (t *test) RequiresBoardType(boardType boardtype.SBC) *test { 27 | t.requiredBoardType = &boardType 28 | return t 29 | } 30 | 31 | func (t *test) RequiresSbc() *test { 32 | t.requiresSbc = true 33 | return t 34 | } 35 | 36 | func (t *test) ShouldSkip(test *testing.T) { 37 | if t.requiresSbc { 38 | boardType, err := sbcidentify.GetBoardType() 39 | if err != nil { 40 | test.Log(err) 41 | } 42 | if boardType == nil { 43 | test.Skip("Test requires physical SBC, not running on SBC") 44 | } 45 | } 46 | 47 | if t.requiredBoardType != nil && !sbcidentify.IsBoardType(*t.requiredBoardType) { 48 | test.Skipf("Test requires board type %v", (*t.requiredBoardType).GetPrettyName()) 49 | } 50 | if t.requiresRoot && !IsRoot() { 51 | test.Skip("Test requires root, not running as root") 52 | } 53 | } 54 | 55 | func IsRoot() bool { 56 | return os.Geteuid() == 0 57 | } 58 | -------------------------------------------------------------------------------- /test/test_utils_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "log/slog" 5 | "testing" 6 | 7 | "github.com/rinzlerlabs/sbcidentify" 8 | "github.com/rinzlerlabs/sbcidentify/boardtype" 9 | ) 10 | 11 | func TestShouldSkip(t *testing.T) { 12 | sbcidentify.SetLogLevel(slog.LevelDebug) 13 | tests := []struct { 14 | name string 15 | setup func() *test 16 | shouldSkip bool 17 | msg string 18 | }{ 19 | { 20 | name: "No requirements", 21 | setup: func() *test { 22 | return Test() 23 | }, 24 | shouldSkip: false, 25 | msg: "Test has no requirements", 26 | }, 27 | { 28 | name: "Requires SBC but no SBC present", 29 | setup: func() *test { 30 | return Test().RequiresSbc() 31 | }, 32 | shouldSkip: true, 33 | msg: "Test requires physical SBC", 34 | }, 35 | { 36 | name: "Requires specific board type but different board type present", 37 | setup: func() *test { 38 | return Test().RequiresBoardType(boardtype.RaspberryPi3B) 39 | }, 40 | shouldSkip: true, 41 | msg: "Test requires board type", 42 | }, 43 | { 44 | name: "Requires root but not running as root", 45 | setup: func() *test { 46 | return Test().RequiresRoot() 47 | }, 48 | shouldSkip: true, 49 | msg: "Test requires root", 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | tt.setup().ShouldSkip(t) 56 | if tt.shouldSkip { 57 | t.Errorf("%v should have skipped, but did not", tt.name) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cmd/sbcidentify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log/slog" 6 | "os" 7 | 8 | "fmt" 9 | 10 | "github.com/rinzlerlabs/sbcidentify" 11 | ) 12 | 13 | func main() { 14 | debug := flag.Bool("d", false, "Enable debug logging") 15 | output := flag.String("o", "StdOut", "Specify the log output, accept StdOut, StdErr, or a file path") 16 | flag.Parse() 17 | 18 | logLevel := new(slog.LevelVar) 19 | if *debug { 20 | logLevel.Set(slog.LevelDebug) 21 | } else { 22 | logLevel.Set(slog.LevelInfo) 23 | } 24 | 25 | handlerConfig := &sbcidentify.HandlerConfig{Level: logLevel} 26 | 27 | var logger *slog.Logger 28 | switch *output { 29 | case "StdOut": 30 | logger = slog.New(sbcidentify.NewLogHandler(os.Stdout, handlerConfig)) 31 | case "StdErr": 32 | logger = slog.New(sbcidentify.NewLogHandler(os.Stderr, handlerConfig)) 33 | default: 34 | file, err := os.OpenFile(*output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 35 | if err != nil { 36 | panic(err) 37 | } 38 | defer file.Close() 39 | logger = slog.New(sbcidentify.NewLogHandler(file, handlerConfig)) 40 | } 41 | 42 | sbcidentify.SetLogger(logger.With("source", "sbcidentify")) 43 | 44 | board, err := sbcidentify.GetBoardType() 45 | if err != nil { 46 | if errList, ok := err.(interface{ Unwrap() []error }); ok { 47 | // Access the slice of errors 48 | errs := errList.Unwrap() 49 | for _, e := range errs { 50 | fmt.Printf("Error: %v\n", e) 51 | } 52 | } else { 53 | fmt.Printf("Error: %v\n", err) 54 | } 55 | } else { 56 | fmt.Println(board.GetPrettyName()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sbcidentify.go: -------------------------------------------------------------------------------- 1 | package sbcidentify 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | "os" 7 | 8 | "github.com/rinzlerlabs/sbcidentify/boardtype" 9 | "github.com/rinzlerlabs/sbcidentify/identifier" 10 | 11 | _ "github.com/rinzlerlabs/sbcidentify/boardtype/nvidia" 12 | _ "github.com/rinzlerlabs/sbcidentify/boardtype/raspberrypi" 13 | ) 14 | 15 | var ( 16 | ErrUnknownBoard error = errors.New("unknown board") 17 | logLevel *slog.LevelVar = new(slog.LevelVar) 18 | logger *slog.Logger = slog.New(NewLogHandler(os.Stderr, &HandlerConfig{Level: logLevel})).With("source", "sbcidentify") 19 | ) 20 | 21 | func SetLogLevel(level slog.Level) { 22 | logLevel.Set(level) 23 | } 24 | 25 | func SetLogger(l *slog.Logger) { 26 | logger = l 27 | } 28 | 29 | func GetBoardType() (boardtype.SBC, error) { 30 | boardIdentifiers := identifier.BuildIdentifiers(logger) 31 | if len(boardIdentifiers) == 0 { 32 | panic("no board identifiers found") 33 | } 34 | var final error 35 | for _, identifier := range boardIdentifiers { 36 | board, err := identifier.GetBoardType() 37 | if err != nil { 38 | final = errors.Join(final, err) 39 | continue 40 | } 41 | return board, nil 42 | } 43 | return nil, final 44 | } 45 | 46 | func IsBoardType(boardType boardtype.SBC) bool { 47 | board, err := GetBoardType() 48 | if err != nil { 49 | return false 50 | } 51 | if board == nil { 52 | logger.Debug("board is nil, this is unexpected") 53 | return false 54 | } 55 | return board.IsBoardType(boardType) 56 | } 57 | 58 | func IsRaspberryPi() bool { 59 | return IsBoardType(boardtype.RaspberryPi) 60 | } 61 | 62 | func IsNvidia() bool { 63 | return IsBoardType(boardtype.NVIDIA) 64 | } 65 | 66 | func IsJetson() bool { 67 | return IsBoardType(boardtype.Jetson) 68 | } 69 | -------------------------------------------------------------------------------- /boardtype/boardtype.go: -------------------------------------------------------------------------------- 1 | package boardtype 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type BoardType struct { 8 | Manufacturer string 9 | Model string 10 | SubModel string 11 | RAM int 12 | BaseModel *BoardType 13 | } 14 | 15 | func (b BoardType) GetManufacturer() string { 16 | return b.Manufacturer 17 | } 18 | 19 | func (b BoardType) GetModel() string { 20 | return b.Model 21 | } 22 | 23 | func (b BoardType) GetSubModel() string { 24 | return b.SubModel 25 | } 26 | 27 | func (b BoardType) GetRAM() int { 28 | return b.RAM 29 | } 30 | 31 | func (b BoardType) GetBaseModel() *BoardType { 32 | return b.BaseModel 33 | } 34 | 35 | func (b BoardType) IsBoardType(boardType SBC) bool { 36 | return isBoardType(b, boardType) 37 | } 38 | 39 | func isBoardType(have SBC, want SBC) bool { 40 | if have.GetManufacturer() == want.GetManufacturer() && have.GetModel() == want.GetModel() && have.GetSubModel() == want.GetSubModel() && have.GetRAM() == want.GetRAM() { 41 | return true 42 | } 43 | 44 | if have.GetBaseModel() != nil { 45 | return isBoardType(have.GetBaseModel(), want) 46 | } 47 | 48 | return false 49 | } 50 | 51 | func (b BoardType) GetPrettyName() string { 52 | if b.RAM > 0 { 53 | ram := b.RAM 54 | if ram < 1024 { 55 | return b.Manufacturer + " " + b.Model + " " + b.SubModel + " " + strconv.Itoa(b.RAM) + "MB" 56 | } else { 57 | return b.Manufacturer + " " + b.Model + " " + b.SubModel + " " + strconv.Itoa(b.RAM/1024) + "GB" 58 | } 59 | 60 | } 61 | return b.Manufacturer + " " + b.Model + " " + b.SubModel 62 | } 63 | 64 | type SBC interface { 65 | GetManufacturer() string 66 | GetModel() string 67 | GetSubModel() string 68 | GetRAM() int 69 | GetPrettyName() string 70 | GetBaseModel() *BoardType 71 | IsBoardType(SBC) bool 72 | } 73 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | package sbcidentify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | ) 9 | 10 | type handler struct { 11 | writer *os.File 12 | handlerConfig *HandlerConfig 13 | source string 14 | group string 15 | args []string 16 | } 17 | 18 | type HandlerConfig struct { 19 | Level *slog.LevelVar 20 | } 21 | 22 | func NewLogHandler(w *os.File, handlerConfig *HandlerConfig) *handler { 23 | return &handler{ 24 | writer: w, 25 | handlerConfig: handlerConfig, 26 | } 27 | } 28 | 29 | func (h *handler) Handle(ctx context.Context, r slog.Record) error { 30 | args := make([]string, len(h.args)) 31 | copy(args, h.args) 32 | r.Attrs(func(a slog.Attr) bool { 33 | args = append(args, fmt.Sprintf("%v", a.Value)) 34 | return true 35 | }) 36 | 37 | msg := r.Message 38 | if len(args) == 0 { 39 | _, err := h.writer.WriteString(fmt.Sprintf("%s: %s\n", h.source, msg)) 40 | return err 41 | } 42 | argStr := fmt.Sprint(args) 43 | _, err := h.writer.WriteString(fmt.Sprintf("%s: %s %s\n", h.source, msg, argStr)) 44 | 45 | return err 46 | } 47 | 48 | func (h *handler) Enabled(ctx context.Context, level slog.Level) bool { 49 | return level >= h.handlerConfig.Level.Level() 50 | } 51 | 52 | func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler { 53 | handler := &handler{ 54 | writer: h.writer, 55 | handlerConfig: h.handlerConfig, 56 | args: make([]string, 0), 57 | } 58 | for _, a := range attrs { 59 | if a.Key == "source" { 60 | handler.source = fmt.Sprintf("%v", a.Value) 61 | } else { 62 | handler.args = append(handler.args, fmt.Sprintf("%v", a.Value)) 63 | } 64 | } 65 | return handler 66 | } 67 | 68 | func (h *handler) WithGroup(name string) slog.Handler { 69 | return &handler{ 70 | writer: h.writer, 71 | handlerConfig: h.handlerConfig, 72 | group: name, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /identifier/utils.go: -------------------------------------------------------------------------------- 1 | package identifier 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | procDeviceTreeModelFile = "/proc/device-tree/model" 13 | firmwareDeviceTreeModelFile = "/sys/firmware/devicetree/base/model" 14 | socIdFile = "/sys/devices/soc0/soc_id" 15 | ) 16 | 17 | var ( 18 | ErrCannotIdentifyBoard = errors.New("cannot identify board") 19 | ) 20 | 21 | func GetDeviceTreeBaseModel(logger *slog.Logger) (string, error) { 22 | if _, err := os.Stat(firmwareDeviceTreeModelFile); err != nil { 23 | logger.Debug("cannot read firmware device tree model file", slog.Any("error", err)) 24 | return "", ErrCannotIdentifyBoard 25 | } 26 | c, err := os.ReadFile(firmwareDeviceTreeModelFile) 27 | if err != nil { 28 | logger.Debug("cannot read firmware device tree model file", slog.Any("error", err)) 29 | return "", ErrCannotIdentifyBoard 30 | } 31 | str := strings.TrimSuffix(strings.TrimSpace(string(c)), "\x00") 32 | logger.Debug("firmware device tree model", slog.String("model", str)) 33 | return str, nil 34 | } 35 | 36 | func GetDeviceTreeModel(logger *slog.Logger) (string, error) { 37 | if _, err := os.Stat(procDeviceTreeModelFile); err != nil { 38 | logger.Debug("cannot read proc device tree model file", slog.Any("error", err)) 39 | return "", ErrCannotIdentifyBoard 40 | } 41 | c, err := os.ReadFile(procDeviceTreeModelFile) 42 | if err != nil { 43 | logger.Debug("cannot read proc device tree model file", slog.Any("error", err)) 44 | return "", ErrCannotIdentifyBoard 45 | } 46 | str := strings.TrimSpace(string(c)) 47 | logger.Debug("proc device tree model", slog.String("model", str)) 48 | return str, nil 49 | } 50 | 51 | func GetSoCId() (int, error) { 52 | c, err := os.ReadFile("/sys/devices/soc0/soc_id") 53 | if err != nil { 54 | return 0, err 55 | } 56 | str := string(c) 57 | return strconv.Atoi(str) 58 | } 59 | -------------------------------------------------------------------------------- /boardtype/raspberrypi.go: -------------------------------------------------------------------------------- 1 | package boardtype 2 | 3 | var ( 4 | RaspberryPi = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "", RAM: 0} 5 | RaspberryPi3 = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "", RAM: 0, BaseModel: &RaspberryPi} 6 | RaspberryPi3B = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "3B", RAM: 1024, BaseModel: &RaspberryPi3} 7 | RaspberryPi3APlus = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "3A+", RAM: 512, BaseModel: &RaspberryPi3B} 8 | RaspberryPi3BPlus = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "3B+", RAM: 1024, BaseModel: &RaspberryPi3B} 9 | RaspberryPi4 = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "", RAM: 0, BaseModel: &RaspberryPi} 10 | RaspberryPi4B = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "4B", RAM: 0, BaseModel: &RaspberryPi4} 11 | RaspberryPi4B1GB = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "4B", RAM: 1024, BaseModel: &RaspberryPi4B} 12 | RaspberryPi4B2GB = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "4B", RAM: 2048, BaseModel: &RaspberryPi4B} 13 | RaspberryPi4B4GB = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "4B", RAM: 4096, BaseModel: &RaspberryPi4B} 14 | RaspberryPi4B8GB = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "4B", RAM: 8192, BaseModel: &RaspberryPi4B} 15 | RaspberryPi4400 = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "4 400", RAM: 4096, BaseModel: &RaspberryPi4B} 16 | RaspberryPi5 = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "", RAM: 0, BaseModel: &RaspberryPi} 17 | RaspberryPi5B = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "5B", RAM: 0, BaseModel: &RaspberryPi5} 18 | RaspberryPi5B2GB = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "5B", RAM: 2048, BaseModel: &RaspberryPi5B} 19 | RaspberryPi5B4GB = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "5B", RAM: 4096, BaseModel: &RaspberryPi5B} 20 | RaspberryPi5B8GB = BoardType{Manufacturer: "Raspberry Pi", Model: "Raspberry Pi", SubModel: "5B", RAM: 8192, BaseModel: &RaspberryPi5B} 21 | ) 22 | -------------------------------------------------------------------------------- /boardtype/nvidia/nvidia_test.go: -------------------------------------------------------------------------------- 1 | package nvidia 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/rinzlerlabs/sbcidentify/boardtype" 14 | ) 15 | 16 | func TestParseModuleName(t *testing.T) { 17 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) 18 | f, e := getModuleNameFromDtsFilename(logger, "/dvs/git/dirty/git-master_linux/kernel/kernel-5.10/arch/arm64/boot/dts/../../../../../../hardware/nvidia/platform/t23x/p3768/kernel-dts/tegra234-p3767-0003-p3768-0000-a0.dts") 19 | require.NoError(t, e) 20 | require.Equal(t, "tegra234-p3767-0003-p3768-0000-a0", f) 21 | var detectedType boardtype.SBC 22 | f, e = getModuleNameFromDtsFilename(logger, "/nv-public/nv-platform/tegra234-p3768-0000+p3767-0000-nv-dsboard-ornx.dts") 23 | require.NoError(t, e) 24 | require.Equal(t, "tegra234-p3768-0000+p3767-0000-nv-board-orin", f) 25 | for _, m := range jetsonModulesByModelNumber { 26 | if strings.Contains(f, m.Model) { 27 | detectedType = m.Type 28 | } 29 | } 30 | require.Equal(t, boardtype.JetsonOrinNX16GB, detectedType) 31 | } 32 | 33 | func TestParseModelNameFromModuleName(t *testing.T) { 34 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) 35 | f, e := getModuleModelFromModuleName(logger, "tegra234-p3767-0003-p3768-0000-a0") 36 | assert.NoError(t, e) 37 | assert.Equal(t, "p3767-0003", f) 38 | } 39 | 40 | func TestIsBoardType(t *testing.T) { 41 | tests := []struct { 42 | Want boardtype.SBC 43 | Have boardtype.SBC 44 | expected bool 45 | }{ 46 | {boardtype.NVIDIA, boardtype.JetsonAGXOrin64GB, true}, 47 | {boardtype.Jetson, boardtype.JetsonAGXOrin64GB, true}, 48 | {boardtype.JetsonAGXOrin, boardtype.JetsonAGXOrin64GB, true}, 49 | {boardtype.JetsonAGXOrin64GB, boardtype.JetsonAGXOrin, false}, 50 | {boardtype.JetsonOrinNano, boardtype.JetsonOrinNano8GB, true}, 51 | {boardtype.JetsonOrinNano, boardtype.JetsonAGXOrin, false}, 52 | } 53 | for _, test := range tests { 54 | t.Run(fmt.Sprintf("Want_%v_Have_%v", test.Want.GetPrettyName(), test.Have.GetPrettyName()), func(t *testing.T) { 55 | if test.Have.IsBoardType(test.Want) != test.expected { 56 | t.Fatalf("IsBoardType() returned %v, expected %v", !test.expected, test.expected) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sbcidentity 2 | 3 | A simple library for identifying the SBC on which the library is running. See the [cli](cmd/sbcidentify/main.go) for a simple example of usage. This can be used as an import or install the CLI version. 4 | 5 | Currently supported boards: 6 | * Raspberry Pis 7 | * Various Jetson boards 8 | 9 | ## Package 10 | 11 | Install the package with 12 | ``` 13 | go get github.com/rinzlerlabs/sbcidentify@latest 14 | ``` 15 | 16 | To identify a board, simply import 17 | ``` 18 | "github.com/rinzlerlabs/sbcidentify" 19 | ``` 20 | Then to identify the board 21 | ``` 22 | board, err := sbcidentify.GetBoardType() 23 | ``` 24 | 25 | To check if a board is a specific type for hardware specific code, you can use `sbcidentify.IsBoardType()`. The boards definitions are structured such that they go from least to most restrictive. 26 | 27 | For example, if you have code that should _only_ run on Raspberry Pi boards, you can do 28 | ``` 29 | if sbcidentify.IsBoardType(raspberrypi.RaspberryPi) { 30 | // do Pi specifc stuff 31 | } 32 | ``` 33 | 34 | If you need to be more restrictive, say, only Raspberry Pi 5 boards 35 | ``` 36 | if sbcidentify.IsBoardType(raspberrypi.RaspberryPi5) { 37 | // do Pi 5 specifc stuff 38 | } else if sbcidentify.IsBoardType(raspberrypi.RaspberryPi4) { 39 | // do Pi 4 specific stuff 40 | } else { 41 | // fallback 42 | } 43 | ``` 44 | 45 | The device tree heirarchies look like: 46 | ``` 47 | Raspberry Pi 48 | ├── 3 49 | │ ├── 3B 50 | │ │ └── 3BPlus 51 | │ └── 3APlus 52 | ├── 4 53 | │ └── 4B 54 | │ ├── 4B 1GB 55 | │ ├── 4B 2GB 56 | │ ├── 4B 4GB 57 | │ ├── 4B 8GB 58 | │ └── 4400 59 | └── 5 60 | └── 5B 61 | ├── 5B 2GB 62 | ├── 5B 4GB 63 | └── 5B 8GB 64 | 65 | NVIDIA 66 | ├── Jetson 67 | │ ├── Nano 68 | │ │ ├── Nano 2GB 69 | │ │ └── Nano 4GB 70 | │ ├── TX1 71 | │ ├── TX2 72 | │ │ ├── TX2 4GB 73 | │ │ ├── TX2 8GB 74 | │ │ ├── TX2 NX 75 | │ │ └── TX2i 76 | │ ├── Xavier 77 | │ │ ├── Xavier NX 78 | │ │ └── AGX Xavier 79 | │ └── Orin 80 | │ ├── Orin NX 81 | │ ├── Orin Nano 82 | │ └── AGX Orin 83 | ├── Clara AGX 84 | └── Shield TV 85 | ``` 86 | 87 | ## CLI 88 | 89 | To install the CLI version, simply run 90 | ``` 91 | go install github.com/rinzlerlabs/sbcidentify/cmd/sbcidentify@latest 92 | ``` 93 | 94 | Usage 95 | ``` 96 | Usage of sbcidentify: 97 | -d Enable debug logging 98 | -o string 99 | Specify the log output, accept StdOut, StdErr, or a file path (default "StdOut") 100 | ``` 101 | -------------------------------------------------------------------------------- /boardtype/raspberrypi/raspberrypi_test.go: -------------------------------------------------------------------------------- 1 | package raspberrypi 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "os/exec" 8 | "testing" 9 | 10 | "github.com/rinzlerlabs/sbcidentify/boardtype" 11 | "github.com/rinzlerlabs/sbcidentify/identifier" 12 | ) 13 | 14 | func setup(t *testing.T) (*slog.Logger, identifier.BoardIdentifier) { 15 | t.Helper() 16 | execLookPath = exec.LookPath 17 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) 18 | id := NewRaspberryPiIdentifier(logger) 19 | board, err := id.GetBoardType() 20 | if err != nil && err != identifier.ErrCannotIdentifyBoard { 21 | t.Fatalf("GetBoardType() failed: %v", err) 22 | } 23 | if board.GetManufacturer() != "Raspberry Pi" { 24 | t.Skip("Not a Raspberry Pi") 25 | } 26 | return logger, id 27 | } 28 | 29 | func TestGetInstalledRAM(t *testing.T) { 30 | logger, _ := setup(t) 31 | ram, err := getInstalledRAM(logger) 32 | if err != nil { 33 | t.Fatalf("getInstalledRAM() failed: %v", err) 34 | } 35 | t.Logf("RAM: %dMB", ram) 36 | 37 | execLookPath = func(string) (string, error) { 38 | return "", exec.ErrNotFound 39 | } 40 | _, err = getInstalledRAM(logger) 41 | if err != ErrVcgencmdNotFound { 42 | t.Fatalf("getInstalledRAM() returned error %v, expected %v", err, ErrVcgencmdNotFound) 43 | } 44 | } 45 | 46 | func TestParseVcgencmdMemoryOutput(t *testing.T) { 47 | logger, _ := setup(t) 48 | 49 | tests := []struct { 50 | input string 51 | output int 52 | err error 53 | }{ 54 | {"total_mem", 0, ErrInvalidMeminfo}, 55 | {"total_mem=", 0, ErrInvalidMeminfo}, 56 | {"total_mem=foo", 0, ErrInvalidMeminfo}, 57 | {"", 0, ErrInvalidMeminfo}, 58 | {"total_mem=2048MB", 0, ErrInvalidMeminfo}, 59 | {"total_mem=1024", 1024, nil}, 60 | {"total_mem=1024\n", 1024, nil}, 61 | } 62 | for _, test := range tests { 63 | t.Run(test.input, func(t *testing.T) { 64 | ram, err := parseVcgencmdMemoryOutput(logger, test.input) 65 | if err != test.err { 66 | t.Fatalf("parseVcgencmdMemoryOutput() returned error %v, expected %v", err, test.err) 67 | } 68 | if ram != test.output { 69 | t.Fatalf("parseVcgencmdMemoryOutput() returned %d, expected %d", ram, test.output) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func TestIsBoardType(t *testing.T) { 76 | tests := []struct { 77 | Want boardtype.SBC 78 | Have boardtype.SBC 79 | expected bool 80 | }{ 81 | {boardtype.RaspberryPi, boardtype.RaspberryPi4B8GB, true}, 82 | {boardtype.RaspberryPi, boardtype.RaspberryPi5B8GB, true}, 83 | {boardtype.RaspberryPi3B, boardtype.RaspberryPi4B, false}, 84 | {boardtype.RaspberryPi3B, boardtype.RaspberryPi3BPlus, true}, 85 | {boardtype.RaspberryPi3BPlus, boardtype.RaspberryPi3B, false}, 86 | {boardtype.RaspberryPi4, boardtype.RaspberryPi4B8GB, true}, 87 | {boardtype.RaspberryPi4B, boardtype.RaspberryPi4B8GB, true}, 88 | {boardtype.RaspberryPi4B8GB, boardtype.RaspberryPi4B, false}, 89 | {boardtype.RaspberryPi4B4GB, boardtype.RaspberryPi4B8GB, false}, 90 | {boardtype.RaspberryPi5B, boardtype.RaspberryPi5B4GB, true}, 91 | {boardtype.RaspberryPi5B4GB, boardtype.RaspberryPi5B, false}, 92 | } 93 | 94 | for _, test := range tests { 95 | t.Run(fmt.Sprintf("Want_%v_Have_%v", test.Want.GetPrettyName(), test.Have.GetPrettyName()), func(t *testing.T) { 96 | if test.Have.IsBoardType(test.Want) != test.expected { 97 | t.Fatalf("IsBoardType() returned %v, expected %v", !test.expected, test.expected) 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /boardtype/raspberrypi/raspberrypi.go: -------------------------------------------------------------------------------- 1 | package raspberrypi 2 | 3 | import ( 4 | "errors" 5 | "os/exec" 6 | "strconv" 7 | "strings" 8 | 9 | "log/slog" 10 | 11 | "github.com/rinzlerlabs/sbcidentify/boardtype" 12 | "github.com/rinzlerlabs/sbcidentify/identifier" 13 | ) 14 | 15 | func init() { 16 | identifier.RegisterBoardIdentifier(NewRaspberryPiIdentifier) 17 | } 18 | 19 | var ( 20 | ErrVcgencmdNotFound = errors.New("vcgencmd not found") 21 | ErrInvalidMeminfo = errors.New("invalid meminfo") 22 | ErrCannotIdentifyBoard = errors.New("cannot identify Raspberry Pi board") 23 | execLookPath = exec.LookPath 24 | ) 25 | 26 | type raspberryPi struct { 27 | Model string 28 | Memory int 29 | Type boardtype.SBC 30 | Fallback boardtype.SBC 31 | } 32 | 33 | var raspberryPiModels = []raspberryPi{ 34 | {"Raspberry Pi 3 Model B", 1024, boardtype.RaspberryPi3B, boardtype.RaspberryPi3B}, 35 | {"Raspberry Pi 3 Model A", 512, boardtype.RaspberryPi3APlus, boardtype.RaspberryPi3APlus}, 36 | {"Raspberry Pi 3 Model B", 1024, boardtype.RaspberryPi3BPlus, boardtype.RaspberryPi3BPlus}, 37 | {"Raspberry Pi 4 Model B", 1024, boardtype.RaspberryPi4B1GB, boardtype.RaspberryPi4B}, 38 | {"Raspberry Pi 4 Model B", 2048, boardtype.RaspberryPi4B2GB, boardtype.RaspberryPi4B}, 39 | {"Raspberry Pi 4 Model B", 4096, boardtype.RaspberryPi4B4GB, boardtype.RaspberryPi4B}, 40 | {"Raspberry Pi 4 Model B", 8192, boardtype.RaspberryPi4B8GB, boardtype.RaspberryPi4B}, 41 | {"Raspberry Pi 400", 4096, boardtype.RaspberryPi4400, boardtype.RaspberryPi4400}, 42 | {"Raspberry Pi 5 Model B", 2048, boardtype.RaspberryPi5B2GB, boardtype.RaspberryPi5B}, 43 | {"Raspberry Pi 5 Model B", 4096, boardtype.RaspberryPi5B4GB, boardtype.RaspberryPi5B}, 44 | {"Raspberry Pi 5 Model B", 8192, boardtype.RaspberryPi5B8GB, boardtype.RaspberryPi5B}, 45 | } 46 | 47 | func NewRaspberryPiIdentifier(logger *slog.Logger) identifier.BoardIdentifier { 48 | logger.Debug("initializing Raspberry Pi identifier") 49 | newLogger := logger.With(slog.String("source", "RaspberryPiIdentifier")) 50 | return &raspberryPiIdentifier{logger: newLogger} 51 | } 52 | 53 | type raspberryPiIdentifier struct { 54 | logger *slog.Logger 55 | } 56 | 57 | func (r raspberryPiIdentifier) Name() string { 58 | return "Raspberry Pi Identifier" 59 | } 60 | 61 | func (r raspberryPiIdentifier) GetBoardType() (boardtype.SBC, error) { 62 | r.logger.Debug("getting board type") 63 | dtbm, err := identifier.GetDeviceTreeBaseModel(r.logger) 64 | if err == identifier.ErrCannotIdentifyBoard { 65 | dtbm, err = identifier.GetDeviceTreeModel(r.logger) 66 | if err == identifier.ErrCannotIdentifyBoard { 67 | return nil, ErrCannotIdentifyBoard 68 | } else if err != nil { 69 | return nil, err 70 | } 71 | } else if err != nil { 72 | return nil, err 73 | } 74 | r.logger.Debug("device tree model", slog.String("model", dtbm)) 75 | subModels := make([]raspberryPi, 0) 76 | for _, m := range raspberryPiModels { 77 | if strings.Contains(dtbm, m.Model) { 78 | subModels = append(subModels, m) 79 | } 80 | } 81 | if len(subModels) == 0 { 82 | return nil, ErrCannotIdentifyBoard 83 | } 84 | ramMb, err := getInstalledRAM(r.logger) 85 | if err == ErrVcgencmdNotFound { 86 | r.logger.Debug("vcgencmd not found, using fallback", slog.String("model", dtbm), slog.Int("ram", ramMb), slog.Any("fallback", subModels[0].Fallback)) 87 | return subModels[0].Fallback, nil 88 | } else if err != nil { 89 | return nil, err 90 | } 91 | for _, m := range subModels { 92 | if m.Memory == ramMb { 93 | return m.Type, nil 94 | } 95 | } 96 | r.logger.Debug("no matching model found, using fallback", slog.String("model", dtbm), slog.Int("ram", ramMb), slog.Int("subModels", len(subModels)), slog.Any("subModels", subModels), slog.Any("fallback", subModels[0].Fallback)) 97 | return subModels[0].Fallback, nil 98 | } 99 | 100 | func getInstalledRAM(logger *slog.Logger) (int, error) { 101 | if _, err := execLookPath("vcgencmd"); err != nil { 102 | logger.Debug("vcgencmd not found", slog.Any("error", err)) 103 | return 0, ErrVcgencmdNotFound 104 | } 105 | out, err := exec.Command("vcgencmd", "get_config", "total_mem").Output() 106 | if err != nil { 107 | return 0, err 108 | } 109 | output := strings.TrimSpace(string(out)) 110 | return parseVcgencmdMemoryOutput(logger, output) 111 | } 112 | 113 | func parseVcgencmdMemoryOutput(logger *slog.Logger, output string) (int, error) { 114 | logger.Debug("vcgencmd output", slog.String("output", output)) 115 | parts := strings.Split(output, "=") 116 | if len(parts) != 2 { 117 | return 0, ErrInvalidMeminfo 118 | } 119 | installedRam, err := strconv.Atoi(strings.TrimSpace(parts[1])) 120 | if err != nil { 121 | logger.Debug("Failed to parse RAM", slog.String("output", output), slog.Any("error", err)) 122 | return 0, ErrInvalidMeminfo 123 | } 124 | logger.Debug("Parsed RAM", slog.Int("total_mem", installedRam)) 125 | return installedRam, nil 126 | } 127 | -------------------------------------------------------------------------------- /boardtype/nvidia.go: -------------------------------------------------------------------------------- 1 | package boardtype 2 | 3 | var ( 4 | NVIDIA = BoardType{Manufacturer: "NVIDIA", Model: "", SubModel: "", RAM: 0} 5 | Jetson = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "", RAM: 0, BaseModel: &NVIDIA} 6 | JetsonOrin = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin", RAM: 0, BaseModel: &Jetson} 7 | JetsonXavier = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Xavier", RAM: 0, BaseModel: &Jetson} 8 | JetsonOrinNX = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin NX", RAM: 0, BaseModel: &JetsonOrin} 9 | JetsonOrinNX16GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin NX", RAM: 16384, BaseModel: &JetsonOrinNX} 10 | JetsonOrinNX8GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin NX", RAM: 8192, BaseModel: &JetsonOrinNX} 11 | JetsonOrinNano = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin Nano", RAM: 0, BaseModel: &JetsonOrin} 12 | JetsonOrinNano8GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin Nano", RAM: 8192, BaseModel: &JetsonOrinNano} 13 | JetsonOrinNano4GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin Nano", RAM: 4096, BaseModel: &JetsonOrinNano} 14 | JetsonOrinNanoDeveloperKit = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Orin Nano Developer Kit", RAM: 8192, BaseModel: &JetsonOrinNano} 15 | JetsonAGXOrin = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Orin", RAM: 0, BaseModel: &Jetson} 16 | JetsonAGXOrin32GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Orin", RAM: 32768, BaseModel: &JetsonAGXOrin} 17 | JetsonAGXOrin64GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Orin", RAM: 65536, BaseModel: &JetsonAGXOrin} 18 | JetsonXavierNX = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Xavier NX", RAM: 0, BaseModel: &Jetson} 19 | JetsonXavierNXDeveloperKit = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Xavier NX Developer Kit", RAM: 0, BaseModel: &JetsonXavierNX} 20 | JetsonXavierNX8GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Xavier NX", RAM: 8192, BaseModel: &JetsonXavierNX} 21 | JetsonXavierNX16GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Xavier NX", RAM: 16384, BaseModel: &JetsonXavierNX} 22 | JetsonAGXXavier = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Xavier", RAM: 0, BaseModel: &Jetson} 23 | JetsonAGXXavier8GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Xavier", RAM: 8192, BaseModel: &JetsonAGXXavier} 24 | JetsonAGXXavier16GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Xavier", RAM: 16384, BaseModel: &JetsonAGXXavier} 25 | JetsonAGXXavier32GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Xavier", RAM: 32768, BaseModel: &JetsonAGXXavier} 26 | JetsonAGXXavier64GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Xavier", RAM: 65536, BaseModel: &JetsonAGXXavier} 27 | JetsonAGXXavierIndustrial32GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "AGX Xavier Industrial", RAM: 32768, BaseModel: &JetsonAGXXavier} 28 | JetsonNano = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Nano", RAM: 0, BaseModel: &Jetson} 29 | JetsonNanoDeveloperKit = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Nano Developer Kit", RAM: 0, BaseModel: &JetsonNano} 30 | JetsonNano2GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Nano", RAM: 2048, BaseModel: &JetsonNano} 31 | JetsonNano16GbEMMC = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Nano", RAM: 0, BaseModel: &JetsonNano} 32 | JetsonNano4GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "Nano", RAM: 4096, BaseModel: &JetsonNano} 33 | JetsonTX2NX = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "TX2 NX", RAM: 0, BaseModel: &Jetson} 34 | JetsonTX24GB = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "TX2", RAM: 4096, BaseModel: &JetsonTX2} 35 | JetsonTX2i = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "TX2i", RAM: 0, BaseModel: &JetsonTX2} 36 | JetsonTX2 = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "TX2", RAM: 0, BaseModel: &Jetson} 37 | JetsonTX1 = BoardType{Manufacturer: "NVIDIA", Model: "Jetson", SubModel: "TX1", RAM: 0, BaseModel: &Jetson} 38 | ClaraAGX = BoardType{Manufacturer: "NVIDIA", Model: "Clara", SubModel: "AGX", RAM: 0, BaseModel: &NVIDIA} 39 | ShieldTV = BoardType{Manufacturer: "NVIDIA", Model: "Shield", SubModel: "TV", RAM: 0, BaseModel: &NVIDIA} 40 | ) 41 | -------------------------------------------------------------------------------- /boardtype/nvidia/nvidia.go: -------------------------------------------------------------------------------- 1 | package nvidia 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/rinzlerlabs/sbcidentify/boardtype" 11 | "github.com/rinzlerlabs/sbcidentify/identifier" 12 | ) 13 | 14 | func init() { 15 | identifier.RegisterBoardIdentifier(NewNvidiaIdentifier) 16 | } 17 | 18 | const ( 19 | dtsFileName = "/proc/device-tree/nvidia,dtsfilename" 20 | ) 21 | 22 | // NVIDIA Jetson AGX Orin Developer Kit 23 | type jetson struct { 24 | Model string 25 | Type boardtype.SBC 26 | } 27 | 28 | var ( 29 | ErrDtsFileDoesNotExist = errors.New("DTS file does not exist") 30 | ErrCannotIdentifyBoard = errors.New("cannot identify NVIDIA board") 31 | ) 32 | 33 | var jetsonModulesByModelNumber = []jetson{ 34 | {"p3767-0000", boardtype.JetsonOrinNX16GB}, 35 | {"p3767-0001", boardtype.JetsonOrinNX8GB}, 36 | 37 | {"p3767-0003", boardtype.JetsonOrinNano8GB}, 38 | {"p3767-0004", boardtype.JetsonOrinNano4GB}, 39 | {"p3767-0005", boardtype.JetsonOrinNanoDeveloperKit}, 40 | 41 | {"p3701-0000", boardtype.JetsonAGXOrin}, 42 | {"p3701-0004", boardtype.JetsonAGXOrin32GB}, 43 | {"p3701-0005", boardtype.JetsonAGXOrin64GB}, 44 | 45 | {"p3668-0000", boardtype.JetsonXavierNXDeveloperKit}, 46 | {"p3668-0001", boardtype.JetsonXavierNX8GB}, 47 | {"p3668-0003", boardtype.JetsonXavierNX16GB}, 48 | 49 | {"p2888-0001", boardtype.JetsonAGXXavier16GB}, 50 | {"p2888-0003", boardtype.JetsonAGXXavier32GB}, 51 | {"p2888-0004", boardtype.JetsonAGXXavier32GB}, 52 | {"p2888-0005", boardtype.JetsonAGXXavier64GB}, 53 | {"p2888-0006", boardtype.JetsonAGXXavier8GB}, 54 | {"p2888-0008", boardtype.JetsonAGXXavierIndustrial32GB}, 55 | {"p2972-0000", boardtype.JetsonAGXXavier}, 56 | 57 | {"p2771-0000", boardtype.JetsonTX2}, 58 | 59 | {"p3448-0000", boardtype.JetsonNano4GB}, 60 | {"p3448-0002", boardtype.JetsonNano16GbEMMC}, 61 | {"p3448-0003", boardtype.JetsonNano2GB}, 62 | {"p3450-0000", boardtype.JetsonNanoDeveloperKit}, 63 | 64 | {"p3636-0001", boardtype.JetsonTX2NX}, 65 | {"p3509-0000", boardtype.JetsonTX2NX}, 66 | 67 | {"p3489-0888", boardtype.JetsonTX24GB}, 68 | {"p3489-0000", boardtype.JetsonTX2i}, 69 | {"p3310-1000", boardtype.JetsonTX2}, 70 | 71 | {"p2180-1000", boardtype.JetsonTX1}, 72 | {"p2371-2180", boardtype.JetsonTX1}, 73 | 74 | {"p2894-0050", boardtype.ShieldTV}, 75 | 76 | {"p3904-0000", boardtype.ClaraAGX}, 77 | } 78 | 79 | var jetsonModulesByDeviceTreeBaseModel = []jetson{ 80 | {"NVIDIA Jetson Orin NX Engineering Reference Developer Kit", boardtype.JetsonOrinNX16GB}, 81 | {"NVIDIA Jetson Orin Nano Developer Kit", boardtype.JetsonOrinNanoDeveloperKit}, 82 | {"NVIDIA Jetson TX2 Developer Kit", boardtype.JetsonTX2}, 83 | {"NVIDIA Jetson TX2", boardtype.JetsonTX2}, 84 | {"NVIDIA Jetson TX2 NX Developer Kit", boardtype.JetsonTX2NX}, 85 | {"NVIDIA Jetson AGX Xavier", boardtype.JetsonAGXXavier}, 86 | {"NVIDIA Jetson AGX Xavier Developer Kit", boardtype.JetsonAGXXavier}, 87 | {"NVIDIA Jetson Xavier NX Developer Kit (SD-card)", boardtype.JetsonXavierNXDeveloperKit}, 88 | {"NVIDIA Jetson Xavier NX Developer Kit (eMMC)", boardtype.JetsonXavierNXDeveloperKit}, 89 | {"NVIDIA Jetson Xavier NX (SD-card)", boardtype.JetsonXavierNXDeveloperKit}, 90 | {"NVIDIA Jetson Xavier NX (eMMC)", boardtype.JetsonXavierNX8GB}, 91 | {"NVIDIA Jetson TX1", boardtype.JetsonTX1}, 92 | {"NVIDIA Jetson TX1 Developer Kit", boardtype.JetsonTX1}, 93 | {"NVIDIA Shield TV", boardtype.ShieldTV}, 94 | {"NVIDIA Jetson Nano Developer Kit", boardtype.JetsonNanoDeveloperKit}, 95 | {"NVIDIA Jetson AGX Orin Developer Kit", boardtype.JetsonAGXOrin}, 96 | {"NVIDIA Jetson AGX Orin", boardtype.JetsonAGXOrin}, 97 | } 98 | 99 | type jetsonIdentifier struct { 100 | logger *slog.Logger 101 | } 102 | 103 | func NewNvidiaIdentifier(logger *slog.Logger) identifier.BoardIdentifier { 104 | logger.Debug("initializing Jetson identifier") 105 | newLogger := logger.With(slog.String("source", "NVIDIA")) 106 | return jetsonIdentifier{ 107 | logger: newLogger, 108 | } 109 | } 110 | 111 | func (r jetsonIdentifier) Name() string { 112 | return "Jetson Identifier" 113 | } 114 | 115 | func (r jetsonIdentifier) GetBoardType() (boardtype.SBC, error) { 116 | boardType, err := getBoardTypeFromModuleModel(r.logger) 117 | if err == ErrDtsFileDoesNotExist { 118 | r.logger.Debug("DTS file does not exist, falling back to device tree base model") 119 | boardType, err = getBoardTypeByDeviceTreeBaseModel(r.logger) 120 | if err == identifier.ErrCannotIdentifyBoard { 121 | r.logger.Debug("unknown board") 122 | return nil, ErrCannotIdentifyBoard 123 | } else if err != nil { 124 | r.logger.Debug("error getting board type", slog.Any("error", err)) 125 | return nil, err 126 | } else { 127 | r.logger.Debug("board type", slog.String("type", string(boardType.GetPrettyName()))) 128 | return boardType, nil 129 | } 130 | } else if err == identifier.ErrCannotIdentifyBoard { 131 | r.logger.Debug("unknown board, falling back to device tree base model") 132 | boardType, err = getBoardTypeByDeviceTreeBaseModel(r.logger) 133 | if err == identifier.ErrCannotIdentifyBoard { 134 | r.logger.Debug("unknown board") 135 | return nil, ErrCannotIdentifyBoard 136 | } else if err != nil { 137 | r.logger.Debug("error getting board type", slog.Any("error", err)) 138 | return nil, err 139 | } else { 140 | r.logger.Debug("board type", slog.String("type", string(boardType.GetPrettyName()))) 141 | return boardType, nil 142 | } 143 | } else if err != nil { 144 | r.logger.Debug("error getting board type", slog.Any("error", err)) 145 | return nil, err 146 | } else { 147 | r.logger.Debug("board type", slog.String("type", string(boardType.GetPrettyName()))) 148 | return boardType, nil 149 | } 150 | } 151 | 152 | func getBoardTypeFromModuleModel(logger *slog.Logger) (boardtype.SBC, error) { 153 | dtsFilename, err := getDtsFile(logger) 154 | if err != nil { 155 | return nil, err 156 | } 157 | moduleName, err := getModuleNameFromDtsFilename(logger, dtsFilename) 158 | if err != nil { 159 | return nil, err 160 | } 161 | moduleModel, err := getModuleModelFromModuleName(logger, moduleName) 162 | if err != nil { 163 | return nil, err 164 | } 165 | for _, m := range jetsonModulesByModelNumber { 166 | if strings.Contains(moduleModel, m.Model) { 167 | return m.Type, nil 168 | } 169 | } 170 | return nil, identifier.ErrCannotIdentifyBoard 171 | } 172 | 173 | func getBoardTypeByDeviceTreeBaseModel(logger *slog.Logger) (boardtype.SBC, error) { 174 | dtbm, err := identifier.GetDeviceTreeBaseModel(logger) 175 | if err != nil { 176 | return nil, err 177 | } 178 | for _, m := range jetsonModulesByDeviceTreeBaseModel { 179 | if strings.Contains(dtbm, m.Model) { 180 | return m.Type, nil 181 | } 182 | } 183 | logger.Debug("device tree base model does not match any boards", slog.String("model", dtbm)) 184 | return nil, ErrCannotIdentifyBoard 185 | } 186 | 187 | func getDtsFile(logger *slog.Logger) (string, error) { 188 | if _, err := os.Stat(dtsFileName); os.IsNotExist(err) { 189 | logger.Debug("DTS file does not exist", slog.Any("error", err)) 190 | return "", ErrDtsFileDoesNotExist 191 | } 192 | s, e := os.ReadFile(dtsFileName) 193 | if e != nil { 194 | logger.Debug("cannot read DTS file", slog.Any("error", e)) 195 | return "", e 196 | } 197 | str := string(s) 198 | logger.Debug("DTS file", slog.String("filename", str)) 199 | return str, nil 200 | } 201 | 202 | func getModuleNameFromDtsFilename(logger *slog.Logger, dtsFilename string) (string, error) { 203 | filename := filepath.Base(dtsFilename) 204 | ret := strings.TrimSuffix(filename, filepath.Ext(filename)) 205 | logger.Debug("module name", slog.String("name", ret)) 206 | return ret, nil 207 | } 208 | 209 | func getModuleModelFromModuleName(logger *slog.Logger, moduleName string) (string, error) { 210 | parts := strings.Split(moduleName, "-") 211 | if len(parts) >= 4 { 212 | ret := strings.Join(parts[1:3], "-") 213 | logger.Debug("module model", slog.String("model", ret)) 214 | return ret, nil 215 | } 216 | logger.Debug("error parsing module name", slog.String("name", moduleName)) 217 | return "", identifier.ErrCannotIdentifyBoard 218 | } 219 | --------------------------------------------------------------------------------