Custom device support in OpenBalena
So you’ve managed to build BalenaOS for your custom device and would like to connect this device to OpenBalena? Here’s how I managed to get this working:
In order to use custom device types in OpenBalena we need to tell OpenBalena about these device types. For this to work, we need to provide two different types of information:
- Device type contracts
- Device type information
Device type contracts
For OpenBalena to even know about our custom device type, we need to provide a so-called contract for our device type. OpenBalena periodically downlods the contracts from one or two Github repositories. Once the contract has been downloaded, it is possible to create devices and fleets for this custom device type.
Like I said, OpenBalena downloads the device contracts from one or two Github repositories. Out of the box OpenBalena downloads the device type contracts from the contracts
folder in GitHub - balena-io/contracts: Balena.io Base Contracts.
Additionally you can add a second Github repository. This can be done using the following optional ENV variables:
Variable |
Description |
CONTRACTS_PRIVATE_REPO_OWNER |
Github organization |
CONTRACTS_PRIVATE_REPO_NAME |
Github repository |
CONTRACTS_PRIVATE_REPO_BRANCH |
Github branch |
CONTRACTS_PRIVATE_REPO_TOKEN |
Github access token |
Both the public and private repository can be configured simultaniously. As you can see, the private repo can be configured with an access token and does not have to be publically accessible.
In order to configure the ENV variables, we need to update the docker-compose file for the api
service. This file can be found in compose/services.yaml
.
Add the following lines underneath services:
/api:
/environment:
CONTRACTS_PRIVATE_REPO_OWNER: ${OPENBALENA_CONTRACTS_PRIVATE_REPO_OWNER}
CONTRACTS_PRIVATE_REPO_NAME: ${OPENBALENA_CONTRACTS_PRIVATE_REPO_NAME}
CONTRACTS_PRIVATE_REPO_BRANCH: ${OPENBALENA_CONTRACTS_PRIVATE_REPO_BRANCH}
CONTRACTS_PRIVATE_REPO_TOKEN: ${OPENBALENA_CONTRACTS_PRIVATE_REPO_TOKEN}
and configure the actual values by adding to config/activate
:
export OPENBALENA_CONTRACTS_PRIVATE_REPO_OWNER=...
export OPENBALENA_CONTRACTS_PRIVATE_REPO_NAME=...
export OPENBALENA_CONTRACTS_PRIVATE_REPO_BRANCH=...
export OPENBALENA_CONTRACTS_PRIVATE_REPO_TOKEN=...
The added Github repository should at least contain the folder contracts/hw.device-type
. For every device there should be a folder with the device type slug as name containing at least a contract.json
and a logo svg file. Use an existing device type contract as reference.
More information about adding hardware contracts can be found here: meta-balena/contributing-device-support.md at master · balena-os/meta-balena · GitHub
After configuring the public and/or private repos, restart OpenBalena using:
./scripts/compose down
./scripts/compose up -d
and check the logs for the api
service using:
./scripts/compose exec api journalctl -fn100
Contracts are refreshed every 5 minutes, so after a while you should see something like this:
Jun 10 14:25:00 5d0a0b8810a2 api[1456]: [Scheduler] Running job: contractSync
Now all new devices in your contracts repo are available to OpenBalena. You can now create a fleet for your custom device type:
balena fleet create MyCustomDeviceFleet --type my-custom-device-type
However, when you want to create a device configuration or configure a device image by running:
balena config generate --fleet MyCustomDeviceFleet --version 2.12.7
this fails with the following error:
Device type slug not recognized. Perhaps misspelled?
Check available device types with "balena devices supported"
This is not due to Device type slug not recognized
like the error says, but because we do not have the device type information yet. We’ll configure this next.
Device type information
Besides the contracts, a lot of commands in OpenBalena require yet another json file for custom devices to work. This information is loaded from an AWS S3 bucket which also contains the BalenaOS images for all devices.
Out of the box, OpenBalena downloads the device device-type.json
file from https://s3.console.aws.amazon.com/s3/buckets/resin-production-img-cloudformation. Obviously our custom image information is not present there, so just like the Contracts, we need to reconfigure this.
In order to configure the S3 bucket where device images are stored, we need to change a couple of ENV variables again:
Variable |
Description |
IMAGE_STORAGE_ENDPOINT |
S3 server endpoint, defaults to s3.amazonaws.com
|
IMAGE_STORAGE_BUCKET |
S3 bucket name, defaults to resin-production-img-cloudformation
|
IMAGE_STORAGE_PREFIX |
Folder inside the bucket, defaults to images
|
IMAGE_STORAGE_ACCESS_KEY |
S3 Access key, default empty |
IMAGE_STORAGE_SECRET_KEY |
S3 Secret key, default empty |
IMAGE_STORAGE_FORCE_PATH_STYLE |
Setting whether to force path style URLs for S3 objects |
For this setting there is no way to configure a secondary bucket, so the only approach is to overwrite the entire setting. Now we could set up our own AWS S3 bucket, clone the resin-production-img-cloudformation
bucket (6.5 TB total, images
folder is 3.8 TB) and add our own custom device, but I chose a different approach.
At the moment it’s not possible to download device images using the balena os download
command, so we don’t need the actual image files. Just the device-type.json
files are enough. Let’s create a local S3 bucket with just the device-type.json
files for devices we might use and add our custom devices to that. OpenBalena already runs a S3 service to store the docker images, so we can use that and create a new bucket there.
In order to do this, and also set-up an easy way to copy device-type files from the resin-production-img-cloudformation
bucket to our own, we’ll need to add some variables to the compose/services.yaml
.
Under services:
/api:
/environment:
replace
IMAGE_STORAGE_BUCKET: resin-production-img-cloudformation
IMAGE_STORAGE_PREFIX: images
IMAGE_STORAGE_ENDPOINT: s3.amazonaws.com
with
IMAGE_STORAGE_BUCKET: ${OPENBALENA_IMAGES_S3_BUCKET}
IMAGE_STORAGE_PREFIX: images
IMAGE_STORAGE_ENDPOINT: s3.${OPENBALENA_HOST_NAME}
IMAGE_STORAGE_FORCE_PATH_STYLE: true
IMAGE_STORAGE_ACCESS_KEY: ${OPENBALENA_S3_ACCESS_KEY}
IMAGE_STORAGE_SECRET_KEY: ${OPENBALENA_S3_SECRET_KEY}
Next, we’ll edit config/activate
and change:
export OPENBALENA_S3_BUCKETS="registry-data"
to
export OPENBALENA_S3_BUCKETS="registry-data;images-data"
export OPENBALENA_IMAGES_S3_BUCKET=images-data
and restart OpenBalena with:
./scripts/compose down
./scripts/compose up -d
Optional: sync device-type information
If you want to use OpenBalena not only with your own custom device, but also use supported BalenaOS devices, we can synchronize the device-type json files from the resin-production-img-cloudformation
bucket to our own.
For this you’ll need to have an AWS (free) account and create an AWS Access key and Secret key using: Understanding and getting your AWS credentials - AWS General Reference
Edit compose/services.yaml
and under services:
/s3:
/environment:
add:
MC_HOST_s3: https://${OPENBALENA_AWS_ACCESS_KEY}:${OPENBALENA_AWS_SECRET_KEY}@s3.amazonaws.com
and add the following to config/activate
(fill your keys):
export OPENBALENA_AWS_ACCESS_KEY=...
export OPENBALENA_AWS_SECRET_KEY=...
Restart OpenBalena with:
./scripts/compose down
./scripts/compose up -d
and run the following commands to syncronize for example raspberrypi4-64
device image version 2.98.33
from resin-production-img-cloudformation
to your own bucket:
./scripts/compose exec s3 mc cp -r s3/resin-production-img-cloudformation/images/raspberrypi4-64/2.98.33/device-type.json localhost/images-data/images/raspberrypi4-64/2.98.33
Add your custom device to the images bucket
To add your custom device to the images bucket you’ll need to add a file called device-type.json
to the folder images/[my-custom-device-slug]/[my-custom-device-balenaos-version]
. We can do this using the mc
command we’ve used above:
cat my-custom-device-type.json | ./scripts/compose exec -T s3 mc pipe localhost/images-data/images/my-custom-device-type/my-custom-device-version/device-type.json
The my-custom-device-type.json
file is generated when building the BalenaOS image. It’s called the same as the .coffee
file you used and located in the same folder.
Now the command that failed above:
balena config generate --fleet MyCustomDeviceFleet --version 2.12.7
should generate a valid device configuration.
Wrapping up
So, in summary, to add your custom device to OpenBalena you’ll need to:
- Configure OpenBalena to download Hardware contracts from a Github repository where you’ve added the contract json file for your device
- Configure OpenBalena to download device type information from an S3 bucket where you’ve added the device-type json file for your device