Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
\
TrafficControllerProxy
```

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

### TrafficControllerProxy
A TrafficControllerProxy 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
219 changes: 219 additions & 0 deletions documentation/tlc_proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# TLC Proxy

## Overview

The `RSMP::TLC::TrafficControllerProxy` 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 and **acts as a mirror of the remote TLC** by automatically subscribing to status updates to keep the proxy synchronized.

## Features

The TLC proxy provides convenient methods that abstract away the low-level RSMP message handling for common TLC operations:

### Signal Plan Management

- **`set_timeplan(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

### Status Value Storage & Automatic Synchronization

The proxy automatically stores retrieved status values and provides convenient attribute readers:

- **`timeplan`** - The currently active signal plan number (Integer)
- **`current_plan`** - Alias for `timeplan` for compatibility (Integer)
- **`plan_source`** - Source of current plan (String, e.g., "forced", "startup", "clock")
- **`timeplan_attributes`** - All S0014 attributes stored in the main component

### Automatic Status Subscription

The proxy **automatically subscribes to key TLC statuses** after connection is established:

- **Auto-subscribes to S0014** (timeplan status) with "update on change"
- **Automatically processes status updates** to keep local values synchronized
- **Handles subscription cleanup** when the proxy is closed

### Additional Methods

- **`subscribe_to_timeplan(options: {})`** - Manually subscribe to S0014 status updates
- **`unsubscribe_all()`** - Unsubscribe from all auto-subscriptions (inherited from SiteProxy)

### Timeout Configuration

The proxy accepts a `timeouts` configuration option for RSMP operations:

```ruby
timeouts = {
'watchdog' => 0.2,
'acknowledgement' => 0.2,
'command_timeout' => 5.0
}

proxy = TrafficControllerProxy.new(
supervisor: supervisor,
ip: '127.0.0.1',
port: 12345,
site_id: 'TLC001',
timeouts: timeouts
)
```

## Automatic Detection

When a TLC site connects to a supervisor, the supervisor can automatically detect that it's a TLC and create a `TrafficControllerProxy` instead of a generic `SiteProxy`. This behavior is controlled by the `proxy_type` setting in the supervisor configuration:

### Proxy Type Configuration

```ruby
# In supervisor configuration
supervisor_settings = {
'proxy_type' => 'auto', # Can be 'auto', 'generic', or 'tlc'
'sites' => {
'TLC001' => { 'sxl' => 'tlc', 'type' => 'tlc' }
}
}
```

**Proxy Type Options:**
- **`'generic'`** (default) - Always creates generic `SiteProxy` for compatibility
- **`'auto'`** - Auto-detects based on site configuration (`type: 'tlc'`)
- **`'tlc'`** - Always creates `TrafficControllerProxy` regardless of site type

**Example:**
```ruby
# Auto-detection mode for testing
supervisor_settings = {
'proxy_type' => 'auto',
'sites' => {
'TLC001' => { 'sxl' => 'tlc', 'type' => 'tlc' }
}
}

# When TLC001 connects, supervisor creates TrafficControllerProxy automatically
tlc_proxy = supervisor.wait_for_site('TLC001')
# tlc_proxy is now TrafficControllerProxy with automatic mirroring
```

## Usage Examples

### Setting a Signal Plan

```ruby
# Set signal plan 3 with security code
result = tlc_proxy.set_timeplan(3, security_code: '2222')

# Set plan and collect the response
result = tlc_proxy.set_timeplan(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 and store in proxy
result = tlc_proxy.fetch_signal_plan(options: { collect: { timeout: 5 } })

if result[:collector].ok?
# Status values are automatically stored in the proxy
puts "Current signal plan: #{tlc_proxy.timeplan}"
puts "Plan source: #{tlc_proxy.plan_source}"
else
puts "Failed to retrieve signal plan status"
end

# You can also access the raw response if needed
response = result[:collector].messages.first
status_items = response.attribute('sS')
```

### Accessing Stored Status Values

```ruby
# Status values are automatically updated when:
# 1. fetch_signal_plan is called with collection
# 2. Status updates are received from subscriptions

# Access the stored values directly
puts "Current plan: #{tlc_proxy.timeplan}"
puts "Current plan (alias): #{tlc_proxy.current_plan}"
puts "Plan source: #{tlc_proxy.plan_source}"

# Get all timeplan attributes from the component
attributes = tlc_proxy.timeplan_attributes
puts "All S0014 attributes: #{attributes}"

# Values persist until updated by new data
puts "Plan is still: #{tlc_proxy.timeplan}" # Same value
```

### Manual Subscription Management

```ruby
# The proxy automatically subscribes to timeplan status, but you can also:

# Manually subscribe (usually not needed)
tlc_proxy.subscribe_to_timeplan

# Unsubscribe from all auto-subscriptions
tlc_proxy.unsubscribe_all
```

### 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
- Fiber-safe and async-compatible with the rest of the RSMP framework
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_controller_proxy'
require 'rsmp/tlc/detector_logic'
require 'rsmp/tlc/signal_group'
require 'rsmp/tlc/signal_plan'
Expand Down
20 changes: 20 additions & 0 deletions lib/rsmp/site_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -385,5 +385,25 @@ def build_component id:, type:, settings:{}
def infer_component_type component_id
ComponentProxy
end

# Unsubscribe from all subscriptions
# This method provides a centralized way to clean up all subscriptions
def unsubscribe_all
# Create a copy of the subscriptions to avoid modifying while iterating
subscriptions_copy = @status_subscriptions.dup

subscriptions_copy.each do |component_id, component_subscriptions|
component_subscriptions.each do |sCI, sCI_subscriptions|
sCI_subscriptions.each do |n, _subscription_data|
status_list = [{ 'sCI' => sCI, 'n' => n }]
begin
unsubscribe_to_status component_id, status_list
rescue => e
log "Failed to unsubscribe from #{component_id} #{sCI}/#{n}: #{e.message}", level: :warn
end
end
end
end
end
end
end
44 changes: 37 additions & 7 deletions lib/rsmp/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ def handle_supervisor_settings supervisor_settings
defaults = {
'port' => 12111,
'ips' => 'all',
'proxy_type' => 'generic', # Control proxy creation: 'generic', 'auto', or specific type
'guest' => {
'sxl' => 'tlc',
'type' => 'tlc',
'intervals' => {
'timer' => 1,
'watchdog' => 1
Expand Down Expand Up @@ -107,10 +109,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 +177,10 @@ 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

# Build the appropriate proxy type based on site settings
proxy = build_proxy(id, settings)

@proxies.push proxy
end

Expand All @@ -194,6 +195,28 @@ def accept_connection socket, info
stop if @supervisor_settings['one_shot']
end

def build_proxy(site_id, settings)
# Determine the appropriate proxy type based on supervisor configuration
proxy_type_setting = @supervisor_settings['proxy_type']

case proxy_type_setting
when 'auto'
# Auto-detect based on site settings
site_settings = check_site_id site_id
if site_settings && site_settings['type'] == 'tlc'
TLC::TrafficControllerProxy.new settings.merge(site_id: site_id)
else
SiteProxy.new settings.merge(site_id: site_id)
end
when 'tlc'
# Force TLC proxy
TLC::TrafficControllerProxy.new settings.merge(site_id: site_id)
else # 'generic' or any other value defaults to generic SiteProxy
# Use generic SiteProxy (default for compatibility)
SiteProxy.new settings.merge(site_id: site_id)
end
end

def site_ids_changed
@site_id_condition.signal
end
Expand Down Expand Up @@ -264,11 +287,18 @@ def check_site_already_connected site_id

def site_id_to_site_setting site_id
return {} unless @supervisor_settings['sites']
@supervisor_settings['sites'].each_pair do |id,settings|
if id == 'guest' || id == site_id

# First look for specific site_id
@supervisor_settings['sites'].each_pair do |id, settings|
if id == site_id
return settings
end
end

# Fall back to guest configuration if site_id not found
guest_settings = @supervisor_settings['sites']['guest']
return guest_settings if guest_settings

raise HandshakeError.new "site id #{site_id} unknown"
end

Expand Down
Loading