Files
Arch-R/scripts/archr-dtbo.py
Douglas Teles 06dc2d16f3 Add system scripts, services, and boot splash
System daemons: hotkeys (volume/brightness), automount, bluetooth agent,
memory manager, sleep/suspend, USB gadget mode, save-config persistence.

Boot splash: initramfs SVG renderer (fbsplash + svg_parser) for 0.7s splash.

Panel tools: generate-panel-dtbos.sh rewrite, convert-panel.py for ROCKNIX
panel data extraction, archr-dtbo.py for runtime overlay management.

Input: archr-gptokeyb.c gamepad-to-keyboard mapper via uinput.

Launch wrappers: emulationstation.sh and retroarch-launch.sh updated for
KMS/DRM + Mesa 26 Panfrost environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:22:30 -03:00

522 lines
21 KiB
Python
Executable File

#!/usr/bin/env python
# https://pypi.org/project/fdt/
# pip install fdt
import os, sys
import fdt
import math
def prop_default(panel, prop, default):
try:
return panel.get_property(prop).value
except:
return default
def absfrac(x):
return abs(x - round(x))
def panel_to_desc(panel, args):
if 'name' in args:
g_name = args['name'] + ' '
else:
g_name = ''
comment = args.get('comment')
acc = []
delays = [
prop_default(panel, "prepare-delay-ms", 50),
prop_default(panel, "reset-delay-ms", 50),
prop_default(panel, "init-delay-ms", 50),
prop_default(panel, "enable-delay-ms", 50),
20 # ready -- no such timeout in legacy dtbs
]
delays_str = ','.join(map(str, delays))
fmtprop = panel.get_property("dsi,format")
if fmtprop:
fmt = ['rgb888', 'rgb666', 'rgb666_packed', 'rgb565'] [fmtprop.value]
else:
fmt = 'rgb888'
lanes = panel.get_property("dsi,lanes").value
flags = panel.get_property("dsi,flags").value
flags |= 0x0400
timings = panel.get_subnode("display-timings")
if 'Dno' in args['flags']:
# Skip original mode (it may be broken)
native = None
else:
native = timings.get_property("native-mode").value
# Collect vendor modes
modes = {}
orig_def_fps = None
for m in timings.nodes:
clock = round(m.get_property("clock-frequency").value/1000)
hor = [
m.get_property("hactive").value,
m.get_property("hfront-porch").value,
m.get_property("hsync-len").value,
m.get_property("hback-porch").value,
]
ver = [
m.get_property("vactive").value,
m.get_property("vfront-porch").value,
m.get_property("vsync-len").value,
m.get_property("vback-porch").value,
]
mode = {'clock': clock, 'hor': hor, 'ver': ver}
if (m.get_property("phandle").value == native):
mode['default'] = True
htotal = sum(hor)
vtotal = sum(ver)
fps = clock*1000/(htotal*vtotal)
if fps not in modes:
modes[fps] = mode
if (m.get_property("phandle").value == native):
modes[fps]['default'] = True
orig_def_fps = fps
try:
w = panel.get_property("width-mm").value
h = panel.get_property("height-mm").value
except:
if args.diagonal:
diag_mm = args.diagonal * 25.4
else:
diag_mm = 3.5 * 2.54
randmode = list(modes.values())[0]
hactive = randmode['hor'][0]
vactive = randmode['ver'][0]
pxdiag = math.sqrt(hactive*hactive + vactive*vactive)
w = round(diag_mm * hactive/pxdiag)
h = round(diag_mm * vactive/pxdiag)
# G size=52,70 delays=2,1,20,120,50,20 format=rgb888 lanes=4 flags=0xe03
acc += [f"G {g_name}size={w},{h} delays={delays_str} format={fmt} lanes={lanes} flags=0x{flags:x}", ""]
# Based on vendor modes construct a better set of modes
# https://tasvideos.org/PlatformFramerates
# 50, 60 -- generic
# */1.001 -- NTSC hack with 1001 divisor
# 50.0070 -- PAL NES https://www.nesdev.org/wiki/Cycle_reference_chart
# 60.0988 -- NTSC NES
# 54.8766 -- src/mame/toaplan/twincobr.cpp
# 57.5 -- src/mame/kaneko/snowbros.cpp
# 59.7275 -- https://en.wikipedia.org/wiki/Game_Boy
# 75.47 -- https://ws.nesdev.org/wiki/Display
def_fps = 60
if orig_def_fps:
def_fps = orig_def_fps
common_fpss = [50/1.001, 50, 50.0070, 57.5, 59.7275, 60/1.001, 60, 60.0988, 75.47, 90, 120];
common_fpss = [ fps for fps in common_fpss if fps != orig_def_fps]
for targetfps in [orig_def_fps] + common_fpss:
if not targetfps:
continue
warn = ""
# nearest fps to base on
greaterfps = [fps for fps in modes.keys() if fps >= targetfps]
if greaterfps == []:
basefps = max(modes.keys())
basemode = modes[basefps]
clock = None
else:
# Trust original clock. If real clock differs, maybe make a whitelist or blacklist here
basefps = min(greaterfps)
basemode = modes[basefps]
clock = basemode['clock']
hor = basemode['hor'].copy()
ver = basemode['ver'].copy()
# Assume original totals are minimal for the panel at this clock
htotal = sum(hor)
vtotal = sum(ver)
perfectclock = targetfps*htotal*vtotal/1000
if not clock:
warn = "(CAN FAIL) "
# This may fail, but worth trying. Round up to 10kHz
clock = math.ceil(perfectclock/10)*10
elif clock > 1.25*perfectclock:
# Too much deviation may cause no image
clock = math.ceil(perfectclock/10)*10
maxvtotal = round(vtotal*1.25)
# A little bruteforce to find a best totals for target fps
# TODO: maybe iterate over some clock values too
options = [(absfrac(c*1000/targetfps/vt), c, vt)
for vt in range(vtotal, maxvtotal+1)
for c in range(clock, round(1.25*perfectclock), 10)
if ((c*1000/targetfps/vt) >= htotal) and ((c*1000/targetfps/vt) < htotal*1.05) ]
if options == []:
acc += [f"# failed to find mode for fps={targetfps:.6f} c={clock} h={htotal} v={vtotal}"]
continue
(mindev, newclock, newvtotal) = min(options)
# construct a new mode with chosen vtotal
newhtotal = round(newclock*1000/targetfps/newvtotal)
addhtotal = newhtotal - htotal
addvtotal = newvtotal - vtotal
expectedfps = newclock*1000/newvtotal/newhtotal
hor[2] += addhtotal
ver[2] += addvtotal
hor_str = ','.join(map(str, hor))
ver_str = ','.join(map(str, ver))
maybe_default = " default=1" if targetfps == def_fps else ""
maybe_comment = f" # {warn}fps={expectedfps:.6f} (target={targetfps:.6f})" if comment else ""
acc += [f"M clock={newclock} horizontal={hor_str} vertical={ver_str}{maybe_default}{maybe_comment}"]
acc += [""]
iseq0 = panel.get_property("panel-init-sequence")
if (hasattr(iseq0, 'value')) and (isinstance(iseq0.value, (int))):
iseq = b''.join(map(lambda w : w.to_bytes(4, "big"), list(iseq0)))
else:
iseq = bytearray(iseq0)
while iseq:
cmd = iseq[0]
wait = iseq[1]
datalen = iseq[2]
iseq = iseq[3:]
data = iseq[0:datalen]
iseq = iseq[datalen:]
maybe_wait = f" wait={wait}" if (wait) else ""
maybe_comment = f" # orig_cmd=0x{cmd:x}" if comment else ""
acc += [f"I seq={data.hex()}{maybe_wait}{maybe_comment}"]
return acc
def node_by_phandle(dt, phandle):
nodes = [p.parent
for p in dt.search('phandle', fdt.ItemType.PROP_WORDS)
if p.value == phandle]
return nodes[0]
def resolve_phandle(dt, phandle):
p = node_by_phandle(dt, phandle)
return os.path.join(p.path, p.name)
def add_overlay(overlay, path):
for f in range(100):
nodename = 'fragment@' + str(f)
if overlay.exist_node(nodename):
pass
else:
fragnode = fdt.Node(nodename)
if path[0] == '&':
fragnode.set_property('target', 0xffffffff)
overlay.set_property(path[1:], '/'+nodename+':target:0', path='/__fixups__')
else:
fragnode.set_property('target-path', path)
overlay.add_item(fragnode)
ovlnode = fdt.Node('__overlay__')
fragnode.append(ovlnode)
return ovlnode
def add_fixup(overlay, label, fixup_path):
if overlay.exist_node('__fixups__'):
fixups = overlay.get_node('__fixups__')
else:
fixups = fdt.Node('__fixups__')
overlay.add_item(fixups)
prev_paths = fixups.get_property(label)
if prev_paths:
fixups.set_property(label, prev_paths.data + [fixup_path])
else:
fixups.set_property(label, [fixup_path])
def add_local_fixup(overlay, parent_path, name):
overlay.set_property(name, 0, path='__local_fixups__'+parent_path)
def add_gpio_vol_keys(dt, overlay, gpio_keys_ovl):
symbols = dt.get_node('__symbols__')
keydata = dt.get_node('/play_joystick').get_property('key-gpios').data
pindata = dt.get_node('/pinctrl/buttons/gpio-key-pin').get_property('rockchip,pins').data
# extract volume keys from array
vol_up = keydata[14*3:15*3]
vol_up_pin = pindata[14*4:15*4]
vol_dn = keydata[15*3:16*3]
vol_dn_pin = pindata[15*4:16*4]
gpio_path = resolve_phandle(dt, vol_up[0])
gpio_sym = [p.name for p in symbols.props if p.value == gpio_path][0]
gpio_phandle = 0xffffffff
pins_path = gpio_keys_ovl.path+'/__overlay__/pinctrl/btns/btn-pins-vol-overlay'
keys_path = gpio_keys_ovl.path+'/__overlay__/gpio-keys-overlay'
pullup = 0xffffffff
overlay.set_property('rockchip,pins', vol_up_pin[0:3] + [pullup] + vol_dn_pin[0:3] + [pullup], path=pins_path)
pins_phandle = overlay.add_label('overlay_btns')
overlay.set_property('phandle', pins_phandle, path=pins_path)
add_fixup(overlay, 'pcfg_pull_up', pins_path+':rockchip,pins:12')
add_fixup(overlay, 'pcfg_pull_up', pins_path+':rockchip,pins:28')
overlay.set_property('compatible', 'gpio-keys', path=keys_path)
overlay.set_property('autorepeat', None, path=keys_path)
overlay.set_property('pinctrl-0', pins_phandle, path=keys_path)
add_local_fixup(overlay, keys_path, 'pinctrl-0')
overlay.set_property('pinctrl-names', 'default', path=keys_path)
overlay.set_property('gpios', [gpio_phandle, vol_up[1], vol_up[2]], path=keys_path+'/button-vol-up')
overlay.set_property('label', "VOLUMEUP", path=keys_path+'/button-vol-up')
overlay.set_property('linux,code', 0x73, path=keys_path+'/button-vol-up')
add_fixup(overlay, gpio_sym, keys_path+'/button-vol-up:gpios:0')
overlay.set_property('gpios', [gpio_phandle, vol_dn[1], vol_dn[2]], path=keys_path+'/button-vol-down')
overlay.set_property('label', "VOLUMEDOWN", path=keys_path+'/button-vol-down')
overlay.set_property('linux,code', 0x72, path=keys_path+'/button-vol-down')
add_fixup(overlay, gpio_sym, keys_path+'/button-vol-down:gpios:0')
def switch_joypad_to_mymini(overlay, jp_ovl):
# My mini has separate ADC channels for axis, so we need another driver
jp_ovl.set_property('compatible', "rocknix-joypad")
jp_ovl.set_property('io-channel-names', ["key-RY", "key-RX", "key-LY", "key-LX"])
jp_ovl.set_property('io-channels', [0xffffffff, 3, 0xffffffff, 3, 0xffffffff, 2, 0xffffffff, 1])
add_fixup(overlay, 'saradc', jp_ovl.path+'/__overlay__:io-channels:0')
add_fixup(overlay, 'saradc', jp_ovl.path+'/__overlay__:io-channels:8')
add_fixup(overlay, 'saradc', jp_ovl.path+'/__overlay__:io-channels:16')
add_fixup(overlay, 'saradc', jp_ovl.path+'/__overlay__:io-channels:24')
jp_ovl.set_property('button-adc-scale', 2)
jp_ovl.set_property('button-adc-deadzone', 216)
jp_ovl.set_property('button-adc-fuzz', 54)
jp_ovl.set_property('button-adc-flat', 54)
jp_ovl.set_property('abs_x-p-tuning', 180)
jp_ovl.set_property('abs_x-n-tuning', 180)
jp_ovl.set_property('abs_y-p-tuning', 180)
jp_ovl.set_property('abs_y-n-tuning', 180)
jp_ovl.set_property('abs_rx-p-tuning', 0)
jp_ovl.set_property('abs_rx-n-tuning', 0)
jp_ovl.set_property('abs_ry-p-tuning', 0)
jp_ovl.set_property('abs_ry-n-tuning', 0)
jp_ovl.set_property('poll-interval', 10)
def make_dtbo(dtb_data, args):
dt = fdt.parse_dtb(dtb_data)
symbols = dt.get_node('__symbols__')
dsipath = symbols.get_property('dsi').value
# panelpath is /dsi@ff450000/panel@0 on rk3326 and /dsi@fe060000/panel@0 on rk3566
panelpath = dsipath + '/panel@0'
panel = dt.get_node(panelpath)
pdesc = panel_to_desc(panel, args)
# remove empty lines as pyfdt does not like them
pdesc = [ l for l in pdesc if l != '']
panel_rst_gpio = panel.get_property('reset-gpios').data
gpio_sym = [p.name for p in symbols.props if p.value == resolve_phandle(dt, panel_rst_gpio[0])][0]
gpio_num = int(gpio_sym[4:])
# create an overlay tree
overlay = fdt.FDT()
overlay.header.version = 17
panel_ovl = add_overlay(overlay, '/')
panel_ovl_path = panel_ovl.path+'/__overlay__'+panelpath
overlay.set_property('compatible', 'archr,generic-dsi', path=panel_ovl_path)
overlay.set_property('panel_description', pdesc, path=panel_ovl_path)
if 'DR90' in args['flags']:
overlay.set_property('rotation', 90, path=panel_ovl_path)
elif 'DR180' in args['flags']:
overlay.set_property('rotation', 180, path=panel_ovl_path)
elif 'DR270' in args['flags']:
overlay.set_property('rotation', 270, path=panel_ovl_path)
compat = dt.get_node('/').get_property('compatible').data[0]
args['logger'].info(f"compatible {compat}")
if 'odroidgo3' in compat:
# quick return for well supported R36s
return overlay.to_dtb()
# copy reset config
pins_path = panel_ovl.path+'/__overlay__/pinctrl/gpio-lcd/lcd-rst'
overlay.set_property('reset-gpios', [0xffffffff, panel_rst_gpio[1], panel_rst_gpio[2]], path=panel_ovl_path)
add_fixup(overlay, gpio_sym, panel_ovl_path+':reset-gpios:0')
overlay.set_property('rockchip,pins', [gpio_num, panel_rst_gpio[1], 0, 0xffffffff], path=pins_path)
add_fixup(overlay, 'pcfg_pull_none', pins_path+':rockchip,pins:12')
# power supply gpio fetch (some trees do not have power-supply prop)
try:
panel_ps = dt.get_node(resolve_phandle(dt, panel.get_property('power-supply').value))
panel_ps_gpio = panel_ps.get_property('gpio').data
gpio_sym = [p.name for p in symbols.props if p.value == resolve_phandle(dt, panel_ps_gpio[0])][0]
gpio_num = int(gpio_sym[4:])
# power supply reg
panel_reg_path = panel_ovl.path+'/__overlay__/vcc18-lcd0'
overlay.set_property('gpio', [0xffffffff, panel_ps_gpio[1], panel_ps_gpio[2]], path=panel_reg_path)
add_fixup(overlay, gpio_sym, panel_reg_path+':gpio:0')
# power supply pinctrl
panel_ps_pin_path = panel_ovl.path+'/__overlay__/pinctrl/vcc18-lcd/vcc18-lcd-n'
overlay.set_property('rockchip,pins', [gpio_num, panel_ps_gpio[1], 0, 0xffffffff], path=panel_ps_pin_path)
add_fixup(overlay, 'pcfg_pull_none', panel_ps_pin_path+':rockchip,pins:12')
except:
pass
# some devices (e.g. R36s clone) have enable-gpios defined
try:
panel_en_gpio = panel.get_property('enable-gpios').data
gpio_sym = [p.name for p in symbols.props if p.value == resolve_phandle(dt, panel_en_gpio[0])][0]
overlay.set_property('enable-gpios', [0xffffffff, panel_en_gpio[1], panel_en_gpio[2]], path=panel_ovl_path)
add_fixup(overlay, gpio_sym, panel_ovl_path+':enable-gpios:0')
except:
pass
# If stock DTB does not have ADC keys, disable adc-keys in overlay
need_adckeys_disable = False
if not dt.exist_node('/adc-keys'):
need_adckeys_disable = True
else:
adckeys_orig = dt.get_node('/adc-keys')
adckeys_status = adckeys_orig.get_property('status')
if (adckeys_status) and (adckeys_status.value == 'disabled'):
need_adckeys_disable = True
else:
# usually we just don't have status property, so consider this valid
need_adckeys_disable = False
if need_adckeys_disable:
noadck_ovl = add_overlay(overlay, '/')
overlay.set_property('dtbo_comment', 'deliberately-disabled-adc-keys', path=noadck_ovl.path+'/__overlay__/adc-keys')
overlay.set_property('status', 'disabled', path=noadck_ovl.path+'/__overlay__/adc-keys')
args['logger'].info(f"disabled adc-keys")
# TODO: extract GPIO keys from play_joystick.key-gpios[14..15]
if dt.exist_node('/play_joystick'):
add_gpio_vol_keys(dt, overlay, noadck_ovl)
else:
adck_ovl = add_overlay(overlay, '/')
overlay.set_property('status', 'okay', path=adck_ovl.path+'/__overlay__/adc-keys')
# joypad overlay always present to simplify different options
jp_ovl = add_overlay(overlay, '&joypad')
# If not explicitly specified, disabled adc-keys imply multi-adc MyMini-style joypad
if 'JPk36' in args['flags']:
pass
elif ('JPmm' in args['flags']) or (need_adckeys_disable):
args['logger'].info(f"My Mini joypad tweaks on {jp_ovl.path}")
switch_joypad_to_mymini(overlay, jp_ovl)
# Left stick was inverted by default, so invert inversion here
if ('LSi' not in args['flags']) ^ (need_adckeys_disable):
jp_ovl.set_property('invert-absx', 1)
jp_ovl.set_property('invert-absy', 1)
args['logger'].info(f"left stick un-inverted on {jp_ovl.path}")
else:
args['logger'].info(f"left stick left inverted")
# If needed, invert right stick
if 'RSi' in args['flags']:
jp_ovl.set_property('invert-absrx', 1)
jp_ovl.set_property('invert-absry', 1)
args['logger'].info(f"invert right stick on {jp_ovl.path}")
try:
snd = dt.get_node('/rk817-sound')
# fetch raw hp-det-gpio = <0x6f 0x16 0x00>;
hpdet = snd.get_property('hp-det-gpio').data
hp_det = dt.get_node('/pinctrl/headphone/hp-det')
hp_det_pins = hp_det.get_property('rockchip,pins')
hp_det_pull_node = node_by_phandle(dt, hp_det_pins[3])
# resolve <0x6f> into '/pinctrl/gpio2@ff260000'
hpdet_gpio_path = resolve_phandle(dt, hpdet[0])
# find symbol 'gpio2' for path '/pinctrl/gpio2@ff260000'
gpiosyms = [p.name for p in symbols.props if p.value == hpdet_gpio_path]
gpio_sym = gpiosyms[0]
# on success, add overlay
hpdet_ovl = add_overlay(overlay, '/')
force_simple_audio_routing = ('SRs' in args['flags'])
# Determine preset, configure amplifier if needed
if snd.exist_property('spk-con-gpio') and not force_simple_audio_routing:
rk817_path = hpdet_ovl.path+'/__overlay__/rk817-sound-amplified'
amp_gpio = snd.get_property('spk-con-gpio').data
amp_gpio_sym = [p.name for p in symbols.props if p.value == resolve_phandle(dt, amp_gpio[0])][0]
gpio_num = int(amp_gpio_sym[4:])
args['logger'].info(f"spk-con-gpio {amp_gpio_sym} {amp_gpio[1]} on {hpdet_ovl.path}")
amp_path = hpdet_ovl.path+'/__overlay__/audio-amplifier'
overlay.set_property('enable-gpios', [0xffffffff, amp_gpio[1], amp_gpio[2]], path=amp_path)
add_fixup(overlay, amp_gpio_sym, amp_path+':enable-gpios:0')
amp_pc_path = hpdet_ovl.path+'/__overlay__/pinctrl/speaker/spk-amp-enable-h'
overlay.set_property('rockchip,pins', [gpio_num, amp_gpio[1], 0, 0xffffffff], path=amp_pc_path)
add_fixup(overlay, 'pcfg_pull_none', amp_pc_path+':rockchip,pins:12')
else:
rk817_path = hpdet_ovl.path+'/__overlay__/rk817-sound-simple'
overlay.set_property('status', 'okay', path=rk817_path)
# for some reason hp detection polarity needs to be inverted on some devices
if 'HPi' in args['flags']:
if hpdet[2] == 0:
hpdet[2] = 1
else:
hpdet[2] = 0
overlay.set_property('simple-audio-card,hp-det-gpio', [0xffffffff, hpdet[1], hpdet[2]], path=rk817_path)
add_fixup(overlay, gpio_sym, rk817_path+':simple-audio-card,hp-det-gpio:0')
pins_path = hpdet_ovl.path+'/__overlay__/pinctrl/headphone/hp-det'
overlay.set_property('rockchip,pins', hp_det_pins[0:3] + [0xffffffff], path=pins_path)
# Restore bias reference
if hp_det_pull_node.exist_property('bias-pull-down'):
add_fixup(overlay, 'pcfg_pull_down', pins_path+':rockchip,pins:12')
else:
add_fixup(overlay, 'pcfg_pull_up', pins_path+':rockchip,pins:12')
args['logger'].info(f"hp-det-gpio {gpiosyms[0]} on {hpdet_ovl.path}")
except Exception as e:
args['logger'].info(e)
# Move fixups to the very end (if any)
if overlay.exist_node('__fixups__'):
fixups = overlay.get_node('__fixups__')
overlay.remove_node('__fixups__')
overlay.add_item(fixups)
if overlay.exist_node('__local_fixups__'):
fixups = overlay.get_node('__local_fixups__')
overlay.remove_node('__local_fixups__')
overlay.add_item(fixups)
# send the overlay to output
return overlay.to_dtb()
if __name__ == "__main__":
import argparse, logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("dtbo")
parser = argparse.ArgumentParser(description="Generate a dtbo from a stock dtb")
parser.add_argument(dest="src", help="input stock dtb path", metavar="/path/to/stock.dtb")
parser.add_argument(dest="opts", help="dtbo options", nargs='?', metavar="LSi-HPi", default="")
parser.add_argument("-o", "--output", help="output (dtbo) path")
args = parser.parse_args()
with open(args.src, 'rb') as f:
content = f.read()
flags = args.opts.split('-')
flags = [ f for f in flags if f != '']
dtbo = make_dtbo(content, {'flags': flags, 'logger': logger})
if args.output:
with open(args.output, 'wb') as f:
f.write(dtbo)
else:
sys.stdout.buffer.write(dtbo)