ummm new tui thing ?

This commit is contained in:
2026-03-28 21:49:33 +02:00
parent 3db6de2d2c
commit de440305c7
16 changed files with 534 additions and 226 deletions

View File

@@ -24,19 +24,14 @@ from pathlib import Path
from typing import Optional, Tuple, List
from dataclasses import dataclass
# ============================================================================
# Configuration
# ============================================================================
SCRIPT_DIR = Path(__file__).parent.resolve()
SHOFEL_DIR = SCRIPT_DIR / "Shofel"
WORK_DIR = SCRIPT_DIR / "jibo_work"
# eMMC dump parameters
EMMC_TOTAL_SECTORS = 0x1D60000 # Total sectors to dump (~15GB)
EMMC_SECTOR_SIZE = 512
# Colors for terminal output
class Colors:
RED = '\033[91m'
GREEN = '\033[92m'
@@ -47,7 +42,6 @@ class Colors:
RESET = '\033[0m'
BOLD = '\033[1m'
# Disable colors on Windows unless using Windows Terminal
if platform.system() == "Windows" and "WT_SESSION" not in os.environ:
for attr in dir(Colors):
if not attr.startswith('_'):
@@ -64,9 +58,6 @@ class PartitionInfo:
name: str
# ============================================================================
# Utilities
# ============================================================================
def print_banner():
"""Print the tool banner"""
@@ -151,22 +142,17 @@ def _check_payloads_exist() -> bool:
return all((SHOFEL_DIR / p).exists() for p in critical_payloads)
# ============================================================================
# Dependency Checking
# ============================================================================
def check_linux_dependencies() -> Tuple[bool, List[str], List[str]]:
"""Check for required Linux dependencies"""
missing = []
warnings = []
# Required tools for host build
required_tools = {
"gcc": "build-essential or base-devel",
"make": "build-essential or base-devel",
}
# Optional tools (have fallbacks)
optional_tools = {
"lsusb": "usbutils (optional, used for device detection)",
"fdisk": "util-linux (optional, has Python fallback)",
@@ -180,12 +166,10 @@ def check_linux_dependencies() -> Tuple[bool, List[str], List[str]]:
if not shutil.which(tool):
warnings.append(f"{tool} ({package})")
# Check ARM toolchain only if payloads are missing
if not _check_payloads_exist():
if not shutil.which("arm-none-eabi-gcc"):
missing.append("arm-none-eabi-gcc (arm-none-eabi-gcc or arm-none-eabi-toolchain)")
# Check for libusb
try:
result = subprocess.run(
["pkg-config", "--exists", "libusb-1.0"],
@@ -194,7 +178,6 @@ def check_linux_dependencies() -> Tuple[bool, List[str], List[str]]:
if result.returncode != 0:
missing.append("libusb-1.0-dev or libusb1-devel")
except FileNotFoundError:
# pkg-config not found, try alternative check
if not Path("/usr/include/libusb-1.0").exists() and \
not Path("/usr/local/include/libusb-1.0").exists():
missing.append("libusb-1.0-dev or libusb1-devel")
@@ -207,20 +190,16 @@ def check_windows_dependencies() -> Tuple[bool, List[str], List[str]]:
missing = []
warnings = []
# Check for MinGW or MSYS2
if not shutil.which("gcc") and not shutil.which("x86_64-w64-mingw32-gcc"):
missing.append("MinGW-w64 or MSYS2")
# Check for ARM toolchain only if payloads missing
if not _check_payloads_exist():
if not shutil.which("arm-none-eabi-gcc"):
missing.append("ARM GNU Toolchain (arm-none-eabi-gcc)")
# Check for make
if not shutil.which("make") and not shutil.which("mingw32-make"):
missing.append("GNU Make")
# Optional: debugfs for editing ext filesystem images without mounting
if not shutil.which("debugfs") and not shutil.which("debugfs.exe"):
warnings.append("debugfs (e2fsprogs) - optional but recommended for reliable mode.json edits on Windows")
@@ -243,7 +222,6 @@ def print_install_instructions(system: str, missing: List[str], warnings: List[s
print(f"\n{Colors.BOLD}Installation instructions:{Colors.RESET}")
if system == "Linux":
# Detect distro
distro = "unknown"
if Path("/etc/arch-release").exists():
distro = "arch"
@@ -289,9 +267,6 @@ def print_install_instructions(system: str, missing: List[str], warnings: List[s
""")
# ============================================================================
# Shofel Building
# ============================================================================
def check_shofel_built() -> bool:
"""Check if shofel2_t124 is already built"""
@@ -333,18 +308,14 @@ def build_shofel(force_rebuild: bool = False) -> bool:
print_info("Compiling shofel2_t124...")
try:
# Only clean host build (preserves payload .bin files)
if force_rebuild:
run_command(["make", "clean"], cwd=SHOFEL_DIR, capture_output=True, check=False)
# Build (Makefile will skip existing payload .bin files)
result = run_command(["make"], cwd=SHOFEL_DIR, capture_output=True, check=False)
# Check if the main executable was built
if check_shofel_built():
print_success("Host tool (shofel2_t124) built successfully!")
# Check payloads again
payloads_ok, missing_payloads = check_payloads_built()
if not payloads_ok:
print_error("ARM payload binaries are missing!")
@@ -353,7 +324,6 @@ def build_shofel(force_rebuild: bool = False) -> bool:
print(f"{Colors.YELLOW}The ARM toolchain (arm-none-eabi-gcc) is required to build payloads.{Colors.RESET}")
print()
# Detect distro and provide instructions
if Path("/etc/arch-release").exists():
print(f" {Colors.CYAN}Arch/CachyOS:{Colors.RESET} sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib")
elif Path("/etc/debian_version").exists():
@@ -381,20 +351,15 @@ def build_shofel(force_rebuild: bool = False) -> bool:
return False
# ============================================================================
# Jibo Detection
# ============================================================================
def detect_jibo_rcm() -> bool:
"""Detect if Jibo is connected in RCM mode"""
print_info("Looking for Jibo in RCM mode (NVIDIA APX device)...")
if platform.system() == "Linux":
# Try lsusb first
if shutil.which("lsusb"):
try:
result = run_command(["lsusb"], capture_output=True)
# Jibo uses 0955:7740 (NVIDIA APX)
if "0955:7740" in result.stdout:
print_success("Found Jibo in RCM mode!")
return True
@@ -408,7 +373,6 @@ def detect_jibo_rcm() -> bool:
except Exception as e:
print_error(f"lsusb failed: {e}")
# Fallback: check /sys/bus/usb/devices
try:
usb_devices = Path("/sys/bus/usb/devices")
if usb_devices.exists():
@@ -424,16 +388,13 @@ def detect_jibo_rcm() -> bool:
except Exception:
pass
# Final fallback: assume user will connect it
print_warning("Cannot detect USB devices. Please ensure Jibo is in RCM mode.")
print_info("The tool will attempt to connect anyway.")
return True # Let shofel try
elif platform.system() == "Windows":
# On Windows, we need to use different methods
print_warning("Windows USB detection - please ensure Zadig drivers are installed")
print_info("Run Zadig and install WinUSB driver for 'APX' device")
# Try to proceed anyway, shofel will detect it
return True
return False
@@ -459,36 +420,25 @@ def wait_for_jibo_rcm(timeout: int = 60) -> bool:
return False
# ============================================================================
# GPT Partition Parsing
# ============================================================================
def parse_gpt_partitions(dump_path: Path) -> List[PartitionInfo]:
"""Parse GPT partition table from dump file"""
partitions = []
with open(dump_path, "rb") as f:
# Read MBR (sector 0) - skip it
f.seek(512)
# Read GPT header (sector 1)
gpt_header = f.read(512)
# Check GPT signature
signature = gpt_header[:8]
if signature != b'EFI PART':
print_warning("GPT signature not found, trying fdisk parsing...")
return parse_partitions_fdisk(dump_path)
# Parse GPT header
# Offset 72: Partition entries start LBA (8 bytes)
# Offset 80: Number of partition entries (4 bytes)
# Offset 84: Size of partition entry (4 bytes)
partition_entries_lba = struct.unpack("<Q", gpt_header[72:80])[0]
num_entries = struct.unpack("<I", gpt_header[80:84])[0]
entry_size = struct.unpack("<I", gpt_header[84:88])[0]
# Seek to partition entries
f.seek(partition_entries_lba * 512)
for i in range(num_entries):
@@ -496,11 +446,6 @@ def parse_gpt_partitions(dump_path: Path) -> List[PartitionInfo]:
if len(entry) < 128:
break
# Parse partition entry
# Offset 0: Partition type GUID (16 bytes)
# Offset 32: First LBA (8 bytes)
# Offset 40: Last LBA (8 bytes)
# Offset 56: Partition name (72 bytes, UTF-16LE)
type_guid = entry[:16]
if type_guid == b'\x00' * 16:
@@ -509,7 +454,6 @@ def parse_gpt_partitions(dump_path: Path) -> List[PartitionInfo]:
first_lba = struct.unpack("<Q", entry[32:40])[0]
last_lba = struct.unpack("<Q", entry[40:48])[0]
# Parse name (UTF-16LE, null-terminated)
name_bytes = entry[56:128]
try:
name = name_bytes.decode('utf-16le').rstrip('\x00')
@@ -538,14 +482,11 @@ def parse_partitions_fdisk(dump_path: Path) -> List[PartitionInfo]:
check=False
)
# Parse fdisk output
for line in result.stdout.split('\n'):
# Look for lines like: dump.bin1 34 2048033 2048000 1000M Microsoft basic data
if dump_path.name in line and not line.startswith("Disk"):
parts = line.split()
if len(parts) >= 4:
try:
# Extract partition number from name (e.g., dump.bin5 -> 5)
part_name = parts[0]
part_num = int(''.join(c for c in part_name if c.isdigit()) or '0')
@@ -570,15 +511,12 @@ def parse_partitions_fdisk(dump_path: Path) -> List[PartitionInfo]:
def find_var_partition(partitions: List[PartitionInfo]) -> Optional[PartitionInfo]:
"""Find the /var partition (partition 5, ~500MB)"""
# The var partition is typically partition 5 with ~500MB size
for part in partitions:
if part.number == 5:
# Verify it's roughly the right size (450-550 MB)
size_mb = (part.size_sectors * EMMC_SECTOR_SIZE) / (1024 * 1024)
if 400 < size_mb < 600:
return part
# Fallback: look for any ~500MB partition
for part in partitions:
size_mb = (part.size_sectors * EMMC_SECTOR_SIZE) / (1024 * 1024)
if 450 < size_mb < 550:
@@ -588,9 +526,6 @@ def find_var_partition(partitions: List[PartitionInfo]) -> Optional[PartitionInf
return None
# ============================================================================
# Partition Extraction and Modification
# ============================================================================
def extract_partition(dump_path: Path, partition: PartitionInfo, output_path: Path) -> bool:
"""Extract a partition from the dump"""
@@ -626,8 +561,6 @@ def modify_mode_json_direct(partition_path: Path) -> bool:
with open(partition_path, "r+b") as f:
data = bytearray(f.read())
# Best-effort raw replacement.
# IMPORTANT: never change image length and never shift bytes; only overwrite in-place.
json_patterns = [
(b'{"mode":"normal"}', b'{"mode":"int-developer"}'),
(b'{"mode": "normal"}', b'{"mode": "int-developer"}'),
@@ -654,7 +587,6 @@ def modify_mode_json_direct(partition_path: Path) -> bool:
return False
region_len = len(new_json)
# Overwrite the JSON plus the padding region; do NOT shift bytes.
data[offset:offset + region_len] = new_json
f.seek(0)
@@ -680,18 +612,15 @@ def modify_partition_mounted(partition_path: Path) -> bool:
mount_point.mkdir(parents=True, exist_ok=True)
try:
# Mount the partition
print_info(f"Mounting partition at {mount_point}...")
run_command(
["mount", "-o", "loop", str(partition_path), str(mount_point)],
sudo=True
)
# Find and modify mode.json
mode_json_path = mount_point / "jibo" / "mode.json"
if not mode_json_path.exists():
# Try alternative paths
for alt_path in [
mount_point / "mode.json",
mount_point / "etc" / "jibo" / "mode.json",
@@ -703,7 +632,6 @@ def modify_partition_mounted(partition_path: Path) -> bool:
if mode_json_path.exists():
print_info(f"Found mode.json at {mode_json_path}")
# Capture original permissions/ownership so we can restore after copy-write
perm = None
uid = None
gid = None
@@ -720,7 +648,6 @@ def modify_partition_mounted(partition_path: Path) -> bool:
except Exception:
pass
# Save a raw backup copy of mode.json for debugging/recovery
try:
backup_text = run_command(
["cat", str(mode_json_path)],
@@ -732,7 +659,6 @@ def modify_partition_mounted(partition_path: Path) -> bool:
except Exception:
pass
# Read current content (prefer sudo cat so permissions don't bite us)
try:
mode_text = run_command(
["cat", str(mode_json_path)],
@@ -742,23 +668,19 @@ def modify_partition_mounted(partition_path: Path) -> bool:
).stdout
content = json.loads(mode_text)
except Exception:
# Fallback: direct open (works if script is run with sudo)
with open(mode_json_path, "r") as f:
content = json.load(f)
print_info(f"Current mode: {content.get('mode', 'unknown')}")
# Modify
content["mode"] = "int-developer"
# Write back (need sudo)
temp_json = WORK_DIR / "mode_temp.json"
with open(temp_json, "w") as f:
json.dump(content, f)
run_command(["cp", str(temp_json), str(mode_json_path)], sudo=True)
# Restore permissions/ownership if we captured them
if perm is not None:
run_command(["chmod", perm, str(mode_json_path)], sudo=True, check=False)
if uid is not None and gid is not None:
@@ -784,7 +706,6 @@ def modify_partition_mounted(partition_path: Path) -> bool:
return False
finally:
# Always unmount
try:
run_command(["umount", str(mount_point)], sudo=True, check=False)
except:
@@ -811,14 +732,12 @@ def modify_partition_debugfs(partition_path: Path) -> bool:
print_info("Attempting mode.json edit via debugfs (no mount)...")
# Potential locations inside /var
candidate_paths = [
"/jibo/mode.json",
"/mode.json",
"/etc/jibo/mode.json",
]
# Find which path exists by trying to cat it
existing_path: Optional[str] = None
original_text: Optional[str] = None
for p in candidate_paths:
@@ -828,7 +747,6 @@ def modify_partition_debugfs(partition_path: Path) -> bool:
capture_output=True,
check=True,
)
# debugfs prints to stdout for cat
if res.stdout and "File not found" not in res.stdout:
existing_path = p
original_text = res.stdout
@@ -840,7 +758,6 @@ def modify_partition_debugfs(partition_path: Path) -> bool:
print_warning("debugfs could not locate mode.json inside the image")
return False
# Save backup
try:
(WORK_DIR / "mode.json.original").write_text(original_text)
except Exception:
@@ -858,9 +775,6 @@ def modify_partition_debugfs(partition_path: Path) -> bool:
temp_json = WORK_DIR / "mode_temp.json"
temp_json.write_text(new_text)
# Overwrite: remove then write to ensure replacement works even if size differs.
# This may change filesystem allocation, which is fine for full /var write, and
# our patch-write logic can still handle it.
try:
run_command([debugfs, "-w", "-R", f"rm {existing_path}", str(partition_path)], check=False, capture_output=True)
run_command([debugfs, "-w", "-R", f"write {str(temp_json)} {existing_path}", str(partition_path)], capture_output=True)
@@ -881,17 +795,14 @@ def modify_var_partition(partition_path: Path) -> bool:
"""Modify the var partition to enable developer mode"""
print_step(4, 6, "Modifying var partition")
# On Linux, prefer mounting: it's the only truly safe way to update a file in an ext filesystem.
if platform.system() == "Linux":
if modify_partition_mounted(partition_path):
return True
print_warning("Mount-based edit failed; falling back to raw in-place patch")
# If mounting is unavailable (Windows/macOS) or failed, try debugfs (ext filesystem edit without mount)
if modify_partition_debugfs(partition_path):
return True
# Raw patch is a best-effort last resort
if modify_mode_json_direct(partition_path):
return True
@@ -969,7 +880,6 @@ def compute_changed_sector_ranges(original_path: Path, modified_path: Path, sect
base_sector += len(b1) // sector_size
continue
# Chunk differs; identify sector-level diffs within this chunk
sectors_in_chunk = min(len(b1), len(b2)) // sector_size
for i in range(sectors_in_chunk):
s1 = b1[i * sector_size:(i + 1) * sector_size]
@@ -1025,9 +935,6 @@ def write_partition_patch_to_emmc(original_path: Path, modified_path: Path, base
return True
# ============================================================================
# eMMC Operations
# ============================================================================
def get_shofel_path() -> Path:
"""Get the path to shofel2_t124 executable"""
@@ -1058,7 +965,6 @@ def dump_emmc(output_path: Path, start_sector: int = 0, num_sectors: int = EMMC_
str(output_path)
]
# Run with sudo on Linux
if platform.system() == "Linux":
cmd = ["sudo"] + cmd
@@ -1136,7 +1042,6 @@ def verify_write(partition_path: Path, start_sector: int, num_sectors: int) -> b
subprocess.run(cmd, cwd=SHOFEL_DIR, check=True)
# Compare hashes
with open(partition_path, "rb") as f:
original_hash = hashlib.md5(f.read()).hexdigest()
@@ -1157,22 +1062,17 @@ def verify_write(partition_path: Path, start_sector: int, num_sectors: int) -> b
return False
# ============================================================================
# Main Workflow
# ============================================================================
def run_full_mod(args) -> bool:
"""Run the complete modding workflow"""
print_banner()
# Check system
sys_info = get_system_info()
print_info(f"System: {sys_info['os']} ({sys_info['arch']})")
if sys_info['is_wsl']:
print_info("Running in WSL - USB passthrough may require additional setup")
# Check dependencies
print_step(0, 6, "Checking dependencies")
if sys_info['os'] == "Linux":
@@ -1190,25 +1090,20 @@ def run_full_mod(args) -> bool:
print_success("All required dependencies found!")
# Create work directory
WORK_DIR.mkdir(parents=True, exist_ok=True)
# Build Shofel
if not build_shofel(force_rebuild=args.rebuild_shofel):
return False
# Detect or wait for Jibo
if not args.skip_detection:
if not detect_jibo_rcm():
if not wait_for_jibo_rcm(timeout=120):
return False
# Paths
dump_path = WORK_DIR / "jibo_full_dump.bin"
var_partition_path = WORK_DIR / "var_partition.bin"
backup_var_path = WORK_DIR / "var_partition_backup.bin"
# Dump eMMC (or use existing dump)
if args.dump_path:
dump_path = Path(args.dump_path)
if not dump_path.exists():
@@ -1222,7 +1117,6 @@ def run_full_mod(args) -> bool:
if not dump_emmc(dump_path):
return False
# Parse partitions
print_step(3, 6, "Analyzing partition table")
partitions = parse_gpt_partitions(dump_path)
@@ -1235,7 +1129,6 @@ def run_full_mod(args) -> bool:
size_mb = (part.size_sectors * EMMC_SECTOR_SIZE) / (1024 * 1024)
print(f" {part.number}: sectors {part.start_sector}-{part.end_sector} ({size_mb:.1f} MB) - {part.name}")
# Find var partition
var_partition = find_var_partition(partitions)
if not var_partition:
print_error("Could not identify /var partition")
@@ -1243,35 +1136,28 @@ def run_full_mod(args) -> bool:
print_success(f"Identified /var partition: partition {var_partition.number}")
# Extract var partition
if not extract_partition(dump_path, var_partition, var_partition_path):
return False
# Create backup
shutil.copy(var_partition_path, backup_var_path)
print_info(f"Backup created: {backup_var_path}")
# Modify partition
if not modify_var_partition(var_partition_path):
return False
# Check if Jibo still connected (may need to re-enter RCM)
if not args.skip_detection:
print_info("Please ensure Jibo is still in RCM mode")
print_info("If Jibo rebooted, re-enter RCM mode now")
if not wait_for_jibo_rcm(timeout=60):
print_warning("Continuing anyway...")
# Write modified partition
if not write_partition_to_emmc(var_partition_path, var_partition.start_sector):
return False
# Verify
if args.verify:
if not verify_write(var_partition_path, var_partition.start_sector, var_partition.size_sectors):
print_warning("Verification failed, but write may still be successful")
# Done!
print(f"""
{Colors.GREEN}╔═══════════════════════════════════════════════════════════════════╗
{Colors.BOLD}MODDING COMPLETE!{Colors.RESET}{Colors.GREEN}
@@ -1302,11 +1188,9 @@ def run_dump_only(args) -> bool:
WORK_DIR.mkdir(parents=True, exist_ok=True)
# Build Shofel
if not build_shofel(force_rebuild=args.rebuild_shofel):
return False
# Wait for Jibo
if not args.skip_detection:
if not wait_for_jibo_rcm(timeout=120):
return False
@@ -1325,11 +1209,9 @@ def run_write_only(args) -> bool:
print_error(f"Partition file not found: {partition_path}")
return False
# Build Shofel if needed
if not build_shofel():
return False
# Wait for Jibo
if not args.skip_detection:
if not wait_for_jibo_rcm(timeout=120):
return False
@@ -1344,16 +1226,13 @@ def run_mode_json_only(args) -> bool:
WORK_DIR.mkdir(parents=True, exist_ok=True)
# Build Shofel
if not build_shofel(force_rebuild=args.rebuild_shofel):
return False
# Wait for Jibo
if not args.skip_detection:
if not wait_for_jibo_rcm(timeout=120):
return False
# Dump GPT / partition table (small read)
gpt_path = WORK_DIR / "gpt_dump.bin"
gpt_sectors = 4096 # 2MB; safely covers typical GPT entry area
print_info(f"Dumping GPT header/table ({gpt_sectors} sectors)...")
@@ -1375,7 +1254,6 @@ def run_mode_json_only(args) -> bool:
f"(start=0x{var_partition.start_sector:x}, sectors={var_partition.size_sectors})"
)
# Dump /var partition only
original_var_path = WORK_DIR / "var_partition_original.bin"
var_partition_path = WORK_DIR / "var_partition.bin"
backup_var_path = WORK_DIR / "var_partition_backup.bin"
@@ -1388,17 +1266,14 @@ def run_mode_json_only(args) -> bool:
shutil.copy(original_var_path, backup_var_path)
print_info(f"Backup created: {backup_var_path}")
# Modify mode.json inside /var
if not modify_var_partition(var_partition_path):
return False
# Re-check connectivity (optional)
if not args.skip_detection:
print_info("Please ensure Jibo is still in RCM mode")
if not wait_for_jibo_rcm(timeout=60):
print_warning("Continuing anyway...")
# Write back: patch by default, full write if requested
if args.full_var_write:
print_info("Writing full /var partition back to device...")
if not write_partition_to_emmc(var_partition_path, var_partition.start_sector):
@@ -1408,7 +1283,6 @@ def run_mode_json_only(args) -> bool:
if not write_partition_patch_to_emmc(original_var_path, var_partition_path, var_partition.start_sector):
return False
# Verify (reads back full /var; optional)
if args.verify:
if not verify_write(var_partition_path, var_partition.start_sector, var_partition.size_sectors):
print_warning("Verification failed, but write may still be successful")
@@ -1419,9 +1293,6 @@ def run_mode_json_only(args) -> bool:
return True
# ============================================================================
# CLI
# ============================================================================
def main():
parser = argparse.ArgumentParser(
@@ -1436,7 +1307,6 @@ Examples:
"""
)
# Operation modes
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument("--dump-only", action="store_true",
help="Only dump the eMMC without modifying")
@@ -1445,7 +1315,6 @@ Examples:
mode_group.add_argument("--mode-json-only", action="store_true",
help="Fast mode: dump GPT + /var only, patch /var/jibo/mode.json, write back minimal changes")
# Options
parser.add_argument("--dump-path", metavar="FILE",
help="Use existing dump file instead of dumping")
parser.add_argument("--output", "-o", metavar="FILE",
@@ -1467,11 +1336,9 @@ Examples:
args = parser.parse_args()
# Validate arguments
if args.write_partition and not args.start_sector:
parser.error("--write-partition requires --start-sector")
# Run appropriate mode
try:
if args.dump_only:
success = run_dump_only(args)