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.
@@ -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
@@ -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,14 @@
1
+ # Ceilopyter
2
+
3
+ Python package for reading ceilometer data.
4
+
5
+ ## Supported ceilometers
6
+
7
+ - Campbell Scientific CS135
8
+ - Vaisala CL31
9
+ - Vaisala CL51
10
+ - Vaisala CT25K
11
+
12
+ ## License
13
+
14
+ 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