ceilopyter 0.1.0__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.
- ceilopyter-0.1.0/.github/workflows/publish.yml +36 -0
- ceilopyter-0.1.0/.github/workflows/test.yml +30 -0
- ceilopyter-0.1.0/.gitignore +1 -0
- ceilopyter-0.1.0/.pre-commit-config.yaml +40 -0
- ceilopyter-0.1.0/CHANGELOG.md +10 -0
- ceilopyter-0.1.0/LICENSE +21 -0
- ceilopyter-0.1.0/PKG-INFO +36 -0
- ceilopyter-0.1.0/README.md +14 -0
- ceilopyter-0.1.0/ceilopyter/__init__.py +6 -0
- ceilopyter-0.1.0/ceilopyter/common.py +43 -0
- ceilopyter-0.1.0/ceilopyter/py.typed +0 -0
- ceilopyter-0.1.0/ceilopyter/read_cl.py +193 -0
- ceilopyter-0.1.0/ceilopyter/read_cs.py +223 -0
- ceilopyter-0.1.0/ceilopyter/read_ct.py +153 -0
- ceilopyter-0.1.0/ceilopyter/utils.py +90 -0
- ceilopyter-0.1.0/ceilopyter/version.py +1 -0
- ceilopyter-0.1.0/pyproject.toml +48 -0
- ceilopyter-0.1.0/tests/data/celio_chennai_2025-03-11.dat +30 -0
- ceilopyter-0.1.0/tests/test_cl.py +17 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Upload Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.10"
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: python -m pip install build
|
|
22
|
+
- name: Build package
|
|
23
|
+
run: python -m build
|
|
24
|
+
- name: Publish package
|
|
25
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
26
|
+
- name: Generate changelog
|
|
27
|
+
run: |
|
|
28
|
+
version=${GITHUB_REF#refs/tags/v}
|
|
29
|
+
sed "0,/^## ${version//./\\.}/d;/^## /,\$d" CHANGELOG.md > ${{ github.workspace }}-CHANGELOG.txt
|
|
30
|
+
echo "name=Ceilopyter $version" >> $GITHUB_OUTPUT
|
|
31
|
+
id: changelog
|
|
32
|
+
- name: Create release
|
|
33
|
+
uses: softprops/action-gh-release@v1
|
|
34
|
+
with:
|
|
35
|
+
name: ${{ steps.changelog.outputs.name }}
|
|
36
|
+
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Run tests
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
strategy:
|
|
8
|
+
matrix:
|
|
9
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
10
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
11
|
+
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
cache: "pip"
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: |
|
|
22
|
+
pip install --upgrade pip
|
|
23
|
+
pip install .[dev,test]
|
|
24
|
+
- name: Run pre-commit checks
|
|
25
|
+
if: startsWith(matrix.os, 'ubuntu-')
|
|
26
|
+
run: |
|
|
27
|
+
pre-commit run --all-files --show-diff-on-failure
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: |
|
|
30
|
+
pytest
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__pycache__
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
exclude: ^tests/data/
|
|
2
|
+
repos:
|
|
3
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
4
|
+
rev: v5.0.0
|
|
5
|
+
hooks:
|
|
6
|
+
- id: check-case-conflict
|
|
7
|
+
- id: check-executables-have-shebangs
|
|
8
|
+
- id: check-merge-conflict
|
|
9
|
+
- id: check-shebang-scripts-are-executable
|
|
10
|
+
- id: end-of-file-fixer
|
|
11
|
+
- id: fix-byte-order-marker
|
|
12
|
+
- id: mixed-line-ending
|
|
13
|
+
args: ["--fix", "lf"]
|
|
14
|
+
- id: trailing-whitespace
|
|
15
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
16
|
+
rev: v0.11.0
|
|
17
|
+
hooks:
|
|
18
|
+
- id: ruff
|
|
19
|
+
args: ["--fix"]
|
|
20
|
+
- id: ruff-format
|
|
21
|
+
- repo: local
|
|
22
|
+
hooks:
|
|
23
|
+
- id: mypy
|
|
24
|
+
name: mypy
|
|
25
|
+
entry: mypy
|
|
26
|
+
language: system
|
|
27
|
+
types: [python]
|
|
28
|
+
require_serial: true
|
|
29
|
+
- repo: https://github.com/pre-commit/mirrors-prettier
|
|
30
|
+
rev: v3.1.0
|
|
31
|
+
hooks:
|
|
32
|
+
- id: prettier
|
|
33
|
+
- repo: https://github.com/pappasam/toml-sort
|
|
34
|
+
rev: v0.24.2
|
|
35
|
+
hooks:
|
|
36
|
+
- id: toml-sort-fix
|
|
37
|
+
- repo: https://github.com/crate-ci/typos
|
|
38
|
+
rev: v1.30.2
|
|
39
|
+
hooks:
|
|
40
|
+
- id: typos
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## 0.1.0 – 2025-03-24
|
|
9
|
+
|
|
10
|
+
- Initial release
|
ceilopyter-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Finnish Meteorological Institute
|
|
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,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ceilopyter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read ceilometer data
|
|
5
|
+
Author-email: Tuomas Siipola <tuomas.siipola@fmi.fi>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
17
|
+
Requires-Dist: release-version; extra == 'dev'
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: mypy; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Ceilopyter
|
|
24
|
+
|
|
25
|
+
Python package for reading ceilometer data.
|
|
26
|
+
|
|
27
|
+
## Supported ceilometers
|
|
28
|
+
|
|
29
|
+
- Campbell Scientific CS135
|
|
30
|
+
- Vaisala CL31
|
|
31
|
+
- Vaisala CL51
|
|
32
|
+
- Vaisala CT25K
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .read_cl import read_cl_file as read_cl_file
|
|
2
|
+
from .read_cl import read_cl_message as read_cl_message
|
|
3
|
+
from .read_cs import read_cs_file as read_cs_file
|
|
4
|
+
from .read_cs import read_cs_message as read_cs_message
|
|
5
|
+
from .read_ct import read_ct_file as read_ct_file
|
|
6
|
+
from .read_ct import read_ct_message as read_ct_message
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InvalidMessageError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Message:
|
|
13
|
+
"""Data message from ceilometer.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
range_resolution: Range resolution (m).
|
|
17
|
+
laser_pulse_energy: Laser pulse energy (%).
|
|
18
|
+
laser_temperature: Laser temperature (degC).
|
|
19
|
+
tilt_angle: Tilt angle (deg).
|
|
20
|
+
background_light: Background light (mV).
|
|
21
|
+
n_pulses: Number of pulses.
|
|
22
|
+
sample_rate: Sampling rate (MHz).
|
|
23
|
+
beta: Backscatter coefficient (sr-1 m-1).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
range_resolution: int
|
|
27
|
+
laser_pulse_energy: int
|
|
28
|
+
laser_temperature: int
|
|
29
|
+
tilt_angle: int
|
|
30
|
+
background_light: int
|
|
31
|
+
n_pulses: int
|
|
32
|
+
sample_rate: int
|
|
33
|
+
beta: npt.NDArray[np.floating]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Status:
|
|
37
|
+
def __repr__(self):
|
|
38
|
+
return (
|
|
39
|
+
self.__class__.__name__
|
|
40
|
+
+ "("
|
|
41
|
+
+ ", ".join(f"{key}=True" for key, value in vars(self).items() if value)
|
|
42
|
+
+ ")"
|
|
43
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import utils
|
|
8
|
+
from .common import InvalidMessageError, Message, Status
|
|
9
|
+
|
|
10
|
+
FORMAT = utils.date_format_to_regex(rb"-%Y-%m-%d %H:%M:%S\r?\n")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClStatus(Status):
|
|
14
|
+
"""Decoded status bits from Vaisala CL31 or CL51.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
transmitter_shutoff_alarm: Transmitter shut-off.
|
|
18
|
+
transmitter_fail_alarm: Transmitter failure.
|
|
19
|
+
receiver_fail_alarm: Receiver failure.
|
|
20
|
+
voltage_fail_alarm: Voltage failure.
|
|
21
|
+
memory_error_alarm: Memory error.
|
|
22
|
+
light_path_obstruction_alarm: Light path obstruction.
|
|
23
|
+
receiver_saturation_alarm: Receiver saturation.
|
|
24
|
+
coaxial_cable_fail_alarm: Coaxial cable failure.
|
|
25
|
+
ceilometer_board_fail_alarm: Ceilometer engine board failure.
|
|
26
|
+
window_contam_warning: Window contamination.
|
|
27
|
+
battery_low_warning: Battery voltage low.
|
|
28
|
+
transmitter_expire_warning: Transmitter expires.
|
|
29
|
+
humidity_high_warning: High humidity.
|
|
30
|
+
blower_fail_warning: Blower failure.
|
|
31
|
+
humidity_sensor_fail_warning: Humidity sensor failure.
|
|
32
|
+
heater_fault_warning: Heater fault.
|
|
33
|
+
background_radiance_warning: High background radiance.
|
|
34
|
+
ceilometer_board_warning: Ceilometer engine board failure.
|
|
35
|
+
battery_fail_warning: Battery failure.
|
|
36
|
+
laser_monitor_fail_warning: Laser monitor failure.
|
|
37
|
+
receiver_warning: Receiver warning.
|
|
38
|
+
tilt_angle_warning: Tilt angle > 45 degrees warning.
|
|
39
|
+
blower_status: Blower is on.
|
|
40
|
+
blower_heater_status: Blower heater is on.
|
|
41
|
+
internal_heater_status: Internal heater is on.
|
|
42
|
+
battery_power_status: Working from battery.
|
|
43
|
+
standby_status: Standby mode is on.
|
|
44
|
+
self_test_status: Self test in progress.
|
|
45
|
+
manual_data_status: Manual data acquisition settings are effective.
|
|
46
|
+
units_meters: Units are meters if on, else feet.
|
|
47
|
+
manual_blower_status: Manual blower control.
|
|
48
|
+
polling_mode_status: Polling mode is on.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, status_bits: int):
|
|
52
|
+
self.transmitter_shutoff_alarm = bool(status_bits & 0x800000000000)
|
|
53
|
+
self.transmitter_fail_alarm = bool(status_bits & 0x400000000000)
|
|
54
|
+
self.receiver_fail_alarm = bool(status_bits & 0x200000000000)
|
|
55
|
+
self.voltage_fail_alarm = bool(status_bits & 0x100000000000)
|
|
56
|
+
self.memory_error_alarm = bool(status_bits & 0x040000000000)
|
|
57
|
+
self.light_path_obstruction_alarm = bool(status_bits & 0x020000000000)
|
|
58
|
+
self.receiver_saturation_alarm = bool(status_bits & 0x010000000000)
|
|
59
|
+
self.coaxial_cable_fail_alarm = bool(status_bits & 0x000200000000)
|
|
60
|
+
self.ceilometer_board_fail_alarm = bool(status_bits & 0x000100000000)
|
|
61
|
+
self.window_contam_warning = bool(status_bits & 0x000080000000)
|
|
62
|
+
self.battery_low_warning = bool(status_bits & 0x000040000000)
|
|
63
|
+
self.transmitter_expire_warning = bool(status_bits & 0x000020000000)
|
|
64
|
+
self.humidity_high_warning = bool(status_bits & 0x000010000000)
|
|
65
|
+
self.blower_fail_warning = bool(status_bits & 0x000004000000)
|
|
66
|
+
self.humidity_sensor_fail_warning = bool(status_bits & 0x000001000000)
|
|
67
|
+
self.heater_fault_warning = bool(status_bits & 0x000000800000)
|
|
68
|
+
self.background_radiance_warning = bool(status_bits & 0x000000400000)
|
|
69
|
+
self.ceilometer_board_warning = bool(status_bits & 0x000000200000)
|
|
70
|
+
self.battery_fail_warning = bool(status_bits & 0x000000100000)
|
|
71
|
+
self.laser_monitor_fail_warning = bool(status_bits & 0x000000080000)
|
|
72
|
+
self.receiver_warning = bool(status_bits & 0x000000040000)
|
|
73
|
+
self.tilt_angle_warning = bool(status_bits & 0x000000020000)
|
|
74
|
+
self.blower_status = bool(status_bits & 0x000000008000)
|
|
75
|
+
self.blower_heater_status = bool(status_bits & 0x000000004000)
|
|
76
|
+
self.internal_heater_status = bool(status_bits & 0x000000002000)
|
|
77
|
+
self.battery_power_status = bool(status_bits & 0x000000001000)
|
|
78
|
+
self.standby_status = bool(status_bits & 0x000000000800)
|
|
79
|
+
self.self_test_status = bool(status_bits & 0x000000000400)
|
|
80
|
+
self.manual_data_status = bool(status_bits & 0x000000000200)
|
|
81
|
+
self.units_meters = bool(status_bits & 0x000000000080)
|
|
82
|
+
self.manual_blower_status = bool(status_bits & 0x000000000040)
|
|
83
|
+
self.polling_mode_status = bool(status_bits & 0x000000000020)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class ClMessage(Message):
|
|
88
|
+
"""Data message from Vaisala CL31 or CL51.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
window_transmission: Window transmission (%).
|
|
92
|
+
status: Decoded status bits.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
window_transmission: int
|
|
96
|
+
status: ClStatus
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def read_cl_file(
|
|
100
|
+
filename: str | PathLike,
|
|
101
|
+
) -> tuple[list[datetime.datetime], list[ClMessage]]:
|
|
102
|
+
"""Read Vaisala CL31 or CL51 file."""
|
|
103
|
+
content = Path(filename).read_bytes()
|
|
104
|
+
time = []
|
|
105
|
+
data = []
|
|
106
|
+
for ts, msg in utils.parse_file(content, FORMAT):
|
|
107
|
+
try:
|
|
108
|
+
data.append(read_cl_message(msg))
|
|
109
|
+
time.append(ts)
|
|
110
|
+
except (InvalidMessageError, ValueError) as e:
|
|
111
|
+
logging.debug("Invalid message: %s", e)
|
|
112
|
+
return time, data
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_cl_message(message: bytes) -> ClMessage:
|
|
116
|
+
"""Read Vaisala CL31 or CL51 data message."""
|
|
117
|
+
lines = iter(message.splitlines())
|
|
118
|
+
|
|
119
|
+
# Line 1
|
|
120
|
+
line1 = utils.next_line(lines, 8, prefix=b"\x01", suffix=b"\x02")
|
|
121
|
+
if line1[:2] != b"CL":
|
|
122
|
+
msg = "Invalid line 1"
|
|
123
|
+
raise InvalidMessageError(msg)
|
|
124
|
+
msg_no = line1[6:7]
|
|
125
|
+
if msg_no not in (b"1", b"2"):
|
|
126
|
+
msg = f"Invalid message number: {msg_no.decode()}"
|
|
127
|
+
raise InvalidMessageError(msg)
|
|
128
|
+
subclass = line1[7:8]
|
|
129
|
+
if subclass in (b"1", b"2", b"3", b"4"):
|
|
130
|
+
line3_len = 31 # CL31
|
|
131
|
+
elif subclass == b"6":
|
|
132
|
+
line3_len = 40 # CL51
|
|
133
|
+
else:
|
|
134
|
+
msg = f"Invalid message subclass: {subclass.decode()}"
|
|
135
|
+
raise InvalidMessageError(msg)
|
|
136
|
+
check_content = line1 + b"\x02\r\n"
|
|
137
|
+
|
|
138
|
+
# Line 2
|
|
139
|
+
line2 = utils.next_line(lines, 33)
|
|
140
|
+
status_bits = int(line2[21:], 16)
|
|
141
|
+
status = ClStatus(status_bits)
|
|
142
|
+
check_content += line2 + b"\r\n"
|
|
143
|
+
|
|
144
|
+
# Line 3: sky condition
|
|
145
|
+
if msg_no == b"2":
|
|
146
|
+
line3 = utils.next_line(lines).rjust(line3_len)
|
|
147
|
+
check_content += line3 + b"\r\n"
|
|
148
|
+
|
|
149
|
+
# Line 3/4
|
|
150
|
+
line4 = utils.next_line(lines, 47)
|
|
151
|
+
scale = int(line4[0:5])
|
|
152
|
+
range_resolution = int(line4[6:8])
|
|
153
|
+
n_samples = int(line4[9:13])
|
|
154
|
+
laser_pulse_energy = int(line4[14:17])
|
|
155
|
+
laser_temperature = int(line4[18:21])
|
|
156
|
+
window_transmission = int(line4[22:25])
|
|
157
|
+
tilt_angle = int(line4[26:28])
|
|
158
|
+
background_light = int(line4[29:33])
|
|
159
|
+
n_pulses = 1024 * int(line4[35:39])
|
|
160
|
+
sample_rate = int(line4[41:43])
|
|
161
|
+
check_content += line4 + b"\r\n"
|
|
162
|
+
|
|
163
|
+
# Line 4/5: profile
|
|
164
|
+
line5 = utils.next_line(lines, 5 * n_samples)
|
|
165
|
+
raw = utils.read_hex(line5, 5, n_samples)
|
|
166
|
+
beta = raw * 1e-8 * scale / 100
|
|
167
|
+
check_content += line5 + b"\r\n\x03"
|
|
168
|
+
|
|
169
|
+
# Line 5/6: checksum
|
|
170
|
+
line6 = utils.next_line(lines, 4, prefix=b"\x03", suffix=b"\x04")
|
|
171
|
+
expected_checksum = int(line6, 16)
|
|
172
|
+
|
|
173
|
+
actual_checksum = utils.crc16(check_content)
|
|
174
|
+
if expected_checksum != actual_checksum:
|
|
175
|
+
msg = (
|
|
176
|
+
"Invalid checksum: "
|
|
177
|
+
f"expected {expected_checksum:04x}, "
|
|
178
|
+
f"got {actual_checksum:04x}"
|
|
179
|
+
)
|
|
180
|
+
raise InvalidMessageError(msg)
|
|
181
|
+
|
|
182
|
+
return ClMessage(
|
|
183
|
+
range_resolution=range_resolution,
|
|
184
|
+
laser_pulse_energy=laser_pulse_energy,
|
|
185
|
+
laser_temperature=laser_temperature,
|
|
186
|
+
window_transmission=window_transmission,
|
|
187
|
+
tilt_angle=tilt_angle,
|
|
188
|
+
background_light=background_light,
|
|
189
|
+
n_pulses=n_pulses,
|
|
190
|
+
sample_rate=sample_rate,
|
|
191
|
+
status=status,
|
|
192
|
+
beta=beta,
|
|
193
|
+
)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import utils
|
|
8
|
+
from .common import InvalidMessageError, Message, Status
|
|
9
|
+
|
|
10
|
+
FORMATS = [
|
|
11
|
+
utils.date_format_to_regex(rb"%Y-%m-%dT%H:%M:%S.%f,"),
|
|
12
|
+
utils.date_format_to_regex(rb"%%% %Y/%m/%d %H:%M:%S %%%\r?\n"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CsStatus(Status):
|
|
17
|
+
"""Decoded status bits from Campbell Scientific CS135.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
units_meters: Units. Feet = 0, metre = 1.
|
|
21
|
+
dsp_clock_warning: DSP clock out of specification.
|
|
22
|
+
laser_temp_alarm: Laser shut down due to operating temperature out of range.
|
|
23
|
+
battery_voltage_warning: The lead acid battery voltage is reading low.
|
|
24
|
+
mains_supply_alarm: Mains supply has failed (Required a PSU to be present).
|
|
25
|
+
heater_temp_warning: The external heater blower assembly temperature is out of
|
|
26
|
+
bounds.
|
|
27
|
+
heater_blower_alarm: External heater blower failure.
|
|
28
|
+
psu_temp_warning: The PSUs internal temperature is high.
|
|
29
|
+
psu_os_alarm: PSU OS has failed its signature check.
|
|
30
|
+
dsp_psu_comm_alarm: No communications between DSP and PSU.
|
|
31
|
+
laser_window_warning: Photo diode and Laser windows are dirty. This can only be
|
|
32
|
+
set if the laser is on.
|
|
33
|
+
tilt_angle_warning: Tilt beyond limit set by user, default 45 degrees.
|
|
34
|
+
dsp_inclinometer_comm_alarm: No communications between DSP and inclinometer
|
|
35
|
+
board.
|
|
36
|
+
sensor_humidity_warning: The sensors internal humidity is high.
|
|
37
|
+
dsp_temp_humidity_comm_alarm: Communications to the DSP board temperature and
|
|
38
|
+
humidity chip have failed.
|
|
39
|
+
dsp_voltage_warning: DSP input supply voltage is low.
|
|
40
|
+
self_test_status: Self-test active.
|
|
41
|
+
watchdog_status: Watch dog counter updated.
|
|
42
|
+
user_settings_alarm: User setting stored in flash failed their signature checks.
|
|
43
|
+
dsp_calibration_alarm: DSP factory calibration stored in flash has failed its
|
|
44
|
+
signature check.
|
|
45
|
+
dsp_os_alarm: DSP board OS signature test failed.
|
|
46
|
+
dsp_ram_alarm: DSP board RAM test failed.
|
|
47
|
+
dsp_psu_warning: DSP boards on board PSUs are out of bounds.
|
|
48
|
+
top_storage_alarm: TOP board non-volatile storage is corrupt.
|
|
49
|
+
top_os_alarm: TOP board OS signature test has failed.
|
|
50
|
+
top_adc_dac_warning: TOP boards ADC and DAC are not within specifications.
|
|
51
|
+
top_psu_warning: TOP boards on board PSUs are out of bounds.
|
|
52
|
+
top_dsp_comm_alarm: Communications have failed between TOP board and the DSP.
|
|
53
|
+
photo_diode_radiance_warning: Photo diode background radiance is out of range.
|
|
54
|
+
photo_diode_temp_warning: Photo diode temperature is out of range.
|
|
55
|
+
photo_diode_saturation_warning: Photo diode is saturated.
|
|
56
|
+
photo_diode_calibrator_temp_warning: Photo diode calibrator temperature is out
|
|
57
|
+
of range.
|
|
58
|
+
photo_diode_calibrator_alarm: Photo diode calibrator has failed.
|
|
59
|
+
sensor_gain_warning: The sensor could not reach the desired gain levels.
|
|
60
|
+
laser_runtime_alarm: Laser run time or maximum laser drive current has been
|
|
61
|
+
exceeded.
|
|
62
|
+
laser_temp_warning: Laser temperature out of range.
|
|
63
|
+
laser_thermistor_alarm: Laser thermistor failure.
|
|
64
|
+
laser_obscured_warning: Laser is obscured. This can only be set if the laser is
|
|
65
|
+
on.
|
|
66
|
+
laser_power_alarm: Laser did not achieve significant output power.
|
|
67
|
+
laser_max_power_alarm: Laser max power exceeded.
|
|
68
|
+
laser_drive_current_alarm: Laser max drive current exceeded.
|
|
69
|
+
laser_monitor_temp_warning: Laser power monitor temperature out of range.
|
|
70
|
+
laser_monitor_test_alarm: Laser power monitor test fail.
|
|
71
|
+
laser_shutdown_status: Laser shutdown by top board.
|
|
72
|
+
laser_off_status: Laser is off.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, status_bits: int):
|
|
76
|
+
self.units_meters = bool(status_bits & 0x800000000000)
|
|
77
|
+
self.dsp_clock_warning = bool(status_bits & 0x080000000000)
|
|
78
|
+
self.laser_temp_alarm = bool(status_bits & 0x040000000000)
|
|
79
|
+
self.battery_voltage_warning = bool(status_bits & 0x020000000000)
|
|
80
|
+
self.mains_supply_alarm = bool(status_bits & 0x010000000000)
|
|
81
|
+
self.heater_temp_warning = bool(status_bits & 0x008000000000)
|
|
82
|
+
self.heater_blower_alarm = bool(status_bits & 0x004000000000)
|
|
83
|
+
self.psu_temp_warning = bool(status_bits & 0x002000000000)
|
|
84
|
+
self.psu_os_alarm = bool(status_bits & 0x001000000000)
|
|
85
|
+
self.dsp_psu_comm_alarm = bool(status_bits & 0x000800000000)
|
|
86
|
+
self.laser_window_warning = bool(status_bits & 0x000400000000)
|
|
87
|
+
self.tilt_angle_warning = bool(status_bits & 0x000200000000)
|
|
88
|
+
self.dsp_inclinometer_comm_alarm = bool(status_bits & 0x000100000000)
|
|
89
|
+
self.sensor_humidity_warning = bool(status_bits & 0x000080000000)
|
|
90
|
+
self.dsp_temp_humidity_comm_alarm = bool(status_bits & 0x000040000000)
|
|
91
|
+
self.dsp_voltage_warning = bool(status_bits & 0x000020000000)
|
|
92
|
+
self.self_test_status = bool(status_bits & 0x000010000000)
|
|
93
|
+
self.watchdog_status = bool(status_bits & 0x000008000000)
|
|
94
|
+
self.user_settings_alarm = bool(status_bits & 0x000004000000)
|
|
95
|
+
self.dsp_calibration_alarm = bool(status_bits & 0x000002000000)
|
|
96
|
+
self.dsp_os_alarm = bool(status_bits & 0x000001000000)
|
|
97
|
+
self.dsp_ram_alarm = bool(status_bits & 0x000000800000)
|
|
98
|
+
self.dsp_psu_warning = bool(status_bits & 0x000000400000)
|
|
99
|
+
self.top_storage_alarm = bool(status_bits & 0x000000200000)
|
|
100
|
+
self.top_os_alarm = bool(status_bits & 0x000000100000)
|
|
101
|
+
self.top_adc_dac_warning = bool(status_bits & 0x000000080000)
|
|
102
|
+
self.top_psu_warning = bool(status_bits & 0x000000040000)
|
|
103
|
+
self.top_dsp_comm_alarm = bool(status_bits & 0x000000020000)
|
|
104
|
+
self.photo_diode_radiance_warning = bool(status_bits & 0x000000010000)
|
|
105
|
+
self.photo_diode_temp_warning = bool(status_bits & 0x000000008000)
|
|
106
|
+
self.photo_diode_saturation_warning = bool(status_bits & 0x000000004000)
|
|
107
|
+
self.photo_diode_calibrator_temp_warning = bool(status_bits & 0x000000002000)
|
|
108
|
+
self.photo_diode_calibrator_alarm = bool(status_bits & 0x000000001000)
|
|
109
|
+
self.sensor_gain_warning = bool(status_bits & 0x000000000800)
|
|
110
|
+
self.laser_runtime_alarm = bool(status_bits & 0x000000000400)
|
|
111
|
+
self.laser_temp_warning = bool(status_bits & 0x000000000200)
|
|
112
|
+
self.laser_thermistor_alarm = bool(status_bits & 0x000000000100)
|
|
113
|
+
self.laser_obscured_warning = bool(status_bits & 0x000000000080)
|
|
114
|
+
self.laser_power_alarm = bool(status_bits & 0x000000000040)
|
|
115
|
+
self.laser_max_power_alarm = bool(status_bits & 0x000000000020)
|
|
116
|
+
self.laser_drive_current_alarm = bool(status_bits & 0x000000000010)
|
|
117
|
+
self.laser_monitor_temp_warning = bool(status_bits & 0x000000000008)
|
|
118
|
+
self.laser_monitor_test_alarm = bool(status_bits & 0x000000000004)
|
|
119
|
+
self.laser_shutdown_status = bool(status_bits & 0x000000000002)
|
|
120
|
+
self.laser_off_status = bool(status_bits & 0x000000000001)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class CsMessage(Message):
|
|
125
|
+
"""Data message from Campbell Scientific CS135.
|
|
126
|
+
|
|
127
|
+
Attributes:
|
|
128
|
+
window_transmission: Window transmission (%).
|
|
129
|
+
status: Decoded status bits.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
window_transmission: int # %
|
|
133
|
+
status: CsStatus
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def read_cs_file(
|
|
137
|
+
filename: str | PathLike,
|
|
138
|
+
) -> tuple[list[datetime.datetime], list[CsMessage]]:
|
|
139
|
+
"""Read Campbell Scientific CS135 file."""
|
|
140
|
+
content = Path(filename).read_bytes()
|
|
141
|
+
time = []
|
|
142
|
+
data = []
|
|
143
|
+
for fmt in FORMATS:
|
|
144
|
+
for ts, msg in utils.parse_file(content, fmt):
|
|
145
|
+
try:
|
|
146
|
+
data.append(read_cs_message(msg))
|
|
147
|
+
time.append(ts)
|
|
148
|
+
except (InvalidMessageError, ValueError) as e:
|
|
149
|
+
logging.debug("Invalid message: %s", e)
|
|
150
|
+
return time, data
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def read_cs_message(message: bytes) -> CsMessage:
|
|
154
|
+
"""Read Campbell Scientific CS135 data message."""
|
|
155
|
+
lines = iter(message.splitlines())
|
|
156
|
+
|
|
157
|
+
# Line 1
|
|
158
|
+
line1 = utils.next_line(lines, 9, b"\x01", b"\x02")
|
|
159
|
+
if line1[:2] != b"CS":
|
|
160
|
+
msg = "Invalid line 1"
|
|
161
|
+
raise InvalidMessageError(msg)
|
|
162
|
+
msg_no = line1[6:9]
|
|
163
|
+
if msg_no not in (b"002", b"004"):
|
|
164
|
+
msg = f"Invalid message number: {msg_no.decode()}"
|
|
165
|
+
raise InvalidMessageError(msg)
|
|
166
|
+
check_content = line1 + b"\x02\r\n"
|
|
167
|
+
|
|
168
|
+
# Line 2
|
|
169
|
+
line2 = utils.next_line(lines, 43)
|
|
170
|
+
status_bits = int(line2[31:], 16)
|
|
171
|
+
status = CsStatus(status_bits)
|
|
172
|
+
window_transmission = int(line2[3:6])
|
|
173
|
+
check_content += line2 + b"\r\n"
|
|
174
|
+
|
|
175
|
+
# Line 3: sky condition
|
|
176
|
+
if msg_no == b"004":
|
|
177
|
+
line3 = utils.next_line(lines).rjust(40)
|
|
178
|
+
check_content += line3 + b"\r\n"
|
|
179
|
+
|
|
180
|
+
# Line 3/4
|
|
181
|
+
line4 = utils.next_line(lines, 41)
|
|
182
|
+
scale = int(line4[0:5])
|
|
183
|
+
range_resolution = int(line4[6:8])
|
|
184
|
+
n_samples = int(line4[9:13])
|
|
185
|
+
laser_pulse_energy = int(line4[14:17])
|
|
186
|
+
laser_temperature = int(line4[18:21])
|
|
187
|
+
tilt_angle = int(line4[22:24])
|
|
188
|
+
background_light = int(line4[25:29])
|
|
189
|
+
n_pulses = 1000 * int(line4[30:34])
|
|
190
|
+
sample_rate = int(line4[35:37])
|
|
191
|
+
check_content += line4 + b"\r\n"
|
|
192
|
+
|
|
193
|
+
# Line 4/5: profile
|
|
194
|
+
line5 = utils.next_line(lines, 5 * n_samples)
|
|
195
|
+
raw = utils.read_hex(line5, 5, n_samples)
|
|
196
|
+
beta = raw * 1e-8 * scale / 100
|
|
197
|
+
check_content += line5 + b"\r\n\x03"
|
|
198
|
+
|
|
199
|
+
# Line 5/6: checksum
|
|
200
|
+
line6 = utils.next_line(lines, 4, b"\x03", b"\x04")
|
|
201
|
+
expected_checksum = int(line6, 16)
|
|
202
|
+
|
|
203
|
+
actual_checksum = utils.crc16(check_content)
|
|
204
|
+
if expected_checksum != actual_checksum:
|
|
205
|
+
msg = (
|
|
206
|
+
"Invalid checksum: "
|
|
207
|
+
f"expected {expected_checksum:04x}, "
|
|
208
|
+
f"got {actual_checksum:04x}"
|
|
209
|
+
)
|
|
210
|
+
raise InvalidMessageError(msg)
|
|
211
|
+
|
|
212
|
+
return CsMessage(
|
|
213
|
+
range_resolution=range_resolution,
|
|
214
|
+
laser_pulse_energy=laser_pulse_energy,
|
|
215
|
+
laser_temperature=laser_temperature,
|
|
216
|
+
window_transmission=window_transmission,
|
|
217
|
+
tilt_angle=tilt_angle,
|
|
218
|
+
background_light=background_light,
|
|
219
|
+
n_pulses=n_pulses,
|
|
220
|
+
sample_rate=sample_rate,
|
|
221
|
+
status=status,
|
|
222
|
+
beta=beta,
|
|
223
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from os import PathLike
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import utils
|
|
8
|
+
from .common import InvalidMessageError, Message, Status
|
|
9
|
+
|
|
10
|
+
FORMAT = utils.date_format_to_regex(rb"-%Y-%m-%d %H:%M:%S\r?\n")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CtStatus(Status):
|
|
14
|
+
"""Decoded status bits from Vaisala CT25K.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
laser_temp_shutoff_alarm: Laser temperature shut-off.
|
|
18
|
+
laser_fail_alarm: Laser failure.
|
|
19
|
+
receiver_fail_alarm: Receiver failure.
|
|
20
|
+
voltage_fail_alarm: Voltage failure.
|
|
21
|
+
window_contam_warning: Window contaminated.
|
|
22
|
+
battery_low_warning: Battery low.
|
|
23
|
+
laser_power_low_warning: Laser power low.
|
|
24
|
+
laser_temp_warning: Laser temperature high or low.
|
|
25
|
+
internal_temp_warning: Internal temperature high or low.
|
|
26
|
+
voltage_range_warning: Voltage high or low.
|
|
27
|
+
humidity_high_warning: Relative humidity is high > 85 %.
|
|
28
|
+
receiver_crosstalk_warning: Receiver optical cross-talk compensation poor.
|
|
29
|
+
blower_fail_warning: Blower failure.
|
|
30
|
+
blower_status: Blower is ON.
|
|
31
|
+
blower_heater_status: Blower heater is ON.
|
|
32
|
+
internal_heater_status: Internal heater is ON.
|
|
33
|
+
units_meters: Units are METERS if ON, else FEET.
|
|
34
|
+
polling_mode_status: Polling mode is ON.
|
|
35
|
+
battery_power_status: Working from battery.
|
|
36
|
+
single_seq_mode_status: Single sequence mode is ON.
|
|
37
|
+
manual_settings_status: Manual settings are effective.
|
|
38
|
+
tilt_angle_warning: Tilt angle is > 45 degrees.
|
|
39
|
+
background_radiance_warning: High background radiance.
|
|
40
|
+
manual_blower_status: Manual blower control.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, status_bits: int):
|
|
44
|
+
self.laser_temp_shutoff_alarm = bool(status_bits & 0x80000000)
|
|
45
|
+
self.laser_fail_alarm = bool(status_bits & 0x40000000)
|
|
46
|
+
self.receiver_fail_alarm = bool(status_bits & 0x20000000)
|
|
47
|
+
self.voltage_fail_alarm = bool(status_bits & 0x10000000)
|
|
48
|
+
self.window_contam_warning = bool(status_bits & 0x00800000)
|
|
49
|
+
self.battery_low_warning = bool(status_bits & 0x00400000)
|
|
50
|
+
self.laser_power_low_warning = bool(status_bits & 0x00200000)
|
|
51
|
+
self.laser_temp_warning = bool(status_bits & 0x00100000)
|
|
52
|
+
self.internal_temp_warning = bool(status_bits & 0x00080000)
|
|
53
|
+
self.voltage_range_warning = bool(status_bits & 0x00040000)
|
|
54
|
+
self.humidity_high_warning = bool(status_bits & 0x00020000)
|
|
55
|
+
self.receiver_crosstalk_warning = bool(status_bits & 0x00010000)
|
|
56
|
+
self.blower_fail_warning = bool(status_bits & 0x00008000)
|
|
57
|
+
self.blower_status = bool(status_bits & 0x00000800)
|
|
58
|
+
self.blower_heater_status = bool(status_bits & 0x00000400)
|
|
59
|
+
self.internal_heater_status = bool(status_bits & 0x00000200)
|
|
60
|
+
self.units_meters = bool(status_bits & 0x00000100)
|
|
61
|
+
self.polling_mode_status = bool(status_bits & 0x00000080)
|
|
62
|
+
self.battery_power_status = bool(status_bits & 0x00000040)
|
|
63
|
+
self.single_seq_mode_status = bool(status_bits & 0x00000020)
|
|
64
|
+
self.manual_settings_status = bool(status_bits & 0x00000010)
|
|
65
|
+
self.tilt_angle_warning = bool(status_bits & 0x00000008)
|
|
66
|
+
self.background_radiance_warning = bool(status_bits & 0x00000004)
|
|
67
|
+
self.manual_blower_status = bool(status_bits & 0x00000002)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class CtMessage(Message):
|
|
72
|
+
"""Data message from Vaisala CT25K.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
receiver_sensitivity: Receiver sensitivity (%).
|
|
76
|
+
window_contamination: Window contamination (mV).
|
|
77
|
+
status: Decoded status bits.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
receiver_sensitivity: int
|
|
81
|
+
window_contamination: int
|
|
82
|
+
status: CtStatus
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_ct_file(
|
|
86
|
+
filename: str | PathLike,
|
|
87
|
+
) -> tuple[list[datetime.datetime], list[CtMessage]]:
|
|
88
|
+
"""Read Vaisala CT25K file."""
|
|
89
|
+
content = Path(filename).read_bytes()
|
|
90
|
+
time = []
|
|
91
|
+
data = []
|
|
92
|
+
for ts, msg in utils.parse_file(content, FORMAT):
|
|
93
|
+
try:
|
|
94
|
+
data.append(read_ct_message(msg))
|
|
95
|
+
time.append(ts)
|
|
96
|
+
except (InvalidMessageError, ValueError) as e:
|
|
97
|
+
logging.debug("Invalid message: %s", e)
|
|
98
|
+
return time, data
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def read_ct_message(message: bytes) -> CtMessage:
|
|
102
|
+
"""Read Vaisala CT25K data message."""
|
|
103
|
+
lines = iter(message.splitlines())
|
|
104
|
+
|
|
105
|
+
# Line 1
|
|
106
|
+
line1 = utils.next_line(lines, 7, prefix=b"\x01", suffix=b"\x02")
|
|
107
|
+
if line1[:2] != b"CT":
|
|
108
|
+
msg = "Invalid line 1"
|
|
109
|
+
raise InvalidMessageError(msg)
|
|
110
|
+
|
|
111
|
+
msg_no = line1[5:6]
|
|
112
|
+
if msg_no not in (b"2", b"7"):
|
|
113
|
+
msg = f"Invalid message number: {msg_no.decode()}"
|
|
114
|
+
raise InvalidMessageError(msg)
|
|
115
|
+
|
|
116
|
+
# Line 2
|
|
117
|
+
line2 = utils.next_line(lines, 29)
|
|
118
|
+
status_bits = int(line2[21:], 16)
|
|
119
|
+
status = CtStatus(status_bits)
|
|
120
|
+
|
|
121
|
+
# Line 3
|
|
122
|
+
line3 = utils.next_line(lines, 42)
|
|
123
|
+
scale = int(line3[0:3])
|
|
124
|
+
laser_pulse_energy = int(line3[6:9])
|
|
125
|
+
laser_temperature = int(line3[10:13])
|
|
126
|
+
receiver_sensitivity = int(line3[14:17])
|
|
127
|
+
window_contamination = int(line3[18:22])
|
|
128
|
+
tilt_angle = int(line3[23:26])
|
|
129
|
+
background_light = int(line3[27:31])
|
|
130
|
+
n_pulses = 4 ** (int(line3[34:35]) + 1)
|
|
131
|
+
sample_rate = 10 * int(line3[37:38])
|
|
132
|
+
|
|
133
|
+
# Lines 4-19: profile
|
|
134
|
+
data = []
|
|
135
|
+
for _i in range(16):
|
|
136
|
+
line = utils.next_line(lines, 67)
|
|
137
|
+
data.append(line[3:])
|
|
138
|
+
raw = utils.read_hex(b"".join(data), 4, 256)
|
|
139
|
+
beta = raw * 1e-7 * scale / 100
|
|
140
|
+
|
|
141
|
+
return CtMessage(
|
|
142
|
+
range_resolution=30,
|
|
143
|
+
laser_pulse_energy=laser_pulse_energy,
|
|
144
|
+
laser_temperature=laser_temperature,
|
|
145
|
+
receiver_sensitivity=receiver_sensitivity,
|
|
146
|
+
window_contamination=window_contamination,
|
|
147
|
+
tilt_angle=tilt_angle,
|
|
148
|
+
background_light=background_light,
|
|
149
|
+
n_pulses=n_pulses,
|
|
150
|
+
sample_rate=sample_rate,
|
|
151
|
+
status=status,
|
|
152
|
+
beta=beta,
|
|
153
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import binascii
|
|
2
|
+
import datetime
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
|
|
9
|
+
from ceilopyter.common import InvalidMessageError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_hex(data: bytes, n_chars: int, n_gates: int) -> npt.NDArray[np.int32]:
|
|
13
|
+
"""Read backscatter values from hex-encoded two's complement values."""
|
|
14
|
+
n_bits = n_chars * 4
|
|
15
|
+
limit = (1 << (n_bits - 1)) - 1
|
|
16
|
+
offset = 1 << n_bits
|
|
17
|
+
x = np.frombuffer(data.upper(), dtype=np.uint8)
|
|
18
|
+
is_digit = (x >= 48) & (x <= 57) # 0-9
|
|
19
|
+
is_letter = (x >= 65) & (x <= 70) # A-F
|
|
20
|
+
if not np.all(is_digit | is_letter):
|
|
21
|
+
raise ValueError("Invalid hex")
|
|
22
|
+
y = np.where(is_letter, x - 55, x - 48)
|
|
23
|
+
out = np.zeros(n_gates, dtype=np.int32)
|
|
24
|
+
for i in range(n_chars):
|
|
25
|
+
out <<= 4
|
|
26
|
+
out |= y[i::n_chars]
|
|
27
|
+
out[out > limit] -= offset
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def crc16(data: bytes) -> int:
|
|
32
|
+
"""Compute checksum similar to CRC-16-CCITT."""
|
|
33
|
+
return binascii.crc_hqx(data, 0xFFFF) ^ 0xFFFF
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def date_format_to_regex(pattern: bytes) -> re.Pattern:
|
|
37
|
+
"""Converts a date format string to a regex pattern."""
|
|
38
|
+
mapping = {
|
|
39
|
+
b"%Y": rb"\d{4}",
|
|
40
|
+
b"%m": rb"0[1-9]|1[0-2]",
|
|
41
|
+
b"%d": rb"0[1-9]|[12]\d|3[01]",
|
|
42
|
+
b"%H": rb"[01]\d|2[0-3]",
|
|
43
|
+
b"%M": rb"[0-5]\d",
|
|
44
|
+
b"%S": rb"[0-5]\d",
|
|
45
|
+
b"%f": rb"\d{6}",
|
|
46
|
+
}
|
|
47
|
+
for key, value in mapping.items():
|
|
48
|
+
pattern = pattern.replace(key, b"(?P<" + key[1:] + b">" + value + b")")
|
|
49
|
+
return re.compile(pattern)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_file(
|
|
53
|
+
content: bytes, pattern: re.Pattern
|
|
54
|
+
) -> Iterator[tuple[datetime.datetime, bytes]]:
|
|
55
|
+
parts = re.split(pattern, content)
|
|
56
|
+
for i in range(1, len(parts), pattern.groups + 1):
|
|
57
|
+
timestamp = datetime.datetime(
|
|
58
|
+
int(parts[i + pattern.groupindex["Y"] - 1]),
|
|
59
|
+
int(parts[i + pattern.groupindex["m"] - 1]),
|
|
60
|
+
int(parts[i + pattern.groupindex["d"] - 1]),
|
|
61
|
+
int(parts[i + pattern.groupindex["H"] - 1]),
|
|
62
|
+
int(parts[i + pattern.groupindex["M"] - 1]),
|
|
63
|
+
int(parts[i + pattern.groupindex["S"] - 1]),
|
|
64
|
+
int(parts[i + pattern.groupindex["f"] - 1])
|
|
65
|
+
if "f" in pattern.groupindex
|
|
66
|
+
else 0,
|
|
67
|
+
)
|
|
68
|
+
message = parts[i + pattern.groups]
|
|
69
|
+
yield timestamp, message
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def next_line(
|
|
73
|
+
lines: Iterator[bytes],
|
|
74
|
+
length: int | None = None,
|
|
75
|
+
prefix: bytes | None = None,
|
|
76
|
+
suffix: bytes | None = None,
|
|
77
|
+
) -> bytes:
|
|
78
|
+
try:
|
|
79
|
+
line = next(lines)
|
|
80
|
+
except StopIteration:
|
|
81
|
+
msg = "Expected another line"
|
|
82
|
+
raise InvalidMessageError(msg) from None
|
|
83
|
+
if prefix is not None:
|
|
84
|
+
line = line.removeprefix(prefix)
|
|
85
|
+
if suffix is not None:
|
|
86
|
+
line = line.removesuffix(suffix)
|
|
87
|
+
if length is not None and len(line) != length:
|
|
88
|
+
msg = f"Expected {length} characters but got {len(line)} instead"
|
|
89
|
+
raise InvalidMessageError(msg)
|
|
90
|
+
return line
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ceilopyter"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Tuomas Siipola", email = "tuomas.siipola@fmi.fi"},
|
|
9
|
+
]
|
|
10
|
+
description = "Read ceilometer data"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["numpy"]
|
|
22
|
+
dynamic = ["version"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
test = ["mypy", "pytest"]
|
|
26
|
+
dev = ["pre-commit", "release-version"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.version]
|
|
29
|
+
path = "ceilopyter/version.py"
|
|
30
|
+
|
|
31
|
+
[tool.release-version]
|
|
32
|
+
filename = "ceilopyter/version.py"
|
|
33
|
+
pattern = ["__version__ = \"(?P<major>\\d+).(?P<minor>\\d+).(?P<patch>\\d+)\""]
|
|
34
|
+
changelog = "CHANGELOG.md"
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["B", "D", "E", "F", "I", "PLC", "PLE", "PLW", "SIM", "UP"]
|
|
38
|
+
ignore = ["D1"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint.per-file-ignores]
|
|
41
|
+
"__init__.py" = ["PLC0414"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint.pydocstyle]
|
|
44
|
+
convention = "google"
|
|
45
|
+
|
|
46
|
+
[tool.tomlsort]
|
|
47
|
+
trailing_comma_inline_array = true
|
|
48
|
+
sort_inline_arrays = true
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-2025-03-11 08:04:55
|
|
2
|
+
CL010326
|
|
3
|
+
2W 00980 01290 ///// 000004008080
|
|
4
|
+
7 0062 0 //// 0 //// 0 //// 0 ////
|
|
5
|
+
00100 10 1540 101 +43 068 02 0009 L0032HN15 207
|
|
6
|
+
0017600176001760017600176001950019a001e1001d6001ee002110022e00268002ac002dc003200036100395003d500433004b10052e0059e005fb0066b006ff007930080b008620089a008bd008e1008ff008f5008c70089c008710084100823008350088e008fb009430094b0093100929009330094400933008e4008850084600812007d0007970075e0071c006f8006d5006850062d005ea005bf005b3005c90060b006470063f0063e00692006fc0073e0076000787007ae007c5007c3007c1007ce007df007e5007c70079600760007410074200746007300073f007e80084500795006c30065f00673007d000af400dd000f9e011500112600d9f009df007760056d003ce0031d003110033900315002a900264002430020d001e5001d9001e2001e0001e3001e5001ea001ff00220002340024d002af0033f0030b00257002080027d00307002ec0027f00218001c2001a0001a000166001260011a00112000ed000b5000570002b00016ffff0fffecffff3ffff5fffdffffddfffe8ffff1fffdbfffd2ffff3fffe5fffd5fffc9fffcafffd8fffe2fffdbfffcffffd2fffd4fffd7fffdbfffdcfffe7ffff6fffdffffdefffd7fffd9fffd8fffddfffeaffff1fffd7fffbefffb9fffc8fffcafffeffffeafffdafffdafffdbfffd2fffe2fffd5fffe6fffd9fffb4fffadfffc8fffa4fffb3ffffbfffe9fffcffffc4fffbbfffd4fffbefffa6fffcffffd9fffc4fffacfff96fffc2fffd1fffd5fffc8fffb8fffcafffcdfffcbfffcdfffcafff91fff8afffa0fff8bfffb6fff9afffa9fffc7fffcdfffd2fffd8fffcefffd6fffaffffaefff9ffffb6fffccfffc1fffa8fffd4ffff9fffe1fffc6fffcffffbafffdcfffc3fff98fffb3fffdefffcbfffa6fffc6fffc0fff8cfffb6fffcbfffcefffa3fffaefff90fff76fff75fffadffff1fffaffff9efffdbfffeefffeafffcbfffd5fffe0fffc2fffbdfffb1fffb2fffb3fffcefffdafffbafffa0fff9ffffb2fffdbfffebfffd2fff9cfff90fff91fff75fff81fff74fff5bfff86fff91fffc9fffc9fffa3fff8bfff8dfff99fffaafffabfff7dfff97fffb9fff96fff85fff82fffc6fff72fff5efffb7fffc6fff60fff82fff9bfffc1fffebfff88fffeffffeafffb8fff9ffffbdfffb8fffa6fff3cfff4cfff76fffb1fff8ffffabfffbffff85fffb5fffbefffd9fffa4fffb9fff87fffaafffc5fff8efff9bfff8bfffc5fffc1fff3dfff6ffffaffff84fff6afffa2fff8afff90fffb1fffb6000250001800003fff99fffb8fffbafffef0002000006ffffdfff90fffb7fffbefffb7fff82fff87fff9ffffa8fff89fff82ffff2fffe3fff9bfff81fff46fff9cfffcdfff99fff32fff21fffbc00026fffc8fff8bfffad00018fffc1fffb3fff43fff4afffcffffd0fff40fff84fff7afff30fff88fff3fffe9ffff18ffff3fffdefff82fffddfffbefffaffff3effecdfff1afff0efff93fffbcfff69fffabfff47fff94fff36fff48fff23fffa2fff98fff7efffaafff5efff32fff58fff75fffe8fff49fff73fff9ffff49ffefafff44fff9cfff2efff40fff6bffec0fff26fffb2fffc3fff7bffe96fff18fff89fff5efffacfffe4fff59fff58fff32fff10fff22fff23ffee9fff57fff3cfff31fff2efff6cffeb1fff06fff54fffa4fffc2fffdefffc6fffe1fffb8fff56fff7800015fff6bfff3ffff46fff51fff96fff63fff34fff73000040002afff28ffee4fff38fff7bfff62ffe81ffefffff9affed0fff0bfff94fff5cfff6cfff54fff5bfff3afff20fffc6fff7efff78fff46fff03fff1dffed8fffb6fffbcfffb0fff8dfff18fff41ffedaffeefffe93fff2afff36fff26fff9e00089fffdbfffcf00028fffb8fff0afff98ffff7fff10fff28fff1afff7efffe40004afffb3fff1bfff31ffe58ffeb4ffeeffff60fffea00003fffbcfff21fff1bfffcdfff53ffe75ffe9dfff3bfff05ffe56fff31fff7afff11fffabfff0fffe65fff1affe98ffeb6ffec1ffecfffecffff89ffff7000e2fffa8ffe38ffe62fff4bfffc3fffeefff8bfffd6000bf00022ffff70007800081ffea6ffe4affebbfff2e0000d00053fffaafff5dfff3effe94ffea9fff55fff1bfff21ffec7fff59fffddfff6ffff40ffeb8ffedaffe99ffe50ffe56fff2dfff67ffe9effebffff6cffef3ffe79ffe4effe8200005fffc6fff15fff64fff71ffee4ffe24ffdd4ffdf9ffe1cffec7ffe8dffecbfff21fff67fffc9fff27ffee5fff2f00005000effff86ffed00002dfffd1ffef2fff3bffed4ffee0ffe63ffe19ffdc3ffefefff34ffea000045fffe9fff42fff47ffe48ffe66fff95ffed2ffec4ffefafff47ffd7bffe2dfff07ffed4fff5a00063ffd46ffddcffdfbffea2fff4afff32fff3cffeb0fffc6fffaefff890001cfff78fff95ffe46ffd6fffe00ffff700062ffee0ffebbffed8ffe95fff2bffef5ffe26fff48fff6cfff37fffdbfffc1ffedbffeb0fff29ffec0ffe71ffec800037fff48fff26001250003efff03ffed3fffff000b8ffec2fff4c0023500237fff07ffe02ffe8fffe6fffda7ffde3ffe37fff08fff11ffd5dffe53fff83ffe87ffee2fff81ffea7ffe38ffe4cffe91ffd6effd17fff8a00050ffedcffd34ffe4fffea7ffe7bfff87fff2efffeffffbbffe26ffe7cfff15fffa6fffd3ffd64ffc80ffd7dfff9500053ffe78ffe83fffd9fffaffffea0004cffee2ffd8dfff13000d1fff5cfffed00072ffea5fffed001d1000a0ffe44ffe0fffe60ffd2affdc3fff67fff5cffef9fff41fff4dffdfbffdc400023ffec8ffe33fff1fffdd9ffed70000bffe440004200026ffd4cffbc1ffce7ffe05fffd7000d6fffdfffdf5ffdc8fff0e0003bffe81ffefd000aa0008a001090006800041ffdd0ffdadfff81ffffd00083ffe39ffdb700077fffe3ffdb4fff24ffefffff7b00010fff2cffffc0008dffee4ffd11ffe34fff26fff10ffee9fff33fff8bffdcaffe24001ad00026fffeb0008e0013900059ffe7c0010600022ffcb8ffbefffed7ffd60000c000022ffdb40006f000efffeedffd8cffe20ffeb9ffe9afff5dfff2dffdd5ffca6ffdd3ffe5bffd79ffc65ffe2e00040fff8effc92ffc02ffddcffe1effead001c400142ffefdfff54ffe3bffd02ffd38ffd0cfffb900047fffabffe1dffebbffe29ffea2000ec0009affedfffc0affc80ffe10000ba0014bfff18fff33ffefbffeb7000e3fff02ffd06ffe78fff08ffea7ffe3a00047fffe6fffd0fff61ffe51ffc83fffb50001dffef5fff4e001d9ffed8ffe6fffe54ffcd200026fff93ffdcaffd75fff96000affff1dfff70ffe7dfff9d0017fffe62ffe64fff17fff29ffec50004a0024e000d100003ffda2fffbf00087fff650007bfff66fff12fff75fff36ffc84ffc3fffd7afff34fff84ffd05ffb61ffe2900038000d0000d6000c10004500115fff500006500143ffd4afffce0021000113ffee50014400138fff93ffe50ffd36ffcedffda8000b00002d00047ffeae00061ffecbffe58ffca3ffda70018bfff59ffd12ffdc1ffe04ffb7cffd43fff19fff06ffd89ffebfffec6ffd96fff260021d00027000c3002010008ffffc8ffff90017cfffb7ffe78ffefdffb7c000400026effec90015700221ffe5d00000ffec2ffe88ffbbafff5c0003bfff84ffdbdffef8fffb90006d000fe001b000167000dc0008a0004b000b1ffffefffe0ffd13ffdd10017cfffabffc0effdd3ffcb8fff4f000b8fffbbffc98ffbb70019000245ffe61ffdf200146002cd000bcffd1fffea0fff090001300143002d20018a0026800069ffec7ffe69ffb81ffd55000dbffe92ffeedfff01ffecaffde0ffd94ffb42ffcf7001b9ffdf3ffe28000b100082fff52001880010b001f7ffe3cffc01ffd7affec000026ffd37ffd94000f90023cfffd8ffefeffd4efff13ffde5ffd3d0006a00101fff1a0001f00039ffbfbffba2fff8a0000c0016400196fffb1000660037500297000edffc91ffd03ffea3fff13ffea4ffe1f000c2001f30048f00113ffdfdfff46ffe0fffbfeffdaefff3dffd16ffe6bffd99001dcffe8effab9ffff500447001be000970014efff7ffff3400167fffc3001d10008ffffdc0006d00224001b5002d2005befff16ffd3affdcb000560023b00082fff5a00289001cbffdf2ffeb8ffd190001000241ffea3ffd4c0033e002b8002e70046600281ffc72ffd840002c00003005ad002e4001d3008160078b00584002240020300096ffd71ffe70fff06ffc6dffd65ffc300009a0030cffddcffd2d00138004e100184ffcc1fff3c0040900325fffa0fffcffffe9001420040cfff38ffe3800111fffa000292001c4002c9ffee0ffea60023200051fffe9ffc040000300386fffd2fffa6ffdd6ffd2e000cdffe2dffc47fff720018c000eb000a7ffddbffd9c0011000202000d800250fff47ffbcdffca8ffc24ffe77ffd13fff89000a0ffcd3ffdb1001e0000bdffcbb002a900388fff1100032ffc980011f0015e00176001f30005700022ffd2bfff1e00054ffb6cfffd1000c1ffd14000b2ffd30ffb2a0001a0011effea1ffc29ffe04ffe19ffe3bffc46ffbbcffb8dffcad00156001d4ffc9effbd300143001f8ffdc8ffeacffe39000e8ffc3afff57ffe87ffc85ffec10063e00306000cd00050fffc000464004350022e0010f002b3ffa38fff68002670045affe47ffc79ffbbdffaa3ffe29ffd84002b50031d00263000c3fff4affaa8ffbe00011f0037b0088100793ffcc6ffa64ffc08ffdbd006cc00507003c6004d2ffeadffefaffd8ffff0a002300043c0002affbcefffcf000c1000840004e0015b004f3007a8ffffaffc4700135003c60040ffffa1fff210012f0020b00330fffdd000b8006d7004a5002a500380ffe14ffbbaffd140026e0063b00378000b7ffc9affd65fff0900300003d5ffd52ffe5b003a9002dc00048003e400327fff3cffedd00150ffdfaffe4d003ec00089000290057200820003880042700404006a6003fe006230027b004380008dffdd6ffa94ffcceffc2a000f200781002e7fff07001d400427002cc0036f003cfffe3afff6600099ffeccffd67ffef4ffaff000970029d004ee001a8ffa6fffcc500311005c6fff1bffea9000ec0043900593004e30060c002230027600624005450075d00900006cf0075c0052f00304003710016f001f4002e60004bffdb5ffdaeffe76000d0fffcffff1300235003f1001d500582004a100387004e200274003b600737007c200804002da000f1fffe000113003c8ffe8a0034d0019afff280001700365004d70050d005e10031b00164fff4cfff82000b0003990022300377002caffeb7fff8f00171fffdc00010000ecff9a6ffb49ffdb2000a0
|
|
7
|
+
348c
|
|
8
|
+
|
|
9
|
+
-2025-03-11 08:05:25
|
|
10
|
+
CL010326
|
|
11
|
+
1W 00820 ///// ///// 000004008080
|
|
12
|
+
7 0062 0 //// 0 //// 0 //// 0 ////
|
|
13
|
+
00100 10 1540 101 +43 068 02 0010 L0032HN15 205
|
|
14
|
+
00408004080040800408004080044500431004ce0048100482004a3004ce0054a005e90065e006ec00776007f10084b00884008ce00922009760099b009a8009c6009f700a2300a13009cb0098d009740097f0099b009a6009b6009f100a5900ad900b6400c0100c6800c5d00bf400b5e00aa4009b3008b0007ec007720071a006b800659006240061c00619005e800594005570053200501004ef00520005480055f005870059a005c3006360067e0064f006310063a00639006440063c005f1005b10069800ae801149014ff0181f018e80139a00ec400c14008aa005a2003d5002ce00233002190023a002760051800b6200efc00be5006b900366001ba000d0000610002f00006ffff1fffeafffe8fffdefffe3fffeafffe2fffdffffe4fffe2fffd9fffe7fffe1fffd7fffddfffd9fffe1fffe0fffdafffdafffd9fffd9fffdbfffdbfffd9fffddfffd5fffd9fffd9fffd1fffd6fffd3fffd5fffdffffd7fffd2fffd8fffd1fffc9fffc5fffc7fffc8fffd3fffd3fffd4fffcffffc4fffc0fffd1fffdbfffe0fffcbfffccfffdafffccfffc2fffbbfffacfffc1fffcbfffcdfffcbfffc7fffbefffd0fffc3fffc9fffcdfffc1fffc4fffdafffd6fffbffffc5fffb7fffb4fffbbfffb6fffbdfffb6fffc2fffc8fffccfffb1fffbbfffbffffcefffc4fffd2fffc4fffb1fffa8fffc3fffbdfffb4fffbdfffdafffdbfffc4fffb6fffbafffc7fffd7fff9cfff97fffbdfffcdfffb7fffb7fffbbfffd8fffcefffb1fffacfffbafffb4fffa3fffc1fffbcfffc6fffbafffa8fffb5fffa0fffbffffb2fffb0fffb0fffbdfffc4fffd0fffa8fffa0fff97fffb4fffa4fffa3fff90fffbefffc1fffaefffa2fff88fff7cfffb8fffadfffd1fffbefffc6fffa8fff95fffa0fff94fffa5fffb3fffa4fff89fff67fff75fffcefffa0fff65fffabfffb7fff7cfffaafff9dfff86fffacfff87fff61fff75fff7efff8afff91fff8efffaffffc1fffa7fff76fff69fff7efff53fff78fff79fffabfffa7fff9efff8cfff81fff89fff91fff77fff63fff7efff77fff94fff8dfff62fff68fff6bfffc1fffabfff64fff58fff7dfffb1fffb1fffb8fffbefff91fff92f
|
|
15
|
+
Initializing... Ready
|
|
16
|
+
CL010326
|
|
17
|
+
10 00530 ///// ///// 00000000C080
|
|
18
|
+
99 //// 0 //// 0 //// 0 //// 0 ////
|
|
19
|
+
00100 10 1540 101 +43 068 02 0011 L0032HN15 124
|
|
20
|
+
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
|
21
|
+
42a7
|
|
22
|
+
|
|
23
|
+
-2025-03-11 08:06:58
|
|
24
|
+
CL010326
|
|
25
|
+
10 00550 ///// ///// 00000000C080
|
|
26
|
+
99 //// 0 //// 0 //// 0 //// 0 ////
|
|
27
|
+
00100 10 1540 101 +42 068 02 0010 L0032HN15 237
|
|
28
|
+
00d6100d6100d6100d6100d6100de000d9400fc400f2400f6d00fb600fc9010a3011b60123601309013b7014110144101460014a70149c014770145701443014460144c0143901405013b201349012e00125e011ba0110c0105900fa700f0900e8100e0400d8800ce000c2700b6b00ac200a49009d600948008c60085c00800007fc00a410122a01c3501f6c01c05016ba00ffe008dd003dc00160000630000dffff3fffeafffeafffe3fffe2fffe1fffe1fffe0fffe0fffe1fffdefffdafffdcfffdcfffdbfffddfffd9fffd8fffd5fffd5fffd9fffdefffdafffd4fffd6fffd5fffd2fffd3fffdafffcefffcffffd3fffd7fffd2fffc7fffc6fffc8fffc6fffc8fffcffffcdfffc7fffc8fffc9fffc5fffc2fffc4fffc4fffc1fffc4fffc3fffc4fffc3fffc1fffbffffbffffbafffbefffbcfffb9fffbafffc4fffc0fffb4fffb7fffb9fffaafffacfffb8fffbcfffb8fffadfffabfffaafffb1fffaefffaafffadfffaffffabfffb6fffb6fffb5fffb3fffaafffaefffaffff9ffff95fff91fff9bfffa7fff9cfff98fffa5fffa0fffacfffaefffa5fffabfff9cfffa8fffa1fff99fff91fffa1fffa9fffadfffb9fffa1fff95fffaffffb2fffa5fff9ffff9ffffa2fffacfffaefff9efff9efffaefffb3fffbbfffb1fffabfffa5fffaafffb6fffb3fffb0fffaafffa7fff9dfffa3fffa8fffaefffa9fffa7fffacfffacfffbcfffbcfffb6fffa5fffaafffaefffa5fffb4fffbcfffbbfffadfffa5fffa7fffaefffa9fffbbfffb5fffb2fffb4fffb7fffb2fffacfffb0fffbefffb7fffaafffaffffa6fffadfffbffffbffffb8fffb6fffb9fffc2fffb7fffb3fffc2fffbcfffaefffb6fffb9fffb5fffc1fffc3fffacfffadfffadfffc2fffb8fffb8fffb9fffc2fffc2fffc5fffbcfffc1fffcefffc9fffb9fffb5fffb1fffb8fffb1fffabfffaefffb3fffb9fffc7fffc7fffb9fffb2fffbafffbafffc2fffc9fffb0fffb6fffbefffc7fffb3fffc2fffc0fffc5fffc0fffbdfffbcfffb5fffbdfffb4fffb6fffb1fffb0fffbafffc2fffb9fffb2fffbefffabfffabfffbefffccfffc4fffb4fffb3fffb3fffb1fffbffffc2fffb6fffb6fffc1fffc5fffc6fffbafffb6fffbcfffb9fffbbfffcafffcbfffccfffcafffc1fffc1fffb9fffabfffb9fffcffffc7fffbffffbefffc3fffbffffc0fffc2fffb9fffbafffbffffb8fffc4fffc4fffb3fffc7fffcffffc3fffbefffc2fffc2fffbcfffbefffb3fffbefffcafffc2fffc3fffbefffc8fffccfffbafffc8fffccfffb8fffadfffc0fffd2fffcbfffb8fffc3fffc0fffbefffc7fffcffffcefffcffffcefffbcfffbbfffbafffbffffc9fffcefffc8fffcffffc1fffbffffbdfffcffffcafffc8fffbafffb4fffc4fffc3fffc1fffbffffb6fffaefffbffffd7fffd1fffc6fffc1fffc8fffd8fffd2fffc0fffb8fffc1fffc3fffd0fffc5fffd1fffccfffcafffcafffc1fffbafffbcfffbffffc0fffc1fffc1fffc4fffcafffcbfffc2fffbafffb9fffc4fffc7fffcafffc7fffc8fffcbfffc9fffd1fffd6fffcdfffbefffc6fffcdfffc7fffd2fffcffffd3fffcffffbcfffbdfffd1fffd5fffc9fffcafffc4fffdcfffd9fffd4fffd1fffd6fffd2fffcafffc3fffdafffcbfffc3fffd0fffd0fffd0fffd7fffcafffc8fffc7fffc9fffd1fffc5fffd2fffd7fffcdfffcefffdcfffcefffc1fffd0fffdbfffcbfffb7fffc7fffc7fffcefffe3fffdefffe9fffd5fffcbfffd8fffcbfffbafffcefffd1fffcdfffc0fffc5fffccfffcefffc4fffc1fffd4fffcafffcafffdafffcafffc0fffc3fffd3fffe4fffe9fffd3fffc2fffc8fffd9fffdcfffd6fffe2fffd2fffc8fffcafffc4fffc8fffcefffc0fffc4fffc9fffc8fffd6fffd4fffd9fffd9fffd2fffd3fffcafffd0fffd4fffc9fffb8fffcafffdbfffdafffcbfffd2fffc9fffc6fffc2fffd8fffd9fffd0fffd5fffdcfffe2fffdcfffd0fffdcfffdcfffd3fffc9fffd3fffd5fffd4fffc5fffc9fffc0fffd1fffd2fffcefffc9fffc4fffd4fffd3fffd6fffcffffcdfffcefffd5fffd9fffe5fffd3fffd3fffd7fffdefffccfffcefffcdfffd1fffd6fffc7fffd7fffd1fffc3fffc8fffd8fffd4fffcafffd4fffd1fffd7fffdefffd2fffd0fffd0fffdcfffd0fffd5fffd2fffd1fffdafffd2fffc5fffcffffd9fffd5fffd6fffddfffdefffe8fffdefffd6fffd1fffc8fffd0fffd6fffe4fffdbfffd8fffe5fffd8fffd8fffe1fffe2fffd9fffd7fffd4fffd3fffdcfffe0fffdafffdcfffe9fffe0fffe0fffdffffe4fffe8fffe1fffe1fffd9fffdefffe1fffddfffe3fffddfffe3fffd7fffd6fffdffffe3fffdefffdffffdefffd1fffe0fffdcfffc8fffd3fffdbfffd7fffe4fffe9fffd3fffcefffd1fffdafffd0fffcefffdbfffdbfffddfffe2ffff1ffff4fffd9fffd8fffe2fffe5fffe9fffd8fffdbfffe2fffdbfffd5fffd2fffe6fffe5fffdafffe5fffe6fffdbfffe9fffebfffe2fffe4fffdcfffcdfffd6fffddfffdffffd8fffc8fffccfffd9fffe5fffebfffe1fffdffffdafffd6fffe9fffe3fffdefffebfffe5fffddfffe7fffe5fffe0fffddfffdbfffd3fffd9fffe1fffe0fffe6fffdbfffdefffebfffe5fffdbfffe2fffe3fffe6fffe2fffdbfffd2fffdafffe6fffe6ffff1fffecfffdafffe4fffdcfffe0fffedfffe6fffd8fffe0fffecfffe1fffddfffe9fffe5ffff0fffecfffdefffd8fffcffffdafffdcfffdffffe2fffd5fffd1fffe7fffdafffe7fffe7fffd6fffe2fffe4fffe3fffdafffdafffe6fffe1fffebfffebfffddfffd2fffe7fffe8fffcffffd1fffd1fffd7fffe4fffe4fffe3fffe1fffebfffdafffd8fffe7fffe9fffe4fffe9fffeafffdffffe0fffe9fffeefffe6fffe4fffe8fffdcfffe3fffecfffe5fffe0fffe9fffecfffe8fffebfffebfffdcfffe0fffeafffecfffe9fffecffff0fffdefffd4fffe0fffeafffdefffe0fffe7fffd8fffe1fffeffffefffff0fffe4fffe7fffe5fffecffff4fffe3fffedfffe9fffe2fffddfffdffffdefffe8ffff3fffe9fffe7fffe3fffe3fffe6ffff0fffefffff3ffff4fffe8fffe3fffedffff4ffff8ffff8fffeafffd9fffdafffe4fffeafffe0fffdefffdefffe6ffff1ffff2fffe4fffddfffd9fffe2fffedfffe9fffdbfffe3fffddffff1fffecfffddfffe3fffe1fffe0fffe1fffe100000ffff0fffd7fffdafffeafffe0ffff1fffedfffedfffeeffff6fffedfffdefffe7fffecfffedffff3ffffafffeafffe5fffdffffe2fffefffffeffff3fffdefffd3ffff1ffff0ffffaffff9fffe7fffdffffe2ffff6ffffcfffefffff7ffff4fffebfffe7fffeeffff1ffff1ffff10000affffffffe9fffebfffeefffe9fffe8fffeaffff3fffeffffe6fffe5ffff4fffecfffe8ffff4ffff4fffecfffe2fffe0ffff5ffffffffeafffe4fffe9ffffcffff7fffe9fffe4ffff3ffffcfffefffff1fffebfffeffffe9fffe0ffff0ffff6ffff0fffebfffefffff2fffedfffebfffe9ffff8ffff4ffff1ffff2ffff1fffedfffedffffaffff9fffe8ffff9fffeefffebfffebfffeefffe4fffe4fffe4ffff9ffff8ffff7ffff0ffff7ffffefffffffff7fffefffff8fffedffff4ffffafffe9fffe9ffff5fffedffff3ffff1fffedfffebffff4ffff4fffedfffefffffa0000000003ffffa0000800006ffff5fffff00000ffff1fffedfffe8ffff4ffffbffff2ffff3fffeeffff600001ffff0ffff8ffffdffff700001ffffefffedfffeeffff5ffff3ffff30000300001fffe8ffff9ffffdfffe7fffedfffe9ffff1ffff8ffff3ffff6ffff3ffff2fffe9ffffeffffeffffefffffffff5ffffcffffaffffdffff5ffff8ffffdfffffffff5ffff8ffff7ffff900002ffff5ffff00000c0000cffff3ffff8ffffdfffedfffef000070000300003ffff5fffedffffdfffff00000ffffaffff4fffeaffffb0000bffff2ffffe0000bffff9ffff8ffff4ffffa00000ffff1fffe9fffe8ffff5ffffbffff6ffff7ffff0ffff2ffff4ffffdffffcffffeffff4ffff4ffff9ffff9fffecffff3ffff4ffffb00011fffe5fffe1fffea0000000003ffff7ffff3ffff5ffff3ffffbffffaffff4ffff2ffff2fffe6fffdefffee000060001300001ffff5ffff9ffffcffff6ffff800002ffffafffe6ffff600000ffffcffff70001100006fffedfffe8ffff5ffff800001ffffb0000400002ffff3ffff400006ffff5fffedffffcffffbffffaffff9ffff9ffffbffff200006000130000900004ffff800001ffffafffe7fffe3ffff1ffffdffff8ffff5ffffa00001000100000e00004ffffefffff00002ffff4ffff7ffffb00006ffffcffff8ffff600005ffffdffff6ffff4ffff5ffff7fffefffffd00002ffff7ffffe00008ffffe0000a0001100000ffff30000100002fffedffff0ffffa00005000130001400000ffff5ffff5fffffffff7ffff2ffffaffffe0000300004ffff7ffffd000040000800003fffffffffbffffd0000a0000b000030000c0000400007ffff1ffffa0000100002ffff8ffff800008000070000e0000cffff3ffffd0000000003fffff00007ffffdffffe00013000130000c0000affffe0000e000150000500005000170000bffff2ffffb000040001200003ffff6ffffb0001a000170000600011fffff000040000600005ffffeffff6ffffc000080000d0000f0000500013000050000bffffaffff60001100009ffffc0000900010000090000500004fffff0000c0000e0000affffe0000600007000050000a00011fffff000080000b00009000060000a000040000b0000a0000a000040000f0001100003ffffc00010000160000400001ffffe000120000900002ffff8ffff5ffff0000020000f00010000050000a0000b00004000070001700014fffffffff90000400007000210001f00013000110001000006000050001000013000120000c0000c0000100008000120001affffeffff8ffffd000000000700002000080000800004ffff7ffff9fffffffff3ffff1000040000b0000600008000120000d000040000f00016ffffaffffb00006ffffeffffa00003000100000f00013000140000b0000f0000a0000200006000040001400019000120001000013000190000a000170002000002fffed000040001b0001b0001800001000140001b000130000cffff50000a0000800006000180001c0001000007000040000b0000cffffbffffd00008000100002b0000e0000d000120000b0001100015000110000b00002000030000300012000170001b0000e0000d0000500016000170000a0000d0000e0000a000030000a0001c0000e0000700012000100000d00004000110000f000120001d0001d00003000060001400012000250002000000
|
|
29
|
+
d53c
|
|
30
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from ceilopyter import read_cl_file
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_skip_invalid():
|
|
7
|
+
time, data = read_cl_file("tests/data/celio_chennai_2025-03-11.dat")
|
|
8
|
+
assert len(time) == 2
|
|
9
|
+
assert len(data) == 2
|
|
10
|
+
|
|
11
|
+
assert time[0] == datetime.datetime(2025, 3, 11, 8, 4, 55)
|
|
12
|
+
assert data[0].laser_temperature == 43
|
|
13
|
+
assert data[0].status.blower_fail_warning
|
|
14
|
+
|
|
15
|
+
assert time[1] == datetime.datetime(2025, 3, 11, 8, 6, 58)
|
|
16
|
+
assert data[1].laser_temperature == 42
|
|
17
|
+
assert not data[1].status.blower_fail_warning
|