kernel/hi3516cv200: bring up V2 generation against Linux 7.0 (issue #51)#162
Merged
Conversation
Issue #51 tracks running modern kernels on the V2 family (Hi3516CV200, Hi3518EV200, Hi3518EV201 — ARM926EJ-S). Unlike V3+ which abstract all kernel touchpoints through OSAL, V2 vendor blobs were compiled against 4.9.37 and embed kernel-API call sites directly. Two things therefore have to give for cv200 to load on modern kernels: 1. cv200 *source* (the open parts — mmz, sys_config, peripherals, init wrappers) calls APIs that changed across the 4.9 -> 7.0 window. Route these through new shims in kernel/compat/kernel_compat.h and per-site COMPAT_* macros, the same convention cv300 (#148) and cv500/av300/ev300 already follow. No #ifdef LINUX_VERSION_CODE leaks into driver source (per CLAUDE.md). 2. cv200 *blobs* in kernel/obj/hi3516cv200/ import legacy kernel symbols (do_gettimeofday, init_timer_key, register_sysctl_table, strlcpy) that were removed or renamed after 4.9. Add a small loadable shim — kernel/osal_v2_shim/cv200_shim.c — that re-exports those symbols under their legacy names so the blobs resolve at insmod time on >= 5.0 kernels. kernel_compat.h adds: - no_llseek -> NULL on >= 6.12 (helper removed in commit 868941b14441; modern code just leaves .llseek unset) (All other compat helpers cv200 needs — COMPAT_TIMER_SETUP, COMPAT_USE_PROC_OPS, compat_access_ok, compat_DEFINE_SEMAPHORE, compat_platform_remove_ret / _return, compat_spi_busnum_to_controller, COMPAT_NO_SYSCTL_TABLE, compat_register_sysctl — were already in place from earlier cv300/cv500 work.) cv200 sources updated to call the shims: - mmz/media-mem.c : DEFINE_SEMAPHORE 1-arg via compat helper; proc_ops branch under COMPAT_USE_PROC_OPS - mmz/mmz-userdev.c : p4d_offset gated >= 4.11 (folded on ARM32 but symbol absent on 4.9) - himedia/base.c : dev_attrs -> dev_groups (ATTRIBUTE_GROUPS); const-correctness for bus_type callbacks on >= 6.3 - init/base_init.c : flat compat_register_sysctl path under COMPAT_NO_SYSCTL_TABLE - mipi_rx/mipi.c : proc_ops branch; platform_driver.remove return type - sys_config/sys_config.c, isp/.../drv/isp.c, ir/hiir.c, rtc/hi_rtc.c : platform_driver.remove return type via compat_platform_remove_ret / _return - ir/hiir.c, piris/piris.c, rtc/hi_rtc.c : timer_setup + from_timer via COMPAT_TIMER_SETUP - piris/piris.c : access_ok via compat_access_ok - rtc/hi_rtc.c : local spi_read/write renamed hi_spi_read/ write to avoid collision with the new kernel header symbols - sensor_i2c/sensor_i2c.c : fallback macros for HiSi vendor i2c extensions (hi_i2c_master_send/recv, I2C_M_16BIT_REG/DATA); i2c_new_device redirect to i2c_new_client_device on >= 5.8 - sensor_spi/sensor_spi.c : spi_busnum_to_master via compat_spi_busnum_to_controller on >= 6.4 kernel/osal_v2_shim/cv200_shim.c (new module, prefix open_v2_shim.ko): - The entire shim body is gated >= 5.0 — pre-5.0 kernels still natively export every legacy symbol it re-exports, and re-defining them there triggers "conflicting types" compile errors and duplicate-export runtime errors. On 4.9 the module is a benign no-op shell, printing one informational line at init. - On >= 5.0 it re-exports: do_gettimeofday (removed 5.0; backed by ktime_get_real_ts64) register_sysctl_table (removed 6.6; routed through register_sysctl_sz / register_sysctl with a flat "hisi" path — sufficient for symbol resolution; the blob's expected /proc paths may not materialize) init_timer_key (callback signature changed in 4.15; calls timer_setup but blobs using the legacy void(*)(unsigned long) callback will not fire correctly — see in-file caveat) strlcpy (removed 6.8; backed by sized_strscpy) - printk -> _printk rename (6.0) is handled separately by the existing CHIPARCH-wide objcopy --redefine-sym flow (not by this shim — a macro definition of printk in <linux/printk.h> prevents a function-name redefinition). - Wired into kernel/hi3516cv200.kbuild ahead of the MMZ entry so load_hisilicon ordering puts it before any blob module. Verification: * hi3516cv200_lite (4.9 kernel, production target) rebuilt with this tree (via HISILICON_OPENSDK_OVERRIDE_SRCDIR) — rootfs is byte- equivalent to nightly: 697/697 files, same 34 hisilicon .ko set, QEMU lsmod identical, dmesg diff 20 benign lines only (BogoMIPS jitter, random MAC, ramdisk rounding). v2_shim.ko correctly NOT shipped on 4.9 (gated no-op). No regression. * Build-side: all cv200 modules compile clean against the 7.0 base (openipc/linux upstream-patches branch, the same base used by ev300_neo / av300_neo / cv500_neo / cv300_neo). Out of scope for this PR (firmware-side sibling needed): - hi3516cv200_neo defconfig, kernel config, neo post-image script - cv200 mainline DT + CRG: already merged via openipc/linux PR #43 (hi3516cv200.dtsi + hi3516cv200-demb.dts + crg-hi3516cv200.c) Caveat — V2 blobs still carry a struct timer_list.data drift between 4.9 and 4.15+ (chnl / viu / vpss). The shim resolves the symbol but the timers will never fire with the correct argument until either a kernel patch restores .data or the blob is recompiled. This will surface as broken video pipeline on cv200_neo and is the known remaining blocker for a full neo bring-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
widgetii
pushed a commit
that referenced
this pull request
May 21, 2026
) Issue #51 tracks running modern kernels on the V2A family (hi3516av100, hi3516dv100 — Cortex-A7). Like V2 (cv200, #162), V2A has no OSAL — the vendor blobs from Hi3516A_SDK_V1.0.8.0 were compiled against 4.9.37 and embed kernel-API call sites directly. Per issue #50, V2A is the cleanest non-OSAL legacy platform: only 3 legacy symbols (do_gettimeofday in 10 blobs, register_sysctl_table in 2, init_timer_key in 3) plus the printk → _printk rename — no strlcpy, no ARMv5-specific ops (Cortex-A7 is ARMv7), no create_proc_entry, no __arm_ioremap. Same two-layer bring-up pattern as cv200: source-side compat shims + loadable blob-symbol shim. kernel_compat.h adds: - no_llseek -> NULL on >= 6.12 (helper removed in commit 868941b14441; same shim cv200 PR #162 added — needed independently because av100's wdt/hi_wdt.c references it). The two PRs converge on this line. All other compat helpers av100 needs — COMPAT_TIMER_SETUP, COMPAT_USE_PROC_OPS, compat_access_ok, compat_DEFINE_SEMAPHORE, compat_platform_remove_ret / _return, compat_spi_busnum_to_controller, COMPAT_NO_SYSCTL_TABLE, compat_register_sysctl, and the do_gettimeofday macro — were already in place from earlier cv300/cv500/cv200 work. av100 sources updated to call the shims: - mmz/media-mem.c : DEFINE_SEMAPHORE 1-arg via compat helper; proc_ops branch under COMPAT_USE_PROC_OPS - mmz/mmz-userdev.c : p4d_offset gated >= 4.11 (folded on ARM32 but symbol absent on 4.9) - himedia/base.c : dev_attrs -> dev_groups (ATTRIBUTE_GROUPS); const-correctness for bus_type callbacks on >= 6.3 - init/base_init.c : flat compat_register_sysctl path under COMPAT_NO_SYSCTL_TABLE - mipi_rx/mipi.c : drop dead #include <mach/io.h> (removed from modern ARM); proc_ops branch; platform_driver.remove return type - isp/.../drv/isp.c, ir/hiir.c, rtc/hi_rtc.c : platform_driver.remove return type via compat_platform_remove_ret / _return - ir/hiir.c, piris/piris.c, rtc/hi_rtc.c : timer_setup + from_timer via COMPAT_TIMER_SETUP - piris/piris.c : access_ok via compat_access_ok - rtc/hi_rtc.c : local spi_read/write renamed hi_spi_read/write to avoid collision with kernel header symbols (same fix as cv200) - sensor_i2c/sensor_i2c.c : I2C_M_16BIT_REG/DATA fallback macros; i2c_new_device redirect to i2c_new_client_device on >= 5.8 - sensor_spi/sensor_spi.c : spi_busnum_to_master via compat_spi_busnum_to_controller on >= 6.4 kernel/osal_v2a_shim/av100_shim.c (new module, prefix open_v2a_shim.ko): - The entire shim body is gated >= 5.0 — pre-5.0 kernels still natively export every legacy symbol it re-exports, and re-defining them there triggers "conflicting types" compile errors and duplicate-export runtime errors. On 4.9 (av100_lite production target) the module is a benign no-op shell, printing one informational line at init. - On >= 5.0 it re-exports: do_gettimeofday (removed 5.0; backed by ktime_get_real_ts64) register_sysctl_table (removed 6.6; routed through register_sysctl_sz / register_sysctl with a flat "hisi" path) init_timer_key (callback signature changed in 4.15; calls timer_setup but blobs using the legacy void(*)(unsigned long) callback will not fire correctly — see in-file caveat) - strlcpy is intentionally NOT in the av100 shim (cv200 had it; av100 blobs don't reference it per issue #50). - printk -> _printk (6.0) is the same documented limitation as cv200: <linux/printk.h> macro-defines printk so a function-name redefinition isn't viable from this shim. Surfaces at blob insmod time on >= 6.0; doesn't affect 4.9 lite. - Wired into kernel/hi3516av100.kbuild ahead of the MMZ entry so load_hisilicon ordering puts it before any blob module. Verification: * hi3516av100_lite (4.9 kernel, production target) rebuilt with this tree (via HISILICON_OPENSDK_OVERRIDE_SRCDIR) — rootfs file count matches nightly (290/290), hisilicon .ko set identical (61/61), QEMU lsmod parity confirmed. v2a_shim.ko correctly NOT shipped on 4.9 (gated no-op; av100 firmware install block doesn't reference it). No regression. * Build-side: all 39 av100 source modules compile clean against the 7.0 base (openipc/linux upstream-patches, the same base used by ev300_neo / av300_neo / cv500_neo / cv300_neo). Out of scope for this PR (firmware-side sibling needed): - hi3516av100_neo defconfig, kernel config, neo post-image script (mirrors cv200 dependency on firmware-side PR) Caveat — V2A blobs (chnl/viu/vpss) carry the same struct timer_list.data drift between 4.9 and 4.15+ that cv200 has. The shim resolves init_timer_key but the timers will never fire with the correct argument until either a kernel patch restores .data or the blob is recompiled. This will surface as broken video pipeline on av100_neo and is the known remaining blocker for a full neo bring-up — shared with cv200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
widgetii
added a commit
that referenced
this pull request
May 21, 2026
) (#165) Issue #51 tracks running modern kernels on the V2A family (hi3516av100, hi3516dv100 — Cortex-A7). Like V2 (cv200, #162), V2A has no OSAL — the vendor blobs from Hi3516A_SDK_V1.0.8.0 were compiled against 4.9.37 and embed kernel-API call sites directly. Per issue #50, V2A is the cleanest non-OSAL legacy platform: only 3 legacy symbols (do_gettimeofday in 10 blobs, register_sysctl_table in 2, init_timer_key in 3) plus the printk → _printk rename — no strlcpy, no ARMv5-specific ops (Cortex-A7 is ARMv7), no create_proc_entry, no __arm_ioremap. Same two-layer bring-up pattern as cv200: source-side compat shims + loadable blob-symbol shim. kernel_compat.h adds: - no_llseek -> NULL on >= 6.12 (helper removed in commit 868941b14441; same shim cv200 PR #162 added — needed independently because av100's wdt/hi_wdt.c references it). The two PRs converge on this line. All other compat helpers av100 needs — COMPAT_TIMER_SETUP, COMPAT_USE_PROC_OPS, compat_access_ok, compat_DEFINE_SEMAPHORE, compat_platform_remove_ret / _return, compat_spi_busnum_to_controller, COMPAT_NO_SYSCTL_TABLE, compat_register_sysctl, and the do_gettimeofday macro — were already in place from earlier cv300/cv500/cv200 work. av100 sources updated to call the shims: - mmz/media-mem.c : DEFINE_SEMAPHORE 1-arg via compat helper; proc_ops branch under COMPAT_USE_PROC_OPS - mmz/mmz-userdev.c : p4d_offset gated >= 4.11 (folded on ARM32 but symbol absent on 4.9) - himedia/base.c : dev_attrs -> dev_groups (ATTRIBUTE_GROUPS); const-correctness for bus_type callbacks on >= 6.3 - init/base_init.c : flat compat_register_sysctl path under COMPAT_NO_SYSCTL_TABLE - mipi_rx/mipi.c : drop dead #include <mach/io.h> (removed from modern ARM); proc_ops branch; platform_driver.remove return type - isp/.../drv/isp.c, ir/hiir.c, rtc/hi_rtc.c : platform_driver.remove return type via compat_platform_remove_ret / _return - ir/hiir.c, piris/piris.c, rtc/hi_rtc.c : timer_setup + from_timer via COMPAT_TIMER_SETUP - piris/piris.c : access_ok via compat_access_ok - rtc/hi_rtc.c : local spi_read/write renamed hi_spi_read/write to avoid collision with kernel header symbols (same fix as cv200) - sensor_i2c/sensor_i2c.c : I2C_M_16BIT_REG/DATA fallback macros; i2c_new_device redirect to i2c_new_client_device on >= 5.8 - sensor_spi/sensor_spi.c : spi_busnum_to_master via compat_spi_busnum_to_controller on >= 6.4 kernel/osal_v2a_shim/av100_shim.c (new module, prefix open_v2a_shim.ko): - The entire shim body is gated >= 5.0 — pre-5.0 kernels still natively export every legacy symbol it re-exports, and re-defining them there triggers "conflicting types" compile errors and duplicate-export runtime errors. On 4.9 (av100_lite production target) the module is a benign no-op shell, printing one informational line at init. - On >= 5.0 it re-exports: do_gettimeofday (removed 5.0; backed by ktime_get_real_ts64) register_sysctl_table (removed 6.6; routed through register_sysctl_sz / register_sysctl with a flat "hisi" path) init_timer_key (callback signature changed in 4.15; calls timer_setup but blobs using the legacy void(*)(unsigned long) callback will not fire correctly — see in-file caveat) - strlcpy is intentionally NOT in the av100 shim (cv200 had it; av100 blobs don't reference it per issue #50). - printk -> _printk (6.0) is the same documented limitation as cv200: <linux/printk.h> macro-defines printk so a function-name redefinition isn't viable from this shim. Surfaces at blob insmod time on >= 6.0; doesn't affect 4.9 lite. - Wired into kernel/hi3516av100.kbuild ahead of the MMZ entry so load_hisilicon ordering puts it before any blob module. Verification: * hi3516av100_lite (4.9 kernel, production target) rebuilt with this tree (via HISILICON_OPENSDK_OVERRIDE_SRCDIR) — rootfs file count matches nightly (290/290), hisilicon .ko set identical (61/61), QEMU lsmod parity confirmed. v2a_shim.ko correctly NOT shipped on 4.9 (gated no-op; av100 firmware install block doesn't reference it). No regression. * Build-side: all 39 av100 source modules compile clean against the 7.0 base (openipc/linux upstream-patches, the same base used by ev300_neo / av300_neo / cv500_neo / cv300_neo). Out of scope for this PR (firmware-side sibling needed): - hi3516av100_neo defconfig, kernel config, neo post-image script (mirrors cv200 dependency on firmware-side PR) Caveat — V2A blobs (chnl/viu/vpss) carry the same struct timer_list.data drift between 4.9 and 4.15+ that cv200 has. The shim resolves init_timer_key but the timers will never fire with the correct argument until either a kernel patch restores .data or the blob is recompiled. This will surface as broken video pipeline on av100_neo and is the known remaining blocker for a full neo bring-up — shared with cv200. Co-authored-by: Vasiliy Yakovlev <vixand@openipc.org> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 21, 2026
widgetii
added a commit
that referenced
this pull request
May 22, 2026
…set (#170) PRs #162 and #165 landed the initial V2/V2A bring-up shims based on the issue #50 audit (4 symbols for cv200, 3 for av100). Exercising the real firmware build for the first time (hi3516cv200_neo via the upstream-patches 7.0 base) surfaced 8 more undefined symbols that issue #50 missed — the audit only covered blob source-imports, not the compiler-generated calls (memset → __memzero, kmalloc → __kmalloc internal slowpath, etc.) and not the symbols pulled in via inline- function expansion at blob-build time against the 4.9 kernel headers. This commit extends both shims (V2 cv200 + V2A av100) to cover every symbol modpost reports as undefined for the V2/V2A blob set on Linux 7.0. Verified empirically: hi3516cv200_neo now builds, boots in QEMU to a login prompt, and meets the openipc/firmware QEMU DoD (eth0 link, DHCP via SLIRP, ping 10.0.2.2). hi3516cv200_lite and hi3516av100_lite (kernel 4.9 production targets) rebuilt against this tree are byte-equivalent to nightly: - cv200_lite: 331/331 rootfs files, 68/68 hisilicon .ko, sets identical - av100_lite: 290/290 rootfs files, 61/61 hisilicon .ko, sets identical kernel/compat/kernel_compat.h adds: - pte_offset_map(pmd, addr) -> pte_offset_kernel(...) on >= 6.5. Modern pte_offset_map() inlines to __pte_offset_map() which is NOT EXPORT_SYMBOL'd, so modules can't link it. pte_offset_kernel is a static inline over pmd_page_vaddr() + pte_index() — same user-page-table lookup the legacy code already did without locking. Source-side fix because the call is in our own cv200/av100 mmz-userdev usr_virt_to_phys, not the blob. Both shim modules add the following exports (gated >= 5.0 so 4.9 lite stays a no-op shell): Symbol Modern equivalent we forward to ────────────────── ─────────────────────────────────────── _cond_resched __cond_resched __kmalloc __kmalloc_noprof (6.5+) kmem_cache_alloc kmem_cache_alloc_noprof (7.0 macro) __memzero memset(ptr, 0, n) PDE_DATA pde_data (5.17 rename) printk vprintk via _printk (6.0 rename) register_sysctl_paths register_sysctl_sz / register_sysctl jiffies_to_msecs (MSEC_PER_SEC / HZ) * j — header inline del_timer timer_delete (7.0 rename) vmalloc vmalloc_noprof (7.0 macro) dev_err dev_vprintk_emit at KERN_ERR sched_setscheduler sched_set_fifo / sched_set_normal — caller priority is dropped (sched_set_fifo doesn't accept one); SCHED_RR mapped to FIFO. Best- effort for legacy blobs that just want "raise above SCHED_NORMAL". Two header-collision cases needed a Kbuild-level macro rename (passed as -D... in CFLAGS_) so the static-inline definition in <linux/sched.h> / <linux/jiffies.h> doesn't conflict with our EXPORT_SYMBOL'd function under the same name: - _cond_resched (static inline since 5.10 preempt-dynamic rework; commit fe32d3cd5e8e) - jiffies_to_msecs (static inline when HZ | MSEC_PER_SEC — HZ=100 production config) For the macro-style conflicts (kmem_cache_alloc, vmalloc, PDE_DATA, printk, dev_err, del_timer) #undef in the shim source is sufficient. Out of scope for this commit (still tracked in issue #51): - struct timer_list .data ABI drift (chnl/viu/vpss blobs) — the shim resolves init_timer_key but timers don't fire with the correct argument until the blob is recompiled. Documented in both shim sources. Surfaces as broken video pipeline on cv200_neo / av100_neo; not a load-time error. - hi3516av100 mainline DT / CRG — av100_neo firmware target is blocked on a separate openipc/linux PR (av100 has no mainline Kconfig / DT / clock-driver support yet, unlike cv200 which landed in OpenIPC/linux#43). cv200_neo unblocked. Co-authored-by: Vasiliy Yakovlev <vixand@openipc.org> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
kernel/osal_v2_shim/cv200_shim.c— a tiny loadable module that re-exports legacy kernel symbols (do_gettimeofday,register_sysctl_table,init_timer_key,strlcpy) under their old names so the cv200 vendor.koblobs (compiled against 4.9.37) resolve at insmod time on >= 5.0 kernelskernel/compat/kernel_compat.h, never#ifdef LINUX_VERSION_CODEin driver codeWhy two layers?
V2 (ARM926EJ-S) is the legacy generation that doesn't have OSAL — vendor
.koblobs call kernel APIs directly. That makes the kernel-version impedance mismatch a two-front problem, unlike V3+ where bumping OSAL is sufficient:kernel/mmz/hi3516cv200/,kernel/sys_config/,kernel/init/, peripherals…) call APIs whose signatures changed across 4.9 → 7.0. Routed through existing compat helpers (COMPAT_TIMER_SETUP,COMPAT_USE_PROC_OPS,compat_access_ok,compat_DEFINE_SEMAPHORE,compat_platform_remove_ret/_return,COMPAT_NO_SYSCTL_TABLE, …) plus one newno_llseek -> NULLmacro for 6.12+..koblobs inkernel/obj/hi3516cv200/referencedo_gettimeofday/init_timer_key/register_sysctl_table/strlcpyby symbol. Modern kernels no longer export those names. Theopen_v2_shim.komodule backfills them.open_v2_shim.kodesign notes#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 0, 0). On 4.9 the legacy symbols are still native exports; re-defining them there triggers "conflicting types" compile errors and duplicate-export errors at runtime. On 4.9 the module loads as a benign no-op (one informational pr_info at init).printk → _printk(6.0 rename) is NOT handled here — it's covered by the existing CHIPARCH-wideobjcopy --redefine-symflow because<linux/printk.h>macro-definesprintk, blocking a function-name redefinition.kernel/hi3516cv200.kbuildahead of the MMZ entry soload_hisiliconorders it first.Verification
hi3516cv200_lite(kernel 4.9, production target) no-regression confirmed: rebuilt with this tree viaHISILICON_OPENSDK_OVERRIDE_SRCDIR, diffed against nightly squashfs — 697/697 rootfs files, same 34 hisilicon.koset, QEMUlsmodidentical,dmesgdiff 20 benign lines (BogoMIPS jitter, random MAC, ramdisk rounding).v2_shim.kocorrectly not shipped on 4.9 (gated to no-op).Out of scope (follow-ups)
hi3516cv200_neo:hi3516cv200_neo_defconfig, neo kernel config, post-image script, opensdk hash bump. (No CI matrix row added here yet — staged for the firmware PR's red-then-green dance.)hi3516cv200.dtsi,hi3516cv200-demb.dts,crg-hi3516cv200.c).struct timer_list.datadrift between 4.9 and 4.15+ (chnl/viu/vpss blobs).init_timer_keyresolves but timers won't fire with the correct argument until either a kernel patch restores.dataor the blobs are recompiled. Will surface as a broken video pipeline on cv200_neo and is the known remaining blocker for a full neo bring-up. Documented in the shim source.Test plan
openipc/linux:upstream-patcheshi3516cv200_lite(kernel 4.9) byte-equivalence vs nightly — no regressionhi3516cv200_liteboot +lsmodparityhi3516cv200_neo— deferred until firmware-side defconfig lands🤖 Generated with Claude Code