Skip to content

Commit

Permalink
upgrade to bleak 0.14.1, fix #3
Browse files Browse the repository at this point in the history
  • Loading branch information
ph4r05 committed Jan 19, 2022
1 parent ef46f00 commit fbfdbec
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 29 deletions.
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,16 @@ Controller enables to control the belt via CLI shell.

Start controller:
```bash
# Note: use module notation to run the script, no direct script invocation.
python -m ph4_walkingpad.main --stats 750 --json-file ~/walking.json
```

Or alternatively, if package was installed with pip:

```bash
ph4-walkingpad-ctl --stats 750 --json-file ~/walking.json
```

The command asks for periodic statistics fetching at 750 ms, storing records to `~/walking.json`.

Output
Expand Down Expand Up @@ -89,6 +96,21 @@ Due to nature of the BluetoothLE callbacks being executed on the main thread we
so the shell CLI does not support auto-complete, ctrl-r, up-arrow for the last command, etc.
Readline does not have async support at the moment.

### OSX Troubleshooting

This project uses [Bleak Bluetooth library](https://github.com/hbldh/bleak).
It was reported that OSX 12+ changed Bluetooth scanning logic, so it is not possible to connect to a device without scanning Bluetooth first.
Moreover, it blocks for the whole timeout interval.

Thus when using on OSX 12+:
- do not use `-a` parameter
- if there are more WalkingPads scanned, use `--filter` and specify device address prefix
- to modify scanning timeout value use `--scan-timeout`

Minimal required version of Bleak is 0.14.1

Related resources: https://github.com/hbldh/bleak/issues/635, https://github.com/hbldh/bleak/pull/692

### Profile

If the `-p profile.json` argument is passed, profile of the person is loaded from the file, so the controller can count burned calories.
Expand All @@ -101,10 +123,20 @@ Units are in a metric system.
"age": 25,
"weight": 80,
"height": 1.80,
"token": "JWT-token"
"token": "JWT-token",
"did": "ff:ff:ff:ff:ff:ff",
"email": "your-account@gmail.com",
"password": "service-login-password",
"password_md5": "or md5hash of password, hexcoded, to avoid plaintext password in config"
}
```

- `did` is optional field, associates your records with pad MAC address when uploading to the service
- `email` and (`password` or `password_md5`) are optional. If filled, you can call `login` to generate a fresh JWT usable for service auth.

Note that once you use `login` command, other JWTs become invalid, e.g., on your phone.
If you want to use the service on both devices, login with mobile phone while logging output with `adb` and capture JWT from logs (works only for Android phones).

### Stats file

The following arguments enable data collection to a statistic file:
Expand Down Expand Up @@ -254,6 +286,9 @@ When logged by the application, it is printed out as array if bytes:
Meaning of some fields are not known (15) or the value space was not explored. `m[15]` could be for example heart rate
for those models measuring it.

#### Related work
Another reverse engineer of the protocol (under GPL, [tldr](https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3))): https://github.com/DorianRudolph/QWalkingPad/blob/master/Protocol.h

### Donate

Thanks for considering donation if you find this project useful:
Expand Down
45 changes: 33 additions & 12 deletions ph4_walkingpad/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ def __init__(self, *args, **kwargs):
self.calorie_acc = []
self.calorie_acc_net = []

def __del__(self):
self.submit_coro(self.disconnect())

async def disconnect(self):
logger.debug("Disconnecting coroutine")
if self.ctler:
Expand Down Expand Up @@ -125,23 +122,34 @@ async def scan_address(self):
return

address = self.args.address
if address and Scanner.is_darwin():
logger.warning('Specifying address does not work on OSX 12+. '
'If connection cannot be made, omit -a parameter')

if address:
return address

if not address or self.args.scan:
scanner = Scanner()
await scanner.scan()
await scanner.scan(timeout=self.args.scan_timeout)

if scanner.walking_belt_candidates:
logger.info("WalkingPad candidates: %s" % (scanner.walking_belt_candidates,))
candidates = scanner.walking_belt_candidates
logger.info("WalkingPad candidates: %s" % (candidates,))
if self.args.scan:
return
address = scanner.walking_belt_candidates[0].address
return address
return None

if self.args.address_filter:
candidates = [x for x in candidates if str(x.address).startswith(self.args.address_filter)]
return candidates[0] if candidates else None
return None

def init_stats_fetcher(self):
self.stats_loop = asyncio.new_event_loop()
self.stats_thread = threading.Thread(
target=self.looper, args=(self.stats_loop,)
)
self.stats_thread.setDaemon(True)
self.stats_thread.daemon = True
self.stats_thread.start()

def start_stats_fetching(self):
Expand Down Expand Up @@ -362,12 +370,18 @@ async def main(self):
except Exception as e:
logger.debug("Stats loading failed: %s" % (e,))

await self.work()
print(os.environ)
try:
await self.work()
except Exception as e:
logger.error('Exception in the main entry point: %s' % (e,), exc_info=e)
finally:
await self.disconnect()

def argparser(self):
parser = argparse.ArgumentParser(description='ph4 WalkingPad controller')

parser.add_argument('--debug', dest='debug', action='store_const', const=True,
parser.add_argument('-d', '--debug', dest='debug', action='store_const', const=True,
help='enables debug mode')
parser.add_argument('--no-bt', dest='no_bt', action='store_const', const=True,
help='Do not use Bluetooth, no belt interaction enabled')
Expand All @@ -384,7 +398,11 @@ def argparser(self):
parser.add_argument('-p', '--profile', dest='profile',
help='Profile JSON file')
parser.add_argument('-a', '--address', dest='address',
help='Walking pad address (if none, scanner is used)')
help='Walking pad address (if none, scanner is used). OSX 12 have to scan first, do not use')
parser.add_argument('--filter', dest='address_filter',
help='Walking pad address filter, if scanning and multiple devices are found')
parser.add_argument('--scan-timeout', dest='scan_timeout', type=float, default=3.0,
help='Scan timeout in seconds, double')
return parser

async def stop_belt(self, to_standby=False):
Expand Down Expand Up @@ -614,6 +632,9 @@ def main():
br = WalkingPadControl()
loop.run_until_complete(br.main())

# Alternatively
# asyncio.run(br.main())


if __name__ == '__main__':
main()
Expand Down
45 changes: 32 additions & 13 deletions ph4_walkingpad/pad.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import binascii
import logging
import time
import platform

import bleak
from bleak import discover
Expand All @@ -19,22 +20,39 @@


class Scanner:
UUIDS = [
"0000180a-0000-1000-8000-00805f9b34fb",
"00010203-0405-0607-0809-0a0b0c0d1912",
"0000fe00-0000-1000-8000-00805f9b34fb",
]

BLEAK_KWARGS = {
"service_uuids": UUIDS
}

def __init__(self):
self.devices_dict = {}
self.devices_list = []
self.receive_data = []
self.walking_belt_candidates = [] # type: list[BLEDevice]

async def scan(self):
@staticmethod
def is_darwin():
try:
return platform.system().lower() == 'darwin'
except:
return False

@staticmethod
def get_bleak_kwargs():
return Scanner.BLEAK_KWARGS if Scanner.is_darwin() else {}

async def scan(self, timeout=3.0):
kwargs = Scanner.get_bleak_kwargs()
logger.info("Scanning for peripherals...")
scanner_kwargs = {'filters': {"UUIDs": [
"0000180a-0000-1000-8000-00805f9b34fb",
"00010203-0405-0607-0809-0a0b0c0d1912",
"0000fe00-0000-1000-8000-00805f9b34fb",
], "DuplicateData": False}}
scanner = bleak.BleakScanner()

dev = await scanner.discover(timeout=10.0)
logger.debug("Scanning kwargs: %s" % (kwargs,))
scanner = bleak.BleakScanner(**kwargs)
dev = await scanner.discover(timeout=timeout, **kwargs)
for i in range(len(dev)):
# Print the devices discovered
info_str = ', '.join(["[%2d]" % i, str(dev[i].address), str(dev[i].name), str(dev[i].metadata["uuids"])])
Expand Down Expand Up @@ -253,9 +271,10 @@ async def connect(self, address=None):
if not address:
raise ValueError('No address given to connect to')

self.client = BleakClient(address)
logger.info("Connecting")
return await self.client.connect(timeout=10.0)
logger.info("Connecting to %s" % (address,))
kwargs = Scanner.get_bleak_kwargs()
self.client = BleakClient(address, **kwargs)
return await self.client.connect(timeout=10.0, **kwargs)

async def send_cmd(self, cmd):
self.fix_crc(cmd)
Expand Down Expand Up @@ -338,7 +357,7 @@ async def run(self, address=None):
await self.connect(address)
client = self.client

x = await client.is_connected()
x = client.is_connected
logger.info("Connected: {0}".format(x))

self.char_fe01 = None
Expand Down
4 changes: 2 additions & 2 deletions profile-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"height": 1.80,
"did": "ff:ff:ff:ff:ff:ff",
"token": "JWT-token",
"email": "your-email@email.com",
"email": "your-account@email.com",
"password": "service-login-password",
"password_md5": "or use md5 password hash"
"password_md5": "or md5hash of password, hexcoded, to avoid plaintext password in config"
}
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
'future',
'asyncio',
'coloredlogs',
'bleak',
'bleak>=0.14.1',
'ph4-acmd2==0.0.5',
'blessed',
'requests',
Expand Down

0 comments on commit fbfdbec

Please sign in to comment.