timex_datalink_client 0.6.0 → 0.8.0
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 +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
|