├── Log └── .force ├── lib ├── __init__.py ├── courier.ttf ├── models │ ├── .DS_Store │ ├── Boards.bam │ ├── Camera.bam │ ├── Glass.bam │ ├── Lights.bam │ ├── Piping.bam │ ├── Pump.bam │ ├── Raspi.bam │ ├── Rings.bam │ ├── Table.bam │ ├── marker.bam │ ├── Arduino.bam │ ├── Fans_off.bam │ ├── Fans_on.bam │ ├── Growmat.bam │ ├── Sensors1.bam │ ├── Sensors2.bam │ ├── ColorSquare.bam │ ├── ColorsBase.bam │ ├── Reservoir.bam │ ├── Tankwater.bam │ ├── plants │ │ ├── Leaf.bam │ │ ├── BabyLeaf.bam │ │ ├── Planttest.bam │ │ ├── ThickStem.bam │ │ ├── ThinStem.bam │ │ ├── LettuceLeaf.bam │ │ └── BabyLettuceLeaf.bam │ ├── MeasureStick.bam │ ├── ReservoirLid.bam │ ├── Fans_on_blades.bam │ └── ReservoirWater.bam ├── sounds │ ├── .DS_Store │ ├── fanon.wav │ └── pumpon.wav ├── ArduinoCode │ ├── Makefile │ └── ArduinoCode.ino ├── freqmsg.py ├── topic_def.py ├── terrabot_utils.py ├── sim_camera.py ├── baseline.py ├── baselineold.py ├── send_email.py ├── interference.py ├── farduino.py ├── environment.py ├── tester.py ├── plant.py └── render.py ├── .gitignore ├── param ├── dry.bsl ├── moisture.inf ├── cold_and_dry.bsl ├── smoist_up.bsl ├── default_baseline.bsl ├── high_temp_baseline.bsl ├── high_humid_baseline.bsl ├── start_before_midnight.bsl ├── low_temp_baseline.bsl ├── high_everything_baseline.bsl └── interference.inf ├── simulator.JPG ├── system_diagram.jpg ├── stream ├── stream-audio ├── stream-video └── stream-av ├── cleanup_processes ├── docker ├── run_terrabot.sh ├── Makefile ├── run_terrabot.bat ├── Dockerfile ├── DockerSETUP.md └── DockerSETUP.html ├── agents ├── limits.py ├── streaming_behavior.py ├── log_data.py ├── plot_sensors.py ├── interactive_agent.py └── time_series.py ├── M1SETUP.md ├── VIRTUALBOX.md ├── RPISETUP.md ├── TerraBot.py └── README.md /Log/.force: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | __pycache__ 3 | *log 4 | build-mega2560 5 | tracker.txt 6 | -------------------------------------------------------------------------------- /param/dry.bsl: -------------------------------------------------------------------------------- 1 | start = 1-08:00:00 2 | 3 | #smoist = 200 4 | smoist = 0 5 | -------------------------------------------------------------------------------- /simulator.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/simulator.JPG -------------------------------------------------------------------------------- /lib/courier.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/courier.ttf -------------------------------------------------------------------------------- /param/moisture.inf: -------------------------------------------------------------------------------- 1 | AT 1-08:00:00 2 | smoist = [noise, noise] 3 | wlevel = noise 4 | -------------------------------------------------------------------------------- /system_diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/system_diagram.jpg -------------------------------------------------------------------------------- /lib/models/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/.DS_Store -------------------------------------------------------------------------------- /lib/models/Boards.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Boards.bam -------------------------------------------------------------------------------- /lib/models/Camera.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Camera.bam -------------------------------------------------------------------------------- /lib/models/Glass.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Glass.bam -------------------------------------------------------------------------------- /lib/models/Lights.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Lights.bam -------------------------------------------------------------------------------- /lib/models/Piping.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Piping.bam -------------------------------------------------------------------------------- /lib/models/Pump.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Pump.bam -------------------------------------------------------------------------------- /lib/models/Raspi.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Raspi.bam -------------------------------------------------------------------------------- /lib/models/Rings.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Rings.bam -------------------------------------------------------------------------------- /lib/models/Table.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Table.bam -------------------------------------------------------------------------------- /lib/models/marker.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/marker.bam -------------------------------------------------------------------------------- /lib/sounds/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/sounds/.DS_Store -------------------------------------------------------------------------------- /lib/sounds/fanon.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/sounds/fanon.wav -------------------------------------------------------------------------------- /lib/sounds/pumpon.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/sounds/pumpon.wav -------------------------------------------------------------------------------- /lib/models/Arduino.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Arduino.bam -------------------------------------------------------------------------------- /lib/models/Fans_off.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Fans_off.bam -------------------------------------------------------------------------------- /lib/models/Fans_on.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Fans_on.bam -------------------------------------------------------------------------------- /lib/models/Growmat.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Growmat.bam -------------------------------------------------------------------------------- /lib/models/Sensors1.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Sensors1.bam -------------------------------------------------------------------------------- /lib/models/Sensors2.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Sensors2.bam -------------------------------------------------------------------------------- /lib/models/ColorSquare.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/ColorSquare.bam -------------------------------------------------------------------------------- /lib/models/ColorsBase.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/ColorsBase.bam -------------------------------------------------------------------------------- /lib/models/Reservoir.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Reservoir.bam -------------------------------------------------------------------------------- /lib/models/Tankwater.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Tankwater.bam -------------------------------------------------------------------------------- /lib/models/plants/Leaf.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/plants/Leaf.bam -------------------------------------------------------------------------------- /lib/models/MeasureStick.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/MeasureStick.bam -------------------------------------------------------------------------------- /lib/models/ReservoirLid.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/ReservoirLid.bam -------------------------------------------------------------------------------- /lib/models/Fans_on_blades.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/Fans_on_blades.bam -------------------------------------------------------------------------------- /lib/models/ReservoirWater.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/ReservoirWater.bam -------------------------------------------------------------------------------- /lib/models/plants/BabyLeaf.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/plants/BabyLeaf.bam -------------------------------------------------------------------------------- /lib/models/plants/Planttest.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/plants/Planttest.bam -------------------------------------------------------------------------------- /lib/models/plants/ThickStem.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/plants/ThickStem.bam -------------------------------------------------------------------------------- /lib/models/plants/ThinStem.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/plants/ThinStem.bam -------------------------------------------------------------------------------- /lib/models/plants/LettuceLeaf.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/plants/LettuceLeaf.bam -------------------------------------------------------------------------------- /lib/models/plants/BabyLettuceLeaf.bam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reidgs/TerraBot/HEAD/lib/models/plants/BabyLettuceLeaf.bam -------------------------------------------------------------------------------- /stream/stream-audio: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | cvlc -vvv alsa://hw:1,0 --sout '#transcode{acodec=mp3, ab=128} :rtp{mux=ts, sdp=rtsp://:8001/}' -------------------------------------------------------------------------------- /param/cold_and_dry.bsl: -------------------------------------------------------------------------------- 1 | #tests cold and dry conditions 2 | 3 | start = 0 4 | 5 | temperature = 15 6 | humidity = 10 7 | smoist = 0 8 | -------------------------------------------------------------------------------- /stream/stream-video: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | raspivid -o - -t 0 -n -w 640 -h 480 -fps 10 | cvlc -vvv stream:///dev/stdin --sout '#rtp{sdp=rtsp://:8000/}' :demux=h264 4 | -------------------------------------------------------------------------------- /cleanup_processes: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | procs=`ps -auxw | awk '/ros|arduino|Terra/ && !/awk/ && !/local/ {print $2}'` 4 | 5 | if [ -z "$procs" ]; then 6 | echo "All good" 7 | else 8 | kill -9 $procs 9 | echo "Killed" $procs 10 | fi 11 | -------------------------------------------------------------------------------- /param/smoist_up.bsl: -------------------------------------------------------------------------------- 1 | # Test soil moisture below threshold 2 | # Clock starts at 7:55am, first day; set initial sensor readings 3 | START AT 1-07:55:00 4 | smoist = [425, 425] 5 | wlevel = 140.0 # mms of water in reservoir 6 | # Don't have the other FSM's trigger 7 | humidity = [20, 20] 8 | temperature = [25, 25] 9 | -------------------------------------------------------------------------------- /lib/ArduinoCode/Makefile: -------------------------------------------------------------------------------- 1 | ARDUINO_DIR = /usr/share/arduino 2 | ARDUINO_PORT = /dev/ttyACM* 3 | ARDUINO_SKETCHBOOK = ${HOME}/Sketchbook 4 | 5 | #ARDUINO_LIBS = ros_lib TCA9548 DHT20 HX711 Wire dhtnew 6 | ARDUINO_LIBS = ros_lib dhtnew HX711 7 | 8 | BOARD_TAG = mega2560 9 | include /usr/share/arduino/Arduino.mk 10 | -------------------------------------------------------------------------------- /stream/stream-av: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | raspivid -o - -t 0 -w 640 -h 480 -fps 12 -n | ffmpeg -f h264 -r 12 -thread_queue_size 128 -i - -f alsa -ac 1 -thread_queue_size 128 -i plughw:1,0 -map 0:0 -map 1:0 -vcodec copy -acodec libmp3lame -ac 1 -ar 44100 -b:a 128k -f mpegts - | cvlc -vvv stream:///dev/stdin --sout '#rtp{sdp=rtsp://:8000/}' 4 | -------------------------------------------------------------------------------- /docker/run_terrabot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USER_DIR="${1:-.}" 4 | 5 | CONTAINER_NAME=terrabot_container 6 | IMAGE_NAME=terrabot_image 7 | PORT=5901 8 | 9 | # Check if the container exists 10 | if docker ps --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}\$"; then 11 | echo "Starting existing container: $CONTAINER_NAME" 12 | docker exec -it $CONTAINER_NAME bash 13 | else 14 | echo "Starting new container: $CONTAINER_NAME" 15 | docker run -it --rm --name $CONTAINER_NAME -p $PORT:$PORT \ 16 | --volume "$USER_DIR":/home/robotanist/User $IMAGE_NAME 17 | fi -------------------------------------------------------------------------------- /param/default_baseline.bsl: -------------------------------------------------------------------------------- 1 | #These are the default values if not specified 2 | 3 | start = 0 # [0, inf) seconds 4 | #start = 1-00:00:00 # day-hour:min:sec also works, day=1 is the first day 5 | 6 | temperature = 20 7 | humidity = 50 8 | smoist = 800 9 | wlevel = 140 10 | tankwater = 0 11 | 12 | wpump = off 13 | fan = off 14 | led = 0 15 | 16 | 17 | leaf_droop = 0 # [0, 1] 18 | lankiness = 0 # [0, 1] (determines the lankiness of the plant) (only really matters if time is large enough) 19 | plant_health = 1 # [0, 1] (does not affect growth prior to simulation start) 20 | -------------------------------------------------------------------------------- /param/high_temp_baseline.bsl: -------------------------------------------------------------------------------- 1 | #These are the default values if not specified 2 | 3 | #start = 0 # [0, inf) seconds 4 | start = 1-07:00:00 # day-hour:min:sec also works, day=1 is the first day 5 | 6 | temperature = 35 7 | humidity = 50 8 | smoist = 800 9 | wlevel = 140 10 | tankwater = 30 11 | 12 | wpump = off 13 | fan = off 14 | led = 0 15 | 16 | 17 | leaf_droop = 0 # [0, 1] 18 | lankiness = 0 # [0, 1] (determines the lankiness of the plant) (only really matters if time is large enough) 19 | plant_health = 1 # [0, 1] (does not affect growth prior to simulation start) 20 | -------------------------------------------------------------------------------- /lib/freqmsg.py: -------------------------------------------------------------------------------- 1 | from topic_def import sensor_names 2 | 3 | #Takes a name and frequency, and outputs the corresponding string message 4 | def tomsg(name, freq): 5 | if name not in sensor_names: 6 | print("invalid sensor name %s" % name) 7 | return None 8 | return name + '|' + str(freq) 9 | 10 | #Takes a message, and gives the (name, freq) that made it 11 | def frommsg(msg): 12 | pair = msg.split('|') 13 | if len(pair) != 2: 14 | print("message error") 15 | exit() 16 | name = pair[0].strip(' |') 17 | freq = float(pair[1].strip(' |')) 18 | return (name, freq) 19 | 20 | -------------------------------------------------------------------------------- /param/high_humid_baseline.bsl: -------------------------------------------------------------------------------- 1 | #These are the default values if not specified 2 | 3 | #start = 0 # [0, inf) seconds 4 | start = 1-07:00:00 # day-hour:min:sec also works, day=1 is the first day 5 | 6 | temperature = 26 7 | humidity = 80 8 | smoist = 800 9 | wlevel = 140 10 | tankwater = 30 11 | 12 | wpump = off 13 | fan = off 14 | led = 0 15 | 16 | 17 | leaf_droop = 0 # [0, 1] 18 | lankiness = 0 # [0, 1] (determines the lankiness of the plant) (only really matters if time is large enough) 19 | plant_health = 1 # [0, 1] (does not affect growth prior to simulation start) 20 | -------------------------------------------------------------------------------- /param/start_before_midnight.bsl: -------------------------------------------------------------------------------- 1 | #These are the default values if not specified 2 | 3 | #start = 0 # [0, inf) seconds 4 | start = 1-23:00:00 # day-hour:min:sec also works, day=1 is the first day 5 | 6 | temperature = 25 7 | humidity = 80 8 | smoist = 500 9 | wlevel = 140 10 | tankwater = 100 11 | 12 | wpump = off 13 | fan = off 14 | led = 0 15 | 16 | 17 | leaf_droop = 0 # [0, 1] 18 | lankiness = 0 # [0, 1] (determines the lankiness of the plant) (only really matters if time is large enough) 19 | plant_health = 1 # [0, 1] (does not affect growth prior to simulation start) 20 | 21 | -------------------------------------------------------------------------------- /docker/Makefile: -------------------------------------------------------------------------------- 1 | USER_DIR ?= "." 2 | 3 | build: 4 | docker build ${REBUILD} --build-arg FOOBAR="TerraBot" -t terrabot_image . 5 | 6 | rebuild: 7 | make build REBUILD="--no-cache" 8 | 9 | # Need to be able to connect to already running container (exec) 10 | 11 | run: 12 | docker run -it ${ONCE} --name terrabot_container --volume \ 13 | ${USER_DIR}:/home/robotanist/User -p 5901:5901 terrabot_image 14 | 15 | run_once: 16 | make run ONCE="--rm" 17 | 18 | exec: 19 | docker exec -it terrabot_container bash 20 | 21 | stop: 22 | docker container stop terrabot_container 23 | docker container rm terrabot_container 24 | -------------------------------------------------------------------------------- /lib/topic_def.py: -------------------------------------------------------------------------------- 1 | from std_msgs.msg import Int32,Bool,Float32,String,Int32MultiArray,Float32MultiArray 2 | 3 | sensor_names = ['weight', 'smoist', 'light', 'level', 'temp', 'humid'] 4 | actuator_names = ['led', 'wpump', 'fan', 'freq', 'camera'] 5 | 6 | actuator_types = { 7 | 'led' : Int32, 8 | 'wpump' : Bool, 9 | 'fan' : Bool, 10 | 'freq' : String, 11 | 'camera': String } 12 | 13 | sensor_types = { 14 | 'weight' : Float32MultiArray, 15 | 'smoist' : Int32MultiArray, 16 | 'light' : Int32MultiArray, 17 | 'level' : Float32, 18 | 'temp' : Int32MultiArray, 19 | 'humid' : Int32MultiArray, 20 | } 21 | 22 | -------------------------------------------------------------------------------- /param/low_temp_baseline.bsl: -------------------------------------------------------------------------------- 1 | #These are the default values if not specified 2 | 3 | #start = 0 # [0, inf) seconds 4 | start = 1-00:00:00 # Starting at 4am, light behavior should be off 5 | 6 | temperature = 20 # too cold, should turn LED on to heat up 7 | humidity = 50 8 | smoist = 800 9 | wlevel = 140 10 | tankwater = 0 11 | 12 | wpump = off 13 | fan = off 14 | led = 0 15 | 16 | 17 | leaf_droop = 0 # [0, 1] 18 | lankiness = 0 # [0, 1] (determines the lankiness of the plant) (only really matters if time is large enough) 19 | plant_health = 1 # [0, 1] (does not affect growth prior to simulation start) 20 | -------------------------------------------------------------------------------- /docker/run_terrabot.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set USER_DIR="." 4 | if not "%1"=="" set USER_DIR=%1 5 | 6 | set CONTAINER_NAME=terrabot_container 7 | set IMAGE_NAME=terrabot_image 8 | set PORT=5901 9 | 10 | REM Check if the container exists 11 | docker ps --format "{{.Names}}" | findstr /i "^%CONTAINER_NAME%" >nul 12 | if %ERRORLEVEL%==0 ( 13 | echo Starting existing container: %CONTAINER_NAME% 14 | docker exec -it %CONTAINER_NAME% bash 15 | ) else ( 16 | echo Starting new container: %CONTAINER_NAME% 17 | docker run -it --rm --name %CONTAINER_NAME% -p %PORT%:%PORT% ^ 18 | --volume %USER_DIR%:/home/robotanist/User %IMAGE_NAME% 19 | ) 20 | pause -------------------------------------------------------------------------------- /param/high_everything_baseline.bsl: -------------------------------------------------------------------------------- 1 | #These are the default values if not specified 2 | 3 | start = 0 # [0, inf) seconds 4 | #start = 1-00:00:00 # day-hour:min:sec also works, day=1 is the first day 5 | 6 | temperature = 35 # starting too hot, should turn fan on 7 | humidity = 90 # starting at too humid, should turn fan on 8 | smoist = 300 # starting too dry, should turn water pump on 9 | wlevel = 140 10 | tankwater = 100 11 | 12 | wpump = off 13 | fan = off 14 | led = 0 15 | 16 | 17 | leaf_droop = 0 # [0, 1] 18 | lankiness = 0 # [0, 1] (determines the lankiness of the plant) (only really matters if time is large enough) 19 | plant_health = 1 # [0, 1] (does not affect growth prior to simulation start) 20 | 21 | -------------------------------------------------------------------------------- /param/interference.inf: -------------------------------------------------------------------------------- 1 | AT 1-03:00:00 2 | humidity = [noise, normal] # Left humidity sensor is acting noisy 3 | light = [normal, 0] # Right light sensor is stuck off 4 | fan = off # Fans are not working 5 | 6 | AT 1-04:30:0 7 | humidity = [normal, normal] # Humidity sensors back to normal 8 | light = [600, normal] # Left light sensor stuck at 600 9 | fan = normal # Fans are back to normal 10 | current = [12.2, normal] # Current sensor is stuck 11 | 12 | AT 1-09:00:00 13 | fan = on # Fans are stuck on 14 | wpump = off # Water pump is stuck off 15 | 16 | AT 1-09:15:00 17 | fan = normal # Fans are back to normal 18 | wpump = normal # Water pump is back to normal 19 | light = [normal, normal] # Lights are back to normal 20 | -------------------------------------------------------------------------------- /lib/terrabot_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | class Agenda: 4 | schedule = [] 5 | index = 0 6 | time0 = None 7 | def finished(self): return (self.index >= len(self.schedule)) 8 | # Apparently, append and += both change the variable itself - not good 9 | def add_to_schedule(self, x): self.schedule = self.schedule + [x] 10 | 11 | def clock_to_seconds(dtime): 12 | return (((dtime.day-1)*24 + dtime.hour)*3600 + 13 | dtime.minute*60 + dtime.second + dtime.microsecond/1e6) 14 | 15 | def dtime_to_seconds(dtime): 16 | return clock_to_seconds(datetime.strptime(dtime, "%d-%H:%M:%S")) 17 | 18 | def clock_time(time): 19 | return datetime.fromtimestamp(time).strftime("%d-%H:%M:%S") 20 | 21 | def time_since_midnight(time): 22 | dtime = datetime.fromtimestamp(time) 23 | return (dtime.hour*3600 + dtime.minute*60 + dtime.second + 24 | dtime.microsecond/1e6) 25 | -------------------------------------------------------------------------------- /lib/sim_camera.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import glob 4 | 5 | def get_images_from_directory(dir): 6 | filenames = glob.glob("%s/cam.*.jpg" %dir) 7 | filenames.sort() 8 | images = [] 9 | for f in filenames: 10 | time = int(f[f.find("cam.")+4 : f.find(".jpg")-12]) 11 | #print("Time: %.1f" %time) 12 | images += [[time, f]] 13 | return images 14 | 15 | def find_image(time, images): 16 | last_image = None 17 | if (len(images) == 0): return None 18 | for image in images: 19 | dt = image[0] - time 20 | if (dt >= 0): 21 | if (last_image == None or dt < (time - last_image[0])): 22 | return image[1] 23 | else: 24 | return last_image[1] 25 | last_image = image 26 | return images[-1][1] 27 | 28 | #import sys 29 | #images = get_images_from_directory("Photos") 30 | #while True: 31 | # time = int(sys.stdin.readline()) 32 | # print(find_image(time, images)) 33 | -------------------------------------------------------------------------------- /agents/limits.py: -------------------------------------------------------------------------------- 1 | scale = {} 2 | limits = {} 3 | optimal = {} # Note, not all the sensors have 'optimal' levels 4 | names = {} 5 | 6 | scale['light_level'] = [0, 1000] 7 | limits['light_level'] = [850, 950] 8 | optimal['light_level'] = [860,940] 9 | names['light_level'] = 'lights' 10 | 11 | scale['water_level'] = [0, 200] 12 | limits['water_level'] = [5, 200] 13 | optimal['water_level'] = [5,200] 14 | names['water_level'] = 'w_level' 15 | 16 | scale['moisture'] = [0, 1000] 17 | limits['moisture'] = [500, 650] 18 | optimal['moisture'] = [550,600] 19 | names['moisture'] = 'moist' 20 | 21 | scale['humidity'] = [0, 100] 22 | limits['humidity'] = [60, 90] 23 | optimal['humidity'] = [70, 80] 24 | names['humidity'] = 'humid' 25 | 26 | scale['temperature'] = [10, 40] # Celcius 27 | limits['temperature'] = [22, 29] 28 | optimal['temperature'] = [25,27] 29 | names['temperature'] = 'temp' 30 | 31 | scale['weight'] = [0, 2000] 32 | limits['weight'] = [300, 1500] 33 | names['weight'] = 'weight' 34 | 35 | scale['current'] = [0, 1000] 36 | limits['current'] = [500, 600] 37 | names['current'] = 'current' 38 | 39 | scale['energy'] = [0, 15000] 40 | limits['energy'] = [1000, 2000] 41 | names['energy'] = 'energy' 42 | 43 | names['led'] = 'led' 44 | names['fan'] = 'fan' 45 | names['pump'] = 'wpump' 46 | -------------------------------------------------------------------------------- /M1SETUP.md: -------------------------------------------------------------------------------- 1 | # Installing TerraBot on Apple Silicon (M1) 2 | 3 | ## Convert the OVA to QCow2 4 | - On Ubuntu: 5 | ``` 6 | tar -xvf TerraBot\ 2023.ova 7 | sudo apt-get install qemu-utils 8 | qemu-img convert -O qcow2 TerraBot\ 2023-disk001.vmdk TerraBot\ 2023.qcow2 9 | ``` 10 | (For Mac, use `brew install qemu`) 11 | 12 | To check for consistency: `qemu-img check TerraBot\ 2024.qcow2` 13 | 14 | ## Create the Virtual Machine using UTM 15 | Download UTM from ```https://mac.getutm.app``` 16 | (you can also get the exact same app on the app store but it will be $10 instead of free) 17 | 18 | Open UTM and click **Create new Virtual Machine**, then click **Emulate**, followed by **Custom** 19 | 20 | Checkmark **Skip ISO boot** 21 | 22 | Keep clicking **continue** until Wizard is done 23 | 24 | Click on **VM settings**, go to **drives** tab and click **New**. Click **Import** and select the qcow2 drive. Delete the other drive that exists. 25 | 26 | Go to **QEMU** tab and disable **UEFI boot** 27 | 28 | Go to **systems** tab and set CPU cores to total cores of the laptop (8 for basic M1). 29 | 30 | Set RAM to 4GB (set it to 8+ if you have 16GB RAM on your machine) 31 | 32 | Enable **Force Multicore** 33 | 34 | Click **Save** 35 | 36 | **Note:** Firefox can be buggy under UTM; you should use **chromium** (already installed) instead 37 | -------------------------------------------------------------------------------- /agents/streaming_behavior.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess as sp 3 | from behavior import * 4 | from transitions import Machine 5 | 6 | class StreamAV(Behavior): 7 | stream = None 8 | devnull = None 9 | 10 | def __init__(self): 11 | super(StreamAV, self).__init__("StreamAVBehavior") 12 | self.fsm = Machine(self, states=["Halt", "Streaming"], 13 | initial="Halt", ignore_invalid_triggers=True) 14 | 15 | self.fsm.add_transition("enable", "Halt", "Streaming") 16 | self.fsm.add_transition("disable", "Streaming", "Halt") 17 | self.fsm.on_enter_Streaming("startStreaming") 18 | self.fsm.on_enter_Halt("turnOffActuator") 19 | 20 | def startStreaming(self): 21 | terrabotdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 22 | print(terrabotdir) 23 | if (not self.devnull): self.devnull = open(os.devnull, "wb") 24 | #self.stream =sp.Popen(terrabotdir+"/stream/stream-av", 25 | # stdout=self.devnull, stderr=sp.STDOUT) 26 | self.stream = sp.Popen("ls") 27 | 28 | def stopStreaming(self): 29 | if (self.stream): 30 | self.stream.terminate() 31 | self.stream.wait() 32 | self.stream = None 33 | 34 | def turnOffActuator(self): self.stopStreaming() 35 | 36 | def start(self): 37 | print("Enable: %s" %self.name) 38 | self.trigger("enable") 39 | 40 | def stop(self): 41 | print("Disable: %s" %self.name) 42 | self.trigger("disable") 43 | -------------------------------------------------------------------------------- /lib/baseline.py: -------------------------------------------------------------------------------- 1 | from terrabot_utils import dtime_to_seconds 2 | 3 | #TODO add error catching support, used_energy? 4 | class Baseline: 5 | def __init__(self, filename): 6 | self.params = {'start' : 0, 'temperature' : 22, 'humidity' : 50, \ 7 | 'smoist' : 400, 'wlevel' : 140, 'tankwater' : 0, \ 8 | 'wpump' : False, 'fan' : False, 'led' : 0, \ 9 | 'leaf_droop' : 0, 'lankiness' : 0, 'plant_health' : 1} 10 | if not filename: return 11 | with open(filename) as reader: 12 | 13 | for line in reader: 14 | 15 | #Remove comments 16 | line = line.split('#')[0].strip(' \n') 17 | if line == '': continue 18 | if line.find('=') == -1: 19 | print("invalid basline syntax") 20 | exit() 21 | #Get key/value 22 | pair = line.split('=') 23 | key = pair[0].strip(' ') 24 | val = pair[1].strip(' ') 25 | 26 | if key in ['wpump', 'fan']: 27 | self.params[key] = (val == 'on') 28 | elif key in ['leaf_droop', 'lankiness', 'plant_health']: 29 | self.params[key] = min(1, max(0, float(val))) 30 | elif key == 'start': 31 | self.params['start'] = (int(val) if (val.find('-') < 0) else 32 | dtime_to_seconds(val)) 33 | elif key not in self.params.keys(): 34 | print("invalid parameter name: {}".format(key)) 35 | return 36 | elif key == 'led': 37 | self.params[key] = int(float(val)) 38 | else: 39 | self.params[key] = float(val) 40 | 41 | def __str__(self): 42 | return str(self.params) 43 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Basic ROS1 download 2 | FROM ros:noetic 3 | 4 | # install apt-get deps 5 | RUN apt-get update && \ 6 | apt-get install -y git python3.8 python3-pip python-is-python3 tmux && \ 7 | pip install dill opencv-python panda3d transitions scikit-learn ortools \ 8 | matplotlib pandas msal requests && \ 9 | pip install --upgrade --force-reinstall numpy>=1.24 python-dateutil>=2.8.2 10 | 11 | RUN apt-get install -y x11vnc xvfb openbox tint2 mesa-utils python3-tk 12 | EXPOSE 5900 13 | ENV DISPLAY=:1 XRES=1280x800x24 14 | 15 | # install ros apt-get deps 16 | RUN apt-get install -y ros-noetic-ros-base && apt-get clean && \ 17 | rosdep update 18 | 19 | # Create user id 20 | ARG FOOBAR 21 | RUN useradd robotanist -m -s /bin/bash -G sudo && \ 22 | echo "robotanist:$FOOBAR" | chpasswd 23 | RUN sudo mkdir /tmp/.X11-unix && \ 24 | sudo chown root:robotanist /tmp/.X11-unix && \ 25 | sudo chmod 1777 /tmp/.X11-unix 26 | USER robotanist 27 | WORKDIR /home/robotanist 28 | 29 | # install TerraBot software 30 | RUN cd /home/robotanist && \ 31 | git clone https://github.com/reidgs/TerraBot && \ 32 | echo 'source /opt/ros/noetic/setup.bash' >> ~/.bashrc && \ 33 | echo 'export TB_DIR=${HOME}/TerraBot' >> ~/.bashrc && \ 34 | echo 'export PYTHONPATH=${PYTHONPATH}:${TB_DIR}:${TB_DIR}/lib:${TB_DIR}:${TB_DIR}/agents' >> ~/.bashrc 35 | 36 | ENV LIBGL_ALWAYS_SOFTWARE=1 PYOPENGL_PLATFORM=null 37 | RUN echo '#!/bin/sh\n Xvfb $DISPLAY -screen 0 $XRES &\n sleep 1\n \ 38 | openbox > /dev/null 2>&1 &\n \ 39 | tint2 > /dev/null 2>&1 &\n \ 40 | x11vnc -display $DISPLAY -N -rfbport 5901 -forever -nopw -noxdamage > /dev/null 2>&1 &\n \ 41 | exec bash' > ~/.start.sh && \ 42 | chmod a+x ~/.start.sh 43 | 44 | # I like to use this editor; uncomment out for my personal image 45 | #USER root 46 | #RUN apt-get -y install xemacs21 && apt-get clean 47 | #USER robotanist 48 | 49 | # add launcher 50 | CMD ~/.start.sh 51 | -------------------------------------------------------------------------------- /agents/log_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rospy 4 | 5 | def write_log_data_line(log_file, name, data, now): 6 | if (log_file): 7 | if (isinstance(data, tuple)): 8 | log_file.write("%f '%s' %.1f %.1f\n" 9 | %(now, name, data[0], data[1])) 10 | elif (isinstance(data, int)): 11 | log_file.write("%f '%s' %d\n" %(now, name, data)) 12 | elif (isinstance(data, float)): 13 | log_file.write("%f '%s' %.1f\n" %(now, name, data)) 14 | else: 15 | log_file.write("%f '%s' %s\n" %(now, name, data)) 16 | 17 | def process_log_data_line(line): 18 | sline = line.split("'") 19 | data = sline[2].strip(' \n').split(' ') 20 | return (float(sline[0]), sline[1], 21 | ((float(data[0]), float(data[1])) if (len(data) > 1) else 22 | float(data[0]) if ('.' in data[0]) else 23 | int(data[0]) if (data[0].isdigit()) else data[0])) 24 | 25 | def read_log_file(filename): 26 | log_data = [] 27 | with open(filename) as log_file: 28 | for line in log_file: 29 | log_data.append(process_log_data_line(line)) 30 | return log_data 31 | 32 | def log_sensordata(log_data, time, sensordata={}, next_index=0): 33 | for index in range(next_index, len(log_data)): 34 | datum = log_data[index] 35 | #print(index, datum) 36 | if (datum[0] > time): return (sensordata, index) 37 | else: 38 | sensordata[datum[1]] = datum[2] 39 | return (sensordata, len(log_data)) 40 | 41 | if __name__ == '__main__': 42 | import sys, select, os 43 | sys.path.insert(0, os.getcwd()[:os.getcwd().find('TerraBot')]+'TerraBot/lib') 44 | import topic_def as td 45 | from std_msgs.msg import Float32, Int32, Int32MultiArray, Float32MultiArray, Bool, String 46 | import argparse 47 | 48 | parser = argparse.ArgumentParser(description = "Interactive Agent") 49 | parser.add_argument('file', help="log the sensor data to file") 50 | parser.add_argument('-s', '--sim', action = 'store_true', help="use simulator") 51 | args = parser.parse_args() 52 | 53 | if args.sim: rospy.set_param("use_sim_time", True) 54 | rospy.init_node("logger", anonymous = True) 55 | 56 | def log_data_cb(data, file_and_topic): 57 | write_log_data_line(file_and_topic[0], file_and_topic[1], data.data, 58 | rospy.get_time()) 59 | 60 | with open(args.file, "w") as log_file: 61 | for topic in td.sensor_names: 62 | rospy.Subscriber(topic+"_output", td.sensor_types[topic], 63 | log_data_cb, (log_file, topic)) 64 | for topic in td.actuator_names: 65 | rospy.Subscriber(topic+"_input", td.actuator_types[topic], 66 | log_data_cb, (log_file, topic)) 67 | while not rospy.core.is_shutdown(): 68 | rospy.sleep(0.1) 69 | -------------------------------------------------------------------------------- /VIRTUALBOX.md: -------------------------------------------------------------------------------- 1 | ## Creating VirtualBox Image for TerraBot ## 2 | 3 | ### Set up VirtualBox Image ### 4 | In VirtualBox 5 | * Choose “New”; Name: TerraBot; Type: Linux; Version: Ubuntu (64-bit) 6 | * RAM: 4096 MB 7 | * Hard disk: Accept default option, choose “Create” 8 | * Hard disk file type: Accept default option, choose “Next” 9 | * Storage on physical hard disk: either option is fine 10 | * File location and size: Enter 20GB; choose “Create” 11 | 12 | ### Installing Ubuntu ### 13 | * Download Ubuntu (ubuntu-20.04.2.0-desktop-amd64.iso from https://releases.ubuntu.com) 14 | * Power on (start) TerraBot machine; find the ISO Ubuntu image and start creating machine; follow instructions to install Ubuntu; 15 | - “your name” is Robotanist; computer’s name is TerraBot, username is robotanist, password is TerraBot; choose “Log in automatically” 16 | * After installation, restart (**don’t forget to press “Enter”**) 17 | 18 | ### Resizing window ### 19 | * In “Devices” menu bar, select “Insert Guest Additions CD Image” and choose “run” 20 | * Enter password and choose “Authenticate”; press return when completed 21 | * Right-click on disk and eject it 22 | * Restart the machine 23 | * Change window to desired size and then in “View” menu, select “Auto resize guest display” 24 | 25 | ### Installing software ### 26 | * sudo apt install python3 python3-pip git 27 | * sudo apt install python-is-python3 28 | * sudo apt install opencv-python 29 | * pip install panda3d transitions sklearn ortools matplotlib pandas opencv-python dill 30 | * **Optional:** sudo apt install xemacs21 (or your favorite text editor) 31 | * **Optional:** in the applications box, search for and install PyCharm CE 32 | 33 | ### Installing ROS ### 34 | * sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' 35 | * sudo apt install curl # if you haven't already installed curl 36 | * curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - 37 | * sudo apt update 38 | * sudo apt install ros-noetic-ros-base 39 | * add the line to the end of the .bashrc file 40 | - source /opt/ros/noetic/setup.bash 41 | 42 | ### Installing TerraBot ### 43 | * git clone https://github.com/reidgs/TerraBot (use your git name and password) 44 | * add the lines to bashrc: 45 | - export TB_DIR=${HOME}/TerraBot 46 | - export PYTHONPATH=${PYTHONPATH}:${ TB_DIR}:${ TB_DIR}/lib:${ TB_DIR}:${ TB_DIR}/agents 47 | 48 | ### Create OVA ### 49 | * In VirtualBox, File -> Export Appliance 50 | * Be sure to choose the location for writing the file; otherwise accept all the defaults 51 | 52 | ### Optimize Parameters ### 53 | * Choose **Settings -> System -> Motherboard** 54 | * Set RAM to about half of your total RAM (up to 10GB) 55 | * Choose **Processor** tab 56 | * Set it to at least 4 (more is better, but also keep in mind how many CPU threads you have -- at least a couple should be left for your overall machine not given to the submachine) 57 | * Click **OK** 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /lib/baselineold.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from terrabot_utils import dtime_to_seconds, clock_time, time_since_midnight 3 | from terrabot_utils import Agenda 4 | 5 | sensor_names = ['light', 'temperature', 'humidity', 'smoist', 'wlevel', 6 | 'current'] 7 | 8 | class Baseline(Agenda): 9 | def __init__(self, filename, time0): # time0 is midnight of the first day 10 | self.time0 = time0 11 | last_time = time0 12 | if (not filename): return 13 | with open(filename) as f: 14 | for line in f.readlines(): 15 | l = line.split('#')[0].strip(' \n') 16 | if (l.find('START AT') == 0) : 17 | time = dtime_to_seconds(l[len("START AT ")]) 18 | actions = [] 19 | self.add_to_schedule([time, actions]) 20 | self.time0 = time 21 | print("START: %s" %clock_time(time)) 22 | last_time = time 23 | elif (l.find('AT') == 0): 24 | time = dtime_to_seconds(l[len("AT ")]) + time0 25 | if (time < last_time): 26 | print("Time must run forward: %s" %l) 27 | quit() 28 | last_time = time 29 | actions = [] 30 | self.add_to_schedule([time, actions]) 31 | elif (len(l) > 0): 32 | # Should be "sensor = value(s)" 33 | sensor = l[:l.find(" ")] 34 | if (not sensor in sensor_names): 35 | print("%s not a legal baseline sensor name" %sensor) 36 | quit() 37 | sensor_val = l[l.find("=")+1:] 38 | actions.append([sensor, sensor_val]) 39 | 40 | def update(self, time, sensor_values): 41 | if (not self.finished() and (time >= self.schedule[self.index][0])): 42 | print("Updating baseline at %s:" %clock_time(time)) 43 | for a in self.schedule[self.index][1]: 44 | sensor_values[a[0]] = eval(a[1]) 45 | #print(" %s: %s" %(a[0], sensor_values[a[0]])) 46 | self.index += 1 47 | return True 48 | else: return False 49 | 50 | def display(self): 51 | for s in self.schedule: 52 | # Not sure why this prints right - but it does 53 | print("%sAT %s" %("START " if (self.time0 == s[0]) else "", 54 | clock_time(s[0]))) 55 | for sv in s[1]: 56 | print(" %s = %s" %(sv[0], sv[1])) 57 | 58 | if __name__ == '__main__': 59 | import sys, time 60 | if (len(sys.argv) == 2): 61 | now = time.time() 62 | time0 = now - time_since_midnight(now) 63 | baseline = Baseline(sys.argv[1], time0) 64 | baseline.display() 65 | #""" 66 | # Testing how baseline works 67 | t = baseline.time0; sensor_values = {} 68 | while not baseline.finished(): 69 | baseline.update(t, sensor_values) 70 | t += 1 71 | #""" 72 | else: 73 | print("Need to provide baseline file to parse") 74 | -------------------------------------------------------------------------------- /lib/send_email.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created 6/25 5 | 6 | @author: Reid Simmons 7 | 8 | Extensive use of chatGPT to figure out how to send Outlook emails with oauth2 9 | 10 | API: send_email.send(sender, recipients, subject, text, images=[], inline=False) 11 | * sender: email address must match that in ../param/token_cache.json 12 | * recipients: comma-separated list of email addresses 13 | * subject: subject line 14 | * text: can be plain text or html 15 | * images: optional list of images (byte arrays, not file names) 16 | * inline: whether the images are inline, or attachments; if inline, refer to 17 | them in the text using, for instance, '', 18 | for image1, image2, ... 19 | """ 20 | import os, base64, msal, requests 21 | 22 | TOKEN_CACHE_FILE = '../param/token_cache.json' 23 | CLIENT_ID = 'ff1cc03d-5766-430d-b45d-587116b60294' 24 | AUTHORITY = f"https://login.microsoftonline.com/common" 25 | SCOPES = ['User.Read', 'Mail.Send'] 26 | 27 | def init(): 28 | try: 29 | cache = msal.SerializableTokenCache() 30 | with open(TOKEN_CACHE_FILE, "r") as f: 31 | cache.deserialize(f.read()) 32 | except Exception as e: 33 | print("Failed to find token cache:", e) 34 | return None 35 | 36 | app = msal.PublicClientApplication( 37 | CLIENT_ID, 38 | authority=AUTHORITY, 39 | token_cache = cache, 40 | ) 41 | accounts = app.get_accounts() 42 | if accounts: 43 | token_result = app.acquire_token_silent(SCOPES, account=accounts[0]) 44 | else: 45 | print("No cached token found. Initiating device flow login...") 46 | flow = app.initiate_device_flow(scopes=SCOPES) 47 | if "user_code" not in flow: 48 | raise ValueError("Failed to create device flow") 49 | 50 | print(f"\nGo to {flow['verification_uri']} and enter code: {flow['user_code']}") 51 | print("Waiting for authentication...\n") 52 | 53 | token_result = app.acquire_token_by_device_flow(flow) 54 | 55 | # === SAVE TOKEN CACHE === 56 | if cache.has_state_changed: 57 | with open(TOKEN_CACHE_FILE, "w") as f: 58 | f.write(cache.serialize()) 59 | 60 | #print(token_result) 61 | if "access_token" in token_result: 62 | return token_result["access_token"] 63 | else: return None 64 | 65 | def msg_attachments(images, inline): 66 | return [{"@odata.type": "#microsoft.graph.fileAttachment", 67 | "name": "image%d" %(i+1), 68 | "contentType": "image/jpeg", 69 | "contentBytes": base64.b64encode(image).decode('utf-8'), 70 | "isInline": inline, 71 | "contentId": "image%d" %(i+1) 72 | } for i, image in enumerate(images)] 73 | 74 | def send(from_address, to_addresses, subject, text, images=[], inline=False): 75 | try: 76 | access_token = init() 77 | if (not access_token): return False 78 | 79 | headers = { 'Authorization': f'Bearer {access_token}', 80 | 'Content-Type': 'application/json'} 81 | user_info = requests.get('https://graph.microsoft.com/v1.0/me', headers=headers).json() 82 | token_address = user_info.get("userPrincipalName").lower() 83 | if (token_address != from_address.lower()): 84 | raise Exception("from address %s does not match token address %s" 85 | %(from_address, token_address)) 86 | 87 | to_addresses = [{"emailAddress": {"address": address.strip()}} 88 | for address in to_addresses.split(',')] 89 | content_type = "html" if ('<' in text and 'This is a test

', 113 | images, inline=True): 114 | print("Successfully sent!") 115 | ''' -------------------------------------------------------------------------------- /docker/DockerSETUP.md: -------------------------------------------------------------------------------- 1 | ## Installing and Running TerraBot in Docker ## 2 | - [Building a Docker Image](#Build_Docker) 3 | - [Running a Docker Container](#Run_Docker) 4 | - [Running TerraBot and Agent Software](#Run_TerraBot) 5 | - [Visualizing the TerraBot's Graphics](#Visualization) 6 | 7 | ### Build Docker ### 8 | Building the system using docker is very simple: 9 | * Download docker desktop ([docs.docker.com/get-started/introduction/get-docker-desktop/](https://docs.docker.com/get-started/introduction/get-docker-desktop/)) for your machine 10 | * cd to the TerraBot/docker directory 11 | * If you have access to `make`, simply invoke `make build` (or `make rebuild` if you want to rebuild from scratch) 12 | * Otherwise, look at the `build` or `rebuild` instructions in the `Makefile` and run those commands in a shell 13 | 14 | In either case, you will produce a terrabot_image file that can then be run (see next section). 15 | 16 | ### Run Docker ### 17 | The docker image includes Ubuntu, ROS, the TerraBot software, and the packages you will need to run the TerraBot and all the assignments in the course. To run a docker container: 18 | * From a shell, if on Windows, invoke `run_terrabot.bat`; if on Linux or Macs, invoke `sudo ./run_terrabot.sh`, providing the password `TerraBot`, if needed. 19 | * The shell scripts start a container called `terrabot_container`, exposing port 5901 (which is used for graphics visualization). It also maps a directory on your computer to the `User` directory in the container. By default, the directory mapped is the one in which you invoked the shell script. However, you can change that by providing a path to the directory you want to view within the container. For instance: 20 | * `sudo ./run_terrabot.sh "c:\Users\rsimmons\Desktop\ROS_HW"` 21 | * (note: this is equivalent to cd'ing to the ROS_HW directory and invoking the script from there) 22 | * If a container is already running, the shell scripts will connect you to the existing container 23 | * You can also invoke the container from the Docker Desktop, but you will have to add the command-line arguments manually. 24 | 25 | Once you invoke the container, you will end up in a `bash` shell under Ubuntu. From there, you can run the TerraBot software (see next section). 26 | 27 | ### Run TerraBot ### 28 | There are two directories within the container. The `TerraBot` directory contains all the software needed to run the TerraBot simulator; The `User` directory is whichever directory you mapped to when you invoked the shell scripts (default being the directory in which the shell script was invoked). Note that if you edit files in the `User` directory, it will be changed on your computer; if you edit files in the `TerraBot` directory, those changes will last only as long as the container is running. 29 | 30 | To run the TerraBot, do `python TerraBot\TerraBot.py -m sim`. You can also use the `-s ` flag to speed up the simulator by a factor of "x" (e.g., `-s 100` runs the simulator at 100x real speed), and the `-g` flag to run graphics (see next section for how to view the graphics). You will see `Waiting for nodes`, `Starting simulator`, followed by `System started`. The software is running and growing plants (but they will die without an autonomous agent to take care of them). 31 | 32 | For most of the assignments, you will need multiple terminals to be running. There are several ways to accomplish this: 33 | 1. Invoke the shell scripts multiple times; the first invocation starts a new container and subsequent invocations connect a new terminal to that container. 34 | 2. Use `tmux`, which is a terminal multiplexer. Use `ctl-b "` to split screens and `ctl-b o` to move between them. One good reason to use `tmux` with the docker container is that you'll need to use it on the Raspberry Pi, so it is good to get familiar with using it. 35 | 3. You can connect to the docker container through Visual Studio. Open the `Extensions` tab and install `Dev Containers` (need to do this just once). Then, open the `Remote Explorer` tab and select the `terrabot_image (terrabot_container)`. At that point, you can navigate to and execute files (using the `Folders` tab) or open a terminal. Again, if you edit files in the `User` directory, they get changed on your computer; if you edit files in the `TerraBot` directory, they will go away once the container ends. 36 | 4. You can open new terminals using `xterm &`, but to be seen you need to follow the instructions in the next section. 37 | 38 | ### Visualization ### 39 | The TerraBot container runs lightweight versions of X (a VNC server) with a window manager, exporting the display on port 5901. To view the display, you need to run a remote viewer. There are several available options. TigerVNC ([https://tigervnc.org/](https://tigervnc.org/)) is available for Windows, Macs, and Linux, but other options include RealVNC, TightVNC, and Remmina (for Linux). You will need to download and install one of the options. 40 | 41 | When you invoke the viewer, connect to `localhost:5901` - you should then be able to see any graphics that have been started in the container. In particular, when you run `TerraBot.py` with the `-m sim -g` options, it starts up a graphical simulator that shows all the dynamic change to the (simulated) greenhouse. Similarly, the `agents/plot_sensors.py` and `agents/time_series.py` both display graphics. And, as mentioned above, you can start xterm windows and they can be interacted with (e.g., editing and running code). 42 | 43 | **Note:** You can view remotely, from a different computer, by replacing `localhost` with the hostname of the machine that is running the docker container (e.g., `foobar.cs.cmu.edu:5901`). 44 | 45 | 46 | -------------------------------------------------------------------------------- /RPISETUP.md: -------------------------------------------------------------------------------- 1 | ## Installing Software on Raspberry Pi and Arduino ## 2 | 3 | ## Install Ubunu ## 4 | * Get rpi imager (rasberrypi.com/software - choose general-purpose OS -> Ubuntu -> Ubuntu Server 20.04.5 (64 bit) 5 | * Choose "Edit Settings" and set hostname (terrabot), user name (robotanist-admin), and passphrase [GET ADMIN PASSWORD FROM REID] 6 | * Flash to SD card 7 | 8 | ### Connect to Network ### 9 | * Hostname: same as computer name; Address: use `cat /sys/class/net/eth0/address` 10 | * __Wired:__ https://computing.cs.cmu.edu/help-support/equip-registration - Register a New Machine *__or__* Search, Update and Remove Equipment Support 11 | * __Wifi:__ use these instructions: https://linuxconfig.org/ubuntu-20-04-connect-to-wifi-from-command-line to set up via command line 12 | 13 | ### Install desktop (not 100% needed, but makes setup a lot easier) ### 14 | * `sudo apt update & sudo apt upgrade` 15 | * `sudo apt install ubuntu-desktop` 16 | * `sudo reboot` 17 | 18 | ### Set up Logins on the Raspberry Pi ### 19 | * `sudo useradd -m robotanist` 20 | * `sudo passwd robotanist` (password is: TerraBot) 21 | * `sudo usermod -a -G dialout,video,audio,robotanist robotanist-admin` 22 | * `sudo usermod -a -G dialout,video,audio robotanist` 23 | 24 | ### Creating Swap File ### 25 | Do as robotanist-admin; 26 | Check whether already have a swap file: cat /proc/swaps; if not: 27 | * `sudo apt install dphys-swapfile` 28 | * edit /etc/dphys-swapfile and uncomment CONF_SWAPFILE line 29 | * `sudo dphys-swapfile setup` 30 | * `sudo dphys-swapfile swapon` 31 | 32 | 33 | ### Enabling SSH ### 34 | * `sudo systemctl enable ssh` 35 | * `sudo systemctl start ssh` 36 | 37 | * Make sure the /etc/ssh/sshd_config has `PasswordAuthentication yes` 38 | 39 | 44 | 45 | ### Installing software ### 46 | Do as robotanist-admin 47 | * `sudo apt update` 48 | * `sudo apt install git vlc curl` 49 | * `sudo apt install python3 python3-pip python-is-python3` 50 | * `sudo apt install python3-opencv python3-matplotlib tmux dill` 51 | * `sudo apt install python3-transitions python3-sklearn` 52 | * `sudo apt install libraspberrypi-bin` 53 | * Optional: `sudo apt install xemacs21` (or your favorite text editor) 54 | 55 | ### Installing ROS ### 56 | Do as robotanist-admin 57 | * `sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list'` 58 | * `curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -` 59 | * `sudo apt update` 60 | * `sudo apt install build-essential arduino arduino-mk` 61 | * `sudo apt install ros-noetic-rosserial ros-noetic-rosserial-arduino` 62 | * add the lines to the end of the .bashrc file 63 | - `source /opt/ros/noetic/setup.bash` 64 | - `export ROS_LOCALHOST_ONLY=1` 65 | 66 | ### Installing TerraBot Software ### 67 | Switch user to robotanist 68 | * `git clone https://github.com/reidgs/TerraBot` (use your git name and password) 69 | * add the line `source /opt/ros/noetic/setup.bash` to the end of the .bashrc file 70 | * add the lines to bashrc (for both robotanist and robotanist-admin): 71 | - `export TB_DIR=${HOME}/TerraBot` 72 | - `export PYTHONPATH=${PYTHONPATH}:${TB_DIR}:${TB_DIR}/lib:${TB_DIR}/agents` 73 | * `source ~/.bashrc` 74 | 75 | ### Installing Arduino ### 76 | Do as robotanist-admin 77 | * `ln -s /home/robotanist/Terrabot .` 78 | * `mkdir ~/Sketchbook/libraries; cd ~/Sketchbook/libraries` 79 | * `rosrun rosserial_arduino make_libraries.py .` 80 | * `git clone https://github.com/RobTillaart/dhtnew.git` 81 | * `git clone https://github.com/RobTillaart/HX711.git` 82 | * `cd ~/TerraBot/lib/ArduinoCode` 83 | * `make clean; make upload` [note: may have to change the permissions on ArduinoCode to make them available to robotanist-admin) 84 | 85 | ### Installing ortools ### 86 | Do as robotanist-admin 87 | 88 | * `python -m pip install numpy==1.21` 89 | * `python -m pip install ortools` 90 | 91 | 96 | 97 | ### Set up Camera ### 98 | * `df | grep firmware` => use this device name below 99 | * `sudo mount /dev/mmcblk0p1 /boot` 100 | * Edit /boot/firmware/config.txt to add the following lines: 101 | - `start_x=1` 102 | - `gpu_mem=128` 103 | * Reboot (and check using raspivid -t 1000) 104 | 105 | ### Copy Setup to Other SD Cards ### 106 | * https://computers.tutsplus.com/articles/how-to-clone-your-raspberry-pi-sd-cards-with-windows--mac-59294 107 | * Change the machine name accordingly: 108 | - `sudo hostnamectl set-hostname newNameHere` 109 | - `sudo xemacs /etc/hostname` – replace occurrence of existing computer name 110 | - `sudo xemacs /etc/hosts` – replace all occurrences of existing computer name 111 | * Regenerate ssh server keys 112 | - `sudo rm -v /etc/ssh/ssh_host_*` 113 | - `sudo dpkg-reconfigure openssh-server` 114 | - `sudo systemctl restart ssh` 115 | * Deal with the audio server 116 | - `sudo chown robotanist /usr/bin/pulseaudio` 117 | - `sudo chgrp robotanist /usr/bin/pulseaudio` 118 | - `pactl list` -> look for the card associated with the microphone 119 | - edit stream-av and stream-audio to change the plughw card to match (e.g., plughw:1,0) 120 | * Check pump pressure 121 | - if pump is too weak (or too strong), edit wpump_activate in Arduino.ino (and make upload) 122 | 123 | 124 | -------------------------------------------------------------------------------- /agents/plot_sensors.py: -------------------------------------------------------------------------------- 1 | import rospy, sys, select, os, time, argparse 2 | from std_msgs.msg import Float32, Int32, Int32MultiArray, Float32MultiArray, Bool, String 3 | import matplotlib.pyplot as plt 4 | from limits import scale, limits, names 5 | 6 | sensor_levels = {} 7 | actuator_labels = {} 8 | sensor_bars = {} 9 | sensor_values = {} 10 | 11 | def add_sensor(name, fig, nrow, ncol, pos): 12 | global sensor_levels 13 | sensor_levels[name] = fig.add_subplot(nrow, ncol, pos) 14 | update_sensor(name, None) 15 | 16 | def add_actuator(name, fig, nrow, ncol, pos): 17 | global actuator_labels 18 | actuator_labels[name] = fig.add_subplot(nrow, ncol, pos) 19 | update_actuator(name, "") 20 | 21 | def update_sensor(name, value): 22 | global sensor_levels, names, sensor_bars, sensor_values 23 | ax = sensor_levels[name] 24 | if (value == None): 25 | if (value == None): value = 0 26 | sensor_values[name] = value 27 | ax.set_xlim(scale[name]) 28 | bars = ax.barh(names[name], value) 29 | sensor_bars[name] = bars[0] 30 | sensor_bars[name].set_width(value) 31 | sensor_bars[name].set_color('Red' if value < limits[name][0] else 32 | 'Red' if value > limits[name][1] else 'Green') 33 | 34 | def update_actuator(name, actuator): 35 | global actuator_labels, names, sensor_values 36 | ax = actuator_labels[name] 37 | if (not ax.texts): 38 | sensor_values[name] = '' 39 | ax.set_yticklabels([]) 40 | #ax.set_yticks([],[]) 41 | #ax.set_xticks([],['']) 42 | ax.bar(names[name],[0]) 43 | else: 44 | ax.texts[0].remove() 45 | ax.text(0, 0, actuator, ha='center', va='center', color='Blue') 46 | 47 | def init_ros (use_simulator): 48 | global led_pub, wpump_pub, fan_pub, camera_pub 49 | global sensor_values, actuator_labels 50 | 51 | if use_simulator: rospy.set_param("use_sim_time", True) 52 | rospy.init_node("time_series_grapher", anonymous = True) 53 | 54 | rospy.Subscriber("smoist_output", Int32MultiArray, 55 | update_sensor_multi_data, 'moisture') 56 | rospy.Subscriber("light_output", Int32MultiArray, 57 | update_sensor_multi_data, 'light_level') 58 | rospy.Subscriber("level_output", Float32, 59 | update_sensor_data, 'water_level') 60 | rospy.Subscriber("temp_output", Int32MultiArray, 61 | update_sensor_multi_data, 'temperature') 62 | rospy.Subscriber("humid_output", Int32MultiArray, 63 | update_sensor_multi_data, 'humidity') 64 | rospy.Subscriber("weight_output", Float32MultiArray, 65 | update_sensor_multi_data, 'weight') 66 | rospy.Subscriber("cur_output", Float32MultiArray, update_power_data, 'cur') 67 | rospy.Subscriber("led_input", Int32, update_sensor_data, 'led') 68 | rospy.Subscriber("fan_input", Bool, update_binary_data, 'fan') 69 | rospy.Subscriber("wpump_input", Bool, update_binary_data, 'pump') 70 | 71 | # Initialize actuators 72 | sensor_values['led'] = 'Off' 73 | sensor_values['fan'] = 'Off' 74 | sensor_values['pump'] = 'Off' 75 | 76 | def update_sensor_multi_data(data, name): 77 | sensor_values[name] =(data.data[0] + data.data[1])/2 78 | 79 | def update_sensor_data(data, name): 80 | sensor_values[name] = data.data 81 | 82 | def update_binary_data(data, name): 83 | sensor_values[name] = ('On' if data.data else 'Off') 84 | 85 | def update_power_data(data, name): 86 | sensor_values['current'] = data.data[0] 87 | sensor_values['energy'] = data.data[1] 88 | 89 | def init_plotting(): 90 | fig = plt.figure() 91 | plt.subplots_adjust(hspace=0.6) 92 | plt.subplots_adjust(wspace=0.3) 93 | plt.ion() 94 | 95 | add_sensor('light_level', fig, 6,2,1) 96 | add_sensor('humidity', fig, 6,2,3) 97 | add_sensor('temperature', fig, 6,2,5) 98 | add_sensor('moisture', fig, 6,2,7) 99 | add_sensor('weight', fig, 6,2,9) 100 | add_sensor('water_level', fig, 6,2,10) 101 | add_sensor('current', fig, 6,2,11) 102 | add_sensor('energy', fig, 6,2,12) 103 | 104 | add_actuator('led', fig, 6,2,2) 105 | add_actuator('fan', fig, 6,2,4) 106 | add_actuator('pump', fig, 6,2,6) 107 | 108 | plt.show() 109 | 110 | def print_sensor_values(): 111 | print("Light Level: %.2f" %sensor_values['light_level']) 112 | print("Temperature: %.2f" %sensor_values['temperature']) 113 | print("Humidity: %.2f" %sensor_values['humidity']) 114 | print("Weight: %.2f" %sensor_values['weight']) 115 | print("Soil Moisture: %.2f" %sensor_values['moisture']) 116 | print("Water Level: %.2f" %sensor_values['water_level']) 117 | print("LED: %s" %sensor_values['led']) 118 | print("Fan: %s" %sensor_values['fan']) 119 | print("Water Pump: %s" %sensor_values['pump']) 120 | 121 | parser = argparse.ArgumentParser(description = "Interactive Agent") 122 | parser.add_argument('-s', '--sim', action = 'store_true', help="use simulator") 123 | args = parser.parse_args() 124 | 125 | init_plotting() 126 | 127 | init_ros(args.sim) 128 | rospy.sleep(2) # Need to do this even if running simulator to handle messages 129 | 130 | while not rospy.core.is_shutdown(): 131 | for key in sensor_levels: 132 | update_sensor(key, sensor_values[key]) 133 | for key in actuator_labels: 134 | update_actuator(key, sensor_values[key]) 135 | plt.pause(0.0001) 136 | 137 | if sys.stdin in select.select([sys.stdin],[],[],0)[0]: 138 | input = sys.stdin.readline() 139 | if input[0] == 'q': 140 | quit() 141 | elif input[0] == 'v': 142 | print_sensor_values() 143 | else: 144 | print("Usage: q (quit)\n\tv (sensor values)") 145 | 146 | rospy.sleep(1) 147 | -------------------------------------------------------------------------------- /docker/DockerSETUP.html: -------------------------------------------------------------------------------- 1 |

Installing and Running TerraBot in Docker

2 | 8 |

Build Docker

9 |

Building the system using docker is very simple:

10 | 16 |

In either case, you will produce a terrabot_image file that can then be run (see next section).

17 |

Run Docker

The docker image includes Ubuntu, ROS, the TerraBot software, and the packages you will need to run the TerraBot and all the assignments in the course. To run a docker container:

18 | 29 |

Once you invoke the container, you will end up in a bash shell under Ubuntu. From there, you can run the TerraBot software (see next section).

30 |

Run TerraBot

31 |

There are two directories within the container. The TerraBot directory contains all the software needed to run the TerraBot simulator; The User directory is whichever directory you mapped to when you invoked the shell scripts (default being the directory in which the shell script was invoked). Note that if you edit files in the User directory, it will be changed on your computer; if you edit files in the TerraBot directory, those changes will last only as long as the container is running.

32 |

To run the TerraBot, do python TerraBot\TerraBot.py -m sim. You can also use the -s <x> flag to speed up the simulator by a factor of "x" (e.g., -s 100 runs the simulator at 100x real speed), and the -g flag to run graphics (see next section for how to view the graphics). You will see Waiting for nodes, Starting simulator, followed by System started. The software is running and growing plants (but they will die without an autonomous agent to take care of them).

33 |

For most of the assignments, you will need multiple terminals to be running. There are several ways to accomplish this:

34 |
    35 |
  1. Invoke the shell scripts multiple times; the first invocation starts a new container and subsequent invocations connect a new terminal to that container.
  2. 36 |
  3. Use tmux, which is a terminal multiplexer. Use ctl-b " to split screens and ctl-b o to move between them. One good reason to use tmux with the docker container is that you'll need to use it on the Raspberry Pi, so it is good to get familiar with using it.
  4. 37 |
  5. You can connect to the docker container through Visual Studio. Open the Extensions tab and install Dev Containers (need to do this just once). Then, open the Remote Explorer tab and select the terrabot_image (terrabot_container). At that point, you can navigate to and execute files (using the Folders tab) or open a terminal. Again, if you edit files in the User directory, they get changed on your computer; if you edit files in the TerraBot directory, they will go away once the container ends.
  6. 38 |
  7. You can open new terminals using xterm &, but to be seen you need to follow the instructions in the next section.
  8. 39 |
40 |

Visualization

The TerraBot container runs lightweight versions of X (a VNC server) with a window manager, exporting the display on port 5901. To view the display, you need to run a remote viewer. There are several available options. TigerVNC (https://tigervnc.org/) is available for Windows, Macs, and Linux, but other options include RealVNC, TightVNC, and Remmina (for Linux). You will need to download and install one of the options.

41 |

When you invoke the viewer, connect to localhost:5901 - you should then be able to see any graphics that have been started in the container. In particular, when you run TerraBot.py with the -m sim -g options, it starts up a graphical simulator that shows all the dynamic change to the (simulated) greenhouse. Similarly, the agents/plot_sensors.py and agents/time_series.py both display graphics. And, as mentioned above, you can start xterm windows and they can be interacted with (e.g., editing and running code).

42 |

Note: You can view remotely, from a different computer, by replacing localhost with the hostname of the machine that is running the docker container (e.g., foobar.cs.cmu.edu:5901).

43 |
-------------------------------------------------------------------------------- /lib/interference.py: -------------------------------------------------------------------------------- 1 | import rospy 2 | from std_msgs.msg import Int32,Bool,Float32,String,Int32MultiArray,Float32MultiArray 3 | from numpy.random import normal 4 | from datetime import datetime 5 | from terrabot_utils import clock_to_seconds, clock_time, time_since_midnight 6 | from terrabot_utils import Agenda 7 | from topic_def import * 8 | 9 | # If string is a number, return that number, o/w return the string 10 | def floatify (f, name): 11 | if (f.find('prop') > 0): 12 | return ('prop', float(f.strip('()').split(' ')[1])) 13 | else: 14 | try: 15 | return types[name](float(f)) 16 | except ValueError: 17 | return f 18 | 19 | types = { 20 | 21 | 'led' : int, 22 | 'wpump' : bool, 23 | 'fan' : bool, 24 | 'freq' : float, 25 | 26 | #type for each sensor value 27 | 'smoist' : int, 28 | 'cur' : float, 29 | 'light' : int, 30 | 'level' : float, 31 | 'temp' : int, 32 | 'humid' : int, 33 | } 34 | 35 | std_dev = { 'led' : 0, 36 | 'wpump' : 0, 37 | 'fan' : 0, 38 | 'smoist' : 10, 39 | 'cur' : 1, 40 | 'light' : 5, 41 | 'level' : 2, 42 | 'temp' : 1, 43 | 'humid' : 2, 44 | } 45 | 46 | proportionality = { 'led' : 1.0, 47 | 'wpump' : 1.0, 48 | 'fan' : 1.0, 49 | 'smoist' : 1.0, 50 | 'cur' : 1.0, 51 | 'light' : 1.0, 52 | 'level' : 1.0, 53 | 'temp' : 1.0, 54 | 'humid' : 1.0, 55 | } 56 | 57 | name_translations = { 'led' : 'led', 58 | 'wpump' : 'wpump', 59 | 'fan' : 'fan', 60 | 'smoist' : 'smoist', 61 | 'current' : 'cur', 62 | 'light' : 'light', 63 | 'wlevel' : 'level', 64 | 'temperature' : 'temp', 65 | 'humidity' : 'humid'} 66 | 67 | ###interference functions### 68 | 69 | def identity(name, x): 70 | return x 71 | 72 | def off(name, x): 73 | return types[name](0) 74 | 75 | def on(name, x): 76 | return types[name](1) 77 | 78 | def noise(name, x): 79 | return types[name](normal(x, std_dev[name])) 80 | 81 | def proportional(name, x): 82 | return types[name](x * proportionality[name]) 83 | 84 | states_funcs = { 85 | 'normal' : identity, 86 | 'noise' : noise, 87 | 'off' : off, 88 | 'on' : on, 89 | 'prop' : proportional 90 | } 91 | 92 | def get_interf_funcs(value): 93 | if (type(value) is str): 94 | return states_funcs[value] 95 | elif (type(value) is tuple and value[0] == 'prop'): 96 | return lambda name, x: types[name_translations.get(name)](x*value[1]) 97 | else: 98 | return lambda name, x: value 99 | 100 | class Interference(Agenda): 101 | interf_funcs = {} 102 | def __init__(self, filename, time0): 103 | for n in sensor_names: 104 | self.interf_funcs[n] = (identity if n == 'level' else 105 | [identity, identity]) 106 | for n in actuator_names: 107 | self.interf_funcs[n] = identity 108 | 109 | if (not filename): return # Just use the defaults/identities 110 | self.time0 = time0 111 | last_time = time0 112 | with open(filename) as f: 113 | for line in f.readlines(): 114 | l = line.split('#')[0].strip(' \n') 115 | if (l.find('AT') == 0): 116 | dtime = datetime.strptime(l, "AT %d-%H:%M:%S") 117 | time = time0 + clock_to_seconds(dtime) 118 | if (time < last_time): 119 | print("Time must run forward: %s" %l) 120 | quit() 121 | last_time = time 122 | interfs = [] 123 | self.add_to_schedule([time, interfs]) 124 | elif (len(l) > 0): 125 | interf = l.split("=") 126 | if (len(interf) != 2): 127 | print("Illegal syntax: %s" %l); quit() 128 | 129 | interf_name = interf[0].strip() 130 | topic_name = name_translations.get(interf_name) 131 | if (not topic_name): 132 | print("%s not a legal interference sensor name" 133 | %interf_name) 134 | quit() 135 | interf_val = interf[1] 136 | if (interf_val.find('[') >= 0): 137 | interf_val = [floatify(iv.strip(' []'), topic_name) 138 | for iv in interf_val.split(',')] 139 | else: 140 | interf_val = floatify(interf_val.strip(), topic_name) 141 | interfs.append([topic_name, interf_val]) 142 | 143 | def update(self, time): 144 | if (not self.finished() and (time >= self.schedule[self.index][0])): 145 | print("Updating interferences at %s" %clock_time(time)) 146 | for ifs in self.schedule[self.index][1]: 147 | if (type(ifs[1]) == list): 148 | funcs = [get_interf_funcs(i) for i in ifs[1]] 149 | else: 150 | funcs = get_interf_funcs(ifs[1]) 151 | self.interf_funcs[ifs[0]] = funcs 152 | self.index += 1 153 | return True 154 | else: return False 155 | 156 | # Get the interference functions 157 | def edit(self, name, value): 158 | if (type(value) is list or type(value) is tuple): 159 | return (self.interf_funcs[name][0](name, value[0]), 160 | self.interf_funcs[name][1](name, value[1])) 161 | else: 162 | return self.interf_funcs[name](name, value) 163 | 164 | def display(self): 165 | for interf in self.schedule: 166 | print("AT %s" %clock_time(interf[0])) 167 | for iv in interf[1]: 168 | print(" %s = %s" %(iv[0], iv[1])) 169 | 170 | if __name__ == '__main__': 171 | def p(n,v,t): 172 | print("%s: %s" %(n, interference.edit(n,v))) 173 | 174 | import sys, time 175 | if (len(sys.argv) == 2): 176 | now = time.time() 177 | time0 = now - time_since_midnight(now) 178 | interference = Interference(sys.argv[1], time0) 179 | interference.display() 180 | #""" 181 | # Testing how interference works 182 | t = interference.time0; sensor_values = {} 183 | while not interference.finished(): 184 | if (interference.update(t)): 185 | p('humid', [60, 60], t) 186 | p('temp', [30, 30], t) 187 | p('smoist', [450, 450], t) 188 | p('light', [350, 350], t) 189 | p('level', 125.3, t) 190 | p('cur', [52.0, 1000], t) 191 | p('fan', True, t) 192 | p('wpump', True, t) 193 | p('led', 200, t) 194 | t += 1 195 | #""" 196 | else: 197 | print("Need to provide interference file to parse") 198 | -------------------------------------------------------------------------------- /agents/interactive_agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rospy, sys, select, os 4 | from std_msgs.msg import Float32, Int32, Int32MultiArray, Float32MultiArray, Bool, String 5 | import argparse 6 | import limits 7 | from datetime import datetime 8 | sys.path.insert(0, os.getcwd()[:os.getcwd().find('TerraBot')]+'TerraBot/lib') 9 | from terrabot_utils import clock_time 10 | from freqmsg import tomsg 11 | from topic_def import sensor_types, actuator_types 12 | 13 | class Sensors: 14 | time = 0 15 | light_level = 0 16 | moisture = 0 17 | humidity = 0 18 | temperature = 0 19 | weight = 0 20 | water_level = 0 21 | energy = 0 22 | light_level_raw = [0,0] 23 | moisture_raw = [0,0] 24 | humidity_raw = [0,0] 25 | temperature_raw = [0,0] 26 | weight_raw = [0, 0] 27 | 28 | parser = argparse.ArgumentParser(description = "Interactive Agent") 29 | parser.add_argument('-l', '--log', action = 'store_true', 30 | help="print sensor values") 31 | parser.add_argument('-s', '--sim', action = 'store_true', help="use simulator") 32 | args = parser.parse_args() 33 | 34 | sensorsG = Sensors() 35 | is_logging = args.log 36 | use_simulator = args.sim 37 | 38 | def init_sensors(): 39 | global sensorsG 40 | sensorsG.time = rospy.get_time() 41 | 42 | ### ROS-related stuff 43 | ### Set up publishers, subscribers, and message handlers 44 | 45 | def init_ros (): 46 | global led_pub, wpump_pub, fan_pub, camera_pub, speedup_pub, freq_pub, sensorsG 47 | 48 | if use_simulator: rospy.set_param("use_sim_time", True) 49 | rospy.init_node("interactive_agent", anonymous = True) 50 | 51 | led_pub = rospy.Publisher("led_input", actuator_types['led'], 52 | latch = True, queue_size = 1) 53 | wpump_pub = rospy.Publisher("wpump_input", actuator_types['wpump'], 54 | latch = True, queue_size = 1) 55 | fan_pub = rospy.Publisher("fan_input", actuator_types['fan'], 56 | latch = True, queue_size = 1) 57 | 58 | camera_pub = rospy.Publisher("camera", actuator_types['camera'], 59 | latch = True, queue_size = 1) 60 | speedup_pub = rospy.Publisher("speedup", Int32, latch = True, queue_size = 1) 61 | freq_pub = rospy.Publisher("freq_input", actuator_types['freq'], 62 | latch=True, queue_size=1) 63 | 64 | rospy.Subscriber("smoist_output", sensor_types['smoist'], 65 | moisture_reaction, sensorsG) 66 | rospy.Subscriber("light_output", sensor_types['light'], 67 | light_reaction, sensorsG) 68 | rospy.Subscriber("level_output", sensor_types['level'], 69 | level_reaction, sensorsG) 70 | rospy.Subscriber("temp_output", sensor_types['temp'], 71 | temp_reaction, sensorsG) 72 | rospy.Subscriber("humid_output", sensor_types['humid'], 73 | humid_reaction, sensorsG) 74 | rospy.Subscriber("weight_output", sensor_types['weight'], 75 | weight_reaction, sensorsG) 76 | 77 | def moisture_reaction(data, sensorsG): 78 | sensorsG.moisture = (data.data[0] + data.data[1])/2.0 79 | sensorsG.moisture_raw = data.data 80 | if is_logging: print(" Moisture: %d %d" %(data.data[0], data.data[1])) 81 | 82 | def humid_reaction(data, sensorsG): 83 | sensorsG.humidity = (data.data[0] + data.data[1])/2.0 84 | sensorsG.humidity_raw = data.data 85 | if is_logging: print(" Humidity: %d %d" %(data.data[0], data.data[1])) 86 | 87 | def weight_reaction(data, sensorsG): 88 | # Each weight sensor holds half the weight of the pan 89 | sensorsG.weight = (data.data[0] + data.data[1]) 90 | sensorsG.weight_raw = data.data 91 | if is_logging: print(" Weight: %d %d" %(data.data[0], data.data[1])) 92 | 93 | def temp_reaction(data, sensorsG): 94 | sensorsG.temperature = (data.data[0] + data.data[1])/2.0 95 | sensorsG.temperature_raw = data.data 96 | if is_logging: print(" Temperature: %d %d" %(data.data[0], data.data[1])) 97 | 98 | def light_reaction(data, sensorsG): 99 | sensorsG.light_level = (data.data[0] + data.data[1])/2.0 100 | sensorsG.light_level_raw = data.data 101 | if is_logging: print(" Lights: %d %d" %(data.data[0], data.data[1])) 102 | 103 | def level_reaction(data, sensorsG): 104 | sensorsG.water_level = data.data 105 | if is_logging: print(" Level: %.2f" %data.data) 106 | 107 | def cam_reaction(data): 108 | print ("picture taken\t" + data.data) 109 | 110 | def handle_input(input): 111 | if input[0] == 'f': 112 | print("Turning fans %s" %("on" if (input.find("on") > 0) else "off")) 113 | fan_pub.publish(input.find("on") > 0) 114 | elif input[0] == 'p': 115 | print("Turning pump %s" %("on" if (input.find("on") > 0) else "off")) 116 | wpump_pub.publish(input.find("on") > 0) 117 | elif input[0] == 'l': 118 | level = (0 if (input.find("off") > 0) else 119 | 255 if (input.find("on") > 0) else int(input[1:])) 120 | print("Adjusting light level to %d" %level) 121 | led_pub.publish(level) 122 | elif input[0] == 'c': 123 | location = input[2:-1].strip() 124 | if (len(location) == 0): raise Exception("Need to specify file") 125 | print("Taking a picture, storing in %s" %location) 126 | camera_pub.publish(location) 127 | elif input[0] == 'r': 128 | sensor, freq = input[2:-1].split(" ") 129 | msg = tomsg(sensor, float(freq)) 130 | if msg is not None: 131 | print("Updating %s to frequency %s (every %.1f seconds)" 132 | %(sensor, freq, 1/float(freq))) 133 | freq_pub.publish(msg) 134 | elif input[0] == 'e': 135 | sensor, period = input[2:-1].split(" ") 136 | msg = tomsg(sensor, 1/float(period)) 137 | if msg is not None: 138 | print("Updating %s to period of %s seconds (frequency of %.1f)" 139 | %(sensor, period, 1/float(period))) 140 | freq_pub.publish(msg) 141 | elif input[0] == 's': 142 | speedup_pub.publish(int(input[1:])) 143 | elif input[0] == 'v': 144 | print("Sensor values at %s" % clock_time(sensorsG.time)) 145 | print(" Light level: %.1f (%.1f, %.1f)" 146 | %(sensorsG.light_level, sensorsG.light_level_raw[0], 147 | sensorsG.light_level_raw[1])) 148 | print(" Temperature: %.1f (%.1f, %.1f)" 149 | %(sensorsG.temperature, sensorsG.temperature_raw[0], 150 | sensorsG.temperature_raw[1])) 151 | print(" Humidity: %.1f (%.1f, %.1f)" 152 | %(sensorsG.humidity, sensorsG.humidity_raw[0], 153 | sensorsG.humidity_raw[1])) 154 | print(" Soil moisture: %.1f (%.1f, %.1f)" 155 | %(sensorsG.moisture, sensorsG.moisture_raw[0], 156 | sensorsG.moisture_raw[1])) 157 | print(" Weight: %.1f (%.1f, %.1f)" 158 | %(sensorsG.weight, sensorsG.weight_raw[0], 159 | sensorsG.weight_raw[1])) 160 | print(" Reservoir level: %.1f" %sensorsG.water_level) 161 | else: 162 | print("Usage: q (quit)\n\tf [on|off] (fan on/off)\n\tp [on|off] (pump on/off)\n\tl [|on|off] (led set to level ('on'=255; 'off'=0)\n\tr [smoist|light|level|temp|humid][weight] [] (update sensor to frequency)\n\te [smoist|cur|light|level|temp|humid][weight] [] (update sensor to every seconds)\n\tc (take a picture, store in 'file')\n\ts [] (change current speedup)\n\tv (print sensor values)") 163 | 164 | init_ros() 165 | init_sensors() 166 | rospy.sleep(2) # Give a chance for the initial sensor values to be read 167 | while rospy.get_time() == 0: rospy.sleep(0.1) # Wait for clock to start up correctly 168 | print("Connected and ready for interaction") 169 | 170 | while not rospy.core.is_shutdown(): 171 | sensorsG.time = rospy.get_time() 172 | 173 | ### Check for input 174 | if sys.stdin in select.select([sys.stdin],[],[],0)[0]: 175 | input = sys.stdin.readline() 176 | if input[0] == 'q': 177 | quit() 178 | else: 179 | try: 180 | handle_input(input) 181 | except Exception as inst: 182 | print("ERROR: action could not be executed: %s" %str(inst.args)) 183 | rospy.sleep(1) 184 | -------------------------------------------------------------------------------- /lib/farduino.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #David Buffkin 4 | 5 | ### Import used files 6 | import rospy 7 | from std_msgs.msg import Int32,Bool,Float32,String,Int32MultiArray,Float32MultiArray 8 | from topic_def import * 9 | from rosgraph_msgs.msg import Clock 10 | import time 11 | import argparse 12 | from baseline import Baseline 13 | from os.path import abspath 14 | 15 | import environment as env 16 | from render import Terrarium 17 | from freqmsg import frommsg 18 | from direct.stdpy import threading 19 | import sys 20 | from datetime import datetime 21 | from terrabot_utils import clock_time 22 | 23 | ### Parse arguments 24 | 25 | parser = argparse.ArgumentParser(description='simulation parser for Autonomous Systems') 26 | parser.add_argument('--baseline', type = str, default = None, nargs = "?") 27 | parser.add_argument('--speedup', type = float, default = 1, nargs = "?") 28 | parser.add_argument('--graphics', default = False, action = 'store_true') 29 | parser.add_argument('-l', '--log', action = 'store_true') 30 | args = parser.parse_args() 31 | 32 | 33 | 34 | ### Important Variables 35 | 36 | default_speedup = args.speedup 37 | publishers = {} 38 | subscribers = {} 39 | clock_pub = None 40 | 41 | 42 | 43 | ### Handle Sub/Pub 44 | 45 | ## Actuator Callbacks 46 | 47 | def freq_cb(data): 48 | #Parse and update sensor frequency 49 | name, freq = frommsg(data.data) 50 | if freq == 0: 51 | sensor_timing[name][1] = 99999999 52 | else: 53 | sensor_timing[name][1] = 1 / freq 54 | 55 | 56 | def speedup_cb(data): 57 | global speedup, default_speedup 58 | default_speedup = data.data 59 | speedup = default_speedup 60 | 61 | actuator_cbs = { 'led' : (lambda data : env.params.update({'led' : data.data})), 62 | 'wpump' : (lambda data : env.params.update({'wpump' : data.data})), 63 | 'fan' : (lambda data : env.params.update({'fan' : data.data})), 64 | 'freq' : freq_cb} 65 | 66 | ## Actuator Setup 67 | 68 | def generate_subscribers(): 69 | global subscribers 70 | rospy.Subscriber('speedup', Int32, speedup_cb) 71 | for name in actuator_names: 72 | if name != 'camera': 73 | subscribers[name] = rospy.Subscriber(name + '_raw', 74 | actuator_types[name], 75 | actuator_cbs[name]) 76 | 77 | ## Sensor Setup 78 | 79 | def generate_publishers(): 80 | global publishers, clock_pub 81 | clock_pub = rospy.Publisher("clock", Clock, latch = True, queue_size = 1) 82 | for name in sensor_names: 83 | publishers[name] = rospy.Publisher(name + '_raw', sensor_types[name], 84 | latch = True, queue_size = 100) 85 | 86 | 87 | ### SENSORS ### 88 | 89 | # The first element is the current time till resensing, and the second is the chosen frequency 90 | sensor_timing = { 'smoist' : [0.0, 1.0], 91 | 'cur' : [0.0, 1.0], 92 | 'light' : [0.0, 1.0], 93 | 'level' : [0.0, 1.0], 94 | 'temp' : [0.0, 1.0], 95 | 'humid' : [0.0, 1.0], 96 | 'weight' : [0.0, 1.0]} 97 | 98 | ## Sensor functions 99 | #These functions sense from the environment and publish to their topic 100 | 101 | def sense_smoist(): 102 | #should be ~2 * soil water content? 103 | s_array = Int32MultiArray() 104 | s_array.data = [int(env.params['soilwater'] * 2)] * 2 105 | publishers['smoist'].publish(s_array) 106 | 107 | def sense_cur(): 108 | c_array = Float32MultiArray() 109 | c_array.data = [env.get_cur(), env.params['energy']] 110 | publishers['cur'].publish(c_array) 111 | 112 | def sense_light(): 113 | l_array = Int32MultiArray() 114 | l_array.data = [int(env.light_level(env.tank_width / 2))] * 2 115 | publishers['light'].publish(l_array) 116 | 117 | def sense_level(): 118 | publishers['level'].publish(env.params['volume'] / env.volume_rate) 119 | 120 | def sense_temp(): 121 | t_array = Int32MultiArray() 122 | t_array.data = [int(env.params['temperature'])] * 2 123 | publishers['temp'].publish(t_array) 124 | 125 | def sense_humid(): 126 | h_array = Int32MultiArray() 127 | h_array.data = [round(env.params['humidity'])] * 2 128 | publishers['humid'].publish(h_array) 129 | 130 | def sense_weight(): 131 | w_array = Float32MultiArray() 132 | # The sum of the two sensors is the weight; Make it slightly uneven 133 | weight = env.get_weight()/2 134 | w_array.data = [weight*0.9, weight*1.1] 135 | publishers['weight'].publish(w_array) 136 | 137 | def sensor_forward_time(duration): 138 | for name in sensor_names: 139 | sensor_timing[name][0] += duration #Update cooldown 140 | if sensor_timing[name][0] >= sensor_timing[name][1] : #Check to see if cooldown is over 141 | sensor_timing[name][0] = 0 #Reset cooldown 142 | eval("sense_%s()" % name) #Call the sensor function 143 | 144 | 145 | 146 | ### RUNTIME ### 147 | 148 | ## handle env init stuff 149 | 150 | # extract baseline values 151 | bl = None 152 | if args.baseline: 153 | try: 154 | bl = Baseline(abspath(args.baseline)) 155 | except: 156 | print('baseline file %s not found or parse error' %args.baseline) 157 | exit() 158 | 159 | env.init(bl) 160 | 161 | max_speedup_pump = 1 162 | max_speedup_fan = 100 163 | 164 | ## handle ROS init stuff 165 | 166 | rospy.set_param("use_sim_time", True) 167 | rospy.init_node('Simulator', anonymous=True) 168 | 169 | generate_publishers() 170 | generate_subscribers() 171 | 172 | 173 | ### Sim loop (Put here for threading purposes) 174 | doloop = True 175 | 176 | t0 = int(datetime(2000, 1, 1).strftime('%s')) # Initialize to 01-00:00:00, for display purposes 177 | 178 | def sim_loop(): 179 | global doloop 180 | tick_time = .25 # This is how long between runs of the below loop in seconds 181 | speedup = default_speedup 182 | pump_last_on = False 183 | pump_last_off_time = 0 184 | now = t0 185 | if bl is not None: 186 | now += bl.params['start'] 187 | clock_pub.publish(rospy.Time.from_sec(now)) #Publish initial time 188 | 189 | time.sleep(1) #give a sec 190 | 191 | 192 | while not rospy.core.is_shutdown() and doloop: 193 | 194 | time.sleep(tick_time) # Wait for next tick 195 | 196 | speedup = min(default_speedup, #speedup should be maxed if pumping/fanning 197 | (max_speedup_pump if env.params['wpump'] else default_speedup), 198 | (max_speedup_fan if env.params['fan'] else default_speedup)) 199 | 200 | now = rospy.get_time() 201 | if (pump_last_on and not env.params['wpump']): 202 | pump_last_on = False 203 | pump_last_off_time = now 204 | elif (env.params['wpump']): 205 | pump_last_on = True 206 | if (now - pump_last_off_time < 10): speedup = 1 207 | clock_pub.publish(rospy.Time.from_sec(now + (tick_time * speedup))) 208 | 209 | #DO STUFF 210 | #move env forward 211 | duration = env.forward_time(tick_time * speedup) 212 | #move sensor time forward 213 | sensor_forward_time(duration) 214 | 215 | #rerender to the viewing window 216 | renderer.update_env_params(env.params, speedup, env.light_average(), 217 | env.get_weight()) 218 | #Stop panda window 219 | if args.graphics: 220 | renderer.userExit() 221 | 222 | 223 | #Init graphics 224 | 225 | droop = 0 226 | lankiness = 0 227 | plant_health = 1 228 | age = 0 229 | if bl is not None: 230 | age = bl.params['start'] 231 | droop = bl.params['leaf_droop'] 232 | lankiness = bl.params['lankiness'] 233 | plant_health = bl.params['plant_health'] 234 | 235 | renderer = Terrarium(args.graphics, t0, age, droop, lankiness, plant_health) 236 | env.params['time'] = age 237 | renderer.update_env_params(env.params, default_speedup, env.light_average(), 238 | env.get_weight()) 239 | 240 | def camera_cb(data): 241 | global renderer 242 | renderer.takeAndStorePic(data.data) 243 | 244 | #Steup camera subscriber 245 | subscribers['camera'] = rospy.Subscriber('camera_raw', actuator_types['camera'], 246 | camera_cb) 247 | 248 | #Start sim loop THEN panda 249 | 250 | import signal 251 | def handler(signum, frame): 252 | global thread, doloop 253 | doloop = False 254 | thread.join() 255 | sys.exit() 256 | 257 | signal.signal(signal.SIGTERM, handler) 258 | 259 | thread = threading.Thread(target=sim_loop) 260 | thread.start() 261 | 262 | renderer.run() 263 | 264 | # :) 265 | 266 | -------------------------------------------------------------------------------- /agents/time_series.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rospy, sys, select, os, time, argparse 4 | from std_msgs.msg import Float32, Int32, Int32MultiArray, Float32MultiArray, Bool, String 5 | import matplotlib.pyplot as plt 6 | from matplotlib.ticker import FormatStrFormatter 7 | import limits 8 | from log_data import write_log_data_line, process_log_data_line 9 | 10 | class Subplots: 11 | name = None 12 | ax = None 13 | fig = None 14 | color = 'r' 15 | def __init__(self, the_name, the_axis, the_color, the_width, 16 | the_force_update): 17 | self.name = the_name 18 | self.current = -100 19 | self.x = [] 20 | self.y = [] 21 | self.ax = the_axis 22 | self.color = the_color 23 | self.plot_width = the_width 24 | self.last_update = 0 # Time of last plotted value 25 | self.current = (0 if the_force_update else None) 26 | self.frequency = the_width/150 # in hours 27 | 28 | def update(self, time): # in hours since start 29 | if (self.current == None): return False 30 | if ((time - self.last_update) >= self.frequency): 31 | self.update1(time) 32 | return True 33 | elif (len(self.y) == 0): 34 | self.update1(time) 35 | return True 36 | else: 37 | # Plot if more than 10% change in value, regardless of time 38 | last_value = self.y[len(self.y)-1] 39 | if (abs(self.current - last_value) > 0.1*max(self.current,last_value)): 40 | self.update1(time) 41 | return True 42 | return False 43 | 44 | def update1(self, time): # time in hours since start 45 | min = max(0,float(time-self.plot_width)) 46 | self.ax.set_xlim(left=min, right=min+(1.05*self.plot_width)) 47 | self.x.append(time) 48 | self.y.append(self.current) 49 | self.ax.plot(self.x, self.y, self.color) 50 | self.last_update = time 51 | 52 | subplotsG = {} 53 | nrowsG = 0 54 | ncolsG = 0 55 | 56 | def init_ros (use_simulator): 57 | global led_pub, wpump_pub, fan_pub, camera_pub, subplotsG 58 | 59 | if use_simulator: rospy.set_param("use_sim_time", True) 60 | rospy.init_node("time_series_grapher", anonymous = True) 61 | 62 | rospy.Subscriber("smoist_output", Int32MultiArray, 63 | update_sensor_multi_data, subplotsG['Soil Moisture']) 64 | rospy.Subscriber("light_output", Int32MultiArray, 65 | update_sensor_multi_data, subplotsG['Light Level']) 66 | rospy.Subscriber("level_output", Float32, 67 | update_sensor_data, subplotsG['Water Level']) 68 | rospy.Subscriber("temp_output", Int32MultiArray, 69 | update_sensor_multi_data, subplotsG['Temperature']) 70 | rospy.Subscriber("humid_output", Int32MultiArray, 71 | update_sensor_multi_data, subplotsG['Humidity']) 72 | rospy.Subscriber("weight_output", Float32MultiArray, 73 | update_sensor_multi_data, subplotsG['Weight']) 74 | rospy.Subscriber("led_input", Int32, 75 | update_actuator_data, subplotsG['LEDs']) 76 | rospy.Subscriber("fan_input", Bool, update_actuator_data, subplotsG['Fan']) 77 | rospy.Subscriber("wpump_input", Bool, 78 | update_actuator_data, subplotsG['Pump']) 79 | 80 | def update_sensor_multi_data(data, subplot): 81 | subplot.current = (data.data[0] + data.data[1])/2 82 | write_log_data_line(log_file, subplot.name, data.data, rospy.get_time()) 83 | 84 | def update_sensor_data(data, subplot): 85 | subplot.current = data.data 86 | write_log_data_line(log_file, subplot.name, data.data, rospy.get_time()) 87 | 88 | def update_actuator_data(data, subplot): 89 | subplot.current = data.data 90 | write_log_data_line(log_file, subplot.name, data.data, rospy.get_time()) 91 | 92 | def add_time_series(fig, name, limits, force_update, color, pos, plot_width): 93 | global subplotsG, nrowsG, ncolsG 94 | ax = fig.add_subplot(nrowsG, ncolsG, pos) 95 | plt.title(name) 96 | subplotsG[name] = Subplots(name, ax, color, plot_width, force_update) 97 | ax.set_xlim(0, plot_width) # hours 98 | ax.yaxis.set_major_formatter(FormatStrFormatter('%d')) 99 | offset = limits[1]*0.1 # extend limits slightly 100 | ax.set_ylim(limits[0]-offset, limits[1]+offset) 101 | if (limits[0] == 0 and limits[1] == 1): # binary 102 | ax.set_yticks([0, 1]) #ax.set_yticks(['off','on']) 103 | 104 | def init_plotting(plots, plot_width): 105 | global nrowsG, ncolsG 106 | fig = plt.figure() 107 | nrowsG, ncolsG = (5, 2) 108 | plt.subplots_adjust(hspace=0.7) 109 | plt.subplots_adjust(wspace=0.2) 110 | plt.ion() 111 | plt.rc('font', size=6) 112 | plt.rc('axes', titlesize=8) 113 | 114 | for plot in plots: 115 | add_time_series(fig, plot[0], plot[1], plot[2], plot[3], plot[4], 116 | plot_width) 117 | 118 | return fig 119 | 120 | def print_sensor_values(): 121 | print("Light Level: %.2f" %subplotsG['Light Level'].current) 122 | print("Humidity: %.2f" %subplotsG['Humidity'].current) 123 | print("Temperature: %.2f" %subplotsG['Temperature'].current) 124 | print("Soil Moisture: %.2f" %subplotsG['Soil Moisture'].current) 125 | print("Weight: %.2f" %subplotsG['Weight'].current) 126 | print("Water Level: %.2f" %subplotsG['Water Level'].current) 127 | print("LEDs: %d" %subplotsG['LEDs'].current) 128 | print("Fan: %s" %("on" if subplotsG['Fan'].current else "off")) 129 | print("Pump: %s" %("on" if subplotsG['Pump'].current else "off")) 130 | 131 | parser = argparse.ArgumentParser(description = "Time Series Plotter") 132 | parser.add_argument('-w', '--width', default = 24, 133 | help="width of the plot, in hours") 134 | parser.add_argument('-l', '--log', help="log the sensor data to file") 135 | parser.add_argument('-r', '--replay', help="replay the sensor data from file") 136 | parser.add_argument('-s', '--sim', action = 'store_true', help="use simulator") 137 | parser.add_argument('-p', '--speedup', default = 1, help = 'replay playback speedup') 138 | args = parser.parse_args() 139 | plot_widthG = float(args.width) # in hours 140 | 141 | replay_file = (None if not args.replay else open(args.replay, 'r')) 142 | if (args.log): 143 | if (replay_file): print("WARNING: Cannot log while replaying") 144 | else: log_file = open(args.log, 'w') 145 | else: log_file = None 146 | 147 | plotsG = [('Light Level', limits.scale['light_level'], False, 'g', 1), 148 | ('Humidity', limits.scale['humidity'], False, 'g', 3), 149 | ('Temperature', limits.scale['temperature'], False, 'g', 5), 150 | ('Soil Moisture', limits.scale['moisture'], False, 'g', 7), 151 | ('Weight', limits.scale['weight'], False, 'g', 9), 152 | ('Water Level', limits.scale['water_level'], False, 'g', 10), 153 | ('LEDs', [0, 255], True, 'b', 2), 154 | ('Fan', [0, 1], True, 'b', 4), 155 | ('Pump', [0, 1], True, 'b', 6)] 156 | fig = init_plotting(plotsG, plot_widthG) 157 | 158 | def update_plots (hours_since_start): 159 | global plotsG, subplotsG, fig 160 | 161 | updated = False 162 | for plot in plotsG: 163 | if (subplotsG[plot[0]].update(hours_since_start)): updated = True 164 | if (updated): 165 | fig.canvas.draw() 166 | plt.pause(0.001) 167 | plt.show() 168 | return updated 169 | 170 | def handle_stdin (): 171 | if sys.stdin in select.select([sys.stdin],[],[],0)[0]: 172 | input = sys.stdin.readline() 173 | if input[0] == 'q': 174 | plt.close() 175 | quit() 176 | elif input[0] == 'v': 177 | print_sensor_values() 178 | else: 179 | print("Usage: q (quit)\n\tv (sensor values)") 180 | 181 | if (replay_file): 182 | start_time = None 183 | for line in replay_file: 184 | cur_time, name, data = process_log_data_line(line) 185 | if (isinstance(data, tuple)): data = (data[0] + data[1])/2 186 | subplotsG[name].current = data 187 | if (not start_time): start_time = cur_time 188 | hours_since_start = (cur_time - start_time)/3600.0 189 | if (update_plots(hours_since_start)): time.sleep(1 / float(args.speedup)) 190 | handle_stdin() 191 | 192 | print("Done replaying") 193 | time.sleep(5) 194 | replay_file.close() 195 | 196 | else: 197 | init_ros(args.sim) 198 | rospy.sleep(2) # Do this even if running simulator to handle messages 199 | start_time = rospy.get_time() 200 | last_update = start_time 201 | 202 | # Update graph every so often, depending on plot width 203 | while not rospy.core.is_shutdown(): 204 | hours_since_start = (rospy.get_time() - start_time)/3600.0 205 | update_plots(hours_since_start) 206 | handle_stdin() 207 | rospy.sleep(0.1) 208 | -------------------------------------------------------------------------------- /lib/ArduinoCode/ArduinoCode.ino: -------------------------------------------------------------------------------- 1 | //#define USE_DHT20 2 | //#define USE_TCA 3 | #define USE_BOTH 4 | 5 | /* 6 | * Automated Systems TerraBot Arduino File 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #ifdef USE_DHT20 19 | #include 20 | #include 21 | #ifdef USE_TCA 22 | #include 23 | #endif 24 | #else 25 | #include 26 | //#include 27 | #endif 28 | #include 29 | 30 | // Internal Values 31 | #if 1 32 | int temperature1 = 0; 33 | int humidity1 = 0; 34 | int temperature2 = 0; 35 | int humidity2 = 0; 36 | #else 37 | byte temperature1 = 0; 38 | byte humidity1 = 0; 39 | byte temperature2 = 0; 40 | byte humidity2 = 0; 41 | #endif 42 | int lvl = 0; 43 | 44 | ros::NodeHandle nh; 45 | 46 | // Sensor Pins 47 | int light_pin1 = A4; 48 | int smoist_pin1 = A5; 49 | int DHT_pin1 = A6; 50 | 51 | int light_pin2 = A8; 52 | int smoist_pin2 = A9; 53 | int DHT_pin2 = A10; 54 | 55 | int trig_pin = A2; 56 | int echo_pin = A1; 57 | #ifdef USE_CURRENT 58 | int cur_pin = A0; 59 | #endif 60 | 61 | #ifdef USE_TCA 62 | TCA9548 tca(0x70); 63 | #endif 64 | 65 | // Set up DHT sensor 66 | #ifdef USE_DHT20 67 | DHT20 dht1; 68 | #ifdef USE_BOTH 69 | DHT20 dht2; 70 | #endif 71 | #else 72 | DHTNEW dht1(DHT_pin1); 73 | #ifdef USE_BOTH 74 | DHTNEW dht2(DHT_pin2); 75 | #endif 76 | #endif 77 | 78 | // Actuator pins 79 | //int led_pin = 11; 80 | int led_pin = 10; 81 | int wpump_pin = 12; 82 | int fan_pin = 13; 83 | 84 | int weight_sck_pin1 = 2; 85 | int weight_dout_pin1 = 3; 86 | int weight_sck_pin2 = 4; 87 | int weight_dout_pin2 = 5; 88 | 89 | HX711 weight1, weight2; 90 | // All of these should be recalibrated on a per-sensor basis 91 | float weight_scale1 = 1; 92 | float weight_offset1 = 0; 93 | float weight_scale2 = 1; 94 | float weight_offset2 = 0; 95 | struct Timing { 96 | unsigned long next = 0; 97 | unsigned long period = 1000; 98 | }; 99 | 100 | // Time dependant variables 101 | long unsigned int last_dht = 0; 102 | unsigned long time_now = 0; 103 | Timing light_timing, temp_timing, humidity_timing; 104 | Timing wlevel_timing, smoist_timing, current_timing; 105 | Timing weight_timing; 106 | 107 | // Used to make light/current data more useable 108 | long light_sum1 = 0; 109 | long light_sum2 = 0; 110 | long light_count = 0; 111 | 112 | #ifdef USE_CURRENT 113 | long cur_sum = 0; 114 | long cur_count = 0; 115 | #endif 116 | 117 | #define SEN_CMP(sensor) (strncmp(cmd_msg.data, sensor, slen) == 0) 118 | 119 | //Frequency Adjustment 120 | void freq_change( const std_msgs::String& cmd_msg){ 121 | const char *sep = strchr(cmd_msg.data, '|'); 122 | int slen = sep-cmd_msg.data; 123 | double freq = atof(sep+1); 124 | unsigned long period = (freq == 0 ? 99999999 : round(1000.0/freq)); 125 | Timing *timingPtr = (SEN_CMP("light") ? &light_timing : 126 | SEN_CMP("temp") ? &temp_timing : 127 | SEN_CMP("humid") ? &humidity_timing : 128 | SEN_CMP("level") ? &wlevel_timing : 129 | SEN_CMP("smoist") ? &smoist_timing : 130 | SEN_CMP("weight") ? &weight_timing : 131 | SEN_CMP("cur") ? ¤t_timing : NULL); 132 | if (timingPtr == NULL) printf("Oops\n"); 133 | else { 134 | timingPtr->period = period; 135 | timingPtr->next = time_now + period; 136 | } 137 | } 138 | 139 | ros::Subscriber freq_sub("freq_raw", &freq_change); 140 | 141 | // Functions for Actuators 142 | void led_activate( const std_msgs::Int32& cmd_msg){ 143 | analogWrite(led_pin, cmd_msg.data);//toggle led 144 | } 145 | 146 | void wpump_activate(const std_msgs::Bool& cmd_msg){ 147 | // analogWrite(wpump_pin, cmd_msg.data ? 75 : 0); 148 | analogWrite(wpump_pin, cmd_msg.data ? 90 : 0); 149 | } 150 | 151 | void fan_activate(const std_msgs::Bool& cmd_msg){ 152 | analogWrite(fan_pin, cmd_msg.data ? 255 : 0); 153 | } 154 | 155 | // Actuator subscriptions 156 | ros::Subscriber led_sub("led_raw", &led_activate); 157 | ros::Subscriber wpump_sub("wpump_raw", &wpump_activate); 158 | ros::Subscriber fan_sub("fan_raw", &fan_activate); 159 | 160 | 161 | // Sensor helpers 162 | float get_level() { 163 | float duration; 164 | digitalWrite(trig_pin, LOW); 165 | delayMicroseconds(2); 166 | digitalWrite(trig_pin, HIGH); 167 | delayMicroseconds(10); 168 | digitalWrite(trig_pin, LOW); 169 | duration = float(pulseIn(echo_pin, HIGH, 10000)); 170 | return duration / 5.82; 171 | } 172 | 173 | // Sensor publishings 174 | 175 | std_msgs::Int32MultiArray humid_msg; 176 | ros::Publisher humid_pub("humid_raw", &humid_msg); 177 | 178 | std_msgs::Int32MultiArray temp_msg; 179 | ros::Publisher temp_pub("temp_raw", &temp_msg); 180 | 181 | std_msgs::Int32MultiArray light_msg; 182 | ros::Publisher light_pub("light_raw", &light_msg); 183 | 184 | std_msgs::Float32 level_msg; 185 | ros::Publisher level_pub("level_raw", &level_msg); 186 | 187 | #ifdef USE_CURRENT 188 | std_msgs::Float32MultiArray cur_msg; 189 | ros::Publisher cur_pub("cur_raw", &cur_msg); 190 | #endif 191 | 192 | std_msgs::Int32MultiArray smoist_msg; 193 | ros::Publisher smoist_pub("smoist_raw", &smoist_msg); 194 | 195 | std_msgs::Float32MultiArray weight_msg; 196 | ros::Publisher weight_pub("weight_raw", &weight_msg); 197 | 198 | // Code which is run on the Arduino 199 | void setup(){ 200 | pinMode(led_pin, OUTPUT); 201 | pinMode(wpump_pin, OUTPUT); 202 | pinMode(fan_pin, OUTPUT); 203 | pinMode(trig_pin, OUTPUT); 204 | pinMode(echo_pin, INPUT); 205 | 206 | #ifdef USE_TCA 207 | tca.begin(0); 208 | #endif 209 | 210 | //Wire.setClock(5000); 211 | #ifdef USE_DHT20 212 | dht1.begin(); 213 | #ifdef USE_BOTH 214 | dht2.begin(); 215 | #endif 216 | #else 217 | dht1.setType(22); 218 | #ifdef USE_BOTH 219 | dht2.setType(22); 220 | #endif 221 | #endif 222 | 223 | weight1.begin(weight_dout_pin1, weight_sck_pin1); 224 | weight1.set_scale(weight_scale1); 225 | weight1.set_offset(weight_offset1); 226 | weight2.begin(weight_dout_pin2, weight_sck_pin2); 227 | weight2.set_scale(weight_scale2); 228 | weight2.set_offset(weight_offset2); 229 | 230 | nh.initNode(); 231 | nh.subscribe(freq_sub); 232 | 233 | nh.subscribe(led_sub); 234 | nh.subscribe(wpump_sub); 235 | nh.subscribe(fan_sub); 236 | 237 | nh.advertise(temp_pub); 238 | nh.advertise(humid_pub); 239 | nh.advertise(light_pub); 240 | nh.advertise(level_pub); 241 | nh.advertise(smoist_pub); 242 | nh.advertise(weight_pub); 243 | #ifdef USE_CURRENT 244 | nh.advertise(cur_pub); 245 | #endif 246 | } 247 | 248 | float to_amp(int analog) { 249 | return (float(analog) - 512) * .0491; 250 | } 251 | void loop(){ 252 | // Keep track of the amount of light 253 | time_now = millis(); 254 | if (light_timing.next - time_now < 1000) { 255 | light_count++; 256 | light_sum1 += analogRead(light_pin1); 257 | light_sum2 += analogRead(light_pin2); 258 | } 259 | #ifdef USE_CURRENT 260 | // Needed so we can integrate 261 | if (current_timing.next - time_now < 1000) { 262 | cur_count++; 263 | cur_sum += analogRead(cur_pin); 264 | } 265 | #endif 266 | // updates the reading for temp and humidity 267 | if(time_now - last_dht > 2500){ 268 | /* 269 | int status = dht1.read(&temperature1, &humidity1, NULL); 270 | if (status != SimpleDHTErrSuccess) { 271 | temperature1 = status; 272 | humidity1 = 42; 273 | } 274 | */ 275 | #ifdef USE_TCA 276 | tca.selectChannel(1); 277 | #endif 278 | int status = dht1.read(); 279 | #ifdef USE_DHT20 280 | if (status == DHT20_OK) { 281 | #else 282 | if (status == DHTLIB_OK) { 283 | #endif 284 | temperature1 = dht1.getTemperature(); 285 | humidity1 = dht1.getHumidity(); 286 | } 287 | #ifdef USE_BOTH 288 | #ifdef USE_TCA 289 | tca.selectChannel(0); 290 | #endif 291 | status = dht2.read(); 292 | #ifdef USE_DHT20 293 | if (status == DHT20_OK) { 294 | #else 295 | if (status == DHTLIB_OK) { 296 | #endif 297 | temperature2 = dht2.getTemperature(); 298 | humidity2 = dht2.getHumidity(); 299 | } 300 | #endif 301 | last_dht = time_now; 302 | } 303 | 304 | if(time_now >= temp_timing.next){ 305 | temp_timing.next = time_now + temp_timing.period; 306 | 307 | // Get Temperature 308 | temp_msg.data_length = 2; 309 | long int t_array[2]; 310 | t_array[0] = temperature1; 311 | t_array[1] = temperature2; 312 | temp_msg.data = t_array; 313 | temp_pub.publish(&temp_msg); 314 | } 315 | 316 | if(time_now >= humidity_timing.next){ 317 | humidity_timing.next = time_now + humidity_timing.period; 318 | 319 | // Get Humidity 320 | humid_msg.data_length = 2; 321 | long int h_array[2]; 322 | h_array[0] = humidity1; 323 | h_array[1] = humidity2; 324 | humid_msg.data = h_array; 325 | humid_pub.publish(&humid_msg); 326 | } 327 | 328 | if(time_now >= light_timing.next){ 329 | light_timing.next = time_now + light_timing.period; 330 | 331 | // Get light 332 | light_msg.data_length = 2; 333 | long int l_array[2]; 334 | l_array[0] = light_sum1 / light_count; 335 | l_array[1] = light_sum2 / light_count; 336 | light_msg.data = l_array; 337 | light_pub.publish(&light_msg); 338 | 339 | // Reset light values 340 | light_sum1 = 0; 341 | light_sum2 = 0; 342 | light_count = 0; 343 | } 344 | 345 | if(time_now >= wlevel_timing.next){ 346 | wlevel_timing.next = time_now + wlevel_timing.period; 347 | 348 | // Get the level (complicated enough for own function) 349 | level_msg.data = get_level(); 350 | // Returns the distance to the water; we want the height of the water. 351 | // Experimentally, a value of ~180 indicates an empty reservoir 352 | level_msg.data = 180 - level_msg.data; 353 | level_pub.publish(&level_msg); 354 | } 355 | 356 | if(time_now >= smoist_timing.next){ 357 | smoist_timing.next = time_now + smoist_timing.period; 358 | 359 | // Get the soil moisture 360 | smoist_msg.data_length = 2; 361 | long int s_array[2]; 362 | s_array[0] = analogRead(smoist_pin1); 363 | s_array[1] = analogRead(smoist_pin2); 364 | // Invert the reading, so reading increases as moisture increases 365 | s_array[0] = 1023 - s_array[0]; 366 | s_array[1] = 1023 - s_array[1]; 367 | smoist_msg.data = s_array; 368 | smoist_pub.publish(&smoist_msg); 369 | } 370 | 371 | if(time_now >= weight_timing.next){ 372 | weight_timing.next = time_now + weight_timing.period; 373 | 374 | // Get the weight 375 | weight_msg.data_length = 2; 376 | static float w_array[2] = {0,0}; 377 | // The HX711 package sets parameters globally, so rather than 378 | // updating the package, need to set parameters for each sensor 379 | weight1.set_scale(weight_scale1); 380 | weight1.set_offset(weight_offset1); 381 | // Sometimes, the weight sensors go offline for a bit - in that case, 382 | // use the previous value 383 | if (weight1.is_ready()) w_array[0] = weight1.get_units(1); 384 | weight2.set_scale(weight_scale2); 385 | weight2.set_offset(weight_offset2); 386 | if (weight2.is_ready()) w_array[1] = weight2.get_units(1); 387 | weight_msg.data = w_array; 388 | weight_pub.publish(&weight_msg); 389 | } 390 | 391 | #ifdef USE_CURRENT 392 | if(time_now >= current_timing.next){ 393 | current_timing.next = time_now + current_timing.period; 394 | 395 | // Get current 396 | cur_msg.data_length = 2; 397 | float c_array[2]; 398 | c_array[0] = to_amp(analogRead(cur_pin)); 399 | c_array[1] = to_amp(cur_sum / cur_count); 400 | cur_msg.data = c_array; 401 | cur_pub.publish(&cur_msg); 402 | 403 | // Reset current values 404 | cur_sum = 0; 405 | cur_count = 0; 406 | } 407 | #endif 408 | nh.spinOnce(); 409 | } 410 | -------------------------------------------------------------------------------- /lib/environment.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | ### ENVIRONMENT ### 4 | 5 | #Probably a plant class to store a plants' height, color, location, etc. 6 | #{leaf, root health} 7 | #{health -> affect growth function, tilt, color} 8 | #func to forward time. health is 0 smoist, humidity 16 | # smoist <-- plants, temperature, wpump 17 | # smoist --> plants, humidity, tankwater 18 | # humidity <-- plants, smoist, tankwater, fan, temp 19 | # humidity --> plants 20 | # temperature <-- light?, fan 21 | # temperature --> plants, smoist, humidity, tankwater 22 | # light <-- led, time 23 | # light --> plants , temp? 24 | # current <-- led, wpump, fan 25 | # volume <-- wpump 26 | # volume --> wpump 27 | # tankwater <-- smoist 28 | # tankwater --> humidity 29 | # led --> light 30 | # wpump <-- volume 31 | # wpump --> smoist, volume 32 | # fan --> humidity, temperature 33 | 34 | # Natural Constants #TODO tweak these 35 | 36 | max_soilwater = 500 #ml The level at which the soil is fully saturated and will begin to overflow 37 | flow_rate = 3.5 #ml/sec The rate at which the pump will pump water 38 | pipe_capacity = 10 #ml The capacity of the pipe 39 | drip_rate = 1.0 #ml/sec The rate at which water drips from the pipe when the pump is off 40 | uptake_rate = 0.5 #ml/sec The rate at which water is absorbed from the pan to the soil 41 | evap_rate = 1.2 #ml/sec The nominal rate at which water will evaporate 42 | volume_rate = 1000.0 / 45 #ml/mm in the reservoir (used for sensing level) 43 | light_diffuse = .7 #The percentage of sunlight that reaches the other side 44 | max_daylight = 588 #The sunlight right at the window at midday 45 | tank_width = .4 #m the width of the terrarium 46 | led_power = 3.725 #units of light per LED level 47 | room_temp = 22 #degrees C the room temperature out of the greenhouse 48 | room_humidity = 40 # Humidity of air outside greenhouse 49 | fan_cool_rate = .05 / 60 #deg C /min The rate at which temp decreases due to the fan 50 | 51 | led_current = 3.2/255 # 52 | pump_current = .2 # The current needed to support each device when it's on 53 | fan_current = .06 # 54 | 55 | base_weight = 500 # Weight of support panel, dry rockwool and pan, in grams 56 | 57 | #Environment Parameters 58 | params = { 'time' : 0, # This should start at 2000-01-01-00:00:00 59 | 'start' : 0, # This is in seconds, starting at zero 60 | 61 | 'humidity' : 50, # Percent 62 | 'soilwater' : 550/2, 63 | 'airwater' : None, # Calculate from humidity 64 | 'temperature' : room_temp, 65 | 66 | 'volume' : 3000.0, 67 | 'tankwater' : 0.0, 68 | 'pipewater' : 0.0, 69 | 'panwater' : 0.0, 70 | 'energy' : 0, 71 | 72 | 'led' : 0, 73 | 'wpump' : False, 74 | 'fan' : False} 75 | 76 | 77 | # Natural Helper Functions 78 | 79 | seconds_in_day = 3600 * 24 80 | sunrise = 3600 * 7 81 | sunset = 3600 * 19 82 | midday = (sunrise + sunset) / 2 83 | coeff1 = (-4.0 / ((sunrise - sunset)**2)) #This is for saving computation 84 | def day_fraction(time): 85 | #Gives 0 if night, or 0= sunset: return 0 88 | return 1 + coeff1 * ((relative_time - midday)**2) 89 | 90 | coeff2 = (light_diffuse - 1) / tank_width 91 | def light_level(distance): 92 | #Gives light level at a distance from the lit side of the tank TODO change to tanh? or maybe not 93 | sunlight = max_daylight * (1 + distance * coeff2) * day_fraction(params['time']) 94 | ledlight = led_power * params['led'] 95 | return min(1000, sqrt(sunlight**2 + ledlight**2)) #This is done pythagorically, there may be a better way 96 | #I think its ok? for the light to be too high at the extreme 97 | #This is NOT Ok if temp is based on light 98 | 99 | def light_average(): 100 | #A quick estimate of the average light level 101 | samples = 5 102 | return sum([light_level(tank_width * i / samples) for i in range(samples)]) / samples 103 | 104 | def light_heat_rate(): 105 | #The rate at which the temperature increases (deg C /sec) due to light 106 | return light_average() * .5 / (3600 * 180) 107 | 108 | def temp_equil_rate(): 109 | #The rate at which the temperature changes to equilibriate with outside the greenhouse 110 | #Newton's law of cooling 111 | return (room_temp - params['temperature']) * (.6 / 10000) #This constant is a guess atm 112 | 113 | def temp_evap_multiplier(): 114 | #most evaporative movements are multiplied by this to account for temperature 115 | return 1 + params['temperature'] / 200 116 | 117 | def humidity_evap_multiplier(): 118 | # Evaporation is inversely proportional to humidity of greenhouse 119 | # Zero at 100% humidity, 1 at 50%, 2 at 0% 120 | return 2 * (1 - params['humidity'] / 100) 121 | 122 | # TODO This should be based on plants size and health but, as a proxy, 123 | # make it relative to length that the plants have been growing 124 | max_plant_area = 25 # cm^2 125 | def estimated_plant_area(): 126 | dt = (params['time'] - params['start']) 127 | return max_plant_area*dt/(14*86400.0) # Max out after 14 days 128 | 129 | def transpiration_rate(): 130 | #The rate at which water moves soil->air due to plants 131 | return (0.025/3600)*estimated_plant_area() 132 | 133 | def soil_evaporation_rate(): 134 | #The rate at which water moves soil->air due to direct evaporation 135 | #average about 1 ml/hr, based on soil water content 136 | base = (evap_rate / 3600) * (params['soilwater'] / max_soilwater) 137 | if params['fan']: 138 | base *= 3 #Fan increases evaporation speed 139 | return base * temp_evap_multiplier() * humidity_evap_multiplier() 140 | 141 | def tank_evaporation_rate(): 142 | #The rate at which water moves tank->air due to direct evaporation 143 | base = evap_rate / 3600 144 | return base * temp_evap_multiplier() * humidity_evap_multiplier() 145 | 146 | def temp_to_pressure(temp): 147 | return 6.11*10**((7.5*temp)/(237.7+temp)) 148 | 149 | def airwater_to_humid(airwater, temp): 150 | Es = temp_to_pressure(temp) 151 | E = (temp + 273.15)*461.5*(airwater/3933.0) 152 | return 100*E/Es 153 | 154 | def humid_to_airwater(humid, temp): # In percent 155 | Es = temp_to_pressure(temp) 156 | E = Es*humid/100.0 157 | return (3933.0*E)/(461.5*(temp + 273.15)) 158 | 159 | # if humid > 100, then calculate the excess water that should precipitate out 160 | def update_airwater_humid(airwater, temp): 161 | humid = airwater_to_humid(airwater, temp) 162 | if (humid <= 100): return (humid, airwater, 0) 163 | else: 164 | sat_water = humid_to_airwater(100, temp) 165 | return (100, sat_water, airwater - sat_water) 166 | 167 | def exit_rate(): 168 | # The rate at which water leaves the greenhouse via air. 169 | # Based on difference in humidity between greenhouse and room, 170 | # is _much_ faster with the fan 171 | base = params['airwater'] * (3.0 if params['fan'] else 0.1)/3600 172 | return base * (params['humidity']/room_humidity - 1) 173 | 174 | def get_cur(): 175 | return (5.0 + led_current * params['led'] + 176 | (pump_current if params['wpump'] else 0) + 177 | (fan_current if params['fan'] else 0)) 178 | 179 | def get_weight(): 180 | # Water weighs about 1 g per 1 ml, add some for plants and "soil" 181 | return (base_weight + params['soilwater'] + params['panwater'] + 182 | params['tankwater'] + estimated_plant_area()) 183 | 184 | # Environment Runtime Functions 185 | 186 | def forward_water_cycle(duration): 187 | 188 | if duration > 1 and params['wpump']: 189 | duration = .5 190 | 191 | #Pump water if in reservoir and pump on, accounting for pipe lag 192 | # With the addition of the weight sensors, had to change how water movement 193 | # works - now, goes from the pipe to the pan and then to the soil 194 | if params['wpump']: 195 | vol = min(duration * flow_rate, params['volume']) 196 | params['volume'] -= vol 197 | params['pipewater'] += vol 198 | if params['pipewater'] > pipe_capacity: 199 | params['panwater'] += params['pipewater'] - pipe_capacity 200 | params['pipewater'] = pipe_capacity 201 | 202 | elif params['pipewater'] > 0: #Could be if. should dripping be ok when the pump is on? 203 | vol = min(duration * drip_rate, params['pipewater']) 204 | params['pipewater'] -= vol 205 | params['panwater'] += vol 206 | 207 | 208 | #Do water movement: 209 | #from soil to tankwater by overflow if soil fully saturated 210 | total_water = params['soilwater'] + params['panwater'] 211 | if total_water > max_soilwater: 212 | params['tankwater'] += total_water - max_soilwater 213 | params['panwater'] = max(0, max_soilwater - params['soilwater']) 214 | params['soilwater'] = max_soilwater - params['panwater'] 215 | #from pan to soil 216 | if params['panwater'] > 0: 217 | vol = min(duration * uptake_rate, params['panwater']) 218 | params['panwater'] -= vol 219 | params['soilwater'] += vol 220 | 221 | #from soil to air because of plants 222 | aw = params['airwater'] 223 | vol = min(duration * transpiration_rate(), params['soilwater']) 224 | params['soilwater'] -= vol 225 | params['airwater'] += vol 226 | #from soil to air by evaporation 227 | vol = min(duration * soil_evaporation_rate(), params['soilwater']) 228 | params['soilwater'] -= vol 229 | params['airwater'] += vol 230 | #from tankwater to air by evaporation 231 | vol = min(duration * tank_evaporation_rate(), params['tankwater']) 232 | params['tankwater'] -= vol 233 | params['airwater'] += vol 234 | #from air to outside the greenhouse 235 | vol = duration * exit_rate() 236 | params['airwater'] = min(max(0.0, params['airwater'] - vol), 100) 237 | humid, params['airwater'], excess_water = \ 238 | update_airwater_humid(params['airwater'], params['temperature']) 239 | params['humidity'] = humid 240 | params['tankwater'] += excess_water 241 | 242 | return duration 243 | 244 | def forward_temperature(duration): 245 | 246 | #Cooling due to the fan 247 | if params['fan']: 248 | params['temperature'] -= duration * fan_cool_rate 249 | #Heating due to light 250 | params['temperature'] += duration * light_heat_rate() 251 | #Change to equilibriate with outside env TODO if duration is big, newtons law of cooling may get out of hand 252 | params['temperature'] += duration * temp_equil_rate() 253 | params['temperature'] = max(params['temperature'], room_temp) 254 | 255 | 256 | def forward_time(duration): #Here is where most of the env mutation takes place 257 | #to change volume, smoist, humidity, tankwater 258 | duration = forward_water_cycle(duration) 259 | #actually move time 260 | params['time'] += duration #TODO I think weather will be handled here 261 | 262 | #Then also a growplants function (which probably just calls grow on each plant in params[plants]) 263 | 264 | #Change temp based on fan, light, ..? (+ for high light, - for fan on) 265 | forward_temperature(duration) 266 | #add to used energy 267 | params['energy'] += get_cur() * 12 / 1000 * duration 268 | #print(light_average()) 269 | #ordering here is interesting 270 | return duration 271 | 272 | def init(bl): 273 | #print("initializing with " + str(bl.params)) 274 | if bl is not None: 275 | for k,v in bl.params.items(): 276 | if k in params: 277 | params[k] = v 278 | elif k == 'humidity': 279 | params['humidity'] = v 280 | elif k == 'smoist': 281 | params['soilwater'] = v / 2 282 | elif k == 'wlevel': 283 | params['volume'] = v * volume_rate 284 | params['airwater'] = humid_to_airwater(params['humidity'], params['temperature']) 285 | -------------------------------------------------------------------------------- /TerraBot.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python 3 | import os, os.path as op, sys, select 4 | import subprocess as sp 5 | from std_msgs.msg import Int32,Bool,Float32,String,Int32MultiArray,Float32MultiArray, String 6 | import argparse, time, getpass 7 | from shutil import copyfile 8 | import rospy, rosgraph 9 | from lib import topic_def as tdef 10 | from lib import interference as interf_mod 11 | from lib import tester as tester_mod 12 | from lib import send_email 13 | from lib import sim_camera as cam 14 | from lib.terrabot_utils import clock_time, time_since_midnight 15 | from lib.baseline import Baseline 16 | from math import exp 17 | from os import makedirs 18 | 19 | ### Default values for the optional variables 20 | verbose = False 21 | grade = False 22 | log = False 23 | simulate = False 24 | still_running = True 25 | tick_interval = 0.5 26 | interference = None 27 | tester = None 28 | 29 | #lists which will be populated based on the topic_def.py 30 | log_files = {} 31 | publishers = {} 32 | subscribers = {} 33 | 34 | terrabot_dir = op.dirname(op.abspath(__file__)) 35 | log_dir = op.join(terrabot_dir, "Log") 36 | lib_dir = op.join(terrabot_dir, "lib") 37 | 38 | ### Update tester variables, if necessary 39 | var_translations = {'smoist' : 'smoist', 'light' : 'light', 40 | 'level' : 'wlevel', 'weight' : 'weight', 41 | 'temp' : 'temperature', 'humid' : 'humidity', 42 | 'led' : 'led', 'wpump' : 'wpump', 43 | 'fan' : 'fan', 'camera' : 'camera', 44 | 'insolation' : 'insolation'} 45 | def tester_update_var(var, value): 46 | global tester, var_translations 47 | if (tester): 48 | trans = var_translations[var] 49 | if isinstance(value, array): value = list(value) 50 | if (type(value) in [list, tuple]): 51 | svalue = sum(value) if var == 'weight' else sum(value)/2 52 | tester.vars[trans+'_raw'] = value 53 | tester.vars[trans] = svalue 54 | else: 55 | tester.vars[trans] = value 56 | #print(var, value, tester.vars) 57 | 58 | def tester_update_behaviors(behavior, enabled_p): 59 | global tester 60 | if (tester): 61 | if (enabled_p): tester.vars['enabled_behaviors'].add(behavior) 62 | else: tester.vars['enabled_behaviors'].discard(behavior) 63 | 64 | def gen_log_files(): 65 | global log_files 66 | 67 | prefix = time.strftime("%Y%m%d_%H%M%S") + ("_sim" if simulate else "") 68 | makedirs(op.join(log_dir, "Log_%s" %prefix)) 69 | 70 | for name in tdef.sensor_names + tdef.actuator_names: 71 | file_name = op.join(log_dir, "Log_%s/%s_log.csv" % (prefix, name)) 72 | log_files[name] = open(file_name, 'w+', 0) 73 | 74 | def log_print(string): 75 | print("%s%s"%(time.strftime("[%Y%m%d %H:%M:%S]: "),string)) 76 | 77 | def generate_publishers(): 78 | global publishers 79 | 80 | for name in tdef.sensor_names: 81 | pub_name = name + "_output" 82 | publishers[name] = rospy.Publisher( 83 | pub_name, tdef.sensor_types[name], 84 | latch = True, queue_size = 1) 85 | 86 | for name in tdef.actuator_names: 87 | pub_name = name + "_raw" 88 | publishers[name] = rospy.Publisher( 89 | pub_name, tdef.actuator_types[name], 90 | latch = True, queue_size = 1) 91 | 92 | insolation = 0 93 | last_light_reading = 0 94 | light_level = 0 95 | 96 | def cb_generic(name, data): 97 | global now, interference, light_level 98 | original = data.data 99 | edited = data 100 | edited.data = (original if not interference else 101 | interference.edit(name, original)) 102 | 103 | tester_update_var(name, original) 104 | 105 | if (name == 'light'): # Integrate light levels 106 | global last_light_reading, insolation 107 | now = rospy.get_time() 108 | if (last_light_reading > 0): 109 | light_level = (original[0] + original[1])/2.0 110 | dt = now - last_light_reading 111 | insolation += dt*light_level/3600.0 112 | #print("INSOLATION: %.2f %d %.2f" %(insolation, light_level, dt)) 113 | tester_update_var('insolation', insolation) 114 | if (time_since_midnight(now) < time_since_midnight(last_light_reading)): 115 | #print("INSOLATION TODAY: %.1f" %insolation) 116 | insolation = 0 # Reset daily 117 | last_light_reading = now 118 | 119 | publishers[name].publish(edited) 120 | if (log): 121 | log_file = log_files[name] 122 | log_file.write(clock_time(now) + ", internal: " + 123 | str(original) + ", edited: " + str(edited.data) + "\n") 124 | log_file.flush() 125 | if (verbose): 126 | log_print ("Logging %s data" % name) 127 | 128 | def generate_cb(name): 129 | return (lambda data: cb_generic(name, data)) 130 | 131 | def generate_subscribers(): 132 | global subscribers 133 | for name in tdef.sensor_names: 134 | sub_name = name + "_raw" 135 | cb = generate_cb(name) 136 | subscribers[name] = rospy.Subscriber(sub_name, tdef.sensor_types[name], cb) 137 | 138 | for name in tdef.actuator_names: 139 | sub_name = name + "_input" 140 | cb = generate_cb(name) 141 | subscribers[name] = rospy.Subscriber(sub_name, tdef.actuator_types[name], cb) 142 | 143 | ###Start of program 144 | parser = argparse.ArgumentParser(description = "TerraBot arg parser") 145 | parser.add_argument('-v', '--verbose', action = 'store_true') 146 | parser.add_argument('-l', '--log', action = 'store_true') 147 | parser.add_argument('-m', '--mode', default = "serial", 148 | choices = ['serial', 'sim'], 149 | help = "if no mode given, serial is used") 150 | parser.add_argument('-g', '--graphics', default = False, action='store_true') 151 | parser.add_argument('-s', '--speedup', default = 1, type = float) 152 | parser.add_argument('-b', '--baseline', default = None) 153 | parser.add_argument('-i', '--interference', default = None) 154 | parser.add_argument('-t','--test', default = None, 155 | help = "test execution using given tester file") 156 | parser.add_argument('-f', '--fixedshutter', default = None, 157 | help = "use fixed shutter speed") 158 | 159 | args = parser.parse_args() 160 | 161 | verbose = args.verbose 162 | log = args.log 163 | mode = args.mode 164 | tester_file = args.test 165 | simulate = mode == "sim" 166 | fixed_shutter = (args.fixedshutter if args.fixedshutter == None else 167 | int(args.fixedshutter)) 168 | 169 | num_restarts = 0 170 | max_restarts = 5 171 | 172 | def terminate (process, log_file): 173 | if (process.poll() == None): 174 | process.terminate() 175 | process.wait() 176 | if (log_file != None): log_file.close() 177 | 178 | def terminate_core(): 179 | global core_p, core_log 180 | if (core_p != None): 181 | print("Terminating roscore") 182 | terminate(core_p, core_log) 183 | core_p = None; core_log = None 184 | 185 | def terminate_sim(): 186 | global sim_p, sim_log 187 | if (sim_p != None): 188 | print("Terminating sim") 189 | terminate(sim_p, sim_log) 190 | sim_p = None; sim_log = None 191 | 192 | def terminate_serial(): 193 | global serial_p, serial_log 194 | if (serial_p != None): 195 | print("Turning off actuators") 196 | publishers['led'].publish(0) 197 | publishers['wpump'].publish(0) 198 | publishers['fan'].publish(0) 199 | 200 | print("Terminating serial") 201 | terminate(serial_p, serial_log) 202 | serial_p = None; serial_log = None 203 | 204 | 205 | def terminate_gracefully(): 206 | terminate_sim() 207 | terminate_serial() 208 | terminate_core() 209 | sys.exit() 210 | 211 | if log: 212 | gen_log_files() 213 | 214 | ### Open log file for roscore 215 | core_log = open(op.join(log_dir, "roscore.log"), "a+") 216 | 217 | ### Start up roscore, redirecting output to logging files 218 | core_p = sp.Popen("roscore", stdout = core_log, stderr = core_log) 219 | 220 | ### Begin relay node 221 | if simulate: # Use simulated time if starting simulator 222 | while not rosgraph.is_master_online(): 223 | rospy.sleep(1) # Wait for roscore to start up 224 | rospy.set_param("use_sim_time", True) 225 | rospy.init_node('TerraBot', anonymous = True) 226 | 227 | generate_publishers() 228 | generate_subscribers() 229 | 230 | images = None 231 | ### Camera callback - take a picture and store in the given location 232 | def camera_cb(data): 233 | global simulate, images, fixed_shutter 234 | 235 | print("Taking an image at %s, storing it in %s" 236 | %(clock_time(rospy.get_time()), data.data)) 237 | if simulate: 238 | publishers['camera'].publish(data.data) 239 | else: 240 | # Shutter speed in microseconds, 2.8 aperture 241 | shutter_speed = ((fixed_shutter if fixed_shutter != None else 242 | int((1e6*2.8*2.8)/(exp(3.32)*(max(1,light_level)**0.655))))) 243 | print("LIGHT:", light_level, "SHUTTER: ", shutter_speed) 244 | # sp.call("raspistill -n -md 2 -awb off -awbg 1,1 -ss 30000 -o %s" 245 | # sp.call("raspistill -n -md 4 -awb auto -ss 30000 -rot 180 -o %s" 246 | 247 | sp.call("raspistill -n -md 4 -awb auto -ss %d -o %s" 248 | %(shutter_speed, data.data), shell = True) 249 | tester_update_var('camera', data.data) 250 | 251 | camera_sub = rospy.Subscriber('camera', String, camera_cb) 252 | 253 | def enable_cb (data): 254 | #print("Enabling behavior:", data.data) 255 | tester_update_behaviors(data.data, True) 256 | 257 | def disable_cb (data): 258 | #print("Disabling behavior:", data.data) 259 | tester_update_behaviors(data.data, False) 260 | 261 | enable_sub = rospy.Subscriber('enable', String, enable_cb) 262 | disable_sub = rospy.Subscriber('disable', String, disable_cb) 263 | 264 | ### Spawn subprocesses 265 | 266 | sim_p = None 267 | sim_log = None 268 | serial_p = None 269 | serial_log = None 270 | 271 | ### Initiates the Simulator and redirects output 272 | def start_simulator(): 273 | global sim_p, sim_log, args 274 | if (sim_log == None): sim_log = open(op.join(log_dir, "simulator.log"), "w") 275 | fard_args = ["--speedup", str(args.speedup)] 276 | if args.graphics: fard_args += ["--graphics"] 277 | if args.baseline: 278 | try: # In case of syntax errors, try parsing here first 279 | Baseline(op.abspath(args.baseline)) 280 | except Exception as inst: 281 | print("start_simulator: %s" %str(inst.args)) 282 | terminate_gracefully() 283 | fard_args += ["--baseline", args.baseline] 284 | if log: fard_args = fard_args + ["-l"] 285 | sim_p = sp.Popen(["python", op.join(lib_dir, "farduino.py")] + fard_args, 286 | stdout = sim_log, stderr = sim_log) 287 | time.sleep(1) # chance to get started 288 | 289 | ### Initiates the Arduino and redirects output 290 | def start_serial(): 291 | global serial_p, serial_log 292 | if (serial_log == None): serial_log = open(op.join(log_dir, "rosserial.log"), "w") 293 | serial_p = sp.Popen(["rosrun", "rosserial_arduino", 294 | "serial_node.py", "/dev/ttyACM0"], 295 | stdout = serial_log, stderr = serial_log) 296 | time.sleep(1) # chance to get started 297 | print("started serial") 298 | 299 | def adjust_path(pathname, dirname): 300 | return (pathname if op.isabs(pathname) else 301 | op.abspath(dirname + pathname)) 302 | 303 | if tester_file: 304 | tester = tester_mod.Tester() 305 | try: 306 | tester.parse_file(tester_file) 307 | except Exception as inst: 308 | print("Tester parse: %s" %str(inst.args)) 309 | terminate_gracefully() 310 | #tester.display() 311 | dirname = op.dirname(tester_file) + "/" 312 | # Command line option overrides test file 313 | if (not args.baseline and tester.baseline_file): 314 | args.baseline = adjust_path(tester.baseline_file, dirname) 315 | print('baseline', args.baseline) 316 | # Command line option overrides test file 317 | if (not args.interference and tester.interf_file): 318 | args.interference = adjust_path(tester.interf_file, dirname) 319 | 320 | ### Start up arduino/simulator 321 | print("Waiting for nodes") 322 | if simulate: 323 | print(" Starting simulator") 324 | start_simulator() 325 | else: 326 | print(" Starting hardware") 327 | start_serial() 328 | # Wait for clock to start up correctly 329 | while rospy.get_time() == 0: rospy.sleep(0.1) 330 | now = rospy.get_time() 331 | 332 | print("System started") 333 | 334 | if (tester != None): 335 | tester.init_constraints(now) 336 | 337 | if (args.interference): 338 | interference = interf_mod.Interference(args.interference, now) 339 | 340 | ### Main loop 341 | while not rospy.core.is_shutdown(): 342 | ### Check for input 343 | if sys.stdin in select.select([sys.stdin],[],[],0)[0]: 344 | input = sys.stdin.readline() 345 | if input[0] == 'q': 346 | terminate_gracefully() 347 | if input[0] == 't': 348 | print("Current time: %s" %clock_time(now)) 349 | else: 350 | print("Usage: q (quit); t (current time)") 351 | 352 | now = rospy.get_time() 353 | if (interference): interference.update(now) 354 | if tester: 355 | tester.vars['time'] = now 356 | tester.vars['mtime'] = time_since_midnight(now) 357 | tester.process_constraints(now) 358 | tester_update_var('camera', None) # Camera should not be latched 359 | if tester.finished(now): 360 | print("Done testing!") 361 | if (tester.end_status() == 'QUIT'): terminate_gracefully() 362 | else: tester = None 363 | 364 | rospy.sleep(tick_interval) 365 | # End while loop 366 | 367 | 368 | -------------------------------------------------------------------------------- /lib/tester.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from datetime import datetime 3 | from terrabot_utils import clock_time, time_since_midnight, dtime_to_seconds 4 | from terrabot_utils import Agenda 5 | from limits import limits, optimal 6 | 7 | def parse_error(line): 8 | raise Exception("Unknown syntax: %s" % line) 9 | 10 | def is_abs_time(time_str): 11 | try: 12 | datetime.strptime(time_str, "%d-%H:%M:%S") 13 | return True 14 | except ValueError: return False 15 | 16 | class Constraint: 17 | condition = None 18 | rel_time = None 19 | abs_time = None 20 | timeout = 0 21 | 22 | def __init__(self, line): return None 23 | def __str__(self): return "[Generic constraint]" 24 | # Returns -1 (constraint is false), +1 (constraint is true), 0 (still TBD) 25 | def evaluate(self, time, vars): return 0 26 | 27 | def has_time(self): return self.abs_time != None or self.rel_time != None 28 | def set_timeout(self, time, time0): 29 | self.timeout = (time + self.rel_time if (self.rel_time != None) else 30 | time if (self.abs_time == None) else 31 | (time0 - time_since_midnight(time0) + 32 | dtime_to_seconds(self.abs_time))) 33 | def evaluate_time(self, time): return time >= self.timeout 34 | def evaluate_condition(self, vars): 35 | for key in vars: locals()[key] = vars[key] 36 | # A hack to get behavior tracking to work 37 | global enabled_behaviors 38 | enabled_behaviors = vars['enabled_behaviors'] 39 | return eval(self.condition) 40 | 41 | def time_str(self, rel_word, abs_word): 42 | return ("%s %d" %(rel_word, self.rel_time) if self.rel_time != None else 43 | "%s %s" %(abs_word, self.abs_time) if self.abs_time != None else "") 44 | def parse_time(self, line, rel_word, abs_word): 45 | if (line.find(rel_word) > 0): 46 | self.rel_time = int(line.split(rel_word)[1]) 47 | elif (line.find(abs_word) > 0): 48 | self.abs_time = line.split(abs_word)[1].strip() 49 | def parse_time_condition(self, line): 50 | self.parse_time(line, "FOR", "UNTIL") 51 | self.condition = (line if not self.has_time() else 52 | line.split("FOR" if self.rel_time != None else "UNTIL")[0]) 53 | self.condition = self.condition.strip() 54 | 55 | class WaitConstraint(Constraint): 56 | def __init__(self, line): 57 | self.parse_time_condition(line[len("WAIT"):]) 58 | def __str__(self): 59 | return "[WAIT %s %s]" % (self.condition, self.time_str("FOR", "UNTIL")) 60 | def evaluate(self, time, vars): 61 | cres = (self.evaluate_condition(vars) if self.condition else None) 62 | tres = self.evaluate_time(time) 63 | return (1 if tres and cres == None else 1 if cres else -1 if tres else 0) 64 | 65 | class EnsureConstraint(Constraint): 66 | def __init__(self, line): 67 | self.parse_time_condition(line[len("ENSURE"):]) 68 | def __str__(self): 69 | return "[ENSURE %s %s]" %(self.condition, self.time_str("FOR", "UNTIL")) 70 | def evaluate(self, time, vars): 71 | cres = self.evaluate_condition(vars) 72 | tres = (self.evaluate_time(time) if self.has_time() else None) 73 | return (-1 if not cres else 1 if tres == None or tres else 0) 74 | 75 | class SetConstraint(Constraint): 76 | var = None 77 | def __init__(self, line, vars): 78 | s = line[len("SET"):].split('=') 79 | self.condition = s[1].strip() 80 | self.var = s[0].strip() 81 | vars[self.var] = 0 82 | def __str__(self): 83 | return "[SET %s = %s]" %(self.var, self.condition) 84 | def evaluate(self, time, vars): 85 | vars[self.var] = self.evaluate_condition(vars) 86 | return 1 87 | 88 | class PrintConstraint(Constraint): 89 | condition = "" 90 | 91 | def __init__(self, line, vars): 92 | self.condition = line[len("PRINT"):] 93 | def __str__(self): 94 | return "[PRINT %s]" %self.condition 95 | def evaluate(self, time, vars): 96 | print("PRINT (%s): %s" 97 | %(clock_time(time), self.evaluate_condition(vars))) 98 | return 1 99 | 100 | class DelayConstraint(Constraint): 101 | def __init__(self, line): 102 | self.parse_time(line[len("DELAY"):], 'FOR', 'UNTIL') 103 | def __str__(self): 104 | return "" %self.time_str("FOR", "UNTIL") 105 | def evaluate(self, time, vars): return int(self.evaluate_time(time)) 106 | 107 | class EndConstraint(Constraint): 108 | type = 'STOP' 109 | def __init__(self, line): 110 | self.type = line[0:4] 111 | self.parse_time(line[4:], 'AFTER', 'AT') 112 | def __str__(self): 113 | return "<%s %s>" %(self.type, self.time_str("AFTER", "AT")) 114 | def evaluate(self, time, vars): return int(self.evaluate_time(time)) 115 | 116 | class WhileConstraint(Constraint): 117 | def __init__(self, line): 118 | self.condition = line.strip() 119 | def __str__(self): 120 | return f"WHILE {self.condition}" 121 | def evaluate(self, time, vars): 122 | # Return True if the while condition no longer holds 123 | return not self.evaluate_condition(vars) 124 | 125 | class WheneverConstraint(Constraint): 126 | agenda = None 127 | conditionP = False 128 | parent = None 129 | end_cond = None 130 | def __init__(self, line): 131 | self.agenda = Agenda() 132 | trigger = self.parse_while_condition(line[len("WHENEVER"):]).strip() 133 | if (is_abs_time(trigger)): self.abs_time = trigger 134 | else: self.condition = trigger 135 | def __str__(self): 136 | str = "[WHENEVER %s" %(self.condition if self.condition != None else 137 | self.abs_time) 138 | if self.end_cond: str += " " + self.end_cond.__str__() 139 | for constraint in self.agenda.schedule: 140 | str = str + "\n " + constraint.__str__() 141 | return str + "]" 142 | def brief(self): 143 | return "[WHENEVER %s ...]" %(self.condition if self.condition != None 144 | else self.abs_time) 145 | def parse_while_condition(self, line): 146 | if "WHILE" in line: 147 | line, end_cond = line.split("WHILE") 148 | self.end_cond = WhileConstraint(end_cond) 149 | return line 150 | 151 | def evaluate(self, time, vars): 152 | if (self.condition != None): 153 | last_eval = self.conditionP 154 | self.conditionP = self.evaluate_condition(vars) 155 | return int(not last_eval and self.conditionP) 156 | else: 157 | self.conditionP = self.evaluate_time(time) 158 | return int(self.conditionP) 159 | 160 | def activate(self, time): 161 | child = copy.deepcopy(self) 162 | child.parent = self 163 | # Update the timeout so it doesn't keep triggering 164 | if (self.abs_time != None): self.timeout += 24*60*60 # Add a day 165 | 166 | if (child.agenda.time0 == None): 167 | child.agenda.time0 = time - time_since_midnight(time) 168 | first_constraint = child.agenda.schedule[0] 169 | first_constraint.set_timeout(time, child.agenda.time0) 170 | #print("First timeout: %s for %s" %(clock_time(first_constraint.timeout), first_constraint)) 171 | return child 172 | 173 | def deactivate(self): 174 | self.parent.conditionP = False 175 | 176 | def evaluate_agenda(self, time, vars): 177 | if self.end_cond and self.end_cond.evaluate(time, vars): 178 | self.agenda.schedule = [] # Force it to finish successfully 179 | print("ENDED (%s): %s" %(clock_time(time), self.end_cond.__str__())) 180 | return 1 181 | curr_constraint = self.agenda.schedule[self.agenda.index] 182 | res = curr_constraint.evaluate(time, vars) 183 | #print("Test: %s %s: %s: %d" %(curr_constraint, curr_constraint.timeout, time, res)) 184 | if (res == 1): 185 | self.next_constraint(time) 186 | # Keep testing, since subsequent clauses (e.g., SET) may already hold 187 | if (not self.agenda.finished()): 188 | res = self.evaluate_agenda(time, vars) 189 | return res 190 | 191 | def next_constraint(self, time): 192 | self.agenda.index += 1 193 | if (not self.agenda.finished()): 194 | next_constraint = self.agenda.schedule[self.agenda.index] 195 | next_constraint.set_timeout(time, self.agenda.time0) 196 | #print("Next timeout: %s for %s" %(clock_time(next_constraint.timeout), next_constraint)) 197 | 198 | def print_status(self, res, time): 199 | if (res == 1): 200 | print("SUCCESS (%s): %s" %(clock_time(time), self.brief())) 201 | pass 202 | elif (res == -1): 203 | curr_constraint = self.agenda.schedule[self.agenda.index] 204 | print("FAILURE (%s): at %s in %s" 205 | %(clock_time(time), curr_constraint, self.brief())) 206 | 207 | 208 | def enabled(behavior): 209 | global enabled_behaviors 210 | return behavior in enabled_behaviors 211 | 212 | class Tester: 213 | vars = { 'light_raw' : [100,100], 'light' : 100, 214 | 'temperature_raw' : [20,20], 'temperature' : 20, 215 | 'humidity_raw' : [80,80], 'humidity' : 80, 216 | 'smoist_raw' : [500,500], 'smoist' : 500, 217 | 'weight_raw' : [800,800], 'weight' : 800, 218 | 'wlevel' : 150.0, 'led' : 0, 219 | 'insolation' : 0, 'wpump' : False, 220 | 'fan' : False, 'camera' : None, 221 | 'enabled_behaviors' : set() } 222 | baseline_file = None 223 | interf_file = None 224 | delay_time = None 225 | end_time = None 226 | constraints = [] 227 | active = [] 228 | 229 | def parse_file(self, filename): 230 | with open(filename) as f: 231 | for line in f.readlines(): 232 | line = line.split('#')[0].strip(' \n\r') 233 | if (line.startswith("BASELINE")): 234 | baseline = line.split('=')[1].strip() 235 | if (self.baseline_file and self.baseline_file != baseline): 236 | print("WARNING: Baseline file already specified; ignoring %s (in %s)" 237 | %(baseline, filename)) 238 | else: self.baseline_file = baseline 239 | elif (line.startswith("INTERFERENCE")): 240 | interference = line.split('=')[1].strip() 241 | if (self.interf_file and self.interf_file != interference): 242 | print("WARNING: Interference file already specified; ignoring %s (in %s)" 243 | %(interference, filename)) 244 | else: self.interf_file = linterference 245 | elif (line.startswith("INCLUDE")): 246 | include = line.split()[1].strip() 247 | print("Including monitors from", include) 248 | self.parse_file(include) 249 | elif (line.startswith("QUIT") or line.startswith("STOP")): 250 | if (self.end_time): 251 | print("WARNING: Quit/Stop already specified; ignoring %s (in %s)" 252 | %(line, filename)) 253 | else: self.end_time = EndConstraint(line) 254 | elif (line.startswith("DELAY")): 255 | if (self.delay_time): 256 | print("WARNING: Delay already specified; ignoring %s (in %s)" 257 | %(line, filename)) 258 | else: self.delay_time = DelayConstraint(line) 259 | elif (line.startswith("WAIT")): 260 | self.add_to_whenever(WaitConstraint(line)) 261 | elif (line.startswith("ENSURE")): 262 | self.add_to_whenever(EnsureConstraint(line)) 263 | elif (line.startswith("SET")): 264 | self.add_to_whenever(SetConstraint(line, self.vars)) 265 | elif (line.startswith("WHENEVER")): 266 | self.constraints.append(WheneverConstraint(line)) 267 | elif (line.startswith("PRINT")): 268 | self.add_to_whenever(PrintConstraint(line, self.vars)) 269 | elif (len(line) > 0): 270 | parse_error("'%s' %d" %(line, len(line))) 271 | 272 | def add_to_whenever(self, cmd): 273 | if (len(self.constraints) == 0): 274 | parse_error("%s is not within a 'whenever' statement" % cmd) 275 | else: 276 | self.constraints[-1].agenda.add_to_schedule(cmd) 277 | 278 | def set_delay_time(self, time0): 279 | if (self.delay_time): self.delay_time.set_timeout(time0, time0) 280 | 281 | def ready_to_start(self, time): 282 | return (self.delay_time == None or self.delay_time.evaluate(time, []) == 1) 283 | 284 | def set_end_time(self, time0): 285 | if (self.end_time): self.end_time.set_timeout(time0, time0) 286 | 287 | def finished(self, time): 288 | return (self.end_time != None and self.end_time.evaluate(time, []) == 1) 289 | 290 | def end_status(self): 291 | return (None if not self.end_time else self.end_time.type) 292 | 293 | def evaluate_whenever(self, whenever, time): 294 | res = whenever.evaluate_agenda(time, self.vars) 295 | if (res == -1 or whenever.agenda.finished()): 296 | whenever.print_status(res, time) 297 | self.deactivate(whenever) 298 | 299 | def activate(self, whenever, time): 300 | active = whenever.activate(time) 301 | print("ACTIVATE (%s): %s" %(clock_time(time), active.brief())) 302 | self.active.append(active) 303 | 304 | def deactivate(self, whenever): 305 | whenever.deactivate() 306 | self.active.remove(whenever) 307 | 308 | def init_constraints(self, time0): 309 | #print("Start time: %s" %clock_time(time0)) 310 | self.set_delay_time(time0) 311 | self.set_end_time(time0) 312 | for constraint in self.constraints: 313 | constraint.set_timeout(time0, time0) 314 | 315 | def process_constraints(self, time): 316 | # Wait for the delay, if any 317 | if (not self.ready_to_start(time)): return 318 | elif (self.delay_time): 319 | print("Tester: Initial delay achieved") 320 | self.delay_time = None 321 | 322 | # Trigger any of the 'whenever' constraints 323 | for whenever in self.constraints: 324 | if (not whenever.conditionP and 325 | whenever.evaluate(time, self.vars)): 326 | self.activate(whenever, time) 327 | 328 | # Process currently active 'whenever' constraints 329 | for whenever in self.active: 330 | self.evaluate_whenever(whenever, time) 331 | 332 | def display(self): 333 | if (self.baseline_file): print("BASELINE: '%s'" %self.baseline_file) 334 | if (self.interf_file): print("INTERFERENC: '%s'" %self.interf_file) 335 | if (self.delay_time): 336 | print("DELAY %s" %self.delay_time.time_str("FOR", "UNTIL")) 337 | if (self.end_time): 338 | print("%s %s" %(self.end_time.type, self.end_time.time_str("AFTER", "AT"))) 339 | for cmd in self.constraints: print(cmd) 340 | 341 | if __name__ == '__main__': 342 | import sys, time 343 | if (len(sys.argv) == 2): 344 | tester = Tester() 345 | tester.parse_file(sys.argv[1]) 346 | tester.display(); exit() 347 | now = time.time() 348 | time0 = now - time_since_midnight(now) 349 | t = time0 350 | tester.init_constraints(time0) 351 | while not tester.finished(t): 352 | tester.process_constraints(t) 353 | t += 1 354 | else: 355 | print("Need to provide one grader file to parse") 356 | 357 | -------------------------------------------------------------------------------- /lib/plant.py: -------------------------------------------------------------------------------- 1 | #David Buffkin 2 | from random import random 3 | from math import sqrt, sin, cos, pi 4 | from panda3d.core import LVector3 5 | from environment import airwater_to_humid 6 | 7 | day = 3600 * 24 8 | 9 | #Constants 10 | max_leafstem_length = .9 11 | max_stem_length = 4 #cm 12 | max_leaf_size = .6 #unitless 13 | 14 | leaf_growth_max = .7 * 5 * day * .5 #* .5 for splitting factor? 15 | #The problem is that it should be 5 .7 health days, but 16 | #the health will be split up. Hmmmm 17 | stem_growth_max = .8 * leaf_growth_max 18 | droop_rate = 20 / (100 * day) #20 degrees per day per 100ml below optimal 19 | class Leaf: 20 | 21 | def __init__(self, baby, showBase): 22 | 23 | 24 | self.baby = baby #Is it a baby leaf? 25 | self.color = [0, .4, 0] #All leaves have alpha = 1. this is [r, g, b] 26 | self.start_position = LVector3(0, 0, .01) #This is relative to the plant stem top 27 | self.angleBase = 81 #This is the angle from the horizontal 28 | self.angleDelta = 0 #This goes from 0 to 60, and gets subtracted from the leaf angle to represent drooping 29 | self.angle = 81 30 | self.size = .05 #This is the size of the leaf. Unitless--it is the scale in panda. goes 0 -> 1 31 | self.multiplier1 = .8 + random() * .4 32 | self.multiplier2 = .8 + random() * .4 33 | 34 | self.colorRands = [random() * .1 - .05, random() * .1 - .05, random() * .1 - .05] 35 | 36 | self.current_leaf_growth = 0 37 | self.current_stem_growth = 0 38 | 39 | self.stemModel = showBase.loader.loadModel("models/plants/ThinStem.bam") 40 | self.leafModel = showBase.loader.loadModel("models/plants/" + ("Baby" if baby else "") + "Leaf.bam") 41 | 42 | def grow_leaf(self, growth, soilwater, duration): 43 | self.current_leaf_growth += growth 44 | frac = self.current_leaf_growth / (leaf_growth_max * self.multiplier1) 45 | if(frac > 1): frac = 1 46 | self.size = .05 + (max_leaf_size - .05) * (frac) 47 | self.angleBase = 90 * (.9 - frac) 48 | 49 | if(soilwater < optimal_soilwater[0]): 50 | self.angleDelta += droop_rate * (optimal_soilwater[0] - soilwater) * duration 51 | self.angleDelta = min(60, self.angleDelta) 52 | else: 53 | self.angleDelta -= (25 / day) * duration #25 degrees perkier per day when watered well 54 | self.angleDelta = max(0, self.angleDelta) 55 | 56 | 57 | self.angle = self.angleBase - self.angleDelta 58 | 59 | def setDroopFrac(self, frac): 60 | self.angleDelta = 60 * frac 61 | 62 | def grow_stem(self, growth, lightfrac): 63 | growth *= self.multiplier2 64 | self.current_stem_growth += growth if self.current_stem_growth / stem_growth_max < lightfrac else 0 65 | frac = self.current_stem_growth / stem_growth_max 66 | if(frac > 1): frac = 1 67 | self.start_position *= max(1, (max_leafstem_length * self.multiplier2 * frac / self.start_position.length())) 68 | 69 | 70 | def grow(self, health, growth_amount, lightfrac, soilwater, duration): 71 | leaf_amount = .5 * growth_amount 72 | stem_amount = .5 * growth_amount 73 | ''' 74 | if(lightfrac < 1): 75 | stem_amount = (.5 + .3 * (1 - lightfrac)) * growth_amount 76 | leaf_amount = (.5 - .3 * (1 -lightfrac)) * growth_amount''' 77 | 78 | self.grow_leaf(leaf_amount, soilwater, duration) 79 | self.grow_stem(stem_amount, lightfrac) 80 | 81 | class LettuceLeaf: 82 | def __init__(self, baby, showBase): 83 | 84 | 85 | self.baby = baby #Is it a baby leaf? 86 | self.color = [0, .4, 0] #All leaves have alpha = 1. this is [r, g, b] 87 | self.start_position = LVector3(0, 0, .001) #This is relative to the plant stem top 88 | self.angleBase = 81 #This is the angle from the horizontal 89 | self.angleDelta = 0 #This goes from 0 to 60, and gets subtracted from the leaf angle to represent drooping 90 | self.angle = 81 91 | self.size = .05 #This is the size of the leaf. Unitless--it is the scale in panda. goes 0 -> 1 92 | #self.tilt = this could be harder than I thought 93 | self.multiplier1 = .8 + random() * .4 94 | self.multiplier2 = .8 + random() * .4 95 | self.rotationRand = random() * 10 96 | self.colorRands = [random() * .1 - .05, random() * .1 - .05, random() * .1 - .05] 97 | 98 | self.current_leaf_growth = 0 99 | self.current_stem_growth = 0 100 | 101 | self.stemModel = showBase.loader.loadModel("models/plants/ThinStem.bam") 102 | self.leafModel = showBase.loader.loadModel("models/plants/" + ("Baby" if baby else "") + "LettuceLeaf.bam") 103 | 104 | def grow_leaf(self, growth, soilwater, duration): 105 | self.current_leaf_growth += growth 106 | frac = self.current_leaf_growth / (leaf_growth_max * self.multiplier1) 107 | if(frac > 1): frac = 1 108 | self.size = .05 + (max_leaf_size - .05) * (frac) 109 | self.angleBase = 50 - frac * 40 110 | 111 | if(soilwater < optimal_soilwater[0]): 112 | self.angleDelta += droop_rate * (optimal_soilwater[0] - soilwater) * duration 113 | self.angleDelta = min(30, self.angleDelta) 114 | else: 115 | self.angleDelta -= (25 / day) * duration #25 degrees perkier per day when watered well 116 | self.angleDelta = max(0, self.angleDelta) 117 | 118 | 119 | self.angle = self.angleBase - self.angleDelta - self.rotationRand 120 | 121 | def setDroopFrac(self, frac): 122 | self.angleDelta = frac * 30 123 | 124 | 125 | def grow(self, health, growth_amount, lightfrac, soilwater, duration): 126 | 127 | self.grow_leaf(growth_amount / 1.7, soilwater, duration) #The / 1.7 is probably to account for not needing to grow stem 128 | 129 | 130 | 131 | stem_rate = 2 / (.7 * 20 * day) #cm/sec*health takes 20 days to grow 2 cm in full light 132 | leaf_stem_rate = stem_rate * 8 #cm/sec*health leaf stems grow ~8x faster than main (?) 133 | 134 | optimal_temperature = [20, 28] #degrees celcius the temps out of which health will decline 135 | temp_health_rate = .01 / 3600 #health/degree*second the rate at which health declines if outside of this range 136 | #or increases if inside it. 137 | 138 | optimal_soilwater = [500 / 2, 650 / 2] #ml " " 139 | sw_health_dry_rate = .1 / (100 * 3600) #too dry - health/ml*second 140 | sw_health_wet_rate = .01 / (100 * 3600) #too wet - health/ml*second 141 | sw_health_rate = .05 / (100 * 3600) #good - health/ml*second 142 | 143 | optimal_humidity = [60, 85] # Percentage 144 | humidity_health_rate = .01 / (5 * day) #health/ml*sec 145 | 146 | minimum_light = 250 #Again, very ballpark. less light will be detrimental. 147 | light_health_up_rate = .01 / 3600 148 | light_health_down_rate = .005 / (100 * 3600) 149 | 150 | 151 | #I think growth should be a constant based on health, 152 | #Split among the leaves. then each leaf dedicates more to the stem if low light 153 | #Bigger leaves will get less growth than the newer/smaller ones, since they are good already 154 | 155 | def leaf_pair(baby, direction, showBase): 156 | x, y = direction 157 | leaf1 = Leaf(baby, showBase) 158 | leaf1.start_position = LVector3(x, y, random() * 3 + 2) * .001 159 | leaf2 = Leaf(baby, showBase) 160 | leaf2.start_position = LVector3(-x, -y, random() * 3 + 2) * .001 161 | return [leaf1, leaf2] 162 | 163 | def lettuce_leaf_single(baby, direction, showBase): 164 | x, y = direction 165 | leaf1 = LettuceLeaf(baby, showBase) 166 | leaf1.start_position = LVector3(x, y, random() * 3 + 2) * .001 167 | return [leaf1] 168 | 169 | def lettuce_leaf_pair(baby, direction, showBase): 170 | x, y = direction 171 | leaf1 = LettuceLeaf(baby, showBase) 172 | leaf1.start_position = LVector3(x, y, random() * 3 + 2) * .001 173 | leaf2 = LettuceLeaf(baby, showBase) 174 | leaf2.start_position = LVector3(-x, -y, random() * 3 + 2) * .001 175 | return [leaf1, leaf2] 176 | 177 | sicklyLeafGreen = [0, .6, 0] 178 | sicklyStemGreen = [0, .6, 0] 179 | green_rate = .1 / (3 * day) 180 | deadColor = [97.0 / 255, 55.0 / 255, 10.0 / 255] 181 | howLongBelow2 = 6 * day 182 | deathTime = 3.5 * day 183 | 184 | class Plant(object): 185 | def __init__(self, node, age, droop, lankiness, plant_health, showBase): 186 | 187 | #Plant life parameters 188 | self.health = .65 + random() * .1 #a value [0, 1] where 1 is perfect health, .5 is ok, 0 is terrible. 189 | self.cumulative_health = 0 #add to this a bit stochastically 190 | self.timeBelow2 = 0 191 | 192 | #Plant render parameters 193 | self.node = node 194 | self.stem_height = .01 #The height of the base stem 195 | self.leaves = [] #The set of leaves of the plant. see leaf class for more. 196 | self.rotation = (random() * 180, random() * 10 - 5, random() * 10 - 5) 197 | self.delay = random() * 2 * day #A random growth delay 198 | self.stemColor = [.6, .5, .2] 199 | self.leafColor = [0, .3, 0] 200 | self.colorScale = .6 201 | self.showBase = showBase 202 | self.stemModel = showBase.loader.loadModel("models/plants/ThickStem.bam") 203 | self.healthyLeafGreen= [0, .35, .02] 204 | self.healthyStemGreen= [102/255, 28/255, 64/255] 205 | 206 | self.stemColorWhenDied = None 207 | self.leafColorWhenDied = None 208 | 209 | 210 | 211 | iterations = 500 212 | gro = age / iterations 213 | temp = { 'time' : 0, 214 | 215 | 'soilwater' : 350, 216 | 'airwater' : 25, 217 | 'temperature' : 20, 218 | 219 | 'volume' : 3000.0, 220 | 'tankwater' : 0.0, 221 | 'pipewater' : 0.0, 222 | 'energy' : 0, 223 | 224 | 'led' : 0, 225 | 'wpump' : False, 226 | 'fan' : False} 227 | temp['humidity'] = airwater_to_humid(temp['airwater'], 228 | temp['temperature']) 229 | for i in range(iterations): 230 | self.grow(temp, gro, minimum_light + 100) 231 | 232 | for leaf in self.leaves: 233 | leaf.setDroopFrac(droop) 234 | 235 | self.health = min(1, plant_health + .005) 236 | 237 | def change_health(self, amount): 238 | self.health += amount 239 | if self.health > 1: self.health = 1 240 | if self.health < .1: self.health = .1 241 | 242 | def newLeafCheck(self, light): 243 | if self.cumulative_health > .7 * 1 * day and len(self.leaves) < 2: 244 | self.leaves += leaf_pair(True, (1, 0), self.showBase) 245 | if self.cumulative_health > .7 * 6 * day and len(self.leaves) < 4 and light > minimum_light: 246 | angle = (68 + random() * 5) * pi / 180 247 | self.leaves += leaf_pair(False, (cos(angle), sin(angle)), self.showBase) 248 | if self.cumulative_health > .7 * 17 * day and len(self.leaves) < 6 and light > minimum_light * .5: 249 | angle = (124 + random() * 5) * pi / 180 250 | self.leaves += leaf_pair(False, (cos(angle), sin(angle)), self.showBase) 251 | 252 | def growAmount(self, growth_amount, light, lightfrac, env_params, duration): 253 | growth_per = growth_amount / (1 + sqrt(len(self.leaves))) 254 | #Grow the stem 255 | if(light > minimum_light and self.stem_height < .2): 256 | self.stem_height += stem_rate * growth_per 257 | elif light < minimum_light and self.stem_height < 1.35: 258 | self.stem_height += stem_rate * (growth_per + len(self.leaves) * growth_per * (1 - lightfrac)) 259 | growth_per *= lightfrac 260 | #Grow the leaves 261 | for leaf in self.leaves: 262 | leaf.grow(self.health, growth_per, lightfrac, env_params['soilwater'], duration) 263 | 264 | def grow(self, env_params, duration, light): 265 | 266 | if self.health < .2: 267 | self.timeBelow2 += duration 268 | 269 | if self.timeBelow2 > howLongBelow2: 270 | if self.stemColorWhenDied == None: 271 | self.stemColorWhenDied = self.stemColor 272 | self.leafColorWhenDied = self.leafColor 273 | deadFrac = (self.timeBelow2 - howLongBelow2) / deathTime 274 | if deadFrac > 1: return 275 | for i in [0, 1, 2]: 276 | self.stemColor[i] = deadFrac * deadColor[i] + (1 - deadFrac) * self.stemColorWhenDied[i] 277 | self.leafColor[i] = deadFrac * deadColor[i] + (1 - deadFrac) * self.leafColorWhenDied[i] 278 | return 279 | 280 | 281 | if(self.delay > 0): 282 | self.delay -= duration 283 | return 284 | 285 | #Update health based on env 286 | 287 | temp = env_params['temperature'] 288 | min_temp = optimal_temperature[0]; max_temp = optimal_temperature[1] 289 | temp_health_delta = (duration * temp_health_rate * 290 | ((temp - min_temp) if (temp < min_temp) else 291 | (max_temp - temp) if (temp > max_temp) else 1)) 292 | 293 | sw = env_params['soilwater'] 294 | min_sw = optimal_soilwater[0]; max_sw = optimal_soilwater[1] 295 | sw_health_delta = (duration * 296 | (sw_health_dry_rate * (sw - min_sw) 297 | if sw < min_sw else 298 | sw_health_wet_rate * (max_sw - sw) 299 | if sw > max_sw else sw_health_rate)) 300 | 301 | humid = env_params['humidity'] 302 | min_humid = optimal_humidity[0]; max_humid = optimal_humidity[1] 303 | humid_health_delta = (duration * humidity_health_rate * 304 | ((humid - min_humid) if humid < min_humid else 305 | (max_humid - humid) if humid > max_humid else 1)) 306 | light_health_delta = (duration * 307 | (light_health_up_rate if light < minimum_light else 308 | light_health_down_rate * (light - minimum_light))) 309 | 310 | health_delta = (temp_health_delta + sw_health_delta + 311 | humid_health_delta + light_health_delta) 312 | ''' 313 | if (health_delta < -0.0001): 314 | print("HD: %.4f, T: %.4f, SW: %.4f, H: %.4f, L: %.4f" 315 | %(health_delta, temp_health_delta, sw_health_delta, humid_health_delta, light_health_delta)) 316 | print(" T: %.2f, SW: %.2f, H: %.2f, L: %.2f" 317 | %(env_params['temperature'], env_params['soilwater'], 318 | env_params['humidity'], light)) 319 | ''' 320 | self.change_health(health_delta) 321 | 322 | #Update plant based on health, env 323 | growth_amount = self.health * duration 324 | self.cumulative_health += growth_amount 325 | 326 | 327 | 328 | #change color 329 | lightfrac = max(.001, light / minimum_light if light < minimum_light else 1) 330 | 331 | if lightfrac < 1: 332 | self.colorScale = max(0, self.colorScale - green_rate * duration) 333 | else: 334 | self.colorScale = min(1, self.colorScale + green_rate * duration) 335 | #color scale goes up if enough light, down if not 336 | for i in [0, 1, 2]: 337 | self.stemColor[i] = (self.healthyStemGreen[i] * (self.colorScale)) + ((1 - self.colorScale) * sicklyStemGreen[i]) 338 | self.leafColor[i] = self.healthyLeafGreen[i] * (self.colorScale) + (1 - self.colorScale) * sicklyLeafGreen[i] 339 | 340 | 341 | self.newLeafCheck(light) 342 | 343 | self.growAmount(growth_amount, light, lightfrac, env_params, duration) 344 | 345 | 346 | class Radish(Plant): 347 | def __init__(self, node, age, droop, lank, h, showbase): 348 | super(Radish, self).__init__(node, age, droop, lank, h, showbase) 349 | self.healthyLeafGreen = [0, .35, .02] 350 | self.healthyStemGreen = [102/255, 28/255, 64/255] 351 | #6 lank > 0, 4 if lank > .3, 2 if lank > .6 352 | self.leaves = self.leaves[:6 - 2 * min(2, int(lank / .3))] 353 | self.colorScale = 1 - lank 354 | self.stem_height *= (1 + 4 * lank) 355 | for leaf in self.leaves: 356 | leaf.start_position *= (1 + 1 * lank) 357 | 358 | class Lettuce(Plant): 359 | def __init__(self, node, age, droop, lank, h, showbase): 360 | super(Lettuce, self).__init__(node, age, droop, lank, h, showbase) 361 | self.healthyLeafGreen = [0, .35, .02] 362 | self.healthyStemGreen = [0, .35, .02] 363 | #6 lank > 0, 5 if lank > .15, 4 if lank > .3, ... 2 364 | self.leaves = self.leaves[:6 - min(4, int(lank / .15))] 365 | self.colorScale = 1 - lank 366 | self.stem_height *= (1 + 5 * lank) 367 | 368 | def growAmount(self, growth_amount, light, lightfrac, env_params, duration): 369 | growth_per = growth_amount / (1 + sqrt(len(self.leaves))) 370 | #Grow the stem only if too dark 371 | if(light > minimum_light and self.stem_height < .08): 372 | self.stem_height += stem_rate * growth_per 373 | elif light < minimum_light and self.stem_height < .3: 374 | self.stem_height += stem_rate * (growth_per + len(self.leaves) * growth_per * (1 - lightfrac)) 375 | growth_per *= lightfrac 376 | #Grow the leaves 377 | for leaf in self.leaves: 378 | leaf.grow(self.health, growth_per, lightfrac, env_params['soilwater'], duration) 379 | 380 | def newLeafCheck(self, light): 381 | if self.cumulative_health > .7 * 1 * day and len(self.leaves) < 2: 382 | self.leaves += lettuce_leaf_pair(True, (1, 0), self.showBase) 383 | if self.cumulative_health > .7 * 3 * day and len(self.leaves) < 3 and light > minimum_light * .1: 384 | angle = (68 + random() * 5) * pi / 180 385 | self.leaves += lettuce_leaf_single(False, (cos(angle), sin(angle)), self.showBase) 386 | if self.cumulative_health > .7 * 6 * day and len(self.leaves) < 4 and light > minimum_light * .3: 387 | angle = (68 + random() * 5) * pi / 180 388 | self.leaves += lettuce_leaf_single(False, (-cos(angle), -sin(angle)), self.showBase) 389 | if self.cumulative_health > .7 * 10 * day and len(self.leaves) < 5 and light > minimum_light * .5: 390 | angle = (124 + random() * 5) * pi / 180 391 | self.leaves += lettuce_leaf_single(False, (cos(angle), sin(angle)), self.showBase) 392 | if self.cumulative_health > .7 * 13 * day and len(self.leaves) < 6 and light > minimum_light * .7: 393 | angle = (124 + random() * 5) * pi / 180 394 | self.leaves += lettuce_leaf_single(False, (-cos(angle), -sin(angle)), self.showBase) 395 | -------------------------------------------------------------------------------- /lib/render.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #David Buffkin 3 | 4 | from direct.showbase.ShowBase import ShowBase 5 | from panda3d.core import AmbientLight, DirectionalLight, LightAttrib, PointLight, Spotlight, PerspectiveLens 6 | from panda3d.core import LVector3 7 | from panda3d.core import TextNode 8 | from direct.task import Task 9 | from panda3d.core import loadPrcFileData 10 | from direct.gui.OnscreenText import OnscreenText 11 | from panda3d.core import ClockObject 12 | from panda3d.core import AudioSound 13 | from panda3d.core import GraphicsEngine, Filename 14 | from panda3d.core import WindowProperties 15 | from sys import exit 16 | import random 17 | import plant 18 | import math 19 | from time import sleep, time 20 | from terrabot_utils import clock_time 21 | from environment import max_daylight, day_fraction, airwater_to_humid 22 | 23 | # Importing math constants and functions 24 | from math import pi, sin, cos 25 | 26 | import atexit 27 | 28 | class Terrarium(ShowBase): 29 | 30 | def __init__(self, shown, t0, initTime, leafDroop, lankiness, plant_health): 31 | loadPrcFileData('', 'win-size 1024 768') 32 | loadPrcFileData("", "window-type none") 33 | 34 | ShowBase.__init__(self) 35 | 36 | if shown: 37 | self.openMainWindow(type = "onscreen") 38 | props = WindowProperties() 39 | props.setTitle('TerraBot Simulator') 40 | self.win.requestProperties(props) 41 | else: 42 | self.openMainWindow(type = "offscreen") 43 | 44 | base.disableMouse() # Allow manual positioning of the camera 45 | #camera.setPosHpr(-20, 0, -3, -90, 12, 0) # Under 46 | camera.setPosHpr(-20, 0, 7, -90, -12, 0) # Normal 47 | #camera.setPosHpr(0, 0, 30, 0, -90, 0) #TOP 48 | 49 | self.pic = False 50 | self.loc = None 51 | self.shown = shown 52 | self.lastRender = time() 53 | self.minRenderRate = 0.5 # frequency, in seconds 54 | 55 | self.start_time = t0 56 | 57 | atexit.register(self.userExit) 58 | self.BASE_TEXT = ''' 59 | Pump: OFF 60 | Fans: OFF 61 | LEDs: 255 62 | ''' 63 | 64 | self.BASE_TEXT2 = \ 65 | ''' 66 | Time: {:s} 67 | Light level: 0 68 | Temperature: 20 C 69 | Soil moisture: 0 70 | Humidity: 50% 71 | Volume: 3000 ml 72 | Weight: 100 g 73 | Speedup: 1x 74 | '''.format(clock_time(t0 + initTime)) 75 | 76 | self.lastTime = initTime 77 | self.droop = leafDroop 78 | self.lankiness = lankiness 79 | self.plant_health = plant_health 80 | 81 | 82 | #self.accept('escape', self.userExit) 83 | self.accept('r', self.resetCam) 84 | 85 | self.loadModels() 86 | self.setupLights() 87 | self.setupText() 88 | self.setupText2() 89 | self.setupSensorCam() 90 | self.setTankWater(0) 91 | self.setBackgroundColor(.8, .8, .8, 1) 92 | 93 | self.keys = {} 94 | for key in ['arrow_left', 'arrow_right', 'arrow_up', 'arrow_down', 95 | 'a', 'd', 'w', 's']: 96 | self.keys[key] = 0 97 | self.accept(key, self.push_key, [key, 1]) 98 | self.accept('shift-%s' % key, self.push_key, [key, 1]) 99 | self.accept('%s-up' % key, self.push_key, [key, 0]) 100 | 101 | self.fanonSound = loader.loadSfx('sounds/fanon.wav') 102 | self.pumponSound = loader.loadSfx('sounds/pumpon.wav') 103 | self.fanonSound.setLoop(True) 104 | self.pumponSound.setLoop(True) 105 | 106 | #SetupCamera 107 | self.heading = -90.0 108 | self.pitch = -12.0 109 | self.camera.setPos(-20, 0, 7) 110 | 111 | self.picNextFrame = False 112 | 113 | self.taskMgr.add(self.update, 'main loop') 114 | 115 | 116 | def resetCam(self): 117 | camera.setPosHpr(-20, 0, 7, -90, -12, 0) 118 | self.heading = -90.0 119 | self.pitch = -12.0 120 | 121 | def setupSensorCam(self): 122 | #use the same size as the main window 123 | xsize, ysize = self.getSize() 124 | #TESTING purposes 125 | #self.accept("space", self.takeAndStorePic, ["test.png"]) 126 | 127 | #Create the camera's buffer : GraphicsOutput 128 | self.camBuffer = GraphicsEngine.makeParasite(self.graphicsEngine, host=self.win, name="camera", sort=0, x_size = xsize, y_size = ysize) 129 | 130 | self.sensorCam = self.makeCamera(self.camBuffer, camName="sensorCam") 131 | 132 | self.sensorCam.reparentTo(render) 133 | self.sensorCam.setPos(0, 5.56, 4.17) 134 | self.sensorCam.setHpr(180, -12.5, 0) 135 | self.camBuffer.setClearColorActive(True) 136 | self.camBuffer.setClearColor((.8, .8, .8, 1)) 137 | 138 | def takeAndStorePic(self, location): 139 | self.pic = True 140 | self.loc = location 141 | 142 | def setupText(self): 143 | self.textpanel = OnscreenText( 144 | text=self.BASE_TEXT, parent=base.a2dTopLeft, 145 | style=1, font = loader.loadFont('courier.ttf'), fg=(1, 1, 1, 1), pos=(0.06, -0.06), 146 | align=TextNode.ALeft, scale=.05) 147 | 148 | def setupText2(self): 149 | self.textpanel2 = OnscreenText( 150 | text=self.BASE_TEXT2, parent=base.a2dTopRight, 151 | style=1, font = loader.loadFont('courier.ttf'), fg=(1, 1, 1, 1), pos=(-0.06, -0.06), 152 | align=TextNode.ARight, scale=.05) 153 | 154 | def loadModels(self): 155 | self.terrarium = render.attachNewNode('terrarium') 156 | self.terrarium.reparentTo(render) 157 | self.terrarium.setScale(2.6) 158 | 159 | self.t_rings = loader.loadModel('models/Rings.bam') 160 | self.t_rings.reparentTo(self.terrarium) 161 | 162 | self.t_pump = loader.loadModel('models/Pump.bam') 163 | self.t_pump.reparentTo(self.terrarium) 164 | 165 | self.t_table = loader.loadModel('models/Table.bam') 166 | self.t_table.reparentTo(self.terrarium) 167 | self.t_table.setTransparency(True) 168 | 169 | self.t_glass = loader.loadModel('models/Glass.bam') 170 | self.t_glass.reparentTo(self.terrarium) 171 | self.t_glass.setTransparency(True) 172 | self.t_glass.setTwoSided(True) 173 | 174 | self.t_arduino = loader.loadModel('models/Arduino.bam') 175 | self.t_arduino.reparentTo(self.terrarium) 176 | 177 | self.t_boards = loader.loadModel('models/Boards.bam') 178 | self.t_boards.reparentTo(self.terrarium) 179 | 180 | self.t_raspi = loader.loadModel('models/Raspi.bam') 181 | self.t_raspi.reparentTo(self.terrarium) 182 | 183 | self.t_lights = loader.loadModel('models/Lights.bam') 184 | self.t_lights.reparentTo(self.terrarium) 185 | self.t_lights.setTwoSided(True) 186 | 187 | self.t_reservoir = loader.loadModel('models/Reservoir.bam') 188 | self.t_reservoir.reparentTo(self.terrarium) 189 | self.t_reservoir.setTransparency(True) 190 | 191 | self.t_reservoirLid = loader.loadModel('models/ReservoirLid.bam') 192 | self.t_reservoirLid.reparentTo(self.terrarium) 193 | 194 | self.t_reservoirWater = loader.loadModel('models/ReservoirWater.bam') 195 | self.t_reservoirWater.reparentTo(self.terrarium) 196 | self.t_reservoirWater.setTransparency(True) 197 | 198 | self.t_fanon = loader.loadModel('models/Fans_on.bam') 199 | self.t_fanon.reparentTo(self.terrarium) 200 | self.t_fanon.hide() 201 | 202 | self.t_fanonblades = loader.loadModel('models/Fans_on_blades.bam') 203 | self.t_fanonblades.reparentTo(self.terrarium) 204 | self.t_fanonblades.hide() 205 | self.t_fanonblades.setTransparency(True) 206 | 207 | self.t_fanoff = loader.loadModel('models/Fans_off.bam') 208 | self.t_fanoff.reparentTo(self.terrarium) 209 | 210 | self.t_growmat = loader.loadModel('models/Growmat.bam') 211 | self.t_growmat.reparentTo(self.terrarium) 212 | 213 | self.t_piping = loader.loadModel('models/Piping.bam') 214 | self.t_piping.reparentTo(self.terrarium) 215 | 216 | self.t_tankwater = loader.loadModel('models/Tankwater.bam') 217 | self.t_tankwater.reparentTo(self.terrarium) 218 | self.t_tankwater.setTransparency(True) 219 | 220 | self.t_sensors1 = loader.loadModel('models/Sensors1.bam') 221 | self.t_sensors1.reparentTo(self.terrarium) 222 | 223 | self.t_sensors2 = loader.loadModel('models/Sensors2.bam') 224 | self.t_sensors2.reparentTo(self.terrarium) 225 | 226 | self.t_camera = loader.loadModel('models/Camera.bam') 227 | self.t_camera.reparentTo(self.terrarium) 228 | 229 | self.t_colorsBase = loader.loadModel('models/ColorsBase.bam') 230 | self.t_colorsBase.reparentTo(self.terrarium) 231 | #self.t_colorsBase.setPos(0.73, 0, 0) 232 | self.t_colorsBase.setPos(-0.18, 0, 0.1) 233 | self.t_colorsBase.setScale(0.75, 1, 1) 234 | self.t_colorsBase.setColor(.93, .93, .93, 1) 235 | 236 | self.t_colorsRed = loader.loadModel('models/ColorSquare.bam') 237 | self.t_colorsRed.reparentTo(self.terrarium) 238 | self.t_colorsRed.setPos(0.7-0.91, 0, 0.1) 239 | self.t_colorsRed.setColor(1, 0, 0, 1) 240 | 241 | self.t_colorsGreen = loader.loadModel('models/ColorSquare.bam') 242 | self.t_colorsGreen.reparentTo(self.terrarium) 243 | self.t_colorsGreen.setPos(0.43-0.91, 0, 0.1) 244 | self.t_colorsGreen.setColor(0, 1, 0, 1) 245 | 246 | self.t_colorsBlue = loader.loadModel('models/ColorSquare.bam') 247 | self.t_colorsBlue.reparentTo(self.terrarium) 248 | self.t_colorsBlue.setPos(0.17-0.91, 0, 0.1) 249 | self.t_colorsBlue.setColor(0, 0, 1, 1) 250 | 251 | self.plants = [] 252 | self.plantsNode = render.attachNewNode('plants') 253 | self.plantsNode.reparentTo(self.terrarium) 254 | 255 | self.t_measureStick = loader.loadModel('models/MeasureStick.bam') 256 | self.t_measureStick.setScale(0.48) 257 | self.t_measureStick.setPos(-0.01, .8, 1.625) 258 | self.t_measureStick.reparentTo(self.terrarium) 259 | 260 | for i, x in enumerate((-.56, .56)): 261 | for j, y in enumerate((-1.03, -.7, -.355, -.01, .34, .683, 1.03)): 262 | node = render.attachNewNode('lettuce' + str(i) + str(j)) 263 | node.reparentTo(self.plantsNode) 264 | node.setPos(x, y, 1.14) 265 | node.setScale(.3) 266 | self.plants += [plant.Lettuce(node, self.lastTime, self.droop, self.lankiness, self.plant_health, self)] 267 | 268 | for i, x in enumerate((-.185, .185)): 269 | for j, y in enumerate((-1.03, -.7, -.355, -.01, .34, .683, 1.03)): 270 | node = render.attachNewNode('radish' + str(i) + str(j)) 271 | node.reparentTo(self.plantsNode) 272 | node.setPos(x, y, 1.14) 273 | node.setScale(.2) 274 | self.plants += [plant.Radish(node, self.lastTime, self.droop, self.lankiness, self.plant_health, self)] 275 | 276 | self.reRenderPlants() 277 | 278 | 279 | # Panda Lighting 280 | def setupLights(self): 281 | global ambientLight 282 | 283 | ambientLight = AmbientLight("ambientLight") 284 | ambientLight.setColor((.8, .8, .8, 1)) 285 | render.setLight(render.attachNewNode(ambientLight)) 286 | 287 | lightPositions = [(.5, 1, 1.8), (-.5, 1, 1.8), (.5, 0, 1.8), \ 288 | (-.5, 0, 1.8), (.5, -1, 1.8), (-.5, -1, 1.8)] 289 | self.lights = [PointLight('Light{}'.format(i)) for i in range(6)] 290 | for i, l in enumerate(self.lights): 291 | l.setColor((0, 0, 0, 1)) 292 | pln = render.attachNewNode(l) 293 | pln.reparentTo(self.terrarium) 294 | pln.setPos(lightPositions[i]) 295 | render.setLight(pln) 296 | 297 | 298 | def spinCameraTask(self, task): 299 | 300 | angleDegrees = task.time * 10.0 301 | angleRadians = angleDegrees * (pi / 180.0) 302 | self.camera.setPos(20 * sin(angleRadians), -20 * cos(angleRadians), 7) 303 | self.camera.setHpr(angleDegrees, -12, 0) 304 | ''' 305 | temp = int(.5 * task.time) 306 | self.setFans(temp % 2 == 1) 307 | ''' 308 | return Task.cont 309 | 310 | 311 | def setWater(self, volume): 312 | if(volume < .5): 313 | self.t_reservoirWater.hide() 314 | return 315 | self.t_reservoirWater.setScale(1, 1, float(volume) / (170 * 18)) # This is (max_waterlevel * volume_rate) 316 | 317 | def setTankWater(self, volume): 318 | if(volume < .5): 319 | self.t_tankwater.hide() 320 | return 321 | self.t_tankwater.setScale(1, 1, volume / (170 * 18)) 322 | 323 | 324 | def setFans(self, on): 325 | if on: 326 | self.t_fanoff.hide() 327 | self.t_fanon.show() 328 | self.t_fanonblades.show() 329 | else: 330 | self.t_fanoff.show() 331 | self.t_fanon.hide() 332 | self.t_fanonblades.hide() 333 | 334 | def setLights(self, val): 335 | # New grow lights are soft yellow/white 336 | level = 0.8*min(1, val/255.0) 337 | for l in self.lights: 338 | l.setColor((level, level, 0.8*level, 1)) 339 | 340 | def setSoilColor(self, soilwater): 341 | mult = 1 - soilwater / 800.0 342 | if mult < 0: 343 | mult = 0.0 344 | self.t_growmat.setColorScale(.7 + mult / 3, .7 + mult / 3, .7 + mult / 3, 1) 345 | 346 | def setAmbient(self, time): 347 | global ambientLight 348 | sunlight = max(0.2, day_fraction(time)) 349 | ambientLight.setColor((sunlight, sunlight, sunlight, 1)) 350 | 351 | def setUpdates(updates): 352 | self.updates = updates 353 | 354 | def fansound(self, fan): 355 | if not self.shown: 356 | self.fanonSound.stop() 357 | return 358 | if fan and self.fanonSound.status() == AudioSound.READY: 359 | self.fanonSound.play() 360 | if not fan and self.fanonSound.status() == AudioSound.PLAYING: 361 | self.fanonSound.stop() 362 | 363 | def pumpsound(self, pump): 364 | if not self.shown: 365 | self.pumponSound.stop() 366 | return 367 | if pump and self.pumponSound.status() == AudioSound.READY: 368 | self.pumponSound.play() 369 | if not pump and self.pumponSound.status() == AudioSound.PLAYING: 370 | self.pumponSound.stop() 371 | 372 | def reRenderPlants(self): 373 | # I think panda was occasionally dying b/c it was rendering too often 374 | # (it actually was a multi-threading issue, but still reasonable to 375 | # limit the rendering rate). 376 | now = time() 377 | if (now - self.lastRender < self.minRenderRate): return 378 | self.lastRender = now 379 | 380 | for testPlant in self.plants: 381 | baseStem = testPlant.stemModel 382 | baseStem.reparentTo(testPlant.node) 383 | stemFrac = testPlant.stem_height 384 | baseStem.setScale(.5 + .5 * stemFrac, .5 + .5 * stemFrac, stemFrac ) 385 | testPlant.node.setHpr(testPlant.rotation) 386 | sr, sg, sb = testPlant.stemColor 387 | lr, lg, lb = testPlant.leafColor 388 | baseStem.setColor(sr, sg, sb, 1) 389 | #to model Leaf leafToModel on plant testPlant 390 | for leafToModel in testPlant.leaves: 391 | leaf = leafToModel.leafModel 392 | leaf.reparentTo(testPlant.node) 393 | leaf.setScale(leafToModel.size) 394 | rotation = None 395 | if leafToModel.start_position.x == 0: 396 | rotation = 90 if leafToModel.start_position.y > 0 else 270 397 | else: 398 | rotation = 180 / math.pi * math.atan(leafToModel.start_position.y / leafToModel.start_position.x) 399 | if(leafToModel.start_position.x < 0): 400 | rotation += 180 401 | leaf.setHpr(rotation, 0, -leafToModel.angle) 402 | leaf.setPos(leafToModel.start_position + LVector3(0, 0, stemFrac)) 403 | stem = leafToModel.stemModel 404 | stem.reparentTo(testPlant.node) 405 | stem.setPos(0, 0, stemFrac) 406 | leafStemLength= leafToModel.start_position.length() 407 | leafStemFrac = leafStemLength / plant.max_leafstem_length 408 | stem.setScale(.8 * leafStemFrac, .8 * leafStemFrac, leafStemLength) 409 | 410 | #Annoying to have to get this, but here we go 411 | bottom = math.sqrt(leafToModel.start_position.x ** 2 + leafToModel.start_position.y ** 2) 412 | angle = 90 if bottom == 0 else 180 / math.pi * math.atan(leafToModel.start_position.z / bottom) 413 | 414 | stem.setHpr(rotation, 0, 90 - angle) 415 | 416 | #testPlant.node.setColor(r, g, b, 1) 417 | rm, gm, bm = leafToModel.colorRands 418 | #stem.setColor(.8 * sr + .2 * lr, .8 * sg + .2 * lg / 2, .8 * sb + .2 * lb / 2, 1) 419 | stem.setColor(.5 * sr + .5 * lr + rm, .5 * sg + .5 * lg + gm, .5 * sb + .5 * lb, 1 + bm) 420 | 421 | leaf.setColor(lr + rm, lg + gm, lb + bm, 1) 422 | 423 | 424 | def update_env_params(self, params, speedup, light, weight): 425 | self.setWater(params['volume']) #Reservoir update 426 | self.setLights(params['led']) #LED update 427 | self.setFans(params['fan']) #Fan Update 428 | self.setTankWater(params['tankwater']) #Tankwater update 429 | self.setSoilColor(params['soilwater']) 430 | self.setAmbient(params['time']) 431 | 432 | for plant in self.plants: 433 | plant.grow(params, params['time'] - self.lastTime, light) 434 | self.lastTime = params['time'] 435 | 436 | if not self.shown: return 437 | 438 | #self.reRenderPlants() # Do this only within the render loop! 439 | 440 | #Stats panel : 441 | healths = [p.health for p in self.plants] 442 | avgH = sum(healths)/len(healths) 443 | avgH = int(avgH * 100) 444 | 445 | self.textpanel.text = \ 446 | ''' 447 | Pump: {} 448 | Fans: {} 449 | LEDs: {} 450 | 451 | Plant Health: {}% 452 | '''.format('ON' if params['wpump'] else 'off', \ 453 | 'ON' if params['fan'] else 'off', \ 454 | params['led'], avgH) 455 | 456 | self.textpanel2.text = \ 457 | ''' 458 | Time: {:s} 459 | Light level: {:01.0f} 460 | Temperature: {:04.1f} C 461 | Soil moisture: {:03.1f} 462 | Humidity: {:02.0f}% 463 | Volume: {:04.1f} ml 464 | Weight: {:04.1f} g 465 | Speedup: {}x 466 | '''.format(clock_time(self.start_time + params['time']), light, \ 467 | params['temperature'], 2*params['soilwater'], \ 468 | params['humidity'], params['volume'], weight, speedup) 469 | 470 | self.fansound(params['fan']) 471 | self.pumpsound(params['wpump']) 472 | 473 | 474 | def push_key(self, key, value): 475 | """Stores a value associated with a key.""" 476 | self.keys[key] = value 477 | 478 | def update(self, task): 479 | if self.shown: 480 | """Updates the camera based on the keyboard input.""" 481 | delta = globalClock.getDt() * 1.4 482 | move_x = delta * 7 * -self.keys['a'] + delta * 3 * self.keys['d'] 483 | move_z = delta * 7 * self.keys['s'] + delta * 3 * -self.keys['w'] 484 | self.camera.setPos(self.camera, move_x, -move_z, 0) 485 | self.heading += (delta * 30 * self.keys['arrow_left'] + 486 | delta * 30 * -self.keys['arrow_right']) 487 | self.pitch += (delta * 30 * self.keys['arrow_up'] + 488 | delta * 30 * -self.keys['arrow_down']) 489 | self.camera.setHpr(self.heading, self.pitch, 0) 490 | 491 | self.reRenderPlants() 492 | 493 | if(self.picNextFrame): 494 | if self.loc == None: 495 | print("No location specified") 496 | else: 497 | self.camBuffer.saveScreenshot(Filename(self.loc)) 498 | self.picNextFrame = False 499 | 500 | if self.pic: 501 | if not self.shown: 502 | self.reRenderPlants() 503 | self.pic = False 504 | self.picNextFrame = True 505 | return task.cont 506 | 507 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autonomous Agents TerraBot # 2 | 3 | - [Overview](#overview) 4 | - [Installation](#installation) 5 | - [Software Architecture](#terrabot-software-architecture) 6 | - [ROS Communication](#ros-communication) 7 | - [Understanding the System](#understanding-the-system) 8 | - [TerraBot Node](#terrabot-node) 9 | + [Command line arguments](#command-line-arguments) 10 | + [Run time commands](#run-time-commands) 11 | - [Arduino Node](#arduino-node) 12 | - [Agent Node](#agent-node) 13 | + [Interactive Agent](#interactive-agent) 14 | - [Simulator](#simulator) 15 | - [Running the Simulator](#running-the-simulator) 16 | - [Graphics](#graphics) 17 | - [Baseline File](#baseline-file) 18 | - [Speeding up Time](#speeding-up-time) 19 | - [Additional Items](#additional-items) 20 | - [Frequency](#frequency) 21 | - [Camera](#camera) 22 | - [Interference](#interference-file) 23 | - [Time Series](#time-series) 24 | - [Testing](#testing) 25 | - [Test File](#test-file) 26 | + [DELAY](#delay) 27 | + [STOP/QUIT](#stop-or-quit) 28 | + [Variables](#variables) 29 | + [WHENEVER](#whenever) 30 | + [WAIT](#wait) 31 | + [ENSURE](#ensure) 32 | + [SET](#set) 33 | + [PRINT](#print) 34 | 35 | ## Overview ## 36 | Welcome to the Autonomous Agents TerraBot project! For this project you and your partners 37 | will be given a greenhouse outfitted with multiple sensors and actuators and a rockwool "soil" planted with radish and lettuce 38 | seeds. The goal of the assignment is to provide the best environment 39 | for the plants during a two week grow cycle. Each cycle will come with new challenges, so be prepared! 40 | 41 | Each greenhouse contains two temperature, humidity, light, moisture, and weight sensors, one water-level sensor, one current sensor, and one camera. For the redundant sensors, data values are contained in arrays, where index 0 contains the sensor reading of the first sensor and index 1 contains the sensor reading of the second sensor. For the current sensor, the first index is the current, the second is the energy usage, to date. The camera is a bit different - you send a command to take a picture and the image is saved to a given file location. 42 | 43 | The greenhouse also has three actuators - fans, LEDs, and a water pump. The LED's light level can be set between 0-255. The fans and pump can be turned on (True) or off (False). In addition, you can adjust the frequency at which a particular sensor reports data. 44 | 45 | | Name (topic name) | Description | Message Type | Range | 46 | | ---------------------------- | ----------------------------------------------------------- | ----------------- | ---------- | 47 | | **_Sensors_** |**_Use these to determine the system's state_** | **_—_** | **_—_** | 48 | | Temperature (temp) | Internal temperature of the greenhouse (in Celcius) | Int32Array | in Celcius | 49 | | Humidity (humid) | Internal relative humidity (%) | Int32Array | 0-100 | 50 | | Light (light) | Light intesnity in the greenhouse | Int32Array | 0-1000 | 51 | | Soil Moisture (smoist) | The moisture of the "soil" (higher is wetter) | Int32Array | 280-600 | 52 | | Weight (weight) | Weighs soil and plants (in grams); Total weight is average of the 2 sensors | Float32Array | | 53 | | Water level (level) | Height of the water in the reservoir (in mm) | Float32 | 0-180 | 54 | | Camera | Captures a photograph of stystem's state | String | — | 55 | | **_Actuators_** |**_Use these to adjust the system's state_** | **_—_** | **_—_** | 56 | | Fan (fan) | Toggle whether the fans are on or off | Bool | True-False | 57 | | LED (led) | Adjust the power of the system's LED light fixture | Int32 | 0-255 | 58 | | Water Pump (wpump) | Toggle whether the water pump is on or off | Bool | True-False | 59 | | **_Additional_** |**_Use these to adjust the system's state_** | **_—_** | **_—_** | 60 | | Frequency (freq) | Adjust the sensing frequency of a specific sensor | See lib/frqmsg.py | — | 61 | 62 | Note: The water pump pumps approximately 1cm depth of water (~.93cups) per minute. 63 | 64 | For optimal growing parameters, see TerraBot/agents/limits.py 65 | 66 | ### Installation ### 67 | You can install the TerraBot software to run on your own computer, using Docker, or on the Raspbery Pi of the greenhouse. 68 | The instructions for using Docker are found [docker/DockerSETUP.md](docker/DockerSETUP.md); the instructions for the Raspbery Pi are in [RPISETUP.md](RPISETUP.md). Older versions of the software used VirtualBox, and installation instructions can be found [here](VIRTUALBOX.md) for Windows and Linux and [here](M1SETUP.md) for Macs. The Docker version, however, is preferred since it is lighter weight and significantly faster than the VirtualBox (especially on Macs). 69 | 70 | ### TerraBot Software Architecture ### 71 | An Arduino communicates directly with the sensors and actuators, converts the raw data into clean data, and then forwards that data to a Raspberry Pi. 72 | The Raspberry Pi, running ROS (Robot Operating System), receives the sensor data and makes it available 73 | to your AI agent in the formats above. Additionally, it receives your agent's actuator commands, as defined above, 74 | and relays them back to the Arduino. 75 | 76 | For development purposes, there is a simulator that does most everything that the Arduino does on the real hardware. The simulator can display graphics of the terrarium, showing the changes as actuators are turned on and off and plants grow. Just as with the real hardware, you need to maintain a good environment for the simulated plants, or they will not flourish, and even die. **Note: What happens in the simulator is similar to the real greenhouse, but not exact. Thus, you should be sure to thoroughly test all your code on the real hardware before deploying your agent to grow plants!** 77 | 78 | The diagram below shows an overview of the connections between the different nodes (ovals) in the system. 79 | Pay particular attention to the topics (rectangles) connected to the agent, as those are the ones you will be using to 80 | regulate your greenhouse. 81 | 82 | ![System Diagram](https://github.com/reidgs/TerraBot/blob/master/system_diagram.jpg) 83 | 84 | ## ROS Communication ## 85 | In order to get your code working with the ROS messaging system, 86 | follow the tutorial on the ROS website [here](https://wiki.ros.org/ROS/Tutorials). 87 | Please look over the tutorials concerning ROS communication (Nodes, Topics, Publishers + Subscribers) to gain a general understanding of ROS. 88 | You may find the other tutorials there helpful as well. 89 | 90 | The TerraBot consists of three ROS *nodes* or processes: 91 | * the Arduino (real hardware) or farduino (simulator) node communicates with the sensors and actuators 92 | * the TerraBot node publishes the data, optionally adding noise to it, listens for actuation commands, and optionally checks whether a given set of constraints hold on the execution of the system 93 | * the Agent node (which you will write) controls the actuators based on sensor readings and other data 94 | 95 | The Arduino and TerraBot nodes *publish* sensor data and *subscribe* to actuation commands over ROS topics 96 | of specific types (shown above). 97 | Your agent will be a ROS *node* which subscribes to each of the sensors topics, plans actions, and publishes to 98 | actuators. 99 | 100 | Be sure to check 101 | your code to make sure that it is publishing and subscribing as you intend when bug fixing. 102 | 103 | ## Understanding the System ## 104 | Running TerraBot.py will start up three processes. Besides starting the TerraBot and Arduino nodes described above, it will also start up the _roscore_ node, which regulates communications between all the other nodes. 105 | 106 | ### TerraBot Node ### 107 | The TerraBot node transfers actuator data from your agent to the Arduino/farduino (simulator) node: 108 | It subscribes to the topics to which your agent publishes and publishes to the topics to which the Arduino/simulator subscribes. 109 | 110 | Vice versa, the TerraBot node transfers sensor data from the Arduino/simulator node to your agent: 111 | It subscribes to the topics to which the Arudino/simulator publishes and publishes to the topics to which your agent subscribes. 112 | 113 | In the transfering process, the data received by the TerraBot node are (optionally) passed though functions specified by an _interference file_. In order to reliably simulate errors which may happen by chance if run in the real world, an interference file may specify adding noise to the sensor data, or cause a sensor or actuator to act as if it were broken (including being stuck on). A robust agent should be able to handle a variety of such issues, which often do manifest themselves on the real hardware. 114 | 115 | #### Command Line Arguments #### 116 | The following command line arguments are available when running TerraBot.py: 117 | ``` 118 | -h (--help): show help message and exit 119 | -v (--verbose): print more messages describing the workings of the system 120 | -l (--log): log all message traffic (in an auto-generated subdirectory of Log) 121 | -m (--mode) serial | sim : mode to run in (default is serial on the real greenhouse, sim is simulator) 122 | -g (--graphics): Show graphical representation of simulation (only in sim mode) 123 | -s (--speedup) : Increase simulated time (only in sim mode; automatically decreases speedup when fans or pump are on) 124 | -b (--baseline) : Initial clock time, sensor, and actuator values (only in sim mode) 125 | -i (--interference) : Set of instructions for when to manipulate sensor and actuator values 126 | -t (--test) : Tests correct execution based on a set of constraints that describe expected behavior 127 | -a (--agent) : Your agent program (if "none" - the default - the agent node must be run externally) 128 | -f (--fixedshutter) : Use the given value as the camera shutter speed, rather than adjusting dynamically based on light level 129 | ``` 130 | #### Run Time Commands #### 131 | The following commands can be given while TerraBot.py is running: 132 | ``` 133 | q: gracefully quit the system 134 | t: print out the current time (especially useful when running in simulated mode without the graphics displayed) 135 | ``` 136 | 137 | **WARNING: If you ^C out, sometimes not all the processes are killed. You would then need to kill them (the roscore, arduino/simulator, and agent processes) individually. Especially if extra ROS nodes are running, unexpected interactions may occur. To simplify this, just run _./cleanup_processes_ from the TerraBot directory.** 138 | 139 | ### Arduino Node ### 140 | Sensors and actuators are being controlled in the Arduino node: 141 | 142 | * The Arduino reads in all sensor data and translates them from raw values to more meaningful values that are then published to the TerraBot node. 143 | * The Arduino also subscribes to topics containing data values that are published by the TerraBot node. These meaningful values are translated to its raw form, with which the Arduino can write to the actuators. 144 | 145 | All communication to and from the Arduino is done via the TerraBot node, meaning you should 146 | never access the same topics as the Arduino. When not using the actual hardware, use the [simulator](#Simulator). 147 | 148 | ### Agent Node ### 149 | The agent node is how you autonomously control the greenhouse. You will be able to access sensor data by subscribing to the topics to which the TerraBot publishes. You will also be able to write data to the actuators by publishing to the topics to which the TerraBot subscribes. Your agent will base its decisions on the sensor data it receives, some saved state data, and a schedule of behaviors to run. 150 | 151 | #### Interactive Agent #### 152 | The interactive agent (agents/interactive_agent.py) enables you to interact with the TerraBot (either the real hardware or the simulator) using the same topics that your agent will be using. This program can be very useful for exploring how the TerraBot works, debugging, and checking on the statues of sensors. You start the interactive agent in a separate window from TerraBot. The command line options are: 153 | ``` 154 | -s (--sim), to indicate that TerraBot is using the simulator 155 | -l (--log), which prints the sensor data (this latter flag is not really very useful anymore, but is there for historical reasons) 156 | ``` 157 | 158 | Once the interactive agent is started and connects to the TerraBot (while the two programs can be started in any order, it is usually better to start TerraBot first), you can send commands to the TerraBot via text input. The available commands are: 159 | ``` 160 | q: quit 161 | f on / f off: turn fans on/off 162 | p on / p off: turn pump on/off 163 | l on / l off: turn leds on/off 164 | l : turn leds to the given value, between 0 and 255 165 | c : take an image and store in the given file 166 | r : set the frequency of publishing the named sensor's values, in terms of seconds (e.g. 10 is 10 Hz, 0.1 is once every ten seconds, default is 1, for all sensors) 167 | e : set the period of publishing the named sensor's values, in seconds (for convenience - period is 1/frequency) 168 | s : set the speedup time to the given value (simulator only) 169 | v: print the current sensor values 170 | ``` 171 | *Note that the interactive agent can be run concurrently with other agents, so that you can view sensor values or turn on/off actuators manually while your autonomous system is running (this should be used only during development, not during testing).* 172 | 173 | ## Simulator ## 174 | 175 | Since access to the actual greenhouse is somewhat limited, and growing plants can take a long time, we have provided a graphics-based simulator so that you can test your code before deploying it on the actual hardware. 176 | 177 | The simulator uses the same ROS topics as, and works in a way almost identical to, the Arduino node. The code for your agent and the TerraBot node is the exact same 178 | as it would be on the Raspberry Pi. However, instead of having an Arduino node, the simulator comes with a 179 | farduino (fake Arduino) node that mimics the actions of the Arduino node. This difference should 180 | in no way affect how your code operates and should not be (significantly) noticeable from the perspective 181 | of your agent node. 182 | 183 | We are working to try to get the ranges of the sensors, the nominal values, and the rate of change of the sensors, to match the real world. The values are approximate, however, so you should not assume that the real world and the simulator will behave exactly the same. 184 | 185 | ### Running the Simulator ### 186 | To run the simulator, start TerraBot.py with the simulator mode flag (-m sim), and optionally 1) -g, if you wish to see a graphical representation of the terrarium; 2) -s , to specify the multiplier you wish for the speed; and 3) -b , a file that contains baseline (starting) values for the sensors, actuators, and the time which you would like the simulator to start at (seconds since midnight, day 1 of the simulated run). 187 | 188 | >`./TerraBot.py -m sim -g -b param/default_baseline.bsl` 189 | 190 | ### Graphics ### 191 | We have included an option to display a graphical representation of the TerraBot hardware and the state of the simulation (see below). The display also includes a representation of the growing plants, showing how they develop (and either thrive or die) over time. 192 | 193 |

194 | 195 |

196 | 197 | The graphics display is enabled using the -g flag when running TerraBot. The graphics window contains a 3D model of the terrarium and the plants within, along with a text panel containing information about the current environment, e.g., whether the pump is on or off, humidity, etc. Sounds are played to represent the pump and fan. The arrow keys and WASD can be used to navigate the scene, and the viewport can be reset by pressing 'r'. The camera will take pictures directly from this scene, from the perspective of the blue camera model, as in the real terrarium. Note that the camera can still be used even when the -g flag is not included, and the images produced will be equivalent to those taken with the graphics on. See the instructions in [docker/DockerSETUP.md](docker/DockerSETUP.md) for how to view the graphics when running the software from within Docker. 198 | 199 | ### Baseline File ### 200 | The simulator starts up with default values for the sensors, actuators, and clock. You can create a 'baseline' (.bsl) text file to specify different initial values. This enables you to set up specific scenarios that you want to test for, rather than waiting for them to arise at some point in time during the simulation. 201 | 202 | The file param/default_baseline.bsl illustrates the format of the baseline files. In particular, to specify a value, add a line in the format "name = value", optionally appending a comment (any characters after a "#" are ignored). The possible values you can change are : 203 | > start, wpump, fan, led, temperature, humidity, smoist, wlevel, tankwater, plant_health, leaf_droop, lankiness 204 | 205 | The "start" value is used to specify a starting time for the simulation. The plants will be grown automatically up to this date, according to the "leaf_droop", "lankiness", and "plant_health" values (see below). The format is either an int (the time in seconds) OR in the form DAY-HH:MM:SS (note that start=0 and start=1-00:00:00 are equivalent). 206 | 207 | The "plant_health" value (a float in [0, 1]) will set the health of the plants from 0% healthy (0) to 100% healthy (1). 208 | 209 | The "leaf_droop" value (a float in [0, 1]) will cause the leaves of the plants to droop. This normally happens when there is not enough water for the plant, but you can set it manually here. 210 | 211 | The "lankiness" value (a float in [0, 1]) determines how lanky the plants are. A lanky plant is usually indicative of a lack of sunlight and vice versa, but you can set it manually here. 212 | 213 | The rest are pretty self-explanatory. If a value is not specified, the value in param/default_baseline.bsl is what will be used. All numbers can be floats, though "led" will be cast to an int. 214 | 215 | ### Speeding up Time ### 216 | Note that much of what happens in a greenhouse happens very slowly, thus a speedup of 100 or more is recommended for development and testing. However, what happens when actuators are on can happen very quickly (e.g., watering takes just a few seconds). To accommodate this, the simulator automatically sets the speed low when either the pump or fans are on and then sets the speed to the user-desired value whenever they are both are off. Your agent can also publish a "speedup" message to change the default speedup during run time, but this is not standard practice (and has no effect when operating on the actual hardware). 217 | 218 | For example to run the simulator at 100x speed (i.e., 100 seconds of simulated time for every second of wall clock time), use: 219 | >`./TerraBot.py -m sim -s 100` 220 | 221 | *Note: to ensure consistency between your code in simulation and with the actual hardware, you should refrain from referring to outside functions (OS time.time()) and should instead refer to the ROS time topic via rospy.get_time().* 222 | 223 | ## Additional Items ## 224 | There are some additional items that you may need to know about in order to complete the assignments. Specifically, they involve the health of your agent, the frequency of sensor readings, and access to the camera. 225 | 226 | ### Camera ### 227 | The camera is different from the rest of the sensors, as it is controlled directly by the Pi, and not by the Arduino. You can take a picture using the 'camera' topic; the single argument is the name of the file to store the JPEG image. **Note that, if you are using relative path name, the path is relative to the directory where you ran TerraBot, not to the directory you ran your agent!** **Make sure the file path includes a directory that actually exists, otherwise the image will not be saved!** 228 | 229 | ### Frequency ### 230 | It is our intention to eventually have you manage how much energy and water you use. Since reading the sensors takes energy, there is a way to tell the system how frequently for the Arduino will read from the sensors. The more often the sensors are read, the more accurate your data will be, but the more energy you will use as well. 231 | The 'freq' topic is used to indicate how often the sensors will be read. Use the "tomsg" function in freqmsg.py to convert a sensor name/frequency to a string that can be handled by ROS (see agents/interactive_agent.py for how exactly to do this). 232 | 233 | For example, a frequency of 10 polls the given sensors at 10 Hz; a frequency of 0.1 polls the sensors once every 10 seconds. 234 | Notice that this setting is variable, meaning it can be changed over the course of the deployment by sending a new 'freq' message. 235 | This is useful, for instance, if you want to read the sensors infrequently until some event occurrs (such as a behavior that uses that sensor is enabled) and then change the frequency to do better closed-loop control. The default is 1 for all sensors. 236 | 237 | ### Interference File ### 238 | Probably the one factor that makes reliable autonomous systems hard to develop is that unexpected events occur frequently. While these can be rare and occur unpredictably, it is important for your agent to detect and handle anomalies that arise. To that end, we have implemented a mechanism where it can seem as though sensors and actuators are noisy or not working correctly. Which sensors and actuators are malfunctioning and what times, and how they are malfunctioning, are described in an *interference file*. The interference file contains a schedule of times to manipulate the data being transferred between nodes. Interference files can be used in conjunction with the simulator or the real hardware, although it is mainly intended to be used with the simulator to simulate sensor and actuator failures. 239 | 240 | For sensor data and the LEDs, the available modification constraints are: 241 | ``` 242 | normal : transfers data directly without any modifications 243 | noise : slightly modifies data before transfering, adding Gaussian noise 244 | : the sensor is stuck at that value 245 | ``` 246 | For the fan and water pump, the available modification constraints are: 247 | ``` 248 | normal : transfers data directly without any modifications (the default) 249 | off : the actuator is stuck off 250 | on : the actuator is stuck on 251 | ``` 252 | The fan, pump, LEDs, and water level (wlevel) all are singletons, and the modification constraints are indicated thusly: 253 | ``` 254 | wpump = off 255 | ``` 256 | The rest of the sensors are redundant, and one has to indicate the modification constraints for each, separately: 257 | ``` 258 | humidity = [normal, noise] 259 | temperature = [noise, 20.0] 260 | ``` 261 | One can specify a sequence of modifications that take effect at different times, by placing an "AT" command before a set of modification constraints. 262 | ``` 263 | AT 1-03:00:00 # Starting at 3am, the first day 264 | light = [normal, 0] # Right light sensor is stuck off 265 | fan = off # Fans are not working 266 | 267 | AT 1-04:30:00 # Starting at 4:30am 268 | light = [normal, normal] # Lights are now working 269 | ``` 270 | Note that 1) comments can be placed at the end of lines and 2) the time format is day-HH:MM:SS, where 1-00:00:00 is the beginning of the run. 271 | 272 | *If no file is passed in, there will be no intereference in the transfer of data.* 273 | 274 | ### Time Series ### 275 | It is often useful to visualize how the sensor and actuator data change over time. For instance, to see how quickly environmental variables (such as moisture and humidity) change or to see whether the fans turn on at regular intervals. To facilitate this, the time series program (agents/time_series.py) provides a way to visualize the sensor data and actuator commands over time. The program connects to the TerraBot node and subscribes to all the topics that the TerraBot handles (except for frequency and speedup). It can optionally log the data for later replay, so that you can analyze how things are working or input data to a machine learning algorithm. 276 | 277 | The program displays a set of windows, one for each topic, and plots the topic values over time (the X-axis). A window scrolls when the values approach the right side of the window. The Y-axis for the fans and pump are discrete (either 0/off or 1/on), while the Y-axis for the sensors and LEDs are continuous. The sensor values are plotted in green and the actuator values in blue. 278 | 279 | The command line options are: 280 | ``` 281 | -h (--help): show help message and exit 282 | -s (--sim): use the simulator 283 | -w (--width) : width of the windows, in hours (indicates how much data can be shown at once and how quickly it scrolls) 284 | -l (--log) : log the sensor data to the file 285 | -r (--replay) : replay the sensor data from the file 286 | -s (--speedup) : playback speed for replaying (not to be confused with the speedup of the simulator) 287 | ``` 288 | In addition, there are two run-time commands: 289 | ``` 290 | q: quit the program 291 | v: print the current sensor and actuator values 292 | ``` 293 | 294 | ## Testing ## 295 | Your programming assignments will be graded automatically. Test files (.tst) will indicate what behaviors are expected to occur, and the tester will check to see that all the conditions are successfully met. The test files to use can be specified on the command line using the -t option, followed by the .tst file. 296 | 297 | We strongly recommend creating your own baseline and test files to evaluate situations that you think might occur in real life - experience shows that unexpected combinations of factors often occur in practice, especially when running the system for two weeks growing real plants. Creating a comprehensive set of test environments (and sharing these with others) is just good practice. 298 | 299 | ### Test File ### 300 | Test files consist of several parts. First, one can specify a baseline and/or interference file within a test file. This is indicated as such: 301 | ``` 302 | BASELINE = smoist_up.bsl 303 | INTERFERENCE = smoist_up.inf 304 | ``` 305 | **NOTE: All the keywords (such as WHENEVER, WAIT, FOR) must be in all CAPS** 306 | 307 | #### DELAY #### 308 | Next, one can specify how long to wait before applying any of the test constraints (see below). It is useful to delay starting to test for a period of time to give the agent a chance to initialize (for instance, if it takes a while to produce an initial schedule). This constraint can be specified in one of two ways: 309 | ``` 310 | DELAY FOR # Wait "value" seconds before starting to test 311 | DELAY UNTIL day-HH:MM:SS # Wait until the given time (measured from the start of the TerraBot operating) 312 | ``` 313 | So, for instance, "DELAY UNTIL 1-03:30:00" would wait for 3.5 hours before starting to apply the test constraints. As above, the last DELAY constraint in the file is used. 314 | 315 | ### STOP or QUIT ### 316 | Third, one can specify how long to test for. The STOP constraint just ends testing, the QUIT constraint stops testing and causes the TerraBot program to quit. For both variants, the time until ending can be specified either using seconds or date-time: 317 | ``` 318 | STOP AFTER 36000 # Stop testing after 10 hours 319 | QUIT AT 3-23:59:59 # Run testing for 3 full days, and then quit the simulator 320 | ``` 321 | Note again that comments can be place at the end of lines 322 | 323 | ### VARIABLES ### 324 | You can use any of the sensor or actuator values (light, humidity, temperature, smoist, wlevel, weight, camera, fan, pump, led). 'fan' and 'pump' are Booleans, 'camera' is a string (the location of the image file), and the rest are numbers. For sensors that are in pairs (light, humidity, temperature, smoist, weight) you can access each value separately using *sensor*_raw[n], where 'n' is 0 or 1 (for instance, smoist_raw[0]). You can also make use of the dicts 'limits' and 'optimal', which are defined in agents/limits.py. 325 | 326 | Finally, you can use the function 'enabled' to determine whether a behavior has been enabled (assuming your agent publishes that information). For instance 'enabled("LowerHumidBehavior")' is True when the behavior is running and False otherwise. 327 | 328 | ### WHENEVER ### 329 | The bulk of test files consist of WHENEVER constraints. These are subtests that are activiated whenever a particular condition is met. WHENEVER constraints consist of a trigger and a body, which is a sequence of WAIT, ENSURE, SET, and PRINT subconstraints that specify what behavior is expected of the system. 330 | 331 | The triggers for WHENEVER constraints can be a Boolean relation or a date-time. The Boolean relations can consist of numbers, sensor values, and actuator states (**light, temperature, humidity, smoist, weight, current, wlevel, led, wpump, fan, camera, time, mtime**). **time** is the clock time, in seconds; **mtime** is the number of seconds past midnight -- you can get the hour of the day using (mtime//3600). The date-time trigger is specified in terms of the first occurrence, and every time it finishes, another 24 hours are added on to the trigger time. Examples include: 332 | ``` 333 | WHENEVER wpump # Every time the pump is turned on 334 | WHENEVER 1-00:00:00 # Every midnight 335 | WHENEVER temperature < 22 and (mtime//3600) >= 6 # Every time the temperature is below 22 after 6am 336 | WHENEVER smoist_raw[0] < 450 or smoist_raw[1] < 450 # Every time either soil moisture sensor gets below 450 337 | ``` 338 | Note that at most one instance of a given WHENEVER constraint will be active at a given time. 339 | 340 | The body of a WHENEVER constraint indicates a sequence of subconstraints that must hold for the WHENEVER constraint to be successful. If one of the subconstraints fails to hold, then the WHENEVER constraint fails and a failure message is printed out. If all of the subconstraints hold, then the WHENEVER constraint succeeds and a success message is printed out. In either case, the constraint is deactivated (awaiting to be triggered again). The subconstraints are described below, along with several examples. 341 | 342 | You can also use the WHILE condition to indicate a condition that must hold while the WHENEVER constraint is active. When the WHILE condition becomes false, the whole WHENEVER constraint ends successfully, no matter where in the list of subconstraints it is currently. For instance: 343 | ``` 344 | WHENEVER enabled("LowerHumidBehavior") and humidity >= 90 WHILE enabled("LowerHumidBehavior") 345 | ``` 346 | activates when the LowerHumidBehavior is enabled and the humidity is high, but stays active only as long as the behavior is enabled. 347 | 348 | #### WAIT #### 349 | The WAIT subconstraint will wait a certain amount of time for a condition to evaluate to true. If the condition is true then this subconstraint succeeds and control is passed to the next one (if any or, if not, the whole WHENEVER constraint succeeds). If the amount of time passes without the condition being true, then the subconstraint (and the whole WHENEVER constraint) fails. As with other constraints, the time can be specified as a number of seconds or as a date-time: 350 | ``` 351 | WAIT temperature < 25 FOR 3600 # Wait an hour for the temperature to come below 25 C 352 | WAIT not led UNTIL 1-23:00:00 # Wait until 11pm for the LEDs to be turned off 353 | ``` 354 | In addition, a variant of the WAIT constraint can be used without a condition: 355 | ``` 356 | WAIT FOR 60 # Wait a minute before going on to the next subconstraint. 357 | ``` 358 | This variant always succeeds, after the given amount of time has passed. 359 | 360 | #### ENSURE #### 361 | The ENSURE subconstraint will ensure that some condition is true throughout the whole time period. If the condition is ever false then this subconstraint fails (as does the whole WHENEVER constraint). If the amount of time passes and the condition remains true during the whole time period, control is passed to the next subconstraint (if any or, if not, the whole WHENEVER constraint succeeds). As with other constraints, the time can be specified as a number of seconds or as a date-time: 362 | ``` 363 | ENSURE smoist < 700 FOR 3600 # Don't let things get too wet 364 | ENSURE not led UNTIL 2-06:59:59 # Lights must be off until just before 7am the next day 365 | ``` 366 | 367 | #### SET #### 368 | The SET constraint enables you to specify local variables that can be included in the constraint conditions. This provides the ability, for instance, to count the number of camera images taken during the day, or how much the soil moisture has changed since the last time the constraint was run. The general form is "SET = ", where "value" can be a combination of numbers, sensor values, and other variables. The constraint always succeeds. 369 | ``` 370 | SET last_humid = humidity 371 | SET dhumid = last_humid - humidity 372 | SET num_pics = num_pics + 1 373 | ``` 374 | 375 | Simple test examples are provided in the TerraBot/param directory. Here is a test, consisting of two WHENEVER constraints, for the behavior expected of the pump - that it should turn on soon after the soil moisture falls below some threshold and should not overwater the plants: 376 | ``` 377 | WHENEVER enabled('RaiseMoistBehavior') and smoist < 450 378 | WAIT wpump FOR 60 # Wait one minute for water pump to be on 379 | WAIT not wpump FOR 360 # Turn pump off before 6 minutes have elapsed 380 | WAIT smoist_raw[0] > 450 and smoist_raw[1] > 450 FOR 3600 # Wait an hour for both moisture sensors to be above threshold 381 | 382 | # Don't let pump overwater things 383 | WHENEVER wpump 384 | ENSURE smoist < 700 FOR 3600 385 | ``` 386 | 387 | #### PRINT #### 388 | The PRINT constraint enables you to print out information, useful for debugging the testing constraints. The syntax is like the **print** statement in Python, except without parentheses. You can use any of the variables that are allowable in WHENEVER, WAIT, and ENSURE statements (including local variables defined using SET). You can print out the time in string form using **clock_time(time)**. 389 | ``` 390 | PRINT "W1: %s %s %s" %(wlevel, (wlevel_start - wlevel), wpump_today) 391 | PRINT "Current temperature at %s: %d %d" %(clock_time(time), temperature_raw[0], temperature_raw[1]) 392 | ``` 393 | 394 | --------------------------------------------------------------------------------