diff --git a/tools/boards.txt b/tools/boards.txt
new file mode 100644
index 00000000000..8d45426a7bc
--- /dev/null
+++ b/tools/boards.txt
@@ -0,0 +1,381 @@
+
+### THIS IS A GENERATED FILE, DO NOT EDIT.
+### To add a board, see gen_boards.py.
+
+##############################################################
+
+esp32.name=ESP32 Dev Module
+
+esp32.upload.tool=esptool_py
+esp32.upload.maximum_size=
+esp32.upload.maximum_data_size=
+esp32.upload.flags=
+esp32.upload.extra_flags=
+
+esp32.serial.disableDTR=true
+esp32.serial.disableRTS=true
+
+esp32.build.tarch=xtensa
+esp32.build.bootloader_addr=0x1000
+esp32.build.target=esp
+esp32.build.mcu=esp32
+esp32.build.core=esp32
+esp32.build.variant=esp32
+esp32.build.board=ESP32_DEV
+
+esp32.build.f_cpu=240000000
+esp32.build.flash_size=4MB
+esp32.build.flash_freq=40m
+esp32.build.flash_mode=dio
+esp32.build.boot=
+esp32.build.partitions=default
+esp32.build.defines=
+
+
+
+esp32.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
+esp32.menu.PartitionScheme.default.build.partitions=default
+esp32.menu.PartitionScheme.no_ota.upload.maximum_size=1310720
+
+
+
+esp32.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS)
+esp32.menu.PartitionScheme.defaultffat.build.partitions=defaultffat
+esp32.menu.PartitionScheme.no_ota.upload.maximum_size=1310720
+
+
+
+esp32.menu.PartitionScheme.default_8MB=8M Flash (3MB APP/1.5MB FAT)
+esp32.menu.PartitionScheme.default_8MB.build.partitions=default_8MB
+esp32.menu.PartitionScheme.no_ota.upload.maximum_size=3342336
+
+
+
+esp32.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS)
+esp32.menu.PartitionScheme.minimal.build.partitions=minimal
+esp32.menu.PartitionScheme.no_ota.upload.maximum_size=1310720
+
+
+
+esp32.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS)
+esp32.menu.PartitionScheme.no_ota.build.partitions=no_ota
+esp32.menu.PartitionScheme.no_ota.upload.maximum_size=2097152
+
+
+
+esp32.menu.CPUFreq.240=240MHz (WiFi/BT)
+esp32.menu.CPUFreq.240.build.f_cpu=240000000L
+esp32.menu.CPUFreq.160=160MHz (WiFi/BT)
+esp32.menu.CPUFreq.160.build.f_cpu=160000000L
+esp32.menu.CPUFreq.80=80MHz (WiFi/BT)
+esp32.menu.CPUFreq.80.build.f_cpu=80000000L
+esp32.menu.CPUFreq.40=40MHz (40MHz XTAL)
+esp32.menu.CPUFreq.40.build.f_cpu=40000000L
+esp32.menu.CPUFreq.26=26MHz (26MHz XTAL)
+esp32.menu.CPUFreq.26.build.f_cpu=26000000L
+esp32.menu.CPUFreq.20=20MHz (40MHz XTAL)
+esp32.menu.CPUFreq.20.build.f_cpu=20000000L
+esp32.menu.CPUFreq.13=13MHz (26MHz XTAL)
+esp32.menu.CPUFreq.13.build.f_cpu=13000000L
+esp32.menu.CPUFreq.10=10MHz (40MHz XTAL)
+esp32.menu.CPUFreq.10.build.f_cpu=10000000L
+
+
+esp32.menu.FlashMode.qio=QIO
+esp32.menu.FlashMode.qio.build.flash_mode=dio
+esp32.menu.FlashMode.qio.build.boot=qio
+esp32.menu.FlashMode.dio=DIO
+esp32.menu.FlashMode.dio.build.flash_mode=dio
+esp32.menu.FlashMode.dio.build.boot=dio
+esp32.menu.FlashMode.qout=QOUT
+esp32.menu.FlashMode.qout.build.flash_mode=dout
+esp32.menu.FlashMode.qout.build.boot=qout
+esp32.menu.FlashMode.dout=DOUT
+esp32.menu.FlashMode.dout.build.flash_mode=dout
+esp32.menu.FlashMode.dout.build.boot=dout
+
+
+
+esp32.menu.FlashFreq.80=80MHz
+esp32.menu.FlashFreq.80.build.flash_freq=80m
+esp32.menu.FlashFreq.40=40MHz
+esp32.menu.FlashFreq.40.build.flash_freq=40m
+
+
+
+esp32.menu.FlashSize.4M=4MB (32Mb)
+esp32.menu.FlashSize.4M.build.flash_size=4MB
+esp32.menu.FlashSize.2M=2MB (16Mb)
+esp32.menu.FlashSize.2M.build.flash_size=2MB
+esp32.menu.FlashSize.2M.build.partitions=minimal
+esp32.menu.FlashSize.16M=16MB (128Mb)
+esp32.menu.FlashSize.16M.build.flash_size=16MB
+esp32.menu.FlashSize.16M.build.partitions=ffat
+
+
+
+
+esp32.menu.UploadSpeed.921600=921600
+esp32.menu.UploadSpeed.921600.upload.speed=921600
+esp32.menu.UploadSpeed.115200=115200
+esp32.menu.UploadSpeed.115200.upload.speed=115200
+esp32.menu.UploadSpeed.256000.windows=256000
+esp32.menu.UploadSpeed.256000.upload.speed=256000
+esp32.menu.UploadSpeed.230400.windows.upload.speed=256000
+esp32.menu.UploadSpeed.230400=230400
+esp32.menu.UploadSpeed.230400.upload.speed=230400
+esp32.menu.UploadSpeed.460800.linux=460800
+esp32.menu.UploadSpeed.460800.macosx=460800
+esp32.menu.UploadSpeed.460800.upload.speed=460800
+esp32.menu.UploadSpeed.512000.windows=512000
+esp32.menu.UploadSpeed.512000.upload.speed=512000
+
+esp32.menu.DebugLevel.none=None
+esp32.menu.DebugLevel.none.build.code_debug=0
+esp32.menu.DebugLevel.error=Error
+esp32.menu.DebugLevel.error.build.code_debug=1
+esp32.menu.DebugLevel.warn=Warn
+esp32.menu.DebugLevel.warn.build.code_debug=2
+esp32.menu.DebugLevel.info=Info
+esp32.menu.DebugLevel.info.build.code_debug=3
+esp32.menu.DebugLevel.debug=Debug
+esp32.menu.DebugLevel.debug.build.code_debug=4
+esp32.menu.DebugLevel.verbose=Verbose
+esp32.menu.DebugLevel.verbose.build.code_debug=5
+
+
+##############################################################
+
+esp32s2.name=ESP32-S2 Dev Module
+
+esp32s2.upload.tool=esptool_py
+esp32s2.upload.maximum_size=
+esp32s2.upload.maximum_data_size=
+esp32s2.upload.flags=
+esp32s2.upload.extra_flags=
+
+esp32s2.serial.disableDTR=true
+esp32s2.serial.disableRTS=true
+
+esp32s2.build.tarch=xtensa
+esp32s2.build.bootloader_addr=0x1000
+esp32s2.build.target=esp
+esp32s2.build.mcu=esp32s2
+esp32s2.build.core=esp32
+esp32s2.build.variant=esp32s2
+esp32s2.build.board=ESP32S2_DEV
+
+esp32s2.build.f_cpu=240000000
+esp32s2.build.flash_size=4MB
+esp32s2.build.flash_freq=80m
+esp32s2.build.flash_mode=qio
+esp32s2.build.boot=
+esp32s2.build.partitions=default
+esp32s2.build.defines=
+
+
+
+esp32s2.menu.PartitionScheme.default=Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)
+esp32s2.menu.PartitionScheme.default.build.partitions=default
+esp32s2.menu.PartitionScheme.no_ota.upload.maximum_size=1310720
+
+
+
+esp32s2.menu.PartitionScheme.defaultffat=Default 4MB with ffat (1.2MB APP/1.5MB FATFS)
+esp32s2.menu.PartitionScheme.defaultffat.build.partitions=defaultffat
+esp32s2.menu.PartitionScheme.no_ota.upload.maximum_size=1310720
+
+
+
+
+
+esp32s2.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS)
+esp32s2.menu.PartitionScheme.minimal.build.partitions=minimal
+esp32s2.menu.PartitionScheme.no_ota.upload.maximum_size=1310720
+
+
+
+esp32s2.menu.PartitionScheme.no_ota=No OTA (2MB APP/2MB SPIFFS)
+esp32s2.menu.PartitionScheme.no_ota.build.partitions=no_ota
+esp32s2.menu.PartitionScheme.no_ota.upload.maximum_size=2097152
+
+
+
+esp32s2.menu.CPUFreq.240=240MHz (WiFi/BT)
+esp32s2.menu.CPUFreq.240.build.f_cpu=240000000L
+esp32s2.menu.CPUFreq.160=160MHz (WiFi/BT)
+esp32s2.menu.CPUFreq.160.build.f_cpu=160000000L
+esp32s2.menu.CPUFreq.80=80MHz (WiFi/BT)
+esp32s2.menu.CPUFreq.80.build.f_cpu=80000000L
+esp32s2.menu.CPUFreq.40=40MHz (40MHz XTAL)
+esp32s2.menu.CPUFreq.40.build.f_cpu=40000000L
+esp32s2.menu.CPUFreq.26=26MHz (26MHz XTAL)
+esp32s2.menu.CPUFreq.26.build.f_cpu=26000000L
+esp32s2.menu.CPUFreq.20=20MHz (40MHz XTAL)
+esp32s2.menu.CPUFreq.20.build.f_cpu=20000000L
+esp32s2.menu.CPUFreq.13=13MHz (26MHz XTAL)
+esp32s2.menu.CPUFreq.13.build.f_cpu=13000000L
+esp32s2.menu.CPUFreq.10=10MHz (40MHz XTAL)
+esp32s2.menu.CPUFreq.10.build.f_cpu=10000000L
+
+
+esp32s2.menu.FlashMode.qio=QIO
+esp32s2.menu.FlashMode.qio.build.flash_mode=dio
+esp32s2.menu.FlashMode.qio.build.boot=qio
+esp32s2.menu.FlashMode.dio=DIO
+esp32s2.menu.FlashMode.dio.build.flash_mode=dio
+esp32s2.menu.FlashMode.dio.build.boot=dio
+esp32s2.menu.FlashMode.qout=QOUT
+esp32s2.menu.FlashMode.qout.build.flash_mode=dout
+esp32s2.menu.FlashMode.qout.build.boot=qout
+esp32s2.menu.FlashMode.dout=DOUT
+esp32s2.menu.FlashMode.dout.build.flash_mode=dout
+esp32s2.menu.FlashMode.dout.build.boot=dout
+
+
+
+esp32s2.menu.FlashFreq.80=80MHz
+esp32s2.menu.FlashFreq.80.build.flash_freq=80m
+esp32s2.menu.FlashFreq.40=40MHz
+esp32s2.menu.FlashFreq.40.build.flash_freq=40m
+
+
+
+esp32s2.menu.FlashSize.4M=4MB (32Mb)
+esp32s2.menu.FlashSize.4M.build.flash_size=4MB
+esp32s2.menu.FlashSize.2M=2MB (16Mb)
+esp32s2.menu.FlashSize.2M.build.flash_size=2MB
+esp32s2.menu.FlashSize.2M.build.partitions=minimal
+esp32s2.menu.FlashSize.16M=16MB (128Mb)
+esp32s2.menu.FlashSize.16M.build.flash_size=16MB
+esp32s2.menu.FlashSize.16M.build.partitions=ffat
+
+
+
+
+esp32s2.menu.UploadSpeed.921600=921600
+esp32s2.menu.UploadSpeed.921600.upload.speed=921600
+esp32s2.menu.UploadSpeed.115200=115200
+esp32s2.menu.UploadSpeed.115200.upload.speed=115200
+esp32s2.menu.UploadSpeed.256000.windows=256000
+esp32s2.menu.UploadSpeed.256000.upload.speed=256000
+esp32s2.menu.UploadSpeed.230400.windows.upload.speed=256000
+esp32s2.menu.UploadSpeed.230400=230400
+esp32s2.menu.UploadSpeed.230400.upload.speed=230400
+esp32s2.menu.UploadSpeed.460800.linux=460800
+esp32s2.menu.UploadSpeed.460800.macosx=460800
+esp32s2.menu.UploadSpeed.460800.upload.speed=460800
+esp32s2.menu.UploadSpeed.512000.windows=512000
+esp32s2.menu.UploadSpeed.512000.upload.speed=512000
+
+esp32s2.menu.DebugLevel.none=None
+esp32s2.menu.DebugLevel.none.build.code_debug=0
+esp32s2.menu.DebugLevel.error=Error
+esp32s2.menu.DebugLevel.error.build.code_debug=1
+esp32s2.menu.DebugLevel.warn=Warn
+esp32s2.menu.DebugLevel.warn.build.code_debug=2
+esp32s2.menu.DebugLevel.info=Info
+esp32s2.menu.DebugLevel.info.build.code_debug=3
+esp32s2.menu.DebugLevel.debug=Debug
+esp32s2.menu.DebugLevel.debug.build.code_debug=4
+esp32s2.menu.DebugLevel.verbose=Verbose
+esp32s2.menu.DebugLevel.verbose.build.code_debug=5
+
+
+##############################################################
+
+esp32c3.name=ESP32-C3 Dev Module
+
+esp32c3.upload.tool=esptool_py
+esp32c3.upload.maximum_size=
+esp32c3.upload.maximum_data_size=
+esp32c3.upload.flags=
+esp32c3.upload.extra_flags=
+
+esp32c3.serial.disableDTR=true
+esp32c3.serial.disableRTS=true
+
+esp32c3.build.tarch=riscv32
+esp32c3.build.bootloader_addr=0x0
+esp32c3.build.target=esp
+esp32c3.build.mcu=esp32c3
+esp32c3.build.core=esp32
+esp32c3.build.variant=esp32c3
+esp32c3.build.board=ESP32C3_DEV
+
+esp32c3.build.f_cpu=160000000
+esp32c3.build.flash_size=4MB
+esp32c3.build.flash_freq=80m
+esp32c3.build.flash_mode=qio
+esp32c3.build.boot=
+esp32c3.build.partitions=default
+esp32c3.build.defines=
+
+
+
+
+
+
+
+
+
+esp32c3.menu.PartitionScheme.minimal=Minimal (1.3MB APP/700KB SPIFFS)
+esp32c3.menu.PartitionScheme.minimal.build.partitions=minimal
+esp32c3.menu.PartitionScheme.no_ota.upload.maximum_size=1310720
+
+
+
+
+
+
+esp32c3.menu.CPUFreq.160=160MHz (WiFi/BT)
+esp32c3.menu.CPUFreq.160.build.f_cpu=160000000L
+esp32c3.menu.CPUFreq.80=80MHz (WiFi/BT)
+esp32c3.menu.CPUFreq.80.build.f_cpu=80000000L
+esp32c3.menu.CPUFreq.40=40MHz (40MHz XTAL)
+esp32c3.menu.CPUFreq.40.build.f_cpu=40000000L
+esp32c3.menu.CPUFreq.26=26MHz (26MHz XTAL)
+esp32c3.menu.CPUFreq.26.build.f_cpu=26000000L
+esp32c3.menu.CPUFreq.20=20MHz (40MHz XTAL)
+esp32c3.menu.CPUFreq.20.build.f_cpu=20000000L
+esp32c3.menu.CPUFreq.13=13MHz (26MHz XTAL)
+esp32c3.menu.CPUFreq.13.build.f_cpu=13000000L
+esp32c3.menu.CPUFreq.10=10MHz (40MHz XTAL)
+esp32c3.menu.CPUFreq.10.build.f_cpu=10000000L
+
+
+
+
+
+
+
+
+
+esp32c3.menu.UploadSpeed.921600=921600
+esp32c3.menu.UploadSpeed.921600.upload.speed=921600
+esp32c3.menu.UploadSpeed.115200=115200
+esp32c3.menu.UploadSpeed.115200.upload.speed=115200
+esp32c3.menu.UploadSpeed.256000.windows=256000
+esp32c3.menu.UploadSpeed.256000.upload.speed=256000
+esp32c3.menu.UploadSpeed.230400.windows.upload.speed=256000
+esp32c3.menu.UploadSpeed.230400=230400
+esp32c3.menu.UploadSpeed.230400.upload.speed=230400
+esp32c3.menu.UploadSpeed.460800.linux=460800
+esp32c3.menu.UploadSpeed.460800.macosx=460800
+esp32c3.menu.UploadSpeed.460800.upload.speed=460800
+esp32c3.menu.UploadSpeed.512000.windows=512000
+esp32c3.menu.UploadSpeed.512000.upload.speed=512000
+
+esp32c3.menu.DebugLevel.none=None
+esp32c3.menu.DebugLevel.none.build.code_debug=0
+esp32c3.menu.DebugLevel.error=Error
+esp32c3.menu.DebugLevel.error.build.code_debug=1
+esp32c3.menu.DebugLevel.warn=Warn
+esp32c3.menu.DebugLevel.warn.build.code_debug=2
+esp32c3.menu.DebugLevel.info=Info
+esp32c3.menu.DebugLevel.info.build.code_debug=3
+esp32c3.menu.DebugLevel.debug=Debug
+esp32c3.menu.DebugLevel.debug.build.code_debug=4
+esp32c3.menu.DebugLevel.verbose=Verbose
+esp32c3.menu.DebugLevel.verbose.build.code_debug=5
+
diff --git a/tools/boards.txt.jinja b/tools/boards.txt.jinja
new file mode 100644
index 00000000000..504216d341e
--- /dev/null
+++ b/tools/boards.txt.jinja
@@ -0,0 +1,128 @@
+{# The comment on the next line is for the file
+   that will be generated from this template.
+   You CAN edit this file! #}
+### THIS IS A GENERATED FILE, DO NOT EDIT.
+### To add a board, see gen_boards.py.
+{% for board_id, board in boards.items() %}
+##############################################################
+
+{{ board_id }}.name={{ board.name }}
+
+{{ board_id }}.upload.tool=esptool_py
+{{ board_id }}.upload.maximum_size={{ board.maximum_size }}
+{{ board_id }}.upload.maximum_data_size={{ board.maximum_data_size }}
+{{ board_id }}.upload.flags={{ board.flags }}
+{{ board_id }}.upload.extra_flags={{ board.extra_flags }}
+
+{{ board_id }}.serial.disableDTR=true
+{{ board_id }}.serial.disableRTS=true
+
+{{ board_id }}.build.tarch={{ board.tarch }}
+{{ board_id }}.build.bootloader_addr={{ board.bootloader_addr }}
+{{ board_id }}.build.target={{ board.target }}
+{{ board_id }}.build.mcu={{ board.mcu }}
+{{ board_id }}.build.core={{ board.core }}
+{{ board_id }}.build.variant={{ board_id }}
+{{ board_id }}.build.board={{ board.board_macro }}
+
+{{ board_id }}.build.f_cpu={{ board.f_cpu }}
+{{ board_id }}.build.flash_size={{ board.flash_size }}
+{{ board_id }}.build.flash_freq={{ board.flash_freq }}
+{{ board_id }}.build.flash_mode={{ board.flash_mode }}
+{{ board_id }}.build.boot={{ board.boot_flash_mode }}
+{{ board_id }}.build.partitions={{ board.partitions }}
+{{ board_id }}.build.defines={{ board.defines }}
+
+{% for id, part_scheme in partition_schemes.items() %}
+{% if part_scheme.flash_size_required_mb <= board.max_partitions_size_mb %}
+{{ board_id }}.menu.PartitionScheme.{{ id }}={{ part_scheme.name }}
+{{ board_id }}.menu.PartitionScheme.{{ id }}.build.partitions={{ id }}
+{{ board_id }}.menu.PartitionScheme.no_ota.upload.maximum_size={{ part_scheme.maximum_size }}
+{% endif %}
+{% endfor %}
+
+{% if board.f_cpu == 240000000 -%}
+{{ board_id }}.menu.CPUFreq.240=240MHz (WiFi/BT)
+{{ board_id }}.menu.CPUFreq.240.build.f_cpu=240000000L
+{%- endif %}
+{{ board_id }}.menu.CPUFreq.160=160MHz (WiFi/BT)
+{{ board_id }}.menu.CPUFreq.160.build.f_cpu=160000000L
+{{ board_id }}.menu.CPUFreq.80=80MHz (WiFi/BT)
+{{ board_id }}.menu.CPUFreq.80.build.f_cpu=80000000L
+{{ board_id }}.menu.CPUFreq.40=40MHz (40MHz XTAL)
+{{ board_id }}.menu.CPUFreq.40.build.f_cpu=40000000L
+{{ board_id }}.menu.CPUFreq.26=26MHz (26MHz XTAL)
+{{ board_id }}.menu.CPUFreq.26.build.f_cpu=26000000L
+{{ board_id }}.menu.CPUFreq.20=20MHz (40MHz XTAL)
+{{ board_id }}.menu.CPUFreq.20.build.f_cpu=20000000L
+{{ board_id }}.menu.CPUFreq.13=13MHz (26MHz XTAL)
+{{ board_id }}.menu.CPUFreq.13.build.f_cpu=13000000L
+{{ board_id }}.menu.CPUFreq.10=10MHz (40MHz XTAL)
+{{ board_id }}.menu.CPUFreq.10.build.f_cpu=10000000L
+
+{% if board.FlashMode %}
+{{ board_id }}.menu.FlashMode.qio=QIO
+{{ board_id }}.menu.FlashMode.qio.build.flash_mode=dio
+{{ board_id }}.menu.FlashMode.qio.build.boot=qio
+{{ board_id }}.menu.FlashMode.dio=DIO
+{{ board_id }}.menu.FlashMode.dio.build.flash_mode=dio
+{{ board_id }}.menu.FlashMode.dio.build.boot=dio
+{{ board_id }}.menu.FlashMode.qout=QOUT
+{{ board_id }}.menu.FlashMode.qout.build.flash_mode=dout
+{{ board_id }}.menu.FlashMode.qout.build.boot=qout
+{{ board_id }}.menu.FlashMode.dout=DOUT
+{{ board_id }}.menu.FlashMode.dout.build.flash_mode=dout
+{{ board_id }}.menu.FlashMode.dout.build.boot=dout
+{% endif %}
+
+{% if board.FlashFreq %}
+{{ board_id }}.menu.FlashFreq.80=80MHz
+{{ board_id }}.menu.FlashFreq.80.build.flash_freq=80m
+{{ board_id }}.menu.FlashFreq.40=40MHz
+{{ board_id }}.menu.FlashFreq.40.build.flash_freq=40m
+{% endif %}
+
+{% if board.FlashSize %}
+{{ board_id }}.menu.FlashSize.4M=4MB (32Mb)
+{{ board_id }}.menu.FlashSize.4M.build.flash_size=4MB
+{{ board_id }}.menu.FlashSize.2M=2MB (16Mb)
+{{ board_id }}.menu.FlashSize.2M.build.flash_size=2MB
+{{ board_id }}.menu.FlashSize.2M.build.partitions=minimal
+{{ board_id }}.menu.FlashSize.16M=16MB (128Mb)
+{{ board_id }}.menu.FlashSize.16M.build.flash_size=16MB
+{{ board_id }}.menu.FlashSize.16M.build.partitions=ffat
+{% endif %}
+
+{# Assume for now that all boards are capable of all
+   upload speeds. If this isn't the case, it can be solved
+   similar to how partition schemes are handled. #}
+
+{{ board_id }}.menu.UploadSpeed.921600=921600
+{{ board_id }}.menu.UploadSpeed.921600.upload.speed=921600
+{{ board_id }}.menu.UploadSpeed.115200=115200
+{{ board_id }}.menu.UploadSpeed.115200.upload.speed=115200
+{{ board_id }}.menu.UploadSpeed.256000.windows=256000
+{{ board_id }}.menu.UploadSpeed.256000.upload.speed=256000
+{{ board_id }}.menu.UploadSpeed.230400.windows.upload.speed=256000
+{{ board_id }}.menu.UploadSpeed.230400=230400
+{{ board_id }}.menu.UploadSpeed.230400.upload.speed=230400
+{{ board_id }}.menu.UploadSpeed.460800.linux=460800
+{{ board_id }}.menu.UploadSpeed.460800.macosx=460800
+{{ board_id }}.menu.UploadSpeed.460800.upload.speed=460800
+{{ board_id }}.menu.UploadSpeed.512000.windows=512000
+{{ board_id }}.menu.UploadSpeed.512000.upload.speed=512000
+
+{{ board_id }}.menu.DebugLevel.none=None
+{{ board_id }}.menu.DebugLevel.none.build.code_debug=0
+{{ board_id }}.menu.DebugLevel.error=Error
+{{ board_id }}.menu.DebugLevel.error.build.code_debug=1
+{{ board_id }}.menu.DebugLevel.warn=Warn
+{{ board_id }}.menu.DebugLevel.warn.build.code_debug=2
+{{ board_id }}.menu.DebugLevel.info=Info
+{{ board_id }}.menu.DebugLevel.info.build.code_debug=3
+{{ board_id }}.menu.DebugLevel.debug=Debug
+{{ board_id }}.menu.DebugLevel.debug.build.code_debug=4
+{{ board_id }}.menu.DebugLevel.verbose=Verbose
+{{ board_id }}.menu.DebugLevel.verbose.build.code_debug=5
+
+{% endfor %}
diff --git a/tools/gen_boards.py b/tools/gen_boards.py
new file mode 100644
index 00000000000..e1728aa97dc
--- /dev/null
+++ b/tools/gen_boards.py
@@ -0,0 +1,187 @@
+from jinja2 import Environment, FileSystemLoader
+
+
+def merge(*args):
+    """Utility function to combine multiple dictionaries"""
+    result = dict()
+    for dictionary in args:
+        assert isinstance(dictionary, dict)
+        result.update(dictionary)
+    return result
+
+
+# Default values applicable to all chips
+# (can also be overridden)
+global_defaults = {
+    "target": "esp",
+    "core": "esp32",
+    "partitions": "default",
+    "flash_size": "4MB",
+    "cpus": 1
+}
+
+# Chip-specific defaults
+chip_esp32_defaults = {
+    "f_cpu": 240000000,
+    "tarch": "xtensa",
+    "mcu": "esp32",
+    "bootloader_addr": "0x1000",
+    "boot": "dio",
+    "flash_mode": "dio",
+    "flash_freq": "40m",
+    "cpus": 2
+}
+
+
+chip_esp32s2_defaults = {
+    "f_cpu": 240000000,
+    "tarch": "xtensa",
+    "mcu": "esp32s2",
+    "bootloader_addr": "0x1000",
+    "boot": "qio",
+    "flash_mode": "qio",
+    "flash_freq": "80m"
+}
+
+
+chip_esp32c3_defaults = {
+    "f_cpu": 160000000,
+    "tarch": "riscv32",
+    "mcu": "esp32c3",
+    "bootloader_addr": "0x0",
+    "boot": "qio",
+    "flash_mode": "qio",
+    "flash_freq": "80m"
+}
+
+# End of chip-specific defaults. Add new chips above this line.
+
+# Partition schemes definitions
+
+partition_schemes = {
+    "default": {
+        "name": "Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)",
+        "flash_size_required_mb": 4,
+        "maximum_size": 1310720,
+    },
+    "defaultffat": {
+        "name": "Default 4MB with ffat (1.2MB APP/1.5MB FATFS)",
+        "flash_size_required_mb": 4,
+        "maximum_size": 1310720,
+    },
+    "default_8MB": {
+        "name": "8M Flash (3MB APP/1.5MB FAT)",
+        "flash_size_required_mb": 8,
+        "maximum_size": 3342336
+    },
+    "minimal": {
+        "name": "Minimal (1.3MB APP/700KB SPIFFS)",
+        "flash_size_required_mb": 2,
+        "maximum_size": 1310720,
+    },
+    "no_ota": {
+        "name": "No OTA (2MB APP/2MB SPIFFS)",
+        "flash_size_required_mb": 4,
+        "maximum_size": 2097152,
+    }
+}
+
+# These dictionaries are used to enable different menus,
+# for example flash size selection
+
+board_has_flashsize_menu = {
+    "FlashSize": True
+}
+
+board_has_flashfreq_menu = {
+    "FlashFreq": True
+}
+
+board_has_flashmode_menu = {
+    "FlashMode": True
+}
+
+board_has_flash_menus = merge(
+    board_has_flashsize_menu,
+    board_has_flashfreq_menu,
+    board_has_flashmode_menu
+)
+
+# Board definitions. Each board is defined by a dictionary,
+# produced by a combination of global, chip-specific, and
+# board-specific dictionary. Anything defined defined at
+# global or chip level can be overridden in a board-specific
+# dictionary.
+
+board_esp32 = merge(
+    global_defaults,
+    chip_esp32_defaults,
+    board_has_flash_menus,
+    {
+        "name": "ESP32 Dev Module",
+        "board_macro": "ESP32_DEV",
+        "max_partitions_size_mb": 16
+    }
+)
+
+
+board_esp32s2 = merge(
+    global_defaults,
+    chip_esp32s2_defaults,
+    board_has_flash_menus,
+    {
+        "name": "ESP32-S2 Dev Module",
+        "board_macro": "ESP32S2_DEV",
+        "max_partitions_size_mb": 4
+    }
+)
+
+
+board_esp32c3 = merge(
+    global_defaults,
+    chip_esp32c3_defaults,
+    # just an example, let's assume this board
+    # doesn't have flash menus
+    # board_has_flash_menus
+    {
+        "name": "ESP32-C3 Dev Module",
+        "board_macro": "ESP32C3_DEV",
+        # just an example, let's assume this board
+        # has limited flash size
+        "max_partitions_size_mb": 2
+    }
+)
+
+# End of board definitions. When adding a new board,
+# add its dictionary above and then also add it into
+# the "boards" dictionary below.
+
+boards = {
+    "esp32": board_esp32,
+    "esp32s2": board_esp32s2,
+    "esp32c3": board_esp32c3,
+}
+
+
+# This function returns the context used to expand
+# the template.
+def get_context():
+    context = {
+        "boards": boards,
+        "partition_schemes": partition_schemes
+    }
+    return context
+
+
+# The main function simply loads the template and expands
+# it using the context above.
+def main():
+    env = Environment(loader=FileSystemLoader("."))
+    template = env.get_template("boards.txt.jinja")
+    context = get_context()
+    with open("boards.txt", "w") as out:
+        out.write(template.render(context))
+
+
+if __name__ == "__main__":
+    main()