movingsign_api 0.0.1

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.
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