Python library for working with the PD Buddy Sink Serial Console Configuration Interface
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

__init__.py 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842
  1. """Python bindings for PD Buddy Sink configuration"""
  2. from collections import namedtuple
  3. try:
  4. # Try importing enum from the standard library
  5. import enum
  6. # Make sure Flag is available
  7. enum.Flag
  8. except (ImportError, NameError, AttributeError):
  9. # If something above failed, try aenum instead
  10. import aenum as enum
  11. import serial
  12. import serial.tools.list_ports
  13. class Sink:
  14. """Interface for configuring a PD Buddy Sink"""
  15. vid = 0x1209
  16. pid = 0x9DB5
  17. def __init__(self, sp):
  18. """Open a serial port to communicate with the PD Buddy Sink
  19. :param sp: the serial port of the device
  20. :type sp: str or `serial.tools.list_ports.ListPortInfo`
  21. """
  22. try:
  23. self._port = serial.Serial(sp, baudrate=115200)
  24. except ValueError:
  25. self._port = serial.Serial(sp.device, baudrate=115200)
  26. # Put communications in a known state, cancelling any partially-entered
  27. # command that may be sitting in the buffer.
  28. self.send_command("\x04", newline=False)
  29. def __enter__(self):
  30. return self
  31. def __exit__(self, exc_type, exc_value, traceback):
  32. self._port.close()
  33. def send_command(self, cmd, newline=True):
  34. """Send a command to the PD Buddy Sink, returning the result
  35. :param cmd: the text to send to the Sink
  36. :param newline: whether to append a ``\r\n`` to the command
  37. :type cmd: str
  38. :type newline: bool
  39. :returns: a list of zero or more bytes objects, each being one line
  40. printed as a response to the command.
  41. """
  42. # Build the command
  43. cmd = cmd.encode("utf-8")
  44. if newline:
  45. cmd += b"\r\n"
  46. # Send the command
  47. self._port.write(cmd)
  48. self._port.flush()
  49. # Read the result
  50. answer = b""
  51. while not answer.endswith(b"PDBS) "):
  52. answer += self._port.read(1)
  53. answer = answer.split(b"\r\n")
  54. # Remove the echoed command and prompt
  55. answer = answer[1:-1]
  56. # Raise an exception if the command wasn't recognized
  57. if len(answer) and answer[0] == cmd.strip().split()[0] + b" ?":
  58. raise KeyError("command not found")
  59. return answer
  60. def close(self):
  61. """Close the serial port"""
  62. self._port.close()
  63. def help(self):
  64. """Returns the help text from the PD Buddy Sink"""
  65. return self.send_command("help")
  66. def license(self):
  67. """Returns the license text from the PD Buddy Sink"""
  68. return self.send_command("license")
  69. def boot(self):
  70. """Runs the PD Buddy Sink's DFU bootloader and closes the serial port"""
  71. self._port.write(b"boot\r\n")
  72. self._port.flush()
  73. self.close()
  74. def erase(self):
  75. """Synchronously erases all stored configuration from flash"""
  76. self.send_command("erase")
  77. def write(self):
  78. """Synchronously writes the contents of the configuration buffer to flash"""
  79. self.send_command("write")
  80. def load(self):
  81. """Loads the current configuration from flash into the buffer
  82. :raises: KeyError
  83. """
  84. text = self.send_command("load")
  85. if len(text) > 0 and text[0].startswith(b"No configuration"):
  86. raise KeyError("no configuration")
  87. def get_cfg(self, index=None):
  88. """Reads configuration from flash
  89. :param index: optional index of configuration object in flash to read
  90. :returns: a `SinkConfig` object
  91. """
  92. if index is None:
  93. cfg = self.send_command("get_cfg")
  94. else:
  95. cfg = self.send_command("get_cfg {}".format(index))
  96. return SinkConfig.from_text(cfg)
  97. def get_tmpcfg(self):
  98. """Reads the contents of the configuration buffer
  99. :returns: a `SinkConfig` object
  100. """
  101. cfg = self.send_command("get_tmpcfg")
  102. return SinkConfig.from_text(cfg)
  103. def clear_flags(self):
  104. """Clears all the flags in the configuration buffer"""
  105. self.send_command("clear_flags")
  106. def toggle_giveback(self):
  107. """Toggles the GiveBack flag in the configuration buffer"""
  108. self.send_command("toggle_giveback")
  109. def toggle_hv_preferred(self):
  110. """Toggles the HV_Preferred flag in the configuration buffer"""
  111. self.send_command("toggle_hv_preferred")
  112. def set_v(self, mv):
  113. """Sets the voltage of the configuration buffer, in millivolts"""
  114. out = self.send_command("set_v {}".format(mv))
  115. # If that command gave any output, that indicates an error. Raise an
  116. # exception to make that clear.
  117. if len(out):
  118. raise ValueError(out[0])
  119. def set_vrange(self, vmin, vmax):
  120. """Sets the min and max voltage of the configuration buffer, in millivolts"""
  121. # First, make sure we're sending numbers to the Sink in all valid cases
  122. if vmin is None and vmax is None:
  123. vmin = 0
  124. vmax = 0
  125. out = self.send_command("set_vrange {} {}".format(vmin, vmax))
  126. # If that command gave any output, that indicates an error. Raise an
  127. # exception to make that clear.
  128. if len(out):
  129. raise ValueError(out[0])
  130. def set_i(self, ma):
  131. """Sets the current of the configuration buffer, in milliamperes"""
  132. out = self.send_command("set_i {}".format(ma))
  133. # If that command gave any output, that indicates an error. Raise an
  134. # exception to make that clear.
  135. if len(out):
  136. raise ValueError(out[0])
  137. def set_p(self, mw):
  138. """Sets the power of the configuration buffer, in milliwatts"""
  139. out = self.send_command("set_p {}".format(mw))
  140. # If that command gave any output, that indicates an error. Raise an
  141. # exception to make that clear.
  142. if len(out):
  143. raise ValueError(out[0])
  144. def set_r(self, mr):
  145. """Sets the resistance of the configuration buffer, in milliohms"""
  146. out = self.send_command("set_r {}".format(mr))
  147. # If that command gave any output, that indicates an error. Raise an
  148. # exception to make that clear.
  149. if len(out):
  150. raise ValueError(out[0])
  151. def identify(self):
  152. """Blinks the LED quickly"""
  153. self.send_command("identify")
  154. @property
  155. def output(self):
  156. """The state of the Sink's output
  157. Raises KeyError if the ``output`` command is not available on the Sink.
  158. Raises ValueError if an invalid output is read.
  159. """
  160. value = self.send_command("output")
  161. if value[0] == b"enabled":
  162. return True
  163. elif value[0] == b"disabled":
  164. return False
  165. else:
  166. # If unexpected text is returned, raise an exception indicating a
  167. # firmware error
  168. raise ValueError("unknown output state")
  169. @output.setter
  170. def output(self, state):
  171. if state:
  172. self.send_command("output enable")
  173. else:
  174. self.send_command("output disable")
  175. def get_source_cap(self):
  176. """Gets the most recent Source_Capabilities read by the Sink"""
  177. return read_pdo_list(self.send_command("get_source_cap"))
  178. def set_tmpcfg(self, sc):
  179. """Writes a SinkConfig object to the device's configuration buffer
  180. Note: the value of the status field is ignored; it will always be
  181. `SinkStatus.VALID`.
  182. """
  183. # Set flags
  184. self.clear_flags()
  185. if sc.flags & SinkFlags.GIVEBACK:
  186. self.toggle_giveback()
  187. if sc.flags & SinkFlags.HV_PREFERRED:
  188. self.toggle_hv_preferred()
  189. # Set voltage
  190. self.set_v(sc.v)
  191. # Set voltage range
  192. self.set_vrange(sc.vmin, sc.vmax)
  193. if sc.idim is SinkDimension.CURRENT:
  194. # Set current
  195. self.set_i(sc.i)
  196. elif sc.idim is SinkDimension.POWER:
  197. # Set power
  198. self.set_p(sc.i)
  199. elif sc.idim is SinkDimension.RESISTANCE:
  200. # Set resistance
  201. self.set_r(sc.i)
  202. @classmethod
  203. def get_devices(cls):
  204. """Get an iterable of PD Buddy Sink devices
  205. :returns: an iterable of `serial.tools.list_ports.ListPortInfo` objects
  206. """
  207. return serial.tools.list_ports.grep("{:04X}:{:04X}".format(cls.vid,
  208. cls.pid))
  209. class SinkConfig(namedtuple("SinkConfig", "status flags v vmin vmax i idim")):
  210. """Python representation of a PD Buddy Sink configuration object
  211. ``status`` should be a `SinkStatus` object. ``flags`` should be zero or
  212. more `SinkFlags` values. ``v``, ``vmin``, and ``vmax`` are voltages in
  213. millivolts, ``i`` is the value used to set current in the appropriate
  214. milli- SI unit, and ``idim`` is the dimension of ``i``. `None` is also
  215. an acceptible value for any of the fields.
  216. """
  217. __slots__ = ()
  218. def __str__(self):
  219. """Print the SinkStatus in the manner of the configuration shell"""
  220. s = ""
  221. if self.status is not None:
  222. s += "status: "
  223. if self.status is SinkStatus.EMPTY:
  224. s += "empty"
  225. elif self.status is SinkStatus.VALID:
  226. s += "valid"
  227. elif self.status is SinkStatus.INVALID:
  228. s += "invalid"
  229. s += "\n"
  230. if self.flags is not None:
  231. s += "flags: "
  232. if self.flags is SinkFlags.NONE:
  233. s += "(none)"
  234. else:
  235. if self.flags & SinkFlags.GIVEBACK:
  236. s += "GiveBack"
  237. if self.flags & SinkFlags.HV_PREFERRED:
  238. s += "HV_Preferred"
  239. s += "\n"
  240. if self.v is not None:
  241. s += "v: {:.3f} V\n".format(self.v / 1000.0)
  242. if self.vmin is not None:
  243. s += "vmin: {:.3f} V\n".format(self.vmin / 1000.0)
  244. if self.vmax is not None:
  245. s += "vmax: {:.3f} V\n".format(self.vmax / 1000.0)
  246. if self.i is not None:
  247. if self.idim is SinkDimension.CURRENT:
  248. s += "i: {:.2f} A\n".format(self.i / 1000.0)
  249. if self.idim is SinkDimension.POWER:
  250. s += "p: {:.2f} W\n".format(self.i / 1000.0)
  251. if self.idim is SinkDimension.RESISTANCE:
  252. s += "r: {:.2f} \u03A9\n".format(self.i / 1000.0)
  253. # Return all but the last character of s to remove the trailing newline
  254. if s:
  255. return s[:-1]
  256. else:
  257. return "No configuration"
  258. @classmethod
  259. def from_text(cls, text):
  260. """Creates a SinkConfig from text returned by Sink.send_command
  261. :param text: the text to load
  262. :type text: a list of bytes objects
  263. :returns: a new `SinkConfig` object.
  264. :raises: IndexError
  265. """
  266. # Assume the parameters will all be None
  267. status = None
  268. flags = None
  269. v = None
  270. vmin = None
  271. vmax = None
  272. i = None
  273. idim = None
  274. # Iterate over all lines of text
  275. for line in text:
  276. # If the configuration said invalid index, raise an IndexError
  277. if line.startswith(b"Invalid index"):
  278. raise IndexError("configuration index out of range")
  279. # If there is no configuration, return an empty SinkConfig
  280. elif line.startswith(b"No configuration"):
  281. return cls(None, None, None, None, None, None, None)
  282. # If this line is the status field
  283. elif line.startswith(b"status: "):
  284. line = line.split()[1:]
  285. if line[0] == b"empty":
  286. status = SinkStatus.EMPTY
  287. elif line[0] == b"valid":
  288. status = SinkStatus.VALID
  289. elif line[0] == b"invalid":
  290. status = SinkStatus.INVALID
  291. # If this line is the flags field
  292. elif line.startswith(b"flags: "):
  293. line = line.split()[1:]
  294. flags = SinkFlags.NONE
  295. for word in line:
  296. if word == b"(none)":
  297. # If there are no flags set, stop looking
  298. break
  299. elif word == b"GiveBack":
  300. flags |= SinkFlags.GIVEBACK
  301. elif word == b"HV_Preferred":
  302. flags |= SinkFlags.HV_PREFERRED
  303. # If this line is the v field
  304. elif line.startswith(b"v: "):
  305. word = line.split()[1]
  306. v = round(1000*float(word))
  307. # If this line is the vmin field
  308. elif line.startswith(b"vmin: "):
  309. word = line.split()[1]
  310. vmin = round(1000*float(word))
  311. # If this line is the vmax field
  312. elif line.startswith(b"vmax: "):
  313. word = line.split()[1]
  314. vmax = round(1000*float(word))
  315. # If this line is the i field
  316. elif line.startswith(b"i: "):
  317. word = line.split()[1]
  318. i = round(1000*float(word))
  319. idim = SinkDimension.CURRENT
  320. # If this line is the p field
  321. elif line.startswith(b"p: "):
  322. word = line.split()[1]
  323. i = round(1000*float(word))
  324. idim = SinkDimension.POWER
  325. # If this line is the r field
  326. elif line.startswith(b"r: "):
  327. word = line.split()[1]
  328. i = round(1000*float(word))
  329. idim = SinkDimension.RESISTANCE
  330. # Create a new SinkConfig object with the values we just read
  331. return cls(status=status, flags=flags, v=v, vmin=vmin, vmax=vmax, i=i,
  332. idim=idim)
  333. class SinkStatus(enum.Enum):
  334. """Status field of a PD Buddy Sink configuration object"""
  335. EMPTY = 1
  336. VALID = 2
  337. INVALID = 3
  338. class SinkDimension(enum.Enum):
  339. """Dimension of the value used to set the current requested"""
  340. CURRENT = 1
  341. POWER = 2
  342. RESISTANCE = 3
  343. class SinkFlags(enum.Flag):
  344. """Flags field of a PD Buddy Sink configuration object"""
  345. NONE = 0
  346. GIVEBACK = enum.auto()
  347. HV_PREFERRED = enum.auto()
  348. class UnknownPDO(namedtuple("UnknownPDO", "value")):
  349. """A PDO of an unknown type
  350. ``value`` should be a 32-bit integer representing the PDO exactly as
  351. transmitted.
  352. """
  353. __slots__ = ()
  354. pdo_type = "unknown"
  355. def __str__(self):
  356. """Print the UnknownPDO in the manner of the configuration shell"""
  357. return "{:08X}".format(self.value)
  358. class SrcFixedPDO(namedtuple("SrcFixedPDO", "dual_role_pwr usb_suspend "
  359. "unconstrained_pwr usb_comms dual_role_data unchunked_ext_msg peak_i "
  360. "v i")):
  361. """A Source Fixed PDO
  362. ``dual_role_pwr``, ``usb_suspend``, ``unconstrained_pwr``,
  363. ``usb_comms``, ``dual_role_data``, and ``unchunked_ext_msg`` should be
  364. booleans. ``peak_i`` should be an integer in the range [0, 3]. ``v``
  365. is the voltage in millivolts, and ``i`` is the maximum current in
  366. milliamperes.
  367. """
  368. __slots__ = ()
  369. pdo_type = "fixed"
  370. def __str__(self):
  371. """Print the SrcFixedPDO in the manner of the configuration shell"""
  372. s = self.pdo_type + "\n"
  373. if self.dual_role_pwr:
  374. s += "\tdual_role_pwr: 1\n"
  375. if self.usb_suspend:
  376. s += "\tusb_suspend: 1\n"
  377. if self.unconstrained_pwr:
  378. s += "\tunconstrained_pwr: 1\n"
  379. if self.usb_comms:
  380. s += "\tusb_comms: 1\n"
  381. if self.dual_role_data:
  382. s += "\tdual_role_data: 1\n"
  383. if self.unchunked_ext_msg:
  384. s += "\tunchunked_ext_msg: 1\n"
  385. if self.peak_i:
  386. s += "\tpeak_i: {}\n".format(self.peak_i)
  387. s += "\tv: {:.2f} V\n".format(self.v / 1000.0)
  388. s += "\ti: {:.2f} A".format(self.i / 1000.0)
  389. return s
  390. class SrcPPSAPDO(namedtuple("SrcPPSAPDO", "vmin vmax i")):
  391. """A Source Programmable Power Supply APDO
  392. ``vmin`` and ``vmax`` are the minimum and maximum voltage in millivolts,
  393. respectively, and ``i`` is the maximum current in milliamperes.
  394. """
  395. __slots__ = ()
  396. pdo_type = "pps"
  397. def __str__(self):
  398. """Print the SrcPPSAPDO in the manner of the configuration shell"""
  399. s = self.pdo_type + "\n"
  400. s += "\tvmin: {:.2f} V\n".format(self.vmin / 1000.0)
  401. s += "\tvmax: {:.2f} V\n".format(self.vmax / 1000.0)
  402. s += "\ti: {:.2f} A".format(self.i / 1000.0)
  403. return s
  404. class TypeCVirtualPDO(namedtuple("TypeCVirtualPDO", "i")):
  405. """A Type-C Current Virtual PDO
  406. ``i`` is the advertised current in milliamperes.
  407. """
  408. __slots__ = ()
  409. pdo_type = "typec_virtual"
  410. def __str__(self):
  411. """Print the TypeCVirtualPDO in the manner of the configuration shell"""
  412. s = self.pdo_type + "\n"
  413. s += "\ti: {:.2f} A".format(self.i / 1000.0)
  414. return s
  415. def read_pdo(text):
  416. """Create a PDO object from partial text returned by Sink.send_command"""
  417. # First, determine the PDO type
  418. pdo_type = text[0].split(b":")[-1].strip().decode("utf-8")
  419. if pdo_type == SrcFixedPDO.pdo_type:
  420. # Set default values (n.b. there are none for v and i)
  421. dual_role_pwr = False
  422. usb_suspend = False
  423. unconstrained_pwr = False
  424. usb_comms = False
  425. dual_role_data = False
  426. unchunked_ext_msg = False
  427. peak_i = 0
  428. # Load a SrcFixedPDO
  429. for line in text[1:]:
  430. fields = line.split(b":")
  431. fields[0] = fields[0].strip()
  432. fields[1] = fields[1].strip()
  433. if fields[0] == b"dual_role_pwr":
  434. dual_role_pwr = (fields[1] == b"1")
  435. elif fields[0] == b"usb_suspend":
  436. usb_suspend = (fields[1] == b"1")
  437. elif fields[0] == b"unconstrained_pwr":
  438. unconstrained_pwr = (fields[1] == b"1")
  439. elif fields[0] == b"usb_comms":
  440. usb_comms = (fields[1] == b"1")
  441. elif fields[0] == b"dual_role_data":
  442. dual_role_data = (fields[1] == b"1")
  443. elif fields[0] == b"unchunked_ext_msg":
  444. unchunked_ext_msg = (fields[1] == b"1")
  445. elif fields[0] == b"peak_i":
  446. peak_i = int(fields[1])
  447. elif fields[0] == b"v":
  448. v = round(1000*float(fields[1].split()[0]))
  449. elif fields[0] == b"i":
  450. i = round(1000*float(fields[1].split()[0]))
  451. # Make the SrcFixedPDO
  452. return SrcFixedPDO(
  453. dual_role_pwr=dual_role_pwr,
  454. usb_suspend=usb_suspend,
  455. unconstrained_pwr=unconstrained_pwr,
  456. usb_comms=usb_comms,
  457. dual_role_data=dual_role_data,
  458. unchunked_ext_msg=unchunked_ext_msg,
  459. peak_i=peak_i,
  460. v=v,
  461. i=i)
  462. elif pdo_type == SrcPPSAPDO.pdo_type:
  463. # Load a SrcPPSAPDO
  464. for line in text[1:]:
  465. fields = line.split(b":")
  466. fields[0] = fields[0].strip()
  467. fields[1] = fields[1].strip()
  468. if fields[0] == b"vmin":
  469. vmin = round(1000*float(fields[1].split()[0]))
  470. if fields[0] == b"vmax":
  471. vmax = round(1000*float(fields[1].split()[0]))
  472. if fields[0] == b"i":
  473. i = round(1000*float(fields[1].split()[0]))
  474. # Make the SrcPPSAPDO
  475. return SrcPPSAPDO(vmin=vmin, vmax=vmax, i=i)
  476. elif pdo_type == TypeCVirtualPDO.pdo_type:
  477. # Load a TypeCVirtualPDO
  478. for line in text[1:]:
  479. fields = line.split(b":")
  480. fields[0] = fields[0].strip()
  481. fields[1] = fields[1].strip()
  482. if fields[0] == b"i":
  483. i = round(1000*float(fields[1].split()[0]))
  484. # Make the TypeCVirtualPDO
  485. return TypeCVirtualPDO(i=i)
  486. elif pdo_type == "No Source_Capabilities":
  487. return None
  488. else:
  489. # Make an UnknownPDO
  490. return UnknownPDO(value=int(pdo_type, 16))
  491. def read_pdo_list(text):
  492. """Create a list of PDOs from text returned by Sink.send_command"""
  493. # Get the lines where PDOs start
  494. pdo_start_list = []
  495. for index, line in enumerate(text):
  496. if not line.startswith(b"\t"):
  497. pdo_start_list.append(index)
  498. # Append the number of lines so the last slice will work right
  499. pdo_start_list.append(len(text))
  500. # Read the PDOs
  501. pdo_list = []
  502. for start, end in zip(pdo_start_list[:-1], pdo_start_list[1:]):
  503. pdo = read_pdo(text[start:end])
  504. if pdo is not None:
  505. pdo_list.append(pdo)
  506. return pdo_list
  507. def calculate_pdp(pdo_list):
  508. """Calculate the PDP in watts of a list of PDOs
  509. The result is only guaranteed to be correct if the power supply follows
  510. the USB Power Delivery standard. Since quite a few power supplies
  511. unfortunately do not, this can really only be considered an estimate.
  512. """
  513. max_power = 0
  514. # The Source Power Rules make it so the PDP can be determined by the
  515. # highest power available from any fixed supply PDO.
  516. for pdo in pdo_list:
  517. if pdo.pdo_type == "fixed":
  518. max_power = max(max_power, pdo.v / 1000.0 * pdo.i / 1000.0)
  519. elif pdo.pdo_type == "typec_virtual":
  520. max_power = max(max_power, 5.0 * pdo.i / 1000.0)
  521. return max_power
  522. def follows_power_rules(pdo_list):
  523. """Test whether a list of PDOs follows the Power Rules for PD 3.0
  524. This function is a false-biased approximation; that is, when it returns
  525. False it is definitely correct, but when it returns True it might be
  526. incorrect.
  527. """
  528. # First, estimate the PDP assuming the rules are being followed
  529. pdp = calculate_pdp(pdo_list)
  530. # If there's a typec_virtual PDO, there's no Power Delivery so the Power
  531. # Rules cannot be violated. In truth they're not really being followed
  532. # either since they only apply to Power Delivery, but returning True here
  533. # seems like the safer option.
  534. if pdo_list and pdo_list[0].pdo_type == "typec_virtual":
  535. return True
  536. fixed = [p for p in pdo_list if p.pdo_type == "fixed"]
  537. pps = [p for p in pdo_list if p.pdo_type == "pps"]
  538. # Make sure nothing exceeds the PDP
  539. for pdo in fixed:
  540. if pdp < pdo.v / 1000.0 * pdo.i / 1000.0:
  541. return False
  542. # TODO: in the future, there will be more types of PDO checked here
  543. for pdo in pps:
  544. # 5V Prog nominal PDO
  545. if pdo.vmin == 3000 and pdo.vmax == 5900:
  546. if pdp < 5.0 * pdo.i / 1000.0:
  547. return False
  548. # 9V Prog nominal PDO
  549. elif pdo.vmin == 3000 and pdo.vmax == 11000:
  550. if pdp < 9.0 * pdo.i / 1000.0:
  551. return False
  552. # 15V Prog nominal PDO
  553. elif pdo.vmin == 3000 and pdo.vmax == 16000:
  554. if pdp < 15.0 * pdo.i / 1000.0:
  555. return False
  556. # 20V Prog nominal PDO
  557. elif pdo.vmin == 3000 and pdo.vmax == 21000:
  558. if pdp < 20.0 * pdo.i / 1000.0:
  559. return False
  560. # Non-standard programmable PDO
  561. else:
  562. if pdp < pdo.vmax / 1000.0 * pdo.i / 1000.0:
  563. return False
  564. # Check that the fixed supply PDOs look right
  565. seen_5v = False
  566. seen_9v = False
  567. seen_15v = False
  568. seen_20v = False
  569. seen_normative_voltages = False
  570. if pdp == 0:
  571. # No power is fine
  572. seen_normative_voltages = True
  573. elif pdp <= 15:
  574. # Below 15 W, make sure the PDP is available at 5 V.
  575. for pdo in fixed:
  576. if pdo.v == 5000:
  577. seen_5v = True
  578. if pdo.v / 1000.0 * pdo.i / 1000.0 != pdp:
  579. return False
  580. seen_normative_voltages = seen_5v
  581. elif pdp <= 27:
  582. # Between 15 and 27 W, make sure at least 3 A is available at 5 V and
  583. # the PDP is available at 9 V.
  584. for pdo in fixed:
  585. if pdo.v == 5000:
  586. seen_5v = True
  587. if pdo.i < 3000.0:
  588. return False
  589. elif pdo.v == 9000:
  590. seen_9v = True
  591. if pdo.v / 1000.0 * pdo.i / 1000.0 != pdp:
  592. return False
  593. seen_normative_voltages = seen_5v and seen_9v
  594. elif pdp <= 45:
  595. # Between 27 and 45 W, make sure at least 3 A is available at 5 and
  596. # 9 V, and the PDP is available at 15 V.
  597. for pdo in fixed:
  598. if pdo.v == 5000:
  599. seen_5v = True
  600. if pdo.i < 3000.0:
  601. return False
  602. elif pdo.v == 9000:
  603. seen_9v = True
  604. if pdo.i < 3000.0:
  605. return False
  606. elif pdo.v == 15000:
  607. seen_15v = True
  608. if pdo.v / 1000.0 * pdo.i / 1000.0 != pdp:
  609. return False
  610. seen_normative_voltages = seen_5v and seen_9v and seen_15v
  611. else:
  612. # Above 45 W, make sure at least 3 A is available at 5, 9, and 15 V,
  613. # and the PDP is available at 20 V.
  614. for pdo in fixed:
  615. if pdo.v == 5000:
  616. seen_5v = True
  617. if pdo.i < 3000.0:
  618. return False
  619. elif pdo.v == 9000:
  620. seen_9v = True
  621. if pdo.i < 3000.0:
  622. return False
  623. elif pdo.v == 15000:
  624. seen_15v = True
  625. if pdo.i < 3000.0:
  626. return False
  627. elif pdo.v == 20000:
  628. seen_20v = True
  629. if pdo.v / 1000.0 * pdo.i / 1000.0 != pdp:
  630. return False
  631. seen_normative_voltages = seen_5v and seen_9v and seen_15v and seen_20v
  632. if not seen_normative_voltages:
  633. return False
  634. # TODO: there are several things this currently doesn't test, such as
  635. # variable and battery PDOs.
  636. # Check that the PPS APDOs look right
  637. seen_5v = False
  638. seen_9v = False
  639. seen_15v = False
  640. seen_20v = False
  641. if pdp == 0:
  642. # No power is fine
  643. pass
  644. elif pdp <= 15:
  645. # Below 15 W, make sure the PDP is available with 5V Prog.
  646. for pdo in pps:
  647. if pdo.vmin == 3000 and pdo.vmax == 5900:
  648. seen_5v = True
  649. if 5.0 * pdo.i / 1000.0 != pdp:
  650. return False
  651. if pps and not seen_5v:
  652. return False
  653. elif pdp <= 27:
  654. # Between 15 and 27 W, make sure at least 3 A is available at 5V Prog
  655. # and the PDP is available at 9V Prog.
  656. for pdo in pps:
  657. if pdo.vmin == 3000 and pdo.vmax == 5900:
  658. seen_5v = True
  659. if pdo.i < 3000.0:
  660. return False
  661. elif pdo.vmin == 3000 and pdo.vmax == 11000:
  662. seen_9v = True
  663. if 9.0 * pdo.i / 1000.0 != pdp:
  664. return False
  665. # If we're at full power for this range, 5V Prog is optional
  666. if pdp == 27:
  667. seen_5v = True
  668. if pps and not (seen_5v and seen_9v):
  669. return False
  670. elif pdp <= 45:
  671. # Between 27 and 45 W, make sure at least 3 A is available at 9V Prog,
  672. # and the PDP is available at 15V Prog.
  673. for pdo in pps:
  674. # 5V Prog is optional at this power level
  675. if pdo.vmin == 3000 and pdo.vmax == 11000:
  676. seen_9v = True
  677. if pdo.i < 3000.0:
  678. return False
  679. elif pdo.vmin == 3000 and pdo.vmax == 16000:
  680. seen_15v = True
  681. if 15.0 * pdo.i / 1000.0 != pdp:
  682. return False
  683. # If we're at full power for this range, 9V Prog is optional
  684. if pdp == 45:
  685. seen_9v = True
  686. if pps and not (seen_9v and seen_15v):
  687. return False
  688. else:
  689. # Above 45 W, make sure at least 3 A is available at 15V Prog, and the
  690. # PDP is available at 20V Prog.
  691. for pdo in pps:
  692. # 5V and 9V Prog are optional at this power level
  693. if pdo.vmin == 3000 and pdo.vmax == 16000:
  694. seen_15v = True
  695. if pdo.i < 3000.0:
  696. return False
  697. elif pdo.vmin == 3000 and pdo.vmax == 21000:
  698. seen_20v = True
  699. if 20.0 * pdo.i / 1000.0 != pdp:
  700. return False
  701. # If we have at least 60 W, 15V Prog is optional
  702. if pdp >= 60:
  703. seen_15v = True
  704. if pps and not (seen_15v and seen_20v):
  705. return False
  706. return True