sending schedule to controller

pull/37/head
TimEvWw 2014-04-17 13:26:12 -01:00
parent bd0ee43a79
commit 321eaf1a86
14 changed files with 1065 additions and 1 deletions

View File

@ -0,0 +1,76 @@
development:
# Configure available database sessions. (required)
sessions:
# Defines the default session. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: farmbot_backend_development
# Provides the hosts the default session can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- localhost:27017
options:
# Change the default write concern. (default = { w: 1 })
# write:
# w: 1
# Change the default consistency model to primary, secondary.
# 'secondary' will send reads to secondaries, 'primary' sends everything
# to master. (default: primary)
# read: secondary_preferred
# How many times Moped should attempt to retry an operation after
# failure. (default: 30)
# max_retries: 30
# The time in seconds that Moped should wait before retrying an
# operation on failure. (default: 1)
# retry_interval: 1
# Configure Mongoid specific options. (optional)
options:
# Enable the identity map, needed for eager loading. (default: false)
# identity_map_enabled: false
# Includes the root model name in json serialization. (default: false)
# include_root_in_json: false
# Include the _type field in serializaion. (default: false)
# include_type_for_serialization: false
# Preload all models in development, needed when models use
# inheritance. (default: false)
# preload_models: false
# Protect id and type from mass assignment. (default: true)
# protect_sensitive_fields: true
# Raise an error when performing a #find and the document is not found.
# (default: true)
# raise_not_found_error: true
# Raise an error when defining a scope with the same name as an
# existing method. (default: false)
# scope_overwrite_exception: false
# Skip the database version check, used when connecting to a db without
# admin access. (default: false)
# skip_version_check: false
# Use Active Support's time zone in conversions. (default: true)
# use_activesupport_time_zone: true
# Ensure all times are UTC in the app side. (default: false)
# use_utc: false
test:
sessions:
default:
database: farmbot_backend_test
hosts:
- localhost:27017
options:
read: primary
# In the test environment we lower the retries and retry interval to
# low amounts for fast failures.
max_retries: 1
retry_interval: 0

View File

@ -0,0 +1,4 @@
---
:uuid: 40b8cfdf-92e9-450d-b603-1e9efbbecf47
:token: !binary |-
MzIwMTc3M2IwZDA3N2NmM2ZlZmQ5ZDQwZjNiZGE1NmI=

View File

@ -0,0 +1,71 @@
# ####################################
# Send something to farmbot controller
# ####################################
class FarmBotControllerComm
# send command to farmbot
def send_single_command(action, x, y, z, amount, speed, delay)
$skynet.confirmed = false
command =
{
:message_type => 'single_command',
:time_stamp => Time.now.to_f.to_s,
:command => {
:action => action,
:x => x,
:y => y,
:z => z,
:speed => speed,
:amount => amount,
:delay => delay
}}
$skynet.send_message($farmbot_uuid, command)
return wait_for_confirmation()
end
# send schedule to farmbot
def send_schedule(uuid, schedule)
$skynet.confirmed = false
sched_hash = schedule.to_hash
sched_hash[:message_type] = 'crop_schedule_update'
sched_hash[:time_stamp] = Time.now.to_f.to_s
puts sched_hash
$skynet.send_message(uuid, sched_hash)
return wait_for_confirmation()
end
def wait_for_confirmation
puts 'waiting for confirmation'
count = 0
while $skynet.confirmed != true and count < 10
sleep 0.5
print '.'
count += 1
end
puts ''
if $skynet.confirmed
puts 'confirmation received'
return true
else
puts 'confirmation timed out'
sleep 2
return false
end
end
end

View File

@ -0,0 +1,118 @@
#
# Classes to make a schedule that can be transmitted to the bot
#
class CropSchedule
attr_accessor :crop_id
def initialize
@commands = Array.new
end
def read_from_db(crop)
@crop_id = crop.crop_id
crop.scheduled_commands.each do |dbcommand|
command = CropScheduleCommand.new
command.read_from_db(dbcommand)
add_command(command)
end
end
def to_hash
command_nr = 0
schedule_hash = {:crop_id => @crop_id, :commands => {} }
commands = Hash.new
@commands.each do |command|
command_nr += 1
commands["command_#{command_nr}"]= command.to_hash
end
schedule_hash[:commands] = commands
return schedule_hash
end
def add_command(command)
@commands << command
end
end
class CropScheduleCommand
attr_accessor :scheduled_time
def initialize
@command_lines = Array.new
end
def read_from_db(command)
puts command
puts command.scheduled_time
@scheduled_time = command.scheduled_time
seq = 0
command.scheduled_command_lines.each do |dbline|
seq += 1
line = CropScheduleCommandLine.new
line.read_from_db(dbline, seq)
add_command_line(line)
end
end
def to_hash
sequence_nr = 0
command_hash = {:scheduled_time => @scheduled_time, :command_lines => {} }
command_lines = Hash.new
@command_lines.each do |line|
sequence_nr += 1
line.sequence_nr = sequence_nr
command_lines["command_line_#{sequence_nr}"]= line.to_hash
end
command_hash[:command_lines] = command_lines
return command_hash
end
def add_command_line(line)
@command_lines << line
end
end
class CropScheduleCommandLine
attr_accessor :action, :x, :y, :z, :amount, :speed, :sequence_nr
def read_from_db(line, sequence_nr)
@action = line.action
@x = line.coord_x
@y = line.coord_y
@z = line.coord_z
@amount = line.amount
@speed = line.speed
@sequence_nr = sequence_nr
end
def to_hash
line = {
:action => @action ,
:x => @x ,
:y => @y ,
:z => @z ,
:amount => @amount ,
:speed => @speed ,
:sequence_nr => @sequence_nr
}
return line
end
end

View File

@ -0,0 +1,157 @@
require 'bson'
require 'mongo'
require 'mongoid'
# Data classes
# This class is dedicated to retrieving and inserting commands into the schedule
# queue for the farm bot Mongo is used as the database, Mongoid as the
# databasemapper
class Command
include Mongoid::Document
embeds_many :commandlines
field :plant_id
field :crop_id
field :scheduled_time
field :executed_time
field :status
end
class Commandline
include Mongoid::Document
embedded_in :command
#belongs_to :command
field :action
field :coord_x
field :coord_y
field :coord_z
field :speed
field :amount
end
class Refresh
include Mongoid::Document
field :name
field :value
end
# Access class for the database
class DbAccess
def initialize
Mongoid.load!("config/mongo.yml", :development)
@last_command_retrieved = nil
@refresh_value = 0
@refresh_value_new = 0
@new_command = nil
end
def test
db_connection = Mongo::Connection.new
db_farmbot = db_connection['farmbot_development']
db_schedule = db_farmbot['schedule']
db_connection.database_names.each do |name|
db = db_connection.db(name)
db.collections.each do |collection|
puts "#{name} - #{collection.name}"
end
end
end
def create_new_command(scheduled_time, crop_id)
@new_command = Command.new
@new_command.scheduled_time = scheduled_time
@new_command.crop_id = crop_id
end
def add_command_line(action, x = 0, y = 0, z = 0, speed = 0, amount = 0)
if @new_command != nil
line = Commandline.new
line.action = action
line.coord_x = x
line.coord_y = y
line.coord_z = z
line.speed = speed
line.amount = amount
if @new_command.commandlines == nil
@new_command.commandlines = [ line ]
else
@new_command.commandlines << line
end
end
end
def save_new_command
if @new_command != nil
@new_command.status = 'scheduled'
@new_command.save
end
increment_refresh
end
def clear_schedule
Command.where(
:status => 'scheduled',
:scheduled_time.ne => nil
).order_by([:scheduled_time,:asc]).each do |command|
command.status = 'deleted'
command.save
end
end
def clear_crop_schedule(crop_id)
Command.where(
:status => 'scheduled',
:scheduled_time.ne => nil,
:crop_id => crop_id
).order_by([:scheduled_time,:asc]).each do |command|
command.status = 'deleted'
command.save
end
end
def get_command_to_execute
@last_command_retrieved = Command.where(
:status => 'scheduled',
:scheduled_time.ne => nil
).order_by([:scheduled_time,:asc]).first
@last_command_retrieved
end
def set_command_to_execute_status(new_status)
if @last_command_retrieved != nil
@last_command_retrieved.status = new_status
@last_command_retrieved.save
end
end
def check_refresh
r = Refresh.where(:name => 'FarmBotControllerSchedule').first_or_initialize
@refresh_value_new = r.value.to_i
return @refresh_value_new != @refresh_value
end
def save_refresh
@refresh_value = @refresh_value_new
end
def increment_refresh
r = Refresh.where(:name => 'FarmBotControllerSchedule').first_or_initialize
r.value = r.value.to_i + 1
r.save
end
end

View File

@ -0,0 +1,134 @@
# This module holds our data definitions to store all basic plant and watering data
# Mongo is used as the database, Mongoid as the databasemapper
require 'bson'
require 'mongo'
require 'mongoid'
#require 'bson_ext'
# The different farmbots are stored here
class FarmBot
include Mongoid::Document
embeds_many :crops
field :active
field :name
field :environmental_coefficient
field :uuid
# also needs user settings, security and whatsnot
end
# The list of crops tended by one farm bot. The crop is planted as a seed (age = 0) or when it has already sprouted.
# Coordinates x, y and z are used to drive the robot to the right place
# The age of maturing and harvesting should be customized to local conditions
class Crop
include Mongoid::Document
embedded_in :farmbot
embeds_many :grow_coefficients
embeds_many :waterings
embeds_many :historic_actions
embeds_many :scheduled_commands
field :plant_type
field :coord_x
field :coord_y
field :coord_z
field :radius
field :height
field :status
field :date_at_planting
field :age_at_planting
field :age_at_fully_grown
field :age_at_harvest
field :valid_data
field :crop_id
end
# Coefficients are used by the evapotransporation system. It expresses the amount of water (mm/day) the plant needs for a good growth at a certain age
# The values for the coefficient is the result of the local climate reference value multiplied with the
# The age is represented as a precentage, where 100% is fully grown and 200% is ready for harvesting
# a typical curve for a crops. the Y axis is here a multiplication factor for the reference crops (fictional grass or alfalfa)
#
# 1.0 *****
# *| **
# * | ***
# * | |
# ** | |
# 0.1 *** | |
# | | |
# 0% 100% 200%
class GrowCoefficient
include Mongoid::Document
embedded_in :crop
field :age_in_percentage
field :amount_water_manual
end
# These are the times when the robot is supposed to water the crop
class Watering
include Mongoid::Document
embedded_in :crop
field :time
field :percentage
end
# A log of what happended to the plant. Waterings and rainfall are the most important probably
class HistoricAction
include Mongoid::Document
embedded_in :crop
field :start_time
field :stop_time
field :action
field :amount
end
# This is the schedule for the next hours/days that the bot has to execute. This is synchronized to the bot.
class ScheduledCommand
include Mongoid::Document
embedded_in :crop
embeds_many :scheduled_command_lines
field :crop_id
field :schedule_id
field :one_time_command
field :scheduled_time
field :command_id
end
class ScheduledCommandLine
include Mongoid::Document
embedded_in :scheduled_command
field :action
field :coord_x
field :coord_y
field :coord_z
field :speed
field :amount
end

View File

@ -0,0 +1,15 @@
require_relative 'skynet/skynet'
# The unfortunate use of globals in this project: The SocketIO library we use to
# talk to skynet stores blocks as lambdas and calls them later under a different
# context than that which they were defined. This means that even though we
# define the .on() events within the `Device` class, self does NOT refer to the
# device, but rather the current socket connection. Using a global is a quick
# fix to ensure we always have easy access to the device. Pull requests welcome.
$skynet = Skynet.new
#TODO: Daemonize this script:
#https://www.ruby-toolbox.com/categories/daemonizing

View File

@ -0,0 +1,49 @@
require 'securerandom'
module Credentials
# Stores a references to the credentials yml file, which is used to persist
# the user's skynet Token / ID across sessions. Returns String. parameterless
def credentials_file
'credentials.yml'
end
# Returns Hash containing the a :uuid and :token key. Triggers the creation of
# new credentials if the current ones are found to be invalid.
def credentials
if valid_credentials?
return load_credentials
else
return create_credentials
end
end
# Validates that the credentials file has a :uuid and :token key. Returns Bool
#
def valid_credentials?
if File.file?(credentials_file)
cred = load_credentials
return true if cred.has_key?(:uuid) && cred.has_key?(:token)
end
return false
end
# Uses the ruby securerandom library to make a new :uuid and :token. Also
# registers with a new device :uuid and :token on skynet.im . Returns Hash
# containing :uuid and :token key.
def create_credentials
hash = {
uuid: (@uuid = SecureRandom.uuid),
token: (@token = SecureRandom.hex)
}
`curl -s -X POST -d 'uuid=#{@uuid}&token=#{@token}&type=farmbot' \
http://skynet.im/devices`
File.open(credentials_file, 'w+') {|file| file.write(hash.to_yaml) }
return hash
end
### Loads the credentials file from disk and returns it as a ruby hash.
def load_credentials
return YAML.load(File.read(credentials_file))
end
end

View File

@ -0,0 +1,201 @@
require 'json'
#require './lib/database/commandqueue.rb'
require './lib/database/dbcommand.rb'
require 'time'
# Get the JSON command, received through skynet, and send it to the farmbot
# command queue Parses JSON messages received through SkyNet.
class MessageHandler
attr_accessor :message
def initialize
@dbaccess = DbAccess.new
@last_time_stamp = ''
end
# A list of MessageHandler methods (as strings) that a Skynet User may access.
#
def whitelist
["single_command","crop_schedule_update"]
end
# Main entry point for (Hash) commands coming in over SkyNet.
# {
# "message_type" : "single_command",
# "time_stamp" : 2001-01-01 01:01:01.001
# "command" : {
# "action" : "HOME X",
# "x" : 1,
# "y" : 2,
# "z" : 3,
# "speed" : "FAST",
# "amount" : 5,
# "delay" : 6
# }
# }
def handle_message(message)
puts 'handle_message'
#puts message
#puts message['message']
@message = message['message']
#fromUuid = message['fromUuid']
#puts fromUuid
requested_command = message['message']["message_type"].to_s.downcase
#puts requested_command
if whitelist.include?(requested_command)
#puts 'sending'
self.send(requested_command, message)
else
self.error(message)
end
end
# Handles an erorr (typically, an unauthorized or unknown message). Returns
# Hash.
def error
return {error: ""}
end
def single_command(message)
puts 'single_command'
#puts message
time_stamp = message['message']['time_stamp']
sender = message['fromUuid']
if time_stamp != @last_time_stamp
@last_time_stamp = time_stamp
# send the command to the queue
delay = message['message']['command']['delay']
action = message['message']['command']['action']
x = message['message']['command']['x']
y = message['message']['command']['y']
z = message['message']['command']['z']
speed = message['message']['command']['speed']
amount = message['message']['command']['amount']
delay = message['message']['command']['delay']
puts "[new command] received at #{Time.now} from #{sender}"
puts "[#{action}] x: #{x}, y: #{y}, z: #{z}, speed: #{speed}, amount: #{amount} delay: #{delay}"
@dbaccess.create_new_command(Time.now + delay.to_i,'single_command')
@dbaccess.add_command_line(action, x.to_i, y.to_i, z.to_i, speed.to_s, amount.to_i)
@dbaccess.save_new_command
$skynet.confirmed = false
command =
{
:message_type => 'confirmation',
:time_stamp => Time.now.to_f.to_s,
:confirm_id => time_stamp
}
$skynet.send_message(sender, command)
end
end
def crop_schedule_update(message)
puts 'crop_schedule_update'
#puts message
time_stamp = message['message']['time_stamp']
sender = message['fromUuid']
puts "time_stamp #{time_stamp}"
puts "sender #{sender}"
if time_stamp != @last_time_stamp
@last_time_stamp = time_stamp
message_contents = message['message']
#puts message_contents
crop_id = message_contents['crop_id']
puts crop_id
puts 'removing old crop schedule'
@dbaccess.clear_crop_schedule(crop_id)
message_contents['commands'].each do |command|
#puts command
#puts command.class
#puts command[0]
#puts command[0].class
#puts command[1]
#puts command[1].class
scheduled_time = Time.parse(command[1]['scheduled_time'])
@dbaccess.create_new_command(scheduled_time, crop_id)
#@dbaccess.create_new_command(Time.now, 'debug')
puts scheduled_time
puts Time.now
command[1]['command_lines'].each do |command_line|
action = command_line[1]['action']
x = command_line[1]['x']
y = command_line[1]['y']
z = command_line[1]['z']
speed = command_line[1]['speed']
amount = command_line[1]['amount']
puts "[#{action}] x: #{x}, y: #{y}, z: #{z}, speed: #{speed}, amount: #{amount}"
@dbaccess.add_command_line(action, x.to_i, y.to_i, z.to_i, speed.to_s, amount.to_i)
end
@dbaccess.save_new_command
end
# send the command to the queue
#delay = message['message']['command']['delay']
#action = message['message']['command']['action']
#x = message['message']['command']['x']
#y = message['message']['command']['y']
#z = message['message']['command']['z']
#speed = message['message']['command']['speed']
#amount = message['message']['command']['amount']
#delay = message['message']['command']['delay']
#puts "[new command] received at #{Time.now} from #{sender}"
#puts "[#{action}] x: #{x}, y: #{y}, z: #{z}, speed: #{speed}, amount: #{amount} delay: #{delay}"
#@dbaccess.create_new_command(Time.now + delay.to_i)
#@dbaccess.add_command_line(action, x.to_i, y.to_i, z.to_i, speed.to_s, amount.to_i)
#@dbaccess.save_new_command
puts 'sending comfirmation'
$skynet.confirmed = false
command =
{
:message_type => 'confirmation',
:time_stamp => Time.now.to_f.to_s,
:confirm_id => time_stamp
}
$skynet.send_message(sender, command)
end
end
end

View File

@ -0,0 +1,59 @@
require 'json'
require_relative 'credentials'
require_relative 'web_socket'
require_relative 'messagehandler.rb'
# The Device class is temporarily inheriting from Tim's HardwareInterface.
# Eventually, we should merge the two projects, but this is good enough for now.
class Skynet
include Credentials, WebSocket
attr_accessor :socket, :uuid, :token, :identified, :confirmed,
:confirmation_id
# On instantiation #new sets the @uuid, @token variables, connects to skynet
def initialize
super
identified = false
creds = credentials
@uuid = creds[:uuid]
@token = creds[:token]
@socket = SocketIO::Client::Simple.connect 'http://skynet.im:80'
@confirmed = false
create_socket_events
puts "uuid: #{@uuid}"
@message_handler = MessageHandler.new
end
def send_message(devices, message_hash )
@socket.emit("message",{:devices => devices, :message => message_hash})
end
# Acts as the entry point for message traffic captured from Skynet.im.
# This method is a stub for now until I have time to merge into Tim's
# controller code. Returns a MessageHandler object (a class yet created).
#def handle_message(channel, message)
def handle_message(message)
puts "> message received at #{Time.now}"
#puts message
if message.class.to_s == 'Hash'
@message_handler.handle_message(message)
end
if message.class.to_s == 'String'
message_hash = JSON.parse(message)
@message_handler.handle_message(message_hash)
end
rescue
raise "Runtime error while attempting to parse message: #{message}."
end
end

View File

@ -0,0 +1,33 @@
require 'socket.io-client-simple'
module WebSocket
### Bootstraps all the events for skynet in the correct order. Returns Int.
def create_socket_events
#OTHER EVENTS: :identify, :identity, :ready, :disconnect, :message
create_identify_event
create_message_event
end
#Handles self identification on skynet by responding to the :indentify with a
#:identity event / credentials Hash.
def create_identify_event
@socket.on :identify do |data|
self.emit :identity, {
uuid: $skynet.uuid,
token: $skynet.token,
socketid: data['socketid']}
$skynet.identified = true
end
end
### Routes all skynet messages to handle_event() for interpretation.
def create_message_event
#@socket.on :message do |channel, message|
# $skynet.handle_message(channel, message)
#end
@socket.on :message do |message|
$skynet.handle_message(message)
end
end
end

View File

@ -0,0 +1,44 @@
$farmbot_uuid = "063df52b-0698-4e1c-b2bb-4c0890019782"
require_relative 'lib/skynet/skynet'
require_relative 'lib/botcomm'
require_relative 'lib/cropschedule'
require_relative 'lib/database/dbcommand'
require_relative 'lib/database/dbfarmbot'
puts '[FarmBot schedule transmit]'
puts 'starting up'
# connecting to skynet framework
$skynet = Skynet.new
$farmbot_comm = FarmBotControllerComm.new
while $skynet.identified != true
sleep 0.5
print '.'
end
puts ''
puts 'connecting to database'
Mongoid.load!("config/mongo.yml", :development)
puts 'checking list of bots'
FarmBot.where(:active => true).order_by([:name,:asc]).each do |farmbot|
puts "checking bot #{farmbot.name} uuid #(farmbot.uuid)"
farmbot.crops.where(:valid_data => true).each do |crop|
puts "crop type=#{crop.plant_type} @ x=#{crop.coord_x} y=#{crop.coord_y}"
crop_schedule = CropSchedule.new
crop_schedule.read_from_db( crop )
$farmbot_comm.send_schedule(farmbot.uuid,crop_schedule)
end
end

View File

@ -0,0 +1,104 @@
$farmbot_uuid = "063df52b-0698-4e1c-b2bb-4c0890019782"
require_relative 'lib/device/device'
require_relative 'lib/botcomm'
puts '[FarmBot Remote Control]'
puts 'starting up'
# connecting to skynet framework
$device = Device.new
$farmbot_comm = FarmBotControllerComm.new
while $device.identified != true
sleep 0.5
print '.'
end
puts ''
# send schedule to farmbot
def send_singe_command(action, x, y, z, amount, speed, delay)
$farmbot_comm.send_single_command(action, x, y, z, amount, speed, delay)
end
$shutdown = 0
# just a little menu for testing
$move_size = 10
$command_delay = 0
while $shutdown == 0 do
system('cls')
system('clear')
puts '[FarmBot Controller Menu]'
puts ''
puts 'p - stop'
puts ''
puts "move size = #{$move_size}"
puts "command delay = #{$command_delay}"
puts ''
puts 'w - forward'
puts 's - back'
puts 'a - left'
puts 'd - right'
puts 'r - up'
puts 'f - down'
puts ''
puts 'z - home z axis'
puts 'x - home x axis'
puts 'c - home y axis'
puts ''
puts 'y - dose water'
puts ''
puts 'q - step size'
puts 'g - delay seconds'
puts ''
print 'command > '
input = gets
puts ''
case input.upcase[0]
when "P" # Quit
$shutdown = 1
puts 'Shutting down...'
when "O" # Get status
puts 'Not implemented yet. Press \'Enter\' key to continue.'
gets
when "Q" # Set step size
print 'Enter new step size > '
move_size_temp = gets
$move_size = move_size_temp.to_i if move_size_temp.to_i > 0
when "G" # Set step delay (seconds)
print 'Enter new delay in seconds > '
command_delay_temp = gets
$command_delay = command_delay_temp.to_i if command_delay_temp.to_i > 0
when "Y" # Water
send_single_command('DOSE WATER', 0, 0, 0, 15, 0, $command_delay)
when "Z" # Move to home
send_single_command('HOME Z', 0, 0, 0, 0, 0, $command_delay)
when "X" # Move to home
send_single_command('HOME X', 0, 0, 0, 0, 0, $command_delay)
when "C" # Move to home
send_single_command('HOME Y',0 ,0 ,-$move_size, 0, 0, $command_delay)
when "W" # Move forward
send_single_command('MOVE RELATIVE',0,$move_size, 0, 0, 0, $command_delay)
when "S" # Move back
send_single_command('MOVE RELATIVE',0,-$move_size, 0, 0, 0, $command_delay)
when "A" # Move left
send_single_command('MOVE RELATIVE', -$move_size, 0, 0, 0, 0, $command_delay)
when "D" # Move right
send_single_command('MOVE RELATIVE', $move_size, 0, 0, 0, 0, $command_delay)
when "R" # Move up
send_single_command('MOVE RELATIVE', 0, 0, $move_size, 0, 0, $command_delay)
when "F" # Move down
send_single_command("MOVE RELATIVE", 0, 0, -$move_size, 0, 0, $command_delay)
end
end

@ -1 +0,0 @@
Subproject commit 68e4f0cc61ce622cda65c79c2a3f13557077fb02