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/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; + } + + /*-------------------------------------------------------------------------*/ 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'))