Implemented TactilityKernel and DevicetreeCompiler, updated licenses & copyrights (#452)

**New features**

- Created a devicetree DTS and YAML parser in Python
- Created new modules:
  - TactilityKernel (LGPL v3.0 license)
  - Platforms/PlatformEsp32 (LGPL v3.0 license) 
  - Platforms/PlatformPosix (LGPL v3.0 license)
  - Tests/TactilityKernelTests

Most boards have a placeholder DTS file, while T-Lora Pager has a few devices attached.

**Licenses**

Clarified licenses and copyrights better.

- Add explanation about the intent behind them.
- Added explanation about licenses for past and future subprojects
- Added more details explanations with regards to the logo usage
- Copied licenses to subprojects to make it more explicit
This commit is contained in:
Ken Van Hoeylandt
2026-01-24 15:47:11 +01:00
committed by GitHub
parent 0d16eb606f
commit 4b6ed871a9
194 changed files with 7807 additions and 741 deletions
@@ -0,0 +1,19 @@
import os
def find_bindings(directory_path: str) -> list[str]:
yaml_files = []
for root, dirs, files in os.walk(directory_path):
for file in files:
if file.endswith(".yaml"):
full_path = os.path.join(root, file)
yaml_files.append(os.path.abspath(full_path))
return yaml_files
def find_all_bindings(directory_paths: list[str]) -> list[str]:
yaml_files = []
for directory_path in directory_paths:
new_paths = find_bindings(directory_path)
if len(new_paths) == 0:
raise Exception(f"No bindings found in {directory_path}")
yaml_files += new_paths
return yaml_files
@@ -0,0 +1,57 @@
import yaml
import os
from .models import Binding, BindingProperty
def parse_binding(file_path: str, binding_dirs: list[str]) -> Binding:
with open(file_path, 'r') as f:
data = yaml.safe_load(f)
description = data.get('description', '')
bus = data.get('bus', None)
properties_dict = {}
# Handle inclusions
includes = data.get('include', [])
all_includes = list(includes) # Copy for iteration
for include_file in includes:
include_path = None
for binding_dir in binding_dirs:
potential_path = os.path.join(binding_dir, include_file)
if os.path.exists(potential_path):
include_path = potential_path
break
if not include_path:
print(f"Warning: Could not find include file {include_file}")
continue
parent_binding = parse_binding(include_path, binding_dirs)
if not description and parent_binding.description:
description = parent_binding.description
if not bus and parent_binding.bus:
bus = parent_binding.bus
for prop in parent_binding.properties:
properties_dict[prop.name] = prop
for include in parent_binding.includes:
all_includes.append(include)
# Parse local properties
compatible = data.get('compatible', None)
properties_raw = data.get('properties', {})
for name, details in properties_raw.items():
prop = BindingProperty(
name=name,
type=details.get('type', 'unknown'),
required=details.get('required', False),
description=details.get('description', '').strip(),
)
properties_dict[name] = prop
filename = os.path.basename(file_path)
return Binding(
filename=filename,
compatible=compatible,
description=description.strip(),
properties=list(properties_dict.values()),
includes=all_includes,
bus=bus
)
@@ -0,0 +1,50 @@
from dataclasses import dataclass, field
import yaml
import os
@dataclass
class DeviceTreeConfig:
dependencies: list[str] = field(default_factory=list)
bindings: list[str] = field(default_factory=list)
dts: str = ""
def parse_config(file_path: str, project_root: str) -> DeviceTreeConfig:
"""
Parses devicetree.yaml and recursively finds dependencies.
Returns a list of DeviceTreeConfig objects in post-order (dependencies first).
"""
config = DeviceTreeConfig([], [], "")
visited = set()
def _parse_recursive(current_path: str, is_root: bool):
abs_path = os.path.abspath(current_path)
if abs_path in visited:
return
visited.add(abs_path)
# Try to see if it's a directory and contains devicetree.yaml
if os.path.isdir(abs_path):
abs_path = os.path.join(abs_path, "devicetree.yaml")
with open(abs_path, 'r') as f:
data = yaml.safe_load(f) or {}
# Handle dependencies before adding current config (post-order)
deps = data.get("dependencies", [])
for dep in deps:
# Dependencies are relative to project_root
dep_path = os.path.join(project_root, dep)
_parse_recursive(dep_path, False)
if is_root:
config.dependencies += deps
dts_path = data.get("dts", "")
config.dts = os.path.join(current_path, dts_path)
bindings = data.get("bindings", "")
if bindings:
bindings_resolved = os.path.join(current_path, bindings)
config.bindings.append(bindings_resolved)
_parse_recursive(file_path, True)
return config
@@ -0,0 +1,9 @@
def read_file(path: str):
with open(path, "r") as file:
result = file.read()
return result
def write_file(path: str, content: str):
with open(path, "w") as file:
result = file.write(content)
return result
@@ -0,0 +1,217 @@
import os.path
from textwrap import dedent
from source.models import *
def write_include(file, include: IncludeC, verbose: bool):
if verbose:
print("Processing include:")
print(f" {include.statement}")
file.write(include.statement)
file.write('\n')
def get_device_identifier_safe(device: Device):
if device.identifier == "/":
return "root"
else:
return device.identifier
def get_device_type_name(device: Device, bindings: list[Binding]):
device_binding = find_device_binding(device, bindings)
if device_binding is None:
raise Exception(f"Binding not found for {device.identifier}")
if device_binding.compatible is None:
raise Exception(f"Couldn't find compatible binding for {device.identifier}")
compatible_safe = device_binding.compatible.split(",")[-1]
return compatible_safe.replace("-", "_")
def find_device_property(device: Device, name: str) -> DeviceProperty:
for property in device.properties:
if property.name == name:
return property
return None
def find_device_binding(device: Device, bindings: list[Binding]) -> Binding:
compatible_property = find_device_property(device, "compatible")
if compatible_property is None:
raise Exception(f"property 'compatible' not found in device {device.identifier}")
for binding in bindings:
if binding.compatible == compatible_property.value:
return binding
return None
def find_binding(compatible: str, bindings: list[Binding]) -> Binding:
for binding in bindings:
if binding.compatible == compatible:
return binding
return None
def property_to_string(property: DeviceProperty) -> str:
type = property.type
if type == "value":
return property.value
elif type == "text":
return f"\"{property.value}\""
elif type == "values":
return "{ " + ",".join(property.value) + " }"
else:
raise Exception(f"property_to_string() has an unsupported type: {type}")
def resolve_parameters_from_bindings(device: Device, bindings: list[Binding]) -> list:
compatible_property = find_device_property(device, "compatible")
if compatible_property is None:
raise Exception(f"Cannot find 'compatible' property for {device.identifier}")
device_binding = find_binding(compatible_property.value, bindings)
if device_binding is None:
raise Exception(f"Binding not found for {device.identifier} and compatible '{compatible_property.value}'")
# Filter out system properties
binding_properties = []
for property in device_binding.properties:
if property.name != "compatible":
binding_properties.append(property)
# Allocate total expected configuration arguments
result = [0] * len(binding_properties)
for index, binding_property in enumerate(binding_properties):
device_property = find_device_property(device, binding_property.name)
if device_property is None:
if binding_property.required:
raise Exception(f"device {device.identifier} doesn't have property '{binding_property.name}'")
else:
result[index] = '0'
else:
result[index] = property_to_string(device_property)
return result
def write_config(file, device: Device, bindings: list[Binding], type_name: str):
device_identifier = get_device_identifier_safe(device)
config_type = f"{type_name}_config_dt"
config_variable_name = f"{device_identifier}_config"
file.write(f"static const {config_type} {config_variable_name}" " = {\n")
config_params = resolve_parameters_from_bindings(device, bindings)
# Indent all params
for index, config_param in enumerate(config_params):
config_params[index] = f"\t{config_param}"
# Join with command and newline
if len(config_params) > 0:
config_params_joined = ",\n".join(config_params)
file.write(f"{config_params_joined}\n")
file.write("};\n\n")
def write_device_structs(file, device: Device, parent_device: Device, bindings: list[Binding], verbose: bool):
if verbose:
print(f"Writing device struct for '{device.identifier}'")
# Assemble some pre-requisites
type_name = get_device_type_name(device, bindings)
compatible_property = find_device_property(device, "compatible")
if compatible_property is None:
raise Exception(f"Cannot find 'compatible' property for {device.identifier}")
identifier = get_device_identifier_safe(device)
config_variable_name = f"{identifier}_config"
if parent_device is not None:
parent_identifier = get_device_identifier_safe(parent_device)
parent_value = f"&{parent_identifier}"
else:
parent_value = "NULL"
# Write config struct
write_config(file, device, bindings, type_name)
# Write device struct
file.write(f"static struct Device {identifier}" " = {\n")
file.write(f"\t.name = \"{device.identifier}\",\n") # Use original name
file.write(f"\t.config = &{config_variable_name},\n")
file.write(f"\t.parent = {parent_value},\n")
file.write("};\n\n")
# Child devices
for child_device in device.devices:
write_device_structs(file, child_device, device, bindings, verbose)
def write_device_init(file, device: Device, bindings: list[Binding], verbose: bool):
if verbose:
print(f"Processing device init code for '{device.identifier}'")
# Assemble some pre-requisites
compatible_property = find_device_property(device, "compatible")
if compatible_property is None:
raise Exception(f"Cannot find 'compatible' property for {device.identifier}")
# Type & instance names
identifier = get_device_identifier_safe(device)
device_variable = identifier
# Write device struct
file.write(f"\tif (init_builtin_device(&{device_variable}, \"{compatible_property.value}\") != 0) return -1;\n")
# Write children
for child_device in device.devices:
write_device_init(file, child_device, bindings, verbose)
def generate_devicetree_c(filename: str, items: list[object], bindings: list[Binding], verbose: bool):
with open(filename, "w") as file:
file.write(dedent('''\
// Default headers
#include <Tactility/Device.h>
#include <Tactility/Driver.h>
#include <Tactility/Log.h>
// DTS headers
'''))
# Write all headers first
for item in items:
if type(item) is IncludeC:
write_include(file, item, verbose)
file.write("\n")
file.write(dedent('''\
#define TAG LOG_TAG(devicetree)
static int init_builtin_device(struct Device* device, const char* compatible) {
struct Driver* driver = driver_find_compatible(compatible);
if (driver == NULL) {
LOG_E(TAG, "Can't find driver: %s", compatible);
return -1;
}
device_construct(device);
device_set_driver(device, driver);
device_add(device);
const int err = device_start(device);
if (err != 0) {
LOG_E(TAG, "Failed to start device %s with driver %s: error code %d", device->name, compatible, err);
return -1;
}
return 0;
}
'''))
# Then write all devices
for item in items:
if type(item) is Device:
write_device_structs(file, item, None, bindings, verbose)
# Init function body start
file.write("int devices_builtin_init() {\n")
# Init function body logic
for item in items:
if type(item) is Device:
write_device_init(file, item, bindings, verbose)
file.write("\treturn 0;\n")
# Init function body end
file.write("}\n")
def generate_devicetree_h(filename: str):
with open(filename, "w") as file:
file.write(dedent('''\
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
extern int devices_builtin_init();
#ifdef __cplusplus
}
#endif
'''))
def generate(output_path: str, items: list[object], bindings: list[Binding], verbose: bool):
if not os.path.exists(output_path):
os.makedirs(output_path)
devicetree_c_filename = os.path.join(output_path, "devicetree.c")
generate_devicetree_c(devicetree_c_filename, items, bindings, verbose)
devicetree_h_filename = os.path.join(output_path, "devicetree.h")
generate_devicetree_h(devicetree_h_filename)
@@ -0,0 +1,46 @@
%import common.DIGIT -> DIGIT
%import common.LETTER -> LETTER
%import common.HEXDIGIT -> HEXDIGIT
%import common.SIGNED_INT -> SIGNED_INT
%import common.WS -> WS
%import common.SIGNED_NUMBER -> SIGNED_NUMBER
%import common.ESCAPED_STRING -> ESCAPED_STRING
%ignore WS
// Comment
COMMENT: /\/\*([^*]|\*+[^*\/])*\*+\//
%ignore COMMENT
// Boolean
BOOLEAN: "true" | "false"
// Main
INCLUDE_C: /#include <[\w\/.\-]+>/
PROPERTY_NAME: /#?[a-zA-Z0-9_\-,]+/
QUOTE: "\""
QUOTED_TEXT: QUOTE /[^"]+/ QUOTE
quoted_text_array: QUOTED_TEXT ("," " "* QUOTED_TEXT)+
HEX_NUMBER: "0x" HEXDIGIT+
NUMBER: SIGNED_NUMBER | HEX_NUMBER
PHANDLE: /&[0-9a-zA-Z\-]+/
C_VARIABLE: /[0-9a-zA-Z_]+/
VALUE: NUMBER | PHANDLE | C_VARIABLE
value: VALUE
values: VALUE+
array: NUMBER+
property_value: quoted_text_array | QUOTED_TEXT | "<" value ">" | "<" values ">" | "[" array "]"
device_property: PROPERTY_NAME ["=" property_value] ";"
DEVICE_IDENTIFIER: /[a-zA-Z0-9_\-\/@]+/
device: DEVICE_IDENTIFIER "{" (device | device_property)* "};"
dts_version: /[0-9a-zA-Z\-]+/
start: "/" dts_version "/;" INCLUDE_C* device+
@@ -0,0 +1,46 @@
import os
from pprint import pprint
from lark import Lark
from source.files import *
from source.transformer import *
from source.generator import *
from source.binding_files import find_all_bindings
from source.binding_parser import parse_binding
from source.config import *
def main(config_path: str, output_path: str, verbose: bool):
print(f"Generating devicetree code\n config: {config_path}\n output: {output_path}")
if not os.path.isdir(config_path):
raise Exception(f"Directory not found: {config_path}")
config = parse_config(config_path, os.getcwd())
if verbose:
pprint(config)
project_dir = os.path.dirname(os.path.realpath(__file__))
grammar_path = os.path.join(project_dir, "grammar.lark")
lark_data = read_file(grammar_path)
dts_data = read_file(config.dts)
lark = Lark(lark_data)
parsed = lark.parse(dts_data)
if verbose:
print(parsed.pretty())
transformed = DtsTransformer().transform(parsed)
if verbose:
pprint(transformed)
binding_files = find_all_bindings(config.bindings)
if verbose:
print(f"Bindings found:")
for binding_file in binding_files:
print(f" {binding_file}")
if verbose:
print(f"Parsing bindings")
bindings = []
for binding_file in binding_files:
bindings.append(parse_binding(binding_file, config.bindings))
if verbose:
for binding in bindings:
pprint(binding)
generate(output_path, transformed, bindings, verbose)
@@ -0,0 +1,42 @@
from dataclasses import dataclass
@dataclass
class DtsVersion:
version: str
@dataclass
class Device:
identifier: str
properties: list
devices: list
@dataclass
class DeviceProperty:
name: str
type: str
value: object
@dataclass
class PropertyValue:
type: str
value: object
@dataclass
class IncludeC:
statement: str
@dataclass
class BindingProperty:
name: str
type: str
required: bool
description: str
@dataclass
class Binding:
filename: str
compatible: list[str]
description: str
properties: list[BindingProperty]
includes: list[str]
bus: str = None
@@ -0,0 +1,20 @@
import sys
if sys.platform == "win32":
SHELL_COLOR_RED = ""
SHELL_COLOR_ORANGE = ""
SHELL_COLOR_RESET = ""
else:
SHELL_COLOR_RED = "\033[91m"
SHELL_COLOR_ORANGE = "\033[93m"
SHELL_COLOR_RESET = "\033[m"
def print_warning(message):
print(f"{SHELL_COLOR_ORANGE}WARNING: {message}{SHELL_COLOR_RESET}")
def print_error(message):
print(f"{SHELL_COLOR_RED}ERROR: {message}{SHELL_COLOR_RESET}")
def exit_with_error(message):
print_error(message)
sys.exit(1)
@@ -0,0 +1,67 @@
from typing import List
from lark import Transformer
from lark import Token
from source.models import *
def flatten_token_array(tokens: List[Token], name: str):
result_list = list()
for token in tokens:
result_list.append(token.value)
return Token(name, result_list)
class DtsTransformer(Transformer):
# Flatten the start node into a list
def start(self, tokens):
return tokens
def dts_version(self, tokens: List[Token]):
version = tokens[0].value
if version != "dts-v1":
raise Exception(f"Unsupported DTS version: {version}")
return DtsVersion(version)
def device(self, tokens: list):
identifier = "UNKNOWN"
properties = list()
devices = list()
for index, entry in enumerate(tokens):
if index == 0:
identifier = entry.value
elif type(entry) is DeviceProperty:
properties.append(entry)
elif type(entry) is Device:
devices.append(entry)
return Device(identifier, properties, devices)
def device_property(self, objects: List[object]):
name = objects[0]
if len(objects) == 1:
# Boolean property with no value
return DeviceProperty(name, "boolean", True)
if type(objects[1]) is not PropertyValue:
raise Exception(f"Object was not converted to PropertyValue: {objects[1]}")
return DeviceProperty(name, objects[1].type, objects[1].value)
def property_value(self, tokens: List):
token = tokens[0]
if type(token) is Token:
raise Exception(f"Failed to convert token to PropertyValue: {token}")
return token
def values(self, object):
return PropertyValue(type="values", value=object)
def value(self, object):
return PropertyValue(type="value", value=object[0])
def array(self, object):
return PropertyValue(type="array", value=object)
def VALUE(self, token: Token):
return token.value
def NUMBER(self, token: Token):
return token.value
def PROPERTY_NAME(self, token: Token):
return token.value
def QUOTED_TEXT(self, token: Token):
return PropertyValue("text", token.value[1:-1])
def quoted_text_array(self, tokens: List[Token]):
result_list = list()
for token in tokens:
result_list.append(token.value)
return PropertyValue("text_array", result_list)
def INCLUDE_C(self, token: Token):
return IncludeC(token.value)