This commit is contained in:
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.kioskEdid;
|
||||||
|
|
||||||
|
overrideModule = types.submodule {
|
||||||
|
options = {
|
||||||
|
connector = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "HDMI-A-1";
|
||||||
|
description = ''
|
||||||
|
DRM connector name to apply the EDID override to.
|
||||||
|
Check `ls /sys/class/drm/` on target hardware to confirm the
|
||||||
|
exact name (e.g. HDMI-A-1, HDMI-A-2).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
edidFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = ''
|
||||||
|
Path to the binary EDID file to force for this connector.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
forceConnected = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Also force this connector to be treated as connected
|
||||||
|
(video=<connector>:e), in case the real link reports
|
||||||
|
disconnected/garbled state through a splitter or wireless kit.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.services.kioskEdid = {
|
||||||
|
enable = mkEnableOption "forcing custom EDIDs on one or more display connectors";
|
||||||
|
|
||||||
|
overrides = mkOption {
|
||||||
|
type = types.listOf overrideModule;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
List of per-connector EDID overrides to apply. Each entry gets its
|
||||||
|
own firmware file and its own drm.edid_firmware kernel param.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''
|
||||||
|
[
|
||||||
|
{ connector = "HDMI-A-1"; edidFile = ./android-4k30-edid.bin; }
|
||||||
|
{ connector = "HDMI-A-2"; edidFile = ./android-4k30-edid.bin; }
|
||||||
|
]
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
hardware.firmware = map
|
||||||
|
(o: pkgs.runCommand "kiosk-edid-firmware-${o.connector}" { } ''
|
||||||
|
mkdir -p $out/lib/firmware/edid
|
||||||
|
cp ${o.edidFile} $out/lib/firmware/edid/${o.connector}.bin
|
||||||
|
'')
|
||||||
|
cfg.overrides;
|
||||||
|
|
||||||
|
boot.kernelParams = flatten (map
|
||||||
|
(o:
|
||||||
|
[ "drm.edid_firmware=${o.connector}:edid/${o.connector}.bin" ]
|
||||||
|
++ optional o.forceConnected "video=${o.connector}:e"
|
||||||
|
)
|
||||||
|
cfg.overrides);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -216,7 +216,18 @@
|
|||||||
hostName = "radiantwavekiosk-android";
|
hostName = "radiantwavekiosk-android";
|
||||||
diskModule = ./disk-config.nix;
|
diskModule = ./disk-config.nix;
|
||||||
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-android";
|
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git#radiantwavekiosk-android";
|
||||||
extraModules = [./rotated_display.nix];
|
extraModules = [
|
||||||
|
./rotated_display.nix
|
||||||
|
./edid-override.nix
|
||||||
|
{ services.kioskEdid = {
|
||||||
|
enable = true;
|
||||||
|
overrides = [
|
||||||
|
{ connector = "HDMI-A-1"; edidFile = ./android-4k30-edid.bin; }
|
||||||
|
{ connector = "HDMI-A-2"; edidFile = ./android-4k30-edid.bin; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
radiantwavekiosk-android-dev = mkKioskSystem {
|
radiantwavekiosk-android-dev = mkKioskSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
@@ -224,7 +235,18 @@
|
|||||||
hostName = "radiantwavekiosk-android-dev";
|
hostName = "radiantwavekiosk-android-dev";
|
||||||
diskModule = ./disk-config.nix;
|
diskModule = ./disk-config.nix;
|
||||||
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-android-dev";
|
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-android-dev";
|
||||||
extraModules = [./rotated_display.nix];
|
extraModules = [
|
||||||
|
./rotated_display.nix
|
||||||
|
./edid-override.nix
|
||||||
|
{ services.kioskEdid = {
|
||||||
|
enable = true;
|
||||||
|
overrides = [
|
||||||
|
{ connector = "HDMI-A-1"; edidFile = ./android-4k30-edid.bin; }
|
||||||
|
{ connector = "HDMI-A-2"; edidFile = ./android-4k30-edid.bin; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
# Raspberry Pi 5.
|
# Raspberry Pi 5.
|
||||||
radiantwavekiosk-rpi = mkRpiKioskSystem {
|
radiantwavekiosk-rpi = mkRpiKioskSystem {
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Forces 2160x3840 portrait @ 30Hz as the ONLY Detailed Timing Descriptor
|
||||||
|
(DTD slot 1 / preferred timing) and strips the Video Data Block from the
|
||||||
|
CTA-861 extension so no other video mode is advertised in any way.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 force_4k30_portrait_only.py donor.bin fixed.bin
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def build_dtd(pixel_clock_khz, h_active, h_blank, h_sync_offset, h_sync_width,
|
||||||
|
v_active, v_blank, v_sync_offset, v_sync_width,
|
||||||
|
h_size_mm=0, v_size_mm=0, h_border=0, v_border=0,
|
||||||
|
interlaced=False, hsync_pos=True, vsync_pos=True):
|
||||||
|
pc = round(pixel_clock_khz / 10) # EDID stores pixel clock in 10kHz units
|
||||||
|
dtd = bytearray(18)
|
||||||
|
dtd[0] = pc & 0xFF
|
||||||
|
dtd[1] = (pc >> 8) & 0xFF
|
||||||
|
|
||||||
|
dtd[2] = h_active & 0xFF
|
||||||
|
dtd[3] = h_blank & 0xFF
|
||||||
|
dtd[4] = ((h_active >> 8) << 4) | (h_blank >> 8)
|
||||||
|
|
||||||
|
dtd[5] = v_active & 0xFF
|
||||||
|
dtd[6] = v_blank & 0xFF
|
||||||
|
dtd[7] = ((v_active >> 8) << 4) | (v_blank >> 8)
|
||||||
|
|
||||||
|
dtd[8] = h_sync_offset & 0xFF
|
||||||
|
dtd[9] = h_sync_width & 0xFF
|
||||||
|
dtd[10] = ((v_sync_offset & 0xF) << 4) | (v_sync_width & 0xF)
|
||||||
|
dtd[11] = (((h_sync_offset >> 8) & 0x3) << 6) | \
|
||||||
|
(((h_sync_width >> 8) & 0x3) << 4) | \
|
||||||
|
(((v_sync_offset >> 4) & 0x3) << 2) | \
|
||||||
|
((v_sync_width >> 4) & 0x3)
|
||||||
|
|
||||||
|
dtd[12] = h_size_mm & 0xFF
|
||||||
|
dtd[13] = v_size_mm & 0xFF
|
||||||
|
dtd[14] = ((h_size_mm >> 8) << 4) | (v_size_mm >> 8)
|
||||||
|
|
||||||
|
dtd[15] = h_border
|
||||||
|
dtd[16] = v_border
|
||||||
|
|
||||||
|
flags = 0x18 # digital separate sync
|
||||||
|
if interlaced:
|
||||||
|
flags |= 0x80
|
||||||
|
if hsync_pos:
|
||||||
|
flags |= 0x02
|
||||||
|
if vsync_pos:
|
||||||
|
flags |= 0x04
|
||||||
|
dtd[17] = flags
|
||||||
|
|
||||||
|
return dtd
|
||||||
|
|
||||||
|
|
||||||
|
TAG_VIDEO = 2
|
||||||
|
|
||||||
|
|
||||||
|
def strip_video_data_block(ext):
|
||||||
|
"""Remove all Video Data Blocks from a CTA-861 extension block."""
|
||||||
|
dtd_offset = ext[2]
|
||||||
|
pos = 4 # skip 4-byte CTA header
|
||||||
|
blocks = []
|
||||||
|
while pos < dtd_offset:
|
||||||
|
tag = (ext[pos] >> 5) & 0x07
|
||||||
|
length = ext[pos] & 0x1F
|
||||||
|
block_data = bytes(ext[pos + 1:pos + 1 + length])
|
||||||
|
blocks.append((tag, length, block_data))
|
||||||
|
pos += 1 + length
|
||||||
|
|
||||||
|
blocks = [(t, l, d) for t, l, d in blocks if t != TAG_VIDEO]
|
||||||
|
|
||||||
|
new_db = bytearray()
|
||||||
|
for tag, length, data in blocks:
|
||||||
|
new_db.append((tag << 5) | length)
|
||||||
|
new_db.extend(data)
|
||||||
|
|
||||||
|
new_ext = bytearray(128)
|
||||||
|
new_ext[0] = ext[0] # CTA tag
|
||||||
|
new_ext[1] = ext[1] # revision
|
||||||
|
new_ext[2] = 4 + len(new_db) # new DTD offset
|
||||||
|
new_ext[3] = ext[3] # flags (audio, YCbCr, etc.)
|
||||||
|
new_ext[4:4 + len(new_db)] = new_db
|
||||||
|
|
||||||
|
new_ext[0x7F] = 0
|
||||||
|
new_ext[0x7F] = (-sum(new_ext)) % 256
|
||||||
|
return new_ext
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
infile, outfile = sys.argv[1], sys.argv[2]
|
||||||
|
data = bytearray(open(infile, "rb").read())
|
||||||
|
|
||||||
|
# --- 2160x3840 portrait @ 30Hz ---
|
||||||
|
# H blanking uses CVT-RB-style fixed values (front 48 / sync 32 / back 80)
|
||||||
|
# V blanking computed to satisfy CVT-RB min vertical blank time (~460us)
|
||||||
|
# Pixel clock ~= htotal * vtotal * refresh
|
||||||
|
dtd_4k30 = build_dtd(
|
||||||
|
pixel_clock_khz=271020,
|
||||||
|
h_active=2160, h_blank=160, h_sync_offset=48, h_sync_width=32,
|
||||||
|
v_active=3840, v_blank=54, v_sync_offset=3, v_sync_width=5,
|
||||||
|
h_size_mm=685, v_size_mm=1218, # swapped panel physical size for portrait
|
||||||
|
)
|
||||||
|
|
||||||
|
# Slot 1 = bytes 54-71 (was 1920x1080@60, the preferred timing)
|
||||||
|
data[54:72] = dtd_4k30
|
||||||
|
|
||||||
|
# Slot 2 = bytes 72-89 (was 1280x720@60) - blank out to a dummy
|
||||||
|
# descriptor so no second timing is advertised at all.
|
||||||
|
# Dummy Descriptor format: 00 00 00 10 00, then 13 zero-padding bytes.
|
||||||
|
dummy = bytearray(18)
|
||||||
|
dummy[3] = 0x10 # tag byte for "Dummy Descriptor"
|
||||||
|
data[72:90] = dummy
|
||||||
|
|
||||||
|
# Unknown Display Descriptor slot and Product Name descriptor are
|
||||||
|
# left as-is.
|
||||||
|
|
||||||
|
# Recompute base block checksum (byte 127)
|
||||||
|
data[127] = (256 - sum(data[0:127]) % 256) % 256
|
||||||
|
|
||||||
|
# Strip Video Data Block from CTA-861 extension (bytes 128-255)
|
||||||
|
data[128:256] = strip_video_data_block(data[128:256])
|
||||||
|
|
||||||
|
open(outfile, "wb").write(data)
|
||||||
|
print(f"Wrote {outfile}")
|
||||||
|
print("Run `edid-decode` on the output to verify before flashing.")
|
||||||
Reference in New Issue
Block a user