From 3fade3a2c9b968b4693767d7054e86efe5791cb3 Mon Sep 17 00:00:00 2001 From: janis Date: Sat, 14 May 2022 18:04:52 +0200 Subject: [PATCH] Initial Commit --- .../inspectionProfiles/profiles_settings.xml | 6 + .idea/micro_bit_interface.iml | 8 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + bin/com/communication.py | 5 + bin/com/comport_search.py | 24 +++ bin/com/csv_parsers.py | 122 ++++++++++++ bin/com/lib.py | 73 ++++++++ bin/data/full_command_list.csv | 1 + bin/gui/command_screen.kv | 27 +++ bin/gui/load_screen.kv | 23 +++ bin/gui/main_screen.kv | 17 ++ bin/micro_bit/command_list.py | 0 bin/micro_bit/micro_bit_soft.py | 3 + bin/others/autocomplete.py | 48 +++++ bin/others/run_command.py | 26 +++ config/settings.ini | 7 + main.py | 176 ++++++++++++++++++ testfile.py | 3 + 20 files changed, 587 insertions(+) create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/micro_bit_interface.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 bin/com/communication.py create mode 100644 bin/com/comport_search.py create mode 100644 bin/com/csv_parsers.py create mode 100644 bin/com/lib.py create mode 100644 bin/data/full_command_list.csv create mode 100644 bin/gui/command_screen.kv create mode 100644 bin/gui/load_screen.kv create mode 100644 bin/gui/main_screen.kv create mode 100644 bin/micro_bit/command_list.py create mode 100644 bin/micro_bit/micro_bit_soft.py create mode 100644 bin/others/autocomplete.py create mode 100644 bin/others/run_command.py create mode 100644 config/settings.ini create mode 100644 main.py create mode 100644 testfile.py diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/micro_bit_interface.iml b/.idea/micro_bit_interface.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/micro_bit_interface.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..dc9ea49 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..594d054 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/bin/com/communication.py b/bin/com/communication.py new file mode 100644 index 0000000..1420b66 --- /dev/null +++ b/bin/com/communication.py @@ -0,0 +1,5 @@ +class Communication: + def __init__(self): + self.__x = 0 + self.__data_recieve = 0 + self.__output = "" diff --git a/bin/com/comport_search.py b/bin/com/comport_search.py new file mode 100644 index 0000000..c54b8cf --- /dev/null +++ b/bin/com/comport_search.py @@ -0,0 +1,24 @@ +import serial.tools.list_ports + + +class ComportService: + def __init__(self): + self.__comport = [] + self.__import = [] + self.__working = [] + self.__pos = 0 + self.__com_name = "" + + def get_comport(self, special_port=""): + self.__comport = [comport.device for comport in serial.tools.list_ports.comports()] + self.__pos = 0 + if special_port != "": + self.__working = special_port + else: + while self.__working == []: + self.__com_name = serial.tools.list_ports.comports()[self.__pos] + if "USB-Serial Controller" or "Prolific USB-Serial Controller" in self.__com_name: + self.__working = self.__comport.pop(self.__pos) + else: + self.__pos += 1 + return self.__working diff --git a/bin/com/csv_parsers.py b/bin/com/csv_parsers.py new file mode 100644 index 0000000..d1f593c --- /dev/null +++ b/bin/com/csv_parsers.py @@ -0,0 +1,122 @@ +"""@package docstring +This is a simplification of the csv module""" + +import csv + + +class CsvRead: + """This is a class that reads csv files and depending on the module selected does do different things with it""" + def __init__(self): + self.__imp = "" + self.__raw = "" + self.__raw_list = "" + + def importing(self, path): + """Returns a list of the imported csv-file, requires path, either direct system path or relative path""" + self.__imp = open(path) + self.__raw = csv.reader(self.__imp, delimiter=',') + self.__raw_list = list(self.__raw) + self.__imp.close() + return self.__raw_list + + +class CsvWrite: + """This is a class that modifies csv files""" + def __init__(self): + self.__impl = [] + self.__strpop = [] + self.__removed = [] + self.__removing = 0 + self.__change = 0 + self.__appending = 0 + self.__imp = [] + self.__raw = [] + + def rem_str(self, path, row): + """Opens the csv-file in write mode which is specified as an argument either as direct or relative path""" + self.__imp = open(path) + self.__raw = csv.reader(self.__imp, delimiter=',') + self.__impl = list(self.__raw) + self.__removed = self.__impl.pop(row + 1) + with open(path, "w") as removedata: + self.__removing = csv.writer(removedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__removing.writerow(self.__impl.pop(0)) + while len(self.__impl) > 0: + with open(path, "a") as removedata: + self.__removing = csv.writer(removedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__removing.writerow(self.__impl.pop(0)) + self.__imp.close() + removedata.close() + + + def chg_str(self, path, row, pos, new_value): + """Opens the csv-file in write mode to change a value, e.g. if a recipes is changed.""" + self.__imp = open(path) + self.__raw = csv.reader(self.__imp, delimiter=',') + self.__impl = list(self.__raw) + self.__strpop = self.__impl.pop(row) + self.__strpop.pop(pos) + self.__strpop.insert(pos, new_value) + self.__impl.insert(row, self.__strpop) + with open(path, "w") as changedata: + self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__change.writerow(self.__impl.pop(0)) + while len(self.__impl) > 0: + with open(path, "a") as changedata: + self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__removing.writerow(self.__impl.pop(0)) + self.__imp.close() + changedata.close() + + def chg_str_rem(self, path, row, pos): + """Opens the csv-file in write mode to change a value, e.g. if a recipes is changed.""" + self.__imp = open(path) + self.__raw = csv.reader(self.__imp, delimiter=',') + self.__impl = list(self.__raw) + self.__strpop = self.__impl.pop(row) + self.__strpop.pop(pos) + self.__strpop.pop(pos) + self.__impl.insert(row, self.__strpop) + with open(path, "w") as changedata: + self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__change.writerow(self.__impl.pop(0)) + while len(self.__impl) > 0: + with open(path, "a") as changedata: + self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__removing.writerow(self.__impl.pop(0)) + self.__imp.close() + changedata.close() + + def chg_str_add(self, path, row, new_value1, new_value2): + """Opens the csv-file in write mode to change a value, e.g. if a recipes is changed.""" + self.__imp = open(path) + self.__raw = csv.reader(self.__imp, delimiter=',') + self.__impl = list(self.__raw) + self.__strpop = self.__impl.pop(row) + self.__strpop.append(new_value1) + self.__strpop.append(new_value2) + self.__impl.insert(row, self.__strpop) + with open(path, "w") as changedata: + self.__change = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__change.writerow(self.__impl.pop(0)) + while len(self.__impl) > 0: + with open(path, "a") as changedata: + self.__removing = csv.writer(changedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__removing.writerow(self.__impl.pop(0)) + self.__imp.close() + changedata.close() + + def app_str(self, path, value): + """Opens the csv-file in append mode and writes given input. CsvWrite.app_str(path, value). + Path can be specified both as direct or relative. value is a list. Will return an error if type of value is + not a list.""" + with open(path, "a") as appenddata: + self.__appending = csv.writer(appenddata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__appending.writerow(value) + appenddata.close() + + def write_str(self, path, value): + with open(path, "w") as writedata: + self.__change = csv.writer(writedata, delimiter=',', quoting=csv.QUOTE_MINIMAL) + self.__change.writerow(value) + writedata.close() diff --git a/bin/com/lib.py b/bin/com/lib.py new file mode 100644 index 0000000..a4c2238 --- /dev/null +++ b/bin/com/lib.py @@ -0,0 +1,73 @@ +import serial +import struct +import bin.com.comport_search +"""@package docstring +This package can communicate with a microcontroller""" + +coms = bin.com.comport_search.ComportService() + + +class Com: + def __init__(self): + self.xr = "" + self.output = "" + self.str_input = "" + self.str_get_input = "" + self.xs = "" + self.__comport = '/dev/ttyUSB0' + + def connect(self, baudrate, special_port): + try: + self.__comport = coms.get_comport(special_port) + except: + pass + self.ser = serial.Serial(self.__comport, baudrate=baudrate, timeout=5) + + def quitcom(self): + try: + self.ser.close() + except: + pass + + def receive(self, amount_bytes): + self.xr = self.ser.read(amount_bytes) + return self.xr + + def decode_ascii(self, value): + try: + self.output = value.decode() + except: + self.output = "Error" + return self.output + + def check_value(self, value_check, checked_value): + if value_check == checked_value: + return 1 + else: + return 0 + + def decode_int(self, value): + self.i = int(value, base = 16) + return self.i + + def decode_float(self, value): + self.fs = str(value, 'ascii') + '00' + self.f = struct.unpack('>f', bytes.fromhex(self.fs)) + return str(self.f[0]) + + def decode_float_2(self, value): + self.fs = str(value, 'ascii') + '0000' + self.f = struct.unpack('>f', bytes.fromhex(self.fs)) + return str(self.f[0]) + + def get_input(self): + self.str_get_input = input("please enter a character to send: ") + return self.str_get_input + + def send(self, str_input): + self.xs = str_input.encode() + self.ser.write(self.xs) + + def send_float(self, float_input): + ba = bytearray(struct.pack('>f', float_input)) + self.ser.write(ba[0:3]) diff --git a/bin/data/full_command_list.csv b/bin/data/full_command_list.csv new file mode 100644 index 0000000..13faef4 --- /dev/null +++ b/bin/data/full_command_list.csv @@ -0,0 +1 @@ +test1,test2,stop,start,transmit,test,arch \ No newline at end of file diff --git a/bin/gui/command_screen.kv b/bin/gui/command_screen.kv new file mode 100644 index 0000000..94de211 --- /dev/null +++ b/bin/gui/command_screen.kv @@ -0,0 +1,27 @@ +Command: + name: "CommandScreen" + md_bg_color: 0, 0, 0, 1 + FloatLayout: + Label: + id: cmd_out + size_hint: 0.96, 0.6 + pos_hint: {"x": 0.03, "y": 0.35} + text_size: self.size + color: 1, 1, 1, 1 + text: "" + valign: "bottom" + TextInput: + color: 1, 1, 1, 1 + foreground_color: 1, 1, 1, 1 + background_color: 0, 0, 0, 1 + size_hint: 0.96, 0.1 + pos_hint: {"x": 0.02, "y": 0.2} + id: tin + hint_text: "Enter command..." + multiline: False + on_text: root.autocomplete() + on_text_validate: root.runcommand() + Button: + text: "back" + size_hint: 0.1, 0.1 + pos_hint: {"x": 0.02, "y": 0.02} \ No newline at end of file diff --git a/bin/gui/load_screen.kv b/bin/gui/load_screen.kv new file mode 100644 index 0000000..499b0e0 --- /dev/null +++ b/bin/gui/load_screen.kv @@ -0,0 +1,23 @@ +Load: + name: "LoadScreen" + on_enter: root.start_pb() + radius: [25, 25, 25, 25] + md_bg_color: app.theme_cls.accent_light + GridLayout: + cols: 1 + Label: + text: "micro:bit interface" + font_size: 60 + color: app.theme_cls.primary_dark + FloatLayout: + MDProgressBar: + id: progress + size_hint: .6, .6 + pos_hint: {"x": 0.2, "y": 0.3} + color: app.theme_cls.primary_dark + type: "determinate" + running_duration: 0.75 + catching_duration: 0.5 + Label: + color: "black" + text: "starting app ..." diff --git a/bin/gui/main_screen.kv b/bin/gui/main_screen.kv new file mode 100644 index 0000000..1ce10ee --- /dev/null +++ b/bin/gui/main_screen.kv @@ -0,0 +1,17 @@ +Main: + name: "HomeScreen" + md_bg_color: app.theme_cls.accent_light + GridLayout: + cols: 1 + Label: + text: "micro:bit interface" + font_size: 60 + color: app.theme_cls.primary_dark + GridLayout: + cols: 2 + Button: + text: "Start" + on_release: + app.root.current = "CommandScreen" + Button: + text: "Settings" \ No newline at end of file diff --git a/bin/micro_bit/command_list.py b/bin/micro_bit/command_list.py new file mode 100644 index 0000000..e69de29 diff --git a/bin/micro_bit/micro_bit_soft.py b/bin/micro_bit/micro_bit_soft.py new file mode 100644 index 0000000..0724caa --- /dev/null +++ b/bin/micro_bit/micro_bit_soft.py @@ -0,0 +1,3 @@ +import microbit + +microbit.uart.write() \ No newline at end of file diff --git a/bin/others/autocomplete.py b/bin/others/autocomplete.py new file mode 100644 index 0000000..f249ac1 --- /dev/null +++ b/bin/others/autocomplete.py @@ -0,0 +1,48 @@ +import bin.com.csv_parsers + +cvr = bin.com.csv_parsers.CsvRead() + + +class AutoComplete: + def __init__(self): + self.__length_command_completion = 0 + self.__command_list = [] + self.__possible_completion = [] + self.item = "" + self.items = "" + self.__return_value = [] + self.__return_value_assembly = "" + self.__command_count = 0 + self.text = "" + + def autocomplete(self, text, command_list_path): + self.text = str(text).lower() + if self.text[len(self.text) - 2:] == "\t\n": + self.text = self.text[:len(self.text) - 2] + elif self.text[len(self.text) - 1:] == "\t": + self.text = self.text[:len(self.text) - 1] + self.__command_list = cvr.importing(command_list_path).pop(0) + self.__return_value = [] + self.__return_value_assembly = "" + self.__possible_completion = [] + self.__command_count = 0 + for self.item in self.__command_list: + if self.text == self.item[:len(self.text)]: + self.__possible_completion.append(self.item) + else: + pass + if len(self.__possible_completion) < 1: + self.__return_value = [f"{self.text}\n-micro:bit - No such command", self.text[:len(self.text)]] + elif len(self.__possible_completion) == 1: + self.__return_value = ["", str(self.__possible_completion.pop(0))] + else: + for self.items in self.__possible_completion: + self.__return_value_assembly += f"{str(self.items)} " + if self.__command_count > 2: + self.__return_value_assembly += "\n" + self.__command_count = 0 + else: + self.__command_count += 1 + self.__return_value.append(self.__return_value_assembly) + self.__return_value.append(self.text[:len(self.text)]) + return self.__return_value diff --git a/bin/others/run_command.py b/bin/others/run_command.py new file mode 100644 index 0000000..45c7992 --- /dev/null +++ b/bin/others/run_command.py @@ -0,0 +1,26 @@ +import serial +import bin.com.csv_parsers +import bin.com.lib + + +cvr = bin.com.csv_parsers.CsvRead() +com = bin.com.lib.Com() + + +class RunCommand: + def __init__(self): + self.__all_commands = [] + self.__return = "" + + def runcommand(self, command, command_list_path): + self.__all_commands = cvr.importing(command_list_path).pop(0) + if command in self.__all_commands: + try: + com.connect(19200, "") + com.send(command) + self.__return = "The command executed successfully" + except serial.SerialException: + self.__return = f"[micro:bit - {command}]: An error occurred running the command. (Maybe disconnected or no permission?)" + else: + self.__return = "-micro:bit - No such command" + return self.__return diff --git a/config/settings.ini b/config/settings.ini new file mode 100644 index 0000000..ce6704f --- /dev/null +++ b/config/settings.ini @@ -0,0 +1,7 @@ +[Info] +version = V0.1 +subVersion = -dev1 + +[Dev Settings] +log_level = DEBUG + diff --git a/main.py b/main.py new file mode 100644 index 0000000..086e679 --- /dev/null +++ b/main.py @@ -0,0 +1,176 @@ +######################################################### +"""@package docstring +Micro:bit Bluetooth Interface, developed by simplePCBuilding, alpha 1.0 + +This App allows you to connect to a micro:bit via the USB cable and as such transmit to +and recieve Data from it. This file here is the control file for the UI and as such +should not be interfaced with. All the api files are located in the bin directory.""" +######################################################### + +# IMPORTS +import logging +import os +import configparser +import datetime +from kivymd.uix.screen import MDScreen +from kivymd.app import MDApp +from kivy.uix.screenmanager import ScreenManager +from kivy.base import Builder +from kivy.uix.popup import Popup +from kivy.clock import Clock +from kivy.core.window import Window +import bin.others.autocomplete +import bin.others.run_command + + +################################ +# VARIABLE SETUP +################ +config = configparser.ConfigParser() +config.read('./config/settings.ini') +version_app = f"{config['Info']['version']}{config['Info']['subVersion']}" +ac = bin.others.autocomplete.AutoComplete() +rc = bin.others.run_command.RunCommand() + +################################ + + +################################ +# LOGGER SETUP +############## + +# BASIC SETUP +logging.basicConfig(level=logging.DEBUG, filename="./log/main_log.log", filemode="w") +logs = f"./log/{datetime.datetime.now()}-log-main.log" +logger = logging.getLogger(__name__) + +# SETUP OF HANDLER & FORMATTER +handler = logging.FileHandler(logs) +formatter = logging.Formatter("%(levelname)s - %(asctime)s - %(name)s: %(message)s -- %(lineno)d") +handler.setFormatter(formatter) +logger.addHandler(handler) + +# FINAL CONFIG & FIRST LOG ENTRY +logger.setLevel(config['Dev Settings']['log_level']) +logger.info(f"Logger initialized, app is running Version: {version_app}") +################################ + + +################################ +# SETTINGS HANDLER +################## + +class SettingsHandler: + def __init__(self): + pass + + def settings_handler(self): + pass + +################################ + + +############################################################## +# SCREENS +############################################################## + + +################################ +# LOAD SCREEN +############# + +class Load(MDScreen): + def start_pb(self): + self.ids.progress.start() + + +################################ + + +################################ +# MAIN SCREEN +############# + +class Main(MDScreen): + pass + + +################################ + + +################################ +# COMMAND SCREEN +################ + +class Command(MDScreen): + def autocomplete(self): + self.text = self.ids.tin.text + self.input = self.text[len(self.text) - 1:] + if self.input == "\t": + self.__ac = ac.autocomplete(self.text, "./bin/data/full_command_list.csv") + self.__history = self.ids.cmd_out.text + self.__output_text = self.__history + "\n\n" + str(self.__ac.pop(0)) + self.ids.cmd_out.text = self.__output_text + self.ids.tin.text = self.__ac.pop(0) + else: + pass + + def runcommand(self): + self.__info = rc.runcommand(self.ids.tin.text, "./bin/data/full_command_list.csv") + if self.__info == "The command executed successfully": + logger.debug(f"The following command has been run successfully: {self.ids.tin.text}") + else: + logger.debug(f"The following command has failed to run: {self.ids.tin.text}") + self.ids.tin.text = "" + self.__history = self.ids.cmd_out.text + self.__output_text = self.__history + "\n\n" + str(self.__info) + self.ids.cmd_out.text = self.__output_text + + +################################ + + +#################################################### + + +################################ +# SCREEN MANAGER +################ + +class RootScreen(ScreenManager): + pass + + +################################ +# UI MANAGER +################################ + +class MicrobitInterface(MDApp): + global screen_manager + screen_manager = ScreenManager() + logger.info("building app...") + + def build(self): + self.title = f"Microbit Bluetooth Interface {version_app}" + self.theme_cls.primary_palette = "Blue" + self.theme_cls.accent_palette = "BlueGray" + # self.icon = + screen_manager.add_widget(Builder.load_file("./bin/gui/load_screen.kv")) + screen_manager.add_widget(Builder.load_file("./bin/gui/main_screen.kv")) + screen_manager.add_widget(Builder.load_file("./bin/gui/command_screen.kv")) + return screen_manager + + # Redirect start instructions to switch screen + def on_start(self): + logger.info("App is starting") + Clock.schedule_once(self.launch_app, 0.5) + + # Switching screens after init + def launch_app(self, dt): + screen_manager.current = "HomeScreen" + screen_manager.transition.duration = 0.2 + screen_manager.transition.direction = "left" + + +logger.info("App start successful") +MicrobitInterface().run() diff --git a/testfile.py b/testfile.py new file mode 100644 index 0000000..ac8cecb --- /dev/null +++ b/testfile.py @@ -0,0 +1,3 @@ +import bin.others.autocomplete + +print(bin.others.autocomplete.AutoComplete().autocomplete("Sta\t\n", "./bin/data/full_command_list.csv"))