agronode.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. from __future__ import print_function
  2. import wx
  3. import gui
  4. import aioserial
  5. from xmodem import XMODEM
  6. import threading
  7. import io
  8. #import wxmplot
  9. from datetime import datetime
  10. import locale
  11. import json
  12. import serial.tools.list_ports
  13. import shlex
  14. import parse
  15. import os
  16. import dfudfuse
  17. import firmware
  18. import cli
  19. import cli_bt
  20. import traceback
  21. import asyncio
  22. class console( gui.consoleDialog ):
  23. def __init__( self, parent, cli):
  24. #initialize parent class
  25. gui.consoleDialog.__init__(self,parent)
  26. self.cli = cli
  27. def onConsoleClose( self, event ):
  28. self.cli.command(b'stop\n', None)
  29. event.Skip()
  30. class frame( gui.mainFrame ):
  31. #constructor
  32. def __init__( self, parent ):
  33. #initialize parent class
  34. gui.mainFrame.__init__(self,parent)
  35. #decimal point must be .
  36. locale.setlocale(
  37. category=locale.LC_ALL,
  38. locale="US" # Note: do not use "de_DE" as it doesn't work
  39. )
  40. self.alive = threading.Event()
  41. self.mBoxSizerParams = self.m_comboBoxSizer.GetContainingSizer()
  42. # self.pl = wxmplot.PlotPanel(self.m_panelData)
  43. # self.m_customControlData.GetContainingSizer().Add(self.pl, 1, wx.EXPAND, 5)
  44. ports = list(serial.tools.list_ports.comports())
  45. for p in ports:
  46. print(p)
  47. # self.console = None
  48. #dev = usb.core.find(idVendor=0x0483, idProduct=0x5740)
  49. #dev = usb.core.find(idVendor=0x0483, idProduct=0x3748)
  50. #dev = usb.core.find(find_all=True, bDeviceClass=7)
  51. #print(dev)
  52. def onShow( self, event ):
  53. self.connect(event)
  54. event.Skip()
  55. def onClose( self, event ):
  56. try:
  57. self.cli.command(b'reset\n', None)
  58. self.cli.close()
  59. except:
  60. pass
  61. wx.CallAfter(self.Destroy)
  62. event.Skip()
  63. def onSerialChar(self, c):
  64. try:
  65. if c != '\r':
  66. self.console.m_textConsole.AppendText(c)
  67. except Exception as e:
  68. pass
  69. def resInfo(self, lines):
  70. self.m_staticTextNode.SetLabel("".join(lines))
  71. def resFinfo(self, lines):
  72. self.m_staticTextFlash.SetLabel("".join(lines))
  73. def resDateTime(self, lines):
  74. self.m_staticTextDatetime.SetLabel("Datetime: " + lines[0])
  75. def connect( self, event ):
  76. ports = list(serial.tools.list_ports.comports())
  77. for p in ports:
  78. if p.vid==0x483 and p.pid==0x5740:
  79. port = p.device
  80. try:
  81. port
  82. except NameError:
  83. resp = wx.MessageBox('Node is not connected', 'Agronode setup',
  84. wx.OK | wx.ICON_ERROR)
  85. # self.Destroy()
  86. # return
  87. try:
  88. self.cli = cli.CliThread(self,port, self.onSerialChar)
  89. self.cli.start()
  90. except serial.serialutil.SerialException:
  91. resp = wx.MessageBox('Can not open virtual port', 'Agronode setup',
  92. wx.OK | wx.ICON_ERROR)
  93. self.Destroy()
  94. return
  95. self.cli.command(b'stop\n', None)
  96. self.cli.command(b'log 1\n', None)
  97. self.cli.command(b'trace sdi12 0\n', None)
  98. self.cli.command(b'power off\n', None)
  99. self.cli.command(b'info\n', self.resInfo)
  100. self.cli.command(b'finfo\n', self.resFinfo)
  101. now = datetime.now()
  102. cmd = 'date '+ now.strftime("%Y-%m-%d %H:%M:%S") + '\n'
  103. self.cli.command(bytearray(cmd, 'utf-8'), self.resDateTime)
  104. #-------------------------------------------------------------------
  105. def onPageChange( self, event ):
  106. event.Skip()
  107. page_text = self.m_notebook.GetPageText(event.GetSelection())
  108. if page_text == 'script':
  109. self.onScriptWindow(event)
  110. if page_text == 'data':
  111. self.onDataWindow(event)
  112. if page_text == 'sensor':
  113. self.onSensorWindow(event)
  114. if page_text == 'sdi12':
  115. self.onSDI12Window(event)
  116. if page_text == '1wire':
  117. self.on1wireWindow(event)
  118. if page_text == 'firmware':
  119. self.onFirmwareWindow(event)
  120. def resScriptWindow(self, lines):
  121. self.m_gaugeAct.SetValue(0)
  122. self.scriptUpdate(lines)
  123. def onScriptWindow( self, event ):
  124. if len(self.m_textCtrlScript.GetValue()) == 0:
  125. #self.onDownloadScript(None)
  126. self.m_gaugeAct.Pulse()
  127. self.cli.command(b'list\n', self.resScriptWindow)
  128. def onSensorWindow( self, event ):
  129. with open('sensors.json') as json_file:
  130. self.json_data = json.load(json_file)
  131. self.m_comboBoxSensor.Clear()
  132. for sensor in self.json_data['sensors']:
  133. self.m_comboBoxSensor.Append(sensor['name'])
  134. self.m_comboBoxSensor.SetSelection(0)
  135. self.onSensorChange(event)
  136. self.cli.command(b'power on\n', None)
  137. def onSDI12Window( self, event ):
  138. self.cli.command(b'power on\n', None)
  139. def on1wireWindow( self, event ):
  140. self.cli.command(b'power on\n', None)
  141. def onFirmwareWindow( self, event ):
  142. pass
  143. #f = firmware.Firmware(0x483, 0xdf11, 0, 0, 0)
  144. #f.erase()
  145. #dfuse.DfuFile('agronode.dfu'))
  146. #-------------------------------------------------------------------
  147. def dataUpdate(self, lines):
  148. self.m_listCtrlData.ClearAll();
  149. self.m_listCtrlData.InsertColumn(0, "date_time", width = -1);
  150. self.m_listCtrlData.InsertColumn(1, "sensor_address", width = -1);
  151. self.m_listCtrlData.InsertColumn(2, "sensor_id", width = -1);
  152. self.m_listCtrlData.InsertColumn(3, "value", width = -1);
  153. xdata = []
  154. ydata = []
  155. for line in lines:
  156. line = line.replace("\r", "").replace("\n", "")
  157. items = line.split(';')
  158. # xdata.append(datetime.strptime(items[0], "%Y/%m/%d %H:%M:%S").timestamp())
  159. # items[3] = items[3].replace('NAN', 'NaN')
  160. # ydata.append(float(items[3]))
  161. self.m_listCtrlData.Append(items)
  162. # m_gridData(
  163. # self.pl.plot(xdata, ydata, use_dates='True')
  164. self.m_listCtrlData.SetColumnWidth(0, width = wx.LIST_AUTOSIZE);
  165. self.m_listCtrlData.SetColumnWidth(1, width = wx.LIST_AUTOSIZE);
  166. self.m_listCtrlData.SetColumnWidth(2, width = wx.LIST_AUTOSIZE);
  167. self.m_listCtrlData.SetColumnWidth(3, width = wx.LIST_AUTOSIZE);
  168. def onSaveData( self, event ):
  169. with wx.FileDialog(self, "Save CSV file", wildcard="CSV files (*.csv)|*.csv",
  170. style=wx.FD_SAVE) as fileDialog:
  171. if fileDialog.ShowModal() == wx.ID_CANCEL:
  172. return # the user changed their mind
  173. # Proceed loading the file chosen by the user
  174. pathname = fileDialog.GetPath()
  175. try:
  176. with open(pathname, 'w') as file:
  177. file.write("time_stamp;sensor_adr;sensor_id;observed_value\n")
  178. count = self.m_listCtrlData.GetItemCount()
  179. cols = self.m_listCtrlData.GetColumnCount()
  180. for row in range(count):
  181. if ((self.m_listCtrlData.GetSelectedItemCount() == 0) or (self.m_listCtrlData.IsSelected(idx=row))):
  182. line = ""
  183. for col in range(cols):
  184. if (col > 0):
  185. line += ";"
  186. line += self.m_listCtrlData.GetItem(itemIdx=row, col=col).GetText()
  187. file.write(line + "\n")
  188. file.close()
  189. except IOError:
  190. wx.LogError("Cannot save file '%s'." % pathname)
  191. def resDataWindowGet(self, lines):
  192. self.m_gaugeAct.SetValue(0)
  193. self.dataUpdate(lines)
  194. def resDataWindow(self, lines):
  195. self.finfo = parse.parse('Ffs head {head}, tail {tail}, terminus {terminus}, unsent {unsent}', lines[2])
  196. self.m_gaugeAct.Pulse()
  197. self.cli.command(b'fget ' + bytes(self.finfo['terminus'],'utf-8') + b' ' + bytes(str(int(self.finfo['head'])),'utf-8') + b'\n', self.resDataWindowGet)
  198. def onDataWindow( self, event ):
  199. self.m_listCtrlData.ClearAll();
  200. self.m_listCtrlData.InsertColumn(0, "Downloading data", width = -1);
  201. self.cli.command(b'finfo\n', self.resDataWindow)
  202. #-------------------------------------------------------------------
  203. def onDFuse( self, event ):
  204. self.cli.command(b'bootld\n', None)
  205. event.Skip()
  206. #-------------------------------------------------------------------
  207. def onTextChar( self, event ):
  208. key_code = event.GetKeyCode()
  209. # Allow ASCII numerics
  210. if ord('0') <= key_code <= ord('9'):
  211. event.Skip()
  212. return
  213. if ord('A') <= key_code <= ord('Z'):
  214. event.Skip()
  215. return
  216. if ord('a') <= key_code <= ord('z'):
  217. event.Skip()
  218. return
  219. # Allow tabs, for tab navigation between TextCtrls
  220. if key_code < ord(' '):
  221. event.Skip()
  222. return
  223. if key_code == 127:
  224. event.Skip()
  225. return
  226. event.Skip()
  227. return
  228. def scriptUpdate(self, lines):
  229. self.mBoxSizerParams.Clear(True)
  230. #self.m_scrolledWindowParams.Layout()
  231. #self.m_panelScript.SetAutoLayout(1)
  232. self.lines = lines
  233. self.m_textCtrlScript.SetValue("".join(self.lines))
  234. for line in self.lines:
  235. line = line.replace("\r", "").replace("\n", "")
  236. print(line)
  237. #items = line.split(' ')
  238. items = shlex.split(line, posix=False)
  239. if len(items) > 0:
  240. if len(items[0]) > 0:
  241. if items[0] == '@title':
  242. title = wx.StaticText(self.m_scrolledWindowParams, wx.ID_ANY, " ".join(items[1:]), style=wx.ALIGN_CENTRE_HORIZONTAL)
  243. self.mBoxSizerParams.Add(title, 0, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 5)
  244. elif items[0] == '@group':
  245. group = wx.StaticLine(self.m_scrolledWindowParams, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)
  246. self.mBoxSizerParams.Add(group, 0, wx.EXPAND |wx.ALL, 5 )
  247. elif items[0][0] == '@':
  248. p = {}
  249. for i in range(len(items) // 2):
  250. p[items[i * 2]] = items[i * 2 + 1].strip('\"')
  251. label = wx.StaticText(self.m_scrolledWindowParams, wx.ID_ANY, items[0][1:] + ":")
  252. self.mBoxSizerParams.Add(label, 0, wx.LEFT|wx.TOP, 5)
  253. try:
  254. if p.get('gui') == None:
  255. param = wx.TextCtrl(self.m_scrolledWindowParams, wx.ID_ANY, p[items[0]], wx.DefaultPosition, wx.DefaultSize, wx.CB_READONLY)
  256. else:
  257. if p['gui'] == "combo":
  258. choices = p['choices'].split(',')
  259. param = wx.ComboBox(self.m_scrolledWindowParams, wx.ID_ANY, items[0], wx.DefaultPosition, wx.DefaultSize, choices, wx.CB_READONLY)
  260. param.SetValue(p[items[0]])
  261. param.Bind(wx.EVT_TEXT, self.onComboChange)
  262. if p['gui'] == "spin":
  263. param = wx.SpinCtrl(self.m_scrolledWindowParams, wx.ID_ANY, items[0])
  264. param.SetRange(int(p['min']), int(p['max']))
  265. param.SetValue(int(p[items[0]]))
  266. param.Bind(wx.EVT_SPINCTRL, self.onSpinChange)
  267. #self.m_comboBoxVarSelect.Append(items[0][1:])
  268. if p['gui'] == "spinfloat":
  269. param = wx.SpinCtrlDouble(self.m_scrolledWindowParams, wx.ID_ANY, items[0])
  270. param.SetRange(float(p['min']), float(p['max']))
  271. param.SetValue(float(p[items[0]]))
  272. param.SetDigits(3)
  273. param.SetIncrement(float(p['step']))
  274. param.Bind(wx.EVT_SPINCTRLDOUBLE, self.onSpinFloatChange)
  275. if p['gui'] == "text":
  276. param = wx.TextCtrl(self.m_scrolledWindowParams, wx.ID_ANY, p[items[0]], wx.DefaultPosition, wx.DefaultSize)
  277. param.SetMaxLength(int(p['len']))
  278. param.Bind(wx.EVT_TEXT, self.onTextChange)
  279. param.Bind(wx.EVT_CHAR, self.onTextChar)
  280. param.SetName(items[0]);
  281. self.mBoxSizerParams.Add(param, 0, wx.LEFT, 5)
  282. param.SetToolTip(p['tip']);
  283. except Exception as e:
  284. pass
  285. #print(e)
  286. #traceback.print_exc()
  287. #print ()
  288. #self.m_scrolledWindowParams.Fit(self.mBoxSizerParams)
  289. #self.m_scrolledWindowParams.SetMinSize(wx.Size(200,1000))
  290. #self.mBoxSizerParams.SetMinSize(wx.Size(200,1000))
  291. #self.m_scrolledWindowParams.Layout()
  292. #self.m_scrolledWindowParams.Refresh()
  293. self.mBoxSizerParams.Layout()
  294. def onGuiChange(self, name, value):
  295. line_no = [i for i, s in enumerate(self.lines) if name in s][0]
  296. line = self.lines[line_no]
  297. linesplitted = line.split(' ')
  298. linesplitted[1] = value
  299. line = " ".join(linesplitted)
  300. self.lines[line_no] = line
  301. self.m_textCtrlScript.SetValue("".join(self.lines))
  302. def onSpinChange(self, event):
  303. event.Skip()
  304. self.onGuiChange(event.GetEventObject().GetName(), str(event.GetEventObject().GetValue()))
  305. def onSpinFloatChange(self, event):
  306. event.Skip()
  307. self.onGuiChange(event.GetEventObject().GetName(), str(event.GetEventObject().GetValue()))
  308. def onComboChange(self, event):
  309. event.Skip()
  310. self.onGuiChange(event.GetEventObject().GetName(), str(event.GetEventObject().GetValue()))
  311. def onTextChange(self, event):
  312. event.Skip()
  313. self.onGuiChange(event.GetEventObject().GetName(), str(event.GetEventObject().GetValue()))
  314. def onLoadScript( self, event ):
  315. event.Skip()
  316. with wx.FileDialog(self, "Open BAS file", wildcard="BAS files (*.bas)|*.bas",
  317. style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as fileDialog:
  318. if fileDialog.ShowModal() == wx.ID_CANCEL:
  319. return # the user changed their mind
  320. # Proceed loading the file chosen by the user
  321. pathname = fileDialog.GetPath()
  322. try:
  323. with open(pathname, 'r') as file:
  324. lines = file.readlines()
  325. self.scriptUpdate(lines)
  326. file.close()
  327. except IOError:
  328. wx.LogError("Cannot open file '%s'." % pathname)
  329. def onUploadScript( self, event ):
  330. event.Skip()
  331. #self.cli.command(b'xmodem\n', None)
  332. self.cli.xmodem(self.lines)
  333. def onSaveScript( self, event ):
  334. event.Skip()
  335. with wx.FileDialog(self, "Save BAS file", wildcard="BAS files (*.bas)|*.bas",
  336. style=wx.FD_SAVE) as fileDialog:
  337. if fileDialog.ShowModal() == wx.ID_CANCEL:
  338. return # the user changed their mind
  339. # Proceed loading the file chosen by the user
  340. pathname = fileDialog.GetPath()
  341. try:
  342. with open(pathname, 'w') as file:
  343. file.writelines(self.lines)
  344. file.close()
  345. except IOError:
  346. wx.LogError("Cannot save file '%s'." % pathname)
  347. def onDownloadScript( self, event ):
  348. event.Skip()
  349. self.cli.command(b'list\n', self.resScriptWindow)
  350. def resRunScript( self, lines):
  351. self.console = console(self, self.cli)
  352. self.console.ShowModal()
  353. def onRunScript( self, event ):
  354. event.Skip()
  355. self.cli.command(b'run\n', self.resRunScript)
  356. #-------------------------------------------------------------------
  357. def resSDI12AddressQuery( self, lines):
  358. adr = lines[0].split(' ')[2].replace('\r','').replace('\n','')
  359. if adr == 'timeouted':
  360. wx.MessageBox('No sensor found', 'Agronode setup', wx.OK | wx.ICON_ERROR)
  361. else:
  362. self.m_textCtrlSDI12Adress.SetLabel(lines[0].split(' ')[2].replace('\r','').replace('\n',''))
  363. def onSDI12AddressQuery( self, event ):
  364. self.cli.command(b'sdi12 ?!\n', self.resSDI12AddressQuery)
  365. event.Skip()
  366. def onSDI12AddressChange( self, event ):
  367. self.cli.command(bytes('sdi12 ' + self.m_textCtrlSDI12Adress.Value + 'A' + self.m_textCtrlSDI12AdressChange.Value + '!\n', 'utf-8'), None)
  368. event.Skip()
  369. def resSDI12Identify( self, lines ):
  370. self.m_textCtrlSDI12Identification.SetValue(lines[0][7:])
  371. def onSDI12Identify( self, event ):
  372. self.cli.command(bytes('sdi12 ' + self.m_textCtrlSDI12Adress.Value + 'I!\n','utf-8'), self.resSDI12Identify)
  373. event.Skip()
  374. def resSDI12Command( self, lines ):
  375. self.m_textCtrlSDI12CommandResult.SetValue(lines[0].split(':')[1][1:])
  376. def onSDI12Command( self, event ):
  377. self.cli.command(bytes('sdi12 ' + self.m_comboBoxSDI12Command.GetValue() + '\n','utf-8'), self.resSDI12Command)
  378. self.m_comboBoxSDI12Command.Append(self.m_comboBoxSDI12Command.GetValue())
  379. #todo: zabranit zdvojeni v combo boxu kdyz jsou stejny
  380. event.Skip()
  381. def onSDI12Char( self, event ):
  382. key_code = event.GetKeyCode()
  383. # Allow ASCII numerics
  384. if ord('0') <= key_code <= ord('9'):
  385. event.Skip()
  386. return
  387. if ord('A') <= key_code <= ord('Z'):
  388. event.Skip()
  389. return
  390. if ord('a') <= key_code <= ord('z'):
  391. event.Skip()
  392. return
  393. # Allow tabs, for tab navigation between TextCtrls
  394. if key_code < ord(' '):
  395. event.Skip()
  396. return
  397. if key_code == 127:
  398. event.Skip()
  399. return
  400. # Block everything else
  401. return
  402. #-------------------------------------------------------------------
  403. def res1wireSearch(self, lines):
  404. if lines[0][0:13] == 'No chip found':
  405. wx.MessageBox('No sensor found', 'Agronode setup', wx.OK | wx.ICON_ERROR)
  406. else:
  407. for line in lines:
  408. wx.CallAfter(self.m_listBox1wire.Append, line)
  409. def on1wireSearch( self, event ):
  410. self.m_listBox1wire.Clear()
  411. self.m_button1wireRemap.Enable(False)
  412. self.cli.command(b'owsearch\n', self.res1wireSearch)
  413. event.Skip()
  414. def on1wireRemap( self, event ):
  415. self.cli.command(bytes('owremap ' +
  416. self.m_listBox1wire.GetString(self.m_listBox1wire.GetSelection()).split(' ')[0] +
  417. ' ' +
  418. str(self.m_spinCtrl1wireAdr.GetValue()) +
  419. '\n','utf-8'), None)
  420. self.m_listBox1wire.Clear()
  421. self.m_button1wireRemap.Enable(False)
  422. self.cli.command(b'owsearch\n', self.res1wireSearch)
  423. event.Skip()
  424. def on1wireSelected( self, event ):
  425. self.m_spinCtrl1wireAdr.SetValue(self.m_listBox1wire.GetString(self.m_listBox1wire.GetSelection()).split(' ')[1].replace('\r', '').replace('\n',''))
  426. self.m_button1wireRemap.Enable(True)
  427. event.Skip()
  428. #-------------------------------------------------------------------
  429. def onSensorChange( self, event ):
  430. try:
  431. self.m_spinSensorAddress.SetValue(0)
  432. self.m_comboBoxType.Clear()
  433. for type in self.json_data['sensors'][self.m_comboBoxSensor.GetSelection()]['types']:
  434. self.m_comboBoxType.Append(type['desc'])
  435. self.m_comboBoxType.SetSelection(0)
  436. self.m_spinSensorAddress.SetValue(self.json_data['sensors'][self.m_comboBoxSensor.GetSelection()]['adr'])
  437. except Exception as e: #todo: spravnou excepsnu
  438. pass
  439. event.Skip()
  440. def resGet( self, lines ):
  441. self.m_textCtrlSensorValue.SetValue(lines[0].split(':')[1][1:] + ' ' + self.json_data['sensors'][self.m_comboBoxSensor.GetSelection()]['types'][self.m_comboBoxType.GetSelection()]['unit'])
  442. def onGet( self, event ):
  443. self.cli.command(b'invalidate\n', None)
  444. # self.nodeSerial.write(b'invalidate\n')
  445. self.cli.command(bytes('sget '
  446. # self.nodeSerial.write(bytes('sget '
  447. + self.json_data['sensors'][self.m_comboBoxSensor.GetSelection()]['lib']
  448. + ' '
  449. + str(self.m_spinSensorAddress.GetValue())
  450. + ' '
  451. + str(self.json_data['sensors'][self.m_comboBoxSensor.GetSelection()]['types'][self.m_comboBoxType.GetSelection()]['type'])
  452. + '\n'
  453. , 'utf-8'), self.resGet)
  454. # self.m_staticTextValue.SetLabel('')
  455. # self.m_staticTextUnit.SetLabel('')
  456. # self.m_staticTextUnit.SetLabel(self.json_data['sensors'][self.m_comboBoxSensor.GetSelection()]['types'][self.m_comboBoxType.GetSelection()]['unit'])
  457. self.m_textCtrlSensorValue.SetValue('');
  458. event.Skip()
  459. #-------------------------------------------------------------------
  460. if __name__ == '__main__':
  461. # When this module is run (not imported) then create the app, the
  462. # frame, show it, and start the event loop.
  463. app = wx.App()
  464. # setts = LazySettings('settings.cfg')
  465. frm = frame(None)
  466. frm.Show()
  467. app.MainLoop()