Skip to content

Commit 0eb052f

Browse files
authored
Merge pull request #80 from Jakeler/tcp-socket
TCP socket server option
2 parents cd17d97 + efc717b commit 0eb052f

File tree

4 files changed

+140
-16
lines changed

4 files changed

+140
-16
lines changed

README.md

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,13 @@ As you can see, here the read/notify UUID is `00000006-af0e-4c28-95a4-4509fd91e0
183183
The `ble-serial` tool itself has a few more options:
184184
```console
185185
$ ble_serial -h
186-
usage: __main__.py [-h] [-v] -d DEVICE [-t {public,random}] [-i ADAPTER] [-m MTU] [-w WRITE_UUID] [-l FILENAME] [-b]
187-
[-p PORT] [-r READ_UUID]
186+
usage: __main__.py [-h] [-v] [-t SEC] [-i ADAPTER] [-m MTU] [-d DEVICE] [-a {public,random}] [-s SERVICE_UUID] [-w WRITE_UUID] [-r READ_UUID] [--permit {ro,rw,wo}] [-l FILENAME] [-b] [-p PORT] [--expose-tcp-host TCP_HOST] [--expose-tcp-port TCP_PORT]
188187

189188
Create virtual serial ports from BLE devices.
190189

191190
options:
192191
-h, --help show this help message and exit
193192
-v, --verbose Increase verbosity, can be specified multiple times for connection/DBus debugging (default: 0)
194-
-p PORT, --port PORT Symlink to virtual serial port (default: /tmp/ttyBLE)
195193

196194
connection parameters:
197195
-t SEC, --timeout SEC
@@ -212,13 +210,8 @@ device parameters:
212210
-r READ_UUID, --read-uuid READ_UUID
213211
The GATT characteristic to subscribe to notifications to read the serial data (default: None)
214212
--permit {ro,rw,wo} Restrict transfer direction on bluetooth: read only (ro), read+write (rw), write only (wo) (default: rw)
215-
216-
logging options:
217-
-l FILENAME, --log FILENAME
218-
Enable optional logging of all bluetooth traffic to file (default: None)
219-
-b, --binary Log data as raw binary, disable transformation to hex. Works only in combination with -l (default: False)
220-
221213
```
214+
222215
In any case it needs to know which device to connect, the simple and most reliable way to specify this is by device address/id:
223216
```console
224217
$ ble-serial -d 20:91:48:4c:4c:54
@@ -232,7 +225,11 @@ $ ble-serial -d 20:91:48:4c:4c:54
232225

233226
```
234227
This log shows a successful start on Linux, the virtual serial port was opened on `/dev/pts/8`, the number at the end changes, depending on how many pseudo terminals are already open on the system. It uses the same mechanism on macOS, just the path is slightly different, in the format `/dev/ttys000`.
235-
In addition it automatically creates a symlink to `/tmp/ttyBLE`, so you can easily access it always on the same file, the default can be changed with the `-p`/`--port` option.
228+
In addition it automatically creates a symlink to `/tmp/ttyBLE`, so you can easily access it always on the same file, the default can be changed with:
229+
```
230+
serial port parameters:
231+
-p PORT, --port PORT Symlink to virtual serial port (default: /tmp/ttyBLE)
232+
```
236233

237234
On Windows it uses the port pair created in the setup described above, this does not dynamically change and endpoint is always `COM9` if you use the default script.
238235

@@ -273,6 +270,13 @@ This works also the other way around with `wo` = write only.
273270

274271
### Logging
275272
There is an option to log all traffic on the link to a text file:
273+
```
274+
logging options:
275+
-l FILENAME, --log FILENAME
276+
Enable optional logging of all bluetooth traffic to file (default: None)
277+
-b, --binary Log data as raw binary, disable transformation to hex. Works only in combination with -l (default: False)
278+
```
279+
276280
```console
277281
$ ble-serial -d 20:91:48:4c:4c:54 -l demo.txt
278282
...
@@ -281,7 +285,7 @@ $ cat demo.txt
281285
2019-12-09 21:15:53.491681 -> BLE-IN: b0 b0 b0 b0 b0 b0 3b b0 b0 b0 ba b0 0d 8a
282286
2019-12-09 21:15:53.999795 -> BLE-IN: b0 b0 b0 b0 b0 b0 3b b0 b0 b0 ba b0 0d 8a
283287
```
284-
Per default it is transformed to hex bytes, use `-b`/`--binary` to log raw data.
288+
Per default it is transformed to hex bytes, use `-b`/`--binary` to log raw data, useful if your input is already ASCII etc.
285289

286290
You can use `-v` to increase the log verbosity to DEBUG:
287291
```console
@@ -306,10 +310,49 @@ It also helps with figuring out how characteristics are selected:
306310
Always try the verbose option if something is not working properly.
307311

308312
## Advanced Usage
313+
### TCP socket server
314+
Instead of the serial port emulation there is a also builtin raw tcp server since version 2.7:
315+
```
316+
network options:
317+
--expose-tcp-host TCP_HOST
318+
Network interface for the server listen on (default: 127.0.0.1)
319+
--expose-tcp-port TCP_PORT
320+
Port to listen on, disables local serial port and enables TCP server if specified (default: None)
321+
```
322+
This is only activated if TCP_PORT is set. Also it removes the dependency to com0com or other drivers.
323+
324+
The server is listening on localhost per default, therefore only reachable from apps running on the same machine. Other interfaces or `0.0.0.0` for all interfaces can be specified with TCP_HOST.
325+
Security is to consider though, this is plain TCP without encryption or authentication. Only recommended on a separate local network, otherwise stay with the default/localhost.
326+
327+
Note: this is limited to one concurrent connection, it will reject all connection attempts if there is already a client connected and emit a warning, example:
328+
```console
329+
$ ble-serial -d 20:91:48:4C:4C:54 --expose-tcp-port 4002 -v
330+
[...]
331+
19:30:11.650 | INFO | main.py: Running main loop!
332+
19:30:11.650 | INFO | tcp_socket.py: TCP server started
333+
19:30:11.650 | DEBUG | tcp_socket.py: Listening on <asyncio.TransportSocket fd=8, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('127.0.0.1', 4002)>
334+
[...]
335+
19:30:13.618 | INFO | tcp_socket.py: New TCP peer connected: ('127.0.0.1', 49104)
336+
19:30:13.787 | DEBUG | ble_interface.py: Received notify from 0000ffe1-0000-1000-8000-00805f9b34fb (Handle: 17): Vendor specific: bytearray(b'\xb0\xb0\xb0\xb01\xb6;\xb0\xb0\xb0\xba\xb0\r\x8a')
337+
19:30:13.787 | DEBUG | tcp_socket.py: Sending: bytearray(b'\xb0\xb0\xb0\xb01\xb6;\xb0\xb0\xb0\xba\xb0\r\x8a')
338+
[...]
339+
19:30:26.726 | INFO | tcp_socket.py: New TCP peer connected: ('127.0.0.1', 56172)
340+
19:30:26.726 | WARNING | tcp_socket.py: More than one connection is not allowed, closing
341+
```
342+
343+
Now there a various ways to connect to it.
344+
#### Linux and macOS
345+
- Very simple option: `netcat 127.0.0.1 4002` or `telnet 127.0.0.1 4002`
346+
- More powerful: `socat -dd tcp:localhost:4002 -`, can forward data to many modules, not only stdin/stdout.
347+
- Custom apps are easy to make with tcp too
348+
#### Windows
349+
- Graphical: Putty, just put in the IP+port and select Other - Raw as connection type.
350+
- Terminal: netcat/telnet/socat can be installed separately
351+
309352
### Multi device connection
310353
It is possible to connect several devices to a host simultaneously. Limiting factor is only the bluetooth baseband layer, which uses a Active Member Address (AMA, 3 bit). From these 8 possible values address zero is always occupied by the host, so it can be connected to (up to) 7 devices at the same time.
311354

312-
There is no special mode, ble-serial can be just stated multiple times with different parameters. Following are some tips, showing how to do this in practice.
355+
There is no special mode, ble-serial can be just started multiple times with different parameters. Following are some tips, showing how to do this in practice.
313356

314357
#### Linux and macOS
315358
Common shells (bash, zsh, fish) have a useful background job feature. Add the `-p` (port) option to make sure every instance has a unique path. Also you probably want to redirect the log output to keep it separate. Resulting command lines could look like:

ble_serial/cli.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ def parse_args():
77

88
parser.add_argument('-v', '--verbose', dest='verbose', action='count', default=0,
99
help='Increase verbosity, can be specified multiple times for connection/DBus debugging')
10-
parser.add_argument('-p', '--port', dest='port', required=False, default=DEFAULT_PORT,
11-
help=DEFAULT_PORT_MSG)
12-
1310

1411
con_group = parser.add_argument_group('connection parameters')
1512
con_group.add_argument('-t', '--timeout', dest='timeout', required=False, default=5.0, type=float, metavar='SEC',
@@ -40,6 +37,16 @@ def parse_args():
4037
log_group.add_argument('-b', '--binary', dest='binlog', required=False, action='store_true',
4138
help='Log data as raw binary, disable transformation to hex. Works only in combination with -l')
4239

40+
uart_group = parser.add_argument_group('serial port parameters')
41+
uart_group.add_argument('-p', '--port', dest='port', required=False, default=DEFAULT_PORT,
42+
help=DEFAULT_PORT_MSG)
43+
44+
net_group = parser.add_argument_group('network options')
45+
net_group.add_argument('--expose-tcp-host', dest='tcp_host', required=False, default='127.0.0.1',
46+
help='Network interface for the server listen on')
47+
net_group.add_argument('--expose-tcp-port', dest='tcp_port', required=False, default=None, type=int,
48+
help='Port to listen on, disables local serial port and enables TCP server if specified')
49+
4350
args = parser.parse_args()
4451

4552
if not args.device and not args.service_uuid:

ble_serial/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging, asyncio
22
from bleak.exc import BleakError
33
from ble_serial import platform_uart as UART
4+
from ble_serial.ports.tcp_socket import TCP_Socket
45
from ble_serial.bluetooth.ble_interface import BLE_interface
56
from ble_serial.log.fs_log import FS_log, Direction
67
from ble_serial.log.console_log import setup_logger
@@ -24,8 +25,13 @@ async def _run(self):
2425
loop = asyncio.get_event_loop()
2526
loop.set_exception_handler(self.excp_handler)
2627
try:
27-
self.uart = UART(args.port, loop, args.mtu)
28+
if args.tcp_port:
29+
self.uart = TCP_Socket(args.tcp_host, args.tcp_port, args.mtu)
30+
else:
31+
self.uart = UART(args.port, loop, args.mtu)
32+
2833
self.bt = BLE_interface(args.adapter, args.service_uuid)
34+
2935
if args.filename:
3036
self.log = FS_log(args.filename, args.binlog)
3137
self.bt.set_receiver(self.log.middleware(Direction.BLE_IN, self.uart.queue_write))

ble_serial/ports/tcp_socket.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from ble_serial.ports.interface import ISerial
2+
import asyncio, logging
3+
4+
5+
class TCP_Socket(ISerial):
6+
def __init__(self, host: str, port: int, mtu: int):
7+
logging.debug(f'{host=}:{port=}, {mtu=}')
8+
self.host = host
9+
self.port = port
10+
self.mtu = mtu
11+
self.connected = False
12+
13+
def set_receiver(self, callback):
14+
self._cb = callback
15+
16+
def queue_write(self, value: bytes):
17+
if self.connected:
18+
logging.debug(f'Sending: {value}')
19+
self.writer.write(value)
20+
else:
21+
logging.debug(f'No client connected, dropping data...')
22+
23+
def handle_connect(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
24+
logging.info(f'New TCP peer connected: {writer.get_extra_info("peername")}')
25+
if self.connected:
26+
logging.warning('More than one connection is not allowed, closing')
27+
writer.close()
28+
return
29+
self.writer = writer
30+
self.reader = reader
31+
self.connected = True
32+
33+
34+
async def run_loop(self):
35+
server = await asyncio.start_server(
36+
self.handle_connect, self.host, self.port)
37+
38+
logging.info('TCP server started' if server.is_serving() else 'TCP server failed')
39+
for sock in server.sockets:
40+
logging.debug(f'Listening on {sock}')
41+
42+
while True:
43+
if self.connected:
44+
try:
45+
data = await self.reader.read(self.mtu)
46+
except OSError as ose:
47+
logging.warning(f'Client disconnected: {ose}')
48+
self.connected = False
49+
continue
50+
51+
if len(data) > 0:
52+
logging.debug(f'Received {data}')
53+
self._cb(data)
54+
else:
55+
logging.warning('Client disconnected (EOF)')
56+
self.connected = False
57+
else:
58+
await asyncio.sleep(3)
59+
logging.debug('Waiting for client connection')
60+
61+
def start(self):
62+
pass
63+
64+
def stop_loop(self):
65+
pass
66+
67+
def remove(self):
68+
pass

0 commit comments

Comments
 (0)