Skip to content

Commit

Permalink
ELEC-268: Add DBC shim script (#33)
Browse files Browse the repository at this point in the history
Add a shim script to generate a DBC from the ASCIIPB file using the old
CAN protocol. A new Makefile target (gen-dbc) is added. This script
handles all of the old CAN protocol (what I call "CANdlelight 1.0")
used for FSGP/ASC 2018 as far as I know, including:

    * Implicit Messages created as ACKs in the protocol layer
    * Treating the Battery V/T Message as a MUXed Message
    * Handling signed/unsigned typing of various Signals

The following messages (that are currently used) contain signed
signals, and are handled:

    * Drive Output:
        * throttle: int16_t
        * direction: int16_t
        * cruise_control: int16_t
        * mechanical_brake_state: int16_t
    * Cruise Target:
        * target speed: int16_t
    * Battery Aggregate V/C
        * voltage: uint16_t
        * current: int16_t
    * Motor Velocity:
        * vehicle_velocity_left: int16_t
        * vehicle_velocity_right: int16_t

In addition, this also updates the Travis CI configuration to deploy
the DBC (system_can.dbc) as part of a GitHub release.
  • Loading branch information
karlding authored Mar 2, 2019
1 parent f3c11e2 commit d180d9a
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,5 @@ pip-selfcheck.json
# Build artifacts
out/
genfiles/

system_can.dbc
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ script:
- make lint
- make test
- make gen
- make gen-dbc

before_deploy:
- zip codegen-tooling-out.zip out/*
Expand All @@ -39,7 +40,9 @@ deploy:
provider: releases
api_key:
secure: CT4XLLqlq8ZjObfWiP3xcz1aaG+3xbb3NGhL3IN0LVUUk9ADrbI3qE66OX5gEgI5AIpa1uEruD5+vCG27m6xTszrh/EXRhj/6Y/MtkReBmQL1jndR8sWHLN7rEN9dtG0t3LYivXAbmcF3jIQ/sWkUF64z8yAWeXJITBqyLhU77gE2iQ9WU2Yzlwyq9X+EBGR36jKvsmNw5vabqBTeGbqtnJpIdeXM2HF2iwK5ATHcLudzs0OIAqd2NjGfAt5Y8qEUWUWobcc+qxw+Mz/xue+054kSzd03MqSgmDRvxXNieaG5U6e//pzwUpuFlSLud6tBzStjO8K34jusyn9ONTKWcvfYbmPV4NqX60puYZrucUIeGsvn2RtBQY2WqzH3UmYKQVkcO8QlfXAPB1ewiB0mbRxx3ijypNc4hqWQiLunyCDrSuAFBV9h1mMeIKEgPmmgYS24U+1HfSbLWuh0i18eosNvtxwgDiYI6f+Uov0zUda59PCWB0QKZqFajKMypa+3EgHjqtO0kRburBEl7Fm2TMWadwAxae2mFJ9rW9vV6BHus5YPF3oZjrbbn570PZK5LzSDWh++qokj62/PSrIXhRsSU4Z64pex4+vC3GzmVQAzjhWyW8MKXEbGj8K6SEMHYiAgVbmFp2kFphHgMqjtz/lOSFZPNVqzyzDnBmBfDg=
file: codegen-tooling-out.zip
file:
- codegen-tooling-out.zip
- system_can.dbc
skip_cleanup: true
on:
repo: uw-midsun/codegen-tooling
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
.PHONY: lint protos gen test clean

.PHONY: gen
gen: protos
@echo "Generating from templates..."
@python codegen/build.py
@find out -type f \( -iname '*.[ch]' -o -iname '*.ts' \) | xargs -r clang-format -i -fallback-style=Google
@find out -type f \( -iname '*.go' \) | xargs -r gofmt -w

.PHONY: gen-dbc
gen-dbc:
@echo "Generating DBC file"
@python codegen/build_dbc.py

.PHONY: lint
lint:
@echo "Linting..."
@pylint --disable=F0401 codegen/

.PHONY: protos
protos:
@echo "Compiling protos..."
@mkdir -p genfiles
@protoc -I=schema --python_out=genfiles --go_out=genfiles schema/can.proto

.PHONY: test
test: gen
@echo "Testing..."
@python -m unittest discover -s codegen

.PHONY: clean
clean:
@rm -rf genfiles out
320 changes: 320 additions & 0 deletions codegen/build_dbc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
"""
This script generates a DBC file from our custom protobufs in order to make the
transition to DBCs more seamless. This is a temporary measure so we can
continue to use codegen-tooling, while switching over to using DBC decoders so
we do not have to maintain the dump scripts.
The idea is that we will eventually switch over to DBC files, or a higher level
DSL that compiles down into DBC files (ie. if our CAN protocol changes).
"""
from __future__ import absolute_import, division, print_function, unicode_literals

import cantools

# For the proto and asciipb parsing
import data

# The number of battery modules
NUM_BATTERY_MODULES = 36

# Length of fields (in bits)
FIELDS_LEN = {
'u8': 8,
'u16': 16,
'u32': 32,
'u64': 64
}

# pylint: disable=W0511
# TODO: Determine a way of encoding this in the ASCIIPB
SIGNED_MESSAGES = [
'DRIVE_OUTPUT',
'CRUISE_TARGET',
'BATTERY_AGGREGATE_VC',
'MOTOR_VELOCITY'
]

# pylint: disable=W0511
# TODO: Determine a way of encoding this in the ASCIIPB
ACKABLE_MESSAGES = {
0: [
'CHAOS',
'LIGHTS_FRONT',
'PLUTUS_SLAVE',
'DRIVER_CONTROLS'
],
1: [
'DRIVER_CONTROLS'
],
2: [
'PLUTUS'
],
3: [
'PLUTUS_SLAVE'
],
4: [
'MOTOR_CONTROLLER'
],
5: [
'SOLAR_MASTER_REAR'
],
6: [
'SOLAR_MASTER_FRONT'
],
7: [
'CHAOS'
],
8: [
'PLUTUS',
'MOTOR_CONTROLLER',
'DRIVER_CONTROLS'
],
}

def build_arbitration_id(msg_type, source_id, msg_id):
"""
typedef union CanId {
uint16_t raw;
struct {
uint16_t source_id : 4;
uint16_t type : 1;
uint16_t msg_id : 6;
};
} CanId;
"""
return ((source_id & ((0x1 << 4) - 1)) << (0)) | \
((msg_type & ((0x1 << 1) - 1)) << (4)) | \
((msg_id & ((0x1 << 6) - 1)) << (4 + 1))

def main():
"""The main entry-point of the program"""
# pylint: disable=R0914
database = cantools.database.can.Database(version='')

# Iterate through all the CAN device IDs and add them to the DBC.
can_devices = data.parse_can_device_enum()
for device_id, device_name in can_devices.items():
if device_name not in ['RESERVED']:
node = cantools.database.can.Node(
name=device_name,
comment='Device ID: {}'.format(hex(device_id))
)
database.nodes.append(node)

# Iterate through all the CAN messages IDs and:
#
# 1. Convert the Message ID to the CAN Arbitration ID.
# 2. Add the Message to the DBC.
# 3. For each of the signals, add it to the Message.
# 4. If the Message is critical, add an ACK message to the DBC.
can_messages = data.parse_can_frames('can_messages.asciipb')
device_enum = data.parse_can_device_enum()

def get_key_by_val(dictionary, val):
"""Helper function to get key for dictionary value"""
for key, value in dictionary.items():
if val == value:
return key
return None

for msg_id, can_frame in can_messages.items():
source = get_key_by_val(device_enum, can_frame.source)

def get_muxed_voltage_signal():
"""
Get the MUXed signals for the Voltage V/T message.
"""
# The MUX'd message is formatted like this:
#
# 16-bits 16-bits 16-bits
# +--------+---------+-------------+
# | id | voltage | temperature |
# +--------+---------+-------------+
#
# due to CANdlelight 1.0 alignment rules.
results = []

# Generate the signal used as the multiplexer. This is the `id`
# field comprising of the first 2 bytes (even though only 7 bits
# are necessary), since Voltage and Temperature are both 16-bit
# values.
multiplexer = cantools.database.can.Signal(
name='BATTERY_VT_INDEX',
start=0,
length=16,
is_multiplexer=True
)
results.append(multiplexer)

# Generate all the multiplexed signals for the Module Voltage and
# Temperatures
for i in range(NUM_BATTERY_MODULES):
# The voltage is the second signal field
voltage = cantools.database.can.Signal(
name='MODULE_VOLTAGE_{0:03d}'.format(i),
start=16,
length=16,
# The multiplexed ID is just the Cell Index
multiplexer_ids=[i],
# The multiplexer is the Module index
multiplexer_signal=results[0],
is_float=False,
decimal=None
)

# The Temperature is the third field
temperature = cantools.database.can.Signal(
name='MODULE_TEMP_{0:03d}'.format(i),
start=32,
length=16,
byte_order='little_endian',
# The multiplexed ID is just the Cell Index
multiplexer_ids=[i],
# The multiplexer is the Module index
multiplexer_signal=results[0],
is_float=False,
decimal=None
)

results.append(voltage)
results.append(temperature)

return results

# All these message types must be Data messages. ACK messages are
# currently handled implicitly by the protocol layer, and will be
# generated based on whether or not it is an ACKable message.
frame_id = build_arbitration_id(
msg_type=0,
source_id=source,
msg_id=msg_id
)

# We do a special case for BATTERY_VT since this is the only message
# that we currently do that uses MUXed data.
if can_frame.msg_name == 'BATTERY_VT':
signals = get_muxed_voltage_signal()

# It is safe to divide by 8 since every single message under the old
# protocol (aka. what I call CANdlelight 1.0) is byte-aligned. To be
# precise, it uses byte-alignment padding to fit 8, 4, 2, 1 bytes.
message = cantools.database.can.Message(
frame_id=frame_id,
name=can_frame.msg_name,
length=6,
signals=signals,
# The sender is the Message Source
senders=[can_frame.source]
)
database.messages.append(message)
else:
total_length = 0
signals = []
for index, field in enumerate(can_frame.fields):
length = FIELDS_LEN[can_frame.ftype]

# Unfortunately, our ASCIIPB doesn't denote whether a field is
# signed/unsigned, and it is up to the caller to properly unpack
# the CAN signal.
#
# The only Messages (and Signals) that are signed
# (and currently used) are:
#
# - Drive Output:
# - throttle: int16_t
# - direction: int16_t
# - cruise_control: int16_t
# - mechanical_brake_state: int16_t
# - Cruise Target:
# - target speed: int16_t
# - Battery Aggregate V/C
# - voltage: uint16_t
# - current: int16_t
# - Motor Velocity:
# - vehicle_velocity_left: int16_t
# - vehicle_velocity_right: int16_t
if can_frame.msg_name in SIGNED_MESSAGES \
and not field == 'voltage':
signal = cantools.database.can.Signal(
name=field,
start=index*length,
length=length,
is_signed=True
)
else:
# battery voltage is unsigned
signal = cantools.database.can.Signal(
name=field,
start=index*length,
length=length,
is_signed=False
)
signals.append(signal)
total_length += length

# Note: It is safe to divide by 8 since every single message under
# the old protocol (aka. what I call CANdlelight 1.0) is
# byte-aligned. To be precise, it uses byte-alignment padding to
# fit 8, 4, 2, 1 bytes.
message = cantools.database.can.Message(
frame_id=frame_id,
name=can_frame.msg_name,
length=total_length // 8,
signals=signals,
# The sender is the Message Source
senders=[can_frame.source]
)
database.messages.append(message)

def get_ack(sender, msg_name, msg_id):
"""
msg_id: the id of the message we are ACKing
"""
sender_id = get_key_by_val(device_enum, sender)
if sender_id is None:
print("Couldn't find {}".format(sender))

frame_id = build_arbitration_id(
msg_type=1,
source_id=sender_id,
msg_id=msg_id
)

# All ACK responders send a message containing a ACK_STATUS in
# CANdlelight 1.0
signals = [
cantools.database.can.Signal(
name='{}_FROM_{}_ACK_STATUS'.format(msg_name, sender),
start=0,
length=8
)
]
# This ACK message is always length 1, since it just fits the
# ACK_STATUS
message = cantools.database.can.Message(
frame_id=frame_id,
name='{}_ACK_FROM_{}'.format(msg_name, sender),
length=1,
signals=signals,
# The sender is the Message Source
senders=[sender]
)
return message


# If this requires an ACK, then we go through all of the receivers.
# Unfortunately, our ASCIIPB file doesn't have a notion of Receivers,
# so we hardcode this for now.
if msg_id in ACKABLE_MESSAGES:
for acker in ACKABLE_MESSAGES[msg_id]:
message = get_ack(acker, can_frame.msg_name, msg_id)

database.messages.append(message)

# Save as a DBC file
with open('system_can.dbc', 'w') as file_handle:
file_handle.write(database.as_dbc_string())
return

if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ isort==4.2.15
pylint==1.7.2
nose==1.3.7
parameterized==0.6.1
cantools==32.11.2

0 comments on commit d180d9a

Please sign in to comment.