Add core OP support for CRC validation, with extra support for Volkswagen MQB (#836)
* Generalized core OP CRC support plus extra bits for Volkswagen MQB.
parent
26dd7e27c8
commit
d953b6a7aa
|
@ -177,14 +177,14 @@ BO_ 597 ROUGH_WHEEL_SPEED: 8 VSA
|
|||
SG_ SET_TO_X55_2 : 47|8@0+ (1,0) [0|255] "" NEO
|
||||
SG_ LONG_COUNTER : 55|8@0+ (1,0) [0|255] "" XXX
|
||||
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
|
||||
SG_ CHECKSUM : 59|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
|
||||
|
||||
BO_ 660 SCM_COMMANDS: 8 SCM
|
||||
SG_ RIGHT_BLINKER : 6|1@0+ (1,0) [0|1] "" NEO
|
||||
SG_ LEFT_BLINKER : 5|1@0+ (1,0) [0|1] "" NEO
|
||||
SG_ WIPERS_SPEED : 4|2@0+ (1,0) [0|3] "" NEO
|
||||
SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX
|
||||
SG_ CHECKSUM : 59|4@1+ (1,0) [0|15] "" XXX
|
||||
SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX
|
||||
|
||||
BO_ 661 XXX_10: 4 XXX
|
||||
SG_ COUNTER : 29|2@0+ (1,0) [0|3] "" XXX
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
|
||||
#define ARRAYSIZE(x) (sizeof(x)/sizeof(x[0]))
|
||||
|
||||
|
||||
unsigned int honda_checksum(unsigned int address, uint64_t d, int l);
|
||||
unsigned int toyota_checksum(unsigned int address, uint64_t d, int l);
|
||||
unsigned int pedal_checksum(unsigned int address, uint64_t d, int l);
|
||||
|
||||
void init_crc_lookup_tables();
|
||||
unsigned int volkswagen_crc(unsigned int address, uint64_t d, int l);
|
||||
|
||||
struct SignalPackValue {
|
||||
const char* name;
|
||||
double value;
|
||||
|
@ -44,6 +46,8 @@ enum SignalType {
|
|||
TOYOTA_CHECKSUM,
|
||||
PEDAL_CHECKSUM,
|
||||
PEDAL_COUNTER,
|
||||
VOLKSWAGEN_CHECKSUM,
|
||||
VOLKSWAGEN_COUNTER,
|
||||
};
|
||||
|
||||
struct Signal {
|
||||
|
|
|
@ -31,6 +31,10 @@ const Signal sigs_{{address}}[] = {
|
|||
.type = SignalType::PEDAL_CHECKSUM,
|
||||
{% elif address in [512, 513] and sig.name == "COUNTER_PEDAL" %}
|
||||
.type = SignalType::PEDAL_COUNTER,
|
||||
{% elif checksum_type == "volkswagen" and sig.name == "CHECKSUM" %}
|
||||
.type = SignalType::VOLKSWAGEN_CHECKSUM,
|
||||
{% elif checksum_type == "volkswagen" and sig.name == "COUNTER" %}
|
||||
.type = SignalType::VOLKSWAGEN_COUNTER,
|
||||
{% else %}
|
||||
.type = SignalType::DEFAULT,
|
||||
{% endif %}
|
||||
|
|
|
@ -41,6 +41,8 @@ typedef enum {
|
|||
TOYOTA_CHECKSUM,
|
||||
PEDAL_CHECKSUM,
|
||||
PEDAL_COUNTER,
|
||||
VOLKSWAGEN_CHECKSUM,
|
||||
VOLKSWAGEN_COUNTER,
|
||||
} SignalType;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
@ -51,6 +51,8 @@ namespace {
|
|||
signal_lookup[std::make_pair(msg->address, std::string(sig->name))] = *sig;
|
||||
}
|
||||
}
|
||||
|
||||
init_crc_lookup_tables();
|
||||
}
|
||||
|
||||
uint64_t pack(uint32_t address, const std::vector<SignalPackValue> &signals, int counter) {
|
||||
|
@ -82,23 +84,30 @@ namespace {
|
|||
}
|
||||
auto sig = sig_it->second;
|
||||
|
||||
if (sig.type != SignalType::HONDA_COUNTER){
|
||||
if ((sig.type != SignalType::HONDA_COUNTER) && (sig.type != SignalType::VOLKSWAGEN_COUNTER)) {
|
||||
WARN("COUNTER signal type not valid\n");
|
||||
}
|
||||
|
||||
ret = set_value(ret, sig, counter);
|
||||
}
|
||||
|
||||
auto sig_it = signal_lookup.find(std::make_pair(address, "CHECKSUM"));
|
||||
if (sig_it != signal_lookup.end()) {
|
||||
auto sig = sig_it->second;
|
||||
if (sig.type == SignalType::HONDA_CHECKSUM){
|
||||
auto sig_it_checksum = signal_lookup.find(std::make_pair(address, "CHECKSUM"));
|
||||
if (sig_it_checksum != signal_lookup.end()) {
|
||||
auto sig = sig_it_checksum->second;
|
||||
if (sig.type == SignalType::HONDA_CHECKSUM) {
|
||||
unsigned int chksm = honda_checksum(address, ret, message_lookup[address].size);
|
||||
ret = set_value(ret, sig, chksm);
|
||||
}
|
||||
else if (sig.type == SignalType::TOYOTA_CHECKSUM){
|
||||
else if (sig.type == SignalType::TOYOTA_CHECKSUM) {
|
||||
unsigned int chksm = toyota_checksum(address, ret, message_lookup[address].size);
|
||||
ret = set_value(ret, sig, chksm);
|
||||
}
|
||||
else if (sig.type == SignalType::VOLKSWAGEN_CHECKSUM) {
|
||||
// FIXME: Hackish fix for an endianness issue. The message is in reverse byte order
|
||||
// until later in the pack process. Checksums can be run backwards, CRCs not so much.
|
||||
// The correct fix is unclear but this works for the moment.
|
||||
unsigned int chksm = volkswagen_crc(address, ReverseBytes(ret), message_lookup[address].size);
|
||||
ret = set_value(ret, sig, chksm);
|
||||
} else {
|
||||
//WARN("CHECKSUM signal type not valid\n");
|
||||
}
|
||||
|
|
|
@ -20,7 +20,9 @@ ctypedef enum SignalType:
|
|||
HONDA_COUNTER,
|
||||
TOYOTA_CHECKSUM,
|
||||
PEDAL_CHECKSUM,
|
||||
PEDAL_COUNTER
|
||||
PEDAL_COUNTER,
|
||||
VOLKSWAGEN_CHECKSUM,
|
||||
VOLKSWAGEN_COUNTER
|
||||
|
||||
cdef struct Signal:
|
||||
const char* name
|
||||
|
|
|
@ -24,9 +24,11 @@
|
|||
// #define DEBUG printf
|
||||
#define INFO printf
|
||||
|
||||
|
||||
#define MAX_BAD_COUNTER 5
|
||||
|
||||
// Static lookup table for fast computation of CRC8 poly 0x2F, aka 8H2F/AUTOSAR
|
||||
uint8_t crc8_lut_8h2f[256];
|
||||
|
||||
unsigned int honda_checksum(unsigned int address, uint64_t d, int l) {
|
||||
d >>= ((8-l)*8); // remove padding
|
||||
d >>= 4; // remove checksum
|
||||
|
@ -75,6 +77,98 @@ unsigned int pedal_checksum(unsigned int address, uint64_t d, int l) {
|
|||
return crc;
|
||||
}
|
||||
|
||||
void gen_crc_lookup_table(uint8_t poly, uint8_t crc_lut[])
|
||||
{
|
||||
uint8_t crc;
|
||||
int i, j;
|
||||
|
||||
for (i = 0; i < 256; i++) {
|
||||
crc = i;
|
||||
for (j = 0; j < 8; j++) {
|
||||
if ((crc & 0x80) != 0)
|
||||
crc = (uint8_t)((crc << 1) ^ poly);
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
crc_lut[i] = crc;
|
||||
}
|
||||
}
|
||||
|
||||
void init_crc_lookup_tables()
|
||||
{
|
||||
// At init time, set up static lookup tables for fast CRC computation.
|
||||
|
||||
gen_crc_lookup_table(0x2F, crc8_lut_8h2f); // CRC-8 8H2F/AUTOSAR for Volkswagen
|
||||
}
|
||||
|
||||
unsigned int volkswagen_crc(unsigned int address, uint64_t d, int l)
|
||||
{
|
||||
// Volkswagen uses standard CRC8 8H2F/AUTOSAR, but they compute it with
|
||||
// a magic variable padding byte tacked onto the end of the payload.
|
||||
// https://www.autosar.org/fileadmin/user_upload/standards/classic/4-3/AUTOSAR_SWS_CRCLibrary.pdf
|
||||
|
||||
uint8_t *dat = (uint8_t *)&d;
|
||||
uint8_t crc = 0xFF; // Standard init value for CRC8 8H2F/AUTOSAR
|
||||
|
||||
// CRC the payload first, skipping over the first byte where the CRC lives.
|
||||
for (int i = 1; i < l; i++) {
|
||||
crc ^= dat[i];
|
||||
crc = crc8_lut_8h2f[crc];
|
||||
}
|
||||
|
||||
// Look up and apply the magic final CRC padding byte, which permutes by CAN
|
||||
// address, and additionally (for SOME addresses) by the message counter.
|
||||
uint8_t counter = dat[1] & 0x0F;
|
||||
switch(address) {
|
||||
case 0x86: // LWI_01 Steering Angle
|
||||
crc ^= (uint8_t[]){0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86,0x86}[counter];
|
||||
break;
|
||||
case 0x9F: // EPS_01 Electric Power Steering
|
||||
crc ^= (uint8_t[]){0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5,0xF5}[counter];
|
||||
break;
|
||||
case 0xAD: // Getriebe_11 Automatic Gearbox
|
||||
crc ^= (uint8_t[]){0x3F,0x69,0x39,0xDC,0x94,0xF9,0x14,0x64,0xD8,0x6A,0x34,0xCE,0xA2,0x55,0xB5,0x2C}[counter];
|
||||
break;
|
||||
case 0xFD: // ESP_21 Electronic Stability Program
|
||||
crc ^= (uint8_t[]){0xB4,0xEF,0xF8,0x49,0x1E,0xE5,0xC2,0xC0,0x97,0x19,0x3C,0xC9,0xF1,0x98,0xD6,0x61}[counter];
|
||||
break;
|
||||
case 0x106: // ESP_05 Electronic Stability Program
|
||||
crc ^= (uint8_t[]){0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07}[counter];
|
||||
break;
|
||||
case 0x117: // ACC_10 Automatic Cruise Control
|
||||
crc ^= (uint8_t[]){0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC,0xAC}[counter];
|
||||
break;
|
||||
case 0x122: // ACC_06 Automatic Cruise Control
|
||||
crc ^= (uint8_t[]){0x37,0x7D,0xF3,0xA9,0x18,0x46,0x6D,0x4D,0x3D,0x71,0x92,0x9C,0xE5,0x32,0x10,0xB9}[counter];
|
||||
break;
|
||||
case 0x126: // HCA_01 Heading Control Assist
|
||||
crc ^= (uint8_t[]){0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA,0xDA}[counter];
|
||||
break;
|
||||
case 0x12B: // GRA_ACC_01 Steering wheel controls for ACC
|
||||
crc ^= (uint8_t[]){0x6A,0x38,0xB4,0x27,0x22,0xEF,0xE1,0xBB,0xF8,0x80,0x84,0x49,0xC7,0x9E,0x1E,0x2B}[counter];
|
||||
break;
|
||||
case 0x187: // EV_Gearshift "Gear" selection data for EVs with no gearbox
|
||||
crc ^= (uint8_t[]){0x7F,0xED,0x17,0xC2,0x7C,0xEB,0x44,0x21,0x01,0xFA,0xDB,0x15,0x4A,0x6B,0x23,0x05}[counter];
|
||||
break;
|
||||
case 0x30C: // ACC_02 Automatic Cruise Control
|
||||
crc ^= (uint8_t[]){0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F,0x0F}[counter];
|
||||
break;
|
||||
case 0x3C0: // Klemmen_Status_01 ignition and starting status
|
||||
crc ^= (uint8_t[]){0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3,0xC3}[counter];
|
||||
break;
|
||||
case 0x65D: // ESP_20 Electronic Stability Program
|
||||
crc ^= (uint8_t[]){0xAC,0xB3,0xAB,0xEB,0x7A,0xE1,0x3B,0xF7,0x73,0xBA,0x7C,0x9E,0x06,0x5F,0x02,0xD9}[counter];
|
||||
break;
|
||||
default: // As-yet undefined CAN message, CRC check expected to fail
|
||||
INFO("Attempt to CRC check undefined Volkswagen message 0x%02X\n", address);
|
||||
crc ^= (uint8_t[]){0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}[counter];
|
||||
break;
|
||||
}
|
||||
crc = crc8_lut_8h2f[crc];
|
||||
|
||||
return crc ^ 0xFF; // Return after standard final XOR for CRC8 8H2F/AUTOSAR
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t read_u64_be(const uint8_t* v) {
|
||||
|
@ -129,11 +223,11 @@ struct MessageState {
|
|||
tmp -= (tmp >> (sig.b2-1)) ? (1ULL << sig.b2) : 0; //signed
|
||||
}
|
||||
|
||||
DEBUG("parse %X %s -> %lld\n", address, sig.name, tmp);
|
||||
DEBUG("parse 0x%X %s -> %lld\n", address, sig.name, tmp);
|
||||
|
||||
if (sig.type == SignalType::HONDA_CHECKSUM) {
|
||||
if (honda_checksum(address, dat, size) != tmp) {
|
||||
INFO("%X CHECKSUM FAIL\n", address);
|
||||
INFO("0x%X CHECKSUM FAIL\n", address);
|
||||
return false;
|
||||
}
|
||||
} else if (sig.type == SignalType::HONDA_COUNTER) {
|
||||
|
@ -142,12 +236,21 @@ struct MessageState {
|
|||
}
|
||||
} else if (sig.type == SignalType::TOYOTA_CHECKSUM) {
|
||||
if (toyota_checksum(address, dat, size) != tmp) {
|
||||
INFO("%X CHECKSUM FAIL\n", address);
|
||||
INFO("0x%X CHECKSUM FAIL\n", address);
|
||||
return false;
|
||||
}
|
||||
} else if (sig.type == SignalType::VOLKSWAGEN_CHECKSUM) {
|
||||
if (volkswagen_crc(address, dat, size) != tmp) {
|
||||
INFO("0x%X CRC FAIL\n", address);
|
||||
return false;
|
||||
}
|
||||
} else if (sig.type == SignalType::VOLKSWAGEN_COUNTER) {
|
||||
if (!update_counter_generic(tmp, sig.b2)) {
|
||||
return false;
|
||||
}
|
||||
} else if (sig.type == SignalType::PEDAL_CHECKSUM) {
|
||||
if (pedal_checksum(address, dat, size) != tmp) {
|
||||
INFO("%X PEDAL CHECKSUM FAIL\n", address);
|
||||
INFO("0x%X PEDAL CHECKSUM FAIL\n", address);
|
||||
return false;
|
||||
}
|
||||
} else if (sig.type == SignalType::PEDAL_COUNTER) {
|
||||
|
@ -171,7 +274,7 @@ struct MessageState {
|
|||
if (((old_counter+1) & ((1 << cnt_size) -1)) != v) {
|
||||
counter_fail += 1;
|
||||
if (counter_fail > 1) {
|
||||
INFO("%X COUNTER FAIL %d -- %d vs %d\n", address, counter_fail, old_counter, (int)v);
|
||||
INFO("0x%X COUNTER FAIL %d -- %d vs %d\n", address, counter_fail, old_counter, (int)v);
|
||||
}
|
||||
if (counter_fail >= MAX_BAD_COUNTER) {
|
||||
return false;
|
||||
|
@ -223,7 +326,9 @@ class CANParser {
|
|||
}
|
||||
|
||||
dbc = dbc_lookup(dbc_name);
|
||||
assert(dbc);
|
||||
assert(dbc);
|
||||
init_crc_lookup_tables();
|
||||
|
||||
for (const auto& op : options) {
|
||||
MessageState state = {
|
||||
.address = op.address,
|
||||
|
|
|
@ -14,7 +14,9 @@ ctypedef enum SignalType:
|
|||
HONDA_COUNTER,
|
||||
TOYOTA_CHECKSUM,
|
||||
PEDAL_CHECKSUM,
|
||||
PEDAL_COUNTER
|
||||
PEDAL_COUNTER,
|
||||
VOLKSWAGEN_CHECKSUM,
|
||||
VOLKSWAGEN_COUNTER
|
||||
|
||||
cdef struct Signal:
|
||||
const char* name
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import glob
|
||||
import sys
|
||||
|
@ -39,50 +38,78 @@ def main():
|
|||
if dbc_mtime < out_mtime and template_mtime < out_mtime and this_file_mtime < out_mtime:
|
||||
continue #skip output is newer than template and dbc
|
||||
|
||||
msgs = [(address, msg_name, msg_size, sorted(msg_sigs, key=lambda s: s.name not in (b"COUNTER", b"CHECKSUM"))) # process counter and checksums first
|
||||
msgs = [(address, msg_name, msg_size, sorted(msg_sigs, key=lambda s: s.name not in ("COUNTER", "CHECKSUM"))) # process counter and checksums first
|
||||
for address, ((msg_name, msg_size), msg_sigs) in sorted(can_dbc.msgs.items()) if msg_sigs]
|
||||
|
||||
def_vals = {a: set(b) for a,b in can_dbc.def_vals.items()} #remove duplicates
|
||||
def_vals = [(address, sig) for address, sig in sorted(def_vals.items())]
|
||||
|
||||
if can_dbc.name.startswith("honda") or can_dbc.name.startswith("acura"):
|
||||
if can_dbc.name.startswith(("honda_", "acura_")):
|
||||
checksum_type = "honda"
|
||||
checksum_size = 4
|
||||
elif can_dbc.name.startswith("toyota") or can_dbc.name.startswith("lexus"):
|
||||
counter_size = 2
|
||||
checksum_start_bit = 3
|
||||
counter_start_bit = 5
|
||||
little_endian = False
|
||||
elif can_dbc.name.startswith(("toyota_", "lexus_")):
|
||||
checksum_type = "toyota"
|
||||
checksum_size = 8
|
||||
counter_size = None
|
||||
checksum_start_bit = 7
|
||||
counter_start_bit = None
|
||||
little_endian = False
|
||||
elif can_dbc.name.startswith(("vw_", "volkswagen_", "audi_", "seat_", "skoda_")):
|
||||
checksum_type = "volkswagen"
|
||||
checksum_size = 8
|
||||
counter_size = 4
|
||||
checksum_start_bit = 0
|
||||
counter_start_bit = 0
|
||||
little_endian = True
|
||||
else:
|
||||
checksum_type = None
|
||||
checksum_size = None
|
||||
counter_size = None
|
||||
checksum_start_bit = None
|
||||
counter_start_bit = None
|
||||
little_endian = None
|
||||
|
||||
# sanity checks on expected COUNTER and CHECKSUM rules, as packer and parser auto-compute those signals
|
||||
for address, msg_name, msg_size, sigs in msgs:
|
||||
dbc_msg_name = dbc_name + " " + msg_name
|
||||
for sig in sigs:
|
||||
if checksum_type is not None and sig.name == b"CHECKSUM":
|
||||
if sig.size != checksum_size:
|
||||
sys.exit("CHECKSUM is not %d bits longs %s" % (checksum_size, msg_name))
|
||||
if checksum_type == "honda" and sig.start_bit % 8 != 3:
|
||||
sys.exit("CHECKSUM starts at wrong bit %s" % msg_name)
|
||||
if checksum_type == "toyota" and sig.start_bit % 8 != 7:
|
||||
sys.exit("CHECKSUM starts at wrong bit %s" % msg_name)
|
||||
if checksum_type == "honda" and sig.name == b"COUNTER":
|
||||
if sig.size != 2:
|
||||
sys.exit("COUNTER is not 2 bits longs %s" % msg_name)
|
||||
if sig.start_bit % 8 != 5:
|
||||
sys.exit("COUNTER starts at wrong bit %s" % msg_name)
|
||||
if checksum_type is not None:
|
||||
# checksum rules
|
||||
if sig.name == "CHECKSUM":
|
||||
if sig.size != checksum_size:
|
||||
sys.exit("%s: CHECKSUM is not %d bits long" % (dbc_msg_name, checksum_size))
|
||||
if sig.start_bit % 8 != checksum_start_bit:
|
||||
sys.exit("%s: CHECKSUM starts at wrong bit" % dbc_msg_name)
|
||||
if little_endian != sig.is_little_endian:
|
||||
sys.exit("%s: CHECKSUM has wrong endianess" % dbc_msg_name)
|
||||
# counter rules
|
||||
if sig.name == "COUNTER":
|
||||
if counter_size is not None and sig.size != counter_size:
|
||||
sys.exit("%s: COUNTER is not %d bits long" % (dbc_msg_name, counter_size))
|
||||
if counter_start_bit is not None and sig.start_bit % 8 != counter_start_bit:
|
||||
print(counter_start_bit, sig.start_bit)
|
||||
sys.exit("%s: COUNTER starts at wrong bit" % dbc_msg_name)
|
||||
if little_endian != sig.is_little_endian:
|
||||
sys.exit("%s: COUNTER has wrong endianess" % dbc_msg_name)
|
||||
# pedal rules
|
||||
if address in [0x200, 0x201]:
|
||||
if sig.name == b"COUNTER_PEDAL" and sig.size != 4:
|
||||
sys.exit("PEDAL COUNTER is not 4 bits longs %s" % msg_name)
|
||||
if sig.name == b"CHECKSUM_PEDAL" and sig.size != 8:
|
||||
sys.exit("PEDAL CHECKSUM is not 8 bits longs %s" % msg_name)
|
||||
if sig.name == "COUNTER_PEDAL" and sig.size != 4:
|
||||
sys.exit("%s: PEDAL COUNTER is not 4 bits long" % dbc_msg_name)
|
||||
if sig.name == "CHECKSUM_PEDAL" and sig.size != 8:
|
||||
sys.exit("%s: PEDAL CHECKSUM is not 8 bits long" % dbc_msg_name)
|
||||
|
||||
# Fail on duplicate message names
|
||||
c = Counter([msg_name for address, msg_name, msg_size, sigs in msgs])
|
||||
for name, count in c.items():
|
||||
if count > 1:
|
||||
sys.exit("Duplicate message name in DBC file %s" % name)
|
||||
sys.exit("%s: Duplicate message name in DBC file %s" % (dbc_name, name))
|
||||
|
||||
parser_code = template.render(dbc=can_dbc, checksum_type=checksum_type, msgs=msgs, def_vals=def_vals, len=len)
|
||||
|
||||
|
||||
with open(out_fn, "w") as out_f:
|
||||
out_f.write(parser_code)
|
||||
|
||||
|
|
Loading…
Reference in New Issue