diff --git a/dashboard/src/lib/components/DeviceIcon.svelte b/dashboard/src/lib/components/DeviceIcon.svelte
index 774be2f9..ac980e74 100644
--- a/dashboard/src/lib/components/DeviceIcon.svelte
+++ b/dashboard/src/lib/components/DeviceIcon.svelte
@@ -9,7 +9,7 @@
*/
interface Props {
- /** "macbook pro" | "mac studio" | "mac mini" etc. */
+ /** "macbook pro" | "mac studio" | "mac mini" | "dgx spark" | "linux" etc. */
deviceType: string;
/** Center X coordinate in SVG space */
cx: number;
@@ -38,10 +38,50 @@
const LOGO_NATIVE_WIDTH = 814;
const LOGO_NATIVE_HEIGHT = 1000;
+ // NVIDIA logo SVG path
+ const NVIDIA_LOGO_PATH =
+ "M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538";
+
+ // Tux penguin paths (viewBox 0 0 100 100)
+ const TUX_BODY_PATH =
+ "M50 8c-8 0-14 6-14 13 0 4 2 8 5 10-8 4-16 14-16 28v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V59c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z";
+ const TUX_BELLY_PATH =
+ "M38 52c0-8 5-15 12-15s12 7 12 15v14c0 4-5 7-12 7s-12-3-12-7V52z";
+
const wireColor = "rgba(179,179,179,0.8)";
const strokeWidth = 1.5;
const modelLower = $derived(deviceType.toLowerCase());
+ const isSpark = $derived(
+ modelLower.includes("dgx") || modelLower.includes("gx10"),
+ );
+ const isLinux = $derived(!isSpark && modelLower.includes("linux"));
+
+ // ── DGX Spark dimensions ──
+ const dgxW = $derived(size * 1.55);
+ const dgxH = $derived(size * 0.58);
+ const dgxX = $derived(cx - dgxW / 2);
+ const dgxY = $derived(cy - dgxH / 2);
+ const dgxChassisX = $derived(dgxX - dgxW * 0.03);
+ const dgxChassisW = $derived(dgxW * 1.05);
+ const dgxHandleW = $derived(dgxW * 0.27);
+ const dgxHandleGap = $derived(dgxH * 0.05);
+ const dgxHandleH = $derived(dgxH - dgxHandleGap * 2);
+ const dgxHandleY = $derived(dgxY + dgxHandleGap);
+ const dgxInnerHandleW = $derived(dgxW * 0.12);
+ const dgxInnerHandleH = $derived(dgxHandleH - dgxH * 0.06);
+ const dgxLeftHandleX = $derived(dgxX + 4);
+ const dgxRightHandleX = $derived(dgxX + dgxW - dgxHandleW - 4);
+ const dgxClipId = $derived(`di-dgx-${uid}`);
+ const dgxTextureId = $derived(`di-dgx-tex-${uid}`);
+
+ // ── Linux Tux dimensions ──
+ const tuxSize = $derived(Math.min(size * 1.0, size * 1.1));
+ const tuxScale = $derived(tuxSize / 100);
+ const tuxTx = $derived(cx - (100 * tuxScale) / 2);
+ const tuxTy = $derived(cy - (100 * tuxScale) / 2);
+ const tuxEyeR = $derived(2.5 * tuxScale);
+ const tuxBellyClipId = $derived(`di-tux-belly-${uid}`);
// ── Mac Studio dimensions (same ratios as TopologyGraph) ──
const studioW = $derived(size * 1.25);
@@ -114,7 +154,230 @@
const studioClipId = $derived(`di-studio-${uid}`);
-{#if modelLower === "mac studio" || modelLower === "mac mini"}
+{#if isSpark}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if ramPercent > 0}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+ {@const badgeW = dgxW * 0.09}
+ {@const badgeH = dgxHandleH * 0.5}
+ {@const badgeX = dgxLeftHandleX + dgxHandleW - badgeW - dgxHandleW * 0.06}
+ {@const badgeYPos = dgxHandleY + (dgxHandleH - badgeH) / 2}
+ {@const textSz = badgeW * 0.58}
+ {@const logoW = textSz * 1.2}
+ {@const logoH = logoW * (1.438 / 2.174)}
+ {@const ctrX = badgeX + badgeW / 2 - badgeW * 0.03}
+ {@const ctrY = badgeYPos + badgeH / 2}
+ {@const labelGap = badgeW * 0.15}
+ {@const totalW = logoW + labelGap + textSz * 3.6}
+
+
+ NVIDIA
+
+{:else if isLinux}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if ramPercent > 0}
+ {@const bellyTop = tuxTy + 37 * tuxScale}
+ {@const bellyH = 36 * tuxScale}
+ {@const memH = (ramPercent / 100) * bellyH}
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{:else if modelLower === "mac studio" || modelLower === "mac mini"}
diff --git a/dashboard/src/lib/components/ModelCard.svelte b/dashboard/src/lib/components/ModelCard.svelte
index b432b7a8..ce3449d3 100644
--- a/dashboard/src/lib/components/ModelCard.svelte
+++ b/dashboard/src/lib/components/ModelCard.svelte
@@ -169,8 +169,10 @@
function getDeviceType(
name: string,
- ): "macbook" | "studio" | "mini" | "unknown" {
+ ): "macbook" | "studio" | "mini" | "dgx" | "linux" | "unknown" {
const lower = name.toLowerCase();
+ if (lower.includes("dgx") || lower.includes("gx10")) return "dgx";
+ if (lower.includes("linux")) return "linux";
if (lower.includes("macbook")) return "macbook";
if (lower.includes("studio")) return "studio";
if (lower.includes("mini")) return "mini";
diff --git a/dashboard/src/lib/components/TopologyGraph.svelte b/dashboard/src/lib/components/TopologyGraph.svelte
index 0d0d3c3d..cc9823bf 100644
--- a/dashboard/src/lib/components/TopologyGraph.svelte
+++ b/dashboard/src/lib/components/TopologyGraph.svelte
@@ -117,6 +117,17 @@
const LOGO_NATIVE_WIDTH = 814;
const LOGO_NATIVE_HEIGHT = 1000;
+ // NVIDIA logo SVG path (from exo-nvidia)
+ const NVIDIA_LOGO_PATH =
+ "M0.81 0.429V0.299c0.013 -0.001 0.026 -0.002 0.038 -0.002 0.355 -0.011 0.588 0.306 0.588 0.306S1.186 0.952 0.916 0.952c-0.036 0 -0.071 -0.006 -0.105 -0.017V0.542c0.138 0.017 0.166 0.078 0.249 0.216l0.185 -0.155s-0.135 -0.177 -0.362 -0.177c-0.024 -0.001 -0.048 0.001 -0.072 0.003m0 -0.429v0.194l0.038 -0.002c0.494 -0.017 0.816 0.405 0.816 0.405s-0.37 0.45 -0.754 0.45c-0.034 0 -0.066 -0.003 -0.099 -0.009v0.12c0.027 0.003 0.055 0.006 0.082 0.006 0.358 0 0.618 -0.183 0.869 -0.399 0.042 0.034 0.212 0.114 0.247 0.15 -0.238 0.2 -0.794 0.361 -1.11 0.361 -0.03 0 -0.059 -0.002 -0.088 -0.005v0.169h1.362V0zm0 0.935v0.102c-0.331 -0.059 -0.423 -0.404 -0.423 -0.404s0.159 -0.176 0.423 -0.205v0.112h-0.001C0.671 0.524 0.562 0.654 0.562 0.654s0.062 0.218 0.248 0.282m-0.588 -0.316s0.196 -0.29 0.589 -0.32V0.194C0.376 0.229 0 0.597 0 0.597s0.213 0.616 0.81 0.672v-0.112c-0.438 -0.054 -0.588 -0.538 -0.588 -0.538";
+
+ // Tux penguin SVG path — simplified silhouette for topology view
+ // viewBox: 0 0 100 100
+ const TUX_BODY_PATH =
+ "M50 8c-8 0-14 6-14 13 0 4 2 8 5 10-8 4-16 14-16 28v12c0 4 2 7 5 9l-6 4c-2 1-3 3-3 5v3c0 2 2 4 4 4h10l4-4h22l4 4h10c2 0 4-2 4-4v-3c0-2-1-4-3-5l-6-4c3-2 5-5 5-9V59c0-14-8-24-16-28 3-2 5-6 5-10 0-7-6-13-14-13z";
+ const TUX_BELLY_PATH =
+ "M38 52c0-8 5-15 12-15s12 7 12 15v14c0 4-5 7-12 7s-12-3-12-7V52z";
+
function formatBytes(bytes: number, decimals = 1): string {
if (!bytes || bytes === 0) return "0B";
const k = 1024;
@@ -554,6 +565,12 @@
const clipPathId = `clip-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
const modelLower = modelId.toLowerCase();
+ const identity = identitiesData[nodeInfo.id];
+ const nameLower = (friendlyName || "").toLowerCase();
+ const isSpark =
+ modelLower.includes("dgx") || modelLower.includes("gx10");
+ const isLinux =
+ !isSpark && (modelLower === "linux" || identity?.osVersion === "Linux");
// Check node states for styling
const isHighlighted = highlightedNodes.has(nodeInfo.id);
@@ -623,7 +640,294 @@
`${friendlyName}\nID: ${nodeInfo.id.slice(-8)}\nMemory: ${formatBytes(ramUsed)}/${formatBytes(ramTotal)}`,
);
- if (modelLower === "mac studio") {
+ if (isSpark) {
+ // NVIDIA DGX Spark — gold chassis with textured front, side handles, and NVIDIA badge
+ iconBaseWidth = nodeRadius * 1.55;
+ iconBaseHeight = nodeRadius * 0.58;
+ const x = nodeInfo.x - iconBaseWidth / 2;
+ const y = nodeInfo.y - iconBaseHeight / 2;
+ const chassisX = x - iconBaseWidth * 0.03;
+ const chassisWidth = iconBaseWidth * 1.05;
+ const cornerRadius = 3;
+
+ const dgxClipId = `dgx-clip-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
+ defs
+ .append("clipPath")
+ .attr("id", dgxClipId)
+ .append("rect")
+ .attr("x", x)
+ .attr("y", y)
+ .attr("width", iconBaseWidth)
+ .attr("height", iconBaseHeight)
+ .attr("rx", cornerRadius);
+
+ // Chassis texture pattern
+ const textureId = `chassis-texture-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
+ defs
+ .append("pattern")
+ .attr("id", textureId)
+ .attr("patternUnits", "userSpaceOnUse")
+ .attr("width", 8)
+ .attr("height", 8);
+ const texturePattern = defs.select(`#${textureId}`);
+ texturePattern
+ .append("rect")
+ .attr("width", 8)
+ .attr("height", 8)
+ .attr("fill", "#6f6248");
+ texturePattern
+ .append("circle")
+ .attr("cx", 2)
+ .attr("cy", 2)
+ .attr("r", 1)
+ .attr("fill", "#5a4f3b")
+ .attr("opacity", 0.5);
+ texturePattern
+ .append("circle")
+ .attr("cx", 6)
+ .attr("cy", 6)
+ .attr("r", 1)
+ .attr("fill", "#4a4232")
+ .attr("opacity", 0.45);
+
+ // Main body
+ nodeG
+ .append("rect")
+ .attr("class", "node-outline")
+ .attr("x", chassisX)
+ .attr("y", y)
+ .attr("width", chassisWidth)
+ .attr("height", iconBaseHeight)
+ .attr("rx", cornerRadius)
+ .attr("fill", `url(#${textureId})`)
+ .attr("stroke", wireColor)
+ .attr("stroke-width", strokeWidth);
+
+ // Side border accents
+ const sideThickness = iconBaseWidth * 0.02;
+ nodeG
+ .append("rect")
+ .attr("x", chassisX)
+ .attr("y", y)
+ .attr("width", sideThickness)
+ .attr("height", iconBaseHeight)
+ .attr("fill", "#8a7a56");
+ nodeG
+ .append("rect")
+ .attr("x", chassisX + chassisWidth - sideThickness)
+ .attr("y", y)
+ .attr("width", sideThickness)
+ .attr("height", iconBaseHeight)
+ .attr("fill", "#8a7a56");
+
+ // Memory fill (bottom up)
+ if (ramUsagePercent > 0) {
+ const memFillHeight = (ramUsagePercent / 100) * iconBaseHeight;
+ nodeG
+ .append("rect")
+ .attr("x", x)
+ .attr("y", y + iconBaseHeight - memFillHeight)
+ .attr("width", iconBaseWidth)
+ .attr("height", memFillHeight)
+ .attr("fill", "rgba(255,215,0,0.45)")
+ .attr("clip-path", `url(#${dgxClipId})`);
+ }
+
+ // Side handles with inner recess
+ const handleWidth = iconBaseWidth * 0.27;
+ const handleGap = iconBaseHeight * 0.05;
+ const handleHeight = iconBaseHeight - handleGap * 2;
+ const handleY = y + handleGap;
+ const innerHandleWidth = iconBaseWidth * 0.12;
+ const innerHandleHeight = handleHeight - iconBaseHeight * 0.06;
+ const leftHandleX = x + 4;
+ const rightHandleX = x + iconBaseWidth - handleWidth - 4;
+
+ // Left handle
+ nodeG
+ .append("rect")
+ .attr("x", leftHandleX)
+ .attr("y", handleY)
+ .attr("width", handleWidth)
+ .attr("height", handleHeight)
+ .attr("rx", 2.4)
+ .attr("fill", "#b3a170")
+ .attr("stroke", "#403723")
+ .attr("stroke-width", 0.7);
+ nodeG
+ .append("rect")
+ .attr("x", leftHandleX + handleWidth * 0.06)
+ .attr("y", handleY + iconBaseHeight * 0.03)
+ .attr("width", innerHandleWidth)
+ .attr("height", innerHandleHeight)
+ .attr("rx", 1.6)
+ .attr("fill", "#8a7a56");
+
+ // Right handle
+ nodeG
+ .append("rect")
+ .attr("x", rightHandleX)
+ .attr("y", handleY)
+ .attr("width", handleWidth)
+ .attr("height", handleHeight)
+ .attr("rx", 2.4)
+ .attr("fill", "#b3a170")
+ .attr("stroke", "#403723")
+ .attr("stroke-width", 0.7);
+ nodeG
+ .append("rect")
+ .attr(
+ "x",
+ rightHandleX + handleWidth - innerHandleWidth - handleWidth * 0.08,
+ )
+ .attr("y", handleY + iconBaseHeight * 0.03)
+ .attr("width", innerHandleWidth)
+ .attr("height", innerHandleHeight)
+ .attr("rx", 1.6)
+ .attr("fill", "#8a7a56");
+
+ // NVIDIA logo + text label (rotated 90 deg on left handle)
+ const badgeWidth = iconBaseWidth * 0.09;
+ const badgeHeight = handleHeight * 0.5;
+ const badgeX =
+ leftHandleX + handleWidth - badgeWidth - handleWidth * 0.06;
+ const badgeY = handleY + (handleHeight - badgeHeight) / 2;
+ const textSize = badgeWidth * 0.58;
+ const logoWidth = textSize * 1.2;
+ const logoHeight = logoWidth * (1.438 / 2.174);
+ const centerX = badgeX + badgeWidth / 2 - badgeWidth * 0.03;
+ const centerY = badgeY + badgeHeight / 2;
+ const gap = badgeWidth * 0.15;
+ const totalWidth = logoWidth + gap + textSize * 3.6;
+
+ const labelGroup = nodeG
+ .append("g")
+ .attr("transform", `rotate(90 ${centerX} ${centerY})`);
+
+ labelGroup
+ .append("svg")
+ .attr("x", centerX - totalWidth / 2)
+ .attr("y", centerY - logoHeight / 2)
+ .attr("width", logoWidth)
+ .attr("height", logoHeight)
+ .attr("viewBox", "0 0 2.174 1.438")
+ .append("path")
+ .attr("d", NVIDIA_LOGO_PATH)
+ .attr("fill", "#76b900");
+
+ labelGroup
+ .append("text")
+ .attr("x", centerX - totalWidth / 2 + logoWidth + gap)
+ .attr("y", centerY)
+ .attr("text-anchor", "start")
+ .attr("dominant-baseline", "middle")
+ .attr("fill", "#8a7a56")
+ .attr("font-size", textSize)
+ .attr("font-family", "monospace")
+ .attr("font-weight", "700")
+ .text("NVIDIA");
+ } else if (isLinux) {
+ // Linux — Tux penguin
+ iconBaseWidth = nodeRadius * 1.0;
+ iconBaseHeight = nodeRadius * 1.1;
+ const tuxSize = Math.min(iconBaseWidth, iconBaseHeight);
+ const scale = tuxSize / 100;
+ const tx = nodeInfo.x - (100 * scale) / 2;
+ const ty = nodeInfo.y - (100 * scale) / 2;
+
+ // Body (dark outline)
+ nodeG
+ .append("path")
+ .attr("class", "node-outline")
+ .attr("d", TUX_BODY_PATH)
+ .attr("transform", `translate(${tx}, ${ty}) scale(${scale})`)
+ .attr("fill", "#1a1a1a")
+ .attr("stroke", wireColor)
+ .attr("stroke-width", strokeWidth / scale);
+
+ // White belly
+ nodeG
+ .append("path")
+ .attr("d", TUX_BELLY_PATH)
+ .attr("transform", `translate(${tx}, ${ty}) scale(${scale})`)
+ .attr("fill", "rgba(220,220,220,0.85)");
+
+ // Memory fill on belly (from bottom up)
+ if (ramUsagePercent > 0) {
+ const bellyClipId = `tux-belly-${nodeInfo.id.replace(/[^a-zA-Z0-9]/g, "-")}`;
+ defs
+ .append("clipPath")
+ .attr("id", bellyClipId)
+ .append("path")
+ .attr("d", TUX_BELLY_PATH)
+ .attr("transform", `translate(${tx}, ${ty}) scale(${scale})`);
+
+ const bellyTop = ty + 37 * scale;
+ const bellyHeight = 36 * scale;
+ const memHeight = (ramUsagePercent / 100) * bellyHeight;
+ nodeG
+ .append("rect")
+ .attr("x", tx + 38 * scale)
+ .attr("y", bellyTop + bellyHeight - memHeight)
+ .attr("width", 24 * scale)
+ .attr("height", memHeight)
+ .attr("fill", "rgba(255,215,0,0.75)")
+ .attr("clip-path", `url(#${bellyClipId})`);
+ }
+
+ // Eyes
+ const eyeRadius = 2.5 * scale;
+ nodeG
+ .append("circle")
+ .attr("cx", tx + 44 * scale)
+ .attr("cy", ty + 16 * scale)
+ .attr("r", eyeRadius)
+ .attr("fill", "white");
+ nodeG
+ .append("circle")
+ .attr("cx", tx + 56 * scale)
+ .attr("cy", ty + 16 * scale)
+ .attr("r", eyeRadius)
+ .attr("fill", "white");
+ // Pupils
+ nodeG
+ .append("circle")
+ .attr("cx", tx + 44 * scale)
+ .attr("cy", ty + 16 * scale)
+ .attr("r", eyeRadius * 0.5)
+ .attr("fill", "#1a1a1a");
+ nodeG
+ .append("circle")
+ .attr("cx", tx + 56 * scale)
+ .attr("cy", ty + 16 * scale)
+ .attr("r", eyeRadius * 0.5)
+ .attr("fill", "#1a1a1a");
+
+ // Beak
+ nodeG
+ .append("path")
+ .attr(
+ "d",
+ `M${tx + 46 * scale} ${ty + 22 * scale} L${tx + 50 * scale} ${ty + 27 * scale} L${tx + 54 * scale} ${ty + 22 * scale} Z`,
+ )
+ .attr("fill", "#E8A317");
+
+ // Feet
+ nodeG
+ .append("ellipse")
+ .attr("cx", tx + 42 * scale)
+ .attr("cy", ty + 94 * scale)
+ .attr("rx", 6 * scale)
+ .attr("ry", 2.5 * scale)
+ .attr("fill", "#E8A317");
+ nodeG
+ .append("ellipse")
+ .attr("cx", tx + 58 * scale)
+ .attr("cy", ty + 94 * scale)
+ .attr("rx", 6 * scale)
+ .attr("ry", 2.5 * scale)
+ .attr("fill", "#E8A317");
+ } else if (modelLower === "mac studio") {
// Mac Studio - classic cube with memory fill
iconBaseWidth = nodeRadius * 1.25;
iconBaseHeight = nodeRadius * 0.85;
@@ -1182,8 +1486,12 @@
debugLabelY += debugLineHeight;
}
- const identity = identitiesData[nodeInfo.id];
- if (identity?.osVersion) {
+ const dbgIdentity = identitiesData[nodeInfo.id];
+ if (dbgIdentity?.osVersion) {
+ const osLabel =
+ dbgIdentity.osVersion === "Linux"
+ ? "Linux"
+ : `macOS ${dbgIdentity.osVersion}${dbgIdentity.osBuildVersion ? ` (${dbgIdentity.osBuildVersion})` : ""}`;
nodeG
.append("text")
.attr("x", nodeInfo.x)
@@ -1192,9 +1500,7 @@
.attr("fill", "rgba(179,179,179,0.7)")
.attr("font-size", debugFontSize)
.attr("font-family", "SF Mono, Monaco, monospace")
- .text(
- `macOS ${identity.osVersion}${identity.osBuildVersion ? ` (${identity.osBuildVersion})` : ""}`,
- );
+ .text(osLabel);
}
}
});
diff --git a/src/exo/utils/info_gatherer/system_info.py b/src/exo/utils/info_gatherer/system_info.py
index 0cc13f9c..ad8a6754 100644
--- a/src/exo/utils/info_gatherer/system_info.py
+++ b/src/exo/utils/info_gatherer/system_info.py
@@ -1,6 +1,7 @@
import platform
import socket
import sys
+from pathlib import Path
from subprocess import CalledProcessError
import psutil
@@ -117,12 +118,84 @@ async def get_network_interfaces() -> list[NetworkInterfaceInfo]:
return interfaces_info
+def _read_dmi_field(name: str) -> str | None:
+ """Read a single DMI sysfs field, returning ``None`` on failure."""
+ try:
+ path = Path(f"/sys/class/dmi/id/{name}")
+ if path.exists():
+ return path.read_text().strip()
+ except (OSError, PermissionError):
+ pass
+ return None
+
+
+async def _get_linux_model_and_chip() -> tuple[str, str]:
+ """Get Linux system information using DMI and /proc/cpuinfo.
+
+ Detects NVIDIA DGX Spark (DMI product_name ``"DGX_Spark"``) and other
+ NVIDIA systems via the ``sys_vendor`` DMI field, falling back to generic
+ Linux identification.
+ """
+ model = "Linux"
+ chip = "Unknown Chip"
+
+ product_name = _read_dmi_field("product_name")
+ sys_vendor = _read_dmi_field("sys_vendor")
+
+ # DGX Spark: DMI product_name may be "DGX_Spark" or "gx10" variant
+ product_lower = (product_name or "").lower()
+ if product_name and ("dgx" in product_lower or "gx10" in product_lower):
+ model = "DGX Spark"
+ try:
+ process = await run_process(
+ ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"]
+ )
+ gpu_name = process.stdout.decode().strip().split("\n")[0]
+ chip = gpu_name if gpu_name and gpu_name != "[N/A]" else "NVIDIA GB10"
+ except (CalledProcessError, FileNotFoundError):
+ chip = "NVIDIA GB10"
+ return (model, chip)
+
+ # Other NVIDIA systems (sys_vendor contains "NVIDIA")
+ if sys_vendor and "NVIDIA" in sys_vendor:
+ model = product_name.replace("_", " ") if product_name else "NVIDIA System"
+ try:
+ process = await run_process(
+ ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"]
+ )
+ gpu_name = process.stdout.decode().strip().split("\n")[0]
+ if gpu_name and gpu_name != "[N/A]":
+ chip = gpu_name
+ except (CalledProcessError, FileNotFoundError):
+ pass
+ return (model, chip)
+
+ # Generic Linux — use /proc/cpuinfo for chip
+ cpuinfo_path = Path("/proc/cpuinfo")
+ if cpuinfo_path.exists():
+ try:
+ for line in cpuinfo_path.read_text().splitlines():
+ if line.startswith("model name"):
+ chip = line.split(":", 1)[1].strip()
+ break
+ except OSError:
+ pass
+
+ return (model, chip)
+
+
async def get_model_and_chip() -> tuple[str, str]:
- """Get Mac system information using system_profiler."""
+ """Get system model and chip information.
+
+ On macOS, uses ``system_profiler``. On Linux, reads DMI data from
+ sysfs and CPU info from ``/proc/cpuinfo``.
+ """
model = "Unknown Model"
chip = "Unknown Chip"
- # TODO: better non mac support
+ if sys.platform == "linux":
+ return await _get_linux_model_and_chip()
+
if sys.platform != "darwin":
return (model, chip)
diff --git a/tmp/spark/spark-build-apple-cdc-ncm.sh b/tmp/spark/spark-build-apple-cdc-ncm.sh
index f83f4778..0b21c8b8 100644
--- a/tmp/spark/spark-build-apple-cdc-ncm.sh
+++ b/tmp/spark/spark-build-apple-cdc-ncm.sh
@@ -14,11 +14,14 @@ DSC="${SRCNAME}_${SRCVER}.dsc"
KBUILD="/lib/modules/${KVER}/build"
msg() { printf '\n==> %s\n' "$*"; }
-die() { echo "ERROR: $*" >&2; exit 1; }
+die() {
+ echo "ERROR: $*" >&2
+ exit 1
+}
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"; }
[[ $EUID -eq 0 ]] || die "Run with sudo"
-[[ -d "$KBUILD" ]] || die "Missing kernel headers/build dir: $KBUILD"
+[[ -d $KBUILD ]] || die "Missing kernel headers/build dir: $KBUILD"
need_cmd curl
need_cmd dpkg-source
@@ -52,13 +55,13 @@ cd "$WORKDIR"
msg "Downloading exact source package for ${SRCNAME} ${SRCVER}"
curl -fL "${BASE_URL}/${ORIG}" -o "${ORIG}"
curl -fL "${BASE_URL}/${DIFF}" -o "${DIFF}"
-curl -fL "${BASE_URL}/${DSC}" -o "${DSC}"
+curl -fL "${BASE_URL}/${DSC}" -o "${DSC}"
msg "Extracting source"
dpkg-source -x "${DSC}"
SRCDIR="$(find . -maxdepth 1 -mindepth 1 -type d -name "${SRCNAME}-*" | head -n1)"
-[[ -n "${SRCDIR}" ]] || die "Could not find extracted source directory"
+[[ -n ${SRCDIR} ]] || die "Could not find extracted source directory"
cd "${SRCDIR}"
[[ -f drivers/net/usb/cdc_ncm.c ]] || die "cdc_ncm.c not found in source tree"
@@ -127,7 +130,7 @@ msg "Preparing out-of-tree build dir"
mkdir -p buildmod
cp drivers/net/usb/cdc_ncm.c buildmod/
-cat > buildmod/Makefile <<'EOF'
+cat >buildmod/Makefile <<'EOF'
obj-m += cdc_ncm.o
ccflags-y += -Wno-error
@@ -142,7 +145,7 @@ msg "Building patched module"
cd buildmod
make -C "$KBUILD" M="$PWD" modules
PATCHED_KO="$PWD/cdc_ncm.ko"
-[[ -f "$PATCHED_KO" ]] || die "Patched cdc_ncm.ko was not built"
+[[ -f $PATCHED_KO ]] || die "Patched cdc_ncm.ko was not built"
msg "Patched module info"
modinfo "$PATCHED_KO" | sed -n '1,20p'