En DOSCLIC, a menudo nos enfrentamos a la necesidad de inventariar rápidamente las especificaciones de hardware de múltiples equipos. La búsqueda de una herramienta que ofreciera un detalle similar a HWInfo64, pero sin dependencias externas ni costos de licencia, me llevó a explorar el desarrollo de una aplicación de consola en C#.
Mi objetivo inicial era obtener información clave como el procesador (incluyendo su socket), el fabricante y modelo de la placa base, la capacidad y tipo de memoria RAM, y los detalles de almacenamiento (capacidad, fabricante y tipo, diferenciando entre NVMe, SATA y HDD).
1. Python vs. C#: La elección de la plataforma
Mi primera idea fue utilizar Python, ya que es una herramienta versátil para scripts. Sin embargo, rápidamente me di cuenta de un inconveniente importante: para que un script de Python se ejecutara en cualquier máquina, Python y sus dependencias (como la librería wmi) tendrían que estar instalados. Aunque existe la opción de empaquetar el script con PyInstaller (pyinstaller --onefile script.py), los ejecutables resultantes suelen ser voluminosos (entre 20 y 50 MB) y, en ocasiones, son marcados como sospechosos por algunos antivirus, lo cual no era ideal para una herramienta de inventario.
Fue entonces cuando consideré C# y .NET, una opción que, para entornos Windows, ofrece una integración nativa superior. La principal ventaja de C# es la capacidad de compilar una aplicación de consola en un ejecutable completamente independiente (standalone). Esto significa que el usuario final no necesita tener .NET instalado en su sistema, lo que simplifica enormemente la distribución y ejecución del programa.
dotnet publish -r win-x64 --self-contained true /p:PublishSingleFile=true
Este comando genera un único archivo .exe que incluye todas las dependencias necesarias, haciendo que la aplicación sea verdaderamente portable.
2. WMI: La puerta de acceso al hardware de Windows
La clave para acceder a la información detallada del hardware en Windows es WMI (Windows Management Instrumentation). WMI expone una vasta cantidad de datos del sistema a través de clases específicas que podemos consultar desde C# utilizando el namespace System.Management. La complejidad de obtener los datos deseados se distribuye de la siguiente manera:
- CPU y Socket: Muy fácil con
Win32_Processor. - Motherboard (fabricante y modelo): Muy fácil con
Win32_BaseBoard. - RAM (capacidad y tipo): Fácil con
Win32_PhysicalMemory. - Discos (capacidad, fabricante, tipo): Fácil con
Win32_DiskDrive. - Detección de NVMe/SATA/HDD: Media, requiere un poco de lógica adicional.
- GPU (nombre y VRAM): Fácil con
Win32_VideoController.
En total, estimé que el proyecto requeriría entre 50 y 120 líneas de código, lo cual era perfectamente manejable.
3. Primeros pasos: Extracción básica de datos
Comencé extrayendo la información más básica para cada componente. La estructura general implicaba crear un ManagementObjectSearcher con una consulta WQL (WMI Query Language) y luego iterar sobre los resultados para obtener las propiedades deseadas.
3.1. CPU y Socket
Para el procesador y su socket, la clase Win32_Processor ofrecía las propiedades Name y SocketDesignation.
var searcher = new ManagementObjectSearcher("SELECT Name, SocketDesignation FROM Win32_Processor");
foreach (ManagementObject obj in searcher.Get())
{
Console.WriteLine("PROCESADOR:");
Console.WriteLine("CPU|SOCKET");
Console.WriteLine($"{obj["Name"]}|{obj["SocketDesignation"]}");
}
Una salida típica sería:
PROCESADOR:
CPU|SOCKET
Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz|U3E1
SocketDesignation de WMI a menudo devuelve códigos genéricos (como U3E1) en lugar de nombres legibles (como LGA1200). Esto requeriría un mapeo posterior para una salida más amigable.
3.2. Motherboard
Para la placa base, utilicé la clase Win32_BaseBoard y sus propiedades Manufacturer y Product.
var searcher = new ManagementObjectSearcher("SELECT Manufacturer, Product FROM Win32_BaseBoard");
foreach (ManagementObject obj in searcher.Get())
{
Console.WriteLine("TARJETA:");
Console.WriteLine("FAB|MOD");
Console.WriteLine($"{obj["Manufacturer"]}|{obj["Product"]}");
}
3.3. RAM
La memoria RAM se obtuvo de Win32_PhysicalMemory, sumando la Capacity de cada módulo para obtener el total y extrayendo el SMBIOSMemoryType.
var searcher = new ManagementObjectSearcher("SELECT Capacity, SMBIOSMemoryType FROM Win32_PhysicalMemory");
ulong total = 0;
foreach (ManagementObject obj in searcher.Get())
{
total += (ulong)obj["Capacity"];
}
Console.WriteLine("RAM:");
Console.WriteLine("CAP|TIPO");
Console.WriteLine($"{total / (1024*1024*1024)}GB|DDR4");
SMBIOSMemoryType devuelve un código numérico (ej. 20 para DDR4), no el nombre legible directamente. Además, la frecuencia de la RAM no se obtenía en esta primera iteración.
3.4. Discos de almacenamiento
Para los discos, la clase Win32_DiskDrive proporcionaba Model y Size. La detección del tipo (NVMe/SATA/HDD) era la parte más compleja.
var searcher = new ManagementObjectSearcher("SELECT Model, Size FROM Win32_DiskDrive");
foreach (ManagementObject obj in searcher.Get())
{
Console.WriteLine("ALMACENAMIENTO:");
Console.WriteLine("CAP|FAB|TIPO");
ulong size = (ulong)obj["Size"];
string model = obj["Model"].ToString();
Console.WriteLine($"{size / (1024*1024*1024)}GB|{model}|SSD");
}
InterfaceType o analizando el Model.
4. Refinando la salida: Hacia un formato legible tipo HWInfo64
Para que la aplicación fuera realmente útil, la salida debía ser tan legible como la de HWInfo64. Esto implicaba mapear los códigos WMI a sus equivalentes humanos. Decidí implementar funciones auxiliares para esta tarea.
4.1. Mapeo de Sockets, Tipos de RAM y Discos
Creé diccionarios para traducir los códigos de socket (ej. U3E1 a LGA1200) y los tipos de memoria SMBIOS (ej. 20 a DDR4). Para los discos, combiné InterfaceType, MediaType y el propio Model para inferir si era NVMe, SATA o HDD.
InterfaceType es "PCI" o su MediaType es "NVMe". Si es "SCSI" o "SATA", es SATA. De lo contrario, se asume HDD.
Con estos mapeos, el código inicial se transformó en una versión más completa y legible:
using System;
using System.Management;
using System.Collections.Generic;
class HardwareInfo
{
static void Main()
{
Console.WriteLine(GetCpuInfo());
Console.WriteLine(GetMotherboardInfo());
Console.WriteLine(GetRamInfo());
Console.WriteLine(GetDiskInfo());
}
static string GetCpuInfo()
{
string result = "PROCESADOR:\nCPU|SOCKET\n";
var searcher = new ManagementObjectSearcher("SELECT Name, SocketDesignation FROM Win32_Processor");
foreach (ManagementObject obj in searcher.Get())
{
string cpuName = obj["Name"]?.ToString()?.Trim() ?? "Desconocido";
string socket = MapSocket(obj["SocketDesignation"]?.ToString()?.Trim());
result += $"{cpuName}|{socket}";
}
return result;
}
static string GetMotherboardInfo()
{
string result = "\nTARJETA:\nFAB|MOD\n";
var searcher = new ManagementObjectSearcher("SELECT Manufacturer, Product FROM Win32_BaseBoard");
foreach (ManagementObject obj in searcher.Get())
{
string fab = obj["Manufacturer"]?.ToString()?.Trim() ?? "Desconocido";
string mod = obj["Product"]?.ToString()?.Trim() ?? "Desconocido";
result += $"{fab}|{mod}";
}
return result;
}
static string GetRamInfo()
{
string result = "\nRAM:\nCAP|TIPO\n";
var searcher = new ManagementObjectSearcher("SELECT Capacity, SMBIOSMemoryType FROM Win32_PhysicalMemory");
ulong totalBytes = 0;
string type = "Desconocido";
foreach (ManagementObject obj in searcher.Get())
{
totalBytes += (ulong)obj["Capacity"];
if (type == "Desconocido")
{
type = MapMemoryType(Convert.ToInt32(obj["SMBIOSMemoryType"]));
}
}
ulong totalGB = totalBytes / (1024 * 1024 * 1024);
result += $"{totalGB}GB|{type}";
return result;
}
static string GetDiskInfo()
{
string result = "\nALMACENAMIENTO:\nCAP|FAB|TIPO\n";
var searcher = new ManagementObjectSearcher("SELECT Model, Size, InterfaceType, MediaType FROM Win32_DiskDrive");
foreach (ManagementObject obj in searcher.Get())
{
ulong sizeGB = (ulong)obj["Size"] / (1024 * 1024 * 1024);
string fab = obj["Model"]?.ToString()?.Trim() ?? "Desconocido";
string type = MapDiskType(obj["InterfaceType"]?.ToString(), obj["MediaType"]?.ToString(), fab);
result += $"{sizeGB}GB|{fab}|{type}\n";
}
return result.TrimEnd();
}
// Map WMI socket codes to human-readable socket names
static string MapSocket(string socket)
{
if (string.IsNullOrEmpty(socket)) return "Desconocido";
var map = new Dictionary()
{
{"U3E1","LGA1200"}, {"LGA1151","LGA1151"}, {"AM4","AM4"}, {"TR4","TR4"}
};
return map.ContainsKey(socket) ? map[socket] : socket;
}
// Map SMBIOS memory type to DDR type
static string MapMemoryType(int type)
{
var map = new Dictionary()
{
{20,"DDR4"},
{21,"DDR3"},
{22,"DDR2"},
{24,"DDR"},
{26,"DDR5"}
};
return map.ContainsKey(type) ? map[type] : "Desconocido";
}
// Map disk interface/media to human-readable type
static string MapDiskType(object interfaceTypeObj, object mediaTypeObj, string model)
{
string iface = interfaceTypeObj?.ToString() ?? "";
string media = mediaTypeObj?.ToString() ?? "";
if (model.ToUpper().Contains("NVME") || iface.ToUpper().Contains("PCI") || media.ToUpper().Contains("NVME"))
return "NVMe";
else if (iface.ToUpper().Contains("SCSI") || iface.ToUpper().Contains("SATA"))
return "SATA";
else
return "HDD";
}
}
Con esta versión, la salida ya era mucho más útil y se asemejaba al formato deseado:
PROCESADOR:
CPU|SOCKET
Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz|LGA1200
TARJETA:
FAB|MOD
GIGABYTE|H410M H V3
RAM:
CAP|TIPO
8GB|DDR4
ALMACENAMIENTO:
CAP|FAB|TIPO
250GB|ADATA|NVMe
5. La versión "HWInfo64-like" completa: Más allá de lo básico
Para alcanzar un nivel de detalle comparable al de HWInfo64 Pro, decidí ampliar la información recolectada para cada componente. Esto implicó añadir más propiedades a las consultas WMI y refinar la lógica de presentación.
5.1. CPU: Núcleos e Hilos
Añadí NumberOfCores y NumberOfLogicalProcessors a la consulta de Win32_Processor para obtener el recuento de núcleos e hilos.
5.2. Motherboard: Chipset
Para la placa base, incluí la propiedad Version de Win32_BaseBoard, que a menudo contiene información del chipset o la revisión.
5.3. RAM: Frecuencia y Slots Usados
La clase Win32_PhysicalMemory también ofrece la propiedad Speed para la frecuencia. Además, conté el número de objetos Win32_PhysicalMemory para saber cuántos slots de RAM estaban ocupados.
5.4. GPU: Nombre y VRAM
Para la tarjeta gráfica, la clase Win32_VideoController es ideal, proporcionando Name y AdapterRAM.
El código final, que incorpora todas estas mejoras y los mapeos necesarios, se presenta a continuación:
using System;
using System.Management;
using System.Collections.Generic;
class HardwareInventory
{
static void Main()
{
Console.WriteLine(GetCpuInfo());
Console.WriteLine(GetMotherboardInfo());
Console.WriteLine(GetRamInfo());
Console.WriteLine(GetDiskInfo());
Console.WriteLine(GetGpuInfo());
}
// ================= CPU =================
static string GetCpuInfo()
{
string result = "PROCESADOR:\nCPU|SOCKET|NUCLEOS|HILOS\n";
var searcher = new ManagementObjectSearcher("SELECT Name, SocketDesignation, NumberOfCores, NumberOfLogicalProcessors FROM Win32_Processor");
foreach (ManagementObject obj in searcher.Get())
{
string name = obj["Name"]?.ToString()?.Trim() ?? "Desconocido";
string socket = MapSocket(obj["SocketDesignation"]?.ToString()?.Trim());
string cores = obj["NumberOfCores"]?.ToString() ?? "0";
string threads = obj["NumberOfLogicalProcessors"]?.ToString() ?? "0";
result += $"{name}|{socket}|{cores}|{threads}";
}
return result;
}
// ================= Motherboard =================
static string GetMotherboardInfo()
{
string result = "\nTARJETA:\nFAB|MOD|CHIPSET\n";
var searcher = new ManagementObjectSearcher("SELECT Manufacturer, Product, Version FROM Win32_BaseBoard");
foreach (ManagementObject obj in searcher.Get())
{
string fab = obj["Manufacturer"]?.ToString()?.Trim() ?? "Desconocido";
string mod = obj["Product"]?.ToString()?.Trim() ?? "Desconocido";
string chipset = obj["Version"]?.ToString()?.Trim() ?? "Desconocido";
result += $"{fab}|{mod}|{chipset}";
}
return result;
}
// ================= RAM =================
static string GetRamInfo()
{
string result = "\nRAM:\nCAP_TOTAL|TIPO|FRECUENCIA|SLOTS_USADOS\n";
var searcher = new ManagementObjectSearcher("SELECT Capacity, SMBIOSMemoryType, Speed FROM Win32_PhysicalMemory");
ulong totalBytes = 0;
int slotsUsed = 0;
string type = "Desconocido";
int frequency = 0;
foreach (ManagementObject obj in searcher.Get())
{
totalBytes += (ulong)obj["Capacity"];
slotsUsed++;
if (type == "Desconocido")
type = MapMemoryType(Convert.ToInt32(obj["SMBIOSMemoryType"]));
if (frequency == 0)
frequency = Convert.ToInt32(obj["Speed"]);
}
ulong totalGB = totalBytes / (1024 * 1024 * 1024);
result += $"{totalGB}GB|{type}|{frequency}MHz|{slotsUsed}";
return result;
}
// ================= Discos =================
static string GetDiskInfo()
{
string result = "\nALMACENAMIENTO:\nCAP|FAB|TIPO\n";
var searcher = new ManagementObjectSearcher("SELECT Model, Size, InterfaceType, MediaType FROM Win32_DiskDrive");
foreach (ManagementObject obj in searcher.Get())
{
ulong sizeGB = (ulong)obj["Size"] / (1024 * 1024 * 1024);
string fab = obj["Model"]?.ToString()?.Trim() ?? "Desconocido";
string type = MapDiskType(obj["InterfaceType"]?.ToString(), obj["MediaType"]?.ToString(), fab);
result += $"{sizeGB}GB|{fab}|{type}\n";
}
return result.TrimEnd();
}
// ================= GPU =================
static string GetGpuInfo()
{
string result = "\nGPU:\nNOMBRE|VRAM\n";
var searcher = new ManagementObjectSearcher("SELECT Name, AdapterRAM FROM Win32_VideoController");
foreach (ManagementObject obj in searcher.Get())
{
string name = obj["Name"]?.ToString()?.Trim() ?? "Desconocido";
ulong vramGB = (ulong)obj["AdapterRAM"] / (1024 * 1024 * 1024);
result += $"{name}|{vramGB}GB\n";
}
return result.TrimEnd();
}
// ================= Helpers =================
static string MapSocket(string socket)
{
if (string.IsNullOrEmpty(socket)) return "Desconocido";
var map = new Dictionary()
{
{"U3E1","LGA1200"}, {"LGA1151","LGA1151"}, {"AM4","AM4"}, {"TR4","TR4"}
};
return map.ContainsKey(socket) ? map[socket] : socket;
}
static string MapMemoryType(int type)
{
var map = new Dictionary()
{
{20,"DDR4"}, {21,"DDR3"}, {22,"DDR2"}, {24,"DDR"}, {26,"DDR5"}
};
return map.ContainsKey(type) ? map[type] : "Desconocido";
}
static string MapDiskType(object ifaceObj, object mediaObj, string model)
{
string iface = ifaceObj?.ToString() ?? "";
string media = mediaObj?.ToString() ?? "";
if (model.ToUpper().Contains("NVME") || iface.ToUpper().Contains("PCI") || media.ToUpper().Contains("NVME"))
return "NVMe";
else if (iface.ToUpper().Contains("SCSI") || iface.ToUpper().Contains("SATA"))
return "SATA";
else
return "HDD";
}
}
La salida de esta versión es mucho más completa y comparable a una herramienta de inventario profesional:
PROCESADOR:
CPU|SOCKET|NUCLEOS|HILOS
Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz|LGA1200|6|12
TARJETA:
FAB|MOD|CHIPSET
GIGABYTE|H410M H V3|Rev1.0
RAM:
CAP_TOTAL|TIPO|FRECUENCIA|SLOTS_USADOS
16GB|DDR4|2666MHz|2
ALMACENAMIENTO:
CAP|FAB|TIPO
512GB|Samsung SSD 970 EVO|NVMe
GPU:
NOMBRE|VRAM
NVIDIA GeForce GTX 1660 Ti|6GB
6. Conclusiones y próximos pasos
Este proyecto demostró que es totalmente factible crear una herramienta de inventario de hardware detallada en C# utilizando WMI, sin depender de software de terceros ni de licencias. La capacidad de generar un ejecutable auto-contenido es una ventaja significativa para la distribución en entornos empresariales.
Aunque la aplicación actual ya es muy robusta, existen posibilidades de expansión. Se podría integrar la detección de sensores de temperatura, velocidad de ventiladores (quizás con librerías como OpenHardwareMonitor para datos más allá de WMI), o incluso añadir opciones para exportar los datos directamente a formatos como CSV o JSON, o enviarlos a una API para un sistema de inventario centralizado.
La historia detrás de la nota
Fue gratificante descubrir que podíamos replicar la funcionalidad de herramientas de pago como HWInfo64 Pro, pero con una solución propia, ligera y completamente gratuita. La flexibilidad de C# y la potencia de WMI abren un mundo de posibilidades para la gestión de activos en Windows.