Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OTA Updates #62

Merged
merged 7 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/source/architecture/django.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@ the Django app that contains the application logic.
of the file, a URL from where the device can retrieve the firmware, and
timestamps for tracking when each firmware record was created and last
updated.

FirmwareUpdate
keeps track of the execution of firmware updates for each endpoint. It adds
references to the two required resources from server to endpoint (Send URI,
execute Update). Furthermore it adds a field for the State and the Result
of an update.
1 change: 1 addition & 0 deletions doc/source/architecture/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ interact with each other.
django
leshan
communication_interfaces
ota
106 changes: 106 additions & 0 deletions doc/source/architecture/ota.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
Over the Air Updates
====================

Over the Air (OTA) updates are a way to update the firmware of a device
remotely. This is a mandatory feature for any IoT device. The device must be
able to update its firmware without any physical user intervention.

flownexus implements OTA updates according to the LwM2M protocol. The LwM2M
protocol specifies how to update the firmware of a device. The OTA process is
implemented by sending a link to a firmware via LwM2M object 5/0/1 - Package
URI. This method is called Pull via HTTP(s) and is faster, compared to a
transfer via CoAP. Updates via CoAP blockwise transfer to download the firmware
are not supported.

More information about a possible firmware implementation can be found in the
``lwm2m_client`` firmware sample at
``flownexus/simulation/lwm2m_client/src/firmware_update.c``.

.. note::

Firmware Updates are implemented in the LwM2M server and the example
firmware. A set up a PKI with self-signed certificates is required for the
server to communicate with the device via https. This setup will be
described in upcoming releases shortly.

Update process of a Firmware
----------------------------

The update process is described in detail in the `LwM2M core specification
v1.1.1`_ Chapter *E.6 LwM2M Object: Firmware Update*. This chapter summarizes
the update process:

1. flownexus sends a link to the firmware to the device via LwM2M object 5/0/1
- Package URI. It is a relative link to the firmware on the server, e.g.
``/media/firmware/v0.1.0.bin``. The host ``https://flownexus.org`` is
fixed in firmware.
2. The device downloads the firmware from the server via http(s) and sets the
state to ``STATE_DOWNLOADING``.
3. The device sets the state to ``STATE_DOWNLOADED`` after the download is
complete.
4. flownexus executes the command 5/0/2 - Update to the device.
5. The device sets the state to ``STATE_UPDATING`` upon receiving the command.
6. The device updates the firmware and communicates the result to flownexus.

This process ensures that errors during the update process are handled and
communicated to the backend.

States of a OTA Update
......................

There are 4 device states during an OTA update. flownexus tracks the state of
the device during the update process in the database (FirmwareUpdate table).
Once the update is completed (failed or successful), the state is set always
set to ``STATE_IDLE``.


.. table:: Device States during an OTA Update

+-------------------+-----------------------+
| State | Description |
+===================+=======================+
| STATE_IDLE | device is idle |
+-------------------+-----------------------+
| STATE_DOWNLOADING | device is downloading |
+-------------------+-----------------------+
| STATE_DOWNLOADED | download is complete |
+-------------------+-----------------------+
| STATE_UPDATING | device is updating |
+-------------------+-----------------------+

Result Codes for an OTA Update
..............................

For every OTA update, a result is generated after the update failed or
successful. There can be only one active OTA update at a time for each client.
An active OTA is indicated by the result ``RESULT_DEFAULT``. The result for
each OTA update is stored in the database (FirmwareUpdate table).

.. table:: Result Codes for an OTA Update

+-------------------------+------------------------+
| Result | Description |
+=========================+========================+
| RESULT_DEFAULT | default state |
+-------------------------+------------------------+
| RESULT_SUCCESS | update was successful |
+-------------------------+------------------------+
| RESULT_NO_STORAGE | no storage available |
+-------------------------+------------------------+
| RESULT_OUT_OF_MEM | out of memory |
+-------------------------+------------------------+
| RESULT_CONNECTION_LOST | connection was lost |
+-------------------------+------------------------+
| RESULT_INTEGRITY_FAILED | integrity check failed |
+-------------------------+------------------------+
| RESULT_UNSUP_FW | unsupported firmware |
+-------------------------+------------------------+
| RESULT_INVALID_URI | invalid uri provided |
+-------------------------+------------------------+
| RESULT_UPDATE_FAILED | update failed |
+-------------------------+------------------------+
| RESULT_UNSUP_PROTO | unsupported protocol |
+-------------------------+------------------------+


.. _LwM2M core specification v1.1.1: https://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/OMA-TS-LightweightM2M_Core-V1_1_1-20190617-A.pdf
19 changes: 5 additions & 14 deletions server/django/db_initial_resource_types.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"object_id": 3,
"resource_id": 4,
"name": "reboot",
"data_type": ""
"data_type": "NONE"
}
},
{
Expand All @@ -68,7 +68,7 @@
"object_id": 3,
"resource_id": 5,
"name": "factory_reset",
"data_type": ""
"data_type": "NONE"
}
},
{
Expand Down Expand Up @@ -131,7 +131,7 @@
"object_id": 3,
"resource_id": 12,
"name": "reset_error_code",
"data_type": ""
"data_type": "NONE"
}
},
{
Expand Down Expand Up @@ -248,7 +248,7 @@
"object_id": 5,
"resource_id": 2,
"name": "update",
"data_type": ""
"data_type": "NONE"
}
},
{
Expand All @@ -264,20 +264,11 @@
"model": "sensordata.resourcetype",
"fields": {
"object_id": 5,
"resource_id": 4,
"resource_id": 5,
"name": "update_result",
"data_type": "INTEGER"
}
},
{
"model": "sensordata.resourcetype",
"fields": {
"object_id": 5,
"resource_id": 5,
"name": "package_name",
"data_type": "STRING"
}
},
{
"model": "sensordata.resourcetype",
"fields": {
Expand Down
31 changes: 30 additions & 1 deletion server/django/sensordata/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
Event,
EventResource,
EndpointOperation,
Firmware
Firmware,
FirmwareUpdate
)

log = logging.getLogger('sensordata')
Expand All @@ -37,6 +38,7 @@ def get_model_perms(self, request):
'view': True,
}


@admin.register(ResourceType)
class ResourceTypeAdmin(admin.ModelAdmin):
list_display = ('object_id', 'resource_id', 'name', 'data_type')
Expand All @@ -58,13 +60,15 @@ class EventResourceInline(admin.TabularInline):
can_delete = False
readonly_fields = ('resource',)


@admin.register(Resource)
class ResourceAdmin(admin.ModelAdmin):
list_display = ('endpoint', 'resource_type', 'timestamp_created')
search_fields = ('endpoint__endpoint', 'resource_type__name')
list_filter = ('endpoint__endpoint', 'resource_type', 'timestamp_created')
readonly_fields = ('timestamp_created',)


@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
list_display = ('endpoint', 'event_type', 'time')
Expand All @@ -73,6 +77,7 @@ class EventAdmin(admin.ModelAdmin):
readonly_fields = ('endpoint', 'event_type', 'time')
inlines = [EventResourceInline]


@admin.register(EventResource)
class EventResourceAdmin(admin.ModelAdmin):
list_display = ('event', 'resource')
Expand All @@ -90,6 +95,12 @@ class EndpointOperationAdmin(admin.ModelAdmin):
readonly_fields = ('status', 'timestamp_created', 'transmit_counter',
'last_attempt', 'operation_type')

# "sesource" has to be setup during create, make read-only after that
def get_readonly_fields(self, request, obj=None):
if obj is not None:
return ('resource',) + self.readonly_fields
return self.readonly_fields

def save_model(self, request, obj, form, change):
# Update created timestamp, as we handle a manual entry.
obj.timestamp_created = timezone.now()
Expand Down Expand Up @@ -118,3 +129,21 @@ def get_form(self, request, obj=None, **kwargs):
if obj:
form.base_fields['binary'].disabled = True
return form


@admin.register(FirmwareUpdate)
class FirmwareUpdateAdmin(admin.ModelAdmin):
list_display = ('endpoint', 'firmware', 'state', 'result',
'timestamp_created', 'timestamp_updated',
'send_uri_operation', 'execute_operation')
search_fields = ('endpoint__endpoint', 'firmware__version', 'state', 'result')
list_filter = ('state', 'result', 'timestamp_created', 'timestamp_updated')
readonly_fields = ('timestamp_created', 'timestamp_updated',
'result', 'state', 'send_uri_operation', 'execute_operation')

def save_model(self, request, obj, form, change):
# Custom save logic if needed
super().save_model(request, obj, form, change)

# Trigger the async task to process the operation
process_pending_operations.delay(obj.endpoint.endpoint)
28 changes: 28 additions & 0 deletions server/django/sensordata/migrations/0010_firmwareupdate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.0.6 on 2024-07-31 14:21

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sensordata', '0009_remove_firmware_download_url_and_more'),
]

operations = [
migrations.CreateModel(
name='FirmwareUpdate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('state', models.IntegerField(choices=[(0, 'IDLE'), (1, 'DOWNLOADING'), (2, 'DOWNLOADED'), (3, 'UPDATING')], default=0)),
('result', models.IntegerField(choices=[(0, 'DEFAULT'), (1, 'SUCCESS'), (2, 'NO STORAGE'), (3, 'OUT OF MEMORY'), (4, 'CONNECTION LOST'), (5, 'INTEGRITY FAILED'), (6, 'UNSUPPORTED FIRMWARE'), (7, 'INVALID URI'), (8, 'UPDATE FAILED'), (9, 'UNSUPPORTED PROTOCOL')], default=0)),
('timestamp_created', models.DateTimeField(auto_now_add=True)),
('timestamp_updated', models.DateTimeField(auto_now=True)),
('endpoint', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sensordata.endpoint')),
('execute_operation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='execute_operation', to='sensordata.endpointoperation')),
('firmware', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='sensordata.firmware')),
('send_uri_operation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='send_uri_operation', to='sensordata.endpointoperation')),
],
),
]
Loading
Loading