Build PineTime Firmware in the Cloud with GitHub Actions

PineTime Firmware Programming vs Fortnite

Programming the firmware of our gadgets (like PineTime Smart Watch) has always been cumbersome...

  1. Get a proper computer (Windows tends to be problematic)

  2. Install the right tools and libraries to cross-compile our firmware (Depends on our operating system)

  3. If the build fails, tweak the build scripts (It's probably just Windows)

  4. If the build still fails... We're stuck! (Good Luck!)

Nowhere as fun as a game like Fortnite!

Can Firmware Programming be as fun as Fortnite?

Well Fortnite runs in the Cloud on massive servers...

Can we build our firmware in the Cloud?

In under 2 minutes?

Without any computer setup!

Yes we can!

Today we'll learn GitHub Actions for building InfiniTime Firmware for PineTime Smart Watch in the GitHub Cloud.

It's Fully Automated...

  1. Check in our updated source files to GitHub

  2. Wait 2 Minutes

  3. Out comes a piping-hot New Firmware Image for testing on PineTime!

(Feels like a Microwave!)

We'll make PineTime Programming as enjoyable as Fortnite... But less violent... And in 3D!

1 Create a Fork of PineTime Source Files

(Nope no spoon!)

  1. Create a free GitHub Account if we haven't got one...

    ▶️   github.com

  2. Browse to the GitHub Repository for the PineTime Firmware...

    ▶️   github.com/JF002/Pinetime

    Here's the complete Source Code for the InfiniTime Firmware (based on FreeRTOS).

  3. Click the Fork button at top right...

    Create a fork

  4. This creates a Fork of the PineTime Repository under our GitHub Account...

    Created the fork

    The URL looks like this...

    https://github.com/ACCOUNT_NAME/Pinetime
  5. The Fork contains our own copy of the entire Source Code for the PineTime Firmware... Ready for us to make any updates!

    GitHub helpfully tracks updates to our Fork, so that one day we may submit a Pull Request to sync our updates (only the useful ones) back to the original PineTime Repository.

    And we may also Pull Updates from the original PineTime Repository and apply them to our Fork.

    That's how we maintain Open Source Projects!

Read on to learn how we add GitHub Actions to our Fork to build the firmware automagically...

2 Add GitHub Actions to our Fork

  1. In our Fork on GitHub, click Actions

    GitHub Actions

  2. Click Skip this and set up a workflow yourself

    GitHub Actions

  3. GitHub brings us to a page to edit .github/workflows/main.yml

    GitHub Actions

  4. Open a new web browser tab.

    Browse to this page...

    github.com/pinetime-lab/.github/workflows/main.yml

    Copy the contents of this page.

  5. Switch back to the earlier page: .github/workflows/main.yml

    Paste and overwrite the contents of the file...

    GitHub Actions

  6. Click Start Commit at the right or bottom of the page...

    GitHub Actions

  7. Click Commit New File

    GitHub Actions

We have just created a Workflow... An automated job that will be run by GitHub whenever we update our source files.

If we ever need to edit the Workflow, just browse to this URL...

https://github.com/ACCOUNT_NAME/Pinetime/blob/master/.github/workflows/main.yml

(Change ACCOUNT_NAME to our GitHub Account Name)

Let's change a PineTime source file... And trigger our very first PineTime Firmware Build in the Cloud!

3 Modify the PineTime Source Code

We shall modify the source code so that the PineTime Watch Face shows our own special message...

  1. Browse to this URL...

    https://github.com/ACCOUNT_NAME/Pinetime/blob/master/src/DisplayApp/Screens/Clock.cpp

    (Change ACCOUNT_NAME to our GitHub Account Name)

  2. Click the Edit icon at the right...

    Edit Source File

  3. Look for the line with "BPM" (line 71)...

    Edit Source File

  4. BPM is the text that's displayed on the PineTime Watch Face.

    Change BPM to our own short message, like LOVE...

    Edit Source File

  5. Scroll to the bottom of the page.

    Click Commit Changes

    Edit Source File

Guess what?

We have just triggered Our Very First PineTime Firmware Build In The Cloud!

(Because the Firmware Build is triggered by any file update)

Let's check the result of our Firmware Build in the Cloud...

Check out this article to learn more about Clock.cpp

4 Our First PineTime Firmware Build

(Sorry our first build may fail with an error in TwiMaster.cpp... More about this in a while)

  1. Click Actions at the top.

    Click the first row that appears: Update Clock.cpp

    Build Result

  2. Click build at left...

    Build Result

  3. We'll see each step of the firmware building process...

    Build Result

  4. Click Make

    This shows the messages that were generated by the cross-compiler...

    Build Result

If we see this error...

/home/runner/work/Pinetime/Pinetime/src/drivers/TwiMaster.cpp:1:10: fatal error: sdk/integration/nrfx/nrfx_log.h: No such file or directory
 #include <sdk/integration/nrfx/nrfx_log.h>
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compilation terminated.
make[3]: *** [src/CMakeFiles/pinetime-app.dir/drivers/TwiMaster.cpp.o] Error 1

Browse to...

https://github.com/ACCOUNT_NAME/Pinetime/blob/master/src/drivers/TwiMaster.cpp

(Change ACCOUNT_NAME to our GitHub Account Name)

Edit the first two lines...

#include <sdk/integration/nrfx/nrfx_log.h>
#include <sdk/modules/nrfx/hal/nrf_gpio.h>

To...

#include <nrfx_log.h>
#include <nrf_gpio.h>

Click Commit Changes to save the file.

This triggers a new Firmware Build, which should succeed now.

Check out my build logs

5 Download and Test Our PineTime Firmware

Now let's download and flash the new firmware to PineTime!

(We assume that our PineTime has been flashed with the latest firmware that supports wireless firmware updates)

  1. On our Android Phone, launch the Web Browser.

    Browse to this URL to see GitHub Actions for our Fork...

    https://github.com/ACCOUNT_NAME/Pinetime/actions

    (Change ACCOUNT_NAME to our GitHub Account Name)

  2. Tap on the first row that appears: Update Clock.cpp

    Tap Sign In For Full Log View at top right.

    Sign in with our GitHub Account.

  3. Tap Artifacts at the top.

    Tap pinetime-mcuboot-app-dfu.zip

    When the file has been downloaded, tap Open

    Build Artifact

    The file should appear under Downloads like above.

  4. Launch the nRF Connect mobile app.

    Scan for devices and look for Pinetime-JF

    Tap Connect

    Build Artifact

    Tap on the DFU circular icon at the top right.

    DFU means Device Firmware Update. We'll be uploading the DFU Package pinetime-mcuboot-app-dfu.zip to update the firmware on PineTime.

  5. For File Type, select Distribution Packet (ZIP) and tap OK

    Build Artifact

    In the Search Box, enter dfu

    Our downloaded file pinetime-mcuboot-app-dfu.zip should appear.

    Tap on pinetime-mcuboot-app-dfu.zip

  6. The nRF Connect app begins transmitting the file to PineTime over Bluetooth LE.

    Build Artifact

    When it's done, it shows Disconnecting

  7. PineTime restarts with the new firmware and shows our message "LOVE"!

PineTime shows some LOVE

Watch the video on YouTube

Will this work on iPhone?

Yes, with the iPhone version of the nRF Connect mobile app.

I have a request...

If you could... With your kind permission... Please post to Twitter and/or Mastodon a pic of your PineTime with the new firmware.

Tag the post with #PineTime so we know that building PineTime Firmware in the Cloud works OK for you. Thanks! :-)

If you're stuck, please chat with us in the PineTime Chatroom...

PineTime Chatroom on Discord / Matrix / Telegram / IRC

6 Set PineTime Date and Time with nRF Connect

To set the date and time on PineTime, we use the nRF Connect mobile app...

  1. In nRF Connect, browse for the Pinetime-JF device and tap Connect

  2. Tap on MenuConfigure GATT ServerAdd Service

  3. Set Server Configuration to Current Time Service

  4. Tap OK

PineTime should automatically sync the date and time.

This works only Android, not iPhone. If you can help fix this, please chat with us in the PineTime Chatroom...

PineTime Chatroom on Discord / Matrix / Telegram / IRC

7 Other Options

  1. Can we edit our files in GitHub without using the web browser?

    We recommend VSCode or VSCodium for editing files with Git Version Control. (Which works with GitHub files)

    Remember to Commit any updated files and Push the Commits to the master Branch to trigger the firmware build.

  2. Can we build the firmware on our own computers?

    Follow the instructions in the firmware building doc and the DFU packaging doc.

    To troubleshoot the build, compare with my build logs.

  3. What if we don't wish to make our repos public?

    Only public repos get GitHub Actions for free... But there's an alternative:

    Self-Hosted Runners for GitHub Actions

  4. What's in the artifact pinetime-app.out?

    This is the Standalone PineTime Firmware... It's self-contained firmware that works without the MCUBoot Bootloader. Which makes it simpler for GDB debugging.

  5. How do we flash pinetime-app.out?

    Download the artifact pinetime-app.out from GitHub Actions.

    We'll get a ZIP file. Extract the PineTime Firmware Image inside: pinetime-app.out

    Flash with PineTime Updater...

  6. Is it really necessary to build the Standalone Firmware pinetime-app.out?

    Nope. To speed up the build, we may comment out the "Make pinetime-app" and "Upload Standalone Firmware" steps in the GitHub Actions Workflow.

  7. Can GitHub Actions build other flavours of PineTime Firmware?

    Yes! GitHub Actions can build RIOT, Mynewt and wasp-os firmware for PineTime.

8 What's Next?

The PineTime Community shall extend this Build Firmware Workflow into a centralised system for maintaining the PineTime Community Firmware that will be preloaded at the PineTime Factory.

The centralised Continuous Integration system is helpful because...

  1. It compiles the PineTime Community Firmware source code whenever there are updates.

    And instantly catches any bad code that can't be compiled.

  2. It can run Automated Tests in the Cloud after building the PineTime Community Firmware.

    So we will know rightaway if the firmware won't boot on an emulated PineTime. (Hopefully)

  3. And it can publish New Firmware Releases for the PineTime Community to download... If the Automated Tests pass.

We have a lot to do, please chat with us if you're keen to help...

PineTime Chatroom on Discord / Matrix / Telegram / IRC

And remember to enjoy your PineTime :-)

Check out my RSS Feed

9 How It Works

(Warning: The topics below are deeply technical... If you're keen please read on!)

Let's look at the GitHub Actions Workflow we used for building PineTime Firmware: .github/workflows/main.yml

# GitHub Actions Workflow to build FreeRTOS Firmware for PineTime Smart Watch
# See https://lupyuen.github.io/pinetime-rust-mynewt/articles/cloud
# Based on https://github.com/JF002/Pinetime/blob/master/doc/buildAndProgram.md
# and https://github.com/JF002/Pinetime/blob/master/bootloader/README.md

# Name of this Workflow
name: Build PineTime Firmware

# When to run this Workflow...
on:

  # Run this Workflow when files are updated (Pushed) in the "master" Branch
  push:
    branches: [ master ]
    
  # Also run this Workflow when a Pull Request is created or updated in the "master" Branch
  pull_request:
    branches: [ master ]

Here we see the conditions that will trigger our Workflow...

  1. When files are updated (or Pushed) in the master Branch

  2. When a Pull Request is created or updated in the master Branch

More details

Next we specify which Operating System GitHub should use to execute the Workflow Steps...

# Steps to run for the Workflow
jobs:
  build:

    # Run these steps on Ubuntu
    runs-on: ubuntu-latest

    steps:
      ...

This asks GitHub to allocate a free Virtual Machine (Docker Container) to build our firmware, based on Ubuntu 18.04.

We're using Ubuntu, but GitHub supports Windows and macOS as well.

More details

After that we specify the steps to be executed for our Workflow...

9.1 Install cmake

The steps for building PineTime Firmware are based on the firmware building doc and the DFU packaging doc.

We use a popular tool called cmake. (It's like an evolved make)

Here's how we install cmake...

    - name: Install cmake
      uses: lukka/get-cmake@v3.18.0

Why do we need to install build tools like cmake?

Because GitHub only provides bare bones Ubuntu with simple command-line tools like make.

For special tools like cmake, we'll have to install ourselves.

What's get-cmake?

That's a GitHub Action provided by the community for installing cmake

Browse the available GitHub Actions

9.2 Check Cache for Embedded Arm Toolchain

Our Ubuntu Virtual Machine in the GitHub Cloud is based on the Intel x64 platform... But we're building firmware for PineTime, which is based on Arm Cortex-M4.

To do that, we need to install a cross-compiler: Embedded Arm Toolchain arm-none-eabi-gcc

We'll install this in the next step, but first we check whether the toolchain is in our cache...

    - name: Check cache for Embedded Arm Toolchain arm-none-eabi-gcc
      id:   cache-toolchain
      uses: actions/cache@v2
      env:
        cache-name: cache-toolchain
      with:
        path: ${{ runner.temp }}/arm-none-eabi
        key:  ${{ runner.os }}-build-${{ env.cache-name }}
        restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}

Why cache the Embedded Arm Toolchain?

The Embedded Arm Toolchain is a huge 102 MB download (compressed).

Every time GitHub builds our firmware, it creates a fresh new empty Virtual Machine.

(So that our firmware builds may be reproduced consistently... And for security too)

GitHub will take roughly a minute to download and unpack the toolchain... Unless we cache it.

    - name: Check cache for Embedded Arm Toolchain arm-none-eabi-gcc
      id:   cache-toolchain
      uses: actions/cache@v2

The actions/cache GitHub Action lets us cache the toolchain for future builds.

We can have multiple caches. Here's our cache for the toolchain...

      env:
        cache-name: cache-toolchain

Next we tell GitHub what to cache...

      with:
        path: ${{ runner.temp }}/arm-none-eabi
        key:  ${{ runner.os }}-build-${{ env.cache-name }}
        restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}

Given these build settings...

runner.temp    = /home/runner/work/_temp
runner.os      = Linux
env.cache-name = cache-toolchain

This means...

9.3 Install Embedded Arm Toolchain

Now we download and unpack the Embedded Arm Toolchain into the temporary toolchain folder /home/runner/work/_temp/arm-none-eabi...

    - name: Install Embedded Arm Toolchain arm-none-eabi-gcc
      if:   steps.cache-toolchain.outputs.cache-hit != 'true'  # Install toolchain if not found in cache
      uses: fiam/arm-none-eabi-gcc@v1.0.2
      with:
        # GNU Embedded Toolchain for Arm release name, in the V-YYYY-qZ format (e.g. "9-2019-q4")
        release: 8-2019-q3
        # Directory to unpack GCC to. Defaults to a temporary directory.
        directory: ${{ runner.temp }}/arm-none-eabi

We use the community GitHub Action fiam/arm-none-eabi-gcc to do this. So easy!

Why is there a condition for the step?

      # Install toolchain if not found in cache
      if:   steps.cache-toolchain.outputs.cache-hit != 'true'

This says that GitHub shall download the toolchain only if the previous step cache-toolchain couldn't find an existing cache for the toolchain.

Huge downloads and reinstallation averted... So neat!

What software is preinstalled on the GitHub Virtual Machine?

Check out the preinstalled software on Ubuntu 18.04 for GitHub Actions

9.4 Check Cache for nRF5 SDK

Next we download the nRF5 SDK by Nordic Semiconductor.

The SDK is needed for building PineTime Firmware because PineTime is based on the nRF52832 Microcontroller.

Before downloading and unpacking the SDK into /home/runner/work/_temp/nrf5_sdk, we check whether the cache exists for the unique cache key Linux-build-cache-nrf5sdk...

    - name: Check cache for nRF5 SDK
      id:   cache-nrf5sdk
      uses: actions/cache@v2
      env:
        cache-name: cache-nrf5sdk
      with:
        path: ${{ runner.temp }}/nrf5_sdk
        key:  ${{ runner.os }}-build-${{ env.cache-name }}
        restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}

9.5 Install nRF5 SDK

Can we download and install packages into the GitHub Virtual Machine without using a GitHub Action?

Yes we can, through the Ubuntu command line...

    - name: Install nRF5 SDK
      if:   steps.cache-nrf5sdk.outputs.cache-hit != 'true'  # Install SDK if not found in cache
      run:  |
        cd ${{ runner.temp }}
        curl https://developer.nordicsemi.com/nRF5_SDK/nRF5_SDK_v15.x.x/nRF5_SDK_15.3.0_59ac345.zip -o nrf5_sdk.zip
        unzip nrf5_sdk.zip
        mv nRF5_SDK_15.3.0_59ac345 nrf5_sdk

This expands to...

cd /home/runner/work/_temp
curl \
  https://developer.nordicsemi.com/nRF5_SDK/nRF5_SDK_v15.x.x/nRF5_SDK_15.3.0_59ac345.zip \
  -o nrf5_sdk.zip
unzip nrf5_sdk.zip
mv nRF5_SDK_15.3.0_59ac345 nrf5_sdk

Here we call curl to download the nRF5 SDK by Nordic Semiconductor.

We unpack the SDK into /home/runner/work/_temp/nrf5_sdk, which is cached by the previous step.

      if:   steps.cache-nrf5sdk.outputs.cache-hit != 'true'  # Install SDK if not found in cache

Again, GitHub shall download the SDK only if the cache couldn't be found.

GitHub will remove any cache entries that have not been accessed in over 7 days.

9.6 Install Adafruit nrfutil Library

We install the Adafruit nrfutil Library to create the DFU Package for flashing over Bluetooth LE...

    - name: Install adafruit-nrfutil
      run:  |
        pip3 install --user wheel
        pip3 install --user setuptools
        pip3 install --user adafruit-nrfutil

Yes pip3 is available for installing Python 3 packages.

But in the GitHub Ubuntu environment, the installed Python packages are not accessible via the default PATH

In a while we'll see that they are accessible via ~/.local/bin/...

9.7 Checkout Source Files

Now we fetch a complete set of source files from our Fork...

    - name: Checkout source files
      uses: actions/checkout@v2

The actions/checkout GitHub Action copies the source files into /home/runner/work/Pinetime/Pinetime

9.8 Show Files

Let's take a peek at the environment variables and the files that have been checked out...

    - name: Show files
      run:  set ; pwd ; ls -l

The current directory pwd is shown as...

/home/runner/work/Pinetime/Pinetime

The list of files and folders in that directory...

total 48
-rw-r--r--  1 runner docker 2194 Jul 26 14:39 CMakeLists.txt
-rw-r--r--  1 runner docker 5079 Jul 26 14:39 README.md
drwxr-xr-x  3 runner docker 4096 Jul 26 14:39 bootloader
drwxr-xr-x  3 runner docker 4096 Jul 26 14:39 cmake-nRF5x
drwxr-xr-x  3 runner docker 4096 Jul 26 14:39 doc
-rw-r--r--  1 runner docker 2952 Jul 26 14:39 gcc_nrf52-mcuboot.ld
-rw-r--r--  1 runner docker 2952 Jul 26 14:39 gcc_nrf52.ld
drwxr-xr-x  4 runner docker 4096 Jul 26 14:39 images
-rw-r--r--  1 runner docker 4475 Jul 26 14:39 nrf_common.ld
drwxr-xr-x 10 runner docker 4096 Jul 26 14:39 src

Check the section "Environment Variables" below for the complete list of environment variables.

9.9 CMake

Now that we have cmake installed and the complete set of source files, let's start building the firmware...

    - name: CMake
      run:  mkdir -p build && cd build && cmake -DARM_NONE_EABI_TOOLCHAIN_PATH=${{ runner.temp }}/arm-none-eabi -DNRF5_SDK_PATH=${{ runner.temp }}/nrf5_sdk -DUSE_OPENOCD=1 ../

This expands to...

mkdir -p build
cd build
cmake \
  -DARM_NONE_EABI_TOOLCHAIN_PATH=/home/runner/work/_temp/arm-none-eabi \
  -DNRF5_SDK_PATH=/home/runner/work/_temp/nrf5_sdk \
  -DUSE_OPENOCD=1 \
  ../

We call cmake passing the locations of the Embedded Arm Toolchain and the nRF5 SDK.

This is exactly as prescribed by the build doc.

9.10 Make DFU Firmware pinetime-mcuboot-app

    - name: Make pinetime-mcuboot-app
      run:  |
        cd build
        make pinetime-mcuboot-app

This generates the PineTime Firmware File pinetime-mcuboot-app.out, as shown in the log...

[100%] Linking CXX executable pinetime-mcuboot-app.out
post build steps for pinetime-mcuboot-app
   text	   data	    bss	    dec	    hex	filename
 238012	    772	  35784	 274568	  43088	pinetime-mcuboot-app.out

This says...

If the build fails, can we see the complete list of options passed to the cross-compiler?

Add the --trace option like so...

# For Debugging Builds: Add "--trace" to see details.
make --trace pinetime-mcuboot-app

The log shows that the make step takes 2.5 minutes to execute. Can we compile faster?

Add the -j option like so...

# For Faster Builds: Add "make" option "-j"
make -j pinetime-mcuboot-app

This runs a parallel build with multiple processes. It shaves about 30 seconds off the build time.

We don't recommend adding -j for normal builds because it becomes harder to spot the compiler error.

9.11 Create Firmware Image

PineTime uses the MCUBoot Bootloader to do nifty tricks... Like rolling back the PineTime firmware to the previous version if the new one fails to start.

To do this the MCUBoot Bootloader needs our firmware to be formatted in a way that it understands.

We call this format the MCUBoot Firmware Image. We create the Firmware Image like so...

    - name: Create firmware image
      run:  |
        ${{ runner.temp }}/mcuboot/scripts/imgtool.py create --align 4 --version 1.0.0 --header-size 32 --slot-size 475136 --pad-header build/src/pinetime-mcuboot-app.bin build/src/pinetime-mcuboot-app-img.bin
        ${{ runner.temp }}/mcuboot/scripts/imgtool.py verify build/src/pinetime-mcuboot-app-img.bin

The above expands to...

  /home/runner/work/_temp/mcuboot/scripts/imgtool.py create --align 4 --version 1.0.0 --header-size 32 --slot-size 475136 --pad-header build/src/pinetime-mcuboot-app.bin build/src/pinetime-mcuboot-app-img.bin

  /home/runner/work/_temp/mcuboot/scripts/imgtool.py verify build/src/pinetime-mcuboot-app-img.bin

This is prescribed by the DFU packaging doc.

imgtool comes from the MCUBoot Bootloader, as explained here...

"Firmware Update over Bluetooth Low Energy on PineTime Smart Watch"

9.12 Create DFU Package

We'll be using the nRF Connect mobile app to transmit a DFU Package to update PineTime's firmware.

We wrap up the Firmware Image (from the previous step) into a DFU Package by calling adafruit-nrfutil ...

    - name: Create DFU package
      run:  |
        ~/.local/bin/adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/src/pinetime-mcuboot-app-img.bin build/src/pinetime-mcuboot-app-dfu.zip
        unzip -v build/src/pinetime-mcuboot-app-dfu.zip
        # Unzip the package because Upload Artifact will zip up the files
        unzip build/src/pinetime-mcuboot-app-dfu.zip -d build/src/pinetime-mcuboot-app-dfu

The above expands to...

  ~/.local/bin/adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application build/src/pinetime-mcuboot-app-img.bin build/src/pinetime-mcuboot-app-dfu.zip

  # Display the contents of the package
  unzip -v build/src/pinetime-mcuboot-app-dfu.zip

  # Unzip the package because Upload Artifact will zip up the files
  unzip build/src/pinetime-mcuboot-app-dfu.zip -d build/src/pinetime-mcuboot-app-dfu

This creates the DFU Package pinetime-mcuboot-app-dfu.zip.

The DFU Package contains 3 files...

Archive:  build/src/pinetime-mcuboot-app-dfu.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
      14  Stored       14   0% 2020-07-30 06:47 8cf6f003  pinetime-mcuboot-app-img.dat
     498  Stored      498   0% 2020-07-30 06:47 b0b12660  manifest.json
  238856  Stored   238856   0% 2020-07-30 06:47 ded98812  pinetime-mcuboot-app-img.bin
--------          -------  ---                            -------
  239368           239368   0%                            3 files

Why did we unzip the DFU Package?

Let's say we didn't unzip the DFU Package... And we pass pinetime-mcuboot-app-dfu.zip to GitHub Actions to store as an Arfitact for downloading.

GitHub Actions always zips up artifacts for downloading... And we'll end up with a DFU Package that's zipped twice!

nRF Connect doesn't like double-zipped DFU Packages.

So to work around this issue, we unzip the DFU Package and pass the 3 files inside to be stored as an Artifact.

GitHub Actions will gladly zip up the 3 files to create our DFU Package.

9.13 Upload DFU Package

GitHub will wipe out our entire Virtual Machine and the files inside (like Langoliers)... So we need to save the PineTime DFU Package pinetime-mcuboot-app-dfu.zip

    - name: Upload DFU package
      uses: actions/upload-artifact@v2
      with:
        name: pinetime-mcuboot-app-dfu.zip
        path: build/src/pinetime-mcuboot-app-dfu/*

The actions/upload-artifact GitHub Action saves the PineTime DFU Package pinetime-mcuboot-app-dfu.zip as an Artifact for us to download and flash to PineTime.

Remember that this path contains 3 files...

        path: build/src/pinetime-mcuboot-app-dfu/*

So this step will zip up the 3 files to create our DFU Package pinetime-mcuboot-app-dfu.zip

9.14 Make Standalone Firmware pinetime-app

Our Workflow also creates the Standalone PineTime Firmware... It's self-contained firmware that runs without the MCUBoot Bootloader. Which makes it simpler for GDB debugging.

    - name: Make pinetime-app
      run:  |
        cd build
        make pinetime-app

This generates the PineTime Firmware File pinetime-app.out, as shown in the log...

[100%] Linking CXX executable pinetime-app.out
post build steps for pinetime-app
  text	   data	    bss	    dec	    hex	filename
238012	    772	  35784	 274568	  43088	pinetime-app.out

9.15 Upload Standalone Firmware

This step uploads the Standalone Firmware as an Artifact for us to download...

    - name: Upload standalone firmware
      uses: actions/upload-artifact@v2
      with:
        name: pinetime-app.out
        path: build/src/pinetime-app.out

To flash the Standalone Firmware to PineTime, check the instructions below.

Why is the pinetime-app.out firmware 6.4 MB in size when the build log shows that the cross-compiler output (text) is 233 KB?

Because pinetime-app.out is an ELF File. It contains the firmware image as well as the debugging symbols.

(Useful for a debugger like GDB)

9.16 Find Output

For curiosity, let's discover what other outputs are generated during the PineTime Firmware Build...

    - name: Find output
      run:  |
        find . -name "pinetime-app.*" -ls
        find . -name "pinetime-mcuboot-app.*" -ls

Some of these files may be useful for troubleshooting our firmware (like the Linker Maps *.map)...

  1327374    656 -rw-r--r--   1 runner   docker     671691 Jul 30 06:50 ./build/src/pinetime-app.hex
  1327372   6504 -rwxr-xr-x   1 runner   docker    6740720 Jul 30 06:50 ./build/src/pinetime-app.out
  1326257      4 drwxr-xr-x  11 runner   docker       4096 Jul 30 06:48 ./build/src/CMakeFiles/pinetime-app.dir
  1327371   5004 -rw-r--r--   1 runner   docker    5122576 Jul 30 06:50 ./build/src/pinetime-app.map
  1327373    236 -rwxr-xr-x   1 runner   docker     238784 Jul 30 06:50 ./build/src/pinetime-app.bin
  1326403      4 drwxr-xr-x  11 runner   docker       4096 Jul 30 06:45 ./build/src/CMakeFiles/pinetime-mcuboot-app.dir
  1326990    236 -rwxr-xr-x   1 runner   docker     238784 Jul 30 06:47 ./build/src/pinetime-mcuboot-app.bin
  1326989   6504 -rwxr-xr-x   1 runner   docker    6740728 Jul 30 06:47 ./build/src/pinetime-mcuboot-app.out
  1326988   5304 -rw-r--r--   1 runner   docker    5427760 Jul 30 06:47 ./build/src/pinetime-mcuboot-app.map
  1326991    656 -rw-r--r--   1 runner   docker     671708 Jul 30 06:47 ./build/src/pinetime-mcuboot-app.hex

9.17 Caching At The End

Here's a tip about the caches we have created for the Embedded Arm Toolchain and the nRF5 SDK...

# Embedded Arm Toolchain and nRF5 SDK will only be cached if the build succeeds.
# So make sure that the first build always succeeds, e.g. comment out the "Make" step.

The files get cached only if the build succeeds

If the first few builds fail (say due to coding errors), the files will never get cached. And restarting the build becomes painfully slow.

Therefore it's good to tweak the Workflow to make sure that the first build always succeeds... Like commenting out the make section.

Subsequent builds will be a lot faster with the caching.

And that's how we build PineTime Firmware in the Cloud!

GitHub Actions Workflow Syntax

10 Environment Variables

This step in our GitHub Actions Workflow...

    - name: Show files
      run:  set ; pwd ; ls -l

Shows these environment variables...

AGENT_TOOLSDIRECTORY=/opt/hostedtoolcache
ANDROID_HOME=/usr/local/lib/android/sdk
ANDROID_SDK_ROOT=/usr/local/lib/android/sdk
ANT_HOME=/usr/share/ant
AZURE_EXTENSION_DIR=/opt/az/azcliextensions
BASH=/bin/bash
lquote:extquote:force_fignore:hostcomplete:interactive_comments:progcomp:p
BASH_ALIASES=()
BASH_ARGC=()
BASH_ARGV=()
BASH_CMDS=()
BASH_LINENO=([0]="0")
BASH_SOURCE=([0]="/home/runner/work/_temp/a3bba1d.sh")
BASH_VERSINFO=([0]="4" [1]="4" [2]="20" [3]="1" [4]="release" [5]
BASH_VERSION='4.4.20(1)-release'
BOOST_ROOT_1_69_0=/opt/hostedtoolcache/boost/1.69.0/x64
BOOST_ROOT_1_72_0=/opt/hostedtoolcache/boost/1.72.0/x64
CHROMEWEBDRIVER=/usr/local/share/chrome_driver
CHROME_BIN=/usr/bin/google-chrome
CI=true
CONDA=/usr/share/miniconda
DEBIAN_FRONTEND=noninteractive
DEPLOYMENT_BASEPATH=/opt/runner
DIRSTACK=()
DOTNET_NOLOGO='"1"'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE='"1"'
EUID=1001
GECKOWEBDRIVER=/usr/local/share/gecko_driver
GITHUB_ACTION=run2
GITHUB_ACTIONS=true
GITHUB_ACTOR=lupyuen
GITHUB_API_URL=https://api.github.com
GITHUB_BASE_REF=
GITHUB_EVENT_NAME=push
GITHUB_EVENT_PATH=/home/runner/work/_temp/_github_workflow/event.json
GITHUB_GRAPHQL_URL=https://api.github.com/graphql
GITHUB_HEAD_REF=
GITHUB_JOB=build
GITHUB_REF=refs/heads/master
GITHUB_REPOSITORY=AppKaki/Pinetime
GITHUB_REPOSITORY_OWNER=AppKaki
GITHUB_RUN_ID=183212738
GITHUB_RUN_NUMBER=2
GITHUB_SERVER_URL=https://github.com
GITHUB_SHA=bce10a451e6cef08c30b1d6ac297e1f50cf57bf3
GITHUB_WORKFLOW='Build PineTime Firmware'
GITHUB_WORKSPACE=/home/runner/work/Pinetime/Pinetime
GOROOT=/opt/hostedtoolcache/go/1.14.4/x64
GOROOT_1_11_X64=/opt/hostedtoolcache/go/1.11.13/x64
GOROOT_1_12_X64=/opt/hostedtoolcache/go/1.12.17/x64
GOROOT_1_13_X64=/opt/hostedtoolcache/go/1.13.12/x64
GOROOT_1_14_X64=/opt/hostedtoolcache/go/1.14.4/x64
GRADLE_HOME=/usr/share/gradle
GROUPS=()
HOME=/home/runner
HOMEBREW_CELLAR='"/home/linuxbrew/.linuxbrew/Cellar"'
HOMEBREW_PREFIX='"/home/linuxbrew/.linuxbrew"'
HOMEBREW_REPOSITORY='"/home/linuxbrew/.linuxbrew/Homebrew"'
HOSTNAME=fv-az20
HOSTTYPE=x86_64
IFS=$' \t\n'
INVOCATION_ID=cc632305776e4c49848d4644a457d167
ImageOS=ubuntu18
ImageVersion=20200717.1
JAVA_HOME=/usr/lib/jvm/adoptopenjdk-8-hotspot-amd64
JAVA_HOME_11_X64=/usr/lib/jvm/adoptopenjdk-11-hotspot-amd64
JAVA_HOME_12_X64=/usr/lib/jvm/adoptopenjdk-12-hotspot-amd64
JAVA_HOME_7_X64=/usr/lib/jvm/zulu-7-azure-amd64
JAVA_HOME_8_X64=/usr/lib/jvm/adoptopenjdk-8-hotspot-amd64
JOURNAL_STREAM=9:31251
LANG=C.UTF-8
LEIN_HOME=/usr/local/lib/lein
LEIN_JAR=/usr/local/lib/lein/self-installs/leiningen-2.9.4-standalone.jar
M2_HOME=/usr/share/apache-maven-3.6.3
MACHTYPE=x86_64-pc-linux-gnu
OPTERR=1
OPTIND=1
OSTYPE=linux-gnu
PATH=/home/runner/work/_temp/arm-none-eabi/bin:/home/runner/work/_temp/-x86_64/bin/:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/in:/home/runner/.config/composer/vendor/bin:/home/runner/.dotnet/tools://local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
PERFLOG_LOCATION_SETTING=RUNNER_PERFLOG
POWERSHELL_DISTRIBUTION_CHANNEL=GitHub-Actions-ubuntu18
PPID=2451
PS4='+ '
PWD=/home/runner/work/Pinetime/Pinetime
RUNNER_OS=Linux
RUNNER_PERFLOG=/home/runner/perflog
RUNNER_TEMP=/home/runner/work/_temp
RUNNER_TOOL_CACHE=/opt/hostedtoolcache
RUNNER_TRACKING_ID=github_3a45354c-437f-42c1-b8fb-cff7fa3cf2a0
RUNNER_USER=runner
RUNNER_WORKSPACE=/home/runner/work/Pinetime
SELENIUM_JAR_PATH=/usr/share/java/selenium-server-standalone.jar
SHELL=/bin/bash
SHELLOPTS=braceexpand:errexit:hashall:interactive-comments
SHLVL=1
SWIFT_PATH=/usr/share/swift/usr/bin
TERM=dumb
UID=1001
USER=runner
VCPKG_INSTALLATION_ROOT=/usr/local/share/vcpkg
_=/bin/bash