bumble 0.0.202__py3-none-any.whl → 0.0.204__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bumble/profiles/bass.py CHANGED
@@ -276,10 +276,7 @@ class BroadcastReceiveState:
276
276
  subgroups: List[SubgroupInfo]
277
277
 
278
278
  @classmethod
279
- def from_bytes(cls, data: bytes) -> Optional[BroadcastReceiveState]:
280
- if not data:
281
- return None
282
-
279
+ def from_bytes(cls, data: bytes) -> BroadcastReceiveState:
283
280
  source_id = data[0]
284
281
  _, source_address = hci.Address.parse_address_preceded_by_type(data, 2)
285
282
  source_adv_sid = data[8]
@@ -357,7 +354,7 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
357
354
  SERVICE_CLASS = BroadcastAudioScanService
358
355
 
359
356
  broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
360
- broadcast_receive_states: List[gatt.DelegatedCharacteristicAdapter]
357
+ broadcast_receive_states: List[gatt.SerializableCharacteristicAdapter]
361
358
 
362
359
  def __init__(self, service_proxy: gatt_client.ServiceProxy):
363
360
  self.service_proxy = service_proxy
@@ -381,8 +378,8 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
381
378
  "Broadcast Receive State characteristic not found"
382
379
  )
383
380
  self.broadcast_receive_states = [
384
- gatt.DelegatedCharacteristicAdapter(
385
- characteristic, decode=BroadcastReceiveState.from_bytes
381
+ gatt.SerializableCharacteristicAdapter(
382
+ characteristic, BroadcastReceiveState
386
383
  )
387
384
  for characteristic in characteristics
388
385
  ]
@@ -64,7 +64,10 @@ class DeviceInformationService(TemplateService):
64
64
  ):
65
65
  characteristics = [
66
66
  Characteristic(
67
- uuid, Characteristic.Properties.READ, Characteristic.READABLE, field
67
+ uuid,
68
+ Characteristic.Properties.READ,
69
+ Characteristic.READABLE,
70
+ bytes(field, 'utf-8'),
68
71
  )
69
72
  for (field, uuid) in (
70
73
  (manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
@@ -30,6 +30,7 @@ from ..gatt import (
30
30
  TemplateService,
31
31
  Characteristic,
32
32
  CharacteristicValue,
33
+ SerializableCharacteristicAdapter,
33
34
  DelegatedCharacteristicAdapter,
34
35
  PackedCharacteristicAdapter,
35
36
  )
@@ -150,15 +151,14 @@ class HeartRateService(TemplateService):
150
151
  body_sensor_location=None,
151
152
  reset_energy_expended=None,
152
153
  ):
153
- self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
154
+ self.heart_rate_measurement_characteristic = SerializableCharacteristicAdapter(
154
155
  Characteristic(
155
156
  GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
156
157
  Characteristic.Properties.NOTIFY,
157
158
  0,
158
159
  CharacteristicValue(read=read_heart_rate_measurement),
159
160
  ),
160
- # pylint: disable=unnecessary-lambda
161
- encode=lambda value: bytes(value),
161
+ HeartRateService.HeartRateMeasurement,
162
162
  )
163
163
  characteristics = [self.heart_rate_measurement_characteristic]
164
164
 
@@ -204,9 +204,8 @@ class HeartRateServiceProxy(ProfileServiceProxy):
204
204
  if characteristics := service_proxy.get_characteristics_by_uuid(
205
205
  GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC
206
206
  ):
207
- self.heart_rate_measurement = DelegatedCharacteristicAdapter(
208
- characteristics[0],
209
- decode=HeartRateService.HeartRateMeasurement.from_bytes,
207
+ self.heart_rate_measurement = SerializableCharacteristicAdapter(
208
+ characteristics[0], HeartRateService.HeartRateMeasurement
210
209
  )
211
210
  else:
212
211
  self.heart_rate_measurement = None
@@ -0,0 +1,330 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ # -----------------------------------------------------------------------------
17
+ # Imports
18
+ # -----------------------------------------------------------------------------
19
+ import struct
20
+ from dataclasses import dataclass
21
+ from typing import Optional
22
+
23
+ from bumble.device import Connection
24
+ from bumble.att import ATT_Error
25
+ from bumble.gatt import (
26
+ Characteristic,
27
+ DelegatedCharacteristicAdapter,
28
+ TemplateService,
29
+ CharacteristicValue,
30
+ UTF8CharacteristicAdapter,
31
+ InvalidServiceError,
32
+ GATT_VOLUME_OFFSET_CONTROL_SERVICE,
33
+ GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
34
+ GATT_AUDIO_LOCATION_CHARACTERISTIC,
35
+ GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
36
+ GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
37
+ )
38
+ from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
39
+ from bumble.utils import OpenIntEnum
40
+ from bumble.profiles.bap import AudioLocation
41
+
42
+ # -----------------------------------------------------------------------------
43
+ # Constants
44
+ # -----------------------------------------------------------------------------
45
+
46
+ MIN_VOLUME_OFFSET = -255
47
+ MAX_VOLUME_OFFSET = 255
48
+ CHANGE_COUNTER_MAX_VALUE = 0xFF
49
+
50
+
51
+ class SetVolumeOffsetOpCode(OpenIntEnum):
52
+ SET_VOLUME_OFFSET = 0x01
53
+
54
+
55
+ class ErrorCode(OpenIntEnum):
56
+ """
57
+ See Volume Offset Control Service 1.6. Application error codes.
58
+ """
59
+
60
+ INVALID_CHANGE_COUNTER = 0x80
61
+ OPCODE_NOT_SUPPORTED = 0x81
62
+ VALUE_OUT_OF_RANGE = 0x82
63
+
64
+
65
+ # -----------------------------------------------------------------------------
66
+ @dataclass
67
+ class VolumeOffsetState:
68
+ volume_offset: int = 0
69
+ change_counter: int = 0
70
+ attribute_value: Optional[CharacteristicValue] = None
71
+
72
+ def __bytes__(self) -> bytes:
73
+ return struct.pack('<hB', self.volume_offset, self.change_counter)
74
+
75
+ @classmethod
76
+ def from_bytes(cls, data: bytes):
77
+ volume_offset, change_counter = struct.unpack('<hB', data)
78
+ return cls(volume_offset, change_counter)
79
+
80
+ def increment_change_counter(self) -> None:
81
+ self.change_counter = (self.change_counter + 1) % (CHANGE_COUNTER_MAX_VALUE + 1)
82
+
83
+ async def notify_subscribers_via_connection(self, connection: Connection) -> None:
84
+ assert self.attribute_value is not None
85
+ await connection.device.notify_subscribers(
86
+ attribute=self.attribute_value, value=bytes(self)
87
+ )
88
+
89
+ def on_read(self, _connection: Optional[Connection]) -> bytes:
90
+ return bytes(self)
91
+
92
+
93
+ @dataclass
94
+ class VocsAudioLocation:
95
+ audio_location: AudioLocation = AudioLocation.NOT_ALLOWED
96
+ attribute_value: Optional[CharacteristicValue] = None
97
+
98
+ def __bytes__(self) -> bytes:
99
+ return struct.pack('<I', self.audio_location)
100
+
101
+ @classmethod
102
+ def from_bytes(cls, data: bytes):
103
+ audio_location = AudioLocation(struct.unpack('<I', data)[0])
104
+ return cls(audio_location)
105
+
106
+ def on_read(self, _connection: Optional[Connection]) -> bytes:
107
+ return bytes(self)
108
+
109
+ async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
110
+ assert connection
111
+ assert self.attribute_value
112
+
113
+ self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
114
+ await connection.device.notify_subscribers(
115
+ attribute=self.attribute_value, value=value
116
+ )
117
+
118
+
119
+ @dataclass
120
+ class VolumeOffsetControlPoint:
121
+ volume_offset_state: VolumeOffsetState
122
+
123
+ async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
124
+ assert connection
125
+
126
+ opcode = value[0]
127
+ if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
128
+ raise ATT_Error(ErrorCode.OPCODE_NOT_SUPPORTED)
129
+
130
+ change_counter, volume_offset = struct.unpack('<Bh', value[1:])
131
+ await self._set_volume_offset(connection, change_counter, volume_offset)
132
+
133
+ async def _set_volume_offset(
134
+ self,
135
+ connection: Connection,
136
+ change_counter_operand: int,
137
+ volume_offset_operand: int,
138
+ ) -> None:
139
+ change_counter = self.volume_offset_state.change_counter
140
+
141
+ if change_counter != change_counter_operand:
142
+ raise ATT_Error(ErrorCode.INVALID_CHANGE_COUNTER)
143
+
144
+ if not MIN_VOLUME_OFFSET <= volume_offset_operand <= MAX_VOLUME_OFFSET:
145
+ raise ATT_Error(ErrorCode.VALUE_OUT_OF_RANGE)
146
+
147
+ self.volume_offset_state.volume_offset = volume_offset_operand
148
+ self.volume_offset_state.increment_change_counter()
149
+ await self.volume_offset_state.notify_subscribers_via_connection(connection)
150
+
151
+
152
+ @dataclass
153
+ class AudioOutputDescription:
154
+ audio_output_description: str = ''
155
+ attribute_value: Optional[CharacteristicValue] = None
156
+
157
+ @classmethod
158
+ def from_bytes(cls, data: bytes):
159
+ return cls(audio_output_description=data.decode('utf-8'))
160
+
161
+ def __bytes__(self) -> bytes:
162
+ return self.audio_output_description.encode('utf-8')
163
+
164
+ def on_read(self, _connection: Optional[Connection]) -> bytes:
165
+ return bytes(self)
166
+
167
+ async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
168
+ assert connection
169
+ assert self.attribute_value
170
+
171
+ self.audio_output_description = value.decode('utf-8')
172
+ await connection.device.notify_subscribers(
173
+ attribute=self.attribute_value, value=value
174
+ )
175
+
176
+
177
+ # -----------------------------------------------------------------------------
178
+ class VolumeOffsetControlService(TemplateService):
179
+ UUID = GATT_VOLUME_OFFSET_CONTROL_SERVICE
180
+
181
+ def __init__(
182
+ self,
183
+ volume_offset_state: Optional[VolumeOffsetState] = None,
184
+ audio_location: Optional[VocsAudioLocation] = None,
185
+ audio_output_description: Optional[AudioOutputDescription] = None,
186
+ ) -> None:
187
+
188
+ self.volume_offset_state = (
189
+ VolumeOffsetState() if volume_offset_state is None else volume_offset_state
190
+ )
191
+
192
+ self.audio_location = (
193
+ VocsAudioLocation() if audio_location is None else audio_location
194
+ )
195
+
196
+ self.audio_output_description = (
197
+ AudioOutputDescription()
198
+ if audio_output_description is None
199
+ else audio_output_description
200
+ )
201
+
202
+ self.volume_offset_control_point: VolumeOffsetControlPoint = (
203
+ VolumeOffsetControlPoint(self.volume_offset_state)
204
+ )
205
+
206
+ self.volume_offset_state_characteristic = DelegatedCharacteristicAdapter(
207
+ Characteristic(
208
+ uuid=GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
209
+ properties=(
210
+ Characteristic.Properties.READ | Characteristic.Properties.NOTIFY
211
+ ),
212
+ permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
213
+ value=CharacteristicValue(read=self.volume_offset_state.on_read),
214
+ ),
215
+ encode=lambda value: bytes(value),
216
+ )
217
+
218
+ self.audio_location_characteristic = DelegatedCharacteristicAdapter(
219
+ Characteristic(
220
+ uuid=GATT_AUDIO_LOCATION_CHARACTERISTIC,
221
+ properties=(
222
+ Characteristic.Properties.READ
223
+ | Characteristic.Properties.NOTIFY
224
+ | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
225
+ ),
226
+ permissions=(
227
+ Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
228
+ | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
229
+ ),
230
+ value=CharacteristicValue(
231
+ read=self.audio_location.on_read,
232
+ write=self.audio_location.on_write,
233
+ ),
234
+ ),
235
+ encode=lambda value: bytes(value),
236
+ decode=VocsAudioLocation.from_bytes,
237
+ )
238
+ self.audio_location.attribute_value = self.audio_location_characteristic.value
239
+
240
+ self.volume_offset_control_point_characteristic = Characteristic(
241
+ uuid=GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC,
242
+ properties=Characteristic.Properties.WRITE,
243
+ permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
244
+ value=CharacteristicValue(write=self.volume_offset_control_point.on_write),
245
+ )
246
+
247
+ self.audio_output_description_characteristic = DelegatedCharacteristicAdapter(
248
+ Characteristic(
249
+ uuid=GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
250
+ properties=(
251
+ Characteristic.Properties.READ
252
+ | Characteristic.Properties.NOTIFY
253
+ | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
254
+ ),
255
+ permissions=(
256
+ Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
257
+ | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
258
+ ),
259
+ value=CharacteristicValue(
260
+ read=self.audio_output_description.on_read,
261
+ write=self.audio_output_description.on_write,
262
+ ),
263
+ )
264
+ )
265
+
266
+ self.audio_output_description.attribute_value = (
267
+ self.audio_output_description_characteristic.value
268
+ )
269
+
270
+ super().__init__(
271
+ characteristics=[
272
+ self.volume_offset_state_characteristic, # type: ignore
273
+ self.audio_location_characteristic, # type: ignore
274
+ self.volume_offset_control_point_characteristic, # type: ignore
275
+ self.audio_output_description_characteristic, # type: ignore
276
+ ],
277
+ primary=False,
278
+ )
279
+
280
+
281
+ # -----------------------------------------------------------------------------
282
+ # Client
283
+ # -----------------------------------------------------------------------------
284
+ class VolumeOffsetControlServiceProxy(ProfileServiceProxy):
285
+ SERVICE_CLASS = VolumeOffsetControlService
286
+
287
+ def __init__(self, service_proxy: ServiceProxy) -> None:
288
+ self.service_proxy = service_proxy
289
+
290
+ if not (
291
+ characteristics := service_proxy.get_characteristics_by_uuid(
292
+ GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC
293
+ )
294
+ ):
295
+ raise InvalidServiceError("Volume Offset State characteristic not found")
296
+ self.volume_offset_state = DelegatedCharacteristicAdapter(
297
+ characteristics[0], decode=VolumeOffsetState.from_bytes
298
+ )
299
+
300
+ if not (
301
+ characteristics := service_proxy.get_characteristics_by_uuid(
302
+ GATT_AUDIO_LOCATION_CHARACTERISTIC
303
+ )
304
+ ):
305
+ raise InvalidServiceError("Audio Location characteristic not found")
306
+ self.audio_location = DelegatedCharacteristicAdapter(
307
+ characteristics[0],
308
+ encode=lambda value: bytes(value),
309
+ decode=VocsAudioLocation.from_bytes,
310
+ )
311
+
312
+ if not (
313
+ characteristics := service_proxy.get_characteristics_by_uuid(
314
+ GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC
315
+ )
316
+ ):
317
+ raise InvalidServiceError(
318
+ "Volume Offset Control Point characteristic not found"
319
+ )
320
+ self.volume_offset_control_point = characteristics[0]
321
+
322
+ if not (
323
+ characteristics := service_proxy.get_characteristics_by_uuid(
324
+ GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC
325
+ )
326
+ ):
327
+ raise InvalidServiceError(
328
+ "Audio Output Description characteristic not found"
329
+ )
330
+ self.audio_output_description = UTF8CharacteristicAdapter(characteristics[0])
bumble/sdp.py CHANGED
@@ -344,9 +344,6 @@ class DataElement:
344
344
  ] # Keep a copy so we can re-serialize to an exact replica
345
345
  return result
346
346
 
347
- def to_bytes(self):
348
- return bytes(self)
349
-
350
347
  def __bytes__(self):
351
348
  # Return early if we have a cache
352
349
  if self.bytes:
@@ -623,11 +620,8 @@ class SDP_PDU:
623
620
  def init_from_bytes(self, pdu, offset):
624
621
  return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
625
622
 
626
- def to_bytes(self):
627
- return self.pdu
628
-
629
623
  def __bytes__(self):
630
- return self.to_bytes()
624
+ return self.pdu
631
625
 
632
626
  def __str__(self):
633
627
  result = f'{color(self.name, "blue")} [TID={self.transaction_id}]'
bumble/smp.py CHANGED
@@ -298,11 +298,8 @@ class SMP_Command:
298
298
  def init_from_bytes(self, pdu: bytes, offset: int) -> None:
299
299
  return HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
300
300
 
301
- def to_bytes(self):
302
- return self.pdu
303
-
304
301
  def __bytes__(self):
305
- return self.to_bytes()
302
+ return self.pdu
306
303
 
307
304
  def __str__(self):
308
305
  result = color(self.name, 'yellow')
@@ -698,6 +695,7 @@ class Session:
698
695
  self.ltk_ediv = 0
699
696
  self.ltk_rand = bytes(8)
700
697
  self.link_key: Optional[bytes] = None
698
+ self.maximum_encryption_key_size: int = 0
701
699
  self.initiator_key_distribution: int = 0
702
700
  self.responder_key_distribution: int = 0
703
701
  self.peer_random_value: Optional[bytes] = None
@@ -744,6 +742,10 @@ class Session:
744
742
  else:
745
743
  self.pairing_result = None
746
744
 
745
+ self.maximum_encryption_key_size = (
746
+ pairing_config.delegate.maximum_encryption_key_size
747
+ )
748
+
747
749
  # Key Distribution (default values before negotiation)
748
750
  self.initiator_key_distribution = (
749
751
  pairing_config.delegate.local_initiator_key_distribution
@@ -996,7 +998,7 @@ class Session:
996
998
  io_capability=self.io_capability,
997
999
  oob_data_flag=self.oob_data_flag,
998
1000
  auth_req=self.auth_req,
999
- maximum_encryption_key_size=16,
1001
+ maximum_encryption_key_size=self.maximum_encryption_key_size,
1000
1002
  initiator_key_distribution=self.initiator_key_distribution,
1001
1003
  responder_key_distribution=self.responder_key_distribution,
1002
1004
  )
@@ -1008,7 +1010,7 @@ class Session:
1008
1010
  io_capability=self.io_capability,
1009
1011
  oob_data_flag=self.oob_data_flag,
1010
1012
  auth_req=self.auth_req,
1011
- maximum_encryption_key_size=16,
1013
+ maximum_encryption_key_size=self.maximum_encryption_key_size,
1012
1014
  initiator_key_distribution=self.initiator_key_distribution,
1013
1015
  responder_key_distribution=self.responder_key_distribution,
1014
1016
  )
@@ -1949,7 +1951,7 @@ class Manager(EventEmitter):
1949
1951
  f'{connection.peer_address}: {command}'
1950
1952
  )
1951
1953
  cid = SMP_BR_CID if connection.transport == BT_BR_EDR_TRANSPORT else SMP_CID
1952
- connection.send_l2cap_pdu(cid, command.to_bytes())
1954
+ connection.send_l2cap_pdu(cid, bytes(command))
1953
1955
 
1954
1956
  def on_smp_security_request_command(
1955
1957
  self, connection: Connection, request: SMP_Security_Request_Command
@@ -0,0 +1,130 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ import logging
19
+ import pathlib
20
+ import urllib.request
21
+ import urllib.error
22
+
23
+ import click
24
+
25
+ from bumble.colors import color
26
+ from bumble.drivers import intel
27
+
28
+
29
+ # -----------------------------------------------------------------------------
30
+ # Logging
31
+ # -----------------------------------------------------------------------------
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ # -----------------------------------------------------------------------------
36
+ # Constants
37
+ # -----------------------------------------------------------------------------
38
+ LINUX_KERNEL_GIT_SOURCE = "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git/plain/intel"
39
+
40
+
41
+ # -----------------------------------------------------------------------------
42
+ # Functions
43
+ # -----------------------------------------------------------------------------
44
+ def download_file(base_url, name):
45
+ url = f"{base_url}/{name}"
46
+ with urllib.request.urlopen(url) as file:
47
+ data = file.read()
48
+ print(f"Downloaded {name}: {len(data)} bytes")
49
+ return data
50
+
51
+
52
+ # -----------------------------------------------------------------------------
53
+ @click.command
54
+ @click.option(
55
+ "--output-dir",
56
+ default="",
57
+ help="Output directory where the files will be saved. Defaults to the OS-specific"
58
+ "app data dir, which the driver will check when trying to find firmware",
59
+ show_default=True,
60
+ )
61
+ @click.option(
62
+ "--source",
63
+ type=click.Choice(["linux-kernel"]),
64
+ default="linux-kernel",
65
+ show_default=True,
66
+ )
67
+ @click.option("--single", help="Only download a single image set, by its base name")
68
+ @click.option("--force", is_flag=True, help="Overwrite files if they already exist")
69
+ def main(output_dir, source, single, force):
70
+ """Download Intel firmware images and configs."""
71
+
72
+ # Check that the output dir exists
73
+ if output_dir == '':
74
+ output_dir = intel.intel_firmware_dir()
75
+ else:
76
+ output_dir = pathlib.Path(output_dir)
77
+ if not output_dir.is_dir():
78
+ print("Output dir does not exist or is not a directory")
79
+ return
80
+
81
+ base_url = {
82
+ "linux-kernel": LINUX_KERNEL_GIT_SOURCE,
83
+ }[source]
84
+
85
+ print("Downloading")
86
+ print(color("FROM:", "green"), base_url)
87
+ print(color("TO:", "green"), output_dir)
88
+
89
+ if single:
90
+ images = [(f"{single}.sfi", f"{single}.ddc")]
91
+ else:
92
+ images = [
93
+ (f"{base_name}.sfi", f"{base_name}.ddc")
94
+ for base_name in intel.INTEL_FW_IMAGE_NAMES
95
+ ]
96
+
97
+ for fw_name, config_name in images:
98
+ print(color("---", "yellow"))
99
+ fw_image_out = output_dir / fw_name
100
+ if not force and fw_image_out.exists():
101
+ print(color(f"{fw_image_out} already exists, skipping", "red"))
102
+ continue
103
+ if config_name:
104
+ config_image_out = output_dir / config_name
105
+ if not force and config_image_out.exists():
106
+ print(color("f{config_image_out} already exists, skipping", "red"))
107
+ continue
108
+
109
+ try:
110
+ fw_image = download_file(base_url, fw_name)
111
+ except urllib.error.HTTPError as error:
112
+ print(f"Failed to download {fw_name}: {error}")
113
+ continue
114
+
115
+ config_image = None
116
+ if config_name:
117
+ try:
118
+ config_image = download_file(base_url, config_name)
119
+ except urllib.error.HTTPError as error:
120
+ print(f"Failed to download {config_name}: {error}")
121
+ continue
122
+
123
+ fw_image_out.write_bytes(fw_image)
124
+ if config_image:
125
+ config_image_out.write_bytes(config_image)
126
+
127
+
128
+ # -----------------------------------------------------------------------------
129
+ if __name__ == '__main__':
130
+ main()