Add direct BullTron BLE connect option
This commit is contained in:
@@ -98,6 +98,16 @@ permission issue rather than a protocol issue:
|
|||||||
sudo .venv/bin/python bulltron_gui.py
|
sudo .venv/bin/python bulltron_gui.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The scan list shows every BLE device returned by the OS; BullTron-looking
|
||||||
|
devices are only sorted first and marked with `*`. If the battery is not shown
|
||||||
|
at all, close the Android BullTron app so it is not holding the GATT connection,
|
||||||
|
increase the GUI scan duration, and try the `Direct connect` field with the
|
||||||
|
address observed in the HCI capture:
|
||||||
|
|
||||||
|
```text
|
||||||
|
7C:3E:82:1C:7A:86
|
||||||
|
```
|
||||||
|
|
||||||
### Windows 11
|
### Windows 11
|
||||||
|
|
||||||
Install Python 3.11 or newer from <https://www.python.org/downloads/windows/>
|
Install Python 3.11 or newer from <https://www.python.org/downloads/windows/>
|
||||||
@@ -127,6 +137,12 @@ Windows Settings and that the BullTron battery is nearby and not already held by
|
|||||||
the Android app. Pairing in Windows should not be required for normal telemetry;
|
the Android app. Pairing in Windows should not be required for normal telemetry;
|
||||||
the original Android flow also reads telemetry without bonding.
|
the original Android flow also reads telemetry without bonding.
|
||||||
|
|
||||||
|
If the Windows scan does not list the battery, first close the Android app and
|
||||||
|
use the GUI's longer scan duration. If the PC can still not discover it, use the
|
||||||
|
`Direct connect` field with the captured address `7C:3E:82:1C:7A:86`. Some
|
||||||
|
Windows Bluetooth adapters will only connect to an address after they have seen
|
||||||
|
at least one advertisement from that peripheral in the current session.
|
||||||
|
|
||||||
The GUI shows:
|
The GUI shows:
|
||||||
|
|
||||||
- live pack voltage, current, discharge watts, SOC, remaining Ah, cell max/min,
|
- live pack voltage, current, discharge watts, SOC, remaining Ah, cell max/min,
|
||||||
|
|||||||
+27
-4
@@ -55,6 +55,7 @@ WRITE_CHAR = "0000fff2-0000-1000-8000-00805f9b34fb"
|
|||||||
AT_CHAR = "0000fff3-0000-1000-8000-00805f9b34fb"
|
AT_CHAR = "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
SECRET_KEY_CHAR = "02f00000-0000-0000-0000-00000000ff05"
|
SECRET_KEY_CHAR = "02f00000-0000-0000-0000-00000000ff05"
|
||||||
VERSION_CHAR = "02f00000-0000-0000-0000-00000000ff04"
|
VERSION_CHAR = "02f00000-0000-0000-0000-00000000ff04"
|
||||||
|
BULLTRON_CAPTURE_ADDRESS = "7C:3E:82:1C:7A:86"
|
||||||
BULLTRON_NAME_MARKERS = ("dl", "b35")
|
BULLTRON_NAME_MARKERS = ("dl", "b35")
|
||||||
BULLTRON_ADV_MARKERS = (b"DL", b"PU", b"JHB")
|
BULLTRON_ADV_MARKERS = (b"DL", b"PU", b"JHB")
|
||||||
|
|
||||||
@@ -324,7 +325,7 @@ class BleWorker:
|
|||||||
def submit(self, coro: Any) -> None:
|
def submit(self, coro: Any) -> None:
|
||||||
asyncio.run_coroutine_threadsafe(coro, self.loop)
|
asyncio.run_coroutine_threadsafe(coro, self.loop)
|
||||||
|
|
||||||
def scan(self, timeout: float = 6.0) -> None:
|
def scan(self, timeout: float = 12.0) -> None:
|
||||||
self.submit(self._scan(timeout))
|
self.submit(self._scan(timeout))
|
||||||
|
|
||||||
async def _scan(self, timeout: float) -> None:
|
async def _scan(self, timeout: float) -> None:
|
||||||
@@ -454,8 +455,18 @@ class BullTronGui(tk.Tk):
|
|||||||
|
|
||||||
left = ttk.Frame(root, width=300)
|
left = ttk.Frame(root, width=300)
|
||||||
root.add(left, weight=0)
|
root.add(left, weight=0)
|
||||||
ttk.Button(left, text="Scan", command=self._scan).pack(fill=tk.X, pady=(0, 6))
|
scan_row = ttk.Frame(left)
|
||||||
ttk.Button(left, text="Connect", command=self._connect_selected).pack(fill=tk.X)
|
scan_row.pack(fill=tk.X, pady=(0, 6))
|
||||||
|
ttk.Button(scan_row, text="Scan", command=self._scan).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
ttk.Label(scan_row, text="sec").pack(side=tk.RIGHT, padx=(6, 0))
|
||||||
|
self.scan_timeout = tk.StringVar(value="12")
|
||||||
|
ttk.Spinbox(scan_row, from_=3, to=60, textvariable=self.scan_timeout, width=4).pack(side=tk.RIGHT)
|
||||||
|
ttk.Button(left, text="Connect selected", command=self._connect_selected).pack(fill=tk.X)
|
||||||
|
direct = ttk.LabelFrame(left, text="Direct connect")
|
||||||
|
direct.pack(fill=tk.X, pady=(8, 0))
|
||||||
|
self.direct_address = tk.StringVar(value=BULLTRON_CAPTURE_ADDRESS)
|
||||||
|
ttk.Entry(direct, textvariable=self.direct_address).pack(fill=tk.X, padx=6, pady=(6, 4))
|
||||||
|
ttk.Button(direct, text="Connect address", command=self._connect_direct).pack(fill=tk.X, padx=6, pady=(0, 6))
|
||||||
self.device_list = tk.Listbox(left, height=26)
|
self.device_list = tk.Listbox(left, height=26)
|
||||||
self.device_list.pack(fill=tk.BOTH, expand=True, pady=8)
|
self.device_list.pack(fill=tk.BOTH, expand=True, pady=8)
|
||||||
self.device_list.bind("<Double-Button-1>", lambda _event: self._connect_selected())
|
self.device_list.bind("<Double-Button-1>", lambda _event: self._connect_selected())
|
||||||
@@ -541,7 +552,12 @@ class BullTronGui(tk.Tk):
|
|||||||
self.raw_text.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
|
self.raw_text.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
|
||||||
|
|
||||||
def _scan(self) -> None:
|
def _scan(self) -> None:
|
||||||
self.worker.scan()
|
try:
|
||||||
|
timeout = float(self.scan_timeout.get())
|
||||||
|
except ValueError:
|
||||||
|
messagebox.showerror("Scan", "Scan duration must be a number of seconds")
|
||||||
|
return
|
||||||
|
self.worker.scan(max(3.0, min(timeout, 60.0)))
|
||||||
|
|
||||||
def _connect_selected(self) -> None:
|
def _connect_selected(self) -> None:
|
||||||
selection = self.device_list.curselection()
|
selection = self.device_list.curselection()
|
||||||
@@ -549,6 +565,13 @@ class BullTronGui(tk.Tk):
|
|||||||
return
|
return
|
||||||
self.worker.connect(self.devices[selection[0]]["address"])
|
self.worker.connect(self.devices[selection[0]]["address"])
|
||||||
|
|
||||||
|
def _connect_direct(self) -> None:
|
||||||
|
address = self.direct_address.get().strip()
|
||||||
|
if not address:
|
||||||
|
messagebox.showerror("Address", "Enter a BLE address first")
|
||||||
|
return
|
||||||
|
self.worker.connect(address)
|
||||||
|
|
||||||
def _unlock(self) -> None:
|
def _unlock(self) -> None:
|
||||||
entered = self.pin_entry.get().strip()
|
entered = self.pin_entry.get().strip()
|
||||||
expected = self.vars.get("read_pin", tk.StringVar(value="000000")).get()
|
expected = self.vars.get("read_pin", tk.StringVar(value="000000")).get()
|
||||||
|
|||||||
Reference in New Issue
Block a user