Skip to content

Commit e50e600

Browse files
Merge pull request #93 from FrameworkComputer/ccg3-hid
2 parents 94b7e13 + d188a71 commit e50e600

File tree

14 files changed

+1127
-153
lines changed

14 files changed

+1127
-153
lines changed

README.md

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ see the [Support Matrices](support-matrices.md).
4444
- [x] PD
4545
- [x] ME (Only on Linux)
4646
- [x] Retimer
47-
- [x] Get firmware version of expansion cards (Not on UEFI so far)
47+
- [x] Get Expansion Card Firmware (Not on UEFI so far)
4848
- [x] HDMI Expansion Card (`--dp-hdmi-info`)
4949
- [x] DisplayPort Expansion Card (`--dp-hdmi-info`)
5050
- [x] Audio Expansion Card (`--audio-card-info`)
51+
- [x] Update Expansion Card Firmware (Not on UEFI so far)
52+
- [x] HDMI Expansion Card (`--dp-hdmi-update`)
53+
- [x] DisplayPort Expansion Card (`--dp-hdmi-update`)
54+
- [ ] Audio Expansion Card
5155

5256
###### Firmware Update
5357

@@ -155,25 +159,30 @@ Swiss army knife for Framework laptops
155159
Usage: framework_tool [OPTIONS]
156160
157161
Options:
158-
-v, --verbose... More output per occurrence
159-
-q, --quiet... Less output per occurrence
160-
--versions List current firmware versions version
161-
--esrt Display the UEFI ESRT table
162-
--power Show current power status (battery and AC)
163-
--pdports Show information about USB-C PD prots
164-
--info Show info from SMBIOS (Only on UEFI)
165-
--pd-info Show details about the PD controllers
166-
--privacy Show privacy switch statuses (camera and microphone)
167-
--pd-bin <PD_BIN> Parse versions from PD firmware binary file
168-
--ec-bin <EC_BIN> Parse versions from EC firmware binary file
169-
--capsule <CAPSULE> Parse UEFI Capsule information from binary file
170-
--dump <DUMP> Dump extracted UX capsule bitmap image to a file
171-
--intrusion Show status of intrusion switch
172-
--kblight [<KBLIGHT>] Set keyboard backlight percentage or get, if no value provided
173-
--console <CONSOLE> Select which driver is used. By default portio is used [possible values: recent, follow]
174-
--driver <DRIVER> Select which driver is used. By default portio is used [possible values: portio, cros-ec, windows]
175-
-t, --test Run self-test to check if interaction with EC is possible
176-
-h, --help Print help information
162+
-v, --verbose... More output per occurrence
163+
-q, --quiet... Less output per occurrence
164+
--versions List current firmware versions version
165+
--esrt Display the UEFI ESRT table
166+
--power Show current power status (battery and AC)
167+
--pdports Show information about USB-C PD prots
168+
--info Show info from SMBIOS (Only on UEFI)
169+
--pd-info Show details about the PD controllers
170+
--dp-hdmi-info Show details about connected DP or HDMI Expansion Cards
171+
--dp-hdmi-update <UPDATE_BIN> Update the DisplayPort or HDMI Expansion Card
172+
--audio-card-info Show details about connected Audio Expansion Cards (Needs root privileges)
173+
--privacy Show privacy switch statuses (camera and microphone)
174+
--pd-bin <PD_BIN> Parse versions from PD firmware binary file
175+
--ec-bin <EC_BIN> Parse versions from EC firmware binary file
176+
--capsule <CAPSULE> Parse UEFI Capsule information from binary file
177+
--dump <DUMP> Dump extracted UX capsule bitmap image to a file
178+
--ho2-capsule <HO2_CAPSULE> Parse UEFI Capsule information from binary file
179+
--intrusion Show status of intrusion switch
180+
--inputmodules Show status of the input modules (Framework 16 only)
181+
--kblight [<KBLIGHT>] Set keyboard backlight percentage or get, if no value provided
182+
--console <CONSOLE> Get EC console, choose whether recent or to follow the output [possible values: recent, follow]
183+
--driver <DRIVER> Select which driver is used. By default portio is used [possible values: portio, cros-ec, windows]
184+
-t, --test Run self-test to check if interaction with EC is possible
185+
-h, --help Print help information
177186
```
178187

179188
Many actions require root. First build with cargo and then run the binary with sudo:

decode_pcapng.py

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
from collections import namedtuple
5+
import struct
6+
import sys
7+
8+
from pcapng import FileScanner, blocks
9+
10+
FORMATS = [
11+
# Both binaries stitched right after another
12+
'binary',
13+
# Flashimage, like it's on the flash, with padded regions
14+
'flashimage',
15+
# Two CYACD files, like the CCG3 SDK outputs and FWUPD expects
16+
'cyacd'
17+
]
18+
19+
# CCG3 row size
20+
ROW_SIZE = 128
21+
# CCG3 has a maximum of 1024 rows
22+
MAX_ROWS = 1024
23+
24+
DEBUG = False
25+
VERBOSE = False
26+
27+
# Run the updater on Windows and capture the USB packets with Wireshark and USBPcap
28+
# Then you can use this script to extract the binary from it
29+
#
30+
# Sample files
31+
# -t dp -V 006 -b 1.7 dp-flash-006.pcapng
32+
# -t dp -V 008 -b 1.8 flash-100-to-8.pcapng
33+
# -t dp -V 100 -b 1.6 reflash100.pcapng
34+
# -t dp -V 101 -b 2.12 reflash101.pcapng
35+
# -t hdmi -V 005 -b 1.4 --second-first hdmi-flash-005.pcapng
36+
# -t hdmi -V 006 -b 1.29 hdmi-flash-6.pcapng
37+
# -t hdmi -V 102 -b 2.8 --second-first hdmi-flash-102.pcapng
38+
# -t hdmi -V 103 -b 2.6 --second-first hdmi-flash-103.pcapng
39+
# -t hdmi -V 104 -b 2.4 --second-first hdmi-flash-104.pcapng
40+
# -t hdmi -V 105 -b 1.26 hdmi-flash-105.pcapng
41+
42+
# From https://github.com/JohnDMcMaster/usbrply/blob/master/usbrply/win_pcap.py#L171
43+
# transfer_type=2 is URB_CONTROL
44+
# irp_info: 0 means from host, 1 means from device
45+
usb_urb_win_nt = namedtuple(
46+
'usb_urb_win',
47+
(
48+
# Length of entire packet entry including htis header and additional pkt_len data
49+
'pcap_hdr_len',
50+
# IRP ID
51+
# buffer ID or something like that
52+
# it is not a unique packet ID
53+
# but can be used to match up submit and response
54+
'id',
55+
# IRP_USBD_STATUS
56+
'irp_status',
57+
# USB Function
58+
'usb_func',
59+
# IRP Information
60+
# Ex: Direction: PDO => FDO
61+
'irp_info',
62+
# USB port
63+
# Ex: 3
64+
'bus_id',
65+
# USB device on that port
66+
# Ex: 16
67+
'device',
68+
# Which endpoint on that bus
69+
# Ex: 0x80 (0 in)
70+
'endpoint',
71+
# Ex: URB_CONTROL
72+
'transfer_type',
73+
# Length of data beyond header
74+
'data_length',
75+
))
76+
usb_urb_win_fmt = (
77+
'<'
78+
'H' # pcap_hdr_len
79+
'Q' # irp_id
80+
'i' # irp_status
81+
'H' # usb_func
82+
'B' # irp_info
83+
'H' # bus_id
84+
'H' # device
85+
'B' # endpoint
86+
'B' # transfer_type
87+
'I' # data_length
88+
)
89+
usb_urb_sz = struct.calcsize(usb_urb_win_fmt)
90+
def usb_urb(s):
91+
return usb_urb_win_nt(*struct.unpack(usb_urb_win_fmt, bytes(s)))
92+
93+
94+
def format_hex(buf):
95+
return ''.join('{:02x} '.format(x) for x in buf)
96+
97+
def print_image_info(binary, index):
98+
rows = len(binary)
99+
size = rows * len(binary[0][1])
100+
print("Image {} Size: {} B, {} rows".format(index, size, rows))
101+
print(" FW at: 0x{:04X} Metadata at 0x{:04X}".format(binary[0][0], binary[-1][0]))
102+
103+
def twos_comp(val, bits):
104+
"""compute the 2's complement of int value val"""
105+
if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255
106+
val = val - (1 << bits) # compute negative value
107+
return val # return positive value as isdef twos_comp(val, bits):
108+
"""compute the 2's complement of int value val"""
109+
if (val & (1 << (bits - 1))) != 0: # if sign bit is set e.g., 8bit: 128-255
110+
val = val - (1 << bits) # compute negative value
111+
return val # return positive value as isi
112+
113+
def checksum_calc(s):
114+
sum = 0
115+
for c in s:
116+
sum = (sum + c) & 0xFF
117+
sum = -(sum % 256)
118+
return (sum & 0xFF)
119+
120+
def write_cyacd_row(f, row_no, data):
121+
# No idea what array ID is but it seems fine to keep it 0. Official builds also have that
122+
array_id = 0
123+
data_len = len(data)
124+
125+
# Sum all bytes and calc two's complement
126+
cs_bytes = bytes([array_id, row_no&0xFF, (row_no&0xFF00)>>8, data_len&0xFF, (data_len&0xFF00)>>8])
127+
checksum = checksum_calc(cs_bytes+data)
128+
129+
if data_len != 0x80:
130+
print("Len is {} instead of 0x80", data_len)
131+
sys.exit(1)
132+
data_hex = ''.join("{:02X}".format(x) for x in data)
133+
134+
f.write(":{:02X}{:04X}{:04X}{}{:02X}\n".format(array_id, row_no, data_len, data_hex, checksum))
135+
136+
# Write the binary as cyacd file. Can only hold one firmware image per file
137+
def write_cyacd(path, binary1):
138+
with open(path, "w") as f:
139+
# CYACD Header
140+
# Si ID ########
141+
# Si Rev ##
142+
# Checksum Type ##
143+
f.write("1D0011AD0000\n")
144+
145+
for (addr, row) in binary1[0:-1]:
146+
write_cyacd_row(f, addr, row)
147+
148+
write_cyacd_row(f, binary1[-1][0], binary1[-1][1])
149+
150+
# Just concatenate both firmware binaries
151+
def write_bin(path, binary1, binary2):
152+
with open(path, "wb") as f:
153+
for (_, row) in binary1:
154+
f.write(row)
155+
156+
for (_, row) in binary2:
157+
f.write(row)
158+
159+
# Write the binary in the same layout with padding as on flash
160+
def write_flashimage(path, binary1, binary2):
161+
with open(path, "wb") as f:
162+
# Write fist padding
163+
# Verified
164+
print("Padding 1: {:04X} to {:04X}".format(0, binary1[0][0]))
165+
for row in range(0, binary1[0][0]):
166+
f.write(b'\0' * ROW_SIZE)
167+
168+
# Verified
169+
print("FW IMG 1: {:04X} to {:04X}".format(binary1[0][0], binary1[-2][0]))
170+
for (addr, row) in binary1[0:-1]:
171+
f.write(row)
172+
173+
print("Padding 2: {:04X} to {:04X}".format(binary1[-2][0], binary2[0][0]-1))
174+
for row in range(binary1[-2][0], binary2[0][0]-1):
175+
f.write(b'\0' * ROW_SIZE)
176+
177+
print("FW IMG 2: {:04X} to {:04X}".format(binary2[0][0], binary2[-2][0]))
178+
for (addr, row) in binary2[0:-1]:
179+
f.write(row)
180+
181+
print("Padding 3: {:04X} to {:04X}".format(binary2[-2][0], binary2[-1][0]-1))
182+
for row in range(binary2[-2][0], binary2[-1][0]-1):
183+
f.write(b'\0' * ROW_SIZE)
184+
185+
# Notice that these are in reverse order!
186+
# FW2 metadata is before FW1 metadata
187+
print("Metadata 2: {:04X} to {:04X}".format(binary2[-1][0], binary2[-1][0]))
188+
f.write(binary2[-1][1])
189+
print("Metadata 1: {:04X} to {:04X}".format(binary1[-1][0], binary1[-1][0]))
190+
f.write(binary1[-1][1])
191+
192+
# Pad from end of FW1 metadata
193+
print("Padding 4: {:04X} to {:04X}".format(binary1[-1][0], MAX_ROWS-1))
194+
for row in range(binary1[-1][0], MAX_ROWS-1):
195+
f.write(b'\0' * ROW_SIZE)
196+
197+
198+
def check_assumptions(img1_binary, img2_binary):
199+
# TODO: Check that addresses are in order
200+
# TODO: Metadata right after each other
201+
202+
# Check assumptions that the updater relies on
203+
if len(img1_binary) != len(img2_binary):
204+
print("VIOLATED Assumption that both images are of the same size!")
205+
sys.exit(1)
206+
if len(img1_binary[0][1]) != ROW_SIZE:
207+
print("VIOLATED Assumption that the row size is {} bytes! Is: {}", ROW_SIZE, img1_binary[0][1])
208+
sys.exit(1)
209+
if img1_binary[0][0] != 0x0030:
210+
print("VIOLATED Assumption that start row of image 1 is at 0x0030. Is at 0x{:04X}".format(img1_binary[0][0]))
211+
sys.exit(1)
212+
if img1_binary[-1][0] != 0x03FF:
213+
print("VIOLATED Assumption that metadata row of image 1 is at 0x03FF. Is at 0x{:04X}".format(img1_binary[-1][0]))
214+
sys.exit(1)
215+
if img2_binary[0][0] != 0x0200:
216+
print("VIOLATED Assumption that start row of image 2 is at 0x0200. Is at 0x{:04X}".format(img2_binary[0]))
217+
sys.exit(1)
218+
if img2_binary[-1][0] != 0x03FE:
219+
print("VIOLATED Assumption that metadata row of image 2 is at 0x03FE. Is at 0x{:04X}".format(img2_binary[-1][0]))
220+
sys.exit(1)
221+
if img1_binary == img2_binary:
222+
print("VIOLATED Assumption that both images are not the same");
223+
sys.exit(1)
224+
225+
226+
def decode_pcapng(path, bus_id, dev, second_first):
227+
img1_binary = [] # [(addr, row)]
228+
img2_binary = [] # [(addr, row)]
229+
with open(path, "rb") as f:
230+
scanner = FileScanner(f)
231+
block_no = 1
232+
for i, block in enumerate(scanner):
233+
if type(block) is blocks.EnhancedPacket or type(block) is blocks.SimplePacket:
234+
img1 = False
235+
img2 = False
236+
237+
#print(block_no, block.packet_len, block.packet_data)
238+
packet = block.packet_data
239+
urb = usb_urb(packet[0:usb_urb_sz])
240+
241+
# Filter device
242+
if urb.bus_id == bus_id and urb.device == dev:
243+
img1 = True
244+
elif urb.bus_id == bus_id and urb.device == dev+1:
245+
img2 = True
246+
else:
247+
#print(f"Other device bus_id: {urb.bus_id}, dev: {urb.device}")
248+
#print(f"bus_id: {bus_id}, dev_id: {dev}")
249+
continue
250+
251+
# Only consider outgoing packets
252+
if urb.irp_info != 0:
253+
continue
254+
255+
#print(block_no, urb)
256+
257+
# Skip small packets
258+
if urb.data_length != 140:
259+
continue
260+
261+
if DEBUG:
262+
print(block_no, " ", format_hex(packet))
263+
264+
hid_packet = packet[36:]
265+
if DEBUG:
266+
print(block_no, " ", format_hex(hid_packet))
267+
268+
addr = (hid_packet[3] << 8) + hid_packet[2]
269+
payload = hid_packet[4:]
270+
if VERBOSE:
271+
print("{:4d} 0x{:08X} {}".format(block_no, addr, format_hex(payload)))
272+
273+
if img1:
274+
if second_first:
275+
img2_binary.append((addr, payload))
276+
else:
277+
img1_binary.append((addr, payload))
278+
elif img2:
279+
if second_first:
280+
img1_binary.append((addr, payload))
281+
else:
282+
img2_binary.append((addr, payload))
283+
284+
block_no += 1
285+
else:
286+
pass
287+
#print(block)
288+
return (img1_binary, img2_binary)
289+
290+
291+
def main(args):
292+
[bus_id, dev] = args.bus_dev.split('.')
293+
(img1_binary, img2_binary) = decode_pcapng(args.pcap, int(bus_id), int(dev), args.second_first)
294+
295+
check_assumptions(img1_binary, img2_binary)
296+
297+
print("Firmware version: {}".format(args.version))
298+
299+
print_image_info(img1_binary, 1)
300+
print_image_info(img2_binary, 2)
301+
302+
if args.format == 'binary':
303+
write_bin("{}-{}.bin".format(args.type, args.version), img1_binary, img2_binary)
304+
elif args.format == 'flashimage':
305+
write_flashimage("{}-{}.bin".format(args.type, args.version), img1_binary, img2_binary)
306+
elif args.format == 'cyacd':
307+
write_cyacd("{}-{}-1.cyacd".format(args.type, args.version), img1_binary)
308+
write_cyacd("{}-{}-2.cyacd".format(args.type, args.version), img2_binary)
309+
else:
310+
print(f"Invalid Format {args.format}")
311+
sys.exit(1)
312+
313+
if __name__ == "__main__":
314+
parser = argparse.ArgumentParser(description='Extract firmware from PCAPNG capture')
315+
parser.add_argument('-t', '--type', help='Which type of card', required=True, choices=['dp', 'hdmi'])
316+
parser.add_argument('-V', '--version', help='Which firmware version', required=True)
317+
parser.add_argument('-f', '--format', help='Which output format', required=True, choices=FORMATS)
318+
parser.add_argument('-v', '--verbose', help='Verbose', action='store_true')
319+
parser.add_argument('-b', '--bus-dev', help='Bus ID and Device of first time. Example: 1.23')
320+
parser.add_argument('--second-first', help='If the second image was update first', default=False, action='store_true')
321+
parser.add_argument('pcap', help='Path to the pcap file')
322+
args = parser.parse_args()
323+
324+
if args.verbose:
325+
VERBOSE = True
326+
327+
main(args)

0 commit comments

Comments
 (0)