brownie1 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.
- brownie1-0.1.0/.pypirc +3 -0
- brownie1-0.1.0/PKG-INFO +37 -0
- brownie1-0.1.0/README.md +24 -0
- brownie1-0.1.0/pyproject.toml +33 -0
- brownie1-0.1.0/src/brownie/__init__.py +1 -0
- brownie1-0.1.0/src/brownie/cli.py +72 -0
- brownie1-0.1.0/src/brownie/core.py +55 -0
- brownie1-0.1.0/tests/test_core.py +79 -0
- brownie1-0.1.0/upload_to_pypi.sh +25 -0
brownie1-0.1.0/.pypirc
ADDED
brownie1-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: brownie1
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimalistic chat clone
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: bleak>=0.21.1
|
|
7
|
+
Requires-Dist: rich>=13.0.0
|
|
8
|
+
Requires-Dist: typer>=0.9.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.21.1; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# BlueChat Lite
|
|
15
|
+
|
|
16
|
+
BlueChat Lite is a minimalistic clone of BlueChat. It focuses purely on Bluetooth LE (BLE) client capabilities using `bleak`, acting as a central device that connects to Nordic UART Service (NUS) compatible peripherals.
|
|
17
|
+
|
|
18
|
+
It strips out the complex cryptographic authentication, file transfer chunks, and specific native GATT server implementations (like BlueZ, WinRT, and CoreBluetooth).
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- Scan for BLE devices (`bluechat-lite devices`)
|
|
23
|
+
- Connect and chat with a BLE device hosting Nordic UART Service (`bluechat-lite join <MAC_ADDRESS>`)
|
|
24
|
+
- Multiplatform client support (macOS, Linux, Windows) via `bleak`.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Testing
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
pytest
|
|
37
|
+
```
|
brownie1-0.1.0/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# BlueChat Lite
|
|
2
|
+
|
|
3
|
+
BlueChat Lite is a minimalistic clone of BlueChat. It focuses purely on Bluetooth LE (BLE) client capabilities using `bleak`, acting as a central device that connects to Nordic UART Service (NUS) compatible peripherals.
|
|
4
|
+
|
|
5
|
+
It strips out the complex cryptographic authentication, file transfer chunks, and specific native GATT server implementations (like BlueZ, WinRT, and CoreBluetooth).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Scan for BLE devices (`bluechat-lite devices`)
|
|
10
|
+
- Connect and chat with a BLE device hosting Nordic UART Service (`bluechat-lite join <MAC_ADDRESS>`)
|
|
11
|
+
- Multiplatform client support (macOS, Linux, Windows) via `bleak`.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Testing
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install -e ".[dev]"
|
|
23
|
+
pytest
|
|
24
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "brownie1"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A minimalistic chat clone"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"bleak>=0.21.1",
|
|
13
|
+
"typer>=0.9.0",
|
|
14
|
+
"rich>=13.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=7.0",
|
|
20
|
+
"pytest-asyncio>=0.21.1",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.sdist]
|
|
24
|
+
exclude = [
|
|
25
|
+
"/venv",
|
|
26
|
+
"/.venv",
|
|
27
|
+
"/dist",
|
|
28
|
+
"/build",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
only-include = ["src/brownie"]
|
|
33
|
+
sources = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Brownie Lite Package"""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from brownie.core import scan_devices, BlueChatLiteClient
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Brownie Lite - Minimalistic BLE chat client")
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
@app.command("devices")
|
|
12
|
+
def list_devices(timeout: float = 5.0):
|
|
13
|
+
"""Scan and list nearby BLE devices."""
|
|
14
|
+
console.print(f"Scanning for devices for {timeout} seconds...")
|
|
15
|
+
|
|
16
|
+
async def _scan():
|
|
17
|
+
devices = await scan_devices(timeout)
|
|
18
|
+
table = Table(title="Nearby BLE Devices")
|
|
19
|
+
table.add_column("Address", style="cyan")
|
|
20
|
+
table.add_column("Name", style="green")
|
|
21
|
+
|
|
22
|
+
for dev in devices:
|
|
23
|
+
table.add_row(dev.address, dev.name or "Unknown")
|
|
24
|
+
|
|
25
|
+
console.print(table)
|
|
26
|
+
|
|
27
|
+
asyncio.run(_scan())
|
|
28
|
+
|
|
29
|
+
@app.command("join")
|
|
30
|
+
def join(address: str):
|
|
31
|
+
"""Connect to a BLE device by its MAC address."""
|
|
32
|
+
console.print(f"Connecting to {address}...")
|
|
33
|
+
|
|
34
|
+
async def _chat():
|
|
35
|
+
client = BlueChatLiteClient(address)
|
|
36
|
+
|
|
37
|
+
def handle_message(text: str):
|
|
38
|
+
# Print the received message to standard output using rich
|
|
39
|
+
console.print(f"\n[bold green]Peer:[/bold green] {text}")
|
|
40
|
+
# Reprint prompt since it got messed up
|
|
41
|
+
console.print("> ", end="")
|
|
42
|
+
|
|
43
|
+
client.on_message(handle_message)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
await client.connect()
|
|
47
|
+
console.print("[bold green]Connected! Type your messages (Ctrl+C to exit).[/bold green]")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
console.print(f"[bold red]Failed to connect: {e}[/bold red]")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Start a simple input loop
|
|
53
|
+
loop = asyncio.get_event_loop()
|
|
54
|
+
try:
|
|
55
|
+
while True:
|
|
56
|
+
# Use executor to not block asyncio event loop
|
|
57
|
+
msg = await loop.run_in_executor(None, input, "> ")
|
|
58
|
+
if msg.strip():
|
|
59
|
+
await client.send_message(msg)
|
|
60
|
+
except (KeyboardInterrupt, EOFError):
|
|
61
|
+
console.print("\nDisconnecting...")
|
|
62
|
+
finally:
|
|
63
|
+
await client.disconnect()
|
|
64
|
+
console.print("[bold green]Disconnected.[/bold green]")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
asyncio.run(_chat())
|
|
68
|
+
except KeyboardInterrupt:
|
|
69
|
+
sys.exit(0)
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
app()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Callable, List, Optional
|
|
3
|
+
from bleak import BleakScanner, BleakClient
|
|
4
|
+
from bleak.backends.device import BLEDevice
|
|
5
|
+
|
|
6
|
+
# Nordic UART Service UUIDs
|
|
7
|
+
NUS_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
|
|
8
|
+
NUS_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
|
|
9
|
+
NUS_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
|
|
10
|
+
|
|
11
|
+
async def scan_devices(timeout: float = 5.0) -> List[BLEDevice]:
|
|
12
|
+
"""Scan for BLE devices."""
|
|
13
|
+
devices = await BleakScanner.discover(timeout=timeout)
|
|
14
|
+
return devices
|
|
15
|
+
|
|
16
|
+
class BlueChatLiteClient:
|
|
17
|
+
def __init__(self, address: str):
|
|
18
|
+
self.address = address
|
|
19
|
+
self.client: Optional[BleakClient] = None
|
|
20
|
+
self._message_callback: Optional[Callable[[str], None]] = None
|
|
21
|
+
|
|
22
|
+
def on_message(self, callback: Callable[[str], None]):
|
|
23
|
+
"""Register a callback for incoming messages."""
|
|
24
|
+
self._message_callback = callback
|
|
25
|
+
|
|
26
|
+
async def connect(self):
|
|
27
|
+
"""Connect to the BLE device."""
|
|
28
|
+
self.client = BleakClient(self.address)
|
|
29
|
+
await self.client.connect()
|
|
30
|
+
# Start notifying on the TX characteristic (device to host)
|
|
31
|
+
await self.client.start_notify(NUS_TX_UUID, self._notification_handler)
|
|
32
|
+
|
|
33
|
+
async def disconnect(self):
|
|
34
|
+
"""Disconnect from the BLE device."""
|
|
35
|
+
if self.client and self.client.is_connected:
|
|
36
|
+
await self.client.stop_notify(NUS_TX_UUID)
|
|
37
|
+
await self.client.disconnect()
|
|
38
|
+
|
|
39
|
+
def _notification_handler(self, sender: int, data: bytearray):
|
|
40
|
+
"""Handle incoming notifications (messages)."""
|
|
41
|
+
if self._message_callback:
|
|
42
|
+
try:
|
|
43
|
+
text = data.decode("utf-8")
|
|
44
|
+
self._message_callback(text)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"[Error decoding message]: {e}")
|
|
47
|
+
|
|
48
|
+
async def send_message(self, message: str):
|
|
49
|
+
"""Send a message to the BLE device."""
|
|
50
|
+
if not self.client or not self.client.is_connected:
|
|
51
|
+
raise RuntimeError("Not connected")
|
|
52
|
+
|
|
53
|
+
data = message.encode("utf-8")
|
|
54
|
+
# Send data to the RX characteristic (host to device)
|
|
55
|
+
await self.client.write_gatt_char(NUS_RX_UUID, data, response=True)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import asyncio
|
|
3
|
+
from unittest.mock import AsyncMock, patch, MagicMock
|
|
4
|
+
from bluechat_lite.core import scan_devices, BlueChatLiteClient, NUS_RX_UUID, NUS_TX_UUID
|
|
5
|
+
|
|
6
|
+
@pytest.mark.asyncio
|
|
7
|
+
async def test_scan_devices():
|
|
8
|
+
"""Test device scanning mock."""
|
|
9
|
+
mock_device = MagicMock()
|
|
10
|
+
mock_device.address = "00:11:22:33:44:55"
|
|
11
|
+
mock_device.name = "Test BLE Device"
|
|
12
|
+
|
|
13
|
+
with patch('bluechat_lite.core.BleakScanner.discover', new_callable=AsyncMock) as mock_discover:
|
|
14
|
+
mock_discover.return_value = [mock_device]
|
|
15
|
+
|
|
16
|
+
devices = await scan_devices(timeout=1.0)
|
|
17
|
+
|
|
18
|
+
mock_discover.assert_called_once_with(timeout=1.0)
|
|
19
|
+
assert len(devices) == 1
|
|
20
|
+
assert devices[0].address == "00:11:22:33:44:55"
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_client_connect_and_disconnect():
|
|
24
|
+
"""Test connecting and disconnecting."""
|
|
25
|
+
with patch('bluechat_lite.core.BleakClient') as MockClient:
|
|
26
|
+
mock_client_instance = MockClient.return_value
|
|
27
|
+
mock_client_instance.connect = AsyncMock()
|
|
28
|
+
mock_client_instance.start_notify = AsyncMock()
|
|
29
|
+
mock_client_instance.stop_notify = AsyncMock()
|
|
30
|
+
mock_client_instance.disconnect = AsyncMock()
|
|
31
|
+
mock_client_instance.is_connected = True
|
|
32
|
+
|
|
33
|
+
client = BlueChatLiteClient("00:11:22:33:44:55")
|
|
34
|
+
|
|
35
|
+
await client.connect()
|
|
36
|
+
mock_client_instance.connect.assert_called_once()
|
|
37
|
+
mock_client_instance.start_notify.assert_called_once_with(NUS_TX_UUID, client._notification_handler)
|
|
38
|
+
|
|
39
|
+
await client.disconnect()
|
|
40
|
+
mock_client_instance.stop_notify.assert_called_once_with(NUS_TX_UUID)
|
|
41
|
+
mock_client_instance.disconnect.assert_called_once()
|
|
42
|
+
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_client_send_and_receive_messages():
|
|
45
|
+
"""Test sending a message and receiving a notification."""
|
|
46
|
+
with patch('bluechat_lite.core.BleakClient') as MockClient:
|
|
47
|
+
mock_client_instance = MockClient.return_value
|
|
48
|
+
mock_client_instance.connect = AsyncMock()
|
|
49
|
+
mock_client_instance.start_notify = AsyncMock()
|
|
50
|
+
mock_client_instance.write_gatt_char = AsyncMock()
|
|
51
|
+
mock_client_instance.is_connected = True
|
|
52
|
+
|
|
53
|
+
client = BlueChatLiteClient("00:11:22:33:44:55")
|
|
54
|
+
|
|
55
|
+
received_messages = []
|
|
56
|
+
def handler(msg):
|
|
57
|
+
received_messages.append(msg)
|
|
58
|
+
|
|
59
|
+
client.on_message(handler)
|
|
60
|
+
|
|
61
|
+
await client.connect()
|
|
62
|
+
|
|
63
|
+
# Test receiving a message
|
|
64
|
+
client._notification_handler(123, b"Hello World")
|
|
65
|
+
assert len(received_messages) == 1
|
|
66
|
+
assert received_messages[0] == "Hello World"
|
|
67
|
+
|
|
68
|
+
# Test sending a message
|
|
69
|
+
await client.send_message("Hello back")
|
|
70
|
+
mock_client_instance.write_gatt_char.assert_called_once_with(NUS_RX_UUID, b"Hello back", response=True)
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_client_send_disconnected_raises_error():
|
|
74
|
+
"""Test sending when disconnected raises error."""
|
|
75
|
+
client = BlueChatLiteClient("00:11:22:33:44:55")
|
|
76
|
+
# Client is not connected
|
|
77
|
+
|
|
78
|
+
with pytest.raises(RuntimeError, match="Not connected"):
|
|
79
|
+
await client.send_message("Failed message")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Ensure we are in the project root
|
|
5
|
+
cd "$(dirname "$0")"
|
|
6
|
+
|
|
7
|
+
echo "Activating virtual environment if it exists..."
|
|
8
|
+
if [ -d "venv" ]; then
|
|
9
|
+
source venv/bin/activate
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
echo "Installing build and twine..."
|
|
13
|
+
pip install --upgrade build twine
|
|
14
|
+
|
|
15
|
+
echo "Cleaning up previous builds..."
|
|
16
|
+
rm -rf dist/
|
|
17
|
+
|
|
18
|
+
echo "Building the package..."
|
|
19
|
+
python3 -m build
|
|
20
|
+
|
|
21
|
+
echo "Uploading to PyPI..."
|
|
22
|
+
echo "You will be prompted for your PyPI API token (username: __token__)"
|
|
23
|
+
python3 -m twine upload dist/*
|
|
24
|
+
|
|
25
|
+
echo "Done!"
|