ents 2.3.4__py3-none-any.whl → 2.3.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ents/__init__.py CHANGED
@@ -3,10 +3,11 @@ from .proto.encode import (
3
3
  encode_power_measurement,
4
4
  encode_teros12_measurement,
5
5
  encode_phytos31_measurement,
6
+ encode_user_configuration,
6
7
  encode_bme280_measurement,
7
8
  )
8
9
 
9
- from .proto.decode import decode_response, decode_measurement
10
+ from .proto.decode import decode_response, decode_measurement, decode_user_configuration
10
11
 
11
12
  from .proto.esp32 import encode_esp32command, decode_esp32command
12
13
 
@@ -20,4 +21,6 @@ __all__ = [
20
21
  "decode_measurement",
21
22
  "encode_esp32command",
22
23
  "decode_esp32command",
24
+ "encode_user_configuration",
25
+ "decode_user_configuration",
23
26
  ]
@@ -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 decode_measurement
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 = decode_measurement(reply) # decode using protobuf
119
+ meas_dict = decode_repeated_sensor_measurements(reply) # decode using protobuf
120
120
 
121
- voltage_value = meas_dict["data"]["voltage"]
122
- current_value = meas_dict["data"]["current"]
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
 
ents/cli.py CHANGED
@@ -29,7 +29,14 @@ from .proto.encode import (
29
29
  from .proto.decode import decode_measurement, decode_response
30
30
  from .proto.esp32 import encode_esp32command, decode_esp32command
31
31
 
32
- from .simulator.node import NodeSimulator
32
+ from .proto.sensor import (
33
+ encode_repeated_sensor_measurements,
34
+ decode_repeated_sensor_measurements,
35
+ encode_sensor_response,
36
+ decode_sensor_response,
37
+ )
38
+
39
+ from .simulator.node import NodeSimulator, NodeSimulatorGeneric
33
40
 
34
41
 
35
42
  def entry():
@@ -37,17 +44,81 @@ def entry():
37
44
 
38
45
  parser = argparse.ArgumentParser()
39
46
 
40
- subparsers = parser.add_subparsers(help="Ents Utilities")
47
+ subparsers = parser.add_subparsers(help="Ents Utilities", required=True)
41
48
 
42
49
  create_encode_parser(subparsers)
50
+ create_encode_generic_parser(subparsers)
43
51
  create_decode_parser(subparsers)
52
+ create_decode_generic_parser(subparsers)
44
53
  create_calib_parser(subparsers)
45
54
  create_sim_parser(subparsers)
55
+ create_sim_generic_parser(subparsers)
46
56
 
47
57
  args = parser.parse_args()
48
58
  args.func(args)
49
59
 
50
60
 
61
+ def create_sim_generic_parser(subparsers):
62
+ """Creates the generic simulation subparser
63
+
64
+ Args:
65
+ subparsers: Reference to subparser group
66
+ Returns:
67
+ Reference to new subparser
68
+ """
69
+
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
+
79
+ sim_p.add_argument(
80
+ "--url",
81
+ default="http://localhost:8000/api/sensor",
82
+ type=str,
83
+ help="URL of the dirtviz instance (default: http://localhost:8000)",
84
+ )
85
+ sim_p.add_argument(
86
+ "mode",
87
+ choices=["batch", "stream"],
88
+ type=str,
89
+ help="Upload mode",
90
+ )
91
+ sim_p.add_argument(
92
+ "--sensor",
93
+ required=True,
94
+ type=str,
95
+ nargs="+",
96
+ help="Type of sensor to simulate",
97
+ )
98
+
99
+ sim_p.add_argument(
100
+ "--min", type=float, default=-1.0, help="Minimum sensor value (default: -1.0)"
101
+ )
102
+ sim_p.add_argument(
103
+ "--max", type=float, default=1.0, help="Maximum sensor value (default: 1.0)"
104
+ )
105
+ sim_p.add_argument("--cell", required=True, type=int, help="Cell Id")
106
+ sim_p.add_argument("--logger", required=True, type=int, help="Logger Id")
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(
114
+ "--freq", default=10.0, type=float, help="Frequency of uploads (default: 10s)"
115
+ )
116
+
117
+ sim_p.set_defaults(func=simulate_generic)
118
+
119
+ return sim_p
120
+
121
+
51
122
  def create_sim_parser(subparsers):
52
123
  """Creates the simulation subparser
53
124
 
@@ -90,6 +161,63 @@ def create_sim_parser(subparsers):
90
161
  return sim_p
91
162
 
92
163
 
164
+ def simulate_generic(args):
165
+ if args.verbose:
166
+ print("Arguments:")
167
+ print(args)
168
+ print()
169
+
170
+ simulation = NodeSimulatorGeneric(
171
+ cell=args.cell,
172
+ logger=args.logger,
173
+ sensors=args.sensor,
174
+ _min=args.min,
175
+ _max=args.max,
176
+ )
177
+
178
+ if args.mode == "batch":
179
+ if (args.start is None) or (args.end is None):
180
+ raise ValueError("Start and end date must be provided for batch mode.")
181
+
182
+ # format dates
183
+ curr_dt = datetime.fromisoformat(args.start)
184
+ end_dt = datetime.fromisoformat(args.end)
185
+
186
+ # create list of measurements
187
+ while curr_dt <= end_dt:
188
+ ts = int(curr_dt.timestamp())
189
+ simulation.measure(ts)
190
+ curr_dt += timedelta(seconds=args.freq)
191
+
192
+ # send measurements
193
+ while simulation.send_next(args.url):
194
+ print(simulation)
195
+
196
+ print("Done!")
197
+
198
+ elif args.mode == "stream":
199
+ print("Use CTRL+C to stop the simulation")
200
+ try:
201
+ while True:
202
+ dt = datetime.now()
203
+ ts = int(dt.timestamp())
204
+ simulation.measure(ts)
205
+
206
+ while simulation.send_next(args.url):
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
+
216
+ time.sleep(args.freq)
217
+ except KeyboardInterrupt as _:
218
+ print("Stopping simulation")
219
+
220
+
93
221
  def simulate(args):
94
222
  simulation = NodeSimulator(
95
223
  cell=args.cell,
@@ -169,6 +297,133 @@ def create_calib_parser(subparsers):
169
297
  return calib_p
170
298
 
171
299
 
300
+ def create_encode_generic_parser(subparsers):
301
+ """Create generic encode command subparser
302
+
303
+ Args:
304
+ subparsers: Reference to subparser group
305
+
306
+ Returns:
307
+ Reference to new subparser
308
+ """
309
+
310
+ encode_parser = subparsers.add_parser("encode_generic", help="Encode generic data")
311
+
312
+ print_format = encode_parser.add_mutually_exclusive_group()
313
+ print_format.add_argument(
314
+ "--hex", action="store_true", help="Print as hex values (default)"
315
+ )
316
+ print_format.add_argument(
317
+ "--raw", action="store_true", help="Print raw bytes object"
318
+ )
319
+ print_format.add_argument(
320
+ "--c", action="store_true", help="Print bytes for copying to c"
321
+ )
322
+
323
+ encode_subparsers = encode_parser.add_subparsers(
324
+ title="Message type", dest="type", required=True
325
+ )
326
+
327
+ # sensor measurement
328
+ measurement_parser = encode_subparsers.add_parser(
329
+ "meas", help='Proto "Measurement" message'
330
+ )
331
+ measurement_parser.add_argument("--ts", type=int, help="Unix epoch timestamp")
332
+ measurement_parser.add_argument("--cell", type=int, help="Cell Id")
333
+ measurement_parser.add_argument("--logger", type=int, help="Logger Id")
334
+ measurement_parser.add_argument("--idx", type=int, default=1, help="Upload index")
335
+ measurement_parser.add_argument(
336
+ "--sensor",
337
+ nargs=2,
338
+ metavar=("type", "value"),
339
+ action="append",
340
+ required=True,
341
+ help="Specify as: --sensor <type> <value>",
342
+ )
343
+ measurement_parser.set_defaults(func=handle_encode_generic_measurement)
344
+
345
+ # response
346
+ response_parser = encode_subparsers.add_parser(
347
+ "resp", help='Proto "Response" message'
348
+ )
349
+ response_parser.add_argument(
350
+ "--resp",
351
+ nargs=2,
352
+ metavar=("idx", "status"),
353
+ action="append",
354
+ required=True,
355
+ help="Specify as: --resp <idx> <status>",
356
+ )
357
+ response_parser.set_defaults(func=handle_encode_generic_response)
358
+
359
+ return encode_parser
360
+
361
+
362
+ def parse_number(s: str) -> tuple:
363
+ try:
364
+ i = int(s)
365
+ if i >= 0:
366
+ return i, "unsignedInt"
367
+ return i, "signedInt"
368
+ except ValueError:
369
+ pass
370
+
371
+ try:
372
+ return float(s), "decimal"
373
+ except ValueError:
374
+ pass
375
+
376
+ raise ValueError(f"Invalid numeric value: {s}")
377
+
378
+
379
+ def handle_encode_generic_measurement(args):
380
+ """Take arguments and encode a repeated senosr measurement message"""
381
+
382
+ meas = {
383
+ "meta": {
384
+ "ts": args.ts,
385
+ "cellId": args.cell,
386
+ "loggerId": args.logger,
387
+ },
388
+ "measurements": [],
389
+ }
390
+
391
+ for s in args.sensor:
392
+ val, val_type = parse_number(s[1])
393
+ meas["measurements"].append(
394
+ {
395
+ "type": s[0],
396
+ val_type: val,
397
+ "idx": args.idx,
398
+ }
399
+ )
400
+ args.idx += 1
401
+
402
+ data = encode_repeated_sensor_measurements(meas)
403
+ print_data(args, data)
404
+
405
+
406
+ def handle_encode_generic_response(args):
407
+ """Takes arguments and encode a sensor response message"""
408
+
409
+ resp = {
410
+ "responses": [],
411
+ }
412
+
413
+ for r in args.resp:
414
+ idx = int(r[0])
415
+ status = r[1]
416
+ resp["responses"].append(
417
+ {
418
+ "uploadIndex": idx,
419
+ "status": status,
420
+ }
421
+ )
422
+
423
+ data = encode_sensor_response(resp)
424
+ print_data(args, data)
425
+
426
+
172
427
  def create_encode_parser(subparsers):
173
428
  """Create encode command subparser
174
429
 
@@ -192,14 +447,19 @@ def create_encode_parser(subparsers):
192
447
  "--c", action="store_true", help="Print bytes for copying to c"
193
448
  )
194
449
 
195
- encode_subparsers = encode_parser.add_subparsers(title="Message type", dest="type")
450
+ encode_subparsers = encode_parser.add_subparsers(
451
+ title="Message type",
452
+ dest="type",
453
+ required=True,
454
+ )
196
455
 
197
456
  def create_measurement_parser(encode_subparsers):
198
457
  measurement_parser = encode_subparsers.add_parser(
199
458
  "measurement", help='Proto "Measurement" message'
200
459
  )
201
460
  measurement_subparser = measurement_parser.add_subparsers(
202
- title="Measurement type"
461
+ title="Measurement type",
462
+ required=True,
203
463
  )
204
464
 
205
465
  # metadata
@@ -246,7 +506,9 @@ def create_encode_parser(subparsers):
246
506
  "esp32command", help='Proto "Esp32Command" message'
247
507
  )
248
508
  esp32command_subparser = esp32command_parser.add_subparsers(
249
- title="type", help="PageCommand"
509
+ title="type",
510
+ help="PageCommand",
511
+ required=True,
250
512
  )
251
513
 
252
514
  test_parser = esp32command_subparser.add_parser("test", help="TestCommand")
@@ -383,6 +645,55 @@ def print_bytes_c(data: bytes) -> str:
383
645
  print(f"size_t data_len = {len(hex_str)};")
384
646
 
385
647
 
648
+ def create_decode_generic_parser(subparsers):
649
+ """Create generic decode command parser
650
+
651
+ Args:
652
+ subparsers: Reference to subparser group
653
+
654
+ Returns:
655
+ Reference to new subparser
656
+ """
657
+
658
+ decode_parser = subparsers.add_parser("decode_generic", help="Decode generic data")
659
+
660
+ decode_subparsers = decode_parser.add_subparsers(
661
+ title="Message type",
662
+ dest="type",
663
+ required=True,
664
+ )
665
+
666
+ decode_parser.add_argument(
667
+ "data", type=str, help="Protobuf serialized data in hex format"
668
+ )
669
+
670
+ # sensor measurement
671
+ measurement_parser = decode_subparsers.add_parser(
672
+ "meas", help='Proto "Measurement" message'
673
+ )
674
+ measurement_parser.set_defaults(func=handle_decode_generic_measurement)
675
+
676
+ # response
677
+ response_parser = decode_subparsers.add_parser(
678
+ "resp", help='Proto "Response" message'
679
+ )
680
+ response_parser.set_defaults(func=handle_decode_generic_response)
681
+
682
+ return decode_parser
683
+
684
+
685
+ def handle_decode_generic_measurement(args):
686
+ data = bytes.fromhex(args.data)
687
+ vals = decode_repeated_sensor_measurements(data)
688
+ print(vals)
689
+
690
+
691
+ def handle_decode_generic_response(args):
692
+ data = bytes.fromhex(args.data)
693
+ vals = decode_sensor_response(data)
694
+ print(vals)
695
+
696
+
386
697
  def create_decode_parser(subparsers):
387
698
  """Create decode command parser
388
699
 
@@ -395,7 +706,11 @@ def create_decode_parser(subparsers):
395
706
 
396
707
  decode_parser = subparsers.add_parser("decode", help="Decode data")
397
708
 
398
- decode_subparsers = decode_parser.add_subparsers(title="Message type", dest="type")
709
+ decode_subparsers = decode_parser.add_subparsers(
710
+ title="Message type",
711
+ dest="type",
712
+ required=True,
713
+ )
399
714
 
400
715
  # measurement
401
716
  measurement_parser = decode_subparsers.add_parser(
@@ -24,10 +24,7 @@ import sys
24
24
  import serial
25
25
  import serial.tools.list_ports
26
26
  import re # For validating URL input
27
- from ents.proto import (
28
- encode_user_configuration,
29
- decode_user_configuration,
30
- )
27
+ from ..proto import encode_user_configuration, decode_user_configuration
31
28
 
32
29
 
33
30
  class Ui_MainWindow(object):
@@ -0,0 +1,8 @@
1
+ from .client import BackendClient
2
+
3
+ from .plots import plot_data
4
+
5
+ __all__ = [
6
+ "BackendClient",
7
+ "plot_data",
8
+ ]
ents/dirtviz/client.py ADDED
@@ -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