rsmp 0.8.4 → 0.9.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.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yaml +21 -0
- data/.ruby-version +1 -1
- data/Gemfile.lock +51 -67
- data/README.md +2 -12
- data/bin/console +1 -1
- data/cucumber.yml +1 -0
- data/documentation/classes_and_modules.md +4 -4
- data/documentation/collecting_message.md +2 -2
- data/documentation/tasks.md +149 -0
- data/lib/rsmp/archive.rb +3 -3
- data/lib/rsmp/cli.rb +32 -4
- data/lib/rsmp/collect/aggregated_status_collector.rb +1 -1
- data/lib/rsmp/collect/collector.rb +13 -6
- data/lib/rsmp/collect/command_response_collector.rb +1 -1
- data/lib/rsmp/collect/state_collector.rb +1 -1
- data/lib/rsmp/collect/status_collector.rb +2 -1
- data/lib/rsmp/components.rb +3 -3
- data/lib/rsmp/convert/export/json_schema.rb +4 -4
- data/lib/rsmp/convert/import/yaml.rb +1 -1
- data/lib/rsmp/deep_merge.rb +1 -0
- data/lib/rsmp/error.rb +0 -3
- data/lib/rsmp/inspect.rb +1 -1
- data/lib/rsmp/logger.rb +5 -5
- data/lib/rsmp/logging.rb +1 -1
- data/lib/rsmp/message.rb +1 -1
- data/lib/rsmp/node.rb +10 -45
- data/lib/rsmp/proxy.rb +184 -134
- data/lib/rsmp/rsmp.rb +1 -1
- data/lib/rsmp/site.rb +23 -60
- data/lib/rsmp/site_proxy.rb +33 -37
- data/lib/rsmp/supervisor.rb +25 -21
- data/lib/rsmp/supervisor_proxy.rb +58 -29
- data/lib/rsmp/task.rb +84 -0
- data/lib/rsmp/tlc/signal_group.rb +5 -3
- data/lib/rsmp/tlc/signal_plan.rb +2 -2
- data/lib/rsmp/tlc/traffic_controller.rb +97 -29
- data/lib/rsmp/tlc/traffic_controller_site.rb +43 -36
- data/lib/rsmp/version.rb +1 -1
- data/lib/rsmp.rb +1 -1
- data/rsmp.gemspec +7 -7
- metadata +21 -20
- data/lib/rsmp/site_proxy_wait.rb +0 -0
- data/lib/rsmp/wait.rb +0 -16
- data/test.rb +0 -27
    
        data/lib/rsmp/site_proxy.rb
    CHANGED
    
    | @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # Handles a supervisor connection to a remote client
         | 
| 2 2 |  | 
| 3 | 
            -
            module RSMP | 
| 3 | 
            +
            module RSMP
         | 
| 4 4 | 
             
              class SiteProxy < Proxy
         | 
| 5 5 | 
             
                include Components
         | 
| 6 6 |  | 
| @@ -14,33 +14,37 @@ module RSMP | |
| 14 14 | 
             
                  @site_id = options[:site_id]
         | 
| 15 15 | 
             
                end
         | 
| 16 16 |  | 
| 17 | 
            +
                # handle communication
         | 
| 18 | 
            +
                # when we're created, the socket is already open
         | 
| 19 | 
            +
                def run
         | 
| 20 | 
            +
                  set_state :connected
         | 
| 21 | 
            +
                  start_reader
         | 
| 22 | 
            +
                  wait_for_reader   # run until disconnected
         | 
| 23 | 
            +
                rescue RSMP::ConnectionError => e
         | 
| 24 | 
            +
                  log e, level: :error
         | 
| 25 | 
            +
                rescue StandardError => e
         | 
| 26 | 
            +
                  notify_error e, level: :internal
         | 
| 27 | 
            +
                ensure
         | 
| 28 | 
            +
                  close
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 17 31 | 
             
                def revive options
         | 
| 18 32 | 
             
                  super options
         | 
| 19 33 | 
             
                  @supervisor = options[:supervisor]
         | 
| 20 34 | 
             
                  @settings = @supervisor.supervisor_settings.clone
         | 
| 21 35 | 
             
                end
         | 
| 22 36 |  | 
| 23 | 
            -
             | 
| 24 37 | 
             
                def inspect
         | 
| 25 38 | 
             
                  "#<#{self.class.name}:#{self.object_id}, #{inspector(
         | 
| 26 39 | 
             
                    :@acknowledgements,:@settings,:@site_settings,:@components
         | 
| 27 40 | 
             
                    )}>"
         | 
| 28 41 | 
             
                end
         | 
| 42 | 
            +
             | 
| 29 43 | 
             
                def node
         | 
| 30 44 | 
             
                  supervisor
         | 
| 31 45 | 
             
                end
         | 
| 32 46 |  | 
| 33 | 
            -
                def  | 
| 34 | 
            -
                  super
         | 
| 35 | 
            -
                  start_reader
         | 
| 36 | 
            -
                end
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                def stop
         | 
| 39 | 
            -
                  log "Closing connection to site", level: :info
         | 
| 40 | 
            -
                  super
         | 
| 41 | 
            -
                end
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                def connection_complete
         | 
| 47 | 
            +
                def handshake_complete
         | 
| 44 48 | 
             
                  super
         | 
| 45 49 | 
             
                  sanitized_sxl_version = RSMP::Schemer.sanitize_version(@site_sxl_version)
         | 
| 46 50 | 
             
                  log "Connection to site #{@site_id} established, using core #{@rsmp_version}, #{@sxl} #{sanitized_sxl_version}", level: :info
         | 
| @@ -68,7 +72,7 @@ module RSMP | |
| 68 72 | 
             
                    else
         | 
| 69 73 | 
             
                      super message
         | 
| 70 74 | 
             
                  end
         | 
| 71 | 
            -
                rescue RSMP::RepeatedAlarmError, RSMP::RepeatedStatusError | 
| 75 | 
            +
                rescue RSMP::RepeatedAlarmError, RSMP::RepeatedStatusError => e
         | 
| 72 76 | 
             
                  str = "Rejected #{message.type} message,"
         | 
| 73 77 | 
             
                  dont_acknowledge message, str, "#{e}"
         | 
| 74 78 | 
             
                  notify_error e.exception("#{str}#{e.message} #{message.json}")
         | 
| @@ -101,13 +105,11 @@ module RSMP | |
| 101 105 | 
             
                      "cId" => component,
         | 
| 102 106 | 
             
                      "mId" => m_id
         | 
| 103 107 | 
             
                  })
         | 
| 104 | 
            -
                  send_and_optionally_collect message, options do | | 
| 105 | 
            -
                     | 
| 108 | 
            +
                  send_and_optionally_collect message, options do |collect_options|
         | 
| 109 | 
            +
                    AggregatedStatusCollector.new(
         | 
| 106 110 | 
             
                      self,
         | 
| 107 | 
            -
                       | 
| 111 | 
            +
                      collect_options.merge(task:@task,m_id: m_id, num:1)
         | 
| 108 112 | 
             
                    )
         | 
| 109 | 
            -
                    collector.collect
         | 
| 110 | 
            -
                    collector
         | 
| 111 113 | 
             
                  end
         | 
| 112 114 | 
             
                end
         | 
| 113 115 |  | 
| @@ -150,7 +152,7 @@ module RSMP | |
| 150 152 | 
             
                end
         | 
| 151 153 |  | 
| 152 154 | 
             
                def version_acknowledged
         | 
| 153 | 
            -
                   | 
| 155 | 
            +
                  handshake_complete
         | 
| 154 156 | 
             
                end
         | 
| 155 157 |  | 
| 156 158 | 
             
                def process_watchdog message
         | 
| @@ -179,14 +181,12 @@ module RSMP | |
| 179 181 | 
             
                      "sS" => request_list,
         | 
| 180 182 | 
             
                      "mId" => m_id
         | 
| 181 183 | 
             
                  })
         | 
| 182 | 
            -
                  send_and_optionally_collect message, options do | | 
| 183 | 
            -
                     | 
| 184 | 
            +
                  send_and_optionally_collect message, options do |collect_options|
         | 
| 185 | 
            +
                    StatusCollector.new(
         | 
| 184 186 | 
             
                      self,
         | 
| 185 187 | 
             
                      status_list,
         | 
| 186 | 
            -
                       | 
| 188 | 
            +
                      collect_options.merge(task:@task,m_id: m_id)
         | 
| 187 189 | 
             
                      )
         | 
| 188 | 
            -
                    collector.collect
         | 
| 189 | 
            -
                    collector
         | 
| 190 190 | 
             
                  end
         | 
| 191 191 | 
             
                end
         | 
| 192 192 |  | 
| @@ -200,7 +200,7 @@ module RSMP | |
| 200 200 | 
             
                def subscribe_to_status component_id, status_list, options={}
         | 
| 201 201 | 
             
                  validate_ready 'subscribe to status'
         | 
| 202 202 | 
             
                  m_id = options[:m_id] || RSMP::Message.make_m_id
         | 
| 203 | 
            -
             | 
| 203 | 
            +
             | 
| 204 204 | 
             
                  # additional items can be used when verifying the response,
         | 
| 205 205 | 
             
                  # but must to remove from the subscribe message
         | 
| 206 206 | 
             
                  subscribe_list = status_list.map { |item| item.slice('sCI','n','uRt') }
         | 
| @@ -215,14 +215,12 @@ module RSMP | |
| 215 215 | 
             
                      "sS" => subscribe_list,
         | 
| 216 216 | 
             
                      'mId' => m_id
         | 
| 217 217 | 
             
                  })
         | 
| 218 | 
            -
                  send_and_optionally_collect message, options do | | 
| 219 | 
            -
                     | 
| 218 | 
            +
                  send_and_optionally_collect message, options do |collect_options|
         | 
| 219 | 
            +
                    StatusCollector.new(
         | 
| 220 220 | 
             
                      self,
         | 
| 221 221 | 
             
                      status_list,
         | 
| 222 | 
            -
                       | 
| 222 | 
            +
                      collect_options.merge(task:@task,m_id: m_id)
         | 
| 223 223 | 
             
                    )
         | 
| 224 | 
            -
                    collector.collect
         | 
| 225 | 
            -
                    collector
         | 
| 226 224 | 
             
                  end
         | 
| 227 225 | 
             
                end
         | 
| 228 226 |  | 
| @@ -267,14 +265,12 @@ module RSMP | |
| 267 265 | 
             
                      "arg" => command_list,
         | 
| 268 266 | 
             
                      "mId" => m_id
         | 
| 269 267 | 
             
                  })
         | 
| 270 | 
            -
                  send_and_optionally_collect message, options do | | 
| 271 | 
            -
                     | 
| 268 | 
            +
                  send_and_optionally_collect message, options do |collect_options|
         | 
| 269 | 
            +
                    CommandResponseCollector.new(
         | 
| 272 270 | 
             
                      self,
         | 
| 273 271 | 
             
                      command_list,
         | 
| 274 | 
            -
                       | 
| 272 | 
            +
                      collect_options.merge(task:@task,m_id: m_id)
         | 
| 275 273 | 
             
                      )
         | 
| 276 | 
            -
                    collector.collect
         | 
| 277 | 
            -
                    collector
         | 
| 278 274 | 
             
                  end
         | 
| 279 275 | 
             
                end
         | 
| 280 276 |  | 
| @@ -332,7 +328,7 @@ module RSMP | |
| 332 328 | 
             
                    log "Using site settings for guest", level: :debug
         | 
| 333 329 | 
             
                    return @settings['guest']
         | 
| 334 330 | 
             
                  end
         | 
| 335 | 
            -
             | 
| 331 | 
            +
             | 
| 336 332 | 
             
                  nil
         | 
| 337 333 | 
             
                end
         | 
| 338 334 |  | 
    
        data/lib/rsmp/supervisor.rb
    CHANGED
    
    | @@ -58,26 +58,31 @@ module RSMP | |
| 58 58 | 
             
                  end
         | 
| 59 59 | 
             
                end
         | 
| 60 60 |  | 
| 61 | 
            -
                 | 
| 61 | 
            +
                # listen for connections
         | 
| 62 | 
            +
                # Async::IO::Endpoint#accept createa an async task that we will wait for
         | 
| 63 | 
            +
                def run
         | 
| 64 | 
            +
                  log "Starting supervisor on port #{@supervisor_settings["port"]}",
         | 
| 65 | 
            +
                      level: :info,
         | 
| 66 | 
            +
                      timestamp: @clock.now
         | 
| 67 | 
            +
             | 
| 62 68 | 
             
                  @endpoint = Async::IO::Endpoint.tcp('0.0.0.0', @supervisor_settings["port"])
         | 
| 63 | 
            -
                  @endpoint.accept do |socket|  # creates async tasks
         | 
| 69 | 
            +
                  tasks = @endpoint.accept do |socket|  # creates async tasks
         | 
| 64 70 | 
             
                    handle_connection(socket)
         | 
| 65 71 | 
             
                  rescue StandardError => e
         | 
| 66 72 | 
             
                    notify_error e, level: :internal
         | 
| 67 73 | 
             
                  end
         | 
| 74 | 
            +
                  tasks.each { |task| task.wait }
         | 
| 68 75 | 
             
                rescue StandardError => e
         | 
| 69 76 | 
             
                  notify_error e, level: :internal
         | 
| 70 77 | 
             
                end
         | 
| 71 78 |  | 
| 79 | 
            +
                # stop
         | 
| 72 80 | 
             
                def stop
         | 
| 73 81 | 
             
                  log "Stopping supervisor #{@supervisor_settings["site_id"]}", level: :info
         | 
| 74 | 
            -
                  @proxies.each { |proxy| proxy.stop }
         | 
| 75 | 
            -
                  @proxies.clear
         | 
| 76 82 | 
             
                  super
         | 
| 77 | 
            -
                  @tcp_server.close if @tcp_server
         | 
| 78 | 
            -
                  @tcp_server = nil
         | 
| 79 83 | 
             
                end
         | 
| 80 84 |  | 
| 85 | 
            +
                # handle an incoming connction by either accepting of rejecting it
         | 
| 81 86 | 
             
                def handle_connection socket
         | 
| 82 87 | 
             
                  remote_port = socket.remote_address.ip_port
         | 
| 83 88 | 
             
                  remote_hostname = socket.remote_address.ip_address
         | 
| @@ -85,9 +90,9 @@ module RSMP | |
| 85 90 |  | 
| 86 91 | 
             
                  info = {ip:remote_ip, port:remote_port, hostname:remote_hostname, now:Clock.now}
         | 
| 87 92 | 
             
                  if accept? socket, info
         | 
| 88 | 
            -
                     | 
| 93 | 
            +
                    accept_connection socket, info
         | 
| 89 94 | 
             
                  else
         | 
| 90 | 
            -
                     | 
| 95 | 
            +
                    reject_connection socket, info
         | 
| 91 96 | 
             
                  end
         | 
| 92 97 | 
             
                rescue ConnectionError => e
         | 
| 93 98 | 
             
                  log "Rejected connection from #{remote_ip}:#{remote_port}, #{e.to_s}", level: :warning
         | 
| @@ -99,12 +104,6 @@ module RSMP | |
| 99 104 | 
             
                  close socket, info
         | 
| 100 105 | 
             
                end
         | 
| 101 106 |  | 
| 102 | 
            -
                def starting
         | 
| 103 | 
            -
                  log "Starting supervisor on port #{@supervisor_settings["port"]}", 
         | 
| 104 | 
            -
                      level: :info,
         | 
| 105 | 
            -
                      timestamp: @clock.now
         | 
| 106 | 
            -
                end
         | 
| 107 | 
            -
             | 
| 108 107 | 
             
                def accept? socket, info
         | 
| 109 108 | 
             
                  true
         | 
| 110 109 | 
             
                end
         | 
| @@ -143,7 +142,8 @@ module RSMP | |
| 143 142 | 
             
                  message.attribute('siteId').first['sId']
         | 
| 144 143 | 
             
                end
         | 
| 145 144 |  | 
| 146 | 
            -
                 | 
| 145 | 
            +
                # accept an incoming connecting by creating and starting a proxy
         | 
| 146 | 
            +
                def accept_connection socket, info
         | 
| 147 147 | 
             
                  log "Site connected from #{format_ip_and_port(info)}",
         | 
| 148 148 | 
             
                      ip: info[:ip],
         | 
| 149 149 | 
             
                      port: info[:port],
         | 
| @@ -182,7 +182,8 @@ module RSMP | |
| 182 182 | 
             
                    proxy = build_proxy settings.merge(site_id:id)    # keep the id learned by peeking above
         | 
| 183 183 | 
             
                    @proxies.push proxy
         | 
| 184 184 | 
             
                  end
         | 
| 185 | 
            -
                  proxy. | 
| 185 | 
            +
                  proxy.start     # will run until the site disconnects
         | 
| 186 | 
            +
                  proxy.wait
         | 
| 186 187 | 
             
                ensure
         | 
| 187 188 | 
             
                  site_ids_changed
         | 
| 188 189 | 
             
                  stop if @supervisor_settings['one_shot']
         | 
| @@ -192,7 +193,7 @@ module RSMP | |
| 192 193 | 
             
                  @site_id_condition.signal
         | 
| 193 194 | 
             
                end
         | 
| 194 195 |  | 
| 195 | 
            -
                def  | 
| 196 | 
            +
                def reject_connection socket, info
         | 
| 196 197 | 
             
                  log "Site rejected", ip: info[:ip], level: :info
         | 
| 197 198 | 
             
                end
         | 
| 198 199 |  | 
| @@ -224,10 +225,13 @@ module RSMP | |
| 224 225 | 
             
                  nil
         | 
| 225 226 | 
             
                end
         | 
| 226 227 |  | 
| 227 | 
            -
                def wait_for_site site_id, timeout
         | 
| 228 | 
            +
                def wait_for_site site_id, timeout:
         | 
| 228 229 | 
             
                  site = find_site site_id
         | 
| 229 230 | 
             
                  return site if site
         | 
| 230 | 
            -
                   | 
| 231 | 
            +
                  wait_for_condition(@site_id_condition,timeout:timeout) do
         | 
| 232 | 
            +
                    find_site site_id
         | 
| 233 | 
            +
                  end
         | 
| 234 | 
            +
             | 
| 231 235 | 
             
                rescue Async::TimeoutError
         | 
| 232 236 | 
             
                  if site_id == :any
         | 
| 233 237 | 
             
                    str = "No site connected"
         | 
| @@ -237,8 +241,8 @@ module RSMP | |
| 237 241 | 
             
                  raise RSMP::TimeoutError.new "#{str} within #{timeout}s"
         | 
| 238 242 | 
             
                end
         | 
| 239 243 |  | 
| 240 | 
            -
                def wait_for_site_disconnect site_id, timeout
         | 
| 241 | 
            -
                   | 
| 244 | 
            +
                def wait_for_site_disconnect site_id, timeout:
         | 
| 245 | 
            +
                  wait_for_condition(@site_id_condition,timeout:timeout) { true unless find_site site_id }
         | 
| 242 246 | 
             
                rescue Async::TimeoutError
         | 
| 243 247 | 
             
                  raise RSMP::TimeoutError.new "Site '#{site_id}' did not disconnect within #{timeout}s"
         | 
| 244 248 | 
             
                end
         | 
| @@ -2,7 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            require 'digest'
         | 
| 4 4 |  | 
| 5 | 
            -
            module RSMP | 
| 5 | 
            +
            module RSMP
         | 
| 6 6 | 
             
              class SupervisorProxy < Proxy
         | 
| 7 7 |  | 
| 8 8 | 
             
                attr_reader :supervisor_id, :site
         | 
| @@ -22,44 +22,70 @@ module RSMP | |
| 22 22 | 
             
                  site
         | 
| 23 23 | 
             
                end
         | 
| 24 24 |  | 
| 25 | 
            -
                 | 
| 25 | 
            +
                # handle communication
         | 
| 26 | 
            +
                # if disconnected, then try to reconnect
         | 
| 27 | 
            +
                def run
         | 
| 28 | 
            +
                  loop do
         | 
| 29 | 
            +
                    connect
         | 
| 30 | 
            +
                    start_reader
         | 
| 31 | 
            +
                    start_handshake
         | 
| 32 | 
            +
                    wait_for_reader   # run until disconnected
         | 
| 33 | 
            +
                    break if reconnect_delay == false
         | 
| 34 | 
            +
                  rescue Restart
         | 
| 35 | 
            +
                    @logger.mute @ip, @port
         | 
| 36 | 
            +
                    raise
         | 
| 37 | 
            +
                  rescue RSMP::ConnectionError => e
         | 
| 38 | 
            +
                    log e, level: :error
         | 
| 39 | 
            +
                    break if reconnect_delay == false
         | 
| 40 | 
            +
                  rescue StandardError => e
         | 
| 41 | 
            +
                    notify_error e, level: :internal
         | 
| 42 | 
            +
                    break if reconnect_delay == false
         | 
| 43 | 
            +
                  ensure
         | 
| 44 | 
            +
                    close
         | 
| 45 | 
            +
                    stop_subtasks
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def start_handshake
         | 
| 50 | 
            +
                  send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                # connect to the supervisor and initiate handshake supervisor
         | 
| 54 | 
            +
                def connect
         | 
| 26 55 | 
             
                  log "Connecting to supervisor at #{@ip}:#{@port}", level: :info
         | 
| 27 | 
            -
                   | 
| 28 | 
            -
                   | 
| 56 | 
            +
                  set_state :connecting
         | 
| 57 | 
            +
                  connect_tcp
         | 
| 29 58 | 
             
                  @logger.unmute @ip, @port
         | 
| 30 59 | 
             
                  log "Connected to supervisor at #{@ip}:#{@port}", level: :info
         | 
| 31 | 
            -
                  start_reader
         | 
| 32 | 
            -
                  send_version @site_settings['site_id'], @site_settings["rsmp_versions"]
         | 
| 33 60 | 
             
                rescue SystemCallError => e
         | 
| 34 | 
            -
                   | 
| 35 | 
            -
                  retry_notice
         | 
| 61 | 
            +
                  raise ConnectionError.new "Could not connect to supervisor at #{@ip}:#{@port}: Errno #{e.errno} #{e}"
         | 
| 36 62 | 
             
                rescue StandardError => e
         | 
| 37 | 
            -
                   | 
| 38 | 
            -
                  retry_notice
         | 
| 39 | 
            -
                end
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                def retry_notice
         | 
| 42 | 
            -
                  unless @site.site_settings['intervals']['reconnect'] == :no
         | 
| 43 | 
            -
                    log "Will try to reconnect again every #{@site.site_settings['intervals']['reconnect']} seconds..", level: :info
         | 
| 44 | 
            -
                    @logger.mute @ip, @port
         | 
| 45 | 
            -
                  end
         | 
| 63 | 
            +
                  raise ConnectionError.new "Error while connecting to supervisor at #{@ip}:#{@port}: #{e}"
         | 
| 46 64 | 
             
                end
         | 
| 47 65 |  | 
| 48 | 
            -
                def  | 
| 49 | 
            -
                  log "Closing connection to supervisor", level: :info
         | 
| 66 | 
            +
                def stop_task
         | 
| 50 67 | 
             
                  super
         | 
| 51 68 | 
             
                  @last_status_sent = nil
         | 
| 52 69 | 
             
                end
         | 
| 53 70 |  | 
| 54 | 
            -
                def  | 
| 55 | 
            -
                  return if @socket
         | 
| 71 | 
            +
                def connect_tcp
         | 
| 56 72 | 
             
                  @endpoint = Async::IO::Endpoint.tcp(@ip, @port)
         | 
| 57 | 
            -
             | 
| 73 | 
            +
             | 
| 74 | 
            +
                  # Async::IO::Endpoint#connect renames the current task. run in a subtask to avoid this see issue #22
         | 
| 75 | 
            +
                  @task.async do |task|
         | 
| 76 | 
            +
                    task.annotate 'socket task'
         | 
| 77 | 
            +
                    # this timeout is a workaround for #connect hanging on windows if the other side is not present yet
         | 
| 78 | 
            +
                    task.with_timeout 1.1 do
         | 
| 79 | 
            +
                      @socket = @endpoint.connect
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
                  end.wait
         | 
| 82 | 
            +
             | 
| 58 83 | 
             
                  @stream = Async::IO::Stream.new(@socket)
         | 
| 59 84 | 
             
                  @protocol = Async::IO::Protocol::Line.new(@stream,WRAPPING_DELIMITER) # rsmp messages are json terminated with a form-feed
         | 
| 85 | 
            +
                  set_state :connected
         | 
| 60 86 | 
             
                end
         | 
| 61 87 |  | 
| 62 | 
            -
                def  | 
| 88 | 
            +
                def handshake_complete
         | 
| 63 89 | 
             
                  super
         | 
| 64 90 | 
             
                  sanitized_sxl_version = RSMP::Schemer.sanitize_version(sxl_version)
         | 
| 65 91 | 
             
                  log "Connection to supervisor established, using core #{@rsmp_version}, #{sxl} #{sanitized_sxl_version}", level: :info
         | 
| @@ -114,16 +140,19 @@ module RSMP | |
| 114 140 | 
             
                end
         | 
| 115 141 |  | 
| 116 142 | 
             
                def reconnect_delay
         | 
| 143 | 
            +
                  return false if @site_settings['intervals']['reconnect'] == :no
         | 
| 117 144 | 
             
                  interval = @site_settings['intervals']['reconnect']
         | 
| 118 | 
            -
                  log " | 
| 145 | 
            +
                  log "Will try to reconnect again every #{interval} seconds...", level: :info
         | 
| 146 | 
            +
                  @logger.mute @ip, @port
         | 
| 119 147 | 
             
                  @task.sleep interval
         | 
| 148 | 
            +
                  true
         | 
| 120 149 | 
             
                end
         | 
| 121 150 |  | 
| 122 151 | 
             
                def version_accepted message
         | 
| 123 152 | 
             
                  log "Received Version message, using RSMP #{@rsmp_version}", message: message, level: :log
         | 
| 124 153 | 
             
                  start_timer
         | 
| 125 154 | 
             
                  acknowledge message
         | 
| 126 | 
            -
                   | 
| 155 | 
            +
                  handshake_complete
         | 
| 127 156 | 
             
                  @version_determined = true
         | 
| 128 157 | 
             
                end
         | 
| 129 158 |  | 
| @@ -138,8 +167,8 @@ module RSMP | |
| 138 167 | 
             
                    "mId" => m_id,
         | 
| 139 168 | 
             
                  })
         | 
| 140 169 |  | 
| 141 | 
            -
                  send_and_optionally_collect message, options do | | 
| 142 | 
            -
                     | 
| 170 | 
            +
                  send_and_optionally_collect message, options do |collect_options|
         | 
| 171 | 
            +
                    Collector.new self, collect_options.merge(task:@task, type: 'MessageAck')
         | 
| 143 172 | 
             
                  end
         | 
| 144 173 | 
             
                end
         | 
| 145 174 |  | 
| @@ -234,7 +263,7 @@ module RSMP | |
| 234 263 | 
             
                  update_list = {}
         | 
| 235 264 | 
             
                  component = message.attributes["cId"]
         | 
| 236 265 | 
             
                  @status_subscriptions[component] ||= {}
         | 
| 237 | 
            -
                  update_list[component] ||= {} | 
| 266 | 
            +
                  update_list[component] ||= {}
         | 
| 238 267 | 
             
                  now = Time.now  # internal timestamp
         | 
| 239 268 | 
             
                  subs = @status_subscriptions[component]
         | 
| 240 269 |  | 
| @@ -299,7 +328,7 @@ module RSMP | |
| 299 328 | 
             
                      by_name.each_pair do |name,subscription|
         | 
| 300 329 | 
             
                        current = nil
         | 
| 301 330 | 
             
                        should_send = false
         | 
| 302 | 
            -
                        if subscription[:interval] == 0 | 
| 331 | 
            +
                        if subscription[:interval] == 0
         | 
| 303 332 | 
             
                          # send as soon as the data changes
         | 
| 304 333 | 
             
                          if component_object
         | 
| 305 334 | 
             
                            current, age = *(component_object.get_status code, name)
         | 
    
        data/lib/rsmp/task.rb
    ADDED
    
    | @@ -0,0 +1,84 @@ | |
| 1 | 
            +
            module RSMP
         | 
| 2 | 
            +
              class Restart < StandardError
         | 
| 3 | 
            +
              end
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              module Task
         | 
| 6 | 
            +
                attr_reader :task
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize_task
         | 
| 9 | 
            +
                  @task = nil
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                # start our async tasks and return immediately
         | 
| 13 | 
            +
                # run() will be called inside the task to perform actual long-running work
         | 
| 14 | 
            +
                def start
         | 
| 15 | 
            +
                  return if @task
         | 
| 16 | 
            +
                  Async do |task|
         | 
| 17 | 
            +
                    task.annotate "#{self.class.name} main task"
         | 
| 18 | 
            +
                    @task = task
         | 
| 19 | 
            +
                    run
         | 
| 20 | 
            +
                    stop_subtasks
         | 
| 21 | 
            +
                    @task = nil
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                  self
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                # initiate restart by raising a Restart exception
         | 
| 27 | 
            +
                def restart
         | 
| 28 | 
            +
                  raise Restart.new "restart initiated by #{self.class.name}:#{object_id}"
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                # get the status of our task, or nil of no task
         | 
| 32 | 
            +
                def task_status
         | 
| 33 | 
            +
                  @task.status if @task
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                # perform any long-running work
         | 
| 37 | 
            +
                # the method will be called from an async task, and should not return
         | 
| 38 | 
            +
                # if subtasks are needed, the method should call wait() on each of them
         | 
| 39 | 
            +
                # once running, ready() must be called
         | 
| 40 | 
            +
                def run
         | 
| 41 | 
            +
                  start_subtasks
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                # wait for our task to complete
         | 
| 45 | 
            +
                def wait
         | 
| 46 | 
            +
                  @task.wait if @task
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                # stop our task
         | 
| 50 | 
            +
                def stop
         | 
| 51 | 
            +
                  stop_subtasks
         | 
| 52 | 
            +
                  stop_task if @task
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def stop_subtasks
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # stop our task and any subtask
         | 
| 59 | 
            +
                def stop_task
         | 
| 60 | 
            +
                  @task.stop
         | 
| 61 | 
            +
                  @task = nil
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                # wait for an async condition to signal, then yield to block
         | 
| 65 | 
            +
                # if block returns true we're done. otherwise, wait again
         | 
| 66 | 
            +
                def wait_for_condition condition, timeout:, task:Async::Task.current, &block
         | 
| 67 | 
            +
                  unless task
         | 
| 68 | 
            +
                    raise RuntimeError.new("Can't wait without a task")
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                  task.with_timeout(timeout) do
         | 
| 71 | 
            +
                    while task.running?
         | 
| 72 | 
            +
                      value = condition.wait
         | 
| 73 | 
            +
                      return value unless block
         | 
| 74 | 
            +
                      result = yield value
         | 
| 75 | 
            +
                      return result if result
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
                    raise RuntimeError.new("Can't wait for condition because task #{task.object_id} #{task.annotation} is not running")
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                rescue Async::TimeoutError
         | 
| 80 | 
            +
                  raise RSMP::TimeoutError.new
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
              end
         | 
| 84 | 
            +
            end
         | 
| @@ -9,25 +9,27 @@ module RSMP | |
| 9 9 | 
             
                  end
         | 
| 10 10 |  | 
| 11 11 | 
             
                  def timer
         | 
| 12 | 
            -
                    @state =  | 
| 12 | 
            +
                    @state = compute_state
         | 
| 13 13 | 
             
                  end
         | 
| 14 14 |  | 
| 15 | 
            -
                  def  | 
| 15 | 
            +
                  def compute_state
         | 
| 16 16 | 
             
                    return 'a' if node.main.dark_mode
         | 
| 17 17 | 
             
                    return 'c' if node.main.yellow_flash
         | 
| 18 18 |  | 
| 19 19 | 
             
                    cycle_counter = node.main.cycle_counter
         | 
| 20 20 |  | 
| 21 21 | 
             
                    if node.main.startup_sequence_active
         | 
| 22 | 
            -
                       | 
| 22 | 
            +
                      return node.main.startup_state || 'a'
         | 
| 23 23 | 
             
                    end
         | 
| 24 24 |  | 
| 25 25 | 
             
                    default = 'a'   # phase a means disabled/dark
         | 
| 26 26 | 
             
                    plan = node.main.current_plan
         | 
| 27 27 | 
             
                    return default unless plan
         | 
| 28 28 | 
             
                    return default unless plan.states
         | 
| 29 | 
            +
             | 
| 29 30 | 
             
                    states = plan.states[c_id]
         | 
| 30 31 | 
             
                    return default unless states
         | 
| 32 | 
            +
             | 
| 31 33 | 
             
                    state = states[cycle_counter]
         | 
| 32 34 | 
             
                    return default unless state =~ /[a-hA-G0-9N-P]/  # valid signal group states
         | 
| 33 35 | 
             
                    state
         | 
    
        data/lib/rsmp/tlc/signal_plan.rb
    CHANGED
    
    | @@ -1,8 +1,8 @@ | |
| 1 1 | 
             
            module RSMP
         | 
| 2 2 | 
             
              module TLC
         | 
| 3 3 | 
             
                # A Traffic Light Controller Signal Plan.
         | 
| 4 | 
            -
                # A signal plan is a description of how all signal groups should change | 
| 5 | 
            -
                # state over time. | 
| 4 | 
            +
                # A signal plan is a description of how all signal groups should change
         | 
| 5 | 
            +
                # state over time.
         | 
| 6 6 | 
             
                class SignalPlan
         | 
| 7 7 | 
             
                  attr_reader :nr, :states, :dynamic_bands
         | 
| 8 8 | 
             
                  def initialize nr:, states:, dynamic_bands:
         |