Reprogramming a Raspberry Pi with Particle’s Asset OTA and a B504e
Ready to build your IoT product?
Create your Particle account and get access to:
- Discounted IoT devices
- Device management console
- Developer guides and resources
Background
There are many circumstances where you would need to update the software running on a Raspberry Pi that is already deployed. But, if that Raspberry Pi doesn’t have reliable access to Wi-Fi, this can become difficult to manage. Let’s explore how you might perform a remote software update on a Raspberry Pi over LTE by using the B504e SoM and Particle’s Asset OTA feature.
Overview
To accomplish this, we’ll have an app running on the Raspberry Pi that is responsible for storing and running the incoming code in a subprocess. The new code will be received from the connected B504e module over a serial connection.
The B504e will be running simple firmware that writes the contents of the asset received from the Particle Asset OTA update to the serial port.
The Particle Muon is used to power the B504e and connect the Particle device to the Raspberry Pi.
B504e firmware
Let’s start with the firmware that will be running on our B504e.
First, create a new Particle project. I’ll name mine rpi-asset-ota-firmware.
Configure the project for Device OS 6.3.0 (or greater) targeting the B5 series devices.
Then, create an assets
folder at the root of the Particle project with an app.py
file. In this file, you can put the test code shown below. This is the script to be sent to the Raspberry Pi.
import time while True: print("Hello world! From the app.py file") time.sleep(2)
Next, in project.properties
, uncomment the assetOtaDir
line. This tells the Particle compiler where to find the bundled assets.
name=rpi-asset-ota-firmware assetOtaDir=assets
In the main cpp file we can start writing out the logic. At the top of the file, add a function signature for the asset handler. Assign it to the onAssetOta
callback.
void handleAssets(spark::Vector<ApplicationAsset> assets);
STARTUP(System.onAssetOta(handleAssets));
Below that, let’s fill out the asset handler function. This function finds the asset named app.py
and begins to print its contents to the serial port using Serial.write
.
Notice that we’re not using the logging subsystem here so that metadata doesn’t get sent along with the script contents. These calls to Serial
will get swapped with Serial1
later on.
I’ve provided a unique start line (--- Begin … ---
) and end line (--- End of file ---
) so our future Raspberry Pi handler knows when to start saving the script data.
void handleAssets(spark::Vector<ApplicationAsset> assets)
{
Log.info("Processing assets...");
for (ApplicationAsset &asset : assets)
{
Log.info("Asset name: %s", asset.name().c_str());
if (asset.name() == "app.py")
{
Log.info("Python file found, sending to serial");
// Reset the asset to ensure we're at the beginning
asset.reset();
// Create a buffer for reading chunks
const size_t BUFFER_SIZE = 128;
char buffer[BUFFER_SIZE];
// Print a header to serial
Serial.printlnf("--- Begin %s (size: %d bytes) ---", asset.name().c_str(), asset.size());
// Read and send data in chunks
while (asset.available() > 0)
{
int bytesRead = asset.read(buffer, BUFFER_SIZE - 1);
if (bytesRead > 0)
{
// Write directly to serial
Serial.write((const uint8_t *)buffer, bytesRead);
}
}
// Print a footer to serial
Serial.println("\n--- End of file ---");
// Add a small delay to ensure the data has been sent
delay(100);
}
}
System.assetsHandled(true);
}
Now, in the setup
function we can configure the serial port and call handleAssets
. The call to handleAssets
only exists as a debugging step, feel free to remove this later.
void setup()
{
Serial.begin(115200);
waitFor(Serial.isConnected, 10000);
delay(2000);
// redo assets handling on next boot for demo purposes
// This is just here to make it easier to see the early log messages on
// the USB serial debug. You probably don't want this in production code.
handleAssets(System.assetsAvailable());
}
Compile and flash the device to make sure everything is working as expected. Open the serial monitor and reset the board to confirm that the contents of app.py
are being printed.
center
This is just for testing purposes! Later, when we wire up the Muon to the Raspberry Pi, we’ll need to replace any calls to Serial
with Serial1
For future reference, when uploading a firmware release to the Particle console, you’ll want to use the .zip file that gets generated in target > 6.3.0 > b5som
now that we have a bundled asset with our project. This is instead of the .bin file you’d normally upload. You can read more about this in the Particle documentation.
center
With this code running, your BSoM will log out the contents app.py
on both a system reset and a successful Particle OTA. When you want to re-program your Raspberry Pi, just re-compile a new Particle firmware build and release it via the console.
You can find the full reference firmware in this GitHub repository.
Raspberry Pi application
We can now move onto the the application running on the Raspberry Pi. It should be capable of monitoring the serial port for a new script, saving it to memory, then executing the contents in a subprocess.
Before we do anything though, we’ll need to enable serial on our Raspberry Pi. You can do this by executing:
sudo raspi-config
And navigating to: Interface Options > Serial Port > Would you like login shell to be accessible over serial? (NO) > Would you like the serial port hardware to be enabled? (YES)
The Raspberry Pi app should overwrite app.py
and restart the subprocess any time a new script is received.
We’ll start by setting up a while loop that waits for a serial connection. If a connection is made, it will continuously read a new line from the serial port. If an exception is thrown, then the serial port is probably not available and we’ll try to reconnect.
If serial data is available and we’ve detected the start of the file, save its contents. If the end of the file is detected, try to execute the script.
Find the full Raspberry Pi application in the rpi-asset-ota-app GitHub repository.
def listen_serial(): """Continuously listen for incoming Python scripts over serial and execute them.""" print(f"Listening on {SERIAL_PORT} at {BAUD_RATE} baud...") retry_delay = 2 # Initial retry delay while True: try: with serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=10) as ser: print("Serial connection established.") retry_delay = 2 # Reset delay on successful connection capturing = False script_data = [] # Read lines continuously until an exception occurs while True: line = ser.readline().decode("utf-8") if "--- Begin app.py" in line: capturing = True script_data = [] elif "--- End of file ---" in line: capturing = False script_content = "\n".join(script_data) print("Received script!") save_script(script_content) execute_script() elif capturing: script_data.append(line) except (serial.SerialException, FileNotFoundError) as e: print(f"Serial error: {e}") print(f"Retrying in {retry_delay} seconds...") time.sleep(retry_delay) retry_delay = min(retry_delay * 2, 60) # Exponential backoff (max 60s) if __name__ == "__main__": listen_serial()
Wiring it all up
Now we’re ready to try an end to end test of the system. First, make the serial connections as shown below:
center
Next, run the rpi-asset-ota-app code on the Raspberry Pi with the docker-compose up --build
command.
Note that you will have to have Docker and Docker Compose installed on your Raspberry Pi. If the application runs successfully, you should see:
Starting rpi-asset-ota-app_serial-executor_1 ... done Attaching to rpi-asset-ota-app_serial-executor_1 serial-executor_1 | Listening on /dev/ttyAMA0 at 115200 baud... serial-executor_1 | Serial connection established.
Now, head over the the rpi-asset-ota-firmware project.
Make sure to change all instances of Serial
to Serial1
in your firmware! This will ensure that the data is being sent via the RX/TX pin connections instead of the USB port as we did for testing.
Compile and flash the firmware with your updates to the Serial
calls. Now, back in your Raspberry Pi application, you should see logging that indicates the new script has been loaded and is running.
serial-executor_1 | Received script! serial-executor_1 | Executing app.py... serial-executor_1 | Hello world! From the app.py file serial-executor_1 | Hello world! From the app.py file
You can either re-flash the B504 with an updated app.py
or perform an Particle OTA to show that the Raspberry Pi loads and executes the latest version of the script.
serial-executor_1 | Received script! serial-executor_1 | Executing app.py... serial-executor_1 | Hello world! From the UPDATED app.py file serial-executor_1 | Hello world! From the UPDATED app.py file
Conclusion
Hopefully this post shows you how to use a low-power, LTE-enabled supervisor processor such as the Particle B504e to manage the software running on a Raspberry Pi. This makes for a useful setup if you’ve got a remote Raspberry Pi application running that lacks reliable Wi-Fi access. Using Particle, you can unlock a continuous software deployment pipeline on your Pi by leveraging powerful device management tooling.
Ready to get started?
Order your B504e SoM from the store.
