hitl_tester.modules.bms.hitl_data

(c) 2020-2024 TurnAround Factor, Inc.

#

CUI DISTRIBUTION CONTROL

Controlled by: DLA J68 R&D SBIP

CUI Category: Small Business Research and Technology

Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS

POC: GOV SBIP Program Manager Denise Price, 571-767-0111

Distribution authorized to U.S. Government Agencies only, to protect information not owned by the

U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that

it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests

for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,

Fort Belvoir, VA 22060-6221

#

SBIR DATA RIGHTS

Contract No.:SP4701-23-C-0083

Contractor Name: TurnAround Factor, Inc.

Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005

Expiration of SBIR Data Rights Period: September 24, 2029

The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer

software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights

in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause

contained in the above identified contract. No restrictions apply after the expiration date shown above. Any

reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce

the markings.

  1# TODO: fix Python warnings
  2# fmt: off
  3# pylint: skip-file
  4
  5"""
  6# (c) 2020-2024 TurnAround Factor, Inc.
  7#
  8# CUI DISTRIBUTION CONTROL
  9# Controlled by: DLA J68 R&D SBIP
 10# CUI Category: Small Business Research and Technology
 11# Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
 12# POC: GOV SBIP Program Manager Denise Price, 571-767-0111
 13# Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
 14# U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
 15# it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
 16# for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
 17# Fort Belvoir, VA 22060-6221
 18#
 19# SBIR DATA RIGHTS
 20# Contract No.:SP4701-23-C-0083
 21# Contractor Name: TurnAround Factor, Inc.
 22# Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
 23# Expiration of SBIR Data Rights Period: September 24, 2029
 24# The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
 25# software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
 26# in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
 27# contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
 28# reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
 29# the markings.
 30"""
 31
 32import datetime
 33import struct
 34
 35C_CONSTANTS = {
 36    "n_currents": 2,
 37    "n_cells_series": 4,
 38    "n_cells_parallel": 3,
 39    "n_temps": 2,
 40    "n_batteries": 1,
 41    "n_terminals": 1,
 42    "n_adcs": 3,
 43    "n_smbus_word_size": 2,
 44    "n_smbus_string_size": 25,
 45}
 46board_arguement = ""
 47build_folder = ""
 48
 49
 50# {CVARIABLE_START}
 51# These translate into C types and help Python struct understand sizing.
 52class CVariable:
 53    """Represents a C variable that Python can also understand."""
 54
 55    def __init__(self, name, *args, enum={}):
 56        """
 57        Supply the name of the variable to construct.
 58        Supply multiple optional numbers to represent an array or nested array.
 59        """
 60        # Save passed in args
 61        self.name = name
 62        self.array_length = args
 63        # track the parent CVariable that this CVariable belongs to
 64        self.container_cvar = None
 65        # This determines if the CVariable was nested (e.g. StructVar)
 66        self.is_nested = False
 67        # Mark no value set yet; variables can store values and interpret them.
 68        self.value = None
 69        # Store the enum of values this can take on for lookup purposes.
 70        self.enum = enum
 71        # Assume no reverse enum entries at first
 72        self.reverse_enum = {}
 73        # Populate reverse enum entries if any exist
 74        for key, value in enum.items():
 75            self.reverse_enum[value] = key
 76
 77    def set_value(self, value):
 78        self.value = value
 79
 80    def _C_name(self):
 81        return f"{self.c_type} {self.name}"
 82
 83    def set_container(self, container):
 84        self.container_cvar = container
 85
 86    def __repr__(self):
 87        """Basic string representing this object."""
 88        string = ""
 89        if self.container_cvar is not None:
 90            string += str(self.container_cvar)
 91        string += self.name
 92        return string
 93
 94    def __str__(self):
 95        """Make this object look nice when printing."""
 96        string = repr(self)
 97        if self.value is not None:
 98            if len(self.reverse_enum) > 0:
 99                string += f" = {self.reverse_enum[self.value]}"
100            else:
101                string += f" = {self.value}"
102        return string
103
104    def to_C(self):
105        """Returns a C-compatible version of this variable."""
106        code = self._C_name()
107        for array in self.array_length:
108            code += f"[{array}]"
109        return code
110
111    def to_Python(self):
112        """Returns a Python struct version of this variable."""
113        if self.is_nested:
114            # the struct_string will already be fully formed if the CVar
115            # is nested from another object
116            return self.struct_string
117        repeats = 1
118        for array in self.array_length:
119            # check to see if the value is a string
120            if array == str(array):
121                # look up the number by the string name
122                array = C_CONSTANTS[array]
123            # multiply the total number of nested values together to represent
124            # the full length of the primitive type.
125            repeats *= array
126        return f"{repeats}{self.struct_string}"
127
128    def _array_depth_first_search(self, array_length, prefix=[]):
129        """
130        Helper recursive function to parse through a nested array of CVars
131        until leaf CVars are found. Tracks variable names as it recurses
132        through and converts to C-friendly text reference.
133        """
134        current_set = []
135        array = array_length[0]
136        # check to see if the value is a string
137        if array == str(array):
138            # look up the number by the string name
139            array = C_CONSTANTS[array]
140        for i in range(0, array):
141            if len(array_length) > 1:
142                # nested array; recurse after removing one nested level
143                current_set += self._array_depth_first_search(array_length[1:], prefix + [i])
144            else:
145                lookup = prefix + [i]
146                # leaf node; add it to the list!
147                array_self = ArrayVar(self, lookup)
148                current_set.append(array_self)
149        return current_set
150
151    def to_variables(self):
152        """
153        Converts this structure into an ordered list of variables that
154        matches the order the data will be presented by serial;
155        generates multiple results for the array structures.
156        """
157        current_set = []
158        # if self.is_nested:
159        #    return self.struct.to_variables()
160        if self.array_length:
161            # concatenate nested list
162            current_set += self._array_depth_first_search(self.array_length)
163        else:
164            # leaf node. add it to the list!
165            current_set.append(self)
166        return current_set
167
168
169class Uint8(CVariable):
170    c_type = "uint8_t"
171    struct_string = "B"
172
173
174class Uint16(CVariable):
175    c_type = "uint16_t"
176    struct_string = "H"
177
178
179class Uint32(CVariable):
180    c_type = "uint32_t"
181    struct_string = "I"
182
183
184class Int32(CVariable):
185    c_type = "int32_t"
186    struct_string = "i"
187
188
189class Uint64(CVariable):
190    c_type = "uint64_t"
191    struct_string = "Q"
192
193
194class Timestamp(Uint64):
195    """
196    64-bit milliseconds-since-epoch.
197    It's a uint64, but make it print dates nicely.
198    """
199
200    def __str__(self):
201        """Make this object look nice when printing."""
202        string = super(Uint64, self).__str__()
203        if self.value is not None:
204            # convert value from epoch to string
205            seconds = int(self.value / 1000)
206            mseconds = self.value - (seconds * 1000)
207            string += " (" + str(datetime.datetime.utcfromtimestamp(seconds)) + "." + str(mseconds) + ")"
208        return string
209
210
211class StructVar(CVariable):
212    """
213    Represent a Structure as if it is a CVariable. Useful when a struct contains
214    a struct.
215    """
216
217    def __init__(self, struct, *args, **kwargs):
218        """
219        Pass in the structure that will be a variable.
220        Pass in all the other arguments expected of a CVariable after that.
221        """
222        # Call the original CVariable
223        super().__init__(*args, **kwargs)
224        # Parent struct
225        self.struct = struct
226        # The type is the struct's name
227        self.c_type = f"struct {struct.name}"
228        # The Python struct string is that of the structure
229        self.struct_string = struct.to_Python(root=False)
230        # Make sure Python struct is formatted correctly
231        self.is_nested = True
232
233    def __str__(self):
234        """Clean up structure naming"""
235        string = ""
236        if self.container_cvar is not None:
237            string += str(self.container_cvar)
238        string += self.name
239        string += "."
240        return string
241
242    def to_variables(self):
243        """
244        Converts this structure into an ordered list of variables that
245        matches the order the data will be presented by serial.
246        """
247        current_set = []
248        for cvar in self.struct.cvars:
249            cvar.set_container(self)
250            current_set += cvar.to_variables()
251        return current_set
252
253    # TODO values don't get saved for ArrayVar cuz they are generated by
254    # to_variables then thrown away.
255    def set_values(self, values):
256        """
257        Pass in values for each variable all at once.
258        Returns a list of variables with their value assignments.
259        """
260        # generate the full set of vars
261        all_vars = self.to_variables()
262        n_vars = len(all_vars)
263        if len(all_vars) != len(values):
264            raise f"Expected {n_vars} values but got {len(values)}"
265        for i in range(0, n_vars):
266            all_vars[i].set_value(values[i])
267        return all_vars
268
269
270class ArrayVar(CVariable):
271    """
272    Represent a particular index of some CVariable that is an array or a
273    nested array of the CVariable type in question.
274    """
275
276    def __init__(self, parent, index_path):
277        """
278        Pass in the parent CVariable that this is an element of.
279        Pass in the index path (nested index) for this element within the parent.
280        """
281        # Call the original CVariable
282        super().__init__(parent.name, parent.array_length, enum=parent.enum)
283        # The type matches the parent.
284        self.c_type = parent.c_type
285        # Save passed in args
286        self.parent = parent
287        self.index_path = index_path
288
289    def __repr__(self):
290        """Override string representation to include element position."""
291        return str(self.parent) + f"{self.index_path}"
292
293
294class Struct:
295    """Represents a C struct that contains CVariables."""
296
297    def __init__(self, name):
298        """Supply the name of the variable to construct."""
299        self.name = name
300        self.cvars = []
301
302    def __iter__(self):
303        """Iterate over CVariables within the struct."""
304        return iter(self.cvars)
305
306    def __add__(self, cvar):
307        """
308        Override the + operator so that this object can '+ Uint32()' for example.
309        returns itself, but tracks the C variable that was added in order.
310        """
311        self.cvars.append(cvar)
312        return self
313
314    def __len__(self):
315        """
316        Override the len() operator to return byte size of this structure.
317        """
318        return struct.calcsize(self.to_Python())
319
320    def __str__(self):
321        """Human readable form for this struct."""
322        return f"struct {self.name}"
323
324    def __call__(self, name, *args, **kwargs):
325        """
326        overload the () operator so that this struct can be used as a cvar in
327        other structs.
328        Pass in arguments as for a new CVariable.
329        """
330        return StructVar(self, name, *args, **kwargs)
331
332    def to_C(self):
333        """Converts this struct into C code."""
334        defines = []
335        code = f"struct {self.name} {{\n"
336        # convert each contained CVar into C code.
337        for cvar in self.cvars:
338            if cvar.enum is not None:
339                # track enums defined in each CVar for later
340                defines.append(cvar.enum)
341            code += f"  {cvar.to_C()};\n"
342        code += "};\n"
343        # convert each enum into #defines
344        for enum in defines:
345            for key, value in enum.items():
346                code += f"#define {key} {value}\n"
347        return code
348
349    def to_Python(self, root=True):
350        """Converts this struct into Python struct string."""
351        code = ""
352        if root:
353            # Only add the endian indicator on the outer-most call
354            code += "<"
355        for cvar in self.cvars:
356            code += cvar.to_Python()
357        return code
358
359
360# {FLAG_PROCESSOR}
361class GenerateFile:
362    """Code to dynamically generate files for C"""
363
364    front_matter = """
365/*
366 * THIS FILE WAS GENERATED BY A SCRIPT.
367 * EDIT FILES IN project_root/processing INSTEAD OF THIS FILE.
368 */
369"""
370
371    def __init__(self, filename):
372        """
373        Provide a filename without path or extension; subclasses will handle
374        those parts.
375        """
376        self.filename = filename
377
378    def to_C(self, rep_dict):
379        """
380        Pass in replacement dictionary with keys being search text and value
381        being replace text.
382        Returns the full file content.
383        """
384        with open(f"{self.path}{self.filename}{self.ext}", "w") as out_data:
385            with open(f"{self.filename}/{self.filename}{self.ext}", "r") as in_data:
386                code = self.front_matter
387                code += in_data.read()
388                for key, value in rep_dict.items():
389                    code_chunk = ""
390                    if type(value) is not list:
391                        # pack the value into a list so the for loop works
392                        value = [value]
393                    for val in value:
394                        if hasattr(val, "to_C"):
395                            # the value can be converted to C code, so do that
396                            code_chunk += val.to_C()
397                        else:
398                            # the value is already C code
399                            code_chunk += str(val)
400                    code = code.replace(key, str(code_chunk))
401                out_data.write(code)
402                print(f"{self.filename}{self.ext} generated")
403
404    def to_Code(self, table):
405        with open(f"{self.path}{self.filename}{self.ext}", "w") as out_data:
406            with open(f"{self.filename}/{self.filename}{self.ext}", "r") as in_data:
407                code = self.front_matter
408                code += in_data.read()
409                code_chunk = ""
410                for x in table:
411                    if self.ext == ".h":
412                        code_chunk += x.to_h()
413                    else:
414                        code_chunk += x.to_c()
415
416                if self.ext == ".c":
417                    code_chunk += f'const char Chemistry[] = "{x.chemistry}"' + " ;  \n"
418                if self.ext == ".h":
419                    code_chunk += f"extern const char Chemistry[];"
420                code = code.replace("//{DYNAMIC_CONTENT}//", str(code_chunk))
421                out_data.write(code)
422                print(f"{self.filename}{self.ext} generated")
423
424    def to_Mock(self):
425        with open(f"{self.main_path}{self.filename}{self.ext}", "r") as source_data:
426            with open(f"{self.filename}_mocks/{self.filename}{self.ext}", "r") as place_holder_data:
427                code = place_holder_data.read()
428                code_chunk = source_data.read()
429                code = code.replace("//{DYNAMIC_CONTENT}//", code_chunk)
430                with open(f"{self.mock_path}{self.filename}{self.ext}", "w") as mock_data:
431                    mock_data.write(code)
432                print(f"{self.filename}{self.ext} generated")
433
434
435class HeaderFile(GenerateFile):
436    """Code to dynamically generate a HeaderFile"""
437
438    ext = ".h"
439    path = "../" + build_folder + "/Core/Inc/"
440    main_path = "../Core/Inc/"
441    mock_path = "../test/mocks/"
442
443
444class CFile(GenerateFile):
445    """Code to dynamically generate a CFile"""
446
447    ext = ".c"
448    path = "../" + build_folder + "/Core/Src/"
449    main_path = "../Core/Src/"
450    mock_path = "../test/mocks/"
451
452
453class FlagProcessor:
454    """
455    Manage and build flag content.
456    """
457
458    # redundant h code to be generated
459    h_template = """
460// {comment}
461uint8_t flags_runtime_read_{section}_{name}();
462void flags_runtime_set_{section}_{name}();
463void flags_runtime_reset_{section}_{name}();
464"""
465
466    # redundant c code
467    c_template = """
468// {comment} is table {table}, bit {bit}
469uint8_t flags_runtime_read_{section}_{name}() {{
470  return flags_read(runtime_get_flags(), {table}, {bit});
471}}
472void flags_runtime_set_{section}_{name}() {{
473  flags_set(runtime_get_flags(), {table}, {bit});
474}}
475void flags_runtime_reset_{section}_{name}() {{
476  flags_reset(runtime_get_flags(), {table}, {bit});
477}}
478"""
479
480    # redundant flag section mask code
481    mask_h_template = """
482extern const uint64_t flags_runtime_{section}_any_mask;
483uint8_t flags_runtime_{section}_any_flag();
484"""
485    mask_c_template = """
486const uint64_t flags_runtime_{section}_any_mask = {mask};
487
488// returns 0 if no {section} flags, 1 if any {section} flags are present
489uint8_t flags_runtime_{section}_any_flag() {{
490  struct flags_collection *flags_runtime = runtime_get_flags();
491  return (flags_runtime->flags[{table}] & flags_runtime_{section}_any_mask) > 0;
492}}
493
494"""
495
496    # TODO calculate flag collection size dynamically
497    def __init__(self, flag_collection_size, flag_definitions):
498        """
499        Supply flag collection size; this is how many 64-bit pages are used.
500        Supply flag definitions. It should be a dictionary with named sections.
501        Each section's value should be a list of dictionaries. Each dictionary
502        inside should define the fields "name" and "comment".
503        """
504        self.flag_collection_size = flag_collection_size
505        self.sections = flag_definitions
506        self.section_masks = {}
507        self.name2bit = {}
508        self.bit2name = {}
509        self.bit2comment = {}
510
511        # preprocess bit data
512        self.generate_table_bits()
513
514    def get_name(self, section, item):
515        """Convert the given item and section into a canonical name."""
516        return f"{section}_{item}"
517
518    def generate_table_bits(self):
519        """Enumerates all flags in all sections with table and bit values."""
520        table_counter = 0
521        bit_counter = 0
522        section_mask_previous = 0
523        # Iterate over all items in sections
524        for section in self.sections:
525            section_name = section["name"]
526            for item in section["fields"]:
527                # assign table and bit
528                item["section"] = section_name
529                item["table"] = table_counter
530                item["bit"] = bit_counter
531                # establish simple lookups
532                c_name = self.get_name(section_name, item["name"])
533                self.name2bit[c_name] = (table_counter, bit_counter)
534                self.bit2name[(table_counter, bit_counter)] = c_name
535                self.bit2comment[(table_counter, bit_counter)] = item["comment"]
536                # increment bit
537                bit_counter += 1
538                # TODO sections should NOT cross table boundaries so that flag
539                # masks work correctly. This needs to be more intelligent.
540                # increment table if necessary
541                if bit_counter == 64:
542                    bit_counter = 0
543                    table_counter += 1
544            # generate a bit mask to cover all bits from this section but excluding
545            # other sections
546            section_mask = 2**bit_counter - 1 - section_mask_previous
547            self.section_masks[section_name] = section_mask
548            # include this new section in the mask of previous sections to be
549            # excluded later
550            section_mask_previous |= section_mask
551
552    def write_header(self):
553        """Generate a header file."""
554        DYNAMIC_CONTENT = ""
555        for section in self.sections:
556            section_name = section["name"]
557            DYNAMIC_CONTENT += f"\n//// {section_name}s\n"
558            for item in section["fields"]:
559                # item["section"] = section_name
560                DYNAMIC_CONTENT += self.h_template.format(**item)
561            DYNAMIC_CONTENT += self.mask_h_template.format(section=section_name)
562
563        HeaderFile("task_flags").to_C(
564            {
565                "{FLAG_COLLECTION_SIZE}": str(self.flag_collection_size),
566                "//{DYNAMIC_CONTENT}//": DYNAMIC_CONTENT,
567            }
568        )
569
570    def write_c(self):
571        """Generate a C file."""
572        DYNAMIC_CONTENT = ""
573        for section in self.sections:
574            section_name = section["name"]
575            DYNAMIC_CONTENT += f"//// {section_name}s\n"
576            for item in section["fields"]:
577                DYNAMIC_CONTENT += self.c_template.format(**item)
578            # fetch section mask
579            section_mask = self.section_masks[section_name]
580            # fetch the table for the last item processed. all items should be on
581            # the same table.
582            table_counter = self.name2bit[self.get_name(section_name, item["name"])][0]
583            DYNAMIC_CONTENT += self.mask_c_template.format(section=section_name, mask=section_mask, table=table_counter)
584
585        CFile("task_flags").to_C(
586            {
587                "//{DYNAMIC_CONTENT}//": DYNAMIC_CONTENT,
588            }
589        )
590
591    def interpret_flags(self, table, value):
592        """Given a table and its value, interpret the flags."""
593        flags = []
594        for i in range(0, 64):
595            # iterate over 64 bits of the table
596            if value & 2**i != 0:
597                flags.append(self.bit2name[(table, i)] + " // " + self.bit2comment[(table, i)])
598        return flags
599
600    def bit_2_name(self, table, i):
601        return self.bit2name[(table, i)]
602
603
604# {INITIAL_VARS}
605VERSION = 7
606
607# How many calculation records to keep as history
608CALCULATION_DATA_HISTORY = 3
609
610# This is used to determine how many "pages" of 64-bit integers will be used
611# to store flags.
612
613FLAG_COLLECTION_SIZE = 1
614# {STRUCTS}
615# Determine how many user defined runtime variables to track
616RUNTIME_UINT = {
617    8: 8,
618    16: 4,
619    32: 2,
620    64: 1,
621}
622
623# Build the flags_collection struct
624flags_collection = Struct("flags_collection") + Uint64("flags", FLAG_COLLECTION_SIZE)
625
626# Build the measurements_raw struct
627measurements_raw = (
628    Struct("measurements_raw")
629    + Timestamp("timestamp")
630    + Uint32("current", "n_currents")
631    + Uint32("voltage_cell", "n_cells_series", 1)
632    + Uint32("voltage_terminal", "n_terminals")
633    + Uint32("temperature", "n_temps")
634    + Uint16("micro_temperature_raw")
635    + Uint8("ncharge_en_gpio")
636    + Uint8("n_wakeup_gpio")
637)
638
639# Build the measurements_cooked struct
640# TODO meas_timestamp is maybe not necessary since raw and cooked go together
641# linked inside of runtime_data now. no longer need time to correlate
642# calculations with their raw data?
643measurements_cooked = (
644    Struct("measurements_cooked")
645    + Timestamp("meas_timestamp")
646    + Timestamp("timestamp")
647    + Int32("mamps")
648    + Uint32("mvolt_battery", "n_batteries")
649    + Uint32("mvolt_cell", "n_cells_series")
650    + Uint32("mvolt_terminal", "n_terminals")
651    + Uint32("dk_temp", "n_temps")
652    + Uint16("micro_temperature_dc")
653    + Uint8("ncharge_en_gpio")
654    + Uint8("n_wakeup_gpio")
655)  #                    + Uint32('time_delta') \ # TODO add this for logging? \
656if board_arguement == "AVEX_v1_BOARD" or board_arguement == "AVEX_v2_BOARD":
657    measurements_cooked += Int32("charger_mamps")
658
659state_of_charge = (
660    Struct("state_of_charge")
661    + Uint8(
662        "cell_state",
663        "n_cells_series",
664        enum={
665            "SOC_CELL_STATE_UNKNOWN": 0,
666            "SOC_CELL_STATE_RESTING": 1,
667            "SOC_CELL_STATE_CHARGING_EXCITED": 2,
668            "SOC_CELL_STATE_DISCHARGING_EXCITED": 3,
669            "SOC_CELL_STATE_CHARGING_TIMER": 4,
670            "SOC_CELL_STATE_DISCHARGING_TIMER": 5,
671            "CALCULATION_DATA_HISTORY": CALCULATION_DATA_HISTORY,
672        },
673    )
674    + Uint8("percent_charged")
675    + Uint8("percent_health")
676    + Uint16("milliamp_hour_used")
677    + Uint16("milliamp_hour_remaining")
678    + Uint16("charge_cycles")
679    + Uint16("milliamp_hour_capacity")
680    + Uint32("Q_Max")
681    + Uint32("git_hash")
682    + Uint64("Columb_Count")
683)
684
685# Build the runtime_data_format struct
686runtime_data_format = (
687    Struct("runtime_data_format")
688    + Uint8("version")
689    + Uint32("build_date")
690    + measurements_raw("measures")
691    + measurements_cooked("calcs")
692    + flags_collection("flags")
693    + state_of_charge("soc")
694    + Uint8("BMS_State")
695    + Uint16("Wakeup_Counter")
696    + Uint32("Reset_Flags")
697    + Uint8("userdef_8", RUNTIME_UINT[8])
698    + Uint16("userdef_16", RUNTIME_UINT[16])
699    + Uint32("userdef_32", RUNTIME_UINT[32])
700    + Uint64("userdef_64", RUNTIME_UINT[64])
701)
702
703calculation_history = Struct("calculation_history_format") + measurements_cooked("calcs", CALCULATION_DATA_HISTORY)
704
705FLAGS_SECTION = [
706    {
707        "name": "interrupt",
708        "fields": [
709            {"comment": "RTC Wakeup Interrupt", "name": "rtc_wakeup"},
710            {"comment": "SMBUS Interrupt", "name": "smbus_wakeup"},
711            {"comment": "n_WAKEUP Interrupt", "name": "n_wakeup"},
712            {"comment": "OC_ONESHOT Interrupt", "name": "oc_oneshot"},
713            {"comment": "nCHARGE_EN Interrupt", "name": "ncharge_en"},
714        ],
715    },
716    {
717        "name": "fault",
718        "fields": [
719            {"comment": "Too hot to charge", "name": "overtemp_charge"},
720            {"comment": "Too hot to discharge", "name": "overtemp_discharge"},
721            {"comment": "Too cold to charge", "name": "undertemp_charge"},
722            {"comment": "Too cold to discharge", "name": "undertemp_discharge"},
723            {"comment": "Any cell voltage too high to charge", "name": "overvoltage_charge"},
724            {"comment": "Any cell voltage too high to discharge", "name": "overvoltage_discharge"},
725            {"comment": "Any cell voltage too low to charge", "name": "undervoltage_charge"},
726            {"comment": "Terminal voltage too high", "name": "terminalvoltage_charge"},
727            {"comment": "Max charging current exceeded", "name": "overcurrent_charge"},
728            {"comment": "Battery charging when nCHARGE_EN == HIGH", "name": "nce_charge"},
729            {"comment": "Over current interrupt event", "name": "overcurrent_discharge"},
730            {"comment": "Sw overcurrent protection low current longer time", "name": "sw_overcurrent_discharge_1"},
731            {"comment": "Sw overcurrent protection high current short time", "name": "sw_overcurrent_discharge_2"},
732            {"comment": "Charging is too high for the temperature", "name": "undertemp_charge_rate"},
733            {"comment": "We can not sit on the charger for too long if so we will fault", "name": "overtime_charge"},
734        ],
735    },
736    {
737        "name": "faultslumber",
738        "fields": [{"comment": "Any cell voltage too low to discharge", "name": "undervoltage_discharge"}],
739    },
740    {
741        "name": "permanentdisable",
742        "fields": [
743            {"comment": "OverTemp - Cell Temperature too High", "name": "overtemp"},
744            {"comment": "OverVoltage - Cell Voltage too High", "name": "overvoltage"},
745            {"comment": "UnderVoltage - Cell Voltage too Low", "name": "undervoltage"},
746            {"comment": "CellImbalance - Cell Imbalance too Large", "name": "cellimbalance"},
747        ],
748    },
749    {
750        "name": "prefault",
751        "fields": [
752            {"comment": "Too hot to charge", "name": "overtemp_charge"},
753            {"comment": "Too hot to discharge", "name": "overtemp_discharge"},
754            {"comment": "Too cold to charge", "name": "undertemp_charge"},
755            {"comment": "Too cold to discharge", "name": "undertemp_discharge"},
756            {"comment": "Any cell voltage too high to charge", "name": "overvoltage_charge"},
757            {"comment": "Any cell voltage too high to discharge", "name": "overvoltage_discharge"},
758            {"comment": "Any cell voltage too low to charge", "name": "undervoltage_charge"},
759            {"comment": "Terminal voltage too high", "name": "terminalvoltage_charge"},
760            {"comment": "Max charging current exceeded", "name": "overcurrent_charge"},
761            {"comment": "Battery charging when nCHARGE_EN == HIGH", "name": "nce_charge"},
762            {"comment": "Over current interrupt event", "name": "overcurrent_discharge"},
763            {"comment": "Sw overcurrent protection low current longer time", "name": "sw_overcurrent_discharge_1"},
764            {"comment": "Sw overcurrent protection high current short time", "name": "sw_overcurrent_discharge_2"},
765            {"comment": "Charging is too high for the temperature", "name": "undertemp_charge_rate"},
766            {"comment": "We can not sit on the charger for too long if so we will fault", "name": "overtime_charge"},
767            {"comment": "Any cell voltage too low to discharge", "name": "undervoltage_discharge"},
768            {"comment": "OverTemp - Cell Temperature too High", "name": "overtemp"},
769            {"comment": "OverVoltage - Cell Voltage too High", "name": "overvoltage"},
770            {"comment": "UnderVoltage - Cell Voltage too Low", "name": "undervoltage"},
771            {"comment": "CellImbalance - Cell Imbalance too Large", "name": "cellimbalance"},
772        ],
773    },
774    {
775        "name": "measure",
776        "fields": [
777            {"comment": "Output fets are disabled so current cannot be measured", "name": "output_fets_disabled"}
778        ],
779    },
780    {
781        "name": "output",
782        "fields": [
783            {"comment": "Tracks whether C1_BAL is high or low", "name": "c1_bal"},
784            {"comment": "Tracks whether C2_BAL is high or low", "name": "c2_bal"},
785            {"comment": "Tracks whether C3_BAL is high or low", "name": "c3_bal"},
786            {"comment": "Tracks whether C4_BAL is high or low", "name": "c4_bal"},
787            {"comment": "If enabled writes runtime data to serial every cycle", "name": "constant_serial_runtime_data"},
788            {
789                "comment": "Excited lets us know we are (dis)charging past a certain current threshold",
790                "name": "excited",
791            },
792        ],
793    },
794    {
795        "name": "state",
796        "fields": [
797            {"comment": "Current state is calibration", "name": "calibration"},
798            {"comment": "Current state is Deep Slumber", "name": "deepslumber"},
799            {"comment": "Current state is Fast Sample", "name": "fastsample"},
800            {"comment": "Current state is Fault", "name": "fault"},
801            {"comment": "Current state is Fault Permanently Disabled", "name": "faultpermanentdisable"},
802            {"comment": "Current state is Fault Slumber", "name": "faultslumber"},
803            {"comment": "Current state is Prefault", "name": "prefault"},
804            {"comment": "Current state is Slow Sample", "name": "slowsample"},
805            {"comment": "State that doesnt sleep to take smbus packets", "name": "mcuboot"}
806        ],
807    },
808    {
809        "name": "cell_state",
810        "fields": [
811            {"comment": "Charging is active when current is above a certain threshold", "name": "charging"},
812            {"comment": "Discharging is active when current is below a certain threshold", "name": "discharging"},
813            {"comment": "Relaxed is active when the current is neither charging or discharging", "name": "relaxed"},
814        ],
815    },
816]
C_CONSTANTS = {'n_currents': 2, 'n_cells_series': 4, 'n_cells_parallel': 3, 'n_temps': 2, 'n_batteries': 1, 'n_terminals': 1, 'n_adcs': 3, 'n_smbus_word_size': 2, 'n_smbus_string_size': 25}
board_arguement = ''
build_folder = ''
class CVariable:
 53class CVariable:
 54    """Represents a C variable that Python can also understand."""
 55
 56    def __init__(self, name, *args, enum={}):
 57        """
 58        Supply the name of the variable to construct.
 59        Supply multiple optional numbers to represent an array or nested array.
 60        """
 61        # Save passed in args
 62        self.name = name
 63        self.array_length = args
 64        # track the parent CVariable that this CVariable belongs to
 65        self.container_cvar = None
 66        # This determines if the CVariable was nested (e.g. StructVar)
 67        self.is_nested = False
 68        # Mark no value set yet; variables can store values and interpret them.
 69        self.value = None
 70        # Store the enum of values this can take on for lookup purposes.
 71        self.enum = enum
 72        # Assume no reverse enum entries at first
 73        self.reverse_enum = {}
 74        # Populate reverse enum entries if any exist
 75        for key, value in enum.items():
 76            self.reverse_enum[value] = key
 77
 78    def set_value(self, value):
 79        self.value = value
 80
 81    def _C_name(self):
 82        return f"{self.c_type} {self.name}"
 83
 84    def set_container(self, container):
 85        self.container_cvar = container
 86
 87    def __repr__(self):
 88        """Basic string representing this object."""
 89        string = ""
 90        if self.container_cvar is not None:
 91            string += str(self.container_cvar)
 92        string += self.name
 93        return string
 94
 95    def __str__(self):
 96        """Make this object look nice when printing."""
 97        string = repr(self)
 98        if self.value is not None:
 99            if len(self.reverse_enum) > 0:
100                string += f" = {self.reverse_enum[self.value]}"
101            else:
102                string += f" = {self.value}"
103        return string
104
105    def to_C(self):
106        """Returns a C-compatible version of this variable."""
107        code = self._C_name()
108        for array in self.array_length:
109            code += f"[{array}]"
110        return code
111
112    def to_Python(self):
113        """Returns a Python struct version of this variable."""
114        if self.is_nested:
115            # the struct_string will already be fully formed if the CVar
116            # is nested from another object
117            return self.struct_string
118        repeats = 1
119        for array in self.array_length:
120            # check to see if the value is a string
121            if array == str(array):
122                # look up the number by the string name
123                array = C_CONSTANTS[array]
124            # multiply the total number of nested values together to represent
125            # the full length of the primitive type.
126            repeats *= array
127        return f"{repeats}{self.struct_string}"
128
129    def _array_depth_first_search(self, array_length, prefix=[]):
130        """
131        Helper recursive function to parse through a nested array of CVars
132        until leaf CVars are found. Tracks variable names as it recurses
133        through and converts to C-friendly text reference.
134        """
135        current_set = []
136        array = array_length[0]
137        # check to see if the value is a string
138        if array == str(array):
139            # look up the number by the string name
140            array = C_CONSTANTS[array]
141        for i in range(0, array):
142            if len(array_length) > 1:
143                # nested array; recurse after removing one nested level
144                current_set += self._array_depth_first_search(array_length[1:], prefix + [i])
145            else:
146                lookup = prefix + [i]
147                # leaf node; add it to the list!
148                array_self = ArrayVar(self, lookup)
149                current_set.append(array_self)
150        return current_set
151
152    def to_variables(self):
153        """
154        Converts this structure into an ordered list of variables that
155        matches the order the data will be presented by serial;
156        generates multiple results for the array structures.
157        """
158        current_set = []
159        # if self.is_nested:
160        #    return self.struct.to_variables()
161        if self.array_length:
162            # concatenate nested list
163            current_set += self._array_depth_first_search(self.array_length)
164        else:
165            # leaf node. add it to the list!
166            current_set.append(self)
167        return current_set

Represents a C variable that Python can also understand.

CVariable(name, *args, enum={})
56    def __init__(self, name, *args, enum={}):
57        """
58        Supply the name of the variable to construct.
59        Supply multiple optional numbers to represent an array or nested array.
60        """
61        # Save passed in args
62        self.name = name
63        self.array_length = args
64        # track the parent CVariable that this CVariable belongs to
65        self.container_cvar = None
66        # This determines if the CVariable was nested (e.g. StructVar)
67        self.is_nested = False
68        # Mark no value set yet; variables can store values and interpret them.
69        self.value = None
70        # Store the enum of values this can take on for lookup purposes.
71        self.enum = enum
72        # Assume no reverse enum entries at first
73        self.reverse_enum = {}
74        # Populate reverse enum entries if any exist
75        for key, value in enum.items():
76            self.reverse_enum[value] = key

Supply the name of the variable to construct. Supply multiple optional numbers to represent an array or nested array.

name
array_length
container_cvar
is_nested
value
enum
reverse_enum
def set_value(self, value):
78    def set_value(self, value):
79        self.value = value
def set_container(self, container):
84    def set_container(self, container):
85        self.container_cvar = container
def to_C(self):
105    def to_C(self):
106        """Returns a C-compatible version of this variable."""
107        code = self._C_name()
108        for array in self.array_length:
109            code += f"[{array}]"
110        return code

Returns a C-compatible version of this variable.

def to_Python(self):
112    def to_Python(self):
113        """Returns a Python struct version of this variable."""
114        if self.is_nested:
115            # the struct_string will already be fully formed if the CVar
116            # is nested from another object
117            return self.struct_string
118        repeats = 1
119        for array in self.array_length:
120            # check to see if the value is a string
121            if array == str(array):
122                # look up the number by the string name
123                array = C_CONSTANTS[array]
124            # multiply the total number of nested values together to represent
125            # the full length of the primitive type.
126            repeats *= array
127        return f"{repeats}{self.struct_string}"

Returns a Python struct version of this variable.

def to_variables(self):
152    def to_variables(self):
153        """
154        Converts this structure into an ordered list of variables that
155        matches the order the data will be presented by serial;
156        generates multiple results for the array structures.
157        """
158        current_set = []
159        # if self.is_nested:
160        #    return self.struct.to_variables()
161        if self.array_length:
162            # concatenate nested list
163            current_set += self._array_depth_first_search(self.array_length)
164        else:
165            # leaf node. add it to the list!
166            current_set.append(self)
167        return current_set

Converts this structure into an ordered list of variables that matches the order the data will be presented by serial; generates multiple results for the array structures.

class Uint8(CVariable):
170class Uint8(CVariable):
171    c_type = "uint8_t"
172    struct_string = "B"

Represents a C variable that Python can also understand.

c_type = 'uint8_t'
struct_string = 'B'
class Uint16(CVariable):
175class Uint16(CVariable):
176    c_type = "uint16_t"
177    struct_string = "H"

Represents a C variable that Python can also understand.

c_type = 'uint16_t'
struct_string = 'H'
class Uint32(CVariable):
180class Uint32(CVariable):
181    c_type = "uint32_t"
182    struct_string = "I"

Represents a C variable that Python can also understand.

c_type = 'uint32_t'
struct_string = 'I'
class Int32(CVariable):
185class Int32(CVariable):
186    c_type = "int32_t"
187    struct_string = "i"

Represents a C variable that Python can also understand.

c_type = 'int32_t'
struct_string = 'i'
class Uint64(CVariable):
190class Uint64(CVariable):
191    c_type = "uint64_t"
192    struct_string = "Q"

Represents a C variable that Python can also understand.

c_type = 'uint64_t'
struct_string = 'Q'
class Timestamp(Uint64):
195class Timestamp(Uint64):
196    """
197    64-bit milliseconds-since-epoch.
198    It's a uint64, but make it print dates nicely.
199    """
200
201    def __str__(self):
202        """Make this object look nice when printing."""
203        string = super(Uint64, self).__str__()
204        if self.value is not None:
205            # convert value from epoch to string
206            seconds = int(self.value / 1000)
207            mseconds = self.value - (seconds * 1000)
208            string += " (" + str(datetime.datetime.utcfromtimestamp(seconds)) + "." + str(mseconds) + ")"
209        return string

64-bit milliseconds-since-epoch. It's a uint64, but make it print dates nicely.

class StructVar(CVariable):
212class StructVar(CVariable):
213    """
214    Represent a Structure as if it is a CVariable. Useful when a struct contains
215    a struct.
216    """
217
218    def __init__(self, struct, *args, **kwargs):
219        """
220        Pass in the structure that will be a variable.
221        Pass in all the other arguments expected of a CVariable after that.
222        """
223        # Call the original CVariable
224        super().__init__(*args, **kwargs)
225        # Parent struct
226        self.struct = struct
227        # The type is the struct's name
228        self.c_type = f"struct {struct.name}"
229        # The Python struct string is that of the structure
230        self.struct_string = struct.to_Python(root=False)
231        # Make sure Python struct is formatted correctly
232        self.is_nested = True
233
234    def __str__(self):
235        """Clean up structure naming"""
236        string = ""
237        if self.container_cvar is not None:
238            string += str(self.container_cvar)
239        string += self.name
240        string += "."
241        return string
242
243    def to_variables(self):
244        """
245        Converts this structure into an ordered list of variables that
246        matches the order the data will be presented by serial.
247        """
248        current_set = []
249        for cvar in self.struct.cvars:
250            cvar.set_container(self)
251            current_set += cvar.to_variables()
252        return current_set
253
254    # TODO values don't get saved for ArrayVar cuz they are generated by
255    # to_variables then thrown away.
256    def set_values(self, values):
257        """
258        Pass in values for each variable all at once.
259        Returns a list of variables with their value assignments.
260        """
261        # generate the full set of vars
262        all_vars = self.to_variables()
263        n_vars = len(all_vars)
264        if len(all_vars) != len(values):
265            raise f"Expected {n_vars} values but got {len(values)}"
266        for i in range(0, n_vars):
267            all_vars[i].set_value(values[i])
268        return all_vars

Represent a Structure as if it is a CVariable. Useful when a struct contains a struct.

StructVar(struct, *args, **kwargs)
218    def __init__(self, struct, *args, **kwargs):
219        """
220        Pass in the structure that will be a variable.
221        Pass in all the other arguments expected of a CVariable after that.
222        """
223        # Call the original CVariable
224        super().__init__(*args, **kwargs)
225        # Parent struct
226        self.struct = struct
227        # The type is the struct's name
228        self.c_type = f"struct {struct.name}"
229        # The Python struct string is that of the structure
230        self.struct_string = struct.to_Python(root=False)
231        # Make sure Python struct is formatted correctly
232        self.is_nested = True

Pass in the structure that will be a variable. Pass in all the other arguments expected of a CVariable after that.

struct
c_type
struct_string
is_nested
def to_variables(self):
243    def to_variables(self):
244        """
245        Converts this structure into an ordered list of variables that
246        matches the order the data will be presented by serial.
247        """
248        current_set = []
249        for cvar in self.struct.cvars:
250            cvar.set_container(self)
251            current_set += cvar.to_variables()
252        return current_set

Converts this structure into an ordered list of variables that matches the order the data will be presented by serial.

def set_values(self, values):
256    def set_values(self, values):
257        """
258        Pass in values for each variable all at once.
259        Returns a list of variables with their value assignments.
260        """
261        # generate the full set of vars
262        all_vars = self.to_variables()
263        n_vars = len(all_vars)
264        if len(all_vars) != len(values):
265            raise f"Expected {n_vars} values but got {len(values)}"
266        for i in range(0, n_vars):
267            all_vars[i].set_value(values[i])
268        return all_vars

Pass in values for each variable all at once. Returns a list of variables with their value assignments.

class ArrayVar(CVariable):
271class ArrayVar(CVariable):
272    """
273    Represent a particular index of some CVariable that is an array or a
274    nested array of the CVariable type in question.
275    """
276
277    def __init__(self, parent, index_path):
278        """
279        Pass in the parent CVariable that this is an element of.
280        Pass in the index path (nested index) for this element within the parent.
281        """
282        # Call the original CVariable
283        super().__init__(parent.name, parent.array_length, enum=parent.enum)
284        # The type matches the parent.
285        self.c_type = parent.c_type
286        # Save passed in args
287        self.parent = parent
288        self.index_path = index_path
289
290    def __repr__(self):
291        """Override string representation to include element position."""
292        return str(self.parent) + f"{self.index_path}"

Represent a particular index of some CVariable that is an array or a nested array of the CVariable type in question.

ArrayVar(parent, index_path)
277    def __init__(self, parent, index_path):
278        """
279        Pass in the parent CVariable that this is an element of.
280        Pass in the index path (nested index) for this element within the parent.
281        """
282        # Call the original CVariable
283        super().__init__(parent.name, parent.array_length, enum=parent.enum)
284        # The type matches the parent.
285        self.c_type = parent.c_type
286        # Save passed in args
287        self.parent = parent
288        self.index_path = index_path

Pass in the parent CVariable that this is an element of. Pass in the index path (nested index) for this element within the parent.

c_type
parent
index_path
class Struct:
295class Struct:
296    """Represents a C struct that contains CVariables."""
297
298    def __init__(self, name):
299        """Supply the name of the variable to construct."""
300        self.name = name
301        self.cvars = []
302
303    def __iter__(self):
304        """Iterate over CVariables within the struct."""
305        return iter(self.cvars)
306
307    def __add__(self, cvar):
308        """
309        Override the + operator so that this object can '+ Uint32()' for example.
310        returns itself, but tracks the C variable that was added in order.
311        """
312        self.cvars.append(cvar)
313        return self
314
315    def __len__(self):
316        """
317        Override the len() operator to return byte size of this structure.
318        """
319        return struct.calcsize(self.to_Python())
320
321    def __str__(self):
322        """Human readable form for this struct."""
323        return f"struct {self.name}"
324
325    def __call__(self, name, *args, **kwargs):
326        """
327        overload the () operator so that this struct can be used as a cvar in
328        other structs.
329        Pass in arguments as for a new CVariable.
330        """
331        return StructVar(self, name, *args, **kwargs)
332
333    def to_C(self):
334        """Converts this struct into C code."""
335        defines = []
336        code = f"struct {self.name} {{\n"
337        # convert each contained CVar into C code.
338        for cvar in self.cvars:
339            if cvar.enum is not None:
340                # track enums defined in each CVar for later
341                defines.append(cvar.enum)
342            code += f"  {cvar.to_C()};\n"
343        code += "};\n"
344        # convert each enum into #defines
345        for enum in defines:
346            for key, value in enum.items():
347                code += f"#define {key} {value}\n"
348        return code
349
350    def to_Python(self, root=True):
351        """Converts this struct into Python struct string."""
352        code = ""
353        if root:
354            # Only add the endian indicator on the outer-most call
355            code += "<"
356        for cvar in self.cvars:
357            code += cvar.to_Python()
358        return code

Represents a C struct that contains CVariables.

Struct(name)
298    def __init__(self, name):
299        """Supply the name of the variable to construct."""
300        self.name = name
301        self.cvars = []

Supply the name of the variable to construct.

name
cvars
def to_C(self):
333    def to_C(self):
334        """Converts this struct into C code."""
335        defines = []
336        code = f"struct {self.name} {{\n"
337        # convert each contained CVar into C code.
338        for cvar in self.cvars:
339            if cvar.enum is not None:
340                # track enums defined in each CVar for later
341                defines.append(cvar.enum)
342            code += f"  {cvar.to_C()};\n"
343        code += "};\n"
344        # convert each enum into #defines
345        for enum in defines:
346            for key, value in enum.items():
347                code += f"#define {key} {value}\n"
348        return code

Converts this struct into C code.

def to_Python(self, root=True):
350    def to_Python(self, root=True):
351        """Converts this struct into Python struct string."""
352        code = ""
353        if root:
354            # Only add the endian indicator on the outer-most call
355            code += "<"
356        for cvar in self.cvars:
357            code += cvar.to_Python()
358        return code

Converts this struct into Python struct string.

class GenerateFile:
362class GenerateFile:
363    """Code to dynamically generate files for C"""
364
365    front_matter = """
366/*
367 * THIS FILE WAS GENERATED BY A SCRIPT.
368 * EDIT FILES IN project_root/processing INSTEAD OF THIS FILE.
369 */
370"""
371
372    def __init__(self, filename):
373        """
374        Provide a filename without path or extension; subclasses will handle
375        those parts.
376        """
377        self.filename = filename
378
379    def to_C(self, rep_dict):
380        """
381        Pass in replacement dictionary with keys being search text and value
382        being replace text.
383        Returns the full file content.
384        """
385        with open(f"{self.path}{self.filename}{self.ext}", "w") as out_data:
386            with open(f"{self.filename}/{self.filename}{self.ext}", "r") as in_data:
387                code = self.front_matter
388                code += in_data.read()
389                for key, value in rep_dict.items():
390                    code_chunk = ""
391                    if type(value) is not list:
392                        # pack the value into a list so the for loop works
393                        value = [value]
394                    for val in value:
395                        if hasattr(val, "to_C"):
396                            # the value can be converted to C code, so do that
397                            code_chunk += val.to_C()
398                        else:
399                            # the value is already C code
400                            code_chunk += str(val)
401                    code = code.replace(key, str(code_chunk))
402                out_data.write(code)
403                print(f"{self.filename}{self.ext} generated")
404
405    def to_Code(self, table):
406        with open(f"{self.path}{self.filename}{self.ext}", "w") as out_data:
407            with open(f"{self.filename}/{self.filename}{self.ext}", "r") as in_data:
408                code = self.front_matter
409                code += in_data.read()
410                code_chunk = ""
411                for x in table:
412                    if self.ext == ".h":
413                        code_chunk += x.to_h()
414                    else:
415                        code_chunk += x.to_c()
416
417                if self.ext == ".c":
418                    code_chunk += f'const char Chemistry[] = "{x.chemistry}"' + " ;  \n"
419                if self.ext == ".h":
420                    code_chunk += f"extern const char Chemistry[];"
421                code = code.replace("//{DYNAMIC_CONTENT}//", str(code_chunk))
422                out_data.write(code)
423                print(f"{self.filename}{self.ext} generated")
424
425    def to_Mock(self):
426        with open(f"{self.main_path}{self.filename}{self.ext}", "r") as source_data:
427            with open(f"{self.filename}_mocks/{self.filename}{self.ext}", "r") as place_holder_data:
428                code = place_holder_data.read()
429                code_chunk = source_data.read()
430                code = code.replace("//{DYNAMIC_CONTENT}//", code_chunk)
431                with open(f"{self.mock_path}{self.filename}{self.ext}", "w") as mock_data:
432                    mock_data.write(code)
433                print(f"{self.filename}{self.ext} generated")

Code to dynamically generate files for C

GenerateFile(filename)
372    def __init__(self, filename):
373        """
374        Provide a filename without path or extension; subclasses will handle
375        those parts.
376        """
377        self.filename = filename

Provide a filename without path or extension; subclasses will handle those parts.

front_matter = '\n/*\n * THIS FILE WAS GENERATED BY A SCRIPT.\n * EDIT FILES IN project_root/processing INSTEAD OF THIS FILE.\n */\n'
filename
def to_C(self, rep_dict):
379    def to_C(self, rep_dict):
380        """
381        Pass in replacement dictionary with keys being search text and value
382        being replace text.
383        Returns the full file content.
384        """
385        with open(f"{self.path}{self.filename}{self.ext}", "w") as out_data:
386            with open(f"{self.filename}/{self.filename}{self.ext}", "r") as in_data:
387                code = self.front_matter
388                code += in_data.read()
389                for key, value in rep_dict.items():
390                    code_chunk = ""
391                    if type(value) is not list:
392                        # pack the value into a list so the for loop works
393                        value = [value]
394                    for val in value:
395                        if hasattr(val, "to_C"):
396                            # the value can be converted to C code, so do that
397                            code_chunk += val.to_C()
398                        else:
399                            # the value is already C code
400                            code_chunk += str(val)
401                    code = code.replace(key, str(code_chunk))
402                out_data.write(code)
403                print(f"{self.filename}{self.ext} generated")

Pass in replacement dictionary with keys being search text and value being replace text. Returns the full file content.

def to_Code(self, table):
405    def to_Code(self, table):
406        with open(f"{self.path}{self.filename}{self.ext}", "w") as out_data:
407            with open(f"{self.filename}/{self.filename}{self.ext}", "r") as in_data:
408                code = self.front_matter
409                code += in_data.read()
410                code_chunk = ""
411                for x in table:
412                    if self.ext == ".h":
413                        code_chunk += x.to_h()
414                    else:
415                        code_chunk += x.to_c()
416
417                if self.ext == ".c":
418                    code_chunk += f'const char Chemistry[] = "{x.chemistry}"' + " ;  \n"
419                if self.ext == ".h":
420                    code_chunk += f"extern const char Chemistry[];"
421                code = code.replace("//{DYNAMIC_CONTENT}//", str(code_chunk))
422                out_data.write(code)
423                print(f"{self.filename}{self.ext} generated")
def to_Mock(self):
425    def to_Mock(self):
426        with open(f"{self.main_path}{self.filename}{self.ext}", "r") as source_data:
427            with open(f"{self.filename}_mocks/{self.filename}{self.ext}", "r") as place_holder_data:
428                code = place_holder_data.read()
429                code_chunk = source_data.read()
430                code = code.replace("//{DYNAMIC_CONTENT}//", code_chunk)
431                with open(f"{self.mock_path}{self.filename}{self.ext}", "w") as mock_data:
432                    mock_data.write(code)
433                print(f"{self.filename}{self.ext} generated")
class HeaderFile(GenerateFile):
436class HeaderFile(GenerateFile):
437    """Code to dynamically generate a HeaderFile"""
438
439    ext = ".h"
440    path = "../" + build_folder + "/Core/Inc/"
441    main_path = "../Core/Inc/"
442    mock_path = "../test/mocks/"

Code to dynamically generate a HeaderFile

ext = '.h'
path = '..//Core/Inc/'
main_path = '../Core/Inc/'
mock_path = '../test/mocks/'
class CFile(GenerateFile):
445class CFile(GenerateFile):
446    """Code to dynamically generate a CFile"""
447
448    ext = ".c"
449    path = "../" + build_folder + "/Core/Src/"
450    main_path = "../Core/Src/"
451    mock_path = "../test/mocks/"

Code to dynamically generate a CFile

ext = '.c'
path = '..//Core/Src/'
main_path = '../Core/Src/'
mock_path = '../test/mocks/'
class FlagProcessor:
454class FlagProcessor:
455    """
456    Manage and build flag content.
457    """
458
459    # redundant h code to be generated
460    h_template = """
461// {comment}
462uint8_t flags_runtime_read_{section}_{name}();
463void flags_runtime_set_{section}_{name}();
464void flags_runtime_reset_{section}_{name}();
465"""
466
467    # redundant c code
468    c_template = """
469// {comment} is table {table}, bit {bit}
470uint8_t flags_runtime_read_{section}_{name}() {{
471  return flags_read(runtime_get_flags(), {table}, {bit});
472}}
473void flags_runtime_set_{section}_{name}() {{
474  flags_set(runtime_get_flags(), {table}, {bit});
475}}
476void flags_runtime_reset_{section}_{name}() {{
477  flags_reset(runtime_get_flags(), {table}, {bit});
478}}
479"""
480
481    # redundant flag section mask code
482    mask_h_template = """
483extern const uint64_t flags_runtime_{section}_any_mask;
484uint8_t flags_runtime_{section}_any_flag();
485"""
486    mask_c_template = """
487const uint64_t flags_runtime_{section}_any_mask = {mask};
488
489// returns 0 if no {section} flags, 1 if any {section} flags are present
490uint8_t flags_runtime_{section}_any_flag() {{
491  struct flags_collection *flags_runtime = runtime_get_flags();
492  return (flags_runtime->flags[{table}] & flags_runtime_{section}_any_mask) > 0;
493}}
494
495"""
496
497    # TODO calculate flag collection size dynamically
498    def __init__(self, flag_collection_size, flag_definitions):
499        """
500        Supply flag collection size; this is how many 64-bit pages are used.
501        Supply flag definitions. It should be a dictionary with named sections.
502        Each section's value should be a list of dictionaries. Each dictionary
503        inside should define the fields "name" and "comment".
504        """
505        self.flag_collection_size = flag_collection_size
506        self.sections = flag_definitions
507        self.section_masks = {}
508        self.name2bit = {}
509        self.bit2name = {}
510        self.bit2comment = {}
511
512        # preprocess bit data
513        self.generate_table_bits()
514
515    def get_name(self, section, item):
516        """Convert the given item and section into a canonical name."""
517        return f"{section}_{item}"
518
519    def generate_table_bits(self):
520        """Enumerates all flags in all sections with table and bit values."""
521        table_counter = 0
522        bit_counter = 0
523        section_mask_previous = 0
524        # Iterate over all items in sections
525        for section in self.sections:
526            section_name = section["name"]
527            for item in section["fields"]:
528                # assign table and bit
529                item["section"] = section_name
530                item["table"] = table_counter
531                item["bit"] = bit_counter
532                # establish simple lookups
533                c_name = self.get_name(section_name, item["name"])
534                self.name2bit[c_name] = (table_counter, bit_counter)
535                self.bit2name[(table_counter, bit_counter)] = c_name
536                self.bit2comment[(table_counter, bit_counter)] = item["comment"]
537                # increment bit
538                bit_counter += 1
539                # TODO sections should NOT cross table boundaries so that flag
540                # masks work correctly. This needs to be more intelligent.
541                # increment table if necessary
542                if bit_counter == 64:
543                    bit_counter = 0
544                    table_counter += 1
545            # generate a bit mask to cover all bits from this section but excluding
546            # other sections
547            section_mask = 2**bit_counter - 1 - section_mask_previous
548            self.section_masks[section_name] = section_mask
549            # include this new section in the mask of previous sections to be
550            # excluded later
551            section_mask_previous |= section_mask
552
553    def write_header(self):
554        """Generate a header file."""
555        DYNAMIC_CONTENT = ""
556        for section in self.sections:
557            section_name = section["name"]
558            DYNAMIC_CONTENT += f"\n//// {section_name}s\n"
559            for item in section["fields"]:
560                # item["section"] = section_name
561                DYNAMIC_CONTENT += self.h_template.format(**item)
562            DYNAMIC_CONTENT += self.mask_h_template.format(section=section_name)
563
564        HeaderFile("task_flags").to_C(
565            {
566                "{FLAG_COLLECTION_SIZE}": str(self.flag_collection_size),
567                "//{DYNAMIC_CONTENT}//": DYNAMIC_CONTENT,
568            }
569        )
570
571    def write_c(self):
572        """Generate a C file."""
573        DYNAMIC_CONTENT = ""
574        for section in self.sections:
575            section_name = section["name"]
576            DYNAMIC_CONTENT += f"//// {section_name}s\n"
577            for item in section["fields"]:
578                DYNAMIC_CONTENT += self.c_template.format(**item)
579            # fetch section mask
580            section_mask = self.section_masks[section_name]
581            # fetch the table for the last item processed. all items should be on
582            # the same table.
583            table_counter = self.name2bit[self.get_name(section_name, item["name"])][0]
584            DYNAMIC_CONTENT += self.mask_c_template.format(section=section_name, mask=section_mask, table=table_counter)
585
586        CFile("task_flags").to_C(
587            {
588                "//{DYNAMIC_CONTENT}//": DYNAMIC_CONTENT,
589            }
590        )
591
592    def interpret_flags(self, table, value):
593        """Given a table and its value, interpret the flags."""
594        flags = []
595        for i in range(0, 64):
596            # iterate over 64 bits of the table
597            if value & 2**i != 0:
598                flags.append(self.bit2name[(table, i)] + " // " + self.bit2comment[(table, i)])
599        return flags
600
601    def bit_2_name(self, table, i):
602        return self.bit2name[(table, i)]

Manage and build flag content.

FlagProcessor(flag_collection_size, flag_definitions)
498    def __init__(self, flag_collection_size, flag_definitions):
499        """
500        Supply flag collection size; this is how many 64-bit pages are used.
501        Supply flag definitions. It should be a dictionary with named sections.
502        Each section's value should be a list of dictionaries. Each dictionary
503        inside should define the fields "name" and "comment".
504        """
505        self.flag_collection_size = flag_collection_size
506        self.sections = flag_definitions
507        self.section_masks = {}
508        self.name2bit = {}
509        self.bit2name = {}
510        self.bit2comment = {}
511
512        # preprocess bit data
513        self.generate_table_bits()

Supply flag collection size; this is how many 64-bit pages are used. Supply flag definitions. It should be a dictionary with named sections. Each section's value should be a list of dictionaries. Each dictionary inside should define the fields "name" and "comment".

h_template = '\n// {comment}\nuint8_t flags_runtime_read_{section}_{name}();\nvoid flags_runtime_set_{section}_{name}();\nvoid flags_runtime_reset_{section}_{name}();\n'
c_template = '\n// {comment} is table {table}, bit {bit}\nuint8_t flags_runtime_read_{section}_{name}() {{\n return flags_read(runtime_get_flags(), {table}, {bit});\n}}\nvoid flags_runtime_set_{section}_{name}() {{\n flags_set(runtime_get_flags(), {table}, {bit});\n}}\nvoid flags_runtime_reset_{section}_{name}() {{\n flags_reset(runtime_get_flags(), {table}, {bit});\n}}\n'
mask_h_template = '\nextern const uint64_t flags_runtime_{section}_any_mask;\nuint8_t flags_runtime_{section}_any_flag();\n'
mask_c_template = '\nconst uint64_t flags_runtime_{section}_any_mask = {mask};\n\n// returns 0 if no {section} flags, 1 if any {section} flags are present\nuint8_t flags_runtime_{section}_any_flag() {{\n struct flags_collection *flags_runtime = runtime_get_flags();\n return (flags_runtime->flags[{table}] & flags_runtime_{section}_any_mask) > 0;\n}}\n\n'
flag_collection_size
sections
section_masks
name2bit
bit2name
bit2comment
def get_name(self, section, item):
515    def get_name(self, section, item):
516        """Convert the given item and section into a canonical name."""
517        return f"{section}_{item}"

Convert the given item and section into a canonical name.

def generate_table_bits(self):
519    def generate_table_bits(self):
520        """Enumerates all flags in all sections with table and bit values."""
521        table_counter = 0
522        bit_counter = 0
523        section_mask_previous = 0
524        # Iterate over all items in sections
525        for section in self.sections:
526            section_name = section["name"]
527            for item in section["fields"]:
528                # assign table and bit
529                item["section"] = section_name
530                item["table"] = table_counter
531                item["bit"] = bit_counter
532                # establish simple lookups
533                c_name = self.get_name(section_name, item["name"])
534                self.name2bit[c_name] = (table_counter, bit_counter)
535                self.bit2name[(table_counter, bit_counter)] = c_name
536                self.bit2comment[(table_counter, bit_counter)] = item["comment"]
537                # increment bit
538                bit_counter += 1
539                # TODO sections should NOT cross table boundaries so that flag
540                # masks work correctly. This needs to be more intelligent.
541                # increment table if necessary
542                if bit_counter == 64:
543                    bit_counter = 0
544                    table_counter += 1
545            # generate a bit mask to cover all bits from this section but excluding
546            # other sections
547            section_mask = 2**bit_counter - 1 - section_mask_previous
548            self.section_masks[section_name] = section_mask
549            # include this new section in the mask of previous sections to be
550            # excluded later
551            section_mask_previous |= section_mask

Enumerates all flags in all sections with table and bit values.

def write_header(self):
553    def write_header(self):
554        """Generate a header file."""
555        DYNAMIC_CONTENT = ""
556        for section in self.sections:
557            section_name = section["name"]
558            DYNAMIC_CONTENT += f"\n//// {section_name}s\n"
559            for item in section["fields"]:
560                # item["section"] = section_name
561                DYNAMIC_CONTENT += self.h_template.format(**item)
562            DYNAMIC_CONTENT += self.mask_h_template.format(section=section_name)
563
564        HeaderFile("task_flags").to_C(
565            {
566                "{FLAG_COLLECTION_SIZE}": str(self.flag_collection_size),
567                "//{DYNAMIC_CONTENT}//": DYNAMIC_CONTENT,
568            }
569        )

Generate a header file.

def write_c(self):
571    def write_c(self):
572        """Generate a C file."""
573        DYNAMIC_CONTENT = ""
574        for section in self.sections:
575            section_name = section["name"]
576            DYNAMIC_CONTENT += f"//// {section_name}s\n"
577            for item in section["fields"]:
578                DYNAMIC_CONTENT += self.c_template.format(**item)
579            # fetch section mask
580            section_mask = self.section_masks[section_name]
581            # fetch the table for the last item processed. all items should be on
582            # the same table.
583            table_counter = self.name2bit[self.get_name(section_name, item["name"])][0]
584            DYNAMIC_CONTENT += self.mask_c_template.format(section=section_name, mask=section_mask, table=table_counter)
585
586        CFile("task_flags").to_C(
587            {
588                "//{DYNAMIC_CONTENT}//": DYNAMIC_CONTENT,
589            }
590        )

Generate a C file.

def interpret_flags(self, table, value):
592    def interpret_flags(self, table, value):
593        """Given a table and its value, interpret the flags."""
594        flags = []
595        for i in range(0, 64):
596            # iterate over 64 bits of the table
597            if value & 2**i != 0:
598                flags.append(self.bit2name[(table, i)] + " // " + self.bit2comment[(table, i)])
599        return flags

Given a table and its value, interpret the flags.

def bit_2_name(self, table, i):
601    def bit_2_name(self, table, i):
602        return self.bit2name[(table, i)]
VERSION = 7
CALCULATION_DATA_HISTORY = 3
FLAG_COLLECTION_SIZE = 1
RUNTIME_UINT = {8: 8, 16: 4, 32: 2, 64: 1}
flags_collection = <Struct object>
measurements_raw = <Struct object>
measurements_cooked = <Struct object>
state_of_charge = <Struct object>
runtime_data_format = <Struct object>
calculation_history = <Struct object>
FLAGS_SECTION = [{'name': 'interrupt', 'fields': [{'comment': 'RTC Wakeup Interrupt', 'name': 'rtc_wakeup'}, {'comment': 'SMBUS Interrupt', 'name': 'smbus_wakeup'}, {'comment': 'n_WAKEUP Interrupt', 'name': 'n_wakeup'}, {'comment': 'OC_ONESHOT Interrupt', 'name': 'oc_oneshot'}, {'comment': 'nCHARGE_EN Interrupt', 'name': 'ncharge_en'}]}, {'name': 'fault', 'fields': [{'comment': 'Too hot to charge', 'name': 'overtemp_charge'}, {'comment': 'Too hot to discharge', 'name': 'overtemp_discharge'}, {'comment': 'Too cold to charge', 'name': 'undertemp_charge'}, {'comment': 'Too cold to discharge', 'name': 'undertemp_discharge'}, {'comment': 'Any cell voltage too high to charge', 'name': 'overvoltage_charge'}, {'comment': 'Any cell voltage too high to discharge', 'name': 'overvoltage_discharge'}, {'comment': 'Any cell voltage too low to charge', 'name': 'undervoltage_charge'}, {'comment': 'Terminal voltage too high', 'name': 'terminalvoltage_charge'}, {'comment': 'Max charging current exceeded', 'name': 'overcurrent_charge'}, {'comment': 'Battery charging when nCHARGE_EN == HIGH', 'name': 'nce_charge'}, {'comment': 'Over current interrupt event', 'name': 'overcurrent_discharge'}, {'comment': 'Sw overcurrent protection low current longer time', 'name': 'sw_overcurrent_discharge_1'}, {'comment': 'Sw overcurrent protection high current short time', 'name': 'sw_overcurrent_discharge_2'}, {'comment': 'Charging is too high for the temperature', 'name': 'undertemp_charge_rate'}, {'comment': 'We can not sit on the charger for too long if so we will fault', 'name': 'overtime_charge'}]}, {'name': 'faultslumber', 'fields': [{'comment': 'Any cell voltage too low to discharge', 'name': 'undervoltage_discharge'}]}, {'name': 'permanentdisable', 'fields': [{'comment': 'OverTemp - Cell Temperature too High', 'name': 'overtemp'}, {'comment': 'OverVoltage - Cell Voltage too High', 'name': 'overvoltage'}, {'comment': 'UnderVoltage - Cell Voltage too Low', 'name': 'undervoltage'}, {'comment': 'CellImbalance - Cell Imbalance too Large', 'name': 'cellimbalance'}]}, {'name': 'prefault', 'fields': [{'comment': 'Too hot to charge', 'name': 'overtemp_charge'}, {'comment': 'Too hot to discharge', 'name': 'overtemp_discharge'}, {'comment': 'Too cold to charge', 'name': 'undertemp_charge'}, {'comment': 'Too cold to discharge', 'name': 'undertemp_discharge'}, {'comment': 'Any cell voltage too high to charge', 'name': 'overvoltage_charge'}, {'comment': 'Any cell voltage too high to discharge', 'name': 'overvoltage_discharge'}, {'comment': 'Any cell voltage too low to charge', 'name': 'undervoltage_charge'}, {'comment': 'Terminal voltage too high', 'name': 'terminalvoltage_charge'}, {'comment': 'Max charging current exceeded', 'name': 'overcurrent_charge'}, {'comment': 'Battery charging when nCHARGE_EN == HIGH', 'name': 'nce_charge'}, {'comment': 'Over current interrupt event', 'name': 'overcurrent_discharge'}, {'comment': 'Sw overcurrent protection low current longer time', 'name': 'sw_overcurrent_discharge_1'}, {'comment': 'Sw overcurrent protection high current short time', 'name': 'sw_overcurrent_discharge_2'}, {'comment': 'Charging is too high for the temperature', 'name': 'undertemp_charge_rate'}, {'comment': 'We can not sit on the charger for too long if so we will fault', 'name': 'overtime_charge'}, {'comment': 'Any cell voltage too low to discharge', 'name': 'undervoltage_discharge'}, {'comment': 'OverTemp - Cell Temperature too High', 'name': 'overtemp'}, {'comment': 'OverVoltage - Cell Voltage too High', 'name': 'overvoltage'}, {'comment': 'UnderVoltage - Cell Voltage too Low', 'name': 'undervoltage'}, {'comment': 'CellImbalance - Cell Imbalance too Large', 'name': 'cellimbalance'}]}, {'name': 'measure', 'fields': [{'comment': 'Output fets are disabled so current cannot be measured', 'name': 'output_fets_disabled'}]}, {'name': 'output', 'fields': [{'comment': 'Tracks whether C1_BAL is high or low', 'name': 'c1_bal'}, {'comment': 'Tracks whether C2_BAL is high or low', 'name': 'c2_bal'}, {'comment': 'Tracks whether C3_BAL is high or low', 'name': 'c3_bal'}, {'comment': 'Tracks whether C4_BAL is high or low', 'name': 'c4_bal'}, {'comment': 'If enabled writes runtime data to serial every cycle', 'name': 'constant_serial_runtime_data'}, {'comment': 'Excited lets us know we are (dis)charging past a certain current threshold', 'name': 'excited'}]}, {'name': 'state', 'fields': [{'comment': 'Current state is calibration', 'name': 'calibration'}, {'comment': 'Current state is Deep Slumber', 'name': 'deepslumber'}, {'comment': 'Current state is Fast Sample', 'name': 'fastsample'}, {'comment': 'Current state is Fault', 'name': 'fault'}, {'comment': 'Current state is Fault Permanently Disabled', 'name': 'faultpermanentdisable'}, {'comment': 'Current state is Fault Slumber', 'name': 'faultslumber'}, {'comment': 'Current state is Prefault', 'name': 'prefault'}, {'comment': 'Current state is Slow Sample', 'name': 'slowsample'}, {'comment': 'State that doesnt sleep to take smbus packets', 'name': 'mcuboot'}]}, {'name': 'cell_state', 'fields': [{'comment': 'Charging is active when current is above a certain threshold', 'name': 'charging'}, {'comment': 'Discharging is active when current is below a certain threshold', 'name': 'discharging'}, {'comment': 'Relaxed is active when the current is neither charging or discharging', 'name': 'relaxed'}]}]