Farmbot-Web-App/app/models/celery_script_settings_bag.rb

308 lines
14 KiB
Ruby

# All configuration related to validation of sequences. This includes
# information such as which operators and labels are allowed, custom validations
# when a sequence is saved and related error messages that result.
# This module helps unclutter sequence.rb by sweeping CeleryScript config under
# the rug. Shoving configuration into a module is not a design pattern. Feedback
# welcome for refactoring of this code.
module CeleryScriptSettingsBag
class BoxLed
def self.name
"Raspberry Pi Box LED"
end
def self.exists?(id)
true # Not super important right now. - RC 22 JUL 18
end
end
# List of all celery script nodes that can be used as a varaible...
ANY_VARIABLE = %i(tool coordinate point identifier every_point)
PLANT_STAGES = %w(planned planted harvested sprouted)
ALLOWED_PIN_MODES = [DIGITAL = 0, ANALOG = 1]
ALLOWED_RPC_NODES = %w(home emergency_lock emergency_unlock read_status
sync check_updates power_off reboot toggle_pin
config_update calibrate execute move_absolute
move_relative write_pin read_pin send_message
factory_reset execute_script set_user_env wait
install_farmware update_farmware take_photo zero
install_first_party_farmware remove_farmware
find_home register_gpio unregister_gpio
set_servo_angle change_ownership dump_info)
ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware)
ALLOWED_CHAGES = %w(add remove update)
RESOURCE_NAME = %w(Device FarmEvent Image Log Peripheral Plant Point
Regimen Sequence Tool ToolSlot User GenericPointer)
ALLOWED_MESSAGE_TYPES = %w(success busy warn error info fun debug)
ALLOWED_CHANNEL_NAMES = %w(ticker toast email espeak)
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant)
ALLOWED_OPS = %w(< > is not is_undefined)
ALLOWED_AXIS = %w(x y z all)
ALLOWED_LHS_TYPES = [String, :named_pin]
ALLOWED_LHS_STRINGS = [*(0..69)].map{|x| "pin#{x}"}.concat(%w(x y z))
ALLOWED_SPEC_ACTION = %w(dump_info emergency_lock emergency_unlock power_off
read_status reboot sync take_photo)
STEPS = %w(_if execute execute_script find_home move_absolute
move_relative read_pin send_message take_photo wait
write_pin resource_update)
BAD_ALLOWED_PIN_MODES = '"%s" is not a valid pin_mode. Allowed values: %s'
BAD_LHS = 'Can not put "%s" into a left hand side (LHS) '\
'argument. Allowed values: %s'
BAD_SUB_SEQ = 'Sequence #%s does not exist.'
NO_SUB_SEQ = 'missing a sequence selection for `execute` block.'
BAD_REGIMEN = 'Regimen #%s does not exist.'
BAD_OP = 'Can not put "%s" into an operand (OP) argument. '\
'Allowed values: %s'
BAD_CHANNEL_NAME = '"%s" is not a valid channel_name. Allowed values: %s'
BAD_MESSAGE_TYPE = '"%s" is not a valid message_type. Allowed values: %s'
BAD_MESSAGE = "Messages must be between 1 and 300 characters"
BAD_RESOURCE_TYPE = '"%s" is not a valid resource_type. Allowed values: %s'
BAD_TOOL_ID = 'Tool #%s does not exist.'
BAD_PERIPH_ID = 'Peripheral #%s does not exist.'
BAD_PACKAGE = '"%s" is not a valid package. Allowed values: %s'
BAD_AXIS = '"%s" is not a valid axis. Allowed values: %s'
BAD_POINTER_ID = "Bad point ID: %s"
BAD_RESOURCE_ID = "Can't find %s with id of %s"
NO_PIN_ID = "%s requires a valid pin number"
BAD_POINTER_TYPE = '"%s" is not a type of point. Allowed values: %s'
BAD_PIN_TYPE = '"%s" is not a type of pin. Allowed values: %s'
BAD_SPEED = "Speed must be a percentage between 1-100"
PIN_TYPE_MAP = { "Peripheral" => Peripheral,
"Sensor" => Sensor,
"BoxLed3" => BoxLed,
"BoxLed4" => BoxLed }
CANT_ANALOG = "Analog modes are not supported for Box LEDs"
ALLOWED_PIN_TYPES = PIN_TYPE_MAP.keys
RESOURCE_UPDATE_ARGS = [:resource_type, :resource_id, :label, :value]
ONLY_ONE_COORD = "Move Absolute does not accept a group of locations"\
" as input. Please change your selection to a "\
"single location."
SCOPE_DECLARATIONS = [:variable_declaration, :parameter_declaration]
ALLOWED_EVERY_POINT_TYPE = %w(Tool GenericPointer Plant ToolSlot)
BAD_EVERY_POINT_TYPE = '"%s" is not a type of group. Allowed values: %s'
Corpus = CeleryScript::Corpus.new
.arg(:_else, [:execute, :nothing])
.arg(:_then, [:execute, :nothing])
.arg(:locals, [:scope_declaration])
.arg(:offset, [:coordinate])
.arg(:pin_number, [Integer, :named_pin]) # HETEROGENUS ARG TYPE => BAD
.arg(:data_value, ANY_VARIABLE)
.arg(:default_value,ANY_VARIABLE)
.arg(:location, ANY_VARIABLE)
.arg(:label, [String])
.arg(:milliseconds, [Integer])
.arg(:package, [String])
.arg(:pin_value, [Integer])
.arg(:radius, [Integer])
.arg(:rhs, [Integer])
.arg(:url, [String])
.arg(:value, [String, Integer, TrueClass, FalseClass])
.arg(:version, [Integer])
.arg(:x, [Integer, Float])
.arg(:y, [Integer, Float])
.arg(:z, [Integer, Float])
.arg(:pin_id, [Integer])
.arg(:pin_type, [String]) do |node|
within(ALLOWED_PIN_TYPES, node) do |val|
BAD_PIN_TYPE % [val.to_s, ALLOWED_PIN_TYPES.inspect]
end
end
.arg(:pointer_id, [Integer]) do |node, device|
bad_node = !Point.where(id: node.value, device_id: device.id).exists?
node.invalidate!(BAD_POINTER_ID % node.value) if bad_node
end
.arg(:pointer_type, [String]) do |node|
within(ALLOWED_POINTER_TYPE, node) do |val|
BAD_POINTER_TYPE % [val.to_s, ALLOWED_POINTER_TYPE.inspect]
end
end
.arg(:pin_mode, [Integer]) do |node|
within(ALLOWED_PIN_MODES, node) do |val|
BAD_ALLOWED_PIN_MODES % [val.to_s, ALLOWED_PIN_MODES.inspect]
end
end
.arg(:sequence_id, [Integer]) do |node|
if (node.value == 0)
node.invalidate!(NO_SUB_SEQ)
else
missing = !Sequence.exists?(node.value)
node.invalidate!(BAD_SUB_SEQ % [node.value]) if missing
end
end
.arg(:lhs, ALLOWED_LHS_TYPES) do |node|
case node
when CeleryScript::AstNode
# Validate `named_location` and friends.
else
# Validate strings.
within(ALLOWED_LHS_STRINGS, node) do |val|
BAD_LHS % [val.to_s, ALLOWED_LHS_STRINGS.inspect]
end
end
end
.arg(:op, [String]) do |node|
within(ALLOWED_OPS, node) do |val|
BAD_OP % [val.to_s, ALLOWED_OPS.inspect]
end
end
.arg(:channel_name, [String]) do |node|
within(ALLOWED_CHANNEL_NAMES, node) do |val|
BAD_CHANNEL_NAME % [val.to_s, ALLOWED_CHANNEL_NAMES.inspect]
end
end
.arg(:message_type, [String]) do |node|
within(ALLOWED_MESSAGE_TYPES, node) do |val|
BAD_MESSAGE_TYPE % [val.to_s, ALLOWED_MESSAGE_TYPES.inspect]
end
end
.arg(:tool_id, [Integer]) do |node|
node.invalidate!(BAD_TOOL_ID % node.value) if !Tool.exists?(node.value)
end
.arg(:package, [String]) do |node|
within(ALLOWED_PACKAGES, node) do |val|
BAD_PACKAGE % [val.to_s, ALLOWED_PACKAGES.inspect]
end
end
.arg(:axis, [String]) do |node|
within(ALLOWED_AXIS, node) do |val|
BAD_AXIS % [val.to_s, ALLOWED_AXIS.inspect]
end
end
.arg(:message, [String]) do |node|
notString = !node.value.is_a?(String)
tooShort = notString || node.value.length == 0
tooLong = notString || node.value.length > 300
node.invalidate! BAD_MESSAGE if (tooShort || tooLong)
end
.arg(:speed, [Integer]) do |node|
node.invalidate!(BAD_SPEED) unless node.value.between?(1, 100)
end
.arg(:resource_id, [Integer])
.arg(:resource_type, [String]) do |n|
within(RESOURCE_NAME, n) do |v|
BAD_RESOURCE_TYPE % [v.to_s, RESOURCE_NAME]
end
end
.arg(:every_point_type, [String]) do |node|
within(ALLOWED_EVERY_POINT_TYPE, node) do |val|
BAD_EVERY_POINT_TYPE % [val.to_s, ALLOWED_EVERY_POINT_TYPE.inspect]
end
end
.node(:named_pin, [:pin_type, :pin_id]) do |node|
args = HashWithIndifferentAccess.new(node.args)
klass = PIN_TYPE_MAP.fetch(args[:pin_type].value)
id = args[:pin_id].value
node.invalidate!(NO_PIN_ID % [klass.name]) if (id == 0)
bad_node = !klass.exists?(id)
no_resource(node, klass, id) if bad_node
end
.node(:nothing, [])
.node(:tool, [:tool_id])
.node(:coordinate, [:x, :y, :z])
.node(:move_absolute, [:location, :speed, :offset]) do |n|
loc = n.args[:location].try(:kind)
n.invalidate!(ONLY_ONE_COORD) if loc == "every_point"
end
.node(:move_relative, [:x, :y, :z, :speed])
.node(:write_pin, [:pin_number, :pin_value, :pin_mode ]) do |n|
no_rpi_analog(n)
end
.node(:read_pin, [:pin_number, :label, :pin_mode]) do |n|
no_rpi_analog(n)
end
.node(:channel, [:channel_name])
.node(:wait, [:milliseconds])
.node(:send_message, [:message, :message_type], [:channel])
.node(:execute, [:sequence_id], [:parameter_application])
.node(:_if, [:lhs, :op, :rhs, :_then, :_else], [:pair])
.node(:sequence, [:version, :locals], STEPS)
.node(:home, [:speed, :axis], [])
.node(:find_home, [:speed, :axis], [])
.node(:zero, [:axis], [])
.node(:emergency_lock, [], [])
.node(:emergency_unlock, [], [])
.node(:read_status, [], [])
.node(:sync, [], [])
.node(:check_updates, [:package], [])
.node(:power_off, [], [])
.node(:reboot, [:package], [])
.node(:toggle_pin, [:pin_number], [])
.node(:explanation, [:message], [])
.node(:rpc_request, [:label], ALLOWED_RPC_NODES)
.node(:rpc_ok, [:label], [])
.node(:rpc_error, [:label], [:explanation])
.node(:calibrate, [:axis], [])
.node(:pair, [:label, :value], [])
.node(:register_gpio, [:pin_number, :sequence_id])
.node(:unregister_gpio, [:pin_number])
.node(:config_update, [:package], [:pair])
.node(:factory_reset, [:package], [])
.node(:execute_script, [:label], [:pair])
.node(:set_user_env, [], [:pair])
.node(:take_photo, [], [])
.node(:point, [:pointer_type, :pointer_id], [])
.node(:install_farmware, [:url])
.node(:update_farmware, [:package])
.node(:remove_farmware, [:package])
.node(:scope_declaration, [], SCOPE_DECLARATIONS)
.node(:identifier, [:label])
.node(:variable_declaration, [:label, :data_value], []) # <= duplicate nodes
.node(:parameter_application, [:label, :data_value], []) # <= not a mistake - RC
.node(:parameter_declaration, [:label, :default_value], [])
.node(:set_servo_angle, [:pin_number, :pin_value], [])
.node(:change_ownership, [], [:pair])
.node(:dump_info, [], [])
.node(:install_first_party_farmware, [])
.node(:internal_farm_event, [], [:parameter_application])
.node(:internal_regimen, [], [:parameter_application])
.node(:internal_entry_point, [], [])
.node(:every_point, [:every_point_type], [])
.node(:resource_update, RESOURCE_UPDATE_ARGS) do |x|
resource_type = x.args.fetch(:resource_type).value
resource_id = x.args.fetch(:resource_id).value
check_resource_type(x, resource_type, resource_id)
end
ANY_ARG_NAME = Corpus.as_json[:args].pluck("name").map(&:to_s)
ANY_NODE_NAME = Corpus.as_json[:nodes].pluck("name").map(&:to_s)
def self.no_resource(node, klass, resource_id)
node.invalidate!(BAD_RESOURCE_ID % [klass.name, resource_id])
end
def self.check_resource_type(node, resource_type, resource_id)
case resource_type # <= Security critical code (for const_get'ing)
when "Device"
# When "resource_type" is "Device", resource_id always refers to
# the current_device.
# For convinience, we try to set it here, defaulting to 0
node.args[:resource_id].instance_variable_set("@value", 0)
when *RESOURCE_NAME.without("Device")
klass = Kernel.const_get(resource_type)
resource_ok = klass.exists?(resource_id)
no_resource(node, klass, resource_id) unless resource_ok
end
end
# Given an array of allowed values and a CeleryScript AST node, will DETERMINE
# if the node contains a legal value. Throws exception and invalidates if not.
def self.within(array, node)
val = node.try(:value)
node.invalidate!(yield(val)) if !array.include?(val)
end
def self.no_rpi_analog(node)
args = HashWithIndifferentAccess.new(node.args)
pin_mode = args.fetch(:pin_mode).try(:value) || DIGITAL
pin_number = args.fetch(:pin_number)
is_analog = pin_mode == ANALOG
is_node = pin_number.is_a?(CeleryScript::AstNode)
needs_check = is_analog && is_node
if needs_check
pin_type_args = pin_number.args.with_indifferent_access
pin_type = pin_type_args.fetch(:pin_type).try(:value) || ""
node.invalidate!(CANT_ANALOG) if pin_type.include?("BoxLed")
end
end
end