def __init__(self, isServer: bool = True): self._client = FBServer() if isServer else FBClient() self._client.registerRecvCallback(self._send) self._serThread: ReaderThread = None self._connected = threading.Event() self._running = True self._conCB: List[callable] = [] self._disconCB: List[callable] = []
def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBPos") self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._root.rowconfigure(0, weight=1) self._root.columnconfigure(0, weight=1) self._recv = FBFloatMultiRecv() self._client = FBServer() if isServer else FBClient() self._client.registerRecvCallback(self._recv.input) self._plotFrame = ttk.Frame(self._root) self._plotFrame.grid(row=0, column=0, sticky="wesn") self._plotFrame.rowconfigure(0, weight=1) self._plotFrame.columnconfigure(0, weight=1) self._fig = plt.Figure() self._fig.subplots_adjust(top=0.965, bottom=0.055, left=0.115, right=0.97, wspace=0, hspace=0) self._canvas = FigureCanvasTkAgg(self._fig, master=self._plotFrame) self._canvas.get_tk_widget().grid(row=0, column=0, sticky="wesn") self._ax = self._fig.add_subplot(1, 1, 1) self._ax.set_aspect("equal", adjustable="datalim") self._toolbar = NavigationToolbar2Tk(self._canvas, self._plotFrame, pack_toolbar=False) self._toolbar.update() self._hiddenPanButton = self._toolbar._buttons["Pan"] self._prePanState = False self._canvas.mpl_connect("button_press_event", self._control_on_press) self._canvas.mpl_connect("button_release_event", self._control_on_release) self._canvas.mpl_connect("motion_notify_event", self._control_on_move) self._control_event: SimpleNamespace = None self._control_event_lock = threading.Lock() self._control_arrow = FancyArrow(0, 0, 1, 0, width=0.1) self._ax.add_patch(self._control_arrow) self._control_arrow.set_visible(False) self._opFrame = ttk.Frame(self._root) self._opFrame.grid(row=1, column=0, sticky="we") self._resetLimButton = ttk.Button(self._opFrame, text="视野重置", command=self._resetLim) self._resetLimButton.pack(side="left", padx=5, pady=5) self._panButton = ttk.Checkbutton( self._opFrame, text="拖拽", bootstyle=("warning", "outline", "toolbutton"), command=self._onPanClick, ) self._panButton.pack(side="left", padx=5, pady=5) self._controlButton = ttk.Checkbutton( self._opFrame, text="控制", bootstyle=("error", "outline", "toolbutton"), command=self._onControlClick, ) self._controlButton.pack(side="left", padx=5, pady=5) self._pauseButton = ttk.Checkbutton(self._opFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._pauseButton.pack(side="left", padx=5, pady=5) self._resetPosButton = ttk.Button(self._opFrame, text="重置位置", command=self._resetPos) self._resetPosButton.pack(side="left", padx=5, pady=5) self._stopButton = ttk.Button(self._opFrame, text="停止", command=self._stop) self._stopButton.pack(side="left", padx=5, pady=5) self._robots: List[Robot] = [] self._scatters: List[Scatter] = []
class FBPosApp: def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBPos") self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._root.rowconfigure(0, weight=1) self._root.columnconfigure(0, weight=1) self._recv = FBFloatMultiRecv() self._client = FBServer() if isServer else FBClient() self._client.registerRecvCallback(self._recv.input) self._plotFrame = ttk.Frame(self._root) self._plotFrame.grid(row=0, column=0, sticky="wesn") self._plotFrame.rowconfigure(0, weight=1) self._plotFrame.columnconfigure(0, weight=1) self._fig = plt.Figure() self._fig.subplots_adjust(top=0.965, bottom=0.055, left=0.115, right=0.97, wspace=0, hspace=0) self._canvas = FigureCanvasTkAgg(self._fig, master=self._plotFrame) self._canvas.get_tk_widget().grid(row=0, column=0, sticky="wesn") self._ax = self._fig.add_subplot(1, 1, 1) self._ax.set_aspect("equal", adjustable="datalim") self._toolbar = NavigationToolbar2Tk(self._canvas, self._plotFrame, pack_toolbar=False) self._toolbar.update() self._hiddenPanButton = self._toolbar._buttons["Pan"] self._prePanState = False self._canvas.mpl_connect("button_press_event", self._control_on_press) self._canvas.mpl_connect("button_release_event", self._control_on_release) self._canvas.mpl_connect("motion_notify_event", self._control_on_move) self._control_event: SimpleNamespace = None self._control_event_lock = threading.Lock() self._control_arrow = FancyArrow(0, 0, 1, 0, width=0.1) self._ax.add_patch(self._control_arrow) self._control_arrow.set_visible(False) self._opFrame = ttk.Frame(self._root) self._opFrame.grid(row=1, column=0, sticky="we") self._resetLimButton = ttk.Button(self._opFrame, text="视野重置", command=self._resetLim) self._resetLimButton.pack(side="left", padx=5, pady=5) self._panButton = ttk.Checkbutton( self._opFrame, text="拖拽", bootstyle=("warning", "outline", "toolbutton"), command=self._onPanClick, ) self._panButton.pack(side="left", padx=5, pady=5) self._controlButton = ttk.Checkbutton( self._opFrame, text="控制", bootstyle=("error", "outline", "toolbutton"), command=self._onControlClick, ) self._controlButton.pack(side="left", padx=5, pady=5) self._pauseButton = ttk.Checkbutton(self._opFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._pauseButton.pack(side="left", padx=5, pady=5) self._resetPosButton = ttk.Button(self._opFrame, text="重置位置", command=self._resetPos) self._resetPosButton.pack(side="left", padx=5, pady=5) self._stopButton = ttk.Button(self._opFrame, text="停止", command=self._stop) self._stopButton.pack(side="left", padx=5, pady=5) self._robots: List[Robot] = [] self._scatters: List[Scatter] = [] def _resetPos(self): data = HEADER + STATE_ID + b"".join(map(as_float, (0, 0, 0, 0, 0, 0))) self._client.send(data) def _stop(self): data = HEADER + STOP_ID self._client.send(data) def _get_control_data(self): return ( self._control_event.x, self._control_event.y, self._control_event.dx, self._control_event.dy, ) def _control_on_press(self, event): if event.xdata is None: return if self._controlButton.instate(["selected"]): with self._control_event_lock: self._control_event = SimpleNamespace(x=event.xdata, y=event.ydata, dx=0, dy=0) def _control_on_move(self, event): if event.xdata is None: return with self._control_event_lock: if self._control_event is not None: self._control_event.dx, self._control_event.dy = ( event.xdata - self._control_event.x, event.ydata - self._control_event.y, ) def _control_on_release(self, event): with self._control_event_lock: if self._control_event is None or self._control_event.dx == self._control_event.dy == 0: self._control_event = None return x, y, dx, dy = self._get_control_data() self._control_event = None yaw = atan2(dy, dx) print("x:%.2f y:%.2f yaw:%.2f" % (x, y, yaw)) if event.button == 1: data = HEADER + CONTROL_ID + b"".join(map(as_float, (x, y, yaw))) else: data = HEADER + STATE_ID + b"".join( map(as_float, (x, y, yaw, 0, 0, 0))) self._client.send(data) def _getLimScale(self): xlim, ylim = self._ax.get_xlim(), self._ax.get_ylim() return min(xlim[1] - xlim[0], ylim[1] - ylim[0]) def _updateControlArrow(self): with self._control_event_lock: if self._control_event is None or self._control_event.dx == self._control_event.dy == 0: self._control_arrow.set_visible(False) return [self._control_arrow] else: self._control_arrow.set_visible(True) x, y, dx, dy = self._get_control_data() k = self._getLimScale() * 0.2 / sqrt(dx * dx + dy * dy) self._control_arrow.set_data(x=x, y=y, dx=dx * k, dy=dy * k) return [self._control_arrow] def registerScatter(self, id: int, *args, **kwargs) -> Scatter: scatter = Scatter(self._ax, *args, **kwargs) self._recv.setConfig(id, 0, 4, True) self._recv.registerRecvCallback( id, lambda data: scatter.set_data(np.reshape(data, (-1, 2)))) self._scatters.append(scatter) return scatter def _updateScatters(self): if not self._pauseButton.instate(["selected"]): for scatter in self._scatters: scatter.update_data() return [scatter.get_artist() for scatter in self._scatters] def registerRobot(self, id: Optional[int] = None, *args, **kwargs) -> Robot: robot = Robot(*args, **kwargs) if id is not None: self._recv.setConfig(id, 3, 4, True) self._recv.registerRecvCallback(id, lambda data: robot.set_pose(*data)) self._robots.append(robot) return robot def _updateRobots(self): if not self._pauseButton.instate(["selected"]): for robot in self._robots: robot.update_pose() return self._robots def _updatePlot(self, *_): changed = [] changed.extend(self._updateControlArrow()) changed.extend(self._updateRobots()) changed.extend(self._updateScatters()) return changed def _onPanClick(self, *_): cur = self._panButton.instate(["selected"]) if cur != self._prePanState: self._toolbar.pan() self._prePanState = cur if cur: self._controlButton.state(["!selected"]) def _onControlClick(self, *_): if self._controlButton.instate(["selected"]): self._panButton.state(["!selected"]) self._onPanClick() def _initPlot(self): self._ax.cla() self._ax.grid(True) for robot in self._robots: self._ax.add_patch(robot) self._resetLim() def _resetLim(self, *_): self._ax.set_ylim((-1, 8)) self._ax.set_xlim((-1, 8)) def mainloop(self): self._client.start() self._recv.start() self._initPlot() self._animation = animation.FuncAnimation(self._fig, self._updatePlot, interval=20, blit=True) self._root.mainloop() def shutdown(self): self._recv.shutdown() self._client.shutdown() self._root.destroy()
if data is None: continue for cb in self._recvCBs: if not self._running: break cb(data) __all__ = ["FBFloatRecv"] if __name__ == "__main__": import sys, os.path sys.path.append(os.path.join(os.path.dirname(__file__), "..")) from FBSocket import FBServer server = FBServer() recv = FBFloatRecv() server.registerRecvCallback(recv.input) recv.registerRecvCallback(print) recv.start() server.start() input() recv.setConfig(cnt=2) input() recv.shutdown() server.shutdown()
def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBImg") self._root.rowconfigure(0, weight=1) self._root.columnconfigure(0, weight=1) self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._imgCanvas = ttk.Canvas(self._root) self._imgCanvas.config(bg="light gray") self._imgCanvas.grid(row=0, column=0, sticky="nsew") self._imgCanvas_shape = (0, 0) opFrame = ttk.Frame(self._root) opFrame.grid(row=1, column=0, sticky="we") opFrame.columnconfigure(0, weight=1) val = lambda s: ValEntry.type_validator(int)(s) and int(s) > 0 imgFrame = ttk.Frame(opFrame) imgFrame.grid(row=0, column=0, sticky="we") ttk.Label(imgFrame, text="宽度").pack(side="left", pady=5) self.wEntry = ValEntry(val, imgFrame, width=5) self.wEntry.pack(side="left", pady=5) ttk.Label(imgFrame, text="高度").pack(side="left", pady=5) self.hEntry = ValEntry(val, imgFrame, width=5) self.hEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="应用", command=self._applySize).pack(side="left", padx=2, pady=5) self._pauseButton = ttk.Checkbutton(imgFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._pauseButton.pack(side="left", padx=5, pady=5) ttk.Label(imgFrame, text="保存路径").pack(side="left", pady=5) self._dirEntry = ttk.Entry(imgFrame, width=30) self._dirEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="选择", command=self._selectDir).pack(side="left", pady=5) self._saveButton = ttk.Button(imgFrame, text="保存", bootstyle=("success"), command=self.saveImg) self._saveButton.pack(side="left", padx=5, pady=5) self._recordButton = ttk.Checkbutton(imgFrame, text="录制", command=self._toggleRecord, bootstyle=("warning", "outline", "toolbutton")) self._recordButton.pack(side="left", pady=5) self._cntLabel = ttk.Label(imgFrame) self._cntLabel.pack(side="left", pady=5) self._client = FBServer() if isServer else FBClient() self._recv = FBRawRecv() self._client.registerRecvCallback(self._recv.input) self._recv.registerRecvCallback(self.updateData) self._imgLock = threading.Lock() self._dirLock = threading.Lock() self.loadConfig()
class FBImgApp: def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBImg") self._root.rowconfigure(0, weight=1) self._root.columnconfigure(0, weight=1) self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._imgCanvas = ttk.Canvas(self._root) self._imgCanvas.config(bg="light gray") self._imgCanvas.grid(row=0, column=0, sticky="nsew") self._imgCanvas_shape = (0, 0) opFrame = ttk.Frame(self._root) opFrame.grid(row=1, column=0, sticky="we") opFrame.columnconfigure(0, weight=1) val = lambda s: ValEntry.type_validator(int)(s) and int(s) > 0 imgFrame = ttk.Frame(opFrame) imgFrame.grid(row=0, column=0, sticky="we") ttk.Label(imgFrame, text="宽度").pack(side="left", pady=5) self.wEntry = ValEntry(val, imgFrame, width=5) self.wEntry.pack(side="left", pady=5) ttk.Label(imgFrame, text="高度").pack(side="left", pady=5) self.hEntry = ValEntry(val, imgFrame, width=5) self.hEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="应用", command=self._applySize).pack(side="left", padx=2, pady=5) self._pauseButton = ttk.Checkbutton(imgFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._pauseButton.pack(side="left", padx=5, pady=5) ttk.Label(imgFrame, text="保存路径").pack(side="left", pady=5) self._dirEntry = ttk.Entry(imgFrame, width=30) self._dirEntry.pack(side="left", pady=5) ttk.Button(imgFrame, text="选择", command=self._selectDir).pack(side="left", pady=5) self._saveButton = ttk.Button(imgFrame, text="保存", bootstyle=("success"), command=self.saveImg) self._saveButton.pack(side="left", padx=5, pady=5) self._recordButton = ttk.Checkbutton(imgFrame, text="录制", command=self._toggleRecord, bootstyle=("warning", "outline", "toolbutton")) self._recordButton.pack(side="left", pady=5) self._cntLabel = ttk.Label(imgFrame) self._cntLabel.pack(side="left", pady=5) self._client = FBServer() if isServer else FBClient() self._recv = FBRawRecv() self._client.registerRecvCallback(self._recv.input) self._recv.registerRecvCallback(self.updateData) self._imgLock = threading.Lock() self._dirLock = threading.Lock() self.loadConfig() def _toggleRecord(self, *_): if self._recordButton.instate(["selected"]): self._saveButton.config(state="disabled") self._recordButton.config(text="停止") else: self._saveButton.config(state="!disabled") self._recordButton.config(text="录制") def _updateDir(self): os.makedirs(self.save_dir, exist_ok=True) self._dirEntry.config(state="normal") self._dirEntry.delete(0, tk.END) self._dirEntry.insert(0, self.save_dir) self._dirEntry.config(state="readonly") def _selectDir(self, *_): with self._dirLock: cur = self.save_dir s = filedialog.askdirectory(master=self._root, initialdir=cur, title="选择保存路径", mustexist=False) if not s: return with self._dirLock: self.save_dir = s self._updateDir() def saveImg(self, *_): def save(): with self._imgLock: img = self.img with self._dirLock: Dir = self.save_dir name = datetime.datetime.now().strftime( "%Y-%m-%d-%H-%M-%S-%f") + SAVE_EXT cv.imwrite(os.path.join(Dir, name), img) threading.Thread(target=save).start() def updateData(self, data: bytes) -> None: if self._pauseButton.instate(["selected"]): return with self._imgLock: w, h = self.w, self.h img = np.frombuffer(data, dtype=np.uint8).reshape((h, w)) with self._imgLock: if w != self.w or h != self.h: return self.img = img self._updateFlag = True if self._recordButton.instate(["selected"]): self.saveImg() def _applySize(self): with self._imgLock: self.w, self.h = int(self.wEntry.get()), int(self.hEntry.get()) self.img = np.zeros((self.h, self.w), dtype=np.uint8) self._updateFlag = True self._recv.setConfig(cnt=self.w * self.h) def loadConfig(self): os.makedirs(CFG_DIR, exist_ok=True) cfg = {} if os.path.exists(CFG_PATH): with open(CFG_PATH, "r") as f: cfg = json.load(f) self._root.geometry(cfg.get("geometry", "876x600+30+30")) self.w = cfg.get("w", 752) self.h = cfg.get("h", 480) self.wEntry.set(str(self.w)) self.hEntry.set(str(self.h)) self.save_dir = cfg.get("save_dir", SAVE_DIR) self._updateDir() self._applySize() def saveConfig(self): cfg = { "geometry": self._root.geometry(), "w": self.w, "h": self.h, "save_dir": self.save_dir } with open(CFG_PATH, "w") as f: json.dump(cfg, f) def create_image(self): canvas_w, canvas_h = self._imgCanvas.winfo_width( ), self._imgCanvas.winfo_height() with self._imgLock: if (canvas_w, canvas_h ) == self._imgCanvas_shape and not self._updateFlag: return img = self.img self._updateFlag = False self._imgCanvas_shape = (canvas_w, canvas_h) img_w, img_h = img.shape[1], img.shape[0] canvas_k, img_k = canvas_w / canvas_h, img_w / img_h w, h = (canvas_w, round(canvas_w / img_k)) if canvas_k < img_k else (round(canvas_h * img_k), canvas_h) resized_img = cv.resize(img, (max(1, w), max(1, h)), interpolation=cv.INTER_NEAREST) cvt_img = resized_img self._imgTK = ImageTk.PhotoImage(master=self._imgCanvas, image=Image.fromarray(cvt_img)) self._imgCanvas.create_image(canvas_w >> 1, canvas_h >> 1, image=self._imgTK) def _create_image_task(self): if self._running: self.create_image() if self._running: self._imgCanvas.after(50, self._create_image_task) def mainloop(self): self._running = True self._updateFlag = True self._create_image_task() self._client.start() self._recv.start() self._root.mainloop() def shutdown(self): self._running = False self._recv.shutdown() self._client.shutdown() self.saveConfig() self._root.destroy()
class FBSerial: def __init__(self, isServer: bool = True): self._client = FBServer() if isServer else FBClient() self._client.registerRecvCallback(self._send) self._serThread: ReaderThread = None self._connected = threading.Event() self._running = True self._conCB: List[callable] = [] self._disconCB: List[callable] = [] @staticmethod def _protocalFactory(master: "FBSerial"): class PrintLines(Protocol): def connection_made(self, transport): for cb in master._conCB: if not master._running: break cb() self.port = transport.serial.port print(f"[FBSerial]连接建立: {self.port}") master._connected.set() def data_received(self, data): master._client.send(data) def connection_lost(self, exc): master._connected.clear() master._serThread = None for cb in master._disconCB: if not master._running: break cb() print(f"[FBSerial]连接断开: {self.port}") return PrintLines def _send(self, data: bytes) -> None: if self._running: self._connected.wait() if not self._running: return try: self._serThread.write(data) except: pass def start(self): self._running = True self._client.start() def shutdown(self) -> None: self._running = False self.close() self._client.shutdown() def connect(self, port, baudrate=115200): self.close() ser = serial.Serial(port=port, baudrate=baudrate) self._serThread = ReaderThread(ser, self._protocalFactory(self)) self._serThread.start() def close(self) -> None: if self._serThread is not None: if self._running: threading.Thread(target=self._serThread.close).start() else: self._serThread.close() def registerConnectCallback(self, func: callable) -> None: self._conCB.append(func) def registerDisconnectCallback(self, func: callable) -> None: self._disconCB.append(func)
def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBRecorder") self._root.resizable(False, False) self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._recv = FBFloatMultiRecv() self._recv.registerRecvAllCallback(self.updateData) self._client = FBServer() if isServer else FBClient() self._client.registerRecvCallback(self._recv.input) self._csvWriter: FBCSVWriter = None self._cvsWriterLock = threading.Lock() self.cnt = 0 self._cntLock = threading.Lock() self._cntVar = tk.StringVar(value="0") cfgFrame = ttk.Frame(self._root) cfgFrame.pack(padx=5, pady=5) self.cfg_path = tk.StringVar() self._cfgPathEntry = ttk.Entry(cfgFrame, textvariable=self.cfg_path, state="readonly", width=30) self._cfgPathButton = ttk.Button(cfgFrame, text="选择", command=self._selectCfgPath) ttk.Label(cfgFrame, text="配置文件").pack(side="left") self._cfgPathEntry.pack(side="left") self._cfgPathButton.pack(side="left") saveFrame = ttk.Frame(self._root) saveFrame.pack(padx=5, pady=5) self.save_path = tk.StringVar() self._savePathEntry = ttk.Entry(saveFrame, textvariable=self.save_path, state="readonly", width=30) self._savePathButton = ttk.Button(saveFrame, text="选择", command=self._selectSavePath) ttk.Label(saveFrame, text="保存文件").pack(side="left") self._savePathEntry.pack(side="left") self._savePathButton.pack(side="left") opFrame = ttk.Frame(self._root) opFrame.pack(padx=5, pady=5, fill="x", expand=True) self._appendsButton = ttk.Checkbutton( opFrame, text="追加", bootstyle=("info", "outline", "toolbutton"), ) self._recordButton = ttk.Checkbutton(opFrame, text="录制", command=self._toggleRecord, bootstyle=("warning", "outline", "toolbutton")) self._pauseButton = ttk.Checkbutton(opFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._appendsButton.pack(side="left") self._recordButton.pack(side="left", padx=5) self._pauseButton.pack(side="left") ttk.Label(opFrame, text="已接收:").pack(side="left") ttk.Label(opFrame, textvariable=self._cntVar).pack(side="left")
class FBRecorderApp: def __init__(self, isServer: bool = False): style = ttk.Style("cosmo") self._root = style.master self._root.title("FBRecorder") self._root.resizable(False, False) self._root.protocol("WM_DELETE_WINDOW", self.shutdown) self._recv = FBFloatMultiRecv() self._recv.registerRecvAllCallback(self.updateData) self._client = FBServer() if isServer else FBClient() self._client.registerRecvCallback(self._recv.input) self._csvWriter: FBCSVWriter = None self._cvsWriterLock = threading.Lock() self.cnt = 0 self._cntLock = threading.Lock() self._cntVar = tk.StringVar(value="0") cfgFrame = ttk.Frame(self._root) cfgFrame.pack(padx=5, pady=5) self.cfg_path = tk.StringVar() self._cfgPathEntry = ttk.Entry(cfgFrame, textvariable=self.cfg_path, state="readonly", width=30) self._cfgPathButton = ttk.Button(cfgFrame, text="选择", command=self._selectCfgPath) ttk.Label(cfgFrame, text="配置文件").pack(side="left") self._cfgPathEntry.pack(side="left") self._cfgPathButton.pack(side="left") saveFrame = ttk.Frame(self._root) saveFrame.pack(padx=5, pady=5) self.save_path = tk.StringVar() self._savePathEntry = ttk.Entry(saveFrame, textvariable=self.save_path, state="readonly", width=30) self._savePathButton = ttk.Button(saveFrame, text="选择", command=self._selectSavePath) ttk.Label(saveFrame, text="保存文件").pack(side="left") self._savePathEntry.pack(side="left") self._savePathButton.pack(side="left") opFrame = ttk.Frame(self._root) opFrame.pack(padx=5, pady=5, fill="x", expand=True) self._appendsButton = ttk.Checkbutton( opFrame, text="追加", bootstyle=("info", "outline", "toolbutton"), ) self._recordButton = ttk.Checkbutton(opFrame, text="录制", command=self._toggleRecord, bootstyle=("warning", "outline", "toolbutton")) self._pauseButton = ttk.Checkbutton(opFrame, text="暂停", bootstyle=("success", "outline", "toolbutton")) self._appendsButton.pack(side="left") self._recordButton.pack(side="left", padx=5) self._pauseButton.pack(side="left") ttk.Label(opFrame, text="已接收:").pack(side="left") ttk.Label(opFrame, textvariable=self._cntVar).pack(side="left") def loadConfig(self): os.makedirs(CFG_DIR, exist_ok=True) cfg = {} if os.path.exists(CFG_PATH): with open(CFG_PATH, "r") as f: cfg = json.load(f) self.cfg_path.set(cfg.get("cfg_path", "")) self.save_path.set(cfg.get("save_path", SAVE_PATH)) if cfg.get("appends", False): self._appendsButton.invoke() def saveConfig(self): cfg = { "cfg_path": self.cfg_path.get(), "save_path": self.save_path.get(), "appends": self._appendsButton.instate(["selected"]), } with open(CFG_PATH, "w") as f: json.dump(cfg, f) def _increaseCnt(self): with self._cntLock: self.cnt += 1 def _updateCnt(self): with self._cntLock: cnt = self.cnt self._cntVar.set(str(cnt)) self._root.after(100, self._updateCnt) def mainloop(self): self.loadConfig() self._client.start() self._recv.start() self._updateCnt() self._root.mainloop() def shutdown(self): self._recv.shutdown() self._client.shutdown() self._stopWriter() self.saveConfig() self._root.destroy() def updateData(self, id: int, data: List[float]): if self._pauseButton.instate(["selected"]): return with self._cvsWriterLock: writer = self._csvWriter if writer is None: return self._increaseCnt() writer.write(map(str, (datetime.timestamp(datetime.now()), id, *data))) def _toggleRecord(self, *_): if self._recordButton.instate(["selected"]): self._recordButton.config(text="停止") self._cfgPathButton.config(state="disabled") self._savePathButton.config(state="disabled") self._appendsButton.config(state="disabled") self._startWriter() else: self._recordButton.config(text="录制") self._cfgPathButton.config(state="!disabled") self._savePathButton.config(state="!disabled") self._appendsButton.config(state="!disabled") self._stopWriter() def _startWriter(self): self._stopWriter() self._recv.resetConfig() cfg_path = self.cfg_path.get() if not isfile(cfg_path): messagebox.showerror("错误", "配置文件不存在", parent=self._root) self._recordButton.invoke() return self._recv.loadCfgFromCSV(cfg_path) save_path = self.save_path.get() with self._cvsWriterLock: self._csvWriter = FBCSVWriter(save_path, appends=self._appendsButton.instate( ["selected"])) def _stopWriter(self): with self._cvsWriterLock: writer = self._csvWriter self._csvWriter = None if writer is not None: writer.close() def _selectCfgPath(self, *_): s = filedialog.askopenfilename( master=self._root, initialfile=self.cfg_path.get(), filetypes=[("csv file", "*.csv"), ("all files", "*.*")], defaultextension="csv file", title="选择配置文件", ) if s and isfile(s): self.cfg_path.set(s) def _selectSavePath(self, *_): s = filedialog.asksaveasfilename( master=self._root, initialfile=self.save_path.get(), filetypes=[("csv file", "*.csv"), ("all files", "*.*")], defaultextension="csv file", title="选择保存路径", confirmoverwrite=False, ) if s: os.makedirs(dirname(s), exist_ok=True) self.save_path.set(s)