movingsign_api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +8 -0
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +11 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +60 -0
  10. data/Rakefile +1 -0
  11. data/lib/movingsign_api/commands/command.rb +76 -0
  12. data/lib/movingsign_api/commands/hard_reset_command.rb +16 -0
  13. data/lib/movingsign_api/commands/internal/align_mode.rb +50 -0
  14. data/lib/movingsign_api/commands/internal/display_mode.rb +61 -0
  15. data/lib/movingsign_api/commands/internal/display_pause.rb +33 -0
  16. data/lib/movingsign_api/commands/internal/display_speed.rb +45 -0
  17. data/lib/movingsign_api/commands/internal/file_handle.rb +54 -0
  18. data/lib/movingsign_api/commands/internal/pretty_keyable.rb +50 -0
  19. data/lib/movingsign_api/commands/internal/sender_receiver_address.rb +82 -0
  20. data/lib/movingsign_api/commands/internal/utilities.rb +8 -0
  21. data/lib/movingsign_api/commands/set_clock_command.rb +32 -0
  22. data/lib/movingsign_api/commands/set_sound_command.rb +21 -0
  23. data/lib/movingsign_api/commands/software_reset_command.rb +16 -0
  24. data/lib/movingsign_api/commands/write_control_command.rb +35 -0
  25. data/lib/movingsign_api/commands/write_text_command.rb +116 -0
  26. data/lib/movingsign_api/errors.rb +22 -0
  27. data/lib/movingsign_api/sign.rb +144 -0
  28. data/lib/movingsign_api/version.rb +3 -0
  29. data/lib/movingsign_api.rb +13 -0
  30. data/movingsign_api.gemspec +30 -0
  31. data/spec/align_mode_spec.rb +28 -0
  32. data/spec/commands/set_sound_command_spec.rb +19 -0
  33. data/spec/display_mode_spec.rb +57 -0
  34. data/spec/display_speed_spec.rb +23 -0
  35. data/spec/examples/example_A_spec.rb +24 -0
  36. data/spec/examples/example_C_spec.rb +16 -0
  37. data/spec/examples/example_D_spec.rb +14 -0
  38. data/spec/examples/example_K_spec.rb +14 -0
  39. data/spec/examples/expected.rb +130 -0
  40. data/spec/file_handle_spec.rb +17 -0
  41. data/spec/sender_receiver_address_spec.rb +60 -0
  42. data/spec/spec_helper.rb +19 -0
  43. data/spec/tutorials/readme_1.rb +5 -0
  44. data/spec/tutorials/readme_2.rb +5 -0
  45. data/spec/tutorials/tutorials_spec.rb +14 -0
  46. metadata +188 -0
@@ -0,0 +1,16 @@
1
+ require 'movingsign_api/commands/write_control_command'
2
+
3
+ module MovingsignApi
4
+ # Performs a soft reset of the sign
5
+ #
6
+ # @note The sign will restart after receiving this command.
7
+ class SoftwareResetCommand < WriteControlCommand
8
+ def subcommand_code
9
+ 'B'
10
+ end
11
+
12
+ def subcommand_payload_bytes
13
+ []
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ require 'movingsign_api/commands/command'
2
+
3
+ module MovingsignApi
4
+ # Write control command base class, subclassed by actual implementations
5
+ #
6
+ # In subclasses, be sure to implement:
7
+ # - {#subcommand_code}
8
+ # - {#subcommand_payload_bytes}
9
+ class WriteControlCommand < Command
10
+ def command_code
11
+ 'W'
12
+ end
13
+
14
+ def subcommand_code
15
+ raise MovingsignApi::NotImplementedError, "Needs to be implemented in subclass."
16
+ end
17
+
18
+ private
19
+
20
+ def command_payload_bytes
21
+ bytes = []
22
+
23
+ bytes.concat string_to_ascii_bytes subcommand_code
24
+ bytes.concat subcommand_payload_bytes
25
+
26
+ bytes
27
+ end
28
+
29
+ # generates control sub-command specific bytes
30
+ def subcommand_payload_bytes
31
+ raise MovingsignApi::NotImplementedError, "Needs to be implemented in subclass."
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,116 @@
1
+ require 'movingsign_api/commands/command'
2
+ require 'movingsign_api/commands/internal/file_handle'
3
+ require 'movingsign_api/commands/internal/display_mode'
4
+ require 'movingsign_api/commands/internal/display_speed'
5
+ require 'movingsign_api/commands/internal/display_pause'
6
+ require 'movingsign_api/commands/internal/align_mode'
7
+
8
+ module MovingsignApi
9
+ # Writes text to a sign
10
+ class WriteTextCommand < MovingsignApi::Command
11
+ # Index of the file to write to. 0 - 35 (363 total). See {FileHandle}
12
+ #
13
+ # @param value [Integer] 0 - 35 (default: 0)
14
+ attr_accessor :file_handle
15
+
16
+ # Display mode to use. See {DisplayMode}
17
+ #
18
+ # @param value [Sybol] (default: +:hold+)
19
+ attr_accessor :display_mode
20
+
21
+ # Display speed of effects. See {DisplaySpeed}
22
+ #
23
+ # @param value [Symbol] default: +:normal+
24
+ attr_accessor :display_speed
25
+
26
+ # Display pause between screens of information. See {DisplayPause}
27
+ #
28
+ # @param value [Integer] default: +2+
29
+ attr_accessor :display_pause
30
+
31
+ # Day of week mask. See specification for an explaination of this parameter
32
+ #
33
+ # @param value [String] default: +'7F'+
34
+ attr_accessor :dayofweek_mask
35
+
36
+ # Align mode to use. See {AlignMode}
37
+ #
38
+ # @param value [Symbol] One of +:left+ +:right+ +:center+ (default +:left+)
39
+ attr_accessor :align_mode
40
+
41
+ # Display start time
42
+ # @note work in progress
43
+ #
44
+ # @param value [String] default: +'0000'+
45
+ attr_accessor :start_time
46
+
47
+ # Display end time
48
+ # @note work in progress
49
+ #
50
+ # @param value [String] default: +'2359'+
51
+ attr_accessor :end_time
52
+
53
+ # Text to display
54
+ #
55
+ # @param value [String] the message to display
56
+ attr_accessor :text
57
+
58
+ def file_handle=(handle)
59
+ @file_handle = FileHandle.new(handle)
60
+ end
61
+
62
+ def display_mode=(mode)
63
+ @display_mode = DisplayMode.parse(mode)
64
+ end
65
+
66
+ def display_speed=(speed)
67
+ @display_speed = DisplaySpeed.parse(speed)
68
+ end
69
+
70
+ def display_pause=(seconds)
71
+ @display_pause = DisplayPause.new seconds
72
+ end
73
+
74
+ def align_mode=(align)
75
+ @align_mode = AlignMode.parse(align)
76
+ end
77
+
78
+ # (see Command#command_code)
79
+ # 'A' for the "Write Text" command
80
+ def command_code
81
+ 'A'
82
+ end
83
+
84
+ private
85
+
86
+ def command_payload_bytes
87
+ # set defaults if needed
88
+ self.file_handle ||= 0
89
+ self.display_mode ||= :hold
90
+ self.display_speed ||= :normal
91
+ self.display_pause ||= 2
92
+ self.dayofweek_mask ||= '7F'
93
+ self.align_mode ||= :left
94
+ self.start_time ||= '0000'
95
+ self.end_time ||= '2359'
96
+ raise InvalidInputError, "text not set" unless self.text
97
+
98
+ bytes = []
99
+
100
+ bytes.concat self.file_handle.to_bytes # Filename
101
+ bytes.concat self.display_mode.to_bytes # Display Mode
102
+ bytes.concat self.display_speed.to_bytes # Display Speed
103
+ bytes.concat self.display_pause.to_bytes # Display Pause
104
+ bytes.concat string_to_ascii_bytes(self.dayofweek_mask) # Show Date
105
+ bytes.concat string_to_ascii_bytes(self.start_time) # Start Time
106
+ bytes.concat string_to_ascii_bytes(self.end_time) # End Time
107
+ bytes.concat string_to_ascii_bytes('000') # Reserved
108
+ bytes.concat self.align_mode.to_bytes # Align Mode
109
+ #bytes.concat string_to_ascii_bytes("\xFD\x42" + self.text.gsub("\n", "\x7F")) # Text
110
+ bytes.concat string_to_ascii_bytes(self.text.gsub("\n", "\x7F")) # Text
111
+
112
+ bytes
113
+ end
114
+ end
115
+ end
116
+
@@ -0,0 +1,22 @@
1
+ module MovingsignApi
2
+ # Base error for all MovingsignApi errors.
3
+ class Error < StandardError
4
+
5
+ end
6
+
7
+ # Raised when a method has been be implemented, usually due to improperly subclassed class or code that is in
8
+ # progress.
9
+ class NotImplementedError < Error
10
+
11
+ end
12
+
13
+ # Raised when an input is invalid in the given context.
14
+ class InvalidInputError < Error
15
+
16
+ end
17
+
18
+ # Raised when there are errors reading/writing to the serial port
19
+ class IOError < Error
20
+
21
+ end
22
+ end
@@ -0,0 +1,144 @@
1
+ require 'serialport'
2
+
3
+ module MovingsignApi
4
+ # Manipulates a Movingsign attached to a serial port
5
+ #
6
+ # @example Construct a sign
7
+ # require 'movingsign_api'
8
+ #
9
+ # sign = MovingsignApi::Sign.new('/dev/ttyUSB0')
10
+ #
11
+ # @example Show Text
12
+ # sign.show_text('Hello World!')
13
+ #
14
+ # @example Turn Sound Off
15
+ # sign.set_sound false
16
+ #
17
+ class Sign
18
+ # Serial port device path (ie: /dev/ttyUSB0)
19
+ attr_accessor :device_path
20
+
21
+ # @param device_path [String] the serial port device path (ie: /dev/ttyUSB0, different depending on platform)
22
+ def initialize(device_path)
23
+ @device_path = device_path
24
+ end
25
+
26
+ # Show the {#device_path} value on the display (useful for diagnostics)
27
+ def show_identity
28
+ cmd = WriteTextCommand.new
29
+ cmd.text = self.device_path
30
+
31
+ send_command cmd
32
+ end
33
+
34
+ # Displays the given text on the board.
35
+ #
36
+ # This is short-hand for the {WriteTextCommand}
37
+ #
38
+ # @param text [String] the text to display on the sign
39
+ # @param options [Hash] options for {WriteTextCommand}
40
+ # @option options [Integer] :display_pause (2) Time to pause (in seconds) between pages of text. See {WriteTextCommand#display_pause}
41
+ #
42
+ # @return [self]
43
+ def show_text(text, options = {})
44
+ cmd = WriteTextCommand.new
45
+ cmd.display_pause = options[:display_pause] if options[:display_pause]
46
+ cmd.text = text
47
+
48
+ send_command cmd
49
+ end
50
+ alias :write_text :show_text
51
+
52
+ # Turns on/off sound when the sign receives a command
53
+ #
54
+ # This is a shorthand for {SetSoundCommand}
55
+ #
56
+ # @param on [Boolean] true to turn sound on, false otherwise
57
+ #
58
+ # @return [self]
59
+ def set_sound(on)
60
+ cmd = SetSoundCommand.new on
61
+
62
+ send_command cmd
63
+ end
64
+
65
+ # Sends the specified Movingsign command to this sign's serial port
66
+ #
67
+ # @param [MovingsignApi::Command] command subclass to send
68
+ #
69
+ # @return [self]
70
+ def send_command(command)
71
+ SerialPort.open(self.device_path, 9600, 8, 1) do |port|
72
+ # flush anything existing on the port
73
+ port.flush
74
+ flush_read_buffer(port)
75
+
76
+ byte_string = command.to_bytes.pack('C*')
77
+
78
+ begin
79
+ while byte_string && byte_string.length != 0
80
+ count = port.write_nonblock(byte_string)
81
+ byte_string = byte_string[count,-1]
82
+
83
+ port.flush
84
+ end
85
+ rescue IO::WaitWritable
86
+ if IO.select([], [port], [], 5)
87
+ retry
88
+ else
89
+ raise IOError, "Timeout writing command to #{self.device_path}"
90
+ end
91
+ end
92
+
93
+ # wait for expected confirmation signals
94
+ got_eot = false
95
+ got_soh = false
96
+ loop do
97
+ begin
98
+ c = port.read_nonblock(1)
99
+
100
+ case c
101
+ when "\x04"
102
+ if ! got_eot
103
+ got_eot = true
104
+ else
105
+ raise IOError, "Got EOT reply twice from #{self.device_path}"
106
+ end
107
+ when "\x01"
108
+ if got_eot
109
+ if ! got_soh
110
+ got_soh = true
111
+
112
+ break
113
+ else
114
+ raise IOError, "Got SOH twice from #{self.device_path}"
115
+ end
116
+ else
117
+ raise IOError, "Got SOH before EOT from #{self.device_path}"
118
+ end
119
+ end
120
+ rescue IO::WaitReadable
121
+ if IO.select([port], [], [], 3)
122
+ retry
123
+ else
124
+ raise IOError, "Timeout waiting for command reply from #{self.device_path}. EOT:#{got_eot} SOH:#{got_soh}"
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ self
131
+ end
132
+
133
+ private
134
+
135
+ def flush_read_buffer(port)
136
+ begin
137
+ while true
138
+ port.read_nonblock(1024)
139
+ end
140
+ rescue IO::WaitReadable
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,3 @@
1
+ module MovingsignApi
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,13 @@
1
+ require "movingsign_api/errors"
2
+ require "movingsign_api/version"
3
+ require 'movingsign_api/commands/write_text_command'
4
+ require 'movingsign_api/commands/set_clock_command'
5
+ require 'movingsign_api/commands/software_reset_command'
6
+ require 'movingsign_api/commands/hard_reset_command'
7
+ require 'movingsign_api/commands/set_clock_command'
8
+ require 'movingsign_api/commands/set_sound_command'
9
+ require 'movingsign_api/sign'
10
+
11
+ module MovingsignApi
12
+ # Your code goes here...
13
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'movingsign_api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "movingsign_api"
8
+ spec.version = MovingsignApi::VERSION
9
+ spec.authors = ["Eric Webb"]
10
+ spec.email = ["opensource@collectivegenius.net"]
11
+ spec.description = "MovingSign Communication Protocol V2.1 Implementation in Ruby"
12
+ spec.summary = "MovingSign Communication Protocol V2.1 Implementation in Ruby"
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ .reject { |path| path.match /\A\.idea\// } # Ignore .idea/ IntelliJ project directory
18
+ .reject { |path| path.match /\A[^\/]+\.iml/ } # Ignore *.iml IntelliJ module file
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec", "~> 2.14"
26
+ spec.add_development_dependency "yard", "0.8.7.3"
27
+ spec.add_development_dependency "wwtd"
28
+
29
+ spec.add_runtime_dependency 'serialport', '~> 1.3'
30
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ ALIGN_MODES = {
5
+ :left => '1',
6
+ :right => '3',
7
+ :center => '2',
8
+ }
9
+
10
+ describe MovingsignApi::AlignMode do
11
+ describe '::parse' do
12
+ it "Symbol" do
13
+ # valid keys
14
+ ALIGN_MODES.keys.each do |key|
15
+ expect(described_class.parse(key).key).to eq key
16
+ end
17
+
18
+ # invalid keys
19
+ expect {described_class.parse(:invalid).key}.to raise_error(MovingsignApi::InvalidInputError)
20
+ expect {described_class.parse(nil).key}.to raise_error(MovingsignApi::InvalidInputError)
21
+ end
22
+
23
+ it 'Code' do
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe MovingsignApi::SetSoundCommand do
4
+ describe "sound on" do
5
+ subject { MovingsignApi::SetSoundCommand.new(true) }
6
+
7
+ it "#subcommand_payload_bytes" do
8
+ expect(subject.subcommand_payload_bytes).to eq [0x31]
9
+ end
10
+ end
11
+
12
+ describe "sound off" do
13
+ subject { MovingsignApi::SetSoundCommand.new(false) }
14
+
15
+ it "#subcommand_payload_bytes" do
16
+ expect(subject.subcommand_payload_bytes).to eq [0x30]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ DISPLAY_MODES = {
5
+ :auto => 'A',
6
+ :flash => 'B',
7
+ :hold => 'C',
8
+ :interlock => 'D',
9
+ :rolldown => 'E',
10
+ :rollup => 'F',
11
+ :rollin => 'G',
12
+ :rollout => 'H',
13
+ :rollleft => 'I',
14
+ :rollright => 'J',
15
+ :rotate => 'K',
16
+ :slide => 'L',
17
+ :snow => 'M',
18
+ :sparkle => 'N',
19
+ :spray => 'O',
20
+ :starburst => 'P',
21
+ :switch => 'Q',
22
+ :twinkle => 'R',
23
+ :wipedown => 'S',
24
+ :wipeup => 'T',
25
+ :wipein => 'U',
26
+ :wipeout => 'V',
27
+ :wipeleft => 'W',
28
+ :wiperight => 'X',
29
+ :cyclecolor => 'Y',
30
+ :clock => 'Z',
31
+ }
32
+
33
+ describe MovingsignApi::DisplayMode do
34
+ describe '::parse' do
35
+ it "Symbol" do
36
+ # All valid keys
37
+ DISPLAY_MODES.keys.each do |key|
38
+ expect(described_class.parse(key).key).to eq key
39
+ end
40
+
41
+ # invalid keys
42
+ expect {described_class.parse(:invalid).key}.to raise_error(MovingsignApi::InvalidInputError)
43
+ expect {described_class.parse(:+).key}.to raise_error(MovingsignApi::InvalidInputError)
44
+ expect {described_class.parse(nil).key}.to raise_error(MovingsignApi::InvalidInputError)
45
+ end
46
+
47
+ it "ASCII Code" do
48
+ # All valid ASCII codes
49
+ DISPLAY_MODES.each_pair do |key, code|
50
+ expect(described_class.parse(code).key).to eq key
51
+ end
52
+
53
+ expect {described_class.parse('0').key}.to raise_error(MovingsignApi::InvalidInputError)
54
+ expect {described_class.parse('z').key}.to raise_error(MovingsignApi::InvalidInputError)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ DISPLAY_SPEEDS = {
4
+ :fastest => '1',
5
+ :faster => '2',
6
+ :normal => '3',
7
+ :slow => '4',
8
+ :slower => '5',
9
+ }
10
+
11
+ describe MovingsignApi::DisplaySpeed do
12
+ describe '::parse' do
13
+ it "Symbol" do
14
+ DISPLAY_SPEEDS.keys.each do |key|
15
+ expect(described_class.parse(key).key).to eq key
16
+ end
17
+
18
+ # invalid keys
19
+ expect {described_class.parse(:invalid).key}.to raise_error(MovingsignApi::InvalidInputError)
20
+ expect {described_class.parse(nil).key}.to raise_error(MovingsignApi::InvalidInputError)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative 'expected'
4
+
5
+ describe "Specification Example A - Write Text File" do
6
+ it "Generates Expected Output" do
7
+ command = MovingsignApi::WriteTextCommand.new
8
+
9
+ command.sender = :pc
10
+ command.receiver = 3
11
+
12
+ command.file_handle = 'A' # aka 10
13
+ command.display_mode = :auto
14
+ command.display_speed = :faster
15
+ command.display_pause = 2
16
+ command.dayofweek_mask = '7F' # todo: make this pretty
17
+ command.start_time = '0100'
18
+ command.end_time = '1200'
19
+ command.align_mode = :left
20
+ command.text = 'HELLO'
21
+
22
+ expect(command.to_bytes).to eq EXAMPLE_A_EXPECTED
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative 'expected'
4
+
5
+ describe "Specification Example C - Set Clock Command" do
6
+ it "Generates Expected Output" do
7
+ command = MovingsignApi::SetClockCommand.new
8
+
9
+ command.sender = :pc
10
+ command.receiver = 34
11
+
12
+ command.datetime = Time.new(2004,04,02,12,36,23)
13
+
14
+ expect(command.to_bytes).to eq EXAMPLE_C_EXPECTED
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative 'expected'
4
+
5
+ describe "Specification Example D - Software Reset Command" do
6
+ it "Generates Expected Output" do
7
+ command = MovingsignApi::SoftwareResetCommand.new
8
+
9
+ command.sender = :pc
10
+ command.receiver = 34
11
+
12
+ expect(command.to_bytes).to eq EXAMPLE_D_EXPECTED
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative 'expected'
4
+
5
+ describe "Specification Example K - Hard Reset Command" do
6
+ it "Generates Expected Output" do
7
+ command = MovingsignApi::HardResetCommand.new
8
+
9
+ command.sender = :pc
10
+ command.receiver = 16
11
+
12
+ expect(command.to_bytes).to eq EXAMPLE_K_EXPECTED
13
+ end
14
+ end