ents 2.3.2__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/cli.py ADDED
@@ -0,0 +1,546 @@
1
+ import argparse
2
+
3
+ import os
4
+ import pandas as pd
5
+ import numpy as np
6
+ from datetime import datetime, timedelta
7
+ import time
8
+
9
+ from .calibrate.recorder import Recorder
10
+ from .calibrate.linear_regression import (
11
+ linear_regression,
12
+ print_eval,
13
+ print_coef,
14
+ print_norm,
15
+ )
16
+ from .calibrate.plots import (
17
+ plot_measurements,
18
+ plot_calib,
19
+ plot_residuals,
20
+ plot_residuals_hist,
21
+ )
22
+
23
+ from .proto.encode import (
24
+ encode_power_measurement,
25
+ encode_phytos31_measurement,
26
+ encode_teros12_measurement,
27
+ encode_response,
28
+ )
29
+ from .proto.decode import decode_measurement, decode_response
30
+ from .proto.esp32 import encode_esp32command, decode_esp32command
31
+
32
+ from .simulator.node import NodeSimulator
33
+
34
+
35
+ def entry():
36
+ """Command line interface entry point"""
37
+
38
+ parser = argparse.ArgumentParser()
39
+
40
+ subparsers = parser.add_subparsers(help="Ents Utilities")
41
+
42
+ create_encode_parser(subparsers)
43
+ create_decode_parser(subparsers)
44
+ create_calib_parser(subparsers)
45
+ create_sim_parser(subparsers)
46
+
47
+ args = parser.parse_args()
48
+ args.func(args)
49
+
50
+
51
+ def create_sim_parser(subparsers):
52
+ """Creates the simulation subparser
53
+
54
+ Args:
55
+ subparsers: Reference to subparser group
56
+ Returns:
57
+ Reference to new subparser
58
+ """
59
+
60
+ sim_p = subparsers.add_parser("sim", help="Simluate sensor uploads")
61
+ sim_p.add_argument(
62
+ "--url",
63
+ required=True,
64
+ type=str,
65
+ help="URL of the dirtviz instance (default: http://localhost:8000)",
66
+ )
67
+ sim_p.add_argument(
68
+ "--mode",
69
+ required=True,
70
+ choices=["batch", "stream"],
71
+ type=str,
72
+ help="Upload mode",
73
+ )
74
+ sim_p.add_argument(
75
+ "--sensor",
76
+ required=True,
77
+ choices=["power", "teros12", "teros21", "bme280"],
78
+ type=str,
79
+ nargs="+",
80
+ )
81
+ sim_p.add_argument("--cell", required=True, type=int, help="Cell Id")
82
+ sim_p.add_argument("--logger", required=True, type=int, help="Logger Id")
83
+ sim_p.add_argument("--start", type=str, help="Start date")
84
+ sim_p.add_argument("--end", type=str, help="End date")
85
+ sim_p.add_argument(
86
+ "--freq", default=10.0, type=float, help="Frequency of uploads (default: 10s)"
87
+ )
88
+ sim_p.set_defaults(func=simulate)
89
+
90
+ return sim_p
91
+
92
+
93
+ def simulate(args):
94
+ simulation = NodeSimulator(
95
+ cell=args.cell,
96
+ logger=args.logger,
97
+ sensors=args.sensor,
98
+ )
99
+
100
+ if args.mode == "batch":
101
+ if (args.start is None) or (args.end is None):
102
+ raise ValueError("Start and end date must be provided for batch mode.")
103
+
104
+ # format dates
105
+ curr_dt = datetime.fromisoformat(args.start)
106
+ end_dt = datetime.fromisoformat(args.end)
107
+
108
+ # create list of measurements
109
+ while curr_dt <= end_dt:
110
+ ts = int(curr_dt.timestamp())
111
+ simulation.measure(ts)
112
+ curr_dt += timedelta(seconds=args.freq)
113
+
114
+ # send measurements
115
+ while simulation.send_next(args.url):
116
+ print(simulation)
117
+
118
+ print("Done!")
119
+
120
+ elif args.mode == "stream":
121
+ print("Use CTRL+C to stop the simulation")
122
+ try:
123
+ while True:
124
+ dt = datetime.now()
125
+ ts = int(dt.timestamp())
126
+ simulation.measure(ts)
127
+ while simulation.send_next(args.url):
128
+ print(simulation)
129
+ time.sleep(args.freq)
130
+ except KeyboardInterrupt as _:
131
+ print("Stopping simulation")
132
+
133
+
134
+ def create_calib_parser(subparsers):
135
+ """Creates the calibration subparser
136
+
137
+ Args:
138
+ subparsers: Reference to subparser group
139
+ Returns:
140
+ Reference to new subparser
141
+ """
142
+
143
+ # calibration parser
144
+ calib_p = subparsers.add_parser("calib", help="Calibrate power measurements")
145
+ calib_p.add_argument(
146
+ "--samples",
147
+ type=int,
148
+ default=10,
149
+ required=False,
150
+ help="Samples taken at each step (default: 10)",
151
+ )
152
+ calib_p.add_argument(
153
+ "--plot", action="store_true", help="Show calibration parameter plots"
154
+ )
155
+ calib_p.add_argument(
156
+ "--mode",
157
+ type=str,
158
+ default="both",
159
+ required=False,
160
+ help="Either both, voltage, or current (default: both)",
161
+ )
162
+ calib_p.add_argument(
163
+ "--output", type=str, required=False, help="Output directory for measurements"
164
+ )
165
+ calib_p.add_argument("port", type=str, help="Board serial port")
166
+ calib_p.add_argument("host", type=str, help="Address and port of smu (ip:port)")
167
+ calib_p.set_defaults(func=calibrate)
168
+
169
+ return calib_p
170
+
171
+
172
+ def create_encode_parser(subparsers):
173
+ """Create encode command subparser
174
+
175
+ Args:
176
+ subparsers: Reference to subparser group
177
+
178
+ Returns:
179
+ Reference to new subparser
180
+ """
181
+
182
+ encode_parser = subparsers.add_parser("encode", help="Encode data")
183
+
184
+ print_format = encode_parser.add_mutually_exclusive_group()
185
+ print_format.add_argument(
186
+ "--hex", action="store_true", help="Print as hex values (default)"
187
+ )
188
+ print_format.add_argument(
189
+ "--raw", action="store_true", help="Print raw bytes object"
190
+ )
191
+ print_format.add_argument(
192
+ "--c", action="store_true", help="Print bytes for copying to c"
193
+ )
194
+
195
+ encode_subparsers = encode_parser.add_subparsers(title="Message type", dest="type")
196
+
197
+ def create_measurement_parser(encode_subparsers):
198
+ measurement_parser = encode_subparsers.add_parser(
199
+ "measurement", help='Proto "Measurement" message'
200
+ )
201
+ measurement_subparser = measurement_parser.add_subparsers(
202
+ title="Measurement type"
203
+ )
204
+
205
+ # metadata
206
+ measurement_parser.add_argument("ts", type=int, help="Unix epoch timestamp")
207
+ measurement_parser.add_argument("cell", type=int, help="Cell Id")
208
+ measurement_parser.add_argument("logger", type=int, help="Logger Id")
209
+
210
+ power_parser = measurement_subparser.add_parser(
211
+ "power", help="PowerMeasurement"
212
+ )
213
+ power_parser.add_argument("voltage", type=float, help="Voltage in (V)")
214
+ power_parser.add_argument("current", type=float, help="Current in (A)")
215
+ power_parser.set_defaults(func=handle_encode_measurement_power)
216
+
217
+ teros12_parser = measurement_subparser.add_parser(
218
+ "teros12", help="Teros12Measurement"
219
+ )
220
+ teros12_parser.add_argument("vwc_raw", type=float, help="Raw vwc")
221
+ teros12_parser.add_argument("vwc_adj", type=float, help="Calibrated vwc")
222
+ teros12_parser.add_argument("temp", type=float, help="Temperature in C")
223
+ teros12_parser.add_argument("ec", type=int, help="Electrical conductivity")
224
+ teros12_parser.set_defaults(func=handle_encode_measurement_teros12)
225
+
226
+ phytos31_parser = measurement_subparser.add_parser(
227
+ "phytos31", help="Phytos31Measurement"
228
+ )
229
+ phytos31_parser.add_argument("voltage", type=float, help="Raw voltage (V)")
230
+ phytos31_parser.add_argument("leaf_wetness", type=float, help="Leaf wetness")
231
+ phytos31_parser.set_defaults(func=handle_encode_measurement_phytos31)
232
+
233
+ return measurement_parser
234
+
235
+ def create_response_parser(encode_subparsers):
236
+ response_parser = encode_subparsers.add_parser(
237
+ "response", help='Proto "Response" message'
238
+ )
239
+ response_parser.add_argument("status", type=str, help="Status")
240
+ response_parser.set_defaults(func=handle_encode_response)
241
+
242
+ return response_parser
243
+
244
+ def create_esp32command_parser(encode_subparsers):
245
+ esp32command_parser = encode_subparsers.add_parser(
246
+ "esp32command", help='Proto "Esp32Command" message'
247
+ )
248
+ esp32command_subparser = esp32command_parser.add_subparsers(
249
+ title="type", help="PageCommand"
250
+ )
251
+
252
+ test_parser = esp32command_subparser.add_parser("test", help="TestCommand")
253
+ test_parser.add_argument("state", type=str, help="State to put module into")
254
+ test_parser.add_argument("data", type=int, help="Data associated with command")
255
+ test_parser.set_defaults(func=handle_encode_esp32command_test)
256
+
257
+ esp32_parser = esp32command_subparser.add_parser("page", help="PageCommand")
258
+ esp32_parser.add_argument("type", type=str, help="Request type")
259
+ esp32_parser.add_argument("fd", type=int, help="File descriptor")
260
+ esp32_parser.add_argument("bs", type=int, help="Block size")
261
+ esp32_parser.add_argument("num", type=int, help="Number of bytes")
262
+ esp32_parser.set_defaults(func=handle_encode_esp32command_page)
263
+
264
+ wifi_parser = esp32command_subparser.add_parser("wifi", help="WiFiCommand")
265
+ wifi_parser.add_argument("type", type=str, help="WiFi command type")
266
+ wifi_parser.add_argument("--ssid", type=str, default="", help="WiFi SSID")
267
+ wifi_parser.add_argument("--passwd", type=str, default="", help="WiFi password")
268
+ wifi_parser.add_argument("--url", type=str, default="", help="Endpoint url")
269
+ wifi_parser.add_argument("--port", type=int, default=0, help="Endpoint port")
270
+ wifi_parser.add_argument("--rc", type=int, default=0, help="Return code")
271
+ wifi_parser.add_argument("--ts", type=int, help="Timestamp in unix epochs")
272
+ wifi_parser.add_argument(
273
+ "--resp", type=str, default=b"", help="Serialized response message"
274
+ )
275
+ wifi_parser.set_defaults(func=handle_encode_esp32command_wifi)
276
+
277
+ return esp32command_parser
278
+
279
+ # create subparsers
280
+ create_measurement_parser(encode_subparsers)
281
+ create_response_parser(encode_subparsers)
282
+ create_esp32command_parser(encode_subparsers)
283
+
284
+ return encode_parser
285
+
286
+
287
+ def handle_encode_measurement_power(args):
288
+ data = encode_power_measurement(
289
+ ts=args.ts,
290
+ cell_id=args.cell,
291
+ logger_id=args.logger,
292
+ voltage=args.voltage,
293
+ current=args.current,
294
+ )
295
+
296
+ print_data(args, data)
297
+
298
+
299
+ def handle_encode_measurement_teros12(args):
300
+ data = encode_teros12_measurement(
301
+ ts=args.ts,
302
+ cell_id=args.cell,
303
+ logger_id=args.logger,
304
+ vwc_raw=args.vwc_raw,
305
+ vwc_adj=args.vwc_adj,
306
+ temp=args.temp,
307
+ ec=args.ec,
308
+ )
309
+
310
+ print_data(args, data)
311
+
312
+
313
+ def handle_encode_measurement_phytos31(args):
314
+ data = encode_phytos31_measurement(
315
+ ts=args.ts,
316
+ cell_id=args.cell,
317
+ logger_id=args.logger,
318
+ voltage=args.voltage,
319
+ leaf_wetness=args.leaf_wetness,
320
+ )
321
+
322
+ print_data(args, data)
323
+
324
+
325
+ def handle_encode_response(args):
326
+ valid_status = ["SUCCESS", "ERROR"]
327
+ if args.status not in valid_status:
328
+ raise NotImplementedError(f'Response status "{args.status}" not implemented')
329
+
330
+ data = encode_response(args.status)
331
+ print_data(args, data)
332
+
333
+
334
+ def handle_encode_esp32command_test(args):
335
+ data = encode_esp32command("test", state=args.state, data=args.data)
336
+ print_data(args, data)
337
+
338
+
339
+ def handle_encode_esp32command_page(args):
340
+ data = encode_esp32command(
341
+ "page", req=args.type.lower(), fd=args.fd, bs=args.bs, n=args.num
342
+ )
343
+ print_data(args, data)
344
+
345
+
346
+ def handle_encode_esp32command_wifi(args):
347
+ data = encode_esp32command(
348
+ "wifi",
349
+ _type=args.type.lower(),
350
+ ssid=args.ssid,
351
+ passwd=args.passwd,
352
+ url=args.url,
353
+ port=args.port,
354
+ rc=args.rc,
355
+ ts=args.ts,
356
+ resp=args.resp,
357
+ )
358
+ print_data(args, data)
359
+
360
+
361
+ def print_data(args, data: bytes) -> str:
362
+ if args.c:
363
+ print_bytes_c(data)
364
+ elif args.raw:
365
+ print(data)
366
+ else:
367
+ print(data.hex())
368
+
369
+
370
+ def print_bytes_c(data: bytes) -> str:
371
+ """Formats serialized data into c bytes array"""
372
+
373
+ # format data string
374
+ data_str = "uint8_t data[] = {"
375
+ hex_str = [hex(d) for d in data]
376
+ data_str += ", ".join(hex_str)
377
+ data_str += "};"
378
+
379
+ # print data string
380
+ print(data_str)
381
+
382
+ # print length of data string
383
+ print(f"size_t data_len = {len(hex_str)};")
384
+
385
+
386
+ def create_decode_parser(subparsers):
387
+ """Create decode command parser
388
+
389
+ Args:
390
+ subparsers: Reference to subparser group
391
+
392
+ Returns:
393
+ Reference to new subparser
394
+ """
395
+
396
+ decode_parser = subparsers.add_parser("decode", help="Decode data")
397
+
398
+ decode_subparsers = decode_parser.add_subparsers(title="Message type", dest="type")
399
+
400
+ # measurement
401
+ measurement_parser = decode_subparsers.add_parser(
402
+ "measurement", help='Proto "Measurement" message'
403
+ )
404
+ measurement_parser.set_defaults(func=handle_decode_measurement)
405
+
406
+ # response
407
+ response_parser = decode_subparsers.add_parser(
408
+ "response", help='Proto "Response" message'
409
+ )
410
+ response_parser.set_defaults(func=handle_decode_response)
411
+
412
+ # esp32command
413
+ esp32command_parser = decode_subparsers.add_parser(
414
+ "esp32command", help='Proto "Esp32Command" message'
415
+ )
416
+ esp32command_parser.set_defaults(func=handle_decode_esp32command)
417
+
418
+ decode_parser.add_argument(
419
+ "data", type=str, help="Protobuf serialized data in hex format"
420
+ )
421
+
422
+ return decode_parser
423
+
424
+
425
+ def handle_decode_measurement(args):
426
+ data = bytes.fromhex(args.data)
427
+ vals = decode_measurement(data)
428
+ print(vals)
429
+
430
+
431
+ def handle_decode_response(args):
432
+ data = bytes.fromhex(args.data)
433
+ vals = decode_response(data)
434
+ print(vals)
435
+
436
+
437
+ def handle_decode_esp32command(args):
438
+ data = bytes.fromhex(args.data)
439
+ vals = decode_esp32command(data)
440
+ print(vals)
441
+
442
+
443
+ def calibrate(args):
444
+ print(
445
+ "If you don't see any output for 5 seconds, restart the calibration after resetting the ents board"
446
+ )
447
+
448
+ host, port = args.host.split(":")
449
+ rec = Recorder(args.port, host, int(port))
450
+
451
+ if args.mode == "both":
452
+ run_v = True
453
+ run_i = True
454
+ elif args.mode in ["v", "volts", "voltage"]:
455
+ run_v = True
456
+ run_i = False
457
+ elif args.mode in ["i", "amps", "current"]:
458
+ run_v = True
459
+ run_i = True
460
+ else:
461
+ raise NotImplementedError(f"Calbration mode: {args.mode} not implemented")
462
+
463
+ V_START = -2.0
464
+ V_STOP = 2.0
465
+ V_STEP = 0.5
466
+
467
+ I_START = -0.009
468
+ I_STOP = 0.009
469
+ I_STEP = 0.0045
470
+
471
+ def record_calibrate(start, stop, step, name: str):
472
+ """Record and calibrate
473
+
474
+ Args:
475
+ start: Start value
476
+ stop: Stop value (inclusive)
477
+ step: Step between values
478
+ name: Name of channel
479
+ """
480
+
481
+ # TODO Unjank reference to member variables by moving the selection to
482
+ # the class.
483
+ if name == "voltage":
484
+ iterator = Recorder.record_voltage
485
+ elif name == "current":
486
+ iterator = Recorder.record_current
487
+
488
+ # collect data
489
+ print("Collecting calibration data")
490
+ cal = iterator(rec, start, stop, step, args.samples)
491
+ if args.output:
492
+ save_csv(cal, args.output, f"{name}-cal.csv")
493
+
494
+ print("Collecting evaluation data")
495
+ _eval = iterator(rec, start, stop, step, args.samples)
496
+ if args.output:
497
+ save_csv(_eval, args.output, f"{name}-eval.csv")
498
+
499
+ model = linear_regression(
500
+ np.array(cal["meas"]).reshape(-1, 1), np.array(cal["actual"]).reshape(-1, 1)
501
+ )
502
+ pred = model.predict(np.array(_eval["meas"]).reshape(-1, 1))
503
+ residuals = np.array(_eval["actual"]) - pred.flatten()
504
+
505
+ print("")
506
+ print("\r\rnCoefficients")
507
+ print_coef(model)
508
+ print("\r\nEvaluation")
509
+ print_eval(pred, _eval["actual"])
510
+ print("\r\nNormal fit")
511
+ print_norm(residuals)
512
+ print("")
513
+
514
+ # plots
515
+ if args.plot:
516
+ plot_measurements(cal["actual"], cal["meas"], title=name)
517
+ plot_calib(_eval["meas"], pred, title=name)
518
+ plot_residuals(pred, residuals, title=name)
519
+ plot_residuals_hist(residuals, title=name)
520
+
521
+ if run_v:
522
+ print("Connect smu to voltage inputs device and press ENTER")
523
+ input()
524
+ record_calibrate(V_START, V_STOP, V_STEP, "voltage")
525
+
526
+ if run_i:
527
+ print(
528
+ "Connect smu to a resistor in series with the current channels and press ENTER"
529
+ )
530
+ input()
531
+ record_calibrate(I_START, I_STOP, I_STEP, "current")
532
+
533
+ print("Press enter to close plots")
534
+ input()
535
+
536
+
537
+ def save_csv(data: dict[str, list], path: str, name: str):
538
+ """Save measurement dictionary to csv
539
+
540
+ Args:
541
+ data: Measurement data
542
+ path: Folder path
543
+ name: Name of csv file
544
+ """
545
+ path = os.path.join(path, name)
546
+ pd.DataFrame(data).to_csv(path, index=False)
ents/config/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # GUI Application
2
+
3
+ ## Overview
4
+
5
+ This PyQt5-based graphical user interface (GUI) allows users to configure settings for a device. It supports input fields for Logger ID, Cell ID, upload method (WiFi or LoRa), upload interval, enabled sensors, and calibration parameters for voltage and current (V/I Slope and Offset). Settings can be saved to a JSON file, loaded from a saved file, or directly transmitted to the device via UART.
6
+
7
+ The application provides options to:
8
+
9
+ - **Save configurations to a JSON file**
10
+ - **Load previously saved configurations**
11
+ - **Directly transmit settings to the device via UART**, using Protobuf for serialization.
12
+ - **Load current configuration** directly from the device.
13
+
14
+ ## Key Features
15
+
16
+ - **Save and Load Configurations**: Save configurations to a JSON file named with the cell ID, or load an existing configuration.
17
+ - **Real-time Configuration Transmission**: Settings are serialized using Protobuf when the "Send Configuration" button is clicked, then transmitted over UART to the STM32 device.
18
+ - **Load Current Configuration**: Users can load the current configuration from the device by clicking the "Load Current Configuration" button, which reads the configuration stored in FRAM and displays it in the fields.
19
+ - **Input Validation**: Ensures only valid data is entered, prompting error messages for any invalid fields to guide users.
20
+
21
+ ## Installation
22
+
23
+ To install the required dependencies, use the following command:
24
+
25
+ ```bash
26
+ pip install -r requirements.txt
27
+ ```
28
+
29
+ Run the application by executing the following command:
30
+
31
+ ```bash
32
+ python user_config.py
33
+ ```
34
+
35
+ ## Opening the GUI from the Executable (No Python Required)
36
+
37
+ If you don't have Python installed or prefer to use a pre-built executable, follow these steps to open the GUI from the desktop executable:
38
+
39
+ **1- Open the Executable file path:**
40
+
41
+ - The executable file can be found in the following path:
42
+
43
+ ```bash
44
+ \proto\python\src\ents\config\dist
45
+ ```
46
+
47
+ **2- Run the Executable:**
48
+
49
+ - Navigate to the directory where **userConfig.exe** is located.
50
+
51
+ **3- Start Using the GUI:**
52
+
53
+ - Once the executable is opened, the GUI will appear. You can now configure your device settings as usual without needing Python or any development tools.
54
+
55
+ - The GUI will work on Windows, MacOS, and Linux platforms.
56
+
57
+ ## Example JSON Configuration File
58
+
59
+ The following is an example JSON file generated after saving or sending user configuration settings:
60
+
61
+ ```bash
62
+ {
63
+ "Logger ID": 5,
64
+ "Cell ID": 5,
65
+ "Upload Method": "LoRa",
66
+ "Days": 5,
67
+ "Hours": 5,
68
+ "Minutes": 5,
69
+ "Seconds": 5,
70
+ "Enabled Sensors": {
71
+ "Voltage": true,
72
+ "Current": true,
73
+ "Teros12": true,
74
+ "Teros21": true,
75
+ "BME280": true
76
+ },
77
+ "Calibration V Slope": 5.0,
78
+ "Calibration V Offset": 5.0,
79
+ "Calibration I Slope": 5.0,
80
+ "Calibration I Offset": 5.0,
81
+ "WiFi SSID": "",
82
+ "WiFi Password": "",
83
+ "API Endpoint URL": "",
84
+ "API Port": 0
85
+ }
86
+ ```
87
+
88
+ ## Screenshots
89
+
90
+ ### Application Screenshots
91
+
92
+ The following images show the GUI interface in different configurations:
93
+
94
+ |**WiFi Upload Method**|**LoRa Upload Method**|
95
+ |---------------------|-----------|
96
+ |![User Configuration diagram](../../../../../images/user_config_GUI_WiFi.png)|![User Configuration diagram](../../../../../images/user_config_GUI_LoRa.png)|
97
+
98
+ ### GUI Flow Diagram
99
+
100
+ The flow diagram below illustrates the general process and structure of the application:
101
+
102
+ ![User Configuration diagram](../../../../../images/GUI_flow_diagram.png)
103
+
104
+ ## User Manual
105
+
106
+ ### How to Use the GUI
107
+
108
+ **1- Save Configuration:**
109
+
110
+ - Fill in the necessary fields such as Logger ID, Cell ID, upload method, etc.
111
+ Click on the **"Save"** button to save the configuration to a JSON file. The file will be named with the Cell ID as the filename.
112
+
113
+ **2- Load Configuration:**
114
+
115
+ - To load a previously saved configuration, click on the **"Load"** button. Browse to the desired JSON file and load it into the application.
116
+
117
+ **3- Send Configuration to Device:**
118
+
119
+ - After entering the configuration settings, click on the **"Send Configuration"** button to transmit the settings to the device via UART. The data will be serialized using Protobuf and sent to the device.
120
+
121
+ **4- Load Current Configuration from Device:**
122
+
123
+ - To read the current configuration from the device, click on the **"Load Current Configuration"** button. This retrieves the stored configuration from FRAM and populates the fields with the current values.
@@ -0,0 +1 @@
1
+ # This file makes this directory a Python package.