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:
co-authored by
Claude Fable 5.1
parent
488d929733
commit
232163ff4c
@@ -573,6 +573,10 @@ func (c *Collector) net(dt float64) []Net {
|
||||
var skipDiskPrefix = []string{"loop", "ram", "zram", "nbd", "sr", "fd", "dm-", "md", "drbd", "zd", "pmem"}
|
||||
|
||||
func skipDisk(n string) bool {
|
||||
// eMMC boot/RPMB/general-purpose areas: regions of the chip, not drives
|
||||
if strings.HasPrefix(n, "mmcblk") && (strings.Contains(n, "boot") || strings.HasSuffix(n, "rpmb") || strings.Contains(n, "gp")) {
|
||||
return true
|
||||
}
|
||||
for _, p := range skipDiskPrefix {
|
||||
if strings.HasPrefix(n, p) {
|
||||
return true
|
||||
|
||||
+82
-23
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user