Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit f02ccf74da1021b4cd0a34b29c3163e86fc68354 @jeremycole committed Jan 30, 2012
Showing with 470 additions and 0 deletions.
  1. +8 −0 bin/floating_head
  2. +198 −0 lib/floating_head.rb
  3. +53 −0 lib/serial_pan_tilt.rb
  4. +211 −0 lib/skype_events.rb
8 bin/floating_head
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+
+$:.push "lib"
+
+require 'rubygems'
+require 'floating_head'
+
+FloatingHead.new.run
198 lib/floating_head.rb
@@ -0,0 +1,198 @@
+require 'skype_events'
+require 'serial_pan_tilt'
+require 'sqlite3'
+require 'ostruct'
+require 'getoptlong'
+
+class FloatingHead
+ attr_accessor :skype_events
+ attr_accessor :camera
+ attr_accessor :data
+
+ def initialize
+ @options = OpenStruct.new
+ parse_arguments
+
+ @skype_events = SkypeEvents.new("floating_head")
+ @camera = SerialPanTilt.new(@options.device)
+ @data = SQLite3::Database.new(@options.data_file)
+ end
+
+ def parse_arguments
+ @options.device = nil
+ @options.data_file = "floating_head.db"
+
+ getopt = GetoptLong.new(
+ [ "--help", "-?", GetoptLong::NO_ARGUMENT ],
+ [ "--device", "-d", GetoptLong::REQUIRED_ARGUMENT ],
+ [ "--data-file", "-f", GetoptLong::REQUIRED_ARGUMENT ]
+ )
+
+ getopt.each do |opt, arg|
+ case opt
+ when "--help"
+ when "--device"
+ @options.device = arg
+ when "--data-file"
+ @options.data_file = arg
+ end
+ end
+
+ if @options.device.nil?
+ raise "device not set"
+ end
+
+ if @options.data_file.nil?
+ raise "data-file not set"
+ end
+
+ self
+ end
+
+ def active_call_partner?(handle)
+ return nil unless call = skype_events.each_call.first
+ call["PARTNER_HANDLE"] == handle
+ end
+
+ def reply(chat, message)
+ puts "Replying with:\n#{message}\n"
+ skype_events.api_chatmessage(chat["_id"], message)
+ end
+
+ def help(chat)
+ help_message = [
+ "The following commands are understood:",
+ " !help | Show this help.",
+ " !list | List all saved camera positions.",
+ " !save <name> | Save the current camera position as <name>.",
+ " !erase <name> | Erase the saved camera position as <name>.",
+ " !pos <pan>, <tilt> | Position the camera at <pan>, <tilt> degrees.",
+ " !left <deg> | Pan the camera <deg> degrees left.",
+ " !right <deg> | Pan the camera <deg> degrees right.",
+ " !down <deg> | Tilt the camera <deg> degrees down.",
+ " !up <deg> | Tilt the camera <deg> degrees up.",
+ " !go <name> | Go to saved camera position <name>.",
+ " @<name> | Alias for !go <name>.",
+ "",
+ ].join("\n")
+
+ reply(chat, help_message)
+ end
+
+ def list(chat)
+ locations = data.execute("SELECT * FROM locations")
+ if locations.empty?
+ reply(chat, "No locations known!")
+ return
+ end
+
+ location_reply = "The following locations are known:\n"
+ locations.each do |row|
+ location_reply << " #{row[0]} (#{row[1]}, #{row[2]})\n"
+ end
+ reply(chat, location_reply)
+ end
+
+ def save(chat, name, pan, tilt)
+ puts "SAVE: #{name}, #{pan}, #{tilt}"
+ result = data.execute("INSERT INTO locations (name, pan, tilt) VALUES (?, ?, ?)", name, pan, tilt)
+ if result
+ reply(chat, "Saved '#{name}' as (#{pan}, #{tilt})!")
+ else
+ reply(chat, "Failed to save '#{name}'!")
+ end
+ end
+
+ def erase(chat, name)
+ puts "ERASE: #{name}"
+ result = data.execute("DELETE FROM locations WHERE name = ?", name)
+ if result
+ reply(chat, "Erased '#{name}'!")
+ else
+ reply(chat, "Failed to erase '#{name}'!")
+ end
+ end
+
+ def pos(chat, pan, tilt)
+ camera.pan_tilt(pan, tilt)
+ reply(chat, "Going to (#{pan}, #{tilt})!")
+ end
+
+ def go(chat, name)
+ puts "GO: #{name}"
+ location = data.get_first_row("SELECT * FROM locations WHERE name = ?", name)
+ unless location
+ reply(chat, "Location '#{name}' is not known!")
+ return
+ end
+ db_name, pan, tilt = location
+
+ pos(chat, pan, tilt)
+ end
+
+ def left(chat, degrees)
+ pos(chat, camera.pan_position-degrees.to_i, camera.tilt_position)
+ end
+
+ def right(chat, degrees)
+ pos(chat, camera.pan_position+degrees.to_i, camera.tilt_position)
+ end
+
+ def down(chat, degrees)
+ pos(chat, camera.pan_position, camera.tilt_position-degrees.to_i)
+ end
+
+ def up(chat, degrees)
+ pos(chat, camera.pan_position, camera.tilt_position+degrees.to_i)
+ end
+
+ def handle_command(chat, message)
+ case
+ when m = /^!help/.match(message)
+ help(chat)
+ when m = /^!list/.match(message)
+ list(chat)
+ when m = /^!save (\S+)/.match(message)
+ pan, tilt = camera.position
+ save(chat, m[1], pan, tilt)
+ when m = /^!erase (\S+)/.match(message)
+ erase(chat, m[1])
+ when m = /^!go (\S+)/.match(message)
+ go(chat, m[1])
+ when m = /^@(\S+)/.match(message)
+ go(chat, m[1])
+ when m = /^!pos (\d+), *(\d+)/.match(message)
+ pos(chat, m[1], m[2])
+ when m = /^!left (\d+)/.match(message)
+ left(chat, m[1])
+ when m = /^!right (\d+)/.match(message)
+ right(chat, m[1])
+ when m = /^!down (\d+)/.match(message)
+ down(chat, m[1])
+ when m = /^!up (\d+)/.match(message)
+ up(chat, m[1])
+ else
+ reply(chat, "Didn't understand '#{message}'!")
+ end
+ end
+
+ def run
+ # Throw away any unread messages before we get started, in case there
+ # are commands in there that we missed.
+ skype_events.each_unread_chatmessage {}
+
+ while true
+ sleep 0.1
+ skype_events.each_unread_chatmessage do |chat, chatmessage|
+ handle = chatmessage['FROM_HANDLE']
+ message = chatmessage['BODY']
+ unless active_call_partner? handle
+ puts "Got message from #{handle}, who is not my active call partner!"
+ next
+ end
+ puts "Message from #{handle}: #{message}"
+ handle_command(chat, message)
+ end
+ end
+ end
+end
53 lib/serial_pan_tilt.rb
@@ -0,0 +1,53 @@
+require 'serialport'
+
+class SerialPanTilt
+ attr_accessor :device, :baudrate, :serial
+ attr_reader :pan_position, :tilt_position
+
+ def initialize(device, baudrate=9600)
+ @device = device
+ @baudrate = baudrate
+
+ if device == "dummy"
+ @serial = nil
+ else
+ @serial = SerialPort.new(device, baudrate)
+ end
+
+ @pan_position = nil
+ @tilt_position = nil
+
+ home
+ end
+
+ def write(string)
+ if serial.nil?
+ puts "Dummy write: #{string}"
+ else
+ serial.write(string)
+ end
+ end
+
+ def pan(degrees)
+ write("P#{degrees.to_i}\n")
+ @pan_position = degrees
+ end
+
+ def tilt(degrees)
+ write("T#{degrees.to_i}\n")
+ @tilt_position = degrees
+ end
+
+ def pan_tilt(pan_degrees, tilt_degrees)
+ pan(pan_degrees)
+ tilt(tilt_degrees)
+ end
+
+ def home
+ pan_tilt(90, 90)
+ end
+
+ def position
+ [pan_position, tilt_position]
+ end
+end
211 lib/skype_events.rb
@@ -0,0 +1,211 @@
+require 'appscript'
+
+class SkypeEvents
+ attr_accessor :skype
+
+ CALL_PROPERTIES = [
+ "TIMESTAMP",
+ "PARTNER_HANDLE",
+ "PARTNER_DISPNAME",
+ "TYPE",
+ "STATUS",
+ "VIDEO_STATUS",
+ "FAILUREREASON",
+ "DURATION",
+ ]
+
+ CHAT_PROPERTIES = [
+ "NAME",
+ "TIMESTAMP",
+ "ADDER",
+ "STATUS",
+ "POSTERS",
+ "MEMBERS",
+ "TOPIC",
+ "RECENTCHATMESSAGES",
+ "ACTIVEMEMBERS",
+ "FRIENDLYNAME",
+ ]
+
+ CHATMESSAGE_PROPERTIES = [
+ "CHATNAME",
+ "TIMESTAMP",
+ "FROM_HANDLE",
+ "FROM_DISPNAME",
+ "TYPE",
+ "USERS",
+ "LEAVEREASON",
+ "BODY",
+ "STATUS",
+ ]
+
+ def initialize(script_name)
+ @skype = Appscript.app("Skype.app")
+ @script_name = script_name
+ end
+
+ def command(command_text)
+ skype.send_ :script_name => @script_name, :command => command_text
+ end
+
+ # -> "SEARCH ACTIVECALLS"
+ # <- "CALLS <call1>[, <call2>]
+ def api_search_activecalls
+ result_pattern = /CALLS (.*)/
+ result = command("SEARCH ACTIVECALLS")
+ unless calls_match = result_pattern.match(result)
+ return nil
+ end
+
+ calls_match[1].split(/,\s*/)
+ end
+
+ # -> "GET CALL <call> <property>"
+ # <- "CALL <call> <property> <content>"
+ def api_get_call_property(call, property)
+ result_pattern = /CALL (\S+) (\S+) (.*)/
+ result = command("GET CALL #{call} #{property}")
+ unless call_property_match = result_pattern.match(result)
+ return nil
+ end
+
+ value = call_property_match[3]
+ case property
+ when "TIMESTAMP"
+ Time.at(value.to_i)
+ else
+ value
+ end
+ end
+
+ # Helper to loop over valid properties and get them all.
+ def api_get_call(call)
+ CALL_PROPERTIES.inject({"_id" => call}) do |result_hash, property|
+ result_hash[property] = api_get_call_property(call, property)
+ result_hash
+ end
+ end
+
+ # -> "SEARCH RECENTCHATS"
+ # <- "CHATS <chat1>[, <chat2>]"
+ def api_search_recentchats
+ result_pattern = /CHATS (.*)/
+ result = command("SEARCH RECENTCHATS")
+ unless chats_match = result_pattern.match(result)
+ return nil
+ end
+
+ chats_match[1].split(/,\s*/)
+ end
+
+ # -> "CHATMESSAGE <chat> <message>
+ # <- "MESSAGE <chatmessage> STATUS <status>"
+ def api_chatmessage(chat, message)
+ result_pattern = /MESSAGE (\S+) STATUS (\S+)/
+ result = command("CHATMESSAGE #{chat} #{message}")
+ unless message_match = result_pattern.match(result)
+ return nil
+ end
+ message_match[2]
+ end
+
+ # -> "GET CHAT <chat> <property>"
+ # <- "CHAT <chat> <property> <content>"
+ def api_get_chat_property(chat, property)
+ result_pattern = /CHAT (\S+) (\S+) (.*)/
+ result = command("GET CHAT #{chat} #{property}")
+ unless chat_property_match = result_pattern.match(result)
+ return nil
+ end
+
+ value = chat_property_match[3]
+ case property
+ when "TIMESTAMP"
+ Time.at(value.to_i)
+ when "RECENTCHATMESSAGES"
+ value.split(/,\s*/)
+ when "MEMBERS", "ACTIVEMEMBERS"
+ value.split(/\s+/)
+ else
+ value
+ end
+ end
+
+ # Helper to loop over valid properties and get them all.
+ def api_get_chat(chat)
+ CHAT_PROPERTIES.inject({"_id" => chat}) do |result_hash, property|
+ result_hash[property] = api_get_chat_property(chat, property)
+ result_hash
+ end
+ end
+
+ # -> GET CHATMESSAGE <chatmessage> <property>
+ # <- MESSAGE <chatmessage> <property> <content>
+ def api_get_chatmessage_property(chatmessage, property)
+ result_pattern = /MESSAGE (\S+) (\S+) (.*)/
+ result = command("GET CHATMESSAGE #{chatmessage} #{property}")
+ unless chatmessage_property_match = result_pattern.match(result)
+ return nil
+ end
+
+ value = chatmessage_property_match[3]
+ case property
+ when "TIMESTAMP"
+ Time.at(value.to_i)
+ else
+ value
+ end
+ end
+
+ # Helper to loop over valid properties and get them all.
+ def api_get_chatmessage(chatmessage)
+ CHATMESSAGE_PROPERTIES.inject({"_id" => chatmessage}) do |result_hash, property|
+ result_hash[property] =
+ api_get_chatmessage_property(chatmessage, property)
+ result_hash
+ end
+ end
+
+ def api_set_chatmessage_read(chatmessage)
+ result_pattern = /MESSAGE (\S+) STATUS READ/
+ result = command("SET CHATMESSAGE #{chatmessage} SEEN") # Yes, seen.
+ !!result_pattern.match(result)
+ end
+
+ def each_call
+ unless block_given?
+ return Enumerable::Enumerator.new(self, :each_call)
+ end
+
+ api_search_activecalls.each do |call_id|
+ yield api_get_call(call_id)
+ end
+ end
+
+ def each_chat
+ unless block_given?
+ return Enumerable::Enumerator.new(self, :each_chat)
+ end
+
+ api_search_recentchats.each do |chat_id|
+ yield api_get_chat(chat_id)
+ end
+ end
+
+ def each_unread_chatmessage
+ unless block_given?
+ return Enumerable::Enumerator.new(self, :each_unread_chatmessage)
+ end
+
+ api_search_recentchats.each do |chat_id|
+ chat = api_get_chat(chat_id)
+ chat["RECENTCHATMESSAGES"].each do |chatmessage_id|
+ chatmessage = api_get_chatmessage(chatmessage_id)
+ if chatmessage["STATUS"] == "RECEIVED"
+ yield chat, chatmessage
+ api_set_chatmessage_read(chatmessage_id)
+ end
+ end
+ end
+ end
+end

0 comments on commit f02ccf7

Please sign in to comment.
Something went wrong with that request. Please try again.