├── README.md ├── autoupdatedata.sh ├── doc └── images │ ├── fig-nxvlogbatt-data-charging-oem-m2-1.png │ ├── nxvcontrol-1.png │ ├── nxvcontrol-2.png │ ├── nxvcontrol-3.png │ ├── nxvcontrol-4.png │ ├── nxvcontrol-5.png │ ├── nxvcontrol-6.png │ ├── nxvcontrol-7.png │ ├── nxvforward-1.png │ ├── nxvforward-2.png │ ├── nxvforward-3.png │ └── nxvforward-4.png ├── editabletreeview.py ├── guilog.py ├── langinit.sh ├── ledred-off.gif ├── ledred-on.gif ├── neatocmdapi.py ├── neatocmdsim.py ├── nxvcontrol.bat ├── nxvcontrol.py ├── nxvforward.bat ├── nxvforward.py ├── nxvlogbatt-plotfig.sh ├── nxvlogbatt.bat ├── nxvlogbatt.py ├── nxvlogbatt.sh ├── translations ├── en_US.po └── zh_CN.po └── updatedata.sh /README.md: -------------------------------------------------------------------------------- 1 | Neato XV Robot Setup Utilities 2 | ============================== 3 | 4 | [![Python 2, 3](https://img.shields.io/badge/python-2%2C%203-blue.svg)](https://www.python.org/downloads/) 5 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](http://www.gnu.org/licenses/gpl-3.0) 6 | 7 | The tool sets includes the followings 8 | 9 | nxvcontrol -- control Neato XV robot from GUI, supports functions include motor control, show sensors, LiDAR show and wheel moving etc. 10 | nxvforward -- forward the control over network 11 | nxvlogbatt -- log the battery status 12 | 13 | 14 | # Usage 15 | 16 | ## nxvcontrol 17 | 18 | ### Connection 19 | 20 | Connect to network 21 | Connect to simulator 22 | 23 | Before you control the Neato XV robot, you need to setup your robot so that 24 | `nxvcontrol` can connect to your robot by clicking the *Connect* button in the *Connection* tab of the main program. 25 | 26 | There're three types of connection for Neato XV devices, which are directly serial port, network connection, and simulation. 27 | * directly serial port: you may specify the device name, for example, `dev://COM11:115200` is for the port `COM11` and the baudrate is 115200 in Windows. 28 | * network connection: you may use a TCP port forwarder for your device and connect remotely from you PC. 29 | 30 | If you use a Linux box connected to your robot via USB, you may choose `socat` as port forwarder, such as: 31 | 32 | sudo socat TCP-LISTEN:3333,fork,reuseaddr FILE:/dev/ttyACM0,b115200,raw 33 | 34 | or use the `nxvforward` program provided by this package. 35 | 36 | The address *Connect to* line has its format such as `tcp://192.168.1.2:3333`, it'll connect to the port 3333 of the host `192.168.1.2`. 37 | 38 | * simulator: this feature is for software development or testing. just use the "sim:" as the address line, and click "Connect" to start simulation. 39 | 40 | Command line 41 | Schedule 42 | Motors 43 | Sensors 44 | LiDAR 45 | 46 | 47 | ## nxvforward 48 | 49 | Connect to /dev/ttyACM0 50 | Connect to COM12 51 | Client Test 52 | 53 | This is a TCP server, which can forward commands and data between device/simulator and clients. 54 | The bind address can be `120.0.0.1:3333` to accept connection from local only, or `0.0.0.0:3333` for connections from other hosts. 55 | The address line of *Connect to* is the same as `nxvcontrol` described above. 56 | 57 | 58 | ## nxvlogbatt 59 | 60 | To log the Neato XV robot's battery status, you need to specify the port and the data file. For example: 61 | 62 | # for Linux 63 | python3 nxvlogbatt.py -l nxvlogbatt.log -o nxvlogbatt-data.txt -a dev://ttyACM0:115200 64 | 65 | # for Windows 66 | python3 nxvlogbatt.py -l nxvlogbatt.log -o nxvlogbatt-data.txt -a dev://COM12:115200 67 | 68 | 69 | To plot the figures from the data file, you may want to use the script `nxvlogbatt-plotfig.sh`: 70 | 71 | nxvlogbatt-plotfig.sh PREFIX VBATRAT XRANGE TITLE COMMENTS 72 | 73 | The arguments are 74 | 75 | * PREFIX -- the data text file prefix, for example, `prefix.txt` 76 | * VBATRAT -- draw the line "VBattV*200/%" 77 | * XRANGE -- set the range of X(time), `[0:2000]` 78 | * TITLE -- the figure title 79 | * COMMENTS -- the comments of the figure 80 | 81 | Example: 82 | 83 | nxvlogbatt-plotfig.sh nxvlogbatt-data 0 "" "Battery Status" "" 84 | 85 | Charging 86 | 87 | 88 | Implementation Details 89 | ---------------------- 90 | 91 | 1. Neato XV Abstract Serial Interface 92 | 93 | Serial Port: COM11, /dev/ttyACM0 94 | Network Forward: tcp://localhost:3333 95 | Simulator: sim: 96 | 97 | The toolset interprets with above interfaces via unified APIs. 98 | The implementation is in file `neatocmdapi.py`, and the simulator functions is in file `neatocmdsim.py`. 99 | The simulator can simulate the return data described in the [Neato Programmer's Manual](https://www.neatorobotics.com/resources/programmersmanual_20140305.pdf). 100 | It also simulates that the returned LiDAR data would be all zero if the motor is not running. 101 | 102 | 2. Scheduler: It's in file `neatocmdapi.py`, in which all of the tasks are scheduled by a center scheduler in one program, so the execution of the commands would not interference with each other, 103 | even in a network forward mode which supports serving multiple clients. (The commands which are mutual exclusion may still have interferences). 104 | The class provides a callback function, so the user can specify the working function to execute once one task is ready to go. 105 | 106 | 3. NCIService 107 | 108 | It basically is a "hub" for the Neato serial interface, integrated a Scheduler and abstract serial interface, 109 | to support that the serial interface can only execute one task one time. 110 | The class provides `open()/close()` to start/stop the service; 111 | provides a `mailbox`(`class MailPipe`) and function `get_request_block()` to allow 112 | user implement their own processing task functions. 113 | 114 | 4. GUI 115 | 116 | The toolset use Python `tkinter` to implement the GUI for the maximum portabilities between Linux, Windows, and other platforms. 117 | The tkinter GUI application uses the callback function to handle widget events, and to use the `widget.after(time, callback)` functions to preodically update the interfaces. 118 | To make the GUI convinient, the `guilog.py` provides several extensions for the widget, such as 119 | * Log output to textarea 120 | * ToggleButton 121 | * add right click pop-up menu to copy/paste text 122 | 123 | 5. Multi-Thread GUI 124 | 125 | To make the GUI more responsible for the user interactive, the toolset uses multiple threads to process requests in the background. 126 | Since only GUI thread can update the `tkinter` interface, the other threads have to use `Queue` to ask the GUI thread to update the interfaces. 127 | In this implementation, the `mailbox`(`class MailPipe`, in file `neatocmdapi.py`) is used as a center solution for updating various widgets to simplified the code. 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /autoupdatedata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | while [ 1 = 1 ]; do 4 | ./updatedata.sh 5 | echo "delay 60 seconds..." 6 | sleep 30 7 | done 8 | 9 | -------------------------------------------------------------------------------- /doc/images/fig-nxvlogbatt-data-charging-oem-m2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/fig-nxvlogbatt-data-charging-oem-m2-1.png -------------------------------------------------------------------------------- /doc/images/nxvcontrol-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvcontrol-1.png -------------------------------------------------------------------------------- /doc/images/nxvcontrol-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvcontrol-2.png -------------------------------------------------------------------------------- /doc/images/nxvcontrol-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvcontrol-3.png -------------------------------------------------------------------------------- /doc/images/nxvcontrol-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvcontrol-4.png -------------------------------------------------------------------------------- /doc/images/nxvcontrol-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvcontrol-5.png -------------------------------------------------------------------------------- /doc/images/nxvcontrol-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvcontrol-6.png -------------------------------------------------------------------------------- /doc/images/nxvcontrol-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvcontrol-7.png -------------------------------------------------------------------------------- /doc/images/nxvforward-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvforward-1.png -------------------------------------------------------------------------------- /doc/images/nxvforward-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvforward-2.png -------------------------------------------------------------------------------- /doc/images/nxvforward-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvforward-3.png -------------------------------------------------------------------------------- /doc/images/nxvforward-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/doc/images/nxvforward-4.png -------------------------------------------------------------------------------- /editabletreeview.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf8 3 | # 4 | # Copyright 2012-2013 Alejandro Autalán 5 | # 6 | # This program is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License version 3, as published 8 | # by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranties of 12 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 13 | # PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program. If not, see . 17 | # 18 | # For further info, check https://github.com/alejandroautalan/pygubu 19 | 20 | from __future__ import unicode_literals 21 | import functools 22 | 23 | try: 24 | import tkinter as tk 25 | import tkinter.ttk as ttk 26 | except: 27 | import Tkinter as tk 28 | import ttk 29 | 30 | class EditableTreeview(ttk.Treeview): 31 | """A simple editable treeview 32 | 33 | It uses the following events from Treeview: 34 | <> 35 | <4> 36 | <5> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | If you need them use add=True when calling bind method. 45 | 46 | It Generates two virtual events: 47 | <> 48 | <> 49 | The first is used to configure cell editors. 50 | The second is called after a cell was changed. 51 | You can know wich cell is being configured or edited, using: 52 | get_event_info() 53 | """ 54 | def __init__(self, master=None, **kw): 55 | ttk.Treeview.__init__(self, master, **kw) 56 | 57 | self._curfocus = None 58 | self._inplace_widgets = {} 59 | self._inplace_widgets_show = {} 60 | self._inplace_vars = {} 61 | self._header_clicked = False 62 | self._header_dragged = False 63 | 64 | self.bind('<>', self.__check_focus) 65 | #Wheel events? 66 | self.bind('<4>', lambda e: self.after_idle(self.__updateWnds)) 67 | self.bind('<5>', lambda e: self.after_idle(self.__updateWnds)) 68 | #self.bind('', self.__check_focus) 69 | self.bind('', self.__check_focus) 70 | self.bind('', functools.partial(self.__on_key_press, 'Home')) 71 | self.bind('', functools.partial(self.__on_key_press, 'End')) 72 | self.bind('', self.__on_button1) 73 | self.bind('', self.__on_button1_release) 74 | self.bind('', self.__on_mouse_motion) 75 | self.bind('', 76 | lambda e: self.after_idle(self.__updateWnds)) 77 | 78 | 79 | def __on_button1(self, event): 80 | r = event.widget.identify_region(event.x, event.y) 81 | if r in ('separator', 'header'): 82 | self._header_clicked = True 83 | 84 | def __on_mouse_motion(self, event): 85 | if self._header_clicked: 86 | self._header_dragged = True 87 | 88 | def __on_button1_release(self, event): 89 | if self._header_dragged: 90 | self.after_idle(self.__updateWnds) 91 | self._header_clicked = False 92 | self._header_dragged = False 93 | 94 | def __on_key_press(self, key, event): 95 | if key == 'Home': 96 | self.selection_set("") 97 | self.focus(self.get_children()[0]) 98 | if key == 'End': 99 | self.selection_set("") 100 | self.focus(self.get_children()[-1]) 101 | 102 | def delete(self, *items): 103 | self.after_idle(self.__updateWnds) 104 | ttk.Treeview.delete(self, *items) 105 | 106 | def yview(self, *args): 107 | """Update inplace widgets position when doing vertical scroll""" 108 | self.after_idle(self.__updateWnds) 109 | ttk.Treeview.yview(self, *args) 110 | 111 | def yview_scroll(self, number, what): 112 | self.after_idle(self.__updateWnds) 113 | ttk.Treeview.yview_scroll(self, number, what) 114 | 115 | def yview_moveto(self, fraction): 116 | self.after_idle(self.__updateWnds) 117 | ttk.Treeview.yview_moveto(self, fraction) 118 | 119 | def xview(self, *args): 120 | """Update inplace widgets position when doing horizontal scroll""" 121 | self.after_idle(self.__updateWnds) 122 | ttk.Treeview.xview(self, *args) 123 | 124 | def xview_scroll(self, number, what): 125 | self.after_idle(self.__updateWnds) 126 | ttk.Treeview.xview_scroll(self, number, what) 127 | 128 | def xview_moveto(self, fraction): 129 | self.after_idle(self.__updateWnds) 130 | ttk.Treeview.xview_moveto(self, fraction) 131 | 132 | def __check_focus(self, event): 133 | """Checks if the focus has changed""" 134 | #print('Event:', event.type, event.x, event.y) 135 | changed = False 136 | if not self._curfocus: 137 | changed = True 138 | elif self._curfocus != self.focus(): 139 | self.__clear_inplace_widgets() 140 | changed = True 141 | newfocus = self.focus() 142 | if changed: 143 | if newfocus: 144 | #print('Focus changed to:', newfocus) 145 | self._curfocus= newfocus 146 | self.__focus(newfocus) 147 | self.__updateWnds() 148 | 149 | def __focus(self, item): 150 | """Called when focus item has changed""" 151 | cols = self.__get_display_columns() 152 | for col in cols: 153 | self.__event_info =(col,item) 154 | self.event_generate('<>') 155 | if col in self._inplace_widgets: 156 | w = self._inplace_widgets[col] 157 | w.bind('', 158 | lambda e: w.tk_focusNext().focus_set()) 159 | w.bind('', 160 | lambda e: w.tk_focusPrev().focus_set()) 161 | 162 | def __updateWnds(self, event=None): 163 | if not self._curfocus: 164 | return 165 | item = self._curfocus 166 | cols = self.__get_display_columns() 167 | for col in cols: 168 | if col in self._inplace_widgets: 169 | wnd = self._inplace_widgets[col] 170 | bbox = '' 171 | if self.exists(item): 172 | bbox = self.bbox(item, column=col) 173 | if bbox == '': 174 | wnd.place_forget() 175 | elif col in self._inplace_widgets_show: 176 | wnd.place(x=bbox[0], y=bbox[1], 177 | width=bbox[2], height=bbox[3]) 178 | 179 | def __clear_inplace_widgets(self): 180 | """Remove all inplace edit widgets.""" 181 | cols = self.__get_display_columns() 182 | #print('Clear:', cols) 183 | for c in cols: 184 | if c in self._inplace_widgets: 185 | widget = self._inplace_widgets[c] 186 | widget.place_forget() 187 | self._inplace_widgets_show.pop(c, None) 188 | #widget.destroy() 189 | #del self._inplace_widgets[c] 190 | 191 | def __get_display_columns(self): 192 | cols = self.cget('displaycolumns') 193 | show = (str(s) for s in self.cget('show')) 194 | if '#all' in cols: 195 | cols = self.cget('columns') + ('#0',) 196 | elif 'tree' in show: 197 | cols = cols + ('#0',) 198 | return cols 199 | 200 | def get_event_info(self): 201 | return self.__event_info; 202 | 203 | def __get_value(self, col, item): 204 | if col == '#0': 205 | return self.item(item, 'text') 206 | else: 207 | return self.set(item, col) 208 | 209 | def __set_value(self, col, item, value): 210 | if col == '#0': 211 | self.item(item, text=value) 212 | else: 213 | self.set(item, col, value) 214 | self.__event_info =(col,item) 215 | self.event_generate('<>') 216 | 217 | def __update_value(self, col, item): 218 | if not self.exists(item): 219 | return 220 | value = self.__get_value(col, item) 221 | newvalue = self._inplace_vars[col].get() 222 | if value != newvalue: 223 | self.__set_value(col, item, newvalue) 224 | 225 | 226 | def inplace_entry(self, col, item): 227 | if col not in self._inplace_vars: 228 | self._inplace_vars[col] = tk.StringVar() 229 | svar = self._inplace_vars[col] 230 | svar.set(self.__get_value(col, item)) 231 | if col not in self._inplace_widgets: 232 | self._inplace_widgets[col] = ttk.Entry(self, textvariable=svar) 233 | entry = self._inplace_widgets[col] 234 | entry.bind('', lambda e: self.__update_value(col, item)) 235 | entry.bind('', lambda e: self.__update_value(col, item)) 236 | self._inplace_widgets_show[col] = True 237 | 238 | def inplace_checkbutton(self, col, item, onvalue='True', offvalue='False'): 239 | if col not in self._inplace_vars: 240 | self._inplace_vars[col] = tk.StringVar() 241 | svar = self._inplace_vars[col] 242 | svar.set(self.__get_value(col, item)) 243 | if col not in self._inplace_widgets: 244 | self._inplace_widgets[col] = ttk.Checkbutton(self, 245 | textvariable=svar, variable=svar, onvalue=onvalue, offvalue=offvalue) 246 | cb = self._inplace_widgets[col] 247 | cb.bind('', lambda e: self.__update_value(col, item)) 248 | cb.bind('', lambda e: self.__update_value(col, item)) 249 | self._inplace_widgets_show[col] = True 250 | 251 | def inplace_combobox(self, col, item, values, readonly=True): 252 | state = 'readonly' if readonly else 'normal' 253 | if col not in self._inplace_vars: 254 | self._inplace_vars[col] = tk.StringVar() 255 | svar = self._inplace_vars[col] 256 | svar.set(self.__get_value(col, item)) 257 | if col not in self._inplace_widgets: 258 | self._inplace_widgets[col] = ttk.Combobox(self, 259 | textvariable=svar, values=values, state=state) 260 | cb = self._inplace_widgets[col] 261 | cb.bind('', lambda e: self.__update_value(col, item)) 262 | cb.bind('', lambda e: self.__update_value(col, item)) 263 | self._inplace_widgets_show[col] = True 264 | 265 | def inplace_spinbox(self, col, item, min, max, step): 266 | if col not in self._inplace_vars: 267 | self._inplace_vars[col] = tk.StringVar() 268 | svar = self._inplace_vars[col] 269 | svar.set(self.__get_value(col, item)) 270 | if col not in self._inplace_widgets: 271 | self._inplace_widgets[col] = tk.Spinbox(self, 272 | textvariable=svar, from_=min, to=max, increment=step) 273 | sb = self._inplace_widgets[col] 274 | sb.bind('', lambda e: self.__update_value(col, item)) 275 | cb.bind('', lambda e: self.__update_value(col, item)) 276 | self._inplace_widgets_show[col] = True 277 | 278 | 279 | def inplace_custom(self, col, item, widget): 280 | if col not in self._inplace_vars: 281 | self._inplace_vars[col] = tk.StringVar() 282 | svar = self._inplace_vars[col] 283 | svar.set(self.__get_value(col, item)) 284 | self._inplace_widgets[col] = widget 285 | widget.bind('', lambda e: self.__update_value(col, item)) 286 | widget.bind('', lambda e: self.__update_value(col, item)) 287 | self._inplace_widgets_show[col] = True 288 | 289 | 290 | if __name__ == "__main__": 291 | # other examples: https://github.com/hunterjm/futgui/blob/master/frames/playersearch.py 292 | _=lambda x: x 293 | 294 | class MyApplication(object): 295 | 296 | def __init__(self, master): 297 | 298 | self.mainwindow = tk.Tk() 299 | self.etv = EditableTreeview(self.mainwindow) 300 | 301 | self.etv["columns"]=("type","url") 302 | self.etv.column("type", anchor='center', width=60) 303 | self.etv.column("url") 304 | self.etv.column('#0', anchor='w', width=30) 305 | self.etv.heading('#0', text=_("Header"), anchor='w') 306 | self.etv.heading("type", text=_("Type")) 307 | self.etv.heading("url", text=_("URL")) 308 | self.etv.bind('<>', self.on_row_edit) 309 | self.etv.bind('<>', self.on_cell_changed) 310 | self.etv.pack() 311 | self.allow_edit = tk.BooleanVar() 312 | self.allow_edit.set(True) 313 | 314 | # Add some data to the treeview 315 | data = [ 316 | ('news', 'http://www.tn.com.ar'), 317 | ('games', 'https://www.gog.com'), 318 | ('search', 'https://duckduckgo.com') 319 | ] 320 | for d in data: 321 | self.etv.insert('', tk.END, values=d) 322 | 323 | def on_row_edit(self, event): 324 | # Get the column id and item id of the cell 325 | # that is going to be edited 326 | col, item = self.etv.get_event_info() 327 | 328 | # Allow edition only if allow_edit variable is checked 329 | if self.allow_edit.get() == True: 330 | # Define the widget editor to be used to edit the column value 331 | if col in ('url',): 332 | self.etv.inplace_entry(col, item) 333 | 334 | def on_cell_changed(self, event): 335 | col, item = self.etv.get_event_info() 336 | print('Column {0} of item {1} was changed'.format(col, item)) 337 | 338 | def on_row_selected(self, event): 339 | print('Rows selected', event.widget.selection()) 340 | 341 | def run(self): 342 | self.mainwindow.mainloop() 343 | 344 | app = MyApplication(None) 345 | app.run() 346 | -------------------------------------------------------------------------------- /guilog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | if sys.version_info[0] < 3: 6 | import Tkinter as tk 7 | import ttk 8 | 9 | else: 10 | import tkinter as tk 11 | from tkinter import ttk 12 | 13 | import logging as L 14 | 15 | def textarea_append(text_area, msg): 16 | # Disabling states so no user can write in it 17 | text_area.configure(state=tk.NORMAL) 18 | text_area.insert(tk.END, msg) #Inserting the logger message in the widget 19 | text_area.configure(state=tk.DISABLED) 20 | text_area.see(tk.END) 21 | #text_area.update_idletasks() 22 | 23 | # logging redirector 24 | class IODirector(object): 25 | def __init__(self, text_area): 26 | self.text_area = text_area 27 | class TextareaStream(IODirector): 28 | def write(self, msg): 29 | # Disabling states so no user can write in it 30 | textarea_append(self.text_area, msg) 31 | def flush(self): 32 | pass 33 | class TextareaLogHandler(L.StreamHandler): 34 | def __init__(self, textctrl): 35 | L.StreamHandler.__init__(self) # initialize parent 36 | self.text_area = textctrl 37 | self.text_area.tag_config("INFO", foreground="black") 38 | self.text_area.tag_config("DEBUG", foreground="grey") 39 | self.text_area.tag_config("WARNING", foreground="orange") 40 | self.text_area.tag_config("ERROR", foreground="red") 41 | self.text_area.tag_config("CRITICAL", foreground="red", underline=1) 42 | 43 | # for logging.StreamHandler 44 | def emit(self, record): 45 | textarea_append(self.text_area, self.format(record) + "\n") 46 | 47 | def set_log_stderr(): 48 | logger = L.getLogger() 49 | formatter = L.Formatter('%(asctime)s %(levelname)s: %(message)s') 50 | 51 | console0 = L.StreamHandler() # no arguments => stderr 52 | console0.setFormatter(formatter) 53 | logger.addHandler(console0) 54 | 55 | def set_log_textarea(textarea): 56 | #L.basicConfig(level=L.DEBUG) 57 | logger = L.getLogger() 58 | formatter = L.Formatter('%(asctime)s %(levelname)s: %(message)s') 59 | 60 | if 1 == 2: 61 | handler = TextareaStream(textarea) 62 | console = L.StreamHandler(handler) 63 | #console.setFormatter(formatter) 64 | logger.addHandler(console) 65 | else: 66 | console2 = TextareaLogHandler(textarea) 67 | console2.setFormatter(formatter) 68 | logger.addHandler(console2) 69 | 70 | L.info("set logger done") 71 | L.debug("debug test 1") 72 | 73 | 74 | class ToggleButton(tk.Button): 75 | # txtt: the toggled text 76 | # txtr: the release text 77 | def __init__(self, master, txtt="toggled", txtr="released", imgt=None, imgr=None, command=None, *args, **kwargs): 78 | self.master = master 79 | self.command = command 80 | self.txtt = txtt 81 | self.txtr = txtr 82 | self.imgt = imgt 83 | self.imgr = imgr 84 | tk.Button.__init__(self, master, compound="left", command=self._command, relief="raised", text=self.txtr, image=self.imgr, *args, **kwargs) 85 | 86 | #perhaps set command event to send a message 87 | #self['command'] = lambda: self.message_upstream(Message(self.name, "I Got Clicked")) 88 | 89 | #do widget declarations 90 | #self.widgets = [] 91 | 92 | def message_downstream(self, message): 93 | #for widget in self.widgets: 94 | # widget.receive_message(message) 95 | pass 96 | 97 | def message_upstream(self, message): 98 | #self.master.message_upstream(self, message) 99 | pass 100 | 101 | def config(self, mapstr=None, relief=None, *args, **kwargs): 102 | if mapstr != None: 103 | return tk.Button.config(self, mapstr, *args, **kwargs) 104 | 105 | if relief != None: 106 | if relief=='sunken': 107 | return tk.Button.config(self, relief="sunken", text=self.txtt, image=self.imgt, *args, **kwargs) 108 | else: 109 | return tk.Button.config(self, relief="raised", text=self.txtr, image=self.imgr, *args, **kwargs) 110 | else: 111 | return tk.Button.config(self, *args, **kwargs) 112 | 113 | def _command(self): 114 | if tk.Button.config(self, 'relief')[-1] == 'sunken': 115 | tk.Button.config(self, relief="raised", text=self.txtr, image=self.imgr) 116 | else: 117 | tk.Button.config(self, relief="sunken", text=self.txtt, image=self.imgt) 118 | 119 | if self.command != None: 120 | self.command() 121 | pass 122 | 123 | def test_togglebutton(): 124 | root=tk.Tk() 125 | 126 | img_ledon=tk.PhotoImage(file="ledred-on.gif") 127 | img_ledoff=tk.PhotoImage(file="ledred-off.gif") 128 | 129 | def tracebtn(): 130 | if b1.config('relief')[-1] == 'sunken': 131 | L.debug("pressed!") 132 | else: 133 | L.debug("released!") 134 | 135 | b1 = ToggleButton(root, txtt="ON", txtr="OFF", imgt=img_ledon, imgr=img_ledoff, command=tracebtn) 136 | b1.pack(pady=5) 137 | 138 | root.mainloop() 139 | 140 | 141 | 142 | def rClicker(e): 143 | ''' right click context menu for all Tk Entry and Text widgets 144 | ''' 145 | 146 | try: 147 | def rClick_Copy(e, apnd=0): 148 | e.widget.event_generate('') 149 | 150 | def rClick_Cut(e): 151 | e.widget.event_generate('') 152 | 153 | def rClick_Paste(e): 154 | e.widget.event_generate('') 155 | 156 | def rClick_SelectAll(e): 157 | e.widget.focus_force() 158 | e.widget.tag_add("sel","1.0","end") 159 | if "selection_range" in dir(e.widget): 160 | e.widget.selection_range(0, tk.END) 161 | 162 | e.widget.focus() 163 | 164 | nclst=[ 165 | (' Select All', lambda e=e: rClick_SelectAll(e)), 166 | (' Copy', lambda e=e: rClick_Copy(e)), 167 | (' Cut', lambda e=e: rClick_Cut(e)), 168 | (' Paste', lambda e=e: rClick_Paste(e)), 169 | ] 170 | 171 | rmenu = tk.Menu(None, tearoff=0, takefocus=0) 172 | 173 | for (txt, cmd) in nclst: 174 | rmenu.add_command(label=txt, command=cmd) 175 | 176 | rmenu.tk_popup(e.x_root+40, e.y_root+10,entry="0") 177 | 178 | except TclError: 179 | L.error(' - rClick menu, something wrong') 180 | pass 181 | 182 | return "break" 183 | 184 | 185 | def rClickbinder(r): 186 | 187 | try: 188 | for b in [ 'Text', 'Entry', 'Listbox', 'Label' , 'Combobox']: # 189 | r.bind_class(b, sequence='', 190 | func=rClicker, add='') 191 | except tk.TclError: 192 | L.error(' - rClickbinder, something wrong') 193 | pass 194 | 195 | 196 | def test_mouserightclick(): 197 | master = tk.Tk() 198 | ent = tk.Entry(master, width=50) 199 | ent.pack(anchor="w") 200 | 201 | #bind context menu to a specific element 202 | ent.bind('', rClicker, add='') 203 | #or bind it to any Text/Entry/Listbox/Label element 204 | #rClickbinder(master) 205 | 206 | master.mainloop() 207 | 208 | 209 | 210 | 211 | 212 | if __name__ == "__main__": 213 | #test_togglebutton() 214 | test_mouserightclick() 215 | 216 | -------------------------------------------------------------------------------- /langinit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # initialize transloation files of international languages 3 | 4 | parse_pyfile() { 5 | local PARAM_PREFIX_OUTMO=$1 6 | shift 7 | local PARAM_LANGS=$1 8 | shift 9 | local PARAM_FILES=$1 10 | shift 11 | 12 | local FN_TEMPLATE="${PARAM_PREFIX_OUTMO}.pot" 13 | 14 | if [ ! -f "${FN_TEMPLATE}" ]; then 15 | # xgettext --language=Python --keyword=_ --from-code utf-8 --output=nxvcontrol.pot nxvforward.py 16 | pygettext3 --output="${FN_TEMPLATE}" ${PARAM_FILES} 17 | else 18 | pygettext3 --output=tmp.pot ${PARAM_FILES} 19 | cat tmp.pot | egrep "msgid |msgstr |^$" | grep -v 'msgid ""\nmsgstr ""' >> "${FN_TEMPLATE}" 20 | rm -f tmp.pot 21 | fi 22 | 23 | mkdir -p translations/ 24 | for i in ${PARAM_LANGS}; do 25 | if [ ! -f "translations/${i}.po" ]; then 26 | msginit --input="${FN_TEMPLATE}" --locale=${i} --output-file=translations/${i}.po 27 | fi 28 | msgmerge --update translations/${i}.po "${FN_TEMPLATE}" 29 | 30 | mkdir -p languages/${i}/LC_MESSAGES/ 31 | msgfmt --output-file="languages/${i}/LC_MESSAGES/${PARAM_PREFIX_OUTMO}.mo" "translations/${i}.po" 32 | done 33 | } 34 | 35 | rm -f "nxvcontrol.pot" 36 | parse_pyfile "nxvcontrol" "en_US zh_CN" "nxvforward.py nxvcontrol.py" 37 | 38 | -------------------------------------------------------------------------------- /ledred-off.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/ledred-off.gif -------------------------------------------------------------------------------- /ledred-on.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yhfudev/python-nxvcontrol/4160f8ededb5d3193af1d242e6e6cbcb38213d60/ledred-on.gif -------------------------------------------------------------------------------- /neatocmdapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf8 3 | # 4 | # Copyright 2016-2017 Yunhui Fu 5 | # 6 | # This program is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License version 3, as published 8 | # by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranties of 12 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 13 | # PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program. If not, see . 17 | # 18 | # For further info, check https://github.com/yhfudev/python-nxvcontrol.git 19 | 20 | import time 21 | import logging as L 22 | #L.basicConfig(filename='neatocmdapi.log', level=L.DEBUG, format='%(asctime)s %(levelname)s: %(message)s') 23 | 24 | class NeatoCommandInterface(object): 25 | "Neato Command Interface abstraction interface" 26 | def open(self): 27 | pass 28 | def close(self): 29 | pass 30 | def ready(self): 31 | return False 32 | def flush(self): 33 | pass 34 | def put(self, line="Help"): 35 | return "" 36 | def get(self): 37 | return "" 38 | 39 | class NCISimulator(NeatoCommandInterface): 40 | "Neato Command Interface for simulation" 41 | lastcmd="" 42 | def open(self): 43 | pass 44 | def close(self): 45 | pass 46 | def ready(self): 47 | return True 48 | def flush(self): 49 | pass 50 | def put(self, lines): 51 | self.lastcmd += lines.strip() + "\n" 52 | def get(self): 53 | import neatocmdsim as nsim 54 | cmdstr = self.lastcmd.strip() + "\n" 55 | self.lastcmd = "" 56 | requests = cmdstr.split('\n') 57 | response = "" 58 | for i in range(0,len(requests)): 59 | retline = nsim.fake_respose(requests[i].strip()) 60 | retline.strip() + "\n" 61 | retline = retline.replace('\x1A', '\n') 62 | retline = retline.replace('\r\n', '\n') 63 | retline = retline.replace('\n\n', '\n') 64 | response += retline 65 | 66 | return response 67 | 68 | import serial # sudo apt-get install python3-serial 69 | class NCISerial(NeatoCommandInterface): 70 | "Neato Command Interface for serial ports" 71 | 72 | def __init__(self, port="/dev/ttyACM0", baudrate=115200, timeout=0.5): 73 | self.ser = serial.Serial() 74 | self.ser.port = port 75 | self.ser.baudrate = baudrate 76 | self.ser.bytesize = serial.EIGHTBITS 77 | self.ser.parity = serial.PARITY_NONE 78 | self.ser.stopbits = serial.STOPBITS_ONE 79 | self.ser.timeout = timeout 80 | #self.ser.xonxoff= False 81 | #self.ser.rtscts = False 82 | #self.ser.dsrdtr = False 83 | self.ser.write_timeout = 2 84 | self.isready = False 85 | 86 | def open(self): 87 | try: 88 | self.ser.open() 89 | self.isready = True 90 | except Exception as e: 91 | L.error("[NCISerial] Error open serial port: " + str(e)) 92 | self.isready = False 93 | return False 94 | return True 95 | 96 | def close(self): 97 | self.isready = False 98 | self.ser.close() 99 | 100 | def ready(self): 101 | if self.isready: 102 | return self.ser.isOpen() 103 | return False 104 | 105 | def flush(self): 106 | if self.ready(): 107 | self.ser.flushInput() 108 | self.ser.flushOutput() 109 | self.ser.flush() 110 | 111 | def put(self, line): 112 | if self.ready() == False: 113 | return "" 114 | sendcmd = line.strip() + "\n" 115 | self.ser.write(sendcmd.encode('ASCII')) 116 | 117 | def get(self): 118 | if self.ready() == False: 119 | return "" 120 | retval = "" 121 | while True: 122 | try: 123 | #L.debug('[NCISerial] readline ...') 124 | response = self.ser.readline() 125 | except TimeoutError: 126 | L.debug('[NCISerial] timeout read') 127 | break 128 | if len(response) < 1: 129 | L.debug('[NCISerial] read null') 130 | break 131 | response = response.decode('ASCII') 132 | #L.debug('[NCISerial] received: ' + response) 133 | #L.debug('read size=' + len(response) ) 134 | if len(response) < 1: 135 | L.debug('[NCISerial] read null 2') 136 | break 137 | response = response.strip() 138 | if len(response) == 1: 139 | if response[0] == '\x1A': 140 | break 141 | response = response.replace('\x1A', '\n') 142 | response = response.replace('\r\n', '\n') 143 | #response = response.replace('\n\n', '\n') 144 | retval += response + "\n" 145 | retval = retval.replace('\n\n', '\n') 146 | return retval.strip() + "\n\n" 147 | 148 | import select 149 | import socket 150 | class NCINetwork(NeatoCommandInterface): 151 | "Neato Command Interface for TCP pipe" 152 | def __init__(self, address="localhost", port=3333, timeout=2): 153 | self.address = address 154 | self.port = port 155 | self.timeout = timeout 156 | self.isready = False 157 | 158 | def open(self): 159 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 160 | self.sock.settimeout(15) 161 | try: 162 | self.sock.connect((self.address, self.port)) 163 | self.isready = True 164 | except socket.timeout: 165 | self.sock = None 166 | self.isready = False 167 | return False; 168 | self.sock.settimeout(None) 169 | L.debug ('[NCINetwork] Client has been assigned socket name' + str(self.sock.getsockname())) 170 | self.sock.setblocking(True) 171 | self.sock.settimeout(self.timeout) 172 | self.data = "" 173 | return True 174 | 175 | def close(self): 176 | self.isready = False 177 | self.sock.close() 178 | 179 | def ready(self): 180 | return self.isready 181 | 182 | def flush(self): 183 | #self.sock.flush() 184 | pass 185 | 186 | def put(self, line): 187 | if self.sock == None: 188 | raise ConnectionResetError; 189 | cli_log_head = "[NCINetwork] put() NOCONN " 190 | #if self.sock: 191 | # cli_log_head = "[NCINetwork] put() " + str(self.sock.getpeername()) + " " 192 | 193 | sendcmd = line.strip() + "\n" 194 | L.debug (cli_log_head + 'sendcmd: ' + sendcmd) 195 | 196 | ready_to_read = [] 197 | ready_to_write = [] 198 | while True: 199 | try: 200 | L.debug(cli_log_head + "select() ...") 201 | ready_to_read, ready_to_write, in_error = select.select([self.sock,], [self.sock,], [self.sock,], 5) 202 | #L.debug(cli_log_head + "put select() return read=" + str(ready_to_read)) 203 | #L.debug(cli_log_head + "put select() return write=" + str(ready_to_write)) 204 | #L.debug(cli_log_head + "put select() return err=" + str(in_error)) 205 | 206 | except select.error: 207 | self.sock.shutdown(2) # 0 = done receiving, 1 = done sending, 2 = both 208 | self.sock.close() 209 | self.sock = None 210 | # connection error event here, maybe reconnect 211 | print ('connection error') 212 | raise ConnectionResetError 213 | break 214 | if len(ready_to_write) > 0: 215 | break 216 | 217 | if len(ready_to_write) > 0: 218 | try: 219 | self.sock.sendall (bytes(sendcmd, 'ascii')) 220 | #self.sock.flush() 221 | except Exception: 222 | raise ConnectionResetError 223 | 224 | return "" 225 | 226 | def get(self): 227 | 228 | BUFFER_SIZE = 4096 229 | #MAXIUM_SIZE = BUFFER_SIZE * 5 230 | 231 | if self.sock == None: 232 | raise ConnectionResetError 233 | 234 | cli_log_head = "[NCINetwork] get() NOCONN " 235 | #if self.sock: 236 | # cli_log_head = "[NCINetwork] get() " + str(self.sock.getpeername()) + " " 237 | 238 | while True: 239 | 240 | try: 241 | L.debug(cli_log_head + 'recv()...') 242 | recvdat = self.sock.recv(BUFFER_SIZE) 243 | except socket.timeout: 244 | L.debug(cli_log_head + 'timeout read') 245 | if len(self.data) > 0: 246 | break 247 | continue 248 | except (socket.error, socket.gaierror, ConnectionResetError) as e: 249 | L.debug(cli_log_head + 'cannot deliver remote keyfiles: {}'.format(e), file=sys.stderr) 250 | break 251 | except Exception: 252 | raise ConnectionResetError 253 | 254 | if not recvdat: 255 | # EOF, client closed, just return 256 | L.info(cli_log_head + "disconnected, datalen=" + str(len(self.data))) 257 | #self.data += "\n\n" 258 | if len(self.data) <= 0: 259 | raise ConnectionResetError 260 | else: 261 | #L.debug(cli_log_head + "recv size=" + str(len(recvdat))) 262 | recvstr = str(recvdat, 'ascii') 263 | recvstr = recvstr.replace('\x1A', '\n') 264 | recvstr = recvstr.replace('\r\n', '\n') 265 | self.data += recvstr 266 | 267 | # TODO: clean the lines, remove blanks for each line from the begin and end 268 | # ... 269 | if self.data.find("\n\n") >= 0: 270 | break 271 | 272 | pos = self.data.find("\n\n") 273 | if pos < 0: 274 | retstr = self.data.strip() 275 | self.data = "" 276 | if len(retstr) > 0: 277 | return retstr + "\n\n" 278 | return "" 279 | else: 280 | # end mode: 281 | self.data = self.data.lstrip() 282 | pos = self.data.find("\n\n") 283 | if pos < 0: 284 | # this means the "\n\n" is at left(and be removed) 285 | L.debug(cli_log_head + 'tcp return null') 286 | return "\n\n" 287 | else: 288 | retstr = self.data[0:pos] 289 | self.data = self.data[pos+2:len(self.data)] 290 | L.debug(cli_log_head + 'tcp return ' + retstr) 291 | return retstr + "\n\n" 292 | 293 | 294 | from multiprocessing import Queue, Lock 295 | import threading 296 | import heapq 297 | from datetime import datetime 298 | from itertools import count 299 | 300 | class MyHeap(object): 301 | def __init__(self, initial=None, key=lambda x:x): 302 | self._counter = count() 303 | self.key = key 304 | if initial: 305 | self._data = [(key(item), next(self._counter), item) for item in initial] 306 | heapq.heapify(self._data) 307 | else: 308 | self._data = [] 309 | 310 | def size(self): 311 | return len(self._data) 312 | 313 | def push(self, item): 314 | heapq.heappush(self._data, (self.key(item), next(self._counter), item)) 315 | 316 | def pop(self): 317 | return heapq.heappop(self._data)[-1] 318 | 319 | # MailPipe 320 | # each pipe has its address 321 | # the message in the pipe is in Queue 322 | class MailPipe(object): 323 | def __init__(self): 324 | self._counter = count() 325 | self._idx = {} 326 | 327 | # declair a new mail pipe, return the handler 328 | def declair(self): 329 | mid = next(self._counter) 330 | self._idx[mid] = Queue() 331 | return mid 332 | 333 | # close a mail pipe; mid the id of the mail pipe 334 | def close(self, mid): 335 | self._idx.pop(mid) 336 | pass 337 | 338 | # get the # of mail pipe 339 | def size(self, mid): 340 | return len(self._idx) 341 | 342 | # get the # of messages of a specified mail pipe 343 | def count(self, mid): 344 | if mid in self._idx: 345 | return self._idx[mid].qsize() 346 | return -1 347 | 348 | # get a message from specified mail pipe 349 | def get(self, mid, isblock=True): 350 | if mid in self._idx: 351 | return self._idx[mid].get(isblock) 352 | return None 353 | 354 | # add a message to a specified mail pipe 355 | def put(self, mid, msg, isblock=True): 356 | if mid in self._idx: 357 | self._idx[mid].put(msg, isblock) 358 | return True 359 | return False 360 | 361 | # internal class 362 | class AtomTask(object): 363 | PRIORITY_DEFAULT = 5 364 | PRIORITY_MAX=255 365 | 366 | def __init__(self, req=None, newid=0, priority=None): 367 | self.req = req # user define 368 | self.tid = newid 369 | self.priority = priority 370 | self.execute_time = None 371 | self.request_time = None 372 | self.start_time = None 373 | self.finish_time = None 374 | self.is_run = False 375 | 376 | def setPriority(self, pri): 377 | self.priority = pri 378 | 379 | def setRequestTime(self, etime): 380 | self.request_time = etime 381 | 382 | def setExecuteTime(self, etime): 383 | self.execute_time = etime 384 | 385 | def setStartTime(self, etime): 386 | self.start_time = etime 387 | 388 | def setFinishTime(self, etime): 389 | self.finish_time = etime 390 | 391 | # each task will executed until finished 392 | # supports priority, 0 -- critial for important task, 1-n -- normal priority tasks 393 | # supports execute at a exact time. 394 | class AtomTaskScheduler(object): 395 | 396 | # use two queues to accept and cache the requests, 397 | # queue_priority is for tasks with priority 398 | # queue_time is for tasks with exact time 399 | # use two internal heap/priorityQueue to manage the task to decide which task should run next 400 | # heap_priority sort and store the task will be run 401 | # heap_time sort and store the tasks of time bound 402 | # and a main function/loop to move the requests from queue_xxx to heap_xxx, and run the task from heap_priority 403 | # 404 | # loop: 405 | # move tasks from queue_priority to heap_priority 406 | # move tasks from queue_time to heap_time 407 | # if heap_time has expired tasks need to execute, move the tasks(priority=1) to heap_priority 408 | # if heap_priority has task, do the task, remove it from heap_priority, return results(id, request time, begin time, finish time, response) 409 | # if has do task, goto loop 410 | # wait_time=1 411 | # if has task in heap_time, wait_time = wait time for the top task 412 | # cond_wait(wait_time) 413 | def __init__(self, cb_task=None): 414 | self.cb_task = cb_task 415 | self.queue_priority = Queue() 416 | self.queue_time = Queue() 417 | self.heap_priority = MyHeap(key=lambda x:x.priority); 418 | self.heap_time = MyHeap(key=lambda x:x.execute_time); 419 | 420 | self._counter = count() 421 | self.idlock = Lock() 422 | self.apilock = Lock() 423 | self.apicond = threading.Condition(self.apilock) 424 | 425 | # create a new request id 426 | def getNewId(self): 427 | self.idlock.acquire() 428 | try: 429 | return next(self._counter) 430 | finally: 431 | self.idlock.release() 432 | 433 | # request for a task, with the priority 434 | def request(self, req, priority): 435 | newid = self.getNewId() 436 | newreq = AtomTask(req=req, newid=newid, priority=priority) 437 | newreq.setRequestTime(datetime.now()) 438 | self.queue_priority.put(newreq) 439 | with self.apicond: 440 | self.apicond.notifyAll() 441 | return newid 442 | 443 | # request for a task, with the exact time 444 | def request_time (self, req, exacttime): 445 | newid = self.getNewId() 446 | newreq = AtomTask(req=req, newid=newid) 447 | newreq.setRequestTime(datetime.now()) 448 | newreq.setExecuteTime(exacttime) 449 | self.queue_time.put(newreq) 450 | with self.apicond: 451 | self.apicond.notifyAll() 452 | return newid 453 | 454 | def do_work_once (self): 455 | # move tasks from queue_priority to heap_priority 456 | while not self.queue_priority.empty(): 457 | newreq = None 458 | try: 459 | newreq = self.queue_priority.get() 460 | L.debug("get req from queue_priority: " + str(newreq)) 461 | except Queue.Empty: 462 | break; 463 | self.heap_priority.push (newreq) 464 | # move tasks from queue_time to heap_time 465 | while not self.queue_time.empty(): 466 | newreq = None 467 | try: 468 | newreq = self.queue_time.get() 469 | except Queue.Empty: 470 | break; 471 | self.heap_time.push (newreq) 472 | # if heap_time has expired tasks need to execute, move the tasks(priority=1) to heap_priority 473 | wait_time=0.5 # seconds, wait time for cond 474 | while (self.heap_time.size() > 0): 475 | newreq = self.heap_time.pop() 476 | tmnow = datetime.now() 477 | if newreq.execute_time <= tmnow: 478 | newreq.setPriority(1) 479 | self.heap_priority.push(newreq) 480 | else: 481 | # push back the item 482 | delta = newreq.execute_time - tmnow 483 | wait_time = delta.days * 86400 + delta.seconds + delta.microseconds/1000000 484 | self.heap_time.push(newreq) 485 | # if heap_priority has task, do the task, remove it from heap_priority, return results(id, request time, begin time, finish time, response) 486 | if (self.heap_priority.size() > 0): 487 | newreq = self.heap_priority.pop() 488 | newreq.setStartTime(datetime.now()) 489 | self.cb_task (newreq.tid, newreq.req) # do the job 490 | newreq.setFinishTime(datetime.now()) 491 | #return True # if has done task, goto loop 492 | if (self.heap_priority.size() > 0): 493 | wait_time = 0 494 | 495 | return wait_time 496 | 497 | def do_wait_queue(self, wait_time): 498 | # if has task in heap_time, wait_time = wait time for the top task 499 | try: 500 | #L.debug("waiting queues for " + str(wait_time) + " seconds ...") 501 | with self.apicond: 502 | self.apicond.wait(wait_time) 503 | #L.debug("endof wait!") 504 | except RuntimeError: 505 | #L.debug("wait timeout!") 506 | #time.sleep(0) # Effectively yield this thread. 507 | pass 508 | 509 | def stop(self): 510 | self.is_run = False 511 | 512 | def serve_forever (self): 513 | self.is_run = True 514 | while self.is_run: 515 | wait_time = self.do_work_once() 516 | if self.is_run and wait_time > 0: 517 | self.do_wait_queue(wait_time) 518 | 519 | #from urlparse import urlparse 520 | from urllib.parse import urlparse 521 | import neatocmdapi 522 | 523 | # the service thread, 524 | # 525 | class NCIService(object): 526 | "Neato Command Interface all" 527 | 528 | def ready(self): 529 | return self.isready 530 | 531 | # target: example: tcp://localhost:3333 sim:// 532 | def __init__(self, target="tcp://localhost:3333", timeout=2): 533 | "target accepts: 'tcp://localhost:3333', 'dev://ttyUSB0:115200', 'dev://COM12:115200', 'sim:' " 534 | self.api = None 535 | self.th_sche = None 536 | self.sche = None 537 | self.isready = False 538 | 539 | result = urlparse(target) 540 | if result.scheme == "tcp": 541 | addr = result.netloc.split(':') 542 | port = 3333 543 | if len(addr) > 1: 544 | port = int(addr[1]) 545 | self.api = neatocmdapi.NCINetwork(timeout = timeout, port = port, address=addr[0]) 546 | 547 | elif result.scheme == "sim": 548 | self.api = neatocmdapi.NCISimulator() 549 | 550 | else: 551 | addr = result.netloc.split(':') 552 | baudrate = 115200 553 | if len(addr) > 1: 554 | baudrate = int(addr[1]) 555 | port = addr[0] 556 | import re 557 | if re.match('tty.*', port): 558 | port = "/dev/" + addr[0] 559 | L.debug('serial open: ' + port + ", " + str(baudrate)) 560 | self.api = neatocmdapi.NCISerial(timeout = timeout, port = port, baudrate = baudrate) 561 | 562 | # block read and get 563 | def get_request_block(self, req): 564 | self.api.put(req) 565 | return self.api.get() 566 | 567 | #def cb_task1(self, tid, req): 568 | # L.debug("do task: tid=" + str(tid) + ", req=" + str(req)) 569 | # resp = self.get_request_block(req) 570 | 571 | def open(self, cb_task1): 572 | try: 573 | L.debug('api.open() ...') 574 | self.api.open() 575 | time.sleep(0.5) 576 | cnt=1 577 | while self.api.ready() == False and cnt < 2: 578 | time.sleep(1) 579 | cnt += 1 580 | if self.api.ready() == False: 581 | self.api = None 582 | return False 583 | self.api.flush() 584 | 585 | # creat a thread to run the task in background 586 | self.sche = AtomTaskScheduler(cb_task=cb_task1) 587 | 588 | self.th_sche = threading.Thread(target=self.sche.serve_forever) 589 | self.th_sche.setDaemon(True) 590 | self.th_sche.start() 591 | 592 | except Exception as e1: 593 | L.error ('Error in read serial: ' + str(e1)) 594 | return False 595 | 596 | self.isready = True 597 | return True 598 | 599 | def close(self): 600 | isrun = False; 601 | if self.th_sche != None: 602 | if self.th_sche.isAlive(): 603 | if self.sche != None: 604 | self.sche.stop() 605 | isrun = True 606 | if isrun: 607 | while self.th_sche.isAlive(): 608 | time.sleep(1) 609 | if self.api != None: 610 | self.api.close() 611 | self.api = None 612 | self.sche = None 613 | self.th_sche = None 614 | self.isready = False 615 | 616 | def request(self, req): 617 | if self.ready() and self.sche != None: 618 | return self.sche.request(req, 5) 619 | return -1 620 | 621 | def request_time (self, req, exacttime): 622 | if self.ready() and self.sche != None: 623 | return self.sche.request_time(req, exacttime) 624 | return -1 625 | 626 | 627 | def test_nci_service(): 628 | a = NCIService() 629 | 630 | 631 | 632 | def test_heap_priority(): 633 | hp = MyHeap() 634 | hp.push (5) 635 | hp.push (3) 636 | hp.push (2) 637 | hp.push (0) 638 | hp.push (6) 639 | hp.push (4) 640 | hp.push (1) 641 | tmpre = hp.pop() 642 | while hp.size() > 0: 643 | tm = hp.pop() 644 | assert (tm > tmpre); 645 | tmpre = tm 646 | 647 | def test_heap_time(): 648 | hp = MyHeap() 649 | tm = datetime.now() 650 | hp.push(tm) 651 | time.sleep(0.01) 652 | tm = datetime.now() 653 | hp.push(tm) 654 | time.sleep(0.31) 655 | tm = datetime.now() 656 | hp.push(tm) 657 | tmpre = hp.pop() 658 | while hp.size() > 0: 659 | tm = hp.pop() 660 | assert (tm > tmpre); 661 | tmpre = tm 662 | 663 | def test_heap_atomtask_priority(): 664 | hp = MyHeap(key=lambda x:x.priority); 665 | newreq = AtomTask(req="req4", priority=4) 666 | hp.push (newreq) 667 | newreq = AtomTask(req="req2", priority=2) 668 | hp.push (newreq) 669 | newreq = AtomTask(req="req1", priority=1) 670 | hp.push (newreq) 671 | newreq = AtomTask(req="req0", priority=0) 672 | hp.push (newreq) 673 | newreq = AtomTask(req="req5", priority=5) 674 | hp.push (newreq) 675 | newreq = AtomTask(req="req3", priority=3) 676 | hp.push (newreq) 677 | tmpre = hp.pop() 678 | while hp.size() > 0: 679 | tm = hp.pop() 680 | assert (tm.priority > tmpre.priority); 681 | tmpre = tm 682 | 683 | def test_heap_atomtask_time(): 684 | hp = MyHeap(key=lambda x:x.execute_time); 685 | newreq = AtomTask(req="req1") 686 | newreq.setExecuteTime(datetime.now()) 687 | hp.push(newreq) 688 | time.sleep(0.01) 689 | newreq = AtomTask(req="req2") 690 | newreq.setExecuteTime(datetime.now()) 691 | hp.push(newreq) 692 | time.sleep(0.31) 693 | tm = datetime.now() 694 | newreq = AtomTask(req="req3") 695 | newreq.setExecuteTime(datetime.now()) 696 | hp.push(newreq) 697 | 698 | tmpre = hp.pop() 699 | while hp.size() > 0: 700 | tm = hp.pop() 701 | assert (tm.execute_time > tmpre.execute_time); 702 | tmpre = tm 703 | 704 | 705 | def test_heap_atomtask_priority_class(): 706 | class TestContainer(object): 707 | def __init__(self): 708 | self.hp = MyHeap(key=lambda x:x.priority); 709 | def selftest(self): 710 | newreq = AtomTask(req="req4", priority=4) 711 | self.hp.push (newreq) 712 | newreq = AtomTask(req="req2", priority=2) 713 | self.hp.push (newreq) 714 | newreq = AtomTask(req="req1", priority=1) 715 | self.hp.push (newreq) 716 | newreq = AtomTask(req="req0", priority=0) 717 | self.hp.push (newreq) 718 | newreq = AtomTask(req="req5", priority=5) 719 | self.hp.push (newreq) 720 | newreq = AtomTask(req="req3", priority=3) 721 | self.hp.push (newreq) 722 | tmpre = self.hp.pop() 723 | while self.hp.size() > 0: 724 | tm = self.hp.pop() 725 | assert (tm.priority >= tmpre.priority); 726 | assert (tm.priority >= tmpre.priority); 727 | tmpre = tm 728 | tc = TestContainer() 729 | tc.selftest() 730 | 731 | # test the AtomTaskScheduler in a function 732 | def test_atomtask(): 733 | def cb_task1(tid, req): 734 | L.debug("(infunc) do task: tid=" + str(tid) + ", req=" + str(req)) 735 | #def cb_signal1(tid, request_time, start_time, finish_time, resp): 736 | # L.debug("(infunc) signal: tid=" + str(tid) + ", reqtime=" + str(request_time) + ", starttime=" + str(start_time) + ", fintime=" + str(finish_time) + ", resp=" + str(resp) ) 737 | 738 | def th_setup(sche): 739 | sche.request("init work 2-1", 2) 740 | sche.request("init work 2-3", 2) 741 | sche.request("init work 1-2", 1) 742 | sche.request("init work 2-2", 2) 743 | sche.request("init work 0-1", 0) 744 | sche.request("init work 1-1", 1) 745 | sche.request("init work 2-4", 2) 746 | sche.request("init work 0-2", 0) 747 | time.sleep(5) 748 | sche.request("normal work 5-1", 5) 749 | sche.request("normal work 5-2", 5) 750 | sche.request("normal work 5-3", 5) 751 | sche.request("normal work 5-4", 5) 752 | L.debug("sleep 5") 753 | time.sleep(5.31) 754 | L.debug("sche stop") 755 | sche.stop() 756 | 757 | sche = AtomTaskScheduler(cb_task=cb_task1) 758 | 759 | if 1 == 1: 760 | runT = threading.Thread(target=sche.serve_forever) 761 | runT.setDaemon(True) 762 | runT.start() 763 | th_setup(sche) 764 | else: 765 | runT = threading.Thread(target=th_setup, args=(sche,)) 766 | runT.setDaemon(True) 767 | runT.start() 768 | sche.serve_forever() 769 | 770 | # test the AtomTaskScheduler in a class 771 | class TestAtomtask(object): 772 | def cb_task1(self, tid, req): 773 | L.debug("(inclas) do task: tid=" + str(tid) + ", req=" + str(req)) 774 | #def cb_signal1(self, tid, request_time, start_time, finish_time, resp): 775 | # L.debug("(inclas) signal: tid=" + str(tid) + ", reqtime=" + str(request_time) + ", starttime=" + str(start_time) + ", fintime=" + str(finish_time) + ", resp=" + str(resp) ) 776 | 777 | def th_setup(self, sche): 778 | sche.request("init work 2-1", 2) 779 | sche.request("init work 2-3", 2) 780 | sche.request("init work 1-2", 1) 781 | sche.request("init work 2-2", 2) 782 | sche.request("init work 0-1", 0) 783 | sche.request("init work 1-1", 1) 784 | sche.request("init work 2-4", 2) 785 | sche.request("init work 0-2", 0) 786 | time.sleep(5) 787 | sche.request("normal work 5-1", 5) 788 | sche.request("normal work 5-2", 5) 789 | sche.request("normal work 5-3", 5) 790 | sche.request("normal work 5-4", 5) 791 | L.debug("sleep 5") 792 | time.sleep(5.31) 793 | L.debug("sche stop") 794 | sche.stop() 795 | 796 | def run_test(self): 797 | self.sche = AtomTaskScheduler(cb_task=self.cb_task1) 798 | 799 | if 1 == 0: 800 | runT = threading.Thread(target=self.sche.serve_forever) 801 | runT.setDaemon(True) 802 | runT.start() 803 | self.th_setup(self.sche) 804 | else: 805 | runT = threading.Thread(target=self.th_setup, args=(self.sche,)) 806 | runT.setDaemon(True) 807 | runT.start() 808 | self.sche.serve_forever() 809 | def test_atomtask_class(): 810 | run1 = TestAtomtask() 811 | run1.run_test() 812 | 813 | def testme(): 814 | #test_heap_time() 815 | #test_heap_priority() 816 | #test_heap_atomtask_priority() 817 | #test_heap_atomtask_time() 818 | #test_heap_atomtask_priority_class() 819 | #test_atomtask() 820 | test_atomtask_class() 821 | #test_nci_service() 822 | 823 | 824 | if __name__ == "__main__": 825 | testme() 826 | 827 | 828 | 829 | -------------------------------------------------------------------------------- /nxvcontrol.bat: -------------------------------------------------------------------------------- 1 | 2 | rem set LANG=en_US 3 | rem set LANG=zh_CN 4 | 5 | python nxvcontrol.py 6 | -------------------------------------------------------------------------------- /nxvcontrol.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf8 3 | # 4 | # Copyright 2016-2017 Yunhui Fu 5 | # 6 | # This program is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License version 3, as published 8 | # by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranties of 12 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 13 | # PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program. If not, see . 17 | # 18 | # For further info, check https://github.com/yhfudev/python-nxvcontrol.git 19 | 20 | try: 21 | import Tkinter as tk 22 | import ttk 23 | import ScrolledText 24 | import tkFileDialog as fd 25 | except ImportError: 26 | import tkinter as tk 27 | from tkinter import ttk 28 | from tkinter.scrolledtext import ScrolledText 29 | import tkinter.filedialog as fd 30 | 31 | import os 32 | import sys 33 | 34 | import queue 35 | 36 | # print("arg[0]: " + sys.argv[0]) 37 | # print("program name: " + os.path.basename(__file__)) 38 | #import __main__ as main0 39 | # print("main: " + main0.__file__) 40 | PROGRAM_PREFIX = os.path.basename(__file__).split('.')[0] 41 | 42 | import logging as L 43 | L.basicConfig(filename=PROGRAM_PREFIX+'.log', level=L.DEBUG, format='%(asctime)s %(levelname)s: %(message)s') 44 | 45 | import neatocmdapi 46 | import guilog 47 | 48 | import locale 49 | import gettext 50 | _=gettext.gettext 51 | 52 | APP_NAME="nxvcontrol" 53 | def gettext_init(): 54 | global _ 55 | langs = [] 56 | 57 | language = os.environ.get('LANG', None) 58 | if (language): 59 | langs += language.split(":") 60 | language = os.environ.get('LANGUAGE', None) 61 | if (language): 62 | langs += language.split(":") 63 | lc, encoding = locale.getdefaultlocale() 64 | if (lc): 65 | langs += [lc] 66 | # we know that we have 67 | langs += ["en_US", "zh_CN"] 68 | local_path = os.path.realpath(os.path.dirname(sys.argv[0])) 69 | local_path = "languages/" 70 | gettext.bindtextdomain(APP_NAME, local_path) 71 | gettext.textdomain(APP_NAME) 72 | lang = gettext.translation(APP_NAME, local_path, languages=langs, fallback = True) 73 | #_=gettext.gettext 74 | _=lang.gettext 75 | L.debug("local=" + str(lc) + ", encoding=" + str(encoding) + ", langs=" + str(langs) + ", lang=" + str(lang) ) 76 | 77 | str_progname="nxvControl" 78 | str_version="0.1" 79 | 80 | LARGE_FONT= ("Verdana", 18) 81 | NORM_FONT = ("Helvetica", 12) 82 | SMALL_FONT = ("Helvetica", 8) 83 | 84 | # key for state machine wheel 85 | KEY_NONE=0 86 | KEY_LEFT=1 87 | KEY_RIGHT=2 88 | KEY_UP=3 89 | KEY_DOWN=4 90 | KEY_BACK=5 91 | # the states 92 | STATE_STOP=1 93 | STATE_FORWARD=2 94 | STATE_BACK=3 95 | STATE_LEFT=4 96 | STATE_RIGHT=5 97 | 98 | # TODO: 99 | # the background thread to handle the communication with the neato 100 | # a request scheduler is used because of the serial port 101 | # the scheduler supports the time tagged request which is executed at the exact time 102 | # the scheduler supports the the requst which has no required time, it will executed at the idle time 103 | # a request contains the exact time tag, repeat time, user data, callback? 104 | # return the result, execute start time, execute end time, 105 | 106 | 107 | 108 | def test_pack(main_container): 109 | main_container.pack(side="top", fill="both", expand=True) 110 | top_frame = tk.Frame(main_container, background="green") 111 | bottom_frame = tk.Frame(main_container, background="yellow") 112 | top_frame.pack(side="top", fill="x", expand=False) 113 | bottom_frame.pack(side="bottom", fill="both", expand=True) 114 | 115 | top_left = tk.Frame(top_frame, background="pink") 116 | top_center = tk.Frame(top_frame, background="red") 117 | top_right = tk.Frame(top_frame, background="blue") 118 | top_left.pack(side="left", fill="x", expand=True) 119 | top_center.pack(side="left", fill="x", expand=True) 120 | top_right.pack(side="right", fill="x", expand=True) 121 | 122 | top_left_label = tk.Label(top_left, text="Top Left") 123 | top_center_label = tk.Label(top_center, text="Top Center") 124 | top_right_label = tk.Label(top_right, text="Top Right") 125 | top_left_label.pack(side="left") 126 | top_center_label.pack(side="top") 127 | top_right_label.pack(side="right") 128 | 129 | text_box = tk.Text(bottom_frame, height=5, width=40, background="gray") 130 | text_box.pack(side="top", fill="both", expand=True) 131 | 132 | 133 | def test_grid(myParent, main_container): 134 | main_container.grid(row=0, column=0, sticky="nsew") 135 | myParent.grid_rowconfigure(0, weight=1) 136 | myParent.grid_columnconfigure(0, weight=1) 137 | top_frame = tk.Frame(main_container, background="green") 138 | bottom_frame = tk.Frame(main_container, background="yellow") 139 | top_frame.grid(row=0, column=0, sticky="ew") 140 | bottom_frame.grid(row=1, column=0,sticky="nsew") 141 | main_container.grid_rowconfigure(1, weight=1) 142 | main_container.grid_columnconfigure(0, weight=1) 143 | top_left = tk.Frame(top_frame, background="pink") 144 | top_center = tk.Frame(top_frame, background="red") 145 | top_right = tk.Frame(top_frame, background="blue") 146 | top_left.grid(row=0, column=0, sticky="w") 147 | top_center.grid(row=0, column=1) 148 | top_right.grid(row=0, column=2, sticky="e") 149 | top_frame.grid_columnconfigure(1, weight=1) 150 | top_left_label = tk.Label(top_left, text="Top Left") 151 | top_center_label = tk.Label(top_center, text="Top Center") 152 | top_right_label = tk.Label(top_right, text="Top Right") 153 | top_left_label.grid(row=0, column=0, sticky="w") 154 | top_center_label.grid(row=0, column=0) 155 | top_right_label.grid(row=0, column=0, sticky="e") 156 | 157 | def set_readonly_text(text, msg): 158 | text.config(state=tk.NORMAL) 159 | text.delete(1.0, tk.END) 160 | text.insert(tk.END, msg) 161 | text.config(state=tk.DISABLED) 162 | 163 | import math 164 | # const 165 | MAXDIST=16700 # the maxmium allowed of the distance value of lidar 166 | MAXDIST=4000 # the maxmium allowed of the distance value of lidar 167 | CONST_RAD=math.pi / 180 168 | 169 | import editabletreeview as et 170 | 171 | class ScheduleTreeview(et.EditableTreeview): 172 | 173 | def __init__(self, parent): 174 | et.EditableTreeview.__init__(self, parent) 175 | 176 | def initUI(self): 177 | tree = self 178 | tree["columns"]=("time") 179 | tree.column('#0', anchor='w', width=30) 180 | tree.heading('#0', text=_("Day of Week"), anchor='w') 181 | tree.heading("time", text=_("Time")) 182 | tree.bind('<>', self.on_row_edit) 183 | tree.bind('<>', self.on_cell_changed) 184 | self.updateSchedule("") 185 | tree.tag_configure('schedis', background='grey') 186 | tree.tag_configure('scheon', background='white') 187 | tree.tag_configure('scheoff', background='grey') 188 | pass 189 | 190 | def on_row_edit(self, event): 191 | tree = self 192 | col, item = tree.get_event_info() 193 | 194 | if col in ("time",): 195 | #tree.inplace_entry(col, item) 196 | tree.inplace_combobox(col, item, ("00:00 - None -", "00:00 H"), readonly=False) 197 | 198 | def on_cell_changed(self, event): 199 | tree = self 200 | col, item = tree.get_event_info() 201 | #L.debug('Column {0} of item {1} was changed={2}'.format(col, item, tree.item(item)["values"])) 202 | if col in ("time",): 203 | self.changed[item] = tree.item(item)["text"] 204 | tag = self.val2tag(tree.item(item)["values"][0]) 205 | tree.item(item, tags = (tag,) ) 206 | 207 | def on_row_selected(self, event): 208 | #print('Rows selected', event.widget.selection()) 209 | pass 210 | 211 | # convert the value to tag 212 | def val2tag(self, val): 213 | columnlst = val.split(' ') 214 | tag = "schedis" 215 | if columnlst[1] == "H": 216 | tag = "scheon" 217 | return tag 218 | 219 | def _updateItems(self, nxvretstr, subtree, nxvretstr_default, list_keys): 220 | daytransmap={'Sun':_('Sunday'), 'Mon':_('Monday'), 'Tue':_('Tuesday'), 'Tues':_('Tuesday'), 'Wed':_('Wednesday'), 'Thu':_('Thursday'), 'Thur':_('Thursday'), 'Fri':_('Friday'), 'Sat':_('Saturday')} 221 | tree = self 222 | subtree='' 223 | if nxvretstr == "": 224 | # remove all of items in the tree 225 | tree.delete(*tree.get_children(subtree)) 226 | # init the structure 227 | nxvretstr=nxvretstr_default 228 | 229 | items_children = self.get_children(subtree) 230 | if items_children == None or len(items_children) < 1: 231 | # create children 232 | for itemstr in list_keys: 233 | tree.insert(subtree, 'end', text=itemstr, values=("")) 234 | items_children = self.get_children(subtree) 235 | 236 | # parse the string 237 | linestr = nxvretstr.strip() + '\n' 238 | linelst = linestr.split('\n') 239 | for i in range(0, len(linelst)): 240 | line = linelst[i].strip() 241 | if len(line) < 1: 242 | break 243 | columnlst = line.split(' ') 244 | if len(columnlst) > 1: 245 | if columnlst[0] in list_keys: 246 | idx = list_keys[columnlst[0]] 247 | val = " ".join(columnlst[1:len(columnlst)]) 248 | tag = self.val2tag(val) 249 | #L.debug("len(lst) = " + str(len(columnlst))) 250 | #L.debug("schedule[" + str(idx) + "], val=" + val + "tag=" + tag) 251 | tree.item(items_children[idx], values=(val,), tags = (tag,), text=daytransmap[columnlst[0]]) 252 | 253 | def updateSchedule(self, nxvretstr): 254 | subtree = self 255 | nxvretstr_default="""Schedule is Disabled 256 | Sun 00:00 - None - 257 | Mon 00:00 - None - 258 | Tue 00:00 - None - 259 | Wed 00:00 - None - 260 | Thu 00:00 - None - 261 | Fri 00:00 - None - 262 | Sat 00:00 - None - 263 | """ 264 | list_keys = { 265 | "Sun":0, 266 | "Mon":1, 267 | "Tue":2, 268 | "Wed":3, 269 | "Thu":4, 270 | "Fri":5, 271 | "Sat":6, 272 | } 273 | self._updateItems(nxvretstr, subtree, nxvretstr_default, list_keys) 274 | self.changed={} 275 | 276 | # pack the schedule to commands, split by \n 277 | def packSchedule(self): 278 | tree = self 279 | daymap={_('Sunday'):0, _('Monday'):1, _('Tuesday'):2, _('Wednesday'):3, _('Thursday'):4, _('Friday'):5, _('Saturday'):6} 280 | retstr = "" 281 | for item in self.changed: 282 | day = self.changed[item] 283 | scheline = tree.item(item)["values"][0].strip() 284 | schelst = scheline.split(' ') 285 | tmlst = schelst[0].split(':') 286 | stype = "None" 287 | if schelst[1] == "H": 288 | stype = "House" 289 | retstr += "SetSchedule Day " + str(daymap[day]) + " Hour " + tmlst[0] + " Min " + tmlst[1] + " " + stype + "\n" 290 | L.debug("save schedule: " + retstr) 291 | return retstr 292 | 293 | class SensorTreeview(ttk.Treeview): 294 | 295 | def __init__(self, parent): 296 | ttk.Treeview.__init__(self, parent) 297 | 298 | def initUI(self): 299 | tree = self 300 | tree["columns"]=("value")#,"max","min") 301 | tree.column("value", anchor='center') #, width=100 ) 302 | #tree.column("max", width=100) 303 | #tree.column("min", width=100) 304 | tree.column('#0', anchor='w') 305 | tree.heading('#0', text=_("Sensors"), anchor='w') 306 | tree.heading("value", text=_("Value")) 307 | #tree.heading("max", text=_("Max Value")) 308 | #tree.heading("min", text=_("Min Value")) 309 | self.subtree_digital = tree.insert('', 'end', "digital", text=_("Digital Sensors")) 310 | self.subtree_analogy = tree.insert('', 'end', "analogy", text=_("Analog Sensors")) 311 | self.subtree_buttons = tree.insert('', 'end', "buttons", text=_("Buttons")) 312 | self.subtree_motors = tree.insert('', 'end', "motors", text=_("Motors")) 313 | self.subtree_accel = tree.insert('', 'end', "accel", text=_("Accelerometer")) 314 | self.subtree_charger = tree.insert('', 'end', "charger", text=_("Charger")) 315 | self.updateDigitalSensors("") 316 | self.updateButtons("") 317 | self.updateAnalogSensors("") 318 | self.updateMotors("") 319 | self.updateAccel("") 320 | self.updateCharger("") 321 | # colors 322 | tree.tag_configure('digiudef', background='grey') 323 | tree.tag_configure('digion', background='#ff6699') 324 | tree.tag_configure('digioff', background='white') 325 | 326 | def getType(self, node): 327 | if node == self.subtree_digital: 328 | return "digital" 329 | elif node == self.subtree_analogy: 330 | return "analogy" 331 | elif node == self.subtree_buttons: 332 | return "buttons" 333 | elif node == self.subtree_motors: 334 | return "motors" 335 | elif node == self.subtree_accel: 336 | return "accel" 337 | elif node == self.subtree_charger: 338 | return "charger" 339 | return "unknown" 340 | 341 | def _updateDigitalSensors(self, nxvretstr, subtree, nxvretstr_default, list_keys, list_values): 342 | tree = self 343 | if nxvretstr == "": 344 | # remove all of items in the tree 345 | tree.delete(*tree.get_children(subtree)) 346 | # init the structure 347 | nxvretstr=nxvretstr_default 348 | 349 | items_children = self.get_children(subtree) 350 | if items_children == None or len(items_children) < 1: 351 | # create children 352 | for itemstr in list_keys: 353 | #L.debug("Digital Sensors add key: " + itemstr) 354 | tree.insert(subtree, 'end', text=itemstr, values=("")) 355 | items_children = self.get_children(subtree) 356 | 357 | # parse the string 358 | linestr = nxvretstr.strip() + '\n' 359 | linelst = linestr.split('\n') 360 | for i in range(0, len(linelst)): 361 | line = linelst[i].strip() 362 | if len(line) < 1: 363 | break 364 | columnlst = line.split(',') 365 | if len(columnlst) > 1: 366 | if columnlst[0] in list_keys: 367 | idx = list_keys[columnlst[0]] 368 | value = _("Unknown") 369 | tag = "digiudef" 370 | if columnlst[1] in list_values: 371 | value = list_values[columnlst[1]][0] 372 | tag = list_values[columnlst[1]][1] 373 | #L.debug("Digital Sensors add key value: [" + str(idx) + "] " + str(items_children[idx]) + ": " + columnlst[0] + "=" + columnlst[1]) 374 | tree.item(items_children[idx], values=(value,), tags = (tag,), text=columnlst[0]) 375 | pass 376 | 377 | def updateDigitalSensors(self, nxvretstr): 378 | subtree = self.subtree_digital 379 | nxvretstr_default="""Digital Sensor Name, Value 380 | SNSR_DC_JACK_CONNECT,-1 381 | SNSR_DUSTBIN_IS_IN,-1 382 | SNSR_LEFT_WHEEL_EXTENDED,-1 383 | SNSR_RIGHT_WHEEL_EXTENDED,-1 384 | LSIDEBIT,-1 385 | LFRONTBIT,-1 386 | RSIDEBIT,-1 387 | RFRONTBIT,-1 388 | """ 389 | list_keys = { 390 | "SNSR_DC_JACK_CONNECT":0, 391 | "SNSR_DUSTBIN_IS_IN":1, 392 | "SNSR_LEFT_WHEEL_EXTENDED":2, 393 | "SNSR_RIGHT_WHEEL_EXTENDED":3, 394 | "LSIDEBIT":4, 395 | "LFRONTBIT":5, 396 | "RSIDEBIT":6, 397 | "RFRONTBIT":7, 398 | } 399 | list_values = { 400 | "0": (_("Released"), "digioff"), 401 | "1": (_("Pressed"), "digion"), 402 | } 403 | self._updateDigitalSensors(nxvretstr, subtree, nxvretstr_default, list_keys, list_values) 404 | 405 | def updateButtons(self, nxvretstr): 406 | subtree = self.subtree_buttons 407 | nxvretstr_default="""Button Name,Pressed 408 | BTN_SOFT_KEY,-1 409 | BTN_SCROLL_UP,-1 410 | BTN_START,-1 411 | BTN_BACK,-1 412 | BTN_SCROLL_DOWN,-1 413 | """ 414 | 415 | list_keys = { 416 | "BTN_SCROLL_UP":0, 417 | "BTN_SCROLL_DOWN":1, 418 | "BTN_BACK":2, 419 | "BTN_START":3, 420 | "BTN_SOFT_KEY":4, 421 | } 422 | list_values = { 423 | "0": (_("Released"), "digioff"), 424 | "1": (_("Pressed"), "digion"), 425 | } 426 | 427 | self._updateDigitalSensors(nxvretstr, subtree, nxvretstr_default, list_keys, list_values) 428 | 429 | def _updateAnalogSensors(self, nxvretstr, subtree, nxvretstr_default, list_keys): 430 | tree = self 431 | if nxvretstr == "": 432 | # remove all of items in the tree 433 | tree.delete(*tree.get_children(subtree)) 434 | # init the structure 435 | nxvretstr=nxvretstr_default 436 | 437 | items_children = self.get_children(subtree) 438 | if items_children == None or len(items_children) < 1: 439 | # create children 440 | for itemstr in list_keys: 441 | tree.insert(subtree, 'end', text=itemstr, values=("")) 442 | items_children = self.get_children(subtree) 443 | 444 | # parse the string 445 | linestr = nxvretstr.strip() + '\n' 446 | linelst = linestr.split('\n') 447 | for i in range(0, len(linelst)): 448 | line = linelst[i].strip() 449 | if len(line) < 1: 450 | break 451 | columnlst = line.split(',') 452 | if len(columnlst) > 1: 453 | if columnlst[0] in list_keys: 454 | idx = list_keys[columnlst[0]] 455 | tree.item(items_children[idx], values=(columnlst[1],), text=columnlst[0]) 456 | pass 457 | 458 | def updateAnalogSensors(self, nxvretstr): 459 | subtree = self.subtree_analogy 460 | nxvretstr_default="""SensorName,Value 461 | WallSensorInMM,-1, 462 | BatteryVoltageInmV,-1, 463 | LeftDropInMM,-1, 464 | RightDropInMM,-1, 465 | LeftMagSensor,-1, 466 | RightMagSensor,-1, 467 | UIButtonInmV,-1, 468 | VacuumCurrentInmA,-1, 469 | ChargeVoltInmV,-1, 470 | BatteryTemp0InC,-1, 471 | BatteryTemp1InC,-1, 472 | CurrentInmA,-1, 473 | SideBrushCurrentInmA,-1, 474 | VoltageReferenceInmV,-1, 475 | AccelXInmG,-1, 476 | AccelYInmG,-1, 477 | AccelZInmG,-1, 478 | """ 479 | 480 | list_keys = { 481 | "WallSensorInMM":0, 482 | "BatteryVoltageInmV":1, 483 | "LeftDropInMM":2, 484 | "RightDropInMM":3, 485 | "LeftMagSensor":4, 486 | "RightMagSensor":5, 487 | "UIButtonInmV":6, 488 | "VacuumCurrentInmA":7, 489 | "ChargeVoltInmV":8, 490 | "BatteryTemp0InC":9, 491 | "BatteryTemp1InC":10, 492 | "CurrentInmA":11, 493 | "SideBrushCurrentInmA":12, 494 | "VoltageReferenceInmV":13, 495 | "AccelXInmG":14, 496 | "AccelYInmG":15, 497 | "AccelZInmG":16, 498 | } 499 | self._updateAnalogSensors(nxvretstr, subtree, nxvretstr_default, list_keys) 500 | 501 | 502 | def updateMotors(self, nxvretstr): 503 | subtree = self.subtree_motors 504 | nxvretstr_default="""Parameter,Value 505 | Brush_RPM,0 506 | Brush_mA,0 507 | Vacuum_RPM,0 508 | Vacuum_mA,0 509 | LeftWheel_RPM,0 510 | LeftWheel_Load%,0 511 | LeftWheel_PositionInMM,0 512 | LeftWheel_Speed,0 513 | RightWheel_RPM,0 514 | RightWheel_Load%,0 515 | RightWheel_PositionInMM,0 516 | RightWheel_Speed,0 517 | Charger_mAH, 0 518 | SideBrush_mA,0 519 | """ 520 | list_keys = { 521 | "Brush_RPM":0, 522 | "Brush_mA":1, 523 | "Vacuum_RPM":2, 524 | "Vacuum_mA":3, 525 | "LeftWheel_RPM":4, 526 | "LeftWheel_Load%":5, 527 | "LeftWheel_PositionInMM":6, 528 | "LeftWheel_Speed":7, 529 | "RightWheel_RPM":8, 530 | "RightWheel_Load%":9, 531 | "RightWheel_PositionInMM":10, 532 | "RightWheel_Speed":11, 533 | "Charger_mAH":12, 534 | "SideBrush_mA":13, 535 | } 536 | self._updateAnalogSensors(nxvretstr, subtree, nxvretstr_default, list_keys) 537 | 538 | def updateAccel(self, nxvretstr): 539 | subtree = self.subtree_accel 540 | nxvretstr_default="""Label,Value 541 | PitchInDegrees,0.00 542 | RollInDegrees,0.00 543 | XInG,0.000 544 | YInG,0.000 545 | ZInG,0.000 546 | SumInG,0.000 547 | """ 548 | list_keys = { 549 | "PitchInDegrees":0, 550 | "RollInDegrees":1, 551 | "XInG":2, 552 | "YInG":3, 553 | "ZInG":4, 554 | "SumInG%":5, 555 | } 556 | self._updateAnalogSensors(nxvretstr, subtree, nxvretstr_default, list_keys) 557 | 558 | def updateCharger(self, nxvretstr): 559 | subtree = self.subtree_charger 560 | nxvretstr_default="""Label,Value 561 | FuelPercent,-1 562 | BatteryOverTemp,-1 563 | ChargingActive,-1 564 | ChargingEnabled,-1 565 | ConfidentOnFuel,-1 566 | OnReservedFuel,-1 567 | EmptyFuel,-1 568 | BatteryFailure,-1 569 | ExtPwrPresent,-1 570 | ThermistorPresent[0],-1 571 | ThermistorPresent[1],-1 572 | BattTempCAvg[0],-1 573 | BattTempCAvg[1],-1 574 | VBattV,-1 575 | VExtV,-1 576 | Charger_mAH,-1 577 | """ 578 | list_keys = { 579 | "FuelPercent":0, 580 | "BatteryOverTemp":1, 581 | "ChargingActive":2, 582 | "ChargingEnabled":3, 583 | "ConfidentOnFuel":4, 584 | "OnReservedFuel":5, 585 | "EmptyFuel":6, 586 | "BatteryFailure":7, 587 | "ExtPwrPresent":8, 588 | "ThermistorPresent[0]":9, 589 | "ThermistorPresent[1]":10, 590 | "BattTempCAvg[0]":11, 591 | "BattTempCAvg[1]":12, 592 | "VBattV":13, 593 | "VExtV":14, 594 | "Charger_mAH":15, 595 | } 596 | self._updateAnalogSensors(nxvretstr, subtree, nxvretstr_default, list_keys) 597 | 598 | class MyTkAppFrame(ttk.Notebook): #(tk.Frame): 599 | def show_battery_level(self, level): 600 | self.style_battstat.configure("LabeledProgressbar", text="{0} % ".format(level)) 601 | self.progress_batt["value"]=level 602 | #self.frame_status.update() 603 | #self.progress_batt.update_idletasks() 604 | 605 | def show_robot_version(self, msg): 606 | if msg.find("GetVersion") >= 0: 607 | set_readonly_text(self.text_version, msg) 608 | else: 609 | guilog.textarea_append(self.text_version, "\n\n" + msg) 610 | 611 | def show_robot_time(self, msg): 612 | self.lbl_synctime['text'] = msg 613 | 614 | def show_robot_testmode(self, onoff=False): 615 | if onoff: 616 | self.lbl_testmode['text'] = _("ON") 617 | self.lbl_testmode['fg'] = "red" 618 | else: 619 | self.lbl_testmode['text'] = _("OFF") 620 | self.lbl_testmode['fg'] = "green" 621 | 622 | # the functions for log file in status 623 | def onSelectAllLogname(self, event): 624 | self.combobox_logfile.tag_add(tk.SEL, "1.0", tk.END) 625 | self.combobox_logfile.mark_set(tk.INSERT, "1.0") 626 | self.combobox_logfile.see(tk.INSERT) 627 | return 'break' 628 | 629 | def onLogfileCheckChanged(self): 630 | # read self.use_logfile changed by self.check_logfile 631 | # check if the file exists 632 | # start to log 633 | return 634 | 635 | 636 | def onLogfileSelected(self, event): 637 | # read self.use_logfile changed by self.check_logfile 638 | # change log file 639 | return 640 | 641 | def onSelectLogfile(self): 642 | #dir_path = os.path.dirname(os.path.realpath(__file__)) 643 | pname_input = self.combobox_logfile.get().strip() 644 | dir_path=os.path.dirname(os.path.realpath(pname_input)) 645 | if True != os.path.exists(dir_path): 646 | dir_path = os.getcwd() 647 | fname = fd.askopenfilename( 648 | initialdir=dir_path, 649 | filetypes=( 650 | (_("Text files"), "*.txt"), 651 | (_("All files"), "*.*") 652 | ) 653 | ) 654 | if fname: 655 | self.combobox_logfile.set(fname.strip()) 656 | return 657 | 658 | def __init__(self, tk_frame_parent): 659 | global MAXDIST 660 | global CONST_RAD 661 | #nb = ttk.Notebook(tk_frame_parent) 662 | ttk.Notebook.__init__(self, tk_frame_parent) 663 | nb = self 664 | self.serv_cli = None 665 | self.istestmode = False 666 | self.mailbox = neatocmdapi.MailPipe() 667 | # the error to disconnect socket 668 | self.mid_socket_disconnect = self.mailbox.declair() 669 | 670 | 671 | # the images for toggle buttons 672 | self.img_ledon=tk.PhotoImage(file="ledred-on.gif") 673 | self.img_ledoff=tk.PhotoImage(file="ledred-off.gif") 674 | 675 | guilog.rClickbinder(tk_frame_parent) 676 | 677 | # page for test pack() 678 | #page_testpack = tk.Frame(nb) 679 | #test_pack(page_testpack) 680 | 681 | # page for test grid 682 | #page_testgrid = tk.Frame(nb) 683 | #myParent = nb 684 | #main_container = page_testgrid 685 | #test_grid(myParent, main_container) 686 | 687 | # page for About 688 | page_about = tk.Frame(nb) 689 | lbl_about_head = tk.Label(page_about, text=_("About"), font=LARGE_FONT) 690 | lbl_about_head.pack(side="top", fill="x", pady=10) 691 | lbl_about_main = tk.Label(page_about 692 | , font=NORM_FONT 693 | , text="\n" + str_progname + "\n" + str_version + "\n" 694 | + _("Setup your Neato Robot") + "\n" 695 | + "\n" 696 | + _("Copyright © 2015-2016 The nxvControl Authors") + "\n" 697 | + "\n" 698 | + _("This program comes with absolutely no warranty.") + "\n" 699 | + _("See the GNU General Public License, version 3 or later for details.") + "\n" 700 | ) 701 | lbl_about_main.pack(side="top", fill="x", pady=10) 702 | 703 | # adding Frames as pages for the ttk.Notebook 704 | # first page, which would get widgets gridded into it 705 | page_conn = tk.Frame(nb) 706 | # includes: 707 | # connect with port selection or Serial-TCP connection; 708 | # button to shutdown 709 | # testmode indicator and button to enter/leave test mode 710 | # the robot time, sync with pc 711 | # textarea of version info 712 | # log file name, enable/disable: all of connection message and input output will be here! 713 | lbl_conn_head = tk.Label(page_conn, text=_("Connection"), font=LARGE_FONT) 714 | lbl_conn_head.pack(side="top", fill="x", pady=10) 715 | self.frame_status = ttk.LabelFrame(page_conn, text=_("Status")) 716 | 717 | 718 | # connection 719 | frame_cli = ttk.LabelFrame(page_conn, text=_("Conection")) 720 | line=0 721 | client_port_history = ('tcp://192.168.3.163:3333', 'dev://ttyACM0:115200', 'dev://ttyUSB0:115200', 'dev://COM11:115200', 'dev://COM12:115200', 'sim:', 'tcp://localhost:3333') 722 | self.client_port = tk.StringVar() 723 | lbl_cli_port = tk.Label(frame_cli, text=_("Connect to:")) 724 | lbl_cli_port.grid(row=line, column=0, padx=5, sticky=tk.N+tk.S+tk.W) 725 | combobox_client_port = ttk.Combobox(frame_cli, textvariable=self.client_port) 726 | combobox_client_port['values'] = client_port_history 727 | combobox_client_port.grid(row=line, column=1, padx=5, pady=5, sticky=tk.N+tk.S+tk.W) 728 | combobox_client_port.current(0) 729 | # Buttons 730 | self.btn_cli_connect = tk.Button(frame_cli, text=_("Connect"), command=self.do_cli_connect) 731 | self.btn_cli_connect.grid(row=line, column=2, columnspan=1, padx=5, sticky=tk.N+tk.S+tk.W+tk.E) 732 | #btn_cli_connect.pack(side="left", fill="both", padx=5, pady=5, expand=True) 733 | self.btn_cli_disconnect = tk.Button(frame_cli, text=_("Disconnect"), state=tk.DISABLED, command=self.do_cli_disconnect) 734 | self.btn_cli_disconnect.grid(row=line, column=3, columnspan=1, padx=5, sticky=tk.N+tk.S+tk.W+tk.E) 735 | #btn_cli_disconnect.pack(side="left", fill="both", padx=5, pady=5, expand=True) 736 | frame_cli.pack(side="top", fill="x", pady=10) 737 | 738 | self.status_isactive = False # shall we refresh the time and battery info? 739 | 740 | # status 741 | line = 0 # line 742 | lbl_synctime_conn = tk.Label(self.frame_status, text=_("Robot Time:")) 743 | lbl_synctime_conn.grid(row=line, column=0, padx=5, sticky=tk.N+tk.S+tk.W) 744 | self.lbl_synctime = tk.Label(self.frame_status, text="00:00:00") 745 | self.lbl_synctime.grid(row=line, column=1, padx=5) 746 | #self.lbl_synctime.pack(side="right", fill="x", pady=10) 747 | btn_synctime = tk.Button(self.frame_status, text=_("Sync PC time to robot"), command=self.set_robot_time_from_pc) 748 | btn_synctime.grid(row=line, column=2, padx=5, sticky=tk.N+tk.S+tk.E+tk.W) 749 | #btn_synctime.pack(side="right", fill="x", pady=10) 750 | line += 1 751 | lbl_testmode_conn = tk.Label(self.frame_status, text=_("Test Mode:")) 752 | lbl_testmode_conn.grid(row=line, column=0, padx=5) 753 | self.lbl_testmode = tk.Label(self.frame_status, text=_("Unknown")) 754 | self.lbl_testmode.grid(row=line, column=1, padx=5) 755 | btn_testmode_on = tk.Button(self.frame_status, text=_("Test ON"), command=lambda: self.set_robot_testmode(True)) 756 | btn_testmode_off = tk.Button(self.frame_status, text=_("Test OFF"), command=lambda: self.set_robot_testmode(False)) 757 | btn_testmode_on.grid(row=line, column=2, padx=5, sticky=tk.N+tk.S+tk.W) 758 | btn_testmode_off.grid(row=line, column=2, padx=5, sticky=tk.N+tk.S+tk.E) 759 | line += 1 760 | lbl_battstat_conn = tk.Label(self.frame_status, text=_("Battery Status:")) 761 | lbl_battstat_conn.grid(row=line, column=0, padx=5) 762 | self.style_battstat = ttk.Style(self.frame_status) 763 | # add the label to the progressbar style 764 | self.style_battstat.layout("LabeledProgressbar", 765 | [('LabeledProgressbar.trough', 766 | {'children': [('LabeledProgressbar.pbar', 767 | {'side': 'left', 'sticky': 'ns'}), 768 | ("LabeledProgressbar.label", 769 | {"sticky": ""})], 770 | 'sticky': 'nswe'})]) 771 | self.style_battstat.configure('LabeledProgressbar', foreground='red', background='#00ff00') 772 | self.progress_batt = ttk.Progressbar(self.frame_status, orient=tk.HORIZONTAL, style="LabeledProgressbar", mode='determinate', length=300) 773 | self.progress_batt.grid(row=line, column=1, padx=5) 774 | #btn_battstat = tk.Button(self.frame_status, text="") 775 | #btn_battstat.grid(row=line, column=2, padx=5, sticky=tk.E) 776 | line += 1 777 | lbl_battstat_conn = tk.Label(self.frame_status, text=_("Version:")) 778 | lbl_battstat_conn.grid(row=line, column=0, padx=5) 779 | self.text_version = ScrolledText(self.frame_status, wrap=tk.WORD, height=10) 780 | self.text_version.configure(state='disabled') 781 | #self.text_version.pack(expand=True, fill="both", side="top") 782 | self.text_version.grid(row=line, column=1, columnspan=2, padx=5) 783 | self.text_version.bind("<1>", lambda event: self.text_version.focus_set()) # enable highlighting and copying 784 | 785 | # save log file? 786 | #line += 1 787 | #self.use_logfile = tk.StringVar() 788 | #self.check_logfile = ttk.Checkbutton(self.frame_status, text=_("Use Log File"), 789 | #command=self.onLogfileCheckChanged, variable=self.use_logfile, 790 | #onvalue='metric', offvalue='imperial') 791 | #self.check_logfile.grid(row=line, column=0, padx=5) 792 | #sellogfiles = tk.StringVar() 793 | #self.combobox_logfile = ttk.Combobox(self.frame_status, textvariable=sellogfiles) 794 | #self.combobox_logfile.bind('<>', self.onLogfileSelected) 795 | #self.combobox_logfile.bind("", self.onSelectAllLogname) 796 | #self.combobox_logfile.bind("", self.onSelectAllLogname) 797 | #self.combobox_logfile['values'] = ('neatologfile.txt', '/tmp/neatologfile.txt', '$HOME/logfile.txt') 798 | #self.combobox_logfile.current(0) 799 | #self.combobox_logfile.grid(row=line, column=1, sticky=tk.W+tk.E) 800 | #self.button_select_logfile = tk.Button(self.frame_status, text=" ... ", command=self.onSelectLogfile) 801 | #self.button_select_logfile.grid(row=line, column=2, sticky=tk.W) 802 | 803 | frame_cli.pack(side="top", fill="x", pady=10) 804 | self.frame_status.pack(side="top", fill="both", pady=10) 805 | 806 | #ttk.Separator(page_conn, orient=HORIZONTAL).pack() 807 | #b1 = tk.Button(page_about, text=_("Button 1")) 808 | 809 | # page for commands 810 | page_command = tk.Frame(nb) 811 | # combox list for all available know commands, select one will show the help message in text area 812 | # edit line which supports history 813 | # output 814 | # help message area 815 | lbl_command_head = tk.Label(page_command, text=_("Commands"), font=LARGE_FONT) 816 | lbl_command_head.pack(side="top", fill="x", pady=10) 817 | 818 | frame_top = tk.Frame(page_command)#, background="green") 819 | frame_bottom = tk.Frame(page_command)#, background="yellow") 820 | frame_top.pack(side="top", fill="both", expand=True) 821 | frame_bottom.pack(side="bottom", fill="x", expand=False) 822 | 823 | self.text_cli_command = ScrolledText(frame_top, wrap=tk.WORD) 824 | #self.text_cli_command.insert(tk.END, "Some Text\ntest 1\ntest 2\n") 825 | self.text_cli_command.configure(state='disabled') 826 | self.text_cli_command.pack(expand=True, fill="both", side="top") 827 | # make sure the widget gets focus when clicked 828 | # on, to enable highlighting and copying to the 829 | # clipboard. 830 | self.text_cli_command.bind("<1>", lambda event: self.text_cli_command.focus_set()) 831 | 832 | btn_clear_cli_command = tk.Button(frame_bottom, text=_("Clear"), command=lambda: (set_readonly_text(self.text_cli_command, ""), self.text_cli_command.update_idletasks()) ) 833 | btn_clear_cli_command.pack(side="left", fill="x", padx=5, pady=5, expand=False) 834 | self.cli_command = tk.StringVar() 835 | self.combobox_cli_command = ttk.Combobox(frame_bottom, textvariable=self.cli_command) 836 | self.combobox_cli_command['values'] = ('Help', 'GetAccel', 'GetButtons', 'GetCalInfo', 'GetCharger', 'GetDigitalSensors', 'GetErr', 'GetLDSScan', 'GetLifeStatLog', 'GetMotors', 'GetSchedule', 'GetTime', 'GetVersion', 'GetWarranty', 'PlaySound 0', 'Clean House', 'DiagTest MoveAndBump', 'DiagTest DropTest', 'RestoreDefaults', 'SetDistanceCal DropMinimum', 'SetFuelGauge Percent 100', 'SetIEC FloorSelection carpet', 'SetLCD BGWhite', 'SetLDSRotation On', 'SetLED BacklightOn', 'SetMotor VacuumOn', 'SetSchedule Day Sunday Hour 17 Min 0 House ON', 'SetSystemMode Shutdown', 'SetTime Day Sunday Hour 12 Min 5 Sec 25', 'SetWallFollower Enable', 'TestMode On', 'Upload' ) 837 | self.combobox_cli_command.pack(side="left", fill="both", padx=5, pady=5, expand=True) 838 | self.combobox_cli_command.bind("", self.do_cli_run_ev) 839 | self.combobox_cli_command.bind("<>", self.do_select_clicmd) 840 | self.combobox_cli_command.current(0) 841 | btn_run_cli_command = tk.Button(frame_bottom, text=_("Run"), command=self.do_cli_run) 842 | btn_run_cli_command.pack(side="right", fill="x", padx=5, pady=5, expand=False) 843 | 844 | # page for scheduler 845 | page_sche = tk.Frame(nb) 846 | # indicator, enable/disable button 847 | # save/load file 848 | # the list of scheduler 849 | lbl_sche_head = tk.Label(page_sche, text=_("Schedule"), font=LARGE_FONT) 850 | lbl_sche_head.pack(side="top", fill="x", pady=10) 851 | 852 | frame_top = tk.Frame(page_sche)#, background="green") 853 | frame_bottom = tk.Frame(page_sche)#, background="yellow") 854 | frame_top.pack(side="top", fill="both", expand=True) 855 | frame_bottom.pack(side="bottom", fill="x", expand=False) 856 | 857 | self.mid_query_schedule = -1 858 | self.etv_schedule = ScheduleTreeview(frame_top) 859 | self.etv_schedule.initUI() 860 | self.etv_schedule.pack(pady=5, fill="both", side="left", expand=True) 861 | 862 | self.schedule_isenabled = False 863 | devstr = _("Schedule") 864 | self.btn_enable_schedule = guilog.ToggleButton(frame_bottom, txtt=_("Enabled: ")+devstr, txtr=_("Disabled: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_schedule_enable) 865 | self.btn_enable_schedule.pack(pady=5, side="left") 866 | btn_save_schedule = tk.Button(frame_bottom, text=_("Save schedule"), command=lambda: (btn_save_schedule.focus_set(), self.guiloop_save_schedule(), )) 867 | btn_save_schedule.pack(side="right", fill="x", padx=5, pady=5, expand=False) 868 | btn_get_schedule = tk.Button(frame_bottom, text=_("Get schedule"), command=lambda: (btn_get_schedule.focus_set(), self.guiloop_get_schedule(), )) 869 | btn_get_schedule.pack(side="right", fill="x", padx=5, pady=5, expand=False) 870 | 871 | # page for sensors 872 | page_sensors = tk.Frame(nb) 873 | # the list of sensor status, includes sensor and value 874 | # indicator of testmode, no control 875 | # indicator, enable/disable auto update 876 | lbl_sensor_head = tk.Label(page_sensors, text=_("Sensors"), font=LARGE_FONT) 877 | lbl_sensor_head.pack(side="top", fill="x", pady=10) 878 | 879 | frame_top = tk.Frame(page_sensors)#, background="green") 880 | frame_bottom = tk.Frame(page_sensors)#, background="yellow") 881 | frame_top.pack(side="top", fill="both", expand=True) 882 | frame_bottom.pack(side="bottom", fill="x", expand=False) 883 | 884 | self.mid_query_digitalsensors = -1 885 | self.mid_query_analogysensors = -1 886 | self.mid_query_buttonssensors = -1 887 | self.mid_query_motorssensors = -1 888 | self.mid_query_accelsensors = -1 889 | self.tab_sensors_isactive = False # if the tab is "Sensors status" 890 | # flag to signal the command is finished 891 | self.sensors_request_full_digital = False 892 | self.sensors_request_full_analogy = False 893 | self.sensors_request_full_buttons = False 894 | self.sensors_request_full_motors = False 895 | self.sensors_request_full_accel = False 896 | self.sensors_request_full_charger = False 897 | self.sensors_update_isopen_digital = False # if the tree is open? 898 | self.sensors_update_isopen_analogy = False 899 | self.sensors_update_isopen_buttons = False 900 | self.sensors_update_isopen_motors = False 901 | self.sensors_update_isopen_accel = False 902 | self.sensors_update_isopen_charger = False 903 | 904 | self.sensors_update_isactive = False 905 | devstr = _("Update Sensors") 906 | self.btn_sensors_update_enable = guilog.ToggleButton(frame_bottom, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_sensors_update_enable) 907 | self.btn_sensors_update_enable.pack(pady=5, side="right") 908 | 909 | self.sensor_tree_status = SensorTreeview(frame_top) 910 | self.sensor_tree_status.initUI() 911 | self.sensor_tree_status.pack(pady=5, fill="both", side="left", expand=True) 912 | # <> <> 913 | self.sensor_tree_status.bind('<>', lambda ev:self.guiloop_sensors_changed_to(ev,True)) 914 | self.sensor_tree_status.bind('<>', lambda ev:self.guiloop_sensors_changed_to(ev,False)) 915 | scrollbar_y_tree = ttk.Scrollbar(frame_top, orient="vertical") 916 | scrollbar_y_tree.configure(command=self.sensor_tree_status.yview) 917 | self.sensor_tree_status.configure(yscrollcommand=scrollbar_y_tree.set) 918 | scrollbar_y_tree.pack( side = tk.RIGHT, fill=tk.Y ) 919 | #scrollbar_x_tree = ttk.Scrollbar(frame_top, orient="horizontal") 920 | #scrollbar_x_tree.configure(command=self.sensor_tree_status.xview) 921 | #self.sensor_tree_status.configure(xscrollcommand=scrollbar_x_tree.set) 922 | #scrollbar_x_tree.pack( side = tk.BOTTOM, fill=tk.X ) 923 | 924 | # page for LiDAR 925 | page_lidar = tk.Frame(nb) 926 | # graph for current data 927 | # indicator, enable/disable scanning 928 | # buttons to remote control: left/right/up/down/rotate 929 | lbl_lidar_head = tk.Label(page_lidar, text=_("LiDAR"), font=LARGE_FONT) 930 | lbl_lidar_head.pack(side="top", fill="x", pady=10) 931 | 932 | frame_top = tk.Frame(page_lidar)#, background="green") 933 | frame_bottom = tk.Frame(page_lidar)#, background="yellow") 934 | frame_top.pack(side="top", fill="both", expand=True) 935 | frame_bottom.pack(side="bottom", fill="x", expand=False) 936 | 937 | self.canvas_lidar = tk.Canvas(frame_top) 938 | self.canvas_lidar.pack(side="top", fill="both", expand="yes", pady=10) 939 | self.canvas_lidar_points = {} # 360 items, 0 - 359 degree, lines 940 | self.canvas_lidar_lines = {} 941 | self.map_sin_lidar = {} 942 | self.map_cos_lidar = {} 943 | for i in range(0,360): 944 | self.map_cos_lidar[i] = math.cos(CONST_RAD * i) / MAXDIST 945 | self.map_sin_lidar[i] = math.sin(CONST_RAD * i) / MAXDIST 946 | self.mid_query_lidar = -1 947 | self.canvas_lidar_isfocused = False 948 | self.canvas_lidar_isactive = False 949 | self.canvas_lidar_request_full = False 950 | self.state_wheel = STATE_STOP 951 | self.speed_wheel = 0 952 | 953 | devstr = _("LiDAR") 954 | self.btn_lidar_enable = guilog.ToggleButton(frame_bottom, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_lidar_enable) 955 | self.btn_lidar_enable.pack(pady=5, side="left") 956 | self.setup_keypad_navigate(self.canvas_lidar) 957 | 958 | self.wheelctrl_isactive = False 959 | devstr = _("Wheels Controlled by Keypad") 960 | self.btn_wheelctrl_enable = guilog.ToggleButton(frame_bottom, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_wheelctrl_enable) 961 | self.btn_wheelctrl_enable.pack(pady=5, side="right") 962 | 963 | # page for motors 964 | page_moto = tk.Frame(nb) 965 | # list of motors and each has indicator, start/stop button 966 | # warnning message: flip the robot upside down so the wheels are faceing up, before enable wheels moto! 967 | lbl_moto_head = tk.Label(page_moto, text=_("Motors"), font=LARGE_FONT) 968 | lbl_moto_head.pack(side="top", fill="x", pady=10) 969 | s = ttk.Scale(page_moto, orient=tk.HORIZONTAL, length=200, from_=1.0, to=100.0) 970 | 971 | devstr = _("Left Wheel") 972 | self.btn_enable_leftwheel = guilog.ToggleButton(page_moto, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_enable_leftwheel) 973 | self.btn_enable_leftwheel.pack(pady=5) 974 | 975 | devstr = _("Right Wheel") 976 | self.btn_enable_rightwheel = guilog.ToggleButton(page_moto, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_enable_rightwheel ) 977 | self.btn_enable_rightwheel.pack(pady=5) 978 | 979 | devstr = _("LiDAR Motor") 980 | self.btn_enable_lidarmoto = guilog.ToggleButton(page_moto, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_enable_lidarmoto ) 981 | self.btn_enable_lidarmoto.pack(pady=5) 982 | 983 | devstr = _("Vacuum") 984 | self.btn_enable_vacuum = guilog.ToggleButton(page_moto, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_enable_vacuum ) 985 | self.btn_enable_vacuum.pack(pady=5) 986 | 987 | devstr = _("Brush") 988 | self.btn_enable_brush = guilog.ToggleButton(page_moto, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_enable_brush ) 989 | self.btn_enable_brush.pack(pady=5) 990 | 991 | devstr = _("Side Brush") 992 | self.btn_enable_sidebrush = guilog.ToggleButton(page_moto, txtt=_("ON: ")+devstr, txtr=_("OFF: ")+devstr, imgt=self.img_ledon, imgr=self.img_ledoff, command=self.guiloop_enable_sidebrush ) 993 | self.btn_enable_sidebrush.pack(pady=5) 994 | 995 | # page for Recharge 996 | page_recharge = tk.Frame(nb) 997 | # only available when connected to Serial port directly, not for TCP 998 | lbl_recharge_head = tk.Label(page_recharge, text=_("Recharge"), font=LARGE_FONT) 999 | lbl_recharge_head.pack(side="top", fill="x", pady=10) 1000 | 1001 | self.tabtxt_sensors = _("Sensors") 1002 | self.tabtxt_lidar = _("LiDAR") 1003 | self.tabtxt_status = _("Connection") 1004 | nb.add(page_conn, text=self.tabtxt_status) 1005 | nb.add(page_command, text=_("Commands")) 1006 | nb.add(page_sche, text=_("Schedule")) 1007 | nb.add(page_moto, text=_("Motors")) 1008 | nb.add(page_sensors, text=self.tabtxt_sensors) 1009 | nb.add(page_lidar, text=self.tabtxt_lidar) 1010 | #nb.add(page_recharge, text='Recharge') 1011 | nb.add(page_about, text=_("About")) 1012 | #nb.add(page_testgrid, text='TestGrid') 1013 | #nb.add(page_testpack, text='TestPack') 1014 | nb.bind('<>', self.guiloop_nb_tabchanged) 1015 | 1016 | self.do_cli_disconnect() 1017 | 1018 | # 1019 | # schedule: support functions 1020 | # 1021 | def guiloop_get_schedule(self): 1022 | if self.serv_cli != None and self.mid_query_schedule >= 0: 1023 | self.serv_cli.request(["GetSchedule", self.mid_query_schedule]) 1024 | 1025 | def guiloop_save_schedule(self): 1026 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1027 | cmdstr = self.etv_schedule.packSchedule().strip() 1028 | if cmdstr != "": 1029 | self.serv_cli.request([cmdstr, self.mid_2b_ignored]) 1030 | self.guiloop_get_schedule() 1031 | 1032 | def setup_schedule_enable(self, isenable): 1033 | btn = self.btn_enable_schedule 1034 | if isenable: 1035 | btn.config(relief='sunken') 1036 | btn['fg'] = "red" 1037 | else: 1038 | btn.config(relief='raised') 1039 | btn['fg'] = "green" 1040 | 1041 | def guiloop_schedule_enable(self): 1042 | btn = self.btn_enable_schedule 1043 | btn.focus_set() 1044 | if btn.config('relief')[-1] == 'sunken': 1045 | btn['fg'] = "red" 1046 | self.schedule_isenabled=True 1047 | else: 1048 | btn['fg'] = "green" 1049 | self.schedule_isenabled=False 1050 | 1051 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1052 | if self.schedule_isenabled: 1053 | self.serv_cli.request(["SetSchedule ON", self.mid_2b_ignored]) 1054 | else: 1055 | self.serv_cli.request(["SetSchedule OFF", self.mid_2b_ignored]) 1056 | 1057 | # 1058 | # lidar: support functions 1059 | # 1060 | def guiloop_sensors_changed_to(self, event, isopen): 1061 | tree = event.widget 1062 | node = tree.focus() 1063 | trtype = tree.getType(node) 1064 | L.debug("treeview " + trtype + " changed to: " + str(isopen) + "; sensor update=" + str(self.sensors_update_isactive)) 1065 | if trtype == "digital": 1066 | self.sensors_update_isopen_digital = isopen 1067 | elif node == "analogy": 1068 | self.sensors_update_isopen_analogy = isopen 1069 | elif node == "buttons": 1070 | self.sensors_update_isopen_buttons = isopen 1071 | elif node == "motors": 1072 | self.sensors_update_isopen_motors = isopen 1073 | elif node == "accel": 1074 | self.sensors_update_isopen_accel = isopen 1075 | elif node == "charger": 1076 | self.sensors_update_isopen_charger = isopen 1077 | pass 1078 | 1079 | def guiloop_sensors_update_enable(self): 1080 | b1 = self.btn_sensors_update_enable 1081 | if b1.config('relief')[-1] == 'sunken': 1082 | b1['fg'] = "red" 1083 | self.sensors_update_isactive=True 1084 | else: 1085 | b1['fg'] = "green" 1086 | self.sensors_update_isactive=False 1087 | def guiloop_wheelctrl_enable(self): 1088 | b1 = self.btn_wheelctrl_enable 1089 | if b1.config('relief')[-1] == 'sunken': 1090 | b1['fg'] = "red" 1091 | self.wheelctrl_isactive=True 1092 | else: 1093 | b1['fg'] = "green" 1094 | self.wheelctrl_isactive=False 1095 | def guiloop_lidar_enable(self): 1096 | b1 = self.btn_lidar_enable 1097 | if b1.config('relief')[-1] == 'sunken': 1098 | b1['fg'] = "red" 1099 | self.canvas_lidar_isactive=True 1100 | self.canvas_lidar_isfocused=False 1101 | self._enable_lidar_moto(True) 1102 | self.guiloop_process_lidar(True) 1103 | else: 1104 | b1['fg'] = "green" 1105 | self._enable_lidar_moto(False) 1106 | self.canvas_lidar_isactive=False 1107 | 1108 | def guiloop_enable_leftwheel(self): 1109 | enable = False 1110 | b1 = self.btn_enable_leftwheel 1111 | if b1.config('relief')[-1] == 'sunken': 1112 | b1['fg'] = "red" 1113 | enable = True 1114 | else: 1115 | b1['fg'] = "green" 1116 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1117 | self.set_robot_testmode(True) 1118 | if enable: 1119 | self.serv_cli.request(["SetMotor LWheelEnable\nSetMotor LWheelDist 200 Speed 100", self.mid_2b_ignored]) 1120 | else: 1121 | self.serv_cli.request(["SetMotor LWheelDisable", self.mid_2b_ignored]) 1122 | 1123 | def guiloop_enable_rightwheel(self): 1124 | enable = False 1125 | b1 = self.btn_enable_rightwheel 1126 | if b1.config('relief')[-1] == 'sunken': 1127 | b1['fg'] = "red" 1128 | enable = True 1129 | else: 1130 | b1['fg'] = "green" 1131 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1132 | self.set_robot_testmode(True) 1133 | if enable: 1134 | self.serv_cli.request(["SetMotor RWheelEnable\nSetMotor RWheelDist 200 Speed 100", self.mid_2b_ignored]) 1135 | else: 1136 | self.serv_cli.request(["SetMotor RWheelDisable", self.mid_2b_ignored]) 1137 | 1138 | def _enable_lidar_moto(self, enable=False): 1139 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1140 | self.set_robot_testmode(True) 1141 | if enable: 1142 | self.serv_cli.request(["SetLDSRotation On", self.mid_2b_ignored]) 1143 | else: 1144 | self.serv_cli.request(["SetLDSRotation Off", self.mid_2b_ignored]) 1145 | def guiloop_enable_lidarmoto(self): 1146 | enable = False 1147 | b1 = self.btn_enable_lidarmoto 1148 | if b1.config('relief')[-1] == 'sunken': 1149 | b1['fg'] = "red" 1150 | enable = True 1151 | else: 1152 | b1['fg'] = "green" 1153 | self._enable_lidar_moto(enable) 1154 | 1155 | def guiloop_enable_vacuum(self): 1156 | enable = False 1157 | b1 = self.btn_enable_vacuum 1158 | if b1.config('relief')[-1] == 'sunken': 1159 | b1['fg'] = "red" 1160 | enable = True 1161 | else: 1162 | b1['fg'] = "green" 1163 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1164 | self.set_robot_testmode(True) 1165 | if enable: 1166 | self.serv_cli.request(["SetMotor VacuumOn", self.mid_2b_ignored]) 1167 | else: 1168 | self.serv_cli.request(["SetMotor VacuumOff", self.mid_2b_ignored]) 1169 | 1170 | def guiloop_enable_brush(self): 1171 | enable = False 1172 | b1 = self.btn_enable_brush 1173 | if b1.config('relief')[-1] == 'sunken': 1174 | b1['fg'] = "red" 1175 | enable = True 1176 | else: 1177 | b1['fg'] = "green" 1178 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1179 | self.set_robot_testmode(True) 1180 | if enable: 1181 | self.serv_cli.request(["SetMotor BrushEnable\nSetMotor Brush RPM 250", self.mid_2b_ignored]) 1182 | else: 1183 | self.serv_cli.request(["SetMotor BrushDisable", self.mid_2b_ignored]) 1184 | 1185 | def guiloop_enable_sidebrush(self): 1186 | enable = False 1187 | b1 = self.btn_enable_sidebrush 1188 | if b1.config('relief')[-1] == 'sunken': 1189 | b1['fg'] = "red" 1190 | enable = True 1191 | else: 1192 | b1['fg'] = "green" 1193 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1194 | self.set_robot_testmode(True) 1195 | if enable: 1196 | self.serv_cli.request(["SetMotor SidebrushEnable\nSetMotor SidebrushOn", self.mid_2b_ignored]) 1197 | else: 1198 | self.serv_cli.request(["SetMotor SidebrushOff\nSetMotor SidebrushDisable", self.mid_2b_ignored]) 1199 | 1200 | # called by GUI when the tab is changed 1201 | def guiloop_nb_tabchanged(self, event): 1202 | # check if the LiDAR tab is open 1203 | cur_focus = False 1204 | if event.widget.tab(event.widget.index("current"),"text") == self.tabtxt_lidar: 1205 | cur_focus = True 1206 | self.canvas_lidar.focus_set() # when switch to the lidar page, use the canvas as the front widget to receive key events! 1207 | self.guiloop_process_lidar(cur_focus) 1208 | 1209 | # check if the Sensors tab is open 1210 | cur_focus = False 1211 | if event.widget.tab(event.widget.index("current"),"text") == self.tabtxt_sensors: 1212 | cur_focus = True 1213 | self.guiloop_process_sensors(cur_focus) 1214 | 1215 | # check if the Status tab is open 1216 | cur_focus = False 1217 | if event.widget.tab(event.widget.index("current"),"text") == self.tabtxt_status: 1218 | cur_focus = True 1219 | self.guiloop_process_status(cur_focus) 1220 | 1221 | # called by GUI when the tab is changed to status 1222 | def guiloop_process_status(self, cur_focus): 1223 | L.info('switched to tab status: previous=' + str(self.status_isactive) + ", current=" + str(cur_focus)) 1224 | self.status_isactive = cur_focus 1225 | #self.status_request() 1226 | 1227 | # called by GUI when the tab is changed to sensors 1228 | def guiloop_process_sensors(self, cur_focus): 1229 | L.info('switched to tab sensor: previous=' + str(self.tab_sensors_isactive) + ", current=" + str(cur_focus)) 1230 | self.tab_sensors_isactive = cur_focus 1231 | self.buttons_sensors_request() 1232 | 1233 | # the state machine for controling the wheel's movement 1234 | def smachine_wheelctrl(self, key): 1235 | #try: 1236 | # { 1237 | # STATE_STOP: case_wheelctrl_ststop, 1238 | # STATE_FORWARD: case_wheelctrl_stforword, 1239 | # STATE_BACK: case_wheelctrl_stback, 1240 | # STATE_LEFT: case_wheelctrl_stleft, 1241 | # STATE_RIGHT: case_wheelctrl_stright, 1242 | # }[self.state_wheel](key) 1243 | #except KeyError: 1244 | # # default action 1245 | # L.error("no such state: " + str(self.state_wheel)) 1246 | if key == KEY_UP: 1247 | if self.state_wheel == STATE_BACK: 1248 | self.state_wheel = STATE_STOP 1249 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1250 | self.set_robot_testmode(True) 1251 | self.serv_cli.request(["SetMotor LWheelDisable RWheelDisable", self.mid_2b_ignored]) 1252 | #self.serv_cli.request(["SetMotor RWheelEnable LWheelEnable\nSetMotor LWheelDist 2500 RWheelDist -2500 Speed 100", self.mid_2b_ignored]) 1253 | elif self.state_wheel == STATE_FORWARD: 1254 | if self.speed_wheel < 300: 1255 | self.speed_wheel += 50 1256 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1257 | self.set_robot_testmode(True) 1258 | self.serv_cli.request(["SetMotor LWheelDist 5000 RWheelDist 5000 Speed " + str(self.speed_wheel), self.mid_2b_ignored]) 1259 | else: 1260 | self.state_wheel = STATE_FORWARD 1261 | self.speed_wheel = 50 1262 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1263 | self.set_robot_testmode(True) 1264 | self.serv_cli.request(["SetMotor RWheelEnable LWheelEnable\nSetMotor LWheelDist 5000 RWheelDist 5000 Speed " + str(self.speed_wheel), self.mid_2b_ignored]) 1265 | elif key == KEY_DOWN: 1266 | if self.state_wheel == STATE_STOP: 1267 | self.state_wheel = STATE_BACK 1268 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1269 | self.set_robot_testmode(True) 1270 | self.serv_cli.request(["SetMotor RWheelEnable LWheelEnable\nSetMotor LWheelDist -5000 RWheelDist -5000 Speed 100", self.mid_2b_ignored]) 1271 | else: 1272 | self.state_wheel = STATE_STOP 1273 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1274 | self.set_robot_testmode(True) 1275 | self.serv_cli.request(["SetMotor LWheelDisable RWheelDisable", self.mid_2b_ignored]) 1276 | elif key == KEY_LEFT: 1277 | if self.state_wheel == STATE_RIGHT: 1278 | self.state_wheel = STATE_STOP 1279 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1280 | self.set_robot_testmode(True) 1281 | self.serv_cli.request(["SetMotor LWheelDisable RWheelDisable", self.mid_2b_ignored]) 1282 | else: 1283 | self.state_wheel = STATE_LEFT 1284 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1285 | self.set_robot_testmode(True) 1286 | self.serv_cli.request(["SetMotor RWheelEnable LWheelEnable\nSetMotor LWheelDist -2500 RWheelDist 2500 Speed 100", self.mid_2b_ignored]) 1287 | elif key == KEY_RIGHT: 1288 | if self.state_wheel == STATE_LEFT: 1289 | self.state_wheel = STATE_STOP 1290 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1291 | self.set_robot_testmode(True) 1292 | self.serv_cli.request(["SetMotor LWheelDisable RWheelDisable", self.mid_2b_ignored]) 1293 | else: 1294 | self.state_wheel = STATE_RIGHT 1295 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1296 | self.set_robot_testmode(True) 1297 | self.serv_cli.request(["SetMotor RWheelEnable LWheelEnable\nSetMotor LWheelDist 2500 RWheelDist -2500 Speed 100", self.mid_2b_ignored]) 1298 | elif key == KEY_BACK: 1299 | self.state_wheel = STATE_STOP 1300 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1301 | self.set_robot_testmode(True) 1302 | self.serv_cli.request(["SetMotor LWheelDisable RWheelDisable", self.mid_2b_ignored]) 1303 | 1304 | # keypad for navigation 1305 | #def keyup_navigate(self, event): 1306 | # L.info('key up' + event.char) 1307 | def keydown_navigate(self, event): 1308 | if self.wheelctrl_isactive == False: 1309 | return 1310 | #L.info('key down ' + event.char) 1311 | # WSAD 1312 | key = KEY_NONE 1313 | if event.keysym == 'Right': 1314 | L.info('key down: Arrow Right') 1315 | key = KEY_RIGHT 1316 | pass 1317 | elif event.keysym == 'Left': 1318 | L.info('key down: Arrow Left') 1319 | key = KEY_LEFT 1320 | pass 1321 | elif event.keysym == 'Up': 1322 | L.info('key down: Arrow Up') 1323 | key = KEY_UP 1324 | pass 1325 | elif event.keysym == 'Down': 1326 | L.info('key down: Arrow Down') 1327 | key = KEY_DOWN 1328 | pass 1329 | elif event.keysym == 'space' or event.keysym == 'BackSpace' or event.keysym == 'Escape': 1330 | L.info('key down: Esc') 1331 | key = KEY_BACK 1332 | pass 1333 | elif event.char == 'w' or event.char == 'W': 1334 | L.info('key: w') 1335 | key = KEY_UP 1336 | pass 1337 | elif event.char == 's' or event.char == 'S': 1338 | L.info('key: s') 1339 | key = KEY_DOWN 1340 | pass 1341 | elif event.char == 'a' or event.char == 'A': 1342 | L.info('key: a') 1343 | key = KEY_LEFT 1344 | pass 1345 | elif event.char == 'd' or event.char == 'D': 1346 | L.info('key: d') 1347 | key = KEY_RIGHT 1348 | pass 1349 | else: 1350 | L.info('other key down: ' + event.char) 1351 | self.smachine_wheelctrl(key) 1352 | 1353 | def setup_keypad_navigate(self, widget): 1354 | #widget.bind("", self.keyup_navigate) 1355 | widget.bind("", self.keydown_navigate) 1356 | 1357 | # called by GUI when the tab is changed to lidar 1358 | def guiloop_process_lidar(self, cur_focus): 1359 | if self.canvas_lidar_isactive == False: 1360 | self.canvas_lidar_isfocused = cur_focus 1361 | return 1362 | if self.canvas_lidar_isfocused == False: 1363 | if cur_focus == True: 1364 | self.canvas_lidar_isfocused = cur_focus 1365 | self._canvas_lidar_process_focus() 1366 | else: 1367 | if cur_focus == False: 1368 | self.canvas_lidar_isfocused = cur_focus 1369 | self._canvas_lidar_process_focus() 1370 | 1371 | # the periodical routine for the widgets of Sensors 1372 | def buttons_sensors_request(self): 1373 | if self.tab_sensors_isactive: 1374 | if self.serv_cli != None: 1375 | if self.mid_query_digitalsensors < 0 : 1376 | L.info('create mid_query_digitalsensors') 1377 | self.mid_query_digitalsensors = self.mailbox.declair() 1378 | 1379 | if self.mid_query_analogysensors < 0 : 1380 | L.info('create mid_query_analogsensors') 1381 | self.mid_query_analogysensors = self.mailbox.declair() 1382 | 1383 | if self.mid_query_buttonssensors < 0 : 1384 | L.info('create mid_query_buttonssensors') 1385 | self.mid_query_buttonssensors = self.mailbox.declair() 1386 | 1387 | if self.mid_query_motorssensors < 0 : 1388 | L.info('create mid_query_motorssensors') 1389 | self.mid_query_motorssensors = self.mailbox.declair() 1390 | 1391 | if self.mid_query_accelsensors < 0 : 1392 | L.info('create mid_query_accelsensors') 1393 | self.mid_query_accelsensors = self.mailbox.declair() 1394 | 1395 | if self.mid_query_chargersensors < 0 : 1396 | L.info('create mid_query_chargersensors') 1397 | self.mid_query_chargersensors = self.mailbox.declair() 1398 | 1399 | L.debug( 1400 | "sensor flags[digital]: mid=" + str(self.mid_query_digitalsensors) 1401 | + "; req full=" + str(self.sensors_request_full_digital) 1402 | + "; update isopen=" + str(self.sensors_update_isopen_digital) 1403 | + "; update isactive=" + str(self.sensors_update_isactive) 1404 | ) 1405 | if self.mid_query_digitalsensors >= 0 and self.sensors_request_full_digital == False and self.sensors_update_isopen_digital and self.sensors_update_isactive: 1406 | L.info('Request GetDigitalSensors ...') 1407 | self.sensors_request_full_digital = True 1408 | self.serv_cli.request(["GetDigitalSensors\n", self.mid_query_digitalsensors]) 1409 | 1410 | if self.mid_query_analogysensors >= 0 and self.sensors_request_full_analogy == False and self.sensors_update_isopen_analogy and self.sensors_update_isactive: 1411 | L.info('Request GetAnalogSensors ...') 1412 | self.sensors_request_full_analogy = True 1413 | self.serv_cli.request(["GetAnalogSensors\n", self.mid_query_analogysensors]) 1414 | 1415 | if self.mid_query_buttonssensors >= 0 and self.sensors_request_full_buttons == False and self.sensors_update_isopen_buttons and self.sensors_update_isactive: 1416 | L.info('Request GetButtons ...') 1417 | self.sensors_request_full_buttons = True 1418 | self.serv_cli.request(["GetButtons\n", self.mid_query_buttonssensors]) 1419 | 1420 | if self.mid_query_motorssensors >= 0 and self.sensors_request_full_motors == False and self.sensors_update_isopen_motors and self.sensors_update_isactive: 1421 | L.info('Request GetMotors ...') 1422 | self.sensors_request_full_motors = True 1423 | self.serv_cli.request(["GetMotors\n", self.mid_query_motorssensors]) 1424 | 1425 | if self.mid_query_accelsensors >= 0 and self.sensors_request_full_accel == False and self.sensors_update_isopen_accel and self.sensors_update_isactive: 1426 | L.info('Request GetAccel ...') 1427 | self.sensors_request_full_accel = True 1428 | self.serv_cli.request(["GetAccel\n", self.mid_query_accelsensors]) 1429 | 1430 | if self.mid_query_chargersensors >= 0 and self.sensors_request_full_charger == False and self.sensors_update_isopen_charger and self.sensors_update_isactive: 1431 | L.info('Request GetCharger ...') 1432 | self.sensors_request_full_charger = True 1433 | self.serv_cli.request(["GetCharger\n", self.mid_query_chargersensors]) 1434 | 1435 | #L.info('setup next call buttons_sensors_request ...') 1436 | self.after(500, self.buttons_sensors_request) 1437 | 1438 | # the periodical routine for the widgets of LiDAR 1439 | def canvas_lidar_request(self): 1440 | if self.serv_cli != None and self.mid_query_lidar >= 0: 1441 | if self.canvas_lidar_isfocused and self.canvas_lidar_request_full == False: 1442 | self.serv_cli.request(["GetLDSScan\n", self.mid_query_lidar]) 1443 | self.canvas_lidar_request_full = True 1444 | 1445 | if self.canvas_lidar_isfocused and self.canvas_lidar_isactive: 1446 | self.after(300, self.canvas_lidar_request) 1447 | 1448 | def _canvas_lidar_process_focus(self): 1449 | if self.canvas_lidar_isfocused == True: 1450 | self.set_robot_testmode(True) 1451 | if self.serv_cli != None: 1452 | if self.mid_query_lidar < 0 : 1453 | L.info('LiDAR canvas focus <---') 1454 | self.mid_query_lidar = self.mailbox.declair() 1455 | self.canvas_lidar_request() 1456 | #else: 1457 | #self.canvas_lidar_isactive = False 1458 | #self.mailbox.close(self.mid_query_lidar) 1459 | 1460 | def mailpipe_process_schedule(self): 1461 | mid = self.mid_query_schedule 1462 | if self.serv_cli != None and mid >= 0: 1463 | try: 1464 | pre=None 1465 | while True: 1466 | # remove all of items in the queue 1467 | try: 1468 | respstr = self.mailbox.get(mid, False) 1469 | if respstr == None: 1470 | break 1471 | L.info('schedule data pulled out!') 1472 | pre = respstr 1473 | except queue.Empty: 1474 | # ignore 1475 | break 1476 | respstr = pre 1477 | if respstr == None: 1478 | return 1479 | 1480 | self.etv_schedule.updateSchedule(respstr) 1481 | if respstr.find("Schedule is Enabled") >= 0: 1482 | self.setup_schedule_enable(True) 1483 | else: 1484 | self.setup_schedule_enable(False) 1485 | 1486 | L.info('Schedule updated!') 1487 | return True 1488 | except queue.Empty: 1489 | # ignore 1490 | pass 1491 | return False 1492 | 1493 | def _process_treeview_sensors(self, trtype, mid): 1494 | if self.serv_cli != None and mid >= 0: 1495 | try: 1496 | pre=None 1497 | while True: 1498 | # remove all of items in the queue 1499 | try: 1500 | respstr = self.mailbox.get(mid, False) 1501 | if respstr == None: 1502 | break 1503 | L.info('sensors data pulled out!') 1504 | pre = respstr 1505 | except queue.Empty: 1506 | # ignore 1507 | break 1508 | respstr = pre 1509 | if respstr == None: 1510 | return 1511 | 1512 | if trtype == "digital": 1513 | self.sensor_tree_status.updateDigitalSensors(respstr) 1514 | elif trtype == "analogy": 1515 | self.sensor_tree_status.updateAnalogSensors(respstr) 1516 | elif trtype == "buttons": 1517 | self.sensor_tree_status.updateButtons(respstr) 1518 | elif trtype == "motors": 1519 | self.sensor_tree_status.updateMotors(respstr) 1520 | elif trtype == "accel": 1521 | self.sensor_tree_status.updateAccel(respstr) 1522 | elif trtype == "charger": 1523 | self.sensor_tree_status.updateCharger(respstr) 1524 | 1525 | L.info('digital sensors updated!') 1526 | return True 1527 | except queue.Empty: 1528 | # ignore 1529 | pass 1530 | return False 1531 | 1532 | def mailpipe_process_digitalsensors(self): 1533 | if self._process_treeview_sensors("digital", self.mid_query_digitalsensors): 1534 | self.sensors_request_full_digital = False 1535 | if self._process_treeview_sensors("analogy", self.mid_query_analogysensors): 1536 | self.sensors_request_full_analogy = False 1537 | if self._process_treeview_sensors("buttons", self.mid_query_buttonssensors): 1538 | self.sensors_request_full_buttons = False 1539 | if self._process_treeview_sensors("motors", self.mid_query_motorssensors): 1540 | self.sensors_request_full_motors = False 1541 | if self._process_treeview_sensors("accel", self.mid_query_accelsensors): 1542 | self.sensors_request_full_accel = False 1543 | if self._process_treeview_sensors("charger", self.mid_query_chargersensors): 1544 | self.sensors_request_full_charger = False 1545 | 1546 | def mailpipe_process_lidar(self): 1547 | if self.serv_cli != None and self.mid_query_lidar >= 0: 1548 | try: 1549 | pre=None 1550 | while True: 1551 | # remove all of items in the queue 1552 | try: 1553 | respstr = self.mailbox.get(self.mid_query_lidar, False) 1554 | if respstr == None: 1555 | break 1556 | L.info('LiDAR data pulled out!') 1557 | pre = respstr 1558 | except queue.Empty: 1559 | # ignore 1560 | break 1561 | respstr = pre 1562 | if respstr == None: 1563 | return 1564 | width = self.canvas_lidar.winfo_width() 1565 | height = self.canvas_lidar.winfo_height() 1566 | MAXCOOD = height 1567 | if width < height: 1568 | MAXCOOD = width 1569 | MAXCOOD = int(MAXCOOD / 2) 1570 | MAXCOODX = int(width / 2) 1571 | MAXCOODY = int(height / 2) 1572 | CIRRAD=2 1573 | if 1 == 1: 1574 | #self.canvas_lidar.xview_scroll(width, "units") 1575 | #self.canvas_lidar.yview_scroll(height, "units") 1576 | self.canvas_lidar.configure(scrollregion=(0-MAXCOODX, 0-MAXCOODY, MAXCOODX, MAXCOODY)) 1577 | MAXCOODX = 0 1578 | MAXCOODY = 0 1579 | #L.info('LiDAR canvas sz=(' + str(width) + ", " + str(height) + "), maxcood=(" + str(MAXCOODX) + ", " + str(MAXCOODY) + ") " + str(MAXCOOD)) 1580 | 1581 | retlines = respstr.strip() + '\n' 1582 | responses = retlines.split('\n') 1583 | for i in range(0,len(responses)): 1584 | response = responses[i].strip() 1585 | if len(response) < 1: 1586 | break 1587 | lst = response.split(',') 1588 | if len(lst) < 4: 1589 | continue 1590 | if lst[0].lower() == 'AngleInDegrees'.lower(): 1591 | continue 1592 | angle = int(lst[0]) 1593 | if angle < 0 or angle > 359: 1594 | continue 1595 | distmm = int(lst[1]) 1596 | intensity = int(lst[2]) 1597 | #errval = lst[3] 1598 | #if distmm > 1600: 1599 | # distmm = MAXDIST 1600 | #if errval != "0": 1601 | # distmm = MAXDIST 1602 | #L.info('LiDAR angle=' + str(angle) + ", dist=" + str(distmm) + ", intensity=" + str(intensity) ) 1603 | 1604 | if distmm == 0: 1605 | posx = MAXCOODX 1606 | posy = MAXCOODY 1607 | else: 1608 | off = distmm * MAXCOOD 1609 | posx = MAXCOODX + off * self.map_cos_lidar[angle] 1610 | posy = MAXCOODY - off * self.map_sin_lidar[angle] 1611 | #L.info('LiDAR angle=' + str(angle) + ", pos=(" + str(posx) + "," + str(posy) +")" ) 1612 | 1613 | #save to the list 1614 | if angle in self.canvas_lidar_lines: 1615 | # update 1616 | i = self.canvas_lidar_lines[angle] 1617 | self.canvas_lidar.coords(i, MAXCOODX, MAXCOODY, posx, posy) 1618 | else: 1619 | # create a new line 1620 | i = self.canvas_lidar.create_line(MAXCOODX, MAXCOODY, posx, posy, fill="red", dash=(4, 4)) 1621 | self.canvas_lidar_lines[angle] = i 1622 | 1623 | if angle in self.canvas_lidar_points: 1624 | # update 1625 | i = self.canvas_lidar_points[angle] 1626 | self.canvas_lidar.coords(i, posx - CIRRAD, posy - CIRRAD, posx + CIRRAD, posy + CIRRAD) 1627 | else: 1628 | # create a new line 1629 | i = self.canvas_lidar.create_oval(posx - CIRRAD, posy - CIRRAD, posx + CIRRAD, posy + CIRRAD, outline="green", fill="green", width=1) 1630 | #i = self.canvas_lidar.create_circle(posx, posy, CIRRAD, outline="green", fill="green", width=1) 1631 | self.canvas_lidar_points[angle] = i 1632 | 1633 | L.info('LiDAR canvas updated!') 1634 | except queue.Empty: 1635 | # ignore 1636 | pass 1637 | self.canvas_lidar_request_full = False 1638 | 1639 | # 1640 | # connection and command: support functions 1641 | # 1642 | def do_select_clicmd(self, event): 1643 | self.combobox_cli_command.select_range(0, tk.END) 1644 | return 1645 | 1646 | # the req is a list 1647 | def cb_task_cli(self, tid, req): 1648 | L.debug("do task: tid=" + str(tid) + ", req=" + str(req)) 1649 | reqstr = req[0] 1650 | try: 1651 | resp = self.serv_cli.get_request_block(reqstr) 1652 | if resp != None: 1653 | if resp.strip() != "": 1654 | self.mailbox.put(req[1], resp.strip()) 1655 | except ConnectionResetError: 1656 | self.mailbox.put(self.mid_socket_disconnect, "ConnectionResetError") 1657 | 1658 | return 1659 | 1660 | def mailpipe_process_socket_error(self): 1661 | 1662 | if self.mid_socket_disconnect >= 0: 1663 | try: 1664 | pre=None 1665 | while True: 1666 | # remove all of items in the queue 1667 | try: 1668 | respstr = self.mailbox.get(self.mid_socket_disconnect, False) 1669 | if respstr == None: 1670 | break 1671 | L.info('Socket error pulled out!') 1672 | pre = respstr 1673 | 1674 | except queue.Empty: 1675 | # ignore 1676 | break 1677 | 1678 | respstr = pre 1679 | if respstr == None: 1680 | return 1681 | 1682 | L.info('LiDAR canvas updated!') 1683 | except queue.Empty: 1684 | # ignore 1685 | pass 1686 | 1687 | self.do_cli_disconnect() 1688 | 1689 | 1690 | def do_cli_connect(self): 1691 | self.do_cli_disconnect() 1692 | L.info('client connect ...') 1693 | L.info('connect to ' + self.client_port.get()) 1694 | self.serv_cli = neatocmdapi.NCIService(target=self.client_port.get().strip(), timeout=2) 1695 | if self.serv_cli.open(self.cb_task_cli) == False: 1696 | L.error ('Error in open serial') 1697 | return 1698 | L.info ('serial opened') 1699 | 1700 | self.mid_2b_ignored = self.mailbox.declair() # the index of the return data Queue for any command that don't need to be parsed the data 1701 | self.mid_cli_command = self.mailbox.declair() # the index of the return data Queue for 'Commands' tab 1702 | self.mid_query_version = self.mailbox.declair() # the index of the return data Queue for version textarea 1703 | self.mid_query_time = self.mailbox.declair() # the index of the return data Queue for robot time label 1704 | self.mid_query_battery = self.mailbox.declair() # the index of the return data Queue for robot battery % ratio 1705 | self.mid_query_schedule = self.mailbox.declair() 1706 | 1707 | self.serv_cli.request(["GetVersion", self.mid_query_version]) 1708 | self.serv_cli.request(["GetWarranty", self.mid_query_version]) 1709 | self.guiloop_get_schedule() 1710 | self.guiloop_check_rightnow() 1711 | self.guiloop_check_per1sec() 1712 | self.guiloop_check_per30sec() 1713 | self.btn_cli_connect.config(state=tk.DISABLED) 1714 | self.btn_cli_disconnect.config(state=tk.NORMAL) 1715 | L.info('do_cli_connect() DONE') 1716 | return 1717 | 1718 | def do_cli_disconnect(self): 1719 | import time 1720 | 1721 | L.info('do_cli_disconnect() ...') 1722 | if self.serv_cli != None: 1723 | self.set_robot_testmode(False) 1724 | time.sleep(1) 1725 | self.serv_cli.close() 1726 | else: 1727 | L.info('client is not connected, skip.') 1728 | 1729 | self.serv_cli = None 1730 | self.mid_2b_ignored = -1 1731 | self.mid_cli_command = -1 1732 | self.mid_query_version = -1 1733 | self.mid_query_time = -1 1734 | self.mid_query_battery = -1 1735 | self.mid_query_lidar = -1 1736 | self.mid_query_digitalsensors = -1 1737 | self.mid_query_analogysensors = -1 1738 | self.mid_query_buttonssensors = -1 1739 | self.mid_query_motorssensors = -1 1740 | self.mid_query_accelsensors = -1 1741 | self.mid_query_chargersensors = -1 1742 | self.mid_query_schedule = -1 1743 | 1744 | # flag to signal the command is finished 1745 | self.canvas_lidar_request_full = False 1746 | self.sensors_request_full_digital = False 1747 | self.sensors_request_full_analogy = False 1748 | self.sensors_request_full_buttons = False 1749 | self.sensors_request_full_motors = False 1750 | self.sensors_request_full_accel = False 1751 | self.sensors_request_full_charger = False 1752 | 1753 | self.btn_cli_connect.config(state=tk.NORMAL) 1754 | self.btn_cli_disconnect.config(state=tk.DISABLED) 1755 | L.info('do_cli_disconnect() DONE') 1756 | 1757 | def do_cli_run(self): 1758 | if self.serv_cli == None: 1759 | L.error('client is not connected, please connect it first!') 1760 | return 1761 | L.info('client run ...') 1762 | reqstr = self.cli_command.get().strip() 1763 | if reqstr != "": 1764 | self.serv_cli.request([reqstr, self.mid_cli_command]) 1765 | return 1766 | 1767 | def do_cli_run_ev(self, event): 1768 | self.do_cli_run() 1769 | return 1770 | 1771 | def mailpipe_process_conn_cmd(self): 1772 | 1773 | if self.mid_2b_ignored >= 0: 1774 | try: 1775 | while True: 1776 | # remove all of items in the queue 1777 | try: 1778 | respstr = self.mailbox.get(self.mid_2b_ignored, False) 1779 | if respstr == None: 1780 | break 1781 | L.info('ignore the response: ' + respstr) 1782 | except queue.Empty: 1783 | break 1784 | except queue.Empty: 1785 | # ignore 1786 | pass 1787 | 1788 | if self.mid_cli_command >= 0: 1789 | try: 1790 | resp = self.mailbox.get(self.mid_cli_command, False) 1791 | respstr = resp.strip() + "\n\n" 1792 | # put the content to the end of the textarea 1793 | guilog.textarea_append (self.text_cli_command, respstr) 1794 | self.text_cli_command.update_idletasks() 1795 | except queue.Empty: 1796 | # ignore 1797 | pass 1798 | 1799 | if self.mid_query_version >= 0: 1800 | try: 1801 | resp = self.mailbox.get(self.mid_query_version, False) 1802 | respstr = resp.strip() 1803 | self.show_robot_version (respstr) 1804 | except queue.Empty: 1805 | # ignore 1806 | pass 1807 | 1808 | if self.mid_query_battery >= 0: 1809 | try: 1810 | while True: 1811 | respstr = self.mailbox.get(self.mid_query_battery, False) 1812 | if respstr == None: 1813 | break 1814 | retlines = respstr.strip() + '\n' 1815 | responses = retlines.split('\n') 1816 | for i in range(0,len(responses)): 1817 | response = responses[i].strip() 1818 | if len(response) < 1: 1819 | #L.debug('read null 2') 1820 | break 1821 | lst = response.split(',') 1822 | if len(lst) > 1: 1823 | if lst[0].lower() == 'FuelPercent'.lower(): 1824 | L.debug('got fule percent: ' + lst[1]) 1825 | self.show_battery_level(int(lst[1])) 1826 | except queue.Empty: 1827 | # ignore 1828 | pass 1829 | 1830 | if self.mid_query_time >= 0: 1831 | import re 1832 | try: 1833 | while True: 1834 | respstr = self.mailbox.get(self.mid_query_time, False) 1835 | if respstr == None: 1836 | break 1837 | retlines = respstr.strip() 1838 | retlines = respstr.strip() + '\n' 1839 | responses = retlines.split('\n') 1840 | for i in range(0,len(responses)): 1841 | response = responses[i].strip() 1842 | if len(response) < 1: 1843 | #L.debug('read null 2') 1844 | break 1845 | lst1 = response.split(' ') 1846 | if lst1[0] in {'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',}: 1847 | L.debug("gettime: " + response) 1848 | self.show_robot_time(response) 1849 | except queue.Empty: 1850 | # ignore 1851 | pass 1852 | 1853 | def guiloop_check_rightnow(self): 1854 | if self.serv_cli != None: 1855 | self.mailpipe_process_conn_cmd() 1856 | self.mailpipe_process_lidar() 1857 | self.mailpipe_process_digitalsensors() 1858 | self.mailpipe_process_schedule() 1859 | self.mailpipe_process_socket_error() 1860 | # setup next 1861 | self.after(100, self.guiloop_check_rightnow) 1862 | return 1863 | 1864 | def guiloop_check_per1sec(self): 1865 | if self.serv_cli != None: 1866 | # setup next 1867 | if self.status_isactive == True: 1868 | self.serv_cli.request(["GetTime", self.mid_query_time]) # query the time 1869 | self.after(5000, self.guiloop_check_per1sec) 1870 | return 1871 | 1872 | def guiloop_check_per30sec(self): 1873 | if self.serv_cli != None: 1874 | # setup next 1875 | if self.status_isactive == True: 1876 | self.serv_cli.request(["GetCharger", self.mid_query_battery]) # query the level of battery 1877 | self.after(30000, self.guiloop_check_per30sec) 1878 | return 1879 | 1880 | def set_robot_time_from_pc(self): 1881 | if self.serv_cli == None: 1882 | L.error('client is not connected, please connect it first!') 1883 | return 1884 | import time 1885 | tm_now = time.localtime() 1886 | cmdstr = time.strftime("SetTime Day %w Hour %H Min %M Sec %S", tm_now) 1887 | self.serv_cli.request([cmdstr, self.mid_2b_ignored]) 1888 | 1889 | def set_robot_testmode (self, istest = False): 1890 | L.info('call set_robot_testmode("' + str(istest) + '")!') 1891 | if self.istestmode != istest: 1892 | if self.serv_cli != None and self.mid_2b_ignored >= 0: 1893 | if istest: 1894 | self.serv_cli.request(["TestMode On", self.mid_2b_ignored]) 1895 | else: 1896 | self.serv_cli.request(["SetLDSRotation Off\nSetMotor LWheelDisable RWheelDisable BrushDisable VacuumOff\nTestMode Off", self.mid_2b_ignored]) 1897 | self.istestmode = istest 1898 | self.show_robot_testmode(istest) 1899 | 1900 | def nxvcontrol_main(): 1901 | guilog.set_log_stderr() 1902 | 1903 | gettext_init() 1904 | 1905 | root = tk.Tk() 1906 | root.title(str_progname + " - " + str_version) 1907 | 1908 | #nb.pack(expand=1, fill="both") 1909 | MyTkAppFrame(root).pack(fill="both", expand=True) 1910 | #ttk.Sizegrip(root).grid(column=999, row=999, sticky=(tk.S,tk.E)) 1911 | ttk.Sizegrip(root).pack(side="right") 1912 | root.mainloop() 1913 | 1914 | if __name__ == "__main__": 1915 | nxvcontrol_main() 1916 | -------------------------------------------------------------------------------- /nxvforward.bat: -------------------------------------------------------------------------------- 1 | 2 | rem set LANG=en_US 3 | rem set LANG=zh_CN 4 | 5 | python nxvforward.py 6 | -------------------------------------------------------------------------------- /nxvforward.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf8 3 | # 4 | # Copyright 2016-2017 Yunhui Fu 5 | # 6 | # This program is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License version 3, as published 8 | # by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranties of 12 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 13 | # PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program. If not, see . 17 | # 18 | # For further info, check https://github.com/yhfudev/python-nxvcontrol.git 19 | 20 | try: 21 | import Tkinter as tk 22 | import ttk 23 | import ScrolledText 24 | import tkFileDialog as fd 25 | import Queue 26 | except ImportError: 27 | import tkinter as tk 28 | from tkinter import ttk 29 | from tkinter.scrolledtext import ScrolledText 30 | import tkinter.filedialog as fd 31 | import multiprocessing 32 | from multiprocessing import Queue 33 | 34 | import os 35 | import sys 36 | import importlib.util as importutil 37 | #if None != importlib.find_loader("intl"): 38 | #if None != importutil.find_spec("intl"): 39 | 40 | import time 41 | from threading import Thread 42 | import queue 43 | 44 | PROGRAM_PREFIX = os.path.basename(__file__).split('.')[0] 45 | 46 | import logging as L 47 | L.basicConfig(filename=PROGRAM_PREFIX+'.log', level=L.DEBUG, format='%(asctime)s %(levelname)s: %(message)s') 48 | # if we use the textarea to output log. There's problem when multiple threads output to the same textarea 49 | config_use_textarea_log=False 50 | 51 | import neatocmdapi 52 | import guilog 53 | 54 | import locale 55 | import gettext 56 | _=gettext.gettext 57 | 58 | APP_NAME="nxvcontrol" 59 | def gettext_init(): 60 | global _ 61 | langs = [] 62 | 63 | language = os.environ.get('LANG', None) 64 | if (language): 65 | langs += language.split(":") 66 | language = os.environ.get('LANGUAGE', None) 67 | if (language): 68 | langs += language.split(":") 69 | lc, encoding = locale.getdefaultlocale() 70 | if (lc): 71 | langs += [lc] 72 | # we know that we have 73 | langs += ["en_US", "zh_CN"] 74 | local_path = os.path.realpath(os.path.dirname(sys.argv[0])) 75 | local_path = "languages/" 76 | gettext.bindtextdomain(APP_NAME, local_path) 77 | gettext.textdomain(APP_NAME) 78 | lang = gettext.translation(APP_NAME, local_path, languages=langs, fallback = True) 79 | #_=gettext.gettext 80 | _=lang.gettext 81 | L.debug("local=" + str(lc) + ", encoding=" + str(encoding) + ", langs=" + str(langs) + ", lang=" + str(lang) ) 82 | 83 | str_progname=_("nxvForward") 84 | str_version="0.1" 85 | 86 | LARGE_FONT= ("Verdana", 18) 87 | NORM_FONT = ("Helvetica", 12) 88 | SMALL_FONT = ("Helvetica", 8) 89 | 90 | def set_readonly_text(text, msg): 91 | text.config(state=tk.NORMAL) 92 | text.delete(1.0, tk.END) 93 | text.insert(tk.END, msg) 94 | text.config(state=tk.DISABLED) 95 | 96 | class MyTkAppFrame(ttk.Notebook): 97 | 98 | # the req is a list 99 | def cb_task(self, tid, req): 100 | L.debug("do task: tid=" + str(tid) + ", req=" + str(req)) 101 | reqstr = req[0] 102 | resp = self.serv.get_request_block(reqstr) 103 | if resp != None: 104 | if resp.strip() != "": 105 | self.mymailbox.mailbox_serv.put(req[1], resp) 106 | 107 | def do_stop(self): 108 | isrun = False; 109 | if self.runth_svr != None: 110 | if self.runth_svr.isAlive(): 111 | #L.info('signal server to stop ...') 112 | self.server.shutdown() 113 | #L.info('server close ...') 114 | self.server.server_close() 115 | #L.info('server closed.') 116 | isrun = True 117 | if isrun == False: 118 | L.info('server is not running. skip') 119 | if self.serv != None: 120 | self.serv.close() 121 | self.serv = None 122 | 123 | self.btn_svr_start.config(state=tk.NORMAL) 124 | self.btn_svr_stop.config(state=tk.DISABLED) 125 | 126 | #return 127 | 128 | def do_start(self): 129 | import socketserver as ss 130 | import neatocmdsim as nsim 131 | 132 | class ThreadedTCPRequestHandler(ss.BaseRequestHandler): 133 | # override base class handle method 134 | def handle(self): 135 | BUFFER_SIZE = 4096 136 | MAXIUM_SIZE = BUFFER_SIZE * 5 137 | data = "" 138 | L.info("server connectd by client: " + str(self.client_address)) 139 | mbox_id = self.server.mydata.mailbox_serv.declair() 140 | 141 | cli_log_head = "CLI" + str(self.client_address) 142 | while 1: 143 | try: 144 | # receive the requests 145 | recvdat = self.request.recv(BUFFER_SIZE) 146 | if not recvdat: 147 | # EOF, client closed, just return 148 | L.info(cli_log_head + " disconnected: " + str(self.client_address)) 149 | break 150 | data += str(recvdat, 'ascii') 151 | L.debug(cli_log_head + " all of data: " + data) 152 | cntdata = data.count('\n') 153 | L.debug(cli_log_head + " the # of newline: %d"%cntdata) 154 | if (cntdata < 1): 155 | L.debug(cli_log_head + " not receive newline, skip: " + data) 156 | continue 157 | # process the requests after a '\n' 158 | requests = data.split('\n') 159 | for i in range(0, cntdata): 160 | # for each line: 161 | request = requests[i].strip() 162 | L.info(cli_log_head + " request [" + str(i+1) + "/" + str(cntdata) + "] '" + request + "'") 163 | self.server.serv.request ([request, mbox_id]) 164 | response = self.server.mydata.mailbox_serv.get(mbox_id) 165 | if response != "": 166 | L.debug(cli_log_head + 'send data back: sz=' + str(len(response))) 167 | self.request.sendall(bytes(response, 'ascii')) 168 | 169 | data = requests[-1] 170 | 171 | except BrokenPipeError: 172 | L.error (cli_log_head + 'remote closed: ' + str(self.client_address)) 173 | break 174 | except ConnectionResetError: 175 | L.error (cli_log_head + 'remote reset: ' + str(self.client_address)) 176 | break 177 | except Exception as e1: 178 | L.error (cli_log_head + 'Error in read serial: ' + str(e1)) 179 | break 180 | 181 | L.error (cli_log_head + 'close: ' + str(self.client_address)) 182 | self.server.mydata.mailbox_serv.close(mbox_id) 183 | 184 | 185 | # pass the data strcture from main frame to all of subclasses 186 | # mailbox_serv, mailbox_servcli 187 | 188 | class ThreadedTCPServer(ss.ThreadingMixIn, ss.TCPServer): 189 | daemon_threads = True 190 | allow_reuse_address = True 191 | # pass the serv to handler 192 | def __init__(self, host_port_tuple, streamhandler, serv, mydata): 193 | super().__init__(host_port_tuple, streamhandler) 194 | self.serv = serv 195 | self.mydata = mydata 196 | 197 | if self.runth_svr != None: 198 | if self.runth_svr.isAlive(): 199 | L.info('server is already running. skip') 200 | return True 201 | 202 | L.info('connect to ' + self.conn_port.get()) 203 | self.serv = neatocmdapi.NCIService(target=self.conn_port.get().strip(), timeout=0.5) 204 | if self.serv.open(self.cb_task) == False: 205 | L.error ('Error in open serial') 206 | return False 207 | 208 | L.info('start server ' + self.bind_port.get()) 209 | b = self.bind_port.get().split(":") 210 | L.info('b=' + str(b)) 211 | HOST=b[0] 212 | PORT=3333 213 | if len(b) > 1: 214 | PORT=int(b[1]) 215 | L.info('server is running ...') 216 | try: 217 | self.server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler, self.serv, self.mymailbox) 218 | except Exception as e: 219 | L.error("Error in starting service: " + str(e)) 220 | return False 221 | ip, port = self.server.server_address 222 | L.info("server listened to: " + str(ip) + ":" + str(port)) 223 | self.runth_svr = Thread(target=self.server.serve_forever) 224 | self.runth_svr.setDaemon(True) # When closing the main thread, which is our GUI, all daemons will automatically be stopped as well. 225 | self.runth_svr.start() 226 | 227 | self.btn_svr_start.config(state=tk.DISABLED) 228 | self.btn_svr_stop.config(state=tk.NORMAL) 229 | L.info('server started.') 230 | return True 231 | 232 | def get_log_text(self): 233 | return self.text_log 234 | 235 | def __init__(self, tk_frame_parent): 236 | global config_use_textarea_log 237 | ttk.Notebook.__init__(self, tk_frame_parent) 238 | nb = self 239 | self.runth_svr = None 240 | self.serv = None 241 | self.serv_cli = None 242 | 243 | class MyMailbox(object): 244 | "mymailbox" 245 | def __init__(self): 246 | self.mailbox_serv = neatocmdapi.MailPipe() 247 | self.mailbox_servcli = neatocmdapi.MailPipe() 248 | 249 | self.mymailbox = MyMailbox() 250 | 251 | guilog.rClickbinder(tk_frame_parent) 252 | 253 | # page for About 254 | page_about = ttk.Frame(nb) 255 | lbl_about_head = tk.Label(page_about, text=_("About"), font=LARGE_FONT) 256 | lbl_about_head.pack(side="top", fill="x", pady=10) 257 | lbl_about_main = tk.Label(page_about 258 | , font=NORM_FONT 259 | , text="\n" + str_progname + "\n" + str_version + "\n" 260 | + _("Forward Neato XV control over network") + "\n" 261 | + "\n" 262 | + _("Copyright © 2015–2016 The nxvForward Authors") + "\n" 263 | + "\n" 264 | + _("This program comes with absolutely no warranty.") + "\n" 265 | + _("See the GNU General Public License, version 3 or later for details.") 266 | ) 267 | lbl_about_main.pack(side="top", fill="x", pady=10) 268 | 269 | # page for server 270 | page_server = ttk.Frame(nb) 271 | lbl_svr_head = tk.Label(page_server, text=_("Server"), font=LARGE_FONT) 272 | lbl_svr_head.pack(side="top", fill="x", pady=10) 273 | 274 | frame_svr = ttk.LabelFrame(page_server, text=_("Setup")) 275 | 276 | line=0 277 | bind_port_history = ('localhost:3333', '127.0.0.1:4444', '0.0.0.0:3333') 278 | self.bind_port = tk.StringVar() 279 | lbl_svr_port = tk.Label(frame_svr, text=_("Bind Address:")) 280 | lbl_svr_port.grid(row=line, column=0, padx=5, sticky=tk.N+tk.S+tk.W) 281 | combobox_bind_port = ttk.Combobox(frame_svr, textvariable=self.bind_port) 282 | combobox_bind_port['values'] = bind_port_history 283 | combobox_bind_port.grid(row=line, column=1, padx=5, pady=5, sticky=tk.N+tk.S+tk.W) 284 | combobox_bind_port.current(0) 285 | 286 | line += 1 287 | conn_port_history = ('dev://ttyACM0:115200', 'dev://ttyUSB0:115200', 'dev://COM11:115200', 'dev://COM12:115200', 'sim:', 'tcp://localhost:3333', 'tcp://192.168.3.163:3333') 288 | self.conn_port = tk.StringVar() 289 | lbl_svr_port = tk.Label(frame_svr, text=_("Connect to:")) 290 | lbl_svr_port.grid(row=line, column=0, padx=5, sticky=tk.N+tk.S+tk.W) 291 | combobox_conn_port = ttk.Combobox(frame_svr, textvariable=self.conn_port) 292 | combobox_conn_port['values'] = conn_port_history 293 | combobox_conn_port.grid(row=line, column=1, padx=5, pady=5, sticky=tk.N+tk.S+tk.W) 294 | combobox_conn_port.current(0) 295 | 296 | line -= 1 297 | self.btn_svr_start = tk.Button(frame_svr, text=_("Start"), command=self.do_start) 298 | self.btn_svr_start.grid(row=line, column=2, padx=5, sticky=tk.N+tk.S+tk.W+tk.E) 299 | #self.btn_svr_start.pack(side="right", fill="both", padx=5, pady=5, expand=True) 300 | 301 | line += 1 302 | self.btn_svr_stop = tk.Button(frame_svr, text=_("Stop"), command=self.do_stop) 303 | self.btn_svr_stop.grid(row=line, column=2, padx=5, sticky=tk.N+tk.S+tk.W+tk.E) 304 | #self.btn_svr_stop.pack(side="right", fill="both", padx=5, pady=5, expand=True) 305 | 306 | frame_svr.pack(side="top", fill="x", pady=10) 307 | 308 | self.text_log = None 309 | if config_use_textarea_log: 310 | self.text_log = tk.scrolledtext.ScrolledText(page_server, wrap=tk.WORD, height=1) 311 | self.text_log.configure(state='disabled') 312 | self.text_log.pack(expand=True, fill="both", side="top") 313 | #self.text_log.grid(row=line, column=1, columnspan=2, padx=5) 314 | self.text_log.bind("<1>", lambda event: self.text_log.focus_set()) # enable highlighting and copying 315 | #set_readonly_text(self.text_log, "Version Info\nver 1\nver 2\n") 316 | 317 | btn_log_clear = tk.Button(page_server, text=_("Clear"), command=lambda: (set_readonly_text(self.text_log, ""), self.text_log.update_idletasks())) 318 | #btn_log_clear.grid(row=line, column=0, columnspan=2, padx=5, sticky=tk.N+tk.S+tk.W+tk.E) 319 | btn_log_clear.pack(side="right", fill="both", padx=5, pady=5, expand=True) 320 | 321 | # page for client 322 | page_client = ttk.Frame(nb) 323 | lbl_cli_head = tk.Label(page_client, text=_("Test Client"), font=LARGE_FONT) 324 | lbl_cli_head.pack(side="top", fill="x", pady=10) 325 | 326 | frame_cli = ttk.LabelFrame(page_client, text=_("Connection")) 327 | 328 | line=0 329 | client_port_history = ('tcp://192.168.3.163:3333', 'dev://ttyACM0:115200', 'dev://ttyUSB0:115200', 'dev://COM11:115200', 'dev://COM12:115200', 'sim:', 'tcp://localhost:3333') 330 | self.client_port = tk.StringVar() 331 | lbl_cli_port = tk.Label(frame_cli, text=_("Connect to:")) 332 | lbl_cli_port.grid(row=line, column=0, padx=5, sticky=tk.N+tk.S+tk.W) 333 | combobox_client_port = ttk.Combobox(frame_cli, textvariable=self.client_port) 334 | combobox_client_port['values'] = client_port_history 335 | combobox_client_port.grid(row=line, column=1, padx=5, pady=5, sticky=tk.N+tk.S+tk.W) 336 | combobox_client_port.current(0) 337 | 338 | self.btn_cli_connect = tk.Button(frame_cli, text=_("Connect"), command=self.do_cli_connect) 339 | self.btn_cli_connect.grid(row=line, column=2, columnspan=1, padx=5, sticky=tk.N+tk.S+tk.W+tk.E) 340 | #self.btn_cli_connect.pack(side="left", fill="both", padx=5, pady=5, expand=True) 341 | 342 | self.btn_cli_disconnect = tk.Button(frame_cli, text=_("Disconnect"), command=self.do_cli_disconnect) 343 | self.btn_cli_disconnect.grid(row=line, column=3, columnspan=1, padx=5, sticky=tk.N+tk.S+tk.W+tk.E) 344 | #self.btn_cli_disconnect.pack(side="left", fill="both", padx=5, pady=5, expand=True) 345 | 346 | frame_cli.pack(side="top", fill="x", pady=10) 347 | 348 | page_command = page_client 349 | frame_top = tk.Frame(page_command)#, background="green") 350 | frame_bottom = tk.Frame(page_command)#, background="yellow") 351 | frame_top.pack(side="top", fill="both", expand=True) 352 | frame_bottom.pack(side="bottom", fill="x", expand=False) 353 | 354 | self.text_cli_command = ScrolledText(frame_top, wrap=tk.WORD) 355 | #self.text_cli_command.insert(tk.END, "Some Text\ntest 1\ntest 2\n") 356 | self.text_cli_command.configure(state='disabled') 357 | self.text_cli_command.pack(expand=True, fill="both", side="top") 358 | # make sure the widget gets focus when clicked 359 | # on, to enable highlighting and copying to the 360 | # clipboard. 361 | self.text_cli_command.bind("<1>", lambda event: self.text_cli_command.focus_set()) 362 | 363 | btn_clear_cli_command = tk.Button(frame_bottom, text=_("Clear"), command=lambda: (set_readonly_text(self.text_cli_command, ""), self.text_cli_command.update_idletasks()) ) 364 | btn_clear_cli_command.pack(side="left", fill="x", padx=5, pady=5, expand=False) 365 | self.cli_command = tk.StringVar() 366 | self.combobox_cli_command = ttk.Combobox(frame_bottom, textvariable=self.cli_command) 367 | self.combobox_cli_command['values'] = ('Help', 'GetAccel', 'GetButtons', 'GetCalInfo', 'GetCharger', 'GetDigitalSensors', 'GetErr', 'GetLDSScan', 'GetLifeStatLog', 'GetMotors', 'GetSchedule', 'GetTime', 'GetVersion', 'GetWarranty', 'PlaySound 0', 'Clean House', 'DiagTest MoveAndBump', 'DiagTest DropTest', 'RestoreDefaults', 'SetDistanceCal DropMinimum', 'SetFuelGauge Percent 100', 'SetIEC FloorSelection carpet', 'SetLCD BGWhite', 'SetLDSRotation On', 'SetLED BacklightOn', 'SetMotor VacuumOn', 'SetSchedule Day Sunday Hour 17 Min 0 House ON', 'SetSystemMode Shutdown', 'SetTime Day Sunday Hour 12 Min 5 Sec 25', 'SetWallFollower Enable', 'TestMode On', 'Upload' ) 368 | self.combobox_cli_command.pack(side="left", fill="both", padx=5, pady=5, expand=True) 369 | self.combobox_cli_command.bind("", self.do_cli_run_ev) 370 | self.combobox_cli_command.bind("<>", self.do_select_clicmd) 371 | self.combobox_cli_command.current(0) 372 | btn_run_cli_command = tk.Button(frame_bottom, text=_("Run"), command=self.do_cli_run) 373 | btn_run_cli_command.pack(side="right", fill="x", padx=5, pady=5, expand=False) 374 | 375 | self.check_mid_cli_command() 376 | 377 | 378 | # last 379 | nb.add(page_server, text=_("Server")) 380 | nb.add(page_client, text=_("Test Client")) 381 | nb.add(page_about, text=_("About")) 382 | combobox_bind_port.focus() 383 | 384 | self.do_stop() 385 | self.do_cli_disconnect() 386 | return 387 | 388 | # 389 | # connection and command: support functions 390 | # 391 | def do_select_clicmd(self, event): 392 | self.combobox_cli_command.select_range(0, tk.END) 393 | return 394 | 395 | # the req is a list 396 | def cb_task_cli(self, tid, req): 397 | L.debug("do task: tid=" + str(tid) + ", req=" + str(req)) 398 | reqstr = req[0] 399 | resp = self.serv_cli.get_request_block(reqstr) 400 | if resp != None: 401 | if resp.strip() != "": 402 | self.mymailbox.mailbox_servcli.put(req[1], resp.strip()) 403 | return 404 | 405 | def do_cli_connect(self): 406 | self.do_cli_disconnect() 407 | L.info('client connect ...') 408 | L.info('connect to ' + self.client_port.get()) 409 | self.serv_cli = neatocmdapi.NCIService(target=self.client_port.get().strip(), timeout=0.5) 410 | if self.serv_cli.open(self.cb_task_cli) == False: 411 | L.error ('Error in open serial') 412 | return 413 | self.mid_cli_command = self.mymailbox.mailbox_servcli.declair(); 414 | L.info ('serial opened') 415 | self.btn_cli_connect.config(state=tk.DISABLED) 416 | self.btn_cli_disconnect.config(state=tk.NORMAL) 417 | return 418 | 419 | def do_cli_disconnect(self): 420 | if self.serv_cli != None: 421 | L.info('client disconnect ...') 422 | self.serv_cli.close() 423 | else: 424 | L.info('client is not connected, skip.') 425 | self.serv_cli = None 426 | self.mid_cli_command = -1; 427 | self.btn_cli_connect.config(state=tk.NORMAL) 428 | self.btn_cli_disconnect.config(state=tk.DISABLED) 429 | return 430 | 431 | def do_cli_run(self): 432 | if self.serv_cli == None: 433 | L.error('client is not connected, please connect it first!') 434 | return 435 | L.info('client run ...') 436 | reqstr = self.cli_command.get().strip() 437 | if reqstr != "": 438 | self.serv_cli.request([reqstr, self.mid_cli_command]) 439 | return 440 | 441 | def do_cli_run_ev(self, event): 442 | self.do_cli_run() 443 | return 444 | 445 | def check_mid_cli_command(self): 446 | if self.serv_cli != None and self.mid_cli_command >= 0: 447 | try: 448 | resp = self.mymailbox.mailbox_servcli.get(self.mid_cli_command, False) 449 | respstr = resp.strip() + "\n\n" 450 | # put the content to the end of the textarea 451 | guilog.textarea_append (self.text_cli_command, respstr) 452 | self.text_cli_command.update_idletasks() 453 | except queue.Empty: 454 | # ignore 455 | pass 456 | # setup next 457 | self.after(300, self.check_mid_cli_command) 458 | return 459 | 460 | def nxvforward_main(): 461 | global config_use_textarea_log 462 | guilog.set_log_stderr() 463 | 464 | gettext_init() 465 | 466 | root = tk.Tk() 467 | root.title(str_progname + " - " + str_version) 468 | app = MyTkAppFrame(root) 469 | app.pack(fill="both", expand=True) 470 | ttk.Sizegrip(root).pack(side="right") 471 | 472 | if config_use_textarea_log: 473 | guilog.set_log_textarea (app.get_log_text()) 474 | 475 | root.mainloop() 476 | 477 | if __name__ == "__main__": 478 | nxvforward_main() 479 | 480 | -------------------------------------------------------------------------------- /nxvlogbatt-plotfig.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PNGSIZE="1024,768" 4 | #PNGSIZE="800,600" 5 | PNGSIZE="1800,900" 6 | 7 | EPSSIZE="8,4" 8 | 9 | function plot_charging() { 10 | # the data file name prefix 11 | local PARAM_PREFIX=$1 12 | shift 13 | # 1 -- if draw VBattV*200/% 14 | local PARAM_VBATRAT=$1 15 | shift 16 | # set the x range 17 | local PARAM_XRANGE=$1 18 | shift 19 | # fig title 20 | local PARAM_TITLE=$1 21 | shift 22 | # fig comments 23 | local PARAM_COMMENTS=$1 24 | shift 25 | 26 | 27 | FN_DATA="${PARAM_PREFIX}.txt" 28 | # figure -- charging current, voltage/% and tempreture 29 | FN_GPOUT="fig-${PARAM_PREFIX}" 30 | cat << EOF > "${FN_GPOUT}.gp" 31 | # set terminal png transparent nocrop enhanced size 450,320 font "arial,8" 32 | set terminal png size ${PNGSIZE} 33 | set output "${FN_GPOUT}.png" 34 | 35 | #set key left bottom 36 | #set key center bottom 37 | #set key right bottom 38 | #set key center 39 | #set key right top 40 | set key center top 41 | 42 | set autoscale x 43 | #set autoscale x2 44 | set autoscale y 45 | #set autoscale y2 46 | #set yrange [-500<*:*<2500] 47 | set yrange [-400:2100] 48 | #set y2range [0<*:10<*<50] 49 | set y2range [0:50] 50 | 51 | # On both the x and y axes split each space in half and put a minor tic there 52 | set mxtics 2 53 | set mytics 2 54 | set my2tics 2 55 | 56 | # Line style for axes 57 | # Define a line style (we're calling it 80) and set 58 | # lt = linetype to 0 (dashed line) 59 | # lc = linecolor to a gray defined by that number 60 | set style line 80 lt 0 lc rgb "#808080" 61 | 62 | # Set the border using the linestyle 80 that we defined 63 | # 3 = 1 + 2 (1 = plot the bottom line and 2 = plot the left line) 64 | # back means the border should be behind anything else drawn 65 | set border 3 back ls 80 66 | 67 | # Line style for grid 68 | # Define a new linestyle (81) 69 | # linetype = 0 (dashed line) 70 | # linecolor = gray 71 | # lw = lineweight, make it half as wide as the axes lines 72 | set style line 81 lt 0 lc rgb "#808080" lw 0.5 73 | 74 | # Draw the grid lines for both the major and minor tics 75 | #set grid x y y2 76 | set grid xtics 77 | set grid ytics 78 | set grid y2tics 79 | set grid mxtics 80 | #set grid mytics 81 | #set grid my2tics 82 | 83 | # Put the grid behind anything drawn and use the linestyle 81 84 | set grid back ls 81 85 | EOF 86 | 87 | if [ ! "${PARAM_XRANGE}" = "" ]; then 88 | cat << EOF >> "${FN_GPOUT}.gp" 89 | set xrange [${PARAM_XRANGE}] 90 | EOF 91 | fi 92 | 93 | 94 | if [ ! "${PARAM_TITLE}" = "" ]; then 95 | cat << EOF >> "${FN_GPOUT}.gp" 96 | set title "${PARAM_TITLE}" 97 | EOF 98 | fi 99 | 100 | if [ ! "${PARAM_COMMENTS}" = "" ]; then 101 | cat << EOF >> "${FN_GPOUT}.gp" 102 | set label "${PARAM_COMMENTS}" at graph 1.05,-.065 right #first -1 103 | #set label "${PARAM_COMMENTS}" at 2.5,0.5 tc rgb "white" font ",30" front 104 | EOF 105 | fi 106 | 107 | cat << EOF >> "${FN_GPOUT}.gp" 108 | #set xtics 0,.5,10 109 | set ytics -500,50,2500 nomirror 110 | set y2tics 0,1,50 nomirror #textcolor rgb "red" 111 | 112 | set xlabel 'Time (seconds)' 113 | set ylabel 'Current (mA)' 114 | set y2label 'Voltage (V), %/10, Temp (°C)' 115 | 116 | EOF 117 | 118 | if [ "${PARAM_VBATRAT}" = "1" ]; then 119 | cat << EOF >> "${FN_GPOUT}.gp" 120 | # x -- time 121 | # y -- current 122 | # y2 -- temperature, voltage, %, voltage/% 123 | plot '${FN_DATA}' using 1:7 title 'VBattV' with lines axes x1y2 \ 124 | , '${FN_DATA}' using 1:8 title 'VExtV' with lines axes x1y2 \ 125 | , '${FN_DATA}' using 1:(\$2/-1) title 'Battery Current (mA)' with lines axes x1y1 \ 126 | , '${FN_DATA}' using 1:(\$9/10) title 'FuelPercent (%/10)' with lines axes x1y2 \ 127 | , '${FN_DATA}' using 1:4 title 'BatteryTemp0InC (°C)' with lines axes x1y2 \ 128 | , '${FN_DATA}' using 1:5 title 'BatteryTemp1InC (°C)' with lines axes x1y2 \ 129 | , '${FN_DATA}' using 1:(\$7*200/\$9) title 'VBattV*200/%' with lines axes x1y2 130 | EOF 131 | else 132 | cat << EOF >> "${FN_GPOUT}.gp" 133 | # x -- time 134 | # y -- current 135 | # y2 -- temperature, voltage, %, voltage/% 136 | plot '${FN_DATA}' using 1:7 title 'VBattV' with lines axes x1y2 \ 137 | , '${FN_DATA}' using 1:8 title 'VExtV' with lines axes x1y2 \ 138 | , '${FN_DATA}' using 1:(\$2/-1) title 'Battery Current (mA)' with lines axes x1y1 \ 139 | , '${FN_DATA}' using 1:(\$9/10) title 'FuelPercent (%/10)' with lines axes x1y2 \ 140 | , '${FN_DATA}' using 1:4 title 'BatteryTemp0InC (°C)' with lines axes x1y2 \ 141 | , '${FN_DATA}' using 1:5 title 'BatteryTemp1InC (°C)' with lines axes x1y2 142 | EOF 143 | #, '${FN_DATA}' using 1:11 title 'Charging Current' with lines axes x1y1 144 | fi 145 | 146 | cat << EOF >> "${FN_GPOUT}.gp" 147 | #set terminal pdf color solid lw 1 size 5.83,4.13 font "cmr12" enh 148 | #set pointsize 1 149 | #set output "${FN_GPOUT}.pdf" 150 | set terminal postscript eps size ${EPSSIZE} color enhanced 151 | set output "${FN_GPOUT}.eps" 152 | replot 153 | 154 | EOF 155 | 156 | gnuplot "${FN_GPOUT}.gp" 157 | } 158 | 159 | # 2016-12-07 10: m2 upgrade to 3.4 160 | LIST_DATA=( 161 | # , <1: plot VBattV*400/%>, , , 162 | "nxvlogbatt-data-charging-mcnair-old-m2-1,0,McNair Ni-MH 7.2V 3200mAh for Neato XV x2,1,McNair battery old-in the machine-first charging-machine2-firmware 3.1" 163 | "nxvlogbatt-data-charging-mcnair-old-m2-2,0,McNair Ni-MH 7.2V 3200mAh for Neato XV x2,1,McNair battery old-in the machine-second charging-machine2-firmware 3.1" 164 | "nxvlogbatt-data-charging-mcnair-old-m2-3,0,McNair Ni-MH 7.2V 3200mAh for Neato XV x2,1,McNair battery old-in the machine-third charging-machine2-firmware 3.4" 165 | "nxvlogbatt-data-charging-oem-m1-1,1,OEM Ni-MH 7.2V 3200mAh for Neato XV x2,1,OEM battery old-in the machine-first charging-machine1-firmware 3.1" 166 | "nxvlogbatt-data-charging-powerextra-r1-m2-1,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round1-first charging-machine2-firmware 3.1" 167 | "nxvlogbatt-data-charging-powerextra-r1-m2-2,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round1-second charging-machine2-firmware 3.4" 168 | "nxvlogbatt-data-charging-powerextra-r1-m2-3,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round1-third charging-machine2-firmware 3.4-after refresh with neatoctrl (deep recharge), there's a dust box install at the middle of charging" 169 | "nxvlogbatt-data-charging-powerextra-r1-m2-4,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round1-4th charging-machine2-firmware 3.4-can only last 5min?" 170 | "nxvlogbatt-data-standby-powerextra-r1-m2-2,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round1-standby after second charging-machine2-firmware 3.4" 171 | "nxvlogbatt-data-charging-oem-m2-1,1,OEM Ni-MH 7.2V 3200mAh for Neato XV x2,1,OEM battery old-in the machine-first charging-machine2-firmware 3.4" 172 | "nxvlogbatt-data-charging-oem-m2-2,1,OEM Ni-MH 7.2V 3200mAh for Neato XV x2,1,OEM battery old-in the machine-second charging-machine2-firmware 3.4" 173 | "nxvlogbatt-data-charging-oem-m2-3,1,OEM Ni-MH 7.2V 3200mAh for Neato XV x2,1,OEM battery old-in the machine-third charging-machine2-firmware 3.4" 174 | "nxvlogbatt-data-charging-powerextra-r1-m1-1,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round1-first charging-machine1-firmware 3.1" 175 | 176 | "nxvlogbatt-data-charging-powerextra-r2-m1-1,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round2-first charging-machine1-firmware 3.1" #TODO: the second round of battery, with old firmware and old hardware 177 | "nxvlogbatt-data-charging-powerextra-r2-m1-2,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round2-second charging-machine1-firmware 3.4" #TODO: the second round of battery, with firmware 3.4 and updated hardware(capacitors) 178 | "nxvlogbatt-data-charging-powerextra-r2-m2-1,1,Powerextra Ni-MH 7.2V 4000mAh for Neato XV x2,1,powerextra battery-round2-third charging-machine2-firmware 3.4" #TODO: the second round of battery, with firmware 3.4 and updated hardware(capacitors) 179 | ) 180 | LIST_DATA1=( 181 | "nxvlogbatt-data-charging-oem-m2-3,1,OEM Ni-MH 7.2V 3200mAh for Neato XV x2,1,OEM battery old-in the machine-third charging-machine2-firmware 3.4" 182 | ) 183 | function do_workplot_test_data { 184 | local i=0 185 | while (( ${i} < ${#LIST_DATA[*]} )); do 186 | local LINE1="${LIST_DATA[${i}]}" 187 | local PREFIX1=$(echo "${LINE1}" | awk -F, '{print $1}') 188 | local VBATRAT=$(echo "${LINE1}" | awk -F, '{print $2}') 189 | local BRAND=$(echo "${LINE1}" | awk -F, '{print $3}') 190 | local SERIAL=$(echo "${LINE1}" | awk -F, '{print $4}') 191 | local COMMENTS=$(echo "${LINE1}" | awk -F, '{print $5}') 192 | date 193 | plot_charging "${PREFIX1}" "${VBATRAT}" "" "Charging '${BRAND}' (${SERIAL})" "${COMMENTS}" 194 | date 195 | i=$((i + 1)) 196 | done 197 | } 198 | 199 | 200 | if [ "$1" = "" ]; then 201 | do_workplot_test_data 202 | 203 | else 204 | plot_charging "$1" "$2" "$3" "$4" "$5" 205 | fi 206 | 207 | -------------------------------------------------------------------------------- /nxvlogbatt.bat: -------------------------------------------------------------------------------- 1 | 2 | rem python nxvlogbatt.py -l nxvlogbatt.log -o nxvlogbatt-data-charging-oem-m2-1.txt -a dev://COM12:115200 3 | python nxvlogbatt.py -l nxvlogbatt.log -o nxvlogbatt-data-charging-powerextra-r1-m1-1.txt -a dev://COM12:115200 4 | -------------------------------------------------------------------------------- /nxvlogbatt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf8 3 | # 4 | # Copyright 2016-2017 Yunhui Fu 5 | # 6 | # This program is free software: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License version 3, as published 8 | # by the Free Software Foundation. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranties of 12 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 13 | # PURPOSE. See the GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program. If not, see . 17 | # 18 | # For further info, check https://github.com/yhfudev/python-nxvcontrol.git 19 | 20 | import sys 21 | import time 22 | import argparse # argparse > optparse > getopt 23 | import logging as L 24 | from datetime import datetime 25 | 26 | from multiprocessing import Queue, Lock 27 | 28 | import neatocmdapi 29 | 30 | L.basicConfig(level=L.DEBUG, format='%(levelname)s:%(message)s') 31 | 32 | parser=argparse.ArgumentParser(description='Log battery data from USB port(serial) of a Neato Xv robot.') 33 | #parser.add_argument('intergers', metavar='N', type=int, nargs='+', help='an integer for the accumulator') 34 | parser.add_argument('-l', '--logfile', type=str, dest='fnlog', default="/dev/stderr", help='the file to output the log') 35 | parser.add_argument('-o', '--output', type=str, dest='fnout', default="/dev/stdout", help='the file to output the data') 36 | parser.add_argument('-a', '--target', type=str, dest='target', default="", help='tcp://localhost:3333, dev://ttyUSB0:115200, dev://COM12:115200, sim:') 37 | parser.add_argument('-i', '--interval', type=float, dest='interval', default=0.5, help='the interval between log records') 38 | parser.add_argument('-t', '--time', type=int, dest='time', default=0, help='the second time length of log records') 39 | parser.add_argument('-d', '--drain', type=int, dest='draintime', default=0, help='the second time length of draining the battery(start fan motor)') 40 | args = parser.parse_args() 41 | 42 | if args.fnout == "/dev/stdout": 43 | L.info ("output data to standard output") 44 | else: 45 | sys.stdout = open(args.fnout, 'w') 46 | 47 | if args.fnlog == "/dev/stderr": 48 | L.info ("output log to standard error") 49 | else: 50 | L.info ("output log to " + args.fnlog) 51 | ch = L.FileHandler(args.fnlog) 52 | ch.setLevel(L.DEBUG) 53 | ch.setFormatter(L.Formatter('%(asctime)s %(levelname)s:%(message)s')) 54 | L.getLogger().addHandler(ch) 55 | 56 | serv = None 57 | L.debug('serv.open() ...') 58 | serv = neatocmdapi.NCIService(target=args.target.strip(), timeout=args.interval) 59 | 60 | def cb_task(tid, req): 61 | L.debug("do task: tid=" + str(tid) + ", req=" + str(req)) 62 | reqstr = req[0] 63 | resp = serv.get_request_block(reqstr) 64 | if resp != None: 65 | if resp.strip() != "": 66 | mbox.put(req[1], resp.strip()) 67 | 68 | mbox = neatocmdapi.MailPipe() 69 | mbox_id = mbox.declair() 70 | 71 | if serv.open(cb_task) == False: 72 | L.error ('time out for connection') 73 | exit(0) 74 | 75 | try: 76 | if args.draintime > 0: 77 | serv.request(["TestMode On\nSetMotor VacuumOn", mbox_id]) 78 | time.sleep(args.draintime) 79 | serv.request(["SetMotor VacuumOff\nTestMode Off", mbox_id]) 80 | 81 | list_commands = ( 82 | 'GetCharger', 83 | 'GetAnalogSensors', 84 | ) 85 | list_keys = ( 86 | # GetAnalogSensors: 87 | 'CurrentInmA', 88 | 'BatteryVoltageInmV', 89 | 'BatteryTemp0InC', 90 | 'BatteryTemp1InC', 91 | 'ChargeVoltInmV', 92 | # GetCharger: 93 | 'VBattV', 94 | 'VExtV', 95 | 'FuelPercent', 96 | 'ChargingActive', 97 | 'Charger_mAH', 98 | # others: 99 | 'UIButtonInmV', 100 | 'VacuumCurrentInmA', 101 | 'SideBrushCurrentInmA', 102 | 'VoltageReferenceInmV', 103 | 'BattTempCAvg[0]', 104 | 'BattTempCAvg[1]', 105 | 'BatteryOverTemp', 106 | 'ChargingEnabled', 107 | 'ConfidentOnFuel', 108 | 'OnReservedFuel', 109 | 'EmptyFuel', 110 | 'BatteryFailure', 111 | 'ExtPwrPresent', 112 | 'ThermistorPresent[0]', 113 | 'ThermistorPresent[1]', 114 | # 115 | #'WallSensorInMM', 116 | #'LeftDropInMM', 117 | #'RightDropInMM', 118 | #'LeftMagSensor', 119 | #'RightMagSensor', 120 | #'AccelXInmG', 121 | #'AccelYInmG', 122 | #'AccelZInmG', 123 | ) 124 | tm_begin = datetime.now() 125 | print ("# start time " + str(tm_begin) + ", " + ", ".join(list_keys)) 126 | while True: 127 | tm_now = datetime.now() 128 | delta = tm_now - tm_begin 129 | if args.time > 0: 130 | if delta.total_seconds() > args.time: 131 | L.debug("exceed time!") 132 | break 133 | 134 | data = dict() 135 | sendcmd = "" 136 | for cmd0 in list_commands: 137 | L.debug ('send command ' + cmd0) 138 | sendcmd += cmd0 + "\n" 139 | serv.request([sendcmd, mbox_id]) 140 | 141 | tm_now = datetime.now() 142 | #record = [-1] * 16 143 | respstr = mbox.get(mbox_id) 144 | retlines = respstr.strip() + '\n' 145 | responses = retlines.split('\n') 146 | for i in range(0,len(responses)): 147 | response = responses[i].strip() 148 | L.debug('received: ' + response) 149 | #L.debug('read size=' + len(response) ) 150 | if len(response) < 1: 151 | L.debug('read null 2') 152 | break 153 | lst = response.split(',') 154 | #L.debug('lst=' + ",".join(lst)) 155 | #L.debug('lst size=%d'%lst.__len__()) 156 | #L.debug('lst len=%d'%len(lst)) 157 | if len(lst) > 1: 158 | if lst[0].lower() == 'Label'.lower(): 159 | # ignore 160 | L.debug('ignore header') 161 | elif lst[0] in list_keys: 162 | data[lst[0]] = lst[1] 163 | L.debug("process response: " + response) 164 | else: 165 | L.debug("ignore return response: " + response) 166 | else: 167 | L.debug('ignore response with: ' + lst[0]) 168 | 169 | #fmt0="%d.%06d" + ", " + ", ".join(data[x] for x in list_keys) 170 | linedata = "" 171 | for val in list_keys: 172 | if val in data: 173 | linedata += data[val] 174 | linedata += ", " 175 | fmt0="%d.%06d" + ", " + linedata 176 | 177 | L.debug("fmt=" + fmt0) 178 | print (fmt0 %(delta.days * 86400 + delta.seconds, delta.microseconds)) # (24*60*60)=86400 179 | sys.stdout.flush() 180 | 181 | tm_now2 = datetime.now() 182 | delta = tm_now2 - tm_now 183 | timepast = 1.0 * delta.microseconds / 1000000 184 | timepast += delta.days * 86400 + delta.seconds 185 | if timepast < args.interval: 186 | time.sleep(args.interval - timepast) 187 | 188 | serv.close() 189 | 190 | except Exception as e1: 191 | L.error ('Error in read serial: ' + str(e1)) 192 | -------------------------------------------------------------------------------- /nxvlogbatt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #python3 nxvlogbatt.py -l nxvlogbatt.log -o nxvlogbatt-data-charging-powerextra-r1-m1-1.txt -a dev://ttyACM0:115200 4 | python3 nxvlogbatt.py -l nxvlogbatt.log -o nxvlogbatt-data-charging-oem-m2-xx.txt -a dev://ttyACM0:115200 5 | -------------------------------------------------------------------------------- /translations/en_US.po: -------------------------------------------------------------------------------- 1 | # English translations for 00job-2015 package. 2 | # Copyright (C) 2016 ORGANIZATION 3 | # yhfu , 2016. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: 00job-2015\n" 8 | "POT-Creation-Date: 2017-01-02 19:24-0500\n" 9 | "PO-Revision-Date: 2016-12-31 16:48-0500\n" 10 | "Last-Translator: yhfu \n" 11 | "Language-Team: English\n" 12 | "Language: en_US\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: pygettext.py 1.5\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | 19 | #: nxvcontrol.py:156 20 | msgid "Day of Week" 21 | msgstr "Day of Week" 22 | 23 | #: nxvcontrol.py:157 24 | msgid "Time" 25 | msgstr "Time" 26 | 27 | #: nxvcontrol.py:196 nxvcontrol.py:255 28 | msgid "Friday" 29 | msgstr "Friday" 30 | 31 | #: nxvcontrol.py:196 nxvcontrol.py:255 32 | msgid "Monday" 33 | msgstr "Monday" 34 | 35 | #: nxvcontrol.py:196 nxvcontrol.py:255 36 | msgid "Saturday" 37 | msgstr "Saturday" 38 | 39 | #: nxvcontrol.py:196 nxvcontrol.py:255 40 | msgid "Sunday" 41 | msgstr "Sunday" 42 | 43 | #: nxvcontrol.py:196 nxvcontrol.py:255 44 | msgid "Thursday" 45 | msgstr "Thursday" 46 | 47 | #: nxvcontrol.py:196 nxvcontrol.py:255 48 | msgid "Tuesday" 49 | msgstr "Tuesday" 50 | 51 | #: nxvcontrol.py:196 nxvcontrol.py:255 52 | msgid "Wednesday" 53 | msgstr "Wednesday" 54 | 55 | #: nxvcontrol.py:281 nxvcontrol.py:848 nxvcontrol.py:973 56 | msgid "Sensors" 57 | msgstr "Sensors" 58 | 59 | #: nxvcontrol.py:282 60 | msgid "Value" 61 | msgstr "Value" 62 | 63 | #: nxvcontrol.py:285 64 | msgid "Digital Sensors" 65 | msgstr "Digital Sensors" 66 | 67 | #: nxvcontrol.py:286 68 | msgid "Analog Sensors" 69 | msgstr "Analog Sensors" 70 | 71 | #: nxvcontrol.py:287 72 | msgid "Buttons" 73 | msgstr "Buttons" 74 | 75 | #: nxvcontrol.py:288 nxvcontrol.py:939 nxvcontrol.py:979 76 | msgid "Motors" 77 | msgstr "Motors" 78 | 79 | #: nxvcontrol.py:289 80 | msgid "Accelerometer" 81 | msgstr "Accelerometer" 82 | 83 | #: nxvcontrol.py:290 84 | msgid "Charger" 85 | msgstr "Charger" 86 | 87 | #: nxvcontrol.py:344 nxvcontrol.py:725 88 | msgid "Unknown" 89 | msgstr "Unknown" 90 | 91 | #: nxvcontrol.py:376 nxvcontrol.py:399 92 | msgid "Released" 93 | msgstr "Released" 94 | 95 | #: nxvcontrol.py:377 nxvcontrol.py:400 96 | msgid "Pressed" 97 | msgstr "Pressed" 98 | 99 | #: nxvcontrol.py:592 100 | msgid "ON" 101 | msgstr "ON" 102 | 103 | #: nxvcontrol.py:595 104 | msgid "OFF" 105 | msgstr "OFF" 106 | 107 | #: nxvcontrol.py:626 108 | msgid "Text files" 109 | msgstr "Text files" 110 | 111 | #: nxvcontrol.py:627 112 | msgid "All files" 113 | msgstr "All files" 114 | 115 | #: nxvcontrol.py:661 nxvcontrol.py:983 nxvforward.py:221 nxvforward.py:347 116 | msgid "About" 117 | msgstr "About" 118 | 119 | #: nxvcontrol.py:666 120 | msgid "Setup your Neato Robot" 121 | msgstr "Setup your Neato Robot" 122 | 123 | #: nxvcontrol.py:668 124 | msgid "Copyright © 2015-2016 The nxvControl Authors" 125 | msgstr "Copyright © 2015–2016 The nxvControl Authors" 126 | 127 | #: nxvcontrol.py:670 nxvforward.py:230 128 | msgid "This program comes with absolutely no warranty." 129 | msgstr "This program comes with absolutely no warranty." 130 | 131 | #: nxvcontrol.py:671 nxvforward.py:231 132 | msgid "See the GNU General Public License, version 3 or later for details." 133 | msgstr "See the GNU General Public License, version 3 or later for details." 134 | 135 | #: nxvcontrol.py:685 nxvcontrol.py:975 nxvforward.py:292 136 | msgid "Connection" 137 | msgstr "Connection" 138 | 139 | #: nxvcontrol.py:687 140 | msgid "Status" 141 | msgstr "Status" 142 | 143 | #: nxvcontrol.py:691 144 | msgid "Conection" 145 | msgstr "Connection" 146 | 147 | #: nxvcontrol.py:695 nxvforward.py:255 nxvforward.py:297 148 | msgid "Connect to:" 149 | msgstr "Connect to:" 150 | 151 | #: nxvcontrol.py:702 nxvforward.py:304 152 | msgid "Connect" 153 | msgstr "Connect" 154 | 155 | #: nxvcontrol.py:705 nxvforward.py:308 156 | msgid "Disconnect" 157 | msgstr "Disconnect" 158 | 159 | #: nxvcontrol.py:714 160 | msgid "Robot Time:" 161 | msgstr "Robot Time:" 162 | 163 | #: nxvcontrol.py:719 164 | msgid "Sync PC time to robot" 165 | msgstr "Sync PC time to robot" 166 | 167 | #: nxvcontrol.py:723 168 | msgid "Test Mode:" 169 | msgstr "Test Mode:" 170 | 171 | #: nxvcontrol.py:727 172 | msgid "Test ON" 173 | msgstr "Test ON" 174 | 175 | #: nxvcontrol.py:728 176 | msgid "Test OFF" 177 | msgstr "Test OFF" 178 | 179 | #: nxvcontrol.py:732 180 | msgid "Battery Status:" 181 | msgstr "Battery Status:" 182 | 183 | #: nxvcontrol.py:749 184 | msgid "Version:" 185 | msgstr "Version:" 186 | 187 | #: nxvcontrol.py:787 nxvcontrol.py:977 188 | msgid "Commands" 189 | msgstr "Commands" 190 | 191 | #: nxvcontrol.py:804 nxvforward.py:283 nxvforward.py:329 192 | msgid "Clear" 193 | msgstr "Clear" 194 | 195 | #: nxvcontrol.py:813 nxvforward.py:338 196 | msgid "Run" 197 | msgstr "Run" 198 | 199 | #: nxvcontrol.py:821 nxvcontrol.py:835 nxvcontrol.py:978 200 | msgid "Schedule" 201 | msgstr "Schedule" 202 | 203 | #: nxvcontrol.py:836 204 | msgid "Disabled: " 205 | msgstr "Disabled: " 206 | 207 | #: nxvcontrol.py:836 208 | msgid "Enabled: " 209 | msgstr "Enabled: " 210 | 211 | #: nxvcontrol.py:838 212 | msgid "Save schedule" 213 | msgstr "Save schedule" 214 | 215 | #: nxvcontrol.py:840 216 | msgid "Get schedule" 217 | msgstr "Get schedule" 218 | 219 | #: nxvcontrol.py:877 220 | msgid "Update Sensors" 221 | msgstr "Update Sensors" 222 | 223 | #: nxvcontrol.py:878 nxvcontrol.py:926 nxvcontrol.py:932 nxvcontrol.py:944 224 | #: nxvcontrol.py:948 nxvcontrol.py:952 nxvcontrol.py:956 nxvcontrol.py:960 225 | #: nxvcontrol.py:964 226 | msgid "OFF: " 227 | msgstr "OFF: " 228 | 229 | #: nxvcontrol.py:878 nxvcontrol.py:926 nxvcontrol.py:932 nxvcontrol.py:944 230 | #: nxvcontrol.py:948 nxvcontrol.py:952 nxvcontrol.py:956 nxvcontrol.py:960 231 | #: nxvcontrol.py:964 232 | msgid "ON: " 233 | msgstr "ON: " 234 | 235 | #: nxvcontrol.py:901 nxvcontrol.py:925 nxvcontrol.py:974 236 | msgid "LiDAR" 237 | msgstr "LiDAR" 238 | 239 | #: nxvcontrol.py:931 240 | msgid "Wheels Controlled by Keypad" 241 | msgstr "Wheels Controlled by Keypad" 242 | 243 | #: nxvcontrol.py:943 244 | msgid "Left Wheel" 245 | msgstr "Left Wheel" 246 | 247 | #: nxvcontrol.py:947 248 | msgid "Right Wheel" 249 | msgstr "Right Wheel" 250 | 251 | #: nxvcontrol.py:951 252 | msgid "LiDAR Motor" 253 | msgstr "LiDAR Motor" 254 | 255 | #: nxvcontrol.py:955 256 | msgid "Vacuum" 257 | msgstr "Vacuum" 258 | 259 | #: nxvcontrol.py:959 260 | msgid "Brush" 261 | msgstr "Brush" 262 | 263 | #: nxvcontrol.py:963 264 | msgid "Side Brush" 265 | msgstr "Side Brush" 266 | 267 | #: nxvcontrol.py:970 268 | msgid "Recharge" 269 | msgstr "Recharge" 270 | 271 | #: nxvforward.py:61 272 | msgid "nxvForward" 273 | msgstr "nxvForward" 274 | 275 | #: nxvforward.py:226 276 | msgid "Forward Neato XV control over network" 277 | msgstr "Forward Neato XV control over network" 278 | 279 | #: nxvforward.py:228 280 | msgid "Copyright © 2015–2016 The nxvForward Authors" 281 | msgstr "Copyright © 2015–2016 The nxvForward Authors" 282 | 283 | #: nxvforward.py:237 nxvforward.py:345 284 | msgid "Server" 285 | msgstr "Server" 286 | 287 | #: nxvforward.py:240 288 | msgid "Setup" 289 | msgstr "Setup" 290 | 291 | #: nxvforward.py:245 292 | msgid "Bind Address:" 293 | msgstr "Bind Address:" 294 | 295 | #: nxvforward.py:263 296 | msgid "Start" 297 | msgstr "Start" 298 | 299 | #: nxvforward.py:268 300 | msgid "Stop" 301 | msgstr "Stop" 302 | 303 | #: nxvforward.py:289 nxvforward.py:346 304 | msgid "Test Client" 305 | msgstr "Test Client" 306 | 307 | #~ msgid "Power DC Jack" 308 | #~ msgstr "Power DC Jack" 309 | 310 | #~ msgid "Connected: " 311 | #~ msgstr "Connected: " 312 | 313 | #~ msgid "Disconnected: " 314 | #~ msgstr "Disconnected: " 315 | 316 | #~ msgid "Dustbin" 317 | #~ msgstr "Dustbin" 318 | 319 | #~ msgid "Out: " 320 | #~ msgstr "Out: " 321 | 322 | #~ msgid "In: " 323 | #~ msgstr "In: " 324 | 325 | #~ msgid "Left Side Key" 326 | #~ msgstr "Left Side Key" 327 | 328 | #~ msgid "Kicked: " 329 | #~ msgstr "Kicked: " 330 | 331 | #~ msgid "Released: " 332 | #~ msgstr "Released: " 333 | 334 | #~ msgid "Left Front Key" 335 | #~ msgstr "Left Front Key" 336 | 337 | #~ msgid "Right Side Key" 338 | #~ msgstr "Right Side Key" 339 | 340 | #~ msgid "Right Front Key" 341 | #~ msgstr "Right Front Key" 342 | -------------------------------------------------------------------------------- /translations/zh_CN.po: -------------------------------------------------------------------------------- 1 | # Chinese translations for 00job-2015 package. 2 | # Copyright (C) 2016 THE 00job-2015'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the 00job-2015 package. 4 | # yhfu , 2016. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 00job-2015\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-01-02 19:24-0500\n" 11 | "PO-Revision-Date: 2016-12-31 14:10-0500\n" 12 | "Last-Translator: yhfu \n" 13 | "Language-Team: Chinese (simplified)\n" 14 | "Language: zh_CN\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | 19 | #: nxvcontrol.py:156 20 | msgid "Day of Week" 21 | msgstr "星期" 22 | 23 | #: nxvcontrol.py:157 24 | msgid "Time" 25 | msgstr "时间" 26 | 27 | #: nxvcontrol.py:196 nxvcontrol.py:255 28 | msgid "Friday" 29 | msgstr "周五" 30 | 31 | #: nxvcontrol.py:196 nxvcontrol.py:255 32 | msgid "Monday" 33 | msgstr "周一" 34 | 35 | #: nxvcontrol.py:196 nxvcontrol.py:255 36 | msgid "Saturday" 37 | msgstr "周六" 38 | 39 | #: nxvcontrol.py:196 nxvcontrol.py:255 40 | msgid "Sunday" 41 | msgstr "周日" 42 | 43 | #: nxvcontrol.py:196 nxvcontrol.py:255 44 | msgid "Thursday" 45 | msgstr "周四" 46 | 47 | #: nxvcontrol.py:196 nxvcontrol.py:255 48 | msgid "Tuesday" 49 | msgstr "周二" 50 | 51 | #: nxvcontrol.py:196 nxvcontrol.py:255 52 | msgid "Wednesday" 53 | msgstr "周三" 54 | 55 | #: nxvcontrol.py:281 nxvcontrol.py:848 nxvcontrol.py:973 56 | msgid "Sensors" 57 | msgstr "传感器" 58 | 59 | #: nxvcontrol.py:282 60 | msgid "Value" 61 | msgstr "值" 62 | 63 | #: nxvcontrol.py:285 64 | msgid "Digital Sensors" 65 | msgstr "数字传感器" 66 | 67 | #: nxvcontrol.py:286 68 | msgid "Analog Sensors" 69 | msgstr "模拟传感器" 70 | 71 | #: nxvcontrol.py:287 72 | msgid "Buttons" 73 | msgstr "按钮" 74 | 75 | #: nxvcontrol.py:288 nxvcontrol.py:939 nxvcontrol.py:979 76 | msgid "Motors" 77 | msgstr "电动机" 78 | 79 | #: nxvcontrol.py:289 80 | msgid "Accelerometer" 81 | msgstr "加速计" 82 | 83 | #: nxvcontrol.py:290 84 | msgid "Charger" 85 | msgstr "充电" 86 | 87 | #: nxvcontrol.py:344 nxvcontrol.py:725 88 | msgid "Unknown" 89 | msgstr "未知" 90 | 91 | #: nxvcontrol.py:376 nxvcontrol.py:399 92 | msgid "Released" 93 | msgstr "已释放" 94 | 95 | #: nxvcontrol.py:377 nxvcontrol.py:400 96 | msgid "Pressed" 97 | msgstr "已按住" 98 | 99 | #: nxvcontrol.py:592 100 | msgid "ON" 101 | msgstr "开" 102 | 103 | #: nxvcontrol.py:595 104 | msgid "OFF" 105 | msgstr "关" 106 | 107 | #: nxvcontrol.py:626 108 | msgid "Text files" 109 | msgstr "文本文件" 110 | 111 | #: nxvcontrol.py:627 112 | msgid "All files" 113 | msgstr "全部文件" 114 | 115 | #: nxvcontrol.py:661 nxvcontrol.py:983 nxvforward.py:221 nxvforward.py:347 116 | msgid "About" 117 | msgstr "关于" 118 | 119 | #: nxvcontrol.py:666 120 | msgid "Setup your Neato Robot" 121 | msgstr "设置你的 Neato 机器人" 122 | 123 | #: nxvcontrol.py:668 124 | msgid "Copyright © 2015-2016 The nxvControl Authors" 125 | msgstr "版权所有 © 2015–2016 nxvControl 作者" 126 | 127 | #: nxvcontrol.py:670 nxvforward.py:230 128 | msgid "This program comes with absolutely no warranty." 129 | msgstr "本程序完全不提供保障。" 130 | 131 | #: nxvcontrol.py:671 nxvforward.py:231 132 | msgid "See the GNU General Public License, version 3 or later for details." 133 | msgstr "参看 GNU 通用公众协议版本3或之后版本" 134 | 135 | #: nxvcontrol.py:685 nxvcontrol.py:975 nxvforward.py:292 136 | msgid "Connection" 137 | msgstr "连接" 138 | 139 | #: nxvcontrol.py:687 140 | msgid "Status" 141 | msgstr "状态" 142 | 143 | #: nxvcontrol.py:691 144 | msgid "Conection" 145 | msgstr "连接" 146 | 147 | #: nxvcontrol.py:695 nxvforward.py:255 nxvforward.py:297 148 | msgid "Connect to:" 149 | msgstr "连接到:" 150 | 151 | #: nxvcontrol.py:702 nxvforward.py:304 152 | msgid "Connect" 153 | msgstr "连接" 154 | 155 | #: nxvcontrol.py:705 nxvforward.py:308 156 | msgid "Disconnect" 157 | msgstr "断开" 158 | 159 | #: nxvcontrol.py:714 160 | msgid "Robot Time:" 161 | msgstr "机器时间:" 162 | 163 | #: nxvcontrol.py:719 164 | msgid "Sync PC time to robot" 165 | msgstr "同步PC时间到机器" 166 | 167 | #: nxvcontrol.py:723 168 | msgid "Test Mode:" 169 | msgstr "测试模式:" 170 | 171 | #: nxvcontrol.py:727 172 | msgid "Test ON" 173 | msgstr "开启测试模式" 174 | 175 | #: nxvcontrol.py:728 176 | msgid "Test OFF" 177 | msgstr "关闭测试模式" 178 | 179 | #: nxvcontrol.py:732 180 | msgid "Battery Status:" 181 | msgstr "电池状态:" 182 | 183 | #: nxvcontrol.py:749 184 | msgid "Version:" 185 | msgstr "版本:" 186 | 187 | #: nxvcontrol.py:787 nxvcontrol.py:977 188 | msgid "Commands" 189 | msgstr "命令" 190 | 191 | #: nxvcontrol.py:804 nxvforward.py:283 nxvforward.py:329 192 | msgid "Clear" 193 | msgstr "清除" 194 | 195 | #: nxvcontrol.py:813 nxvforward.py:338 196 | msgid "Run" 197 | msgstr "运行" 198 | 199 | #: nxvcontrol.py:821 nxvcontrol.py:835 nxvcontrol.py:978 200 | msgid "Schedule" 201 | msgstr "日程表" 202 | 203 | #: nxvcontrol.py:836 204 | msgid "Disabled: " 205 | msgstr "已禁止:" 206 | 207 | #: nxvcontrol.py:836 208 | msgid "Enabled: " 209 | msgstr "已生效:" 210 | 211 | #: nxvcontrol.py:838 212 | msgid "Save schedule" 213 | msgstr "保存日程表" 214 | 215 | #: nxvcontrol.py:840 216 | msgid "Get schedule" 217 | msgstr "获取日程表" 218 | 219 | #: nxvcontrol.py:877 220 | msgid "Update Sensors" 221 | msgstr "更新传感器" 222 | 223 | #: nxvcontrol.py:878 nxvcontrol.py:926 nxvcontrol.py:932 nxvcontrol.py:944 224 | #: nxvcontrol.py:948 nxvcontrol.py:952 nxvcontrol.py:956 nxvcontrol.py:960 225 | #: nxvcontrol.py:964 226 | msgid "OFF: " 227 | msgstr "关闭:" 228 | 229 | #: nxvcontrol.py:878 nxvcontrol.py:926 nxvcontrol.py:932 nxvcontrol.py:944 230 | #: nxvcontrol.py:948 nxvcontrol.py:952 nxvcontrol.py:956 nxvcontrol.py:960 231 | #: nxvcontrol.py:964 232 | msgid "ON: " 233 | msgstr "开启:" 234 | 235 | #: nxvcontrol.py:901 nxvcontrol.py:925 nxvcontrol.py:974 236 | msgid "LiDAR" 237 | msgstr "光达" 238 | 239 | #: nxvcontrol.py:931 240 | msgid "Wheels Controlled by Keypad" 241 | msgstr "键盘控制轮子" 242 | 243 | #: nxvcontrol.py:943 244 | msgid "Left Wheel" 245 | msgstr "左轮" 246 | 247 | #: nxvcontrol.py:947 248 | msgid "Right Wheel" 249 | msgstr "右轮" 250 | 251 | #: nxvcontrol.py:951 252 | msgid "LiDAR Motor" 253 | msgstr "光达电动机" 254 | 255 | #: nxvcontrol.py:955 256 | msgid "Vacuum" 257 | msgstr "真空吸尘" 258 | 259 | #: nxvcontrol.py:959 260 | msgid "Brush" 261 | msgstr "刷子" 262 | 263 | #: nxvcontrol.py:963 264 | msgid "Side Brush" 265 | msgstr "边刷" 266 | 267 | #: nxvcontrol.py:970 268 | msgid "Recharge" 269 | msgstr "充电" 270 | 271 | #: nxvforward.py:61 272 | msgid "nxvForward" 273 | msgstr "nxvForward" 274 | 275 | #: nxvforward.py:226 276 | msgid "Forward Neato XV control over network" 277 | msgstr "通过网络控制Neato XV" 278 | 279 | #: nxvforward.py:228 280 | msgid "Copyright © 2015–2016 The nxvForward Authors" 281 | msgstr "版权所有 © 2015–2016" 282 | 283 | #: nxvforward.py:237 nxvforward.py:345 284 | msgid "Server" 285 | msgstr "服务器" 286 | 287 | #: nxvforward.py:240 288 | msgid "Setup" 289 | msgstr "设置" 290 | 291 | #: nxvforward.py:245 292 | msgid "Bind Address:" 293 | msgstr "绑定地址" 294 | 295 | #: nxvforward.py:263 296 | msgid "Start" 297 | msgstr "开始" 298 | 299 | #: nxvforward.py:268 300 | msgid "Stop" 301 | msgstr "停止" 302 | 303 | #: nxvforward.py:289 nxvforward.py:346 304 | msgid "Test Client" 305 | msgstr "测试客户端" 306 | 307 | #~ msgid "Power DC Jack" 308 | #~ msgstr "电源插座" 309 | 310 | #~ msgid "Connected: " 311 | #~ msgstr "已连接" 312 | 313 | #~ msgid "Disconnected: " 314 | #~ msgstr "已断开: " 315 | 316 | #~ msgid "Dustbin" 317 | #~ msgstr "尘盒" 318 | 319 | #~ msgid "Out: " 320 | #~ msgstr "出:" 321 | 322 | #~ msgid "In: " 323 | #~ msgstr "入:" 324 | 325 | #~ msgid "Left Side Key" 326 | #~ msgstr "左边键" 327 | 328 | #~ msgid "Kicked: " 329 | #~ msgstr "碰到:" 330 | 331 | #~ msgid "Released: " 332 | #~ msgstr "已释放:" 333 | 334 | #~ msgid "Left Front Key" 335 | #~ msgstr "左前键" 336 | 337 | #~ msgid "Right Side Key" 338 | #~ msgstr "右边键" 339 | 340 | #~ msgid "Right Front Key" 341 | #~ msgstr "右前键" 342 | -------------------------------------------------------------------------------- /updatedata.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FN_DATA="nxvlogbatt-data-charging-oem-m2-1.txt nxvlogbatt-data-charging-oem-m2-2.txt nxvlogbatt-data-charging-powerextra-r1-m1-1.txt" 4 | FN_DATA="nxvlogbatt-data-charging-oem-m2-3.txt" 5 | 6 | # pacman -S inotify-tools gnuplot 7 | # apt-get install inotify-tools gnuplot 8 | if inotifywait -e modify ${FN_DATA}; then 9 | echo "plotting ..." 10 | date 11 | ./nxvlogbatt-plotfig.sh 12 | date 13 | echo "ploting done!" 14 | fi 15 | 16 | --------------------------------------------------------------------------------