├── 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 | [](https://www.python.org/downloads/)
5 | [](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 |
21 |
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 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | ## nxvforward
48 |
49 |
50 |
51 |
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 |
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 |
--------------------------------------------------------------------------------