Skip to content

Commit 06e4ffd

Browse files
committed
Add Python tool for testing
1 parent da61af9 commit 06e4ffd

File tree

2 files changed

+163
-3
lines changed

2 files changed

+163
-3
lines changed

src/esp32/esp32_bt_gatts.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,11 +336,13 @@ int esp32_bt_gatts_event(const struct ble_gap_event *ev, void *arg) {
336336
break;
337337
}
338338
ce->gc.conn_id = conn_id;
339-
ce->gc.mtu = ble_att_mtu(conn_id);
339+
//ce->gc.mtu = ble_att_mtu(conn_id);
340+
ce->gc.mtu = 1024; // mtu;
340341
esp32_bt_addr_to_mgos(&cd.peer_ota_addr, &ce->gc.addr);
341342
SLIST_INIT(&ce->pending_nm);
342343
STAILQ_INIT(&ce->pending_inds);
343344
SLIST_INSERT_HEAD(&s_conns, ce, next);
345+
esp32_bt_gatts_create_sessions(ce);
344346
ble_gattc_exchange_mtu(conn_id, NULL, NULL);
345347
break;
346348
}
@@ -411,7 +413,6 @@ int esp32_bt_gatts_event(const struct ble_gap_event *ev, void *arg) {
411413
mgos_bt_addr_to_str(&ce->gc.addr, MGOS_BT_ADDR_STRINGIFY_TYPE, buf1),
412414
mtu));
413415
ce->gc.mtu = 1024; // mtu;
414-
esp32_bt_gatts_create_sessions(ce);
415416
break;
416417
}
417418
case BLE_GAP_EVENT_SUBSCRIBE: {
@@ -540,7 +541,7 @@ static int esp32_gatts_attr_access_cb(uint16_t conn_handle,
540541
struct esp32_bt_gatts_session_entry *sse =
541542
find_session(conn_handle, attr_handle, &ai);
542543
const ble_uuid_t *uuid = NULL;
543-
LOG(LL_DEBUG, ("GATTS ATTR OP %d sse %p", ctxt->op, sse));
544+
LOG(LL_DEBUG, ("GATTS ATTR %d OP %d sse %p", attr_handle, ctxt->op, sse));
544545
if (sse == NULL) return BLE_ATT_ERR_UNLIKELY;
545546
switch (ctxt->op) {
546547
case BLE_GATT_ACCESS_OP_READ_CHR:

tools/rpc.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2021 Deomid "rojer" Ryabkov
4+
# All rights reserved
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
import argparse
19+
import binascii
20+
import logging
21+
import json
22+
import random
23+
import struct
24+
import time
25+
import sys
26+
27+
from bluepy import btle
28+
29+
30+
class ScanDelegate(btle.DefaultDelegate):
31+
32+
def handleDiscovery(self, data, isNewDev, isNewData):
33+
if data.updateCount > 1:
34+
return
35+
name = data.getValueText(9) # Complete Local Name
36+
if not name:
37+
name = data.getValueText(8) # Shortened Local Name
38+
if not name:
39+
name = '?'
40+
print(data.addr, data.rssi, name)
41+
42+
43+
class ResolveDelegate(btle.DefaultDelegate):
44+
_name: str
45+
_addr: str
46+
_addrType: int
47+
_scanner: btle.Scanner
48+
49+
def __init__(self, name: str):
50+
super().__init__()
51+
self._name = name
52+
self._addr = None
53+
self._addrType = 0
54+
self._scanner = None
55+
56+
def handleDiscovery(self, data, isNewDev, isNewData):
57+
name = data.getValueText(9) # Complete Local Name
58+
if not name:
59+
name = data.getValueText(8) # Shortened Local Name
60+
logging.debug(f"{data.addr} {data.rssi} {name}")
61+
if name == self._name:
62+
self._addr = data.addr
63+
self._addrType = data.addrType
64+
65+
def getAddr(self):
66+
return (self._addr, self._addrType)
67+
68+
69+
class Device(btle.Peripheral):
70+
71+
def __init__(self, addr, addrType):
72+
super().__init__(addr, addrType=btle.ADDR_TYPE_PUBLIC)
73+
# See https://github.com/mongoose-os-libs/rpc-gatts#attribute-description
74+
svc = self.getServiceByUUID("5f6d4f53-5f52-5043-5f53-56435f49445f")
75+
self._data_char, self._tx_ctl_char, self._rx_ctl_char = None, None, None
76+
for char in svc.getCharacteristics():
77+
if char.uuid == "5f6d4f53-5f52-5043-5f64-6174615f5f5f":
78+
self._data_char = char
79+
elif char.uuid == "5f6d4f53-5f52-5043-5f74-785f63746c5f":
80+
self._tx_ctl_char = char
81+
elif char.uuid == "5f6d4f53-5f52-5043-5f72-785f63746c5f":
82+
self._rx_ctl_char = char
83+
if not (self._data_char and self._tx_ctl_char and self._rx_ctl_char):
84+
raise TypeError("invalid service")
85+
86+
def call(self, method, params=None, resp=True):
87+
req = {
88+
"method": method,
89+
"params": params or {},
90+
}
91+
if resp:
92+
req["id"] = random.randint(1, 1000000000)
93+
reqJSON = json.dumps(req)
94+
reqLen = len(reqJSON)
95+
time.sleep(10)
96+
logging.debug(f"Request: {reqJSON}")
97+
logging.info("Writing length ({reqLen})...")
98+
self._tx_ctl_char.write(struct.pack(">I", reqLen), withResponse=True)
99+
logging.debug(f"Writing data...")
100+
self._data_char.write(reqJSON.encode("ascii"), withResponse=True)
101+
102+
def unlock(self):
103+
self.writeCharacteristic(0x0047, bytes([0, 0, 0, 0]))
104+
v3d = self.readCharacteristic(0x003d)
105+
106+
107+
def main():
108+
parser = argparse.ArgumentParser()
109+
parser.add_argument("-v", "--v", type=int, default=logging.INFO, help="Verbosity level")
110+
sp = parser.add_subparsers(title="Actions", dest="action", required=True)
111+
# scan
112+
scan_parser = sp.add_parser("scan", help="Scan for devices")
113+
scan_parser.add_argument("-t", "--time", type=float, default=10.0, help="Scan for this long")
114+
# call
115+
call_parser = sp.add_parser("call", help="Invoke an RPC method")
116+
call_parser.add_argument("--addr_type", type=str, default="public", help="Address type, public or random")
117+
call_parser.add_argument("target", action="store", help="Name or MAC address of the device")
118+
call_parser.add_argument("method", action="store", help="Method to invoke")
119+
call_parser.add_argument("params", action="store", nargs="?", help="Call parameters, JSON object")
120+
args = parser.parse_args()
121+
122+
logging.basicConfig(level=args.v, format="[%(asctime)s %(levelno)d] %(message)s", datefmt="%Y/%m/%d %H:%M:%S")
123+
124+
if args.action == "scan":
125+
sd = ScanDelegate()
126+
scanner = btle.Scanner().withDelegate(sd)
127+
devices = scanner.scan(args.time)
128+
return
129+
elif args.action == "call":
130+
addrType = args.addr_type
131+
if len(args.target.split(":")) == 6:
132+
addr = args.target
133+
elif len(args.target.split("-")) == 6:
134+
addr = ":".join(args.target.split("-"))
135+
else:
136+
logging.info(f"Resolving {args.target}...")
137+
rd = ResolveDelegate(args.target)
138+
scanner = btle.Scanner().withDelegate(rd)
139+
scanner.clear()
140+
scanner.start()
141+
start = time.time()
142+
while not rd.getAddr()[0] and time.time() - start < 5:
143+
scanner.process(timeout=0.5)
144+
scanner.stop()
145+
addr, addrType = rd.getAddr()
146+
if not addr:
147+
logging.error(f"Could not resolve {args.target}")
148+
sys.exit(1)
149+
logging.info(f"Connecting to {addr}...")
150+
dev = None
151+
try:
152+
dev = Device(addr, addrType)
153+
dev.call(args.method, params=args.params, resp=True)
154+
finally:
155+
if dev:
156+
dev.disconnect()
157+
158+
if __name__ == "__main__":
159+
main()

0 commit comments

Comments
 (0)