Skip to content

Commit 5a33a4d

Browse files
committed
Add official Framework EC driver support
1 parent 12b3ea9 commit 5a33a4d

File tree

5 files changed

+272
-7
lines changed

5 files changed

+272
-7
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ It's easiest just to run your script as root.
5555
> [!NOTE]
5656
> Loading a driver will likely require administrator permissions, so you may need to run your script as an administrator.
5757
58-
The Windows version supports 2 different drivers to access the EC:
58+
The Windows version supports 3 different drivers to access the EC:
5959

60+
- The Framework EC driver which is provided in the driver bundles and requires a supported BIOS version. This driver does not require administrator and is the recommended way of using CrOS_EC_Python on supported devices.
6061
- `PawnIO` which is a fairly new scriptable kernel driver, the official signed version can be downloaded [here](https://pawnio.eu/).
6162
- `WinRing0` which while still signed, it has been abandoned and is listed on the Microsoft Vulnerable Driver Blocklist.
6263

@@ -69,7 +70,7 @@ pip install cros-ec-python
6970
#### PawnIO
7071

7172
> [!TIP]
72-
> PawnIO is [designed to be a secure alternative to WinRing0](https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/issues/984#issuecomment-1585591691), and is the recommended driver for Windows.
73+
> PawnIO is [designed to be a secure alternative to WinRing0](https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/issues/984#issuecomment-1585591691), and is recommended over WinRing0.
7374
7475
[PawnIO](https://pawnio.eu/) can be installed using the installer from the website.
7576
PawnIO is signed and does not require disabling driver signature enforcement.
@@ -122,6 +123,7 @@ pdoc cros_ec_python
122123
### Supported Interfaces
123124

124125
- [x] Linux Device (Requires the `cros_ec_dev` kernel module)
126+
- [x] Windows Framework EC Driver
125127
- [x] Windows [PawnIO](https://pawnio.eu/) using [LpcCrOSEC](https://github.com/namazso/PawnIO.Modules/pull/3)
126128
- [x] LPC Bus Interface (Soft-requires the [`portio` package](https://pypi.org/project/portio/))
127129
- [ ] MEC LPC Interface

cros_ec_python/commands/general.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ def get_build_info(ec: CrosEcClass) -> str:
8282
:param ec: The CrOS_EC object.
8383
:return: The build info as a string.
8484
"""
85-
resp = ec.command(0, EC_CMD_GET_BUILD_INFO, 0, 0xf8, warn=False)
85+
# 0xEC is the max command size on some platforms
86+
resp = ec.command(0, EC_CMD_GET_BUILD_INFO, 0, 0xEC, warn=False)
8687
return resp.decode("utf-8").rstrip("\x00")
8788

8889

cros_ec_python/cros_ec.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99

1010
from .constants.COMMON import *
1111
from .baseclass import CrosEcClass
12-
from .devices import lpc, pawnio
12+
from .devices import lpc
1313
if sys.platform == "linux":
1414
from .devices import dev
1515
else:
1616
dev = None
17-
17+
if sys.platform == "win32":
18+
from .devices import pawnio, win_fw_ec
19+
else:
20+
pawnio = win_fw_ec = None
1821

1922
class DeviceTypes(Enum):
2023
"""
@@ -25,9 +28,14 @@ class DeviceTypes(Enum):
2528
This is the Linux device interface, which uses the `/dev/cros_ec` device file.
2629
*Recommended if you have the `cros_ec_dev` kernel module loaded.*
2730
"""
28-
PawnIO = 1
31+
WinFrameworkEC = 1
32+
"""
33+
This is the Framework Windows driver interface.
34+
*Recommended if you have a Framework laptop with a supported BIOS and driver.*
35+
"""
36+
PawnIO = 2
2937
"This is the Windows PawnIO interface, which is recommended on Windows Systems."
30-
LPC = 2
38+
LPC = 3
3139
"This manually talks to the EC over the LPC interface, using the ioports."
3240

3341

@@ -40,6 +48,8 @@ def pick_device() -> DeviceTypes:
4048
"""
4149
if dev and dev.CrosEcDev.detect():
4250
return DeviceTypes.LinuxDev
51+
elif win_fw_ec and win_fw_ec.WinFrameworkEc.detect():
52+
return DeviceTypes.WinFrameworkEC
4353
elif pawnio and pawnio.CrosEcPawnIO.detect():
4454
return DeviceTypes.PawnIO
4555
elif lpc and lpc.CrosEcLpc.detect():
@@ -62,6 +72,8 @@ def get_cros_ec(dev_type: DeviceTypes | None = None, **kwargs) -> CrosEcClass:
6272
match dev_type:
6373
case DeviceTypes.LinuxDev:
6474
return dev.CrosEcDev(**kwargs)
75+
case DeviceTypes.WinFrameworkEC:
76+
return win_fw_ec.WinFrameworkEc(**kwargs)
6577
case DeviceTypes.PawnIO:
6678
return pawnio.CrosEcPawnIO(**kwargs)
6779
case DeviceTypes.LPC:
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
from typing import Final
2+
import warnings
3+
import os
4+
import ctypes
5+
from ctypes import wintypes, windll
6+
7+
from ..baseclass import CrosEcClass
8+
from ..constants.COMMON import *
9+
from ..constants.MEMMAP import EC_MEMMAP_SIZE
10+
from ..exceptions import ECError
11+
12+
FILE_GENERIC_READ: Final = 0x80000000
13+
FILE_GENERIC_WRITE: Final = 0x40000000
14+
FILE_SHARE_READ: Final = 0x00000001
15+
FILE_SHARE_WRITE: Final = 0x00000002
16+
OPEN_EXISTING: Final = 3
17+
18+
FILE_READ_DATA: Final = 0x0001
19+
FILE_WRITE_DATA: Final = 0x0002
20+
FILE_READ_ACCESS: Final = 0x0001
21+
METHOD_BUFFERED: Final = 0
22+
23+
INVALID_HANDLE_VALUE: Final = -1
24+
25+
26+
def ctl_code(device_type, function, method, access) -> int:
27+
return ((device_type) << 16) + ((access) << 14) + ((function) << 2) + method
28+
29+
30+
CROSEC_CMD_MAX_REQUEST = 0x100
31+
32+
FILE_DEVICE_CROS_EMBEDDED_CONTROLLER = 0x80EC
33+
34+
IOCTL_CROSEC_XCMD = ctl_code(
35+
FILE_DEVICE_CROS_EMBEDDED_CONTROLLER,
36+
0x801,
37+
METHOD_BUFFERED,
38+
FILE_READ_DATA | FILE_WRITE_DATA,
39+
)
40+
IOCTL_CROSEC_RDMEM = ctl_code(
41+
FILE_DEVICE_CROS_EMBEDDED_CONTROLLER,
42+
0x802,
43+
METHOD_BUFFERED,
44+
FILE_READ_ACCESS,
45+
)
46+
47+
48+
def CreateFileW(filename, access, mode, creation, flags) -> wintypes.HANDLE:
49+
knl32_CreateFileW = windll.kernel32.CreateFileW
50+
knl32_CreateFileW.argtypes = [
51+
wintypes.LPCWSTR, # lpFileName
52+
wintypes.DWORD, # dwDesiredAccess
53+
wintypes.DWORD, # dwShareMode
54+
wintypes.LPVOID, # lpSecurityAttributes
55+
wintypes.DWORD, # dwCreationDisposition
56+
wintypes.DWORD, # dwFlagsAndAttributes
57+
wintypes.HANDLE, # hTemplateFile
58+
]
59+
knl32_CreateFileW.restype = wintypes.HANDLE
60+
61+
return wintypes.HANDLE(
62+
knl32_CreateFileW(filename, access, mode, 0, creation, flags, 0)
63+
)
64+
65+
66+
def DeviceIoControl(devhandle, ioctl, inbuf, inbufsiz, outbuf, outbufsiz) -> bool:
67+
knl32_DeviceIoControl = windll.kernel32.DeviceIoControl
68+
knl32_DeviceIoControl.argtypes = [
69+
wintypes.HANDLE, # hDevice
70+
wintypes.DWORD, # dwIoControlCode
71+
wintypes.LPVOID, # lpInBuffer
72+
wintypes.DWORD, # nInBufferSize
73+
wintypes.LPVOID, # lpOutBuffer
74+
wintypes.DWORD, # nOutBufferSize
75+
wintypes.LPDWORD, # lpBytesReturned
76+
wintypes.LPVOID, # lpOverlapped
77+
]
78+
knl32_DeviceIoControl.restype = wintypes.BOOL
79+
80+
status = knl32_DeviceIoControl(
81+
devhandle, ioctl, inbuf, inbufsiz, outbuf, outbufsiz, None, None
82+
)
83+
84+
return bool(status)
85+
86+
87+
class WinFrameworkEc(CrosEcClass):
88+
"""
89+
Class to interact with the EC using the Framework EC Windows Driver.
90+
"""
91+
92+
def __init__(self, handle: wintypes.HANDLE | None = None):
93+
"""
94+
Initialise the EC using the Linux cros_ec device.
95+
:param handle: Use a custom device handle, opens one by default.
96+
"""
97+
if handle is None:
98+
handle = CreateFileW(
99+
r"\\.\GLOBALROOT\Device\CrosEC",
100+
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
101+
FILE_SHARE_READ | FILE_SHARE_WRITE,
102+
OPEN_EXISTING,
103+
0,
104+
)
105+
106+
if handle.value == wintypes.HANDLE(INVALID_HANDLE_VALUE).value:
107+
errno = windll.kernel32.GetLastError()
108+
raise OSError(f"{errno}: {ctypes.FormatError(errno)}")
109+
110+
self.handle: wintypes.HANDLE = handle
111+
r"""The handle for \\.\GLOBALROOT\Device\CrosEC."""
112+
113+
def __del__(self):
114+
self.ec_exit()
115+
116+
@staticmethod
117+
def detect() -> bool:
118+
r"""
119+
Checks for `\\.\GLOBALROOT\Device\CrosEC` and returns True if it exists.
120+
"""
121+
return os.path.exists(r"\\.\GLOBALROOT\Device\CrosEC")
122+
123+
def ec_init(self) -> None:
124+
pass
125+
126+
def ec_exit(self) -> None:
127+
"""
128+
Close the file on exit.
129+
"""
130+
if hasattr(self, "handle"):
131+
windll.kernel32.CloseHandle(self.handle)
132+
133+
def command(
134+
self,
135+
version: Int32,
136+
command: Int32,
137+
outsize: Int32,
138+
insize: Int32,
139+
data: bytes = None,
140+
warn: bool = True,
141+
) -> bytes:
142+
"""
143+
Send a command to the EC and return the response.
144+
:param version: Command version number (often 0).
145+
:param command: Command to send (EC_CMD_...).
146+
:param outsize: Outgoing length in bytes.
147+
:param insize: Max number of bytes to accept from the EC.
148+
:param data: Outgoing data to EC.
149+
:param warn: Whether to warn if the response size is not as expected. Default is True.
150+
:return: Incoming data from EC.
151+
"""
152+
if data is None:
153+
data = bytes(outsize)
154+
155+
class CrosEcCommand(ctypes.Structure):
156+
_fields_ = [
157+
("version", ctypes.c_uint32),
158+
("command", ctypes.c_uint32),
159+
("outsize", ctypes.c_uint32),
160+
("insize", ctypes.c_uint32),
161+
("result", ctypes.c_uint32),
162+
("buffer", ctypes.c_ubyte * (CROSEC_CMD_MAX_REQUEST - (4 * 5))),
163+
]
164+
165+
cmd = CrosEcCommand()
166+
cmd.version = version
167+
cmd.command = command
168+
cmd.outsize = outsize
169+
cmd.insize = insize
170+
cmd.result = 0xFF
171+
ctypes.memmove(ctypes.addressof(cmd.buffer), data, len(data))
172+
p_cmd = ctypes.pointer(cmd)
173+
174+
status = DeviceIoControl(
175+
self.handle,
176+
IOCTL_CROSEC_XCMD,
177+
p_cmd,
178+
ctypes.sizeof(CrosEcCommand),
179+
p_cmd,
180+
ctypes.sizeof(CrosEcCommand),
181+
)
182+
if not status:
183+
errno = windll.kernel32.GetLastError()
184+
raise IOError(
185+
f"ioctl failed with error {errno}: {ctypes.FormatError(errno)}"
186+
)
187+
188+
if cmd.insize != insize:
189+
warnings.warn(
190+
f"Expected {insize} bytes, got {cmd.insize} back from EC",
191+
RuntimeWarning,
192+
)
193+
194+
if cmd.result != 0:
195+
raise ECError(cmd.result)
196+
197+
return bytes(cmd.buffer[:insize])
198+
199+
def memmap(self, offset: Int32, num_bytes: Int32) -> bytes:
200+
"""
201+
Read memory from the EC.
202+
:param offset: Offset to read from.
203+
:param num_bytes: Number of bytes to read.
204+
:return: Bytes read from the EC.
205+
"""
206+
207+
class CrosEcReadMem(ctypes.Structure):
208+
_fields_ = [
209+
("offset", ctypes.c_uint32),
210+
("bytes", ctypes.c_uint32),
211+
("buffer", ctypes.c_ubyte * EC_MEMMAP_SIZE),
212+
]
213+
214+
data = CrosEcReadMem()
215+
data.offset = offset
216+
data.bytes = num_bytes
217+
p_data = ctypes.pointer(data)
218+
219+
status = DeviceIoControl(
220+
self.handle,
221+
IOCTL_CROSEC_RDMEM,
222+
p_data,
223+
ctypes.sizeof(CrosEcReadMem),
224+
p_data,
225+
ctypes.sizeof(CrosEcReadMem),
226+
)
227+
228+
if not status:
229+
errno = windll.kernel32.GetLastError()
230+
raise IOError(
231+
f"ioctl failed with error {errno}: {ctypes.FormatError(errno)}"
232+
)
233+
234+
if data.bytes != num_bytes:
235+
warnings.warn(
236+
f"Expected {num_bytes} bytes, got {data.bytes} back from EC",
237+
RuntimeWarning,
238+
)
239+
240+
return bytes(data.buffer[:num_bytes])

tests/general.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import unittest
2+
import sys
23
from cros_ec_python import get_cros_ec, ECError, general as ec_general
34

5+
if sys.platform == "win32":
6+
from cros_ec_python.devices import win_fw_ec
7+
else:
8+
win_fw_ec = None
9+
410
ec = get_cros_ec()
511

612

@@ -90,6 +96,10 @@ def test_error(self):
9096
with self.assertRaises(ECError):
9197
ec_general.test_protocol(ec, 1, 0, bytes())
9298

99+
@unittest.skipIf(
100+
win_fw_ec is not None and isinstance(ec, win_fw_ec.WinFrameworkEc),
101+
"not supported on Framework's Driver",
102+
)
93103
def test_warning(self):
94104
with self.assertWarns(RuntimeWarning):
95105
length = 16

0 commit comments

Comments
 (0)