-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
5 changed files
with
337 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -165,3 +165,5 @@ pip-selfcheck.json | |
# Build artifacts | ||
out/ | ||
genfiles/ | ||
|
||
system_can.dbc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ isort==4.2.15 | |
pylint==1.7.2 | ||
nose==1.3.7 | ||
parameterized==0.6.1 | ||
cantools==32.11.2 |