├── .gitignore
├── README.md
└── iap2
├── __init__.py
├── __main__.py
├── carplay_bonjour.py
├── control_session_message
├── __init__.py
├── authentication.py
├── car_play.py
├── eap.py
├── identification.py
├── vehicle_status.py
└── wifi.py
├── link_layer.py
├── mfi_auth_coprocessor.py
├── requirements.txt
├── tests
├── __init__.py
├── test_control_session_message.py
├── test_link_layer.py
└── utils.py
└── transport
├── __init__.py
├── bluetooth.py
├── usb_device.py
└── usb_host.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | htmlcov/
3 | .coverage
4 | .idea/
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | More details on my [blogpost](https://wiomoc.de/misc/posts/mfi_iap.html)
2 |
--------------------------------------------------------------------------------
/iap2/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["control_session_message", "mfi_auth_coprocessor", "link_layer"]
2 |
3 | import iap2.tests
4 |
--------------------------------------------------------------------------------
/iap2/__main__.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from iap2.control_session_message.wifi import AccessoryWiFiConfigurationInformation, \
4 | RequestAccessoryWiFiConfigurationInformation, SecurityType
5 | from iap2.control_session_message.car_play import DeviceTransportIdentifierNotification, WirelessCarPlayUpdate, \
6 | WirelessCarPlayStatus
7 | from iap2.control_session_message import read_csm, register_csm, write_csm, Uint16, Uint8
8 | from iap2.control_session_message.identification import IdentificationRejected, IdentificationAccepted, \
9 | StartIdentification, IdentificationInformation, PowerProvidingCapability, ExternalAccessoryProtocol, MatchAction, \
10 | BluetoothTransportComponent, VehicleInformationComponent, EngineType, VehicleStatusComponent, \
11 | WirelessCarPlayTransportComponent
12 | from iap2.control_session_message.eap import StartExternalAccessoryProtocolSession, StopExternalAccessoryProtocolSession
13 | from iap2.control_session_message.vehicle_status import StartVehicleStatusUpdates, StopVehicleStatusUpdates, \
14 | VehicleStatusUpdate
15 | from iap2.mfi_auth_coprocessor import read_certificate, generate_challenge_response
16 | from iap2.link_layer import IAP2Connection
17 | from iap2.transport.bluetooth import BluetoothTransport
18 | from iap2.control_session_message.authentication import RequestAuthenticationCertificate, AuthenticationCertificate, \
19 | RequestAuthenticationChallengeResponse, AuthenticationResponse, AuthenticationSucceeded, AuthenticationFailed
20 |
21 |
22 | if __name__ == '__main__':
23 | loop = asyncio.get_event_loop()
24 | register_csm(RequestAuthenticationCertificate)
25 | register_csm(RequestAuthenticationChallengeResponse)
26 | register_csm(AuthenticationSucceeded)
27 | register_csm(AuthenticationFailed)
28 |
29 | register_csm(StartIdentification)
30 | register_csm(IdentificationAccepted)
31 | register_csm(IdentificationRejected)
32 |
33 | register_csm(StartVehicleStatusUpdates)
34 | register_csm(StopVehicleStatusUpdates)
35 |
36 | register_csm(DeviceTransportIdentifierNotification)
37 | register_csm(WirelessCarPlayUpdate)
38 |
39 | register_csm(RequestAccessoryWiFiConfigurationInformation)
40 |
41 |
42 | async def handle_auth(stream, cert):
43 | while True:
44 | incoming_message = await read_csm(stream)
45 | print(incoming_message)
46 | if isinstance(incoming_message, RequestAuthenticationCertificate):
47 | await write_csm(stream, AuthenticationCertificate(certificate=cert))
48 | elif isinstance(incoming_message, RequestAuthenticationChallengeResponse):
49 | response = await loop.run_in_executor(None,
50 | lambda: generate_challenge_response(incoming_message.challenge))
51 | await write_csm(stream, AuthenticationResponse(response=response))
52 | elif isinstance(incoming_message, AuthenticationSucceeded):
53 | return
54 | else:
55 | raise Exception("auth failed")
56 |
57 |
58 | async def handle_identification(stream):
59 | def messages_ids(*messages):
60 | from struct import Struct
61 | word = Struct(">H")
62 | return b''.join((word.pack(m.CSM_MSG_ID) for m in messages))
63 |
64 | while True:
65 | incoming_message = await read_csm(stream)
66 | print("incoming", str(incoming_message))
67 | if isinstance(incoming_message, StartIdentification):
68 | identification = IdentificationInformation(
69 | name="raspberrypi",
70 | model_identifier="raspberrypi",
71 | manufacturer="wiomoc",
72 | serial_number="0122349",
73 | fireware_version="1.0.1",
74 | hardware_version="2.0",
75 | messages_sent_by_accessory=messages_ids(VehicleStatusUpdate,
76 | AccessoryWiFiConfigurationInformation),
77 | messages_received_from_accessory=messages_ids(StartExternalAccessoryProtocolSession,
78 | StopExternalAccessoryProtocolSession,
79 | StartVehicleStatusUpdates,
80 | StopVehicleStatusUpdates,
81 | WirelessCarPlayUpdate,
82 | DeviceTransportIdentifierNotification,
83 | RequestAccessoryWiFiConfigurationInformation),
84 | power_providing_capability=PowerProvidingCapability.NONE,
85 | maximum_current_drawn_from_device=Uint16(20),
86 | supported_external_accessory_protocol=[ExternalAccessoryProtocol(
87 | id=Uint8(1),
88 | name="de.wiomoc.test",
89 | match_action=MatchAction.NONE,
90 | )],
91 | current_language="de",
92 | supported_language=["de", "en"],
93 | app_match_team_id=None,
94 | bluetooth_transport_component=[BluetoothTransportComponent(
95 | id=Uint16(0),
96 | name="blue",
97 | supports_iap2_connection=True,
98 | bluetooth_transport_mac=b'\xB8\x27\xEB\x23\x6A\xF4'
99 | )],
100 | vehicle_information_component=VehicleInformationComponent(
101 | id=Uint16(0),
102 | name="Tesla Model X",
103 | engine_type=EngineType.ELECTRIC
104 | ),
105 | vehicle_status_component=VehicleStatusComponent(
106 | id=Uint16(0),
107 | name="Tesla Model X",
108 | range_warning=True
109 | ),
110 | wireless_car_play_transport_component=WirelessCarPlayTransportComponent(
111 | id=Uint16(1),
112 | name="raspberrypi",
113 | supports_iap2_connection=True,
114 | supports_car_play=True
115 | )
116 | )
117 | await write_csm(stream, identification)
118 | elif isinstance(incoming_message, IdentificationAccepted):
119 | return
120 | else:
121 | raise Exception("identification failed")
122 |
123 |
124 | async def main():
125 |
126 | def on_connection(reader, writer):
127 | print(reader, writer)
128 |
129 | async def iap_handler():
130 | cert = await loop.run_in_executor(None, lambda: read_certificate())
131 |
132 | conn = IAP2Connection(writer, reader, loop, max_outgoing=4)
133 | conn.start()
134 | stream = conn.control_session
135 | await handle_auth(stream, cert)
136 | await handle_identification(stream)
137 |
138 | while True:
139 | incoming = await read_csm(stream)
140 | print(incoming)
141 | if isinstance(incoming, RequestAccessoryWiFiConfigurationInformation):
142 | info = AccessoryWiFiConfigurationInformation(
143 | ssid="teslamodelx",
144 | passphrase="testtest12",
145 | security_type=SecurityType.WPA_WPA2,
146 | channel=Uint8(10)
147 | )
148 | await write_csm(stream, info)
149 |
150 | loop.create_task(iap_handler())
151 |
152 | BluetoothTransport(on_connection, loop)
153 |
154 |
155 | loop.create_task(main())
156 | loop.run_forever()
157 |
--------------------------------------------------------------------------------
/iap2/carplay_bonjour.py:
--------------------------------------------------------------------------------
1 | import dbus
2 | import avahi
3 |
4 |
5 | def start_service(device_id):
6 | bus = dbus.SystemBus()
7 | server = dbus.Interface(bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER)
8 |
9 | group = dbus.Interface(bus.get_object(avahi.DBUS_NAME, server.EntryGroupNew()),
10 | avahi.DBUS_INTERFACE_ENTRY_GROUP)
11 |
12 | group.AddService(avahi.IF_UNSPEC, avahi.PROTO_INET, 0, "raspberrypi", "_airplay._tcp", '', '', 7000,
13 | avahi.string_array_to_txt_array([
14 | f"deviceID={device_id}",
15 | "features=0x44540380,0x21",
16 | "model=raspberrypi",
17 | "srcvers=280.33.8",
18 | "flags=0x4"]))
19 | group.Commit()
20 |
21 | def on_service(interface, protocol, name, type, domain, flags):
22 | try:
23 | interface, protocol, name, type, domain, host, aprotocol, address, port, txt, flags = server.ResolveService(
24 | interface, protocol, name, type, domain, avahi.PROTO_INET, 0)
25 | print(txt)
26 | txt = [''.join((str(t) for t in txt_entry)) for txt_entry in txt]
27 | print(host, port, txt)
28 |
29 | mac_int = int(device_id.replace(":",""), 16)
30 | from urllib import request
31 | ip = "192.168.2.10"
32 | req = request.Request(f"http://{host}:{port}/ctrl-int/1/connect", headers={
33 | "Host": host,
34 | "User-Agent": "AirPlay/280.33.8",
35 | "AirPlay-Receiver-Device-ID": str(mac_int),
36 | })
37 | print(request.urlopen(req).read())
38 | except:
39 | pass
40 |
41 | browser = server.ServiceBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_INET, '_carplay-ctrl._tcp', 'local', 0)
42 | bus.add_signal_receiver(on_service, "ItemNew", path=browser)
43 |
44 |
--------------------------------------------------------------------------------
/iap2/control_session_message/__init__.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 | from struct import Struct
3 | from typing import get_type_hints, NewType, get_args, get_origin, Annotated, Dict, Type, Optional, Union
4 | from warnings import warn
5 |
6 | CSM_STRUCT = Struct(">HHH")
7 | CSM_PARAM_STRUCT = Struct(">HH")
8 | CSM_START = 0x4040
9 |
10 | _MESSAGE_TYPES: Dict[int, Type] = dict()
11 |
12 |
13 | def register_csm(csm_class):
14 | _MESSAGE_TYPES[csm_class.CSM_MSG_ID] = csm_class
15 |
16 |
17 | async def read_csm(reader):
18 | start, length, msg_id = CSM_STRUCT.unpack(
19 | await reader.readexactly(6))
20 | if start != CSM_START:
21 | return
22 | payload = await reader.readexactly(length - 6)
23 | message_type = _MESSAGE_TYPES.get(msg_id)
24 | if message_type:
25 | message_instance = message_type.__new__(message_type)
26 | message_instance.csm_deserialize_params(payload)
27 | return message_instance
28 | else:
29 | return None
30 |
31 |
32 | async def write_csm(writer, message):
33 | writer.write(message.csm_serialize())
34 | await writer.drain()
35 |
36 |
37 | Int8 = NewType("Int8", int)
38 | Int16 = NewType("Int16", int)
39 | Int32 = NewType("Int32", int)
40 | Int64 = NewType("Int64", int)
41 | Uint8 = NewType("Uint8", int)
42 | Uint16 = NewType("Uint16", int)
43 | Uint32 = NewType("Uint32", int)
44 | Uint64 = NewType("Uint64", int)
45 | NoneLike = NewType("None", Optional[bool])
46 |
47 |
48 | def csm(msg_id: int):
49 | def decorator(clazz):
50 | def build_deserialize_params(handlers):
51 | def deserialize_params(self, payload):
52 | handlers_clone = handlers.copy()
53 | while len(payload) > 0:
54 | length, param_id = CSM_PARAM_STRUCT.unpack(
55 | payload[:4])
56 | param_payload = payload[4:length]
57 | payload = payload[length:]
58 | if param_id in handlers_clone:
59 | _serializer, deserializer, name, is_list, _is_optional = handlers_clone.pop(param_id)
60 | value = deserializer(param_payload)
61 | if is_list:
62 | value_list = []
63 | value_list.append(value)
64 | setattr(self, name, value_list)
65 | else:
66 | setattr(self, name, value)
67 | for _serializer, _deserializer, name, is_list, is_optional in handlers_clone.values():
68 | if not is_optional and not is_list:
69 | raise ValueError(f"{name} is not optional")
70 | setattr(self, name, [] if is_list else None)
71 |
72 | return deserialize_params
73 |
74 | def build_serialize_params(handlers):
75 | def serialize_params(self):
76 | params_bytes = bytearray()
77 | for param_id, (serializer, _deserializer, name, is_list, is_optional) in handlers.items():
78 | value = getattr(self, name)
79 |
80 | if value is not None:
81 | if is_list:
82 | if len(value) == 0:
83 | continue
84 | payload = b''.join((serializer(val) for val in value))
85 | else:
86 | payload = serializer(value)
87 | params_bytes.extend(CSM_PARAM_STRUCT.pack(
88 | len(payload) + 4, param_id) + payload)
89 | elif not is_optional and not is_list:
90 | warn(f"{name} is not optional")
91 | return params_bytes
92 |
93 | return serialize_params
94 |
95 | def build_handlers(clazz):
96 | handlers = dict()
97 | hints = get_type_hints(clazz, include_extras=True).items()
98 | base_class = clazz.__base__
99 | if base_class != object and base_class is not None:
100 | extended_hints = []
101 | extended_hints.extend(get_type_hints(base_class, include_extras=True).items())
102 | extended_hints.extend(hints)
103 | hints = extended_hints
104 |
105 | for param_id, (name, hint) in enumerate(hints):
106 | is_list = False
107 | is_optional = False
108 | if get_origin(hint) == Annotated:
109 | args = get_args(hint)
110 | param_id = args[1]
111 | hint = args[0]
112 | if get_origin(hint) == Union:
113 | args = get_args(hint)
114 | if args[1] == type(None):
115 | is_optional = True
116 | hint = args[0]
117 | if get_origin(hint) == list:
118 | is_list = True
119 | args = get_args(hint)
120 | hint = args[0]
121 | s = None
122 | if hint == bool:
123 | s = Struct(">?")
124 | if hint == Int8:
125 | s = Struct(">b")
126 | elif hint == Uint8:
127 | s = Struct(">B")
128 | elif hint == Int16:
129 | s = Struct(">h")
130 | elif hint == Uint16:
131 | s = Struct(">H")
132 | elif hint == Int32:
133 | s = Struct(">i")
134 | elif hint == Uint32:
135 | s = Struct(">I")
136 | elif hint == Int64:
137 | s = Struct(">q")
138 | elif hint == Uint64:
139 | s = Struct(">Q")
140 |
141 | serializer = None
142 | deserializer = None
143 | if s is not None:
144 | serializer = lambda val, s=s: s.pack(val)
145 | deserializer = lambda buffer, s=s: s.unpack(buffer)[0] if len(buffer) > 0 else None
146 | elif hint == NoneLike or hint == type(None):
147 | is_optional = True
148 | serializer = lambda val: b""
149 | deserializer = lambda buffer: True
150 | elif issubclass(hint, IntEnum):
151 | serializer = lambda val: bytes([val.value])
152 | deserializer = lambda buffer, hint=hint: hint(buffer[0])
153 | elif hint == str:
154 | serializer = lambda val: val.encode("utf-8") + b"\0"
155 | deserializer = lambda buffer: buffer[:-1].decode("utf-8")
156 | elif hint == bytes:
157 | serializer = lambda val: val
158 | deserializer = lambda buffer: buffer
159 | elif isinstance(hint, type) and hint is not None:
160 | group_handlers = build_handlers(hint)
161 | serializer = build_serialize_params(group_handlers)
162 |
163 | def de(buffer, de_params=build_deserialize_params(group_handlers), param_class=hint):
164 | instance = param_class.__new__(param_class)
165 | de_params(instance, buffer)
166 | return instance
167 |
168 | deserializer = de
169 |
170 | if not serializer:
171 | raise TypeError("Invalid type for csm")
172 |
173 | handlers[param_id] = (serializer, deserializer, name, is_list, is_optional)
174 | return handlers
175 |
176 | message_handlers = build_handlers(clazz)
177 |
178 | setattr(clazz, "csm_deserialize_params", build_deserialize_params(message_handlers))
179 | message_serialize_params = build_serialize_params(message_handlers)
180 |
181 | def serialize(self):
182 | params_bytes = message_serialize_params(self)
183 | header_bytes = CSM_STRUCT.pack(
184 | CSM_START,
185 | len(params_bytes) + 6, msg_id)
186 | return header_bytes + params_bytes
187 |
188 | setattr(clazz, "csm_serialize", serialize)
189 | setattr(clazz, "CSM_MSG_ID", msg_id)
190 |
191 | return clazz
192 |
193 | return decorator
194 |
--------------------------------------------------------------------------------
/iap2/control_session_message/authentication.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | from iap2.control_session_message import csm
4 |
5 |
6 | @csm(0xAA00)
7 | class RequestAuthenticationCertificate:
8 | pass
9 |
10 |
11 | @csm(0xAA01)
12 | @dataclass
13 | class AuthenticationCertificate:
14 | certificate: bytes
15 |
16 |
17 | @csm(0xAA02)
18 | class RequestAuthenticationChallengeResponse:
19 | challenge: bytes
20 |
21 |
22 | @csm(0xAA03)
23 | @dataclass
24 | class AuthenticationResponse:
25 | response: bytes
26 |
27 |
28 | @csm(0xAA04)
29 | class AuthenticationFailed:
30 | pass
31 |
32 |
33 | @csm(0xAA05)
34 | class AuthenticationSucceeded:
35 | pass
36 |
--------------------------------------------------------------------------------
/iap2/control_session_message/car_play.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from enum import IntEnum
3 |
4 | from iap2.control_session_message import csm, Uint16, Uint8
5 |
6 |
7 | @csm(0x4E0E)
8 | @dataclass
9 | class DeviceTransportIdentifierNotification:
10 | bluetooth_transport_id: str
11 | usb_transport_id: str
12 |
13 |
14 | class WirelessCarPlayStatus(IntEnum):
15 | UNAVAILABLE = 0
16 | AVAILABLE = 1
17 |
18 |
19 | @csm(0x4E0D)
20 | @dataclass
21 | class WirelessCarPlayUpdate:
22 | status: WirelessCarPlayStatus
23 |
--------------------------------------------------------------------------------
/iap2/control_session_message/eap.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 | from iap2.control_session_message import csm, Uint16, Uint8
4 |
5 |
6 | @csm(0xEA00)
7 | class StartExternalAccessoryProtocolSession:
8 | protocol_id: Uint8
9 | session_id: Uint16
10 |
11 |
12 | @csm(0xEA01)
13 | class StopExternalAccessoryProtocolSession:
14 | session_id: Uint16
15 |
16 |
17 | class SessionStatus(IntEnum):
18 | OK = 0
19 | CLOSE = 1
20 |
21 |
22 | @csm(0xEA03)
23 | class StatusExternalAccessoryProtocolSession:
24 | session_id: Uint16
25 | status: SessionStatus
26 |
--------------------------------------------------------------------------------
/iap2/control_session_message/identification.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from enum import IntEnum
3 | from typing import List, Annotated, Optional
4 |
5 | from iap2.control_session_message import csm, Uint16, Uint8, NoneLike
6 |
7 |
8 | class PowerProvidingCapability(IntEnum):
9 | NONE = 0
10 | RESERVED = 1
11 | ADVANCED = 2
12 |
13 |
14 | class MatchAction(IntEnum):
15 | NONE = 0
16 | SETTINGS_AND_PROMPT = 1
17 | SETTINGS_ONLY = 2
18 |
19 |
20 | @dataclass
21 | class ExternalAccessoryProtocol:
22 | id: Uint8
23 | name: str
24 | match_action: MatchAction
25 | native_transport_component_identifier: Optional[Uint16] = None
26 |
27 |
28 | @dataclass
29 | class TransportComponent:
30 | id: Uint16
31 | name: str
32 | supports_iap2_connection: NoneLike = None
33 |
34 |
35 | @dataclass
36 | class SerialTransportComponent(TransportComponent):
37 | pass
38 |
39 |
40 | @dataclass
41 | class BluetoothTransportComponent(TransportComponent):
42 | bluetooth_transport_mac: Annotated[bytes, 3] = None
43 |
44 |
45 | @dataclass
46 | class USBDeviceTransportComponent(TransportComponent):
47 | audio_sample_rate: Annotated[Optional[Uint8], 3] = None # Fixme
48 |
49 |
50 | @dataclass
51 | class WirelessCarPlayTransportComponent(TransportComponent):
52 | supports_car_play: Annotated[NoneLike, 4] = None
53 |
54 |
55 | @dataclass
56 | class USBHostTransportComponent(TransportComponent):
57 | car_play_interface_number: Annotated[Optional[Uint8], 3] = None
58 |
59 |
60 | class EngineType(IntEnum):
61 | GAS = 0
62 | DIESEL = 1
63 | ELECTRIC = 2
64 | CNG = 3
65 |
66 |
67 | @dataclass
68 | class VehicleInformationComponent:
69 | id: Uint16
70 | name: str
71 | engine_type: EngineType
72 |
73 |
74 | @dataclass
75 | class VehicleStatusComponent:
76 | id: Uint16
77 | name: str
78 | range: Annotated[NoneLike, 3] = None
79 | outside_temperature: Annotated[NoneLike, 4] = None
80 | range_warning: Annotated[NoneLike, 5] = None
81 |
82 |
83 | @csm(0x1D00)
84 | class StartIdentification:
85 | pass
86 |
87 |
88 | @csm(0x1D01)
89 | @dataclass
90 | class IdentificationInformation:
91 | name: str
92 | model_identifier: str
93 | manufacturer: str
94 | serial_number: str
95 | fireware_version: str
96 | hardware_version: str
97 | messages_sent_by_accessory: bytes
98 | messages_received_from_accessory: bytes
99 | power_providing_capability: PowerProvidingCapability
100 | maximum_current_drawn_from_device: Uint16
101 | supported_external_accessory_protocol: List[ExternalAccessoryProtocol]
102 | app_match_team_id: Optional[str]
103 | current_language: str
104 | supported_language: List[str]
105 | serial_transport_component: List[SerialTransportComponent] = field(default_factory=list)
106 | usb_device_transport_component: List[USBDeviceTransportComponent] = field(default_factory=list)
107 | usb_host_transport_component: List[USBHostTransportComponent] = field(default_factory=list)
108 | bluetooth_transport_component: List[BluetoothTransportComponent] = field(default_factory=list)
109 | vehicle_information_component: Annotated[Optional[VehicleInformationComponent], 20] = None
110 | vehicle_status_component: Annotated[Optional[VehicleStatusComponent], 21] = None
111 | wireless_car_play_transport_component: Annotated[Optional[WirelessCarPlayTransportComponent], 24] = None
112 |
113 |
114 | @csm(0x1D02)
115 | class IdentificationAccepted:
116 | pass
117 |
118 |
119 | @csm(0x1D03)
120 | @dataclass
121 | class IdentificationRejected:
122 | name: NoneLike
123 | model_identifier: NoneLike
124 | manufacturer: NoneLike
125 | serial_number: NoneLike
126 | fireware_version: NoneLike
127 | hardware_version: NoneLike
128 | messages_sent_by_accessory: NoneLike
129 | messages_received_from_accessory: NoneLike
130 | power_providing_capability: NoneLike
131 | maximum_current_drawn_from_device: NoneLike
132 | supported_external_accessory_protocol: NoneLike
133 | app_match_team_id: NoneLike
134 | current_language: NoneLike
135 | supported_language: NoneLike
136 | serial_transport_component: NoneLike
137 | usb_device_transport_component: NoneLike
138 | usb_host_transport_component: NoneLike
139 | bluetooth_transport_component: NoneLike
140 | vehicle_information_component: Annotated[NoneLike, 20]
141 | vehicle_status_component: Annotated[NoneLike, 21]
142 | wireless_car_play_transport_component: Annotated[NoneLike, 24]
143 |
--------------------------------------------------------------------------------
/iap2/control_session_message/vehicle_status.py:
--------------------------------------------------------------------------------
1 | from typing import Annotated
2 |
3 | from iap2.control_session_message import csm, Uint16, Int16
4 |
5 |
6 | @csm(0xA100)
7 | class StartVehicleStatusUpdates:
8 | pass
9 |
10 |
11 | @csm(0xA101)
12 | class VehicleStatusUpdate:
13 | range: Annotated[Uint16, 3]
14 | outside_temperature: Annotated[Int16, 4]
15 | range_warning: Annotated[bool, 5]
16 |
17 |
18 | @csm(0xA102)
19 | class StopVehicleStatusUpdates:
20 | pass
21 |
--------------------------------------------------------------------------------
/iap2/control_session_message/wifi.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from enum import IntEnum
3 | from typing import Optional, Annotated
4 |
5 | from iap2.control_session_message import csm, Uint8
6 |
7 |
8 | @csm(0x5700)
9 | class RequestWiFiInformation:
10 | pass
11 |
12 |
13 | class WiFiRequestStatus(IntEnum):
14 | SUCCESS = 0
15 | USER_DECLINED = 1
16 | NET_WORK_INFORMATION_UNAVAILABLE = 2
17 |
18 |
19 | @csm(0x5701)
20 | class WiFiInformation:
21 | status: WiFiRequestStatus
22 | ssid: Optional[str]
23 | passphrase: Optional[str]
24 |
25 |
26 | @csm(0x5702)
27 | class RequestAccessoryWiFiConfigurationInformation:
28 | pass
29 |
30 |
31 | class SecurityType(IntEnum):
32 | NONE = 0
33 | WEP_NEW = 1
34 | WPA_WPA2 = 2
35 |
36 |
37 | @csm(0x5703)
38 | @dataclass
39 | class AccessoryWiFiConfigurationInformation:
40 | ssid: Annotated[Optional[str], 1]
41 | passphrase: Annotated[Optional[str], 2]
42 | security_type: Annotated[SecurityType, 3]
43 | channel: Annotated[Uint8, 4]
44 |
--------------------------------------------------------------------------------
/iap2/link_layer.py:
--------------------------------------------------------------------------------
1 | __all__ = ["IAP2Connection", "IAP2Stream"]
2 |
3 | import asyncio
4 | from collections import namedtuple
5 | from dataclasses import dataclass
6 | from functools import reduce
7 | from struct import Struct
8 | from typing import ClassVar, List, Callable, Any
9 |
10 | CONTROL_SYN = 0x80
11 | CONTROL_ACK = 0x40
12 | CONTROL_EAK = 0x20
13 | CONTROL_RST = 0x10
14 |
15 | loop = asyncio.get_event_loop()
16 |
17 |
18 | @dataclass
19 | class LinkPacketHeader:
20 | struct: ClassVar = Struct(">HHBBBB")
21 | start: ClassVar = 0xFF5A
22 | length: int
23 | control: int
24 | seq: int
25 | ack: int
26 | session_id: int
27 |
28 | @staticmethod
29 | def from_bytes(header_bytes):
30 | if not check_checksum(header_bytes):
31 | return None
32 | (start, length, control, seq, ack,
33 | session_id) = LinkPacketHeader.struct.unpack(header_bytes[:-1])
34 | if start != LinkPacketHeader.start:
35 | return None
36 | return LinkPacketHeader(length, control, seq, ack, session_id)
37 |
38 | def pack(self):
39 | header_bytes = LinkPacketHeader.struct.pack(LinkPacketHeader.start,
40 | self.length, self.control,
41 | self.seq, self.ack,
42 | self.session_id)
43 | return header_bytes + bytes([gen_checksum(header_bytes)])
44 |
45 |
46 | def signed_add(a, b):
47 | return (a + b) & 0xff
48 |
49 |
50 | def gen_checksum(packet):
51 | return -reduce(signed_add, packet) & 0xff
52 |
53 |
54 | def check_checksum(packet):
55 | return reduce(signed_add, packet) == 0
56 |
57 |
58 | LSPSession = namedtuple('LSPSession', 'id type version')
59 |
60 |
61 | @dataclass
62 | class LinkSynchronizationPayload:
63 | struct: ClassVar = Struct(">BBHHHBB")
64 | version: ClassVar = 0x01
65 | max_outgoing: int
66 | max_len: int
67 | retransmission_timeout: int
68 | ack_timeout: int
69 | max_retransmissions: int
70 | max_ack: int
71 | sessions: list
72 |
73 | @staticmethod
74 | def from_bytes(payload):
75 | (version, max_outgoing, max_len, retransmission_timeout, ack_timeout,
76 | max_retransmissions,
77 | max_ack) = LinkSynchronizationPayload.struct.unpack(payload[:10])
78 | if version != LinkSynchronizationPayload.version:
79 | return None
80 |
81 | sessions_bytes = payload[10:]
82 | sessions = []
83 | for i in range(0, len(sessions_bytes), 3):
84 | sessions.append(LSPSession._make(sessions_bytes[i:i + 3]))
85 | return LinkSynchronizationPayload(max_outgoing, max_len,
86 | retransmission_timeout, ack_timeout,
87 | max_retransmissions, max_ack,
88 | sessions)
89 |
90 | def pack(self):
91 | payload = LinkSynchronizationPayload.struct.pack(
92 | LinkSynchronizationPayload.version, self.max_outgoing,
93 | self.max_len, self.retransmission_timeout, self.ack_timeout,
94 | self.max_retransmissions, self.max_ack)
95 | return payload + b''.join([bytes([*s]) for s in self.sessions])
96 |
97 |
98 | STATE_DETECT_IAP2_SUPPORT = 0
99 | STATE_NEGOTIATE = 1
100 | STATE_NORMAL = 2
101 | STATE_DEAD = 3
102 |
103 | IAP2_MARKER = b'\xFF\x55\x02\x00\xEE\x10'
104 | EA_SESSION_ID_STRUCT = Struct(">H")
105 |
106 |
107 | class IAP2Packet:
108 | def __init__(self, data: bytes, psn: int = None, session_id: int = 0):
109 | self.psn = psn
110 | self.data = data
111 | self.session_id = session_id
112 |
113 |
114 | class IAP2Stream:
115 | def __init__(self, conn: "IAP2Connection", session_id: int, stream_id: int = None):
116 | self.conn = conn
117 | self.session_id = session_id
118 | self.stream_id = stream_id
119 | self.out_buffer = bytearray()
120 | self.in_buffer = bytearray()
121 | self.in_waiter_fut = None
122 | self.in_waiter_count = None
123 | if self.stream_id != None:
124 | self.out_buffer += EA_SESSION_ID_STRUCT.pack(self.stream_id)
125 | self.closed = False
126 |
127 | def write(self, data):
128 | if self.closed:
129 | raise IOError("closed")
130 | if len(self.out_buffer) == 0:
131 | self.out_buffer = data
132 | else:
133 | self.out_buffer += data
134 | while len(self.out_buffer) >= self.conn.lsp.max_len and self.conn.write_allowed_event.is_set():
135 | self.conn.send_packet(
136 | IAP2Packet(self.out_buffer[:self.conn.lsp.max_len],
137 | session_id=self.session_id))
138 | del self.out_buffer[:self.conn.lsp.max_len]
139 |
140 | async def drain(self):
141 | if self.closed:
142 | raise IOError("closed")
143 | if len(self.out_buffer) == 0:
144 | return
145 | await self.conn.write_allowed_event.wait()
146 | self.conn.send_packet(
147 | IAP2Packet(self.out_buffer, session_id=self.session_id))
148 | self.out_buffer = bytearray()
149 | if self.stream_id != None:
150 | self.out_buffer += EA_SESSION_ID_STRUCT.pack(self.stream_id)
151 |
152 | def received_data(self, data):
153 | self.in_buffer += data
154 | if self.in_waiter_fut and self.in_waiter_count <= len(self.in_buffer):
155 | self.in_waiter_fut.set_result(True)
156 | self.in_waiter_fut = None
157 |
158 | async def readexactly(self, nbytes):
159 | if self.in_waiter_fut:
160 | return
161 |
162 | if len(self.in_buffer) < nbytes:
163 | if self.closed:
164 | raise asyncio.exceptions.IncompleteReadError(partial=self.in_buffer, expected=nbytes)
165 | self.in_waiter_count = nbytes
166 | fut = self.conn._loop.create_future()
167 | self.in_waiter_fut = fut
168 | await fut
169 | if self.closed:
170 | raise asyncio.exceptions.IncompleteReadError(partial=self.in_buffer, expected=nbytes)
171 |
172 | d = self.in_buffer[:nbytes]
173 | del self.in_buffer[:nbytes]
174 | return d
175 |
176 | def feed_eof(self):
177 | self.closed = True
178 | if self.in_waiter_fut:
179 | self.in_waiter_fut.set_result(True)
180 | self.in_waiter_fut = None
181 |
182 |
183 | class IAP2Connection:
184 | CONTROL_SESSION_ID = 10
185 | EA_SESSION_ID = 11
186 |
187 | def __init__(self,
188 | output: asyncio.StreamWriter,
189 | input: asyncio.StreamReader,
190 | loop: asyncio.AbstractEventLoop = asyncio.get_event_loop(),
191 | max_outgoing: int = 30,
192 | max_outgoing_delta: int = 0,
193 | ack_timeout=500,
194 | on_error: Callable[[Any], None] = None):
195 | self.on_error = on_error
196 | self.state = None
197 | self.lsp = LinkSynchronizationPayload(
198 | max_outgoing=max_outgoing,
199 | max_len=65535,
200 | retransmission_timeout=4000,
201 | ack_timeout=ack_timeout,
202 | max_retransmissions=4,
203 | max_ack=3,
204 | sessions=[
205 | LSPSession(id=IAP2Connection.CONTROL_SESSION_ID,
206 | type=0,
207 | version=1),
208 | LSPSession(id=IAP2Connection.EA_SESSION_ID, type=2, version=1)
209 | ])
210 | self._max_outgoing_delta = max_outgoing_delta
211 | self._sent_psn = 99
212 | self._last_sent_acknowledged_psn = None
213 | self._unack_packets = []
214 | self._queued_packets = []
215 |
216 | self._last_received_in_sequence_psn = 0
217 | self._last_acked_psn = None
218 | self._initial_received_psn = None
219 | self._received_out_of_sequence = []
220 | self._cumulative_received = 0
221 | self._loop = loop
222 | self._output = output
223 | self._input = input
224 | self._send_ack_timer = None
225 | self._recv_ack_timer = None
226 | self.write_allowed_event = asyncio.Event()
227 | self.control_session = IAP2Stream(self,
228 | IAP2Connection.CONTROL_SESSION_ID)
229 | self.ea_streams = dict()
230 | self._receive_loop_task = None
231 |
232 | def create_ea_stream(self, stream_id):
233 | stream = IAP2Stream(self, IAP2Connection.EA_SESSION_ID, stream_id)
234 | self.ea_streams[stream_id] = stream
235 | return stream
236 |
237 | def start(self):
238 | if self.state:
239 | return
240 | self._receive_loop_task = self._loop.create_task(self._receive_loop())
241 | self.state = STATE_DETECT_IAP2_SUPPORT
242 | self._send_detect_iap2_support()
243 |
244 | def close(self):
245 | self._input.feed_eof()
246 |
247 | def _write_packet(self, payload=None, seq=0, control=0, session_id=0):
248 | self._cumulative_received = 0
249 | if payload:
250 | length = len(payload) + 10
251 | else:
252 | length = 9
253 | header = LinkPacketHeader(control=control,
254 | length=length,
255 | seq=seq,
256 | ack=self._last_received_in_sequence_psn,
257 | session_id=session_id)
258 | print(">", header, payload)
259 | header_bytes = header.pack()
260 | if payload:
261 | self._output.write(header_bytes + payload +
262 | bytes([gen_checksum(payload)]))
263 |
264 | else:
265 | self._output.write(header_bytes)
266 |
267 | def _send_ack(self):
268 | self._write_packet(seq=self._sent_psn, control=CONTROL_ACK)
269 |
270 | def _send_eak(self, num):
271 | self._write_packet(bytes(num), seq=self._sent_psn, control=CONTROL_EAK)
272 |
273 | def _send_data(self, p):
274 | self._write_packet(p.data,
275 | seq=p.psn,
276 | control=CONTROL_ACK,
277 | session_id=p.session_id)
278 |
279 | def _send_detect_iap2_support(self):
280 | if self.state != STATE_DETECT_IAP2_SUPPORT:
281 | return
282 | self._output.write(IAP2_MARKER)
283 | self._loop.call_later(1, self._send_detect_iap2_support)
284 |
285 | def _send_negotiate(self):
286 | if self.state != STATE_NEGOTIATE:
287 | return
288 | lsp_bytes = self.lsp.pack()
289 | self._write_packet(lsp_bytes, self._sent_psn, CONTROL_SYN)
290 | self._loop.call_later(0.5, self._send_negotiate)
291 |
292 | async def _receive_loop(self):
293 | try:
294 | recv_marker = await self._input.readexactly(len(IAP2_MARKER))
295 | if recv_marker != IAP2_MARKER:
296 | self._bailout("IAP2 not supported")
297 | return
298 | if hasattr(self._input, "reset"):
299 | self._input.reset()
300 | self.state = STATE_NEGOTIATE
301 | self._send_negotiate()
302 | while True:
303 | header_bytes = await self._input.readexactly(9)
304 | while True:
305 | if int(header_bytes[0]) << 8 | int(
306 | header_bytes[1]) == LinkPacketHeader.start:
307 | break
308 | header_bytes = header_bytes[1:] + await self._input.readexactly(
309 | 1)
310 | header = LinkPacketHeader.from_bytes(header_bytes)
311 | if not header:
312 | continue
313 | payload = None
314 | if header.length > 9:
315 | payload_with_checksum = await self._input.readexactly(
316 | header.length - 9)
317 | if not check_checksum(payload_with_checksum):
318 | continue
319 | payload = payload_with_checksum[:-1]
320 | print("<", header, payload)
321 | if hasattr(self._input, "reset"):
322 | self._input.reset()
323 | if (header.control & CONTROL_RST) != 0:
324 | self._bailout("device sent reset message")
325 | if (header.control & CONTROL_SYN) != 0:
326 | lsp = LinkSynchronizationPayload.from_bytes(payload)
327 | if not lsp:
328 | continue
329 | self._handle_syn(lsp, header.seq)
330 | if (header.control & CONTROL_ACK) != 0:
331 | self._cumulative_received += 1
332 | self._handle_ack(header.ack)
333 | if (header.control & CONTROL_EAK) != 0 and payload:
334 | self._handle_eak([int(x) for x in payload])
335 | if (header.control & ~CONTROL_ACK) == 0 and payload != None:
336 | self._handle_data(
337 | IAP2Packet(payload, header.seq, header.session_id))
338 | if self._cumulative_received >= self.lsp.max_ack:
339 | self._cumulative_received = 0
340 | self._last_acked_psn = self._last_received_in_sequence_psn
341 | self._send_ack()
342 | except asyncio.exceptions.IncompleteReadError:
343 | self._bailout(None)
344 | except Exception as e:
345 | self._bailout(e)
346 |
347 | def _bailout(self, error):
348 | if self.state == STATE_DEAD:
349 | return
350 | self._disarm_send_ack_timer()
351 | self._disarm_recv_ack_timer()
352 | self.state = STATE_DEAD
353 | try:
354 | self._output.close()
355 | except:
356 | pass
357 | try:
358 | self.control_session.feed_eof()
359 | for stream in self.ea_streams.values():
360 | stream.feed_eof()
361 | except:
362 | pass
363 | if self._receive_loop_task:
364 | try:
365 | self._receive_loop_task.cancel()
366 | except:
367 | pass
368 | if error is not None and self.on_error:
369 | self.on_error(error)
370 |
371 | def send_packet(self, p: IAP2Packet):
372 | if distance(self._sent_psn, self._last_sent_acknowledged_psn
373 | ) > self.lsp.max_outgoing or self.state != STATE_NORMAL:
374 | self._queued_packets.append(p)
375 | self.write_allowed_event.clear()
376 | return
377 |
378 | self._sent_psn = signed_add(self._sent_psn, 1)
379 | p.counter = 0
380 | p.psn = self._sent_psn
381 | p.timeout = self._loop.time() + self.lsp.retransmission_timeout / 1000
382 | self._disarm_send_ack_timer()
383 | self._send_data(p)
384 | self._last_acked_psn = self._last_received_in_sequence_psn
385 | self._rearm_recv_ack_timer(p.timeout)
386 | self._unack_packets.append(p)
387 |
388 | def _handle_syn(self, lsp: LinkSynchronizationPayload, psn: int):
389 | if self.state != STATE_NEGOTIATE:
390 | return
391 | print("Device:", lsp)
392 | print("Accessory:", self.lsp)
393 | self.lsp = lsp
394 | self._last_received_in_sequence_psn = psn
395 | self._last_acked_psn = psn
396 | self._send_ack()
397 |
398 | def _handle_ack(self, num: int):
399 | if self.state == STATE_NEGOTIATE:
400 | self.state = STATE_NORMAL
401 | self.write_allowed_event.set()
402 | self._last_sent_acknowledged_psn = num
403 |
404 | while len(self._unack_packets) != 0:
405 | d = distance(
406 | self._unack_packets[0].psn,
407 | self._last_sent_acknowledged_psn)
408 | if 0 < d <= self.lsp.max_ack + 10:
409 | self._rearm_recv_ack_timer(self._unack_packets[0].timeout)
410 | break
411 | else:
412 | del self._unack_packets[0]
413 | else:
414 | self._disarm_recv_ack_timer()
415 |
416 | while distance(self._sent_psn, self._last_sent_acknowledged_psn
417 | ) < self.lsp.max_outgoing and len(
418 | self._queued_packets) > 0:
419 | self.send_packet(self._queued_packets.pop(0))
420 | self.write_allowed_event.set()
421 |
422 | def _on_expect_ack_timer(self):
423 | if len(self._unack_packets) == 0 or self.state != STATE_NORMAL:
424 | return
425 | unack_packets = sorted(self._unack_packets, key=lambda x: x.timeout)
426 | p = unack_packets[0]
427 | p.timeout = self._loop.time() + self.lsp.retransmission_timeout / 1000
428 | p.counter += 1
429 | if p.counter == self.lsp.max_retransmissions:
430 | self._bailout(p)
431 | return
432 | self._send_data(p)
433 | self._rearm_recv_ack_timer(unack_packets[0 if len(unack_packets) == 1 else 1].timeout)
434 |
435 | def _handle_eak(self, nums: List[int]):
436 | if self.state != STATE_NORMAL:
437 | return
438 | for p in self._unack_packets:
439 | if p.psn in nums:
440 | p.counter += 1
441 | if p.counter == self.lsp.max_retransmissions:
442 | self._bailout(p)
443 | continue
444 | self._send_data(p)
445 | self._disarm_send_ack_timer()
446 | self._rearm_recv_ack_timer(p.timeout)
447 |
448 | def _on_send_ack_timer(self):
449 | if self.state != STATE_NORMAL:
450 | return
451 | self._last_acked_psn = self._last_received_in_sequence_psn
452 | self._send_ack()
453 |
454 | def _handle_data(self, p: IAP2Packet):
455 | d = distance(p.psn, self._last_received_in_sequence_psn)
456 | if d > self.lsp.max_outgoing + 10 or d == 0:
457 | self._send_ack()
458 | return
459 |
460 | if d > 1:
461 | self._received_out_of_sequence.append(p)
462 | if d >= self.lsp.max_outgoing:
463 | eak = []
464 | x = self._last_received_in_sequence_psn
465 | while distance(p.psn, x) > 1:
466 | x = signed_add(x, 1)
467 | eak.append(x)
468 | self._disarm_send_ack_timer()
469 | self._send_eak(eak)
470 | return
471 |
472 | self._received_out_of_sequence.append(p)
473 | for pp in sorted(self._received_out_of_sequence,
474 | key=lambda x: distance(
475 | x.psn, self._last_received_in_sequence_psn)):
476 | if distance(pp.psn, self._last_received_in_sequence_psn) > 1:
477 | break
478 | self._received_data(pp)
479 | self._last_received_in_sequence_psn = pp.psn
480 | self._received_out_of_sequence.remove(pp)
481 |
482 | if distance(self._last_received_in_sequence_psn, self._last_acked_psn
483 | ) >= self.lsp.max_outgoing - self._max_outgoing_delta:
484 | self._disarm_send_ack_timer()
485 | self._last_acked_psn = self._last_received_in_sequence_psn
486 | self._send_ack()
487 | else:
488 | self._rearm_send_ack_timer()
489 |
490 | def _received_data(self, p: IAP2Packet):
491 | if p.session_id == IAP2Connection.CONTROL_SESSION_ID:
492 | self.control_session.received_data(p.data)
493 | elif p.session_id == IAP2Connection.EA_SESSION_ID and len(p.data) >= 2:
494 | stream_id = EA_SESSION_ID_STRUCT.unpack(p.data[:2])[0]
495 | stream = self.ea_streams.get(stream_id)
496 | if stream:
497 | stream.received_data(p.data[2:])
498 |
499 | def _disarm_send_ack_timer(self):
500 | if self._send_ack_timer:
501 | self._send_ack_timer.cancel()
502 | self._send_ack_timer = None
503 |
504 | def _rearm_send_ack_timer(self):
505 | if self._send_ack_timer:
506 | self._send_ack_timer.cancel()
507 | self._send_ack_timer = self._loop.call_later(self.lsp.ack_timeout / 1000,
508 | self._on_send_ack_timer)
509 |
510 | def _disarm_recv_ack_timer(self):
511 | if self._recv_ack_timer:
512 | self._recv_ack_timer.cancel()
513 | self._recv_ack_timer = None
514 |
515 | def _rearm_recv_ack_timer(self, time):
516 | if self._recv_ack_timer:
517 | self._recv_ack_timer.cancel()
518 | self._recv_ack_timer = self._loop.call_at(time, self._on_expect_ack_timer)
519 |
520 |
521 | def distance(a: int, b: int):
522 | if b is None:
523 | return 0
524 | elif a >= b:
525 | return a - b
526 | else:
527 | return a + 256 - b
528 |
--------------------------------------------------------------------------------
/iap2/mfi_auth_coprocessor.py:
--------------------------------------------------------------------------------
1 | import smbus2
2 | import time
3 | from struct import Struct
4 |
5 | Word = Struct(">H")
6 | # use bit-banged i2c, because coprocessor is to slow for native i2c 'dtoverlay=i2c-gpio,i2c_gpio_sda=2,i2c_gpio_scl=3,i2c_gpio_delay_us=50'
7 | bus = smbus2.SMBus("/dev/i2c-11")
8 | DEV_ADDR = 0x10
9 |
10 |
11 | def _read_i2c(addr, n):
12 | addr_msg = smbus2.i2c_msg.write(DEV_ADDR, bytes([addr]))
13 | read_msg = smbus2.i2c_msg.read(DEV_ADDR, n)
14 | for _ in range(5):
15 | try:
16 | bus.i2c_rdwr(addr_msg, read_msg)
17 | return bytes(read_msg)
18 | except OSError:
19 | time.sleep(0.0005)
20 | raise Exception("timeout")
21 |
22 |
23 | def _write_i2c(addr, arr):
24 | bus.write_i2c_block_data(DEV_ADDR, addr, [int(x) for x in arr])
25 |
26 |
27 | def read_certificate():
28 | size = Word.unpack(_read_i2c(0x30, 2))[0] # Read Accessory Certificate Data Length
29 | return _read_i2c(0x31, size) # Read Accessory Certificate Data
30 |
31 |
32 | def generate_challenge_response(challenge):
33 | _write_i2c(0x20, Word.pack(len(challenge))) # Write Challenge Data Length
34 | _write_i2c(0x21, challenge) # Write Challenge Data
35 | bus.write_byte_data(DEV_ADDR, 0x10, 0x01) # Write Authentication Control and Status = Start
36 | time.sleep(0.01)
37 | for _ in range(10):
38 | try:
39 | if bus.read_byte_data(DEV_ADDR, 0x10) == 0x10: # Read Authentication Control and Status == Success
40 | break
41 | except OSError:
42 | pass
43 | time.sleep(0.1)
44 | else:
45 | raise Exception("timeout")
46 | size = Word.unpack(_read_i2c(0x11, 2))[0] # Read Challenge Response Data Length
47 | return _read_i2c(0x12, size) # Read Challenge Response Data
48 |
49 |
50 | if __name__ == "__main__":
51 | print("CERT", read_certificate().hex())
52 | print("CERT", generate_challenge_response(b"12211213131231231231").hex())
53 |
--------------------------------------------------------------------------------
/iap2/requirements.txt:
--------------------------------------------------------------------------------
1 | libusb1==2.0.1
2 | functionfs==0.8.0
3 | dbus-python==1.2.18
4 | hid==1.0.4
5 | smbus2==0.4.1
6 | zeroconf==0.38.1
7 |
--------------------------------------------------------------------------------
/iap2/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import iap2.tests.test_control_session_message
2 | import iap2.tests.test_link_layer
3 |
--------------------------------------------------------------------------------
/iap2/tests/test_control_session_message.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from dataclasses import dataclass
3 | from typing import Annotated, List
4 |
5 | from iap2.control_session_message.identification import MatchAction, ExternalAccessoryProtocol, PowerProvidingCapability, IdentificationInformation, \
6 | BluetoothTransportComponent
7 | from iap2.control_session_message import Uint16, register_csm, Uint8, csm, read_csm
8 | from iap2.tests.utils import gen_pipe
9 |
10 |
11 | class TestControlSessionMessage(unittest.TestCase):
12 | @dataclass
13 | @csm(0xAA01)
14 | class Test:
15 | @dataclass
16 | class TestGroup:
17 | num: Uint8 = None
18 |
19 | first: str = None
20 | second: Annotated[str, 100] = None
21 | group: Annotated[List[TestGroup], 101] = None
22 |
23 | def test_roundtrip(self):
24 | import asyncio
25 | loop = asyncio.get_event_loop()
26 | register_csm(IdentificationInformation)
27 |
28 | async def test():
29 | reader, writer = await gen_pipe(loop)
30 | expected_csm = IdentificationInformation(
31 | name="raspberrypi",
32 | model_identifier="pi",
33 | manufacturer="wiomoc",
34 | serial_number="0122349",
35 | fireware_version="1.0.1",
36 | hardware_version="2.0",
37 | messages_sent_by_accessory=b'',
38 | messages_received_from_accessory=b'123123',
39 | power_providing_capability=PowerProvidingCapability.NONE,
40 | maximum_current_drawn_from_device=Uint16(20),
41 | supported_external_accessory_protocol=[ExternalAccessoryProtocol(
42 | id=Uint8(1),
43 | name="de.wiomoc.test",
44 | match_action=MatchAction.NONE,
45 | )],
46 | current_language="de",
47 | supported_language=["de"], #, "en"],
48 | app_match_team_id="",
49 | bluetooth_transport_component=[BluetoothTransportComponent(
50 | id=Uint16(0),
51 | name="blue",
52 | supports_iap2_connection=None,
53 | bluetooth_transport_mac=b'\xB8\x27\xEB\x23\x6A\xF4'
54 | )]
55 | )
56 |
57 | async def write():
58 | writer.write(expected_csm.csm_serialize())
59 | await writer.drain()
60 |
61 | loop.create_task(write())
62 | actual_csm = await read_csm(reader)
63 | print(actual_csm)
64 | self.assertEqual(expected_csm, actual_csm)
65 |
66 | loop.run_until_complete(test())
67 |
--------------------------------------------------------------------------------
/iap2/tests/test_link_layer.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import unittest
3 | from unittest.mock import Mock, call
4 | import random
5 |
6 | from iap2.link_layer import CONTROL_SYN, CONTROL_ACK, LinkSynchronizationPayload, LinkPacketHeader, IAP2_MARKER, \
7 | STATE_NORMAL, gen_checksum, IAP2Connection, LSPSession
8 | from iap2.tests.utils import gen_pipe
9 |
10 |
11 | class TestLinkPacketHeader(unittest.TestCase):
12 | def test_round_trip(self):
13 | header_bytes = b'\xffZ\x00\x1a\x80+\x00\x00\xe2'
14 | header = LinkPacketHeader.from_bytes(header_bytes)
15 | self.assertEqual(
16 | header,
17 | LinkPacketHeader(length=26,
18 | control=128,
19 | seq=43,
20 | ack=0,
21 | session_id=0))
22 | repacked_header_bytes = header.pack()
23 | self.assertEqual(repacked_header_bytes, header_bytes)
24 |
25 | def test_invalid_check(self):
26 | header_bytes = b'\xffZ\x00\x1a\x80+\x00\x00\xe1'
27 | header = LinkPacketHeader.from_bytes(header_bytes)
28 | self.assertIsNone(header)
29 |
30 | def test_invalid_start(self):
31 | header_bytes = b'\xfeZ\x00\x1a\x80+\x00\x00\xe2'
32 | header = LinkPacketHeader.from_bytes(header_bytes)
33 | self.assertIsNone(header)
34 |
35 |
36 | class TestLinkSynchronizationPayload(unittest.TestCase):
37 | def test_round_trip(self):
38 | payload = b'\x01\x05\x10\x00\x04\x0B\x00\x17\x03\x03\x0A\x00\x01\x0B\x02\x01'
39 | lsp = LinkSynchronizationPayload.from_bytes(payload)
40 | self.assertEqual(
41 | lsp,
42 | LinkSynchronizationPayload(max_outgoing=5,
43 | max_len=4096,
44 | retransmission_timeout=1035,
45 | ack_timeout=23,
46 | max_retransmissions=3,
47 | max_ack=3,
48 | sessions=[
49 | LSPSession(id=10, type=0,
50 | version=1),
51 | LSPSession(id=11, type=2, version=1)
52 | ]))
53 | repacked_payload = lsp.pack()
54 | self.assertEqual(repacked_payload, payload)
55 |
56 |
57 | class TestIAP2Connection(unittest.TestCase):
58 | class TestPacket:
59 | def __init__(self):
60 | self.id = random.random()
61 |
62 | def test_normal(self):
63 | conn = IAP2Connection(input=None, output=None, max_outgoing=3)
64 | conn.state = STATE_NORMAL
65 | conn._sent_psn = 199
66 | conn._rearm_send_ack_timer = Mock()
67 | conn._received_data = Mock()
68 | conn._send_data = Mock()
69 | conn._disarm_send_ack_timer = Mock()
70 | conn._rearm_recv_ack_timer = Mock()
71 | conn._last_acked_psn = 99
72 | conn._last_received_in_sequence_psn = 99
73 |
74 | p1 = TestIAP2Connection.TestPacket()
75 | p1.psn = 100
76 |
77 | conn._handle_data(p1)
78 |
79 | conn._received_data.assert_called_with(p1)
80 | conn._rearm_send_ack_timer.assert_called()
81 |
82 | p2 = TestIAP2Connection.TestPacket()
83 |
84 | conn.send_packet(p2)
85 |
86 | conn._send_data.assert_called_with(p2)
87 | self.assertEqual(conn._last_received_in_sequence_psn, p1.psn)
88 | conn._disarm_send_ack_timer.assert_called()
89 | conn._rearm_recv_ack_timer.assert_called()
90 | self.assertEqual(p2.psn, 200)
91 | self.assertEqual(conn._unack_packets, [p2])
92 |
93 | p3 = TestIAP2Connection.TestPacket()
94 | p3.psn = 101
95 |
96 | conn._handle_data(p3)
97 |
98 | conn._received_data.assert_called_with(p3)
99 | conn._rearm_send_ack_timer.assert_called()
100 |
101 | conn._disarm_recv_ack_timer = Mock()
102 |
103 | conn._handle_ack(200)
104 |
105 | conn._disarm_recv_ack_timer.assert_called()
106 | self.assertEqual(conn._unack_packets, [])
107 |
108 | p4 = TestIAP2Connection.TestPacket()
109 | p4.psn = 102
110 |
111 | conn._handle_data(p4)
112 |
113 | conn._handle_ack(200)
114 | conn._disarm_recv_ack_timer.assert_called()
115 |
116 | p5 = TestIAP2Connection.TestPacket()
117 |
118 | conn.send_packet(p5)
119 |
120 | conn._send_data.assert_called_with(p5)
121 | self.assertEqual(conn._last_received_in_sequence_psn, p4.psn)
122 | conn._disarm_send_ack_timer.assert_called()
123 | conn._rearm_recv_ack_timer.assert_called()
124 | self.assertEqual(p5.psn, 201)
125 |
126 | def test_ack_timeout(self):
127 | conn = IAP2Connection(input=None, output=None, max_outgoing=3, ack_timeout=20)
128 | conn.state = STATE_NORMAL
129 | conn._sent_psn = 199
130 | conn._rearm_recv_ack_timer = Mock()
131 | conn._send_data = Mock()
132 | conn._disarm_send_ack_timer = Mock()
133 | conn._last_received_in_sequence_psn = 99
134 | conn._disarm_recv_ack_timer = Mock()
135 |
136 | p1 = TestIAP2Connection.TestPacket()
137 |
138 | conn.send_packet(p1)
139 |
140 | conn._send_data.assert_called_with(p1)
141 | conn._rearm_recv_ack_timer.assert_called()
142 |
143 | p2 = TestIAP2Connection.TestPacket()
144 |
145 | conn.send_packet(p2)
146 |
147 | conn._send_data.assert_called_with(p2)
148 | conn._rearm_recv_ack_timer.assert_called()
149 |
150 | conn._on_expect_ack_timer()
151 | conn._send_data.assert_called_with(p1)
152 |
153 | conn._rearm_recv_ack_timer.assert_called()
154 | conn._on_expect_ack_timer()
155 | conn._send_data.assert_called_with(p2)
156 |
157 | conn._handle_ack(p1.psn)
158 | self.assertEqual(conn._unack_packets, [p2])
159 |
160 | conn._on_expect_ack_timer()
161 | conn._send_data.assert_called_with(p2)
162 |
163 | conn._handle_ack(p2.psn)
164 |
165 | conn._disarm_recv_ack_timer.asser_called()
166 |
167 | def test_buffer(self):
168 | conn = IAP2Connection(input=None, output=None, max_outgoing=2)
169 | conn.state = STATE_NORMAL
170 | conn._sent_psn = 199
171 | conn._last_sent_acknowledged_psn = 198
172 | conn._rearm_recv_ack_timer = Mock()
173 | conn._disarm_send_ack_timer = Mock()
174 | conn._disarm_recv_ack_timer = Mock()
175 | conn._send_data = Mock()
176 |
177 | p1 = TestIAP2Connection.TestPacket()
178 | conn.send_packet(p1)
179 | conn._send_data.reset_mock()
180 | conn._rearm_recv_ack_timer.assert_called()
181 |
182 | p2 = TestIAP2Connection.TestPacket()
183 | conn.send_packet(p2)
184 | conn._send_data.assert_called_with(p2)
185 | conn._send_data.reset_mock()
186 | conn._rearm_recv_ack_timer.assert_called()
187 |
188 | p3 = TestIAP2Connection.TestPacket()
189 | conn.send_packet(p3)
190 | conn._send_data.assert_not_called()
191 |
192 | conn._handle_ack(p2.psn)
193 | conn._send_data.assert_called_with(p3)
194 |
195 | def test_cumulative(self):
196 | conn = IAP2Connection(input=None, output=None, max_outgoing=2)
197 | conn.state = STATE_NORMAL
198 | conn._last_acked_psn = 99
199 | conn._last_received_in_sequence_psn = 99
200 | conn._rearm_send_ack_timer = Mock()
201 | conn._received_data = Mock()
202 |
203 | p1 = TestIAP2Connection.TestPacket()
204 | p1.psn = 100
205 |
206 | conn._handle_data(p1)
207 |
208 | conn._received_data.assert_called_with(p1)
209 | conn._rearm_send_ack_timer.assert_called()
210 |
211 | p2 = TestIAP2Connection.TestPacket()
212 | p2.psn = 101
213 | conn._disarm_send_ack_timer = Mock()
214 | conn._send_ack = Mock()
215 |
216 | conn._handle_data(p2)
217 |
218 | conn._received_data.assert_called_with(p2)
219 | conn._send_ack.assert_called()
220 | self.assertEqual(conn._last_received_in_sequence_psn, p2.psn)
221 | conn._disarm_send_ack_timer.assert_called()
222 |
223 | def test_out_of_order(self):
224 | conn = IAP2Connection(input=None, output=None, max_outgoing=10)
225 | conn.state = STATE_NORMAL
226 | conn._last_acked_psn = 102
227 | conn._last_received_in_sequence_psn = 102
228 | conn._rearm_send_ack_timer = Mock()
229 | conn._received_data = Mock()
230 |
231 | p1 = TestIAP2Connection.TestPacket()
232 | p1.psn = 103
233 |
234 | conn._handle_data(p1)
235 |
236 | conn._received_data.assert_called_with(p1)
237 | conn._rearm_send_ack_timer.assert_called()
238 |
239 | p2 = TestIAP2Connection.TestPacket()
240 | p2.psn = 107
241 |
242 | conn._handle_data(p2)
243 |
244 | p3 = TestIAP2Connection.TestPacket()
245 | p3.psn = 105
246 |
247 | conn._handle_data(p3)
248 |
249 | p4 = TestIAP2Connection.TestPacket()
250 | p4.psn = 104
251 | conn._disarm_send_ack_timer = Mock()
252 | conn._send_ack = Mock()
253 |
254 | conn._handle_data(p4)
255 |
256 | conn._received_data.assert_has_calls([call(p4), call(p3)])
257 | self.assertEqual(conn._last_received_in_sequence_psn, p3.psn)
258 | conn._rearm_send_ack_timer.assert_called()
259 |
260 | def test_out_of_order_overflow(self):
261 | conn = IAP2Connection(input=None, output=None, max_outgoing=3)
262 | conn.state = STATE_NORMAL
263 | conn._last_acked_psn = 253
264 | conn._last_received_in_sequence_psn = 253
265 | conn._rearm_send_ack_timer = Mock()
266 | conn._received_data = Mock()
267 |
268 | p1 = TestIAP2Connection.TestPacket()
269 | p1.psn = 254
270 |
271 | conn._handle_data(p1)
272 |
273 | conn._received_data.assert_called_with(p1)
274 | conn._rearm_send_ack_timer.assert_called()
275 |
276 | p2 = TestIAP2Connection.TestPacket()
277 | p2.psn = 0
278 |
279 | conn._handle_data(p2)
280 |
281 | p3 = TestIAP2Connection.TestPacket()
282 | p3.psn = 255
283 | conn._disarm_send_ack_timer = Mock()
284 | conn._send_ack = Mock()
285 |
286 | conn._handle_data(p3)
287 |
288 | conn._received_data.assert_has_calls([call(p3), call(p2)])
289 | conn._send_ack.assert_called()
290 | self.assertEqual(conn._last_received_in_sequence_psn, p2.psn)
291 | conn._disarm_send_ack_timer.assert_called()
292 |
293 | def test_eak(self):
294 | conn = IAP2Connection(input=None, output=None, max_outgoing=2)
295 | conn.state = STATE_NORMAL
296 | conn._last_received_in_sequence_psn = 102
297 | conn._last_acked_psn = 102
298 | conn._rearm_send_ack_timer = Mock()
299 | conn._received_data = Mock()
300 |
301 | p1 = TestIAP2Connection.TestPacket()
302 | p1.psn = 103
303 |
304 | conn._handle_data(p1)
305 |
306 | conn._received_data.assert_called_with(p1)
307 | conn._rearm_send_ack_timer.assert_called()
308 |
309 | p2 = TestIAP2Connection.TestPacket()
310 | p2.psn = 105
311 | conn._disarm_send_ack_timer = Mock()
312 | conn._send_ack = Mock()
313 | conn._send_eak = Mock()
314 |
315 | conn._handle_data(p2)
316 |
317 | conn._disarm_send_ack_timer.assert_called()
318 | conn._send_eak.assert_called_with([104])
319 | self.assertEqual(conn._last_received_in_sequence_psn, p1.psn)
320 |
321 | def test_eak_overflow(self):
322 | conn = IAP2Connection(input=None, output=None, max_outgoing=2)
323 | conn.state = STATE_NORMAL
324 | conn._last_received_in_sequence_psn = 254
325 | conn._last_acked_psn = 254
326 | conn._rearm_send_ack_timer = Mock()
327 | conn._received_data = Mock()
328 |
329 | p1 = TestIAP2Connection.TestPacket()
330 | p1.psn = 255
331 |
332 | conn._handle_data(p1)
333 |
334 | conn._received_data.assert_called_with(p1)
335 | conn._rearm_send_ack_timer.assert_called()
336 |
337 | p2 = TestIAP2Connection.TestPacket()
338 | p2.psn = 1
339 | conn._disarm_send_ack_timer = Mock()
340 | conn._send_ack = Mock()
341 | conn._send_eak = Mock()
342 |
343 | conn._handle_data(p2)
344 |
345 | conn._disarm_send_ack_timer.assert_called()
346 | conn._send_eak.assert_called_with([0])
347 | self.assertEqual(conn._last_received_in_sequence_psn, p1.psn)
348 |
349 |
350 | def async_test(f):
351 | def wrapper(*args, **kwargs):
352 | coro = asyncio.coroutine(f)
353 | future = coro(*args, **kwargs)
354 | loop = asyncio.get_event_loop()
355 | loop.run_until_complete(future)
356 |
357 | return wrapper
358 |
359 |
360 | class SmokeTest(unittest.TestCase):
361 | @async_test
362 | async def test(self):
363 | loop = asyncio.get_event_loop()
364 | input_rx, input_tx = await gen_pipe(loop)
365 | output_rx, output_tx = await gen_pipe(loop)
366 | conn = IAP2Connection(
367 | output_tx,
368 | input_rx,
369 | loop,
370 | max_outgoing=8,
371 | )
372 | conn.start()
373 |
374 | init = await output_rx.readexactly(6)
375 | self.assertEqual(init, IAP2_MARKER)
376 | input_tx.write(IAP2_MARKER)
377 | await input_tx.drain()
378 |
379 | header_bytes = await output_rx.readexactly(9)
380 | header = LinkPacketHeader.from_bytes(header_bytes)
381 | lsp_bytes = (await output_rx.readexactly(header.length - 9))[:-1]
382 | lsp = LinkSynchronizationPayload.from_bytes(lsp_bytes)
383 | self.assertEqual(lsp, conn.lsp)
384 |
385 | header.control = CONTROL_SYN | CONTROL_ACK
386 | header.ack = header.seq
387 | header.seq = 200
388 |
389 | input_tx.write(b'\x30') # Fault
390 | input_tx.write(header.pack())
391 | input_tx.write(lsp_bytes)
392 | input_tx.write(bytes([gen_checksum(lsp_bytes)]))
393 | await input_tx.drain()
394 | await asyncio.sleep(1)
395 | self.assertEqual(conn.state, STATE_NORMAL)
396 | header_bytes = await output_rx.readexactly(9)
397 | header = LinkPacketHeader.from_bytes(header_bytes)
398 |
399 | header.ack = 50
400 | header.seq = 201
401 | header.control = 0
402 | header.session_id = 10
403 |
404 | payload = b"hello world!"
405 | header.length = 10 + len(payload)
406 | input_tx.write(header.pack())
407 | input_tx.write(payload)
408 | input_tx.write(bytes([gen_checksum(payload)]))
409 |
410 | await asyncio.sleep(1)
411 |
412 | header_bytes = await output_rx.readexactly(9)
413 | header = LinkPacketHeader.from_bytes(header_bytes)
414 |
415 | stream = conn.create_ea_stream(0x42)
416 | stream.write(b'life')
417 | await stream.drain()
418 |
419 | header_bytes = await output_rx.readexactly(9)
420 | header = LinkPacketHeader.from_bytes(header_bytes)
421 | payload = (await output_rx.readexactly(header.length - 9))[:-1]
422 | self.assertEqual(payload, b'\x00\x42life')
423 |
424 | header.ack = 51
425 | header.seq = 202
426 | header.control = 0
427 | header.session_id = 11
428 | payload = b"\x00\x42abc"
429 | header.length = 10 + len(payload)
430 | input_tx.write(header.pack())
431 | input_tx.write(payload)
432 | input_tx.write(bytes([gen_checksum(payload)]))
433 | await input_tx.drain()
434 | self.assertEqual(await stream.readexactly(3), b'abc')
435 |
436 | control_session = conn.control_session
437 | control_session.write(b'pong')
438 | await control_session.drain()
439 | header_bytes = await output_rx.readexactly(9)
440 | header = LinkPacketHeader.from_bytes(header_bytes)
441 | payload = (await output_rx.readexactly(header.length - 9))[:-1]
442 | self.assertEqual(payload, b'pong')
443 | self.assertEqual(await control_session.readexactly(5), b'hello')
444 | loop.call_soon(lambda: input_rx.feed_eof())
445 | self.assertEqual(await control_session.readexactly(5), b' worl')
446 |
447 | with self.assertRaises(asyncio.exceptions.IncompleteReadError):
448 | await control_session.readexactly(5)
449 |
450 | with self.assertRaises(asyncio.exceptions.IncompleteReadError):
451 | await stream.readexactly(5)
452 |
453 | @async_test
454 | async def test_bailout(self):
455 | on_error = Mock()
456 | loop = asyncio.get_event_loop()
457 | input_rx, input_tx = await gen_pipe(loop)
458 | output_rx, output_tx = await gen_pipe(loop)
459 | conn = IAP2Connection(
460 | output_tx,
461 | input_rx,
462 | loop,
463 | max_outgoing=8,
464 | on_error=on_error
465 | )
466 | conn.start()
467 |
468 | init = await output_rx.readexactly(6)
469 | self.assertEqual(init, IAP2_MARKER)
470 | input_tx.write(IAP2_MARKER)
471 | await input_tx.drain()
472 |
473 | exception = Exception("42")
474 | input_rx.set_exception(exception)
475 | await asyncio.sleep(1)
476 |
477 | on_error.assert_called_with(exception)
478 |
--------------------------------------------------------------------------------
/iap2/tests/utils.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 |
5 | async def gen_pipe(loop):
6 | read_fd, write_fd = os.pipe()
7 | reader = asyncio.StreamReader()
8 | read_protocol = asyncio.StreamReaderProtocol(reader)
9 | read_transport, _ = await loop.connect_read_pipe(lambda: read_protocol,
10 | os.fdopen(read_fd))
11 | write_protocol = asyncio.StreamReaderProtocol(asyncio.StreamReader())
12 | write_transport, _ = await loop.connect_write_pipe(
13 | lambda: write_protocol, os.fdopen(write_fd, 'w'))
14 | writer = asyncio.StreamWriter(write_transport, write_protocol, None, loop)
15 | return reader, writer
16 |
--------------------------------------------------------------------------------
/iap2/transport/__init__.py:
--------------------------------------------------------------------------------
1 | __all__ = ["bluetooth", "usb_host", "usb_device"]
2 |
--------------------------------------------------------------------------------
/iap2/transport/bluetooth.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # sudo hciconfig hci0 inqdata 0e0972617370626572727970693031020a00091002006b1d460237051107FFCACADEAFDECADEDEFACADE00000000
3 | # 0d0950455547454f2d3633383400110600000000DECAFADEDECADEAFDECACAFF1107D31FBF505D572797A24041CD484388EC
4 | import asyncio
5 | import socket
6 | import threading
7 | import time
8 |
9 | import dbus
10 | import dbus.service
11 | from dbus.mainloop.glib import DBusGMainLoop
12 | from gi.repository import GLib
13 |
14 | import iap2.carplay_bonjour as carplay_bonjour
15 |
16 | BUS_NAME = 'org.bluez'
17 | PROFILE_INTERFACE = 'org.bluez.Profile1'
18 | AGENT_INTERFACE = 'org.bluez.Agent1'
19 |
20 |
21 | def ask(prompt):
22 | try:
23 | return raw_input(prompt)
24 | except:
25 | return input(prompt)
26 |
27 |
28 | # def set_trusted(path):
29 | # props = dbus.Interface(bus.get_object(BUS_NAME, path),
30 | # "org.freedesktop.DBus.Properties")
31 | # props.Set("org.bluez.Device1", "Trusted", True)
32 |
33 |
34 | class Rejected(dbus.DBusException):
35 | _dbus_error_name = "org.bluez.Error.Rejected"
36 |
37 |
38 | class Agent(dbus.service.Object):
39 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="")
40 | def Release(self):
41 | print("Release")
42 |
43 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="")
44 | def AuthorizeService(self, device, uuid):
45 | print("AuthorizeService (%s, %s)" % (device, uuid))
46 |
47 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="s")
48 | def RequestPinCode(self, device):
49 | print("RequestPinCode (%s)" % (device))
50 | return ask("Enter PIN Code: ")
51 |
52 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="u")
53 | def RequestPasskey(self, device):
54 | print("RequestPasskey (%s)" % (device))
55 | passkey = ask("Enter passkey: ")
56 | return dbus.UInt32(passkey)
57 |
58 | @dbus.service.method(AGENT_INTERFACE, in_signature="ouq", out_signature="")
59 | def DisplayPasskey(self, device, passkey, entered):
60 | print("DisplayPasskey (%s, %06u entered %u)" %
61 | (device, passkey, entered))
62 |
63 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="")
64 | def DisplayPinCode(self, device, pincode):
65 | print("DisplayPinCode (%s, %s)" % (device, pincode))
66 |
67 | @dbus.service.method(AGENT_INTERFACE, in_signature="ou", out_signature="")
68 | def RequestConfirmation(self, device, passkey):
69 | print("RequestConfirmation (%s, %06d)" % (device, passkey))
70 | time.sleep(2)
71 |
72 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="")
73 | def RequestAuthorization(self, device):
74 | print("RequestAuthorization (%s)" % (device))
75 |
76 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="")
77 | def Cancel(self):
78 | print("Cancel")
79 |
80 |
81 | class IAPProfile(dbus.service.Object):
82 | def __init__(self, bus, path, on_connection, loop):
83 | dbus.service.Object.__init__(self, bus, path)
84 | self.__path = path
85 | self.on_connection = on_connection
86 | self._loop = loop
87 |
88 | @dbus.service.method(dbus_interface=PROFILE_INTERFACE, in_signature='')
89 | def Release(self):
90 | print("Release")
91 |
92 | @dbus.service.method(dbus_interface=PROFILE_INTERFACE,
93 | in_signature='oha{sv}')
94 | def NewConnection(self, device, fd, opts):
95 | print("new bluetooth connection", device, self.__path)
96 | raw_fd = fd.take()
97 | s = socket.fromfd(raw_fd, socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM)
98 | s.settimeout(None)
99 |
100 | async def on_connection():
101 | reader, writer = await asyncio.open_connection(sock=s)
102 | self.on_connection(reader, writer)
103 |
104 | self._loop.call_soon_threadsafe(lambda: asyncio.create_task(on_connection()))
105 |
106 | @dbus.service.method(dbus_interface=PROFILE_INTERFACE, in_signature='o')
107 | def RequestDisconnection(self, device):
108 | print("Disconnect")
109 |
110 |
111 | IAP_SERVER_UUID = "00000000-deca-fade-deca-deafdecacaff"
112 | IAP_CLIENT_UUID = "00000000-deca-fade-deca-deafdecacafe"
113 | CHANNEL = 3
114 | IAP_RECORD = f"""
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | """
173 |
174 |
175 | class BluetoothTransport:
176 | def __init__(self, on_connection, loop):
177 |
178 | self._glib_loop = GLib.MainLoop()
179 | self._loop = loop
180 | self.on_connection = on_connection
181 | threading.Thread(target=self._bluetooth_thread).start()
182 |
183 | def close(self):
184 | if self._glib_loop:
185 | self._glib_loop.quit()
186 | self._glib_loop = None
187 |
188 | def _bluetooth_thread(self):
189 | DBusGMainLoop(set_as_default=True)
190 | carplay_bonjour.start_service("DC:A6:32:63:35:20")
191 | bus = dbus.SystemBus()
192 | bluez = bus.get_object(BUS_NAME, '/org/bluez')
193 | iapServerProfile = IAPProfile(bus, "/org/bluez/iap_server", self.on_connection, self._loop)
194 | iapClientProfile = IAPProfile(bus, "/org/bluez/iap_client", self.on_connection, self._loop)
195 | profileManager = dbus.Interface(bluez, 'org.bluez.ProfileManager1')
196 | profileManager.RegisterProfile(iapServerProfile, IAP_SERVER_UUID, {
197 | 'Role': 'server',
198 | 'Channel': dbus.types.UInt16(CHANNEL),
199 | 'ServiceRecord': IAP_RECORD,
200 | 'RequireAuthentication': False,
201 | 'RequireAuthorization': False
202 | })
203 |
204 | profileManager.RegisterProfile(iapClientProfile, IAP_CLIENT_UUID, {
205 | 'Role': 'client',
206 | 'AutoConnect': True
207 | })
208 |
209 | agent = Agent(bus, "/org/bluez/iap_agent")
210 | agent_manager = dbus.Interface(bluez, "org.bluez.AgentManager1")
211 | agent_manager.RegisterAgent(agent, "KeyboardDisplay")
212 | agent_manager.RequestDefaultAgent(agent)
213 | adapter = dbus.Interface(bus.get_object(BUS_NAME, "/org/bluez/hci0"), dbus.PROPERTIES_IFACE)
214 | adapter.Set("org.bluez.Adapter1", 'Powered', True)
215 | adapter.Set("org.bluez.Adapter1", 'Discoverable', True)
216 | adapter.Set("org.bluez.Adapter1", 'Pairable', True)
217 | if self._glib_loop:
218 | self._glib_loop.run()
219 | profileManager.UnregisterProfile(iapServerProfile)
220 | profileManager.UnregisterProfile(iapClientProfile)
221 | agent_manager.UnregisterAgent(agent)
222 |
--------------------------------------------------------------------------------
/iap2/transport/usb_device.py:
--------------------------------------------------------------------------------
1 | import usb1
2 | import threading
3 | import queue
4 | import hid
5 | import asyncio
6 |
7 |
8 | class BaseUSBDeviceHandler:
9 | def __init__(self):
10 | context = usb1.USBContext()
11 | context.open()
12 |
13 | loop = asyncio.get_event_loop()
14 |
15 | def added_cb(fd, events):
16 | if events & 1:
17 | loop.add_reader(fd, context.handleEventsTimeout)
18 | if events & 4:
19 | loop.add_writer(fd, context.handleEventsTimeout)
20 |
21 | def removed_cb(fd):
22 | loop.remove_reader(fd)
23 | loop.remove_writer(fd)
24 |
25 | for fd, events in context.getPollFDList():
26 | added_cb(fd, events)
27 |
28 | context._USBContext__has_pollfd_finalizer = True
29 | context.setPollFDNotifiers(added_cb=added_cb, removed_cb=removed_cb)
30 | context.setDebug(usb1.LOG_LEVEL_DEBUG)
31 |
32 | def hotplug_callback(context, device, event):
33 | print("event", repr(device), event)
34 | if event == usb1.HOTPLUG_EVENT_DEVICE_ARRIVED:
35 | loop.create_task(self._handle_new_device(device))
36 |
37 | context.hotplugRegisterCallback(callback=hotplug_callback, vendor_id=0x5ac)
38 | self._context = context
39 |
40 | def close(self):
41 | self._context.setPollFDNotifiers(None, None)
42 | self._context.close()
43 |
44 |
45 | class USBRoleSwitchHandler(BaseUSBDeviceHandler):
46 | def __init__(self, after_role_switch, car_play=False):
47 | super().__init__()
48 | self._after_role_switch = after_role_switch
49 | self._car_play = car_play
50 |
51 | async def _handle_new_device(self, device):
52 | open_device = device.open()
53 |
54 | await _usb_control_transfer(
55 | open_device, usb1.RECIPIENT_DEVICE | usb1.LIBUSB_REQUEST_TYPE_VENDOR,
56 | 0x51, 1 if self._car_play else 0, 0, 0)
57 | self._after_role_switch()
58 |
59 |
60 | class USBDeviceTransport(BaseUSBDeviceHandler):
61 | def __init__(self, on_connection):
62 | super().__init__()
63 | self._on_connection = on_connection
64 |
65 | async def _handle_new_device(self, device):
66 | CONFIGURATION_VALUE = 2
67 | configs = [
68 | c for c in device.iterConfigurations()
69 | if c.getConfigurationValue() == CONFIGURATION_VALUE
70 | ]
71 | config = configs[0]
72 |
73 | interfaces = [
74 | s for i in config.iterInterfaces() for s in i.iterSettings()
75 | if s.getClassTuple() == (3, 0)
76 | ]
77 | interface_setting = interfaces[0]
78 | interface_num = interface_setting.getNumber()
79 | endpoints = list(interface_setting.iterEndpoints())
80 | endpoint = endpoints[0]
81 | print(interface_num)
82 |
83 | open_device = device.open()
84 | open_device.setConfiguration(CONFIGURATION_VALUE)
85 |
86 | report_descriptor = await _usb_control_transfer(
87 | open_device, usb1.ENDPOINT_IN | usb1.RECIPIENT_INTERFACE,
88 | usb1.REQUEST_GET_DESCRIPTOR, (usb1.DT_REPORT << 8), interface_num,
89 | 2000)
90 | print(report_descriptor)
91 | output_report_ids = []
92 | input_report_ids = dict()
93 | report_id = None
94 | report_count = None
95 | for tag, item in get_descriptor_items(report_descriptor):
96 | if tag == 0x84:
97 | report_id = item[0]
98 | elif tag == 0x94:
99 | report_count = int(item[0]) if len(
100 | item) == 1 else int(item[1]) << 8 | int(item[0])
101 | elif tag == 0x90:
102 | output_report_ids.append((report_id, report_count))
103 | elif tag == 0x80:
104 | input_report_ids[report_id] = report_count
105 |
106 | output_report_ids.sort(key=lambda a:a[0])
107 |
108 | hid_device = hid.Device(vid=device.getVendorID(),
109 | pid=device.getProductID(),
110 | serial=device.getSerialNumber())
111 | w = HIDWriter(hid_device, output_report_ids)
112 | r = HIDReader(hid_device, input_report_ids)
113 | self._on_connection(w, r)
114 |
115 |
116 | def _usb_control_transfer(device, request_type, request, value, index, length):
117 | transfer = device.getTransfer()
118 | future = asyncio.get_event_loop().create_future()
119 |
120 | def cb(transfer):
121 | status = transfer.getStatus()
122 | if status == usb1.LIBUSB_TRANSFER_COMPLETED:
123 | future.set_result(transfer.getBuffer()[:transfer.getActualLength()])
124 | else:
125 | future.set_exception(IOError(f"USB error {status}"))
126 |
127 | transfer.setControl(request_type, request, value, index, length, callback=cb)
128 | transfer.submit()
129 | return future
130 |
131 |
132 | def get_descriptor_items(descriptor):
133 | i = 0
134 | while i < len(descriptor):
135 | tag = descriptor[i]
136 | if tag == 0xFE:
137 | size = descriptor[i + 1]
138 | tag = descriptor[i + 2]
139 | i += 3
140 | data = descriptor[i:i + size]
141 | i += size
142 | else:
143 | size = (1 << (tag & 3)) >> 1
144 | i += 1
145 | data = descriptor[i:i + size]
146 | tag &= 0xFC
147 | i += size
148 | yield (tag, data)
149 |
150 |
151 | LCB_CONTINUATION = 1
152 | LCB_MORE_TO_FOLLOW = 2
153 |
154 |
155 | class HIDReader:
156 | def __init__(self, hid_device, input_report_ids):
157 | self._loop = asyncio.get_event_loop()
158 | self._hid_device = hid_device
159 | self._input_report_ids = input_report_ids
160 | self._read_buffer_semaphore = threading.Semaphore(value=3)
161 | self._read_buffer_queue = asyncio.Queue()
162 | self._max_len = max(input_report_ids.values())
163 | self.eof = False
164 | self._read_buffer = None
165 | threading.Thread(target=self._read_loop).start()
166 |
167 | async def readexactly(self, nbytes):
168 | if not self._read_buffer or len(self._read_buffer) == 0:
169 | self._read_buffer_semaphore.release()
170 | if self.eof:
171 | raise asyncio.exceptions.IncompleteReadError(partial=self._read_buffer, expected=nbytes)
172 | self._read_buffer = await self._read_buffer_queue.get()
173 |
174 | if len(self._read_buffer) >= nbytes:
175 | b = self._read_buffer[:nbytes]
176 | self._read_buffer = self._read_buffer[nbytes:]
177 | return b
178 | else:
179 | b = bytearray(self._read_buffer)
180 | while len(b) <= nbytes:
181 | self._read_buffer_semaphore.release()
182 | if self.eof:
183 | raise asyncio.exceptions.IncompleteReadError(partial=self._read_buffer, expected=nbytes)
184 | b.extend(await self._read_buffer_queue.get())
185 | self._read_buffer = b[nbytes:]
186 | return b[:nbytes]
187 |
188 | def reset(self):
189 | self._read_buffer = None
190 |
191 | def _read_loop(self):
192 | buf = bytearray()
193 | try:
194 | while not self.eof:
195 | self._read_buffer_semaphore.acquire()
196 | while not self.eof:
197 | report = self._hid_device.read(self._max_len + 2)
198 | if len(report) <= 2:
199 | continue
200 | lcb = report[1]
201 | payload = report[2:]
202 | if (lcb & LCB_CONTINUATION) == 0:
203 | buf.clear()
204 | if (lcb & LCB_MORE_TO_FOLLOW) != 0:
205 | buf.extend(payload)
206 | else:
207 | if len(buf) > 0:
208 | buf.extend(payload)
209 | packet = bytes(buf)
210 | buf.clear()
211 | else:
212 | packet = payload
213 | self._loop.call_soon_threadsafe(
214 | lambda: self._read_buffer_queue.put_nowait(packet))
215 | break
216 | except:
217 | self.feed_eof()
218 |
219 | def feed_eof(self):
220 | self.eof = True
221 | self._read_buffer_semaphore.acquire()
222 |
223 |
224 | class HIDWriter:
225 | def __init__(self, hid_device, output_report_ids):
226 | self.closed = False
227 | self._hid_device = hid_device
228 | self._output_report_ids = output_report_ids
229 | self._write_buffer_queue = queue.Queue()
230 | threading.Thread(target=self._write_loop).start()
231 |
232 | def write(self, buffer):
233 | if self.closed:
234 | raise IOError("closed")
235 | self._write_buffer_queue.put_nowait(buffer)
236 |
237 | def _write_loop(self):
238 | while True:
239 | first = True
240 | buf = self._write_buffer_queue.get()
241 | if buf is None or self.closed:
242 | return
243 | while len(buf) > 0:
244 | report_id = None
245 | report_count = None
246 | for id, count in self._output_report_ids:
247 | count -= 1 # take lcb into account
248 | report_id = id
249 | report_count = count
250 | if count > len(buf):
251 | break
252 | lcb = 0
253 | if first:
254 | first = False
255 | else:
256 | lcb |= LCB_CONTINUATION
257 | if report_count < len(buf):
258 | lcb |= LCB_MORE_TO_FOLLOW
259 | padding = b'\0' * max(report_count - len(buf), 0)
260 | self._hid_device.write(
261 | bytes([report_id, lcb]) + buf[:report_count] + padding)
262 | buf = buf[report_count:]
263 |
264 | def close(self):
265 | self.closed = True
266 | self._write_buffer_queue.put_nowait(None)
267 |
--------------------------------------------------------------------------------
/iap2/transport/usb_host.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python -u
2 | # This file is part of python-functionfs
3 | # Copyright (C) 2016-2021 Vincent Pelletier
4 | #
5 | # python-functionfs is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # python-functionfs is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with python-functionfs. If not, see .
17 |
18 | from collections import deque
19 | import errno
20 | import fcntl
21 | import functools
22 | import os
23 | import select
24 | import sys
25 | import functionfs
26 | from functionfs.gadget import (
27 | GadgetSubprocessManager,
28 | ConfigFunctionFFS,
29 | )
30 | import functionfs.ch9
31 | import asyncio
32 |
33 | # Large-ish buffer, to tolerate bursts without becoming a context switch storm.
34 | BUF_SIZE = 1024 * 1024
35 |
36 | trace = functools.partial(print, file=sys.stderr)
37 |
38 | class EndpointOUTFile(functionfs.EndpointOUTFile, asyncio.StreamReader):
39 | def __init__(self, *args, **kw):
40 | functionfs.EndpointOUTFile.__init__(self, *args, **kw)
41 | asyncio.StreamReader.__init__(self)
42 |
43 | def onComplete(self, data, status):
44 | if data is None:
45 | trace('aio read completion error:', -status)
46 | else:
47 | trace('aio read completion received', len(data), 'bytes')
48 | self.feed_data(data)
49 |
50 | class EndpointINFile(functionfs.EndpointINFile):
51 | def __init__(self, *args, **kw):
52 | self.__stranded_buffer_list_queue = deque()
53 | self._full = False
54 | super().__init__(*args, **kw)
55 |
56 | def write(self, data):
57 | if self._full:
58 | return
59 | if type(data) == bytes:
60 | data = bytearray(data)
61 | self.submit([bytearray(data)])
62 |
63 | def onComplete(self, buffer_list, user_data, status):
64 | if status < 0:
65 | trace('aio write completion error:', -status)
66 | else:
67 | trace('aio write completion sent', status, 'bytes')
68 | if status != -errno.ESHUTDOWN and self.__stranded_buffer_list_queue:
69 | buffer_list = self.__stranded_buffer_list_queue.popleft()
70 | self._full = not self.__stranded_buffer_list_queue
71 | return buffer_list
72 | return None
73 |
74 | def onSubmitEAGAIN(self, buffer_list, user_data):
75 | self.__stranded_buffer_list_queue.append(buffer_list)
76 | trace('send queue full, pause sending')
77 | self._full = True
78 |
79 | def forgetStranded(self):
80 | self.__stranded_buffer_list_queue.clear()
81 |
82 | class USBCat(functionfs.Function):
83 |
84 | def __init__(self, path):
85 | fs_list, hs_list, ss_list = functionfs.getInterfaceInAllSpeeds(
86 | interface={
87 | 'bInterfaceClass': functionfs.ch9.USB_CLASS_VENDOR_SPEC,
88 | 'bInterfaceSubClass': 0xF0,
89 | 'iInterface': 1,
90 | },
91 | endpoint_list=[
92 | {
93 | 'endpoint': {
94 | 'bEndpointAddress': functionfs.ch9.USB_DIR_IN,
95 | 'bmAttributes': functionfs.ch9.USB_ENDPOINT_XFER_BULK,
96 | },
97 | }, {
98 | 'endpoint': {
99 | 'bEndpointAddress': functionfs.ch9.USB_DIR_OUT,
100 | 'bmAttributes': functionfs.ch9.USB_ENDPOINT_XFER_BULK,
101 | },
102 | },
103 | ],
104 | )
105 | super().__init__(
106 | path,
107 | fs_list=fs_list,
108 | hs_list=hs_list,
109 | ss_list=ss_list,
110 | lang_dict={
111 | 0x0409: [
112 | "iAP Interface",
113 | ],
114 | },
115 | )
116 |
117 | def getEndpointClass(self, is_in, descriptor):
118 | return (EndpointINFile if is_in else EndpointOUTFile)
119 |
120 | def __enter__(self):
121 | result = super().__enter__()
122 | self.in_ep = self.getEndpoint(1)
123 | self.out_ep = self.getEndpoint(2)
124 | return result
125 |
126 |
127 | def onBind(self):
128 | trace('onBind')
129 | super().onBind()
130 |
131 | def onUnbind(self):
132 | trace('onUnbind')
133 | super().onUnbind()
134 |
135 | def onEnable(self):
136 | trace('onEnable')
137 | super().onEnable()
138 |
139 | def onDisable(self):
140 | trace('onDisable')
141 | self.in_ep.forgetStranded()
142 | super().onDisable()
143 |
144 | def onSuspend(self):
145 | trace('onSuspend')
146 | super().onSuspend()
147 |
148 | def onResume(self):
149 | trace('onResume')
150 | super().onResume()
151 |
152 | class SubprocessCat(ConfigFunctionFFS):
153 | def getFunction(self):
154 | return USBCat(path=self._mountpoint)
155 |
156 |
157 |
158 |
159 | loop = asyncio.get_event_loop()
160 |
161 |
162 | def main():
163 | s = SubprocessCat()
164 | s.start(path="/sys/kernel/config/usb_gadget/isticktoit/ffs.sda")
165 | s.function = function = s.getFunction()
166 | loop.add_reader(function.eventfd.fileno(), function.processEvents)
167 | try:
168 | with function as f:
169 | async def g():
170 | input = f.getEndpoint(2)
171 | print(await input.readexactly(4))
172 |
173 | loop.create_task(g())
174 | out = f.getEndpoint(1)
175 | def w():
176 | out.write(b'\xFF\x55\x02\x00\xEE\x10')
177 | print("w")
178 | loop.call_later(1, w)
179 | w()
180 | loop.run_forever()
181 | except BaseException as e:
182 | s.join()
183 | raise e
184 |
185 |
186 | if __name__ == '__main__':
187 | main()
--------------------------------------------------------------------------------