moat-api-bosch 0.1.1__tar.gz

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.
@@ -0,0 +1,26 @@
1
+ **
2
+ ** This license only applies to the Python wrapper.
3
+ ** Any binaries included in this package (or not)
4
+ ** are covered by their own license.
5
+ **
6
+
7
+ The MIT License (MIT)
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining
10
+ a copy of this software and associated documentation files (the
11
+ "Software"), to deal in the Software without restriction, including
12
+ without limitation the rights to use, copy, modify, merge, publish,
13
+ distribute, sublicense, and/or sell copies of the Software, and to
14
+ permit persons to whom the Software is furnished to do so, subject to
15
+ the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be
18
+ included in all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/make -f
2
+
3
+ PACKAGE = moat-api-bosch
4
+ MAKEINCL ?= $(shell python3 -mmoat src path)/make/py
5
+
6
+ ifneq ($(wildcard $(MAKEINCL)),)
7
+ include $(MAKEINCL)
8
+ # availabe via http://github.com/smurfix/sourcemgr
9
+
10
+ else
11
+ %:
12
+ @echo "Please fix 'python3 -mmoat src path'."
13
+ @exit 1
14
+ endif
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: moat-api-bosch
3
+ Version: 0.1.1
4
+ Summary: MoaT API wrappers for Bosch Sensortec chips
5
+ Author-email: Matthias Urlichs <matthias@urlichs.de>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://m-o-a-t.org
8
+ Project-URL: repository, https://github.com/M-o-a-T/moat
9
+ Keywords: MoaT,Bosch,BMV080,sensor
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE.txt
16
+ Requires-Dist: cffi
17
+ Dynamic: license-file
18
+
19
+ # MoaT API: Bosch Sensortec
20
+
21
+ % start synopsis
22
+ % start main
23
+
24
+ This module collects CFFI-based Python wrappers for Bosch Sensortec sensors.
25
+
26
+ - BMV080 Particulate Matter Sensor
27
+
28
+ % end synopsis
29
+
30
+ ## Requirements
31
+
32
+ - BMV080: the shared libraries (`libbmv080.so` / `bmv080.dll`) must be
33
+ obtained from Bosch Sensortec.
34
+
35
+ ## Availability
36
+
37
+ The Bosch libraries may or may not be available for your OS and architecture.
38
+ If they are not, acquire a suitable device (e.g. a Raspberry Pi 4) and use
39
+ MoaT-Link to connect to the library remotely.
40
+
41
+ % end main
42
+
43
+ ## License
44
+
45
+ This library is licensed under the MIT license. The license of the Bosch
46
+ binares unfortunately does not explicitly allow redistribution, thus they
47
+ cannot be included here.
@@ -0,0 +1,29 @@
1
+ # MoaT API: Bosch Sensortec
2
+
3
+ % start synopsis
4
+ % start main
5
+
6
+ This module collects CFFI-based Python wrappers for Bosch Sensortec sensors.
7
+
8
+ - BMV080 Particulate Matter Sensor
9
+
10
+ % end synopsis
11
+
12
+ ## Requirements
13
+
14
+ - BMV080: the shared libraries (`libbmv080.so` / `bmv080.dll`) must be
15
+ obtained from Bosch Sensortec.
16
+
17
+ ## Availability
18
+
19
+ The Bosch libraries may or may not be available for your OS and architecture.
20
+ If they are not, acquire a suitable device (e.g. a Raspberry Pi 4) and use
21
+ MoaT-Link to connect to the library remotely.
22
+
23
+ % end main
24
+
25
+ ## License
26
+
27
+ This library is licensed under the MIT license. The license of the Bosch
28
+ binares unfortunately does not explicitly allow redistribution, thus they
29
+ cannot be included here.
@@ -0,0 +1,7 @@
1
+ /files
2
+ /*.log
3
+ /*.debhelper
4
+ /*.debhelper-build-stamp
5
+ /*.substvars
6
+ /debhelper-build-stamp
7
+ /python3-moat-api-bosch
@@ -0,0 +1,11 @@
1
+ moat-api-bosch (0.1.1-3) unstable; urgency=medium
2
+
3
+ * New release for 26.0.0
4
+
5
+ -- Matthias Urlichs <matthias@urlichs.de> Sun, 08 Feb 2026 11:05:50 +0100
6
+
7
+ moat-api-bosch (0.1.1-3) unstable; urgency=medium
8
+
9
+ * Initial release for 26.0.0
10
+
11
+ -- Matthias Urlichs <matthias@urlichs.de> Sun, 08 Feb 2026 11:01:04 +0100
@@ -0,0 +1,19 @@
1
+ Source: moat-api-bosch
2
+ Maintainer: "Matthias Urlichs" <matthias@urlichs.de>
3
+ Section: python
4
+ Priority: optional
5
+ Build-Depends: dh-python, python3-all, debhelper (>= 13),
6
+ python3-setuptools,
7
+ python3-wheel,
8
+ python3-anyio (>= 4.0),
9
+ python3-cffi,
10
+ Standards-Version: 3.9.6
11
+ Homepage: https://m-o-a-t.org
12
+ X-DH-Compat: 13
13
+
14
+ Package: python3-moat-api-bosch
15
+ Architecture: all
16
+ Depends: ${misc:Depends}, ${python3:Depends},
17
+ python3-anyio (>= 4.0),
18
+ Description: APIs for Bosch sensors
19
+ This package wraps various Bosch Sensortec libraries.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/make -f
2
+
3
+ export PYBUILD_NAME=moat-api-bosch
4
+ %:
5
+ dh $@ --with python3 --buildsystem=pybuild
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [ "setuptools", "wheel",]
4
+
5
+ [project]
6
+ classifiers = [
7
+ "Intended Audience :: Developers",
8
+ "Programming Language :: Python :: 3",
9
+ "Development Status :: 3 - Alpha",
10
+ ]
11
+ dependencies = [
12
+ "cffi",
13
+ ]
14
+ version = "0.1.1"
15
+ keywords = [ "MoaT", "Bosch", "BMV080", "sensor",]
16
+ requires-python = ">=3.11"
17
+ name = "moat-api-bosch"
18
+ description = "MoaT API wrappers for Bosch Sensortec chips"
19
+ readme = "README.md"
20
+ license = "MIT"
21
+
22
+ [[project.authors]]
23
+ email = "matthias@urlichs.de"
24
+ name = "Matthias Urlichs"
25
+
26
+ [project.urls]
27
+ homepage = "https://m-o-a-t.org"
28
+ repository = "https://github.com/M-o-a-T/moat"
29
+
30
+ [tool.setuptools]
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.setuptools.package-data]
35
+ "*" = ["*.yaml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """MoaT API wrappers for Bosch Sensortec chips."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,712 @@
1
+ """
2
+ CFFI-based interface to the Bosch BMV080 particulate matter sensor.
3
+
4
+ This module provides a Python wrapper around the BMV080 C library,
5
+ allowing measurement of PM1, PM2.5, and PM10 concentrations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from anyio import ContextManagerMixin
12
+ from contextlib import contextmanager
13
+ from dataclasses import dataclass
14
+ from enum import IntEnum
15
+
16
+ from moat.api._dll import DLL
17
+
18
+ from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
19
+
20
+ if TYPE_CHECKING:
21
+ from cffi import FFI
22
+
23
+ from collections.abc import Generator
24
+
25
+ __all__ = [
26
+ "BMV080",
27
+ "BMV080Error",
28
+ "BMV080Output",
29
+ "BMV080_Link",
30
+ "DutyCyclingMode",
31
+ "MeasurementAlgorithm",
32
+ "StatusCode",
33
+ ]
34
+
35
+ # Required library version: >= 11.2.0, < 12.0.0
36
+ _MIN_VERSION = (24, 2, 0)
37
+ _MAX_VERSION = (25, 0, 0)
38
+
39
+
40
+ class StatusCode(IntEnum):
41
+ """Status codes returned by the BMV080 sensor driver."""
42
+
43
+ OK = 0
44
+
45
+ # Warnings
46
+ WARNING_INVALID_REG_READ = 1
47
+ WARNING_INVALID_REG_WRITE = 2
48
+ WARNING_FIFO_READ = 3
49
+ WARNING_FIFO_EVENTS_OVERFLOW = 4
50
+ WARNING_FIFO_SW_BUFFER_SIZE = 208
51
+ WARNING_FIFO_HW_BUFFER_SIZE = 209
52
+
53
+ # Errors
54
+ ERROR_NULLPTR = 100
55
+ ERROR_REG_ADDR = 101
56
+ ERROR_PARAM_LOCKED = 179
57
+ ERROR_PARAM_INVALID = 115
58
+ ERROR_PARAM_INVALID_CHANNEL = 102
59
+ ERROR_PARAM_INVALID_VALUE = 123
60
+ ERROR_PARAM_INVALID_INTERNAL_CONFIG = 104
61
+ ERROR_PRECONDITION_UNSATISFIED = 180
62
+ ERROR_HW_READ = 105
63
+ ERROR_HW_WRITE = 106
64
+ ERROR_MISMATCH_CHIP_ID = 107
65
+ ERROR_MISMATCH_REG_VALUE = 160
66
+ ERROR_OPERATION_MODE_INVALID = 116
67
+ ERROR_OPERATION_MODE_CHANGE = 113
68
+ ERROR_OPERATION_MODE_CHANNELS_OUT_OF_SYNC = 114
69
+ ERROR_ASIC_NOT_CONFIGURED = 157
70
+ ERROR_MEM_READ = 133
71
+ ERROR_MEM_ADDRESS = 135
72
+ ERROR_MEM_CMD = 136
73
+ ERROR_MEM_TIMEOUT = 137
74
+ ERROR_MEM_INVALID = 138
75
+ ERROR_MEM_OBSOLETE = 139
76
+ ERROR_MEM_OPERATION_MODE = 140
77
+ ERROR_MEM_DATA_INTEGRITY = 153
78
+ ERROR_MEM_DATA_INTEGRITY_INTERNAL_TEST_1 = 154
79
+ ERROR_MEM_INTERNAL_TEST_1 = 156
80
+ ERROR_MEM_DATA_INTEGRITY_INTERNAL_TEST_2 = 159
81
+ ERROR_MEM_INTERNAL_TEST_2 = 181
82
+ ERROR_FIFO_FORMAT = 210
83
+ ERROR_FIFO_EVENTS_COUNT_SATURATED = 213
84
+ ERROR_FIFO_UNAVAILABLE = 174
85
+ ERROR_FIFO_EVENTS_COUNT_DIFF = 214
86
+ ERROR_SYNC_COMM = 161
87
+ ERROR_SYNC_CTRL = 162
88
+ ERROR_SYNC_MEAS = 163
89
+ ERROR_SYNC_LOCKED = 164
90
+ ERROR_DC_CANCEL_RANGE = 165
91
+ ERROR_DC_ESTIM_RANGE = 166
92
+ ERROR_LPWR_T = 167
93
+ ERROR_LPWR_RANGE = 168
94
+ ERROR_POWER_DOMAIN = 169
95
+ ERROR_HEADROOM_VDDL = 170
96
+ ERROR_HEADROOM_LDV_OUTPUT = 171
97
+ ERROR_HEADROOM_LDV_REF = 172
98
+ ERROR_HEADROOM_INTERNAL = 173
99
+ ERROR_SAFETY_PRECAUTION = 120
100
+ ERROR_TIMESTAMP_DIFFERENCE = 211
101
+ ERROR_TIMESTAMP_OVERFLOW = 212
102
+ ERROR_LIB_VERSION_INCOMPATIBLE = 300
103
+ ERROR_INTERNAL_PARAMETER_VERSION_INVALID = 301
104
+ ERROR_INTERNAL_PARAMETER_INDEX_INVALID = 302
105
+ ERROR_MEMORY_ALLOCATION = 403
106
+ ERROR_CALLBACK_DELAY = 303
107
+ ERROR_INCOMPATIBLE_SENSOR_HW = 418
108
+
109
+
110
+ class MeasurementAlgorithm(IntEnum):
111
+ """Measurement algorithm choices."""
112
+
113
+ FAST_RESPONSE = 1
114
+ BALANCED = 2
115
+ HIGH_PRECISION = 3
116
+
117
+
118
+ class DutyCyclingMode(IntEnum):
119
+ """Modes of performing a duty cycling measurement."""
120
+
121
+ MODE_0 = 0 # Fixed duty cycle, ON time = integration_time, OFF time = sleep time
122
+
123
+
124
+ class BMV080Error(Exception):
125
+ """Exception raised for BMV080 errors."""
126
+
127
+ def __init__(self, status: StatusCode, message: str = "") -> None:
128
+ self.status = status
129
+ name_val = f"{status.name} ({status.value})"
130
+ super().__init__(f"{name_val}: {message}" if message else name_val)
131
+
132
+
133
+ @dataclass
134
+ class BMV080Output:
135
+ """Output data from BMV080 sensor measurement.
136
+
137
+ Attributes:
138
+ runtime_in_sec: Estimate of time passed since measurement start, in seconds.
139
+ pm2_5_mass_concentration: PM2.5 value in µg/m³.
140
+ pm1_mass_concentration: PM1 value in µg/m³.
141
+ pm10_mass_concentration: PM10 value in µg/m³.
142
+ pm2_5_number_concentration: PM2.5 value in particles/cm³.
143
+ pm1_number_concentration: PM1 value in particles/cm³.
144
+ pm10_number_concentration: PM10 value in particles/cm³.
145
+ is_obstructed: Whether the sensor is obstructed.
146
+ is_outside_measurement_range: Whether PM2.5 is outside 0..1000 µg/m³.
147
+ """
148
+
149
+ runtime_in_sec: float
150
+ pm2_5_mass_concentration: float
151
+ pm1_mass_concentration: float
152
+ pm10_mass_concentration: float
153
+ pm2_5_number_concentration: float
154
+ pm1_number_concentration: float
155
+ pm10_number_concentration: float
156
+ is_obstructed: bool
157
+ is_outside_measurement_range: bool
158
+
159
+
160
+ @runtime_checkable
161
+ class BMV080_Link(Protocol):
162
+ """Protocol for serial communication interface to BMV080.
163
+
164
+ The caller must provide an object implementing these methods.
165
+ """
166
+
167
+ def read(self, header: int, length: int) -> list[int]:
168
+ """Read data from the sensor.
169
+
170
+ Args:
171
+ header: 16-bit header information.
172
+ length: Number of 16-bit words to read.
173
+
174
+ Returns:
175
+ List of 16-bit words read from sensor.
176
+
177
+ Raises:
178
+ IOError: If read fails.
179
+ """
180
+ ...
181
+
182
+ def write(self, header: int, payload: list[int]) -> None:
183
+ """Write data to the sensor.
184
+
185
+ Args:
186
+ header: 16-bit header information.
187
+ payload: List of 16-bit words to write.
188
+
189
+ Raises:
190
+ IOError: If write fails.
191
+ """
192
+ ...
193
+
194
+ def delay_ms(self, duration_ms: int) -> None:
195
+ """Delay for specified milliseconds.
196
+
197
+ Args:
198
+ duration_ms: Duration in milliseconds.
199
+ """
200
+ ...
201
+
202
+ def time_ms(self) -> int:
203
+ """Get current tick value in milliseconds.
204
+
205
+ Returns:
206
+ Current tick value for timing purposes.
207
+ """
208
+ ...
209
+
210
+ def process(self, output: BMV080Output) -> None:
211
+ """Process a measurement output.
212
+
213
+ Override this method to handle measurements. Called for each
214
+ output from serve_interrupt().
215
+
216
+ Args:
217
+ output: Measurement data from the sensor.
218
+ """
219
+
220
+
221
+ # C declarations for CFFI
222
+ _CDEF = """
223
+ typedef int8_t bmv080_status_code_t;
224
+ typedef void* bmv080_handle_t;
225
+ typedef void* bmv080_sercom_handle_t;
226
+
227
+ typedef enum {
228
+ E_BMV080_DUTY_CYCLING_MODE_0 = 0
229
+ } bmv080_duty_cycling_mode_t;
230
+
231
+ typedef enum {
232
+ E_BMV080_MEASUREMENT_ALGORITHM_FAST_RESPONSE = 1,
233
+ E_BMV080_MEASUREMENT_ALGORITHM_BALANCED = 2,
234
+ E_BMV080_MEASUREMENT_ALGORITHM_HIGH_PRECISION = 3
235
+ } bmv080_measurement_algorithm_t;
236
+
237
+ struct bmv080_extended_info_s;
238
+
239
+ typedef struct {
240
+ float runtime_in_sec;
241
+ float pm2_5_mass_concentration;
242
+ float pm1_mass_concentration;
243
+ float pm10_mass_concentration;
244
+ float pm2_5_number_concentration;
245
+ float pm1_number_concentration;
246
+ float pm10_number_concentration;
247
+ bool is_obstructed;
248
+ bool is_outside_measurement_range;
249
+ float reserved_0;
250
+ float reserved_1;
251
+ float reserved_2;
252
+ struct bmv080_extended_info_s *extended_info;
253
+ } bmv080_output_t;
254
+
255
+ typedef int8_t(*bmv080_callback_read_t)(bmv080_sercom_handle_t sercom_handle, uint16_t header,
256
+ uint16_t* payload, uint16_t payload_length);
257
+ typedef int8_t(*bmv080_callback_write_t)(bmv080_sercom_handle_t sercom_handle, uint16_t header,
258
+ const uint16_t* payload, uint16_t payload_length);
259
+ typedef int8_t(*bmv080_callback_delay_t)(uint32_t duration_in_ms);
260
+ typedef uint32_t(*bmv080_callback_tick_t)(void);
261
+ typedef void(*bmv080_callback_data_ready_t)(bmv080_output_t bmv080_output,
262
+ void* callback_parameters);
263
+
264
+ bmv080_status_code_t bmv080_open(bmv080_handle_t* handle,
265
+ const bmv080_sercom_handle_t sercom_handle, const bmv080_callback_read_t read,
266
+ const bmv080_callback_write_t write, const bmv080_callback_delay_t delay_ms);
267
+
268
+ bmv080_status_code_t bmv080_get_driver_version(uint16_t* major, uint16_t* minor, uint16_t* patch,
269
+ char git_hash[12], int32_t* num_commits_ahead);
270
+
271
+ bmv080_status_code_t bmv080_reset(const bmv080_handle_t handle);
272
+
273
+ bmv080_status_code_t bmv080_get_parameter(const bmv080_handle_t handle,
274
+ const char* key, void* value);
275
+
276
+ bmv080_status_code_t bmv080_set_parameter(const bmv080_handle_t handle, const char* key,
277
+ const void* value);
278
+
279
+ bmv080_status_code_t bmv080_get_sensor_id(const bmv080_handle_t handle, char id[13]);
280
+
281
+ bmv080_status_code_t bmv080_start_continuous_measurement(const bmv080_handle_t handle);
282
+
283
+ bmv080_status_code_t bmv080_start_duty_cycling_measurement(const bmv080_handle_t handle,
284
+ const bmv080_callback_tick_t get_tick_ms, bmv080_duty_cycling_mode_t duty_cycling_mode);
285
+
286
+ bmv080_status_code_t bmv080_serve_interrupt(const bmv080_handle_t handle,
287
+ bmv080_callback_data_ready_t data_ready, void* callback_parameters);
288
+
289
+ bmv080_status_code_t bmv080_stop_measurement(const bmv080_handle_t handle);
290
+
291
+ bmv080_status_code_t bmv080_close(bmv080_handle_t* handle);
292
+ """
293
+
294
+
295
+ class BMV080(ContextManagerMixin):
296
+ """CFFI-based interface to the BMV080 particulate matter sensor.
297
+
298
+ This class wraps the BMV080 C library, providing a Pythonic interface
299
+ for measuring particulate matter concentrations.
300
+
301
+ Args:
302
+ link: Serial communication interface implementing read/write/delay_ms/time_ms.
303
+ libs_path: Path to the BMV080 shared libraries (.so/.dll). The lib_postProcessor.so
304
+ library must be loaded first.
305
+
306
+ Example:
307
+ >>> class MySPI:
308
+ ... def read(self, header, length): ...
309
+ ... def write(self, header, payload): ...
310
+ ... def delay_ms(self, ms): ...
311
+ ... def time_ms(self): ...
312
+ >>> with BMV080(MySPI(), "/path/to/libbmv080.so") as sensor:
313
+ ... sensor.start_continuous_measurement()
314
+ ... sensor.serve_interrupt()
315
+ ... sensor.stop_measurement()
316
+ """
317
+
318
+ def __init__(self, link: BMV080_Link, libs_path: str) -> None:
319
+ """Initialize BMV080 interface.
320
+
321
+ Args:
322
+ link: Object with read/write/delay_ms/time_ms methods.
323
+ libs_path: Paths to the BMV080 shared libraries, colon-separated
324
+ (use semicolon on Windows).
325
+ """
326
+ self._link = link
327
+ self._libs_path = libs_path
328
+
329
+ self._ffi: FFI | None = None
330
+ self._lib = None
331
+ self._handle = None
332
+ self._handle_ptr = None
333
+
334
+ # Store callbacks to prevent garbage collection
335
+ self._read_cb = None
336
+ self._write_cb = None
337
+ self._delay_cb = None
338
+ self._tick_cb = None
339
+ self._data_ready_cb = None
340
+
341
+ # Store exception from callback for re-raising
342
+ self._logged_exc: BaseException | None = None
343
+
344
+ @contextmanager
345
+ def __contextmanager__(self) -> Generator[Self]:
346
+ with DLL("moat.api.bosch.bmv080", _CDEF, *self._libs_path.split(os.pathsep)) as (
347
+ self._ffi,
348
+ self._lib,
349
+ ):
350
+ try:
351
+ self._open()
352
+ yield self
353
+ finally:
354
+ self._close()
355
+
356
+ def _log_exc(self, exc: BaseException) -> None:
357
+ """Store an exception from a callback for later re-raising.
358
+
359
+ Args:
360
+ exc: The exception to store.
361
+ """
362
+ if self._logged_exc is None:
363
+ self._logged_exc = exc
364
+
365
+ def _check_status(self, status: int) -> None:
366
+ """Check status code and raise exception if error.
367
+
368
+ Args:
369
+ status: Status code from C library.
370
+
371
+ Raises:
372
+ BMV080Error: If status indicates an error (>= 100).
373
+ BaseException: Re-raises stored callback exception if status is -1.
374
+ """
375
+ if status == -1 and self._logged_exc is not None:
376
+ exc = self._logged_exc
377
+ self._logged_exc = None
378
+ raise exc
379
+ if status >= 100:
380
+ raise BMV080Error(StatusCode(status))
381
+
382
+ def _open(self) -> None:
383
+ """Open connection to the BMV080 sensor.
384
+
385
+ Initializes the C library and creates a sensor handle.
386
+ Verifies that the library version is compatible (>= 11.2, < 12).
387
+
388
+ Raises:
389
+ BMV080Error: If opening fails.
390
+ FileNotFoundError: If library file not found.
391
+ RuntimeError: If library version is incompatible.
392
+ """
393
+ if self._handle is not None:
394
+ return
395
+
396
+ # Check library version before proceeding
397
+ version = self._get_driver_version_raw()
398
+ if version < _MIN_VERSION or version >= _MAX_VERSION:
399
+ min_str = ".".join(str(x) for x in _MIN_VERSION)
400
+ max_str = ".".join(str(x) for x in _MAX_VERSION)
401
+ ver_str = ".".join(str(x) for x in version)
402
+ raise RuntimeError(
403
+ f"BMV080 library version {ver_str} not supported "
404
+ f"(requires >= {min_str}, < {max_str})"
405
+ )
406
+
407
+ # Create callbacks
408
+ # sercom_handle is passed by the C library but unused; we use self._link
409
+ @self._ffi.callback("bmv080_callback_read_t")
410
+ def read_cb(
411
+ sercom_handle: object, # noqa: ARG001
412
+ header: int,
413
+ payload: object,
414
+ length: int,
415
+ ) -> int:
416
+ try:
417
+ data = self._link.read(header, length)
418
+ for i, val in enumerate(data[:length]):
419
+ payload[i] = val
420
+ return 0
421
+ except BaseException as exc:
422
+ self._log_exc(exc)
423
+ return -1
424
+
425
+ @self._ffi.callback("bmv080_callback_write_t")
426
+ def write_cb(
427
+ sercom_handle: object, # noqa: ARG001
428
+ header: int,
429
+ payload: object,
430
+ length: int,
431
+ ) -> int:
432
+ try:
433
+ data = [payload[i] for i in range(length)]
434
+ self._link.write(header, data)
435
+ return 0
436
+ except BaseException as exc:
437
+ self._log_exc(exc)
438
+ return -1
439
+
440
+ @self._ffi.callback("bmv080_callback_delay_t")
441
+ def delay_cb(duration_ms: int) -> int:
442
+ try:
443
+ self._link.delay_ms(duration_ms)
444
+ return 0
445
+ except BaseException as exc:
446
+ self._log_exc(exc)
447
+ return -1
448
+
449
+ # params is passed by the C library but unused
450
+ @self._ffi.callback("bmv080_callback_data_ready_t")
451
+ def data_ready_cb(output: object, _params: object) -> None:
452
+ out = BMV080Output(
453
+ runtime_in_sec=output.runtime_in_sec,
454
+ pm2_5_mass_concentration=output.pm2_5_mass_concentration,
455
+ pm1_mass_concentration=output.pm1_mass_concentration,
456
+ pm10_mass_concentration=output.pm10_mass_concentration,
457
+ pm2_5_number_concentration=output.pm2_5_number_concentration,
458
+ pm1_number_concentration=output.pm1_number_concentration,
459
+ pm10_number_concentration=output.pm10_number_concentration,
460
+ is_obstructed=output.is_obstructed,
461
+ is_outside_measurement_range=output.is_outside_measurement_range,
462
+ )
463
+ self._link.process(out)
464
+
465
+ self._read_cb = read_cb
466
+ self._write_cb = write_cb
467
+ self._delay_cb = delay_cb
468
+ self._data_ready_cb = data_ready_cb
469
+
470
+ # Create handle
471
+ self._handle_ptr = self._ffi.new("bmv080_handle_t*")
472
+ status = self._lib.bmv080_open(
473
+ self._handle_ptr,
474
+ self._handle_ptr, # self._ffi.NULL -- not used but lib complains if NULL
475
+ self._read_cb,
476
+ self._write_cb,
477
+ self._delay_cb,
478
+ )
479
+ self._check_status(status)
480
+ self._handle = self._handle_ptr[0]
481
+
482
+ def _get_driver_version_raw(self) -> tuple[int, int, int]:
483
+ """Get the driver version without requiring an open handle.
484
+
485
+ Returns:
486
+ Tuple of (major, minor, patch) version numbers.
487
+
488
+ Raises:
489
+ BMV080Error: If getting version fails.
490
+ """
491
+ major = self._ffi.new("uint16_t*")
492
+ minor = self._ffi.new("uint16_t*")
493
+ patch = self._ffi.new("uint16_t*")
494
+ git_hash = self._ffi.new("char[12]")
495
+ commits = self._ffi.new("int32_t*")
496
+
497
+ status = self._lib.bmv080_get_driver_version(major, minor, patch, git_hash, commits)
498
+ self._check_status(status)
499
+ return (major[0], minor[0], patch[0])
500
+
501
+ def _close(self) -> None:
502
+ """Close connection to the BMV080 sensor.
503
+
504
+ Releases the sensor handle and resources.
505
+ """
506
+ if self._handle is not None:
507
+ self._lib.bmv080_close(self._handle_ptr)
508
+ self._handle = None
509
+
510
+ self._handle_ptr = None
511
+ self._read_cb = None
512
+ self._write_cb = None
513
+ self._delay_cb = None
514
+ self._tick_cb = None
515
+ self._data_ready_cb = None
516
+ self._lib = None
517
+ self._ffi = None
518
+ self._logged_exc = None
519
+
520
+ def reset(self) -> None:
521
+ """Reset the sensor including hardware and software.
522
+
523
+ All parameters revert to defaults.
524
+
525
+ Raises:
526
+ BMV080Error: If reset fails.
527
+ """
528
+ status = self._lib.bmv080_reset(self._handle)
529
+ self._check_status(status)
530
+
531
+ def get_driver_version(self) -> tuple[int, int, int]:
532
+ """Get the driver version.
533
+
534
+ Returns:
535
+ Tuple of (major, minor, patch) version numbers.
536
+
537
+ Raises:
538
+ BMV080Error: If getting version fails.
539
+ """
540
+ return self._get_driver_version_raw()
541
+
542
+ def get_sensor_id(self) -> str:
543
+ """Get the sensor ID.
544
+
545
+ Returns:
546
+ 13-character sensor ID string.
547
+
548
+ Raises:
549
+ BMV080Error: If getting ID fails.
550
+ """
551
+ id_buf = self._ffi.new("char[13]")
552
+ status = self._lib.bmv080_get_sensor_id(self._handle, id_buf)
553
+ self._check_status(status)
554
+ return self._ffi.string(id_buf).decode("ascii")
555
+
556
+ def set_parameter(self, key: str, value: bool | float | str) -> None:
557
+ """Set a sensor parameter.
558
+
559
+ Args:
560
+ key: Parameter key (e.g., "integration_time", "do_vibration_filtering").
561
+ value: Parameter value of appropriate type. Use int for integer params.
562
+
563
+ Raises:
564
+ BMV080Error: If setting parameter fails.
565
+ TypeError: If value type is not supported.
566
+ """
567
+ key_bytes = key.encode("utf-8")
568
+
569
+ if isinstance(value, bool):
570
+ val_ptr = self._ffi.new("bool*", value)
571
+ elif isinstance(value, int):
572
+ # int is a subtype of float in type hints but not at runtime
573
+ val_ptr = self._ffi.new("uint16_t*", value)
574
+ elif isinstance(value, float):
575
+ val_ptr = self._ffi.new("float*", value)
576
+ elif isinstance(value, str):
577
+ val_ptr = self._ffi.new("char[]", value.encode("utf-8"))
578
+ else:
579
+ raise TypeError(f"Unsupported parameter type: {type(value)}")
580
+
581
+ status = self._lib.bmv080_set_parameter(self._handle, key_bytes, val_ptr)
582
+ self._check_status(status)
583
+
584
+ def get_parameter_bool(self, key: str) -> bool:
585
+ """Get a boolean parameter.
586
+
587
+ Args:
588
+ key: Parameter key.
589
+
590
+ Returns:
591
+ Parameter value.
592
+
593
+ Raises:
594
+ BMV080Error: If getting parameter fails.
595
+ """
596
+ key_bytes = key.encode("utf-8")
597
+ val_ptr = self._ffi.new("bool*")
598
+ status = self._lib.bmv080_get_parameter(self._handle, key_bytes, val_ptr)
599
+ self._check_status(status)
600
+ return val_ptr[0]
601
+
602
+ def get_parameter_int(self, key: str) -> int:
603
+ """Get an integer parameter.
604
+
605
+ Args:
606
+ key: Parameter key.
607
+
608
+ Returns:
609
+ Parameter value.
610
+
611
+ Raises:
612
+ BMV080Error: If getting parameter fails.
613
+ """
614
+ key_bytes = key.encode("utf-8")
615
+ val_ptr = self._ffi.new("uint16_t*")
616
+ status = self._lib.bmv080_get_parameter(self._handle, key_bytes, val_ptr)
617
+ self._check_status(status)
618
+ return val_ptr[0]
619
+
620
+ def get_parameter_float(self, key: str) -> float:
621
+ """Get a float parameter.
622
+
623
+ Args:
624
+ key: Parameter key.
625
+
626
+ Returns:
627
+ Parameter value.
628
+
629
+ Raises:
630
+ BMV080Error: If getting parameter fails.
631
+ """
632
+ key_bytes = key.encode("utf-8")
633
+ val_ptr = self._ffi.new("float*")
634
+ status = self._lib.bmv080_get_parameter(self._handle, key_bytes, val_ptr)
635
+ self._check_status(status)
636
+ return val_ptr[0]
637
+
638
+ def get_parameter_str(self, key: str) -> str:
639
+ """Get a string parameter.
640
+
641
+ Args:
642
+ key: Parameter key.
643
+
644
+ Returns:
645
+ Parameter value (max 256 characters).
646
+
647
+ Raises:
648
+ BMV080Error: If getting parameter fails.
649
+ """
650
+ key_bytes = key.encode("utf-8")
651
+ val_ptr = self._ffi.new("char[256]")
652
+ status = self._lib.bmv080_get_parameter(self._handle, key_bytes, val_ptr)
653
+ self._check_status(status)
654
+ return self._ffi.string(val_ptr).decode("utf-8")
655
+
656
+ def start_continuous_measurement(self) -> None:
657
+ """Start particle measurement in continuous mode.
658
+
659
+ The sensor stays in measurement mode until stop_measurement() is called.
660
+ Call serve_interrupt() regularly to get data.
661
+
662
+ Raises:
663
+ BMV080Error: If starting measurement fails.
664
+ """
665
+ status = self._lib.bmv080_start_continuous_measurement(self._handle)
666
+ self._check_status(status)
667
+
668
+ def start_duty_cycling_measurement(
669
+ self,
670
+ mode: DutyCyclingMode = DutyCyclingMode.MODE_0,
671
+ ) -> None:
672
+ """Start particle measurement in duty cycling mode.
673
+
674
+ Args:
675
+ mode: Duty cycling mode (currently only MODE_0).
676
+
677
+ Raises:
678
+ BMV080Error: If starting measurement fails.
679
+ """
680
+
681
+ @self._ffi.callback("bmv080_callback_tick_t")
682
+ def tick_cb() -> int:
683
+ return self._link.time_ms() & 0xFFFFFFFF
684
+
685
+ self._tick_cb = tick_cb
686
+
687
+ status = self._lib.bmv080_start_duty_cycling_measurement(self._handle, self._tick_cb, mode)
688
+ self._check_status(status)
689
+
690
+ def stop_measurement(self) -> None:
691
+ """Stop particle measurement.
692
+
693
+ Raises:
694
+ BMV080Error: If stopping measurement fails.
695
+ """
696
+ status = self._lib.bmv080_stop_measurement(self._handle)
697
+ self._check_status(status)
698
+
699
+ def serve_interrupt(self) -> None:
700
+ """Service an interrupt.
701
+
702
+ Should be called regularly (at least once per second in duty cycling mode).
703
+ Calls the process() hook for each available output.
704
+
705
+ Raises:
706
+ BMV080Error: If serving interrupt fails.
707
+ """
708
+
709
+ status = self._lib.bmv080_serve_interrupt(
710
+ self._handle, self._data_ready_cb, self._ffi.NULL
711
+ )
712
+ self._check_status(status)
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: moat-api-bosch
3
+ Version: 0.1.1
4
+ Summary: MoaT API wrappers for Bosch Sensortec chips
5
+ Author-email: Matthias Urlichs <matthias@urlichs.de>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://m-o-a-t.org
8
+ Project-URL: repository, https://github.com/M-o-a-T/moat
9
+ Keywords: MoaT,Bosch,BMV080,sensor
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE.txt
16
+ Requires-Dist: cffi
17
+ Dynamic: license-file
18
+
19
+ # MoaT API: Bosch Sensortec
20
+
21
+ % start synopsis
22
+ % start main
23
+
24
+ This module collects CFFI-based Python wrappers for Bosch Sensortec sensors.
25
+
26
+ - BMV080 Particulate Matter Sensor
27
+
28
+ % end synopsis
29
+
30
+ ## Requirements
31
+
32
+ - BMV080: the shared libraries (`libbmv080.so` / `bmv080.dll`) must be
33
+ obtained from Bosch Sensortec.
34
+
35
+ ## Availability
36
+
37
+ The Bosch libraries may or may not be available for your OS and architecture.
38
+ If they are not, acquire a suitable device (e.g. a Raspberry Pi 4) and use
39
+ MoaT-Link to connect to the library remotely.
40
+
41
+ % end main
42
+
43
+ ## License
44
+
45
+ This library is licensed under the MIT license. The license of the Bosch
46
+ binares unfortunately does not explicitly allow redistribution, thus they
47
+ cannot be included here.
@@ -0,0 +1,15 @@
1
+ LICENSE.txt
2
+ Makefile
3
+ README.md
4
+ pyproject.toml
5
+ debian/.gitignore
6
+ debian/changelog
7
+ debian/control
8
+ debian/rules
9
+ src/moat/api/bosch/__init__.py
10
+ src/moat/api/bosch/bmv080.py
11
+ src/moat_api_bosch.egg-info/PKG-INFO
12
+ src/moat_api_bosch.egg-info/SOURCES.txt
13
+ src/moat_api_bosch.egg-info/dependency_links.txt
14
+ src/moat_api_bosch.egg-info/requires.txt
15
+ src/moat_api_bosch.egg-info/top_level.txt