Merge upstream changes from Marlin 2.1.2.2

This commit is contained in:
Stefan Kalscheuer
2024-02-13 20:32:22 +01:00
parent e466309bfb
commit 752476dc8f
1367 changed files with 41601 additions and 31124 deletions

View File

@@ -2,8 +2,14 @@
#
# schema.py
#
# Used by signature.py via common-dependencies.py to generate a schema file during the PlatformIO build.
# This script can also be run standalone from within the Marlin repo to generate all schema files.
# 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
from pathlib import Path
@@ -74,7 +80,26 @@ def load_boards():
return ''
#
# Extract a schema from the current configuration files
# Extract the current 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.
#
# The returned object is a nested dictionary with the following indexing:
#
# - schema[filekey][section][define_name] = define_info
#
# Where the define_info contains the following keyed fields:
# - section = The @section the define is in
# - name = The name of the define
# - enabled = True if the define is enabled (not commented out)
# - line = The line number of the define
# - sid = A serial ID for the define
# - value = The value of the define, if it has one
# - type = The type of the define, if it has one
# - 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
#
def extract():
# Load board names from boards.h
@@ -85,7 +110,8 @@ def extract():
NORMAL = 0 # No condition yet
BLOCK_COMMENT = 1 # Looking for the end of the block comment
EOL_COMMENT = 2 # EOL comment started, maybe add the next comment?
GET_SENSORS = 3 # Gathering temperature sensor options
SLASH_COMMENT = 3 # Block-like comment, starting with aligned //
GET_SENSORS = 4 # Gathering temperature sensor options
ERROR = 9 # Syntax error
# List of files to process, with shorthand
@@ -94,6 +120,8 @@ def extract():
sch_out = { 'basic':{}, 'advanced':{} }
# 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')
# Start with unknown state
@@ -107,6 +135,7 @@ def extract():
line_number = 0 # Counter for the line number of the file
conditions = [] # Create a condition stack for the current file
comment_buff = [] # A temporary buffer for comments
prev_comment = '' # Copy before reset for an EOL comment
options_json = '' # A buffer for the most recent options JSON found
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
@@ -143,9 +172,13 @@ def extract():
if not defmatch and the_line.startswith('//'):
comment_buff.append(the_line[2:].strip())
else:
last_added_ref['comment'] = ' '.join(comment_buff)
comment_buff = []
state = Parse.NORMAL
cline = ' '.join(comment_buff)
comment_buff = []
if cline != '':
# A (block or slash) comment was already added
cfield = 'notes' if 'comment' in last_added_ref else 'comment'
last_added_ref[cfield] = cline
def use_comment(c, opt, sec, bufref):
if c.startswith(':'): # If the comment starts with : then it has magic JSON
@@ -162,6 +195,15 @@ def extract():
bufref.append(c)
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)
continue
else:
state = Parse.NORMAL
# In a block comment, capture lines up to the end of the comment.
# Assume nothing follows the comment closure.
if state in (Parse.BLOCK_COMMENT, Parse.GET_SENSORS):
@@ -178,19 +220,19 @@ def extract():
state = Parse.NORMAL
# Strip the leading '*' from block comments
if cline.startswith('*'): cline = cline[1:].strip()
cline = re.sub(r'^\* ?', '', cline)
# Collect temperature sensors
if state == Parse.GET_SENSORS:
sens = re.match(r'^(-?\d+)\s*:\s*(.+)$', cline)
if sens:
s2 = sens[2].replace("'","''")
options_json += f"{sens[1]}:'{s2}', "
options_json += f"{sens[1]}:'{sens[1]} - {s2}', "
elif state == Parse.BLOCK_COMMENT:
# Look for temperature sensors
if cline == "Temperature sensors available:":
if re.match(r'temperature sensors.*:', cline, re.IGNORECASE):
state, cline = Parse.GET_SENSORS, "Temperature Sensors"
options_json, section = use_comment(cline, options_json, section, comment_buff)
@@ -216,15 +258,19 @@ def extract():
# Comment after a define may be continued on the following lines
if defmatch != None and cpos > 10:
state = Parse.EOL_COMMENT
prev_comment = '\n'.join(comment_buff)
comment_buff = []
else:
state = Parse.SLASH_COMMENT
# Process the start of a new comment
if cpos != -1:
comment_buff = []
cline, line = line[cpos+2:].strip(), line[:cpos].strip()
if state == Parse.BLOCK_COMMENT:
# Strip leading '*' from block comments
if cline.startswith('*'): cline = cline[1:].strip()
cline = re.sub(r'^\* ?', '', cline)
else:
# Expire end-of-line options after first use
if cline.startswith(':'): eol_options = True
@@ -295,32 +341,33 @@ def extract():
}
# Type is based on the value
if val == '':
value_type = 'switch'
elif re.match(r'^(true|false)$', val):
value_type = 'bool'
val = val == 'true'
elif re.match(r'^[-+]?\s*\d+$', val):
value_type = 'int'
val = int(val)
elif re.match(r'[-+]?\s*(\d+\.|\d*\.\d+)([eE][-+]?\d+)?[fF]?', val):
value_type = 'float'
val = float(val.replace('f',''))
else:
value_type = '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*)?)+}$', val) \
else 'float[]' if re.match(r'^{(\s*[-+]?\s*(\d+\.|\d*\.\d+)([eE][-+]?\d+)?[fF]?\s*(,\s*)?)+}$', val) \
else 'array' if val[0] == '{' \
else ''
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 ''
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
# Join up accumulated conditions with &&
if conditions: define_info['requires'] = ' && '.join(sum(conditions, []))
if conditions: define_info['requires'] = '(' + ') && ('.join(sum(conditions, [])) + ')'
# If the comment_buff is not empty, add the comment to the info
if comment_buff:
@@ -383,25 +430,35 @@ def main():
if schema:
# Get the first command line argument
# Get the command line arguments after the script name
import sys
if len(sys.argv) > 1:
arg = sys.argv[1]
else:
arg = 'some'
args = sys.argv[1:]
if len(args) == 0: args = ['some']
# Does the given array intersect at all with args?
def inargs(c): return len(set(args) & set(c)) > 0
# Help / Unknown option
unk = not inargs(['some','json','jsons','group','yml','yaml'])
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")
return
# JSON schema
if arg in ['some', 'json', 'jsons']:
if inargs(['some', 'json', 'jsons']):
print("Generating JSON ...")
dump_json(schema, Path('schema.json'))
# JSON schema (wildcard names)
if arg in ['group', 'jsons']:
if inargs(['group', 'jsons']):
group_options(schema)
dump_json(schema, Path('schema_grouped.json'))
# YAML
if arg in ['some', 'yml', 'yaml']:
if inargs(['some', 'yml', 'yaml']):
try:
import yaml
except ImportError: