diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..0cfdcb6a --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,39 @@ +name: "CodeQL" + +on: + push: + branches: [ master, develop ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '39 7 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index db992258..7b85a714 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,21 +4,15 @@ name: Coverage (Coveralls) on: [push, pull_request] jobs: - # Runs coverage and upload it coverage: name: Coverage Test - # The type of runner that the job will run on runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - name: Set up Python environment uses: actions/setup-python@v2 with: python-version: 3.6 - # Runs a set of commands using the runners shell - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 982b8598..e3b9e93d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,9 @@ on: jobs: deploy: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - uses: actions/checkout@v2 @@ -69,6 +72,17 @@ jobs: run: | python setup.py sdist bdist_wheel twine upload --skip-existing dist/* + - name: login-to-ghcr + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.LUXROBO_TOKEN }} + - name: build-and-push-docker-image + uses: docker/build-push-action@v2 + with: + push: true + tags: ghcr.io/luxrobo/pymodi:${{ steps.checkout_branch.outputs.version }} - name: send-release-email uses: dawidd6/action-send-mail@v2.2.0 with: diff --git a/AUTHORS.md b/AUTHORS.md index 930e754d..6201de94 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -11,7 +11,9 @@ Maintainer Committer --------- -- Jinsung Ha (jinsung@luxrobo.com) +- Jinsoo Heo (koriel@luxrobo.com) +- Jinsung Ha (jsung5381@naver.com) +- Sebeom Lee (moses97@gmail.com) Contributors ------------ @@ -22,3 +24,6 @@ Contributors - Sebeom Lee (moses97@gmail.com) - Doyeon Kim (archdo0909@gmail.com) - Seokyoung Woo (wsy8029@gmail.com) +- Seungyoon Lee (syoon624@gmail.com) +- Jemin Yeon (yjm9507@yonsei.ac.kr) + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b3ba8143 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.6-slim-buster +COPY . /usr/src/app +WORKDIR /usr/src/app +RUN python setup.py install --user +WORKDIR /workspace + +LABEL org.opencontainers.image.source="https://github.com/luxrobo/pymodi" diff --git a/HISTORY.md b/HISTORY.md index 924e6da7..a29b9c7d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -104,7 +104,7 @@ History 2. Improve BLE connection stability in Linux Platform 3. Implement module usage manual (i.e. quick-start) 4. Implement module inspection functionality (i.e. diagnose) -5. Refactor GUI debugger design, fix initialization issue on macOS +5. Refactor GUI debugger design, fix initialization issue on macOS 1.1.0 (2020-12-17) -- @@ -112,3 +112,11 @@ History 2. Implement network base (STM32) firmware updater 3. Implement GD32 firmware updater 4. Implement [VirtualMODI](https://github.com/luxrobo/virtual-modi) integration code + +1.2.0 (2022-02-16) +-- +1. Detach GUI Firmware Updater from PyMODI to individual repo +2. Implement web client method for MODI json connection +3. Package PyMODI into a dockerfile via GHCR +4. Fix minor bug in BLE connection + diff --git a/README.md b/README.md index a4547a16..54f4b174 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,6 @@ PyMODI provides a control of modular electronics, [MODI](https://modi.luxrobo.co * Update of MODI firmware consisting of both ESP32 and STM32 modules * Direct manipulation of MODI network module using [MODI Play](https://play.google.com/store/apps/details?id=com.luxrobo.modiplay&hl=en_US) -UML Diagram ------------ -

- -

- Build Status ------------ |master|develop| @@ -80,7 +74,7 @@ python --version Install the latest PyMODI if you haven't installed it yet: ```commandline -python -m pip install -U pymodi --user +python -m pip install pymodi --user --upgrade ``` You can also install PyMODI at develop branch (containing latest changes but it can be unstable) with: @@ -181,11 +175,6 @@ To update MODI modules (all modules but the network module), $ python -m modi --update_modules ``` -To update firmware in GUI mode (all three commands above are available) -```commandline -$ python -m modi --update_in_gui -``` - To diagnose MODI modules (helpful to find existing malfunctioning modules), ```commandline $ python -m modi --inspect diff --git a/docs/_static/img/logo.png b/docs/_static/img/logo.png index a46118a7..b6fb827a 100644 Binary files a/docs/_static/img/logo.png and b/docs/_static/img/logo.png differ diff --git a/docs/_static/img/logo2.jpg b/docs/_static/img/logo2.jpg deleted file mode 100644 index d7f7f8c6..00000000 Binary files a/docs/_static/img/logo2.jpg and /dev/null differ diff --git a/docs/_static/img/logo3.jpg b/docs/_static/img/logo3.jpg deleted file mode 100644 index 08838e7e..00000000 Binary files a/docs/_static/img/logo3.jpg and /dev/null differ diff --git a/modi/__main__.py b/modi/__main__.py index 50d4448c..21255201 100644 --- a/modi/__main__.py +++ b/modi/__main__.py @@ -7,10 +7,10 @@ import modi -from modi.util.debugger import Debugger -from modi.util.usage_instructor import UsageInstructor -from modi.util.tutor import Tutor -from modi.util.inspector import Inspector +from modi.util.debugging_util import Debugger +from modi.util.usage_util import UsageInstructor +from modi.util.tutorial_util import Tutor +from modi.util.inspection_util import Inspector from modi.util.firmware_updater import STM32FirmwareUpdater from modi.util.firmware_updater import ESP32FirmwareUpdater @@ -50,7 +50,7 @@ def check_option(*options): try: # all commands should be defined here in advance opts, args = getopt( - sys.argv[1:], "tamhvpdinubgxy", + sys.argv[1:], "tamhvpdinubxy", [ "tutorial", "initialize", @@ -63,7 +63,6 @@ def check_option(*options): "update_network", "usage", "update_network_base", - "update_in_gui", "update_modules_gd", "update_network_base_gd", ] @@ -200,12 +199,3 @@ def check_option(*options): usage = UsageInstructor() usage.run_usage_manual() os._exit(0) - - # Run GUI MODI Firmware Updater - if check_option('-g', '--update_in_gui'): - from PyQt5 import QtWidgets - from modi.util.gui_firmware_updater import Form - app = QtWidgets.QApplication(sys.argv) - w = Form() - sys.exit(app.exec()) - os._exit(0) diff --git a/modi/about.py b/modi/about.py index 678ab3fc..e61630c3 100644 --- a/modi/about.py +++ b/modi/about.py @@ -1,5 +1,5 @@ __title__ = "pymodi" -__version__ = "1.1.0" +__version__ = "1.2.0" __author__ = "LUXROBO" __email__ = "tech@luxrobo.com" __description__ = "Python API for controlling modular electronics, MODI." diff --git a/modi/assets/image/button.png b/modi/assets/image/button.png deleted file mode 100644 index b23aebd6..00000000 Binary files a/modi/assets/image/button.png and /dev/null differ diff --git a/modi/assets/image/dial.png b/modi/assets/image/dial.png deleted file mode 100644 index a954fc5b..00000000 Binary files a/modi/assets/image/dial.png and /dev/null differ diff --git a/modi/assets/image/display.png b/modi/assets/image/display.png deleted file mode 100644 index a5263aee..00000000 Binary files a/modi/assets/image/display.png and /dev/null differ diff --git a/modi/assets/image/env.png b/modi/assets/image/env.png deleted file mode 100644 index ec6fd228..00000000 Binary files a/modi/assets/image/env.png and /dev/null differ diff --git a/modi/assets/image/gyro.png b/modi/assets/image/gyro.png deleted file mode 100644 index 19e03096..00000000 Binary files a/modi/assets/image/gyro.png and /dev/null differ diff --git a/modi/assets/image/ir.png b/modi/assets/image/ir.png deleted file mode 100644 index 92dbe5cd..00000000 Binary files a/modi/assets/image/ir.png and /dev/null differ diff --git a/modi/assets/image/led.png b/modi/assets/image/led.png deleted file mode 100644 index 8e5667d5..00000000 Binary files a/modi/assets/image/led.png and /dev/null differ diff --git a/modi/assets/image/mic.png b/modi/assets/image/mic.png deleted file mode 100644 index a06174dd..00000000 Binary files a/modi/assets/image/mic.png and /dev/null differ diff --git a/modi/assets/image/motor.png b/modi/assets/image/motor.png deleted file mode 100644 index 885a18a0..00000000 Binary files a/modi/assets/image/motor.png and /dev/null differ diff --git a/modi/assets/image/network.png b/modi/assets/image/network.png deleted file mode 100644 index 25eb4520..00000000 Binary files a/modi/assets/image/network.png and /dev/null differ diff --git a/modi/assets/image/speaker.png b/modi/assets/image/speaker.png deleted file mode 100644 index 94b7d402..00000000 Binary files a/modi/assets/image/speaker.png and /dev/null differ diff --git a/modi/assets/image/ultrasonic.png b/modi/assets/image/ultrasonic.png deleted file mode 100644 index 3f74da09..00000000 Binary files a/modi/assets/image/ultrasonic.png and /dev/null differ diff --git a/modi/assets/modi_firmware_updater.ui b/modi/assets/modi_firmware_updater.ui deleted file mode 100644 index 671f3591..00000000 --- a/modi/assets/modi_firmware_updater.ui +++ /dev/null @@ -1,165 +0,0 @@ - - - MODI_FIRMWARE_UPDATER - - - - 0 - 0 - 1141 - 338 - - - - Dialog - - - - - - - 0 - 0 - - - - - 550 - 0 - - - - - - - - - - - - - 0 - 0 - - - - - 10000 - 16777215 - - - - - 28 - 75 - true - false - - - - Qt::StrongFocus - - - 네트워크 모듈 업데이트 - - - - 64 - 64 - - - - - - - - - 0 - 0 - - - - - 10000 - 16777215 - - - - - 28 - 75 - true - false - - - - Qt::NoFocus - - - 모듈 초기화 - - - - 64 - 64 - - - - - - - - - 0 - 0 - - - - - 10000 - 16777215 - - - - - 28 - 75 - true - false - - - - Qt::NoFocus - - - 네트워크 모듈 초기화 - - - - 64 - 64 - - - - - - - - 버튼 텍스트 한/영 전환 - - - - - - - - - - - - - slot1() - slot2() - slot3() - - diff --git a/modi/modi.py b/modi/modi.py index 2a95589f..ebd0ca98 100644 --- a/modi/modi.py +++ b/modi/modi.py @@ -9,9 +9,9 @@ from modi._exe_thrd import ExeThrd from modi.util.connection_util import is_network_module_connected, is_on_pi -from modi.util.miscellaneous import ModuleList -from modi.util.stranger import check_complete -from modi.util.topology_manager import TopologyManager +from modi.util.miscellaneous_util import ModuleList +from modi.util.strange_util import check_complete +from modi.util.topology_util import TopologyManager from modi.util.firmware_updater import STM32FirmwareUpdater from modi.util.firmware_updater import ESP32FirmwareUpdater @@ -19,6 +19,19 @@ class MODI: + network_uuids = {} + + def __call__(cls, *args, **kwargs): + network_uuid = kwargs.get('network_uuid') + conn_type = kwargs.get('conn_type') + if conn_type != 'ble': + return super(MODI, cls).__call__(*args, **kwargs) + if not network_uuid: + raise ValueError('Should input a valid network uuid!') + if network_uuid not in cls.network_uuids: + cls.network_uuids[network_uuid] = \ + super(MODI, cls).__call__(*args, **kwargs) + return cls.network_uuids[network_uuid] def __init__( self, modi_version=1, conn_type="", verbose=False, port=None, @@ -48,9 +61,10 @@ def __init__( init_time = time.time() while not self._topology_manager.is_topology_complete(): time.sleep(0.1) - if time.time() - init_time > 5: - print("MODI init timeout over. " - "Check your module connection.") + if time.time() - init_time > 3: + print( + 'MODI init timeout over. Check your module connection.' + ) break check_complete(self) print("MODI modules are initialized!") @@ -106,9 +120,8 @@ def is_not_checked(module): bad_modules.append(module) return bad_modules - @staticmethod def __init_task( - conn_type, verbose, port, network_uuid, + self, conn_type, verbose, port, network_uuid, ): if not conn_type: is_can = not is_network_module_connected() and is_on_pi() @@ -116,19 +129,24 @@ def __init_task( if conn_type == 'ser': return im('modi.task.ser_task').SerTask(verbose, port) - elif conn_type == 'can': - return im('modi.task.can_task').CanTask(verbose) + elif conn_type == 'soc': + return im('modi.task.soc_task').SocTask(verbose, port) elif conn_type == 'vir': return im('modi.task.vir_task').VirTask(verbose, port) + elif conn_type == 'can': + return im('modi.task.can_task').CanTask(verbose) elif conn_type == 'ble': + if not network_uuid: + raise ValueError('Network UUID not specified!') + self.network_uuids[network_uuid] = self mod_path = { - 'win32': 'modi.task.ble_task.ble_task_win', + 'win32': 'modi.task.ble_task.ble_task_mac', 'linux': 'modi.task.ble_task.ble_task_rpi', 'darwin': 'modi.task.ble_task.ble_task_mac', }.get(sys.platform) return im(mod_path).BleTask(verbose, network_uuid) else: - raise ValueError(f'Invalid conn mode {conn_type}') + raise ValueError(f'Invalid conn mode: {conn_type}') def open(self): atexit.register(self.close) diff --git a/modi/task/ble_task/ble_task_mac.py b/modi/task/ble_task/ble_task_mac.py index c7314aa7..12ad92e8 100644 --- a/modi/task/ble_task/ble_task_mac.py +++ b/modi/task/ble_task/ble_task_mac.py @@ -1,81 +1,82 @@ +import sys import json -import base64 -import asyncio import time +import base64 +import asyncio as aio +import nest_asyncio as nest_aio + from typing import Optional from queue import Queue from threading import Thread -from bleak import discover, BleakClient, BleakError -from bleak.backends.corebluetooth import client as mac_client +from bleak import BleakClient, BleakError, BleakScanner from modi.task.conn_task import ConnTask from modi.util.connection_util import MODIConnectionError -from modi.util.miscellaneous import ask_modi_device -class BleTask(ConnTask): +nest_aio.apply() + +class BleTask(ConnTask): CHAR_UUID = '00008421-0000-1000-8000-00805f9b34fb' def __init__(self, verbose=False, uuid=None): super().__init__(verbose=verbose) - print("Initiating ble_task connection...") - self._loop = asyncio.get_event_loop() - self.__uuid = uuid + self.modi_name = f'MODI_{uuid.upper()}' + print(f'Initiating ble_task connection with {self.modi_name}') + self._loop = aio.get_event_loop() self._recv_q = Queue() self._send_q = Queue() self.__close_event = False - self.__get_service = \ - mac_client.BleakClientCoreBluetooth.get_services - mac_client.BleakClientCoreBluetooth.get_services = self.mac_get_service + + if sys.platform == 'darwin': + from bleak.backends.corebluetooth import client as mac_client + self.__get_service = \ + mac_client.BleakClientCoreBluetooth.get_services + mac_client.BleakClientCoreBluetooth.get_services = \ + self.mac_get_service @staticmethod - # The 'self' parameter of this function is necessary async def mac_get_service(client): return None - async def _list_modi_devices(self): - devices = await discover(timeout=1) - modi_devies = [] - for d in devices: - if 'MODI' in d.name: - modi_devies.append(d) - if not self.__uuid: - self.__uuid = ask_modi_device( - [d.name.upper() for d in modi_devies]) - for d in modi_devies: - if self.__uuid in d.name.upper(): - return d - return None + def match_device(self, device, _): + return device.name == self.modi_name async def __connect(self, address): - client = BleakClient(address, timeout=1) - await client.connect(timeout=1) - await asyncio.sleep(1) - await self.__get_service(client) + client = BleakClient( + address, disconnected_callback=self.handle_disconnected, timeout=2 + ) + await client.connect(timeout=2) + await aio.sleep(1) + if sys.platform == 'darwin': + await self.__get_service(client) return client def __run_loop(self): - asyncio.set_event_loop(self._loop) - tasks = asyncio.gather(self.__send_handler(), self.__watch_notify()) + aio.set_event_loop(self._loop) + tasks = aio.gather(self.__send_handler(), self.__watch_notify()) self._loop.run_until_complete(tasks) async def __watch_notify(self): await self._bus.start_notify(self.CHAR_UUID, self.__recv_handler) while True: - await asyncio.sleep(0.001) + await aio.sleep(0.001) if self.__close_event: break async def __send_handler(self): while True: if self._send_q.empty(): - await asyncio.sleep(0.001) + await aio.sleep(0.001) else: - await self._bus.write_gatt_char( - self.CHAR_UUID, self._send_q.get() - ) + try: + await self._bus.write_gatt_char( + self.CHAR_UUID, self._send_q.get() + ) + except BleakError: + self.__close_event = True if self.__close_event: break @@ -83,8 +84,10 @@ def __recv_handler(self, _, data): self._recv_q.put(self.__parse_ble_msg(data)) def open_conn(self): - loop = asyncio.get_event_loop() - modi_device = loop.run_until_complete(self._list_modi_devices()) + loop = aio.get_event_loop() + modi_device = loop.run_until_complete( + BleakScanner.find_device_by_filter(self.match_device) + ) if modi_device: self._bus = self._loop.run_until_complete( self.__connect(modi_device.address) @@ -92,8 +95,10 @@ def open_conn(self): Thread(target=self.__run_loop, daemon=True).start() print(f"Connected to {modi_device.name}") else: - raise MODIConnectionError(f"Network module of {self.__uuid}" - f" not found!") + raise MODIConnectionError( + f"Network module of {self.modi_name} not found!" + 'Perhaps, the module is already paired with your device?' + ) async def __close_client(self): try: @@ -108,6 +113,10 @@ def close_conn(self): while self._loop.is_running(): time.sleep(0.1) self._loop.run_until_complete(self.__close_client()) + self._loop.close() + + def handle_disconnected(self, _): + print('Device is being properly disconnected...') def recv(self) -> Optional[str]: if self._recv_q.empty(): diff --git a/modi/task/ble_task/ble_task_rpi.py b/modi/task/ble_task/ble_task_rpi.py index 6771e774..ef839144 100644 --- a/modi/task/ble_task/ble_task_rpi.py +++ b/modi/task/ble_task/ble_task_rpi.py @@ -9,7 +9,7 @@ from threading import Thread from modi.task.conn_task import ConnTask -from modi.util.miscellaneous import ask_modi_device +from modi.util.miscellaneous_util import ask_modi_device class BleTask(ConnTask): diff --git a/modi/task/exe_task.py b/modi/task/exe_task.py index ab339a30..c7e24081 100644 --- a/modi/task/exe_task.py +++ b/modi/task/exe_task.py @@ -4,8 +4,8 @@ from modi.module.module import Module, BROADCAST_ID from modi.module.setup_module.battery import Battery -from modi.util.miscellaneous import get_module_from_name -from modi.util.miscellaneous import get_module_type_from_uuid +from modi.util.miscellaneous_util import get_module_from_name +from modi.util.miscellaneous_util import get_module_type_from_uuid from modi.util.message_util import unpack_data, decode_data, parse_message diff --git a/modi/task/soc_task.py b/modi/task/soc_task.py new file mode 100644 index 00000000..7bb28ec9 --- /dev/null +++ b/modi/task/soc_task.py @@ -0,0 +1,43 @@ + + +from websocket import create_connection + +from modi.task.conn_task import ConnTask + + +class SocTask(ConnTask): + """ + SocTask implements a websocket client. + Thus, PyMODI works as a websocket client, listening to a MODI Client. + However, it's possible for PyMODI client to connect to a MODI relay server. + A connection between a server and a client must be one-to-one. + """ + + def __init__(self, verbose=False, port=None): + super().__init__(verbose=verbose) + print('Initiating soc_task connection...') + DEFAULT_SOC_PORT = 8765 + self.port = port if port else DEFAULT_SOC_PORT + + def open_conn(self): + self._bus = create_connection(f'ws://localhost:{self.port}') + + def close_conn(self): + self._bus.close() + + def recv(self): + json_pkt = self._bus.recv() + if self.verbose: + print(f'recv: {json_pkt}') + return json_pkt + + @ConnTask.wait + def send(self, pkt): + self._bus.send(pkt.encode()) + if self.verbose: + print(f'send: {pkt}') + + def send_nowait(self, pkt): + self._bus.send(pkt.encode()) + if self.verbose: + print(f'send: {pkt}') diff --git a/modi/util/debugger.py b/modi/util/debugging_util.py similarity index 100% rename from modi/util/debugger.py rename to modi/util/debugging_util.py diff --git a/modi/util/firmware_updater.py b/modi/util/firmware_updater.py index b85e5974..4adb43fa 100644 --- a/modi/util/firmware_updater.py +++ b/modi/util/firmware_updater.py @@ -20,7 +20,7 @@ from modi.module.module import Module from modi.util.message_util import unpack_data, decode_message, parse_message from modi.util.connection_util import list_modi_ports, is_on_pi -from modi.util.miscellaneous import get_module_type_from_uuid +from modi.util.miscellaneous_util import get_module_type_from_uuid class STM32FirmwareUpdater: @@ -58,6 +58,12 @@ def __init__( self.request_network_id() + def __del__(self): + try: + self.close() + except Exception as e: + print('Magic del is called with an exception:', e) + def set_ui(self, ui): self.ui = ui @@ -99,6 +105,7 @@ def update_module_firmware(self, update_network_base=False): f'Sending a request to update firmware of network ' f'({self.network_id})' ) + if not self.update_in_progress: self.request_to_update_firmware( self.network_id, is_network=True ) @@ -121,12 +128,42 @@ def __open_conn(self): else: return im('modi.task.ser_task').SerTask() - def reinitialize_serial_connection(self): + def _reconnect_serial_connection(self, modi_num): + while True: + time.sleep(0.1) + disconnect = False + if not list_modi_ports(): + disconnect = True + if disconnect: + if modi_num == list_modi_ports(): + self.__reinitialize_serial_connection() + break + + def reinitialize_serial_connection(self, mode=1): + if self.ui and self.update_network_base and mode == 2: + modi_num = len(list_modi_ports()) + if self.ui.is_english: + self.ui.update_network_stm32.setText( + "Reconnect network module and " + "click the button again please." + ) + else: + self.ui.update_network_stm32.setText( + "네트워크 모듈을 재연결 후 버튼을 다시 눌러주십시오." + ) + th.Thread( + target=self._reconnect_serial_connection, + args=(modi_num,), + daemon=True + ).start() + else: + self.__reinitialize_serial_connection() + + def __reinitialize_serial_connection(self): print('Temporally disconnecting the serial connection...') self.close() - - print('Re-init serial connection for the update, in 2 seconds...') time.sleep(2) + print('Re-init serial connection for the update, in 2 seconds...') self.__conn = self.__open_conn() self.__conn.open_conn() self.__running = True @@ -151,7 +188,7 @@ def request_to_update_firmware(self, module_id, is_network=False) -> None: module_id, 4, Module.PNP_OFF ) self.__conn.send_nowait(firmware_update_message) - self.reinitialize_serial_connection() + self.reinitialize_serial_connection(2) else: firmware_update_message = self.__set_module_state( module_id, Module.UPDATE_FIRMWARE, Module.PNP_OFF @@ -219,13 +256,13 @@ def __update_firmware(self, module_id: int, module_type: str) -> None: root_path = ( 'https://download.luxrobo.com/modi-network-os' ) - zip_path = path.join(root_path, 'network.zip') + zip_path = root_path + '/network.zip' bin_path = 'network.bin' else: root_path = ( 'https://download.luxrobo.com/modi-skeleton-mobile' ) - zip_path = path.join(root_path, 'skeleton.zip') + zip_path = root_path + '/skeleton.zip' bin_path = ( path.join(f'skeleton/{module_type.lower()}.bin') if module_type != 'env' else @@ -259,6 +296,30 @@ def __update_firmware(self, module_id: int, module_type: str) -> None: page_offset = 0 if not self.update_network_base else 0x8800 for page_begin in range(bin_begin, bin_end + 1, page_size): progress = 100 * page_begin // bin_end + + if self.ui: + if self.update_network_base: + if self.ui.is_english: + self.ui.update_network_stm32.setText( + f"Network STM32 update is in progress. " + f"({progress}%)" + ) + else: + self.ui.update_network_stm32.setText( + f"네트워크 모듈 초기화가 진행중입니다. " + f"({progress}%)" + ) + else: + if self.ui.is_english: + self.ui.update_stm32_modules.setText( + f"STM32 modules update is in progress. " + f"({progress}%)" + ) + else: + self.ui.update_stm32_modules.setText( + f"모듈 초기화가 진행중입니다. ({progress}%)" + ) + print( f"\rUpdating {module_type} ({module_id}) " f"{self.__progress_bar(page_begin, bin_end)} " @@ -313,13 +374,13 @@ def __update_firmware(self, module_id: int, module_type: str) -> None: # Get version info from version_path, using appropriate methods version_info, version_file = None, 'version.txt' if self.ui: - version_path = path.join(root_path, version_file) + version_path = root_path + '/' + version_file for line in ur.urlopen(version_path, timeout=5): version_info = line.decode('utf-8').lstrip('v') else: if self.update_network_base: version_file = 'base_' + version_file - version_path = path.join(root_path, version_file) + version_path = root_path + '/' + version_file with open(version_path) as version_file: version_info = version_file.readline().lstrip('v').rstrip('\n') version_digits = [int(digit) for digit in version_info.split('.')] @@ -359,13 +420,45 @@ def __update_firmware(self, module_id: int, module_type: str) -> None: print("Reboot message has been sent to all connected modules") self.reset_state() if self.update_network_base: - self.reinitialize_serial_connection() + self.reinitialize_serial_connection(1) time.sleep(0.5) time.sleep(1) self.update_in_progress = False self.update_event.set() + if self.ui: + if self.update_network_base: + self.ui.update_stm32_modules.setStyleSheet( + f'border-image: url({self.ui.active_path})' + ) + self.ui.update_stm32_modules.setEnabled(True) + if self.ui.is_english: + self.ui.update_network_stm32.setText( + "Update Network STM32" + ) + else: + self.ui.update_network_stm32.setText( + "네트워크 모듈 초기화" + ) + else: + self.ui.update_network_stm32.setStyleSheet( + f'border-image: url({self.ui.active_path})' + ) + self.ui.update_network_stm32.setEnabled(True) + if self.ui.is_english: + self.ui.update_stm32_modules.setText( + "Update STM32 Modules." + ) + else: + self.ui.update_stm32_modules.setText( + "모듈 초기화" + ) + self.ui.update_network_esp32.setStyleSheet( + f'border-image: url({self.ui.active_path})' + ) + self.ui.update_network_esp32.setEnabled(True) + @staticmethod def __delay(span): init_time = time.perf_counter() @@ -698,6 +791,24 @@ def update_firmware(self, force=False): self.flushOutput() self.close() + if self.ui: + self.ui.update_stm32_modules.setStyleSheet( + f'border-image: url({self.ui.active_path})' + ) + self.ui.update_stm32_modules.setEnabled(True) + self.ui.update_network_stm32.setStyleSheet( + f'border-image: url({self.ui.active_path})' + ) + self.ui.update_network_stm32.setEnabled(True) + if self.ui.is_english: + self.ui.update_network_esp32.setText( + "Update Network ESP32" + ) + else: + self.ui.update_network_esp32.setText( + "네트워크 모듈 업데이트" + ) + def __device_ready(self): print("Redirecting connection to esp device...") self.write(b'{"c":43,"s":0,"d":4095,"b":"AA==","l":1}') @@ -971,6 +1082,15 @@ def __write_binary_firmware(self, binary_firmware: bytes, manager): ) if manager: manager.quit() + if self.ui: + if self.ui.is_english: + self.ui.update_network_esp32.setText( + "Network ESP32 update is in progress. (100%)" + ) + else: + self.ui.update_network_esp32.setText( + "네트워크 모듈 업데이트가 진행중입니다. (100%)" + ) print(f"\r{self.__progress_bar(1, 1)}") print("Firmware Upload Complete") @@ -986,6 +1106,17 @@ def __write_chunk(self, chunk, curr_seq, total_seq, manager): for seq, block in enumerate(block_queue): if manager: manager.status = self.__progress_bar(curr_seq + seq, total_seq) + if self.ui: + if self.ui.is_english: + self.ui.update_network_esp32.setText( + f"Network ESP32 update is in progress. " + f"({int((curr_seq+seq)/total_seq*100)}%)" + ) + else: + self.ui.update_network_esp32.setText( + f"네트워크 모듈 업데이트가 진행중입니다. " + f"({int((curr_seq+seq)/total_seq*100)}%)" + ) print( f'\r{self.__progress_bar(curr_seq + seq, total_seq)}', end='' ) diff --git a/modi/util/gui_firmware_updater.py b/modi/util/gui_firmware_updater.py deleted file mode 100644 index 2ab61898..00000000 --- a/modi/util/gui_firmware_updater.py +++ /dev/null @@ -1,228 +0,0 @@ -import os -import sys -import time -import logging - -import threading as th - - -from PyQt5 import uic -from PyQt5 import QtWidgets, QtCore, QtGui -from PyQt5.QtCore import QObject, pyqtSignal - -from modi.util.firmware_updater import STM32FirmwareUpdater -from modi.util.firmware_updater import ESP32FirmwareUpdater - - -class StdoutRedirect(QObject): - printOccur = pyqtSignal(str, str, name='print') - - def __init__(self): - QObject.__init__(self, None) - self.daemon = True - self.sysstdout = sys.stdout.write - self.sysstderr = sys.stderr.write - self.logger = None - - def stop(self): - sys.stdout.write = self.sysstdout - sys.stderr.write = self.sysstderr - - def start(self): - sys.stdout.write = self.write - sys.stderr.write = lambda msg: self.write(msg, color='red') - - def write(self, s, color="black"): - sys.stdout.flush() - self.printOccur.emit(s, color) - if self.logger and not self.__is_redundant_line(s): - self.logger.info(s) - - @staticmethod - def __is_redundant_line(line): - if line.startswith('\rUpdating'): - return True - - if line.startswith('\rFirmware Upload: ['): - return True - - if len(line) < 3: - return True - - return False - - -class Form(QtWidgets.QDialog): - """ - GUI Form of MODI Firmware Updater - """ - - def __init__(self, installer=False): - self.logger = self.__init_logger() - QtWidgets.QDialog.__init__(self) - if installer: - ui_path = os.path.dirname(__file__).replace( - 'util', 'modi_firmware_updater.ui' - ) - else: - ui_path = ( - os.path.join( - os.path.dirname(__file__), - '..', 'assets', 'modi_firmware_updater.ui' - ) - ) - self.ui = uic.loadUi(ui_path) - self.ui.setWindowTitle('MODI Firmware Updater') - self.setFixedSize(self.size()) - self.ui.show() - - # Redirect stdout to text browser (i.e. console in our UI) - self.stdout = StdoutRedirect() - self.stdout.start() - self.stdout.printOccur.connect( - lambda line: self.__append_text_line(line) - ) - self.stdout.logger = self.logger - - # Init variable to check if the program is in installation mode - self.ui.installation = installer - - # Connect up the buttons - self.ui.update_network_esp32.clicked.connect(self.update_network_esp32) - self.ui.update_stm32_modules.clicked.connect(self.update_stm32_modules) - self.ui.update_network_stm32.clicked.connect(self.update_network_stm32) - self.ui.translate_button.clicked.connect(self.translate_button_text) - - self.buttons = [ - self.ui.update_network_esp32, - self.ui.update_stm32_modules, - self.ui.update_network_stm32, - self.ui.translate_button, - ] - - # Disable the first button to be focused when UI is loaded - self.ui.update_network_esp32.setAutoDefault(False) - self.ui.update_network_esp32.setDefault(False) - - # Print init status - time_now_str = time.strftime('[%Y/%m/%d@%X]', time.localtime()) - print(time_now_str + ' GUI MODI Firmware Updater has been started!') - - # Set up field variables - self.firmware_updater = None - self.button_in_english = False - - # - # Main methods - # - def update_network_esp32(self): - if self.firmware_updater and self.firmware_updater.update_in_progress: - return - self.ui.console.clear() - print( - 'ESP32 Firmware Updater has been initialized for esp update!' - ) - esp32_updater = ESP32FirmwareUpdater() - esp32_updater.set_ui(self.ui) - th.Thread(target=esp32_updater.update_firmware, daemon=True).start() - self.firmware_updater = esp32_updater - - def update_stm32_modules(self): - if self.firmware_updater and self.firmware_updater.update_in_progress: - return - self.ui.console.clear() - print( - 'STM32 Firmware Updater has been initialized for module update!' - ) - stm32_updater = STM32FirmwareUpdater() - stm32_updater.set_ui(self.ui) - th.Thread( - target=stm32_updater.update_module_firmware, daemon=True - ).start() - self.firmware_updater = stm32_updater - - def update_network_stm32(self): - if self.firmware_updater and self.firmware_updater.update_in_progress: - return - self.ui.console.clear() - print( - 'STM32 Firmware Updater has been initialized for base update!' - ) - stm32_updater = STM32FirmwareUpdater() - stm32_updater.set_ui(self.ui) - th.Thread( - target=stm32_updater.update_module_firmware, - args=(True,), - daemon=True - ).start() - self.firmware_updater = stm32_updater - - def translate_button_text(self): - button_en = [ - 'Update Network ESP32', - 'Update STM32 Modules', - 'Update Network STM32', - 'Translate Button Text To Korean', - ] - button_kr = [ - '네트워크 모듈 업데이트', - '모듈 초기화', - '네트워크 모듈 초기화', - '버튼 텍스트를 영어로 변경', - ] - appropriate_translation = \ - button_kr if self.button_in_english else button_en - self.button_in_english = not self.button_in_english - for i, button in enumerate(self.buttons): - button.setText(appropriate_translation[i]) - - # - # Helper functions - # - @staticmethod - def __init_logger(): - logger = logging.getLogger('GUI MODI Firmware Updater Logger') - logger.setLevel(logging.DEBUG) - - formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - file_handler = logging.FileHandler('gmfu.log') - file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(formatter) - - logger.addHandler(file_handler) - return logger - - def __append_text_line(self, line): - self.ui.console.moveCursor( - QtGui.QTextCursor.End, QtGui.QTextCursor.MoveAnchor - ) - self.ui.console.moveCursor( - QtGui.QTextCursor.StartOfLine, QtGui.QTextCursor.MoveAnchor - ) - self.ui.console.moveCursor( - QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor - ) - - # Remove new line character if current line represents update_progress - if self.__is_update_progress_line(line): - self.ui.console.textCursor().removeSelectedText() - self.ui.console.textCursor().deletePreviousChar() - - # Display user text input - self.ui.console.moveCursor(QtGui.QTextCursor.End) - self.ui.console.insertPlainText(line) - QtWidgets.QApplication.processEvents( - QtCore.QEventLoop.ExcludeUserInputEvents - ) - - @staticmethod - def __is_update_progress_line(line): - if line.startswith('\rUpdating'): - return True - - if line.startswith('\rFirmware Upload: ['): - return True - - return False diff --git a/modi/util/inspector.py b/modi/util/inspection_util.py similarity index 100% rename from modi/util/inspector.py rename to modi/util/inspection_util.py diff --git a/modi/util/miscellaneous.py b/modi/util/miscellaneous_util.py similarity index 94% rename from modi/util/miscellaneous.py rename to modi/util/miscellaneous_util.py index 145045c2..2346c1f7 100644 --- a/modi/util/miscellaneous.py +++ b/modi/util/miscellaneous_util.py @@ -43,6 +43,11 @@ def get_module_from_name(module_type: str): def ask_modi_device(devices): + if not devices: + raise ValueError( + 'No MODI network module(s) available!\n' + 'The network module that you\'re trying to connect, may in use.' + ) for idx, dev in enumerate(devices): print(f"<{idx}>: {dev}") i = input("Choose your device index (ex: 0) : ") diff --git a/modi/util/stranger.py b/modi/util/strange_util.py similarity index 100% rename from modi/util/stranger.py rename to modi/util/strange_util.py diff --git a/modi/util/topology_manager.py b/modi/util/topology_util.py similarity index 99% rename from modi/util/topology_manager.py rename to modi/util/topology_util.py index b7de95e2..b0bc2a04 100644 --- a/modi/util/topology_manager.py +++ b/modi/util/topology_util.py @@ -1,7 +1,7 @@ import time from typing import Dict, List, Tuple -from modi.util.miscellaneous import ModuleList +from modi.util.miscellaneous_util import ModuleList class TopologyManager: diff --git a/modi/util/tutor.py b/modi/util/tutorial_util.py similarity index 100% rename from modi/util/tutor.py rename to modi/util/tutorial_util.py diff --git a/modi/util/usage_instructor.py b/modi/util/usage_util.py similarity index 100% rename from modi/util/usage_instructor.py rename to modi/util/usage_util.py diff --git a/requirements-dev.txt b/requirements-dev.txt index cb7bb097..4b167e08 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,6 @@ -pip -flake8 -black -tox -coverage -pre-commit -bumpversion +flake8==4.0.1 +black==21.10b0 +tox==3.24.4 +coverage==6.1.1 +pre-commit==2.15.0 +bumpversion==0.6.0 diff --git a/requirements.txt b/requirements.txt index b81824a5..76afbe61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ # common -pyserial -pyqt5 +pyserial==3.5 +nest-asyncio==1.5.4 +websocket-client==1.2.3 # windows -bleak==0.8.0; sys_platform == 'win32' +bleak==0.13.0; sys_platform == 'win32' # mac tkmacosx; sys_platform == 'darwin' -bleak==0.8.0; sys_platform == 'darwin' +bleak==0.13.0; sys_platform == 'darwin' # linux python-can==3.3.4; sys_platform == 'linux' diff --git a/setup.cfg b/setup.cfg index 7e74acfe..f553df5f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.1.0 +current_version = 1.2.0 commit = True tag = False diff --git a/tests/module/input_module/test_button.py b/tests/module/input_module/test_button.py index 2c87737f..996a84ab 100644 --- a/tests/module/input_module/test_button.py +++ b/tests/module/input_module/test_button.py @@ -2,7 +2,7 @@ from modi.module.input_module.button import Button from modi.util.message_util import parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestButton(unittest.TestCase): diff --git a/tests/module/input_module/test_dial.py b/tests/module/input_module/test_dial.py index 741429ee..aff42131 100644 --- a/tests/module/input_module/test_dial.py +++ b/tests/module/input_module/test_dial.py @@ -2,7 +2,7 @@ from modi.module.input_module.dial import Dial from modi.util.message_util import parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestDial(unittest.TestCase): diff --git a/tests/module/input_module/test_env.py b/tests/module/input_module/test_env.py index a261caa9..316133d8 100644 --- a/tests/module/input_module/test_env.py +++ b/tests/module/input_module/test_env.py @@ -2,7 +2,7 @@ from modi.module.input_module.env import Env from modi.util.message_util import parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestEnv(unittest.TestCase): diff --git a/tests/module/input_module/test_gyro.py b/tests/module/input_module/test_gyro.py index 736fea90..58d4458b 100644 --- a/tests/module/input_module/test_gyro.py +++ b/tests/module/input_module/test_gyro.py @@ -2,7 +2,7 @@ from modi.module.input_module.gyro import Gyro from modi.util.message_util import parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestGyro(unittest.TestCase): diff --git a/tests/module/input_module/test_ir.py b/tests/module/input_module/test_ir.py index 2c17324f..f0d80f9c 100644 --- a/tests/module/input_module/test_ir.py +++ b/tests/module/input_module/test_ir.py @@ -2,7 +2,7 @@ from modi.module.input_module.ir import Ir from modi.util.message_util import parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestIr(unittest.TestCase): diff --git a/tests/module/input_module/test_mic.py b/tests/module/input_module/test_mic.py index af34b374..cce3064c 100644 --- a/tests/module/input_module/test_mic.py +++ b/tests/module/input_module/test_mic.py @@ -3,7 +3,7 @@ from modi.module.input_module.mic import Mic from modi.util.message_util import parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestMic(unittest.TestCase): diff --git a/tests/module/input_module/test_ultrasonic.py b/tests/module/input_module/test_ultrasonic.py index 623ab1c8..7cab36ad 100644 --- a/tests/module/input_module/test_ultrasonic.py +++ b/tests/module/input_module/test_ultrasonic.py @@ -2,7 +2,7 @@ from modi.module.input_module.ultrasonic import Ultrasonic from modi.util.message_util import parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestUltrasonic(unittest.TestCase): diff --git a/tests/module/output_module/test_display.py b/tests/module/output_module/test_display.py index 0a15e494..6ab6eb53 100644 --- a/tests/module/output_module/test_display.py +++ b/tests/module/output_module/test_display.py @@ -2,7 +2,7 @@ from modi.module.output_module.display import Display from modi.util.message_util import parse_data, parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestDisplay(unittest.TestCase): diff --git a/tests/module/output_module/test_led.py b/tests/module/output_module/test_led.py index 833003cf..39000ae9 100644 --- a/tests/module/output_module/test_led.py +++ b/tests/module/output_module/test_led.py @@ -2,7 +2,7 @@ from modi.module.output_module.led import Led from modi.util.message_util import parse_data, parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestLed(unittest.TestCase): diff --git a/tests/module/output_module/test_motor.py b/tests/module/output_module/test_motor.py index 631fdc42..d401300f 100644 --- a/tests/module/output_module/test_motor.py +++ b/tests/module/output_module/test_motor.py @@ -2,7 +2,7 @@ from modi.module.output_module.motor import Motor from modi.util.message_util import parse_data, parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestMotor(unittest.TestCase): diff --git a/tests/module/output_module/test_speaker.py b/tests/module/output_module/test_speaker.py index 94a5cfaf..68b8e2bc 100644 --- a/tests/module/output_module/test_speaker.py +++ b/tests/module/output_module/test_speaker.py @@ -2,7 +2,7 @@ from modi.module.output_module.speaker import Speaker from modi.util.message_util import parse_data, parse_message -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn class TestSpeaker(unittest.TestCase): diff --git a/tests/task/test_exe_task.py b/tests/task/test_exe_task.py index 5c8ff704..2b7c954f 100644 --- a/tests/task/test_exe_task.py +++ b/tests/task/test_exe_task.py @@ -1,6 +1,6 @@ import unittest -from modi.util.miscellaneous import MockConn +from modi.util.miscellaneous_util import MockConn from modi.task.exe_task import ExeTask