Skip to content

Commit fbde691

Browse files
Merge pull request #4 from Zubax/crc-script
A script for populating the CRC and size fields in the AppDescriptor structure
2 parents a4053c6 + cca366a commit fbde691

File tree

5 files changed

+316
-0
lines changed

5 files changed

+316
-0
lines changed

.travis.yml

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ before_script:
1515
- sudo apt-get install python3-pip
1616
- sudo pip3 install "uavcan>=1.0.0.dev30"
1717

18+
after_script:
19+
- cd test/make_boot_descriptor/
20+
- ./test.sh
21+
1822
matrix:
1923
include:
2024
#

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ The parameters of the CRC-64 algorithm are the following:
9191
* Output xor: 0xFFFF'FFFF'FFFF'FFFF
9292
* Check: 0x62EC'59E3'F1A4'F00A
9393

94+
The CRC and size fields cannot be populated until after the application binary is compiled and linked.
95+
A possible way to populate these fields is to initialize them with zeroes in the source code,
96+
and then use the script `populate_app_descriptor.py` after the binary is generated to update the fields
97+
with their actual values.
98+
The script can be invoked from the build system (e.g., from a Makefile rule) trivially as follows:
99+
100+
```sh
101+
populate_app_descriptor.py firmware.bin
102+
```
103+
94104
The following diagram documents the state machine implemented in the `BootloaderController` class:
95105
![Kocherga State Machine Diagram](state_machine.svg "Kocherga State Machine Diagram")
96106

populate_app_descriptor.py

+293
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2012-2015 PX4 Development Team. All rights reserved.
4+
# Copyright (c) 2018-2019 Zubax Robotics <[email protected]>. All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions
8+
# are met:
9+
#
10+
# 1. Redistributions of source code must retain the above copyright
11+
# notice, this list of conditions and the following disclaimer.
12+
# 2. Redistributions in binary form must reproduce the above copyright
13+
# notice, this list of conditions and the following disclaimer in
14+
# the documentation and/or other materials provided with the
15+
# distribution.
16+
# 3. Neither the name PX4 nor the names of its contributors may be
17+
# used to endorse or promote products derived from this software
18+
# without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24+
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
27+
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28+
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31+
# POSSIBILITY OF SUCH DAMAGE.
32+
#
33+
# Based on make_can_boot_descriptor.py originally created by Ben Dyer, David Sidrane and Pavel Kirienko for PX4.
34+
# See https://github.com/PX4/Firmware/blob/nuttx_next/Tools/make_can_boot_descriptor.py
35+
#
36+
37+
import os
38+
import sys
39+
import struct
40+
import optparse
41+
import binascii
42+
from io import BytesIO
43+
44+
45+
class AppDescriptor(object):
46+
"""
47+
Kocherga application image descriptor format:
48+
uint64_t signature (bytes [7:0] set to 'APDesc00' in source)
49+
uint64_t image_crc (set to 0 in source, updated by this script)
50+
uint32_t image_size (set to 0 in source, updated by this script)
51+
uint32_t vcs_commit (set in source)
52+
uint8_t version_major (set in source)
53+
uint8_t version_minor (set in source)
54+
uint8_t flags (1 - release build, 2 - dirty build) (set in source)
55+
(uint8_t reserved)
56+
uint32_t build_timestamp_utc (set in source)
57+
"""
58+
59+
LENGTH = 32
60+
SIGNATURE = b'APDesc00'
61+
FORMAT = '<8sQLLBBBxL'
62+
63+
def __init__(self, bytes=None):
64+
self.signature = AppDescriptor.SIGNATURE
65+
self.image_crc = 0
66+
self.image_size = 0
67+
self.vcs_commit = 0
68+
self.version_major = 0
69+
self.version_minor = 0
70+
self.release_build = False
71+
self.dirty_build = False
72+
self.build_timestamp_utc = 0
73+
74+
if bytes:
75+
try:
76+
self.unpack(bytes)
77+
except Exception:
78+
raise ValueError("Invalid AppDescriptor: {0}".format(binascii.b2a_hex(bytes)))
79+
80+
def pack(self):
81+
flags = 0
82+
if self.release_build:
83+
flags |= 1
84+
85+
if self.dirty_build:
86+
flags |= 2
87+
88+
return struct.pack(self.FORMAT, self.signature, self.image_crc, self.image_size, self.vcs_commit,
89+
self.version_major, self.version_minor, flags, self.build_timestamp_utc)
90+
91+
def unpack(self, bytes):
92+
(self.signature,
93+
self.image_crc,
94+
self.image_size,
95+
self.vcs_commit,
96+
self.version_major,
97+
self.version_minor,
98+
flags,
99+
self.build_timestamp_utc) = struct.unpack(self.FORMAT, bytes)
100+
101+
self.release_build = bool(flags & 1)
102+
self.dirty_build = bool(flags & 2)
103+
104+
if not self.empty and not self.valid:
105+
raise ValueError()
106+
107+
@property
108+
def empty(self):
109+
return (self.signature == AppDescriptor.SIGNATURE and
110+
self.image_crc == 0 and self.image_size == 0)
111+
112+
@property
113+
def valid(self):
114+
return (self.signature == AppDescriptor.SIGNATURE and
115+
self.image_crc != 0 and self.image_size > 0 and
116+
self.build_timestamp_utc > 0)
117+
118+
119+
class FirmwareImage(object):
120+
# Padding to 8 bytes is required by Kocherga
121+
PADDING = 8
122+
123+
def __init__(self, path, mode="r"):
124+
self._file = open(path, (mode + "b").replace("bb", "b"))
125+
self._padding = self.PADDING
126+
127+
if "r" in mode:
128+
self._contents = BytesIO(self._file.read())
129+
else:
130+
self._contents = BytesIO()
131+
self._do_write = False
132+
133+
self._length = None
134+
self._descriptor_offset = None
135+
self._descriptor_bytes = None
136+
self._descriptor = None
137+
138+
def __enter__(self):
139+
return self
140+
141+
def __getattr__(self, attr):
142+
if attr == "write":
143+
self._do_write = True
144+
return getattr(self._contents, attr)
145+
146+
def __iter__(self):
147+
return iter(self._contents)
148+
149+
def __exit__(self, *args):
150+
if self._do_write:
151+
if getattr(self._file, "seek", None):
152+
self._file.seek(0)
153+
self._file.write(self._contents.getvalue())
154+
if self._padding:
155+
self._file.write(b'\xff' * self._padding)
156+
157+
self._file.close()
158+
159+
def _write_descriptor_raw(self):
160+
# Seek to the appropriate location, write the serialized descriptor, and seek back.
161+
prev_offset = self._contents.tell()
162+
self._contents.seek(self._descriptor_offset)
163+
self._contents.write(self._descriptor.pack())
164+
self._contents.seek(prev_offset)
165+
166+
def write_descriptor(self):
167+
# Set the descriptor's length and CRC to the values required for CRC computation
168+
self.app_descriptor.image_size = self.length
169+
self.app_descriptor.image_crc = 0
170+
171+
self._write_descriptor_raw()
172+
173+
# Update the descriptor's CRC based on the computed value and write it out again
174+
self.app_descriptor.image_crc = self.crc
175+
176+
self._write_descriptor_raw()
177+
178+
@property
179+
def crc(self):
180+
MASK = 0xFFFFFFFFFFFFFFFF
181+
POLY = 0x42F0E1EBA9EA3693
182+
183+
# Calculate the image CRC with the image_crc field in the app descriptor zeroed out.
184+
crc_offset = self.app_descriptor_offset + len(AppDescriptor.SIGNATURE)
185+
content = bytearray(self._contents.getvalue())
186+
content[crc_offset:crc_offset + 8] = bytearray(b"\x00" * 8)
187+
if self._padding:
188+
content += bytearray(b"\xff" * self._padding)
189+
val = MASK
190+
for byte in content:
191+
val ^= (byte << 56) & MASK
192+
for bit in range(8):
193+
if val & (1 << 63):
194+
val = ((val << 1) & MASK) ^ POLY
195+
else:
196+
val <<= 1
197+
198+
return (val & MASK) ^ MASK
199+
200+
@property
201+
def length(self):
202+
if not self._length:
203+
# Find the length of the file by seeking to the end and getting the offset
204+
prev_offset = self._contents.tell()
205+
self._contents.seek(0, os.SEEK_END)
206+
self._length = self._contents.tell()
207+
if self._padding:
208+
mod = self._length % self._padding
209+
self._padding = self._padding - mod if mod else 0
210+
self._length += self._padding
211+
self._contents.seek(prev_offset)
212+
213+
return self._length
214+
215+
@property
216+
def app_descriptor_offset(self):
217+
if not self._descriptor_offset:
218+
# Save the current position
219+
prev_offset = self._contents.tell()
220+
# Check each byte in the file to see if a valid descriptor starts at that location.
221+
# Slow, but not slow enough to matter.
222+
offset = 0
223+
while offset < self.length - AppDescriptor.LENGTH:
224+
self._contents.seek(offset)
225+
try:
226+
# If this throws an exception, there isn't a valid descriptor at this offset
227+
AppDescriptor(self._contents.read(AppDescriptor.LENGTH))
228+
except Exception:
229+
offset += 1
230+
else:
231+
self._descriptor_offset = offset
232+
break
233+
# Go back to the previous position
234+
self._contents.seek(prev_offset)
235+
236+
return self._descriptor_offset
237+
238+
@property
239+
def app_descriptor(self):
240+
if not self._descriptor:
241+
# Save the current position
242+
prev_offset = self._contents.tell()
243+
# Jump to the descriptor and parse it
244+
self._contents.seek(self.app_descriptor_offset)
245+
self._descriptor_bytes = self._contents.read(AppDescriptor.LENGTH)
246+
self._descriptor = AppDescriptor(self._descriptor_bytes)
247+
# Go back to the previous offset
248+
self._contents.seek(prev_offset)
249+
250+
return self._descriptor
251+
252+
@app_descriptor.setter
253+
def app_descriptor(self, value):
254+
self._descriptor = value
255+
256+
257+
if __name__ == "__main__":
258+
parser = optparse.OptionParser(usage=
259+
"Usage: %prog [options] <input binary>\n"
260+
"Example: %prog firmware.bin")
261+
parser.add_option("--also-patch-descriptor-in",
262+
dest="also_patch_descriptor_in",
263+
default=[],
264+
action='append',
265+
help="file(s) where the descriptor will be updated too (e.g., an ELF executable for debugging)",
266+
metavar="PATH")
267+
268+
options, args = parser.parse_args()
269+
if len(args) != 1:
270+
parser.error("Invalid usage")
271+
272+
input_binary = args[0]
273+
base_name_without_extension = os.path.basename(input_binary).rsplit('.', 1)[0]
274+
275+
with FirmwareImage(input_binary, "rb") as in_image:
276+
# Firmware version format adopted by this script (feel free to change):
277+
# <base name>-<major>.<minor>.<VCS commit>.<CRC>.application.bin
278+
out_file = '%s-%s.%s.%08x.%016x.application.bin' % (base_name_without_extension,
279+
in_image.app_descriptor.version_major,
280+
in_image.app_descriptor.version_minor,
281+
in_image.app_descriptor.vcs_commit,
282+
in_image.crc)
283+
with FirmwareImage(out_file, "wb") as out_image:
284+
image = in_image.read()
285+
out_image.write(image)
286+
out_image.write_descriptor()
287+
288+
for patchee in options.also_patch_descriptor_in:
289+
with open(patchee, "rb") as im:
290+
also_image = im.read()
291+
also_image = also_image.replace(in_image.app_descriptor.pack(), out_image.app_descriptor.pack())
292+
with open(patchee, "wb") as im:
293+
im.write(also_image)
111 Bytes
Binary file not shown.

test/populate_app_descriptor/test.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
../../populate_app_descriptor.py test.image-1.0.bin
6+
7+
# Padded correctly? File created?
8+
size=$(stat --printf="%s" *.application.bin)
9+
[ $(($size % 8)) -eq 0 ]

0 commit comments

Comments
 (0)