Skip site navigation (1)Skip section navigation (2)
Date:      Fri, 17 Apr 2026 02:40:14 +0000
From:      Adrian Chadd <adrian@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Cc:        Abdelkader Boudih <chaos@seuros.com>
Subject:   git: aae906840494 - main - asmc: add automatic voltage/current/power/ambient sensor detection
Message-ID:  <69e19d8e.1fe9f.68a0f647@gitrepo.freebsd.org>

index | next in thread | raw e-mail

The branch main has been updated by adrian:

URL: https://cgit.FreeBSD.org/src/commit/?id=aae9068404947dd9ffd8522359d0f9dffaa70414

commit aae9068404947dd9ffd8522359d0f9dffaa70414
Author:     Abdelkader Boudih <chaos@seuros.com>
AuthorDate: 2026-04-17 02:31:21 +0000
Commit:     Adrian Chadd <adrian@FreeBSD.org>
CommitDate: 2026-04-17 02:31:37 +0000

    asmc: add automatic voltage/current/power/ambient sensor detection
    
    Apple SMCs contain numerous undocumented voltage, current, power,
    and ambient light sensors. This change adds automatic detection
    and registration of these sensors as sysctls.
    
    New sysctl trees:
      dev.asmc.0.voltage.*  - Voltage sensors (millivolts)
      dev.asmc.0.current.*  - Current sensors (milliamps)
      dev.asmc.0.power.*    - Power sensors (milliwatts)
      dev.asmc.0.ambient.*  - Ambient light sensors
    
    Implementation:
    - Scans all SMC keys at attach time via asmc_key_dump_by_index()
    - Identifies sensors by key prefix patterns:
      - Voltage: VC*, VD*, VG*, VP*, VI*
      - Current: I{C,D,G,M,N,O,H,P,B,A,L}*
      - Power:   P{C,D,N,S,T,H,F,Z,z}*
      - Light:   ALV*, ALS*
    - Dynamically creates sysctls for detected sensors
    - Supports 8 fixed-point SMC data types:
      - sp78, sp87, sp4b, sp5a, sp69, sp96, sp2d, ui16
    - Auto-converts all values to milli-units (mV, mA, mW)
    
    On Mac Mini 5,1, detects:
    - 7 voltage sensors
    - 18 current sensors
    - 27 power sensors
    - 2 ambient light sensors
    
    Enables power consumption monitoring, voltage rail debugging,
    and ambient light detection without hardcoding model-specific
    sensor lists.
    
    Tested on:
      - Mac Mini 5,1 (2011) running FreeBSD 15.0-RELEASE
      - 54 sensors auto-detected and exposed via sysctl
      - All sensor types verified with multimeter readings
      - Fixed-point conversions validated against known values
      - Memory management tested (malloc/free on detach)
    
    Reviewed by:    adrian
    Differential Revision:  https://reviews.freebsd.org/D55807
---
 sys/dev/asmc/asmc.c    | 437 ++++++++++++++++++++++++++++++++++++++++++++++++-
 sys/dev/asmc/asmcvar.h |  10 ++
 2 files changed, 445 insertions(+), 2 deletions(-)

diff --git a/sys/dev/asmc/asmc.c b/sys/dev/asmc/asmc.c
index 7cd5181605a8..0a701e6fd663 100644
--- a/sys/dev/asmc/asmc.c
+++ b/sys/dev/asmc/asmc.c
@@ -123,15 +123,22 @@ static int 	asmc_mbp_sysctl_light_control(SYSCTL_HANDLER_ARGS);
 static int 	asmc_mbp_sysctl_light_left_10byte(SYSCTL_HANDLER_ARGS);
 static int	asmc_wol_sysctl(SYSCTL_HANDLER_ARGS);
 
+static int	asmc_key_getinfo(device_t, const char *, uint8_t *, char *);
+
 #ifdef ASMC_DEBUG
 /* Raw key access */
-static int	asmc_key_getinfo(device_t, const char *, uint8_t *, char *);
 static int	asmc_raw_key_sysctl(SYSCTL_HANDLER_ARGS);
 static int	asmc_raw_value_sysctl(SYSCTL_HANDLER_ARGS);
 static int	asmc_raw_len_sysctl(SYSCTL_HANDLER_ARGS);
 static int	asmc_raw_type_sysctl(SYSCTL_HANDLER_ARGS);
 #endif
 
+/* Voltage/Current/Power/Light sensor support */
+static int	asmc_sensor_read(device_t, const char *, int *);
+static int	asmc_sensor_sysctl(SYSCTL_HANDLER_ARGS);
+static int	asmc_detect_sensors(device_t);
+static int	asmc_key_dump_by_index(device_t, int, char *, char *, uint8_t *);
+
 struct asmc_model {
 	const char *smc_model; /* smbios.system.product env var. */
 	const char *smc_desc;  /* driver description */
@@ -963,6 +970,16 @@ asmc_detach(device_t dev)
 	if (sc->sc_kbd_bkl != NULL)
 		backlight_destroy(sc->sc_kbd_bkl);
 
+	/* Free sensor key arrays */
+	for (int i = 0; i < sc->sc_voltage_count; i++)
+		free(sc->sc_voltage_sensors[i], M_DEVBUF);
+	for (int i = 0; i < sc->sc_current_count; i++)
+		free(sc->sc_current_sensors[i], M_DEVBUF);
+	for (int i = 0; i < sc->sc_power_count; i++)
+		free(sc->sc_power_sensors[i], M_DEVBUF);
+	for (int i = 0; i < sc->sc_light_count; i++)
+		free(sc->sc_light_sensors[i], M_DEVBUF);
+
 	if (sc->sc_sms_tq) {
 		taskqueue_drain(sc->sc_sms_tq, &sc->sc_sms_task);
 		taskqueue_free(sc->sc_sms_tq);
@@ -1134,6 +1151,12 @@ nosms:
 		sc->sc_nkeys = 0;
 	}
 
+	/*
+	 * Auto-detect and register voltage/current/power/ambient sensors.
+	 * Scans SMC keys and creates sysctls for detected sensors.
+	 */
+	asmc_detect_sensors(dev);
+
 out_err:
 #ifdef ASMC_DEBUG
 	asmc_dumpall(dev);
@@ -1337,10 +1360,10 @@ out:
 
 	return (0);
 }
+#endif /* ASMC_DEBUG */
 
 /*
  * Get key info (length and type) from SMC using command 0x13.
- * Returns 0 on success, -1 on failure.
  * If len is non-NULL, stores the key's value length.
  * If type is non-NULL, stores the 4-char type string (must be at least 5 bytes).
  */
@@ -1389,6 +1412,7 @@ out:
 	return (error);
 }
 
+#ifdef ASMC_DEBUG
 /*
  * Raw SMC key access sysctls - enables reading/writing any SMC key by name
  * Usage:
@@ -1491,6 +1515,415 @@ asmc_raw_type_sysctl(SYSCTL_HANDLER_ARGS)
 }
 #endif
 
+/*
+ * Convert signed fixed-point SMC values to milli-units.
+ * Format "spXY" means signed with X integer bits and Y fraction bits.
+ */
+static int
+asmc_sp78_to_milli(const uint8_t *buf)
+{
+	int16_t val = (int16_t)be16dec(buf);
+
+	return ((int)val * 1000) / 256;
+}
+
+static int
+asmc_sp87_to_milli(const uint8_t *buf)
+{
+	int16_t val = (int16_t)be16dec(buf);
+
+	return ((int)val * 1000) / 128;
+}
+
+static int
+asmc_sp4b_to_milli(const uint8_t *buf)
+{
+	int16_t val = (int16_t)be16dec(buf);
+
+	return ((int)val * 1000) / 2048;
+}
+
+static int
+asmc_sp5a_to_milli(const uint8_t *buf)
+{
+	int16_t val = (int16_t)be16dec(buf);
+
+	return ((int)val * 1000) / 1024;
+}
+
+static int
+asmc_sp69_to_milli(const uint8_t *buf)
+{
+	int16_t val = (int16_t)be16dec(buf);
+
+	return ((int)val * 1000) / 512;
+}
+
+static int
+asmc_sp96_to_milli(const uint8_t *buf)
+{
+	int16_t val = (int16_t)be16dec(buf);
+
+	return ((int)val * 1000) / 64;
+}
+
+static int
+asmc_sp2d_to_milli(const uint8_t *buf)
+{
+	int16_t val = (int16_t)be16dec(buf);
+
+	return ((int)val * 1000) / 8192;
+}
+
+static bool
+asmc_sensor_type_supported(const char *type)
+{
+
+	return (strncmp(type, "sp78", 4) == 0 ||
+	    strncmp(type, "sp87", 4) == 0 ||
+	    strncmp(type, "sp4b", 4) == 0 ||
+	    strncmp(type, "sp5a", 4) == 0 ||
+	    strncmp(type, "sp69", 4) == 0 ||
+	    strncmp(type, "sp96", 4) == 0 ||
+	    strncmp(type, "sp2d", 4) == 0 ||
+	    strncmp(type, "ui16", 4) == 0);
+}
+
+/*
+ * Generic sensor value reader with automatic type conversion.
+ * Reads an SMC key, detects its type, and converts to millivalue.
+ */
+static int
+asmc_sensor_read(device_t dev, const char *key, int *millivalue)
+{
+	uint8_t buf[2];
+	char type[ASMC_TYPELEN + 1];
+	uint8_t len;
+	int error;
+
+	error = asmc_key_getinfo(dev, key, &len, type);
+	if (error != 0)
+		return (error);
+
+	if (len != 2) {
+		if (bootverbose)
+			device_printf(dev,
+			    "%s: key %s unexpected length %d\n",
+			    __func__, key, len);
+		return (ENXIO);
+	}
+
+	error = asmc_key_read(dev, key, buf, sizeof(buf));
+	if (error != 0)
+		return (error);
+
+	if (strncmp(type, "sp78", 4) == 0) {
+		*millivalue = asmc_sp78_to_milli(buf);
+	} else if (strncmp(type, "sp87", 4) == 0) {
+		*millivalue = asmc_sp87_to_milli(buf);
+	} else if (strncmp(type, "sp4b", 4) == 0) {
+		*millivalue = asmc_sp4b_to_milli(buf);
+	} else if (strncmp(type, "sp5a", 4) == 0) {
+		*millivalue = asmc_sp5a_to_milli(buf);
+	} else if (strncmp(type, "sp69", 4) == 0) {
+		*millivalue = asmc_sp69_to_milli(buf);
+	} else if (strncmp(type, "sp96", 4) == 0) {
+		*millivalue = asmc_sp96_to_milli(buf);
+	} else if (strncmp(type, "sp2d", 4) == 0) {
+		*millivalue = asmc_sp2d_to_milli(buf);
+	} else if (strncmp(type, "ui16", 4) == 0) {
+		*millivalue = be16dec(buf);
+	} else {
+		if (bootverbose)
+			device_printf(dev,
+			    "%s: unknown type '%s' for key %s\n",
+			    __func__, type, key);
+		return (ENXIO);
+	}
+
+	return (0);
+}
+
+/*
+ * Generic sensor sysctl handler for voltage/current/power/light sensors.
+ * arg2 encodes: sensor_type (high byte) | sensor_index (low byte)
+ * Sensor types: 'V'=voltage, 'I'=current, 'P'=power, 'L'=light
+ */
+static int
+asmc_sensor_sysctl(SYSCTL_HANDLER_ARGS)
+{
+	device_t dev = (device_t) arg1;
+	struct asmc_softc *sc = device_get_softc(dev);
+	int error, val;
+	int sensor_type = (arg2 >> 8) & 0xFF;
+	int sensor_idx = arg2 & 0xFF;
+	const char *key = NULL;
+
+	/* Select sensor based on type and index */
+	switch (sensor_type) {
+	case 'V':  /* Voltage */
+		if (sensor_idx < sc->sc_voltage_count)
+			key = sc->sc_voltage_sensors[sensor_idx];
+		break;
+	case 'I':  /* Current */
+		if (sensor_idx < sc->sc_current_count)
+			key = sc->sc_current_sensors[sensor_idx];
+		break;
+	case 'P':  /* Power */
+		if (sensor_idx < sc->sc_power_count)
+			key = sc->sc_power_sensors[sensor_idx];
+		break;
+	case 'L':  /* Light */
+		if (sensor_idx < sc->sc_light_count)
+			key = sc->sc_light_sensors[sensor_idx];
+		break;
+	default:
+		return (EINVAL);
+	}
+
+	if (key == NULL)
+		return (ENOENT);
+
+	error = asmc_sensor_read(dev, key, &val);
+	if (error != 0)
+		return (error);
+
+	return (sysctl_handle_int(oidp, &val, 0, req));
+}
+
+/*
+ * Detect and register voltage/current/power/ambient sensors.
+ * Scans all SMC keys and identifies sensor keys by prefix.
+ * Returns 0 on success, -1 on error.
+ */
+static int
+asmc_detect_sensors(device_t dev)
+{
+	struct asmc_softc *sc = device_get_softc(dev);
+	struct sysctl_ctx_list *sysctlctx;
+	struct sysctl_oid *tree_node;
+	char key[ASMC_KEYLEN + 1];
+	char type[ASMC_TYPELEN + 1];
+	uint8_t len;
+	unsigned int nkeys;
+	unsigned int i;
+	int error;
+	char *sensor_key;
+
+	/* Initialize counts */
+	sc->sc_voltage_count = 0;
+	sc->sc_current_count = 0;
+	sc->sc_power_count = 0;
+	sc->sc_light_count = 0;
+
+	if (sc->sc_nkeys == 0)
+		return (0);
+	nkeys = sc->sc_nkeys;
+
+	/* Scan all keys for voltage/current/power/ambient light sensors */
+	for (i = 0; i < nkeys; i++) {
+		/* Get key name by index */
+		error = asmc_key_dump_by_index(dev, i, key, type, &len);
+		if (error != 0)
+			continue;
+		if (!asmc_sensor_type_supported(type))
+			continue;
+
+		/* Voltage sensors (VC*, VD*, VG*, VP*, VI*) */
+		if (key[0] == 'V' && (key[1] == 'C' || key[1] == 'D' ||
+		    key[1] == 'G' || key[1] == 'P' || key[1] == 'I') &&
+		    len == 2) {
+			if (sc->sc_voltage_count >= ASMC_MAX_SENSORS)
+				continue;
+			sensor_key = malloc(ASMC_KEYLEN + 1,
+			    M_DEVBUF, M_WAITOK);
+			memcpy(sensor_key, key, ASMC_KEYLEN + 1);
+			sc->sc_voltage_sensors[sc->sc_voltage_count++] =
+			    sensor_key;
+		} else if (key[0] == 'I' && (key[1] == 'C' ||
+		    key[1] == 'D' || key[1] == 'G' || key[1] == 'M' ||
+		    key[1] == 'N' || key[1] == 'O' || key[1] == 'H' ||
+		    key[1] == 'P' || key[1] == 'B' || key[1] == 'A' ||
+		    key[1] == 'L') && len == 2) {
+			/* Current sensors */
+			if (sc->sc_current_count >= ASMC_MAX_SENSORS)
+				continue;
+			sensor_key = malloc(ASMC_KEYLEN + 1,
+			    M_DEVBUF, M_WAITOK);
+			memcpy(sensor_key, key, ASMC_KEYLEN + 1);
+			sc->sc_current_sensors[sc->sc_current_count++] =
+			    sensor_key;
+		} else if (key[0] == 'P' && (key[1] == 'C' ||
+		    key[1] == 'D' || key[1] == 'N' || key[1] == 'S' ||
+		    key[1] == 'T' || key[1] == 'H' || key[1] == 'F' ||
+		    key[1] == 'Z' || key[1] == 'z') && len == 2) {
+			/* Power sensors */
+			if (sc->sc_power_count >= ASMC_MAX_SENSORS)
+				continue;
+			sensor_key = malloc(ASMC_KEYLEN + 1,
+			    M_DEVBUF, M_WAITOK);
+			memcpy(sensor_key, key, ASMC_KEYLEN + 1);
+			sc->sc_power_sensors[sc->sc_power_count++] =
+			    sensor_key;
+		} else if (key[0] == 'A' && key[1] == 'L' &&
+		    (key[2] == 'V' || key[2] == 'S') && len == 2) {
+			/* Ambient light sensors */
+			if (sc->sc_light_count >= ASMC_MAX_SENSORS)
+				continue;
+			sensor_key = malloc(ASMC_KEYLEN + 1,
+			    M_DEVBUF, M_WAITOK);
+			memcpy(sensor_key, key, ASMC_KEYLEN + 1);
+			sc->sc_light_sensors[sc->sc_light_count++] =
+			    sensor_key;
+		}
+	}
+
+	if (bootverbose)
+		device_printf(dev,
+		    "detected %d voltage, %d current, "
+		    "%d power, %d light sensors\n",
+		    sc->sc_voltage_count, sc->sc_current_count,
+		    sc->sc_power_count, sc->sc_light_count);
+
+	/* Register sysctls for detected sensors */
+	sysctlctx = device_get_sysctl_ctx(dev);
+
+	/* Voltage sensors */
+	if (sc->sc_voltage_count > 0) {
+		tree_node = SYSCTL_ADD_NODE(sysctlctx,
+		    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+		    "voltage", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Voltage sensors (millivolts)");
+
+		for (i = 0; i < sc->sc_voltage_count; i++) {
+			SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
+			    OID_AUTO, sc->sc_voltage_sensors[i],
+			    CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
+			    dev, ('V' << 8) | i, asmc_sensor_sysctl, "I",
+			    "Voltage sensor (millivolts)");
+		}
+	}
+
+	/* Current sensors */
+	if (sc->sc_current_count > 0) {
+		tree_node = SYSCTL_ADD_NODE(sysctlctx,
+		    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+		    "current", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Current sensors (milliamps)");
+
+		for (i = 0; i < sc->sc_current_count; i++) {
+			SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
+			    OID_AUTO, sc->sc_current_sensors[i],
+			    CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
+			    dev, ('I' << 8) | i, asmc_sensor_sysctl, "I",
+			    "Current sensor (milliamps)");
+		}
+	}
+
+	/* Power sensors */
+	if (sc->sc_power_count > 0) {
+		tree_node = SYSCTL_ADD_NODE(sysctlctx,
+		    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+		    "power", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Power sensors (milliwatts)");
+
+		for (i = 0; i < sc->sc_power_count; i++) {
+			SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
+			    OID_AUTO, sc->sc_power_sensors[i],
+			    CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
+			    dev, ('P' << 8) | i, asmc_sensor_sysctl, "I",
+			    "Power sensor (milliwatts)");
+		}
+	}
+
+	/* Ambient light sensors */
+	if (sc->sc_light_count > 0) {
+		tree_node = SYSCTL_ADD_NODE(sysctlctx,
+		    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO,
+		    "ambient", CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Ambient light sensors");
+
+		for (i = 0; i < sc->sc_light_count; i++) {
+			SYSCTL_ADD_PROC(sysctlctx, SYSCTL_CHILDREN(tree_node),
+			    OID_AUTO, sc->sc_light_sensors[i],
+			    CTLTYPE_INT | CTLFLAG_RD | CTLFLAG_MPSAFE,
+			    dev, ('L' << 8) | i, asmc_sensor_sysctl, "I",
+			    "Light sensor value");
+		}
+	}
+
+	return (0);
+}
+
+/*
+ * Helper function to get key info by index (for sensor detection).
+ */
+static int
+asmc_key_dump_by_index(device_t dev, int index, char *key_out,
+    char *type_out, uint8_t *len_out)
+{
+	struct asmc_softc *sc = device_get_softc(dev);
+	uint8_t index_buf[ASMC_KEYLEN];
+	uint8_t key_buf[ASMC_KEYLEN];
+	uint8_t info_buf[ASMC_KEYINFO_RESPLEN];
+	int error = ENXIO, try = 0;
+	int i;
+
+	mtx_lock_spin(&sc->sc_mtx);
+
+	index_buf[0] = (index >> 24) & 0xff;
+	index_buf[1] = (index >> 16) & 0xff;
+	index_buf[2] = (index >> 8) & 0xff;
+	index_buf[3] = index & 0xff;
+
+begin:
+	if (asmc_command(dev, ASMC_CMDGETBYINDEX))
+		goto out;
+
+	for (i = 0; i < ASMC_KEYLEN; i++) {
+		ASMC_DATAPORT_WRITE(sc, index_buf[i]);
+		if (asmc_wait(dev, ASMC_STATUS_AWAIT_DATA))
+			goto out;
+	}
+
+	ASMC_DATAPORT_WRITE(sc, ASMC_KEYLEN);
+
+	for (i = 0; i < ASMC_KEYLEN; i++) {
+		if (asmc_wait(dev, ASMC_STATUS_DATA_READY))
+			goto out;
+		key_buf[i] = ASMC_DATAPORT_READ(sc);
+	}
+
+	if (asmc_command(dev, ASMC_CMDGETINFO))
+		goto out;
+
+	for (i = 0; i < ASMC_KEYLEN; i++) {
+		ASMC_DATAPORT_WRITE(sc, key_buf[i]);
+		if (asmc_wait(dev, ASMC_STATUS_AWAIT_DATA))
+			goto out;
+	}
+
+	ASMC_DATAPORT_WRITE(sc, ASMC_KEYINFO_RESPLEN);
+
+	for (i = 0; i < ASMC_KEYINFO_RESPLEN; i++) {
+		if (asmc_wait(dev, ASMC_STATUS_DATA_READY))
+			goto out;
+		info_buf[i] = ASMC_DATAPORT_READ(sc);
+	}
+
+	memcpy(key_out, key_buf, ASMC_KEYLEN);
+	key_out[ASMC_KEYLEN] = '\0';
+	*len_out = info_buf[0];
+	memcpy(type_out, &info_buf[1], ASMC_TYPELEN);
+	type_out[ASMC_TYPELEN] = '\0';
+	error = 0;
+
+out:
+	if (error) {
+		if (++try < ASMC_MAXRETRIES)
+			goto begin;
+	}
+
+	mtx_unlock_spin(&sc->sc_mtx);
+	return (error);
+}
+
 static int
 asmc_key_write(device_t dev, const char *key, uint8_t *buf, uint8_t len)
 {
diff --git a/sys/dev/asmc/asmcvar.h b/sys/dev/asmc/asmcvar.h
index 00857fe5fea8..43f679f3fef0 100644
--- a/sys/dev/asmc/asmcvar.h
+++ b/sys/dev/asmc/asmcvar.h
@@ -31,6 +31,7 @@
 #define ASMC_MAXVAL	32	/* Maximum SMC value size */
 #define ASMC_KEYLEN	4	/* SMC key name length */
 #define ASMC_TYPELEN	4	/* SMC type string length */
+#define ASMC_MAX_SENSORS	64	/* Max sensors per type */
 
 struct asmc_softc {
 	device_t 		sc_dev;
@@ -64,6 +65,15 @@ struct asmc_softc {
 	uint8_t			sc_rawlen;
 	char			sc_rawtype[ASMC_TYPELEN + 1];
 #endif
+	/* Voltage/Current/Power/Light sensors */
+	char			*sc_voltage_sensors[ASMC_MAX_SENSORS];
+	int			sc_voltage_count;
+	char			*sc_current_sensors[ASMC_MAX_SENSORS];
+	int			sc_current_count;
+	char			*sc_power_sensors[ASMC_MAX_SENSORS];
+	int			sc_power_count;
+	char			*sc_light_sensors[ASMC_MAX_SENSORS];
+	int			sc_light_count;
 };
 
 /*


home | help

Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?69e19d8e.1fe9f.68a0f647>