ents 2.3.5__tar.gz → 2.3.6__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.
- {ents-2.3.5 → ents-2.3.6}/PKG-INFO +58 -104
- {ents-2.3.5 → ents-2.3.6}/README.md +56 -102
- {ents-2.3.5 → ents-2.3.6}/pyproject.toml +2 -2
- {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/recorder.py +4 -4
- {ents-2.3.5 → ents-2.3.6}/src/ents/cli.py +35 -8
- ents-2.3.6/src/ents/dirtviz/__init__.py +8 -0
- ents-2.3.6/src/ents/dirtviz/client.py +223 -0
- ents-2.3.6/src/ents/dirtviz/plots.py +50 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/simulator/node.py +40 -2
- {ents-2.3.5 → ents-2.3.6}/.gitignore +0 -0
- {ents-2.3.5 → ents-2.3.6}/LICENSE +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/__init__.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/PingSMU.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/PingSPS.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/README.md +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/__init__.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/linear_regression.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/calibrate/plots.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/config/README.md +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/config/__init__.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/config/adv_trace.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/config/user_config.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/proto/__init__.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/proto/decode.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/proto/encode.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/proto/esp32.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/proto/sensor.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/proto/sensor_pb2.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/proto/soil_power_sensor_pb2.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/src/ents/simulator/__init__.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/tests/__init__.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/tests/test_generic.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/tests/test_sensor.py +0 -0
- {ents-2.3.5 → ents-2.3.6}/tests/test_simulator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ents
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.6
|
|
4
4
|
Summary: Python package for Environmental NeTworked Sensor (ENTS)
|
|
5
5
|
Project-URL: Homepage, https://github.com/jlab-sensing/soil-power-sensor-firmware
|
|
6
6
|
Project-URL: Issues, https://github.com/jlab-sensing/soil-power-sensor-firmware/issues
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: >=3.11
|
|
13
13
|
Requires-Dist: matplotlib
|
|
14
14
|
Requires-Dist: pandas
|
|
15
|
-
Requires-Dist: protobuf==6.33.
|
|
15
|
+
Requires-Dist: protobuf==6.33.4
|
|
16
16
|
Requires-Dist: pyserial
|
|
17
17
|
Requires-Dist: requests
|
|
18
18
|
Requires-Dist: scikit-learn
|
|
@@ -34,14 +34,14 @@ The soil power sensor protobuf protocol is implemented as a Python package that
|
|
|
34
34
|
Use the following to install the `ents` package with gui via `pip`:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
pip install ents
|
|
37
|
+
pip install ents
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
You can also install the package from source with the following:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# install package
|
|
44
|
-
pip install .
|
|
44
|
+
pip install .
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
If you are planning to develop the package we recommend you install the package
|
|
@@ -51,114 +51,68 @@ reinstall it.
|
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
# install development dependencies
|
|
54
|
-
pip install -e .[
|
|
54
|
+
pip install -e .[dev]
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
To install the *deprecated* user config gui, use the following: =
|
|
58
|
+
```bash
|
|
59
|
+
pip install -e ents[gui]
|
|
60
|
+
```
|
|
58
61
|
|
|
59
|
-
The following example code demonstrates decoding the measurement message and encoding a response.
|
|
60
62
|
|
|
61
|
-
```python
|
|
62
|
-
from ents import encode, decode
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
data = ...
|
|
64
|
+
## Simulator (New)
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
The webserver `tools/http_decoder.py` can be used to decode uploaded measurements.
|
|
67
|
+
|
|
68
|
+
### CLI Usage
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
usage: ents sim_generic [-h] [-v] [--url URL] --sensor SENSOR [SENSOR ...] [--min MIN] [--max MAX] --cell CELL --logger LOGGER [--start START] [--end END] [--freq FREQ] {batch,stream}
|
|
72
|
+
|
|
73
|
+
positional arguments:
|
|
74
|
+
{batch,stream} Upload mode
|
|
75
|
+
|
|
76
|
+
options:
|
|
77
|
+
-h, --help show this help message and exit
|
|
78
|
+
-v, --verbose Print addiitional request information.
|
|
79
|
+
--url URL URL of the dirtviz instance (default: http://localhost:8000)
|
|
80
|
+
--sensor SENSOR [SENSOR ...]
|
|
81
|
+
Type of sensor to simulate
|
|
82
|
+
--min MIN Minimum sensor value (default: -1.0)
|
|
83
|
+
--max MAX Maximum sensor value (default: 1.0)
|
|
84
|
+
--cell CELL Cell Id
|
|
85
|
+
--logger LOGGER Logger Id
|
|
86
|
+
|
|
87
|
+
Batch:
|
|
88
|
+
--start START Start date
|
|
89
|
+
--end END End date
|
|
90
|
+
|
|
91
|
+
Stream:
|
|
92
|
+
--freq FREQ Frequency of uploads (default: 10s)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Examples
|
|
96
|
+
|
|
97
|
+
You can find the available sensors in the `sensors.proto` file.
|
|
98
|
+
|
|
99
|
+
Example uploading single measurement
|
|
100
|
+
```
|
|
101
|
+
ents sim_generic stream --sensor POWER_VOLTAGE --min 20 --max 30 --cell 1 --logger 1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Example uploading multiple measuremnets
|
|
105
|
+
```
|
|
106
|
+
ents sim_generic stream --sensor TEROS12_VWC_ADJ TEROS12_TEMP TEROS12_EC --min 10 --max 100 --cell 1 --logger 1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Example batch uploads
|
|
110
|
+
```
|
|
111
|
+
ents sim_generic batch --sensor POWER_CURRENT --cell 1 --logger 1 --start 2026-01-19 --end 2026-01-20 --freq 60
|
|
112
|
+
```
|
|
68
113
|
|
|
69
|
-
# process data
|
|
70
|
-
...
|
|
71
114
|
|
|
72
|
-
|
|
73
|
-
resp_str = encode(success=True)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The formatting of the dictionary depends on the type of measurement sent. The key `type` is included on all measurement types and can be used to determine the type of message. See the source `*.proto` files to get the full list of types to get the full list of types and keys. A list is provided in [Message Types](#message-types). The Python protobuf API uses camel case when naming keys. The key `ts` is in ISO 8601 format as a string.
|
|
77
|
-
|
|
78
|
-
## Message Types
|
|
79
|
-
|
|
80
|
-
Type `power`
|
|
81
|
-
```python
|
|
82
|
-
meas_dict = {
|
|
83
|
-
"type": "power",
|
|
84
|
-
"loggerId": ...,
|
|
85
|
-
"cellId": ...,
|
|
86
|
-
"ts": ...,
|
|
87
|
-
"data": {
|
|
88
|
-
"voltage": ...,
|
|
89
|
-
"current": ...
|
|
90
|
-
},
|
|
91
|
-
"data_type": {
|
|
92
|
-
"voltage": float,
|
|
93
|
-
"voltage": float
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Type `teros12`
|
|
99
|
-
```python
|
|
100
|
-
meas_dict = {
|
|
101
|
-
"type": "teros12",
|
|
102
|
-
"loggerId": ...,
|
|
103
|
-
"cellId": ...,
|
|
104
|
-
"ts": ...,
|
|
105
|
-
"data": {
|
|
106
|
-
"vwcRaw": ...,
|
|
107
|
-
"vwcAdj": ...,
|
|
108
|
-
"temp": ...,
|
|
109
|
-
"ec": ...
|
|
110
|
-
},
|
|
111
|
-
"data_type": {
|
|
112
|
-
"vwcRaw": float,
|
|
113
|
-
"vwcAdj": float,
|
|
114
|
-
"temp": float,
|
|
115
|
-
"ec": int
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Type `bme280` with `raw=True` (default)
|
|
121
|
-
```python
|
|
122
|
-
meas_dict = {
|
|
123
|
-
"type": "bme280",
|
|
124
|
-
"loggerId": ...,
|
|
125
|
-
"cellId": ...,
|
|
126
|
-
"ts": ...,
|
|
127
|
-
"data": {
|
|
128
|
-
"pressure": ...,
|
|
129
|
-
"temperature": ...,
|
|
130
|
-
"humidity": ...,
|
|
131
|
-
},
|
|
132
|
-
"data_type": {
|
|
133
|
-
"pressure": int,
|
|
134
|
-
"temperature": int,
|
|
135
|
-
"humidity": int,
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Type `bme280` with `raw=False`
|
|
141
|
-
```python
|
|
142
|
-
meas_dict = {
|
|
143
|
-
"type": "bme280",
|
|
144
|
-
"loggerId": ...,
|
|
145
|
-
"cellId": ...,
|
|
146
|
-
"ts": ...,
|
|
147
|
-
"data": {
|
|
148
|
-
"pressure": ...,
|
|
149
|
-
"temperature": ...,
|
|
150
|
-
"humidity": ...,
|
|
151
|
-
},
|
|
152
|
-
"data_type": {
|
|
153
|
-
"pressure": float,
|
|
154
|
-
"temperature": float,
|
|
155
|
-
"humidity": float,
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
## Simulator
|
|
115
|
+
## Simulator (Old)
|
|
162
116
|
|
|
163
117
|
Simulate WiFi sensor uploads without requiring ENTS hardware.
|
|
164
118
|
|
|
@@ -8,14 +8,14 @@ The soil power sensor protobuf protocol is implemented as a Python package that
|
|
|
8
8
|
Use the following to install the `ents` package with gui via `pip`:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
pip install ents
|
|
11
|
+
pip install ents
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
You can also install the package from source with the following:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
# install package
|
|
18
|
-
pip install .
|
|
18
|
+
pip install .
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
If you are planning to develop the package we recommend you install the package
|
|
@@ -25,114 +25,68 @@ reinstall it.
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
# install development dependencies
|
|
28
|
-
pip install -e .[
|
|
28
|
+
pip install -e .[dev]
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
To install the *deprecated* user config gui, use the following: =
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e ents[gui]
|
|
34
|
+
```
|
|
32
35
|
|
|
33
|
-
The following example code demonstrates decoding the measurement message and encoding a response.
|
|
34
36
|
|
|
35
|
-
```python
|
|
36
|
-
from ents import encode, decode
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
data = ...
|
|
38
|
+
## Simulator (New)
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
The webserver `tools/http_decoder.py` can be used to decode uploaded measurements.
|
|
41
|
+
|
|
42
|
+
### CLI Usage
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
usage: ents sim_generic [-h] [-v] [--url URL] --sensor SENSOR [SENSOR ...] [--min MIN] [--max MAX] --cell CELL --logger LOGGER [--start START] [--end END] [--freq FREQ] {batch,stream}
|
|
46
|
+
|
|
47
|
+
positional arguments:
|
|
48
|
+
{batch,stream} Upload mode
|
|
49
|
+
|
|
50
|
+
options:
|
|
51
|
+
-h, --help show this help message and exit
|
|
52
|
+
-v, --verbose Print addiitional request information.
|
|
53
|
+
--url URL URL of the dirtviz instance (default: http://localhost:8000)
|
|
54
|
+
--sensor SENSOR [SENSOR ...]
|
|
55
|
+
Type of sensor to simulate
|
|
56
|
+
--min MIN Minimum sensor value (default: -1.0)
|
|
57
|
+
--max MAX Maximum sensor value (default: 1.0)
|
|
58
|
+
--cell CELL Cell Id
|
|
59
|
+
--logger LOGGER Logger Id
|
|
60
|
+
|
|
61
|
+
Batch:
|
|
62
|
+
--start START Start date
|
|
63
|
+
--end END End date
|
|
64
|
+
|
|
65
|
+
Stream:
|
|
66
|
+
--freq FREQ Frequency of uploads (default: 10s)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Examples
|
|
70
|
+
|
|
71
|
+
You can find the available sensors in the `sensors.proto` file.
|
|
72
|
+
|
|
73
|
+
Example uploading single measurement
|
|
74
|
+
```
|
|
75
|
+
ents sim_generic stream --sensor POWER_VOLTAGE --min 20 --max 30 --cell 1 --logger 1
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Example uploading multiple measuremnets
|
|
79
|
+
```
|
|
80
|
+
ents sim_generic stream --sensor TEROS12_VWC_ADJ TEROS12_TEMP TEROS12_EC --min 10 --max 100 --cell 1 --logger 1
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Example batch uploads
|
|
84
|
+
```
|
|
85
|
+
ents sim_generic batch --sensor POWER_CURRENT --cell 1 --logger 1 --start 2026-01-19 --end 2026-01-20 --freq 60
|
|
86
|
+
```
|
|
42
87
|
|
|
43
|
-
# process data
|
|
44
|
-
...
|
|
45
88
|
|
|
46
|
-
|
|
47
|
-
resp_str = encode(success=True)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The formatting of the dictionary depends on the type of measurement sent. The key `type` is included on all measurement types and can be used to determine the type of message. See the source `*.proto` files to get the full list of types to get the full list of types and keys. A list is provided in [Message Types](#message-types). The Python protobuf API uses camel case when naming keys. The key `ts` is in ISO 8601 format as a string.
|
|
51
|
-
|
|
52
|
-
## Message Types
|
|
53
|
-
|
|
54
|
-
Type `power`
|
|
55
|
-
```python
|
|
56
|
-
meas_dict = {
|
|
57
|
-
"type": "power",
|
|
58
|
-
"loggerId": ...,
|
|
59
|
-
"cellId": ...,
|
|
60
|
-
"ts": ...,
|
|
61
|
-
"data": {
|
|
62
|
-
"voltage": ...,
|
|
63
|
-
"current": ...
|
|
64
|
-
},
|
|
65
|
-
"data_type": {
|
|
66
|
-
"voltage": float,
|
|
67
|
-
"voltage": float
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Type `teros12`
|
|
73
|
-
```python
|
|
74
|
-
meas_dict = {
|
|
75
|
-
"type": "teros12",
|
|
76
|
-
"loggerId": ...,
|
|
77
|
-
"cellId": ...,
|
|
78
|
-
"ts": ...,
|
|
79
|
-
"data": {
|
|
80
|
-
"vwcRaw": ...,
|
|
81
|
-
"vwcAdj": ...,
|
|
82
|
-
"temp": ...,
|
|
83
|
-
"ec": ...
|
|
84
|
-
},
|
|
85
|
-
"data_type": {
|
|
86
|
-
"vwcRaw": float,
|
|
87
|
-
"vwcAdj": float,
|
|
88
|
-
"temp": float,
|
|
89
|
-
"ec": int
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Type `bme280` with `raw=True` (default)
|
|
95
|
-
```python
|
|
96
|
-
meas_dict = {
|
|
97
|
-
"type": "bme280",
|
|
98
|
-
"loggerId": ...,
|
|
99
|
-
"cellId": ...,
|
|
100
|
-
"ts": ...,
|
|
101
|
-
"data": {
|
|
102
|
-
"pressure": ...,
|
|
103
|
-
"temperature": ...,
|
|
104
|
-
"humidity": ...,
|
|
105
|
-
},
|
|
106
|
-
"data_type": {
|
|
107
|
-
"pressure": int,
|
|
108
|
-
"temperature": int,
|
|
109
|
-
"humidity": int,
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Type `bme280` with `raw=False`
|
|
115
|
-
```python
|
|
116
|
-
meas_dict = {
|
|
117
|
-
"type": "bme280",
|
|
118
|
-
"loggerId": ...,
|
|
119
|
-
"cellId": ...,
|
|
120
|
-
"ts": ...,
|
|
121
|
-
"data": {
|
|
122
|
-
"pressure": ...,
|
|
123
|
-
"temperature": ...,
|
|
124
|
-
"humidity": ...,
|
|
125
|
-
},
|
|
126
|
-
"data_type": {
|
|
127
|
-
"pressure": float,
|
|
128
|
-
"temperature": float,
|
|
129
|
-
"humidity": float,
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
## Simulator
|
|
89
|
+
## Simulator (Old)
|
|
136
90
|
|
|
137
91
|
Simulate WiFi sensor uploads without requiring ENTS hardware.
|
|
138
92
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ents"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.6"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="John Madden", email="jmadden173@pm.me" },
|
|
10
10
|
]
|
|
@@ -17,7 +17,7 @@ classifiers = [
|
|
|
17
17
|
"Operating System :: OS Independent",
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
|
-
'protobuf==6.33.
|
|
20
|
+
'protobuf==6.33.4',
|
|
21
21
|
'matplotlib',
|
|
22
22
|
'pandas',
|
|
23
23
|
'pyserial',
|
|
@@ -16,7 +16,7 @@ import socket
|
|
|
16
16
|
import serial
|
|
17
17
|
from typing import Tuple
|
|
18
18
|
from tqdm import tqdm
|
|
19
|
-
from ..proto import
|
|
19
|
+
from ..proto.sensor import decode_repeated_sensor_measurements
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class SerialController:
|
|
@@ -116,10 +116,10 @@ class SoilPowerSensorController(SerialController):
|
|
|
116
116
|
|
|
117
117
|
reply = self.ser.read(resp_len) # read said measurment
|
|
118
118
|
|
|
119
|
-
meas_dict =
|
|
119
|
+
meas_dict = decode_repeated_sensor_measurements(reply) # decode using protobuf
|
|
120
120
|
|
|
121
|
-
voltage_value = meas_dict["
|
|
122
|
-
current_value = meas_dict["
|
|
121
|
+
voltage_value = meas_dict["measurements"][0]["decimal"]
|
|
122
|
+
current_value = meas_dict["measurements"][1]["decimal"]
|
|
123
123
|
|
|
124
124
|
return float(voltage_value), float(current_value)
|
|
125
125
|
|
|
@@ -36,7 +36,7 @@ from .proto.sensor import (
|
|
|
36
36
|
decode_sensor_response,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
from .simulator.node import NodeSimulator
|
|
39
|
+
from .simulator.node import NodeSimulator, NodeSimulatorGeneric
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def entry():
|
|
@@ -68,15 +68,22 @@ def create_sim_generic_parser(subparsers):
|
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
sim_p = subparsers.add_parser("sim_generic", help="Simluate generic sensor uploads")
|
|
71
|
+
|
|
72
|
+
sim_p.add_argument(
|
|
73
|
+
"-v",
|
|
74
|
+
"--verbose",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Print addiitional request information.",
|
|
77
|
+
)
|
|
78
|
+
|
|
71
79
|
sim_p.add_argument(
|
|
72
80
|
"--url",
|
|
73
|
-
|
|
81
|
+
default="http://localhost:8000/api/sensor",
|
|
74
82
|
type=str,
|
|
75
83
|
help="URL of the dirtviz instance (default: http://localhost:8000)",
|
|
76
84
|
)
|
|
77
85
|
sim_p.add_argument(
|
|
78
|
-
"
|
|
79
|
-
required=True,
|
|
86
|
+
"mode",
|
|
80
87
|
choices=["batch", "stream"],
|
|
81
88
|
type=str,
|
|
82
89
|
help="Upload mode",
|
|
@@ -88,6 +95,7 @@ def create_sim_generic_parser(subparsers):
|
|
|
88
95
|
nargs="+",
|
|
89
96
|
help="Type of sensor to simulate",
|
|
90
97
|
)
|
|
98
|
+
|
|
91
99
|
sim_p.add_argument(
|
|
92
100
|
"--min", type=float, default=-1.0, help="Minimum sensor value (default: -1.0)"
|
|
93
101
|
)
|
|
@@ -96,11 +104,16 @@ def create_sim_generic_parser(subparsers):
|
|
|
96
104
|
)
|
|
97
105
|
sim_p.add_argument("--cell", required=True, type=int, help="Cell Id")
|
|
98
106
|
sim_p.add_argument("--logger", required=True, type=int, help="Logger Id")
|
|
99
|
-
|
|
100
|
-
sim_p.
|
|
101
|
-
|
|
107
|
+
|
|
108
|
+
batch = sim_p.add_argument_group("Batch")
|
|
109
|
+
batch.add_argument("--start", type=str, help="Start date")
|
|
110
|
+
batch.add_argument("--end", type=str, help="End date")
|
|
111
|
+
|
|
112
|
+
stream = sim_p.add_argument_group("Stream")
|
|
113
|
+
stream.add_argument(
|
|
102
114
|
"--freq", default=10.0, type=float, help="Frequency of uploads (default: 10s)"
|
|
103
115
|
)
|
|
116
|
+
|
|
104
117
|
sim_p.set_defaults(func=simulate_generic)
|
|
105
118
|
|
|
106
119
|
return sim_p
|
|
@@ -149,7 +162,12 @@ def create_sim_parser(subparsers):
|
|
|
149
162
|
|
|
150
163
|
|
|
151
164
|
def simulate_generic(args):
|
|
152
|
-
|
|
165
|
+
if args.verbose:
|
|
166
|
+
print("Arguments:")
|
|
167
|
+
print(args)
|
|
168
|
+
print()
|
|
169
|
+
|
|
170
|
+
simulation = NodeSimulatorGeneric(
|
|
153
171
|
cell=args.cell,
|
|
154
172
|
logger=args.logger,
|
|
155
173
|
sensors=args.sensor,
|
|
@@ -184,8 +202,17 @@ def simulate_generic(args):
|
|
|
184
202
|
dt = datetime.now()
|
|
185
203
|
ts = int(dt.timestamp())
|
|
186
204
|
simulation.measure(ts)
|
|
205
|
+
|
|
187
206
|
while simulation.send_next(args.url):
|
|
188
207
|
print(simulation)
|
|
208
|
+
|
|
209
|
+
if args.verbose:
|
|
210
|
+
print("Request")
|
|
211
|
+
print(simulation.last_request())
|
|
212
|
+
print("\nResponse")
|
|
213
|
+
print(simulation.last_response())
|
|
214
|
+
print()
|
|
215
|
+
|
|
189
216
|
time.sleep(args.freq)
|
|
190
217
|
except KeyboardInterrupt as _:
|
|
191
218
|
print("Stopping simulation")
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Client interface with dirtviz.
|
|
2
|
+
|
|
3
|
+
TODO:
|
|
4
|
+
- Add caching of data
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Cell:
|
|
15
|
+
"""Class representing a cell in the Dirtviz API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, data: str):
|
|
18
|
+
"""Initialize the Cell object from a cell ID.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: json data from the Dirtviz API containing cell information.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
self.id = data["id"]
|
|
25
|
+
self.name = data["name"]
|
|
26
|
+
self.location = data["location"]
|
|
27
|
+
self.latitude = data["latitude"]
|
|
28
|
+
self.longitude = data["longitude"]
|
|
29
|
+
|
|
30
|
+
def __repr__(self):
|
|
31
|
+
return f"Cell(id={self.cell_id}, name={self.name})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BackendClient:
|
|
35
|
+
"""Client for interacting with the Dirtviz API."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, base_url: str = "https://dirtviz.jlab.ucsc.edu/api/"):
|
|
38
|
+
"""Initialize the BackendClient.
|
|
39
|
+
|
|
40
|
+
Sets the base URL for the API. Defaults to the Dirtviz API.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
self.base_url = base_url
|
|
44
|
+
|
|
45
|
+
def get(self, endpoint: str, params: dict = None) -> dict:
|
|
46
|
+
"""Get request to the API.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
endpoint: The API endpoint to request.
|
|
50
|
+
params: Optional parameters for the request.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A dictionary containing the response data.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
url = f"{self.base_url}{endpoint}"
|
|
57
|
+
response = requests.get(url, params=params)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
|
|
60
|
+
return response.json()
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def time_to_params(start: datetime, end: datetime) -> dict:
|
|
64
|
+
"""Puts start and end datetime into an API paramter dictionary
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
dt: The datetime object to format.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A string representing the formatted datetime.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
timestamp_format = "%a, %d %b %Y %H:%M:%S GMT"
|
|
74
|
+
|
|
75
|
+
start_str = start.strftime(timestamp_format)
|
|
76
|
+
end_str = end.strftime(timestamp_format)
|
|
77
|
+
|
|
78
|
+
params = {
|
|
79
|
+
"startTime": start_str,
|
|
80
|
+
"endTime": end_str,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return params
|
|
84
|
+
|
|
85
|
+
def power_data(self, cell: Cell, start: datetime, end: datetime) -> pd.DataFrame:
|
|
86
|
+
"""Gets power data for a specific cell by name.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
cell: The Cell object for which to get power data.
|
|
90
|
+
start: The start date of the data.
|
|
91
|
+
end: The end date of the data.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A pandas DataFrame containing the power data.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
endpoint = f"/power/{cell.id}"
|
|
98
|
+
|
|
99
|
+
params = self.time_to_params(start, end)
|
|
100
|
+
|
|
101
|
+
data = self.get(endpoint, params=params)
|
|
102
|
+
|
|
103
|
+
data_df = pd.DataFrame(data)
|
|
104
|
+
data_df["timestamp"] = pd.to_datetime(data_df["timestamp"])
|
|
105
|
+
|
|
106
|
+
return data_df
|
|
107
|
+
|
|
108
|
+
def teros_data(self, cell: Cell, start: datetime, end: datetime) -> pd.DataFrame:
|
|
109
|
+
"""Gets teros data for a specific cell
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
cell: The Cell object for which to get teros data.
|
|
113
|
+
start: The start date of the data.
|
|
114
|
+
end: The end date of the data.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A pandas DataFrame containing the teros data with columns vwc_raw,
|
|
118
|
+
vwc_adj, temp, ec.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
endpoint = f"/teros/{cell.id}"
|
|
122
|
+
|
|
123
|
+
params = self.time_to_params(start, end)
|
|
124
|
+
|
|
125
|
+
data = self.get(endpoint, params=params)
|
|
126
|
+
|
|
127
|
+
data_df = pd.DataFrame(data)
|
|
128
|
+
data_df["timestamp"] = pd.to_datetime(data_df["timestamp"])
|
|
129
|
+
|
|
130
|
+
return data_df
|
|
131
|
+
|
|
132
|
+
def sensor_data(
|
|
133
|
+
self,
|
|
134
|
+
cell: Cell,
|
|
135
|
+
name: str,
|
|
136
|
+
meas: str,
|
|
137
|
+
start: datetime,
|
|
138
|
+
end: datetime,
|
|
139
|
+
resample: str = "none",
|
|
140
|
+
) -> pd.DataFrame:
|
|
141
|
+
"""Gets generic sensor data for a specific cell
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
cell: The Cell object for which to get sensor data.
|
|
145
|
+
name: Name of the sensor (e.g., "power", "teros").
|
|
146
|
+
meas: The measurement type (e.g., "v", "i", "vwc", "temp", "ec").
|
|
147
|
+
start: The start date of the data.
|
|
148
|
+
end: The end date of the data.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A pandas DataFrame containing the sensor data.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
endpoint = "/sensor/"
|
|
155
|
+
|
|
156
|
+
params = {
|
|
157
|
+
"cellId": cell.id,
|
|
158
|
+
"name": name,
|
|
159
|
+
"measurement": meas,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
params = params | self.time_to_params(start, end)
|
|
163
|
+
|
|
164
|
+
data = self.get(endpoint, params=params)
|
|
165
|
+
|
|
166
|
+
data_df = pd.DataFrame(data)
|
|
167
|
+
data_df["timestamp"] = pd.to_datetime(data_df["timestamp"])
|
|
168
|
+
|
|
169
|
+
return data_df
|
|
170
|
+
|
|
171
|
+
def cell_from_id(self, cell_id: int) -> Cell | None:
|
|
172
|
+
"""Get a Cell object from its ID.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
cell_id: The ID of the cell.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A Cell object. None if the cell does not exist.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
cell_list = self.cells()
|
|
182
|
+
|
|
183
|
+
for cell in cell_list:
|
|
184
|
+
if cell.id == cell_id:
|
|
185
|
+
return cell
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def cell_from_name(self, name: str) -> Cell | None:
|
|
190
|
+
"""Get a Cell object from its name.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
name: The name of the cell.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A Cell object. None if the cell does not exist.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
cell_list = self.cells()
|
|
200
|
+
|
|
201
|
+
for cell in cell_list:
|
|
202
|
+
if cell.name == name:
|
|
203
|
+
return cell
|
|
204
|
+
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
def cells(self) -> list[Cell]:
|
|
208
|
+
"""Gets a list of all cells from the API.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
A list of Cell objects.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
cell_list = []
|
|
215
|
+
|
|
216
|
+
endpoint = "/cell/id"
|
|
217
|
+
cell_data_list = self.get(endpoint)
|
|
218
|
+
|
|
219
|
+
for c in cell_data_list:
|
|
220
|
+
cell = Cell(c)
|
|
221
|
+
cell_list.append(cell)
|
|
222
|
+
|
|
223
|
+
return cell_list
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Common plots for data on dirtviz.
|
|
2
|
+
|
|
3
|
+
Still need to fill in with common functions:
|
|
4
|
+
- Plot power (voltage, current, calcualted power)
|
|
5
|
+
- Plot teros12
|
|
6
|
+
- Plot bme280
|
|
7
|
+
- Plot all (take all available sensors)
|
|
8
|
+
- Plot group (find mean/stddev for each point)
|
|
9
|
+
|
|
10
|
+
Can pull from personal scripts from SenSys (jtmadden)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# matplotlib formatting
|
|
19
|
+
# plt.rcParams["font.size"] = 7
|
|
20
|
+
# plt.rcParams['font.weight'] = 'medium'
|
|
21
|
+
# plt.ion()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def plot_data(data: list[pd.DataFrame], name, **kwargs):
|
|
25
|
+
"""Plots data from one or many cells
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
data: List of dataframes
|
|
29
|
+
name: Column or measurement name
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
fig, ax = plt.subplots()
|
|
33
|
+
for d in data:
|
|
34
|
+
ax.plot(d["timestamp"], d[name], **kwargs)
|
|
35
|
+
|
|
36
|
+
ax.axhline(
|
|
37
|
+
y=0,
|
|
38
|
+
color="black",
|
|
39
|
+
linewidth=2,
|
|
40
|
+
dashes=(2, 2),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
ax.set_xlabel("Timestamp")
|
|
44
|
+
ax.set_ylabel(name)
|
|
45
|
+
|
|
46
|
+
ax.grid()
|
|
47
|
+
|
|
48
|
+
plt.show(block=False)
|
|
49
|
+
|
|
50
|
+
return ax
|
|
@@ -172,6 +172,8 @@ class NodeSimulatorGeneric:
|
|
|
172
172
|
measurements: list[bytes] = []
|
|
173
173
|
# all responses
|
|
174
174
|
responses: list[str] = []
|
|
175
|
+
# all requests in format (headers, body)
|
|
176
|
+
requests: list[tuple[str, str]] = []
|
|
175
177
|
|
|
176
178
|
# metrics for uploads
|
|
177
179
|
metrics: dict[str, int] = {
|
|
@@ -233,14 +235,18 @@ class NodeSimulatorGeneric:
|
|
|
233
235
|
|
|
234
236
|
# get next measurement
|
|
235
237
|
try:
|
|
236
|
-
meas = self.
|
|
238
|
+
meas = self.measurement_buffer.pop()
|
|
237
239
|
except IndexError as _:
|
|
238
240
|
return False
|
|
239
241
|
|
|
240
|
-
headers = {
|
|
242
|
+
headers = {
|
|
243
|
+
"Content-Type": "application/octet-stream",
|
|
244
|
+
"SensorVersion": "2",
|
|
245
|
+
}
|
|
241
246
|
result = requests.post(url, data=meas, headers=headers)
|
|
242
247
|
|
|
243
248
|
# store result
|
|
249
|
+
self.requests.append((result.request.headers, result.request.body))
|
|
244
250
|
self.responses.append(result.text)
|
|
245
251
|
self.metrics["total_requests"] += 1
|
|
246
252
|
if result.status_code == 200:
|
|
@@ -279,4 +285,36 @@ class NodeSimulatorGeneric:
|
|
|
279
285
|
)
|
|
280
286
|
|
|
281
287
|
serialized = encode_repeated_sensor_measurements(meas)
|
|
288
|
+
self.measurements.append(serialized)
|
|
282
289
|
self.measurement_buffer.append(serialized)
|
|
290
|
+
|
|
291
|
+
def last_measurement(self) -> bytes:
|
|
292
|
+
"""Gets the last encoded measurement.
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Last encoded measurement.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
return self.measurements[-1]
|
|
300
|
+
|
|
301
|
+
def last_request(self) -> str:
|
|
302
|
+
"""Gets the last sent request.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Formatted headers and body.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
headers = self.requests[-1][0]
|
|
309
|
+
body = self.requests[-1][1]
|
|
310
|
+
request_str = f"{headers}\n\n{body}"
|
|
311
|
+
return request_str
|
|
312
|
+
|
|
313
|
+
def last_response(self) -> str:
|
|
314
|
+
"""Gets the last response.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Response from server.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
return self.responses[-1]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|