thonny-open-cube 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thonny_open_cube-0.1.0/LICENSE.md +9 -0
- thonny_open_cube-0.1.0/MANIFEST.in +3 -0
- thonny_open_cube-0.1.0/PKG-INFO +17 -0
- thonny_open_cube-0.1.0/README.md +23 -0
- thonny_open_cube-0.1.0/setup.cfg +4 -0
- thonny_open_cube-0.1.0/setup.py +23 -0
- thonny_open_cube-0.1.0/tests/test_program_load_bin.py +99 -0
- thonny_open_cube-0.1.0/thonny_open_cube.egg-info/PKG-INFO +17 -0
- thonny_open_cube-0.1.0/thonny_open_cube.egg-info/SOURCES.txt +15 -0
- thonny_open_cube-0.1.0/thonny_open_cube.egg-info/dependency_links.txt +1 -0
- thonny_open_cube-0.1.0/thonny_open_cube.egg-info/requires.txt +2 -0
- thonny_open_cube-0.1.0/thonny_open_cube.egg-info/top_level.txt +1 -0
- thonny_open_cube-0.1.0/thonnycontrib/open_cube/__init__.py +301 -0
- thonny_open_cube-0.1.0/thonnycontrib/open_cube/btl_logo.png +0 -0
- thonny_open_cube-0.1.0/thonnycontrib/open_cube/oc_run.png +0 -0
- thonny_open_cube-0.1.0/thonnycontrib/open_cube/oc_stop.png +0 -0
- thonny_open_cube-0.1.0/thonnycontrib/open_cube/program_load_bin.py +459 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Open-Cube
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thonny-open-cube
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-Cube Bluetooth serial program upload and run plugin for Thonny
|
|
5
|
+
Author: Vaclav Jelinek
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Requires-Dist: pyserial
|
|
11
|
+
Requires-Dist: thonny>=4.1.3
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: classifier
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: requires-python
|
|
17
|
+
Dynamic: summary
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Thonny Open-Cube Bluetooth Upload Plugin
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
- Download [Open-Cube Thonny plugin](dist/thonny_open_cube-0.1.0-py3-none-any.whl)
|
|
6
|
+
- Open Thonny
|
|
7
|
+
- Tools -> Manage plug-ins... -> Install from local file
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
A Thonny plugin for sending programs over Bluetooth to Open-Cube devices and running.
|
|
12
|
+
|
|
13
|
+
This plugin adds:
|
|
14
|
+
- A new toolbar button: **OC Run**
|
|
15
|
+
- A new toolbar button: **OC Stop**
|
|
16
|
+
- A dockable view for **log messages**
|
|
17
|
+
- A configurable COM port option for Windows and Linux
|
|
18
|
+
- File transfer with feedback using a custom Selective Repeat ARQ protocol
|
|
19
|
+
|
|
20
|
+
- Automatically uploads the active editor file to the Open-Cube device
|
|
21
|
+
- Runs the uploaded script
|
|
22
|
+
- Configurable serial port in Thonny settings
|
|
23
|
+
- Supports Windows (COM ports) and Linux (`/dev/rfcomm`)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
packages = ["thonnycontrib.open_cube"]
|
|
4
|
+
print(f"Found packages: {packages}")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
setup(
|
|
8
|
+
name="thonny-open-cube",
|
|
9
|
+
version="0.1.0",
|
|
10
|
+
description="Open-Cube Bluetooth serial program upload and run plugin for Thonny",
|
|
11
|
+
author="Vaclav Jelinek",
|
|
12
|
+
packages=packages,
|
|
13
|
+
include_package_data=True,
|
|
14
|
+
install_requires=[
|
|
15
|
+
"pyserial",
|
|
16
|
+
"thonny >= 4.1.3",
|
|
17
|
+
],
|
|
18
|
+
classifiers=[
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
],
|
|
22
|
+
python_requires=">=3.6",
|
|
23
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
|
7
|
+
MODULE_PATH = PLUGIN_ROOT / "thonnycontrib" / "open_cube" / "program_load_bin.py"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_program_load_bin_module():
|
|
11
|
+
spec = importlib.util.spec_from_file_location("open_cube_program_load_bin", MODULE_PATH)
|
|
12
|
+
if spec is None or spec.loader is None:
|
|
13
|
+
raise RuntimeError(f"Failed to load sender module from {MODULE_PATH}")
|
|
14
|
+
module = importlib.util.module_from_spec(spec)
|
|
15
|
+
sys.modules.setdefault("open_cube_program_load_bin", module)
|
|
16
|
+
spec.loader.exec_module(module)
|
|
17
|
+
return module
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
plb = load_program_load_bin_module()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FakeSerial:
|
|
24
|
+
def __init__(self, port, baudrate, timeout, responses=None):
|
|
25
|
+
self.port = port
|
|
26
|
+
self.baudrate = baudrate
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
self.responses = list(responses or [])
|
|
29
|
+
self.writes = []
|
|
30
|
+
self.closed = False
|
|
31
|
+
self._buffer = b""
|
|
32
|
+
|
|
33
|
+
def write(self, data):
|
|
34
|
+
self.writes.append(data)
|
|
35
|
+
|
|
36
|
+
def readline(self):
|
|
37
|
+
if self.responses:
|
|
38
|
+
return self.responses.pop(0)
|
|
39
|
+
return b""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def in_waiting(self):
|
|
43
|
+
return len(self._buffer)
|
|
44
|
+
|
|
45
|
+
def read(self, size):
|
|
46
|
+
data = self._buffer[:size]
|
|
47
|
+
self._buffer = self._buffer[size:]
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
def close(self):
|
|
51
|
+
self.closed = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_chunk_data_splits_payload():
|
|
55
|
+
chunks = plb.chunk_data(b"abcdefgh", 3)
|
|
56
|
+
assert chunks == [b"abc", b"def", b"gh"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_set_sock_reopens_when_port_changes(monkeypatch):
|
|
60
|
+
created = []
|
|
61
|
+
|
|
62
|
+
def serial_factory(port, baudrate, timeout):
|
|
63
|
+
fake = FakeSerial(port, baudrate, timeout)
|
|
64
|
+
created.append(fake)
|
|
65
|
+
return fake
|
|
66
|
+
|
|
67
|
+
monkeypatch.setattr(plb.serial, "Serial", serial_factory)
|
|
68
|
+
monkeypatch.setattr(plb.time, "sleep", lambda *_args, **_kwargs: None)
|
|
69
|
+
|
|
70
|
+
sender = plb.Sender()
|
|
71
|
+
sender.set_sock("COM7")
|
|
72
|
+
first = sender.sock
|
|
73
|
+
sender.set_sock("COM7")
|
|
74
|
+
sender.set_sock("COM8")
|
|
75
|
+
|
|
76
|
+
assert len(created) == 2
|
|
77
|
+
assert first.closed is True
|
|
78
|
+
assert sender.sock is created[-1]
|
|
79
|
+
assert sender.port == "COM8"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_send_filename_retries_until_cube_ack(monkeypatch):
|
|
83
|
+
sender = plb.Sender()
|
|
84
|
+
sender.filename = "demo.py"
|
|
85
|
+
sender.sock = FakeSerial("COM7", 921600, 0.1, [b"", b"oc prg ok\n"])
|
|
86
|
+
monkeypatch.setattr(plb.time, "sleep", lambda *_args, **_kwargs: None)
|
|
87
|
+
|
|
88
|
+
assert sender.send_filename() is True
|
|
89
|
+
assert sender.sock.writes == [b"\n", b"oc prg demo.py\n"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_build_seq_packet_contains_header_and_crc():
|
|
93
|
+
sender = plb.Sender()
|
|
94
|
+
sender.set_content("print('ok')\n", "demo.py", packet_size=8)
|
|
95
|
+
|
|
96
|
+
packet = sender.build_seq_packet(0)
|
|
97
|
+
|
|
98
|
+
assert packet[:4] == plb.HEADER.to_bytes(4, "big")
|
|
99
|
+
assert len(packet) > 10
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thonny-open-cube
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-Cube Bluetooth serial program upload and run plugin for Thonny
|
|
5
|
+
Author: Vaclav Jelinek
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Requires-Dist: pyserial
|
|
11
|
+
Requires-Dist: thonny>=4.1.3
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: classifier
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: requires-python
|
|
17
|
+
Dynamic: summary
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE.md
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
setup.py
|
|
5
|
+
tests/test_program_load_bin.py
|
|
6
|
+
thonny_open_cube.egg-info/PKG-INFO
|
|
7
|
+
thonny_open_cube.egg-info/SOURCES.txt
|
|
8
|
+
thonny_open_cube.egg-info/dependency_links.txt
|
|
9
|
+
thonny_open_cube.egg-info/requires.txt
|
|
10
|
+
thonny_open_cube.egg-info/top_level.txt
|
|
11
|
+
thonnycontrib/open_cube/__init__.py
|
|
12
|
+
thonnycontrib/open_cube/btl_logo.png
|
|
13
|
+
thonnycontrib/open_cube/oc_run.png
|
|
14
|
+
thonnycontrib/open_cube/oc_stop.png
|
|
15
|
+
thonnycontrib/open_cube/program_load_bin.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
thonnycontrib
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from thonny import get_workbench
|
|
2
|
+
from thonny.languages import tr
|
|
3
|
+
from tkinter import filedialog, messagebox
|
|
4
|
+
from thonny.tktextext import EnhancedText, TextFrame
|
|
5
|
+
from thonny.ui_utils import TextMenu, AutoScrollbar
|
|
6
|
+
from .program_load_bin import PACKET_TIMEOUT, Sender, PACKET_SIZE
|
|
7
|
+
from thonny.common import is_remote_path
|
|
8
|
+
from thonny.editors import extract_target_path
|
|
9
|
+
from thonny.config_ui import ConfigurationPage
|
|
10
|
+
from thonny.misc_utils import (
|
|
11
|
+
running_on_linux,
|
|
12
|
+
running_on_mac_os,
|
|
13
|
+
running_on_rpi,
|
|
14
|
+
running_on_windows,
|
|
15
|
+
)
|
|
16
|
+
import tkinter as tk
|
|
17
|
+
from tkinter import ttk
|
|
18
|
+
|
|
19
|
+
import webbrowser
|
|
20
|
+
import serial.tools.list_ports
|
|
21
|
+
import threading, os, serial
|
|
22
|
+
|
|
23
|
+
def get_bt_port():
|
|
24
|
+
wb = get_workbench()
|
|
25
|
+
view = wb.get_view(OpenCubeView.__name__)
|
|
26
|
+
port = get_workbench().get_option("oc_bt_run.com_port")
|
|
27
|
+
if port is not None and "No port found" not in port:
|
|
28
|
+
return port
|
|
29
|
+
# fallback
|
|
30
|
+
view.append_text("run")
|
|
31
|
+
if running_on_windows():
|
|
32
|
+
view.append_text("good")
|
|
33
|
+
return "COM3"
|
|
34
|
+
else:
|
|
35
|
+
return "/dev/rfcomm0"
|
|
36
|
+
|
|
37
|
+
def get_current_file_content_and_filename():
|
|
38
|
+
wb = get_workbench()
|
|
39
|
+
editor = wb.get_editor_notebook().get_current_editor()
|
|
40
|
+
filepath = editor.get_filename()
|
|
41
|
+
if filepath is None:
|
|
42
|
+
return None, None
|
|
43
|
+
elif is_remote_path(filepath):
|
|
44
|
+
path = extract_target_path(filepath)
|
|
45
|
+
filename = path.split("/")[-1]
|
|
46
|
+
else:
|
|
47
|
+
filename = os.path.basename(filepath)
|
|
48
|
+
content = editor.get_content()
|
|
49
|
+
return content, filename
|
|
50
|
+
|
|
51
|
+
def open_btl():
|
|
52
|
+
webbrowser.open("https://btl-kariera.cz/")
|
|
53
|
+
|
|
54
|
+
def send_stop():
|
|
55
|
+
wb = get_workbench()
|
|
56
|
+
view = wb.get_view(OpenCubeView.__name__)
|
|
57
|
+
view.append_debug("OC Stop pressed")
|
|
58
|
+
if view.sender.running_program:
|
|
59
|
+
view.append_text("\n***Stopping program***")
|
|
60
|
+
view.sender.reset_cube()
|
|
61
|
+
return
|
|
62
|
+
if view.sender.running:
|
|
63
|
+
view.append_text("\n***Stopping upload***")
|
|
64
|
+
view.sender.abort_upload()
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
#sock_set = view.set_sock()
|
|
68
|
+
#if not sock_set:
|
|
69
|
+
# return
|
|
70
|
+
|
|
71
|
+
def send_file_over_serial():
|
|
72
|
+
wb = get_workbench()
|
|
73
|
+
view = wb.get_view(OpenCubeView.__name__)
|
|
74
|
+
view.append_debug("OC Run pressed")
|
|
75
|
+
wb.show_view(OpenCubeView.__name__, False)
|
|
76
|
+
|
|
77
|
+
if view.sender.running:
|
|
78
|
+
view.append_text("*Sender running*")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
sock_set = view.set_sock()
|
|
82
|
+
if not sock_set:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
content, filename = get_current_file_content_and_filename()
|
|
86
|
+
if content is None:
|
|
87
|
+
view.append_text("No file selected. Open a file in the editor.")
|
|
88
|
+
return
|
|
89
|
+
view.append_text(f"\n***Starting file transfer ({filename})***")
|
|
90
|
+
packet_size = wb.get_option("oc_bt_run.packet_size")
|
|
91
|
+
#check if packet size is valid
|
|
92
|
+
if not isinstance(packet_size, int) or packet_size <= 0 or packet_size > 4096:
|
|
93
|
+
packet_size = PACKET_SIZE
|
|
94
|
+
try:
|
|
95
|
+
packet_timeout = wb.get_option("oc_bt_run.packet_timeout")
|
|
96
|
+
|
|
97
|
+
packet_timeout = float(packet_timeout)/1000
|
|
98
|
+
except (ValueError, TypeError):
|
|
99
|
+
packet_timeout = PACKET_TIMEOUT
|
|
100
|
+
view.append_debug(f"Using packet size: {packet_size}, packet timeout: {packet_timeout}")
|
|
101
|
+
view.sender.set_content(content, filename, packet_size, packet_timeout)
|
|
102
|
+
|
|
103
|
+
view.sender.set_control(wb.get_option("oc_bt_run.take_control"))
|
|
104
|
+
|
|
105
|
+
run_after_upload = wb.get_option("oc_bt_run.run_after_upload")
|
|
106
|
+
view.append_debug("Starting upload thread.")
|
|
107
|
+
view.upload_thread = threading.Thread(target=view.sender.upload, daemon=True, args=(run_after_upload,))
|
|
108
|
+
view.upload_thread.start()
|
|
109
|
+
|
|
110
|
+
class SerialText(EnhancedText):
|
|
111
|
+
def __init__(self, master, **kw):
|
|
112
|
+
EnhancedText.__init__(self, master=master, wrap="word", undo=True, **kw)
|
|
113
|
+
self.context_menu = TextMenu(self)
|
|
114
|
+
|
|
115
|
+
def on_secondary_click(self, event=None):
|
|
116
|
+
super().on_secondary_click(event=event)
|
|
117
|
+
self.context_menu.tk_popup(event.x_root, event.y_root)
|
|
118
|
+
|
|
119
|
+
class OpenCubeView(TextFrame):
|
|
120
|
+
def __init__(self, master):
|
|
121
|
+
super().__init__(
|
|
122
|
+
master, text_class=SerialText, horizontal_scrollbar_class=AutoScrollbar
|
|
123
|
+
)
|
|
124
|
+
self.text.edit_reset()
|
|
125
|
+
self.append_text("***Open-Cube upload and run plugin loaded***")
|
|
126
|
+
self.sender = Sender()
|
|
127
|
+
self.sender.set_logger(self.append_text)
|
|
128
|
+
self.sender.set_debug(self.append_debug)
|
|
129
|
+
self.append_debug(f"Running on Windows: {running_on_windows()}")
|
|
130
|
+
|
|
131
|
+
def set_sock(self):
|
|
132
|
+
try:
|
|
133
|
+
port = get_bt_port()
|
|
134
|
+
self.append_debug(f"Used port: {port}")
|
|
135
|
+
self.sender.set_sock(port)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
self.append_text("Please bind Bluetooth socket:")
|
|
138
|
+
if running_on_windows():
|
|
139
|
+
self.append_text("Set correct COM port in settings")
|
|
140
|
+
else:
|
|
141
|
+
self.append_text("Please bind rfcomm:")
|
|
142
|
+
self.append_text("sudo rfcomm bind /dev/rfcomm0 <device-MAC>")
|
|
143
|
+
self.append_text(str(e))
|
|
144
|
+
return False
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
def append_text(self, msg):
|
|
148
|
+
if msg is None:
|
|
149
|
+
return
|
|
150
|
+
self.text.config(state="normal")
|
|
151
|
+
self.text.insert("end", msg + "\n")
|
|
152
|
+
self.text.config(state="disabled")
|
|
153
|
+
self.text.see("end") # Auto-scrol
|
|
154
|
+
|
|
155
|
+
def append_debug(self, msg):
|
|
156
|
+
if msg is None:
|
|
157
|
+
return
|
|
158
|
+
if get_workbench().get_option("oc_bt_run.debug_log"):
|
|
159
|
+
self.append_text("DEBUG: " + msg)
|
|
160
|
+
|
|
161
|
+
def save_content(self, event=None):
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
def destroy(self):
|
|
165
|
+
if self.sender.sock is not None:
|
|
166
|
+
self.sender.sock.close()
|
|
167
|
+
self.sender.sock = None
|
|
168
|
+
super().destroy()
|
|
169
|
+
|
|
170
|
+
class OpenCubeConfigPage(ConfigurationPage):
|
|
171
|
+
def __init__(self, master):
|
|
172
|
+
super().__init__(master)
|
|
173
|
+
|
|
174
|
+
self.ports = [p.device for p in serial.tools.list_ports.comports()]
|
|
175
|
+
if running_on_windows():
|
|
176
|
+
self.ports = [p for p in self.ports if p.startswith("COM")]
|
|
177
|
+
else:
|
|
178
|
+
self.ports = [p for p in self.ports if p.startswith("/dev/rfcomm")]
|
|
179
|
+
if not self.ports:
|
|
180
|
+
self.ports = ["No port found"]
|
|
181
|
+
self.ports.sort()
|
|
182
|
+
|
|
183
|
+
self.columnconfigure(1, weight=1)
|
|
184
|
+
|
|
185
|
+
self.add_checkbox(
|
|
186
|
+
"oc_bt_run.run_after_upload",
|
|
187
|
+
tr("Run after upload"),
|
|
188
|
+
5,
|
|
189
|
+
0,
|
|
190
|
+
columnspan=2,
|
|
191
|
+
pady=(0, 10),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self.add_checkbox(
|
|
195
|
+
"oc_bt_run.take_control",
|
|
196
|
+
tr("Take control (allow OC Stop command and wireless printing)"),
|
|
197
|
+
10,
|
|
198
|
+
0,
|
|
199
|
+
columnspan=2,
|
|
200
|
+
pady=(0, 10),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.add_checkbox(
|
|
204
|
+
"oc_bt_run.debug_log",
|
|
205
|
+
tr("Print debug messages"),
|
|
206
|
+
15,
|
|
207
|
+
0,
|
|
208
|
+
columnspan=2,
|
|
209
|
+
pady=(0, 10),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
com_port_label = ttk.Label(
|
|
213
|
+
self,
|
|
214
|
+
text=tr("Open-Cube BT port")
|
|
215
|
+
)
|
|
216
|
+
com_port_label.grid(row=20, column=0, sticky=tk.W)
|
|
217
|
+
self.add_combobox("oc_bt_run.com_port",
|
|
218
|
+
self.ports,
|
|
219
|
+
20,
|
|
220
|
+
1,
|
|
221
|
+
columnspan=1,
|
|
222
|
+
width=20)
|
|
223
|
+
|
|
224
|
+
packet_size_label = ttk.Label(
|
|
225
|
+
self,
|
|
226
|
+
text=tr("Packet size")
|
|
227
|
+
)
|
|
228
|
+
packet_size_label.grid(row=30, column=0, sticky=tk.W)
|
|
229
|
+
self.add_entry("oc_bt_run.packet_size",
|
|
230
|
+
row=30,
|
|
231
|
+
column=1,
|
|
232
|
+
columnspan=1,
|
|
233
|
+
pady=0,
|
|
234
|
+
width=None)
|
|
235
|
+
|
|
236
|
+
packet_timeout_label = ttk.Label(
|
|
237
|
+
self,
|
|
238
|
+
text=tr("Packet timeout (ms)")
|
|
239
|
+
)
|
|
240
|
+
packet_timeout_label.grid(row=40, column=0, sticky=tk.W)
|
|
241
|
+
self.add_entry("oc_bt_run.packet_timeout",
|
|
242
|
+
row=40,
|
|
243
|
+
column=1,
|
|
244
|
+
columnspan=1,
|
|
245
|
+
pady=0,
|
|
246
|
+
width=None)
|
|
247
|
+
|
|
248
|
+
def load_plugin():
|
|
249
|
+
# Set plugin variables
|
|
250
|
+
get_workbench().set_default("oc_bt_run.run_after_upload", True)
|
|
251
|
+
get_workbench().set_default("oc_bt_run.take_control", True)
|
|
252
|
+
get_workbench().set_default("oc_bt_run.debug_log", False)
|
|
253
|
+
get_workbench().set_default("oc_bt_run.packet_size", PACKET_SIZE)
|
|
254
|
+
get_workbench().set_default("oc_bt_run.packet_timeout", PACKET_TIMEOUT*1000) # Store in milliseconds for easier user input
|
|
255
|
+
if running_on_windows():
|
|
256
|
+
get_workbench().set_default("oc_bt_run.com_port", "COM3")
|
|
257
|
+
else:
|
|
258
|
+
get_workbench().set_default("oc_bt_run.com_port", "/dev/rfcomm0")
|
|
259
|
+
|
|
260
|
+
# Register run command with toolbar icon
|
|
261
|
+
run_image_path = os.path.join(os.path.dirname(__file__), "oc_run.png")
|
|
262
|
+
get_workbench().add_command(
|
|
263
|
+
command_id="oc_upload_run",
|
|
264
|
+
menu_name="run",
|
|
265
|
+
command_label="Open-Cube upload and run",
|
|
266
|
+
handler=send_file_over_serial,
|
|
267
|
+
include_in_toolbar=True,
|
|
268
|
+
caption="Open-Cube run",
|
|
269
|
+
image=run_image_path,
|
|
270
|
+
group=800,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Register stop command with toolbar icon
|
|
274
|
+
stop_image_path = os.path.join(os.path.dirname(__file__), "oc_stop.png")
|
|
275
|
+
get_workbench().add_command(
|
|
276
|
+
command_id="oc_stop",
|
|
277
|
+
menu_name="run",
|
|
278
|
+
command_label="Open-Cube stop",
|
|
279
|
+
handler=send_stop,
|
|
280
|
+
include_in_toolbar=True,
|
|
281
|
+
caption="Open-Cube stop",
|
|
282
|
+
image=stop_image_path,
|
|
283
|
+
group=800,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Register BTL butto
|
|
287
|
+
btl_image_path = os.path.join(os.path.dirname(__file__), "btl_logo.png")
|
|
288
|
+
get_workbench().add_command(
|
|
289
|
+
command_id="BTL",
|
|
290
|
+
menu_name="help",
|
|
291
|
+
command_label="BTL",
|
|
292
|
+
handler=open_btl,
|
|
293
|
+
include_in_toolbar=True,
|
|
294
|
+
caption="BTL",
|
|
295
|
+
image=btl_image_path,
|
|
296
|
+
group=800,
|
|
297
|
+
)
|
|
298
|
+
# Add Open-Cube log view
|
|
299
|
+
get_workbench().add_view(OpenCubeView, tr("Open-Cube log"), "se")
|
|
300
|
+
# Add Open-Cube config page
|
|
301
|
+
get_workbench().add_configuration_page("Open-Cube", tr("Open-Cube"), OpenCubeConfigPage, order=85)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import serial
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
import zlib
|
|
5
|
+
import threading
|
|
6
|
+
import hashlib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Max safe payload is 660 bytes (BT L2CAP default MTU 672 minus 12 bytes
|
|
10
|
+
# packet overhead: 4 header + 2 length + 2 seq + 4 CRC). Packets above
|
|
11
|
+
# this fragment at the L2CAP layer and become unreliable. 640B works but
|
|
12
|
+
# shows occasional NAKs; 512B is rock-solid with zero NAKs and consistent
|
|
13
|
+
# throughput, so we use it as the default.
|
|
14
|
+
PACKET_SIZE = 512
|
|
15
|
+
WINDOW_SIZE = 20
|
|
16
|
+
ACK_TIMEOUT = 0.4
|
|
17
|
+
RESET_TIMEOUT = 1
|
|
18
|
+
PACKET_TIMEOUT = 0.015
|
|
19
|
+
MIN_PACKET_TIMEOUT = 0.008
|
|
20
|
+
MAX_PACKET_TIMEOUT = 0.015
|
|
21
|
+
HEADER = 0x793F5CFE
|
|
22
|
+
CRC_SIZE = 4
|
|
23
|
+
NUM_ATTEMPTS = 3
|
|
24
|
+
MAX_RESENDS = 3
|
|
25
|
+
|
|
26
|
+
def chunk_data(data, size):
|
|
27
|
+
return [data[i:i+size] for i in range(0, len(data), size)]
|
|
28
|
+
|
|
29
|
+
class Sender:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.filepath = None
|
|
32
|
+
self.sock = None
|
|
33
|
+
self.acked = set()
|
|
34
|
+
self.naks = set()
|
|
35
|
+
self.window_end_idx = WINDOW_SIZE
|
|
36
|
+
self.transfer_complete = False
|
|
37
|
+
self.transfer_success = False
|
|
38
|
+
self.cube_abort = False
|
|
39
|
+
self.logger = None
|
|
40
|
+
self.debug_logger = None
|
|
41
|
+
self.progress_callback = None
|
|
42
|
+
self.ack_thread = None
|
|
43
|
+
self.running = False
|
|
44
|
+
self.running_program = False
|
|
45
|
+
self.port = None
|
|
46
|
+
self.take_control = False
|
|
47
|
+
self.state_lock = threading.Lock()
|
|
48
|
+
self.packet_size = PACKET_SIZE
|
|
49
|
+
self.packet_timeout = PACKET_TIMEOUT
|
|
50
|
+
|
|
51
|
+
def set_sock(self, rfcom_port):
|
|
52
|
+
if not rfcom_port:
|
|
53
|
+
if self.sock:
|
|
54
|
+
self.sock.close()
|
|
55
|
+
self.sock = None
|
|
56
|
+
self.debug("Serial port closed.")
|
|
57
|
+
self.port = None
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if self.sock and self.port == rfcom_port:
|
|
61
|
+
self.debug(f"Serial port unchanged {rfcom_port}.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if self.sock:
|
|
65
|
+
self.sock.close()
|
|
66
|
+
self.sock = None
|
|
67
|
+
|
|
68
|
+
self.port = rfcom_port
|
|
69
|
+
self.sock = serial.Serial(rfcom_port, 921600, timeout=0.1)
|
|
70
|
+
time.sleep(1)
|
|
71
|
+
self.debug(f"Opened serial port {rfcom_port}.")
|
|
72
|
+
|
|
73
|
+
def log_and_debug(self, message, logger):
|
|
74
|
+
if logger is None:
|
|
75
|
+
print(message)
|
|
76
|
+
elif hasattr(logger, "insert"): # Tkinter Text widget
|
|
77
|
+
logger.insert("end", message + "\n")
|
|
78
|
+
logger.see("end")
|
|
79
|
+
elif callable(logger):
|
|
80
|
+
logger(message)
|
|
81
|
+
|
|
82
|
+
def log(self, message):
|
|
83
|
+
self.log_and_debug(message, self.logger)
|
|
84
|
+
|
|
85
|
+
def set_logger(self, logger_widget_or_func):
|
|
86
|
+
self.logger = logger_widget_or_func
|
|
87
|
+
|
|
88
|
+
def debug(self, message):
|
|
89
|
+
self.log_and_debug(message, self.debug_logger)
|
|
90
|
+
|
|
91
|
+
def set_debug(self, debug_widget_or_func):
|
|
92
|
+
self.debug_logger = debug_widget_or_func
|
|
93
|
+
|
|
94
|
+
def set_progress_callback(self, callback_func):
|
|
95
|
+
self.progress_callback = callback_func
|
|
96
|
+
|
|
97
|
+
def set_control(self, take_control=False):
|
|
98
|
+
self.take_control = take_control
|
|
99
|
+
self.debug(f"Take control set to {take_control}.")
|
|
100
|
+
|
|
101
|
+
def adaptive_delay(self):
|
|
102
|
+
with self.state_lock:
|
|
103
|
+
in_flight = self.window_end_idx - len(self.acked)
|
|
104
|
+
if in_flight < WINDOW_SIZE // 2:
|
|
105
|
+
time.sleep(MIN_PACKET_TIMEOUT)
|
|
106
|
+
elif in_flight > WINDOW_SIZE * 3 // 4:
|
|
107
|
+
time.sleep(MAX_PACKET_TIMEOUT)
|
|
108
|
+
else:
|
|
109
|
+
time.sleep((MIN_PACKET_TIMEOUT + MAX_PACKET_TIMEOUT) / 2)
|
|
110
|
+
|
|
111
|
+
def reset(self):
|
|
112
|
+
with self.state_lock:
|
|
113
|
+
self.acked = set()
|
|
114
|
+
self.naks = set()
|
|
115
|
+
self.window_end_idx = WINDOW_SIZE
|
|
116
|
+
self.transfer_complete = False
|
|
117
|
+
self.transfer_success = False
|
|
118
|
+
self.cube_abort = False
|
|
119
|
+
|
|
120
|
+
def set_file(self, filepath, packet_size=PACKET_SIZE):
|
|
121
|
+
self.reset()
|
|
122
|
+
self.filepath = filepath
|
|
123
|
+
self.packet_size = packet_size
|
|
124
|
+
with open(filepath, "rb") as f:
|
|
125
|
+
self.filedata = f.read()
|
|
126
|
+
self.filesize = len(self.filedata)
|
|
127
|
+
self.chunks = chunk_data(self.filedata, packet_size)
|
|
128
|
+
self.total = len(self.chunks)
|
|
129
|
+
self.filename = os.path.basename(filepath)
|
|
130
|
+
self.log(f"File size: {self.filesize} bytes, Number of packets: {self.total}")
|
|
131
|
+
|
|
132
|
+
def set_content(self, content, filename, packet_size=PACKET_SIZE, packet_timeout=PACKET_TIMEOUT):
|
|
133
|
+
self.reset()
|
|
134
|
+
self.packet_size = packet_size
|
|
135
|
+
self.filedata = content.encode()
|
|
136
|
+
self.filesize = len(self.filedata)
|
|
137
|
+
self.chunks = chunk_data(self.filedata, packet_size)
|
|
138
|
+
self.total = len(self.chunks)
|
|
139
|
+
self.filename = filename
|
|
140
|
+
self.log(f"File size: {self.filesize} bytes, Number of packets: {self.total}")
|
|
141
|
+
self.packet_timeout = packet_timeout
|
|
142
|
+
|
|
143
|
+
def set_filename(self, filename):
|
|
144
|
+
self.filename = filename
|
|
145
|
+
|
|
146
|
+
def add_seq(self, data, seq):
|
|
147
|
+
payload_w_seq = seq.to_bytes(2, 'little') + data
|
|
148
|
+
return payload_w_seq
|
|
149
|
+
|
|
150
|
+
def add_crc(self, payload):
|
|
151
|
+
crc = zlib.crc32(payload).to_bytes(4, 'little')
|
|
152
|
+
payload_w_crc = payload + crc
|
|
153
|
+
return payload_w_crc
|
|
154
|
+
|
|
155
|
+
# No crc check on length needed - error in lenght causes complete packet loss
|
|
156
|
+
def add_len(self, payload_w_crc):
|
|
157
|
+
length = len(payload_w_crc).to_bytes(2, 'little')
|
|
158
|
+
payload_w_len = length + payload_w_crc
|
|
159
|
+
return payload_w_len
|
|
160
|
+
|
|
161
|
+
def add_header(self, payload):
|
|
162
|
+
header = HEADER.to_bytes(4, 'big')
|
|
163
|
+
packet = header + payload
|
|
164
|
+
return packet
|
|
165
|
+
|
|
166
|
+
def build_seq_packet(self, seq):
|
|
167
|
+
data = self.chunks[seq]
|
|
168
|
+
payload_w_seq = self.add_seq(data, seq)
|
|
169
|
+
packet = self.build_packet(payload_w_seq)
|
|
170
|
+
return packet
|
|
171
|
+
|
|
172
|
+
def build_packet(self, data):
|
|
173
|
+
# Build the packet with header, length, CRC
|
|
174
|
+
payload_w_crc = self.add_crc(data)
|
|
175
|
+
payload_w_len = self.add_len(payload_w_crc)
|
|
176
|
+
packet = self.add_header(payload_w_len)
|
|
177
|
+
return packet
|
|
178
|
+
|
|
179
|
+
def send_packet(self, seq):
|
|
180
|
+
packet = self.build_seq_packet(seq)
|
|
181
|
+
self.sock.write(packet)
|
|
182
|
+
with self.state_lock:
|
|
183
|
+
acked_count = len(self.acked)
|
|
184
|
+
self.log(f"Sent packet {seq + 1}/{self.total} ({acked_count * 100 // self.total}%)")
|
|
185
|
+
if self.progress_callback:
|
|
186
|
+
self.progress_callback(acked_count, self.total)
|
|
187
|
+
|
|
188
|
+
def ping_cube(self):
|
|
189
|
+
time_start = time.time()
|
|
190
|
+
attempts = 0
|
|
191
|
+
while attempts < NUM_ATTEMPTS:
|
|
192
|
+
attempts += 1
|
|
193
|
+
self.debug(f"Sending ping attempt {attempts}")
|
|
194
|
+
self.sock.write(b"oc ping\n")
|
|
195
|
+
time_start = time.time()
|
|
196
|
+
while time.time() - time_start < ACK_TIMEOUT:
|
|
197
|
+
line = self.sock.readline()
|
|
198
|
+
self.debug(f"Received line: {line}")
|
|
199
|
+
if b"oc pong" in line:
|
|
200
|
+
return True
|
|
201
|
+
self.debug("Ping failed.")
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def send_size(self):
|
|
205
|
+
data = self.total.to_bytes(2, 'little') + self.packet_size.to_bytes(2, 'little')
|
|
206
|
+
packet = self.build_packet(data)
|
|
207
|
+
self.sock.write(packet)
|
|
208
|
+
|
|
209
|
+
def send_filename(self):
|
|
210
|
+
self.sock.write(b"\n")
|
|
211
|
+
attempts = 0
|
|
212
|
+
while attempts < NUM_ATTEMPTS:
|
|
213
|
+
attempts += 1
|
|
214
|
+
self.debug(f"Sending command attempt {attempts}: oc prg {self.filename}")
|
|
215
|
+
self.sock.write(f"oc prg {self.filename}\n".encode())
|
|
216
|
+
time_start = time.time()
|
|
217
|
+
while time.time() - time_start < ACK_TIMEOUT:
|
|
218
|
+
line = self.sock.readline()
|
|
219
|
+
self.debug(f"Received line: {line}")
|
|
220
|
+
if b"oc prg ok" in line:
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
def send_filename_and_size(self):
|
|
225
|
+
self.debug("Send filename.")
|
|
226
|
+
if self.send_filename():
|
|
227
|
+
self.debug("Send size.")
|
|
228
|
+
self.send_size()
|
|
229
|
+
return True
|
|
230
|
+
self.debug("Send filename failed.")
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
def send_file_end(self):
|
|
234
|
+
packet = self.build_packet((0xFFFF).to_bytes(2, 'little'))
|
|
235
|
+
self.sock.write(packet)
|
|
236
|
+
self.log("Sent end of file packet.")
|
|
237
|
+
|
|
238
|
+
def send_file_hash(self):
|
|
239
|
+
file_hash = hashlib.sha256(self.filedata).digest()
|
|
240
|
+
data = self.add_seq(file_hash, 0xFFFF)
|
|
241
|
+
packet = self.build_packet(data)
|
|
242
|
+
self.sock.write(packet)
|
|
243
|
+
|
|
244
|
+
def send_reset(self):
|
|
245
|
+
self.debug("Sending reset command.")
|
|
246
|
+
self.sock.write(b"oc rs\n")
|
|
247
|
+
time.sleep(ACK_TIMEOUT)
|
|
248
|
+
line = self.sock.readline()
|
|
249
|
+
if line == b"oc rs ok\n":
|
|
250
|
+
self.log("Program stopped.")
|
|
251
|
+
|
|
252
|
+
def safe_flush(self):
|
|
253
|
+
try:
|
|
254
|
+
in_waiting = self.sock.in_waiting
|
|
255
|
+
if in_waiting > 0:
|
|
256
|
+
flushed = self.sock.read(in_waiting)
|
|
257
|
+
self.debug(f"Flushing {in_waiting} bytes from input buffer ({flushed}).")
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
self.debug(f"Error flushing input buffer: {e}")
|
|
261
|
+
|
|
262
|
+
def listen_ack(self):
|
|
263
|
+
#self.log("Listening for ACK/NAK...")
|
|
264
|
+
self.debug(f"Start listen thread.")
|
|
265
|
+
try:
|
|
266
|
+
while self.running:
|
|
267
|
+
with self.state_lock:
|
|
268
|
+
done = len(self.acked) >= self.total
|
|
269
|
+
if done:
|
|
270
|
+
break
|
|
271
|
+
try:
|
|
272
|
+
msg = self.sock.readline()
|
|
273
|
+
if msg.startswith(b"ACK"):
|
|
274
|
+
seq = int(msg[3:].decode())
|
|
275
|
+
with self.state_lock:
|
|
276
|
+
self.window_end_idx += 1
|
|
277
|
+
self.acked.add(seq)
|
|
278
|
+
self.naks.discard(seq)
|
|
279
|
+
self.debug(f"Received ACK for packet {seq}")
|
|
280
|
+
elif msg.startswith(b"NAK"):
|
|
281
|
+
seq = int(msg[3:].decode())
|
|
282
|
+
with self.state_lock:
|
|
283
|
+
self.naks.add(seq)
|
|
284
|
+
self.debug(f"Received NAK for packet {seq}")
|
|
285
|
+
elif msg.startswith(b"oc prg ko"):
|
|
286
|
+
self.debug("Received ko from cube.")
|
|
287
|
+
self.log("Error sending file.")
|
|
288
|
+
self.transfer_complete = True
|
|
289
|
+
self.cube_abort = True
|
|
290
|
+
break
|
|
291
|
+
elif msg != b"":
|
|
292
|
+
self.debug(f"Received unknown message: {msg}")
|
|
293
|
+
except:
|
|
294
|
+
pass
|
|
295
|
+
except Exception as e:
|
|
296
|
+
self.debug(f"Error in listen thread: {e}")
|
|
297
|
+
self.debug("Exit listen thread.")
|
|
298
|
+
|
|
299
|
+
def reset_cube(self):
|
|
300
|
+
self.safe_flush()
|
|
301
|
+
self.running_program = False
|
|
302
|
+
time.sleep(ACK_TIMEOUT)
|
|
303
|
+
self.send_reset()
|
|
304
|
+
|
|
305
|
+
def _upload(self, run=False):
|
|
306
|
+
time_start = time.time()
|
|
307
|
+
|
|
308
|
+
self.safe_flush()
|
|
309
|
+
if not self.ping_cube():
|
|
310
|
+
self.log("Cube not responding.")
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
self.safe_flush()
|
|
314
|
+
if not self.send_filename_and_size():
|
|
315
|
+
self.log("Failed to start transfer.")
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
self.ack_thread = threading.Thread(target=self.listen_ack, daemon=True)
|
|
319
|
+
self.ack_thread.start()
|
|
320
|
+
|
|
321
|
+
resends = 0
|
|
322
|
+
i = 0
|
|
323
|
+
while i < self.total and not self.transfer_complete:
|
|
324
|
+
with self.state_lock:
|
|
325
|
+
pending_naks = sorted(seq for seq in self.naks if seq < self.total and seq not in self.acked)
|
|
326
|
+
for seq in pending_naks:
|
|
327
|
+
self.naks.discard(seq)
|
|
328
|
+
for seq in pending_naks:
|
|
329
|
+
self.send_packet(seq)
|
|
330
|
+
self.adaptive_delay()
|
|
331
|
+
|
|
332
|
+
j = i
|
|
333
|
+
with self.state_lock:
|
|
334
|
+
window_end_idx = self.window_end_idx
|
|
335
|
+
acked = set(self.acked)
|
|
336
|
+
while j < window_end_idx and j < self.total and not self.transfer_complete:
|
|
337
|
+
if j not in acked:
|
|
338
|
+
self.send_packet(j)
|
|
339
|
+
self.adaptive_delay()
|
|
340
|
+
j += 1
|
|
341
|
+
resends += 1
|
|
342
|
+
with self.state_lock:
|
|
343
|
+
acked = set(self.acked)
|
|
344
|
+
while i in acked:
|
|
345
|
+
resends = 0
|
|
346
|
+
i += 1
|
|
347
|
+
with self.state_lock:
|
|
348
|
+
acked = set(self.acked)
|
|
349
|
+
#time.sleep(ACK_TIMEOUT)
|
|
350
|
+
if resends > MAX_RESENDS:
|
|
351
|
+
self.log("Too many resends, transfer aborted.")
|
|
352
|
+
self.transfer_complete = True
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
if self.cube_abort:
|
|
356
|
+
if self.progress_callback:
|
|
357
|
+
self.progress_callback(0, self.total)
|
|
358
|
+
self.log("Transfer aborted from cube.")
|
|
359
|
+
return
|
|
360
|
+
elif self.transfer_complete:
|
|
361
|
+
self.log("Transfer aborted.")
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
self.safe_flush()
|
|
365
|
+
attempts = 0
|
|
366
|
+
while attempts < NUM_ATTEMPTS:
|
|
367
|
+
attempts += 1
|
|
368
|
+
self.debug(f"Sending file hash attempt {attempts}")
|
|
369
|
+
self.send_file_hash()
|
|
370
|
+
time.sleep(ACK_TIMEOUT)
|
|
371
|
+
line = self.sock.readline()
|
|
372
|
+
if line == b'ACK65535\n':
|
|
373
|
+
self.log("Sent file hash.")
|
|
374
|
+
self.debug("Received ACK for file hash.")
|
|
375
|
+
while attempts < NUM_ATTEMPTS*2:
|
|
376
|
+
attempts += 1
|
|
377
|
+
time.sleep(ACK_TIMEOUT)
|
|
378
|
+
line = self.sock.readline()
|
|
379
|
+
self.debug(f"Received line: {line}")
|
|
380
|
+
if b"oc prg ok" in line:
|
|
381
|
+
self.log("File transfer succesful.")
|
|
382
|
+
time_end = time.time()
|
|
383
|
+
self.log(f"Transfer time: {time_end - time_start:.2f} seconds")
|
|
384
|
+
self.transfer_success = True
|
|
385
|
+
if self.progress_callback:
|
|
386
|
+
self.progress_callback(self.total, self.total)
|
|
387
|
+
if run:
|
|
388
|
+
time.sleep(ACK_TIMEOUT)
|
|
389
|
+
self.run()
|
|
390
|
+
return
|
|
391
|
+
if b"oc prg hash" in line:
|
|
392
|
+
self.log("Hash mismatch, transfer failed.")
|
|
393
|
+
if self.progress_callback:
|
|
394
|
+
self.progress_callback(0, self.total)
|
|
395
|
+
return
|
|
396
|
+
if b"oc prg ko" in line:
|
|
397
|
+
self.log("Transfer rejected by cube.")
|
|
398
|
+
if self.progress_callback:
|
|
399
|
+
self.progress_callback(0, self.total)
|
|
400
|
+
return
|
|
401
|
+
if self.progress_callback:
|
|
402
|
+
self.progress_callback(0, self.total)
|
|
403
|
+
self.log("Failed to send file hash.")
|
|
404
|
+
|
|
405
|
+
def upload(self, run=False):
|
|
406
|
+
self.debug(f"Starting upload (run: {run})")
|
|
407
|
+
self.running = True
|
|
408
|
+
try:
|
|
409
|
+
self._upload(run=run)
|
|
410
|
+
except Exception as e:
|
|
411
|
+
self.debug(f"Exception in upload: {e}")
|
|
412
|
+
self.set_sock(None)
|
|
413
|
+
self.running = False
|
|
414
|
+
self.running_program = False
|
|
415
|
+
if self.ack_thread is not None and self.ack_thread.is_alive():
|
|
416
|
+
self.ack_thread.join(timeout=ACK_TIMEOUT)
|
|
417
|
+
self.debug("Exit upload thread.")
|
|
418
|
+
|
|
419
|
+
def upload_and_run(self):
|
|
420
|
+
self.upload(run=True)
|
|
421
|
+
|
|
422
|
+
def listen_cube(self):
|
|
423
|
+
self.log("Listening for cube messages:\n")
|
|
424
|
+
while self.running_program:
|
|
425
|
+
line = self.sock.readline()
|
|
426
|
+
if line:
|
|
427
|
+
if line == b"oc menu\n":
|
|
428
|
+
self.log("Program stopped.")
|
|
429
|
+
break
|
|
430
|
+
self.log(line.decode().strip())
|
|
431
|
+
self.debug("Stop listening cube program.")
|
|
432
|
+
|
|
433
|
+
def run(self):
|
|
434
|
+
if self.filename:
|
|
435
|
+
self.log("Running program...")
|
|
436
|
+
if self.take_control:
|
|
437
|
+
write = "oc rtc "
|
|
438
|
+
else:
|
|
439
|
+
write = "oc run "
|
|
440
|
+
self.safe_flush()
|
|
441
|
+
attempts = 0
|
|
442
|
+
while attempts < NUM_ATTEMPTS:
|
|
443
|
+
attempts += 1
|
|
444
|
+
self.debug(f"Sending run command attempt {attempts}: {write}{self.filename[:-3]}")
|
|
445
|
+
self.sock.write(f"{write}{self.filename[:-3]}\n".encode())
|
|
446
|
+
time.sleep(ACK_TIMEOUT)
|
|
447
|
+
line = self.sock.readline()
|
|
448
|
+
if b"oc run ok" in line:
|
|
449
|
+
self.log("Program started.")
|
|
450
|
+
if self.take_control:
|
|
451
|
+
self.running_program = True
|
|
452
|
+
self.listen_cube()
|
|
453
|
+
return
|
|
454
|
+
if b"oc run ko" in line:
|
|
455
|
+
break
|
|
456
|
+
self.log("Failed to start program.")
|
|
457
|
+
|
|
458
|
+
def abort_upload(self):
|
|
459
|
+
self.transfer_complete = True
|