7

In Delphi 7 I'm working on a library implementing an object encapsulating information about the batteries attached to a system. It's working well, except for retrieving the serial number for the battery.

The code I am using for this call is as follows:

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;

Unfortunately, DeviceIoControl() always returns False and if I check GetLastError() afterwards then it comes back with error 87, "the parameter is incorrect."

This doesn't make much sense, because the code works perfectly well if I simply change the InformationLevel from BatterySerialNumber to BatteryUniqueID, say. Also, I've used the handle to the battery (hbat) in other calls in the code before GetSerialNumber and they all work fine, and I can call others after this one fails as well, so that's not the issue.

Any ideas? I'm really at a loss.

Restless
  • 176
  • 1
  • 7
  • Are any other programs in your system able to retrieve the serial number? It could be down to a dodgy DSDT implementation, that's where the serial number is read from, in the _BIF section I think. – James May 31 '12 at 00:18
  • 1
    why you are passing the dwOut variable as @dwOut? try using this code instead `Result := DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, @bqi, SizeOf(bqi), Serial, SerialSize, dwOut, nil); ` – RRUZ May 31 '12 at 00:26
  • @RRUZ is right (it's a `var` parameter) and also, double check that `BatterySerialNumber` = 8 – Francesca May 31 '12 at 00:45
  • The reason I passed the variable as @dwOut is because the library I am used (an older translation of the Win32 API from Project Jedi) defined `DeviceIoControl()` as `function DeviceIoControl(hDevice: HANDLE; dwIoControlCode: DWORD; lpInBuffer: LPVOID; nInBufferSize: DWORD; lpOutBuffer: LPVOID; nOutBufferSize: DWORD; lpBytesReturned: LPDWORD; lpOverlapped: LPOVERLAPPED): BOOL; stdcall;`. – Restless May 31 '12 at 14:24
  • @Restless, try using the `DeviceIoControl` function of the Windows unit. – RRUZ May 31 '12 at 16:07
  • @RRUZ: I put Windows at the end of the uses clauses so it was referenced, and even changed the call to `Windows.DeviceIoControl(...)`. Same thing. If I wasn't seeing it, I wouldn't believe it. I am starting to think it'd be easier to just clean up the `BatteryUniqueID` return value to retrieve the serial number, but that's not a solution, that's a band-aid. – Restless May 31 '12 at 16:27

2 Answers2

12

The issue it seems related to the dwOut variable which is passed as @dwOut, this variable represents the var lpBytesReturned parameter of the DeviceIoControl which is defined as

function DeviceIoControl(hDevice: THandle; dwIoControlCode: DWORD; lpInBuffer: Pointer;
  nInBufferSize: DWORD; lpOutBuffer: Pointer; nOutBufferSize: DWORD;
  var lpBytesReturned: DWORD; lpOverlapped: POverlapped): BOOL; stdcall;

So replacing your code by

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

Must fix the problem.

WinAPI

Also check this code translated to delphi from this msdn entry Enumerating Battery Devices which can help you to detect any additional issues with your code.

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

Finally as aside note, you can use the WMI to retrieve the same info, in this case using the BatteryStaticData WMI class

    {$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.
RRUZ
  • 134,889
  • 20
  • 356
  • 483
  • I took the suggestions and messed with my code, I still get the same error. I also took your API-based code (since my test box doesn't have that WMI class available in that namespace) straight into a new project, compiled it and ran it... same error. I searched my box for different SetupApi.pas implementations, tried them in turn... same error, on both test boxes (one a desktop, the other a Panasonic Toughbook). What Delphi version did you build this on? Where'd you get SetupApi.pas? (I've used three versions from Project Jedi; Delphi 7 didn't come with SetupApi.pas.) – Restless May 31 '12 at 14:30
  • The WinApi code was tested in delphi 7 using the SetupApi unit from the JVCL included in the `jvcl\run` folder. – RRUZ May 31 '12 at 14:47
  • I was afraid you'd say that; that's what I used, too. I just made doubly sure by setting the SetupApi uses clause to that file explicitly, and also made sure the other included libraries weren't changed from the original dates. Still no love, which is strange since, as I mentioned, all the `IOCTL_BATTERY_QUERY_INFORMATION` calls work great... except _this_ one. – Restless May 31 '12 at 15:39
0

In summary, the code @RRUZ and I posted work fine under Windows 7, as well as other third-party applications. They do not work for retrieving the serial number under Windows XP. I've also tested under WinXP and 7 with base installs of the OS on the exact same hardware, with identical results (success under Windows 7, not under windows XP).

It appears that under WinXP the value BatterySerialNumber for IOCTL_BATTERY_QUERY_INFORMATION's InformationLevel member is not supported, but this is not documented directly in the Windows SDK docs. It is documented that invalid entries should return error 1 (ERROR_INVALID_FUNCTION) for GetLastError(), but in this case is returning 87 (for an invalid parameter) instead. I posit that this is because that value in the enumeration is not valid, so it makes the parameter invalid, but I'm not exactly sure.

Thanks to all for their help, especially @RRUZ for going way above and beyond!

(As an aside, it appears that one can extract the serial number from the battery's Unique ID (Using BatteryUniqueID as the InformationLevel member) and removing the manufacturer name and device name from the unique ID. That's a terrible hack, but it's a semi-viable workaround for Windows XP.)

Restless
  • 176
  • 1
  • 7