python-igps-ble 0.1.0__py3-none-any.whl
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.
- igps_ble/__init__.py +7 -0
- igps_ble/__main__.py +46 -0
- igps_ble/client.py +69 -0
- igps_ble/const.py +26 -0
- igps_ble/models.py +23 -0
- igps_ble/parser.py +21 -0
- igps_ble/scanner.py +32 -0
- python_igps_ble-0.1.0.dist-info/METADATA +78 -0
- python_igps_ble-0.1.0.dist-info/RECORD +12 -0
- python_igps_ble-0.1.0.dist-info/WHEEL +5 -0
- python_igps_ble-0.1.0.dist-info/entry_points.txt +2 -0
- python_igps_ble-0.1.0.dist-info/top_level.txt +1 -0
igps_ble/__init__.py
ADDED
igps_ble/__main__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""CLI: `igps-ble scan` e `igps-ble info <ADDRESS>`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
|
|
8
|
+
from .client import IGPSClient
|
|
9
|
+
from .scanner import discover_devices
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def _scan(timeout: float) -> None:
|
|
13
|
+
devices = await discover_devices(timeout=timeout)
|
|
14
|
+
if not devices:
|
|
15
|
+
print("Nenhum iGPSPORT encontrado. Ligue o aparelho e aproxime.")
|
|
16
|
+
return
|
|
17
|
+
for d in devices:
|
|
18
|
+
print(f"{d.address} rssi={d.rssi} name={d.name}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _info(address: str) -> None:
|
|
22
|
+
state = await IGPSClient(address).async_read_state()
|
|
23
|
+
print(f"Endereço: {state.address}")
|
|
24
|
+
print(f"Modelo: {state.model}")
|
|
25
|
+
print(f"Firmware: {state.firmware}")
|
|
26
|
+
print(f"Fabricante: {state.manufacturer}")
|
|
27
|
+
print(f"Bateria: {state.battery_level}%")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
parser = argparse.ArgumentParser(prog="igps-ble")
|
|
32
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
33
|
+
p_scan = sub.add_parser("scan", help="Procura aparelhos iGPSPORT por perto")
|
|
34
|
+
p_scan.add_argument("--timeout", type=float, default=10.0)
|
|
35
|
+
p_info = sub.add_parser("info", help="Conecta e lê bateria/device-info")
|
|
36
|
+
p_info.add_argument("address")
|
|
37
|
+
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
if args.cmd == "scan":
|
|
40
|
+
asyncio.run(_scan(args.timeout))
|
|
41
|
+
elif args.cmd == "info":
|
|
42
|
+
asyncio.run(_info(args.address))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
igps_ble/client.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Client async que conecta no iGS10S e lê as characteristics em escopo.
|
|
2
|
+
|
|
3
|
+
Único módulo que abre conexão BLE. Aceita um BleakClient já criado (ou um
|
|
4
|
+
BLEDevice) — útil pra reaproveitar a conexão do bleak-retry-connector no HA.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from bleak import BleakClient
|
|
10
|
+
|
|
11
|
+
from .const import (
|
|
12
|
+
BATTERY_LEVEL_UUID,
|
|
13
|
+
FIRMWARE_REVISION_UUID,
|
|
14
|
+
HARDWARE_REVISION_UUID,
|
|
15
|
+
MANUFACTURER_NAME_UUID,
|
|
16
|
+
MODEL_NUMBER_UUID,
|
|
17
|
+
SOFTWARE_REVISION_UUID,
|
|
18
|
+
)
|
|
19
|
+
from .models import IGPSDeviceState
|
|
20
|
+
from .parser import decode_device_string, parse_battery_level
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IGPSClient:
|
|
24
|
+
"""Lê presença/bateria/device-info de um iGS10S via BLE."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, address: str, name: str = "iGS10S", rssi: int | None = None
|
|
28
|
+
) -> None:
|
|
29
|
+
self._address = address
|
|
30
|
+
self._name = name
|
|
31
|
+
self._rssi = rssi
|
|
32
|
+
|
|
33
|
+
async def async_read_state(self) -> IGPSDeviceState:
|
|
34
|
+
"""Conecta, lê o que o aparelho expõe, e devolve o snapshot."""
|
|
35
|
+
async with BleakClient(self._address) as client:
|
|
36
|
+
battery = await self._read(client, BATTERY_LEVEL_UUID)
|
|
37
|
+
model = await self._read(client, MODEL_NUMBER_UUID)
|
|
38
|
+
# O iGS10S expõe o firmware na Software Revision (0x2A28); outros
|
|
39
|
+
# aparelhos usam a Firmware Revision (0x2A26). Tentamos as duas.
|
|
40
|
+
firmware = await self._read(client, SOFTWARE_REVISION_UUID)
|
|
41
|
+
if firmware is None:
|
|
42
|
+
firmware = await self._read(client, FIRMWARE_REVISION_UUID)
|
|
43
|
+
hardware = await self._read(client, HARDWARE_REVISION_UUID)
|
|
44
|
+
manufacturer = await self._read(client, MANUFACTURER_NAME_UUID)
|
|
45
|
+
|
|
46
|
+
return IGPSDeviceState(
|
|
47
|
+
address=self._address,
|
|
48
|
+
name=self._name,
|
|
49
|
+
rssi=self._rssi,
|
|
50
|
+
battery_level=parse_battery_level(battery) if battery is not None else None,
|
|
51
|
+
model=decode_device_string(model) if model is not None else None,
|
|
52
|
+
firmware=decode_device_string(firmware) if firmware is not None else None,
|
|
53
|
+
hardware=decode_device_string(hardware) if hardware is not None else None,
|
|
54
|
+
manufacturer=(
|
|
55
|
+
decode_device_string(manufacturer)
|
|
56
|
+
if manufacturer is not None
|
|
57
|
+
else None
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
async def _read(client: BleakClient, uuid: str) -> bytes | None:
|
|
63
|
+
"""Lê a char se ela existir; None se o aparelho não a expõe."""
|
|
64
|
+
if client.services.get_characteristic(uuid) is None:
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
return await client.read_gatt_char(uuid)
|
|
68
|
+
except Exception: # noqa: BLE001 - char some/erra => trata como ausente
|
|
69
|
+
return None
|
igps_ble/const.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""UUIDs Bluetooth SIG e defaults do iGPSPORT iGS10S.
|
|
2
|
+
|
|
3
|
+
UUIDs confirmados no recon (docs/RECON_FINDINGS.md). Ajuste NAME_PREFIXES
|
|
4
|
+
para o local_name exato observado na Task 1.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Battery Service / characteristic (Bluetooth SIG).
|
|
8
|
+
BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb"
|
|
9
|
+
BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb"
|
|
10
|
+
|
|
11
|
+
# Device Information Service / characteristics (Bluetooth SIG).
|
|
12
|
+
DEVICE_INFO_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb"
|
|
13
|
+
MODEL_NUMBER_UUID = "00002a24-0000-1000-8000-00805f9b34fb"
|
|
14
|
+
FIRMWARE_REVISION_UUID = "00002a26-0000-1000-8000-00805f9b34fb"
|
|
15
|
+
HARDWARE_REVISION_UUID = "00002a27-0000-1000-8000-00805f9b34fb"
|
|
16
|
+
# O iGS10S reporta o firmware na Software Revision (0x2A28), não na
|
|
17
|
+
# Firmware Revision (0x2A26, ausente). Confirmado no recon 2026-05-31.
|
|
18
|
+
SOFTWARE_REVISION_UUID = "00002a28-0000-1000-8000-00805f9b34fb"
|
|
19
|
+
MANUFACTURER_NAME_UUID = "00002a29-0000-1000-8000-00805f9b34fb"
|
|
20
|
+
|
|
21
|
+
# Prefixos de local_name usados pra reconhecer o aparelho no advertisement.
|
|
22
|
+
# Confirme/ajuste com o que a Task 1 observou.
|
|
23
|
+
NAME_PREFIXES = ("iGS10S", "iGPSPORT", "iGS")
|
|
24
|
+
|
|
25
|
+
MANUFACTURER = "iGPSPORT"
|
|
26
|
+
DEFAULT_MODEL = "iGS10S"
|
igps_ble/models.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Modelo imutável do estado lido do iGS10S."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class IGPSDeviceState:
|
|
10
|
+
"""Snapshot do que a lib conseguiu ler do aparelho.
|
|
11
|
+
|
|
12
|
+
Campos opcionais ficam None quando o aparelho não expõe a characteristic
|
|
13
|
+
correspondente (ex.: sem Battery Service => battery_level None).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
address: str
|
|
17
|
+
name: str
|
|
18
|
+
rssi: int | None = None
|
|
19
|
+
battery_level: int | None = None
|
|
20
|
+
model: str | None = None
|
|
21
|
+
firmware: str | None = None
|
|
22
|
+
hardware: str | None = None
|
|
23
|
+
manufacturer: str | None = None
|
igps_ble/parser.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Parsers puros (sem I/O) para os bytes das characteristics SIG."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_battery_level(data: bytes) -> int | None:
|
|
7
|
+
"""Battery Level (0x2A19): 1 byte uint8, 0..100. None se ausente/inválido."""
|
|
8
|
+
if not data:
|
|
9
|
+
return None
|
|
10
|
+
value = data[0]
|
|
11
|
+
if value > 100:
|
|
12
|
+
return None
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def decode_device_string(data: bytes) -> str | None:
|
|
17
|
+
"""Device Information string UTF-8; remove NUL de padding e espaços."""
|
|
18
|
+
if not data:
|
|
19
|
+
return None
|
|
20
|
+
text = data.decode("utf-8", errors="replace").replace("\x00", "").strip()
|
|
21
|
+
return text or None
|
igps_ble/scanner.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Descoberta BLE do iGS10S (toca o adaptador Bluetooth)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from bleak import BleakScanner
|
|
6
|
+
|
|
7
|
+
from .const import NAME_PREFIXES
|
|
8
|
+
from .models import IGPSDeviceState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_igps_device(local_name: str | None) -> bool:
|
|
12
|
+
"""True se o nome de advertisement bate com um prefixo conhecido do iGPSPORT."""
|
|
13
|
+
if not local_name:
|
|
14
|
+
return False
|
|
15
|
+
return any(local_name.startswith(prefix) for prefix in NAME_PREFIXES)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def discover_devices(timeout: float = 10.0) -> list[IGPSDeviceState]:
|
|
19
|
+
"""Escaneia e devolve um IGPSDeviceState por aparelho iGPSPORT visto.
|
|
20
|
+
|
|
21
|
+
Não conecta — campos de bateria/device-info ficam None aqui.
|
|
22
|
+
"""
|
|
23
|
+
discovered = await BleakScanner.discover(timeout=timeout, return_adv=True)
|
|
24
|
+
results: list[IGPSDeviceState] = []
|
|
25
|
+
for address, (device, adv) in discovered.items():
|
|
26
|
+
name = adv.local_name or device.name
|
|
27
|
+
if not is_igps_device(name):
|
|
28
|
+
continue
|
|
29
|
+
results.append(
|
|
30
|
+
IGPSDeviceState(address=address, name=name, rssi=adv.rssi)
|
|
31
|
+
)
|
|
32
|
+
return results
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-igps-ble
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python BLE library for the iGPSPORT iGS10S cycling computer (presence, battery, device info)
|
|
5
|
+
Author-email: Hudson Brendon <contato.hudsonbrendon@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hudsonbrendon/python-igps-ble
|
|
8
|
+
Project-URL: Issues, https://github.com/hudsonbrendon/python-igps-ble/issues
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: bleak>=0.22
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# python-igps-ble
|
|
18
|
+
|
|
19
|
+
Pure-Python BLE library for the **iGPSPORT iGS10S** cycling computer.
|
|
20
|
+
|
|
21
|
+
Reads, over standard Bluetooth Low Energy, what the device exposes to
|
|
22
|
+
third parties: **presence**, **battery level**, and **device info**
|
|
23
|
+
(model / firmware / manufacturer). The proprietary `.fit` sync used by the
|
|
24
|
+
official iGPSPORT app is **out of scope** (see `ha-igps-ble/docs/PROTOCOL.md`).
|
|
25
|
+
|
|
26
|
+
> Built to back the [`ha-igps-ble`](https://github.com/hudsonbrendon/ha-igps-ble)
|
|
27
|
+
> Home Assistant integration, but usable standalone.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install python-igps-ble
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## CLI
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
igps-ble scan # procura aparelhos iGPSPORT por perto
|
|
39
|
+
igps-ble info AA:BB:CC:DD:EE:FF # bateria + device-info
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Uso programático
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import asyncio
|
|
46
|
+
from igps_ble import IGPSDeviceState
|
|
47
|
+
from igps_ble.scanner import discover_devices
|
|
48
|
+
from igps_ble.client import IGPSClient
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
for dev in await discover_devices(timeout=10):
|
|
52
|
+
state: IGPSDeviceState = await IGPSClient(dev.address, dev.name).async_read_state()
|
|
53
|
+
print(state)
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Escopo
|
|
59
|
+
|
|
60
|
+
| Dado | Suportado | Fonte |
|
|
61
|
+
|------|-----------|-------|
|
|
62
|
+
| Presença | ✅ | advertisement BLE |
|
|
63
|
+
| Bateria % | ✅* | Battery Service `0x180F` |
|
|
64
|
+
| Modelo / firmware | ✅* | Device Information `0x180A` |
|
|
65
|
+
| Velocidade / distância / pedalada | ❌ | sync proprietário (não exposto por BLE) |
|
|
66
|
+
|
|
67
|
+
\* depende de o iGS10S expor o serviço SIG correspondente — confirmado por recon.
|
|
68
|
+
|
|
69
|
+
## Desenvolvimento
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
73
|
+
.venv/bin/pytest
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Licença
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
igps_ble/__init__.py,sha256=eeV-l5_0sdRkAEPeB9uUUE4G2dZ-rCq1T6y1UJhDWbk,178
|
|
2
|
+
igps_ble/__main__.py,sha256=mCoZbDckv8FDd613fWJE0pTaCVwv2fG3Snn2V6KhWGY,1414
|
|
3
|
+
igps_ble/client.py,sha256=f66p39IP5g-UVkOn30JACZrjOOPuQs5IGJ6_7g665gg,2728
|
|
4
|
+
igps_ble/const.py,sha256=0PfoY4CCFypAJ4bEKyBpFLDZufB6aRZKd04IFdI35sM,1161
|
|
5
|
+
igps_ble/models.py,sha256=H6UNnnMfVovc0EsqYHyC0oIPPvnJ99i8Vi9y_SyWL-I,620
|
|
6
|
+
igps_ble/parser.py,sha256=IkY_fpODaJkXwSilkA5JzXks1VEP5MsoHjf4aOr9JtE,627
|
|
7
|
+
igps_ble/scanner.py,sha256=aJj3bE_K9ky9fJKicKLrRoWf1YSeXZRRsGghpzBauP4,1085
|
|
8
|
+
python_igps_ble-0.1.0.dist-info/METADATA,sha256=QX_XkHtYvVZV8Dg7aNWzuIYMCo7P9bvi8z1aV-QHjcs,2253
|
|
9
|
+
python_igps_ble-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
python_igps_ble-0.1.0.dist-info/entry_points.txt,sha256=WmO3pSZ-qD2ZfHtnxxqyvUycW8LEwMFIJIC1k7oFSuY,52
|
|
11
|
+
python_igps_ble-0.1.0.dist-info/top_level.txt,sha256=69O33W2-0wC37bi3yvYxMJqOVdk612_CqJYgxErMS8A,9
|
|
12
|
+
python_igps_ble-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
igps_ble
|