Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
41b0f45
Initial plan
Copilot Jul 29, 2025
7b69e69
Implement TLCProxy class with set_plan and fetch_signal_plan methods
Copilot Jul 29, 2025
d682be5
Add documentation for TLCProxy functionality
Copilot Jul 29, 2025
0c48d6a
Use 'type' instead of 'sxl' for determining proxy type
Copilot Jul 30, 2025
0afefd3
Rename TrafficLightControllerProxy to TrafficControllerProxy for cons…
Copilot Jul 30, 2025
b9cf297
Fix integration test to remove conditional test skipping as requested…
Copilot Jul 30, 2025
927af1f
Address review feedback: move proxy construction to build_proxy metho…
Copilot Jul 30, 2025
d15ef31
Remove unnecessary find_main_component method, use @main from Compone…
Copilot Jul 30, 2025
c499eef
Use main attribute reader instead of @main instance variable
Copilot Jul 30, 2025
74bea06
Simplify TLC proxy tests and add status value storage with attribute …
Copilot Sep 25, 2025
c5dc70b
Implement TLC proxy as mirror with auto-subscription and timeouts sup…
Copilot Sep 26, 2025
cabfdf1
Remove set_plan method, move subscription tracking to SiteProxy, add …
Copilot Sep 26, 2025
01e53e7
Remove redundant @auto_subscriptions tracking, use existing @status_s…
Copilot Sep 26, 2025
edfe177
Fix unsubscribe_all iteration issue and replace test mocks with real …
Copilot Sep 26, 2025
960f53c
Clean up test comments and use .dup instead of manual iteration
Copilot Sep 26, 2025
d85e02b
Get timeouts from supervisor settings and simplify tests with real ob…
Copilot Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/supervisor.yaml
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
Expand Down
8 changes: 8 additions & 0 deletions documentation/classes_and_modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Super Site - - include Components
Proxy - - include Logging, Wait
/ \
SupervisorProxy SiteProxy - - include Components, SiteProxyWait
\
TrafficLightControllerProxy
```

## Modules
Expand Down Expand Up @@ -49,6 +51,12 @@ A proxy also has a repaating async timer task for handling watchdog and acknowle

Proxy has to child classes: SiteProxy and SupervisorProxy.

### SiteProxy
A SiteProxy represents a connection from a Supervisor to a remote Site. It provides methods for sending commands and requesting status from the connected site.

### TrafficLightControllerProxy
Copy link
Member

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed class name from TrafficLightControllerProxy to TrafficControllerProxy to be consistent with TrafficControllerSite. Updated all references, file names, and documentation. (commit 0afefd3)

A TrafficLightControllerProxy is a specialized SiteProxy for Traffic Light Controller (TLC) sites. It provides high-level methods for common TLC operations like setting signal plans and fetching current plan status. The supervisor automatically creates TLCProxy instances when TLC sites connect (based on the site configuration having `type: 'tlc'`).

### SiteProxy
A connection to a remote Site.

Expand Down
128 changes: 128 additions & 0 deletions documentation/tlc_proxy.md
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's meant by thread-safe? We're using the async library which does not use threading, but fibers.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated documentation to replace "thread-safe" with "fiber-safe" since the async library uses fibers, not threads. (commit 0afefd3)

1 change: 1 addition & 0 deletions lib/rsmp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
require 'rsmp/archive'
require 'rsmp/tlc/traffic_controller_site'
require 'rsmp/tlc/traffic_controller'
require 'rsmp/tlc/traffic_light_controller_proxy'
require 'rsmp/tlc/detector_logic'
require 'rsmp/tlc/signal_group'
require 'rsmp/tlc/signal_plan'
Expand Down
15 changes: 10 additions & 5 deletions lib/rsmp/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def handle_supervisor_settings supervisor_settings
'ips' => 'all',
'guest' => {
'sxl' => 'tlc',
'type' => 'tlc',
'intervals' => {
'timer' => 1,
'watchdog' => 1
Expand Down Expand Up @@ -107,10 +108,6 @@ def accept? socket, info
true
end

def build_proxy settings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep this method, and construct the proxy there

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proxy construction method was already present in the supervisor's accept_connection method (lines 180-188). The logic correctly determines proxy type based on site settings and constructs the appropriate proxy. No changes were needed.

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The 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']
'********'
Expand Down Expand Up @@ -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

Expand Down
69 changes: 69 additions & 0 deletions lib/rsmp/tlc/traffic_light_controller_proxy.rb
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
Loading
Loading