From d0f2545d7fb950239a92ce4eaabba9a2c0f89cb5 Mon Sep 17 00:00:00 2001 From: Victor Coman Date: Tue, 16 Nov 2021 12:45:26 +0100 Subject: [PATCH 1/5] samples: agriculture: use data from weather station and upload to DRM Signed-off-by: Victor Coman --- .../demos/EndToEndAgricultureSample/main.py | 280 +++++++++++++++--- 1 file changed, 247 insertions(+), 33 deletions(-) diff --git a/samples/xbee/demos/EndToEndAgricultureSample/main.py b/samples/xbee/demos/EndToEndAgricultureSample/main.py index 89fe0dd..51e1d3c 100644 --- a/samples/xbee/demos/EndToEndAgricultureSample/main.py +++ b/samples/xbee/demos/EndToEndAgricultureSample/main.py @@ -27,6 +27,7 @@ from datetime import datetime from json import JSONDecodeError from threading import Thread, Timer, Event, Lock +import serial from digi.xbee.devices import RemoteXBeeDevice from digi.xbee.exception import XBeeException @@ -77,9 +78,12 @@ STAT_VALVE = "valve" STAT_TEMPERATURE = "temperature" STAT_MOISTURE = "moisture" +STAT_PRESSURE = "pressure" +STAT_LUMINOSITY = "luminosity" STAT_BATTERY = "battery" STAT_RAIN = "rain" -STAT_WIND = "wind" +STAT_WIND = "wind_speed" +STAT_WIND_DIR = "wind_direction" STAT_RADIATION = "radiation" STAT_LEVEL = "level" @@ -105,8 +109,13 @@ RADIATION_DELTA = 10 # 10 W/m2 +DEFAULT_TEMP = 23 # 23 ºC +DEFAULT_MOIST = 50 # 50 % +DEFAULT_PRES = 25 # 27 hPa DEFAULT_RAIN = 0 # 0 mm DEFAULT_WIND = 5 # 5 km/h +DEFAULT_WIND_DIR = "N" # N (North) +DEFAULT_LUM = 50 # 50 % DEFAULT_RADIATION = 350 # 350 W/m2 DEFAULT_TANK_LEVEL = 50 # 50 % DEFAULT_VALVE_POSITION = False # Closed @@ -158,7 +167,6 @@ ] DATA_SEPARATOR = "@@" - PROP_FW_VERSION = "firmware.version" OLD_FW_YEAR = 21 @@ -167,12 +175,19 @@ # Variables. device = None +start_sending_data = False + current_time = 0 time_factor = 1 weather_condition = CONDITION_SUNNY +temp = DEFAULT_TEMP +moist = DEFAULT_MOIST +pres = DEFAULT_PRES rain = DEFAULT_RAIN wind = DEFAULT_WIND +wind_dir = DEFAULT_WIND_DIR +luminosity = DEFAULT_LUM radiation = DEFAULT_RADIATION tank_level = DEFAULT_TANK_LEVEL tank_valve_open = DEFAULT_VALVE_POSITION @@ -192,6 +207,18 @@ datapoint_lock = Lock() +# TODO: Replace with the serial port used to receive data. +PORT = "/dev/serial/port1" +# TODO: Replace with the preferred baud rate. +BAUD_RATE = 9600 + +# Optional serial settings +STOP_BITS = serial.STOPBITS_ONE +N_DATA_BITS = 8 +PARITY = serial.PARITY_NONE +RTS_CTS = 0 + + def bluetooth_data_callback(data): """ Callback executed every time the XBee module receives data from the @@ -825,7 +852,6 @@ def parse_configurations(payload): def upload_configurations_drm(configurations, sender=None): """ Uploads the given configurations to Digi Remote Manager. - Args: configurations (list of tuple): the configurations to upload to DRM. sender (String, optional): the 64-bit address of the solar panel that @@ -904,7 +930,18 @@ def get_next_random(value, max_value, min_value, max_delta): return value -def get_rain(): +def get_temp(): + """ + Returns the temperature. The value is obtained from the + meteo station. + + Returns: + The new temperature in ºC. + """ + return temp + + +def get_rain(simulation=False): """ Returns the rain during the day. The value is randomly calculate based on the current weather condition. @@ -912,23 +949,82 @@ def get_rain(): Returns: The rain in mm. """ - global rain + rain_var = 0 + + if simulation: + # Report rain only if the condition is 'rainy' (and not always). + if weather_condition == CONDITION_RAINY and random.random() > 0.7: + rain_var += round(random.random(), 2) + + else: + rain_var = rain - # Report rain only if the condition is 'rainy' (and not always). - if weather_condition == CONDITION_RAINY and random.random() > 0.7: - rain += round(random.random(), 2) - return rain + # Conversion from inch to mm + return rain_var * 25.4 -def get_wind(): +def get_wind(simulation=False): """ - Returns the wind speed. The value is randomly calculated based on the - current wind speed value. + Returns the wind speed. If simulation is set to True, the value is randomly + calculated based on the current wind speed value. + If it is set to False, then the value is obtained from the meteo station. Returns: The new wind speed in km/h. """ - return get_next_random(wind, WIND_MAX, WIND_MIN, WIND_DELTA) + + if simulation: + return get_next_random(wind, WIND_MAX, WIND_MIN, WIND_DELTA) + else: + # Conversion from miles/h to km/h + return round(wind * 1.609, 1) + + +def get_moist(): + """ + Returns the moisture/humidity of the air. The value is obtained from the + meteo station. + + Returns: + The new moisture in %. + """ + return moist + + +def get_pres(): + """ + Returns the pressure of the air. The value is obtained from the + meteo station. + + Returns: + The new pressure in hPa. + """ + return pres + + +def get_wind_dir(): + """ + Returns the wind direction. The value is obtained from the + meteo station. + + Returns: + The new wind direction in compass values. + """ + + return wind_dir + + +def get_luminosity(): + """ + Returns the luminosity. The value is obtained from the + meteo station. + + Returns: + The new luminosity in lux. + """ + + # Original value from 0 to 255 + return luminosity * 10000 / 255 def get_radiation(): @@ -1041,34 +1137,75 @@ def drm_report_task(): """ Timer task to send the sensors report to Digi Remote Manager. """ - global rain, wind, radiation configurations = list() - print_log("Sending sensor values to DRM:") + if start_sending_data: + + print_log("Sending sensor values to DRM:") + + # Obtain the values from the sensors. + temp_calc = get_temp() + print_log(" - Temp: {} C".format(temp_calc)) + + moist_calc = get_moist() + print_log(" - Moist: {} %".format(moist_calc)) + + pres_calc = get_pres() + print_log(" - Pres: {} hPa".format(pres_calc)) + + rain_calc = get_rain() + rain_f = "{:.2f}".format(rain_calc) + print_log(" - Rain: {} mm".format(rain_f)) + + wind_calc = get_wind() + print_log(" - Wind: {} km/h".format(wind_calc)) + + wind_dir_calc = get_wind_dir() + wind_dir_degrees = 0 + if wind_dir_calc == "N": + wind_dir_degrees = 0 + elif wind_dir_calc == "E": + wind_dir_degrees = 16 + elif wind_dir_calc == "S": + wind_dir_degrees = 32 + elif wind_dir_calc == "W": + wind_dir_degrees = 48 + elif wind_dir_calc == "NE": + wind_dir_degrees = 8 + elif wind_dir_calc == "NW": + wind_dir_degrees = 56 + elif wind_dir_calc == "SE": + wind_dir_degrees = 24 + else: + wind_dir_degrees = 40 - # Obtain the values from the sensors. - rain = get_rain() - rain_f = "{:.2f}".format(rain) - print_log(" - Rain: {} mm".format(rain_f)) + print_log(" - Wind dir: {} ({})".format(wind_dir, wind_dir_degrees)) - wind = get_wind() - print_log(" - Wind: {} km/h".format(wind)) + luminosity_calc = get_luminosity() + luminosity_f = "{:.2f}".format(luminosity_calc) + print_log(" - Luminosity: {} lux".format(luminosity_f)) - radiation = get_radiation() - print_log(" - Radiation: {} W/m2".format(radiation)) + radiation_calc = get_radiation() + print_log(" - Radiation: {} W/m2".format(radiation_calc)) - tank_level_f = "{:.3f}".format(tank_level) - print_log(" - Tank level: {} %".format(tank_level_f)) + tank_level_f = "{:.3f}".format(tank_level) + print_log(" - Tank level: {} %".format(tank_level_f)) - print_log(" - Tank valve: {}".format("open" if tank_valve_open else "closed")) + print_log(" - Tank valve: {}".format("open" if tank_valve_open else "closed")) + print_log("") - configurations.append((STAT_RAIN, rain_f)) - configurations.append((STAT_WIND, wind)) - configurations.append((STAT_RADIATION, radiation)) - configurations.append((STAT_LEVEL, tank_level_f)) - configurations.append((STAT_VALVE, int(tank_valve_open))) + configurations.append((STAT_TEMPERATURE, temp_calc)) + configurations.append((STAT_MOISTURE, moist_calc)) + configurations.append((STAT_PRESSURE, pres_calc)) + configurations.append((STAT_RAIN, rain_f)) + configurations.append((STAT_WIND, wind_calc)) + configurations.append((STAT_WIND_DIR, wind_dir_degrees)) + configurations.append((STAT_LUMINOSITY, luminosity_f)) + configurations.append((STAT_RADIATION, radiation_calc)) + configurations.append((STAT_LEVEL, tank_level_f)) + configurations.append((STAT_VALVE, int(tank_valve_open))) - upload_configurations_drm(configurations) + upload_configurations_drm(configurations) # Create and start a timer to repeat this task periodically. t = Timer(report_interval, drm_report_task) @@ -1135,7 +1272,6 @@ def is_old_firmware(): """ Returns whether the firmware running is an old version or not in terms of data point upload capabilities. - Returns: Boolean: ``True`` if the device firmware is old, ``False`` otherwise. """ @@ -1154,6 +1290,75 @@ def is_old_firmware(): return False +def microbit_data(data): + """ + Data obtained from the measurement made by the micro:bit application + using a meteorologic station. + Processes the incoming data in JSON format and assigns values to + the program variables. + + Args + data (Bytearray): the received data. + """ + + # Parse the JSON measurement items + try: + meas_items = json.loads(data.decode()) + except JSONDecodeError: + return + + global temp, moist, pres, rain, wind, wind_dir, luminosity + + temp = meas_items["temp"] + moist = meas_items["hum"] + pres = meas_items["pres"] + rain = meas_items["rain"] + wind = meas_items["current_windSpeed"] + wind_dir = meas_items["current_windDirection_List"] + luminosity = meas_items["lightVal"] + + # meas_data = [temp, moist, pres, rain, wind, wind_dir, luminosity] + # return meas_data + + +def read_microbit(): + """ + Reading of the meteo station data with the microbit application + + Returns: + Data collected. + """ + + global start_sending_data + + # Create and configure the serial port settings + ser = serial.Serial() + ser.baudrate = BAUD_RATE + ser.port = PORT + ser.stopbits = STOP_BITS + ser.bytesize = N_DATA_BITS + ser.parity = PARITY + ser.rtscts = RTS_CTS + data = "" + + # Open the serial port + ser.open() + + try: + while True: + if ser.in_waiting: + data = ser.read(ser.in_waiting) + microbit_data(data) + start_sending_data = True + time.sleep(1) + + except KeyboardInterrupt: + # Close the serial port + ser.close() + + return data + + def main(): """ Main execution of the application. @@ -1192,6 +1397,10 @@ def main(): # Load the irrigation schedule from the properties file. load_irrigation_schedule() + # Reads the values from the microbit application connected to the meteo station + # and sets this data to be sent to DRM + Thread(target=read_microbit, daemon=True).start() + # Start a task to move the time based on the factor. time_now = datetime.now().time() current_time = (time_now.hour * 3600) + (time_now.minute * 60) + time_now.second @@ -1200,7 +1409,11 @@ def main(): # Start a task to get the status from the irrigation stations. status_task() + # TODO: Remove for final version as main controller is set in the provisioning step. + set_main_controller(True) + # Start the task to report the sensor values to DRM (only if this is the main controller). + if is_main_controller(): drm_report_task() @@ -1209,3 +1422,4 @@ def main(): if __name__ == '__main__': main() + From 7aba98218fe03074bc6c5e0427bfa6a6173518ce Mon Sep 17 00:00:00 2001 From: Victor Coman Date: Wed, 17 Nov 2021 09:01:12 +0100 Subject: [PATCH 2/5] samples: agriculture: do not disable BLE after provisioning. Signed-off-by: Victor Coman --- samples/xbee/demos/EndToEndAgricultureSample/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/xbee/demos/EndToEndAgricultureSample/main.py b/samples/xbee/demos/EndToEndAgricultureSample/main.py index 51e1d3c..ba7a6c5 100644 --- a/samples/xbee/demos/EndToEndAgricultureSample/main.py +++ b/samples/xbee/demos/EndToEndAgricultureSample/main.py @@ -509,9 +509,10 @@ def finish_provisioning(): Finishes the provisioning phase by disabling the Bluetooth interface. """ # Disable the Bluetooth interface. - cfg = config.load(writable=True) - cfg.set("bluetooth.enable", False) - cfg.commit() + # cfg = config.load(writable=True) + # cfg.set("bluetooth.enable", False) + # cfg.commit() + return def set_auto_irrigation(irrigate, dest_addr=None): From 177789fb47625b31bc664b8f5cb936032212c75f Mon Sep 17 00:00:00 2001 From: Victor Coman Date: Fri, 19 Nov 2021 14:00:39 +0100 Subject: [PATCH 3/5] samples: agriculture: fix how rain gauge works and midnight reset Signed-off-by: Victor Coman --- .../demos/EndToEndAgricultureSample/main.py | 61 +++++++++++++++---- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/samples/xbee/demos/EndToEndAgricultureSample/main.py b/samples/xbee/demos/EndToEndAgricultureSample/main.py index ba7a6c5..615fd74 100644 --- a/samples/xbee/demos/EndToEndAgricultureSample/main.py +++ b/samples/xbee/demos/EndToEndAgricultureSample/main.py @@ -82,6 +82,7 @@ STAT_LUMINOSITY = "luminosity" STAT_BATTERY = "battery" STAT_RAIN = "rain" +STAT_RAIN_DIFF = "rain_diff" STAT_WIND = "wind_speed" STAT_WIND_DIR = "wind_direction" STAT_RADIATION = "radiation" @@ -110,17 +111,17 @@ RADIATION_DELTA = 10 # 10 W/m2 DEFAULT_TEMP = 23 # 23 ºC -DEFAULT_MOIST = 50 # 50 % +DEFAULT_MOIST = 50 # 50 % DEFAULT_PRES = 25 # 27 hPa DEFAULT_RAIN = 0 # 0 mm DEFAULT_WIND = 5 # 5 km/h DEFAULT_WIND_DIR = "N" # N (North) -DEFAULT_LUM = 50 # 50 % +DEFAULT_LUM = 50 # 50 % DEFAULT_RADIATION = 350 # 350 W/m2 DEFAULT_TANK_LEVEL = 50 # 50 % DEFAULT_VALVE_POSITION = False # Closed DEFAULT_IRR_DURATION = 600 # 10 minutes -DEFAULT_REPORT_INTERVAL = 60 # 1 minute +DEFAULT_REPORT_INTERVAL = 12 # 12 seconds TANK_DRAIN_RATE = 0.001 # 0.001 % / second @@ -175,6 +176,7 @@ # Variables. device = None +is_first_data_correct = False start_sending_data = False current_time = 0 @@ -185,6 +187,7 @@ moist = DEFAULT_MOIST pres = DEFAULT_PRES rain = DEFAULT_RAIN +rain_previous = 0 wind = DEFAULT_WIND wind_dir = DEFAULT_WIND_DIR luminosity = DEFAULT_LUM @@ -948,7 +951,7 @@ def get_rain(simulation=False): the current weather condition. Returns: - The rain in mm. + The rain in L/m2. """ rain_var = 0 @@ -1139,6 +1142,7 @@ def drm_report_task(): Timer task to send the sensors report to Digi Remote Manager. """ configurations = list() + global rain_acc, rain_previous if start_sending_data: @@ -1155,8 +1159,21 @@ def drm_report_task(): print_log(" - Pres: {} hPa".format(pres_calc)) rain_calc = get_rain() - rain_f = "{:.2f}".format(rain_calc) - print_log(" - Rain: {} mm".format(rain_f)) + rain_f = "{:.3f}".format(rain_calc) + print_log(" - Rain: {} L/m2".format(rain_f)) + + # This is the difference between the rain recently calculated and the previous sample + # It is used for knowing the current rain data, while maintaining also the accumulated data + rain_diff = rain_calc - rain_previous + rain_diff_f = "{:.3f}".format(rain_diff) + print_log(" - Rain difference: {} L/m2".format(rain_diff_f)) + + # Here we assign the new sample to be the "previous" for the next sample + rain_previous = rain_calc + + # rain_acc = rain_acc + rain_calc + # rain_acc_f = "{:.5f}".format(rain_acc) + # print_log(" - Rain accumulated: {} L".format(rain_acc_f)) wind_calc = get_wind() print_log(" - Wind: {} km/h".format(wind_calc)) @@ -1199,6 +1216,7 @@ def drm_report_task(): configurations.append((STAT_MOISTURE, moist_calc)) configurations.append((STAT_PRESSURE, pres_calc)) configurations.append((STAT_RAIN, rain_f)) + configurations.append((STAT_RAIN_DIFF, rain_diff_f)) configurations.append((STAT_WIND, wind_calc)) configurations.append((STAT_WIND_DIR, wind_dir_degrees)) configurations.append((STAT_LUMINOSITY, luminosity_f)) @@ -1240,14 +1258,31 @@ def time_task(): - Update the tank level based on the position of the valves. - Start the irrigation process. """ - global current_time, rain, tank_level + global current_time, rain, rain_previous, tank_level while True: start_time = time.time() # Reset the rain at midnight. if (current_time + time_factor) // SECONDS_PER_DAY > 0: + # Create and configure the serial port settings + ser = serial.Serial() + ser.baudrate = BAUD_RATE + ser.port = PORT + ser.stopbits = STOP_BITS + ser.bytesize = N_DATA_BITS + ser.parity = PARITY + ser.rtscts = RTS_CTS + data = "reset" + + # Open the serial port + ser.open() + # Send data thought the serial port + ser.write(data.encode()) + ser.close() + rain = 0 + rain_previous = 0 # Update the tank level based on the tank & station valves. if tank_valve_open and any_open_valve(): @@ -1308,6 +1343,10 @@ def microbit_data(data): except JSONDecodeError: return + global is_first_data_correct + is_first_data_correct = True + + print(data.decode()) global temp, moist, pres, rain, wind, wind_dir, luminosity temp = meas_items["temp"] @@ -1344,13 +1383,13 @@ def read_microbit(): # Open the serial port ser.open() - try: while True: if ser.in_waiting: data = ser.read(ser.in_waiting) microbit_data(data) - start_sending_data = True + if is_first_data_correct: + start_sending_data = True time.sleep(1) except KeyboardInterrupt: @@ -1382,10 +1421,8 @@ def main(): # Register a callback to handle incoming data from the Bluetooth interface. device.add_bluetooth_data_received_callback(bluetooth_data_callback) - # Wait until the controller is connect to Digi Remote Manager. wait_drm_connection() - # Register callbacks to handle incoming data from Digi Remote Manager. register_drm_targets() @@ -1397,7 +1434,6 @@ def main(): # Load the irrigation schedule from the properties file. load_irrigation_schedule() - # Reads the values from the microbit application connected to the meteo station # and sets this data to be sent to DRM Thread(target=read_microbit, daemon=True).start() @@ -1423,4 +1459,3 @@ def main(): if __name__ == '__main__': main() - From 2e0a664983e23dd19503661475329c99a296c1cb Mon Sep 17 00:00:00 2001 From: Victor Coman Date: Mon, 22 Nov 2021 12:31:12 +0100 Subject: [PATCH 4/5] samples: agriculture: fix datapoint sending and add resets for values each time the application is restarted Signed-off-by: Victor Coman --- .../demos/EndToEndAgricultureSample/main.py | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/samples/xbee/demos/EndToEndAgricultureSample/main.py b/samples/xbee/demos/EndToEndAgricultureSample/main.py index 615fd74..c3080bd 100644 --- a/samples/xbee/demos/EndToEndAgricultureSample/main.py +++ b/samples/xbee/demos/EndToEndAgricultureSample/main.py @@ -81,7 +81,7 @@ STAT_PRESSURE = "pressure" STAT_LUMINOSITY = "luminosity" STAT_BATTERY = "battery" -STAT_RAIN = "rain" +STAT_RAIN = "rain_acc" STAT_RAIN_DIFF = "rain_diff" STAT_WIND = "wind_speed" STAT_WIND_DIR = "wind_direction" @@ -888,12 +888,12 @@ def upload_configurations_drm(configurations, sender=None): # Create and store the data point. data_points.append(DataPoint(data_stream, conf_value, data_type=DataType.DOUBLE)) - # Upload the data points all at once. - try: - with datapoint_lock: - datapoint.upload_multiple(data_points) - except Exception as e: - print_error("Could not upload datapoints: {}".format(str(e))) + # Upload the data points all at once. + try: + with datapoint_lock: + datapoint.upload_multiple(data_points) + except Exception as e: + print_error("Could not upload datapoints: {}".format(str(e))) def get_next_random(value, max_value, min_value, max_delta): @@ -1142,7 +1142,7 @@ def drm_report_task(): Timer task to send the sensors report to Digi Remote Manager. """ configurations = list() - global rain_acc, rain_previous + global rain_previous if start_sending_data: @@ -1171,10 +1171,6 @@ def drm_report_task(): # Here we assign the new sample to be the "previous" for the next sample rain_previous = rain_calc - # rain_acc = rain_acc + rain_calc - # rain_acc_f = "{:.5f}".format(rain_acc) - # print_log(" - Rain accumulated: {} L".format(rain_acc_f)) - wind_calc = get_wind() print_log(" - Wind: {} km/h".format(wind_calc)) @@ -1357,9 +1353,6 @@ def microbit_data(data): wind_dir = meas_items["current_windDirection_List"] luminosity = meas_items["lightVal"] - # meas_data = [temp, moist, pres, rain, wind, wind_dir, luminosity] - # return meas_data - def read_microbit(): """ @@ -1410,6 +1403,22 @@ def main(): global device, current_time + # Create and configure the serial port settings + ser = serial.Serial() + ser.baudrate = BAUD_RATE + ser.port = PORT + ser.stopbits = STOP_BITS + ser.bytesize = N_DATA_BITS + ser.parity = PARITY + ser.rtscts = RTS_CTS + data = "reset" + + # Open the serial port + ser.open() + # Send data thought the serial port + ser.write(data.encode()) + ser.close() + # Register callbacks for signals processing. signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) From bd5d91b8a769853774f1a461475390c896152f2d Mon Sep 17 00:00:00 2001 From: Victor Coman Date: Thu, 25 Nov 2021 09:52:13 +0100 Subject: [PATCH 5/5] samples:agriculture: fix problem with old fw version -before it gave an exception, now it returns true Signed-off-by: Victor Coman --- samples/xbee/demos/EndToEndAgricultureSample/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/samples/xbee/demos/EndToEndAgricultureSample/main.py b/samples/xbee/demos/EndToEndAgricultureSample/main.py index c3080bd..b7fc0eb 100644 --- a/samples/xbee/demos/EndToEndAgricultureSample/main.py +++ b/samples/xbee/demos/EndToEndAgricultureSample/main.py @@ -1307,8 +1307,10 @@ def is_old_firmware(): Returns: Boolean: ``True`` if the device firmware is old, ``False`` otherwise. """ - # Read firmware version from runt. + # Read firmware version from runt. If it could not be read assume old fw. fw_version = get_runt(PROP_FW_VERSION) + if fw_version is None: + return True # Compare firmware year and month with old versions. year = int(fw_version.split(".")[0]) @@ -1468,3 +1470,4 @@ def main(): if __name__ == '__main__': main() +