Technology Solutions for Everyday Folks
Screenshot of a child task sequence for updating firmware in Full OS mode, with a Run PowerShell Script step highlighted with package and script information.

Addressing Firmware Updates for Dell Latitude 54X0 in a Task Sequence

For about two years we fought with getting firmware (BIOS) updates to install on our Dell Latitude 54X0 models during their build/rebuild using a MEMCM task sequence. No matter what random trick I tried or thing I read, I just couldn't get the update executable to successfully apply the update in our primary build/refresh task sequence. Our techs (self included) would have to apply the update manually after devices were [re]built.

When we encountered a problem that required a firmware update to help solve, it called the question to properly solve this issue. The solution I crafted was to leverage configuration items and baselines to identify devices in scope and offer the update executable (wrapped in a little Powershell) and offered as an available installation via Software Center. We chose to not require the install in lieu of more direct control of the reboot phase of the update. My last post walks through the details of that framework.

While the workaround above was "fine," I was still perplexed angered by the fact that just one model in our fleet (at first, the Latitude 5400) would NEVER properly update the firmware in our task sequence...so during process improvement tasks I was undertaking for the summer 2022 refresh window, I once again decided to tackle this beast.

The Existing Process

Our primary [re]build task sequence is a pretty broad "one size fits all" model where logic, basic tech input, and other derived variables determine the full build process for a given workstation. This allows us to selectively and automatically deploy the "right" things to a disparate set of use cases without requiring a lot of understanding by our techs. Specifically, it's helpful for handling all the nuance with and between our "high affinity" and "multi-user" use cases for employee and public/lab environments, respectfully.

A major part of this task sequence that has long been "automated" is the automatic upgrade of device firmware. This happens quite early in the task sequence (the WinPE phase) and is balanced to address our common models with options to handle our "one-off" models equally. The process has worked quite well for many years and was originally adapted stolen from the work of Mark Godfrey and Gary Blok during their MNIT days. I've rolled this entire process into a child task sequence for ease of long-term maintenance and reuse.

The Latitude 5400 (and models since such as Latitude 5410, 5420, 5430, OptiPlex 5090, 5000), however, no longer requires the Dell's Flash64W.exe utility to apply the firmware update package in WinPE. That change wasn't obvious the first year we had 5400's, though, and due to time constraints we just winged it and did it manually as a post-build step. It wasn't until we deployed the 5420 model I discovered and modified the process to call the exe's directly versus through Flash64W.

Still, no matter what I tried the Latitude 5400 update would never work in the task sequence. The log data was super sparse, throwing a generic exit code of 10 (in most cases)...which conveniently didn't equate to what we'd previously encountered for an exit code 10 (low battery/no AC power). Insert eyeroll here.

So Much Trial and Error

With the dearth of log data or useful/obvious exit code information (not to mention a complete lack of any documentation from Dell on the matter), cracking this nut was going to be a matter of much trial and error. Mostly error.

I'd tried wrapping extra Powershell around the process to catch other typos or whatnot. No bueno. Use Flash64W.exe? Nope. Try newer/different/all the versions I could find of Flash64W.exe...all to no avail. Re-download the exe? Nope. Verify the payload exe was good? Didn't help.

A New Attempt

For the summer 2022 refresh cycle, I focused on improving the logic and reducing the tech debt/maintenance burden on the existing firmware update process...mostly through further generalizing the Powershell scripts I'd been using to deploy updates. The goal was to make a more reusable framework of a single "repository" for all supported models that could be used both in the refresh cycle and as advertised options via Software Center. As one of the "no Flash64W.exe required" models, the 5400 was back on the block for evaluation.

I was able to get the new generalized update process working for the 5400 via Software Center, but once again no matter what options I tried (now two years since the beginning and SEVERAL firmware versions later)...it still just wouldn't work in the task sequence. I was on-site to do a few "Hail Mary" tests and they all failed so I was relegated to just using our backup plan (Software Center). That same night as I was staging all of the production changes I did one last Google search on the topic. Of course, very few viable results were returned...EXCEPT this one random Reddit thread.

My "Duh" Moment

Someone on Reddit posted a question along the lines of what I'd been encountering (failed to update in WinPE). The unhelpful Internet was "don't do it that way, then..." which, while an asshole response for the thread question, was my "A-HA!" moment.

I had a process completely working in the Full OS (e.g. Windows proper) environment. But the task sequence always applied updates in the WinPE stage, and I was convinced this was the only way forward in our fleet/environment.

"What if I added a selective group to the Full OS phase ... used the same logic over there ... just for the Latitude 5400?"

Holy Shit, It Worked!

As it turned out, making this minor selective change actually fixed the problem! I'd been so fixated on making this work in the WinPE phase just because that's where the child task sequence I've been using for years handles all of the other models (and business like setting passwords, etc.). While I'd like to keep the similar logic and bits together for ease of long-term maintenance, I am totally NOT ABOVE leaving a little selective group of commands in a different part of the task sequence if it solves this particular problem. It also eliminates the need for follow-up by anyone, which is well worth the mostly unseen dirtying of the task sequence.

I have since crafted some preflight scripts and checks to classify devices in scope for WinPE or Full OS phases of the firmware updates, so while there are now two child task sequences (one for WinPE and one for Full OS), when looking at the task sequence steps as a whole it's much more obvious what's going on and less technical debt, especially when we presume the devices currently receiving updates in the WinPE phase will be dropping out of support in our environment over the next few years.

The Full OS Update Application

Much like what I designed for the fleet at large in the last post, I added a script "Program" specifically for "Full OS" to the Package for firmware updates:

# Define Model List and Versions
$ModelList = @{
  '5430' = '0001.00xx.00yy'
  '5000' = '0001.00xx.00yy'
}

# BIOS Password (section redacted)
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secString)
$BIOSpassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

# Invoke BIOS Update if necesary
try {
  if (((Get-WmiObject -Class Win32_ComputerSystem).Manufacturer).StartsWith("Dell", $true, $null)) {
    $ModelNumber = (Get-WmiObject Win32_ComputerSystem).Model -replace '\D+(\d+)', '$1'
    if ($ModelList.ContainsKey($ModelNumber)) {
      #Set Command/Arguments for BIOS Update
      $ComputerModel = Get-WmiObject -Class Win32_ComputerSystem | 
          Select-Object -ExpandProperty Model
      $ModelPath = $ComputerModel.trim() -Replace " ",""
      $BiosFileName = Get-ChildItem "$PSScriptRoot\$ModelPath\*.exe" -Verbose | 
          Select-Object -ExpandProperty Name
      $exe = "$ModelPath\$BiosFileName"
      $LogPath = "C:\Windows\CCM\Logs"
      $BiosLogFileName = "$BiosLogFileName.log"
      $cmdArgs = "/p=$BIOSpassword /s /f /r /bls /l=$LogPath\$BiosLogFileName"

      #Invoke BIOS Update
      $Process = Start-Process "$PSScriptRoot\$exe" -ArgumentList $cmdArgs -PassThru -Wait

      Write-Output "Firmware Update Exit Code: " $Process.ExitCode
    }
  }
}
catch {
  Write-Output "ERROR: UPDATE BIOS"
}

Since we're still in the build phase of the task sequence, BitLocker is not yet enabled, and the script is simplified as the child task sequence is only invoked of all the other prerequisites (make/model/etc.) have been satisfied in the task sequence logic.

Next Steps

In a conversation I had with Mike Terrill (Mr. BIOS himself) about the excellent work he's doing on firmware update frameworks, I am considering moving all firmware updates to the Full OS phase in the future...but this will depend on how many of the older models are still receiving updates and balancing out the "the end of the WinPE phased updates is near anyway" with the minor effort of porting what's left to the Full OS side, namely testing older models to make sure things don't break.

TL;DR:

Firmware (BIOS) updates for the Dell Latitude 54X0 series wouldn't run in WinPE regardless of option, and they were a right PITA to troubleshoot. Running firmware updates in the Full OS (Windows) phase of our [re]build task sequence worked like a charm, even if it fell outside the way we've successfully been handling these for several years.

It seems so obvious in hindsight, and I can't understand why the Latitude 54X0 updates still don't work in WinPE (since its predecessors and some successors still do), but if you have/had this problem maybe my little revelation will help you too.

To that end, never give up. Continuous improvement looks ugly at times, but my mantra for many, many years has been "Is it better than yesterday?"