├── 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 '' in text) else "text"
90 | message = {
91 | "message": {
92 | "toRecipients": to_addresses,
93 | "subject": subject,
94 | "body": {"contentType": content_type, "content": text},
95 | "attachments": msg_attachments(images, inline),
96 | },
97 | "saveToSentItems": "true"
98 | }
99 | response = requests.post('https://graph.microsoft.com/v1.0/me/sendMail',
100 | headers=headers, json=message)
101 | return response.status_code == 202
102 | except Exception as e:
103 | print('Failed to send:', e)
104 | return False
105 |
106 | # Here's a simple example (please don't actually use it as is, since it will spam me)
107 | '''
108 | images = []
109 | for file_name in ["../simulator.JPG", "../system_diagram.jpg"]:
110 | with open(file_name, 'rb') as f: images += [f.read()]
111 | if send("terrabot1@outlook.com", "reidgs@hotmail.com, reids@cs.cmu.edu",
112 | "Hello", '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 |
If you have access to make, simply invoke make build (or make rebuild if you want to rebuild from scratch)
14 |
Otherwise, look at the build or rebuild instructions in the Makefile and run those commands in a shell
15 |
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 |
19 |
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.
20 |
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:
21 |
(note: this is equivalent to cd'ing to the ROS_HW directory and invoking the script from there)
24 |
25 |
26 |
If a container is already running, the shell scripts will connect you to the existing container
27 |
You can also invoke the container from the Docker Desktop, but you will have to add the command-line arguments manually.
28 |
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 |
Invoke the shell scripts multiple times; the first invocation starts a new container and subsequent invocations connect a new terminal to that container.
36 |
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.
37 |
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.
38 |
You can open new terminals using xterm &, but to be seen you need to follow the instructions in the next section.
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 | 
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 |
--------------------------------------------------------------------------------