Skip to content

Commit a13dffb

Browse files
committed
Add PawnIO LpcCrOSEC device backend
1 parent 454fa99 commit a13dffb

File tree

5 files changed

+225
-6
lines changed

5 files changed

+225
-6
lines changed

cros_ec_python/__init__.py

100644100755
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@
77
from .commands import memmap, general, features, pwm, leds, thermal, framework_laptop
88
from .exceptions import ECError
99
from .devices.lpc import CrosEcLpc
10-
if __import__("os").name == "posix":
11-
from .devices.dev import CrosEcDev
10+
match __import__("os").name:
11+
case "posix":
12+
from .devices.dev import CrosEcDev
13+
case "nt":
14+
from .devices.pawnio import CrosEcPawnIO

cros_ec_python/commands/general.py

100644100755
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ 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, 0xfc, warn=False)
85+
resp = ec.command(0, EC_CMD_GET_BUILD_INFO, 0, 0xf8, warn=False)
8686
return resp.decode("utf-8").rstrip("\x00")
8787

8888

cros_ec_python/cros_ec.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from .constants.COMMON import *
1111
from .baseclass import CrosEcClass
12-
from .devices import lpc
12+
from .devices import lpc, pawnio
1313
if sys.platform == "linux":
1414
from .devices import dev
1515
else:
@@ -25,7 +25,9 @@ class DeviceTypes(Enum):
2525
This is the Linux device interface, which uses the `/dev/cros_ec` device file.
2626
*Recommended if you have the `cros_ec_dev` kernel module loaded.*
2727
"""
28-
LPC = 1
28+
PawnIO = 1
29+
"This is the Windows PawnIO interface, which is recommended on Windows Systems."
30+
LPC = 2
2931
"This manually talks to the EC over the LPC interface, using the ioports."
3032

3133

@@ -38,6 +40,8 @@ def pick_device() -> DeviceTypes:
3840
"""
3941
if dev and dev.CrosEcDev.detect():
4042
return DeviceTypes.LinuxDev
43+
elif pawnio and pawnio.CrosEcPawnIO.detect():
44+
return DeviceTypes.PawnIO
4145
elif lpc and lpc.CrosEcLpc.detect():
4246
return DeviceTypes.LPC
4347
else:
@@ -58,6 +62,8 @@ def get_cros_ec(dev_type: DeviceTypes | None = None, **kwargs) -> CrosEcClass:
5862
match dev_type:
5963
case DeviceTypes.LinuxDev:
6064
return dev.CrosEcDev(**kwargs)
65+
case DeviceTypes.PawnIO:
66+
return pawnio.CrosEcPawnIO(**kwargs)
6167
case DeviceTypes.LPC:
6268
return lpc.CrosEcLpc(**kwargs)
6369
case _:

cros_ec_python/devices/pawnio.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import struct
2+
import warnings
3+
import ctypes
4+
from ctypes import wintypes
5+
from ctypes import util as ctypes_util
6+
7+
from ..baseclass import CrosEcClass
8+
from ..constants.COMMON import *
9+
from ..exceptions import ECError
10+
11+
12+
class CrosEcPawnIO(CrosEcClass):
13+
"""
14+
Class to interact with the EC using the Windows PawnIO driver.
15+
"""
16+
17+
def __init__(self, dll: str | None = None, bin: str | None = None):
18+
"""
19+
Initialise the EC using the Linux cros_ec device.
20+
:param dll: Path to the DLL to use. If None, will use the default path.
21+
:param bin: Path to the binary to load. If None, will use the default path.
22+
"""
23+
24+
self.bin = bin or "LpcCrOSEC.bin"
25+
26+
if dll or (dll := ctypes_util.find_library("PawnIOLib.dll")):
27+
self.pawniolib = ctypes.OleDLL(dll)
28+
else:
29+
# Let this raise an error if we can't find it
30+
self.pawniolib = ctypes.OleDLL("C:\\Program Files\\PawnIO\\PawnIOLib.dll")
31+
32+
self.pawniolib.pawnio_version.argtypes = [ctypes.POINTER(wintypes.ULONG)]
33+
self.pawniolib.pawnio_open.argtypes = [ctypes.POINTER(wintypes.HANDLE)]
34+
self.pawniolib.pawnio_load.argtypes = [
35+
wintypes.HANDLE,
36+
ctypes.POINTER(ctypes.c_ubyte),
37+
ctypes.c_size_t,
38+
]
39+
self.pawniolib.pawnio_execute.argtypes = [
40+
wintypes.HANDLE,
41+
ctypes.c_char_p,
42+
ctypes.POINTER(ctypes.c_ulonglong),
43+
ctypes.c_size_t,
44+
ctypes.POINTER(ctypes.c_ulonglong),
45+
ctypes.c_size_t,
46+
ctypes.POINTER(ctypes.c_size_t),
47+
]
48+
self.pawniolib.pawnio_close.argtypes = [wintypes.HANDLE]
49+
50+
self.ec_init()
51+
52+
def __del__(self):
53+
self.ec_exit()
54+
55+
def _pawnio_version(self) -> str:
56+
version = wintypes.ULONG()
57+
self.pawniolib.pawnio_version(ctypes.byref(version))
58+
major, minor, patch = (
59+
version.value >> 16,
60+
(version.value >> 8) & 0xFF,
61+
version.value & 0xFF,
62+
)
63+
return f"{major}.{minor}.{patch}"
64+
65+
def _pawnio_open(self) -> None:
66+
self.handle = wintypes.HANDLE()
67+
self.pawniolib.pawnio_open(ctypes.byref(self.handle))
68+
69+
def _pawnio_load(self, filepath: str) -> None:
70+
with open(filepath, "rb") as file:
71+
blob = file.read()
72+
size = len(blob)
73+
blob_array = (ctypes.c_ubyte * size)(*blob)
74+
self.pawniolib.pawnio_load(self.handle, blob_array, size)
75+
76+
def _pawnio_execute(
77+
self,
78+
function: str,
79+
in_data: bytes,
80+
out_size: bytes,
81+
in_size: int | None = None,
82+
) -> tuple[int | ctypes.Array]:
83+
function_bytes = function.encode("utf-8")
84+
in_size = in_size if in_size is not None else len(in_data)
85+
in_array = (ctypes.c_ulonglong * in_size)(*in_data)
86+
out_array = (ctypes.c_ulonglong * out_size)()
87+
return_size = ctypes.c_size_t()
88+
89+
self.pawniolib.pawnio_execute(
90+
self.handle,
91+
function_bytes,
92+
in_array,
93+
in_size,
94+
out_array,
95+
out_size,
96+
ctypes.byref(return_size),
97+
)
98+
99+
return (return_size.value, out_array)
100+
101+
def _pawnio_close(self):
102+
self.pawniolib.pawnio_close(self.handle)
103+
104+
@staticmethod
105+
def detect() -> bool:
106+
# TODO
107+
return True
108+
109+
def ec_init(self) -> None:
110+
self._pawnio_open()
111+
self._pawnio_load(self.bin)
112+
113+
def ec_exit(self) -> None:
114+
"""
115+
Close the file on exit.
116+
"""
117+
if hasattr(self, "handle"):
118+
self._pawnio_close()
119+
120+
def command(
121+
self,
122+
version: Int32,
123+
command: Int32,
124+
outsize: Int32,
125+
insize: Int32,
126+
data: bytes = None,
127+
warn: bool = True,
128+
) -> bytes:
129+
"""
130+
Send a command to the EC and return the response.
131+
:param version: Command version number (often 0).
132+
:param command: Command to send (EC_CMD_...).
133+
:param outsize: Outgoing length in bytes.
134+
:param insize: Max number of bytes to accept from the EC.
135+
:param data: Outgoing data to EC.
136+
:param warn: Whether to warn if the response size is not as expected. Default is True.
137+
:return: Incoming data from EC.
138+
"""
139+
# LpcCrOSEC returns the EC result too
140+
pawn_insize = insize + 1
141+
header = struct.pack("<HB", command, version)
142+
size, res = self._pawnio_execute(
143+
"ioctl_ec_command",
144+
header + (data or b""),
145+
pawn_insize,
146+
in_size=outsize + len(header),
147+
)
148+
149+
if size != pawn_insize and warn:
150+
warnings.warn(
151+
f"Expected {pawn_insize} bytes, got {size} back from PawnIO",
152+
RuntimeWarning,
153+
)
154+
155+
# If the first cell is negative, it has failed
156+
# Also convert to signed
157+
if res[0] & (1 << 63):
158+
signed_res = res[0] - (1 << 64)
159+
raise ECError(-signed_res)
160+
161+
# Otherwise it's the length
162+
if res[0] != insize and warn:
163+
warnings.warn(
164+
f"Expected {pawn_insize} bytes, got {res[0]} back from EC",
165+
RuntimeWarning,
166+
)
167+
168+
# The pawn cells are 64bit but all the values should fit in 8 bits
169+
return bytes(res[1:])
170+
171+
def memmap(self, offset: Int32, num_bytes: Int32) -> bytes:
172+
"""
173+
Read memory from the EC.
174+
:param offset: Offset to read from.
175+
:param num_bytes: Number of bytes to read.
176+
:return: Bytes read from the EC.
177+
"""
178+
size, res = self._pawnio_execute(
179+
"ioctl_ec_readmem", offset.to_bytes(1), num_bytes
180+
)
181+
182+
array = bytearray(size)
183+
for i in range(size):
184+
array[i] = res[i]
185+
return bytes(array)

tests/devices.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import unittest
2-
from cros_ec_python import CrosEcClass, CrosEcDev, CrosEcLpc, general
2+
import sys
3+
from cros_ec_python import CrosEcClass, CrosEcLpc, general
34
from cros_ec_python.constants import MEMMAP
45

6+
7+
if sys.platform == "linux":
8+
from cros_ec_python import CrosEcDev
9+
elif sys.platform == "win32":
10+
from cros_ec_python import CrosEcPawnIO
11+
12+
513
ec: CrosEcClass | None = None
614

715

16+
@unittest.skipUnless(sys.platform == "linux", "requires Linux")
817
class TestLinuxDev(unittest.TestCase):
918
def test1_init(self):
1019
global ec
@@ -20,6 +29,22 @@ def test3_hello(self):
2029
resp = ec.command(0, general.EC_CMD_HELLO, len(data), 4, data)
2130
self.assertEqual(resp, b'\xa4\xb3\xc2\xd1')
2231

32+
@unittest.skipUnless(sys.platform == "win32", "requires Windows")
33+
class TestWinPawnIO(unittest.TestCase):
34+
def test1_init(self):
35+
global ec
36+
ec = CrosEcPawnIO()
37+
self.assertIsNotNone(ec)
38+
39+
def test2_memmap(self):
40+
resp = ec.memmap(MEMMAP.EC_MEMMAP_ID, 2)
41+
self.assertEqual(resp, b'EC')
42+
43+
def test3_hello(self):
44+
data = b'\xa0\xb0\xc0\xd0'
45+
resp = ec.command(0, general.EC_CMD_HELLO, len(data), 4, data)
46+
self.assertEqual(resp, b'\xa4\xb3\xc2\xd1')
47+
2348

2449
class TestLPC(unittest.TestCase):
2550
def test1_init(self):

0 commit comments

Comments
 (0)