Disks: eMMC is one device, not three, and reports wear from its own registers

mmcblkNboot0/boot1/rpmb are hardware areas of the chip; hide them on Disks and
Dashboard. Badge eMMC/SD instead of SSD. smartctl cannot talk to MMC, so read
life_time and pre_eol_info from /sys instead of showing its error.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 04:48:52 -07:00
co-authored by Claude Fable 5.1
parent 488d929733
commit 232163ff4c
4 changed files with 95 additions and 26 deletions
+82 -23
View File
@@ -38,7 +38,7 @@ type Part struct {
type Device struct {
Name string `json:"name"`
Kind string `json:"kind"` // nvme | ssd | hdd | usb | other
Kind string `json:"kind"` // nvme | ssd | hdd | usb | emmc | sd | other
Model string `json:"model,omitempty"`
Vendor string `json:"vendor,omitempty"`
Serial string `json:"serial,omitempty"`
@@ -88,19 +88,24 @@ func Devices(ctx context.Context) ([]Device, error) {
}
devs := []Device{}
for _, n := range root.Blockdevices {
if n.Type != "disk" || n.Size == 0 || strings.HasPrefix(n.Name, "zd") {
if n.Type != "disk" || n.Size == 0 || strings.HasPrefix(n.Name, "zd") || mmcAreaRe.MatchString(n.Name) {
continue
}
d := Device{Name: n.Name, Model: str(n.Model), Vendor: str(n.Vendor), Serial: str(n.Serial), Tran: str(n.Tran),
Size: n.Size, Rotational: n.Rota, Removable: n.RM, Temp: blockTemp(n.Name), Parts: []Part{}}
switch {
case strings.HasPrefix(n.Name, "mmcblk"):
d.Kind = mmcKind(n.Name)
if d.Model == "" {
d.Model = strings.TrimSpace(readStrFile("/sys/block/" + n.Name + "/device/name"))
}
case d.Tran == "usb":
d.Kind = "usb"
case d.Tran == "nvme" || strings.HasPrefix(n.Name, "nvme"):
d.Kind = "nvme"
case n.Rota:
d.Kind = "hdd"
case d.Tran == "sata" || d.Tran == "sas" || strings.HasPrefix(n.Name, "sd") || strings.HasPrefix(n.Name, "mmcblk"):
case d.Tran == "sata" || d.Tran == "sas" || strings.HasPrefix(n.Name, "sd"):
d.Kind = "ssd"
default:
d.Kind = "other"
@@ -199,6 +204,48 @@ func sysBlockDevices() []lsblkNode {
return out
}
// An eMMC chip's boot and RPMB areas are hardware regions of the same chip, not drives;
// the kernel lists them as disks of their own (mmcblk0boot0, mmcblk0boot1, mmcblk0rpmb).
var mmcAreaRe = regexp.MustCompile(`^mmcblk\d+(boot\d+|rpmb|gp\d+)$`)
func mmcKind(name string) string {
if strings.TrimSpace(readStrFile("/sys/block/"+name+"/device/type")) == "MMC" {
return "emmc"
}
return "sd"
}
// mmcHealth is the eMMC answer to SMART: the chip's own wear and end-of-life registers,
// which the kernel publishes in /sys. SD cards have neither; smartctl speaks to neither.
func mmcHealth(s Smart) Smart {
dir := "/sys/block/" + s.Device + "/device/"
s.Available, s.Supported = true, true
s.SelfTestNote = "not a feature of eMMC or SD"
s.Serial = strings.TrimSpace(readStrFile(dir + "serial"))
s.Firmware = strings.TrimSpace(readStrFile(dir + "fwrev"))
s.Model = strings.TrimSpace(readStrFile(dir + "name"))
hex := func(v string) int { n, _ := strconv.ParseInt(strings.TrimPrefix(v, "0x"), 16, 32); return int(n) }
life := strings.Fields(readStrFile(dir + "life_time"))
eol := strings.TrimSpace(readStrFile(dir + "pre_eol_info"))
if len(life) != 2 || eol == "" {
s.Error = "This card reports no health data (SD cards don't; eMMC 5.0 and later does)."
return s
}
m := &MMCHealth{LifeA: hex(life[0]), LifeB: hex(life[1]), PreEOL: hex(eol)}
s.MMC = m
ok := m.PreEOL <= 1 && m.LifeA < 10 && m.LifeB < 10
s.Passed = &ok
if m.PreEOL == 2 {
s.Warnings = append(s.Warnings, "80% of reserved blocks consumed")
} else if m.PreEOL >= 3 {
s.Warnings = append(s.Warnings, "90% of reserved blocks consumed — replace soon")
}
if m.LifeA >= 10 || m.LifeB >= 10 {
s.Warnings = append(s.Warnings, "estimated lifetime reached")
}
return s
}
var nsRe = regexp.MustCompile(`^(nvme\d+)n\d+$`)
// foldNVMe merges nvme0n1, nvme0n2, … into one physical device "nvme0": one SSD, one
@@ -318,26 +365,35 @@ type Attr struct {
}
type Smart struct {
Device string `json:"device"`
Available bool `json:"available"` // smartctl installed
Supported bool `json:"supported"` // device answered
Standby bool `json:"standby"` // spun down; not read
Passed *bool `json:"passed"`
Temp int `json:"temp,omitempty"`
PowerOnHours int `json:"power_on_hours,omitempty"`
PowerCycles int `json:"power_cycles,omitempty"`
Model string `json:"model,omitempty"`
Serial string `json:"serial,omitempty"`
Firmware string `json:"firmware,omitempty"`
Attrs []Attr `json:"attrs"`
NVMe *NVMeLog `json:"nvme,omitempty"`
SelfTest string `json:"self_test,omitempty"`
SelfTestPct int `json:"self_test_remaining,omitempty"`
SelfTestOK bool `json:"self_test_supported"`
SelfTestNote string `json:"self_test_note,omitempty"`
Warnings []string `json:"warnings"`
Error string `json:"error,omitempty"`
FetchedAt int64 `json:"fetched_at"`
Device string `json:"device"`
Available bool `json:"available"` // smartctl installed
Supported bool `json:"supported"` // device answered
Standby bool `json:"standby"` // spun down; not read
Passed *bool `json:"passed"`
Temp int `json:"temp,omitempty"`
PowerOnHours int `json:"power_on_hours,omitempty"`
PowerCycles int `json:"power_cycles,omitempty"`
Model string `json:"model,omitempty"`
Serial string `json:"serial,omitempty"`
Firmware string `json:"firmware,omitempty"`
Attrs []Attr `json:"attrs"`
NVMe *NVMeLog `json:"nvme,omitempty"`
MMC *MMCHealth `json:"mmc,omitempty"`
SelfTest string `json:"self_test,omitempty"`
SelfTestPct int `json:"self_test_remaining,omitempty"`
SelfTestOK bool `json:"self_test_supported"`
SelfTestNote string `json:"self_test_note,omitempty"`
Warnings []string `json:"warnings"`
Error string `json:"error,omitempty"`
FetchedAt int64 `json:"fetched_at"`
}
// MMCHealth is the eMMC EXT_CSD wear report. Life values are tenths: 1 = 0–10% of rated
// life used, 10 = 90–100%, 11 = exceeded, 0 = not reported. A is the SLC area, B the MLC.
type MMCHealth struct {
LifeA int `json:"life_a"`
LifeB int `json:"life_b"`
PreEOL int `json:"pre_eol"` // 1 normal · 2 warning · 3 urgent
}
type NVMeLog struct {
@@ -437,6 +493,9 @@ func (m *Manager) Smart(ctx context.Context, dev string, force bool) Smart {
s.Error = "unknown device"
return s
}
if strings.HasPrefix(dev, "mmcblk") {
return mmcHealth(s)
}
if !m.HasSmartctl() {
return s
}