serial-scale-bench 0.2.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.
@@ -0,0 +1,12 @@
1
+ __author__ = "Lars B. Rollik"
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from serial_scale_bench.scale import AutoReconnectSerialScale, Scale, SerialScale
6
+
7
+ try:
8
+ __version__ = version("serial-scale-bench")
9
+ except PackageNotFoundError:
10
+ __version__ = "unknown"
11
+
12
+ __all__ = ["Scale", "SerialScale", "AutoReconnectSerialScale", "__version__"]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.2.0'
22
+ __version_tuple__ = version_tuple = (0, 2, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,69 @@
1
+ import socket
2
+ from datetime import datetime
3
+
4
+ from fastapi import FastAPI, HTTPException
5
+ from pydantic import BaseModel
6
+
7
+ from serial_scale_bench.scale import AutoReconnectSerialScale
8
+
9
+
10
+ class ScaleStatus(BaseModel):
11
+ scale_id: str
12
+ is_connected: bool
13
+ last_seen: datetime | None = None
14
+ protocol: str | None = None
15
+
16
+
17
+ def create_api(scale: AutoReconnectSerialScale, instance_info: dict) -> FastAPI:
18
+ """"""
19
+ app = FastAPI()
20
+ app.state.scale = scale
21
+ app.state.instance_info = (
22
+ instance_info # TODO: instance id/name, api host/port, serial port/baud rate
23
+ )
24
+
25
+ @app.get("/status", response_model=ScaleStatus)
26
+ def get_scale_info():
27
+ return ScaleStatus(
28
+ scale_id=app.state.instance_info["scale_id"],
29
+ is_connected=app.state.scale.is_connected(),
30
+ last_seen=app.state.scale.last_response_time,
31
+ protocol=app.state.scale.protocol,
32
+ )
33
+
34
+ @app.get("/ping")
35
+ def ping_scale():
36
+ return {"responsive": app.state.scale.is_responsive()}
37
+
38
+ @app.get("/info")
39
+ def get_info():
40
+ return {
41
+ "scale_id": app.state.instance_info,
42
+ "hostname": socket.gethostname(),
43
+ "port": app.state.instance_info["port"],
44
+ }
45
+
46
+ @app.get("/weight")
47
+ def read_weight():
48
+ if not app.state.scale.is_responsive():
49
+ raise HTTPException(status_code=503, detail="Scale not responsive")
50
+ weight = app.state.scale.get_weight()
51
+ if weight is None:
52
+ raise HTTPException(status_code=204, detail="No weight read")
53
+ return {"scale_id": instance_info["id"], "weight": weight}
54
+
55
+ @app.post("/tare")
56
+ def tare_scale():
57
+ if not app.state.scale.is_responsive():
58
+ raise HTTPException(status_code=503, detail="Scale not responsive")
59
+ app.state.scale.tare()
60
+ return {"scale_id": instance_info["id"], "action": "tared"}
61
+
62
+ @app.post("/zero")
63
+ def zero_scale():
64
+ if not app.state.scale.is_responsive():
65
+ raise HTTPException(status_code=503, detail="Scale not responsive")
66
+ app.state.scale.zero()
67
+ return {"scale_id": instance_info["id"], "action": "zeroed"}
68
+
69
+ return app
@@ -0,0 +1,20 @@
1
+ import argparse
2
+ import os
3
+
4
+
5
+ def cli_parser():
6
+ parser = argparse.ArgumentParser(description="Start a SerialScale FastAPI service")
7
+ parser.add_argument("--port", type=int, default=8000, help="Port to run FastAPI server on")
8
+ parser.add_argument(
9
+ "--device",
10
+ type=str,
11
+ default=os.environ.get("SERIAL_DEVICE"),
12
+ help="Serial port (e.g., /dev/ttyUSB0)",
13
+ )
14
+ parser.add_argument(
15
+ "--scale-id", type=str, default=os.environ.get("SCALE_ID"), help="Unique scale ID or name"
16
+ )
17
+ parser.add_argument("--baudrate", type=int, default=4800, help="Serial baud rate")
18
+ parser.add_argument("--timeout", type=float, default=1.0, help="Serial read timeout")
19
+ args = parser.parse_args()
20
+ return args
@@ -0,0 +1,23 @@
1
+ import uvicorn
2
+
3
+ from serial_scale_bench.cli import cli_parser
4
+ from serial_scale_bench.scale import AutoReconnectSerialScale
5
+
6
+
7
+ def entrypoint():
8
+ args = cli_parser()
9
+
10
+ scale_id = args.scale_id
11
+
12
+ _scale = AutoReconnectSerialScale(
13
+ port=args.device,
14
+ baudrate=args.baudrate,
15
+ timeout=args.timeout,
16
+ )
17
+
18
+ print(f"Running FastAPI server for scale '{scale_id}' on port {args.port}")
19
+ uvicorn.run("serial_scale_bench.api:app", host="0.0.0.0", port=args.port, reload=False)
20
+
21
+
22
+ if __name__ == "__main__":
23
+ entrypoint()
File without changes
@@ -0,0 +1,303 @@
1
+ import logging
2
+ import re
3
+ import statistics
4
+ import time
5
+ from collections.abc import Callable
6
+ from datetime import datetime
7
+ from enum import Enum
8
+
9
+ import serial
10
+ from serial import SerialException
11
+
12
+
13
+ class ScaleProtocols(Enum):
14
+ Protocol1 = {"id": 1, "format": ""}
15
+
16
+
17
+ class SerialScale:
18
+ def __init__(
19
+ self,
20
+ port: str,
21
+ baudrate: int = 9600,
22
+ timeout: float = 1,
23
+ protocol: int = 2,
24
+ ):
25
+ self.ser = serial.Serial(
26
+ port=port,
27
+ baudrate=baudrate,
28
+ bytesize=serial.EIGHTBITS,
29
+ parity=serial.PARITY_NONE,
30
+ stopbits=serial.STOPBITS_ONE,
31
+ timeout=timeout,
32
+ )
33
+ self.protocol: int | None = protocol
34
+ self.last_response_time: datetime | None = None
35
+
36
+ self.ser.reset_input_buffer()
37
+ self.ser.reset_output_buffer()
38
+
39
+ self._infer_protocol()
40
+
41
+ def _send_command(self, command: str):
42
+ if not self.ser.is_open:
43
+ raise serial.SerialException("Serial port is not open.")
44
+
45
+ if self.ser.in_waiting:
46
+ self.ser.reset_input_buffer()
47
+
48
+ full_command = f"{command}\r\n".encode("ascii")
49
+ self.ser.write(full_command)
50
+ logging.debug(f"Sending command: {command}")
51
+
52
+ def _read_lines(self) -> list[str]:
53
+ lines = []
54
+
55
+ if self.protocol == 1:
56
+ max_lines = 5
57
+ elif self.protocol == 2:
58
+ max_lines = 1
59
+ else:
60
+ raise NotImplementedError(f"Protocol {self.protocol} is not supported.")
61
+
62
+ for _ in range(max_lines):
63
+ line = self.ser.readline().decode("ascii", errors="ignore").strip()
64
+ if line:
65
+ lines.append(line)
66
+ return lines
67
+
68
+ def _infer_protocol(self):
69
+ if self.protocol:
70
+ return
71
+
72
+ self._send_command("P")
73
+ lines = self._read_lines()
74
+
75
+ if not lines:
76
+ raise RuntimeError("No response from scale.")
77
+
78
+ joined = " ".join(lines)
79
+ if any(h in joined for h in ["GS", "No.", "Total"]):
80
+ self.protocol = 1
81
+
82
+ match = re.search(r"[-+]? *([\d.]+)\s*([a-zA-Z]+)", lines[0])
83
+ if match:
84
+ self.protocol = 2
85
+
86
+ if self.protocol is None:
87
+ raise RuntimeError(f"Unrecognized scale output: {lines}")
88
+
89
+ def get_weight(self) -> float | None:
90
+ """"""
91
+ self._send_command("P")
92
+ lines = self._read_lines()
93
+
94
+ if not lines:
95
+ return None
96
+
97
+ if self.protocol == 1:
98
+ for line in lines:
99
+ if line.startswith(("GS", "NT", "GT")):
100
+ return self._parse_weight_line(line)
101
+ elif self.protocol == 2:
102
+ return self._parse_weight_line(lines[0])
103
+
104
+ return None
105
+
106
+ def tare(self):
107
+ self._send_command("T")
108
+
109
+ def zero(self):
110
+ self._send_command("Z")
111
+
112
+ def set_tare_value(self, value: float):
113
+ self._send_command(f"T{value:.3f}")
114
+
115
+ def _parse_weight_line(self, line: str) -> float | None:
116
+ line = re.sub(r"^(GS|NT|GT|ST|US)?\s*", "", line)
117
+ match = re.search(r"[-+]?\d+\.\d+", line)
118
+ if match:
119
+ return float(match.group(0))
120
+ return None
121
+
122
+ def is_connected(self) -> bool:
123
+ return self.ser and self.ser.is_open
124
+
125
+ def is_responsive(self) -> bool:
126
+ try:
127
+ self.ser.write(b"P\r\n")
128
+ line = self.ser.readline()
129
+ if line:
130
+ self.last_response_time = datetime.utcnow()
131
+ return True
132
+ except serial.SerialException:
133
+ pass
134
+ return False
135
+
136
+ def close(self):
137
+ if self.ser.is_open:
138
+ self.ser.close()
139
+
140
+
141
+ class AutoReconnectSerialScale:
142
+ def __init__(self, *args, retry_delay=2.0, **kwargs):
143
+ self._args = args
144
+ self._kwargs = kwargs
145
+ self._retry_delay = retry_delay
146
+ self._scale: SerialScale | None = None
147
+ self._connect()
148
+
149
+ def _connect(self):
150
+ while True:
151
+ try:
152
+ self._scale = SerialScale(*self._args, **self._kwargs)
153
+ print("SerialScale connected.")
154
+ return
155
+ except SerialException as e:
156
+ print(f"Serial connection failed: {e}. Retrying...")
157
+ time.sleep(self._retry_delay)
158
+
159
+ def _ensure_connection(self):
160
+ if self._scale is None or not self._scale.is_connected():
161
+ print("Lost connection. Attempting reconnect...")
162
+ self._connect()
163
+
164
+ def __getattr__(self, name):
165
+ """Forward method calls to internal SerialScale, with reconnection if needed."""
166
+
167
+ def method(*args, **kwargs):
168
+ self._ensure_connection()
169
+ try:
170
+ return getattr(self._scale, name)(*args, **kwargs)
171
+ except SerialException:
172
+ print("Serial error during operation. Reconnecting...")
173
+ self._connect()
174
+ return getattr(self._scale, name)(*args, **kwargs)
175
+
176
+ return method
177
+
178
+
179
+ class Scale:
180
+ """High-level bench-scale driver with the same interface as serial-scale-hx711.
181
+
182
+ Construction is lightweight; call start() to open the port and infer protocol.
183
+ This allows the same WeighingScaleBase adapter pattern used for the HX711 scale.
184
+ """
185
+
186
+ def __init__(
187
+ self,
188
+ serial_port: str,
189
+ baudrate: int = 4800,
190
+ timeout: float = 1.0,
191
+ protocol: int | None = None,
192
+ ) -> None:
193
+ self.serial_port = serial_port
194
+ self.baudrate = baudrate
195
+ self.timeout = timeout
196
+ self.protocol = protocol
197
+ self._scale: SerialScale | None = None
198
+
199
+ def start(self, timeout: float = 10.0) -> None:
200
+ """Open the serial port, infer protocol, and verify the scale is responsive."""
201
+ deadline = time.time() + timeout
202
+ last_exc: Exception | None = None
203
+ while time.time() < deadline:
204
+ try:
205
+ if self._scale is not None:
206
+ self._scale.close()
207
+ self._scale = None
208
+ self._scale = SerialScale(
209
+ port=self.serial_port,
210
+ baudrate=self.baudrate,
211
+ timeout=self.timeout,
212
+ protocol=self.protocol or 2,
213
+ )
214
+ if self._scale.is_responsive():
215
+ logging.info(f"Bench scale ready on {self.serial_port}")
216
+ return
217
+ except (SerialException, Exception) as exc:
218
+ last_exc = exc
219
+ time.sleep(0.2)
220
+ raise TimeoutError(
221
+ f"Bench scale on {self.serial_port} did not respond within {timeout}s. "
222
+ f"Last error: {last_exc}"
223
+ )
224
+
225
+ def tare(self) -> None:
226
+ assert self._scale is not None
227
+ self._scale.tare()
228
+ time.sleep(0.3)
229
+
230
+ def read_weight(self) -> float | None:
231
+ assert self._scale is not None
232
+ return self._scale.get_weight()
233
+
234
+ def read_weight_repeated(
235
+ self,
236
+ n_readings: int = 5,
237
+ inter_read_delay: float = 0.1,
238
+ ) -> list[float]:
239
+ readings = []
240
+ for _ in range(n_readings):
241
+ r = self.read_weight()
242
+ if r is not None:
243
+ readings.append(r)
244
+ time.sleep(inter_read_delay)
245
+ return readings
246
+
247
+ def read_weight_reliable(
248
+ self,
249
+ n_readings: int = 5,
250
+ inter_read_delay: float = 0.1,
251
+ measure: Callable = statistics.median,
252
+ ) -> float:
253
+ readings = self.read_weight_repeated(n_readings, inter_read_delay)
254
+ if not readings:
255
+ raise RuntimeError(
256
+ f"Bench scale on {self.serial_port} returned no valid readings "
257
+ f"after {n_readings} attempts."
258
+ )
259
+ return measure(readings)
260
+
261
+ def read_weight_blocking(
262
+ self,
263
+ n_valid: int = 3,
264
+ inter_read_delay: float = 0.2,
265
+ timeout: float = 30.0,
266
+ ) -> float:
267
+ readings = []
268
+ deadline = time.time() + timeout
269
+ while time.time() < deadline:
270
+ r = self.read_weight()
271
+ if r is not None:
272
+ readings.append(r)
273
+ if len(readings) >= n_valid:
274
+ return statistics.median(readings)
275
+ time.sleep(inter_read_delay)
276
+ raise TimeoutError(
277
+ f"Bench scale on {self.serial_port} could not produce {n_valid} valid "
278
+ f"readings within {timeout}s."
279
+ )
280
+
281
+ def disconnect(self) -> None:
282
+ if self._scale is not None:
283
+ self._scale.close()
284
+ self._scale = None
285
+
286
+ def __del__(self) -> None:
287
+ self.disconnect()
288
+
289
+
290
+ if __name__ == "__main__":
291
+ scale = SerialScale("/dev/ttyUSB0", protocol=2)
292
+
293
+ if scale.is_connected():
294
+ print("Scale is connected.")
295
+ if scale.is_responsive():
296
+ print("Scale is responsive.")
297
+ print("Current weight:", scale.get_weight())
298
+ else:
299
+ print("Scale is unresponsive.")
300
+ else:
301
+ print("Scale not connected.")
302
+
303
+ scale.close()
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: serial-scale-bench
3
+ Version: 0.2.0
4
+ Summary: Python driver for RS-232/USB bench scales (Kern, Mettler-Toledo, etc.).
5
+ Project-URL: Homepage, https://github.com/MurineShiftWork/serial-scale-bench
6
+ Project-URL: Documentation, https://larsrollik.github.io/serial-scale-bench/
7
+ Project-URL: Issue Tracker, https://github.com/MurineShiftWork/serial-scale-bench/issues
8
+ Author-email: "Lars B. Rollik" <L.B.Rollik@protonmail.com>
9
+ License: BSD 3-Clause License
10
+
11
+ Copyright (c) 2021, Lars B. Rollik
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without
15
+ modification, are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice, this
18
+ list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ 3. Neither the name of the copyright holder nor the names of its
25
+ contributors may be used to endorse or promote products derived from
26
+ this software without specific prior written permission.
27
+
28
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
32
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
34
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
36
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
+ License-File: LICENSE
39
+ Requires-Python: >=3.10
40
+ Requires-Dist: pyserial
41
+ Provides-Extra: api
42
+ Requires-Dist: fastapi; extra == 'api'
43
+ Requires-Dist: pydantic; extra == 'api'
44
+ Requires-Dist: uvicorn[standard]; extra == 'api'
45
+ Provides-Extra: dev
46
+ Requires-Dist: commitizen; extra == 'dev'
47
+ Requires-Dist: pre-commit; extra == 'dev'
48
+ Requires-Dist: pytest; extra == 'dev'
49
+ Requires-Dist: pytest-cov; extra == 'dev'
50
+ Provides-Extra: docs
51
+ Requires-Dist: mkdocs-material; extra == 'docs'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # serial-scale-bench
55
+
56
+ Python driver for RS-232/USB commercial bench scales (Kern, Mettler-Toledo, etc.).
57
+
58
+ Supports two ASCII response protocols:
59
+ - **Protocol 1** — multi-line header responses (`GS`, `NT`, `GT` prefixes); typical of
60
+ Kern and similar label-printing scales
61
+ - **Protocol 2** — single float line; minimal scales and simpler firmware
62
+
63
+ Protocol is auto-detected on first connection. A FastAPI HTTP server is included for
64
+ running the scale as a networked service (e.g. LabWatch integration).
65
+
66
+ ## Install
67
+
68
+ Base (serial only):
69
+
70
+ ```bash
71
+ pip install serial-scale-bench
72
+ ```
73
+
74
+ With HTTP API server:
75
+
76
+ ```bash
77
+ pip install "serial-scale-bench[api]"
78
+ ```
79
+
80
+ Or editable from this repo:
81
+
82
+ ```bash
83
+ pip install -e .
84
+ ```
85
+
86
+ ## Usage
87
+
88
+ ```python
89
+ from serial_scale_bench import SerialScale
90
+
91
+ scale = SerialScale(port="/dev/ttyUSB0", baudrate=9600)
92
+ scale.tare()
93
+ weight = scale.get_weight() # float grams or None
94
+ scale.close()
95
+ ```
96
+
97
+ ### Auto-reconnect wrapper
98
+
99
+ ```python
100
+ from serial_scale_bench import AutoReconnectSerialScale
101
+
102
+ scale = AutoReconnectSerialScale(port="/dev/ttyUSB0")
103
+ weight = scale.get_weight() # reconnects automatically on error
104
+ ```
105
+
106
+ ## HTTP API server
107
+
108
+ Start a local REST server exposing the scale over HTTP:
109
+
110
+ ```bash
111
+ python -m serial_scale_bench.entrypoint --device /dev/ttyUSB0 --port 8080 --scale-id bench1
112
+ ```
113
+
114
+ Endpoints: `GET /weight`, `POST /tare`, `POST /zero`, `GET /status`, `GET /ping`.
115
+
116
+ ## API reference
117
+
118
+ | Method | Description |
119
+ |---|---|
120
+ | `get_weight()` | Poll current weight (float or None) |
121
+ | `tare()` | Send tare command |
122
+ | `zero()` | Send zero command |
123
+ | `set_tare_value(value)` | Set absolute tare |
124
+ | `is_connected()` | True if serial port is open |
125
+ | `is_responsive()` | True if scale responds to ping |
126
+ | `close()` | Close serial connection |
127
+
128
+ ## murineshiftwork integration
129
+
130
+ Planned: `BenchScaleAdapter` in `murineshiftwork.logic.scale`. Install with:
131
+
132
+ ```bash
133
+ pip install "murineshiftwork[calibration]"
134
+ ```
@@ -0,0 +1,11 @@
1
+ serial_scale_bench/__init__.py,sha256=_N0FPvq7LRcS8xmlpGVaKDiARURVmAx_Mhm3EoTn8V0,365
2
+ serial_scale_bench/_version.py,sha256=s9U3X54Pdr-Jlh9GP6LBaa_VRos7qs_wtrVefUHHNIA,520
3
+ serial_scale_bench/api.py,sha256=hDVKtgUrOnMbo0oAeEni7VQRtqwR_LnDzT9Z8ORBsBs,2207
4
+ serial_scale_bench/cli.py,sha256=8CN3UdAyNm0d5iyV3uREU88xt52S7IojTKTNPmqlKBg,751
5
+ serial_scale_bench/entrypoint.py,sha256=vYBBCGV9JrikpgvqpmmNmVhEKCEP8sqQAjSGjKEQGt4,551
6
+ serial_scale_bench/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ serial_scale_bench/scale.py,sha256=qZQUb_EQOn1NqN76M9YOT7Q0wQhAjOsWH-xzVEMo_K4,9040
8
+ serial_scale_bench-0.2.0.dist-info/METADATA,sha256=p6S5epnusgkH2jQBepgHMdo17jx3Cl6mlPJKVpM7tWY,4655
9
+ serial_scale_bench-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ serial_scale_bench-0.2.0.dist-info/licenses/LICENSE,sha256=Ew8cEKZj5F51f1JVCmPZAESmeIe0HCdWdlo_NVKD-8s,1522
11
+ serial_scale_bench-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2021, Lars B. Rollik
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.