timex_datalink_client 0.6.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/timex_datalink_client/helpers/four_byte_formatter.rb +29 -0
- data/lib/timex_datalink_client/protocol_4/alarm.rb +59 -0
- data/lib/timex_datalink_client/protocol_4/eeprom/anniversary.rb +44 -0
- data/lib/timex_datalink_client/protocol_4/eeprom/appointment.rb +49 -0
- data/lib/timex_datalink_client/protocol_4/eeprom/list.rb +43 -0
- data/lib/timex_datalink_client/protocol_4/eeprom/phone_number.rb +56 -0
- data/lib/timex_datalink_client/protocol_4/eeprom.rb +95 -0
- data/lib/timex_datalink_client/protocol_4/end.rb +20 -0
- data/lib/timex_datalink_client/protocol_4/sound_options.rb +42 -0
- data/lib/timex_datalink_client/protocol_4/sound_theme.rb +65 -0
- data/lib/timex_datalink_client/protocol_4/start.rb +20 -0
- data/lib/timex_datalink_client/protocol_4/sync.rb +40 -0
- data/lib/timex_datalink_client/protocol_4/time.rb +77 -0
- data/lib/timex_datalink_client/protocol_4/wrist_app.rb +67 -0
- data/lib/timex_datalink_client/protocol_7/eeprom/activity.rb +94 -0
- data/lib/timex_datalink_client/protocol_7/eeprom/calendar/event.rb +33 -0
- data/lib/timex_datalink_client/protocol_7/eeprom/calendar.rb +91 -0
- data/lib/timex_datalink_client/protocol_7/eeprom/games.rb +124 -0
- data/lib/timex_datalink_client/protocol_7/eeprom/phone_number.rb +79 -0
- data/lib/timex_datalink_client/protocol_7/eeprom/speech.rb +228 -0
- data/lib/timex_datalink_client/protocol_7/eeprom.rb +76 -0
- data/lib/timex_datalink_client/protocol_7/end.rb +20 -0
- data/lib/timex_datalink_client/protocol_7/phrase_builder.rb +70 -0
- data/lib/timex_datalink_client/protocol_7/start.rb +20 -0
- data/lib/timex_datalink_client/protocol_7/sync.rb +40 -0
- data/lib/timex_datalink_client/version.rb +1 -1
- data/lib/timex_datalink_client.rb +34 -5
- metadata +42 -3
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timex_datalink_client/helpers/cpacket_paginator"
|
4
|
+
require "timex_datalink_client/helpers/crc_packets_wrapper"
|
5
|
+
|
6
|
+
class TimexDatalinkClient
|
7
|
+
class Protocol4
|
8
|
+
class WristApp
|
9
|
+
include Helpers::CpacketPaginator
|
10
|
+
prepend Helpers::CrcPacketsWrapper
|
11
|
+
|
12
|
+
CPACKET_CLEAR = [0x93, 0x02]
|
13
|
+
CPACKET_SECT = [0x90, 0x02]
|
14
|
+
CPACKET_DATA = [0x91, 0x02]
|
15
|
+
CPACKET_END = [0x92, 0x02]
|
16
|
+
|
17
|
+
CPACKET_DATA_LENGTH = 32
|
18
|
+
WRIST_APP_DELIMITER = /\xac.*\r\n/n
|
19
|
+
WRIST_APP_CODE_INDEX = 18
|
20
|
+
|
21
|
+
attr_accessor :zap_file
|
22
|
+
|
23
|
+
# Create a WristApp instance.
|
24
|
+
#
|
25
|
+
# @param wrist_app_data [String, nil] WristApp data.
|
26
|
+
# @param zap_file [String, nil] Path to ZAP file.
|
27
|
+
# @return [WristApp] WristApp instance.
|
28
|
+
def initialize(wrist_app_data: nil, zap_file: nil)
|
29
|
+
@wrist_app_data = wrist_app_data
|
30
|
+
@zap_file = zap_file
|
31
|
+
end
|
32
|
+
|
33
|
+
# Compile packets for an alarm.
|
34
|
+
#
|
35
|
+
# @return [Array<Array<Integer>>] Two-dimensional array of integers that represent bytes.
|
36
|
+
def packets
|
37
|
+
[CPACKET_CLEAR, cpacket_sect] + payloads + [CPACKET_END]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def cpacket_sect
|
43
|
+
CPACKET_SECT + [payloads.length, 1]
|
44
|
+
end
|
45
|
+
|
46
|
+
def payloads
|
47
|
+
paginate_cpackets(header: CPACKET_DATA, length: CPACKET_DATA_LENGTH, cpackets: wrist_app_data.bytes)
|
48
|
+
end
|
49
|
+
|
50
|
+
def wrist_app_data
|
51
|
+
@wrist_app_data || zap_file_data_binary
|
52
|
+
end
|
53
|
+
|
54
|
+
def zap_file_data
|
55
|
+
File.open(zap_file, "rb").read
|
56
|
+
end
|
57
|
+
|
58
|
+
def zap_file_data_ascii
|
59
|
+
zap_file_data.split(WRIST_APP_DELIMITER)[WRIST_APP_CODE_INDEX]
|
60
|
+
end
|
61
|
+
|
62
|
+
def zap_file_data_binary
|
63
|
+
[zap_file_data_ascii].pack("H*")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timex_datalink_client/helpers/four_byte_formatter"
|
4
|
+
|
5
|
+
class TimexDatalinkClient
|
6
|
+
class Protocol7
|
7
|
+
class Eeprom
|
8
|
+
class Activity
|
9
|
+
include Helpers::FourByteFormatter
|
10
|
+
|
11
|
+
METADATA_BYTES_BASE = 6
|
12
|
+
METADATA_BYTES_SIZE = 5
|
13
|
+
|
14
|
+
PACKETS_TERMINATOR = 0x04
|
15
|
+
|
16
|
+
# Compile data for all activities.
|
17
|
+
#
|
18
|
+
# @param activities [Array<Activity>] Activities to compile data for.
|
19
|
+
# @return [Array] Compiled data of all activities.
|
20
|
+
def self.packets(activities)
|
21
|
+
header(activities) + metadata_and_messages(activities) + [PACKETS_TERMINATOR]
|
22
|
+
end
|
23
|
+
|
24
|
+
private_class_method def self.header(activities)
|
25
|
+
[
|
26
|
+
random_speech(activities),
|
27
|
+
0,
|
28
|
+
0,
|
29
|
+
0,
|
30
|
+
activities.count,
|
31
|
+
0
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
private_class_method def self.random_speech(activities)
|
36
|
+
activities.each_with_index.sum do |activity, activity_index|
|
37
|
+
activity.random_speech ? 1 << activity_index : 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private_class_method def self.metadata_and_messages(activities)
|
42
|
+
metadata = activities.each_with_index.map do |activity, activity_index|
|
43
|
+
activity.metadata_packet(activities.count + activity_index)
|
44
|
+
end
|
45
|
+
|
46
|
+
messages = activities.map { |activity| activity.messages_packet }
|
47
|
+
|
48
|
+
(metadata + messages).flatten
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_accessor :time, :messages, :random_speech
|
52
|
+
|
53
|
+
# Create an Activity instance.
|
54
|
+
#
|
55
|
+
# @param time [::Time] Time of activity.
|
56
|
+
# @param messages [Array<Array<Integer>>] Messages for activity.
|
57
|
+
# @param random_speech [Boolean] If activity should have random speech.
|
58
|
+
# @return [Activity] Activity instance.
|
59
|
+
def initialize(time:, messages:, random_speech:)
|
60
|
+
@time = time
|
61
|
+
@messages = messages
|
62
|
+
@random_speech = random_speech
|
63
|
+
end
|
64
|
+
|
65
|
+
# Compile a metadata packet for an activity.
|
66
|
+
#
|
67
|
+
# @param activity_index [Integer] Activity index.
|
68
|
+
# @return [Array<Integer>] Array of integers that represent bytes.
|
69
|
+
def metadata_packet(activity_index)
|
70
|
+
[
|
71
|
+
time.hour,
|
72
|
+
time.min,
|
73
|
+
messages.count,
|
74
|
+
metadata_bytes(activity_index),
|
75
|
+
0
|
76
|
+
].flatten
|
77
|
+
end
|
78
|
+
|
79
|
+
# Compile a message packet for an activity.
|
80
|
+
#
|
81
|
+
# @return [Array<Integer>] Array of integers that represent bytes.
|
82
|
+
def messages_packet
|
83
|
+
four_byte_format_for(messages)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def metadata_bytes(activity_index)
|
89
|
+
METADATA_BYTES_BASE + METADATA_BYTES_SIZE * activity_index
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TimexDatalinkClient
|
4
|
+
class Protocol7
|
5
|
+
class Eeprom
|
6
|
+
class Calendar
|
7
|
+
class Event
|
8
|
+
FIVE_MINUTES_SECONDS = 300
|
9
|
+
|
10
|
+
attr_accessor :time, :phrase
|
11
|
+
|
12
|
+
# Create an Event instance.
|
13
|
+
#
|
14
|
+
# @param time [::Time] Time of event.
|
15
|
+
# @param phrase [Array<Integer>] Phrase for event.
|
16
|
+
# @return [Event] Event instance.
|
17
|
+
def initialize(time:, phrase:)
|
18
|
+
@time = time
|
19
|
+
@phrase = phrase
|
20
|
+
end
|
21
|
+
|
22
|
+
def time_formatted(device_time)
|
23
|
+
device_time_midnight = Time.new(device_time.year, device_time.month, device_time.day)
|
24
|
+
seconds = (time - device_time_midnight).to_i
|
25
|
+
five_minutes = seconds / FIVE_MINUTES_SECONDS
|
26
|
+
|
27
|
+
five_minutes.divmod(256).reverse
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timex_datalink_client/helpers/four_byte_formatter"
|
4
|
+
|
5
|
+
class TimexDatalinkClient
|
6
|
+
class Protocol7
|
7
|
+
class Eeprom
|
8
|
+
class Calendar
|
9
|
+
include Helpers::FourByteFormatter
|
10
|
+
|
11
|
+
DAY_START_TIME = Time.new(2000)
|
12
|
+
DAY_SECONDS = 86400
|
13
|
+
|
14
|
+
EVENTS_BYTES_BASE = 2
|
15
|
+
EVENTS_BYTES_EVENT = 4
|
16
|
+
EVENTS_BYTES_PHRASE_PACKET = 5
|
17
|
+
|
18
|
+
PACKETS_TERMINATOR = 0x01
|
19
|
+
|
20
|
+
attr_accessor :time, :events
|
21
|
+
|
22
|
+
# Create a Calendar instance.
|
23
|
+
#
|
24
|
+
# @param time [::Time] Time to set device to.
|
25
|
+
# @param events [Array<Event>] Event instances to add to the calendar.
|
26
|
+
# @return [Calendar] Calendar instance.
|
27
|
+
def initialize(time:, events: [])
|
28
|
+
@time = time
|
29
|
+
@events = events
|
30
|
+
end
|
31
|
+
|
32
|
+
# Compile data for calendar.
|
33
|
+
#
|
34
|
+
# @return [Array<Integer>] Compiled data for calendar.
|
35
|
+
def packet
|
36
|
+
[
|
37
|
+
events_count,
|
38
|
+
event_packets,
|
39
|
+
event_phrases,
|
40
|
+
time.hour,
|
41
|
+
time.min,
|
42
|
+
days_from_2000,
|
43
|
+
time_formatted,
|
44
|
+
PACKETS_TERMINATOR
|
45
|
+
].flatten
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def events_count
|
51
|
+
events.count.divmod(256).reverse
|
52
|
+
end
|
53
|
+
|
54
|
+
def event_packets
|
55
|
+
event_bytes = EVENTS_BYTES_BASE
|
56
|
+
event_bytes += EVENTS_BYTES_EVENT * events.count
|
57
|
+
|
58
|
+
[].tap do |event_packets|
|
59
|
+
events.each_with_index do |event, event_index|
|
60
|
+
event_bytes_formatted = event_bytes.divmod(256).reverse
|
61
|
+
event_time_formatted = event.time_formatted(time)
|
62
|
+
|
63
|
+
event_packets << [event_time_formatted, event_bytes_formatted]
|
64
|
+
|
65
|
+
event_bytes += EVENTS_BYTES_PHRASE_PACKET * (1 + event.phrase.length / 4)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def event_phrases
|
71
|
+
phrases = events.map(&:phrase)
|
72
|
+
|
73
|
+
four_byte_format_for(phrases)
|
74
|
+
end
|
75
|
+
|
76
|
+
def days_from_2000
|
77
|
+
since_start_time_seconds = time - DAY_START_TIME
|
78
|
+
since_start_time_days = since_start_time_seconds.to_i / DAY_SECONDS
|
79
|
+
|
80
|
+
since_start_time_days.divmod(256).reverse
|
81
|
+
end
|
82
|
+
|
83
|
+
def time_formatted
|
84
|
+
five_mintes = (time.hour * 60 + time.min) / 5
|
85
|
+
|
86
|
+
five_mintes.divmod(256).reverse
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timex_datalink_client/helpers/four_byte_formatter"
|
4
|
+
|
5
|
+
class TimexDatalinkClient
|
6
|
+
class Protocol7
|
7
|
+
class Eeprom
|
8
|
+
class Games
|
9
|
+
include Helpers::FourByteFormatter
|
10
|
+
|
11
|
+
COUNTDOWN_TIMER_SECONDS_DEFAULT = 60
|
12
|
+
|
13
|
+
COUNTDOWN_TIMER_SOUND_DEFAULT = 0x062
|
14
|
+
MUSIC_TIME_KEEPER_SOUND_DEFAULT = 0x062
|
15
|
+
|
16
|
+
PACKETS_TERMINATOR = 0x02
|
17
|
+
|
18
|
+
attr_accessor :memory_game_enabled, :fortune_teller_enabled, :countdown_timer_enabled, :countdown_timer_seconds,
|
19
|
+
:countdown_timer_sound, :mind_reader_enabled, :music_time_keeper_enabled, :music_time_keeper_sound,
|
20
|
+
:morse_code_practice_enabled, :treasure_hunter_enabled, :rhythm_rhyme_buster_enabled, :stop_watch_enabled,
|
21
|
+
:red_light_green_light_enabled
|
22
|
+
|
23
|
+
# Create a Games instance.
|
24
|
+
#
|
25
|
+
# @param memory_game_enabled [Boolean] Toggle memory game.
|
26
|
+
# @param fortune_teller_enabled [Boolean] Toggle fortune teller.
|
27
|
+
# @param countdown_timer_enabled [Boolean] Toggle countdown timer.
|
28
|
+
# @param countdown_timer_seconds [Integer] Duration for countdown timer in seconds.
|
29
|
+
# @param countdown_timer_sound [Integer] Sound for countdown timer.
|
30
|
+
# @param mind_reader_enabled [Boolean] Toggle mind reader.
|
31
|
+
# @param music_time_keeper_enabled [Boolean] Toggle music time keeper.
|
32
|
+
# @param music_time_keeper_sound [Integer] Sound for music time keeper.
|
33
|
+
# @param morse_code_practice_enabled [Boolean] Toggle Morse code practice.
|
34
|
+
# @param treasure_hunter_enabled [Boolean] Toggle treasure hunter.
|
35
|
+
# @param rhythm_rhyme_buster_enabled [Boolean] Toggle rhythm & rhyme buster.
|
36
|
+
# @param stop_watch_enabled [Boolean] Toggle stop watch.
|
37
|
+
# @param red_light_green_light_enabled [Boolean] Toggle red light, green light.
|
38
|
+
# @return [Games] Games instance.
|
39
|
+
def initialize(
|
40
|
+
memory_game_enabled: false,
|
41
|
+
fortune_teller_enabled: false,
|
42
|
+
countdown_timer_enabled: false,
|
43
|
+
countdown_timer_seconds: COUNTDOWN_TIMER_SECONDS_DEFAULT,
|
44
|
+
countdown_timer_sound: COUNTDOWN_TIMER_SOUND_DEFAULT,
|
45
|
+
mind_reader_enabled: false,
|
46
|
+
music_time_keeper_enabled: false,
|
47
|
+
music_time_keeper_sound: MUSIC_TIME_KEEPER_SOUND_DEFAULT,
|
48
|
+
morse_code_practice_enabled: false,
|
49
|
+
treasure_hunter_enabled: false,
|
50
|
+
rhythm_rhyme_buster_enabled: false,
|
51
|
+
stop_watch_enabled: false,
|
52
|
+
red_light_green_light_enabled: false
|
53
|
+
)
|
54
|
+
@memory_game_enabled = memory_game_enabled
|
55
|
+
@fortune_teller_enabled = fortune_teller_enabled
|
56
|
+
@countdown_timer_enabled = countdown_timer_enabled
|
57
|
+
@countdown_timer_seconds = countdown_timer_seconds
|
58
|
+
@countdown_timer_sound = countdown_timer_sound
|
59
|
+
@mind_reader_enabled = mind_reader_enabled
|
60
|
+
@music_time_keeper_enabled = music_time_keeper_enabled
|
61
|
+
@music_time_keeper_sound = music_time_keeper_sound
|
62
|
+
@morse_code_practice_enabled = morse_code_practice_enabled
|
63
|
+
@treasure_hunter_enabled = treasure_hunter_enabled
|
64
|
+
@rhythm_rhyme_buster_enabled = rhythm_rhyme_buster_enabled
|
65
|
+
@stop_watch_enabled = stop_watch_enabled
|
66
|
+
@red_light_green_light_enabled = red_light_green_light_enabled
|
67
|
+
end
|
68
|
+
|
69
|
+
# Compile data for games.
|
70
|
+
#
|
71
|
+
# @return [Array<Integer>] Compiled data for games.
|
72
|
+
def packet
|
73
|
+
[
|
74
|
+
enabled_games,
|
75
|
+
countdown_timer_time,
|
76
|
+
sounds,
|
77
|
+
PACKETS_TERMINATOR
|
78
|
+
].flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def enabled_games
|
84
|
+
bitmask = games.each_with_index.sum do |game, game_index|
|
85
|
+
game ? 1 << game_index : 0
|
86
|
+
end
|
87
|
+
|
88
|
+
bitmask.divmod(256).reverse
|
89
|
+
end
|
90
|
+
|
91
|
+
def countdown_timer_time
|
92
|
+
(countdown_timer_seconds * 10).divmod(256).reverse
|
93
|
+
end
|
94
|
+
|
95
|
+
def sounds
|
96
|
+
sounds_extra_packet = four_byte_format_for(
|
97
|
+
[
|
98
|
+
[music_time_keeper_sound],
|
99
|
+
[countdown_timer_sound],
|
100
|
+
[]
|
101
|
+
]
|
102
|
+
)
|
103
|
+
|
104
|
+
sounds_extra_packet.first(10)
|
105
|
+
end
|
106
|
+
|
107
|
+
def games
|
108
|
+
[
|
109
|
+
memory_game_enabled,
|
110
|
+
fortune_teller_enabled,
|
111
|
+
countdown_timer_enabled,
|
112
|
+
mind_reader_enabled,
|
113
|
+
music_time_keeper_enabled,
|
114
|
+
morse_code_practice_enabled,
|
115
|
+
treasure_hunter_enabled,
|
116
|
+
rhythm_rhyme_buster_enabled,
|
117
|
+
stop_watch_enabled,
|
118
|
+
red_light_green_light_enabled
|
119
|
+
]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timex_datalink_client/helpers/four_byte_formatter"
|
4
|
+
|
5
|
+
class TimexDatalinkClient
|
6
|
+
class Protocol7
|
7
|
+
class Eeprom
|
8
|
+
class PhoneNumber
|
9
|
+
include Helpers::FourByteFormatter
|
10
|
+
|
11
|
+
PHONE_NUMBER_DIGITS_MAP = {
|
12
|
+
"0" => 0x001,
|
13
|
+
"1" => 0x002,
|
14
|
+
"2" => 0x003,
|
15
|
+
"3" => 0x004,
|
16
|
+
"4" => 0x005,
|
17
|
+
"5" => 0x006,
|
18
|
+
"6" => 0x007,
|
19
|
+
"7" => 0x008,
|
20
|
+
"8" => 0x009,
|
21
|
+
"9" => 0x00a
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
PACKETS_TERMINATOR = 0x03
|
25
|
+
|
26
|
+
# Compile data for all phone numbers.
|
27
|
+
#
|
28
|
+
# @param phone_numbers [Array<PhoneNumber>] Phone numbers to compile data for.
|
29
|
+
# @return [Array] Compiled data of all phone numbers.
|
30
|
+
def self.packets(phone_numbers)
|
31
|
+
header(phone_numbers) + names_and_numbers(phone_numbers) + [PACKETS_TERMINATOR]
|
32
|
+
end
|
33
|
+
|
34
|
+
private_class_method def self.header(phone_numbers)
|
35
|
+
[
|
36
|
+
phone_numbers.count,
|
37
|
+
0
|
38
|
+
]
|
39
|
+
end
|
40
|
+
|
41
|
+
private_class_method def self.names_and_numbers(phone_numbers)
|
42
|
+
return [] if phone_numbers.empty?
|
43
|
+
|
44
|
+
names_and_numbers = phone_numbers.flat_map(&:name_and_number)
|
45
|
+
|
46
|
+
phone_numbers.first.four_byte_format_for(names_and_numbers)
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_accessor :name, :number
|
50
|
+
|
51
|
+
# Create a PhoneNumber instance.
|
52
|
+
#
|
53
|
+
# @param name [Array<Integer>] Name associated to phone number.
|
54
|
+
# @param number [String] Phone number text.
|
55
|
+
# @return [PhoneNumber] PhoneNumber instance.
|
56
|
+
def initialize(name: [], number:)
|
57
|
+
@name = name
|
58
|
+
@number = number
|
59
|
+
end
|
60
|
+
|
61
|
+
# Compile an unformatted name and phone number.
|
62
|
+
#
|
63
|
+
# @return [Array<Integer>] Array of integers that represent bytes.
|
64
|
+
def name_and_number
|
65
|
+
[
|
66
|
+
name,
|
67
|
+
number_characters
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def number_characters
|
74
|
+
number.each_char.map { |digit| PHONE_NUMBER_DIGITS_MAP[digit] }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|