hybridgroup-pebblewatch 0.0.3
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.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +153 -0
- data/Rakefile +17 -0
- data/lib/pebble/capabilities.rb +26 -0
- data/lib/pebble/endpoints.rb +30 -0
- data/lib/pebble/protocol.rb +162 -0
- data/lib/pebble/system_messages.rb +16 -0
- data/lib/pebble/version.rb +3 -0
- data/lib/pebble/watch/app_message_event.rb +60 -0
- data/lib/pebble/watch/event.rb +14 -0
- data/lib/pebble/watch/log_event.rb +41 -0
- data/lib/pebble/watch/media_control_event.rb +29 -0
- data/lib/pebble/watch/system_message_event.rb +23 -0
- data/lib/pebble/watch.rb +279 -0
- data/lib/pebble.rb +25 -0
- data/lib/pebblewatch.rb +1 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 937789bdc1fc2b9c6c8c925996d0167f408ebb60
|
|
4
|
+
data.tar.gz: 3f1f77aa03e41f69d185a7abb47c8fc401dd8a7b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 54c63c16810a10685fb2c9fb424a3c194aa5cb9a53c80ad55d619d900be76a920855e4b3e037d79468017559884c159fc70d18cf18f667fc31e8dfbe33fc9154
|
|
7
|
+
data.tar.gz: b8752582686510075a44f0b4bffe8fd5b4c2b2c0b47c2489915d5650d0668c463ce6b8f05be3b1b6753b3bacee900f3bf01846f20ba1e8e0641fa261c6dee1fa
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2013 Douwe Maan
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# pebblewatch
|
|
2
|
+
|
|
3
|
+
A Ruby library for communicating with your Pebble smartwatch.
|
|
4
|
+
|
|
5
|
+
The protocol implementation was based on the documentation at http://pebbledev.org/, the Python implementation at [Hexxeh/libpebble](https://github.com/Hexxeh/libpebble) and the .NET implementation at [barometz/flint](https://github.com/barometz/flint).
|
|
6
|
+
|
|
7
|
+
## To Do
|
|
8
|
+
|
|
9
|
+
- [x] Basic protocol communication
|
|
10
|
+
- [x] Sending of messages (notifications etc)
|
|
11
|
+
- [x] Receiving of events (log, music control)
|
|
12
|
+
- [ ] Full 2-way app message support
|
|
13
|
+
- [ ] Firmware/app uploading
|
|
14
|
+
- [ ] CLI
|
|
15
|
+
- [ ] REPL
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
gem install pebblewatch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Make sure your Pebble is paired with 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`.
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "pebble"
|
|
29
|
+
|
|
30
|
+
# Create your watch using the serial port assigned to your Pebble.
|
|
31
|
+
watch = Pebble::Watch.new("7F30", "/dev/tty.Pebble7F30-SerialPortSe")
|
|
32
|
+
# You can also use autodetection if you're on OS X:
|
|
33
|
+
# watch = Pebble::Watch.autodetect
|
|
34
|
+
|
|
35
|
+
# The watch object will be on the receiving end of 3 kinds of events:
|
|
36
|
+
watch.on_event(:log) do |event|
|
|
37
|
+
puts "LOG"
|
|
38
|
+
puts "timestamp: #{event.timestamp}"
|
|
39
|
+
puts "level: #{event.level}"
|
|
40
|
+
puts "filename: #{event.filename}"
|
|
41
|
+
puts "linenumber: #{event.linenumber}"
|
|
42
|
+
puts "message: #{event.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
watch.on_event(:system_message) do |event|
|
|
46
|
+
puts "System Message: #{event.message} (#{event.code})"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
handler = watch.on_event(:media_control) do |event|
|
|
50
|
+
case event.button
|
|
51
|
+
when :playpause
|
|
52
|
+
puts "Play or pause music"
|
|
53
|
+
when :next
|
|
54
|
+
puts "Next track"
|
|
55
|
+
when :previous
|
|
56
|
+
puts "Previous track"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Suddenly had a change of heart? Just cover your ears.
|
|
61
|
+
watch.stop_listening(:media_control, handler)
|
|
62
|
+
|
|
63
|
+
# To make sure we don't miss any events, we haven't connected yet.
|
|
64
|
+
# Because we also want to *send* stuff to the watch, we will now.
|
|
65
|
+
watch.connect
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# We can of course send notifications.
|
|
69
|
+
watch.ping
|
|
70
|
+
watch.notification_sms("Scarlett Johansson", "Hey baby, what are you doing tonight?")
|
|
71
|
+
watch.notification_email("Tim Cook", "RE: Final offer",
|
|
72
|
+
"All right, you drive a hard bargain. We'll pay $2 Billion for the whole shop and that is our final offer.")
|
|
73
|
+
|
|
74
|
+
# Or let Pebble know what we're listening to.
|
|
75
|
+
watch.set_nowplaying_metadata(
|
|
76
|
+
"Artist you've probably never heard of",
|
|
77
|
+
"Album released exclusively through SoundCloud",
|
|
78
|
+
"Song that doesn't suck per se but isn't great either"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# We can also do some maintenancy things, although there's currently no firmware/app uploading.
|
|
82
|
+
versions = watch.get_versions
|
|
83
|
+
puts "Normal firmware version: #{versions[:firmwares][:normal][:version]}"
|
|
84
|
+
puts "Recovery firmware version: #{versions[:firmwares][:recovery][:version]}"
|
|
85
|
+
puts "Bluetooth MAC address: #{versions[:btmac]}"
|
|
86
|
+
|
|
87
|
+
# Fun fact: We don't have to wait for the results synchronously. (This works on every message with a response.)
|
|
88
|
+
watch.get_installed_apps do |apps|
|
|
89
|
+
puts "Installed apps: (#{apps[:apps].length} of #{apps[:banks_count]} banks in use)"
|
|
90
|
+
apps[:apps].each do |app|
|
|
91
|
+
puts "#{app[:index]}/#{app[:id]}: #{app[:name]} by #{app[:author]}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Dieting is not just for dogs anymore.
|
|
96
|
+
watch.remove_app(id, index)
|
|
97
|
+
|
|
98
|
+
time = watch.get_time
|
|
99
|
+
# Or asynchronously: watch.get_time { |time| ... }
|
|
100
|
+
|
|
101
|
+
# Time travel is just one method call away.
|
|
102
|
+
watch.set_time(Time.now + (365 * 24 * 60 * 60))
|
|
103
|
+
|
|
104
|
+
# This is mostly interesting for internal stuff, but since there's a :system_message event as well, I thought why not.
|
|
105
|
+
watch.system_message(Pebble::SystemMessages::FIRMWARE_OUT_OF_DATE)
|
|
106
|
+
|
|
107
|
+
# Yeah, I'd stay away from this one.
|
|
108
|
+
watch.reset
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# If you're done sending messages but want the program to keep listing for incoming events, say so:
|
|
112
|
+
watch.listen_for_events
|
|
113
|
+
# Note that listening will only end when the connection is lost, so anything that comes after this call will only then be executed.
|
|
114
|
+
# This will generally be the last call in your program.
|
|
115
|
+
|
|
116
|
+
# If we don't want to wait around and listen, just let the program exit or disconnect explicitly:
|
|
117
|
+
watch.disconnect
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Oh, and if you want to do some lower level stuff, have a look at [`Pebble::Protocol`](lib/pebble/protocol.rb) which you can access through `watch.protocol`.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
watch.protocol.on_receive(endpoint) do |message|
|
|
124
|
+
# Do whatever
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
watch.protocol.send_message(endpoint, message)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
Check out the [`examples/`](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.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
Copyright (c) 2013 Douwe Maan
|
|
135
|
+
|
|
136
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
137
|
+
a copy of this software and associated documentation files (the
|
|
138
|
+
"Software"), to deal in the Software without restriction, including
|
|
139
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
140
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
141
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
142
|
+
the following conditions:
|
|
143
|
+
|
|
144
|
+
The above copyright notice and this permission notice shall be
|
|
145
|
+
included in all copies or substantial portions of the Software.
|
|
146
|
+
|
|
147
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
148
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
149
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
150
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
151
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
152
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
153
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "rspec/core/rake_task"
|
|
2
|
+
|
|
3
|
+
spec = Gem::Specification.load("pebblewatch.gemspec")
|
|
4
|
+
|
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
6
|
+
|
|
7
|
+
task default: :spec
|
|
8
|
+
|
|
9
|
+
desc "Build the .gem file"
|
|
10
|
+
task :build do
|
|
11
|
+
system "gem build #{spec.name}.gemspec"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Push the .gem file to rubygems.org"
|
|
15
|
+
task release: :build do
|
|
16
|
+
system "gem push #{spec.name}-#{spec.version}.gem"
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Pebble
|
|
2
|
+
module Capabilities
|
|
3
|
+
module Session
|
|
4
|
+
GAMMA_RAY = 0x80000000
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module Client
|
|
8
|
+
UNKNOWN = 0
|
|
9
|
+
IOS = 1
|
|
10
|
+
ANDROID = 2
|
|
11
|
+
OSX = 3
|
|
12
|
+
LINUX = 4
|
|
13
|
+
WINDOWS = 5
|
|
14
|
+
|
|
15
|
+
TELEPHONY = 16
|
|
16
|
+
SMS = 32
|
|
17
|
+
GPS = 64
|
|
18
|
+
BTLE = 128
|
|
19
|
+
# CAMERA_FRONT = 240 # Doesn't make sense as it'd mess up the bitmask, but it's apparently true.
|
|
20
|
+
CAMERA_REAR = 256
|
|
21
|
+
ACCEL = 512
|
|
22
|
+
GYRO = 1024
|
|
23
|
+
COMPASS = 2048
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Pebble
|
|
2
|
+
module Endpoints
|
|
3
|
+
FIRMWARE = 1
|
|
4
|
+
TIME = 11
|
|
5
|
+
VERSION = 16
|
|
6
|
+
PHONE_VERSION = 17
|
|
7
|
+
SYSTEM_MESSAGE = 18
|
|
8
|
+
MUSIC_CONTROL = 32
|
|
9
|
+
PHONE_CONTROL = 33
|
|
10
|
+
APPLICATION_MESSAGE = 48
|
|
11
|
+
LAUNCHER = 49
|
|
12
|
+
LOGS = 2000
|
|
13
|
+
PING = 2001
|
|
14
|
+
DRAW = 2002
|
|
15
|
+
RESET = 2003
|
|
16
|
+
APP = 2004
|
|
17
|
+
NOTIFICATION = 3000
|
|
18
|
+
RESOURCE = 4000
|
|
19
|
+
SYS_REG = 5000
|
|
20
|
+
FCT_REG = 5001
|
|
21
|
+
APP_INSTALL_MANAGER = 6000
|
|
22
|
+
RUNKEEPER = 7000
|
|
23
|
+
PUT_BYTES = 48879
|
|
24
|
+
MAX_ENDPOINT = 65535
|
|
25
|
+
|
|
26
|
+
def self.for_code(code)
|
|
27
|
+
constants.find { |constant| const_get(constant) == code }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require "serialport"
|
|
2
|
+
|
|
3
|
+
module Pebble
|
|
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
|
+
if @port.is_a?(String)
|
|
36
|
+
@serial_port = SerialPort.new(@port, baudrate: 115200)
|
|
37
|
+
@serial_port.read_timeout = 500
|
|
38
|
+
else
|
|
39
|
+
@serial_port = @port
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@connected = true
|
|
43
|
+
Pebble.logger.debug "Connected to port #{@port}"
|
|
44
|
+
|
|
45
|
+
@receive_messages_thread = Thread.new(&method(:receive_messages))
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def disconnect
|
|
51
|
+
raise Errors::NotConnected unless @connected
|
|
52
|
+
|
|
53
|
+
@connected = false
|
|
54
|
+
|
|
55
|
+
@serial_port.close()
|
|
56
|
+
@serial_port = nil
|
|
57
|
+
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def listen_for_messages
|
|
62
|
+
raise Errors::NotConnected unless @connected
|
|
63
|
+
|
|
64
|
+
@receive_messages_thread.join
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def on_receive(endpoint = :any, &handler)
|
|
68
|
+
@message_handlers[endpoint] << handler
|
|
69
|
+
handler
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stop_receiving(*params)
|
|
73
|
+
handler = params.pop
|
|
74
|
+
endpoint = params.pop || :any
|
|
75
|
+
|
|
76
|
+
@message_handlers[endpoint].delete(handler)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def send_message(endpoint, message, async_response_handler = nil, &response_parser)
|
|
80
|
+
raise Errors::NotConnected unless @connected
|
|
81
|
+
|
|
82
|
+
message ||= ""
|
|
83
|
+
|
|
84
|
+
Pebble.logger.debug "Sending #{Endpoints.for_code(endpoint) || endpoint}: #{message.inspect}"
|
|
85
|
+
|
|
86
|
+
data = [message.size, endpoint].pack("S>S>") + message
|
|
87
|
+
|
|
88
|
+
@send_message_mutex.synchronize do
|
|
89
|
+
@serial_port.write(data)
|
|
90
|
+
|
|
91
|
+
if response_parser
|
|
92
|
+
if async_response_handler
|
|
93
|
+
identifier = on_receive(endpoint) do |response_message|
|
|
94
|
+
stop_receiving(endpoint, identifier)
|
|
95
|
+
|
|
96
|
+
parsed_response = response_parser.call(response_message)
|
|
97
|
+
|
|
98
|
+
async_response_handler.call(parsed_response)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
true
|
|
102
|
+
else
|
|
103
|
+
received = false
|
|
104
|
+
parsed_response = nil
|
|
105
|
+
identifier = on_receive(endpoint) do |response_message|
|
|
106
|
+
stop_receiving(endpoint, identifier)
|
|
107
|
+
|
|
108
|
+
parsed_response = response_parser.call(response_message)
|
|
109
|
+
received = true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sleep 0.015 until received
|
|
113
|
+
|
|
114
|
+
parsed_response
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
def receive_messages
|
|
124
|
+
Pebble.logger.debug "Waiting for messages"
|
|
125
|
+
while @connected
|
|
126
|
+
header = @serial_port.read(4)
|
|
127
|
+
next unless header
|
|
128
|
+
|
|
129
|
+
raise Errors::MalformedResponse if header.length < 4
|
|
130
|
+
|
|
131
|
+
size, endpoint = header.unpack("S>S>")
|
|
132
|
+
message = @serial_port.read(size)
|
|
133
|
+
|
|
134
|
+
Pebble.logger.debug "Received #{Endpoints.for_code(endpoint) || endpoint}: #{message.inspect}"
|
|
135
|
+
|
|
136
|
+
trigger_received(endpoint, message)
|
|
137
|
+
end
|
|
138
|
+
rescue IOError => e
|
|
139
|
+
if @connected
|
|
140
|
+
Pebble.logger.debug "Lost connection"
|
|
141
|
+
@connected = false
|
|
142
|
+
raise Errors::LostConnection
|
|
143
|
+
end
|
|
144
|
+
ensure
|
|
145
|
+
Pebble.logger.debug "Finished waiting for messages"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def trigger_received(endpoint, message)
|
|
149
|
+
@message_handlers[:any].each do |handler|
|
|
150
|
+
Thread.new(handler) do |handler|
|
|
151
|
+
handler.call(endpoint, message)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@message_handlers[endpoint].each do |handler|
|
|
156
|
+
Thread.new(handler) do |handler|
|
|
157
|
+
handler.call(message)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
|
16
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Pebble
|
|
2
|
+
class Watch
|
|
3
|
+
class AppMessageEvent < Event
|
|
4
|
+
module TupleType
|
|
5
|
+
ByteArray = 0
|
|
6
|
+
String = 1
|
|
7
|
+
UnsignedInteger = 2
|
|
8
|
+
Integer = 3
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_accessor :transaction_id
|
|
12
|
+
attr_accessor :uuid
|
|
13
|
+
attr_accessor :data
|
|
14
|
+
|
|
15
|
+
def self.parse(raw_message)
|
|
16
|
+
return nil if raw_message.length < 19
|
|
17
|
+
|
|
18
|
+
message_type, transaction_id, uuid, dictionary_length = raw_message[0, 19].unpack("CCA16C")
|
|
19
|
+
|
|
20
|
+
data = {}
|
|
21
|
+
|
|
22
|
+
offset = 19
|
|
23
|
+
dictionary_length.times do |i|
|
|
24
|
+
key, tuple_type, item_length = raw_message[offset, 7].unpack("CL>S<")
|
|
25
|
+
offset += 7
|
|
26
|
+
|
|
27
|
+
format = case tuple_type
|
|
28
|
+
when TupleType::ByteArray; "a*"
|
|
29
|
+
when TupleType::String; "A*"
|
|
30
|
+
when TupleType::UnsignedInteger
|
|
31
|
+
case item_length
|
|
32
|
+
when 1; "C"
|
|
33
|
+
when 2; "S<"
|
|
34
|
+
when 4; "L<"
|
|
35
|
+
end
|
|
36
|
+
when TupleType::Integer
|
|
37
|
+
case item_length
|
|
38
|
+
when 1; "c"
|
|
39
|
+
when 2; "s<"
|
|
40
|
+
when 4; "l<"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
item_data = raw_message[offset, item_length].unpack(format).first
|
|
45
|
+
offset += item_length
|
|
46
|
+
|
|
47
|
+
data[key] = item_data
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
event = new
|
|
51
|
+
|
|
52
|
+
event.transaction_id = transaction_id
|
|
53
|
+
event.uuid = uuid
|
|
54
|
+
event.data = data
|
|
55
|
+
|
|
56
|
+
event
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Pebble
|
|
2
|
+
class Watch
|
|
3
|
+
class Event
|
|
4
|
+
def self.parse(message)
|
|
5
|
+
new
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require "pebble/watch/log_event"
|
|
12
|
+
require "pebble/watch/system_message_event"
|
|
13
|
+
require "pebble/watch/media_control_event"
|
|
14
|
+
require "pebble/watch/app_message_event"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Pebble
|
|
2
|
+
class Watch
|
|
3
|
+
class LogEvent < Event
|
|
4
|
+
attr_accessor :timestamp
|
|
5
|
+
attr_accessor :level
|
|
6
|
+
attr_accessor :filename
|
|
7
|
+
attr_accessor :linenumber
|
|
8
|
+
attr_accessor :message
|
|
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
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Pebble
|
|
2
|
+
class Watch
|
|
3
|
+
class MediaControlEvent < Event
|
|
4
|
+
attr_accessor :button
|
|
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
|
|
27
|
+
end
|
|
28
|
+
end
|
|
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
|
data/lib/pebble/watch.rb
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
require "pebble/watch/event"
|
|
2
|
+
|
|
3
|
+
module Pebble
|
|
4
|
+
class Watch
|
|
5
|
+
module Errors
|
|
6
|
+
class NoWatchesFound < StandardError; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.autodetect
|
|
10
|
+
return nil unless RUBY_PLATFORM =~ /darwin/
|
|
11
|
+
|
|
12
|
+
watches = Dir.glob("/dev/tty.Pebble????-SerialPortSe")
|
|
13
|
+
|
|
14
|
+
raise Errors::NoWatchesFound if watches.length == 0
|
|
15
|
+
Pebble.logger.debug "Found multiple watches: #{watches}" if watches.length > 1
|
|
16
|
+
|
|
17
|
+
port = watches.first
|
|
18
|
+
id = port[15, 4]
|
|
19
|
+
Pebble.logger.debug "Detected 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
|
+
receive_event_messages
|
|
54
|
+
log_log_events
|
|
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
|
+
Pebble.logger.debug "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
|
+
[:app_message, Endpoints::APPLICATION_MESSAGE, AppMessageEvent]
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
events.each do |(name, endpoint, event_klass)|
|
|
210
|
+
@protocol.on_receive(endpoint) do |message|
|
|
211
|
+
event = event_klass.parse(message)
|
|
212
|
+
trigger_event(name, event) if event
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def log_log_events
|
|
218
|
+
on_event(:log) do |event|
|
|
219
|
+
case event.level
|
|
220
|
+
when :error
|
|
221
|
+
Pebble.logger.error event.message
|
|
222
|
+
when :warning
|
|
223
|
+
Pebble.logger.warn event.message
|
|
224
|
+
when :info
|
|
225
|
+
Pebble.logger.info event.message
|
|
226
|
+
when :debug, :verbose
|
|
227
|
+
Pebble.logger.debug event.message
|
|
228
|
+
else
|
|
229
|
+
Pebble.logger.info event.message
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def trigger_event(name, event)
|
|
235
|
+
Pebble.logger.debug "Triggering event '#{name}': #{event.inspect}"
|
|
236
|
+
|
|
237
|
+
@event_handlers[:any].each do |handler|
|
|
238
|
+
Thread.new(handler) do |handler|
|
|
239
|
+
handler.call(name, event)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
@event_handlers[name].each do |handler|
|
|
244
|
+
Thread.new(handler) do |handler|
|
|
245
|
+
handler.call(event)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def notification(type, *params)
|
|
251
|
+
types = {
|
|
252
|
+
email: 0,
|
|
253
|
+
sms: 1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
timestamp = Time.now.to_i
|
|
257
|
+
params.insert(2, timestamp.to_s)
|
|
258
|
+
|
|
259
|
+
message = [types[type]].pack("C")
|
|
260
|
+
message << package_strings(*params)
|
|
261
|
+
|
|
262
|
+
@protocol.send_message(Endpoints::NOTIFICATION, message)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def package_strings(*parts)
|
|
266
|
+
max_part_length = 255
|
|
267
|
+
max_part_length = parts.pop if parts.last.is_a?(Integer)
|
|
268
|
+
|
|
269
|
+
message = ""
|
|
270
|
+
parts.each do |part|
|
|
271
|
+
part ||= ""
|
|
272
|
+
|
|
273
|
+
part = part[0, max_part_length] if part.length > max_part_length
|
|
274
|
+
message << [part.length].pack("C") + part
|
|
275
|
+
end
|
|
276
|
+
message
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
data/lib/pebble.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module Pebble
|
|
4
|
+
def self.logger=(new_logger)
|
|
5
|
+
@@logger = new_logger
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.logger
|
|
9
|
+
return @@logger if defined?(@@logger)
|
|
10
|
+
@@logger = default_logger
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.default_logger
|
|
14
|
+
logger = Logger.new(STDOUT)
|
|
15
|
+
logger.level = Logger::INFO
|
|
16
|
+
logger
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require "pebble/version"
|
|
21
|
+
require "pebble/endpoints"
|
|
22
|
+
require "pebble/capabilities"
|
|
23
|
+
require "pebble/system_messages"
|
|
24
|
+
require "pebble/protocol"
|
|
25
|
+
require "pebble/watch"
|
data/lib/pebblewatch.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "pebble"
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hybridgroup-pebblewatch
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Douwe Maan
|
|
8
|
+
- Ron Evans
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2013-06-26 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: serialport
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - ~>
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '1.1'
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - ~>
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: '1.1'
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: rake
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - '>='
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '0'
|
|
35
|
+
type: :development
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - '>='
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: rspec
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - '>='
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
type: :development
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - '>='
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
description: A Ruby library for communicating with your Pebble smartwatch.
|
|
57
|
+
email:
|
|
58
|
+
- douwe@selenight.nl
|
|
59
|
+
- artoo@hybridgroup.com
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- lib/pebblewatch.rb
|
|
65
|
+
- lib/pebble.rb
|
|
66
|
+
- lib/pebble/protocol.rb
|
|
67
|
+
- lib/pebble/capabilities.rb
|
|
68
|
+
- lib/pebble/system_messages.rb
|
|
69
|
+
- lib/pebble/watch.rb
|
|
70
|
+
- lib/pebble/watch/system_message_event.rb
|
|
71
|
+
- lib/pebble/watch/app_message_event.rb
|
|
72
|
+
- lib/pebble/watch/media_control_event.rb
|
|
73
|
+
- lib/pebble/watch/event.rb
|
|
74
|
+
- lib/pebble/watch/log_event.rb
|
|
75
|
+
- lib/pebble/version.rb
|
|
76
|
+
- lib/pebble/endpoints.rb
|
|
77
|
+
- LICENSE
|
|
78
|
+
- README.md
|
|
79
|
+
- Rakefile
|
|
80
|
+
- Gemfile
|
|
81
|
+
homepage: https://github.com/hybridgroup/pebblewatch
|
|
82
|
+
licenses:
|
|
83
|
+
- MIT
|
|
84
|
+
metadata: {}
|
|
85
|
+
post_install_message:
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - '>='
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - '>='
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubyforge_project:
|
|
101
|
+
rubygems_version: 2.0.3
|
|
102
|
+
signing_key:
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: Pebble communication library
|
|
105
|
+
test_files: []
|