🔨 Build quality improvements

This commit is contained in:
Scott Lahteine
2026-01-22 13:36:29 -06:00
parent 57906a8466
commit e2208ba58f
6 changed files with 307 additions and 128 deletions

View File

@@ -1013,6 +1013,7 @@
#endif
#if ANY(HAS_DWIN_E3V2_BASIC, DWIN_CREALITY_LCD_JYERSUI)
#define HAS_DWIN_E3V2 1
#undef LCD_BED_LEVELING // allow for MESH_EDIT_MENU
#endif
// E3V2 extras

View File

@@ -1528,7 +1528,7 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L
* LCD_BED_LEVELING requirements
*/
#if ENABLED(LCD_BED_LEVELING)
#if !HAS_MARLINUI_MENU
#if NONE(HAS_MARLINUI_MENU, DWIN_LCD_PROUI)
#error "LCD_BED_LEVELING is not supported by the selected LCD controller."
#elif !(ENABLED(MESH_BED_LEVELING) || HAS_ABL_NOT_UBL)
#error "LCD_BED_LEVELING requires MESH_BED_LEVELING or AUTO_BED_LEVELING."

View File

@@ -127,7 +127,7 @@ void DWIN_WriteToMem(uint8_t mem, uint16_t addr, uint16_t length, uint8_t *data)
DWIN_Word(i, addr + indx); // start address of the data block
++i;
for (uint8_t j = 0; j < i; ++j) { LCD_SERIAL.write(DWIN_SendBuf[j]); delayMicroseconds(1); } // Buf header
for (uint16_t j = indx; j <= indx + to_send - 1; j++) LCD_SERIAL.write(*(data + j)); delayMicroseconds(1); // write block of data
for (uint16_t j = indx; j <= indx + to_send - 1; j++) { LCD_SERIAL.write(*(data + j)); delayMicroseconds(1); } // write block of data
for (uint8_t j = 0; j < 4; ++j) { LCD_SERIAL.write(DWIN_BufTail[j]); delayMicroseconds(1); }
block++;
pending -= to_send;

View File

@@ -128,7 +128,7 @@ void onDrawMenuItem(MenuItemClass* menuitem, int8_t line) {
if (menuitem->icon) DWINUI::Draw_Icon(menuitem->icon, ICOX, MBASE(line) - 3);
if (menuitem->frameid)
DWIN_Frame_AreaCopy(menuitem->frameid, menuitem->frame.left, menuitem->frame.top, menuitem->frame.right, menuitem->frame.bottom, LBLX, MBASE(line));
else if (menuitem->caption)
else if (menuitem->caption[0])
DWINUI::Draw_String(LBLX, MBASE(line) - 1, menuitem->caption);
DWIN_Draw_HLine(HMI_data.SplitLine_Color, 16, MYPOS(line + 1), 240);
}

View File

@@ -1,17 +1,35 @@
#!/usr/bin/env python3
#
# schema.py
#
# Used by signature.py via common-dependencies.py to generate a schema file during the PlatformIO build
# when CONFIG_EXPORT is defined in the configuration.
#
# This script can also be run standalone from within the Marlin repo to generate JSON and YAML schema files.
#
# This script is a companion to abm/js/schema.js in the MarlinFirmware/AutoBuildMarlin project, which has
# been extended to evaluate conditions and can determine what options are actually enabled, not just which
# options are uncommented. That will be migrated to this script for standalone migration.
#
import re,json
"""
schema.py
Extract firmware configuration into structured JSON or YAML schema format.
Used by signature.py via common-dependencies.py to generate a schema file during the
PlatformIO build when CONFIG_EXPORT is defined in the configuration.
This script can also be run standalone from within the Marlin repo, and is a companion to
abm/js/schema.js in the MarlinFirmware/AutoBuildMarlin project, which has been extended to
evaluate conditions and can determine what options are actually enabled, not just which
options are uncommented. That will be migrated to this script for standalone migration.
Usage: schema.py [-h] [some|json|jsons|group|yml|yaml]
Process Marlin firmware configuration files (Configuration.h and Configuration_adv.h)
to produce structured output suitable for documentation, tooling, or automated processing.
Positional arguments:
some Generate both JSON and YAML output (schema.json and schema.yml)
json Generate JSON output (schema.json)
jsons Generate grouped JSON output with wildcard options (schema.json and schema_grouped.json)
group Generate grouped JSON output only (schema_grouped.json)
yml Generate YAML output (schema.yml)
yaml Same as 'yml'
Optional arguments:
-h, --help Show this help message and exit
"""
import re, json
from pathlib import Path
def extend_dict(d:dict, k:tuple):
@@ -43,8 +61,7 @@ def find_grouping(gdict, filekey, sectkey, optkey, pindex):
optparts[pindex] = '*'
wildkey = '_'.join(optparts)
kkey = f'{filekey}|{sectkey}|{wildkey}'
if kkey not in gdict: gdict[kkey] = []
gdict[kkey].append((subkey, modkey))
gdict.setdefault(kkey, []).append((subkey, modkey))
# Build a list of potential groups. Only those with multiple items will be grouped.
def group_options(schema):
@@ -70,7 +87,7 @@ def group_options(schema):
def load_boards():
bpath = Path("Marlin/src/core/boards.h")
if bpath.is_file():
with bpath.open() as bfile:
with bpath.open(encoding='utf-8') as bfile:
boards = []
for line in bfile:
if line.startswith("#define BOARD_"):
@@ -80,7 +97,7 @@ def load_boards():
return ''
#
# Extract the current configuration files in the form of a structured schema.
# Extract the specified configuration files in the form of a structured schema.
# Contains the full schema for the configuration files, not just the enabled options,
# Contains the current values of the options, not just data structure, so "schema" is a slight misnomer.
#
@@ -99,9 +116,9 @@ def load_boards():
# - requires = The conditions that must be met for the define to be enabled
# - comment = The comment for the define, if it has one
# - units = The units for the define, if it has one
# - options = The options for the define, if it has one
# - options = The options for the define, if it has any
#
def extract():
def extract_files(filekey):
# Load board names from boards.h
boards = load_boards()
@@ -114,23 +131,21 @@ def extract():
GET_SENSORS = 4 # Gathering temperature sensor options
ERROR = 9 # Syntax error
# List of files to process, with shorthand
filekey = { 'Configuration.h':'basic', 'Configuration_adv.h':'advanced' }
# A JSON object to store the data
sch_out = { 'basic':{}, 'advanced':{} }
sch_out = { key:{} for key in filekey.values() }
# Regex for #define NAME [VALUE] [COMMENT] with sanitized line
defgrep = re.compile(r'^(//)?\s*(#define)\s+([A-Za-z0-9_]+)\s*(.*?)\s*(//.+)?$')
# Pattern to match a float value
flt = r'[-+]?\s*(\d+\.|\d*\.\d+)([eE][-+]?\d+)?[fF]?'
# Defines to ignore
ignore = ('CONFIGURATION_H_VERSION', 'CONFIGURATION_ADV_H_VERSION', 'CONFIG_EXAMPLES_DIR', 'CONFIG_EXPORT')
flt = r'[-+]?\s*(?:\d+\.|\d*\.\d+)(?:[eE][-+]?\d+)?[fF]?'
# Pattern to match an integer expression
int_expr = r'(?:[-+]?\s*\d+(?:\s*[*\/+\-]\s*\d+)*)'
# Start with unknown state
state = Parse.NORMAL
# Serial ID
sid = 0
# Loop through files and parse them line by line
for fn, fk in filekey.items():
with Path("Marlin", fn).open() as fileobj:
with Path("Marlin", fn).open(encoding='utf-8') as fileobj:
section = 'none' # Current Settings section
line_number = 0 # Counter for the line number of the file
conditions = [] # Create a condition stack for the current file
@@ -140,7 +155,7 @@ def extract():
eol_options = False # The options came from end of line, so only apply once
join_line = False # A flag that the line should be joined with the previous one
line = '' # A line buffer to handle \ continuation
last_added_ref = None # Reference to the last added item
last_added_ref = {} # Reference to the last added item
# Loop through the lines in the file
for the_line in fileobj.readlines():
line_number += 1
@@ -180,26 +195,42 @@ def extract():
cfield = 'notes' if 'comment' in last_added_ref else 'comment'
last_added_ref[cfield] = cline
#
# Add the given comment line to the comment buffer, unless:
# - The line starts with ':' and JSON values to assign to 'opt'.
# - The line starts with '@section' so a new section needs to be returned.
# - The line starts with '======' so just skip it.
#
def use_comment(c, opt, sec, bufref):
if c.startswith(':'): # If the comment starts with : then it has magic JSON
d = c[1:].strip() # Strip the leading :
cbr = c.rindex('}') if d.startswith('{') else c.rindex(']') if d.startswith('[') else 0
'''
c - The comment line to parse
opt - Options JSON string to return (if not updated)
sec - Section to return (if not updated)
bufref - The comment buffer to add to
'''
sc = c.strip() # Strip for special patterns
if sc.startswith(':'): # If the comment starts with : then it has magic JSON
d = sc[1:].strip() # Strip the leading : and spaces
# Look for a JSON container
cbr = sc.rindex('}') if d.startswith('{') else sc.rindex(']') if d.startswith('[') else 0
if cbr:
opt, cmt = c[1:cbr+1].strip(), c[cbr+1:].strip()
opt, cmt = sc[1:cbr+1].strip(), sc[cbr+1:].strip()
if cmt != '': bufref.append(cmt)
else:
opt = c[1:].strip()
elif c.startswith('@section'): # Start a new section
sec = c[8:].strip()
elif not c.startswith('========'):
bufref.append(c)
opt = sc[1:].strip() # Some literal value not in a JSON container?
else:
m = re.match(r'@section\s*(.+)', sc) # Start a new section?
if m:
sec = m[1]
elif not sc.startswith('========'):
bufref.append(c) # Anything else is part of the comment
return opt, sec
# For slash comments, capture consecutive slash comments.
# The comment will be applied to the next #define.
if state == Parse.SLASH_COMMENT:
if not defmatch and the_line.startswith('//'):
use_comment(the_line[2:].strip(), options_json, section, comment_buff)
options_json, section = use_comment(the_line[2:].strip(), options_json, section, comment_buff)
continue
else:
state = Parse.NORMAL
@@ -216,17 +247,16 @@ def extract():
# Temperature sensors are done
if state == Parse.GET_SENSORS:
options_json = f'[ {options_json[:-2]} ]'
state = Parse.NORMAL
# Strip the leading '*' from block comments
# Strip the leading '* ' from block comments
cline = re.sub(r'^\* ?', '', cline)
# Collect temperature sensors
if state == Parse.GET_SENSORS:
sens = re.match(r'^(-?\d+)\s*:\s*(.+)$', cline)
sens = re.match(r'^\s*(-?\d+)\s*:\s*(.+)$', cline)
if sens:
s2 = sens[2].replace("'","''")
s2 = sens[2].replace("'", "''")
options_json += f"{sens[1]}:'{sens[1]} - {s2}', "
elif state == Parse.BLOCK_COMMENT:
@@ -251,12 +281,11 @@ def extract():
comment_buff = []
state = Parse.BLOCK_COMMENT
eol_options = False
elif cpos2 != -1 and (cpos2 < cpos1 or cpos1 == -1):
cpos = cpos2
# Comment after a define may be continued on the following lines
if defmatch != None and cpos > 10:
if defmatch is not None and cpos > 10:
state = Parse.EOL_COMMENT
prev_comment = '\n'.join(comment_buff)
comment_buff = []
@@ -286,9 +315,11 @@ def extract():
# Parenthesize the given expression if needed
def atomize(s):
if s == '' \
or re.match(r'^[A-Za-z0-9_]*(\([^)]+\))?$', s) \
or re.match(r'^[A-Za-z0-9_]+ == \d+?$', s):
s = s.strip()
if not s or s.isidentifier() or (s.startswith('(') and s.endswith(')')):
return s
if re.match(r'^[A-Za-z0-9_]*(\([^)]+\))$', s) \
or re.match(r'^[A-Za-z0-9_]+\s*[=!<>]=?\s*.*$', s):
return s
return f'({s})'
@@ -323,10 +354,10 @@ def extract():
conditions.append([ f'!defined({line[7:].strip()})' ])
# Handle a complete #define line
elif defmatch != None:
elif defmatch is not None:
# Get the match groups into vars
enabled, define_name, val = defmatch[1] == None, defmatch[3], defmatch[4]
enabled, define_name, val = defmatch[1] is None, defmatch[3], defmatch[4]
# Increment the serial ID
sid += 1
@@ -341,27 +372,30 @@ def extract():
}
# Type is based on the value
value_type = \
'switch' if val == '' \
else 'bool' if re.match(r'^(true|false)$', val) \
else 'int' if re.match(r'^[-+]?\s*\d+$', val) \
else 'ints' if re.match(r'^([-+]?\s*\d+)(\s*,\s*[-+]?\s*\d+)+$', val) \
else 'floats' if re.match(rf'({flt}(\s*,\s*{flt})+)', val) \
else 'float' if re.match(f'^({flt})$', val) \
else 'string' if val[0] == '"' \
else 'char' if val[0] == "'" \
else 'state' if re.match(r'^(LOW|HIGH)$', val) \
else 'enum' if re.match(r'^[A-Za-z0-9_]{3,}$', val) \
else 'int[]' if re.match(r'^{\s*[-+]?\s*\d+(\s*,\s*[-+]?\s*\d+)*\s*}$', val) \
else 'float[]' if re.match(r'^{{\s*{flt}(\s*,\s*{flt})*\s*}}$', val) \
else 'array' if val[0] == '{' \
else ''
value_type = (
'switch' if val == ''
else 'int' if re.match(r'^[-+]?\s*\d+$', val)
else 'ints' if re.match(r'^[-+]?\s*\d+(?:\s*,\s*[-+]?\s*\d+)+$', val)
else 'floats' if re.match(rf"^{flt}(?:\s*,\s*{flt})+$", val)
else 'float' if re.match(rf"^{flt}$", val)
else 'string' if val.startswith('"')
else 'char' if val.startswith("'")
else 'bool' if val in ('true', 'false')
else 'state' if val in ('HIGH', 'LOW')
else 'int[]' if re.match(rf"^\{{\s*{int_expr}(?:\s*,\s*{int_expr})*\s*\}}$", val)
else 'float[]' if re.match(rf"^\{{\s*{flt}(?:\s*,\s*{flt})*\s*\}}$", val)
else 'array' if val.startswith('{')
else 'enum' if re.match(r'^[A-Za-z0-9_]{3,}$', val)
else ''
)
val = (val == 'true') if value_type == 'bool' \
else int(val) if value_type == 'int' \
else val.replace('f','') if value_type == 'floats' \
else float(val.replace('f','')) if value_type == 'float' \
else val
val = (
(val == 'true') if value_type == 'bool'
else int(val) if value_type == 'int'
else val.replace('f','') if value_type == 'floats'
else float(val.replace('f','')) if value_type == 'float'
else val
)
if val != '': define_info['value'] = val
if value_type != '': define_info['type'] = value_type
@@ -371,7 +405,7 @@ def extract():
# If the comment_buff is not empty, add the comment to the info
if comment_buff:
full_comment = '\n'.join(comment_buff)
full_comment = '\n'.join(comment_buff).strip()
# An EOL comment will be added later
# The handling could go here instead of above
@@ -385,9 +419,17 @@ def extract():
units = re.match(r'^\(([^)]+)\)', full_comment)
if units:
units = units[1]
if units == 's' or units == 'sec': units = 'seconds'
if units in ('s', 'sec'): units = 'seconds'
define_info['units'] = units
if 'comment' not in define_info or define_info['comment'] == '':
if prev_comment:
define_info['comment'] = prev_comment
prev_comment = ''
if 'comment' in define_info and define_info['comment'] == '':
del define_info['comment']
# Set the options for the current #define
if define_name == "MOTHERBOARD" and boards != '':
define_info['options'] = boards
@@ -412,13 +454,31 @@ def extract():
return sch_out
#
# Extract the current configuration files in the form of a structured schema.
#
def extract():
# List of files to process, with shorthand
return extract_files({ 'Configuration.h':'basic', 'Configuration_adv.h':'advanced' })
def dump_json(schema:dict, jpath:Path):
with jpath.open('w') as jfile:
with jpath.open('w', encoding='utf-8') as jfile:
json.dump(schema, jfile, ensure_ascii=False, indent=2)
def dump_yaml(schema:dict, ypath:Path):
import yaml
with ypath.open('w') as yfile:
# Custom representer for all multi-line strings
def str_literal_representer(dumper, data):
if '\n' in data: # Check for multi-line strings
# Add a newline to trigger '|+'
if not data.endswith('\n'): data += '\n'
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
yaml.add_representer(str, str_literal_representer)
with ypath.open('w', encoding='utf-8') as yfile:
yaml.dump(schema, yfile, default_flow_style=False, width=120, indent=2)
def main():
@@ -439,12 +499,17 @@ def main():
def inargs(c): return len(set(args) & set(c)) > 0
# Help / Unknown option
unk = not inargs(['some','json','jsons','group','yml','yaml'])
unk = not inargs(['some','json','jsons','group','yml','yaml', '-h', '--help'])
if (unk): print(f"Unknown option: '{args[0]}'")
if inargs(['-h', '--help']) or unk:
print("Usage: schema.py [some|json|jsons|group|yml|yaml]...")
print(" some = json + yml")
print(" jsons = json + group")
print("Extract firmware configuration into structured JSON or YAML schema format.")
print("Usage: schema.py [-h] [some|json|jsons|group|yml|yaml]")
print(" some Generate both JSON and YAML output (schema.json and schema.yml)")
print(" json Generate JSON output (schema.json)")
print(" jsons Generate grouped JSON output with wildcard options (schema.json and schema_grouped.json)")
print(" group Generate grouped JSON output only (schema_grouped.json)")
print(" yml Generate YAML output (schema.yml)")
print(" yaml Same as 'yml'")
return
# JSON schema

View File

@@ -2,11 +2,10 @@
#
# signature.py
#
import schema
import subprocess,re,json,hashlib
import schema, subprocess, re, json, hashlib
from datetime import datetime
from pathlib import Path
from functools import reduce
def enabled_defines(filepath):
'''
@@ -35,18 +34,29 @@ def enabled_defines(filepath):
'''
outdict = {}
section = "user"
spatt = re.compile(r".*@section +([-a-zA-Z0-9_\s]+)$") # must match @section ...
spatt = re.compile(r".*@section +([-a-zA-Z0-9_\s]+)$") # @section ...
if not Path(filepath).is_file(): return outdict
f = open(filepath, encoding="utf8").read().split("\n")
# Get the full contents of the file and remove all block comments.
# This will avoid false positives from #defines in comments
f = re.sub(r'/\*.*?\*/', '', '\n'.join(f), flags=re.DOTALL).split("\n")
incomment = False
for line in f:
sline = line.strip()
m = re.match(spatt, sline) # @section ...
if m: section = m.group(1).strip() ; continue
if incomment:
if '*/' in sline:
incomment = False
continue
else:
mpos, spos = sline.find('/*'), sline.find('//')
if mpos >= 0 and (spos < 0 or spos > mpos):
incomment = True
continue
if sline[:7] == "#define":
# Extract the key here (we don't care about the value)
kv = sline[8:].strip().split()
@@ -56,9 +66,10 @@ def enabled_defines(filepath):
# Compute the SHA256 hash of a file
def get_file_sha256sum(filepath):
sha256_hash = hashlib.sha256()
with open(filepath,"rb") as f:
if not Path(filepath).is_file(): return ""
with open(filepath, "rb") as f:
# Read and update hash string value in blocks of 4K
for byte_block in iter(lambda: f.read(4096),b""):
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
@@ -67,9 +78,14 @@ def get_file_sha256sum(filepath):
#
import zipfile
def compress_file(filepath, storedname, outpath):
with zipfile.ZipFile(outpath, 'w', compression=zipfile.ZIP_BZIP2, compresslevel=9) as zipf:
zipf.write(filepath, arcname=storedname, compress_type=zipfile.ZIP_BZIP2, compresslevel=9)
with zipfile.ZipFile(outpath, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=False, compresslevel=9) as zipf:
zipf.write(filepath, arcname=storedname)
ignore = ('CONFIGURATION_H_VERSION', 'CONFIGURATION_ADV_H_VERSION', 'CONFIG_EXAMPLES_DIR', 'CONFIG_EXPORT')
#
# Compute a build signature and/or export the configuration
#
def compute_build_signature(env):
'''
Compute the build signature by extracting all configuration settings and
@@ -81,11 +97,17 @@ def compute_build_signature(env):
env.Append(BUILD_SIGNATURE=1)
build_path = Path(env['PROJECT_BUILD_DIR'], env['PIOENV'])
marlin_json = build_path / 'marlin_config.json'
json_name = 'marlin_config.json'
marlin_json = build_path / json_name
marlin_zip = build_path / 'mc.zip'
# ANSI colors
green = "\u001b[32m"
yellow = "\u001b[33m"
red = "\u001b[31m"
# Definitions from these files will be kept
header_paths = [ 'Marlin/Configuration.h', 'Marlin/Configuration_adv.h' ]
header_paths = ('Marlin/Configuration.h', 'Marlin/Configuration_adv.h')
# Check if we can skip processing
hashes = ''
@@ -100,7 +122,7 @@ def compute_build_signature(env):
conf = json.load(infile)
same_hash = conf['__INITIAL_HASH'] == hashes
if same_hash:
compress_file(marlin_json, 'marlin_config.json', marlin_zip)
compress_file(marlin_json, json_name, marlin_zip)
except:
pass
@@ -142,7 +164,8 @@ def compute_build_signature(env):
#
# Continue to gather data for CONFIGURATION_EMBEDDING or CONFIG_EXPORT
#
if not ('CONFIGURATION_EMBEDDING' in build_defines or 'CONFIG_EXPORT' in build_defines):
is_embed = 'CONFIGURATION_EMBEDDING' in build_defines
if not (is_embed or 'CONFIG_EXPORT' in build_defines):
return
# Filter out useless macros from the output
@@ -175,29 +198,60 @@ def compute_build_signature(env):
# Get the CONFIG_EXPORT value and do an extended dump if > 100
# For example, CONFIG_EXPORT 102 will make a 'config.ini' with a [config:] group for each schema @section
config_dump = tryint('CONFIG_EXPORT')
config_dump = 1 if is_embed else tryint('CONFIG_EXPORT')
extended_dump = config_dump > 100
if extended_dump: config_dump -= 100
config_dump %= 100
# Get the schema class for exports that require it
if config_dump in (3, 4) or (extended_dump and config_dump in (2, 5)):
try:
conf_schema = schema.extract()
except Exception as exc:
print(red + "Error: " + str(exc))
conf_schema = None
optorder = ('MOTHERBOARD','SERIAL_PORT','BAUDRATE','USE_WATCHDOG','THERMAL_PROTECTION_HOTENDS','THERMAL_PROTECTION_HYSTERESIS','THERMAL_PROTECTION_PERIOD','BUFSIZE','BLOCK_BUFFER_SIZE','MAX_CMD_SIZE','EXTRUDERS','TEMP_SENSOR_0','TEMP_HYSTERESIS','HEATER_0_MINTEMP','HEATER_0_MAXTEMP','PREHEAT_1_TEMP_HOTEND','BANG_MAX','PIDTEMP','PID_K1','PID_MAX','PID_FUNCTIONAL_RANGE','DEFAULT_KP','DEFAULT_KI','DEFAULT_KD','X_DRIVER_TYPE','Y_DRIVER_TYPE','Z_DRIVER_TYPE','E0_DRIVER_TYPE','X_BED_SIZE','X_MIN_POS','X_MAX_POS','Y_BED_SIZE','Y_MIN_POS','Y_MAX_POS','Z_MIN_POS','Z_MAX_POS','X_HOME_DIR','Y_HOME_DIR','Z_HOME_DIR','X_MIN_ENDSTOP_HIT_STATE','Y_MIN_ENDSTOP_HIT_STATE','Z_MIN_ENDSTOP_HIT_STATE','DEFAULT_AXIS_STEPS_PER_UNIT','AXIS_RELATIVE_MODES','DEFAULT_MAX_FEEDRATE','DEFAULT_MAX_ACCELERATION','HOMING_FEEDRATE_MM_M','HOMING_BUMP_DIVISOR','X_ENABLE_ON','Y_ENABLE_ON','Z_ENABLE_ON','E_ENABLE_ON','INVERT_X_DIR','INVERT_Y_DIR','INVERT_Z_DIR','INVERT_E0_DIR','STEP_STATE_E','STEP_STATE_X','STEP_STATE_Y','STEP_STATE_Z','DISABLE_X','DISABLE_Y','DISABLE_Z','DISABLE_E','PROPORTIONAL_FONT_RATIO','DEFAULT_NOMINAL_FILAMENT_DIA','JUNCTION_DEVIATION_MM','DEFAULT_ACCELERATION','DEFAULT_TRAVEL_ACCELERATION','DEFAULT_RETRACT_ACCELERATION','DEFAULT_MINIMUMFEEDRATE','DEFAULT_MINTRAVELFEEDRATE','MINIMUM_PLANNER_SPEED','MIN_STEPS_PER_SEGMENT','DEFAULT_MINSEGMENTTIME','BED_OVERSHOOT','BUSY_WHILE_HEATING','DEFAULT_EJERK','DEFAULT_KEEPALIVE_INTERVAL','DEFAULT_LEVELING_FADE_HEIGHT','DISABLE_OTHER_EXTRUDERS','DISPLAY_CHARSET_HD44780','EEPROM_BOOT_SILENT','EEPROM_CHITCHAT','ENDSTOPPULLUPS','EXTRUDE_MAXLENGTH','EXTRUDE_MINTEMP','HOST_KEEPALIVE_FEATURE','HOTEND_OVERSHOOT','JD_HANDLE_SMALL_SEGMENTS','LCD_INFO_SCREEN_STYLE','LCD_LANGUAGE','MAX_BED_POWER','MESH_INSET','MIN_SOFTWARE_ENDSTOPS','MAX_SOFTWARE_ENDSTOPS','MIN_SOFTWARE_ENDSTOP_X','MIN_SOFTWARE_ENDSTOP_Y','MIN_SOFTWARE_ENDSTOP_Z','MAX_SOFTWARE_ENDSTOP_X','MAX_SOFTWARE_ENDSTOP_Y','MAX_SOFTWARE_ENDSTOP_Z','PREHEAT_1_FAN_SPEED','PREHEAT_1_LABEL','PREHEAT_1_TEMP_BED','PREVENT_COLD_EXTRUSION','PREVENT_LENGTHY_EXTRUDE','PRINTJOB_TIMER_AUTOSTART','PROBING_MARGIN','SHOW_BOOTSCREEN','SOFT_PWM_SCALE','STRING_CONFIG_H_AUTHOR','TEMP_BED_HYSTERESIS','TEMP_BED_RESIDENCY_TIME','TEMP_BED_WINDOW','TEMP_RESIDENCY_TIME','TEMP_WINDOW','VALIDATE_HOMING_ENDSTOPS','XY_PROBE_FEEDRATE','Z_CLEARANCE_BETWEEN_PROBES','Z_CLEARANCE_DEPLOY_PROBE','Z_CLEARANCE_MULTI_PROBE','ARC_SUPPORT','AUTO_REPORT_TEMPERATURES','AUTOTEMP','AUTOTEMP_OLDWEIGHT','BED_CHECK_INTERVAL','DEFAULT_STEPPER_TIMEOUT_SEC','DEFAULT_VOLUMETRIC_EXTRUDER_LIMIT','DISABLE_IDLE_X','DISABLE_IDLE_Y','DISABLE_IDLE_Z','DISABLE_IDLE_E','E0_AUTO_FAN_PIN','ENCODER_100X_STEPS_PER_SEC','ENCODER_10X_STEPS_PER_SEC','ENCODER_RATE_MULTIPLIER','EXTENDED_CAPABILITIES_REPORT','EXTRUDER_AUTO_FAN_SPEED','EXTRUDER_AUTO_FAN_TEMPERATURE','FANMUX0_PIN','FANMUX1_PIN','FANMUX2_PIN','FASTER_GCODE_PARSER','HOMING_BUMP_MM','MAX_ARC_SEGMENT_MM','MIN_ARC_SEGMENT_MM','MIN_CIRCLE_SEGMENTS','N_ARC_CORRECTION','SERIAL_OVERRUN_PROTECTION','SLOWDOWN','SLOWDOWN_DIVISOR','TEMP_SENSOR_BED','THERMAL_PROTECTION_BED_HYSTERESIS','THERMOCOUPLE_MAX_ERRORS','TX_BUFFER_SIZE','WATCH_BED_TEMP_INCREASE','WATCH_BED_TEMP_PERIOD','WATCH_TEMP_INCREASE','WATCH_TEMP_PERIOD')
def optsort(x, optorder):
return optorder.index(x) if x in optorder else float('inf')
#
# Produce an INI file if CONFIG_EXPORT == 2
# CONFIG_EXPORT 102 = config.ini, 105 = Config.h
# Get sections using the schema class
#
if extended_dump and config_dump in (2, 5):
if not conf_schema: exit(1)
# Start with a preferred @section ordering
preorder = ('test','custom','info','machine','eeprom','stepper drivers','multi stepper','idex','extruder','geometry','homing','kinematics','motion','motion control','endstops','filament runout sensors','probe type','probes','bltouch','leveling','temperature','hotend temp','mpctemp','pid temp','mpc temp','bed temp','chamber temp','fans','tool change','advanced pause','calibrate','calibration','media','lcd','lights','caselight','interface','custom main menu','custom config menu','custom buttons','develop','debug matrix','delta','scara','tpara','polar','polargraph','cnc','nozzle park','nozzle clean','gcode','serial','host','filament width','i2c encoders','i2cbus','joystick','multi-material','nanodlp','network','photo','power','psu control','reporting','safety','security','servos','stats','tmc/config','tmc/hybrid','tmc/serial','tmc/smart','tmc/spi','tmc/stallguard','tmc/status','tmc/stealthchop','tmc/tmc26x','units','volumetrics','extras')
sections = { key:{} for key in preorder }
# Group options by schema @section
for header in real_config:
for name in real_config[header]:
#print(f" name: {name}")
if name in ignore: continue
ddict = real_config[header][name]
#print(f" real_config[{header}][{name}]:", ddict)
sect = ddict['section']
if sect not in sections: sections[sect] = {}
sections[sect][name] = ddict
#
# CONFIG_EXPORT 2 or 102 = config.ini
#
if config_dump == 2:
print("Generating config.ini ...")
print(yellow + "Generating config.ini ...")
ini_fmt = '{0:40} = {1}'
ext_fmt = '{0:40} {1}'
ignore = ('CONFIGURATION_H_VERSION', 'CONFIGURATION_ADV_H_VERSION', 'CONFIG_EXAMPLES_DIR', 'CONFIG_EXPORT')
if extended_dump:
# Extended export will dump config options by section
# We'll use Schema class to get the sections
try:
conf_schema = schema.extract()
except Exception as exc:
print("Error: " + str(exc))
exit(1)
if not conf_schema: exit(1)
# Then group options by schema @section
sections = {}
@@ -230,7 +284,7 @@ def compute_build_signature(env):
for line in sec_lines[1:]: sec_list += '\n' + ext_fmt.format('', line)
config_ini = build_path / 'config.ini'
with config_ini.open('w') as outfile:
with config_ini.open('w', encoding='utf-8', newline='') as outfile:
filegrp = { 'Configuration.h':'config:basic', 'Configuration_adv.h':'config:advanced' }
vers = build_defines["CONFIGURATION_H_VERSION"]
dt_string = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
@@ -293,7 +347,9 @@ f'''#
sani = re.sub(r'[- ]+', '_', skey).lower()
outfile.write(f"\n[config:{sani}]\n")
opts = sections[skey]
for name in sorted(opts):
opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
for name in opts_keys:
if name in ignore: continue
val = opts[name]['value']
if val == '': val = 'on'
#print(f" {name} = {val}")
@@ -304,28 +360,76 @@ f'''#
# Standard export just dumps config:basic and config:advanced sections
for header in real_config:
outfile.write(f'\n[{filegrp[header]}]\n')
for name in sorted(real_config[header]):
if name not in ignore:
val = real_config[header][name]['value']
if val == '': val = 'on'
outfile.write(ini_fmt.format(name.lower(), val) + '\n')
opts = real_config[header]
opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
for name in opts_keys:
if name in ignore: continue
val = opts[name]['value']
if val == '': val = 'on'
outfile.write(ini_fmt.format(name.lower(), val) + '\n')
#
# CONFIG_EXPORT 3 = schema.json, 4 = schema.yml
# CONFIG_EXPORT 5 or 105 = Config.h
#
if config_dump >= 3:
try:
conf_schema = schema.extract()
except Exception as exc:
print("Error: " + str(exc))
conf_schema = None
if config_dump == 5:
print(yellow + "Generating Config-export.h ...")
config_h = Path('Marlin', 'Config-export.h')
with config_h.open('w') as outfile:
filegrp = { 'Configuration.h':'config:basic', 'Configuration_adv.h':'config:advanced' }
vers = build_defines["CONFIGURATION_H_VERSION"]
dt_string = datetime.utcnow().strftime("%Y-%m-%d at %H:%M:%S")
out_text = f'''/**
* Config.h - Marlin Firmware distilled configuration
* Usage: Place this file in the 'Marlin' folder with the name 'Config.h'.
*
* Exported by Marlin build on {dt_string}.
*/
'''
subs = (('Bltouch','BLTouch'),('hchop','hChop'),('Eeprom','EEPROM'),('Gcode','G-code'),('lguard','lGuard'),('Idex','IDEX'),('Lcd','LCD'),('Mpc','MPC'),('Pid','PID'),('Psu','PSU'),('Scara','SCARA'),('Spi','SPI'),('Tmc','TMC'),('Tpara','TPARA'))
define_fmt = '#define {0:40} {1}'
if extended_dump:
# Loop through the sections
for skey in sections:
#print(f" skey: {skey}")
opts = sections[skey]
headed = False
opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
for name in opts_keys:
if name in ignore: continue
val = opts[name]['value']
if not headed:
head = reduce(lambda s, r: s.replace(*r), subs, skey.title())
out_text += f"\n//\n// {head}\n//\n"
headed = True
out_text += define_fmt.format(name, val).strip() + '\n'
else:
# Dump config options in just two sections, by file
for header in real_config:
out_text += f'\n/**\n * Overrides for {header}\n */\n'
opts = real_config[header]
opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
for name in opts_keys:
if name in ignore: continue
val = opts[name]['value']
out_text += define_fmt.format(name, val).strip() + '\n'
outfile.write(out_text)
#
# CONFIG_EXPORT 3 = schema.json, 13 = schema_grouped.json, 4 = schema.yml
#
if config_dump in (3, 4, 13):
if conf_schema:
#
# 3 = schema.json
#
if config_dump in (3, 13):
print("Generating schema.json ...")
print(yellow + "Generating schema.json ...")
schema.dump_json(conf_schema, build_path / 'schema.json')
if config_dump == 13:
schema.group_options(conf_schema)
@@ -335,7 +439,7 @@ f'''#
# 4 = schema.yml
#
elif config_dump == 4:
print("Generating schema.yml ...")
print(yellow + "Generating schema.yml ...")
try:
import yaml
except ImportError:
@@ -347,19 +451,20 @@ f'''#
schema.dump_yaml(conf_schema, build_path / 'schema.yml')
#
# Produce a JSON file for CONFIGURATION_EMBEDDING or CONFIG_EXPORT == 1
# Produce a JSON file for CONFIGURATION_EMBEDDING or CONFIG_EXPORT == 1 or 101
# Skip if an identical JSON file was already present.
#
if not same_hash and (config_dump == 1 or 'CONFIGURATION_EMBEDDING' in build_defines):
if not same_hash and config_dump == 1:
with marlin_json.open('w') as outfile:
json_data = {}
if extended_dump:
print("Extended dump ...")
print(yellow + "Extended dump ...")
for header in real_config:
confs = real_config[header]
json_data[header] = {}
for name in confs:
if name in ignore: continue
c = confs[name]
s = c['section']
if s not in json_data[header]: json_data[header][s] = {}
@@ -369,6 +474,7 @@ f'''#
conf = real_config[header]
#print(f"real_config[{header}]", conf)
for name in conf:
if name in ignore: continue
json_data[name] = conf[name]['value']
json_data['__INITIAL_HASH'] = hashes
@@ -389,13 +495,13 @@ f'''#
#
# The rest only applies to CONFIGURATION_EMBEDDING
#
if not 'CONFIGURATION_EMBEDDING' in build_defines:
if not is_embed:
(build_path / 'mc.zip').unlink(missing_ok=True)
return
# Compress the JSON file as much as we can
if not same_hash:
compress_file(marlin_json, 'marlin_config.json', marlin_zip)
compress_file(marlin_json, json_name, marlin_zip)
# Generate a C source file containing the entire ZIP file as an array
with open('Marlin/src/mczip.h','wb') as result_file:
@@ -415,5 +521,12 @@ f'''#
if __name__ == "__main__":
# Build required. From command line just explain usage.
print("Use schema.py to export JSON and YAML from the command-line.")
print("Build Marlin with CONFIG_EXPORT 2 to export 'config.ini'.")
print("*** THIS SCRIPT USED BY common-dependencies.py ***\n\n"
+ "Current options for config and schema export:\n"
+ " - marlin_config.json : Build Marlin with CONFIG_EXPORT 1 or 101. (Use CONFIGURATION_EMBEDDING for 'mc.zip')\n"
+ " - config.ini : Build Marlin with CONFIG_EXPORT 2 or 102.\n"
+ " - schema.json : Run 'schema.py json' (CONFIG_EXPORT 3).\n"
+ " - schema_grouped.json : Run 'schema.py group' (CONFIG_EXPORT 13).\n"
+ " - schema.yml : Run 'schema.py yml' (CONFIG_EXPORT 4).\n"
+ " - Config-export.h : Build Marlin with CONFIG_EXPORT 5 or 105.\n"
)