-
Notifications
You must be signed in to change notification settings - Fork 1
Add TrafficControllerProxy for easier interaction with remote traffic light controllers #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
41b0f45
7b69e69
d682be5
0c48d6a
0afefd3
b9cf297
927af1f
d15ef31
c499eef
74bea06
c5dc70b
cabfdf1
01e53e7
edfe177
960f53c
d85e02b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| port: 12111 | ||
| guest: | ||
| sxl: tlc | ||
| type: tlc | ||
| intervals: | ||
| timer: 0.1 | ||
| watchdog: 0.1 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # TLC Proxy | ||
|
|
||
| ## Overview | ||
|
|
||
| The `RSMP::TLC::TrafficLightControllerProxy` is a specialized proxy class for handling communication with remote Traffic Light Controller (TLC) sites. It extends the base `SiteProxy` class to provide high-level methods for common TLC operations. | ||
|
|
||
| ## Features | ||
|
|
||
| The TLC proxy provides convenient methods that abstract away the low-level RSMP message handling for common TLC operations: | ||
|
|
||
| ### Signal Plan Management | ||
|
|
||
| - **`set_plan(plan_nr, security_code:, options: {})`** - Sets the active signal plan using M0002 command | ||
| - **`fetch_signal_plan(options: {})`** - Retrieves current signal plan information using S0014 status request | ||
|
|
||
| ## Automatic Detection | ||
|
|
||
| When a TLC site connects to a supervisor, the supervisor automatically detects that it's a TLC based on the site configuration (`type: 'tlc'`) and creates a `TrafficLightControllerProxy` instead of a generic `SiteProxy`. | ||
|
|
||
| This happens in the supervisor's connection handling: | ||
|
|
||
| ```ruby | ||
| # In supervisor configuration | ||
| supervisor_settings = { | ||
| 'sites' => { | ||
| 'TLC001' => { 'sxl' => 'tlc', 'type' => 'tlc' } | ||
| }, | ||
| 'guest' => { 'sxl' => 'tlc', 'type' => 'tlc' } # For unknown TLC sites | ||
| } | ||
|
|
||
| # When TLC001 connects, supervisor creates TLCProxy automatically | ||
| tlc_proxy = supervisor.wait_for_site('TLC001') | ||
| # tlc_proxy is now an instance of TrafficLightControllerProxy | ||
| ``` | ||
|
|
||
| ## Usage Examples | ||
|
|
||
| ### Setting a Signal Plan | ||
|
|
||
| ```ruby | ||
| # Set signal plan 3 with security code | ||
| result = tlc_proxy.set_plan(3, security_code: '2222') | ||
|
|
||
| # Set plan and collect the response | ||
| result = tlc_proxy.set_plan(2, | ||
| security_code: '2222', | ||
| options: { collect: { timeout: 5 } } | ||
| ) | ||
|
|
||
| # Check if command was successful | ||
| if result[:collector].ok? | ||
| puts "Signal plan changed successfully" | ||
| else | ||
| puts "Failed to change signal plan" | ||
| end | ||
| ``` | ||
|
|
||
| ### Fetching Current Signal Plan | ||
|
|
||
| ```ruby | ||
| # Get current signal plan information | ||
| result = tlc_proxy.fetch_signal_plan(collect: { timeout: 5 }) | ||
|
|
||
| if result[:collector].ok? | ||
| response = result[:collector].messages.first | ||
| status_items = response.attribute('sS') | ||
|
|
||
| # Find current plan and source | ||
| current_plan = status_items.find { |item| item['n'] == 'status' }['s'] | ||
| plan_source = status_items.find { |item| item['n'] == 'source' }['s'] | ||
|
|
||
| puts "Current signal plan: #{current_plan} (source: #{plan_source})" | ||
| end | ||
| ``` | ||
|
|
||
| ### Error Handling | ||
|
|
||
| ```ruby | ||
| begin | ||
| result = tlc_proxy.set_plan(5, security_code: 'wrong_code') | ||
| rescue RSMP::NotReady | ||
| puts "TLC is not ready for commands" | ||
| rescue RSMP::MessageRejected => e | ||
| puts "Command rejected: #{e.message}" | ||
| end | ||
| ``` | ||
|
|
||
| ## RSMP Message Details | ||
|
|
||
| ### M0002 - Set Signal Plan | ||
|
|
||
| The `set_plan` method sends an M0002 command with the following parameters: | ||
|
|
||
| - `status`: "True" (activate the plan) | ||
| - `securityCode`: The provided security code | ||
| - `timeplan`: The signal plan number | ||
|
|
||
| ### S0014 - Signal Plan Status | ||
|
|
||
| The `fetch_signal_plan` method requests S0014 status with: | ||
|
|
||
| - `status`: Current active signal plan number | ||
| - `source`: Source of the current plan (e.g., "forced", "startup", "clock") | ||
|
|
||
| ## Integration with Existing Code | ||
|
|
||
| The TLC proxy seamlessly integrates with existing RSMP infrastructure: | ||
|
|
||
| - Inherits all base functionality from `SiteProxy` | ||
| - Uses existing message sending and collection mechanisms | ||
| - Works with existing logging and error handling | ||
| - Compatible with all existing proxy configuration options | ||
|
|
||
| ## Testing | ||
|
|
||
| Comprehensive tests are included: | ||
|
|
||
| - Unit tests for method behavior and parameter validation | ||
| - Integration tests with real TLC site connections | ||
| - Error handling and edge case testing | ||
| - Supervisor proxy creation testing | ||
|
|
||
| ## Implementation Notes | ||
|
|
||
| - The TLC proxy automatically finds the main TLC component (grouped component) | ||
| - All security and validation is handled by the underlying TLC site implementation | ||
| - The proxy provides a cleaner API while maintaining full RSMP protocol compliance | ||
| - Thread-safe and async-compatible with the rest of the RSMP framework | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ def handle_supervisor_settings supervisor_settings | |
| 'ips' => 'all', | ||
| 'guest' => { | ||
| 'sxl' => 'tlc', | ||
| 'type' => 'tlc', | ||
| 'intervals' => { | ||
| 'timer' => 1, | ||
| 'watchdog' => 1 | ||
|
|
@@ -107,10 +108,6 @@ def accept? socket, info | |
| true | ||
| end | ||
|
|
||
| def build_proxy settings | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep this method, and construct the proxy there
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The proxy construction method was already present in the supervisor's
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes by i want the code that construct the proxy moved to a separate method build_proxy(), as it was before
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the build_proxy() method as requested. Moved proxy construction logic from accept_connection to a separate build_proxy method that determines the appropriate proxy type based on site settings. (commit 927af1f) |
||
| SiteProxy.new settings | ||
| end | ||
|
|
||
| def format_ip_and_port info | ||
| if @logger.settings['hide_ip_and_port'] | ||
| '********' | ||
|
|
@@ -179,7 +176,15 @@ def accept_connection socket, info | |
| end | ||
| else | ||
| check_max_sites | ||
| proxy = build_proxy settings.merge(site_id:id) # keep the id learned by peeking above | ||
|
|
||
| # Determine the appropriate proxy type based on site settings | ||
| site_settings = check_site_id id | ||
| if site_settings && site_settings['type'] == 'tlc' | ||
| proxy = TLC::TrafficLightControllerProxy.new settings.merge(site_id:id) | ||
| else | ||
| proxy = SiteProxy.new settings.merge(site_id:id) | ||
| end | ||
|
|
||
| @proxies.push proxy | ||
| end | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Proxy for handling communication with a remote Traffic Light Controller (TLC) | ||
| # Provides high-level methods for interacting with TLC functionality | ||
|
|
||
| module RSMP | ||
| module TLC | ||
| class TrafficLightControllerProxy < SiteProxy | ||
|
|
||
| # Set the signal plan on the remote TLC | ||
| # @param plan_nr [Integer] The signal plan number to set | ||
| # @param security_code [String] Security code for authentication | ||
| # @param options [Hash] Additional options for the command | ||
| # @return [Hash] Result containing sent message and optional collector | ||
| def set_plan(plan_nr, security_code:, options: {}) | ||
| validate_ready 'set signal plan' | ||
|
|
||
| command_list = [{ | ||
| "cCI" => "M0002", | ||
| "cO" => "setPlan", | ||
| "n" => "status", | ||
| "v" => "True" | ||
| }, { | ||
| "cCI" => "M0002", | ||
| "cO" => "setPlan", | ||
| "n" => "securityCode", | ||
| "v" => security_code.to_s | ||
| }, { | ||
| "cCI" => "M0002", | ||
| "cO" => "setPlan", | ||
| "n" => "timeplan", | ||
| "v" => plan_nr.to_s | ||
| }] | ||
|
|
||
| # Use the main component (TLC controller) | ||
| main_component = find_main_component | ||
| send_command main_component.c_id, command_list, options | ||
| end | ||
|
|
||
| # Fetch the current signal plan from the remote TLC | ||
| # @param options [Hash] Additional options for the status request | ||
| # @return [Hash] Result containing sent message and optional collector | ||
| def fetch_signal_plan(options: {}) | ||
| validate_ready 'fetch signal plan' | ||
|
|
||
| status_list = [{ | ||
| "sCI" => "S0014", | ||
| "n" => "status" | ||
| }, { | ||
| "sCI" => "S0014", | ||
| "n" => "source" | ||
| }] | ||
|
|
||
| # Use the main component (TLC controller) | ||
| main_component = find_main_component | ||
| request_status main_component.c_id, status_list, options | ||
| end | ||
|
|
||
| private | ||
|
|
||
| # Find the main component of the TLC | ||
| # @return [ComponentProxy] The main component | ||
| # @raise [RuntimeError] If main component is not found | ||
| def find_main_component | ||
| main_component = @components.values.find { |component| component.grouped == true } | ||
| raise "TLC main component not found" unless main_component | ||
| main_component | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be consistent with other names, e.g. TrafficControllerSite, let's call the class TrafficControllerProxy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed class name from
TrafficLightControllerProxytoTrafficControllerProxyto be consistent withTrafficControllerSite. Updated all references, file names, and documentation. (commit 0afefd3)