openmv/usr/qt4-ide.py

733 lines
25 KiB
Python
Executable File

#!/usr/bin/env python
from __future__ import print_function, division
import serial
from framebuffer import FrameBuffer
from editor import *
import pydfu
from terminal import *
from PyQt4.QtGui import *
from serial import *
from serial.tools import list_ports
from time import sleep
import sys
import os
import signal
import openmv
from ConfigParser import ConfigParser
class OpenMVConnector(QObject):
found = pyqtSignal()
not_found = pyqtSignal()
def __init__(self):
QObject.__init__(self)
self.timer = QTimer()
self.timer.timeout.connect(self.auto_detect)
self.auto_connect = False
def start(self):
#self.timer.start(500)
print('need to add openmv.find() before this can work...')
def stop(self):
self.timer.stop()
def set_auto_connect(self, auto):
self.auto_connect = auto
def auto_detect(self):
if openmv.find():
if self.auto_connect:
self.found.emit()
else:
self.not_found.emit()
class OpenMVIDE(QMainWindow):
def __init__(self):
super(OpenMVIDE, self).__init__()
############################################################################################################
## Connector detector
# Connector detects camera connect/disconnect
self.connector = OpenMVConnector()
self.connector.start()
# Catch connect / disconnect notifications
self.connector.found.connect(self.do_connect)
self.connector.not_found.connect(self.do_disconnect)
############################################################################################################
## Paths
# default working directory
self.dir = os.path.dirname(os.path.realpath(__file__)) + '/'
# default icons dir
self.icon_dir = self.dir + 'icons/'
# default examples location
self.example_dir = self.dir + 'examples/'
# default scripts location
self.script_dir = self.dir + 'scripts/'
# Location for binary firmware
self.flash_dir = self.dir + '../bin/'
# Location for config file
self.config_file = 'openmv.config'
# script filename
self.filename = ''
############################################################################################################
## Configuration
default = {'board': 'openmv1',
'filename': '',
'recent': '',
'bin': '',
'auto_flash': 'False',
'auto_connect': 'True',
'width': '800',
'height': '600',
'scale': '1',
'serial_port': '/dev/openmvcam'}
# load config
config = self.load_config(default)
############################################################################################################
## State variables
# Connect status
self.connected = False
# Script run status
self.running = False
# Frame buffer
self.image = None
# Recent scripts
self.recent = set()
files = config.get('main', 'recent')
for f in files.split(','):
if f:
self.recent.add(f)
# Recent binary
self.flash_file = config.get('main', 'bin')
# Serial port
self.serial_port = config.get('main', 'serial_port')
# Board version
self.board = config.get('main', 'board')
# default auto connect behavior
self.auto_connect = config.getboolean('main', 'auto_connect')
self.connector.set_auto_connect(self.auto_connect)
# default auto flash behavior
self.auto_flash = config.getboolean('main', 'auto_flash')
############################################################################################################
## Components
# Editor
self.editor = PyEditor(self)
self.editor.setMinimumWidth(300)
self.editor.setMinimumHeight(200)
self.editor.document().contentsChanged.connect(self.update_ui)
# FrameBuffer
self.framebuffer = FrameBuffer()
self.framebuffer.set_scale(config.getfloat('main', 'scale'))
self.framebuffer.show()
self.framebuffer.error.connect(self.do_disconnect)
############################################################################################################
## Actions
self.exit_action = QAction(QIcon(self.icon_dir + 'Exit.png'), 'Exit', self)
self.exit_action.setStatusTip('Exit application')
self.exit_action.setShortcut('Ctrl+Q')
self.exit_action.triggered.connect(self.do_quit)
self.connect_action = QAction(QIcon(self.icon_dir + 'Connect.png'), 'Connect', self)
self.connect_action.setStatusTip('Connect to camera')
self.connect_action.triggered.connect(self.do_connect)
self.reset_action = QAction(QIcon(self.icon_dir + 'Eject.png'), 'Reset', self)
self.reset_action.setStatusTip('Reset camera and disconnect')
self.reset_action.triggered.connect(self.do_disconnect)
self.flash_action = QAction(QIcon(self.icon_dir + 'Lightning.png'), 'Flash', self)
self.flash_action.setStatusTip('Flash new firmware')
self.flash_action.triggered.connect(self.do_flash)
self.run_action = QAction(QIcon(self.icon_dir + 'Run.png'), 'Run', self)
self.run_action.setStatusTip('Run script')
self.run_action.triggered.connect(self.do_run)
self.stop_action = QAction(QIcon(self.icon_dir + 'Stop.png'), 'Stop', self)
self.stop_action.setStatusTip('Stop script')
self.stop_action.triggered.connect(self.do_stop)
self.new_action = QAction(QIcon(self.icon_dir + 'NewDocument.png'), 'New', self)
self.new_action.setStatusTip('New script')
self.new_action.triggered.connect(self.do_new)
self.open_action = QAction(QIcon(self.icon_dir + 'Open.png'), 'Open...', self)
self.open_action.setStatusTip('Open script')
self.open_action.setShortcut('Ctrl+O')
self.open_action.triggered.connect(self.do_open)
self.save_action = QAction(QIcon(self.icon_dir + 'Save.png'), 'Save', self)
self.save_action.setStatusTip('Save script')
self.save_action.setShortcut('Ctrl+S')
self.save_action.triggered.connect(self.do_save)
self.save_as_action = QAction(QIcon(self.icon_dir + 'SaveAs.png'), 'Save As...', self)
self.save_as_action.setStatusTip('Save script as new file')
self.save_as_action.triggered.connect(self.do_save_as)
self.zoom_in_action = QAction(QIcon(self.icon_dir + 'ZoomIn.png'), 'Zoom In', self)
self.zoom_in_action.setStatusTip('Make frame buffer preview bigger')
self.zoom_in_action.triggered.connect(self.do_zoom_in)
self.zoom_out_action = QAction(QIcon(self.icon_dir + 'ZoomOut.png'), 'Zoom Out', self)
self.zoom_out_action.setStatusTip('Make frame buffer preview smaller')
self.zoom_out_action.triggered.connect(self.do_zoom_out)
self.zoom_reset_action = QAction(QIcon(self.icon_dir + 'ZoomReset.png'), 'Zoom Reset', self)
self.zoom_reset_action.setStatusTip('Reset frame buffer preview size')
self.zoom_reset_action.triggered.connect(self.do_zoom_reset)
self.cut_action = QAction(QIcon(self.icon_dir+'Cut.png'), 'Cut', self)
self.cut_action.triggered.connect(self.editor.cut)
self.cut_action.setShortcut(QKeySequence.Cut)
self.paste_action = QAction(QIcon(self.icon_dir+'Paste.png'), 'Paste', self)
self.paste_action.triggered.connect(self.editor.paste)
self.paste_action.setShortcut(QKeySequence.Paste)
self.copy_action = QAction(QIcon(self.icon_dir+'Copy.png'), 'Copy', self)
self.copy_action.triggered.connect(self.editor.copy)
self.copy_action.setShortcut(QKeySequence.Copy)
self.auto_flash_action = QAction('Auto-flash', self)
self.auto_flash_action.setStatusTip('Automatically detect DFU mode and flash firmware')
self.auto_flash_action.triggered.connect(self.do_auto_flash)
self.auto_flash_action.setCheckable(True)
self.auto_flash_action.setDisabled(True)
self.auto_connect_action = QAction('Auto-connect', self)
self.auto_connect_action.setStatusTip('Automatically connect to OpenMV when detected')
self.auto_connect_action.triggered.connect(self.do_auto_connect)
self.auto_connect_action.setCheckable(True)
self.about_action = QAction(QIcon(self.icon_dir+'About.png'), 'About', self)
self.about_action.triggered.connect(self.do_about)
############################################################################################################
## Toolbars
self.toolbar1 = self.addToolBar('toolbar1')
self.toolbar1.addAction(self.connect_action)
self.toolbar1.addAction(self.reset_action)
self.toolbar1.addAction(self.flash_action)
self.toolbar1.addAction(self.run_action)
self.toolbar1.addAction(self.stop_action)
self.toolbar2 = self.addToolBar('toolbar2')
self.toolbar2.addAction(self.new_action)
self.toolbar2.addAction(self.open_action)
self.toolbar2.addAction(self.save_action)
self.toolbar3 = self.addToolBar('toolbar3')
self.toolbar3.addAction(self.zoom_reset_action)
self.toolbar3.addAction(self.zoom_out_action)
self.toolbar3.addAction(self.zoom_in_action)
############################################################################################################
## Menus
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu('&File')
file_menu.addAction(self.new_action)
file_menu.addAction(self.open_action)
file_menu.addAction(self.save_action)
file_menu.addAction(self.save_as_action)
file_menu.addSeparator()
self.example_menu = QMenu('Examples')
self.recent_menu = QMenu('Recent')
file_menu.addMenu(self.example_menu)
file_menu.addMenu(self.recent_menu)
file_menu.addSeparator()
file_menu.addAction(self.exit_action)
edit_menu = menu_bar.addMenu('&Edit')
edit_menu.addAction(self.cut_action)
edit_menu.addAction(self.copy_action)
edit_menu.addAction(self.paste_action)
openmv_menu = menu_bar.addMenu('&Tools')
openmv_menu.addAction(self.connect_action)
openmv_menu.addAction(self.reset_action)
openmv_menu.addAction(self.flash_action)
openmv_menu.addAction(self.auto_connect_action)
openmv_menu.addAction(self.auto_flash_action)
view_menu = menu_bar.addMenu('&View')
view_menu.addAction(self.zoom_in_action)
view_menu.addAction(self.zoom_out_action)
view_menu.addAction(self.zoom_reset_action)
help_menu = menu_bar.addMenu('&Help')
help_menu.addAction(self.about_action)
# Dynamically update dyanmic menus
file_menu.aboutToShow.connect(self.update_example_menu)
file_menu.aboutToShow.connect(self.update_recent_menu)
# Connect dynamic menu items with correct handlers
self.example_menu.triggered.connect(self.do_open_example)
self.recent_menu.triggered.connect(self.do_open_recent)
############################################################################################################
## Main Window
# Status bar
self.statusBar()
# Geometry
self.setGeometry(50, 50, config.getint('main', 'width'), config.getint('main', 'height'))
# Vertical box for framebuffer
pvbox = QVBoxLayout()
pvbox.addWidget(self.framebuffer)
pvbox.addStretch(1)
# Horizontal box for editor + framebuffer
hbox = QHBoxLayout()
hbox.addWidget(self.editor)
hbox.addLayout(pvbox)
self.terminal = SerialTerminal(self)
self.terminal.setMinimumHeight(100)
self.terminal.setMaximumHeight(200)
self.terminal.show()
self.serial = Serial()
self.terminal.error.connect(self.do_disconnect)
# Vertical box for hbox + terminal
vbox = QVBoxLayout()
vbox.addLayout(hbox)
vbox.addWidget(self.terminal)
w = QWidget()
w.setLayout(vbox)
self.setCentralWidget(w)
# UI Statuses
self.update_ui()
self.show()
def load_config(self, default):
config = ConfigParser(default)
config.add_section('main')
try:
config.read(self.config_file)
except (IOError, OSError, ConfigParser.Error) as e:
pass
print("Failed to open config file: %s" % e)
finally:
return config
def save_config(self):
config = ConfigParser()
config.add_section('main')
config.set('main', 'serial_port', self.serial_port)
config.set('main', 'auto_connect', self.auto_connect)
config.set('main', 'auto_flash', self.auto_flash)
config.set('main', 'height', self.height())
config.set('main', 'width', self.width())
config.set('main', 'scale', self.framebuffer.scale)
config.set('main', 'recent', ','.join(self.recent))
config.set('main', 'bin', self.flash_file)
try:
with open(self.config_file, 'w') as f:
config.write(f)
except (IOError, OSError) as e:
print('Failed to save config file %s' % e)
def update_ui(self):
self.run_action.setEnabled(self.connected)
self.stop_action.setEnabled(self.connected and self.running)
self.connect_action.setEnabled(not self.connected)
self.reset_action.setEnabled(self.connected)
self.flash_action.setEnabled(self.connected)
self.save_action.setEnabled(self.editor.document().isModified())
self.zoom_in_action.setEnabled(self.connected)
self.zoom_out_action.setEnabled(self.connected)
self.zoom_reset_action.setEnabled(self.connected)
self.auto_connect_action.setChecked(self.auto_connect)
self.auto_flash_action.setChecked(self.auto_flash)
if self.connected:
con = '[connected]'
else:
con = '[disconnected]'
if self.filename:
fn = os.path.basename(self.filename)
else:
fn = 'untitled.py'
self.setWindowTitle('OpenMV IDE (' + fn + ') - ' + con)
def update_example_menu(self):
if os.path.isdir(self.example_dir):
self.example_menu.clear()
files = sorted(os.listdir(self.example_dir))
self.example_menu.setEnabled(len(files) > 0)
for f in files:
if f.endswith(".py"):
action = QAction(f, self)
self.example_menu.addAction(action)
def update_recent_menu(self):
self.recent_menu.clear()
self.recent_menu.setEnabled(len(self.recent) > 0)
for f in self.recent:
self.recent_menu.addAction(QAction(f, self))
def do_auto_connect(self):
self.auto_connect = not self.auto_connect
self.connector.set_auto_connect(self.auto_connect)
self.update_ui()
def do_connect(self):
if not self.connected:
self.connected = True
try:
# init OpenMV
openmv.init()
sleep(0.2)
# interrupt any running code
openmv.stop_script()
sleep(0.2)
# first, check to see if self.serial_port is in the list of enumerated ports
# then try to open it. If that fails, or if the port isn't in the enumerated
# list, then prompt the user for a port
ports = []
for p in list_ports.comports():
name = p[0]
try:
ser = Serial(name, 115200, timeout=1)
ser.close()
ports.append(p)
except (IOError, OSError):
pass
print(ports)
self.serial = Serial(self.serial_port, 115200, timeout=1)
self.terminal.start(self.serial)
except IOError as e:
print('error connecting OpenMV Cam: %s' % e)
self.connected = False
else:
self.update_ui()
self.statusBar().showMessage('OpenMV Cam connected.')
self.framebuffer.start_updater()
def do_disconnect(self):
if self.connected:
self.connected = False
self.update_ui()
self.framebuffer.stop_updater()
openmv.stop_script()
sleep(0.2)
# release OpenMV
openmv.release()
sleep(0.2)
openmv.reset()
sleep(0.2)
self.connector.start()
self.statusBar().showMessage('Camera disconnected.')
try:
if self.serial and self.serial.isOpen():
print('Disconnecting terminal')
self.serial.close()
self.terminal.reset()
except IOError as e:
print('error disconnecting OpenMV Serial: %s' % e)
def do_auto_flash(self):
self.auto_flash = not self.auto_flash
self.update_ui()
def do_flash(self):
self.update_ui()
if self.connected:
dialog = QDialog(self)
filename = QLineEdit()
filename = QLineEdit()
chooser = QFileDialog()
filename.setText(self.flash_file)
chooser.setDirectory(self.flash_dir)
browse = QPushButton('&...')
browse.pressed.connect(chooser.exec_)
chooser.fileSelected.connect(filename.setText)
hlayout1 = QHBoxLayout()
hlayout1.addWidget(filename, stretch=1)
hlayout1.addWidget(browse)
ok_button = QPushButton('&Ok')
cancel_button = QPushButton('&Cancel')
hlayout2 = QHBoxLayout()
hlayout2.addWidget(ok_button)
hlayout2.addWidget(cancel_button)
vlayout = QVBoxLayout()
vlayout.addLayout(hlayout1)
vlayout.addLayout(hlayout2)
dialog.setLayout(vlayout)
ok_button.pressed.connect(dialog.accept)
cancel_button.pressed.connect(dialog.reject)
dialog.exec_()
dialog.hide()
if dialog.result() == QDialog.Accepted:
print('Accepted')
file = filename.text()
try:
with open(file, 'r') as f:
buf = f.read()
except (IOError, OSError) as e:
QErrorMessage(self).showMessage('Error opening binary file:\n%s' % e)
print('Error opening binary file: %s' % e)
else:
self.flash_file = file
self.flash_update(buf)
else:
print('Rejected')
def flash_update(self, buf):
flash_offsets = [0x08000000, 0x08004000, 0x08008000, 0x0800C000,
0x08010000, 0x08020000, 0x08040000, 0x08060000,
0x08080000, 0x080A0000, 0x080C0000, 0x080E0000]
# Erasing
total = len(flash_offsets)
progress = QProgressDialog(self)
progress.setRange(0, total)
progress.setValue(0)
progress.setModal(True)
progress.show()
progress.setAutoReset(False)
progress.setLabelText('Initializing')
# call dfu-util
openmv.enter_dfu()
sleep(1.0)
# Initialize
pydfu.init()
progress.setLabelText('Erasing')
progress.setValue(0)
pg = 0
for offset in flash_offsets:
progress.setValue(pg)
pydfu.page_erase(flash_offsets[pg])
pg += 1
offset = 0
size = len(buf)
progress.setRange(0, size)
progress.setLabelText('Downloading')
while offset < size:
progress.setValue(offset)
pg_size = min(64, size - offset)
page = buf[offset:offset + pg_size]
pydfu.write_page(page, offset)
offset += pg_size
progress.hide()
pydfu.exit_dfu()
def do_run(self):
buf = str(self.editor.toPlainText())
# interrupt any running code
openmv.stop_script()
sleep(0.1)
# exec script
openmv.exec_script(buf)
self.running = True
self.update_ui()
def do_stop(self):
openmv.stop_script()
self.running = False
self.update_ui()
def check_modified(self):
result = True
if self.editor.document().isModified():
msg = QMessageBox(self)
msg.setText('The document has been modified.')
msg.setIcon(QMessageBox.Warning)
msg.setInformativeText('Do you want to save your changes?')
msg.setStandardButtons(QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel)
msg.setDefaultButton(QMessageBox.Save)
msg.exec_()
ret = msg.result()
if ret == QMessageBox.Save:
self.do_save()
result = True
elif ret == QMessageBox.Discard:
result = True
elif ret == QMessageBox.Cancel:
result = False
else:
print('%s' % ret)
return result
def do_new(self):
if self.check_modified():
self.editor.setPlainText('')
self.filename = ''
self.update_ui()
def do_open(self):
if self.check_modified():
if os.path.exists(self.script_dir):
my_dir = self.script_dir
else:
my_dir = self.example_dir
filename = QFileDialog.getOpenFileName(parent=self,
caption=self.tr('Open Micro Python Script'),
directory=my_dir,
filter=self.tr("Python scripts (*.py)"))
self.open_file(filename)
def open_file(self, filename):
if filename:
try:
infile = open(filename, 'r')
except (IOError, OSError) as e:
QErrorMessage(self).showMessage('Error opening file:\n%s' % e)
else:
self.editor.setPlainText(infile.read())
# store new filename
self.filename = str(filename)
self.update_ui()
self.recent.add(self.filename)
def do_save_as(self):
self.do_save(True)
def do_save(self, save_as=False):
if save_as:
filename = ''
else:
filename = self.filename
## TODO: Address file-exists-replace? scenario
# filename will be empty if we haven't saved the document yet
if not filename:
filename = QFileDialog.getSaveFileName(parent=self,
caption=self.tr('Save Micro Python Script'),
directory=self.script_dir,
filter=self.tr("Python scripts (*.py)"))
if filename:
try:
outfile = open(filename, 'w')
outfile.write(self.editor.toPlainText())
# store new filename
self.filename = str(filename)
self.editor.document().setModified(False)
self.update_ui()
except (IOError, OSError) as e:
QErrorMessage(self).showMessage('Error saving file:\n%s' % e)
def do_open_example(self, action):
assert isinstance(action, QAction)
if self.check_modified():
self.open_file(self.example_dir + action.text())
def do_open_recent(self, action):
assert isinstance(action, QAction)
if self.check_modified():
self.open_file(action.text())
def do_quit(self):
if self.check_modified():
if self.connected:
self.do_disconnect()
self.save_config()
self.framebuffer.quit()
QApplication.quit()
def do_zoom_in(self):
self.framebuffer.increase_scale(0.5)
def do_zoom_out(self):
self.framebuffer.decrease_scale(0.5)
def do_zoom_reset(self):
self.framebuffer.reset_scale()
def interrupt_handler(self, signum, frame):
print('CTRL-C caught')
def do_about(self):
msg = QMessageBox(self)
msg.setText('OpenMV IDE (PyQt)')
msg.setIcon(QMessageBox.Question)
msg.setInformativeText('Michael Shimniok')
msg.setStandardButtons(QMessageBox.Ok)
msg.setDefaultButton(QMessageBox.Ok)
msg.exec_()
def closeEvent(self, event):
result = QMessageBox.question(self, "Confirm Exit...", "Are you sure you want to exit ?", QMessageBox.Yes, QMessageBox.No)
event.ignore()
if result == QMessageBox.Yes:
self.do_quit()
def main():
app = QApplication(sys.argv)
ide = OpenMVIDE()
signal.signal(signal.SIGINT, ide.interrupt_handler)
sys.exit(app.exec_())
## TODO: Take script as argument and run straightaway
if __name__ == '__main__':
main()