sensor-sdk 0.3.2__cp313-cp313-win_amd64.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.
- sensor/__init__.py +24 -0
- sensor/bin_recorder.cp313-win_amd64.pyd +0 -0
- sensor/bin_to_csv.cp313-win_amd64.pyd +0 -0
- sensor/bleak_bumble/__init__.py +144 -0
- sensor/bleak_bumble/client.cp313-win_amd64.pyd +0 -0
- sensor/bleak_bumble/scanner.cp313-win_amd64.pyd +0 -0
- sensor/bleak_bumble/utils.cp313-win_amd64.pyd +0 -0
- sensor/bleak_host.cp313-win_amd64.pyd +0 -0
- sensor/bleak_process.cp313-win_amd64.pyd +0 -0
- sensor/bumble_dongle.cp313-win_amd64.pyd +0 -0
- sensor/fb/DataType.cp313-win_amd64.pyd +0 -0
- sensor/fb/Sample.cp313-win_amd64.pyd +0 -0
- sensor/fb/SensorData.cp313-win_amd64.pyd +0 -0
- sensor/fb/__init__.py +0 -0
- sensor/gforce.cp313-win_amd64.pyd +0 -0
- sensor/sdk_log.cp313-win_amd64.pyd +0 -0
- sensor/sensor_controller.cp313-win_amd64.pyd +0 -0
- sensor/sensor_data.cp313-win_amd64.pyd +0 -0
- sensor/sensor_data_context.cp313-win_amd64.pyd +0 -0
- sensor/sensor_data_pool.cp313-win_amd64.pyd +0 -0
- sensor/sensor_device.cp313-win_amd64.pyd +0 -0
- sensor/sensor_profile.cp313-win_amd64.pyd +0 -0
- sensor/sensor_utils.cp313-win_amd64.pyd +0 -0
- sensor/winrt_high_throughput.cp313-win_amd64.pyd +0 -0
- sensor_sdk-0.3.2.dist-info/METADATA +530 -0
- sensor_sdk-0.3.2.dist-info/RECORD +29 -0
- sensor_sdk-0.3.2.dist-info/WHEEL +5 -0
- sensor_sdk-0.3.2.dist-info/licenses/LICENSE.txt +21 -0
- sensor_sdk-0.3.2.dist-info/top_level.txt +1 -0
sensor/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from sensor.sensor_controller import SensorController, SensorControllerInstance
|
|
2
|
+
from sensor.sensor_profile import SensorProfile
|
|
3
|
+
from sensor.sensor_device import BLEDevice, DeviceInfo, DeviceStateEx
|
|
4
|
+
from sensor.sensor_data import DataType, Sample, SensorData
|
|
5
|
+
from sensor.winrt_high_throughput import apply as _apply_winrt_high_throughput_patch
|
|
6
|
+
|
|
7
|
+
# SDK 版本号(单一数据源,setup.py 打包时从这里读取)
|
|
8
|
+
__version__ = "0.3.2"
|
|
9
|
+
|
|
10
|
+
# Windows 下尝试给 bleak WinRT backend 打高吞吐率连接参数补丁
|
|
11
|
+
_apply_winrt_high_throughput_patch()
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"SensorController",
|
|
15
|
+
"SensorControllerInstance",
|
|
16
|
+
"SensorProfile",
|
|
17
|
+
"BLEDevice",
|
|
18
|
+
"DeviceInfo",
|
|
19
|
+
"DeviceStateEx",
|
|
20
|
+
"DataType",
|
|
21
|
+
"Sample",
|
|
22
|
+
"SensorData",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2024 Victor Chavez <vchavezb@protonmail.com>
|
|
3
|
+
"""Bumble backend.
|
|
4
|
+
|
|
5
|
+
本目录为 ekspla/bleak-bumble_dev_host_mode(v0.0.7, MIT)的 vendored 副本,
|
|
6
|
+
上游:https://github.com/ekspla/bleak-bumble_dev_host_mode
|
|
7
|
+
仅修改了包内 import 路径(bleak_bumble → sensor.bleak_bumble),许可证见 LICENSE。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Dict, Final, Optional
|
|
13
|
+
|
|
14
|
+
from bumble.controller import Controller
|
|
15
|
+
from bumble.link import LocalLink
|
|
16
|
+
from bumble.transport import Transport, open_transport
|
|
17
|
+
|
|
18
|
+
transports: Dict[str, Transport] = {}
|
|
19
|
+
_link: Final = LocalLink()
|
|
20
|
+
_scheme_delimiter: Final = ":"
|
|
21
|
+
|
|
22
|
+
_env_transport_cfg: Final = os.getenv("BLEAK_BUMBLE")
|
|
23
|
+
_env_host_mode: Final = os.getenv("BLEAK_BUMBLE_HOST")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TransportScheme(Enum):
|
|
27
|
+
"""The transport schemes supported by bumble.
|
|
28
|
+
|
|
29
|
+
https://google.github.io/bumble/transports
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
SERIAL = "serial"
|
|
33
|
+
""": The serial transport implements sending/receiving HCI
|
|
34
|
+
packets over a UART (a.k.a serial port).
|
|
35
|
+
"""
|
|
36
|
+
UDP = "udp"
|
|
37
|
+
""": The UDP transport is a UDP socket, receiving packets on a specified port number,
|
|
38
|
+
and sending packets to a specified host and port number.
|
|
39
|
+
"""
|
|
40
|
+
TCP_CLIENT = "tcp-client"
|
|
41
|
+
""": The TCP Client transport uses an outgoing TCP connection.
|
|
42
|
+
"""
|
|
43
|
+
TCP_SERVER = "tcp-server"
|
|
44
|
+
""": The TCP Server transport uses an incoming TCP connection.
|
|
45
|
+
"""
|
|
46
|
+
WS_CLIENT = "ws-client"
|
|
47
|
+
""": The WebSocket Client transport is WebSocket connection
|
|
48
|
+
to a WebSocket server over which HCI packets are sent and received.
|
|
49
|
+
"""
|
|
50
|
+
WS_SERVER = "ws-server"
|
|
51
|
+
""": The WebSocket Server transport is WebSocket server that accepts
|
|
52
|
+
connections from a WebSocket client. HCI packets are sent and received over the connection.
|
|
53
|
+
"""
|
|
54
|
+
PTY = "pty"
|
|
55
|
+
""": The PTY transport uses a Unix pseudo-terminal device to communicate
|
|
56
|
+
with another process on the host, as if it were over a serial port.
|
|
57
|
+
"""
|
|
58
|
+
FILE = "file"
|
|
59
|
+
""": The File transport allows opening any named entry on a filesystem
|
|
60
|
+
and use it for HCI transport I/O. This is typically used to open a PTY,
|
|
61
|
+
or unix driver, not for real files.
|
|
62
|
+
"""
|
|
63
|
+
VHCI = "vhci"
|
|
64
|
+
""": The VHCI transport allows attaching a virtual controller
|
|
65
|
+
to the Bluetooth stack on operating systems that offer a
|
|
66
|
+
VHCI driver (Linux, if enabled, maybe others).
|
|
67
|
+
"""
|
|
68
|
+
HCI_SOCKET = "hci-socket"
|
|
69
|
+
""": An HCI Socket can send/receive HCI packets to/from a
|
|
70
|
+
Bluetooth HCI controller managed by the host OS.
|
|
71
|
+
This is only supported on some platforms (currently only tested on Linux).
|
|
72
|
+
"""
|
|
73
|
+
USB = "usb"
|
|
74
|
+
""": The USB transport interfaces with a local Bluetooth USB dongle.
|
|
75
|
+
"""
|
|
76
|
+
ANDROID_NETSIM = "android-netsim"
|
|
77
|
+
""": The Android "netsim" transport either connects, as a host, to a
|
|
78
|
+
Netsim virtual controller ("host" mode), or acts as a virtual
|
|
79
|
+
controller itself ("controller" mode) accepting host connections.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_string(cls, value: str) -> "TransportScheme":
|
|
84
|
+
try:
|
|
85
|
+
return cls(value)
|
|
86
|
+
except ValueError:
|
|
87
|
+
raise ValueError(f"'{value}' is not a valid TransportScheme")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BumbleTransportCfg:
|
|
91
|
+
"""Transport configuration for bumble.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
scheme (TransportScheme): The transport scheme supported by bumble.
|
|
95
|
+
args (Optional[str]): The arguments used to initialize the transport.
|
|
96
|
+
See https://google.github.io/bumble/transports/index.html
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, scheme: TransportScheme, args: Optional[str] = None):
|
|
100
|
+
self.scheme: Final = scheme
|
|
101
|
+
self.args: Final = args
|
|
102
|
+
|
|
103
|
+
def __str__(self):
|
|
104
|
+
return f"{self.scheme.value}:{self.args}" if self.args else self.scheme.value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_default_transport_cfg() -> BumbleTransportCfg:
|
|
108
|
+
if _env_transport_cfg:
|
|
109
|
+
scheme_val, *args = _env_transport_cfg.split(_scheme_delimiter, 1)
|
|
110
|
+
return BumbleTransportCfg(
|
|
111
|
+
TransportScheme.from_string(scheme_val), args[0] if args else None
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return BumbleTransportCfg(TransportScheme.TCP_SERVER, "127.0.0.1:1234")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def is_host_mode_enabled_from_env() -> bool:
|
|
118
|
+
"""Check if host mode is enabled via BLEAK_BUMBLE_HOST environment variable.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
bool: True if BLEAK_BUMBLE_HOST environment variable is set to any non-empty value.
|
|
122
|
+
"""
|
|
123
|
+
return True if _env_host_mode else False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def start_transport(
|
|
127
|
+
cfg: BumbleTransportCfg, host_mode: bool = is_host_mode_enabled_from_env()
|
|
128
|
+
) -> Transport:
|
|
129
|
+
transport_cmd = str(cfg)
|
|
130
|
+
if transport_cmd not in transports.keys():
|
|
131
|
+
transports[transport_cmd] = await open_transport(transport_cmd)
|
|
132
|
+
if not host_mode:
|
|
133
|
+
Controller(
|
|
134
|
+
"ext",
|
|
135
|
+
host_source=transports[transport_cmd].source,
|
|
136
|
+
host_sink=transports[transport_cmd].sink,
|
|
137
|
+
link=_link,
|
|
138
|
+
)
|
|
139
|
+
return transports[transport_cmd]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_link():
|
|
143
|
+
# Assume all transports are linked
|
|
144
|
+
return _link
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
sensor/fb/__init__.py
ADDED
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sensor-sdk
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Python sdk for Synchroni
|
|
5
|
+
Home-page: https://github.com/oymotion/SynchroniSDKPython
|
|
6
|
+
Author: Martin Ye
|
|
7
|
+
Author-email: yecq_82@hotmail.com
|
|
8
|
+
Requires-Python: >=3.10.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: setuptools
|
|
13
|
+
Requires-Dist: bleak>=3.0.2
|
|
14
|
+
Requires-Dist: flatbuffers>=25.0.0
|
|
15
|
+
Requires-Dist: bumble==0.0.233
|
|
16
|
+
Requires-Dist: libusb1
|
|
17
|
+
Requires-Dist: libusb-package
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# sensor-sdk
|
|
29
|
+
|
|
30
|
+
Synchroni sdk for Python
|
|
31
|
+
|
|
32
|
+
## Brief
|
|
33
|
+
|
|
34
|
+
Synchroni SDK is the software development kit for developers to access Synchroni products.
|
|
35
|
+
|
|
36
|
+
## Contributing
|
|
37
|
+
|
|
38
|
+
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
pip install sensor-sdk
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### USB Bluetooth dongle (bumble backend)
|
|
53
|
+
|
|
54
|
+
On macOS, when a USB Bluetooth HCI dongle is plugged in (e.g. Actions `10d7:b012` or `33fa:0012`; several dongles can be used at once, each serving one connection), the SDK automatically drives it directly with a [bumble](https://github.com/google/bumble) host-mode backend instead of CoreBluetooth — no Bluetooth permission prompt, works even when the internal Bluetooth is off, and allows larger ATT MTU. The [bleak-bumble](https://github.com/ekspla/bleak-bumble_dev_host_mode) backend is vendored into the SDK (`sensor/bleak_bumble/`, MIT license), and the bumble stack + libusb are declared as regular dependencies — a plain `pip install sensor-sdk` is all that is needed.
|
|
55
|
+
|
|
56
|
+
Behavior and controls:
|
|
57
|
+
|
|
58
|
+
- With the dependencies installed and a dongle present, the backend is selected automatically on macOS when the SDK starts (native `bleak` is used otherwise). Check the active backend with `SensorController.getBLEBackendName()` (`"bumble"` / `"bleak"`).
|
|
59
|
+
- `SENSOR_SDK_BLE_BACKEND=bleak` forces the native backend; `SENSOR_SDK_BLE_BACKEND=bumble` forces the dongle backend on any platform; `SENSOR_SDK_BUMBLE_TRANSPORT` overrides the bumble transport spec (e.g. `usb:0`).
|
|
60
|
+
- USB access needs no sudo on macOS as long as the OS has not claimed the dongle (it normally doesn't when the internal Bluetooth works).
|
|
61
|
+
- With the bumble backend, each dongle serves one connection at a time: scanning uses a free dongle and is skipped while all dongles are connected; connections fail fast when no dongle is free. Connection timeout is raised to 25s automatically.
|
|
62
|
+
|
|
63
|
+
## 1. Permission
|
|
64
|
+
|
|
65
|
+
Application will obtain bluetooth permission by itself.
|
|
66
|
+
|
|
67
|
+
## 2. Import SDK
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from sensor import *
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## SensorController methods
|
|
74
|
+
|
|
75
|
+
### 1. Initalize
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
SensorControllerInstance = SensorController()
|
|
79
|
+
|
|
80
|
+
# register scan listener
|
|
81
|
+
if not SensorControllerInstance.hasDeviceFoundCallback:
|
|
82
|
+
def on_device_callback(deviceList: List[BLEDevice]):
|
|
83
|
+
# return all devices doesn't connected
|
|
84
|
+
pass
|
|
85
|
+
SensorControllerInstance.onDeviceFoundCallback = on_device_callback
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. Start scan
|
|
89
|
+
|
|
90
|
+
Use `def startScan(period_in_ms: int) -> bool` to start scan
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
success = SensorControllerInstance.startScan(6000)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
returns true if start scan success, periodInMS means onDeviceCallback will be called every periodInMS
|
|
97
|
+
|
|
98
|
+
Use `def scan(period_in_ms: int) -> list[BLEDevice]` to scan once time
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
bleDevices = SensorControllerInstance.scan(6000)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3. Stop scan
|
|
105
|
+
|
|
106
|
+
Use `def stopScan() -> None` to stop scan
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
SensorControllerInstance.stopScan()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 4. Check scaning
|
|
113
|
+
|
|
114
|
+
Use `property isScanning: bool` to check scanning status
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
isScanning = SensorControllerInstance.isScanning
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 5. Check if bluetooth is enabled
|
|
121
|
+
|
|
122
|
+
Use `property isEnable: bool` to check if bluetooth is enable
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
isEnable = SensorControllerInstance.isEnable
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 6. Create SensorProfile
|
|
129
|
+
|
|
130
|
+
Use `def requireSensor(device: BLEDevice) -> SensorProfile | None` to create SensorProfile.
|
|
131
|
+
|
|
132
|
+
If bleDevice is invalid, result is None.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
sensorProfile = SensorControllerInstance.requireSensor(bleDevice)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 7. Get SensorProfile
|
|
139
|
+
|
|
140
|
+
Use `def getSensor(deviceMac: str) -> SensorProfile | None` to get SensorProfile.
|
|
141
|
+
|
|
142
|
+
If SensorProfile didn't created, result is None.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
sensorProfile = SensorControllerInstance.getSensor(bleDevice.Address)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 8. Get Connected SensorProfiles
|
|
149
|
+
|
|
150
|
+
Use `def getConnectedSensors() -> list[SensorProfile]` to get connected SensorProfiles.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
sensorProfiles = SensorControllerInstance.getConnectedSensors()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 9. Get Connected BLE Devices
|
|
157
|
+
|
|
158
|
+
Use `def getConnectedDevices() -> list[BLEDevice]` to get connected BLE Devices.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
bleDevices = SensorControllerInstance.getConnectedDevices()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 10. Terminate
|
|
165
|
+
|
|
166
|
+
Use `def terminate()` to terminate sdk
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
|
|
170
|
+
def terminate():
|
|
171
|
+
SensorControllerInstance.terminate()
|
|
172
|
+
exit()
|
|
173
|
+
|
|
174
|
+
def main():
|
|
175
|
+
signal.signal(signal.SIGINT, lambda signal, frame: terminate())
|
|
176
|
+
time.sleep(30)
|
|
177
|
+
SensorControllerInstance.terminate()
|
|
178
|
+
|
|
179
|
+
Please MAKE SURE to call terminate when exit main() or press Ctrl+C
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## SensorProfile methods
|
|
183
|
+
|
|
184
|
+
### 11. Initalize
|
|
185
|
+
|
|
186
|
+
Please register callbacks for SensorProfile
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
sensorProfile = SensorControllerInstance.requireSensor(bleDevice)
|
|
190
|
+
|
|
191
|
+
# register callbacks
|
|
192
|
+
def on_state_changed(sensor, newState):
|
|
193
|
+
# please do logic when device disconnected unexpected
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
def on_error_callback(sensor, reason):
|
|
197
|
+
# called when error occurs
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
def on_power_changed(sensor, power):
|
|
201
|
+
# callback for get battery level of device, power from 0 - 100, -1 is invalid
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
def on_data_callback(sensor, data):
|
|
205
|
+
# called after start data transfer
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
sensorProfile.onStateChanged = on_state_changed
|
|
209
|
+
sensorProfile.onErrorCallback = on_error_callback
|
|
210
|
+
sensorProfile.onPowerChanged = on_power_changed
|
|
211
|
+
sensorProfile.onDataCallback = on_data_callback
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 12. Connect device
|
|
215
|
+
|
|
216
|
+
Use `def connect() -> bool` to connect.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
success = sensorProfile.connect()
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 13. Disconnect
|
|
223
|
+
|
|
224
|
+
Use `def disconnect() -> bool` to disconnect.
|
|
225
|
+
|
|
226
|
+
If data notification is currently active, `disconnect()` will automatically stop it first before closing the BLE connection.
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
success = sensorProfile.disconnect()
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### 14. Get device status
|
|
233
|
+
|
|
234
|
+
Use `property deviceState: DeviceStateEx` to get device status.
|
|
235
|
+
|
|
236
|
+
Please send command in 'Ready' state, should be after connect() return True.
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
deviceStateEx = sensorProfile.deviceState
|
|
240
|
+
|
|
241
|
+
# deviceStateEx has define:
|
|
242
|
+
# class DeviceStateEx(Enum):
|
|
243
|
+
# Disconnected = 0
|
|
244
|
+
# Connecting = 1
|
|
245
|
+
# Connected = 2
|
|
246
|
+
# Ready = 3
|
|
247
|
+
# Disconnecting = 4
|
|
248
|
+
# Invalid = 5
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 15. Get BLE device of SensorProfile
|
|
252
|
+
|
|
253
|
+
Use `property BLEDevice: BLEDevice` to get BLE device of SensorProfile.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
bleDevice = sensorProfile.BLEDevice
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 16. Get device info of SensorProfile
|
|
260
|
+
|
|
261
|
+
Use `def getDeviceInfo() -> DeviceInfo | None` to get device info of SensorProfile.
|
|
262
|
+
|
|
263
|
+
Please call after device in 'Ready' state and `init()` has succeeded, returns None otherwise.
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
deviceInfo = sensorProfile.getDeviceInfo()
|
|
267
|
+
|
|
268
|
+
# deviceInfo is a DeviceInfo object with attributes:
|
|
269
|
+
# DeviceName, ModelName, HardwareVersion, FirmwareVersion, MTUSize
|
|
270
|
+
# plus a ChannelCount / SampleRate attribute pair for each modality:
|
|
271
|
+
# Ppg, Spo2, Impe, Emg, Eeg, Ecg, Acc, Gyro, Brth, MagAngle, Euler, Quat
|
|
272
|
+
# e.g. deviceInfo.EmgChannelCount, deviceInfo.EegSampleRate
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### 17. Init data transfer
|
|
276
|
+
|
|
277
|
+
Use `def init(packageSampleCount: int, powerRefreshInterval: int) -> bool`.
|
|
278
|
+
|
|
279
|
+
Please call after device in 'Ready' state, return True if init succeed.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
success = sensorProfile.init(5, 60*1000)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
packageSampleCount: set sample counts of SensorData.channelSamples in onDataCallback()
|
|
286
|
+
powerRefreshInterval: callback period for onPowerChanged()
|
|
287
|
+
|
|
288
|
+
### 18. Check if init data transfer succeed
|
|
289
|
+
|
|
290
|
+
Use `property hasInited: bool` to check if init data transfer succeed.
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
hasInited = sensorProfile.hasInited
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 19. DataNotify
|
|
297
|
+
|
|
298
|
+
Use `def startDataNotification() -> bool` to start data notification.
|
|
299
|
+
|
|
300
|
+
Please call if hasInited return True
|
|
301
|
+
|
|
302
|
+
#### 19.1 Start data transfer
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
success = sensorProfile.startDataNotification()
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Data type list:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
class DataType(Enum):
|
|
312
|
+
NTF_ACC = 0x1 # unit is g
|
|
313
|
+
NTF_GYRO = 0x2 # unit is degree/s
|
|
314
|
+
NTF_EEG = 0x10 # unit is uV
|
|
315
|
+
NTF_ECG = 0x11 # unit is uV
|
|
316
|
+
NTF_BRTH = 0x15 # unit is uV
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Process data in onDataCallback.
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
def on_data_callback(sensor, data):
|
|
323
|
+
if data.dataType == DataType.NTF_EEG:
|
|
324
|
+
pass
|
|
325
|
+
elif data.dataType == DataType.NTF_ECG:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
# process data as you wish
|
|
329
|
+
for oneChannelSamples in data.channelSamples:
|
|
330
|
+
for sample in oneChannelSamples:
|
|
331
|
+
if sample.isLost:
|
|
332
|
+
# do some logic
|
|
333
|
+
pass
|
|
334
|
+
else:
|
|
335
|
+
# draw with sample.data & sample.channelIndex
|
|
336
|
+
# print(f"{sample.channelIndex} | {sample.sampleIndex} | {sample.data} | {sample.impedance}")
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
sensorProfile.onDataCallback = on_data_callback
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### 19.2 Stop data transfer
|
|
343
|
+
|
|
344
|
+
Use `def stopDataNotification() -> bool` to stop data transfer.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
success = sensorProfile.stopDataNotification()
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### 19.3 Check if it's data transfering
|
|
351
|
+
|
|
352
|
+
Use `property isDataTransfering: bool` to check if it's data transfering.
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
isDataTransfering = sensorProfile.isDataTransfering
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 20. Get battery level
|
|
359
|
+
|
|
360
|
+
Use `def getBatteryLevel() -> int` to get battery level. Please call after device in 'Ready' state.
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
batteryPower = sensorProfile.getBatteryLevel()
|
|
364
|
+
|
|
365
|
+
# batteryPower is battery level returned, value ranges from 0 to 100, 0 means out of battery, while 100 means full.
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Please check console.py in examples directory
|
|
369
|
+
|
|
370
|
+
### Async methods
|
|
371
|
+
|
|
372
|
+
all methods start with async is async methods, they has same params and return result as sync methods.
|
|
373
|
+
|
|
374
|
+
Please check async_console.py in examples directory
|
|
375
|
+
|
|
376
|
+
### setParam method
|
|
377
|
+
|
|
378
|
+
Use `def setParam(self, key: str, value: str) -> str` to set parameter of sensor profile. Please call after device in 'Ready' state.
|
|
379
|
+
|
|
380
|
+
The asynchronous variant is `asyncSetParam(self, key: str, value: str) -> str`.
|
|
381
|
+
|
|
382
|
+
If the device is already streaming when you change an `NTF_*` or `FILTER_*` key, the SDK will stop and restart the data notification so the new setting takes effect immediately.
|
|
383
|
+
|
|
384
|
+
Below is available key and value:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
# Data stream toggles
|
|
388
|
+
result = sensorProfile.setParam("NTF_GEST", "ON")
|
|
389
|
+
result = sensorProfile.setParam("NTF_EMG", "ON")
|
|
390
|
+
result = sensorProfile.setParam("NTF_EEG", "ON")
|
|
391
|
+
result = sensorProfile.setParam("NTF_ECG", "ON")
|
|
392
|
+
result = sensorProfile.setParam("NTF_IMU", "ON")
|
|
393
|
+
result = sensorProfile.setParam("NTF_BRTH", "ON")
|
|
394
|
+
result = sensorProfile.setParam("NTF_IMPEDANCE", "ON")
|
|
395
|
+
result = sensorProfile.setParam("NTF_MAG_ANGLE", "ON")
|
|
396
|
+
result = sensorProfile.setParam("NTF_PPG_RAW", "ON")
|
|
397
|
+
result = sensorProfile.setParam("NTF_GFORCE_EULER", "ON")
|
|
398
|
+
result = sensorProfile.setParam("NTF_GFORCE_QUAT", "ON")
|
|
399
|
+
result = sensorProfile.setParam("NTF_GFORCE_ACC", "ON")
|
|
400
|
+
result = sensorProfile.setParam("NTF_GFORCE_GYRO", "ON")
|
|
401
|
+
# set data stream to ON or OFF, result is "OK" if succeed
|
|
402
|
+
# Note: on legacy (non-new) EMG devices, NTF_GEST and NTF_EMG are mutually exclusive.
|
|
403
|
+
|
|
404
|
+
# Firmware filter toggles
|
|
405
|
+
result = sensorProfile.setParam("FILTER_50HZ", "ON")
|
|
406
|
+
# set 50Hz notch filter to ON or OFF, result is "OK" if succeed
|
|
407
|
+
|
|
408
|
+
result = sensorProfile.setParam("FILTER_60HZ", "ON")
|
|
409
|
+
# set 60Hz notch filter to ON or OFF, result is "OK" if succeed
|
|
410
|
+
|
|
411
|
+
result = sensorProfile.setParam("FILTER_HPF", "ON")
|
|
412
|
+
# set 0.5Hz hpf filter to ON or OFF, result is "OK" if succeed
|
|
413
|
+
|
|
414
|
+
result = sensorProfile.setParam("FILTER_LPF", "ON")
|
|
415
|
+
# set 80Hz lpf filter to ON or OFF, result is "OK" if succeed
|
|
416
|
+
|
|
417
|
+
result = sensorProfile.setParam("DEBUG_BLE_DATA_PATH", "d:/temp/test.bin")
|
|
418
|
+
# set the bin export path: the session's raw BLE capture is recorded in the system
|
|
419
|
+
# temp directory and copied to this location on stopDataNotification / disconnect;
|
|
420
|
+
# "True" exports to {DeviceName}_data_YYYYMMDD_HHMMSS.bin in the SDK log directory,
|
|
421
|
+
# "False" or "" disables export (the temp bin is just deleted).
|
|
422
|
+
# please give an absolute path and make sure it is valid and writeable by yourself
|
|
423
|
+
|
|
424
|
+
result = sensorProfile.setParam("DEBUG_LOG_PATH", "True")
|
|
425
|
+
# enable SDK file logging to a default file ({DeviceName}_log_YYYYMMDD_HHMMSS.txt),
|
|
426
|
+
# or pass an absolute custom path instead of "True"; "False" or "" disables it.
|
|
427
|
+
# getParam("DEBUG_LOG_PATH") returns the current log file path ("" when disabled).
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
### getParam method
|
|
432
|
+
|
|
433
|
+
Use `def getParam(self, key: str) -> str` to query the current parameter state of a sensor profile. Please call after the device reaches the 'Ready' state.
|
|
434
|
+
|
|
435
|
+
The asynchronous variant is `asyncGetParam(self, key: str) -> str`.
|
|
436
|
+
|
|
437
|
+
Supported aggregate query keys:
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
result = sensorProfile.getParam("FILTER")
|
|
441
|
+
# Returns a pipe-separated string of all filter states, e.g.:
|
|
442
|
+
# "FILTER_50HZ|ON|FILTER_60HZ|ON|FILTER_HPF|ON|FILTER_LPF|ON"
|
|
443
|
+
|
|
444
|
+
result = sensorProfile.getParam("NTF")
|
|
445
|
+
# Returns a pipe-separated string of all notification states, e.g.:
|
|
446
|
+
# "NTF_BRTH|ON|NTF_ECG|ON|NTF_EEG|ON|NTF_EMG|ON|..."
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
If the key is not supported, the result starts with `"Error"`.
|
|
450
|
+
|
|
451
|
+
## Bin file recording and replay
|
|
452
|
+
|
|
453
|
+
On every successful connect, the SDK records all raw BLE packets of the session into a `.bin` file in the SDK log directory (`~/Documents/sensorsdklog` by default, or the directory of the configured log file), named `{DeviceName}_{MAC}_{YYYYMMDD_HHMMSS}.bin`. Recording is always on; it is skipped or stopped with a warning when free disk space is below 100MB or a disk error occurs, without affecting live streaming. Bin files can be replayed offline for debugging and packet-loss analysis.
|
|
454
|
+
|
|
455
|
+
### Get bin file info
|
|
456
|
+
|
|
457
|
+
Use `def getBinFileInfo(self, file_path: str) -> Optional[dict]` to read the config record of a bin file:
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
info = SensorControllerInstance.getBinFileInfo("path/to/session.bin")
|
|
461
|
+
# Returns a dict:
|
|
462
|
+
# device_mac, device_name, chip_type, is_universal_stream, feature_map,
|
|
463
|
+
# device_info, sensor_datas (per data-type parse config),
|
|
464
|
+
# replay_duration (recording seconds, written into the header record at close;
|
|
465
|
+
# older bins without a header fall back to a full-file estimate)
|
|
466
|
+
# Returns None if the file does not exist or has no config record.
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Replay a bin file
|
|
470
|
+
|
|
471
|
+
Use `def replayBinFile(self, file_path: str, sensor: Optional[SensorProfile] = None, realtime: bool = True, timeout: Optional[float] = None) -> Optional[SensorProfile]` to replay a bin file through the normal parsing pipeline. Parsed results arrive via `onDataCallback` on the returned SensorProfile, same as live data.
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
# Simplest form: controller creates the profile from the bin config record
|
|
475
|
+
sensor = SensorControllerInstance.replayBinFile("path/to/session.bin")
|
|
476
|
+
|
|
477
|
+
# Recommended: reuse an existing profile with callbacks registered
|
|
478
|
+
sensor = SensorControllerInstance.requireSensor(device)
|
|
479
|
+
sensor.onDataCallback = on_data
|
|
480
|
+
SensorControllerInstance.replayBinFile("path/to/session.bin", sensor, realtime=True)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
- `sensor`: an existing SensorProfile to replay through. When `None`, the controller creates (or reuses) a profile from the bin config record; in that mode the profile is returned only after replay finishes, so register callbacks on an existing profile to receive data.
|
|
484
|
+
- `realtime`: `True` replays at the recorded pace; `False` replays as fast as possible.
|
|
485
|
+
- `timeout`: seconds to wait for completion. `None` auto-estimates from the bin duration in realtime mode (duration + 30s, min 60s), or 600s otherwise. On timeout the call returns while replay may still be running in the background.
|
|
486
|
+
- Replay is rejected while the target sensor is streaming live data.
|
|
487
|
+
|
|
488
|
+
### Pause / resume / stop replay
|
|
489
|
+
|
|
490
|
+
```python
|
|
491
|
+
result = SensorControllerInstance.pauseBinReplay(sensor) # pause feeding
|
|
492
|
+
result = SensorControllerInstance.resumeBinReplay(sensor) # resume feeding
|
|
493
|
+
result = SensorControllerInstance.stopBinReplay(sensor) # abort; the blocking replayBinFile call returns
|
|
494
|
+
# Each returns "OK" on success or an error string otherwise.
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Parse a bin file to CSV
|
|
498
|
+
|
|
499
|
+
Use `def parseBinToCsv(self, bin_path: str, csv_path: Optional[str] = None) -> str` to convert a recorded bin file to CSV offline (parsing runs through the real pipeline; row timestamps come from the bin records):
|
|
500
|
+
|
|
501
|
+
```python
|
|
502
|
+
csv_path = SensorControllerInstance.parseBinToCsv("d:/temp/test.bin")
|
|
503
|
+
# or with an explicit output path:
|
|
504
|
+
csv_path = SensorControllerInstance.parseBinToCsv("d:/temp/test.bin", "d:/temp/test.csv")
|
|
505
|
+
# Returns the CSV file path. Columns:
|
|
506
|
+
# timestamp, mac, type, raw_hex, data_type, sample_rate, channel_count, lost_count, samples_info, first_sample
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Logging controls
|
|
510
|
+
|
|
511
|
+
File logging is disabled by default. Records emitted before file logging is first enabled are held in a bounded memory buffer and replayed into the file on first enable, so early (scan/connect) logs are not lost. The log path is automatically shared with the BLE subprocess, so both sides write to the same file.
|
|
512
|
+
|
|
513
|
+
```python
|
|
514
|
+
SensorControllerInstance.setDebugEnabled(True)
|
|
515
|
+
# enable/disable SDK debug logs
|
|
516
|
+
|
|
517
|
+
SensorControllerInstance.setLogPath("d:/temp/sdk.log")
|
|
518
|
+
# enable file logging to a custom path; pass "" to disable
|
|
519
|
+
|
|
520
|
+
SensorControllerInstance.enableFileLog(True)
|
|
521
|
+
# enable file logging with the default path
|
|
522
|
+
# (~/Documents/sensorsdklog/log_YYYYMMDD_HHMMSS.txt)
|
|
523
|
+
|
|
524
|
+
SensorControllerInstance.setControllerLogPath("d:/temp/controller.log")
|
|
525
|
+
# SensorController-dedicated log file; pass "" to disable
|
|
526
|
+
|
|
527
|
+
SensorControllerInstance.enableControllerLog(True)
|
|
528
|
+
# enable the controller-dedicated log with the default path
|
|
529
|
+
# (~/Documents/sensorsdklog/sensor_controller_log_YYYYMMDD_HHMMSS.txt)
|
|
530
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
sensor/__init__.py,sha256=c6CvPyKDVJwBVV5ffJh9a118hdLA-AZ9dANRZPO1yP0,793
|
|
2
|
+
sensor/bin_recorder.cp313-win_amd64.pyd,sha256=__0dFXSmR68tK_O97f_pXjmey2lunwMpODi-Vm42KME,188416
|
|
3
|
+
sensor/bin_to_csv.cp313-win_amd64.pyd,sha256=On4acP-jluaLbbBcLPqRzxcEP_8mSSXjah44Y0WQ4ac,104448
|
|
4
|
+
sensor/bleak_host.cp313-win_amd64.pyd,sha256=AEcPJVtLZPb30mOVfl7kiA0HZRHIk6-rJhvXAFSnaBs,189952
|
|
5
|
+
sensor/bleak_process.cp313-win_amd64.pyd,sha256=aVDzsYyrYmps18ndeWXKNCwf84tmG-ptO23VBVQargM,528384
|
|
6
|
+
sensor/bumble_dongle.cp313-win_amd64.pyd,sha256=SjUReCTJEscqBs3RVEIbtHKqQ0cqpEoUtiQVXhVj_Ro,184320
|
|
7
|
+
sensor/gforce.cp313-win_amd64.pyd,sha256=GqnhCaxWx2txku-Lvs_jeSKID7ngAF4B2xTEedTev30,466432
|
|
8
|
+
sensor/sdk_log.cp313-win_amd64.pyd,sha256=DsQeQNLxbEQfaGO2JPXf1eSNqMz6Fh_cu0otrFuuhkk,140800
|
|
9
|
+
sensor/sensor_controller.cp313-win_amd64.pyd,sha256=3qIQrp5LEbobUaPlqHW99PRNIwayQPK1Wgoxv7954ts,213504
|
|
10
|
+
sensor/sensor_data.cp313-win_amd64.pyd,sha256=q8VkqAhwQFhwA1Ka_K0Q8Gyi2LrZ8BdNyDnjgtcF_PM,82432
|
|
11
|
+
sensor/sensor_data_context.cp313-win_amd64.pyd,sha256=DquW9BpXeK9EjigdwHxgcAA2KeqWw0TZat8AAbWtmdg,573952
|
|
12
|
+
sensor/sensor_data_pool.cp313-win_amd64.pyd,sha256=LDT7JsyEPQfqQFNfcJWmHgHweAkdm7hKGBKVP2nh_RY,64000
|
|
13
|
+
sensor/sensor_device.cp313-win_amd64.pyd,sha256=mNu0WlZuxktbjBRS-bZdZivQ1rWGN0sZkn8MwfpO6AM,45568
|
|
14
|
+
sensor/sensor_profile.cp313-win_amd64.pyd,sha256=ehhxQ8ci8brz1VVKbZIPkm21JZ8H4s5KHM6-6ZS_Cfc,260096
|
|
15
|
+
sensor/sensor_utils.cp313-win_amd64.pyd,sha256=U5u94Sb_nFIJPdfdzapCI-2nTvyZpvZSRVC9RiBTZag,130048
|
|
16
|
+
sensor/winrt_high_throughput.cp313-win_amd64.pyd,sha256=qIO3FxfEb8qLPwOU96AR7upLOdIE0NwVcJeZRaV1avk,78848
|
|
17
|
+
sensor/bleak_bumble/__init__.py,sha256=OVjUkxlVNdf2PVO5R0NaBfMKzD6ZWj4FaRIXTwNBwak,5205
|
|
18
|
+
sensor/bleak_bumble/client.cp313-win_amd64.pyd,sha256=T3_7RRv2VHn1DyrdJmsHh0iq0QvCByjgDlxbluQ3cow,180736
|
|
19
|
+
sensor/bleak_bumble/scanner.cp313-win_amd64.pyd,sha256=bmIxxPe9IlfQTlEVsrT20MSQkQM5lWtXj_QvQUBaZRg,103936
|
|
20
|
+
sensor/bleak_bumble/utils.cp313-win_amd64.pyd,sha256=X1w9VOhBZhjc4mnVEHgJuesUBsy346vPGBiCZXgQ2tQ,55808
|
|
21
|
+
sensor/fb/DataType.cp313-win_amd64.pyd,sha256=Hd__ZAFq4x6tbnXHRi60PPF57DAtbTg94d2W0ETpYQc,22528
|
|
22
|
+
sensor/fb/Sample.cp313-win_amd64.pyd,sha256=gy87FS4Vn0MHbFSxAVK-qw5K3MXZsAPu5PGICwlxhLg,61440
|
|
23
|
+
sensor/fb/SensorData.cp313-win_amd64.pyd,sha256=Pm1qMGOraiENrtufXRhz9egzEQAVg4-2u-UAHSTFfR0,143360
|
|
24
|
+
sensor/fb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
sensor_sdk-0.3.2.dist-info/licenses/LICENSE.txt,sha256=8CSivOpub3IuXODTyqBRI91AxouJZk02YrcKuOAkWu8,1111
|
|
26
|
+
sensor_sdk-0.3.2.dist-info/METADATA,sha256=3KUTnyQlUBBEPzclKOTHsBSD4mOw1TJU9ROaPnhdJXI,18379
|
|
27
|
+
sensor_sdk-0.3.2.dist-info/WHEEL,sha256=AP_wkikJcUkDPUGvTNydv7g0R7BhW1u_fVHNKW7TJyI,101
|
|
28
|
+
sensor_sdk-0.3.2.dist-info/top_level.txt,sha256=Ftq49B6bH0Ffdc7c8LkcyakHo6lsg_snlBbpEUoILSk,7
|
|
29
|
+
sensor_sdk-0.3.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OYMotion Technologies Co., Ltd..
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sensor
|