From 97d6a659cea8de21c21dea97d3fbef43d4532f40 Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Wed, 9 Aug 2023 20:30:01 -0400 Subject: [PATCH 1/2] jupiter-hw-support: Provide access to the wrapped d20bootloader script This can be used to gather knowledge about a specific unit. --- pkgs/jupiter-hw-support/firmware.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkgs/jupiter-hw-support/firmware.nix b/pkgs/jupiter-hw-support/firmware.nix index d7e8beef..a2efbb49 100644 --- a/pkgs/jupiter-hw-support/firmware.nix +++ b/pkgs/jupiter-hw-support/firmware.nix @@ -103,6 +103,12 @@ stdenv.mkDerivation { wrapProgram $out/bin/jupiter-controller-update \ --prefix PATH : ${lib.makeBinPath [ jq pythonEnv ]} + mkdir -p $out/libexec + makeShellWrapper \ + $out/share/jupiter_controller_fw_updater/d20bootloader.py \ + $out/libexec/d20bootloader \ + --prefix PATH : ${lib.makeBinPath [ jq pythonEnv ]} + pushd $out/share/jupiter_bios_updater # Upstream comment: # > Remove gtk2 binary and respective build/start script - unused From fa603fc50efb12eec957bff7eff42b6a1c9c178d Mon Sep 17 00:00:00 2001 From: Samuel Dionne-Riel Date: Wed, 9 Aug 2023 21:46:44 -0400 Subject: [PATCH 2/2] jovian-hardware-survey: init --- overlay.nix | 2 + pkgs/jovian-hardware-survey/default.nix | 17 + .../jovian-hardware-survey.rb | 363 ++++++++++++++++++ 3 files changed, 382 insertions(+) create mode 100644 pkgs/jovian-hardware-survey/default.nix create mode 100755 pkgs/jovian-hardware-survey/jovian-hardware-survey.rb diff --git a/overlay.nix b/overlay.nix index 8a810f74..d2be1f62 100644 --- a/overlay.nix +++ b/overlay.nix @@ -47,6 +47,8 @@ rec { jovian-greeter = super.callPackage ./pkgs/jovian-greeter { }; + jovian-hardware-survey = super.callPackage ./pkgs/jovian-hardware-survey { }; + steamPackages = super.steamPackages.overrideScope (scopeFinal: scopeSuper: { steam = final.callPackage ./pkgs/steam-jupiter/unwrapped.nix { steam-original = scopeSuper.steam; diff --git a/pkgs/jovian-hardware-survey/default.nix b/pkgs/jovian-hardware-survey/default.nix new file mode 100644 index 00000000..fe088fe4 --- /dev/null +++ b/pkgs/jovian-hardware-survey/default.nix @@ -0,0 +1,17 @@ +{ lib +, runCommand +, ruby +, steamdeck-firmware +, dmidecode +}: + +runCommand "jovian-hardware-survey" { } '' + mkdir -p $out/bin + ( + echo "#!${ruby}/bin/ruby" + echo 'ENV["PATH"] = "${lib.makeBinPath [ steamdeck-firmware dmidecode ]}:#{ENV["PATH"]}"' + echo 'D20BOOTLOADER = "${steamdeck-firmware}/libexec/d20bootloader"' + cat ${./jovian-hardware-survey.rb} + ) > $out/bin/jovian-hardware-survey + chmod +x $out/bin/jovian-hardware-survey +'' diff --git a/pkgs/jovian-hardware-survey/jovian-hardware-survey.rb b/pkgs/jovian-hardware-survey/jovian-hardware-survey.rb new file mode 100755 index 00000000..e0a22cc2 --- /dev/null +++ b/pkgs/jovian-hardware-survey/jovian-hardware-survey.rb @@ -0,0 +1,363 @@ +#!/usr/bin/env ruby + +require "json" +require "shellwords" + +D20BOOTLOADER ||= "/usr/share/jupiter_controller_fw_updater/d20bootloader.py" + +# Handles joining/escaping a command, and raises on status != 0 +# Additionally prints the command to stderr. +def run(*args, stdout:, silent: false, stderr: false, ignore_fail: false) + cmd = args.shelljoin + if !stdout and stderr then + raise "To use stderr, for now you need to use stdout." + end + stderr = + if stderr then + " 2>&1" + else + "" + end + $stderr.puts " $ #{cmd}" unless silent + ret = + if stdout then + `#{cmd}#{stderr}` + else + system("#{cmd}") + nil + end + unless $?.success? || ignore_fail + raise "Command “#{cmd}” unexpectedly failed..." + end + ret +end + +module DMI + TYPES = { + 0 => "BIOS", + 1 => "System", + 2 => "Base Board", + 3 => "Chassis", + 4 => "Processor", + 5 => "Memory Controller", + 6 => "Memory Module", + 7 => "Cache", + 8 => "Port Connector", + 9 => "System Slots", + 10 => "On Board Devices", + 11 => "OEM Strings", + 12 => "System Configuration Options", + 13 => "BIOS Language", + 14 => "Group Associations", + 15 => "System Event Log", + 16 => "Physical Memory Array", + 17 => "Memory Device", + 18 => "32-bit Memory Error", + 19 => "Memory Array Mapped Address", + 20 => "Memory Device Mapped Address", + 21 => "Built-in Pointing Device", + 22 => "Portable Battery", + 23 => "System Reset", + 24 => "Hardware Security", + 25 => "System Power Controls", + 26 => "Voltage Probe", + 27 => "Cooling Device", + 28 => "Temperature Probe", + 29 => "Electrical Current Probe", + 30 => "Out-of-band Remote Access", + 31 => "Boot Integrity Services", + 32 => "System Boot", + 33 => "64-bit Memory Error", + 34 => "Management Device", + 35 => "Management Device Component", + 36 => "Management Device Threshold Data", + 37 => "Memory Channel", + 38 => "IPMI Device", + 39 => "Power Supply", + 40 => "Additional Information", + 41 => "Onboard Device", + } + + extend self + + def dmi_info() + run("dmidecode", stdout: true, silent: true) + .strip + .split(/\n\n+(?=Handle)/)[1..-2] + .map do |data| + full_header, description, data = data.split(/\n+/, 3) + # full_header: Handle 0x0031, DMI type 40, 18 bytes\n + handle = full_header.split(",", 2).first.split(" ", 2).last + dmi_type_numeric = full_header.split(",")[1].split(/\s+/).last.to_i + dmi_type = TYPES[dmi_type_numeric] + data = data.gsub(/^\t/, "").split(/\n+(?=[^\t])/).map do |line| + k, v = line.split(/\s*:\s*/, 2) + v = v.gsub(/^\t/, "") if v + [k, v] + end + .group_by { |pair| pair.first } + .map do |k, data| + data = data.map(&:last) + data = data.first unless data.length > 1 + [k, data] + end + .to_h + { + "dmi_type" => dmi_type, + "dmi_type_numeric" => dmi_type_numeric, + "handle" => handle, + "full_header" => full_header, + "description" => description, + "data" => data, + } + end + .group_by { |data| data["dmi_type"] } + .map do |k, data| + data = data.first unless data.length > 1 + [k, data] + end + .to_h + end +end + +module ReportData + extend self + + def dmi_info() + @dmi_info ||= DMI.dmi_info + end + + def is_steam_deck?() + system_information["Product Name"] == "Jupiter" + end + + FIELDS = [ + :data_version, + :bios_information, + :manufacturing_information, + :system_information, + :board_information, + :processor_information, + :memory_information, + :onboard_devices_information, + :controller_information, + ] + + def data_version() + 1 + end + + def bios_information() + dmi_info["BIOS"]["data"].select do |k, _| + [ + "Vendor", + "Version", + "Release Date", + "BIOS Revision", + "Firmware Revision", + ].include?(k) + end + .to_h + end + + def manufacturing_information() + if is_steam_deck? then + year = system_information["Serial Number"][4..4] + week = system_information["Serial Number"][5..6] + { + "Year" => "202#{year}", + "Week" => week, + } + end + end + + def system_information() + dmi_info["System"]["data"].select do |k, _| + [ + "Manufacturer", + "Product Name", + "Version", + "Family", + "Serial Number", + ].include?(k) + end + .to_h + end + + def board_information() + dmi_info["Base Board"]["data"].select do |k, _| + [ + "Manufacturer", + "Product Name", + "Version", + "Serial Number", + ].include?(k) + end + .to_h + end + + def processor_information() + dmi_info["Processor"]["data"] + end + + def memory_information() + devices = dmi_info["Memory Device"].map do |entry| + entry["data"].select do |k, _| + [ + "Size", + "Part Number", + "Type", + "Speed", + "Configured Memory Speed", + "Manufacturer", + ].include?(k) + end + .to_h + end + .map do |v| + if v["Manufacturer"] == "Unknown" then + v["Manufacturer"] = + case v["Part Number"] + when /^MT/ + "Micron" + when /^KL/ + "Samsung" + else + "(Unknown)" + end + end + v + end + physical_memory = dmi_info["Physical Memory Array"]["data"].select do |k, _| + [ + "Maximum Capacity", + "Number Of Devices", + ].include?(k) + end + .to_h + { + "Physical Memory" => physical_memory, + "Devices" => devices, + } + end + + def onboard_devices_information() + dmi_info["Onboard Device"].map do |entry| + entry["data"] + end + end + + def controller_information() + return nil unless is_steam_deck? + return @controller_information if @controller_information + begin + info = JSON.parse(run(D20BOOTLOADER, "getdevicesjson", silent: true, stdout: true)).first + bootloader_type = info["release_number"] >> 8 # Shift for the major release byte + raw = run(D20BOOTLOADER, "getinfo", silent: true, stdout: true, stderr: true) + + # Clean up the raw data + raw = + raw + .split(/\n+/) + .grep(/__main__ - INFO/) + .map { |line| line.split(/\s*-\s*/, 6).last } # "2023-08-09 20:35:03,265 - __main__ - INFO - ......" + .join("\n") + + # Extract the info + if bootloader_type == 3 + # RA4 + device_type = + raw + .split(/\n+/) + .grep(/Found a/) + .first + .sub("DeviceType.", "") + .split(/\s+/)[2] + mcu = raw.split("**").last.strip + mcus = [mcu] + else + # D20/D21 + mcus = raw.split("\n\n") + header = mcus.shift.split(/\n/) + device_type = header + .find { |line| line.match(/^Found a/) } + .split(/\s+/)[2] + end + + mcus = mcus.map do |mcu| + mcu.split(/\n/) + .grep(/:/) + .map do |line| + line + .split(/:\s*/, 2) + end + .select { |pair| pair.length == 2 } + .select do |pair| + [ + "Stored board serial", + "Stored hardware ID", + ].include?(pair.first) + end + .to_h + end + ensure + run(D20BOOTLOADER, "reset", silent: true, stdout: true, stderr: true) + sleep(1) + end + @controller_information = { + "Device Type" => device_type, + "Hardware Info" => mcus, + "Bootloader Type" => bootloader_type, + "Hardware ID" => mcus.first["Stored hardware ID"], + } + end + + def raw() + FIELDS.map { |field| [field, ReportData.send(field)] }.to_h + end + + def steam_deck() + ram_chip_count = memory_information["Devices"].length + ram_chip_size = memory_information["Devices"][0]["Size"].split(/\s+/, 2)[0].to_i + + [ + "Serial: #{system_information["Serial Number"]}", + "Manufacturing year: #{manufacturing_information["Year"]}", + "Manufacturing week: #{manufacturing_information["Week"]}", + "Controller type: #{controller_information["Device Type"]}", + "Controller BL type: #{controller_information["Bootloader Type"]}", + "Controller HWID: #{controller_information["Hardware ID"]}", + "RAM config: #{ram_chip_count}×#{ram_chip_size} = #{ram_chip_size * ram_chip_count}", + "RAM Part Number: #{memory_information["Devices"][0]["Part Number"]}", + "RAM Manufacturer: #{memory_information["Devices"][0]["Manufacturer"]}", + ].join("\n") + end +end + +def usage() + puts [ + "Usage: jovian-hardware-survey ", + "", + " --steam-deck-report outputs a brief report only for steam deck.", + " --raw outputs all data gathered to JSON on stdout.", + ].join("\n") +end + +if ARGV.length != 1 then + usage() + exit 1 +end +case ARGV.first +when "-h", "-help" + usage() + exit 0 +when "--steam-deck-report" + if ReportData.is_steam_deck?() + puts ReportData.steam_deck() + else + $stderr.puts "ERROR: This is not a steam deck..." + exit 2 + end +when "--raw" + puts JSON.pretty_generate(ReportData.raw) +end