Skip to content

Commit

Permalink
jovian-hardware-survey: init
Browse files Browse the repository at this point in the history
  • Loading branch information
samueldr committed Aug 11, 2023
1 parent 97d6a65 commit fa603fc
Show file tree
Hide file tree
Showing 3 changed files with 382 additions and 0 deletions.
2 changes: 2 additions & 0 deletions overlay.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions pkgs/jovian-hardware-survey/default.nix
Original file line number Diff line number Diff line change
@@ -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
''
363 changes: 363 additions & 0 deletions pkgs/jovian-hardware-survey/jovian-hardware-survey.rb
Original file line number Diff line number Diff line change
@@ -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 <param>",
"",
" --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

0 comments on commit fa603fc

Please sign in to comment.