Commit e7f58a0b authored by magho's avatar magho

refactored out common frame code to inherited classes

parent a6c285b9
......@@ -4,3 +4,5 @@
*/Hardware/History
*/Hardware/__Previews
*/Hardware/Project\ Logs*
*/__pycache__
......@@ -10,8 +10,8 @@ from frames.Balance import Balance
import api_communication as API
import ble_driver as NeoBLE
import rfid
import ble_driver as ble
import rfid_driver as rfid
class NeoKomp(QtWidgets.QMainWindow):
"""
......@@ -31,10 +31,16 @@ class NeoKomp(QtWidgets.QMainWindow):
self.central_widget = QtWidgets.QStackedWidget()
self.setCentralWidget(self.central_widget)
# Timeout indicator always runs. Frames will connect to this timer if
# they need to repeatedly run a routine to update a label, like a timer
# countdown.
self.indicator_timer = QtCore.QTimer()
self.indicator_timer.start(100)
# Make frames
self.frames = {}
for F in (Guide, Search, Cart, Receipt, Balance):
frame = F(self)
frame = F(parent=self)
self.frames[frame.__class__.__name__] = frame
self.central_widget.addWidget(frame)
......@@ -51,9 +57,10 @@ class NeoKomp(QtWidgets.QMainWindow):
self.rfid_manager.start()
# Make BLE manager
self.ble_manager = NeoBLE.BLEThread(self, self.device_mapping)
self.ble_manager = ble.BLEThread(self, self.device_mapping)
self.ble_manager.start()
def enter_event(self, event):
"""Send enter event to the relevant frame"""
frame = self.frames[self.state]
......@@ -63,6 +70,17 @@ class NeoKomp(QtWidgets.QMainWindow):
self.central_widget.setCurrentWidget(self.frames[frame])
self.frames[frame].focus()
def show_balance(self, rfid):
self.show_frame('Balance')
self.frames['Balance'].show_balance(rfid)
def attempt_to_buy(self, rfid, items):
self.show_frame('Receipt')
self.frames['Receipt'].attempt_to_buy(rfid, items)
def cart_has_items(self):
return self.frames['Cart'].has_items()
@QtCore.pyqtSlot("PyQt_PyObject")
def ble_button_pressed(self, location):
self.frames['Cart'].add_item_from_location(location)
......
......@@ -18,10 +18,9 @@ class BLEThread(QtCore.QThread):
self.parent = parent
self.device_mapping = device_mapping
# Find primary bluetooth controller. TODO: find a better way of getting
# the adapter name
# Find primary Bluetooth controller.
ctrl_info = subprocess.check_output(['btmgmt', 'info']).decode('utf-8')
pattern = r"([a-zA-Z]*[0-9]*):\tPrimary controller"
pattern = r"([a-zA-Z]+[0-9]+):\tPrimary controller"
primary_ctrl = re.search(pattern, ctrl_info).group(1)
# Make sure ble is on and not currently connected
......@@ -29,7 +28,7 @@ class BLEThread(QtCore.QThread):
os.system("bluetoothctl power on")
# Create BLE Manager
self.manager = BLEKomp(parent=self, adapter_name=primary_ctrl)
self.manager = BLEManager(parent=self, adapter_name=primary_ctrl)
# Connect signal
self.button_pressed.connect(parent.ble_button_pressed)
......@@ -45,7 +44,7 @@ class BLEThread(QtCore.QThread):
print("Button order: %s" % position)
self.button_pressed.emit(position)
class BLEKomp(gatt.DeviceManager):
class BLEManager(gatt.DeviceManager):
def __init__(self, *args, **kwargs):
self.parent = kwargs.pop('parent')
super().__init__(*args, **kwargs)
......
......@@ -2,32 +2,23 @@ import sys
import traceback
import math
from PyQt5 import QtCore, QtGui, QtWidgets
from frames.FrameBase import GenericFrame, TimedFrame
import api_communication as API
BALANCE_TIMEOUT_MS = 300000
BALANCE_TIMEOUT_MS = 10000
DEBUG = True
def debug(string):
if DEBUG:
print(string)
class Balance(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
class Balance(GenericFrame, TimedFrame):
def __init__(self, **kwargs):
super(Balance, self).__init__(**dict(kwargs, timeout=BALANCE_TIMEOUT_MS))
self.parent = kwargs['parent']
self.init_layout()
self.timer_init()
## Install/Enable custom eventFilter()
self.installEventFilter(self)
def init_layout(self):
# General vertical layout
self.layout = QtWidgets.QVBoxLayout(self)
title_font = QtGui.QFont()
title_font.setPointSize(20)
self.title = QtWidgets.QLabel("Balance")
......@@ -104,26 +95,6 @@ Start typing to search for a component."""
# Pass event to QWidget
return super(Balance, self).eventFilter(obj, event)
def timer_init(self):
# Timeout is SingleShot timer
self.timeout_timer = QtCore.QTimer()
self.timeout_timer.setSingleShot(True)
self.timeout_timer.timeout.connect(self.timeout)
# Timeout indicator always runs
self.indicator_timer = QtCore.QTimer()
self.indicator_timer.start(100)
self.indicator_timer.timeout.connect(self.update_timeout_indicator)
def restart_timer(self):
if self.timeout_timer.isActive():
self.timeout_timer.stop()
self.timeout_timer.start(BALANCE_TIMEOUT_MS)
def stop_timer(self):
self.timeout_timer.stop()
@QtCore.pyqtSlot()
def timeout(self):
#Clear it and go to guide
......
import sys
import math
from PyQt5 import QtCore, QtGui, QtWidgets
from frames.FrameBase import GenericFrame, TimedFrame
import api_communication as API
......@@ -9,22 +10,15 @@ CART_TIMEOUT_MS = 300000
CART_TABLE_HEIGHT = 500
CART_TABLE_WIDTH = 800
DEBUG = True
def debug(string):
if DEBUG:
print(string)
class Cart(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
class Cart(GenericFrame, TimedFrame):
def __init__(self, **kwargs):
super(Cart, self).__init__(**dict(kwargs, timeout=CART_TIMEOUT_MS))
self.parent = kwargs['parent']
self.headers = ['id', 'navn', 'beskrivelse', 'antall', 'pris']
self.layout_init()
self.timer_init()
self.add_total()
# Connect cell double click to add to cart
......@@ -34,9 +28,6 @@ class Cart(QtWidgets.QWidget):
self.installEventFilter(self)
def layout_init(self):
## General vertical layout
self.layout = QtWidgets.QVBoxLayout(self)
## Title
title_font = QtGui.QFont()
title_font.setPointSize(20)
......@@ -58,10 +49,10 @@ class Cart(QtWidgets.QWidget):
self.layout.addStretch()
## Help text
self.help_text = "\
Press <Return> to remove selected item.\n\
Double click item to remove item.\n\
TODO: Scan RFID card to buy Cart."
self.help_text = """Press <Return> to remove selected item.
Double click item to remove item.
TODO: Scan RFID card to buy Cart.
"""
self.help_label = QtWidgets.QLabel(self.help_text)
self.layout.addWidget(self.help_label)
......@@ -102,21 +93,38 @@ TODO: Scan RFID card to buy Cart."
self.layout.addLayout(self.navigation_buttons)
@QtCore.pyqtSlot()
def timeout(self):
#Clear it and go to guide
self.clear_cart('Guide')
@QtCore.pyqtSlot()
def update_timeout_indicator(self):
if self.timeout_timer.isActive():
time = self.timeout_timer.remainingTime()
time = math.ceil(time / 1000) # ms to s
self.timeout_indicator.setText("Timeout in " + str(time) + "s")
else:
self.timeout_indicator.setText("")
# Install a custom eventFilter to capture enterPressed anywhere
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.ShortcutOverride:
self.restart_timer()
if event.text().isalnum():
self.parent.show_frame('Search')
return False # ?
if event.type() == QtCore.QEvent.KeyPress:
elif event.type() == QtCore.QEvent.KeyPress:
if event.key() == QtCore.Qt.Key_Return:
self.return_pressed()
return True
if event.key() == QtCore.Qt.Key_Escape:
elif event.key() == QtCore.Qt.Key_Escape:
# Emulate timeout
self.clear_cart('Guide')
return True
else:
self.restart_timer()
# Pass event to QWidget
return super(Cart, self).eventFilter(obj, event)
......@@ -138,42 +146,6 @@ TODO: Scan RFID card to buy Cart."
if (row < self.cart.rowCount() - 1):
self.remove_row(row)
def timer_init(self):
# Timeout is SingleShot timer
self.timeout_timer = QtCore.QTimer()
self.timeout_timer.setSingleShot(True)
self.timeout_timer.timeout.connect(self.timeout)
# Timeout indicator always runs
self.indicator_timer = QtCore.QTimer()
self.indicator_timer.start(100)
self.indicator_timer.timeout.connect(self.update_timeout_indicator)
def restart_timer(self):
if self.timeout_timer.isActive():
self.timeout_timer.stop()
self.timeout_timer.start(CART_TIMEOUT_MS)
def stop_timer(self):
self.timeout_timer.stop()
@QtCore.pyqtSlot()
def timeout(self):
#Clear it and go to guide
self.clear_cart()
@QtCore.pyqtSlot()
def update_timeout_indicator(self):
if self.timeout_timer.isActive():
time = self.timeout_timer.remainingTime()
time = math.ceil(time / 1000) # ms to s
self.timeout_indicator.setText("Timeout in " + str(time) + "s")
else:
self.timeout_indicator.setText("")
def focus(self):
return
def add_item_from_location(self, loc):
item = API.AJAX_search_component_pos(loc['rom'], loc['seksjon'], loc['hylle'], loc['rad'], loc['kolonne'], loc['dybde'])
print(item)
......@@ -193,27 +165,56 @@ TODO: Scan RFID card to buy Cart."
def has_items(self):
return self.cart.rowCount() >= 2
def add_to_row_amount(self, row, num):
# Get old amount
# Row 5 has amount
amount = self.cart.item(row, 3).text()
try:
amount = int(amount) + 1
except:
print("EXCET")
amount = 0
self.cart.item(row, 3).setText(str(amount))
def increment_row_amount(self, row):
self.add_to_row_amount(row, 1)
def decrement_row_amount(self, row):
self.add_to_row_amount(row, -1)
def add_item(self, item):
row_positions = self.cart.rowCount()
# New item goes one above the bottom (which is the Total/Sum)
row = row_positions - 1
self.cart.insertRow(row)
for index, field in enumerate(self.headers):
if field in item.keys():
# Check if the item is already in the cart
row_of_existing_item = None
for row in range(row_positions-1):
item_id = self.cart.item(row, 0).data(QtCore.Qt.UserRole)['id']
if (item_id == item['id']):
row_of_existing_item = row
if (row_of_existing_item != None):
self.increment_row_amount(row_of_existing_item)
else:
# New item goes one above the bottom (which is the Total/Sum)
row = row_positions - 1
self.cart.insertRow(row)
for index, field in enumerate(self.headers):
if field in item.keys():
if field == 'antall':
value = QtWidgets.QTableWidgetItem(str(1))
else:
value = QtWidgets.QTableWidgetItem(item[field])
if field == 'antall':
value = QtWidgets.QTableWidgetItem(str(1))
else:
value = QtWidgets.QTableWidgetItem(item[field])
table_item = QtWidgets.QTableWidgetItem(value)
table_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
table_item = QtWidgets.QTableWidgetItem(value)
table_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
if field == 'id':
table_item.setData(QtCore.Qt.UserRole, item)
if field == 'id':
table_item.setData(QtCore.Qt.UserRole, item)
self.cart.setItem(row, index, table_item)
self.cart.setItem(row, index, table_item)
self.cart.resizeColumnsToContents()
self.update_total()
......@@ -242,7 +243,6 @@ TODO: Scan RFID card to buy Cart."
title_table_item.setFont(font)
total_table_item.setFont(font)
def update_total(self):
total = 0
row_count = self.cart.rowCount()
......@@ -310,15 +310,11 @@ TODO: Scan RFID card to buy Cart."
items.append(self.cart.item(row, 0).data(QtCore.Qt.UserRole))
if len(items) == 0:
self.parent.show_frame('Balance')
self.parent.frames['Balance'].show_balance(rfid)
self.parent.show_balance(rfid)
return
# Switch to receipt
self.parent.show_frame('Receipt')
# Issue the buy
self.parent.frames['Receipt'].attempt_to_buy(rfid, items)
# Clear the cart
self.clear_cart('Receipt')
# Issue the buy
self.parent.attempt_to_buy(rfid, items)
from PyQt5 import QtCore, QtGui, QtWidgets
class GenericFrame(QtWidgets.QWidget):
def __init__(self, **kwargs):
super(GenericFrame, self).__init__(**kwargs)
self.generic_layout_init()
def generic_layout_init(self):
## General vertical layout
self.layout = QtWidgets.QVBoxLayout(self)
# This function should be redefined by those frames that need special
# actions on focus
def focus(self):
return
class TimedFrame(QtWidgets.QWidget):
def __init__(self, **kwargs):
timeout = kwargs.pop('timeout')
super(TimedFrame, self).__init__(**kwargs)
self.parent = kwargs['parent']
print(kwargs)
self.TIMEOUT_MS = timeout
self.timer_init()
def timer_init(self):
# Timeout is SingleShot timer
self.timeout_timer = QtCore.QTimer()
self.timeout_timer.setSingleShot(True)
self.timeout_timer.timeout.connect(self.timeout)
self.parent.indicator_timer.timeout.connect(self.update_timeout_indicator)
def restart_timer(self):
if self.timeout_timer.isActive():
self.timeout_timer.stop()
self.timeout_timer.start(self.TIMEOUT_MS)
def stop_timer(self):
self.timeout_timer.stop()
from PyQt5 import QtCore, QtGui, QtWidgets
from frames.FrameBase import GenericFrame
class Guide(QtWidgets.QWidget):
class Guide(GenericFrame):
"""
The Guide frame displays instructional text on how to use the store
"""
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
def __init__(self, **kwargs):
super(Guide, self).__init__(**kwargs)
self.parent = kwargs['parent']
self.init_layout()
......@@ -14,9 +15,6 @@ class Guide(QtWidgets.QWidget):
self.installEventFilter(self)
def init_layout(self):
## General vertical layout
self.layout = QtWidgets.QVBoxLayout(self)
## Title
title_font = QtGui.QFont()
title_font.setPointSize(20)
......@@ -30,14 +28,14 @@ class Guide(QtWidgets.QWidget):
self.help_text = """\
This is a little help text to explain how this store works.
TODO: Scan your RFID-card to check your balance.
Scan your RFID-card to check your balance.
Start typing to search for a component, which can be added to your cart.
TODO: Scan a bar code to add the item to your cart.
TODO: Select a component via a section module to add the item to your cart."""
Select a component via a section module to add the item to your cart."""
self.help_label = QtWidgets.QLabel()
self.help_label = QtWidgets.QLabel(font=title_font)
self.help_label.setText(self.help_text)
self.layout.addWidget(self.help_label)
......@@ -62,8 +60,6 @@ TODO: Select a component via a section module to add the item to your cart."""
self.layout.addLayout(self.navigation_buttons)
def focus(self):
return
# Install a custom eventFilter to focus switch to search when starting to write
def eventFilter(self, obj, event):
......
import sys
import math
from PyQt5 import QtCore, QtGui, QtWidgets
from frames.FrameBase import GenericFrame, TimedFrame
import api_communication as API
RECEIPT_TIMEOUT_MS = 10000
class Receipt(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
class Receipt(GenericFrame, TimedFrame):
def __init__(self, **kwargs):
super(Receipt, self).__init__(**dict(kwargs, timeout=RECEIPT_TIMEOUT_MS))
self.parent = kwargs['parent']
self.init_layout()
self.timer_init()
## Install/Enable custom eventFilter()
self.installEventFilter(self)
def init_layout(self):
# General vertical layout
self.layout = QtWidgets.QVBoxLayout(self)
title_font = QtGui.QFont()
title_font.setPointSize(20)
self.title = QtWidgets.QLabel("Receipt", font=title_font)
......@@ -111,26 +107,6 @@ TODO: Scan RFID card to show Balance."
# Pass event to QWidget
return super(Receipt, self).eventFilter(obj, event)
def timer_init(self):
# Timeout is SingleShot timer
self.timeout_timer = QtCore.QTimer()
self.timeout_timer.setSingleShot(True)
self.timeout_timer.timeout.connect(self.timeout)
# Timeout indicator always runs
self.indicator_timer = QtCore.QTimer()
self.indicator_timer.start(100)
self.indicator_timer.timeout.connect(self.update_timeout_indicator)
def restart_timer(self):
if self.timeout_timer.isActive():
self.timeout_timer.stop()
self.timeout_timer.start(RECEIPT_TIMEOUT_MS)
def stop_timer(self):
self.timeout_timer.stop()
@QtCore.pyqtSlot()
def timeout(self):
#Clear it and go to guide
......@@ -165,7 +141,7 @@ TODO: Scan RFID card to show Balance."
try:
balance = int(user_balance_data['saldo'])
except:
debug("balance not number!")
print("balance not number! setting it to 0")
balance = 0 # TODO: show more specific error
# Display error message if not enough money
......@@ -207,12 +183,12 @@ TODO: Scan RFID card to show Balance."
user_balance = API.API_get_balance(rfid, self.parent.API_key)
if (not isinstance(user_balance, dict)):
debug("get_balance: Item not a dict")
print("get_balance: Item not a dict")
return None
if 'brukernavn' not in user_balance.keys() or 'saldo' not in user_balance.keys():
debug("get_balance: missing brukernavn or saldo")
debug(user_balance)
print("get_balance: missing brukernavn or saldo")
print(user_balance)
return None
return user_balance
......@@ -267,6 +243,3 @@ TODO: Scan RFID card to show Balance."
self.tail1.setText("")
self.tail2.setText("")
self.tail3.setText("")
def focus(self):
return
import sys
import math
from PyQt5 import QtCore, QtGui, QtWidgets
from frames.FrameBase import GenericFrame, TimedFrame
import api_communication as API
......@@ -9,19 +10,14 @@ SEARCH_TIMEOUT_MS = 300000
SEARCH_TABLE_HEIGHT = 500
SEARCH_TABLE_WIDTH = 800
DEBUG = True
def debug(string):
if DEBUG:
print(string)
class Search(QtWidgets.QWidget):
class Search(GenericFrame, TimedFrame):
"""
The Search frame displays search results, and allows selected results to be
inserted into the cart
"""
def __init__(self, parent):
super().__init__()
self.parent = parent
def __init__(self, **kwargs):
super(Search, self).__init__(**dict(kwargs, timeout=SEARCH_TIMEOUT_MS))
self.parent = kwargs['parent']
self.headers = ['id', 'navn', 'beskrivelse', 'produsent', 'pris', 'antall']
......@@ -42,12 +38,7 @@ class Search(QtWidgets.QWidget):
## Locations frame
self.locations_layout = None
self.timer_init()
def layout_init(self):
# General vertical layout
self.layout = QtWidgets.QVBoxLayout(self)
title_font = QtGui.QFont()
title_font.setPointSize(20)
self.title = QtWidgets.QLabel("Search Page")
......@@ -187,27 +178,6 @@ Double click an item to add it to the cart."""
self.results.selectRow(0)
self.results.setFocus()
# TODO: Add signal to show position of item when single-clicking it
def timer_init(self):
# Timeout is SingleShot timer
self.timeout_timer = QtCore.QTimer()
self.timeout_timer.setSingleShot(True)
self.timeout_timer.timeout.connect(self.timeout)
# Timeout indicator always runs
self.indicator_timer = QtCore.QTimer()
self.indicator_timer.start(100)
self.indicator_timer.timeout.connect(self.update_timeout_indicator)
def restart_timer(self):
if self.timeout_timer.isActive():
self.timeout_timer.stop()
self.timeout_timer.start(SEARCH_TIMEOUT_MS)
def stop_timer(self):
self.timeout_timer.stop()
@QtCore.pyqtSlot()
def timeout(self):
# Stop any ongoing search
......@@ -217,7 +187,7 @@ Double click an item to add it to the cart."""
#Clear it and go to guide
self.clear_search()
self.stop_timer()
if self.parent.frames['Cart'].has_items():
if self.parent.cart_has_items():
self.parent.show_frame('Cart')
else:
self.parent.show_frame('Guide')
......@@ -246,7 +216,7 @@ Double click an item to add it to the cart."""
def show_location(self, item):
if self.locations_layout:
# Remove old location
# Remove old location objects
while self.locations_layout.count():
child = self.locations_layout.takeAt(0)
if child.layout():
......@@ -368,9 +338,11 @@ Double click an item to add it to the cart."""
self.restart_timer()
class SearchQuerier(QtCore.QThread):
# This signal will be emitted each time the thread has fetched a search
# result, and is connected to a slot in the Search table instance, which
# will do the actual inserting of the data in the table
"""
This signal will be emitted each time the thread has fetched a search
result, and is connected to a slot in the Search table instance, which
will do the actual inserting of the data in the table
"""
search_result_fetched = QtCore.pyqtSignal("PyQt_PyObject")
def __init__(self, parent, query_string):
......@@ -387,13 +359,13 @@ class SearchQuerier(QtCore.QThread):
self.parent.search_indicator.setText("Searching. Please wait ...")
# Query database
debug("query string: %s" % self.query_string)
print("query string: %s" % self.query_string)
try:
result = API.AJAX_search_component_name(self.query_string)
except:
e = sys.exc_info()[0]
debug("API Exception caught")
debug(e)
print("API Exception caught")
print(e)
self.parent.search_indicator.setText("DB API Error")
return
......@@ -409,12 +381,12 @@ class SearchQuerier(QtCore.QThread):
item = API.AJAX_search_component_ID(ID)
except:
e = sys.exc_info()[0]
debug("API Exception caught")
debug(e)
print("API Exception caught")
print(e)
continue
if (not isinstance(item, dict)):
debug("Item not a dict")
print("Item not a dict")
continue
if ('error' in item.keys()):
......
......@@ -36,7 +36,7 @@ class RFIDManager(QtCore.QThread):
for device_id in devices_by_id:
if 'rfid' in device_id.lower():
rfid_device_id = device_id
print("[RFID]: using input device %s/%s" % (by_id_path, device_id))
print("RFID: using input device %s/%s" % (by_id_path, device_id))