SAE Teensy ECU
IIT SAE Microcontroller programming
Loading...
Searching...
No Matches
vs_conf.py
Go to the documentation of this file.
1"""
2@file vs_conf.py
3@author IR
4@brief Generate configuration files for compilation, intellisense, and post operations
5@version 0.1
6@date 2022-01-06
7
8@copyright Copyright (c) 2022
9
10"""
11
12from operator import truediv
13import os
14import sys
15import subprocess
16from io import TextIOBase
17import json
18import re
19import threading
20import time
21from typing import Any, Callable, Sequence
22
23SETTINGS_PATH = ".vscode/settings.json"
24DOCUMENTATION_URL = "https://illinois-tech-motorsports.github.io/IIT-SAE-ECU/md__github_workspace__c_o_n_t_r_i_b_u_t_i_n_g.html"
25TOOLCHAIN_REPO = "https://github.com/LeHuman/TeensyToolchain"
26
27BACKUP_SET = """{
28 "FRONT_TEENSY_PORT": "COM3",
29 "BACK_TEENSY_PORT": "COM10",
30 "BAUDRATE": "115200",
31 "GRAPH_ARG": "",
32 "CORE": "teensy4",
33 "CORE_MODEL": "41",
34 "CORE_NAME": "MK66FX1M0",
35 "CORE_SPEED": "180000000",
36 "USB_SETTING": "USB_SERIAL",
37 "LOGGING_OPTION": "-l${workspaceFolder}/logs",
38 "TOOLCHAIN_OFFSET": "../TeensyToolchain",
39 "ADDITIONAL_CMAKE_VARS": "-DCUSTOM_BUILD_PATH_PREFIX:STRING=build/Pre_Build/",
40 "CMAKE_FINAL_VARS": "-DENV_CORE_SPEED:STRING=${config:CORE_SPEED} -DENV_CORE_MODEL:STRING=${config:CORE_MODEL} -DENV_USB_SETTING:STRING=${config:USB_SETTING} ${config:ADDITIONAL_CMAKE_VARS}",
41 "doxygen_runner.configuration_file_override": "${workspaceFolder}/docs/Doxyfile",
42 "python.linting.pylintEnabled": true,
43 "python.linting.enabled": true,
44 "python.linting.flake8Enabled": false,
45 "C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: LLVM, UseTab: Never, IndentWidth: 4, TabWidth: 4, BreakBeforeBraces: Attach, AllowShortIfStatementsOnASingleLine: false, IndentCaseLabels: false, ColumnLimit: 0, AccessModifierOffset: -4 }",
46 "search.exclude": {
47 "**/build": true
48 },
49 "files.watcherExclude": {
50 "**/build": true
51 }
52}"""
53
54
55def comment_remover(text: str) -> str:
56 """Remove C style comments from a str
57
58 Args:
59 text (str): str to remove comments from
60
61 Returns:
62 str: text with removed comments
63 """
64
65 def replacer(match):
66 string = match.group(0)
67 if string.startswith("/"):
68 return ""
69 return string.strip()
70
71 pattern = r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"'
72 regex = re.compile(pattern, re.DOTALL | re.MULTILINE)
73 regex = re.sub(r"\n\s*\n", "\n", re.sub(regex, replacer, text), re.MULTILINE)
74 return regex.replace("},\n}", "}\n}")
75
76
77def load_json() -> dict[str, str]:
78 """Load the settings JSON
79
80 Returns:
81 dict[str, str]: the JSON as a dict
82 """
83 with open(SETTINGS_PATH, "r", encoding="UTF-8") as file:
84 data = comment_remover(file.read())
85 return json.loads(data)
86
87
88def serial_ports() -> list[str]:
89 """Lists serial port names when on windows
90
91 Returns:
92 list[str]: A list of the serial ports available on a win system
93 """
94 if sys.platform.startswith("win"):
95 try:
96 fnl: list[str] = []
97 out = subprocess.check_output(["mode"], shell=True, stderr=subprocess.DEVNULL).decode("utf-8")
98 for line in out.splitlines():
99 if line.startswith("Status for device COM"):
100 fnl.append(line[18:-1])
101 return fnl
102 except subprocess.CalledProcessError:
103 pass
104 return []
105
106
107def listify(seq: Sequence) -> str:
108 """Convert sequence to a conventional string list
109
110 Args:
111 seq (Sequence): Sequence object
112
113 Returns:
114 str: the conventional string list
115 """
116 fnl = ""
117 for option in seq:
118 fnl += str(option) + ", "
119 return fnl.strip(", ")
120
121
123 """Wrapper for settings JSON"""
124
125 class Option:
126 """Key wrapper class"""
127
128 key: str
129 desc: str
130 options: tuple
131 advanced: bool
132 setter: Callable[[str], str]
133 getter: Callable[[str], str]
134
135 value: str
136
137 def __init__(
138 self,
139 key: str,
140 desc: str,
141 options: tuple = None,
142 setter: Callable[[str], str] = str,
143 getter: Callable[[str], str] = str,
144 default: object = None,
145 advanced: bool = False,
146 ) -> None:
147 self.keykey = key
148 self.descdesc = desc
149 self.optionsoptions = tuple(str(x) for x in options) if options else None
150 self.settersetter = setter
151 self.gettergetter = getter
152 self.valuevalue = default
153 self.advancedadvanced = advanced and default # Advanced setting must have a default
154
155 def __str__(self) -> str:
156 out = f"{self.key} : {self.desc}"
157 if self.advancedadvanced:
158 out = "* " + out
159 if self.valuevalue:
160 out += f"\n Default: {self.value}"
161 if self.optionsoptions:
162 out += "\n Options: "
163 out += listify(self.optionsoptions)
164 return out
165
166 def set_value(self, value: Any) -> bool:
167 """Sets the value for this option
168
169 Args:
170 value (Any): The value to use to set
171
172 Returns:
173 bool: The value was successfully set
174 """
175 if not value and self.valuevalue:
176 return True
177
178 if self.optionsoptions and str(value) not in self.optionsoptions:
179 return False
180
181 self.valuevalue = self.settersetter(value)
182 return True
183
184 def get_value(self) -> str:
185 """Get the value for this option
186
187 Returns:
188 str: The value of this option
189 """
190 return self.gettergetter(self.valuevalue)
191
192 FRONT_TEENSY_PORT = Option("FRONT_TEENSY_PORT", "Port representing the front ecu (COMx)", getter=lambda x: x.upper())
193 BACK_TEENSY_PORT = Option("BACK_TEENSY_PORT", "Port representing the back ecu (COMx)", getter=lambda x: x.upper())
194 GRAPH_ARG = Option(
195 "GRAPH_ARG",
196 "Enable graphical plotting of data",
197 ("yes", "no"),
198 lambda x: "yes" if x in ("-g", "yes") else "",
199 lambda x: "-g" if x == "yes" else "",
200 default="no",
201 )
202 CORE_MODEL = Option("CORE_MODEL", "Model number of the teensy to compile for", (36, 40, 41), default=41)
203 LOGGING_OPTION = Option(
204 "LOGGING_OPTION",
205 "Enable logging",
206 ("yes", "no"),
207 lambda x: "yes" if x in ("-l${workspaceFolder}\\logs", "yes") else "",
208 lambda x: "-l${workspaceFolder}\\logs" if x == "yes" else "",
209 default="yes",
210 )
211 CORE_SPEED = Option("CORE_SPEED", "Speed at which the CPU will run (MHz)", default="AUTOSET VALUE", advanced=True)
212 CORE_NAME = Option("CORE_NAME", "Model of the cpu", default="AUTOSET VALUE", advanced=True)
213 CORE = Option("CORE", "Core folder to compile with", default="AUTOSET VALUE", advanced=True)
214 BAUDRATE = Option("BAUDRATE", "Baudrate to use with serial", (9600, 19200, 38400, 57600, 115200), default=115200, advanced=True)
215 USB_SETTING = Option("USB_SETTING", "USB behavior of the core", default="USB_SERIAL", advanced=True)
216 TOOLCHAIN_OFFSET = Option("TOOLCHAIN_OFFSET", "Offset to the toolchain", default="../TeensyToolchain", advanced=True)
217 ADDITIONAL_CMAKE_VARS = Option(
218 "ADDITIONAL_CMAKE_VARS", "More defines passed to CMake", default="-DCUSTOM_BUILD_PATH_PREFIX:STRING=build/Pre_Build/", advanced=True
219 )
220
221 options = (
222 CORE_MODEL,
223 FRONT_TEENSY_PORT,
224 BACK_TEENSY_PORT,
225 LOGGING_OPTION,
226 GRAPH_ARG,
227 CORE_SPEED,
228 CORE_NAME,
229 CORE,
230 BAUDRATE,
231 USB_SETTING,
232 TOOLCHAIN_OFFSET,
233 ADDITIONAL_CMAKE_VARS,
234 )
235
236 settings: dict[str, str]
237
238 def __init__(self, settings_dict: dict[str, str]) -> None:
239 self.load(settings_dict)
240 self.CORE_SPEED.getter = self.__get_core_speed
241 self.CORE_NAME.getter = self.__get_core_name
242 self.CORE.getter = self.__get_core
243
244 def __get_core_speed(self, _) -> str:
245 model = int(self.CORE_MODEL.value)
246 if model == 36:
247 return "180000000"
248 if model == 40 or model == 41:
249 return "600000000"
250 return "BAD MODEL"
251
252 def __get_core_name(self, _) -> str:
253 model = int(self.CORE_MODEL.value)
254 if model == 36:
255 return "MK66FX1M0"
256 if model == 40 or model == 41:
257 return "IMXRT1062"
258 return "BAD MODEL"
259
260 def __get_core(self, _) -> str:
261 model = int(self.CORE_MODEL.value)
262 if model == 36:
263 return "teensy3"
264 if model == 40 or model == 41:
265 return "teensy4"
266 return "BAD MODEL"
267
268 def load(self, settings_dict: dict[str, str]) -> None:
269 """Loads settings dict
270
271 Args:
272 settings_dict (dict[str, str]): settings dict
273
274 Raises:
275 KeyError: Key is not found in the settings JSON
276 """
277 self.settingssettings = settings_dict
278 for option in self.options:
279 if option.key not in self.settingssettings:
280 raise KeyError(f"Key not found in settings JSON: {option.value}")
281 else:
282 option.set_value(self.settingssettings[option.key])
283
284 def print_settings(self) -> None:
285 """Print the current settings"""
286 for option in self.options:
287 print(option.key, option.value)
288
289 def unload(self, file: TextIOBase) -> None:
290 """Write settings to file
291
292 Args:
293 file (TextIOBase): The file to write to
294 """
295 for option in self.options:
296 self.settingssettings[option.key] = option.get_value()
297 json.dump(self.settingssettings, file, indent=4)
298
299
301 running = False
302 force = False
303 thread: threading.Thread
304
305 msg = " Available ports:"
306
307 def __init__(self) -> None:
308 self.threadthread = threading.Thread(target=self.run, daemon=True)
309
310 def start(self):
311 print(self.msgmsg)
312 self.forceforce = True
313 if not self.runningrunning:
314 self.runningrunning = True
315 self.threadthread.start()
316
317 def stop(self):
318 self.runningrunning = False
319
320 def run(self):
321 o_ports = None
322 while self.runningrunning:
323 time.sleep(0.5)
324 ports = listify(serial_ports())
325 if self.forceforce or o_ports != ports:
326 self.forceforce = False
327 o_ports = ports
328 print(f"\033[s\r\033[1A\033[K\r{self.msg} {ports}\033[u", end="")
329
330
331def writeBackup() -> None:
332 """Output the backup settings"""
333 with open(SETTINGS_PATH, "w", encoding="UTF-8") as sett:
334 sett.write(BACKUP_SET)
335
336
337def get_settings() -> Settings: # TODO: don't output file until actually configured
338 """Return the current settings or fallback to the backup
339
340 Returns:
341 Settings: The active settings
342 """
343 try:
344 return Settings(load_json())
345 except (json.JSONDecodeError, FileNotFoundError):
346 writeBackup()
347 return get_settings()
348
349
350# IMPROVE: make it ask if we have two Teensies?
351
352
353def main():
354 """Main function"""
355
356 if sys.version_info.major < 3 or sys.version_info.minor < 10:
357 sys.exit("This project requires at least python 3.10")
358
359 vs_code_startup = len(sys.argv) == 2 and sys.argv[1] == "thisisbeingrunonstartup"
360 adv_mode = False
361 first_time = not os.path.exists(SETTINGS_PATH)
362
363 settings = get_settings()
364
365 toolchain_missing = not os.path.exists(settings.TOOLCHAIN_OFFSET.get_value())
366
367 if toolchain_missing:
368 path = settings.TOOLCHAIN_OFFSET.get_value()
369 print(f"Toolchain is missing. Cloning to relative directory {path}")
370 subprocess.call(f"git clone --recurse-submodules -j8 {TOOLCHAIN_REPO}", cwd=os.path.abspath("../"))
371
372 if first_time:
373 subprocess.call("git submodule update --init")
374 elif vs_code_startup:
375 print(f"Configured for Teensy{settings.CORE_MODEL.get_value()} @ {int(int(settings.CORE_SPEED.get_value())/1000000)} Mhz")
376 print(f"Current ports:\n Front:\t{settings.FRONT_TEENSY_PORT.get_value()}\n Back:\t{settings.BACK_TEENSY_PORT.get_value()}")
377 print(f"Data Plotting: {settings.GRAPH_ARG.value}")
378 print(f"Data Logging: {settings.LOGGING_OPTION.value}")
379 print(f"\nRead Documentation Online at: {DOCUMENTATION_URL}")
380 sys.exit(0)
381
382 pp = PortPrinter()
383
384 for option in settings.options:
385 if option.advanced and not adv_mode:
386 if first_time:
387 break
388 if input("Enter 'Yes' to edit advanced options: ") != "Yes":
389 break
390 adv_mode = True
391 print(option)
392 if option is settings.FRONT_TEENSY_PORT or option is settings.BACK_TEENSY_PORT:
393 pp.start()
394 elif pp.running:
395 pp.stop()
396
397 if not option.set_value(input("Input option, blank for default: ")):
398 while not option.set_value(input("Invalid option: ")):
399 pass
400
401 with open(SETTINGS_PATH, "w", encoding="UTF-8") as sett:
402 settings.unload(sett)
403
404 if not vs_code_startup:
405 subprocess.call("git pull --recurse-submodules")
406 subprocess.call("git pull --recurse-submodules", cwd=settings.TOOLCHAIN_OFFSET.get_value())
407
408
409if __name__ == "__main__":
410 main()
threading thread
Definition vs_conf.py:303
Key wrapper class.
Definition vs_conf.py:125
bool set_value(self, Any value)
Sets the value for this option.
Definition vs_conf.py:166
str get_value(self)
Get the value for this option.
Definition vs_conf.py:184
Wrapper for settings JSON.
Definition vs_conf.py:122
None load(self, dict[str, str] settings_dict)
Loads settings dict.
Definition vs_conf.py:268
None unload(self, TextIOBase file)
Write settings to file.
Definition vs_conf.py:289
None print_settings(self)
Print the current settings.
Definition vs_conf.py:284