spec_client.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. #!/usr/bin/env python3
  2. """spec_client.py — тестовый клиент к spec_server (порт 5002).
  3. python spec_client.py --host rp-f0xxxx.local оболочка
  4. python spec_client.py --host 192.168.1.50 status одна команда
  5. python spec_client.py --host 192.168.1.50 --run test_sequence.spec
  6. Оболочка почти прозрачная: разбор команд живёт в сервере, здесь только отправка
  7. строки и печать ответа. Явные do_*-методы нужны ради help и автодополнения по
  8. Tab; всё, что им не подошло, уходит на сервер как есть (см. default).
  9. Сверх серверных команд клиент понимает несколько собственных директив — они
  10. нужны в файлах последовательностей и работают также из оболочки:
  11. echo <текст> напечатать
  12. wait <мс> пауза
  13. waitidle [мс] ждать busy=0 (событие отработало)
  14. waitbuf [мс] ждать buf_valid=0 (команда ушла из буфера в тракт)
  15. waitseq [мс] ждать seq_busy=0 (секвенсор доиграл последовательность)
  16. run [-k] <файл> выполнить файл построчно, ВЖИВУЮ (как раньше)
  17. load <файл> тот же файл, но не исполнять, а ЗАГРУЗИТЬ в
  18. аппаратный секвенсор (seqbegin/.../seqend) —
  19. дальше seqrun запускает всё разом на 125 МГц, без
  20. сети между шагами
  21. Про waitbuf. Маска и длительность TTL — немедленные параметры: ttl_controller
  22. защёлкивает их тогда, когда старт до него доходит. Пока команда лежит в буфере
  23. axi_opcode_ctrl, она ещё ничего не защёлкнула, поэтому менять mask/dur до
  24. снятия buf_valid нельзя — следующее событие уедет с чужой маской.
  25. Про load. Файл — тот же самый .spec, что и для run: сервер сам разворачивает
  26. set/start/op/defaults не в живой тракт, а в память секвенсора, пока идёт
  27. загрузка (seqbegin..seqend) — компилировать на клиенте нечего. А вот
  28. директивы, которые опрашивают ЖИВОЙ статус (wait/waitidle/waitbuf/waitseq),
  29. внутри load-файла смысла не имеют: секвенсор в этот момент ничего не
  30. исполняет, он просто пишет в BRAM. Не используйте их там — используйте после
  31. seqrun, в оболочке или в отдельном live-файле.
  32. Обычный сценарий: load prog.spec, затем seqrun, затем waitseq и waitidle
  33. (секвенсор мог доиграть выдачу команд раньше, чем доиграет само последнее
  34. событие — ждать нужно оба).
  35. """
  36. import argparse
  37. import cmd
  38. import os
  39. import socket
  40. import sys
  41. import time
  42. DEFAULT_PORT = 5001
  43. DEFAULT_TIMEOUT = 5.0
  44. MAX_RUN_DEPTH = 8 # защита от run-петли: файл, зовущий сам себя
  45. POLL_INTERVAL = 0.005 # шаг опроса status в waitidle/waitbuf
  46. class SpecError(Exception):
  47. pass
  48. class Link:
  49. """Строчный обмен с сервером: одна команда — один ответ."""
  50. def __init__(self, host, port, timeout=DEFAULT_TIMEOUT):
  51. self.host = host
  52. self.port = port
  53. self.timeout = timeout
  54. self.sock = None
  55. self.fh = None
  56. def connect(self):
  57. self.close()
  58. self.sock = socket.create_connection((self.host, self.port), self.timeout)
  59. self.sock.settimeout(self.timeout)
  60. # newline='\n', чтобы не пришлось чистить \r в ответах вручную
  61. self.fh = self.sock.makefile("rw", encoding="utf-8", newline="\n")
  62. return self.readline() # приветствие сервера
  63. def close(self):
  64. for obj in (self.fh, self.sock):
  65. if obj is not None:
  66. try:
  67. obj.close()
  68. except OSError:
  69. pass
  70. self.fh = None
  71. self.sock = None
  72. def readline(self):
  73. line = self.fh.readline()
  74. if not line:
  75. raise SpecError("сервер закрыл соединение")
  76. return line.rstrip("\n")
  77. def command(self, line):
  78. if self.fh is None:
  79. raise SpecError("нет соединения (connect)")
  80. try:
  81. self.fh.write(line + "\n")
  82. self.fh.flush()
  83. return self.readline()
  84. except socket.timeout:
  85. raise SpecError(f"нет ответа за {self.timeout} с")
  86. except OSError as exc:
  87. raise SpecError(str(exc))
  88. def status_fields(answer):
  89. """'OK status=0x0a busy=0 ...' -> {'status': '0x0a', 'busy': '0', ...}"""
  90. fields = {}
  91. for token in answer.split():
  92. if "=" in token:
  93. key, _, val = token.partition("=")
  94. fields[key] = val
  95. return fields
  96. class SpecShell(cmd.Cmd):
  97. prompt = "spec> "
  98. def __init__(self, link):
  99. super().__init__()
  100. self.link = link
  101. # ------------------------------------------------------ исполнение строки
  102. def exec_line(self, line, depth=0):
  103. """Выполнить строку: клиентскую директиву или команду сервера.
  104. Возвращает (успех, текст ответа). Печать — на вызывающем, чтобы run мог
  105. добавить к строке имя файла и номер.
  106. """
  107. parts = line.split(None, 1)
  108. if not parts:
  109. return True, ""
  110. verb, rest = parts[0], (parts[1].strip() if len(parts) > 1 else "")
  111. if verb == "echo":
  112. return True, rest
  113. if verb == "wait":
  114. try:
  115. ms = float(rest)
  116. if ms < 0:
  117. raise ValueError
  118. except ValueError:
  119. return False, "! wait: нужно время в мс"
  120. time.sleep(ms / 1000.0)
  121. return True, f"пауза {ms:g} мс"
  122. if verb in ("waitidle", "waitbuf", "waitseq"):
  123. field, want, default_ms = {
  124. "waitidle": ("busy", "0", 5000.0),
  125. "waitbuf": ("buf_valid", "0", 1000.0),
  126. # seq_busy обычно снимается быстро (это просто "выдал все
  127. # слова программы"), а не "доиграло последнее событие" —
  128. # после waitseq почти всегда нужен ещё и waitidle.
  129. "waitseq": ("seq_busy", "0", 1000.0),
  130. }[verb]
  131. try:
  132. ms = float(rest) if rest else default_ms
  133. except ValueError:
  134. return False, f"! {verb}: нужен таймаут в мс"
  135. return self.wait_field(verb, field, want, ms)
  136. if verb == "run":
  137. # depth растёт именно здесь: run_script проверяет его, но сам вглубь
  138. # не спускается — без инкремента файл, зовущий сам себя, уходил в
  139. # RecursionError вместо аккуратной остановки.
  140. return self.do_run(rest, depth=depth + 1), ""
  141. if verb == "load":
  142. return self.do_load(rest, depth=depth + 1), ""
  143. try:
  144. answer = self.link.command(line)
  145. except SpecError as exc:
  146. return False, f"! {exc}"
  147. return not answer.startswith("ERR"), answer
  148. def wait_field(self, verb, field, want, timeout_ms):
  149. """Опрашивать status, пока поле не примет нужное значение."""
  150. started = time.monotonic()
  151. deadline = started + timeout_ms / 1000.0
  152. while True:
  153. try:
  154. answer = self.link.command("status")
  155. except SpecError as exc:
  156. return False, f"! {exc}"
  157. if answer.startswith("ERR"):
  158. return False, answer
  159. fields = status_fields(answer)
  160. if field not in fields:
  161. return False, f"! {verb}: в ответе нет поля {field}: {answer}"
  162. if fields[field] == want:
  163. waited = (time.monotonic() - started) * 1000.0
  164. return True, f"{field}={want} через {waited:.0f} мс"
  165. if time.monotonic() >= deadline:
  166. return False, (f"! {verb}: таймаут {timeout_ms:g} мс, "
  167. f"{field}={fields[field]} ({answer})")
  168. time.sleep(POLL_INTERVAL)
  169. def send(self, line):
  170. ok, text = self.exec_line(line)
  171. if text:
  172. print(text)
  173. return ok
  174. def default(self, line):
  175. """Всё незнакомое уходит на сервер — он и рассудит."""
  176. if line == "EOF":
  177. return self.do_quit("")
  178. self.send(line)
  179. def emptyline(self):
  180. pass
  181. # --------------------------------------------------- файлы с командами
  182. def run_script(self, path, keep_going=False, depth=0):
  183. """Выполнить файл построчно. Возвращает True, если ошибок не было."""
  184. if depth > MAX_RUN_DEPTH:
  185. print(f"! run: слишком глубокая вложенность (> {MAX_RUN_DEPTH})")
  186. return False
  187. try:
  188. with open(path, "r", encoding="utf-8") as fh:
  189. lines = fh.readlines()
  190. except OSError as exc:
  191. print(f"! run: {exc}")
  192. return False
  193. name = os.path.basename(path)
  194. errors = 0
  195. executed = 0
  196. for lineno, raw in enumerate(lines, 1):
  197. line = raw.split("#", 1)[0].strip()
  198. if not line:
  199. continue
  200. executed += 1
  201. ok, text = self.exec_line(line, depth=depth)
  202. if line.split(None, 1)[0] == "echo":
  203. print(text) # echo — заголовок раздела, колонки только мешают
  204. else:
  205. print(f"{name}:{lineno:<4} {line:28s} {text}".rstrip())
  206. if not ok:
  207. errors += 1
  208. if not keep_going:
  209. print(f"! run: остановлено на {name}:{lineno} "
  210. f"(-k — продолжать несмотря на ошибки)")
  211. return False
  212. print(f"run: {name} — строк {executed}, ошибок {errors}")
  213. return errors == 0
  214. def do_run(self, arg, depth=0):
  215. """run [-k] <файл> — выполнить файл с командами построчно.
  216. Пустые строки и всё после "#" игнорируются. По умолчанию исполнение
  217. останавливается на первой ошибке; -k — продолжать до конца.
  218. """
  219. args = arg.split()
  220. keep_going = False
  221. while args and args[0] in ("-k", "--keep-going"):
  222. keep_going = True
  223. args.pop(0)
  224. if not args:
  225. print("! run: нужен путь к файлу")
  226. return False
  227. return self.run_script(" ".join(args), keep_going, depth)
  228. def do_load(self, arg, depth=0):
  229. """load <файл> — загрузить файл как программу в аппаратный
  230. секвенсор (seqbegin, построчно как в run, затем seqend).
  231. Компилировать на клиенте нечего: сервер сам разворачивает set/
  232. start/op/defaults не в живой тракт, а в память секвенсора, пока
  233. идёт загрузка — файл тот же самый, что и для run. Не запускает
  234. воспроизведение — для этого seqrun (или seqrun N с явным числом
  235. слов). Внутри файла не используйте wait/waitidle/waitbuf/waitseq:
  236. секвенсор в момент загрузки ничего не исполняет, эти директивы
  237. опрашивают живой статус и внутри load смысла не имеют.
  238. seqend вызывается даже при ошибке посреди файла — не оставляем
  239. сервер подвешенным в режиме загрузки.
  240. """
  241. args = arg.split()
  242. if not args:
  243. print("! load: нужен путь к файлу")
  244. return False
  245. path = " ".join(args)
  246. ok0, text0 = self.exec_line("seqbegin", depth=depth)
  247. if text0:
  248. print(text0)
  249. if not ok0:
  250. print("! load: seqbegin не удался, отменено")
  251. return False
  252. ok = self.run_script(path, keep_going=False, depth=depth)
  253. ok1, text1 = self.exec_line("seqend", depth=depth)
  254. if text1:
  255. print(text1)
  256. return ok and ok1
  257. # -------------------------------------------------------------- команды
  258. def do_status(self, arg):
  259. """status — состояние секвенсора (busy/ready/buf_valid/overflow)."""
  260. self.send("status " + arg)
  261. def do_params(self, arg):
  262. """params — теневые параметры, какими их помнит сервер."""
  263. self.send("params " + arg)
  264. def do_defaults(self, arg):
  265. """defaults — вернуть параметры тракта к умолчаниям."""
  266. self.send("defaults " + arg)
  267. def do_set(self, arg):
  268. """set dur|mask|freq|inc|phase|pulse|step|env <значения> — параметры события."""
  269. self.send("set " + arg)
  270. def do_start(self, arg):
  271. """start ttl|rf — запустить событие."""
  272. self.send("start " + arg)
  273. def do_seqbegin(self, arg):
  274. """seqbegin — начать загрузку последовательности в секвенсор
  275. (обнуляет указатель BRAM). Дальше set/start/op/defaults пишутся не в
  276. живой тракт, а в память секвенсора — до seqend. Обычно проще
  277. использовать директиву load <файл>, которая сама зовёт
  278. seqbegin/seqend вокруг файла.
  279. """
  280. self.send("seqbegin " + arg)
  281. def do_seqend(self, arg):
  282. """seqend — закончить загрузку, вернуться к живому режиму."""
  283. self.send("seqend " + arg)
  284. def do_seqrun(self, arg):
  285. """seqrun [N] — запустить загруженную последовательность.
  286. Без аргумента — сервер помнит, сколько слов было загружено с
  287. последнего seqbegin. N — сыграть только первые N слов (например, для
  288. отладки не всей последовательности целиком).
  289. """
  290. self.send("seqrun " + arg)
  291. def do_seqabort(self, arg):
  292. """seqabort — немедленно остановить проигрывание последовательности
  293. (уже идущее событие доиграет до конца, как обычно — останавливается
  294. только выдача ДАЛЬНЕЙШИХ слов программы)."""
  295. self.send("seqabort " + arg)
  296. def do_op(self, arg):
  297. """op <код> <параметр> — сырая команда, без проверки контрактов."""
  298. self.send("op " + arg)
  299. def do_peek(self, arg):
  300. """peek <смещение> — прочитать регистр (0x00 OPCODE, 0x04 PARAM, 0x08 STATUS)."""
  301. self.send("peek " + arg)
  302. def do_poke(self, arg):
  303. """poke <смещение> <значение> — записать регистр напрямую."""
  304. self.send("poke " + arg)
  305. def do_echo(self, arg):
  306. """echo <текст> — напечатать строку (полезно в файлах последовательностей)."""
  307. self.send("echo " + arg)
  308. def do_wait(self, arg):
  309. """wait <мс> — пауза."""
  310. self.send("wait " + arg)
  311. def do_waitidle(self, arg):
  312. """waitidle [мс] — ждать busy=0, то есть конца всех событий."""
  313. self.send("waitidle " + arg)
  314. def do_waitbuf(self, arg):
  315. """waitbuf [мс] — ждать buf_valid=0: команда ушла из буфера в тракт.
  316. После этого можно менять маску и длительность следующего события, не
  317. испортив уже поставленное в очередь.
  318. """
  319. self.send("waitbuf " + arg)
  320. def do_waitseq(self, arg):
  321. """waitseq [мс] — ждать seq_busy=0: секвенсор выдал все слова программы.
  322. Это НЕ то же самое, что "последнее событие доиграло" — оно может ещё
  323. идти. После seqrun обычно нужны оба: waitseq, затем waitidle.
  324. """
  325. self.send("waitseq " + arg)
  326. def do_pulse(self, arg):
  327. """pulse <мкс> [частота Гц] [маска] — настроить и выдать один РЧ-импульс.
  328. Составная команда клиента: окно TTL берётся с запасом 20 % от длительности
  329. огибающей, иначе сервер отвергнет старт по контракту 2 (РЧ не должно
  330. переживать окно TTL).
  331. """
  332. args = arg.split()
  333. if not args:
  334. print("! нужна длительность в мкс")
  335. return
  336. try:
  337. us = float(args[0])
  338. freq = float(args[1]) if len(args) > 1 else None
  339. mask = args[2] if len(args) > 2 else None
  340. except ValueError:
  341. print("! не разобраны аргументы")
  342. return
  343. window = max(1, int(us * 1.2 + 0.5))
  344. steps = [f"set dur {window}", f"set pulse {us}"]
  345. if freq is not None:
  346. steps.append(f"set freq {freq}")
  347. if mask is not None:
  348. steps.append(f"set mask {mask}")
  349. steps.append("start rf")
  350. for step in steps:
  351. ok, text = self.exec_line(step)
  352. print(f"{step:22s} -> {text}")
  353. if not ok:
  354. print("! последовательность прервана")
  355. return
  356. def do_connect(self, arg):
  357. """connect [хост [порт]] — переподключиться."""
  358. args = arg.split()
  359. if args:
  360. self.link.host = args[0]
  361. if len(args) > 1:
  362. self.link.port = int(args[1])
  363. try:
  364. print(self.link.connect())
  365. except (OSError, SpecError) as exc:
  366. print(f"! не подключиться к {self.link.host}:{self.link.port}: {exc}")
  367. def do_quit(self, arg):
  368. """quit — закрыть соединение и выйти."""
  369. try:
  370. print(self.link.command("quit"))
  371. except SpecError:
  372. pass
  373. self.link.close()
  374. return True
  375. do_exit = do_quit
  376. do_EOF = do_quit
  377. def main():
  378. ap = argparse.ArgumentParser(description="тестовый клиент spec_server")
  379. ap.add_argument("--host", default="rp-f0xxxx.local", help="адрес платы")
  380. ap.add_argument("--port", type=int, default=DEFAULT_PORT)
  381. ap.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT)
  382. ap.add_argument("--run", metavar="ФАЙЛ",
  383. help="выполнить файл с командами и выйти")
  384. ap.add_argument("-k", "--keep-going", action="store_true",
  385. help="с --run: не останавливаться на ошибках")
  386. ap.add_argument("command", nargs="*",
  387. help="если задана — выполнить одну команду и выйти")
  388. args = ap.parse_args()
  389. link = Link(args.host, args.port, args.timeout)
  390. try:
  391. hello = link.connect()
  392. except (OSError, SpecError) as exc:
  393. print(f"! не подключиться к {args.host}:{args.port}: {exc}", file=sys.stderr)
  394. return 1
  395. shell = SpecShell(link)
  396. try:
  397. if args.run:
  398. print(hello)
  399. return 0 if shell.run_script(args.run, args.keep_going) else 1
  400. if args.command:
  401. line = " ".join(args.command)
  402. ok, text = shell.exec_line(line)
  403. print(text)
  404. return 0 if ok else 2
  405. shell.intro = f"{hello}\nhelp — команды сервера, ? — команды клиента"
  406. shell.cmdloop()
  407. return 0
  408. finally:
  409. link.close()
  410. if __name__ == "__main__":
  411. sys.exit(main())