///|
pub(all) enum DbcWriteError {
DbcWriteInvalidToken(String, String)
DbcWriteInvalidQuotedText(String, String)
DbcWriteInvalidPayloadSize(UInt, Int)
DbcWriteNonFiniteSignalNumber(UInt, String, String)
DbcWriteNegativeMultiplexValue(UInt, String, Int)
DbcWriteInvalidMultiplexRange(UInt, String, Int, Int)
DbcWriteEmptyValueTable(UInt, String)
DbcWriteEmptyGlobalValueTable(String)
DbcWriteDuplicateTransmitter(UInt, String)
DbcWriteInvalidMessageIdentifier(UInt)
DbcWriteInvalidAttribute(String)
DbcWriteInvalidSignalGroup(UInt, String)
DbcWriteInvalidEnvironmentVariable(String)
} derive(Eq, Debug)
///|
fn dbc_token_is_valid(text : StringView) -> Bool {
if text.is_empty() {
return false
}
for char in text.iter() {
if char == ' ' ||
char == '\t' ||
char == '\r' ||
char == '\n' ||
char == ':' ||
char == ',' ||
char == ';' ||
char == '"' {
return false
}
}
true
}
///|
fn check_dbc_token(
context : String,
text : String,
) -> Result[Unit, DbcWriteError] {
if dbc_token_is_valid(text) {
Ok(())
} else {
Err(DbcWriteInvalidToken(context, text))
}
}
///|
fn dbc_quoted_text_is_valid(text : StringView) -> Bool {
for char in text.iter() {
if char == '"' || char == '\r' || char == '\n' {
return false
}
}
true
}
///|
fn check_dbc_quoted_text(
context : String,
text : String,
) -> Result[Unit, DbcWriteError] {
if dbc_quoted_text_is_valid(text) {
Ok(())
} else {
Err(DbcWriteInvalidQuotedText(context, text))
}
}
///|
fn check_signal_number(
message_id : UInt,
signal : Signal,
field : String,
value : Double,
) -> Result[Unit, DbcWriteError] {
if value.is_nan() || value.is_inf() {
Err(DbcWriteNonFiniteSignalNumber(message_id, signal.name, field))
} else {
Ok(())
}
}
///|
fn signal_multiplex_text(
message_id : UInt,
signal : Signal,
) -> Result[String, DbcWriteError] {
match signal.multiplex {
Plain => Ok("")
Multiplexer => Ok(" M")
Multiplexed(value) =>
if value < 0 {
Err(DbcWriteNegativeMultiplexValue(message_id, signal.name, value))
} else {
Ok(" m\{value}")
}
MultiplexedRanges(_, ranges) =>
if ranges.is_empty() {
Err(DbcWriteInvalidMultiplexRange(message_id, signal.name, -1, -1))
} else if ranges[0].start < 0 || ranges[0].end < ranges[0].start {
Err(
DbcWriteInvalidMultiplexRange(
message_id,
signal.name,
ranges[0].start,
ranges[0].end,
),
)
} else {
Ok(" m\{ranges[0].start}")
}
}
}
///|
fn signal_byte_order_text(byte_order : ByteOrder) -> String {
match byte_order {
Motorola => "0"
Intel => "1"
}
}
///|
fn signal_sign_text(value_kind : ValueKind) -> String {
match value_kind {
Unsigned => "+"
Signed => "-"
}
}
///|
fn write_signal_line(
message : Message,
signal : Signal,
) -> Result[String, DbcWriteError] {
match check_dbc_token("signal name", signal.name) {
Err(error) => return Err(error)
Ok(_) => ()
}
match check_dbc_quoted_text("signal unit", signal.unit) {
Err(error) => return Err(error)
Ok(_) => ()
}
for receiver in signal.receivers {
match check_dbc_token("signal receiver", receiver) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
match check_signal_number(message.id, signal, "factor", signal.factor) {
Err(error) => return Err(error)
Ok(_) => ()
}
match check_signal_number(message.id, signal, "offset", signal.offset) {
Err(error) => return Err(error)
Ok(_) => ()
}
match check_signal_number(message.id, signal, "minimum", signal.minimum) {
Err(error) => return Err(error)
Ok(_) => ()
}
match check_signal_number(message.id, signal, "maximum", signal.maximum) {
Err(error) => return Err(error)
Ok(_) => ()
}
let multiplex = match signal_multiplex_text(message.id, signal) {
Err(error) => return Err(error)
Ok(text) => text
}
let receivers = if signal.receivers.is_empty() {
""
} else {
" \{signal.receivers.join(",")}"
}
Ok(
" SG_ \{signal.name}\{multiplex} : \{signal.start_bit}|\{signal.bit_length}@\{signal_byte_order_text(signal.byte_order)}\{signal_sign_text(signal.value_kind)} (\{signal.factor},\{signal.offset}) [\{signal.minimum}|\{signal.maximum}] \"\{signal.unit}\"\{receivers}",
)
}
///|
fn write_message(
lines : Array[String],
message : Message,
) -> Result[Unit, DbcWriteError] {
if dbc_message_id_parts(message.id) is None {
return Err(DbcWriteInvalidMessageIdentifier(message.id))
}
match check_dbc_token("message name", message.name) {
Err(error) => return Err(error)
Ok(_) => ()
}
match check_dbc_token("message transmitter", message.transmitter) {
Err(error) => return Err(error)
Ok(_) => ()
}
if message.payload_size < 0 || message.payload_size > 64 {
return Err(DbcWriteInvalidPayloadSize(message.id, message.payload_size))
}
lines.push(
"BO_ \{message.id} \{message.name}: \{message.payload_size} \{message.transmitter}",
)
for signal in message.signals {
match write_signal_line(message, signal) {
Err(error) => return Err(error)
Ok(line) => lines.push(line)
}
}
Ok(())
}
///|
fn write_message_transmitters(
message : Message,
) -> Result[String?, DbcWriteError] {
if message.additional_transmitters.is_empty() {
return Ok(None)
}
let transmitters = [message.transmitter]
for transmitter in message.additional_transmitters {
match check_dbc_token("message transmitter", transmitter) {
Err(error) => return Err(error)
Ok(_) => ()
}
for existing in transmitters {
if existing == transmitter {
return Err(DbcWriteDuplicateTransmitter(message.id, transmitter))
}
}
transmitters.push(transmitter)
}
Ok(Some("BO_TX_BU_ \{message.id} : \{transmitters.join(",")};"))
}
///|
fn has_additional_transmitters(messages : Array[Message]) -> Bool {
for message in messages {
if !message.additional_transmitters.is_empty() {
return true
}
}
false
}
///|
fn has_signal_value_types(messages : Array[Message]) -> Bool {
for message in messages {
for signal in message.signals {
if signal.value_type != Integer {
return true
}
}
}
false
}
///|
fn has_extended_multiplexing(messages : Array[Message]) -> Bool {
for message in messages {
for signal in message.signals {
if signal.multiplex is MultiplexedRanges(_, _) {
return true
}
}
}
false
}
///|
fn write_extended_multiplexing(
lines : Array[String],
message : Message,
) -> Result[Unit, DbcWriteError] {
for signal in message.signals {
match signal.multiplex {
MultiplexedRanges(selector_name, ranges) => {
match check_dbc_token("multiplexer name", selector_name) {
Err(error) => return Err(error)
Ok(_) => ()
}
if ranges.is_empty() {
return Err(
DbcWriteInvalidMultiplexRange(message.id, signal.name, -1, -1),
)
}
let parts : Array[String] = []
for range in ranges {
if range.start < 0 || range.end < range.start {
return Err(
DbcWriteInvalidMultiplexRange(
message.id,
signal.name,
range.start,
range.end,
),
)
}
parts.push("\{range.start}-\{range.end}")
}
lines.push(
"SG_MUL_VAL_ \{message.id} \{signal.name} \{selector_name} \{parts.join(", ")};",
)
}
_ => ()
}
}
Ok(())
}
///|
fn write_signal_value_types(lines : Array[String], message : Message) -> Unit {
for signal in message.signals {
match signal.value_type {
Integer => ()
Float32 => lines.push("SIG_VALTYPE_ \{message.id} \{signal.name} : 1;")
Float64 => lines.push("SIG_VALTYPE_ \{message.id} \{signal.name} : 2;")
}
}
}
///|
fn write_value_table(
lines : Array[String],
table : ValueTable,
) -> Result[Unit, DbcWriteError] {
if dbc_message_id_parts(table.message_id) is None {
return Err(DbcWriteInvalidMessageIdentifier(table.message_id))
}
match check_dbc_token("value table signal name", table.signal_name) {
Err(error) => return Err(error)
Ok(_) => ()
}
if table.entries.is_empty() {
return Err(DbcWriteEmptyValueTable(table.message_id, table.signal_name))
}
let parts = ["VAL_ \{table.message_id} \{table.signal_name}"]
for entry in table.entries {
let label = match escape_dbc_comment(entry.label) {
Ok(value) => value
Err(error) => return Err(error)
}
parts.push("\{entry.value} \"\{label}\"")
}
let body = parts.join(" ")
lines.push("\{body};")
Ok(())
}
///|
fn write_global_value_table(
lines : Array[String],
table : GlobalValueTable,
) -> Result[Unit, DbcWriteError] {
match check_dbc_token("global value table name", table.name) {
Err(error) => return Err(error)
Ok(_) => ()
}
if table.entries.is_empty() {
return Err(DbcWriteEmptyGlobalValueTable(table.name))
}
let parts = ["VAL_TABLE_ \{table.name}"]
for entry in table.entries {
let label = match escape_dbc_comment(entry.label) {
Ok(value) => value
Err(error) => return Err(error)
}
parts.push("\{entry.value} \"\{label}\"")
}
lines.push("\{parts.join(" ")};")
Ok(())
}
///|
fn write_signal_group(
lines : Array[String],
group : SignalGroup,
) -> Result[Unit, DbcWriteError] {
if dbc_message_id_parts(group.message_id) is None ||
group.name.is_empty() ||
group.repetition < 0 ||
group.signals.is_empty() {
return Err(DbcWriteInvalidSignalGroup(group.message_id, group.name))
}
match check_dbc_token("signal group name", group.name) {
Err(_) =>
return Err(DbcWriteInvalidSignalGroup(group.message_id, group.name))
Ok(_) => ()
}
for signal in group.signals {
match check_dbc_token("signal group member", signal) {
Err(_) =>
return Err(DbcWriteInvalidSignalGroup(group.message_id, group.name))
Ok(_) => ()
}
}
lines.push(
"SIG_GROUP_ \{group.message_id} \{group.name} \{group.repetition} : \{group.signals.join(" ")};",
)
Ok(())
}
///|
fn escape_dbc_comment(text : String) -> Result[String, DbcWriteError] {
let chars : Array[Char] = []
for char in text.iter() {
if char == '\r' || char == '\n' {
return Err(DbcWriteInvalidQuotedText("comment", text))
}
if char == '"' || char == '\\' {
chars.push('\\')
}
chars.push(char)
}
Ok(String::from_array(chars))
}
///|
fn write_comment(comment : DbcComment) -> Result[String, DbcWriteError] {
let prefix = match comment.target {
DatabaseComment => "CM_"
NodeComment(node) => {
match check_dbc_token("comment node", node) {
Err(error) => return Err(error)
Ok(_) => ()
}
"CM_ BU_ \{node}"
}
MessageComment(message_id) => {
if dbc_message_id_parts(message_id) is None {
return Err(DbcWriteInvalidMessageIdentifier(message_id))
}
"CM_ BO_ \{message_id}"
}
SignalComment(message_id, signal_name) => {
if dbc_message_id_parts(message_id) is None {
return Err(DbcWriteInvalidMessageIdentifier(message_id))
}
match check_dbc_token("comment signal", signal_name) {
Err(error) => return Err(error)
Ok(_) => ()
}
"CM_ SG_ \{message_id} \{signal_name}"
}
EnvironmentComment(name) => {
match check_dbc_token("comment environment variable", name) {
Err(error) => return Err(error)
Ok(_) => ()
}
"CM_ EV_ \{name}"
}
}
let text = match escape_dbc_comment(comment.text) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok("\{prefix} \"\{text}\";")
}
///|
fn write_environment_variable(
lines : Array[String],
variable : EnvironmentVariable,
) -> Result[Unit, DbcWriteError] {
let mut valid = dbc_token_is_valid(variable.name) &&
variable.value_type >= 0 &&
variable.value_type <= 2 &&
variable.access_type >= 0 &&
variable.access_type <= 3 &&
!variable.minimum.is_nan() &&
!variable.minimum.is_inf() &&
!variable.maximum.is_nan() &&
!variable.maximum.is_inf() &&
!variable.initial_value.is_nan() &&
!variable.initial_value.is_inf() &&
variable.minimum <= variable.maximum &&
variable.initial_value >= variable.minimum &&
variable.initial_value <= variable.maximum &&
!variable.access_nodes.is_empty()
for node in variable.access_nodes {
if !dbc_token_is_valid(node) {
valid = false
}
}
if !valid {
return Err(DbcWriteInvalidEnvironmentVariable(variable.name))
}
let unit = match escape_dbc_comment(variable.unit) {
Ok(value) => value
Err(_) => return Err(DbcWriteInvalidEnvironmentVariable(variable.name))
}
lines.push(
"EV_ \{variable.name} : \{variable.value_type} [\{variable.minimum}|\{variable.maximum}] \"\{unit}\" \{variable.initial_value} \{variable.id} DUMMY_NODE_VECTOR\{variable.access_type} \{variable.access_nodes.join(",")};",
)
match variable.data_size {
Some(size) => lines.push("ENVVAR_DATA_ \{variable.name} : \{size};")
None => ()
}
Ok(())
}
///|
/// Write the supported database model as deterministic DBC text. Structurally
/// invalid models should be checked with `Database::validate` before export;
/// this method reports values that cannot be represented by the parser syntax.
pub fn Database::to_dbc(self : Database) -> Result[String, DbcWriteError] {
let lines : Array[String] = []
let has_transmitter_section = has_additional_transmitters(self.messages)
let has_value_type_section = has_signal_value_types(self.messages)
let has_extended_multiplex_section = has_extended_multiplexing(self.messages)
let has_attribute_section = !self.attributes.definitions.is_empty() ||
!self.attributes.defaults.is_empty() ||
!self.attributes.assignments.is_empty()
match self.version {
Some(version) => {
match check_dbc_quoted_text("database version", version) {
Err(error) => return Err(error)
Ok(_) => ()
}
lines.push("VERSION \"\{version}\"")
lines.push("")
}
None => ()
}
for node in self.nodes {
match check_dbc_token("node name", node) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
if self.nodes.is_empty() {
lines.push("BU_:")
} else {
lines.push("BU_: \{self.nodes.join(" ")}")
}
if !self.messages.is_empty() ||
!self.comments.is_empty() ||
has_attribute_section ||
!self.value_tables.is_empty() ||
!self.global_value_tables.is_empty() {
lines.push("")
}
for index, message in self.messages {
match write_message(lines, message) {
Err(error) => return Err(error)
Ok(_) => ()
}
if index + 1 < self.messages.length() ||
has_value_type_section ||
has_extended_multiplex_section ||
has_attribute_section ||
has_transmitter_section ||
!self.comments.is_empty() ||
!self.value_tables.is_empty() ||
!self.global_value_tables.is_empty() {
lines.push("")
}
}
for message in self.messages {
write_signal_value_types(lines, message)
}
if has_value_type_section &&
(
has_extended_multiplex_section ||
has_attribute_section ||
has_transmitter_section ||
!self.comments.is_empty() ||
!self.value_tables.is_empty() ||
!self.global_value_tables.is_empty()
) {
lines.push("")
}
for message in self.messages {
match write_extended_multiplexing(lines, message) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
if has_extended_multiplex_section &&
(
has_transmitter_section ||
has_attribute_section ||
!self.comments.is_empty() ||
!self.value_tables.is_empty() ||
!self.global_value_tables.is_empty()
) {
lines.push("")
}
for message in self.messages {
match write_message_transmitters(message) {
Err(error) => return Err(error)
Ok(Some(line)) => lines.push(line)
Ok(None) => ()
}
}
if has_transmitter_section &&
(
has_attribute_section ||
!self.comments.is_empty() ||
!self.value_tables.is_empty() ||
!self.global_value_tables.is_empty()
) {
lines.push("")
}
for comment in self.comments {
match write_comment(comment) {
Err(error) => return Err(error)
Ok(line) => lines.push(line)
}
}
if !self.comments.is_empty() &&
(
has_attribute_section ||
!self.value_tables.is_empty() ||
!self.global_value_tables.is_empty()
) {
lines.push("")
}
match write_attributes(lines, self.attributes) {
Err(error) => return Err(error)
Ok(_) => ()
}
if has_attribute_section &&
(!self.value_tables.is_empty() || !self.global_value_tables.is_empty()) {
lines.push("")
}
for index, table in self.value_tables {
match write_value_table(lines, table) {
Err(error) => return Err(error)
Ok(_) => ()
}
if index + 1 < self.value_tables.length() ||
!self.global_value_tables.is_empty() ||
!self.signal_groups.is_empty() {
lines.push("")
}
}
for index, table in self.global_value_tables {
match write_global_value_table(lines, table) {
Err(error) => return Err(error)
Ok(_) => ()
}
if index + 1 < self.global_value_tables.length() ||
!self.signal_groups.is_empty() {
lines.push("")
}
}
for index, group in self.signal_groups {
match write_signal_group(lines, group) {
Err(error) => return Err(error)
Ok(_) => ()
}
if index + 1 < self.signal_groups.length() {
lines.push("")
}
}
if !self.environment_variables.is_empty() {
if lines.last() != Some("") {
lines.push("")
}
for variable in self.environment_variables {
match write_environment_variable(lines, variable) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
}
Ok(lines.join("\n"))
}