如何正确获取电池序列号?

7
在Delphi 7中,我正在开发一个库,实现了一个封装关于系统上连接的电池信息的对象。它运行良好,但是获取电池序列号时出现了问题。
我使用的调用代码如下:
function TBattery.GetSerialNumber(hbat: THandle): boolean;
var
  bqi:          TBatteryQueryInformation;
  Serial:       PWideChar;
  SerialSize,
  dwOut:        DWORD;
begin
  Result := False;

  if hbat <> INVALID_HANDLE_VALUE then
  begin
    ZeroMemory(@bqi, SizeOf(bqi));
    dwOut := 0;

    bqi.BatteryTag := FBatteryTag;
    bqi.InformationLevel := BatterySerialNumber;

    SerialSize := 2048;
    GetMem(Serial, SerialSize);
    try
      ZeroMemory(Serial, SerialSize);

      Result := DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, @bqi,
                                SizeOf(bqi), Serial, SerialSize, @dwOut, nil);

      if Result then
        FSerialNumber := Serial;
    finally
      FreeMem(Serial, SerialSize);
    end;
  end;
end;

很不幸,DeviceIoControl()总是返回False,如果我之后检查GetLastError(),则会返回错误87,“参数不正确”。

这并没有太多意义,因为如果我只是将InformationLevelBatterySerialNumber更改为BatteryUniqueID,那么代码就可以正常工作。此外,在GetSerialNumber之前,我已经在代码中使用了电池的句柄(hbat)进行了其他调用,并且它们都能正常工作,在这个失败的调用之后,我也可以调用其他函数,所以这不是问题所在。

有什么想法吗? 我真的很无助。


你的系统中是否有其他程序能够检索序列号?这可能是由于不良的DSDT实现引起的,因为序列号是从那里读取的,在_BIF部分。 - James
1
为什么你把dwOut变量作为@dwOut传递?试着使用这段代码代替:Result := DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, @bqi, SizeOf(bqi), Serial, SerialSize, dwOut, nil); - RRUZ
@RRUZ 是正确的(它是一个var参数),还要确认 BatterySerialNumber = 8。 - Francesca
我将变量传递为@dwOut的原因是,我使用的库(Project Jedi的旧版Win32 API翻译)将DeviceIoControl()定义为function DeviceIoControl(hDevice: HANDLE; dwIoControlCode: DWORD; lpInBuffer: LPVOID; nInBufferSize: DWORD; lpOutBuffer: LPVOID; nOutBufferSize: DWORD; lpBytesReturned: LPDWORD; lpOverlapped: LPOVERLAPPED): BOOL; stdcall; - Restless
@Restless,尝试使用Windows单元的DeviceIoControl函数。 - RRUZ
@RRUZ:我把Windows放在uses子句的末尾,这样就被引用了,甚至改变了对Windows.DeviceIoControl(...)的调用。结果还是一样。如果我没有看到它,我是不会相信的。我开始觉得更容易清理BatteryUniqueID返回值以检索序列号,但那不是解决方案,那只是一个权宜之计。 - Restless
2个回答

12
似乎问题与传递为@dwOut的变量有关,该变量代表DeviceIoControl函数的var lpBytesReturned参数。请注意保留HTML标记。
function DeviceIoControl(hDevice: THandle; dwIoControlCode: DWORD; lpInBuffer: Pointer;
  nInBufferSize: DWORD; lpOutBuffer: Pointer; nOutBufferSize: DWORD;
  var lpBytesReturned: DWORD; lpOverlapped: POverlapped): BOOL; stdcall;

所以,将您的代码替换为

  Result := DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, @bqi,
                            SizeOf(bqi), Serial, SerialSize, dwOut, nil);

必须解决问题。

WinAPI

还要检查这段从枚举电池设备的MSDN条目翻译成Delphi的代码,它可以帮助您检测代码中的任何其他问题。

uses
  SetupApi,
  Windows,
  SysUtils;

type

  BATTERY_QUERY_INFORMATION_LEVEL = (
    BatteryInformation,
    BatteryGranularityInformation,
    BatteryTemperature,
    BatteryEstimatedTime,
    BatteryDeviceName,
    BatteryManufactureDate,
    BatteryManufactureName,
    BatteryUniqueID,
    BatterySerialNumber);
  TBatteryQueryInformationLevel = BATTERY_QUERY_INFORMATION_LEVEL;

  _BATTERY_QUERY_INFORMATION = record
    BatteryTag: ULONG;
    InformationLevel: BATTERY_QUERY_INFORMATION_LEVEL;
    AtRate: Longint;
  end;
  BATTERY_QUERY_INFORMATION = _BATTERY_QUERY_INFORMATION;
  PBATTERY_QUERY_INFORMATION = ^BATTERY_QUERY_INFORMATION;
  TBatteryQueryInformation = BATTERY_QUERY_INFORMATION;


const
  GUID_DEVCLASS_BATTERY:TGUID='{72631E54-78A4-11D0-BCF7-00AA00B7B32A}';
  //DEFINE_GUID( GUID_DEVCLASS_BATTERY, 0x72631E54, 0x78A4, 0x11D0, 0xBC, 0xF7, 0x00, 0xAA, 0x00, 0xB7, 0xB3, 0x2A );
  METHOD_BUFFERED     = 0;
  FILE_DEVICE_BATTERY = $00000029;
  FILE_READ_ACCESS    = $0001;    // for files and pipes

  IOCTL_BATTERY_QUERY_TAG =
    (FILE_DEVICE_BATTERY shl 16) or (FILE_READ_ACCESS shl 14) or ($10 shl 2) or (METHOD_BUFFERED);
  IOCTL_BATTERY_QUERY_INFORMATION =
    (FILE_DEVICE_BATTERY shl 16) or (FILE_READ_ACCESS shl 14) or ($11 shl 2) or (METHOD_BUFFERED);

function GetBatteryInfo(InformationLevel : BATTERY_QUERY_INFORMATION_LEVEL) : string;
var
   cbRequired : DWORD;
   hdev     : HDEVINFO;
   idev     : Integer;
   did      : TSPDeviceInterfaceData;
   pdidd    : PSPDeviceInterfaceDetailData;
   hBattery : THandle;
   bqi      : TBatteryQueryInformation;
   dwWait, dwOut : DWORD;
   lpOutBuffer: PWideChar;
begin
  // enumerate the batteries
  hdev :=  SetupDiGetClassDevs(@GUID_DEVCLASS_BATTERY, nil, 0,  DIGCF_PRESENT OR DIGCF_DEVICEINTERFACE);
  if ( INVALID_HANDLE_VALUE <>  THandle(hdev) ) then
  begin
      idev:=0;//first battery
      ZeroMemory(@did, SizeOf(did));
      did.cbSize := SizeOf(did);
      if (SetupDiEnumDeviceInterfaces(hdev, nil, GUID_DEVCLASS_BATTERY, idev, did)) then
      begin
        try
          cbRequired := 0;
          SetupDiGetDeviceInterfaceDetail(hdev, @did, nil, 0, cbRequired, nil);
         if (ERROR_INSUFFICIENT_BUFFER= GetLastError()) then
         begin
            pdidd:=AllocMem(cbRequired);
            try
              pdidd.cbSize := SizeOf(TSPDeviceInterfaceDetailData);
              if (SetupDiGetDeviceInterfaceDetail(hdev, @did, pdidd, cbRequired, cbRequired, nil)) then
              begin
                 hBattery :=CreateFile(pdidd.DevicePath, GENERIC_READ OR GENERIC_WRITE, FILE_SHARE_READ OR FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
                 if (INVALID_HANDLE_VALUE <> hBattery) then
                 begin
                  try
                    ZeroMemory(@bqi, SizeOf(bqi));
                     // With the tag, you can query the battery info.
                    dwWait := 0;
                      if (DeviceIoControl(hBattery, IOCTL_BATTERY_QUERY_TAG,  @dwWait, sizeof(dwWait), @bqi.BatteryTag, sizeof(bqi.BatteryTag), dwOut, nil)) then
                      begin
                        lpOutBuffer:=AllocMem(MAX_PATH);
                        try
                          ZeroMemory(lpOutBuffer,MAX_PATH);
                          bqi.InformationLevel:=InformationLevel;
                          if DeviceIoControl(hBattery, IOCTL_BATTERY_QUERY_INFORMATION, @bqi, SizeOf(BATTERY_QUERY_INFORMATION), lpOutBuffer, 255, dwOut,nil) then
                            Result:= WideCharToString(lpOutBuffer);
                        finally
                          FreeMem(lpOutBuffer);
                        end;
                      end;
                  finally
                    CloseHandle(hBattery)
                  end;
                 end;
              end;
            finally
              FreeMem(pdidd);
            end;
         end;
        finally
          SetupDiDestroyDeviceInfoList(hdev);
        end;
      end;
  end;
end;

begin
  try
    if not LoadsetupAPI then exit;
     Writeln(GetBatteryInfo(BatterySerialNumber));
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  readln;
end.

WMI

最后提一下,你可以使用WMI来检索相同的信息,在这种情况下使用BatteryStaticData WMI类。

    {$APPTYPE CONSOLE}

    uses
      SysUtils,
      ActiveX,
      ComObj,
      Variants;

    // Battery Static Data

    procedure  GetBatteryStaticDataInfo;
    const
      WbemUser            ='';
      WbemPassword        ='';
      WbemComputer        ='localhost';
      wbemFlagForwardOnly = $00000020;
    var
      FSWbemLocator : OLEVariant;
      FWMIService   : OLEVariant;
      FWbemObjectSet: OLEVariant;
      FWbemObject   : OLEVariant;
      oEnum         : IEnumvariant;
      iValue        : LongWord;
    begin;
      FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
      FWMIService   := FSWbemLocator.ConnectServer(WbemComputer, 'root\WMI', WbemUser, WbemPassword);
      FWbemObjectSet:= FWMIService.ExecQuery('SELECT SerialNumber FROM BatteryStaticData','WQL',wbemFlagForwardOnly);
      oEnum         := IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
      while oEnum.Next(1, FWbemObject, iValue) = 0 do
      begin
        Writeln(Format('SerialNumber    %s',[String(FWbemObject.SerialNumber)]));// String

        Writeln('');
        FWbemObject:=Unassigned;
      end;
    end;


    begin
     try
        CoInitialize(nil);
        try
          GetBatteryStaticDataInfo;
        finally
          CoUninitialize;
        end;
     except
        on E:EOleException do
            Writeln(Format('EOleException %s %x', [E.Message,E.ErrorCode])); 
        on E:Exception do
            Writeln(E.Classname, ':', E.Message);
     end;
     Writeln('Press Enter to exit');
     Readln;      
    end.

我接受了建议并修改了我的代码,但仍然遇到相同的错误。我还将你的基于API的代码(因为我的测试环境中该命名空间中没有那个WMI类)直接放入一个新项目中,编译并运行它...但是还是出现了相同的错误。我在我的计算机上搜索了不同的SetupApi.pas实现,并轮流尝试了它们...但是在两个测试计算机(一个台式机,另一个是Panasonic Toughbook)上都遇到了相同的错误。你使用的是哪个Delphi版本进行构建的?你从哪里获取了SetupApi.pas? (我使用了Project Jedi的三个版本;Delphi 7没有附带SetupApi.pas。) - Restless
WinApi代码已在Delphi 7中测试,使用了JVCL中jvcl\run文件夹中的SetupApi单元。 - RRUZ
我担心你会这么说;那也是我使用的。我通过将SetupApi使用条款明确设置为该文件来进行了双重确认,并确保其他包含的库未更改原始日期。仍然没有爱,这很奇怪,因为正如我提到的,所有的“IOCTL_BATTERY_QUERY_INFORMATION”调用都非常好...除了这一个。 - Restless

0
总之,@RRUZ和我发布的代码在Windows 7以及其他第三方应用程序下运行良好。但是它们无法在Windows XP下检索序列号。我还在完全相同的硬件上使用基本安装的WinXP和7进行了测试,并获得了相同的结果(在Windows 7下成功,在Windows XP下失败)。
似乎在WinXP下,IOCTL_BATTERY_QUERY_INFORMATION的InformationLevel成员的BatterySerialNumber值不受支持,但是这并没有直接记录在Windows SDK文档中。文档记录了无效条目应该返回错误1(ERROR_INVALID_FUNCTION)的GetLastError(),但在这种情况下,它返回87(表示无效参数)。我认为这是因为枚举中的该值无效,因此使参数无效,但我不确定。
感谢所有人的帮助,特别是@RRUZ超出预期的帮助!
(顺便说一下,似乎可以从电池的唯一标识符中提取序列号(使用BatteryUniqueID作为InformationLevel成员),并从唯一标识符中删除制造商名称和设备名称。这是一个可怕的黑客行为,但对于Windows XP来说,它是一个半可行的解决方法。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接