Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion boot/initramfs/root/init
Original file line number Diff line number Diff line change
Expand Up @@ -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" &&
Expand Down
8 changes: 8 additions & 0 deletions docs/device-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<profile> ID:<hash>` 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= <brkosebay@users.noreply.github.com>
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= <brkosebay@users.noreply.github.com>
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;
}

/*-------------------------------------------------------------------------*/
17 changes: 15 additions & 2 deletions src/orchard/apple/linux_usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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():
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/orchard/apple/usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions src/orchard/host_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_host_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
21 changes: 21 additions & 0 deletions tests/test_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Loading