dspp-reader 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dspp_reader-0.1.1/.github/workflows/python-publish.yml +70 -0
- dspp_reader-0.1.1/.gitignore +2 -0
- dspp_reader-0.1.1/LICENSE +29 -0
- dspp_reader-0.1.1/PKG-INFO +38 -0
- dspp_reader-0.1.1/README.md +1 -0
- dspp_reader-0.1.1/docs/Makefile +20 -0
- dspp_reader-0.1.1/docs/make.bat +35 -0
- dspp_reader-0.1.1/docs/source/conf.py +33 -0
- dspp_reader-0.1.1/docs/source/index.rst +17 -0
- dspp_reader-0.1.1/dspp_reader/__init__.py +0 -0
- dspp_reader-0.1.1/dspp_reader/sqmle/__init__.py +0 -0
- dspp_reader-0.1.1/dspp_reader/sqmle/scripts.py +85 -0
- dspp_reader-0.1.1/dspp_reader/sqmle/sqmle.py +247 -0
- dspp_reader-0.1.1/dspp_reader/tessw4c/__init__.py +1 -0
- dspp_reader-0.1.1/dspp_reader/tessw4c/scripts.py +84 -0
- dspp_reader-0.1.1/dspp_reader/tessw4c/tessw4c.py +251 -0
- dspp_reader-0.1.1/dspp_reader/tools/__init__.py +3 -0
- dspp_reader-0.1.1/dspp_reader/tools/device.py +24 -0
- dspp_reader-0.1.1/dspp_reader/tools/generics.py +132 -0
- dspp_reader-0.1.1/dspp_reader/tools/site.py +32 -0
- dspp_reader-0.1.1/dspp_reader/version.py +34 -0
- dspp_reader-0.1.1/dspp_reader.egg-info/PKG-INFO +38 -0
- dspp_reader-0.1.1/dspp_reader.egg-info/SOURCES.txt +29 -0
- dspp_reader-0.1.1/dspp_reader.egg-info/dependency_links.txt +1 -0
- dspp_reader-0.1.1/dspp_reader.egg-info/entry_points.txt +3 -0
- dspp_reader-0.1.1/dspp_reader.egg-info/requires.txt +9 -0
- dspp_reader-0.1.1/dspp_reader.egg-info/top_level.txt +1 -0
- dspp_reader-0.1.1/environment.yml +8 -0
- dspp_reader-0.1.1/pyproject.toml +76 -0
- dspp_reader-0.1.1/requirements.txt +7 -0
- dspp_reader-0.1.1/setup.cfg +4 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# This workflow will upload a Python Package to PyPI when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
|
3
|
+
|
|
4
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
5
|
+
# They are provided by a third-party and are governed by
|
|
6
|
+
# separate terms of service, privacy policy, and support
|
|
7
|
+
# documentation.
|
|
8
|
+
|
|
9
|
+
name: Upload Python Package
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
release:
|
|
13
|
+
types: [published]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
release-build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.x"
|
|
28
|
+
|
|
29
|
+
- name: Build release distributions
|
|
30
|
+
run: |
|
|
31
|
+
# NOTE: put your own distribution build steps here.
|
|
32
|
+
python -m pip install build
|
|
33
|
+
python -m build
|
|
34
|
+
|
|
35
|
+
- name: Upload distributions
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: release-dists
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
pypi-publish:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
needs:
|
|
44
|
+
- release-build
|
|
45
|
+
permissions:
|
|
46
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
# Dedicated environments with protections for publishing are strongly recommended.
|
|
50
|
+
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
|
|
54
|
+
# url: https://pypi.org/p/YOURPROJECT
|
|
55
|
+
#
|
|
56
|
+
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
|
|
57
|
+
# ALTERNATIVE: exactly, uncomment the following line instead:
|
|
58
|
+
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- name: Retrieve release distributions
|
|
62
|
+
uses: actions/download-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: release-dists
|
|
65
|
+
path: dist/
|
|
66
|
+
|
|
67
|
+
- name: Publish release distributions to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
with:
|
|
70
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, NOIRLab
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dspp_reader
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Software for reading SQMs and TESS-W4C devices, used for measuring sky brightness.
|
|
5
|
+
Author-email: Simón Torres <simon.torres@noirlab.edu>, Guillermo Damke <guillermo.damke@noirlab.edu>
|
|
6
|
+
Maintainer-email: Simón Torres <simon.torres@noirlab.edu>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://noirlab.edu
|
|
9
|
+
Project-URL: Bug Reports, https://noirlab.edu
|
|
10
|
+
Project-URL: Source, https://noirlab.edu
|
|
11
|
+
Keywords: sqm,sqm-le,tess-4c,sky,darkness,astronomy
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Natural Language :: English
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Operating System :: POSIX :: Other
|
|
21
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.13
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: astropy
|
|
28
|
+
Requires-Dist: astroplan
|
|
29
|
+
Requires-Dist: packaging
|
|
30
|
+
Requires-Dist: requests
|
|
31
|
+
Requires-Dist: sphinx
|
|
32
|
+
Requires-Dist: sphinxcontrib-napoleon
|
|
33
|
+
Requires-Dist: pydata-sphinx-theme
|
|
34
|
+
Requires-Dist: pyyaml
|
|
35
|
+
Requires-Dist: tzlocal
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# Dark Sky Protection Photometers Reader
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Dark Sky Protection Photometers Reader
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Minimal makefile for Sphinx documentation
|
|
2
|
+
#
|
|
3
|
+
|
|
4
|
+
# You can set these variables from the command line, and also
|
|
5
|
+
# from the environment for the first two.
|
|
6
|
+
SPHINXOPTS ?=
|
|
7
|
+
SPHINXBUILD ?= sphinx-build
|
|
8
|
+
SOURCEDIR = source
|
|
9
|
+
BUILDDIR = build
|
|
10
|
+
|
|
11
|
+
# Put it first so that "make" without argument is like "make help".
|
|
12
|
+
help:
|
|
13
|
+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
14
|
+
|
|
15
|
+
.PHONY: help Makefile
|
|
16
|
+
|
|
17
|
+
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
18
|
+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
19
|
+
%: Makefile
|
|
20
|
+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
@ECHO OFF
|
|
2
|
+
|
|
3
|
+
pushd %~dp0
|
|
4
|
+
|
|
5
|
+
REM Command file for Sphinx documentation
|
|
6
|
+
|
|
7
|
+
if "%SPHINXBUILD%" == "" (
|
|
8
|
+
set SPHINXBUILD=sphinx-build
|
|
9
|
+
)
|
|
10
|
+
set SOURCEDIR=source
|
|
11
|
+
set BUILDDIR=build
|
|
12
|
+
|
|
13
|
+
%SPHINXBUILD% >NUL 2>NUL
|
|
14
|
+
if errorlevel 9009 (
|
|
15
|
+
echo.
|
|
16
|
+
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
|
17
|
+
echo.installed, then set the SPHINXBUILD environment variable to point
|
|
18
|
+
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
|
19
|
+
echo.may add the Sphinx directory to PATH.
|
|
20
|
+
echo.
|
|
21
|
+
echo.If you don't have Sphinx installed, grab it from
|
|
22
|
+
echo.https://www.sphinx-doc.org/
|
|
23
|
+
exit /b 1
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if "%1" == "" goto help
|
|
27
|
+
|
|
28
|
+
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
29
|
+
goto end
|
|
30
|
+
|
|
31
|
+
:help
|
|
32
|
+
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
|
33
|
+
|
|
34
|
+
:end
|
|
35
|
+
popd
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Configuration file for the Sphinx documentation builder.
|
|
2
|
+
#
|
|
3
|
+
# For the full list of built-in configuration values, see the documentation:
|
|
4
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
5
|
+
|
|
6
|
+
# -- Project information -----------------------------------------------------
|
|
7
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
8
|
+
from importlib.metadata import version
|
|
9
|
+
|
|
10
|
+
__version__ = version(__name__)
|
|
11
|
+
version = '.'.join(__version__.split('.')[:2])
|
|
12
|
+
release = __version__
|
|
13
|
+
project = 'Dark Sky Protection Project Reader'
|
|
14
|
+
copyright = '2025, NOIRLab'
|
|
15
|
+
author = 'Simón Torres, Guillermo Damke'
|
|
16
|
+
license = 'bsd3'
|
|
17
|
+
|
|
18
|
+
# -- General configuration ---------------------------------------------------
|
|
19
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
20
|
+
|
|
21
|
+
extensions = []
|
|
22
|
+
|
|
23
|
+
templates_path = ['_templates']
|
|
24
|
+
exclude_patterns = []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# -- Options for HTML output -------------------------------------------------
|
|
29
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
|
30
|
+
|
|
31
|
+
html_theme = 'pydata_sphinx_theme'
|
|
32
|
+
html_static_path = ['_static']
|
|
33
|
+
html_static_path = ['_static']
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.. Dark Sky Protection Project Reader documentation master file, created by
|
|
2
|
+
sphinx-quickstart on Thu Nov 20 14:55:08 2025.
|
|
3
|
+
You can adapt this file completely to your liking, but it should at least
|
|
4
|
+
contain the root `toctree` directive.
|
|
5
|
+
|
|
6
|
+
Dark Sky Protection Project Reader documentation
|
|
7
|
+
================================================
|
|
8
|
+
|
|
9
|
+
Add your content using ``reStructuredText`` syntax. See the
|
|
10
|
+
`reStructuredText <https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html>`_
|
|
11
|
+
documentation for details.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
.. toctree::
|
|
15
|
+
:maxdepth: 2
|
|
16
|
+
:caption: Contents:
|
|
17
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from dspp_reader.sqmle.sqmle import SQMLE
|
|
10
|
+
from dspp_reader.tools import get_args, setup_logging
|
|
11
|
+
|
|
12
|
+
__version__ = version("dspp-reader")
|
|
13
|
+
|
|
14
|
+
CONFIG_FIELDS = [
|
|
15
|
+
"site_id",
|
|
16
|
+
"site_name",
|
|
17
|
+
"site_latitude",
|
|
18
|
+
"site_longitude",
|
|
19
|
+
"site_elevation",
|
|
20
|
+
"site_timezone",
|
|
21
|
+
"device_type",
|
|
22
|
+
"device_id",
|
|
23
|
+
"device_altitude",
|
|
24
|
+
"device_azimuth",
|
|
25
|
+
"device_ip",
|
|
26
|
+
"device_port",
|
|
27
|
+
"number_of_reads",
|
|
28
|
+
"reads_frequency",
|
|
29
|
+
"save_to_file",
|
|
30
|
+
"save_to_database",
|
|
31
|
+
"post_to_api",
|
|
32
|
+
"save_files_to",
|
|
33
|
+
"file_format",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_sqmle(args=None):
|
|
38
|
+
args = get_args(args=args, has_upd=False, default_device_type='sqmle')
|
|
39
|
+
|
|
40
|
+
site = {}
|
|
41
|
+
if args.config_file and os.path.isfile(args.config_file):
|
|
42
|
+
with open(args.config_file, "r") as f:
|
|
43
|
+
site = yaml.safe_load(f) or {}
|
|
44
|
+
|
|
45
|
+
config = {}
|
|
46
|
+
for field in CONFIG_FIELDS:
|
|
47
|
+
config[field] = site.get(field, getattr(args, field))
|
|
48
|
+
|
|
49
|
+
if args.config_file_example:
|
|
50
|
+
print("# Add this to a .yaml file, reference it later with --config-file <file_name>.yaml")
|
|
51
|
+
print(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
52
|
+
sys.exit(0)
|
|
53
|
+
|
|
54
|
+
setup_logging(debug=args.debug, device_type=config["device_type"], device_id=config["device_id"])
|
|
55
|
+
logger = logging.getLogger()
|
|
56
|
+
logger.info(f"Starting SQMLE reader, Version: {__version__}")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
sqmle = SQMLE(
|
|
60
|
+
site_id=config["site_id"],
|
|
61
|
+
site_name=config["site_name"],
|
|
62
|
+
site_latitude=config["site_latitude"],
|
|
63
|
+
site_longitude=config["site_longitude"],
|
|
64
|
+
site_elevation=config["site_elevation"],
|
|
65
|
+
site_timezone=config["site_timezone"],
|
|
66
|
+
device_type=config["device_type"],
|
|
67
|
+
device_id=config["device_id"],
|
|
68
|
+
device_altitude=config["device_altitude"],
|
|
69
|
+
device_azimuth=config["device_azimuth"],
|
|
70
|
+
device_ip=config["device_ip"],
|
|
71
|
+
device_port=config["device_port"],
|
|
72
|
+
number_of_reads=config["number_of_reads"],
|
|
73
|
+
reads_frequency=config["reads_frequency"],
|
|
74
|
+
save_to_file=config["save_to_file"],
|
|
75
|
+
save_to_database=config["save_to_database"],
|
|
76
|
+
post_to_api=config["post_to_api"],
|
|
77
|
+
save_files_to=config["save_files_to"],
|
|
78
|
+
file_format=config["file_format"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
sqmle()
|
|
82
|
+
except KeyboardInterrupt:
|
|
83
|
+
print("\n")
|
|
84
|
+
logger.info(f"Exiting SQMLE reader on user request, Version: {__version__}")
|
|
85
|
+
sys.exit(0)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import astropy.units as u
|
|
2
|
+
import datetime
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import socket
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from astropy.units import Quantity
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from time import sleep
|
|
12
|
+
|
|
13
|
+
from dspp_reader.tools import Device, Site
|
|
14
|
+
from dspp_reader.tools.generics import augment_data, get_filename
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger()
|
|
17
|
+
|
|
18
|
+
READ = b'rx\r\n'
|
|
19
|
+
READ_WITH_SERIAL_NUMBER = b'Rx\r\n'
|
|
20
|
+
REQUEST_CALIBRATION_INFORMATION = b'cx\r\n'
|
|
21
|
+
UNIT_INFORMATION_REQUEST = b'ix\r\n'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SQMLE(object):
|
|
25
|
+
def __init__(self,
|
|
26
|
+
site_id: str = '',
|
|
27
|
+
site_name: str = '',
|
|
28
|
+
site_timezone: str = '',
|
|
29
|
+
site_latitude: str = '',
|
|
30
|
+
site_longitude: str = '',
|
|
31
|
+
site_elevation: str = '',
|
|
32
|
+
device_type:str = 'sqmle',
|
|
33
|
+
device_id:str = None,
|
|
34
|
+
device_altitude:float = None,
|
|
35
|
+
device_azimuth:float = None,
|
|
36
|
+
device_ip:str = None,
|
|
37
|
+
device_port=10001,
|
|
38
|
+
number_of_reads=3,
|
|
39
|
+
reads_frequency=30,
|
|
40
|
+
save_to_file=True,
|
|
41
|
+
save_to_database=False,
|
|
42
|
+
post_to_api=False,
|
|
43
|
+
save_files_to: Path = os.getcwd(),
|
|
44
|
+
file_format: str = "tsv",):
|
|
45
|
+
self.site_id = site_id
|
|
46
|
+
self.site_name = site_name
|
|
47
|
+
self.site_timezone = site_timezone
|
|
48
|
+
self.site_latitude = site_latitude
|
|
49
|
+
self.site_longitude = site_longitude
|
|
50
|
+
self.site_elevation = site_elevation
|
|
51
|
+
self.device_type = device_type
|
|
52
|
+
self.device_id = device_id
|
|
53
|
+
self.device_port = device_port
|
|
54
|
+
self.device_altitude = device_altitude
|
|
55
|
+
self.device_azimuth = device_azimuth
|
|
56
|
+
self.device_ip = device_ip
|
|
57
|
+
|
|
58
|
+
self.number_of_reads = number_of_reads
|
|
59
|
+
self.reads_frequency = reads_frequency
|
|
60
|
+
self.save_to_file = save_to_file
|
|
61
|
+
self.save_to_database = save_to_database
|
|
62
|
+
self.post_to_api = post_to_api
|
|
63
|
+
self.save_files_to = Path(save_files_to)
|
|
64
|
+
self.file_format = file_format
|
|
65
|
+
self.separator = ''
|
|
66
|
+
if self.file_format == "tsv":
|
|
67
|
+
self.separator = "\t"
|
|
68
|
+
elif self.file_format == "csv":
|
|
69
|
+
self.separator = ","
|
|
70
|
+
elif self.file_format == "txt":
|
|
71
|
+
self.separator = " "
|
|
72
|
+
else:
|
|
73
|
+
self.separator = " "
|
|
74
|
+
|
|
75
|
+
self.site = None
|
|
76
|
+
if all([self.site_id, self.site_name, self.site_timezone, self.site_latitude, self.site_longitude, self.site_elevation]):
|
|
77
|
+
self.site = Site(
|
|
78
|
+
id=self.site_id,
|
|
79
|
+
name=self.site_name,
|
|
80
|
+
latitude=self.site_latitude,
|
|
81
|
+
longitude=self.site_longitude,
|
|
82
|
+
elevation=self.site_elevation,
|
|
83
|
+
timezone=self.site_timezone)
|
|
84
|
+
else:
|
|
85
|
+
logger.error(f"Not enough site info provided: Please provide: site_id, site_name, site_timezone, site_latitude, site_longitude, site_elevation")
|
|
86
|
+
|
|
87
|
+
self.device = None
|
|
88
|
+
if all([self.device_type, self.device_id, self.device_port, self.device_altitude, self.device_azimuth, self.device_ip, self.device_port]):
|
|
89
|
+
self.device = Device(
|
|
90
|
+
serial_id=self.device_id,
|
|
91
|
+
type=self.device_type,
|
|
92
|
+
altitude=self.device_altitude,
|
|
93
|
+
azimuth=self.device_azimuth,
|
|
94
|
+
site=self.site,
|
|
95
|
+
ip=self.device_ip,
|
|
96
|
+
port=self.device_port,
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
logger.error("Not enough information to define device")
|
|
100
|
+
|
|
101
|
+
self.socket = None
|
|
102
|
+
if self.device:
|
|
103
|
+
while not self.socket:
|
|
104
|
+
try:
|
|
105
|
+
logger.debug(f"Creating socket connection for {self.device.type} {self.device.serial_id}")
|
|
106
|
+
self.socket = socket.create_connection((self.device.ip, self.device.port), timeout=5)
|
|
107
|
+
logger.info(f"Created socket connection for {self.device.type} {self.device.serial_id}")
|
|
108
|
+
except OSError as e:
|
|
109
|
+
timeout = 20
|
|
110
|
+
print(
|
|
111
|
+
f"\r{datetime.datetime.now().astimezone()}: Unable to connect to {self.device.serial_id} at {self.device.ip}:{self.device.port}: {e}")
|
|
112
|
+
for i in range(1, timeout + 1, 1):
|
|
113
|
+
print(f"\rAttempting again in {timeout - i} seconds...", end="", flush=True)
|
|
114
|
+
sleep(1)
|
|
115
|
+
else:
|
|
116
|
+
logger.error(f"A device is needed to be able to continue")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
def __call__(self):
|
|
120
|
+
try:
|
|
121
|
+
while True:
|
|
122
|
+
if self.device and self.socket:
|
|
123
|
+
self.timestamp = datetime.datetime.now(datetime.UTC)
|
|
124
|
+
data = {}
|
|
125
|
+
measurements = []
|
|
126
|
+
for read in range(1, self.number_of_reads + 1, 1):
|
|
127
|
+
logger.debug(f"Reading {read} of {self.number_of_reads}...")
|
|
128
|
+
data = self._send_command(command=READ_WITH_SERIAL_NUMBER, sock=self.socket)
|
|
129
|
+
logger.debug(f"Response: {data}")
|
|
130
|
+
parsed_data = self._parse_data(data=data, command=READ_WITH_SERIAL_NUMBER)
|
|
131
|
+
measurements.append(parsed_data)
|
|
132
|
+
if self.device.serial_id:
|
|
133
|
+
if self.device.serial_id != parsed_data['serial_number']:
|
|
134
|
+
logger.warning(
|
|
135
|
+
f"Serial number mismatch: {self.device.serial_id} != {parsed_data['serial_number']}")
|
|
136
|
+
if len(measurements) == 1:
|
|
137
|
+
data = measurements[0]
|
|
138
|
+
elif len(measurements) > 1:
|
|
139
|
+
raise NotImplementedError("Averaging data does is not yet implemented. Use --number-of-reads 1")
|
|
140
|
+
|
|
141
|
+
augmented_data = augment_data(data=data, timestamp=self.timestamp, device=self.device)
|
|
142
|
+
|
|
143
|
+
if self.save_to_file:
|
|
144
|
+
self._write_to_txt(data=augmented_data)
|
|
145
|
+
if self.save_to_database:
|
|
146
|
+
self._write_to_database()
|
|
147
|
+
if self.post_to_api:
|
|
148
|
+
self._post_to_api()
|
|
149
|
+
for i in range(self.reads_frequency):
|
|
150
|
+
print(f"\rNext read in {self.reads_frequency - i} seconds...", end="", flush=True)
|
|
151
|
+
sleep(1)
|
|
152
|
+
except KeyboardInterrupt:
|
|
153
|
+
logger.info("SQM-LE stopped by user")
|
|
154
|
+
except ConnectionRefusedError:
|
|
155
|
+
logger.info("SQM-LE connection refused")
|
|
156
|
+
finally:
|
|
157
|
+
if self.socket:
|
|
158
|
+
self.socket.close()
|
|
159
|
+
|
|
160
|
+
def _send_command(self, command, sock):
|
|
161
|
+
sock.sendall(command)
|
|
162
|
+
data = sock.recv(1024)
|
|
163
|
+
return data.decode()
|
|
164
|
+
|
|
165
|
+
def _parse_data(self, data, command):
|
|
166
|
+
data = data.split(',')
|
|
167
|
+
if command == READ:
|
|
168
|
+
return {
|
|
169
|
+
'type': data[0],
|
|
170
|
+
'magnitude' : float(re.sub('m', '', data[1])) * u.mag,
|
|
171
|
+
'frequency' : float(re.sub('Hz', '', data[2])) * u.Hz,
|
|
172
|
+
'period_count' : int(re.sub('c', '', data[3])) * u.count,
|
|
173
|
+
'period_seconds' : float(re.sub('s', '', data[4])) * u.second,
|
|
174
|
+
'temperature' : float(re.sub('C', '', data[5])) * u.C,
|
|
175
|
+
}
|
|
176
|
+
elif command == READ_WITH_SERIAL_NUMBER:
|
|
177
|
+
return {
|
|
178
|
+
'type': data[0],
|
|
179
|
+
'magnitude' : float(re.sub('m', '', data[1])) * u.mag,
|
|
180
|
+
'frequency' : float(re.sub('Hz', '', data[2])) * u.Hz,
|
|
181
|
+
'period_count' : int(re.sub('c', '', data[3])) * u.count,
|
|
182
|
+
'period_seconds' : float(re.sub('s', '', data[4])) * u.second,
|
|
183
|
+
'temperature' : float(re.sub('C', '', data[5])) * u.C,
|
|
184
|
+
'serial_number' : str(int(data[6])),
|
|
185
|
+
}
|
|
186
|
+
elif command == REQUEST_CALIBRATION_INFORMATION:
|
|
187
|
+
return {
|
|
188
|
+
'type': data[0],
|
|
189
|
+
'magnitude_offset_calibration': float(data[1]),
|
|
190
|
+
'dark_period': float(data[2]),
|
|
191
|
+
'temperature_light_calibration': float(data[3]),
|
|
192
|
+
'magnitude_offset_manufacturer': float(data[4]),
|
|
193
|
+
'temperature_dark_calibration': float(data[5]),
|
|
194
|
+
}
|
|
195
|
+
elif command == UNIT_INFORMATION_REQUEST:
|
|
196
|
+
return {
|
|
197
|
+
'type': data[0],
|
|
198
|
+
'protocol_number': data[1],
|
|
199
|
+
'model_number': data[2],
|
|
200
|
+
'feature_number': data[3],
|
|
201
|
+
'serial_number': data[4],
|
|
202
|
+
}
|
|
203
|
+
else:
|
|
204
|
+
logger.error(f"Unknown command: {command}")
|
|
205
|
+
return data
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def __get_header(self, data, filename):
|
|
210
|
+
columns = []
|
|
211
|
+
units = []
|
|
212
|
+
for key in data.keys():
|
|
213
|
+
columns.append(key)
|
|
214
|
+
if isinstance(data[key], Quantity):
|
|
215
|
+
units.append(f"# {key}: {data[key].unit}\n")
|
|
216
|
+
return f"# Filename {filename}\n{''.join(units)}# {self.separator.join(columns)}\n"
|
|
217
|
+
|
|
218
|
+
def __get_line_for_plain_text(self, data):
|
|
219
|
+
fields = []
|
|
220
|
+
for key in data.keys():
|
|
221
|
+
if isinstance(data[key], Quantity):
|
|
222
|
+
fields.append(str(data[key].value))
|
|
223
|
+
else:
|
|
224
|
+
fields.append(str(data[key]))
|
|
225
|
+
return f"{self.separator.join(fields)}\n"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _write_to_txt(self, data):
|
|
229
|
+
filename = get_filename(
|
|
230
|
+
save_files_to=self.save_files_to,
|
|
231
|
+
device_name=self.device.serial_id,
|
|
232
|
+
device_type='sqmle',
|
|
233
|
+
file_format=self.file_format)
|
|
234
|
+
if not os.path.exists(filename):
|
|
235
|
+
header = self.__get_header(data=data, filename=filename)
|
|
236
|
+
with open(filename, 'w') as f:
|
|
237
|
+
f.write(header)
|
|
238
|
+
data_line = self.__get_line_for_plain_text(data=data)
|
|
239
|
+
with open(filename, "a") as f:
|
|
240
|
+
f.write(data_line)
|
|
241
|
+
logger.debug(f"Data written to {filename}")
|
|
242
|
+
|
|
243
|
+
def _write_to_database(self):
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
def _post_to_api(self):
|
|
247
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .tessw4c import TESSW4C
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from dspp_reader.tessw4c import TESSW4C
|
|
8
|
+
from dspp_reader.tools import get_args, setup_logging
|
|
9
|
+
|
|
10
|
+
__version__ = version("dspp-reader")
|
|
11
|
+
|
|
12
|
+
CONFIG_FIELDS = [
|
|
13
|
+
"site_id",
|
|
14
|
+
"site_name",
|
|
15
|
+
"site_latitude",
|
|
16
|
+
"site_longitude",
|
|
17
|
+
"site_elevation",
|
|
18
|
+
"site_timezone",
|
|
19
|
+
"device_type",
|
|
20
|
+
"device_id",
|
|
21
|
+
"device_altitude",
|
|
22
|
+
"device_azimuth",
|
|
23
|
+
"device_ip",
|
|
24
|
+
"device_port",
|
|
25
|
+
"use_udp",
|
|
26
|
+
"udp_bind_ip",
|
|
27
|
+
"udp_port",
|
|
28
|
+
"save_to_file",
|
|
29
|
+
"save_to_database",
|
|
30
|
+
"post_to_api",
|
|
31
|
+
"save_files_to",
|
|
32
|
+
"file_format",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def read_tessw4c(args=None):
|
|
36
|
+
args = get_args(args=args, has_upd=True, default_device_type='tessw4c')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
site = {}
|
|
40
|
+
if args.config_file and os.path.isfile(args.config_file):
|
|
41
|
+
with open(args.config_file, 'r') as f:
|
|
42
|
+
site = yaml.safe_load(f) or {}
|
|
43
|
+
|
|
44
|
+
config = {}
|
|
45
|
+
for field in CONFIG_FIELDS:
|
|
46
|
+
config[field] = site.get(field, getattr(args, field))
|
|
47
|
+
|
|
48
|
+
if args.config_file_example:
|
|
49
|
+
print("# Add this to a .yaml file, reference it later with --config-file <file_name>.yaml")
|
|
50
|
+
print(yaml.dump(config, default_flow_style=False, sort_keys=False))
|
|
51
|
+
sys.exit(0)
|
|
52
|
+
|
|
53
|
+
setup_logging(debug=args.debug, device_type=config['device_type'], device_id=config['device_id'])
|
|
54
|
+
logger = logging.getLogger()
|
|
55
|
+
logger.info(f"Starting TESSW4C reader, Version: {__version__}")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
tessw4c = TESSW4C(
|
|
59
|
+
site_id=config["site_id"],
|
|
60
|
+
site_name=config["site_name"],
|
|
61
|
+
site_latitude=config["site_latitude"],
|
|
62
|
+
site_longitude=config["site_longitude"],
|
|
63
|
+
site_elevation=config["site_elevation"],
|
|
64
|
+
site_timezone=config["site_timezone"],
|
|
65
|
+
device_type=config["device_type"],
|
|
66
|
+
device_id=config["device_id"],
|
|
67
|
+
device_altitude=config["device_altitude"],
|
|
68
|
+
device_azimuth=config["device_azimuth"],
|
|
69
|
+
device_ip=config["device_ip"],
|
|
70
|
+
device_port=config["device_port"],
|
|
71
|
+
use_udp=config["use_udp"],
|
|
72
|
+
udp_bind_ip=config["udp_bind_ip"],
|
|
73
|
+
udp_port=config["udp_port"],
|
|
74
|
+
save_to_file=config["save_to_file"],
|
|
75
|
+
save_to_database=config["save_to_database"],
|
|
76
|
+
post_to_api=config["post_to_api"],
|
|
77
|
+
save_files_to=config["save_files_to"],
|
|
78
|
+
file_format=config["file_format"])
|
|
79
|
+
|
|
80
|
+
tessw4c()
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
print("\n")
|
|
83
|
+
logger.info(f"Exiting TESSW4C reader on user request, Version: {__version__}")
|
|
84
|
+
sys.exit(0)
|