Remotely Updating your Medical Devices with the LifeOmic Platform

Schaffer Stewart
Life and Tech @ LifeOmic
6 min readAug 2, 2023

As a medical device manufacturer, you know that getting your device into the hands of customers is not the end of the journey. Feature and security updates are an important part of the device lifecycle. Technician support calls can easily cost hundreds of dollars, so the ability to remotely deploy updates across your fleet of devices is imperative.

With the LifeOmic Platform, you can use the same platform that stores your device data to remotely deploy updates to your fleet of devices. This enables you to quickly add features, keep your devices secure, and save money.

This guide shows you how to update your medical devices with the LifeOmic Platform. We will build a device using M5Stack Core2, ESP32, and Arduino.

About Our Device

For this example, we use the M5Stack Core2 for AWS IoT. It is a prototyping board that uses an ESP32 microcontroller. The board comes with WiFi, Bluetooth, internal sensors, and a large ecosystem of plug-n-play hardware. This example should also work with the standard M5Core2, although that version is untested. Even though we are using a specific device for our example, the LifeOmic Platform follows industry standards and is compatible with most devices.

We will use PlatformIO (pio) to build and manage the project. For simplicity, we will assume that the device is already integrated and registered with the LifeOmic Platform. To learn more about device integration, check out this article.

Overview

You can break device updates into four parts:

  1. Prepare the device to receive and apply updates.
  2. Upload the updated firmware to the LifeOmic Platform.
  3. Create a deployment target with your update.
  4. The device receives and applies the update.

Check out our device-samples project on GitHub for the complete code for this example. For simplicity, we’ll highlight the relevant parts of the code in this article.

For a device to receive updates, it needs to subscribe to a few MQTT topics.

We will subscribe the device to its notify-next MQTT topic. It will receive a message whenever there is an update available. It will not receive another update until it processes the previous update.

// File: src/main.cpp

String JOBS_TOPIC = "$aws/things/" + String(deviceId) + "/jobs";
String JOBS_NOTIFY_NEXT = JOBS_TOPIC + "/notify-next";

// in setupMqtt() function
mqttClient.subscribe(JOBS_NOTIFY_NEXT);

We can also initiate a check for a new update by publishing a message to the DescribeExecution MQTT topic. For our device, pressing button A triggers this request.

// File: src/main.cpp

String JOBS_DESCRIBE_EXECUTION_NEXT = JOBS_TOPIC + "/$next/get";
String JOBS_DESCRIBE_EXECUTION_NEXT_ACCEPTED = JOBS_DESCRIBE_EXECUTION_NEXT + "/accepted";
String JOBS_DESCRIBE_EXECUTION_NEXT_REJECTED = JOBS_DESCRIBE_EXECUTION_NEXT + "/rejected";

// in setupMqtt() function
mqttClient.subscribe(JOBS_DESCRIBE_EXECUTION_NEXT_ACCEPTED);
mqttClient.subscribe(JOBS_DESCRIBE_EXECUTION_NEXT_REJECTED);

// in loop() function
if (M5.BtnA.wasReleased() || M5.BtnA.pressedFor(1000, 200))
{
publishDescribeExecution(); // If there is an update available, handleDescribeJobExecution will change updateState to StartUpdate
}

The publishDescribeExecution function sends a message with a jobId of $next, which will return the next available update. handleDescribeExecution handles the result of that request. If there is an update available, we will set the firmwareUrl and jobId to be used later, and we’ll change the updateState to StartUpdate.

// File: src/main.cpp

String firmwareUrl = "";
String firmwareId = "";
String jobId = "";;

void publishDescribeExecution()
{
String payload = "{\"jobId\": \"$next\", \"thingName\": \"" + String(deviceId) + "\"}";
mqttClient.publish(JOBS_DESCRIBE_EXECUTION_NEXT, payload, false, 1);
}

void handleDescribeJobExecution(String payload)
{
DynamicJsonDocument doc(2048);
DeserializationError err = deserializeJson(doc, payload);
if (err)
{
Serial.print("Deserialization Error: ");
Serial.println(err.f_str());
return;
}
const char *_jobId = doc["execution"]["jobId"];
const char *status = doc["execution"]["status"];
const char *operation = doc["execution"]["jobDocument"]["operation"];
const char *_firmwareId = doc["execution"]["jobDocument"]["firmwareId"];
const char *_firmwareUrl = doc["execution"]["jobDocument"]["firmwareUrl"];
Serial.println(_firmwareId);
Serial.println(_firmwareUrl);
Serial.println(_jobId);
if (_firmwareUrl != NULL && _jobId != NULL)
{
Serial.println("scheduling update");
firmwareId = String(_firmwareId);
firmwareUrl = String(_firmwareUrl);
jobId = _jobId;
Serial.printf("firmwareId=%s;jobId=%s\n", firmwareId.c_str(), jobId.c_str());
updateState = StartUpdate;
}
else
{
Serial.println("no job to execute");
}
}

In our loop() we will subscribe to a current job topic with the job ID provided by handleDescribeJobExecution.

// File: src/main.cpp

// in loop()

switch (updateState)
{
case StartUpdate:
currentJobTopic = JOBS_TOPIC + "/" + jobId + "/update";
Serial.printf("Current job topic %s\n", currentJobTopic.c_str());
setupMqtt(deviceId, true); // Force reconnect mqtt to subscribe to the currentJobTopic
updateState = UpdateStatusInProgress;
break;
/* ... */
}

Next, we’ll update our job status to IN_PROGRESS.

// File: src/main.cpp

// in loop()

switch (updateState)
{
/* ... */
case UpdateStatusInProgress:
publishUpdateExecution(currentJobTopic, jobId, "IN_PROGRESS");
updateState = DownloadAndApplyUpdate;
break;
/* ... */
}

Now we can download the update and apply it to our device.

// File: src/main.cpp

// in loop()

switch (updateState)
{
/* ... */
case DownloadAndApplyUpdate:
{ // new scope required because we declare the int result
int result = downloadAndApply(firmwareUrl);
if (result != 0)
{
updateState = Failure;
}
else
{
updateState = Success;
}
}
break;
/* ... */
}

The downloadAndApply function creates an HTTP stream and streams the firmware update to the device's Update library. If the update succeeds, we will proceed to the Success state. Otherwise, we will go to the Failure state.

If the update succeeded, we will update our execution status to SUCCEEDED. Then, change updateState to Restart.

// File: src/main.cpp

// in loop()

switch (updateState)
{
/* ... */
case Success:
publishUpdateExecution(currentJobTopic, jobId, "SUCCEEDED");
updateState = Restart;
break;
case Failure:
publishUpdateExecution(currentJobTopic, jobId, "FAILED");
updateState = Idle;
break;
/* ... */
}

Lastly, we must restart the device for the update to take effect.

// File: src/main.cpp

// in loop()

switch (updateState)
{
/* ... */
case Restart:
Serial.println("Restarting...");
ESP.restart();
break;
}

Upload Your Firmware

In our sample project, we modify the version number from v1.0.0 to v2.0.0, build the project, and upload the resultant firmware binary.

// File: src/main.cpp

// For demo: change this value, build the project, and upload binary. Then revert this value.
const char *version = "v2.0.0";

In a terminal run:

pio run

(Alternatively, you can execute PlatformIO: Build in your editor of choice with PlatformIO configured.)

This will build the project and output the firmware binary to ./.pio/build/m5stack-core2/firmware.bin

Next, we’ll upload the firmware through the LifeOmic Platform UI:

Go to the Medical Devices Tab, select the “Firmware Updates” group. Then click the “Upload New Firmware” button.

Name your firmware, select the firmware.bin we generated, then click the "Upload Firmware" button.

Create a Deployment Target

To begin the rollout of an update, we must create a deployment target. Deployment targets can be projects or dynamic groups based on device attributes. This makes it easier to incrementally deploy updates across your fleet. For this example, we’ll select a project.

Select the uploaded firmware in the firmware list to go to the deployment details page.

Click the “Deploy Firmware” button, then select the project that our device belongs to.

We can now see that our deployment is targeting one device, and this page will be updated once the device receives the update.

Press the left most button on our physical device. If everything was successful, we should see Current version is: 2.0.0 printed to the LCD and Serial console.

Wrapping Up

In this article, we explored deploying device firmware updates using the LifeOmic Platform. This enables you to keep your devices secure, release feature updates, and save money on technician costs.

If you have questions or are interested in exploring how LifeOmic can help you get the most out of your medical device, please reach out on LinkedIn or at lifeomic.com/solutions/medical-devices/.

Links

See complete example code on our GitHub

For this example we used a generic MQTT Client, but you can also use the official AWS IoT Device SDKs.

--

--

Schaffer Stewart
Life and Tech @ LifeOmic

Senior Software Engineer@LifeOmic; Passionate about learning new things and helping others