From f2921363d33f5153028f454ab0877cc9aeefb8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= Date: Fri, 13 Nov 2026 07:01:27 +0800 Subject: [PATCH 1/2] kernel: Cap a USB host's current grant and draw none while suspended The Lightning charger took the current a USB host grants as reported; it now caps it at the board's maximum. The gadget no longer draws an Apple host's extra current while the bus is suspended. Both apply to every board with this charger and gadget. From orchard PR #108 (commit 0674e1db), the two kernel patches. --- ...star_charger-Cap-the-USB-host-budget.patch | 84 +++++++++++++++++++ ...-extra-Apple-current-while-suspended.patch | 49 +++++++++++ 2 files changed, 133 insertions(+) create mode 100644 kernel/patches/drivers/power/0030-power-supply-apple_tristar_charger-Cap-the-USB-host-budget.patch create mode 100644 kernel/patches/drivers/usb/0013-usb-gadget-composite-Draw-no-extra-Apple-current-while-suspended.patch diff --git a/kernel/patches/drivers/power/0030-power-supply-apple_tristar_charger-Cap-the-USB-host-budget.patch b/kernel/patches/drivers/power/0030-power-supply-apple_tristar_charger-Cap-the-USB-host-budget.patch new file mode 100644 index 00000000..f9c68af4 --- /dev/null +++ b/kernel/patches/drivers/power/0030-power-supply-apple_tristar_charger-Cap-the-USB-host-budget.patch @@ -0,0 +1,84 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] power: supply: apple_tristar_charger: Cap the USB host budget + +An Apple host can grant more current than the standard USB budget, and +the gadget reports the granted budget to the charger. A stale report +from an earlier connection was already dropped, but a report above the +charger's maximum was passed on as is. + +Cap the reported budget and the input current limit at the charger's +maximum, re-evaluate the source when only the budget changes, and log +the reported and accepted values (debug). + +[Orchard: from orchard PR #108, commit 0674e1db; the budget log is a + debug message] + +Assisted-by: LLM +--- + drivers/power/supply/apple_tristar_charger.c | 24 +++++++++++++++----- + 1 file changed, 18 insertions(+), 6 deletions(-) + +diff --git a/drivers/power/supply/apple_tristar_charger.c b/drivers/power/supply/apple_tristar_charger.c +index a953cedcb..c50bfc861 100644 +--- a/drivers/power/supply/apple_tristar_charger.c ++++ b/drivers/power/supply/apple_tristar_charger.c +@@ -649,7 +649,12 @@ static int tristar_identify(struct tristar_charger *chg, struct tristar_ident *i + */ + static unsigned int tristar_usb_ma(struct tristar_charger *chg) + { +- return chg->usb_epoch == READ_ONCE(chg->epoch) ? chg->usb_ma : 0; ++ if (chg->usb_epoch != READ_ONCE(chg->epoch)) { ++ dev_dbg(chg->dev, "USB budget stale: report epoch %u, source epoch %u\n", ++ chg->usb_epoch, READ_ONCE(chg->epoch)); ++ return 0; ++ } ++ return chg->usb_ma; + } + + /* +@@ -1140,7 +1145,8 @@ static int tristar_get_property(struct power_supply *psy, + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + /* What the identified source or USB host allows; 0 when unknown. */ +- val->intval = max(chg->source_ma, tristar_usb_ma(chg)) * 1000; ++ val->intval = min(max(chg->source_ma, tristar_usb_ma(chg)), ++ chg->max_ma) * 1000; + break; + default: + mutex_unlock(&chg->lock); +@@ -1163,23 +1169,29 @@ static int tristar_set_property(struct power_supply *psy, + const union power_supply_propval *val) + { + struct tristar_charger *chg = power_supply_get_drvdata(psy); +- unsigned int old_code; ++ unsigned int old_code, old_ma, report_ma; + + if (psp != POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT) + return -EINVAL; + + mutex_lock(&chg->lock); + old_code = chg->code; +- chg->usb_ma = chg->present && chg->usb_on ? max(val->intval, 0) / 1000 : 0; ++ old_ma = tristar_usb_ma(chg); ++ report_ma = max(val->intval, 0) / 1000; ++ chg->usb_ma = chg->present && chg->usb_on ? ++ min(report_ma, chg->max_ma) : 0; + chg->usb_epoch = READ_ONCE(chg->epoch); +- dev_dbg(chg->dev, "USB host allows %u mA\n", chg->usb_ma); ++ dev_dbg(chg->dev, ++ "USB budget: reported %u mA, accepted %u mA, present %u, data %u, epoch %u\n", ++ report_ma, chg->usb_ma, chg->present, chg->usb_on, ++ chg->usb_epoch); + if (!chg->stopping) { + tristar_update_target(chg); + mod_delayed_work(chg->wq, &chg->poll_work, 0); + } + mutex_unlock(&chg->lock); + +- if (READ_ONCE(chg->code) != old_code) ++ if (READ_ONCE(chg->code) != old_code || old_ma != READ_ONCE(chg->usb_ma)) + power_supply_changed(psy); + return 0; + } diff --git a/kernel/patches/drivers/usb/0013-usb-gadget-composite-Draw-no-extra-Apple-current-while-suspended.patch b/kernel/patches/drivers/usb/0013-usb-gadget-composite-Draw-no-extra-Apple-current-while-suspended.patch new file mode 100644 index 00000000..a72ea30c --- /dev/null +++ b/kernel/patches/drivers/usb/0013-usb-gadget-composite-Draw-no-extra-Apple-current-while-suspended.patch @@ -0,0 +1,49 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] usb: gadget: composite: draw no extra Apple current while + suspended + +The extra current an Apple host grants must not be drawn while the bus +is suspended. reset_config() could apply it during suspend, because the +suspended flag was cleared only at the end of composite_resume(). Skip +the extra current while suspended and clear the flag before the +configuration is applied again on resume. + +[Orchard: from orchard PR #108, commit 0674e1db; unchanged] + +Assisted-by: LLM +--- + drivers/usb/gadget/composite.c | 5 ++--- + 1 file changed, 2 insertions(+), 3 deletions(-) + +diff --git a/drivers/usb/gadget/composite.c b/drivers/usb/gadget/composite.c +index 1da3effb2..ea76d3918 100644 +--- a/drivers/usb/gadget/composite.c ++++ b/drivers/usb/gadget/composite.c +@@ -959,7 +959,7 @@ static void reset_config(struct usb_composite_dev *cdev) + static void composite_vbus_draw(struct usb_composite_dev *cdev, unsigned int mA) + { + cdev->vbus_draw_ma = mA; +- if (mA && cdev->apple_extra_ma) ++ if (mA && !cdev->suspended && cdev->apple_extra_ma) + mA = max(500 + cdev->apple_extra_ma, 0); + usb_gadget_vbus_draw(cdev->gadget, mA); + } +@@ -2665,6 +2665,7 @@ void composite_resume(struct usb_gadget *gadget) + DBG(cdev, "resume\n"); + if (cdev->driver->resume) + cdev->driver->resume(cdev); ++ cdev->suspended = 0; + if (cdev->config) { + list_for_each_entry(f, &cdev->config->functions, list) { + /* +@@ -2695,8 +2696,6 @@ void composite_resume(struct usb_gadget *gadget) + maxpower = min(maxpower, 100U); + composite_vbus_draw(cdev, maxpower); + } +- +- cdev->suspended = 0; + } + + /*-------------------------------------------------------------------------*/ From 8d28040ecc59da1a0da89097ded7a887f19a3121 Mon Sep 17 00:00:00 2001 From: Henrik Klee Date: Fri, 13 Nov 2026 07:01:27 +0800 Subject: [PATCH 2/2] usb: Installed systems ask a Mac for full charging current macOS grants 2400 mA only to USB product IDs on its own list. Installed Orchard systems now present Apple's vendor ID with product ID 0x1014 from that list, as an interface-association composite so macOS binds its generic composite driver (which delivers the grant) beside the CDC network and serial drivers. A Mac then charges the tablet at full speed; the charger caps the grant at the board maximum. The host CLI recognises 0x1014 as an installed system only with an Orchard serial, and the Linux access rules include it. RAM payloads keep their ID. From orchard PR #108 (commit 0674e1db): the identity there was opt-in and J71t-only; it is now the default for every board. --- boot/initramfs/root/init | 10 +++++++++- docs/device-protocol.md | 8 ++++++++ src/orchard/apple/linux_usb.py | 17 +++++++++++++++-- src/orchard/apple/usb.py | 4 ++-- src/orchard/host_setup.py | 5 +++-- tests/test_host_setup.py | 2 +- tests/test_identity.py | 21 +++++++++++++++++++++ 7 files changed, 59 insertions(+), 8 deletions(-) diff --git a/boot/initramfs/root/init b/boot/initramfs/root/init index 14e6083d..16c7913a 100755 --- a/boot/initramfs/root/init +++ b/boot/initramfs/root/init @@ -101,9 +101,17 @@ usb_gadget() { # the per-boot one if the helper fails: USB must always come up. serial=$(sh /etc/orchard/usb-identity 2>/dev/null) || serial="BOOT:$(cat /proc/sys/kernel/random/boot_id) BOARD:${board#apple,}" + # Apple's vendor ID with a product ID macOS lists among the devices it + # grants 2400 mA (docs/device-protocol.md), so a Mac charges the tablet + # at full speed. As an interface-association composite (class 0xef/2/1) + # macOS binds its generic composite driver, which delivers the grant, + # and still the CDC network and serial drivers. mkdir "$g" && echo 0x05ac > "$g/idVendor" && - echo 0x4145 > "$g/idProduct" && + echo 0x1014 > "$g/idProduct" && + echo 0xef > "$g/bDeviceClass" && + echo 0x02 > "$g/bDeviceSubClass" && + echo 0x01 > "$g/bDeviceProtocol" && mkdir -p "$g/strings/0x409" "$g/configs/c.1/strings/0x409" && echo 'Orchard Linux' > "$g/strings/0x409/manufacturer" && echo "${name:-Linux}" > "$g/strings/0x409/product" && diff --git a/docs/device-protocol.md b/docs/device-protocol.md index 518087d8..f2096f50 100644 --- a/docs/device-protocol.md +++ b/docs/device-protocol.md @@ -17,6 +17,14 @@ the host and device side of an install always come from the same CLI version. ## USB identity +Installed systems present Apple's vendor ID 0x05ac with product ID +0x1014, one macOS lists among the devices it grants 2400 mA, and the +interface-association device class (0xef/2/1), so a Mac charges the tablet +at full speed; the charger still caps any grant at the board's maximum. Hosts +treat 0x1014 as an installed system only with an Orchard serial (below), since +real Apple hardware uses that ID too. Older installed systems use 0x4145; RAM +payloads keep 0x4144. + Installed-system and standard RAM gadgets use `BOARD: ID:` as their USB serial number. The ID is the first 12 hex characters of SHA-256 over `orchard-` followed by the device-tree `serial-number` without NUL diff --git a/src/orchard/apple/linux_usb.py b/src/orchard/apple/linux_usb.py index ee3ccd52..bcbc4a6a 100644 --- a/src/orchard/apple/linux_usb.py +++ b/src/orchard/apple/linux_usb.py @@ -8,9 +8,13 @@ # Apple's vendor ID with the product ID of each stage: 0x1227 DFU, 0x4141 # Pongo; Orchard's own USB gadget then uses 0x4142 for recovery, 0x4144 for # a RAM payload's session, 0x4145 for an installed system and 0x4146 for the -# initrd with the install add-on (install_direct.py). +# initrd with the install add-on (install_direct.py). Installed systems now +# use 0x1014, a product ID macOS grants 2400 mA, so a Mac charges them at full +# speed; that ID also belongs to real Apple hardware, so only an Orchard +# serial (BOARD: with ID: or BOOT:) makes it an installed system. APPLE = 0x05ac MODES = {0x1227: 'dfu', 0x4141: 'pongo', 0x4142: 'linux', 0x4144: 'ram', 0x4145: 'system', 0x4146: 'install'} +SYSTEM_MAC_CURRENT = 0x1014 # The stages served by Orchard's gadget, which also has a serial console. SERIAL_MODES = ('linux', 'ram', 'system', 'install') # m1n1's USB proxy (vendor and product ID). @@ -36,6 +40,15 @@ def stable_id(serial): return match.group(1).lower() if match else None +def mode(pid, serial): + """The stage of an Apple-vendor USB device from its product ID and serial.""" + if pid == SYSTEM_MAC_CURRENT: + orchard = re.search(r'\bBOARD:[a-z0-9]+\b', serial) and ( + stable_id(serial) or re.search(r'\bBOOT:[0-9a-fA-F-]{36}\b', serial)) + return 'system' if orchard else 'other' + return MODES.get(pid, 'other') + + def devices(root=Path('/sys/bus/usb/devices')): found = [] for node in root.iterdir(): @@ -57,7 +70,7 @@ def devices(root=Path('/sys/bus/usb/devices')): if len(ports) > 1: raise ValueError('Ambiguous USB serial ports on a device') product = (node / 'product').read_text().strip() if (node / 'product').exists() else '' - found.append(dict(location=node.name, mode=MODES.get(pid, 'other'), + found.append(dict(location=node.name, mode=mode(pid, serial), ecid=int(ecid.group(1), 16) if ecid else None, boot_id=str(uuid.UUID(boot.group(1))) if boot else None, usb_id=usb_id, diff --git a/src/orchard/apple/usb.py b/src/orchard/apple/usb.py index c53aa021..c5426fef 100644 --- a/src/orchard/apple/usb.py +++ b/src/orchard/apple/usb.py @@ -5,7 +5,7 @@ import sys import uuid -from .linux_usb import MODES, M1N1, devices as linux_devices, m1n1_ports as linux_m1n1_ports, serial_tags, stable_id +from .linux_usb import MODES, M1N1, devices as linux_devices, m1n1_ports as linux_m1n1_ports, serial_tags, stable_id, mode as usb_mode from orchard.state import EndpointUnavailable @@ -48,7 +48,7 @@ def devices(): ports.append(port) if len(ports) > 1: raise DeviceError('Ambiguous USB serial ports on the selected device') - found.append({'location': location, 'mode': MODES.get(node.get('idProduct'), 'other'), + found.append({'location': location, 'mode': usb_mode(node.get('idProduct'), serial), 'ecid': int(fields[0].group(1), 16) if fields[0] else None, 'boot_id': str(uuid.UUID(boot.group(1))) if boot else None, 'usb_id': usb_id, diff --git a/src/orchard/host_setup.py b/src/orchard/host_setup.py index a067bb06..facd3d9d 100644 --- a/src/orchard/host_setup.py +++ b/src/orchard/host_setup.py @@ -12,7 +12,7 @@ from typing import Callable, Optional from . import tools -from .apple.linux_usb import APPLE, M1N1, MODES, SERIAL_MODES +from .apple.linux_usb import APPLE, M1N1, MODES, SERIAL_MODES, SYSTEM_MAC_CURRENT from .progress import Progress from .transports.pongo import GLU, library_path @@ -24,7 +24,8 @@ def usb_rules(group): raise ValueError('Unsupported USB access group name') lines = ['# Orchard: every stage of the device and m1n1, with their serial consoles.'] access = f'MODE="0660", GROUP="{group}", TAG+="uaccess"' - devices = [(APPLE, pid, mode in SERIAL_MODES) for pid, mode in MODES.items()] + [(*M1N1, True)] + devices = [(APPLE, pid, mode in SERIAL_MODES) for pid, mode in MODES.items()] + devices += [(APPLE, SYSTEM_MAC_CURRENT, True), (*M1N1, True)] for vendor, product, serial in devices: lines.append(f'SUBSYSTEM=="usb", ATTR{{idVendor}}=="{vendor:04x}", ATTR{{idProduct}}=="{product:04x}", {access}') if serial: diff --git a/tests/test_host_setup.py b/tests/test_host_setup.py index d0741e2d..47d9d28c 100644 --- a/tests/test_host_setup.py +++ b/tests/test_host_setup.py @@ -46,7 +46,7 @@ def run(argv, **kwargs): self.assertEqual(first, second) self.assertEqual(download.call_count, 1) self.assertEqual(commands.call_count, 2) # install rules, reload; never palera/device commands. - self.assertEqual(rule.read_text().count('MODE="0660"'), 12) + self.assertEqual(rule.read_text().count('MODE="0660"'), 14) for pid in ('4142', '4144', '4145', '4146'): self.assertIn(f'SUBSYSTEM=="tty", ATTRS{{idVendor}}=="05ac", ATTRS{{idProduct}}=="{pid}"', rule.read_text()) diff --git a/tests/test_identity.py b/tests/test_identity.py index 5a9524cc..274ba4fd 100644 --- a/tests/test_identity.py +++ b/tests/test_identity.py @@ -182,3 +182,24 @@ def test_installed_and_ram_gadgets_use_the_same_stable_serial_before_binding(sel if __name__ == '__main__': unittest.main() + + +class MacCurrentIdentity(unittest.TestCase): + def test_installed_systems_use_the_mac_current_product_id_only_with_an_orchard_serial(self): + from orchard.apple import linux_usb + self.assertEqual(linux_usb.mode(0x1014, 'BOARD:j71b ID:0123456789ab'), 'system') + self.assertEqual(linux_usb.mode(0x1014, 'BOOT:11111111-1111-4111-8111-111111111111 BOARD:j71b'), 'system') + self.assertEqual(linux_usb.mode(0x1014, 'DWH12345678'), 'other') + self.assertEqual(linux_usb.mode(0x1014, 'BOARD:j71b'), 'other') + self.assertEqual(linux_usb.mode(0x4145, 'BOARD:j71b ID:0123456789ab'), 'system') + self.assertEqual(linux_usb.mode(0x4144, ''), 'ram') + + def test_the_installed_gadget_asks_for_the_mac_current_identity(self): + init = (ROOT / 'boot/initramfs/root/init').read_text() + gadget = init[init.index('usb_gadget() {'):init.index('\nusb_gadget ||')] + self.assertIn('echo 0x1014 > "$g/idProduct"', gadget) + self.assertIn('echo 0xef > "$g/bDeviceClass"', gadget) + + def test_linux_hosts_get_access_to_the_mac_current_identity(self): + from orchard import host_setup + self.assertIn('ATTRS{idProduct}=="1014"', host_setup.usb_rules('plugdev'))