pebblewatch 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +97 -5
- data/lib/pebble/capabilities.rb +1 -1
- data/lib/pebble/endpoints.rb +4 -0
- data/lib/pebble/protocol.rb +155 -0
- data/lib/pebble/system_messages.rb +15 -9
- data/lib/pebble/version.rb +1 -1
- data/lib/pebble/watch.rb +254 -0
- data/lib/pebble/watch/event.rb +2 -1
- data/lib/pebble/watch/log_event.rb +34 -0
- data/lib/pebble/watch/media_control_event.rb +22 -0
- data/lib/pebble/watch/system_message_event.rb +23 -0
- data/lib/{pebble/pebblewatch.rb → pebblewatch.rb} +0 -0
- metadata +12 -11
data/README.md
CHANGED
|
@@ -6,9 +6,9 @@ The protocol implementation was based on the documentation at http://pebbledev.o
|
|
|
6
6
|
|
|
7
7
|
## To Do
|
|
8
8
|
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
9
|
+
- [x] Basic protocol communication
|
|
10
|
+
- [x] Sending of messages (notifications etc)
|
|
11
|
+
- [x] Receiving of events (log, music control)
|
|
12
12
|
- [ ] Firmware/app uploading
|
|
13
13
|
- [ ] CLI
|
|
14
14
|
- [ ] REPL
|
|
@@ -16,17 +16,109 @@ The protocol implementation was based on the documentation at http://pebbledev.o
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
gem install
|
|
19
|
+
gem install pebblewatch
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
23
23
|
|
|
24
|
+
Make sure your Pebble is paired to your computer and set up as a serial port. We're going to need the path or index of the port, which in the case of OS X looks like `/dev/tty.Pebble7F30-SerialPortSe` for Pebble ID `7F30`.
|
|
25
|
+
|
|
24
26
|
```ruby
|
|
25
27
|
require "pebble"
|
|
26
28
|
|
|
27
|
-
#
|
|
29
|
+
# Create your watch using the serial port assigned to your Pebble.
|
|
30
|
+
watch = Pebble::Watch.new("7F30", "/dev/tty.Pebble7F30-SerialPortSe")
|
|
31
|
+
# You can also use autodetection if you're on OS X:
|
|
32
|
+
# watch = Pebble::Watch.autodetect
|
|
33
|
+
|
|
34
|
+
# The watch object will be on the receiving end of 3 kinds of events:
|
|
35
|
+
watch.on_event(:log) do |event|
|
|
36
|
+
puts "LOG"
|
|
37
|
+
puts "timestamp: #{event.timestamp}"
|
|
38
|
+
puts "level: #{event.level}"
|
|
39
|
+
puts "filename: #{event.filename}"
|
|
40
|
+
puts "linenumber: #{event.linenumber}"
|
|
41
|
+
puts "message: #{event.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
watch.on_event(:system_message) do |event|
|
|
45
|
+
puts "System Message: #{event.message} (#{event.code})"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
handler = watch.on_event(:media_control) do |event|
|
|
49
|
+
case event.button
|
|
50
|
+
when :playpause
|
|
51
|
+
puts "Play or pause music"
|
|
52
|
+
when :next
|
|
53
|
+
puts "Next track"
|
|
54
|
+
when :previous
|
|
55
|
+
puts "Previous track"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Suddenly got a change of heart? Just cover your ears.
|
|
60
|
+
watch.stop_listening(:media_control, handler)
|
|
61
|
+
|
|
62
|
+
# To make sure we don't miss any events, we haven't connected yet.
|
|
63
|
+
# To be able to *send* stuff to the watch, we will now.
|
|
64
|
+
watch.connect
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# We can of course send notifications.
|
|
68
|
+
watch.ping
|
|
69
|
+
watch.notification_sms("Scarlett Johansson", "Hey baby, what are you doing tonight?")
|
|
70
|
+
watch.notification_email("Tim Cook", "RE: Final offer",
|
|
71
|
+
"All right, you drive a hard bargain. We'll pay $2 Billion for the whole shop and that is our final offer.")
|
|
72
|
+
|
|
73
|
+
# Or let Pebble know what we're listening to.
|
|
74
|
+
watch.set_nowplaying_metadata(
|
|
75
|
+
"Artist you've probably never heard of",
|
|
76
|
+
"Album released exclusively through SoundCloud",
|
|
77
|
+
"Song that doesn't suck per se but isn't great either"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# We can also do some maintenancy things, although there's currently no firmware/app uploading.
|
|
81
|
+
versions = watch.get_versions
|
|
82
|
+
puts "Normal firmware version: #{versions[:firmwares][:normal][:version]}"
|
|
83
|
+
puts "Recovery firmware version: #{versions[:firmwares][:recovery][:version]}"
|
|
84
|
+
puts "Bluetooth MAC address: #{versions[:btmac]}"
|
|
85
|
+
|
|
86
|
+
# Fun fact: We don't have to wait for the results synchronously. (This works on every message with a response.)
|
|
87
|
+
watch.get_installed_apps do |apps|
|
|
88
|
+
puts "Installed apps: (#{apps[:apps].length} of #{apps[:banks_count]} banks in use)"
|
|
89
|
+
apps[:apps].each do |app|
|
|
90
|
+
puts "#{app[:index]}/#{app[:id]}: #{app[:name]} by #{app[:author]}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Dieting is not just for dogs anymore.
|
|
95
|
+
watch.remove_app(id, index)
|
|
96
|
+
|
|
97
|
+
time = watch.get_time
|
|
98
|
+
# Or asynchronously: watch.get_time { |time| ... }
|
|
99
|
+
|
|
100
|
+
# Time travel is just one method call away.
|
|
101
|
+
watch.set_time(Time.now + (365 * 24 * 60 * 60))
|
|
102
|
+
|
|
103
|
+
# This is mostly interesting for internal stuff, but since there's a :system_message event as well, I thought why not.
|
|
104
|
+
watch.system_message(Pebble::SystemMessages::FIRMWARE_OUT_OF_DATE)
|
|
105
|
+
|
|
106
|
+
# Yeah, I'd stay away from this one.
|
|
107
|
+
watch.reset
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# If you're done sending messages but want the program to keep listing for incoming events, say so:
|
|
111
|
+
watch.listen_for_events
|
|
112
|
+
# Note that listening will only end when the connection is lost, so anything that comes after this call will only then be executed.
|
|
113
|
+
# This will generally be the last call in your program.
|
|
114
|
+
|
|
115
|
+
# If we don't want to wait around and listen, just let the program exit or disconnect explicitly:
|
|
116
|
+
watch.disconnect
|
|
28
117
|
```
|
|
29
118
|
|
|
119
|
+
## Examples
|
|
120
|
+
Check out the `examples/` folder for two examples that I actually use myself. They're kind of similar, but should give you an idea of how this whole thing works.
|
|
121
|
+
|
|
30
122
|
## License
|
|
31
123
|
Copyright (c) 2013 Douwe Maan
|
|
32
124
|
|
data/lib/pebble/capabilities.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Pebble
|
|
|
16
16
|
SMS = 32
|
|
17
17
|
GPS = 64
|
|
18
18
|
BTLE = 128
|
|
19
|
-
CAMERA_FRONT = 240 #
|
|
19
|
+
# CAMERA_FRONT = 240 # Doesn't make sense as it'd mess up the bitmask, but it's apparently true.
|
|
20
20
|
CAMERA_REAR = 256
|
|
21
21
|
ACCEL = 512
|
|
22
22
|
GYRO = 1024
|
data/lib/pebble/endpoints.rb
CHANGED
data/lib/pebble/protocol.rb
CHANGED
|
@@ -1,5 +1,160 @@
|
|
|
1
|
+
require "serialport"
|
|
2
|
+
|
|
1
3
|
module Pebble
|
|
2
4
|
class Protocol
|
|
5
|
+
module Errors
|
|
6
|
+
class NotConnected < StandardError; end
|
|
7
|
+
class LostConnection < StandardError; end
|
|
8
|
+
class MalformedResponse < StandardError; end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :connected
|
|
12
|
+
attr_reader :message_handlers
|
|
13
|
+
|
|
14
|
+
def self.open(port)
|
|
15
|
+
protocol = new(port)
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
protocol.connect
|
|
19
|
+
yield protocol
|
|
20
|
+
ensure
|
|
21
|
+
protocol.disconnect
|
|
22
|
+
end
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(port)
|
|
27
|
+
@port = port
|
|
28
|
+
|
|
29
|
+
@connected = false
|
|
30
|
+
@send_message_mutex = Mutex.new
|
|
31
|
+
@message_handlers = Hash.new { |hash, key| hash[key] = [] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def connect
|
|
35
|
+
puts "Connecting with port #{@port}"
|
|
36
|
+
|
|
37
|
+
@serial_port = SerialPort.new(@port, baudrate: 115200)
|
|
38
|
+
@serial_port.read_timeout = 500
|
|
39
|
+
|
|
40
|
+
@connected = true
|
|
41
|
+
puts "Connected"
|
|
42
|
+
|
|
43
|
+
@receive_messages_thread = Thread.new(&method(:receive_messages))
|
|
44
|
+
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def disconnect
|
|
49
|
+
raise Errors::NotConnected unless @connected
|
|
50
|
+
|
|
51
|
+
@connected = false
|
|
52
|
+
|
|
53
|
+
@serial_port.close()
|
|
54
|
+
@serial_port = nil
|
|
55
|
+
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def listen_for_messages
|
|
60
|
+
raise Errors::NotConnected unless @connected
|
|
61
|
+
|
|
62
|
+
@receive_messages_thread.join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def on_receive(endpoint = :any, &handler)
|
|
66
|
+
@message_handlers[endpoint] << handler
|
|
67
|
+
handler
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stop_receiving(*params)
|
|
71
|
+
handler = params.pop
|
|
72
|
+
endpoint = params.pop || :any
|
|
73
|
+
|
|
74
|
+
@message_handlers[endpoint].delete(handler)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def send_message(endpoint, message, async_response_handler = nil, &response_parser)
|
|
78
|
+
raise Errors::NotConnected unless @connected
|
|
79
|
+
|
|
80
|
+
message ||= ""
|
|
81
|
+
|
|
82
|
+
puts "Sending #{Endpoints.for_code(endpoint)}: #{message.inspect}"
|
|
83
|
+
|
|
84
|
+
data = [message.size, endpoint].pack("S>S>") + message
|
|
85
|
+
|
|
86
|
+
@send_message_mutex.synchronize do
|
|
87
|
+
@serial_port.write data
|
|
88
|
+
|
|
89
|
+
if response_parser
|
|
90
|
+
if async_response_handler
|
|
91
|
+
identifier = on_receive(endpoint) do |response_message|
|
|
92
|
+
stop_receiving(endpoint, identifier)
|
|
93
|
+
|
|
94
|
+
parsed_response = response_parser.call(response_message)
|
|
95
|
+
|
|
96
|
+
async_response_handler.call(parsed_response)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
true
|
|
100
|
+
else
|
|
101
|
+
received = false
|
|
102
|
+
parsed_response = nil
|
|
103
|
+
identifier = on_receive(endpoint) do |response_message|
|
|
104
|
+
stop_receiving(endpoint, identifier)
|
|
105
|
+
|
|
106
|
+
parsed_response = response_parser.call(response_message)
|
|
107
|
+
received = true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sleep 0.015 until received
|
|
111
|
+
|
|
112
|
+
parsed_response
|
|
113
|
+
end
|
|
114
|
+
else
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
def receive_messages
|
|
122
|
+
puts "Waiting for messages"
|
|
123
|
+
while @connected
|
|
124
|
+
header = @serial_port.read(4)
|
|
125
|
+
next unless header
|
|
126
|
+
|
|
127
|
+
raise Errors::MalformedResponse if header.length < 4
|
|
128
|
+
|
|
129
|
+
size, endpoint = header.unpack("S>S>")
|
|
130
|
+
message = @serial_port.read(size)
|
|
131
|
+
|
|
132
|
+
puts "Received #{Endpoints.for_code(endpoint)}: #{message.inspect}"
|
|
133
|
+
|
|
134
|
+
trigger_received(endpoint, message)
|
|
135
|
+
end
|
|
136
|
+
rescue IOError => e
|
|
137
|
+
if @connected
|
|
138
|
+
puts "Lost connection"
|
|
139
|
+
@connected = false
|
|
140
|
+
raise Errors::LostConnection
|
|
141
|
+
end
|
|
142
|
+
ensure
|
|
143
|
+
puts "Finished waiting for messages"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def trigger_received(endpoint, message)
|
|
147
|
+
@message_handlers[:any].each do |handler|
|
|
148
|
+
Thread.new(handler) do |handler|
|
|
149
|
+
handler.call(endpoint, message)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
3
152
|
|
|
153
|
+
@message_handlers[endpoint].each do |handler|
|
|
154
|
+
Thread.new(handler) do |handler|
|
|
155
|
+
handler.call(message)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
4
159
|
end
|
|
5
160
|
end
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
module
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
module Pebble
|
|
2
|
+
module SystemMessages
|
|
3
|
+
FIRMWARE_AVAILABLE = 0
|
|
4
|
+
FIRMWARE_START = 1
|
|
5
|
+
FIRMWARE_COMPLETE = 2
|
|
6
|
+
FIRMWARE_FAIL = 3
|
|
7
|
+
FIRMWARE_UP_TO_DATE = 4
|
|
8
|
+
FIRMWARE_OUT_OF_DATE = 5
|
|
9
|
+
BLUETOOTH_START_DISCOVERABLE = 6
|
|
10
|
+
BLUETOOTH_END_DISCOVERABLE = 7
|
|
11
|
+
|
|
12
|
+
def self.for_code(code)
|
|
13
|
+
constants.find { |constant| const_get(constant) == code }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
10
16
|
end
|
data/lib/pebble/version.rb
CHANGED
data/lib/pebble/watch.rb
CHANGED
|
@@ -2,6 +2,260 @@ require "pebble/watch/event"
|
|
|
2
2
|
|
|
3
3
|
module Pebble
|
|
4
4
|
class Watch
|
|
5
|
+
module Errors
|
|
6
|
+
class NoWatchesFound < StandardError; end
|
|
7
|
+
end
|
|
5
8
|
|
|
9
|
+
def self.autodetect
|
|
10
|
+
return nil unless RUBY_PLATFORM =~ /darwin/
|
|
11
|
+
|
|
12
|
+
devices = Dir.glob("/dev/tty.Pebble????-SerialPortSe")
|
|
13
|
+
|
|
14
|
+
raise NoWatchesFound if devices.length == 0
|
|
15
|
+
puts "Found multiple watches" if devices.length > 1
|
|
16
|
+
|
|
17
|
+
port = devices.first
|
|
18
|
+
id = port[15, 4]
|
|
19
|
+
puts "Found watch with ID #{id}"
|
|
20
|
+
|
|
21
|
+
return new(id, port)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.open(id, port)
|
|
25
|
+
watch = new(id, port)
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
watch.connect
|
|
29
|
+
yield watch
|
|
30
|
+
ensure
|
|
31
|
+
watch.disconnect
|
|
32
|
+
end
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attr_reader :id
|
|
37
|
+
attr_reader :protocol
|
|
38
|
+
attr_reader :event_handlers
|
|
39
|
+
attr_accessor :session_capabilities
|
|
40
|
+
attr_accessor :client_capabilities
|
|
41
|
+
|
|
42
|
+
def initialize(id, port)
|
|
43
|
+
@id = id
|
|
44
|
+
|
|
45
|
+
@protocol = Protocol.new(port)
|
|
46
|
+
@event_handlers = Hash.new { |hash, key| hash[key] = [] }
|
|
47
|
+
|
|
48
|
+
# We're mirroring Android here.
|
|
49
|
+
@session_capabilities = Capabilities::Session::GAMMA_RAY
|
|
50
|
+
@client_capabilities = Capabilities::Client::TELEPHONY | Capabilities::Client::SMS | Capabilities::Client::ANDROID
|
|
51
|
+
|
|
52
|
+
answer_phone_version_message
|
|
53
|
+
|
|
54
|
+
receive_event_messages
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def connect
|
|
58
|
+
@protocol.connect
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def disconnect
|
|
62
|
+
@protocol.disconnect
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def listen_for_events
|
|
66
|
+
@protocol.listen_for_messages
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_event(event = :any, &handler)
|
|
70
|
+
@event_handlers[event] << handler
|
|
71
|
+
handler
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stop_listening(*params)
|
|
75
|
+
handler = params.pop
|
|
76
|
+
event = params.pop || :any
|
|
77
|
+
|
|
78
|
+
@event_handlers[event].delete(handler)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def ping(cookie = 0xDEADBEEF, &async_response_handler)
|
|
83
|
+
message = [0, cookie].pack("CL>")
|
|
84
|
+
|
|
85
|
+
@protocol.send_message(Endpoints::PING, message, async_response_handler) do |message|
|
|
86
|
+
restype, cookie = message.unpack("CL>")
|
|
87
|
+
cookie
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def notification_sms(sender, body)
|
|
92
|
+
notification(:sms, sender, body)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def notification_email(sender, subject, body)
|
|
96
|
+
notification(:email, sender, body, subject)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def set_nowplaying_metadata(artist, album, track)
|
|
100
|
+
message = [16].pack("C")
|
|
101
|
+
message << package_strings(artist, album, track, 30)
|
|
102
|
+
|
|
103
|
+
@protocol.send_message(Endpoints::MUSIC_CONTROL, message)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def get_versions(&async_response_handler)
|
|
107
|
+
@protocol.send_message(Endpoints::VERSION, "\x00", async_response_handler) do |message|
|
|
108
|
+
response = {}
|
|
109
|
+
|
|
110
|
+
response[:firmwares] = {}
|
|
111
|
+
|
|
112
|
+
size = 47
|
|
113
|
+
[:normal, :recovery].each_with_index do |type, index|
|
|
114
|
+
offset = index * size + 1
|
|
115
|
+
|
|
116
|
+
fw = {}
|
|
117
|
+
|
|
118
|
+
fw[:timestamp], fw[:version], fw[:commit], fw[:is_recovery],
|
|
119
|
+
fw[:hardware_platform], fw[:metadata_version] =
|
|
120
|
+
message[offset, size].unpack("L>A32A8ccc")
|
|
121
|
+
|
|
122
|
+
fw[:is_recovery] = (fw[:is_recovery] == 1)
|
|
123
|
+
|
|
124
|
+
response[:firmwares][type] = fw
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
response[:bootloader_timestamp], response[:hardware_version], response[:serial] =
|
|
128
|
+
message[95, 25].unpack("L>A9A12")
|
|
129
|
+
|
|
130
|
+
response[:btmac] = message[120, 6].unpack("H*").first.scan(/../).reverse.map { |c| c.upcase }.join(":")
|
|
131
|
+
|
|
132
|
+
response
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def get_installed_apps(&async_response_handler)
|
|
137
|
+
@protocol.send_message(Endpoints::APP_INSTALL_MANAGER, "\x01", async_response_handler) do |message|
|
|
138
|
+
response = {}
|
|
139
|
+
|
|
140
|
+
response[:banks_count], apps_count = message[1, 8].unpack("L>L>")
|
|
141
|
+
response[:apps] = []
|
|
142
|
+
|
|
143
|
+
size = 78
|
|
144
|
+
apps_count.times do |index|
|
|
145
|
+
offset = index * size + 9
|
|
146
|
+
|
|
147
|
+
app = {}
|
|
148
|
+
|
|
149
|
+
app[:id], app[:index], app[:name], app[:author], app[:flags], app[:version] =
|
|
150
|
+
message[offset, size].unpack("L>L>A32A32L>S>")
|
|
151
|
+
|
|
152
|
+
response[:apps] << app
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
response
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def remove_app(app_id, app_index)
|
|
160
|
+
message = [2, app_id, app_index].pack("cL>L>")
|
|
161
|
+
|
|
162
|
+
@protocol.send_message(Endpoints::APP_INSTALL_MANAGER, message)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def get_time(&async_response_handler)
|
|
166
|
+
@protocol.send_message(Endpoints::TIME, "\x00", async_response_handler) do |message|
|
|
167
|
+
restype, timestamp = message.unpack("CL>")
|
|
168
|
+
Time.at(timestamp)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def set_time(time)
|
|
173
|
+
timestamp = time.to_i
|
|
174
|
+
message = [2, timestamp].pack("CL>")
|
|
175
|
+
|
|
176
|
+
@protocol.send_message(Endpoints::TIME, message)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def system_message(code)
|
|
180
|
+
puts "Sending system message #{SystemMessages.for_code(code)}"
|
|
181
|
+
|
|
182
|
+
message = [code].pack("S>")
|
|
183
|
+
|
|
184
|
+
@protocol.send_message(Endpoints::SYSTEM_MESSAGE, message)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def reset
|
|
188
|
+
@protocol.send_message(Endpoints::RESET, "\x00")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
def answer_phone_version_message
|
|
193
|
+
@protocol.on_receive(Endpoints::PHONE_VERSION) do |message|
|
|
194
|
+
response_message = [1, -1].pack("Cl>")
|
|
195
|
+
response_message << [@session_capabilities, @client_capabilities].pack("L>L>")
|
|
196
|
+
|
|
197
|
+
@protocol.send_message(Endpoints::PHONE_VERSION, response_message)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def receive_event_messages
|
|
202
|
+
events = [
|
|
203
|
+
[:log, Endpoints::LOGS, LogEvent],
|
|
204
|
+
[:system_message, Endpoints::SYSTEM_MESSAGE, SystemMessageEvent],
|
|
205
|
+
[:media_control, Endpoints::MUSIC_CONTROL, MediaControlEvent]
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
events.each do |(name, endpoint, event_klass)|
|
|
209
|
+
@protocol.on_receive(endpoint) do |message|
|
|
210
|
+
event = event_klass.parse(message)
|
|
211
|
+
trigger_event(name, event) if event
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def trigger_event(name, event)
|
|
217
|
+
puts "Event '#{name}': #{event.inspect}"
|
|
218
|
+
|
|
219
|
+
@event_handlers[:any].each do |handler|
|
|
220
|
+
Thread.new(handler) do |handler|
|
|
221
|
+
handler.call(name, event)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
@event_handlers[name].each do |handler|
|
|
226
|
+
Thread.new(handler) do |handler|
|
|
227
|
+
handler.call(event)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def notification(type, *params)
|
|
233
|
+
types = {
|
|
234
|
+
email: 0,
|
|
235
|
+
sms: 1
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
timestamp = Time.now.to_i
|
|
239
|
+
params.insert(2, timestamp.to_s)
|
|
240
|
+
|
|
241
|
+
message = [types[type]].pack("C")
|
|
242
|
+
message << package_strings(*params)
|
|
243
|
+
|
|
244
|
+
@protocol.send_message(Endpoints::NOTIFICATION, message)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def package_strings(*parts)
|
|
248
|
+
max_part_length = 255
|
|
249
|
+
max_part_length = parts.pop if parts.last.is_a?(Integer)
|
|
250
|
+
|
|
251
|
+
message = ""
|
|
252
|
+
parts.each do |part|
|
|
253
|
+
part ||= ""
|
|
254
|
+
|
|
255
|
+
part = part[0, max_part_length] if part.length > max_part_length
|
|
256
|
+
message << [part.length].pack("C") + part
|
|
257
|
+
end
|
|
258
|
+
message
|
|
259
|
+
end
|
|
6
260
|
end
|
|
7
261
|
end
|
data/lib/pebble/watch/event.rb
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
module Pebble
|
|
2
2
|
class Watch
|
|
3
3
|
class LogEvent < Event
|
|
4
|
+
attr_accessor :timestamp
|
|
5
|
+
attr_accessor :level
|
|
6
|
+
attr_accessor :filename
|
|
7
|
+
attr_accessor :linenumber
|
|
8
|
+
attr_accessor :message
|
|
4
9
|
|
|
10
|
+
def self.parse(raw_message)
|
|
11
|
+
return nil if raw_message.length < 8
|
|
12
|
+
|
|
13
|
+
timestamp, level, message_size, linenumber = raw_message[0, 8].unpack("L>CCS>")
|
|
14
|
+
filename = raw_message[8, 16]
|
|
15
|
+
message = raw_message[24, message_size]
|
|
16
|
+
|
|
17
|
+
log_levels = {
|
|
18
|
+
1 => :error,
|
|
19
|
+
50 => :warning,
|
|
20
|
+
100 => :info,
|
|
21
|
+
200 => :debug,
|
|
22
|
+
250 => :verbose
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
event = new
|
|
26
|
+
|
|
27
|
+
event.timestamp = Time.at(timestamp)
|
|
28
|
+
event.level = log_levels[level] || :unknown
|
|
29
|
+
event.linenumber = linenumber
|
|
30
|
+
event.filename = filename
|
|
31
|
+
event.message = message
|
|
32
|
+
|
|
33
|
+
event
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def inspect
|
|
37
|
+
"[#{self.level.to_s.capitalize}] #{self.message}"
|
|
38
|
+
end
|
|
5
39
|
end
|
|
6
40
|
end
|
|
7
41
|
end
|
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
module Pebble
|
|
2
2
|
class Watch
|
|
3
3
|
class MediaControlEvent < Event
|
|
4
|
+
attr_accessor :button
|
|
4
5
|
|
|
6
|
+
BUTTONS = {
|
|
7
|
+
1 => :playpause,
|
|
8
|
+
4 => :next,
|
|
9
|
+
5 => :previous
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def self.parse(message)
|
|
13
|
+
button_id = message.unpack("C").first
|
|
14
|
+
|
|
15
|
+
return nil unless BUTTONS.has_key?(button_id)
|
|
16
|
+
|
|
17
|
+
event = new
|
|
18
|
+
|
|
19
|
+
event.button = BUTTONS[button_id]
|
|
20
|
+
|
|
21
|
+
event
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inspect
|
|
25
|
+
self.button.to_s.capitalize
|
|
26
|
+
end
|
|
5
27
|
end
|
|
6
28
|
end
|
|
7
29
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Pebble
|
|
2
|
+
class Watch
|
|
3
|
+
class SystemMessageEvent < Event
|
|
4
|
+
attr_accessor :code
|
|
5
|
+
|
|
6
|
+
def self.parse(message)
|
|
7
|
+
event = new
|
|
8
|
+
|
|
9
|
+
event.code = message.length == 2 ? message.unpack("S>").first : -1
|
|
10
|
+
|
|
11
|
+
event
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def message
|
|
15
|
+
SystemMessages.for_code(self.code) || "Unknown"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def inspect
|
|
19
|
+
"#{self.message} (#{self.code})"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
File without changes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pebblewatch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,11 +9,11 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2013-
|
|
12
|
+
date: 2013-04-01 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: serialport
|
|
16
|
-
requirement: &
|
|
16
|
+
requirement: &70119208390380 !ruby/object:Gem::Requirement
|
|
17
17
|
none: false
|
|
18
18
|
requirements:
|
|
19
19
|
- - ~>
|
|
@@ -21,10 +21,10 @@ dependencies:
|
|
|
21
21
|
version: '1.1'
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
|
-
version_requirements: *
|
|
24
|
+
version_requirements: *70119208390380
|
|
25
25
|
- !ruby/object:Gem::Dependency
|
|
26
26
|
name: rake
|
|
27
|
-
requirement: &
|
|
27
|
+
requirement: &70119208388900 !ruby/object:Gem::Requirement
|
|
28
28
|
none: false
|
|
29
29
|
requirements:
|
|
30
30
|
- - ! '>='
|
|
@@ -32,10 +32,10 @@ dependencies:
|
|
|
32
32
|
version: '0'
|
|
33
33
|
type: :development
|
|
34
34
|
prerelease: false
|
|
35
|
-
version_requirements: *
|
|
35
|
+
version_requirements: *70119208388900
|
|
36
36
|
- !ruby/object:Gem::Dependency
|
|
37
37
|
name: rspec
|
|
38
|
-
requirement: &
|
|
38
|
+
requirement: &70119208388220 !ruby/object:Gem::Requirement
|
|
39
39
|
none: false
|
|
40
40
|
requirements:
|
|
41
41
|
- - ! '>='
|
|
@@ -43,7 +43,7 @@ dependencies:
|
|
|
43
43
|
version: '0'
|
|
44
44
|
type: :development
|
|
45
45
|
prerelease: false
|
|
46
|
-
version_requirements: *
|
|
46
|
+
version_requirements: *70119208388220
|
|
47
47
|
description: A Ruby library for communicating with your Pebble smartwatch.
|
|
48
48
|
email: douwe@selenight.nl
|
|
49
49
|
executables: []
|
|
@@ -52,15 +52,16 @@ extra_rdoc_files: []
|
|
|
52
52
|
files:
|
|
53
53
|
- lib/pebble/capabilities.rb
|
|
54
54
|
- lib/pebble/endpoints.rb
|
|
55
|
-
- lib/pebble/pebblewatch.rb
|
|
56
55
|
- lib/pebble/protocol.rb
|
|
57
56
|
- lib/pebble/system_messages.rb
|
|
58
57
|
- lib/pebble/version.rb
|
|
59
58
|
- lib/pebble/watch/event.rb
|
|
60
59
|
- lib/pebble/watch/log_event.rb
|
|
61
60
|
- lib/pebble/watch/media_control_event.rb
|
|
61
|
+
- lib/pebble/watch/system_message_event.rb
|
|
62
62
|
- lib/pebble/watch.rb
|
|
63
63
|
- lib/pebble.rb
|
|
64
|
+
- lib/pebblewatch.rb
|
|
64
65
|
- LICENSE
|
|
65
66
|
- README.md
|
|
66
67
|
- Rakefile
|
|
@@ -80,7 +81,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
80
81
|
version: '0'
|
|
81
82
|
segments:
|
|
82
83
|
- 0
|
|
83
|
-
hash:
|
|
84
|
+
hash: 4433695980679618883
|
|
84
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
86
|
none: false
|
|
86
87
|
requirements:
|
|
@@ -89,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
89
90
|
version: '0'
|
|
90
91
|
segments:
|
|
91
92
|
- 0
|
|
92
|
-
hash:
|
|
93
|
+
hash: 4433695980679618883
|
|
93
94
|
requirements: []
|
|
94
95
|
rubyforge_project:
|
|
95
96
|
rubygems_version: 1.8.6
|