simplepwm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. #!/usr/bin/env python3
  2. import serial
  3. import argparse
  4. import sys
  5. import time
  6. import pathlib
  7. from collections import deque
  8. def printDebugDevice(msg):
  9. if getattr(printDebugDevice, "enabled", False):
  10. print("Device debug:", msg, file=sys.stderr)
  11. def printDebug(msg):
  12. if getattr(printDebug, "enabled", False):
  13. print("Debug:", msg, file=sys.stderr)
  14. def printInfo(msg):
  15. print(msg, file=sys.stderr)
  16. def printWarning(msg):
  17. print("WARNING:", msg, file=sys.stderr)
  18. def printError(msg):
  19. print("ERROR:", msg, file=sys.stderr)
  20. def crc8(data):
  21. P = 0x07
  22. crc = 0
  23. for d in data:
  24. tmp = crc ^ d
  25. for i in range(8):
  26. if tmp & 0x80:
  27. tmp = ((tmp << 1) & 0xFF) ^ P
  28. else:
  29. tmp = (tmp << 1) & 0xFF
  30. crc = tmp
  31. return crc ^ 0xFF
  32. class SimplePWMError(Exception):
  33. pass
  34. class SimplePWMMsg(object):
  35. PAYLOAD_SIZE = 8
  36. SIZE = 1 + 1 + 1 + PAYLOAD_SIZE + 1
  37. MSG_MAGIC = 0xAA
  38. MSG_SYNCBYTE = b"\x00"
  39. MSGID_NOP = 0
  40. MSGID_ACK = 1
  41. MSGID_NACK = 2
  42. MSGID_PING = 3
  43. MSGID_PONG = 4
  44. MSGID_GET_CONTROL = 5
  45. MSGID_CONTROL = 6
  46. MSGID_GET_SETPOINTS = 7
  47. MSGID_SETPOINTS = 8
  48. MSGID_GET_BATVOLT = 9
  49. MSGID_BATVOLT = 10
  50. @staticmethod
  51. def _toLe16(value):
  52. return bytearray( (value & 0xFF, (value >> 8) & 0xFF, ) )
  53. @staticmethod
  54. def _fromLe16(data):
  55. return data[0] | (data[1] << 8)
  56. @classmethod
  57. def parse(cls, data):
  58. assert len(data) == cls.SIZE
  59. magic, msgId = data[0:2]
  60. payload = data[3:-1]
  61. crc = data[-1]
  62. if magic != cls.MSG_MAGIC:
  63. printError("Received corrupted message: Magic byte mismatch.")
  64. elif crc8(data[:-1]) != crc:
  65. printError("Received corrupted message: CRC mismatch.")
  66. else:
  67. if msgId == cls.MSGID_NOP:
  68. return SimplePWMMsg_Nop._parse(payload)
  69. elif msgId == cls.MSGID_ACK:
  70. return SimplePWMMsg_Ack._parse(payload)
  71. elif msgId == cls.MSGID_NACK:
  72. return SimplePWMMsg_Nack._parse(payload)
  73. elif msgId == cls.MSGID_PING:
  74. return SimplePWMMsg_Ping._parse(payload)
  75. elif msgId == cls.MSGID_PONG:
  76. return SimplePWMMsg_Pong._parse(payload)
  77. elif msgId == cls.MSGID_GET_CONTROL:
  78. return SimplePWMMsg_GetControl._parse(payload)
  79. elif msgId == cls.MSGID_CONTROL:
  80. return SimplePWMMsg_Control._parse(payload)
  81. elif msgId == cls.MSGID_GET_SETPOINTS:
  82. return SimplePWMMsg_SetSetpoints._parse(payload)
  83. elif msgId == cls.MSGID_SETPOINTS:
  84. return SimplePWMMsg_Setpoints._parse(payload)
  85. elif msgId == cls.MSGID_GET_BATVOLT:
  86. return SimplePWMMsg_GetBatvolt._parse(payload)
  87. elif msgId == cls.MSGID_BATVOLT:
  88. return SimplePWMMsg_Batvolt._parse(payload)
  89. else:
  90. printError(f"Received unknown message: 0x{msgId:X}")
  91. return None
  92. @classmethod
  93. def _parse(cls, payload):
  94. return cls()
  95. def __init__(self, msgId):
  96. self.msgId = msgId
  97. def getData(self, payload=None):
  98. if payload is None:
  99. payload = b"\x00" * self.PAYLOAD_SIZE
  100. if len(payload) < self.PAYLOAD_SIZE:
  101. payload += b"\x00" * (self.PAYLOAD_SIZE - len(payload))
  102. assert len(payload) == self.PAYLOAD_SIZE
  103. data = bytearray( (self.MSG_MAGIC, self.msgId, 0, ) ) + payload
  104. data.append(crc8(data))
  105. assert len(data) == self.SIZE
  106. return data
  107. class SimplePWMMsg_Nop(SimplePWMMsg):
  108. MSGID = SimplePWMMsg.MSGID_NOP
  109. def __init__(self):
  110. super().__init__(self.MSGID)
  111. def __str__(self):
  112. return f"NOP"
  113. class SimplePWMMsg_Ack(SimplePWMMsg):
  114. MSGID = SimplePWMMsg.MSGID_ACK
  115. def __init__(self):
  116. super().__init__(self.MSGID)
  117. def __str__(self):
  118. return f"ACK"
  119. class SimplePWMMsg_Nack(SimplePWMMsg):
  120. MSGID = SimplePWMMsg.MSGID_NACK
  121. def __init__(self):
  122. super().__init__(self.MSGID)
  123. def __str__(self):
  124. return f"NACK"
  125. class SimplePWMMsg_Ping(SimplePWMMsg):
  126. MSGID = SimplePWMMsg.MSGID_PING
  127. def __init__(self):
  128. super().__init__(self.MSGID)
  129. def __str__(self):
  130. return f"PING"
  131. class SimplePWMMsg_Pong(SimplePWMMsg):
  132. MSGID = SimplePWMMsg.MSGID_PONG
  133. def __init__(self):
  134. super().__init__(self.MSGID)
  135. def __str__(self):
  136. return f"PONG"
  137. class SimplePWMMsg_GetControl(SimplePWMMsg):
  138. MSGID = SimplePWMMsg.MSGID_GET_CONTROL
  139. def __init__(self):
  140. super().__init__(self.MSGID)
  141. def __str__(self):
  142. return f"GET_CONTROL"
  143. class SimplePWMMsg_Control(SimplePWMMsg):
  144. MSGID = SimplePWMMsg.MSGID_CONTROL
  145. MSG_CTLFLG_ANADIS = 0x01
  146. MSG_CTLFLG_EEPDIS = 0x02 #TODO
  147. @classmethod
  148. def _parse(cls, payload):
  149. flags = payload[0]
  150. return cls(flags)
  151. def __init__(self, flags):
  152. super().__init__(self.MSGID)
  153. self.flags = flags
  154. def getData(self):
  155. payload = bytearray( (self.flags, ) )
  156. return super().getData(payload)
  157. def __str__(self):
  158. return f"CONTROL(flags=0x{self.flags:X})"
  159. class SimplePWMMsg_GetSetpoints(SimplePWMMsg):
  160. MSGID = SimplePWMMsg.MSGID_GET_SETPOINTS
  161. MSG_GETSPFLG_HSL = 0x01
  162. @classmethod
  163. def _parse(cls, payload):
  164. flags = payload[0]
  165. return cls(flags)
  166. def __init__(self, flags):
  167. super().__init__(self.MSGID)
  168. self.flags = flags
  169. def getData(self):
  170. payload = bytearray( (self.flags, ) )
  171. return super().getData(payload)
  172. def __str__(self):
  173. return f"GET_SETPOINTS(flags=0x{self.flags:X})"
  174. class SimplePWMMsg_Setpoints(SimplePWMMsg):
  175. MSGID = SimplePWMMsg.MSGID_SETPOINTS
  176. MSG_SPFLG_HSL = 0x01
  177. @classmethod
  178. def _parse(cls, payload):
  179. flags = payload[0]
  180. nrSp = payload[1]
  181. if nrSp > 3:
  182. printError("SimplePWMMsg_Setpoints: Received invalid nr_sp.")
  183. return None
  184. setpoints = [ cls._fromLe16(payload[2 + i*2 : 2 + i*2 + 2])
  185. for i in range(nrSp) ]
  186. return cls(flags, setpoints)
  187. def __init__(self, flags, setpoints):
  188. super().__init__(self.MSGID)
  189. self.flags = flags
  190. self.setpoints = setpoints
  191. def getData(self):
  192. payload = bytearray( (self.flags, len(self.setpoints), ) )
  193. for sp in self.setpoints:
  194. payload += self._toLe16(sp)
  195. return super().getData(payload)
  196. def __str__(self):
  197. return f"SETPOINTS(flags=0x{self.flags:X}, setpoints={self.setpoints})"
  198. class SimplePWMMsg_GetBatvolt(SimplePWMMsg):
  199. MSGID = SimplePWMMsg.MSGID_GET_BATVOLT
  200. def __init__(self):
  201. super().__init__(self.MSGID)
  202. def __str__(self):
  203. return f"GET_BATVOLT"
  204. class SimplePWMMsg_Batvolt(SimplePWMMsg):
  205. MSGID = SimplePWMMsg.MSGID_BATVOLT
  206. @classmethod
  207. def _parse(cls, payload):
  208. meas = cls._fromLe16(payload[0:2])
  209. drop = cls._fromLe16(payload[2:4])
  210. return cls(meas, drop)
  211. def __init__(self, meas, drop):
  212. super().__init__(self.MSGID)
  213. self.meas = meas
  214. self.drop = drop
  215. def getData(self):
  216. payload = bytearray()
  217. payload += self._toLe16(self.meas)
  218. payload += self._toLe16(self.drop)
  219. return super().getData(payload)
  220. def __str__(self):
  221. return f"BATVOLT(meas={self.meas}, drop={self.drop})"
  222. class SimplePWM(object):
  223. FLG_8BIT = 0x80 # 8-bit data nibble
  224. FLG_8BIT_UPPER = 0x40 # 8-bit upper data nibble
  225. FLG_8BIT_RSV1 = 0x20 # reserved
  226. FLG_8BIT_RSV0 = 0x10 # reserved
  227. MSK_4BIT = 0x0F # data nibble
  228. MSK_7BIT = 0x7F
  229. REMOTE_STANDBY_DELAY_MS = 5000
  230. REMOTE_STANDBY_DELAY = REMOTE_STANDBY_DELAY_MS / 1000
  231. def __init__(self,
  232. port="/dev/ttyUSB0",
  233. timeout=1.0,
  234. dumpDebugStream=False):
  235. self.__serial = serial.Serial(port=port,
  236. baudrate=19200,
  237. bytesize=8,
  238. parity=serial.PARITY_NONE,
  239. stopbits=2,
  240. timeout=timeout)
  241. self.__timeout = timeout
  242. self.__dumpDebugStream = dumpDebugStream
  243. self.__rxByte = 0
  244. self.__rxBuf = bytearray()
  245. self.__rxMsgs = deque()
  246. self.__debugBuf = bytearray()
  247. self.__nextSyncTime = time.monotonic()
  248. self.__wakeup()
  249. def __synchronize(self):
  250. self.__tx_8bit(SimplePWMMsg.MSG_SYNCBYTE * round(SimplePWMMsg.SIZE * 2.5))
  251. def __wakeup(self):
  252. nextSyncTime = self.__nextSyncTime
  253. self.__nextSyncTime = time.monotonic() + (self.REMOTE_STANDBY_DELAY / 2)
  254. if time.monotonic() < nextSyncTime:
  255. return
  256. count = 0
  257. while True:
  258. try:
  259. self.__synchronize()
  260. self.ping(0.1)
  261. except SimplePWMError as e:
  262. count += 1
  263. if count > 10:
  264. raise e
  265. continue
  266. break
  267. printDebug("Connection synchronized.")
  268. def __tx_8bit(self, data):
  269. self.__wakeup()
  270. # printDebug(f"TX: {bytes(data)}")
  271. for d in data:
  272. lo = ((d & self.MSK_4BIT) |
  273. self.FLG_8BIT)
  274. hi = (((d >> 4) & self.MSK_4BIT) |
  275. self.FLG_8BIT |
  276. self.FLG_8BIT_UPPER)
  277. sendBytes = bytes( (lo, hi) )
  278. self.__serial.write(sendBytes)
  279. def __rx_7bit(self, dataByte):
  280. self.__debugBuf += dataByte
  281. if dataByte == b"\n":
  282. if self.__dumpDebugStream:
  283. text = self.__debugBuf.decode("ASCII", "ignore").strip()
  284. printDebugDevice(text)
  285. self.__debugBuf.clear()
  286. def __rx_8bit(self, dataByte):
  287. # printDebug(f"RX: {dataByte}")
  288. d = dataByte[0]
  289. if d & self.FLG_8BIT_UPPER:
  290. data = (self.__rxByte & self.MSK_4BIT)
  291. data |= (d & self.MSK_4BIT) << 4
  292. self.__rxByte = 0
  293. self.__rxBuf.append(data)
  294. if len(self.__rxBuf) >= SimplePWMMsg.SIZE:
  295. rxMsg = SimplePWMMsg.parse(self.__rxBuf)
  296. self.__rxBuf.clear()
  297. if rxMsg:
  298. self.__rx_message(rxMsg)
  299. else:
  300. self.__rxByte = d
  301. def __tx_message(self, txMsg):
  302. printDebug("TX msg: " + str(txMsg))
  303. self.__tx_8bit(txMsg.getData())
  304. def __rx_message(self, rxMsg):
  305. printDebug("RX msg: " + str(rxMsg))
  306. self.__rxMsgs.append(rxMsg)
  307. def __readNext(self, timeout):
  308. if timeout < 0:
  309. timeout = self.__timeout
  310. self.__serial.timeout = timeout
  311. dataByte = self.__serial.read(1)
  312. if dataByte:
  313. if dataByte[0] & self.FLG_8BIT:
  314. self.__rx_8bit(dataByte)
  315. else:
  316. self.__rx_7bit(dataByte)
  317. def __waitRxMsg(self, msgType=None, timeout=None):
  318. if timeout is None:
  319. timeout = self.__timeout
  320. timeoutEnd = None
  321. if timeout >= 0.0:
  322. timeoutEnd = time.monotonic() + timeout
  323. retMsg = None
  324. while retMsg is None:
  325. if (timeoutEnd is not None and
  326. time.monotonic() >= timeoutEnd):
  327. break
  328. self.__readNext(timeout)
  329. if msgType is not None:
  330. for rxMsg in self.__rxMsgs:
  331. if isinstance(rxMsg, msgType):
  332. retMsg = rxMsg
  333. else:
  334. printError("Received unexpected "
  335. "message: " + str(rxMsg))
  336. self.__rxMsgs.clear()
  337. return retMsg
  338. def dumpDebugStream(self):
  339. self.__dumpDebugStream = True
  340. while True:
  341. self.__waitRxMsg(timeout=-1)
  342. def ping(self, timeout=None):
  343. self.__tx_message(SimplePWMMsg_Ping())
  344. rxMsg = self.__waitRxMsg(SimplePWMMsg_Pong, timeout)
  345. if not rxMsg:
  346. raise SimplePWMError("Ping failed.")
  347. def getAnalogEn(self, timeout=None):
  348. self.__tx_message(SimplePWMMsg_GetControl())
  349. rxMsg = self.__waitRxMsg(SimplePWMMsg_Control, timeout)
  350. if not rxMsg:
  351. raise SimplePWMError("Failed to get control info.")
  352. return not (rxMsg.flags & rxMsg.MSG_CTLFLG_ANADIS)
  353. def setAnalogEn(self, enable, timeout=None):
  354. self.__tx_message(SimplePWMMsg_GetControl())
  355. rxMsg = self.__waitRxMsg(SimplePWMMsg_Control, timeout)
  356. if not rxMsg:
  357. raise SimplePWMError("Failed to get control info.")
  358. txMsg = rxMsg
  359. if enable:
  360. txMsg.flags &= ~txMsg.MSG_CTLFLG_ANADIS
  361. else:
  362. txMsg.flags |= txMsg.MSG_CTLFLG_ANADIS
  363. self.__tx_message(txMsg)
  364. rxMsg = self.__waitRxMsg(SimplePWMMsg_Ack, timeout)
  365. if not rxMsg:
  366. raise SimplePWMError("Failed to get acknowledge.")
  367. def getRGB(self, timeout=None):
  368. self.__tx_message(SimplePWMMsg_GetSetpoints(
  369. flags=0))
  370. rxMsg = self.__waitRxMsg(SimplePWMMsg_Setpoints, timeout)
  371. if not rxMsg:
  372. raise SimplePWMError("Failed to get RGB setpoints.")
  373. return rxMsg.setpoints
  374. def setRGB(self, rgb, timeout=None):
  375. self.setAnalogEn(False, timeout)
  376. txMsg = SimplePWMMsg_Setpoints(
  377. flags=0,
  378. setpoints=rgb)
  379. self.__tx_message(txMsg)
  380. rxMsg = self.__waitRxMsg(SimplePWMMsg_Ack, timeout)
  381. if not rxMsg:
  382. raise SimplePWMError("Failed to set RGB setpoints.")
  383. def getHSL(self, timeout=None):
  384. self.__tx_message(SimplePWMMsg_GetSetpoints(
  385. flags=SimplePWMMsg_GetSetpoints.MSG_GETSPFLG_HSL))
  386. rxMsg = self.__waitRxMsg(SimplePWMMsg_Setpoints, timeout)
  387. if not rxMsg:
  388. raise SimplePWMError("Failed to get HSL setpoints.")
  389. return rxMsg.setpoints
  390. def setHSL(self, hsl, timeout=None):
  391. self.setAnalogEn(False, timeout)
  392. txMsg = SimplePWMMsg_Setpoints(
  393. flags=SimplePWMMsg_Setpoints.MSG_SPFLG_HSL,
  394. setpoints=hsl)
  395. self.__tx_message(txMsg)
  396. rxMsg = self.__waitRxMsg(SimplePWMMsg_Ack, timeout)
  397. if not rxMsg:
  398. raise SimplePWMError("Failed to set RGB setpoints.")
  399. def getBatVoltage(self, timeout=None):
  400. self.__tx_message(SimplePWMMsg_GetBatvolt())
  401. rxMsg = self.__waitRxMsg(SimplePWMMsg_Batvolt, timeout)
  402. if not rxMsg:
  403. raise SimplePWMError("Failed to get battery voltage.")
  404. return (rxMsg.meas, rxMsg.drop)
  405. def parse_setpoints(string):
  406. s = string.split(",")
  407. if len(s) != 3:
  408. raise SimplePWMError("Setpoints are not a comma separated triple.")
  409. def parseOne(v):
  410. try:
  411. v = v.strip()
  412. if v.casefold().startswith("0x".casefold()):
  413. # Raw 16 bit hex value.
  414. v = int(v, 16)
  415. if not 0 <= v <= 0xFFFF:
  416. raise SimplePWMError("Setpoint raw value "
  417. "out of range 0-0xFFFF.")
  418. return v
  419. if v.endswith("%"):
  420. # Percentage
  421. v = float(v[:-1])
  422. if not 0.0 <= v <= 100.0:
  423. raise SimplePWMError("Setpoint percentage "
  424. "out of range 0%-100%.")
  425. return round(v * 0xFFFF / 100.0)
  426. if v.endswith("*"):
  427. # Degrees (0-360)
  428. v = float(v[:-1])
  429. return round((v % 360.0) * 0xFFFF / 360.0)
  430. # Value 0-255
  431. v = int(v)
  432. if 0 <= v <= 0xFF:
  433. return (v << 8) | v
  434. raise ValueError
  435. except ValueError as e:
  436. raise SimplePWMError("Setpoint value parse error.")
  437. return [ parseOne(v) for v in s ]
  438. def main():
  439. try:
  440. class ArgumentParserOrderedNamespace(argparse.Namespace):
  441. def __init__(self, *args, **kwargs):
  442. super().__init__(*args, **kwargs)
  443. super().__setattr__("_setupDone", False)
  444. super().__setattr__("_orderedArgs", [])
  445. def __setattr__(self, name, value):
  446. sanitizedName = name.replace("_", "-")
  447. if (not self._setupDone and
  448. sanitizedName in (n for n, v in self._orderedArgs)):
  449. super().__setattr__("_setupDone", True)
  450. del self._orderedArgs[:]
  451. self._orderedArgs.append( (sanitizedName, value) )
  452. super().__setattr__(name, value)
  453. @property
  454. def orderedArgs(self):
  455. return self._orderedArgs if self._setupDone else ()
  456. p = argparse.ArgumentParser(description="SimplePWM remote control")
  457. p.add_argument("-p", "--ping", action="store_true",
  458. help="Send a ping to the device and wait for pong reply.")
  459. p.add_argument("-b", "--get-battery", action="store_true",
  460. help="Get the battery voltage.")
  461. p.add_argument("-a", "--get-analog", action="store_true",
  462. help="Get the state of the analog inputs.")
  463. p.add_argument("-A", "--analog", type=int, default=None,
  464. help="Enable/disable the analog inputs.")
  465. p.add_argument("-r", "--get-rgb", action="store_true",
  466. help="Get the current RGB setpoint values.")
  467. p.add_argument("-R", "--rgb", type=parse_setpoints, default=None,
  468. help="Set the RGB setpoint values.")
  469. p.add_argument("-s", "--get-hsl", action="store_true",
  470. help="Get the current HSL setpoint values.")
  471. p.add_argument("-S", "--hsl", type=parse_setpoints, default=None,
  472. help="Set the HSL setpoint values.")
  473. p.add_argument("-W", "--wait", type=float,
  474. help="Delay for a fractional number of seconds.")
  475. p.add_argument("-L", "--loop", action="store_true",
  476. help="Repeat the whole sequence.")
  477. p.add_argument("-d", "--debug-device", action="store_true",
  478. help="Read and dump debug messages from device.")
  479. p.add_argument("-D", "--debug", action="store_true",
  480. help="Enable remote side debugging.")
  481. p.add_argument("port", nargs="?", type=pathlib.Path,
  482. default=pathlib.Path("/dev/ttyUSB0"),
  483. help="Serial port.")
  484. args = p.parse_args(namespace=ArgumentParserOrderedNamespace())
  485. if args.debug:
  486. printDebug.enabled = True
  487. if args.debug_device:
  488. printDebugDevice.enabled = True
  489. s = SimplePWM(port=str(args.port),
  490. dumpDebugStream=args.debug_device)
  491. repeat = True
  492. while repeat:
  493. for name, value in args.orderedArgs:
  494. if name == "ping":
  495. if value:
  496. s.ping()
  497. if name == "get-battery":
  498. if value:
  499. meas, drop = s.getBatVoltage()
  500. print(f"Battery: "
  501. f"measured {meas/1000:.2f} V, "
  502. f"drop {drop/1000:.2f} V, "
  503. f"actual {(meas+drop)/1000:.2f} V")
  504. if name == "analog":
  505. s.setAnalogEn(value)
  506. if name == "get-analog":
  507. if value:
  508. enabled = "enabled" if s.getAnalogEn() else "disabled"
  509. print(f"Analog inputs state: "
  510. f"{enabled}")
  511. if name == "rgb":
  512. s.setRGB(value)
  513. if name == "get-rgb":
  514. if value:
  515. rgb = s.getRGB()
  516. print(f"RGB setpoints: "
  517. f"{rgb[0]*100/0xFFFF:.1f}%, "
  518. f"{rgb[1]*100/0xFFFF:.1f}%, "
  519. f"{rgb[2]*100/0xFFFF:.1f}%")
  520. if name == "hsl":
  521. s.setHSL(value)
  522. if name == "get-hsl":
  523. if value:
  524. hsl = s.getHSL()
  525. print(f"HSL setpoints: "
  526. f"{hsl[0]*100/0xFFFF:.1f}%, "
  527. f"{hsl[1]*100/0xFFFF:.1f}%, "
  528. f"{hsl[2]*100/0xFFFF:.1f}%")
  529. if name == "wait":
  530. time.sleep(value)
  531. if name == "loop":
  532. repeat = True
  533. break
  534. else:
  535. repeat = False
  536. if args.debug_device:
  537. s.dumpDebugStream()
  538. except SimplePWMError as e:
  539. printError(e)
  540. return 1
  541. except KeyboardInterrupt as e:
  542. printInfo("Interrupted.")
  543. return 1
  544. return 0
  545. sys.exit(main())