FDS/OS 1.0

This commit is contained in:
2026-09-21 22:29:23 +08:00
commit 99bc3d15c5
430 changed files with 34876 additions and 0 deletions
View File
+5
View File
@@ -0,0 +1,5 @@
# Mandatory hardware additions to every FDS SYSTEM image, including CLI-only.
# The rootfs builder consumes this alongside the fds-base metapackage.
# fds-base also depends on this controller so it cannot be accidentally omitted.
fds-base
fds-dasungd
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Assemble and read back a Pi 5 FAT32 boot partition; never access a host disk."""
import argparse
import datetime
import json
import mmap
import os
from pathlib import Path
import shutil
import struct
import subprocess
import sys
import tarfile
import tempfile
project = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(project/'tools'))
from image_formats import digest
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--mode', choices=['production', 'development'], default='production')
args = parser.parse_args()
subprocess.run([str(project/'tools/prepare-pi-firmware')], check=True)
subprocess.run([str(project/'tools/prepare-image-tools')], check=True)
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
firmware_config = dict(line.split('=', 1) for line in (project/'config/pi-firmware.conf').read_text().splitlines() if line and not line.startswith('#'))
package = project/'out/cache/boot'/f'rpi-firmware-{firmware_config["PI_FIRMWARE_VERSION"]}.aarch64.xbps'
work = Path(tempfile.mkdtemp(prefix='boot-build.', dir=project/'out'))
files = work/'files'
files.mkdir()
with tarfile.open(package) as archive:
for member in archive:
name = member.name.removeprefix('./')
if not member.isfile(): continue
relative = Path(name)
if relative.parent == Path('boot') and relative.suffix in ('.elf', '.dat', '.bin'):
(files/relative.name).write_bytes(archive.extractfile(member).read())
elif relative.parent == Path('usr/share/licenses/rpi-firmware'):
(files/relative.name).write_bytes(archive.extractfile(member).read())
kernel = project/'out/kernel/boot'
shutil.copy2(kernel/'kernel_2712.img', files)
for dtb in sorted(kernel.glob('bcm2712-*.dtb')): shutil.copy2(dtb, files)
shutil.copytree(kernel/'overlays', files/'overlays')
shutil.copy2(project/'out/fds-initramfs.img', files/'fds-initramfs.img')
shutil.copy2(project/'image/pi5/config.txt', files)
shutil.copy2(project/'image/pi5'/f'cmdline-{args.mode}.txt', files/'cmdline.txt')
assert (files/'bcm2712-rpi-5-b.dtb').is_file()
assert len((files/'cmdline.txt').read_text().splitlines()) == 1
for path in sorted(files.rglob('*')): os.utime(path, (epoch, epoch))
image = work/'boot.fat'
runner = str(project/'tools/in-image-tools')
subprocess.run([runner, 'mkfs.fat', '--invariant', '-C', '-F', '32', '-n', 'FDS_BOOT', str(image), '524288'], check=True)
for path in sorted(files.iterdir()):
subprocess.run([runner, 'mcopy', '-m', '-s', '-i', str(image), str(path), '::/'], check=True)
# mtools preserves file times but creates directory records at wall-clock time.
# Normalize FAT32 directory timestamps without touching file data or long names.
stamp = datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc)
date = ((stamp.year-1980)<<9) | (stamp.month<<5) | stamp.day
clock = (stamp.hour<<11) | (stamp.minute<<5) | (stamp.second//2)
with image.open('r+b') as stream, mmap.mmap(stream.fileno(), 0) as disk:
sector = struct.unpack_from('<H', disk, 11)[0]
cluster_bytes = sector*disk[13]
reserved = struct.unpack_from('<H', disk, 14)[0]
fat_sectors = struct.unpack_from('<I', disk, 36)[0]
fat_offset = reserved*sector
data_offset = (reserved+disk[16]*fat_sectors)*sector
root = struct.unpack_from('<I', disk, 44)[0]
visited = set()
def directory(cluster):
while cluster < 0x0ffffff8:
assert cluster >= 2 and cluster not in visited
visited.add(cluster)
start = data_offset+(cluster-2)*cluster_bytes
assert start+cluster_bytes <= len(disk)
for offset in range(start, start+cluster_bytes, 32):
if disk[offset] == 0: break
if disk[offset] == 0xe5 or disk[offset+11] == 0x0f: continue
disk[offset+13] = (stamp.second%2)*100
struct.pack_into('<HHH', disk, offset+14, clock, date, date)
struct.pack_into('<HH', disk, offset+22, clock, date)
if disk[offset+11] & 0x10 and disk[offset] != ord('.'):
child = (struct.unpack_from('<H', disk, offset+20)[0]<<16) | struct.unpack_from('<H', disk, offset+26)[0]
directory(child)
cluster = struct.unpack_from('<I', disk, fat_offset+cluster*4)[0] & 0x0fffffff
directory(root)
disk.flush()
subprocess.run([runner, 'fsck.fat', '-n', str(image)], check=True)
readback = work/'readback'
readback.mkdir()
subprocess.run([runner, 'mcopy', '-s', '-i', str(image), '::*', str(readback)], check=True)
expected = {str(path.relative_to(files)): digest(path) for path in sorted(files.rglob('*')) if path.is_file()}
observed = {str(path.relative_to(readback)): digest(path) for path in sorted(readback.rglob('*')) if path.is_file()}
assert observed == expected, 'FAT read-back mismatch'
(work/'manifest.json').write_text(json.dumps({'mode': args.mode, 'bytes': image.stat().st_size,
'image_sha256': digest(image), 'firmware_sha256': digest(package), 'files': expected}, indent=2)+'\n')
for name, source in [('fds-boot.img', work.name+'/boot.fat'), ('boot-volume', work.name)]:
target = project/'out'/(name+'.next')
target.symlink_to(source)
target.replace(project/'out'/name)
print(f'PASS: 512 MiB FDS_BOOT FAT32 partition image, all files verified: {work}')
print('SKIP: firmware boot, NVMe and display mode validation require a physical Pi')
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Build a deterministic Rust-only initramfs with the required early display daemon."""
import argparse
import gzip
import json
from pathlib import Path
import shutil
import stat
import subprocess
import sys
import tempfile
project = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(project/'tools'))
from image_formats import digest, newc
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--stage0', type=Path, default=project/'out/fds-stage0')
parser.add_argument('--output-directory', type=Path)
args = parser.parse_args()
payloads = {'fds-stage0': args.stage0.resolve(strict=True), 'dasungd': project/'out/dasungd'}
for tool in ('lz4', 'zstd'):
if not shutil.which(tool): sys.exit(f'ERROR: missing host {tool}; see docs/m4-work.md')
for executable in ('fds-stage0', 'dasungd'):
subprocess.run([str(project/'tools/verify-elf'), str(payloads[executable]), 'aarch64', 'static'], check=True)
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
work = args.output_directory or Path(tempfile.mkdtemp(prefix='initramfs-build.', dir=project/'out'))
if not work.is_dir() or any(work.iterdir()): parser.error('output directory must exist and be empty')
entries = []
for name in ['dev', 'proc', 'sys', 'newroot', 'sbin', 'etc', 'usr', 'usr/lib', 'usr/lib/firmware', 'usr/lib/firmware/edid']:
entries.append((name, stat.S_IFDIR | 0o755, b'', 0, 0))
entries.append(('dev/console', stat.S_IFCHR | 0o600, b'', 5, 1))
entries.append(('init', stat.S_IFLNK | 0o777, b'sbin/fds-stage0', 0, 0))
entries.append(('lib', stat.S_IFLNK | 0o777, b'usr/lib', 0, 0))
for executable in ('fds-stage0', 'dasungd'):
entries.append(('sbin/'+executable, stat.S_IFREG | 0o755, payloads[executable].read_bytes(), 0, 0))
config = (project/'packages/fds-dasungd/files/dasungd.toml').read_text().replace('/run/dasungd/', '/dev/fds-early/')
entries.append(('etc/dasungd-early.toml', stat.S_IFREG | 0o600, config.encode(), 0, 0))
entries.append(('usr/lib/firmware/edid/dasung-paperlike13k-37hz.bin', stat.S_IFREG | 0o644,
(project/'rust/dasungd/profiles/paperlike13k-37hz.edid').read_bytes(), 0, 0))
archive = newc(sorted(entries), epoch)
(work/'initramfs.cpio').write_bytes(archive)
(work/'initramfs.cpio.gz').write_bytes(gzip.compress(archive, compresslevel=9, mtime=0))
for tool, options, suffix in [('lz4', ['-q', '-l', '-9', '-c'], 'lz4'), ('zstd', ['-q', '-T1', '-19', '-c'], 'zst')]:
with (work/f'initramfs.cpio.{suffix}').open('wb') as stream:
subprocess.run([tool, *options], input=archive, stdout=stream, check=True)
# All variants must decode to the exact same cpio stream.
assert gzip.decompress((work/'initramfs.cpio.gz').read_bytes()) == archive
for tool, suffix in [('lz4', 'lz4'), ('zstd', 'zst')]:
assert subprocess.check_output([tool, '-q', '-d', '-c', str(work/f'initramfs.cpio.{suffix}')]) == archive
manifest = {path.name: {'bytes': path.stat().st_size, 'sha256': digest(path)} for path in sorted(work.glob('initramfs.cpio*'))}
(work/'formats.json').write_text(json.dumps(manifest, indent=2)+'\n')
for target, source in ([] if args.output_directory else [('initramfs', work.name), ('fds-initramfs.img', work.name+'/initramfs.cpio')]):
temporary = project/'out'/(target+'.next')
temporary.symlink_to(source)
temporary.replace(project/'out'/target)
print('PASS: uncompressed, gzip, legacy-LZ4 and Zstandard initramfs variants agree')
print(f'Initramfs artifacts: {work}')
print('SKIP: compression boot-time comparison requires the physical Pi')
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Create a complete internal NVMe GPT image as a new ordinary file."""
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import uuid
project = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(project / 'tools'))
from image_formats import digest, gpt, NAMESPACE, EFI_SYSTEM, LINUX_FILESYSTEM
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--boot', type=Path, default=project / 'out/fds-boot.img')
parser.add_argument('--recovery', type=Path, default=project / 'out/fds-recovery.img')
parser.add_argument('--machine-config', type=Path, default=project / 'config/machine')
parser.add_argument('--internal-mib', type=int, default=256)
parser.add_argument('--recovery-mib', type=int, choices=[1024, 2048], default=1024)
parser.add_argument('--output-directory', type=Path)
args = parser.parse_args()
if not 32 <= args.internal_mib <= 65536:
parser.error('internal settings partition must be 32..65536 MiB')
boot, recovery = args.boot.resolve(strict=True), args.recovery.resolve(strict=True)
if not boot.is_file() or boot.stat().st_size != 512 * 1024 * 1024:
parser.error('boot input must be a 512 MiB regular FAT32 partition image')
if not recovery.is_file() or not 4096 <= recovery.stat().st_size <= args.recovery_mib * 1024 * 1024:
parser.error('recovery input must be a regular EROFS image fitting its partition')
with boot.open('rb') as stream:
header = stream.read(512)
if header[82:90] != b'FAT32 ' or header[71:82] != b'FDS_BOOT ' or header[510:] != b'\x55\xaa':
parser.error('boot input is not FDS_BOOT FAT32')
with recovery.open('rb') as stream:
stream.seek(1024)
header = stream.read(128)
if header[:4] != bytes.fromhex('e2e1f5e0') or header[64:80].rstrip(b'\0') != b'FDS_RECOVERY':
parser.error('recovery input is not FDS_RECOVERY EROFS')
work = args.output_directory or Path(tempfile.mkdtemp(prefix='internal-build.', dir=project / 'out'))
work = work.resolve(strict=True)
if not work.is_dir() or any(work.iterdir()):
parser.error('output directory must exist and be empty')
subprocess.run([str(project / 'tools/cargo-build'), '--locked', '--offline', '--release', '--target', 'x86_64-unknown-linux-gnu', '-p', 'fds-cli'], cwd=project, check=True)
host_cli = project / 'target/x86_64-unknown-linux-gnu/release/fds'
tree = work / 'settings'
(tree / 'config').mkdir(parents=True)
(tree / 'diagnostics').mkdir(mode=0o700)
subprocess.run([str(host_cli), 'machine', 'pack', str(args.machine_config.resolve(strict=True)), str(tree / 'config/machine.json')], check=True)
epoch = int(subprocess.check_output(['git', '-C', str(project / 'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
for path in [tree, *sorted(tree.rglob('*'))]:
os.utime(path, (epoch, epoch))
runner = str(project / 'tools/in-image-tools')
subprocess.run([str(project / 'tools/prepare-image-tools')], check=True)
subprocess.run([runner, 'fsck.fat', '-n', str(boot)], check=True)
subprocess.run([runner, 'fsck.erofs', '--extract', str(recovery)], check=True)
settings = work / 'internal.ext4'
with settings.open('xb') as stream:
stream.truncate(args.internal_mib * 1024 * 1024)
identity = uuid.uuid5(NAMESPACE, 'internal:' + digest(tree / 'config/machine.json'))
# Source ownership is root inside this unprivileged user namespace, independent
# of the workstation UID. No workstation block devices are exposed.
environment = {**os.environ, 'E2FSPROGS_FAKE_TIME': str(epoch)}
subprocess.run(['bwrap', '--unshare-user', '--uid', '0', '--gid', '0', '--ro-bind', '/', '/',
'--bind', str(work), str(work), '--dev', '/dev', '--proc', '/proc',
runner, 'mke2fs', '-q', '-t', 'ext4', '-F', '-b', '4096', '-m', '0',
'-L', 'FDS_INTERNAL', '-U', str(identity),
'-E', f'root_owner=0:0,lazy_itable_init=0,lazy_journal_init=0,hash_seed={identity}',
'-d', str(tree), str(settings)], env=environment, check=True)
# mke2fs copies source ctimes; normalize every populated inode after construction.
commands = work / 'normalize.debugfs'
commands.write_text(''.join(f'set_inode_field {name} {field} @{epoch}\n'
for name in ['/', '/lost+found', '/config', '/config/machine.json', '/diagnostics']
for field in ['atime', 'ctime', 'mtime', 'crtime']))
subprocess.run([runner, 'debugfs', '-w', '-f', str(commands), str(settings)], env=environment, check=True)
subprocess.run([runner, 'e2fsck', '-f', '-n', str(settings)], env=environment, check=True)
padded = work / 'recovery-partition.erofs'
shutil.copyfile(recovery, padded)
with padded.open('r+b') as stream:
stream.truncate(args.recovery_mib * 1024 * 1024)
image = work / 'internal.img'
layout = gpt(image, [('FDS_BOOT', EFI_SYSTEM, boot), ('FDS_RECOVERY', LINUX_FILESYSTEM, padded), ('FDS_INTERNAL', LINUX_FILESYSTEM, settings)])
layout.update(format=1, image_sha256=digest(image), machine_settings_sha256=digest(tree / 'config/machine.json'),
recovery_payload_sha256=digest(recovery), boot_payload_sha256=digest(boot))
observed = json.loads(subprocess.check_output(['sfdisk', '--json', str(image)]))['partitiontable']
assert observed['label'] == 'gpt' and len(observed['partitions']) == 3
for expected, actual in zip(layout['partitions'], observed['partitions']):
assert (actual['name'], actual['start'], actual['size']) == (expected['name'], expected['start'], expected['size'])
subprocess.run(['sfdisk', '--verify', str(image)], check=True)
(work / 'layout.json').write_text(json.dumps(layout, indent=2) + '\n')
if args.output_directory is None:
link = project / 'out/fds-internal.img.next'
link.symlink_to(work.name + '/internal.img')
link.replace(project / 'out/fds-internal.img')
print(f'PASS: complete internal NVMe image, three verified GPT payloads: {work}')
print('SKIP: no physical disk was written; Pi firmware/NVMe boot requires hardware')
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Build an independent, read-only recovery filesystem from its own rootfs."""
import argparse
import json
from pathlib import Path
import subprocess
import sys
import tarfile
import tempfile
import uuid
project = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(project / 'tools'))
from image_formats import digest, NAMESPACE
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--rootfs', type=Path, default=project / 'out/rootfs-recovery.tar')
parser.add_argument('--output-directory', type=Path)
args = parser.parse_args()
rootfs = args.rootfs.resolve(strict=True)
with tarfile.open(rootfs) as archive:
profile = archive.extractfile('usr/share/fds/image-profile').read(64).decode().strip()
try:
root = archive.getmember('.')
except KeyError:
parser.error('rootfs omits root directory metadata; rebuild the rootfs')
if not root.isdir() or (root.uid, root.gid, root.mode) != (0, 0, 0o755):
parser.error('rootfs must explicitly record / as root:root mode 0755; rebuild the rootfs')
if profile != 'recovery':
parser.error('input must be a separately built recovery rootfs')
work = args.output_directory or Path(tempfile.mkdtemp(prefix='recovery-build.', dir=project / 'out'))
if not work.is_dir() or any(work.iterdir()):
parser.error('output directory must exist and be empty')
epoch = int(subprocess.check_output(['git', '-C', str(project / 'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
identity = uuid.uuid5(NAMESPACE, 'recovery:' + digest(rootfs))
runner = str(project / 'tools/in-image-tools')
image = work / 'recovery.erofs'
subprocess.run([runner, 'mkfs.erofs', '-T', str(epoch), '-U', str(identity), '-b4096',
'-L', 'FDS_RECOVERY', '--tar=f', str(image), str(rootfs)], check=True)
subprocess.run([runner, 'fsck.erofs', '--extract', str(image)], check=True)
(work / 'manifest.json').write_text(json.dumps({
'format': 1, 'profile': profile, 'rootfs_sha256': digest(rootfs),
'image_sha256': digest(image), 'bytes': image.stat().st_size,
'partition_name': 'FDS_RECOVERY', 'filesystem': 'erofs',
}, indent=2) + '\n')
if args.output_directory is None:
target = project / 'out/fds-recovery.img.next'
target.symlink_to(work.name + '/recovery.erofs')
target.replace(project / 'out/fds-recovery.img')
print(f'PASS: independent recovery EROFS (partition payload): {work}')
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Create read-only EROFS and a GPT SYSTEM image from an exported rootfs."""
import argparse
import io
import json
from pathlib import Path
import subprocess
import sys
import tarfile
import tempfile
import uuid
project = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(project/'tools'))
from image_formats import digest, gpt, LINUX_FILESYSTEM, NAMESPACE
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--rootfs', type=Path, default=project/'out/rootfs-aarch64.tar')
parser.add_argument('--profile', choices=['cli', 'development'], default=None)
parser.add_argument('--output-directory', type=Path)
args = parser.parse_args()
rootfs = args.rootfs.resolve(strict=True)
with tarfile.open(rootfs) as archive:
embedded = archive.extractfile('usr/share/fds/image-profile').read(64).decode().strip()
try:
root = archive.getmember('.')
except KeyError:
parser.error('rootfs omits root directory metadata; rebuild the rootfs')
if not root.isdir() or (root.uid, root.gid, root.mode) != (0, 0, 0o755):
parser.error('rootfs must explicitly record / as root:root mode 0755; rebuild the rootfs')
if embedded not in ('cli', 'development'): parser.error('unsupported rootfs profile')
if args.profile is not None and args.profile != embedded: parser.error('requested profile does not match rootfs contents')
args.profile = embedded
work = args.output_directory or Path(tempfile.mkdtemp(prefix='system-build.', dir=project/'out'))
if not work.is_dir() or any(work.iterdir()): parser.error('output directory must exist and be empty')
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
manifest = f'''format = 1\n[cartridge]\nid = "fds.system.{args.profile}"\nname = "FDS/OS {args.profile.upper()}"\nclass = "system"\nversion = "0.1.0"\n[media]\nwritable = false\n'''.encode()
with tarfile.open(rootfs) as source, tarfile.open(work/'system.tar', 'w', format=tarfile.PAX_FORMAT) as output:
for member in source:
if member.name in ('FDS', 'FDS/CARTRIDGE.TOML'): continue
output.addfile(member, source.extractfile(member) if member.isfile() else None)
directory = tarfile.TarInfo('FDS')
directory.type = tarfile.DIRTYPE
directory.mode = 0o755
directory.mtime = epoch
output.addfile(directory)
info = tarfile.TarInfo('FDS/CARTRIDGE.TOML')
info.mode = 0o644
info.mtime = epoch
info.size = len(manifest)
output.addfile(info, io.BytesIO(manifest))
image_uuid = uuid.uuid5(NAMESPACE, digest(work/'system.tar'))
subprocess.run([str(project/'tools/in-image-tools'), 'mkfs.erofs', '-T', str(epoch), '-U', str(image_uuid),
'-b4096', '-L', 'FDS_SYSTEM', '--tar=f', str(work/'system.erofs'), str(work/'system.tar')], check=True)
subprocess.run([str(project/'tools/in-image-tools'), 'fsck.erofs', '--extract', str(work/'system.erofs')], check=True)
layout = gpt(work/'system.img', [('FDS_SYSTEM', LINUX_FILESYSTEM, work/'system.erofs')])
layout['rootfs_sha256'] = digest(rootfs)
layout['image_sha256'] = digest(work/'system.img')
layout['profile'] = args.profile
(work/'layout.json').write_text(json.dumps(layout, indent=2)+'\n')
# An independent implementation checks the actual on-disk partition table.
observed = json.loads(subprocess.check_output(['sfdisk', '--json', str(work/'system.img')]))['partitiontable']
assert observed['label'] == 'gpt' and observed['partitions'][0]['name'] == 'FDS_SYSTEM'
assert observed['partitions'][0]['start'] == layout['partitions'][0]['start']
if args.output_directory is None:
target = project/'out'/f'fds-system-{args.profile}.img'
temporary = target.with_suffix('.img.next')
temporary.symlink_to(work.name+'/system.img')
temporary.replace(target)
print(f'PASS: GPT FDS_SYSTEM and verified EROFS: {work}')
+6
View File
@@ -0,0 +1,6 @@
# Mandatory display requirements, checked by tools/build-kernel after packaging.
# The monitor's companion SPI bridge must not be probed before the daemon claims it.
# CONFIG_SPI_CH341 is not set
CONFIG_USB=y
CONFIG_USB_SUPPORT=y
CONFIG_USB_XHCI_HCD=y
+1
View File
@@ -0,0 +1 @@
console=tty1 console=ttyAMA10,115200 rdinit=/init ro loglevel=7 drm.edid_firmware=edid/dasung-paperlike13k-37hz.bin fds.boot=normal fds.debug=1
+1
View File
@@ -0,0 +1 @@
console=tty1 rdinit=/init ro quiet loglevel=3 vt.global_cursor_default=0 drm.edid_firmware=edid/dasung-paperlike13k-37hz.bin fds.boot=normal
+12
View File
@@ -0,0 +1,12 @@
# FDS/OS Raspberry Pi 5: internal NVMe supplies boot files.
[pi5]
arm_64bit=1
kernel=kernel_2712.img
initramfs fds-initramfs.img followkernel
dtparam=pciex1
dtoverlay=vc4-kms-v3d,noaudio
camera_auto_detect=0
display_auto_detect=0
disable_splash=1
enable_uart=1
[all]