| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- #!/usr/bin/env python3
- """spec_client.py — тестовый клиент к spec_server (порт 5002).
- python spec_client.py --host rp-f0xxxx.local оболочка
- python spec_client.py --host 192.168.1.50 status одна команда
- python spec_client.py --host 192.168.1.50 --run test_sequence.spec
- Оболочка почти прозрачная: разбор команд живёт в сервере, здесь только отправка
- строки и печать ответа. Явные do_*-методы нужны ради help и автодополнения по
- Tab; всё, что им не подошло, уходит на сервер как есть (см. default).
- Сверх серверных команд клиент понимает несколько собственных директив — они
- нужны в файлах последовательностей и работают также из оболочки:
- echo <текст> напечатать
- wait <мс> пауза
- waitidle [мс] ждать busy=0 (событие отработало)
- waitbuf [мс] ждать buf_valid=0 (команда ушла из буфера в тракт)
- waitseq [мс] ждать seq_busy=0 (секвенсор доиграл последовательность)
- run [-k] <файл> выполнить файл построчно, ВЖИВУЮ (как раньше)
- load <файл> тот же файл, но не исполнять, а ЗАГРУЗИТЬ в
- аппаратный секвенсор (seqbegin/.../seqend) —
- дальше seqrun запускает всё разом на 125 МГц, без
- сети между шагами
- Про waitbuf. Маска и длительность TTL — немедленные параметры: ttl_controller
- защёлкивает их тогда, когда старт до него доходит. Пока команда лежит в буфере
- axi_opcode_ctrl, она ещё ничего не защёлкнула, поэтому менять mask/dur до
- снятия buf_valid нельзя — следующее событие уедет с чужой маской.
- Про load. Файл — тот же самый .spec, что и для run: сервер сам разворачивает
- set/start/op/defaults не в живой тракт, а в память секвенсора, пока идёт
- загрузка (seqbegin..seqend) — компилировать на клиенте нечего. А вот
- директивы, которые опрашивают ЖИВОЙ статус (wait/waitidle/waitbuf/waitseq),
- внутри load-файла смысла не имеют: секвенсор в этот момент ничего не
- исполняет, он просто пишет в BRAM. Не используйте их там — используйте после
- seqrun, в оболочке или в отдельном live-файле.
- Обычный сценарий: load prog.spec, затем seqrun, затем waitseq и waitidle
- (секвенсор мог доиграть выдачу команд раньше, чем доиграет само последнее
- событие — ждать нужно оба).
- """
- import argparse
- import cmd
- import os
- import socket
- import sys
- import time
- DEFAULT_PORT = 5001
- DEFAULT_TIMEOUT = 5.0
- MAX_RUN_DEPTH = 8 # защита от run-петли: файл, зовущий сам себя
- POLL_INTERVAL = 0.005 # шаг опроса status в waitidle/waitbuf
- class SpecError(Exception):
- pass
- class Link:
- """Строчный обмен с сервером: одна команда — один ответ."""
- def __init__(self, host, port, timeout=DEFAULT_TIMEOUT):
- self.host = host
- self.port = port
- self.timeout = timeout
- self.sock = None
- self.fh = None
- def connect(self):
- self.close()
- self.sock = socket.create_connection((self.host, self.port), self.timeout)
- self.sock.settimeout(self.timeout)
- # newline='\n', чтобы не пришлось чистить \r в ответах вручную
- self.fh = self.sock.makefile("rw", encoding="utf-8", newline="\n")
- return self.readline() # приветствие сервера
- def close(self):
- for obj in (self.fh, self.sock):
- if obj is not None:
- try:
- obj.close()
- except OSError:
- pass
- self.fh = None
- self.sock = None
- def readline(self):
- line = self.fh.readline()
- if not line:
- raise SpecError("сервер закрыл соединение")
- return line.rstrip("\n")
- def command(self, line):
- if self.fh is None:
- raise SpecError("нет соединения (connect)")
- try:
- self.fh.write(line + "\n")
- self.fh.flush()
- return self.readline()
- except socket.timeout:
- raise SpecError(f"нет ответа за {self.timeout} с")
- except OSError as exc:
- raise SpecError(str(exc))
- def status_fields(answer):
- """'OK status=0x0a busy=0 ...' -> {'status': '0x0a', 'busy': '0', ...}"""
- fields = {}
- for token in answer.split():
- if "=" in token:
- key, _, val = token.partition("=")
- fields[key] = val
- return fields
- class SpecShell(cmd.Cmd):
- prompt = "spec> "
- def __init__(self, link):
- super().__init__()
- self.link = link
- # ------------------------------------------------------ исполнение строки
- def exec_line(self, line, depth=0):
- """Выполнить строку: клиентскую директиву или команду сервера.
- Возвращает (успех, текст ответа). Печать — на вызывающем, чтобы run мог
- добавить к строке имя файла и номер.
- """
- parts = line.split(None, 1)
- if not parts:
- return True, ""
- verb, rest = parts[0], (parts[1].strip() if len(parts) > 1 else "")
- if verb == "echo":
- return True, rest
- if verb == "wait":
- try:
- ms = float(rest)
- if ms < 0:
- raise ValueError
- except ValueError:
- return False, "! wait: нужно время в мс"
- time.sleep(ms / 1000.0)
- return True, f"пауза {ms:g} мс"
- if verb in ("waitidle", "waitbuf", "waitseq"):
- field, want, default_ms = {
- "waitidle": ("busy", "0", 5000.0),
- "waitbuf": ("buf_valid", "0", 1000.0),
- # seq_busy обычно снимается быстро (это просто "выдал все
- # слова программы"), а не "доиграло последнее событие" —
- # после waitseq почти всегда нужен ещё и waitidle.
- "waitseq": ("seq_busy", "0", 1000.0),
- }[verb]
- try:
- ms = float(rest) if rest else default_ms
- except ValueError:
- return False, f"! {verb}: нужен таймаут в мс"
- return self.wait_field(verb, field, want, ms)
- if verb == "run":
- # depth растёт именно здесь: run_script проверяет его, но сам вглубь
- # не спускается — без инкремента файл, зовущий сам себя, уходил в
- # RecursionError вместо аккуратной остановки.
- return self.do_run(rest, depth=depth + 1), ""
- if verb == "load":
- return self.do_load(rest, depth=depth + 1), ""
- try:
- answer = self.link.command(line)
- except SpecError as exc:
- return False, f"! {exc}"
- return not answer.startswith("ERR"), answer
- def wait_field(self, verb, field, want, timeout_ms):
- """Опрашивать status, пока поле не примет нужное значение."""
- started = time.monotonic()
- deadline = started + timeout_ms / 1000.0
- while True:
- try:
- answer = self.link.command("status")
- except SpecError as exc:
- return False, f"! {exc}"
- if answer.startswith("ERR"):
- return False, answer
- fields = status_fields(answer)
- if field not in fields:
- return False, f"! {verb}: в ответе нет поля {field}: {answer}"
- if fields[field] == want:
- waited = (time.monotonic() - started) * 1000.0
- return True, f"{field}={want} через {waited:.0f} мс"
- if time.monotonic() >= deadline:
- return False, (f"! {verb}: таймаут {timeout_ms:g} мс, "
- f"{field}={fields[field]} ({answer})")
- time.sleep(POLL_INTERVAL)
- def send(self, line):
- ok, text = self.exec_line(line)
- if text:
- print(text)
- return ok
- def default(self, line):
- """Всё незнакомое уходит на сервер — он и рассудит."""
- if line == "EOF":
- return self.do_quit("")
- self.send(line)
- def emptyline(self):
- pass
- # --------------------------------------------------- файлы с командами
- def run_script(self, path, keep_going=False, depth=0):
- """Выполнить файл построчно. Возвращает True, если ошибок не было."""
- if depth > MAX_RUN_DEPTH:
- print(f"! run: слишком глубокая вложенность (> {MAX_RUN_DEPTH})")
- return False
- try:
- with open(path, "r", encoding="utf-8") as fh:
- lines = fh.readlines()
- except OSError as exc:
- print(f"! run: {exc}")
- return False
- name = os.path.basename(path)
- errors = 0
- executed = 0
- for lineno, raw in enumerate(lines, 1):
- line = raw.split("#", 1)[0].strip()
- if not line:
- continue
- executed += 1
- ok, text = self.exec_line(line, depth=depth)
- if line.split(None, 1)[0] == "echo":
- print(text) # echo — заголовок раздела, колонки только мешают
- else:
- print(f"{name}:{lineno:<4} {line:28s} {text}".rstrip())
- if not ok:
- errors += 1
- if not keep_going:
- print(f"! run: остановлено на {name}:{lineno} "
- f"(-k — продолжать несмотря на ошибки)")
- return False
- print(f"run: {name} — строк {executed}, ошибок {errors}")
- return errors == 0
- def do_run(self, arg, depth=0):
- """run [-k] <файл> — выполнить файл с командами построчно.
- Пустые строки и всё после "#" игнорируются. По умолчанию исполнение
- останавливается на первой ошибке; -k — продолжать до конца.
- """
- args = arg.split()
- keep_going = False
- while args and args[0] in ("-k", "--keep-going"):
- keep_going = True
- args.pop(0)
- if not args:
- print("! run: нужен путь к файлу")
- return False
- return self.run_script(" ".join(args), keep_going, depth)
- def do_load(self, arg, depth=0):
- """load <файл> — загрузить файл как программу в аппаратный
- секвенсор (seqbegin, построчно как в run, затем seqend).
- Компилировать на клиенте нечего: сервер сам разворачивает set/
- start/op/defaults не в живой тракт, а в память секвенсора, пока
- идёт загрузка — файл тот же самый, что и для run. Не запускает
- воспроизведение — для этого seqrun (или seqrun N с явным числом
- слов). Внутри файла не используйте wait/waitidle/waitbuf/waitseq:
- секвенсор в момент загрузки ничего не исполняет, эти директивы
- опрашивают живой статус и внутри load смысла не имеют.
- seqend вызывается даже при ошибке посреди файла — не оставляем
- сервер подвешенным в режиме загрузки.
- """
- args = arg.split()
- if not args:
- print("! load: нужен путь к файлу")
- return False
- path = " ".join(args)
- ok0, text0 = self.exec_line("seqbegin", depth=depth)
- if text0:
- print(text0)
- if not ok0:
- print("! load: seqbegin не удался, отменено")
- return False
- ok = self.run_script(path, keep_going=False, depth=depth)
- ok1, text1 = self.exec_line("seqend", depth=depth)
- if text1:
- print(text1)
- return ok and ok1
- # -------------------------------------------------------------- команды
- def do_status(self, arg):
- """status — состояние секвенсора (busy/ready/buf_valid/overflow)."""
- self.send("status " + arg)
- def do_params(self, arg):
- """params — теневые параметры, какими их помнит сервер."""
- self.send("params " + arg)
- def do_defaults(self, arg):
- """defaults — вернуть параметры тракта к умолчаниям."""
- self.send("defaults " + arg)
- def do_set(self, arg):
- """set dur|mask|freq|inc|phase|pulse|step|env <значения> — параметры события."""
- self.send("set " + arg)
- def do_start(self, arg):
- """start ttl|rf — запустить событие."""
- self.send("start " + arg)
- def do_seqbegin(self, arg):
- """seqbegin — начать загрузку последовательности в секвенсор
- (обнуляет указатель BRAM). Дальше set/start/op/defaults пишутся не в
- живой тракт, а в память секвенсора — до seqend. Обычно проще
- использовать директиву load <файл>, которая сама зовёт
- seqbegin/seqend вокруг файла.
- """
- self.send("seqbegin " + arg)
- def do_seqend(self, arg):
- """seqend — закончить загрузку, вернуться к живому режиму."""
- self.send("seqend " + arg)
- def do_seqrun(self, arg):
- """seqrun [N] — запустить загруженную последовательность.
- Без аргумента — сервер помнит, сколько слов было загружено с
- последнего seqbegin. N — сыграть только первые N слов (например, для
- отладки не всей последовательности целиком).
- """
- self.send("seqrun " + arg)
- def do_seqabort(self, arg):
- """seqabort — немедленно остановить проигрывание последовательности
- (уже идущее событие доиграет до конца, как обычно — останавливается
- только выдача ДАЛЬНЕЙШИХ слов программы)."""
- self.send("seqabort " + arg)
- def do_op(self, arg):
- """op <код> <параметр> — сырая команда, без проверки контрактов."""
- self.send("op " + arg)
- def do_peek(self, arg):
- """peek <смещение> — прочитать регистр (0x00 OPCODE, 0x04 PARAM, 0x08 STATUS)."""
- self.send("peek " + arg)
- def do_poke(self, arg):
- """poke <смещение> <значение> — записать регистр напрямую."""
- self.send("poke " + arg)
- def do_echo(self, arg):
- """echo <текст> — напечатать строку (полезно в файлах последовательностей)."""
- self.send("echo " + arg)
- def do_wait(self, arg):
- """wait <мс> — пауза."""
- self.send("wait " + arg)
- def do_waitidle(self, arg):
- """waitidle [мс] — ждать busy=0, то есть конца всех событий."""
- self.send("waitidle " + arg)
- def do_waitbuf(self, arg):
- """waitbuf [мс] — ждать buf_valid=0: команда ушла из буфера в тракт.
- После этого можно менять маску и длительность следующего события, не
- испортив уже поставленное в очередь.
- """
- self.send("waitbuf " + arg)
- def do_waitseq(self, arg):
- """waitseq [мс] — ждать seq_busy=0: секвенсор выдал все слова программы.
- Это НЕ то же самое, что "последнее событие доиграло" — оно может ещё
- идти. После seqrun обычно нужны оба: waitseq, затем waitidle.
- """
- self.send("waitseq " + arg)
- def do_pulse(self, arg):
- """pulse <мкс> [частота Гц] [маска] — настроить и выдать один РЧ-импульс.
- Составная команда клиента: окно TTL берётся с запасом 20 % от длительности
- огибающей, иначе сервер отвергнет старт по контракту 2 (РЧ не должно
- переживать окно TTL).
- """
- args = arg.split()
- if not args:
- print("! нужна длительность в мкс")
- return
- try:
- us = float(args[0])
- freq = float(args[1]) if len(args) > 1 else None
- mask = args[2] if len(args) > 2 else None
- except ValueError:
- print("! не разобраны аргументы")
- return
- window = max(1, int(us * 1.2 + 0.5))
- steps = [f"set dur {window}", f"set pulse {us}"]
- if freq is not None:
- steps.append(f"set freq {freq}")
- if mask is not None:
- steps.append(f"set mask {mask}")
- steps.append("start rf")
- for step in steps:
- ok, text = self.exec_line(step)
- print(f"{step:22s} -> {text}")
- if not ok:
- print("! последовательность прервана")
- return
- def do_connect(self, arg):
- """connect [хост [порт]] — переподключиться."""
- args = arg.split()
- if args:
- self.link.host = args[0]
- if len(args) > 1:
- self.link.port = int(args[1])
- try:
- print(self.link.connect())
- except (OSError, SpecError) as exc:
- print(f"! не подключиться к {self.link.host}:{self.link.port}: {exc}")
- def do_quit(self, arg):
- """quit — закрыть соединение и выйти."""
- try:
- print(self.link.command("quit"))
- except SpecError:
- pass
- self.link.close()
- return True
- do_exit = do_quit
- do_EOF = do_quit
- def main():
- ap = argparse.ArgumentParser(description="тестовый клиент spec_server")
- ap.add_argument("--host", default="rp-f0xxxx.local", help="адрес платы")
- ap.add_argument("--port", type=int, default=DEFAULT_PORT)
- ap.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT)
- ap.add_argument("--run", metavar="ФАЙЛ",
- help="выполнить файл с командами и выйти")
- ap.add_argument("-k", "--keep-going", action="store_true",
- help="с --run: не останавливаться на ошибках")
- ap.add_argument("command", nargs="*",
- help="если задана — выполнить одну команду и выйти")
- args = ap.parse_args()
- link = Link(args.host, args.port, args.timeout)
- try:
- hello = link.connect()
- except (OSError, SpecError) as exc:
- print(f"! не подключиться к {args.host}:{args.port}: {exc}", file=sys.stderr)
- return 1
- shell = SpecShell(link)
- try:
- if args.run:
- print(hello)
- return 0 if shell.run_script(args.run, args.keep_going) else 1
- if args.command:
- line = " ".join(args.command)
- ok, text = shell.exec_line(line)
- print(text)
- return 0 if ok else 2
- shell.intro = f"{hello}\nhelp — команды сервера, ? — команды клиента"
- shell.cmdloop()
- return 0
- finally:
- link.close()
- if __name__ == "__main__":
- sys.exit(main())
|