smart_message 0.0.10 → 0.0.12
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/deploy-github-pages.yml +38 -0
- data/.gitignore +5 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +35 -4
- data/README.md +169 -71
- data/Rakefile +29 -4
- data/docs/assets/images/ddq_architecture.svg +130 -0
- data/docs/assets/images/dlq_architecture.svg +115 -0
- data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
- data/docs/assets/images/enhanced-fluent-api.svg +149 -0
- data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
- data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
- data/docs/assets/images/fluent-api-demo.svg +59 -0
- data/docs/assets/images/performance-comparison.svg +161 -0
- data/docs/assets/images/redis-basic-architecture.svg +53 -0
- data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
- data/docs/assets/images/redis-queue-architecture.svg +101 -0
- data/docs/assets/images/smart_message.jpg +0 -0
- data/docs/assets/images/smart_message_walking.jpg +0 -0
- data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
- data/docs/assets/images/transport-comparison-matrix.svg +171 -0
- data/docs/assets/javascripts/mathjax.js +17 -0
- data/docs/assets/stylesheets/extra.css +51 -0
- data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
- data/docs/{architecture.md → core-concepts/architecture.md} +78 -138
- data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
- data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
- data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
- data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
- data/docs/{examples.md → getting-started/examples.md} +115 -89
- data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
- data/docs/guides/redis-queue-getting-started.md +697 -0
- data/docs/guides/redis-queue-patterns.md +889 -0
- data/docs/guides/redis-queue-production.md +1091 -0
- data/docs/index.md +64 -0
- data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
- data/docs/{logging.md → reference/logging.md} +1 -1
- data/docs/{message_deduplication.md → reference/message-deduplication.md} +1 -0
- data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
- data/docs/{serializers.md → reference/serializers.md} +3 -5
- data/docs/{transports.md → reference/transports.md} +133 -11
- data/docs/transports/memory-transport.md +374 -0
- data/docs/transports/redis-enhanced-transport.md +524 -0
- data/docs/transports/redis-queue-transport.md +1304 -0
- data/docs/transports/redis-transport-comparison.md +496 -0
- data/docs/transports/redis-transport.md +509 -0
- data/examples/README.md +98 -5
- data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
- data/examples/city_scenario/README.md +515 -0
- data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
- data/examples/city_scenario/citizen.rb +195 -0
- data/examples/city_scenario/city_diagram.svg +125 -0
- data/examples/city_scenario/common/health_monitor.rb +80 -0
- data/examples/city_scenario/common/logger.rb +30 -0
- data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
- data/examples/city_scenario/fire_department.rb +446 -0
- data/examples/city_scenario/fire_emergency_flow.svg +95 -0
- data/examples/city_scenario/health_department.rb +100 -0
- data/examples/city_scenario/health_monitoring_system.svg +130 -0
- data/examples/city_scenario/house.rb +244 -0
- data/examples/city_scenario/local_bank.rb +217 -0
- data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
- data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
- data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
- data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
- data/examples/city_scenario/messages/health_check_message.rb +22 -0
- data/examples/city_scenario/messages/health_status_message.rb +35 -0
- data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
- data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
- data/examples/city_scenario/police_department.rb +316 -0
- data/examples/city_scenario/redis_monitor.rb +129 -0
- data/examples/city_scenario/redis_stats.rb +743 -0
- data/examples/city_scenario/room_for_improvement.md +240 -0
- data/examples/city_scenario/security_emergency_flow.svg +95 -0
- data/examples/city_scenario/service_internal_architecture.svg +154 -0
- data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
- data/examples/city_scenario/start_demo.sh +236 -0
- data/examples/city_scenario/stop_demo.sh +106 -0
- data/examples/city_scenario/visitor.rb +631 -0
- data/examples/{10_message_deduplication.rb → memory/01_message_deduplication_demo.rb} +1 -1
- data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
- data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
- data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
- data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
- data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
- data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
- data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
- data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
- data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
- data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
- data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
- data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
- data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
- data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
- data/examples/memory/README.md +163 -0
- data/examples/memory/memory_transport_architecture.svg +90 -0
- data/examples/memory/point_to_point_pattern.svg +94 -0
- data/examples/memory/publish_subscribe_pattern.svg +125 -0
- data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
- data/examples/redis/README.md +230 -0
- data/examples/redis/alert_system_flow.svg +127 -0
- data/examples/redis/dashboard_status_flow.svg +107 -0
- data/examples/redis/device_command_flow.svg +113 -0
- data/examples/redis/redis_transport_architecture.svg +115 -0
- data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
- data/examples/redis/smart_home_system_architecture.svg +133 -0
- data/examples/redis_enhanced/README.md +319 -0
- data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/01_basic_messaging.rb +221 -0
- data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
- data/examples/redis_queue/02_pattern_routing.rb +405 -0
- data/examples/redis_queue/03_fluent_api.rb +422 -0
- data/examples/redis_queue/04_load_balancing.rb +486 -0
- data/examples/redis_queue/05_microservices.rb +735 -0
- data/examples/redis_queue/06_emergency_alerts.rb +777 -0
- data/examples/redis_queue/07_queue_management.rb +587 -0
- data/examples/redis_queue/README.md +366 -0
- data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/redis_queue_architecture.svg +148 -0
- data/ideas/README.md +41 -0
- data/ideas/agents.md +1001 -0
- data/ideas/database_transport.md +980 -0
- data/ideas/improvement.md +359 -0
- data/ideas/meshage.md +1788 -0
- data/ideas/message_discovery.md +178 -0
- data/ideas/message_schema.md +1381 -0
- data/lib/smart_message/.idea/.gitignore +8 -0
- data/lib/smart_message/.idea/markdown.xml +6 -0
- data/lib/smart_message/.idea/misc.xml +4 -0
- data/lib/smart_message/.idea/modules.xml +8 -0
- data/lib/smart_message/.idea/smart_message.iml +16 -0
- data/lib/smart_message/.idea/vcs.xml +6 -0
- data/lib/smart_message/addressing.rb +15 -0
- data/lib/smart_message/base.rb +0 -2
- data/lib/smart_message/configuration.rb +1 -1
- data/lib/smart_message/logger.rb +15 -4
- data/lib/smart_message/plugins.rb +5 -2
- data/lib/smart_message/serializer.rb +14 -0
- data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
- data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +34 -1
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message.rb +5 -52
- data/mkdocs.yml +184 -0
- data/p2p_plan.md +326 -0
- data/p2p_roadmap.md +287 -0
- data/smart_message.gemspec +2 -0
- data/smart_message.svg +51 -0
- metadata +170 -44
- data/docs/README.md +0 -57
- data/examples/dead_letters.jsonl +0 -12
- data/examples/temp.txt +0 -94
- data/examples/tmux_chat/README.md +0 -283
- data/examples/tmux_chat/bot_agent.rb +0 -278
- data/examples/tmux_chat/human_agent.rb +0 -199
- data/examples/tmux_chat/room_monitor.rb +0 -160
- data/examples/tmux_chat/shared_chat_system.rb +0 -328
- data/examples/tmux_chat/start_chat_demo.sh +0 -190
- data/examples/tmux_chat/stop_chat_demo.sh +0 -22
- /data/docs/{properties.md → core-concepts/properties.md} +0 -0
- /data/docs/{ideas_to_think_about.md → development/ideas.md} +0 -0
@@ -0,0 +1,446 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/multi_program_demo/fire_department.rb
|
3
|
+
|
4
|
+
require_relative '../../lib/smart_message'
|
5
|
+
|
6
|
+
require_relative 'messages/health_check_message'
|
7
|
+
require_relative 'messages/health_status_message'
|
8
|
+
require_relative 'messages/fire_emergency_message'
|
9
|
+
require_relative 'messages/fire_dispatch_message'
|
10
|
+
require_relative 'messages/emergency_resolved_message'
|
11
|
+
require_relative 'messages/emergency_911_message'
|
12
|
+
|
13
|
+
require_relative 'common/health_monitor'
|
14
|
+
require_relative 'common/logger'
|
15
|
+
|
16
|
+
class FireDepartment
|
17
|
+
include Common::HealthMonitor
|
18
|
+
include Common::Logger
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@service_name = 'fire_department'
|
22
|
+
@status = 'healthy'
|
23
|
+
@start_time = Time.now
|
24
|
+
@active_fires = {}
|
25
|
+
@available_engines = ['Engine-1', 'Engine-2', 'Engine-3', 'Ladder-1', 'Rescue-1']
|
26
|
+
|
27
|
+
setup_messaging
|
28
|
+
setup_signal_handlers
|
29
|
+
setup_health_monitor
|
30
|
+
end
|
31
|
+
|
32
|
+
# def setup_logging
|
33
|
+
# log_file = File.join(__dir__, 'fire_department.log')
|
34
|
+
# logger = Logger.new(log_file)
|
35
|
+
# logger.level = Logger::INFO
|
36
|
+
# logger.formatter = proc do |severity, datetime, progname, msg|
|
37
|
+
# "#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
38
|
+
# end
|
39
|
+
# logger.info("Fire Department logging started")
|
40
|
+
# end
|
41
|
+
|
42
|
+
def setup_messaging
|
43
|
+
Messages::FireDispatchMessage.from = @service_name
|
44
|
+
Messages::EmergencyResolvedMessage.from = @service_name
|
45
|
+
|
46
|
+
# Subscribe to fire emergencies from houses
|
47
|
+
Messages::FireEmergencyMessage.subscribe(to: [@service_name, /fire/i]) do |message|
|
48
|
+
handle_fire_emergency(message)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Subscribe to 911 calls routed from dispatch (medical, rescue, hazmat)
|
52
|
+
Messages::Emergency911Message.subscribe(to: [@service_name, /medical|rescue|hazmat/i]) do |message|
|
53
|
+
handle_911_call(message)
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "🚒 Fire Department ready for service"
|
57
|
+
puts " Available engines: #{@available_engines.join(', ')}"
|
58
|
+
puts " Responding to health checks, fire emergencies, and 911 medical/rescue calls"
|
59
|
+
puts " Press Ctrl+C to stop\n\n"
|
60
|
+
logger.info("Fire Department ready for service with engines: #{@available_engines.join(', ')}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def setup_signal_handlers
|
64
|
+
%w[INT TERM].each do |signal|
|
65
|
+
Signal.trap(signal) do
|
66
|
+
puts "\n🚒 Fire Department out of service..."
|
67
|
+
logger.info("Fire Department out of service")
|
68
|
+
exit(0)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def start_service
|
75
|
+
loop do
|
76
|
+
check_fire_resolutions
|
77
|
+
sleep(3)
|
78
|
+
end
|
79
|
+
rescue => e
|
80
|
+
puts "🚒 Error in fire service: #{e.message}"
|
81
|
+
logger.error("Error in fire service: #{e.message}")
|
82
|
+
retry
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def service_emoji
|
88
|
+
"🚒"
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_status_details
|
92
|
+
# Status depends on active fires and available engines
|
93
|
+
@status = if @active_fires.size >= 3
|
94
|
+
'critical'
|
95
|
+
elsif @active_fires.size >= 2
|
96
|
+
'warning'
|
97
|
+
elsif @available_engines.size < 2
|
98
|
+
'warning'
|
99
|
+
else
|
100
|
+
'healthy'
|
101
|
+
end
|
102
|
+
|
103
|
+
details = case @status
|
104
|
+
when 'healthy' then "All engines available, #{@active_fires.size} active fires"
|
105
|
+
when 'warning' then "#{@available_engines.size} engines available, #{@active_fires.size} active fires"
|
106
|
+
when 'critical' then "All engines deployed, #{@active_fires.size} major fires"
|
107
|
+
when 'failed' then "Equipment failure, emergency mutual aid requested"
|
108
|
+
end
|
109
|
+
|
110
|
+
[@status, details]
|
111
|
+
end
|
112
|
+
|
113
|
+
def handle_fire_emergency(emergency)
|
114
|
+
# Process fire emergency message with colored output
|
115
|
+
severity_color = case emergency.severity
|
116
|
+
when 'small' then "\e[33m" # Yellow
|
117
|
+
when 'medium' then "\e[93m" # Orange
|
118
|
+
when 'large' then "\e[31m" # Red
|
119
|
+
when 'out_of_control' then "\e[91m" # Bright red
|
120
|
+
else "\e[0m"
|
121
|
+
end
|
122
|
+
|
123
|
+
puts "🔥 #{severity_color}FIRE EMERGENCY\e[0m: #{emergency.house_address}"
|
124
|
+
puts " Type: #{emergency.fire_type} | Severity: #{emergency.severity.upcase}"
|
125
|
+
puts " Occupants: #{emergency.occupants_status}" if emergency.occupants_status
|
126
|
+
puts " Spread Risk: #{emergency.spread_risk}" if emergency.spread_risk
|
127
|
+
puts " Time: #{emergency.timestamp}"
|
128
|
+
logger.warn("FIRE EMERGENCY: #{emergency.fire_type} fire at #{emergency.house_address} (#{emergency.severity} severity)")
|
129
|
+
|
130
|
+
# Determine engines needed based on fire severity
|
131
|
+
engines_needed = case emergency.severity
|
132
|
+
when 'small' then 1
|
133
|
+
when 'medium' then 2
|
134
|
+
when 'large' then 3
|
135
|
+
when 'out_of_control' then @available_engines.size
|
136
|
+
else 2
|
137
|
+
end
|
138
|
+
|
139
|
+
assigned_engines = @available_engines.take(engines_needed)
|
140
|
+
@available_engines = @available_engines.drop(engines_needed)
|
141
|
+
|
142
|
+
# Determine equipment needed
|
143
|
+
equipment = determine_equipment_needed(emergency.fire_type, emergency.severity)
|
144
|
+
|
145
|
+
dispatch_id = SecureRandom.hex(4)
|
146
|
+
@active_fires[dispatch_id] = {
|
147
|
+
address: emergency.house_address,
|
148
|
+
start_time: Time.now,
|
149
|
+
engines: assigned_engines,
|
150
|
+
fire_type: emergency.fire_type,
|
151
|
+
severity: emergency.severity
|
152
|
+
}
|
153
|
+
|
154
|
+
# Send dispatch message
|
155
|
+
dispatch = Messages::FireDispatchMessage.new(
|
156
|
+
dispatch_id: dispatch_id,
|
157
|
+
engines_assigned: assigned_engines,
|
158
|
+
location: emergency.house_address,
|
159
|
+
fire_type: emergency.fire_type,
|
160
|
+
equipment_needed: equipment,
|
161
|
+
estimated_arrival: "#{rand(4..10)} minutes",
|
162
|
+
timestamp: Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
163
|
+
)
|
164
|
+
dispatch.publish
|
165
|
+
|
166
|
+
puts "🚒 Dispatched #{assigned_engines.size} engines to #{emergency.house_address}"
|
167
|
+
puts " Fire type: #{emergency.fire_type} (#{emergency.severity})"
|
168
|
+
puts " Available engines: #{@available_engines.size}"
|
169
|
+
logger.info("Dispatched engines #{assigned_engines.join(', ')} to #{emergency.house_address} (#{dispatch_id})")
|
170
|
+
rescue => e
|
171
|
+
puts "🚒 Error handling fire emergency: #{e.message}"
|
172
|
+
logger.error("Error handling fire emergency: #{e.message}")
|
173
|
+
end
|
174
|
+
|
175
|
+
def handle_911_call(call)
|
176
|
+
# Color code based on emergency type
|
177
|
+
type_color = case call.emergency_type
|
178
|
+
when 'medical' then "\e[91m" # Bright red
|
179
|
+
when 'rescue' then "\e[33m" # Yellow
|
180
|
+
when 'hazmat' then "\e[35m" # Magenta
|
181
|
+
when 'accident' then "\e[31m" # Red (multi-car)
|
182
|
+
else "\e[36m" # Cyan
|
183
|
+
end
|
184
|
+
|
185
|
+
puts "\n#{type_color}🚒 911 EMERGENCY\e[0m from dispatch"
|
186
|
+
puts " 📍 Location: #{call.caller_location}"
|
187
|
+
puts " 🚨 Type: #{call.emergency_type.upcase}"
|
188
|
+
puts " 📝 Description: #{call.description}"
|
189
|
+
puts " 👤 Caller: #{call.caller_name || 'Unknown'}"
|
190
|
+
|
191
|
+
logger.warn("911 Emergency: #{call.emergency_type} at #{call.caller_location} - #{call.description}")
|
192
|
+
|
193
|
+
# Handle based on emergency type
|
194
|
+
case call.emergency_type
|
195
|
+
when 'medical'
|
196
|
+
handle_medical_call(call)
|
197
|
+
when 'rescue'
|
198
|
+
handle_rescue_call(call)
|
199
|
+
when 'hazmat'
|
200
|
+
handle_hazmat_call(call)
|
201
|
+
when 'accident'
|
202
|
+
handle_major_accident(call)
|
203
|
+
else
|
204
|
+
handle_general_emergency(call)
|
205
|
+
end
|
206
|
+
rescue => e
|
207
|
+
puts "🚒 Error handling 911 call: #{e.message}"
|
208
|
+
logger.error("Error handling 911 call: #{e.message}")
|
209
|
+
end
|
210
|
+
|
211
|
+
def handle_medical_call(call)
|
212
|
+
incident_id = "MED-#{Time.now.strftime('%H%M%S')}"
|
213
|
+
|
214
|
+
# Medical calls get rescue unit and potentially an engine
|
215
|
+
units_needed = call.severity == 'critical' ? 2 : 1
|
216
|
+
units_needed = [units_needed, @available_engines.size].min
|
217
|
+
|
218
|
+
# Prefer Rescue unit for medical
|
219
|
+
assigned_engines = []
|
220
|
+
if @available_engines.include?('Rescue-1')
|
221
|
+
assigned_engines << @available_engines.delete('Rescue-1')
|
222
|
+
units_needed -= 1
|
223
|
+
end
|
224
|
+
assigned_engines.concat(@available_engines.shift(units_needed)) if units_needed > 0
|
225
|
+
|
226
|
+
if assigned_engines.empty?
|
227
|
+
puts "🚒 ⚠️ No units available for medical response!"
|
228
|
+
logger.error("No units available for medical at #{call.caller_location}")
|
229
|
+
return
|
230
|
+
end
|
231
|
+
|
232
|
+
@active_fires[incident_id] = {
|
233
|
+
type: 'medical',
|
234
|
+
location: call.caller_location,
|
235
|
+
engines: assigned_engines,
|
236
|
+
start_time: Time.now,
|
237
|
+
severity: call.severity,
|
238
|
+
call: call
|
239
|
+
}
|
240
|
+
|
241
|
+
puts "🚒 Dispatched #{assigned_engines.join(', ')} to medical emergency"
|
242
|
+
puts " Injuries: #{call.number_of_victims || 'Unknown'} victims"
|
243
|
+
puts " Severity: #{call.severity&.upcase || 'UNKNOWN'}"
|
244
|
+
logger.info("Dispatched #{assigned_engines.join(', ')} to medical #{incident_id}")
|
245
|
+
end
|
246
|
+
|
247
|
+
def handle_rescue_call(call)
|
248
|
+
incident_id = "RSC-#{Time.now.strftime('%H%M%S')}"
|
249
|
+
|
250
|
+
# Rescue operations need ladder and rescue units
|
251
|
+
units_needed = 2
|
252
|
+
units_needed = [units_needed, @available_engines.size].min
|
253
|
+
|
254
|
+
# Prefer Ladder and Rescue units
|
255
|
+
assigned_engines = []
|
256
|
+
['Rescue-1', 'Ladder-1'].each do |preferred|
|
257
|
+
if @available_engines.include?(preferred)
|
258
|
+
assigned_engines << @available_engines.delete(preferred)
|
259
|
+
units_needed -= 1
|
260
|
+
end
|
261
|
+
end
|
262
|
+
assigned_engines.concat(@available_engines.shift(units_needed)) if units_needed > 0
|
263
|
+
|
264
|
+
if assigned_engines.empty?
|
265
|
+
puts "🚒 ⚠️ No units available for rescue!"
|
266
|
+
logger.error("No units available for rescue at #{call.caller_location}")
|
267
|
+
return
|
268
|
+
end
|
269
|
+
|
270
|
+
@active_fires[incident_id] = {
|
271
|
+
type: 'rescue',
|
272
|
+
location: call.caller_location,
|
273
|
+
engines: assigned_engines,
|
274
|
+
start_time: Time.now,
|
275
|
+
severity: call.severity,
|
276
|
+
call: call
|
277
|
+
}
|
278
|
+
|
279
|
+
puts "🚒 Dispatched #{assigned_engines.join(', ')} to rescue operation"
|
280
|
+
puts " People trapped: #{call.number_of_victims || 'Unknown'}"
|
281
|
+
logger.info("Dispatched #{assigned_engines.join(', ')} to rescue #{incident_id}")
|
282
|
+
end
|
283
|
+
|
284
|
+
def handle_hazmat_call(call)
|
285
|
+
incident_id = "HAZ-#{Time.now.strftime('%H%M%S')}"
|
286
|
+
|
287
|
+
# Hazmat needs multiple units
|
288
|
+
units_needed = 3
|
289
|
+
units_needed = [units_needed, @available_engines.size].min
|
290
|
+
assigned_engines = @available_engines.shift(units_needed)
|
291
|
+
|
292
|
+
if assigned_engines.empty?
|
293
|
+
puts "🚒 ⚠️ No units available for hazmat response!"
|
294
|
+
logger.error("No units available for hazmat at #{call.caller_location}")
|
295
|
+
return
|
296
|
+
end
|
297
|
+
|
298
|
+
@active_fires[incident_id] = {
|
299
|
+
type: 'hazmat',
|
300
|
+
location: call.caller_location,
|
301
|
+
engines: assigned_engines,
|
302
|
+
start_time: Time.now,
|
303
|
+
severity: 'high',
|
304
|
+
call: call
|
305
|
+
}
|
306
|
+
|
307
|
+
puts "🚒 ⚠️ HAZMAT RESPONSE: #{assigned_engines.join(', ')} dispatched"
|
308
|
+
puts " Chemical hazard at #{call.caller_location}"
|
309
|
+
logger.warn("HAZMAT incident #{incident_id} - #{assigned_engines.join(', ')} dispatched")
|
310
|
+
end
|
311
|
+
|
312
|
+
def handle_major_accident(call)
|
313
|
+
incident_id = "MVA-#{Time.now.strftime('%H%M%S')}"
|
314
|
+
|
315
|
+
# Major accidents need rescue and engines
|
316
|
+
units_needed = call.vehicles_involved && call.vehicles_involved > 3 ? 3 : 2
|
317
|
+
units_needed = [units_needed, @available_engines.size].min
|
318
|
+
assigned_engines = @available_engines.shift(units_needed)
|
319
|
+
|
320
|
+
if assigned_engines.empty?
|
321
|
+
puts "🚒 ⚠️ No units available for accident response!"
|
322
|
+
logger.error("No units available for accident at #{call.caller_location}")
|
323
|
+
return
|
324
|
+
end
|
325
|
+
|
326
|
+
@active_fires[incident_id] = {
|
327
|
+
type: 'accident',
|
328
|
+
location: call.caller_location,
|
329
|
+
engines: assigned_engines,
|
330
|
+
start_time: Time.now,
|
331
|
+
severity: call.severity,
|
332
|
+
call: call
|
333
|
+
}
|
334
|
+
|
335
|
+
puts "🚒 Dispatched #{assigned_engines.join(', ')} to major accident"
|
336
|
+
puts " Vehicles: #{call.vehicles_involved || 'Multiple'}"
|
337
|
+
puts " Injuries: #{call.injuries_reported ? 'Yes' : 'Unknown'}"
|
338
|
+
logger.info("Dispatched #{assigned_engines.join(', ')} to accident #{incident_id}")
|
339
|
+
end
|
340
|
+
|
341
|
+
def handle_general_emergency(call)
|
342
|
+
incident_id = "EMG-#{Time.now.strftime('%H%M%S')}"
|
343
|
+
|
344
|
+
assigned_engines = @available_engines.shift(1)
|
345
|
+
|
346
|
+
if assigned_engines.empty?
|
347
|
+
puts "🚒 ⚠️ No units available!"
|
348
|
+
logger.error("No units available for emergency at #{call.caller_location}")
|
349
|
+
return
|
350
|
+
end
|
351
|
+
|
352
|
+
@active_fires[incident_id] = {
|
353
|
+
type: call.emergency_type,
|
354
|
+
location: call.caller_location,
|
355
|
+
engines: assigned_engines,
|
356
|
+
start_time: Time.now,
|
357
|
+
severity: call.severity,
|
358
|
+
call: call
|
359
|
+
}
|
360
|
+
|
361
|
+
puts "🚒 Dispatched #{assigned_engines.join(', ')} to #{call.emergency_type}"
|
362
|
+
logger.info("Dispatched #{assigned_engines.join(', ')} to #{call.emergency_type} #{incident_id}")
|
363
|
+
end
|
364
|
+
|
365
|
+
def check_fire_resolutions
|
366
|
+
@active_fires.each do |fire_id, fire|
|
367
|
+
# Fire resolution time depends on severity (10-15 seconds)
|
368
|
+
base_time = case fire[:severity]
|
369
|
+
when 'small' then 10..12
|
370
|
+
when 'medium' then 11..13
|
371
|
+
when 'large' then 12..14
|
372
|
+
when 'out_of_control' then 13..15
|
373
|
+
else 11..13
|
374
|
+
end
|
375
|
+
|
376
|
+
duration = (Time.now - fire[:start_time]).to_i
|
377
|
+
if duration > rand(base_time)
|
378
|
+
resolve_fire(fire_id, fire, duration)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def resolve_fire(fire_id, fire, duration_seconds)
|
384
|
+
# Return engines to available pool
|
385
|
+
@available_engines.concat(fire[:engines])
|
386
|
+
@active_fires.delete(fire_id)
|
387
|
+
|
388
|
+
outcomes = [
|
389
|
+
'Fire extinguished successfully',
|
390
|
+
'Fire contained with minimal damage',
|
391
|
+
'Structure saved, investigating cause',
|
392
|
+
'Total loss, area secured'
|
393
|
+
]
|
394
|
+
|
395
|
+
resolution = Messages::EmergencyResolvedMessage.new(
|
396
|
+
incident_id: fire_id,
|
397
|
+
incident_type: "#{fire[:fire_type]} fire",
|
398
|
+
location: fire[:location],
|
399
|
+
resolved_by: @service_name,
|
400
|
+
resolution_time: Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
401
|
+
duration_minutes: (duration_seconds / 60.0).round(1),
|
402
|
+
outcome: outcomes.sample,
|
403
|
+
units_involved: fire[:engines]
|
404
|
+
)
|
405
|
+
resolution.publish
|
406
|
+
|
407
|
+
puts "🚒 Fire #{fire_id} extinguished after #{(duration_seconds / 60.0).round(1)} minutes"
|
408
|
+
puts " Engines #{fire[:engines].join(', ')} returning to station"
|
409
|
+
logger.info("Fire #{fire_id} extinguished: #{outcomes.last} after #{(duration_seconds / 60.0).round(1)} minutes")
|
410
|
+
rescue => e
|
411
|
+
puts "🚒 Error resolving fire: #{e.message}"
|
412
|
+
logger.error("Error resolving fire: #{e.message}")
|
413
|
+
end
|
414
|
+
|
415
|
+
def determine_equipment_needed(fire_type, severity)
|
416
|
+
equipment = []
|
417
|
+
|
418
|
+
case fire_type
|
419
|
+
when 'kitchen'
|
420
|
+
equipment << 'Class K extinguishers'
|
421
|
+
when 'electrical'
|
422
|
+
equipment << 'Class C extinguishers'
|
423
|
+
equipment << 'Power shut-off tools'
|
424
|
+
when 'basement'
|
425
|
+
equipment << 'Ventilation fans'
|
426
|
+
equipment << 'Search and rescue gear'
|
427
|
+
when 'garage'
|
428
|
+
equipment << 'Foam suppressant'
|
429
|
+
when 'wildfire'
|
430
|
+
equipment << 'Brush trucks'
|
431
|
+
equipment << 'Water tenders'
|
432
|
+
end
|
433
|
+
|
434
|
+
if ['large', 'out_of_control'].include?(severity)
|
435
|
+
equipment << 'Aerial ladder'
|
436
|
+
equipment << 'Additional water supply'
|
437
|
+
end
|
438
|
+
|
439
|
+
equipment.join(', ')
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
if __FILE__ == $0
|
444
|
+
fire_dept = FireDepartment.new
|
445
|
+
fire_dept.start_service
|
446
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<svg width="700" height="500" viewBox="0 0 700 500" xmlns="http://www.w3.org/2000/svg">
|
3
|
+
<defs>
|
4
|
+
<style>
|
5
|
+
.bg { fill: transparent; }
|
6
|
+
.house { fill: #1a3a1a; stroke: #4aff4a; stroke-width: 2; rx: 8; }
|
7
|
+
.fire-dept { fill: #4a2a1a; stroke: #ffaa4a; stroke-width: 2; rx: 8; }
|
8
|
+
.all-services { fill: #2a2a3a; stroke: #4a9eff; stroke-width: 2; rx: 8; }
|
9
|
+
.message { stroke: #ff4a4a; stroke-width: 3; fill: none; marker-end: url(#arrowhead); }
|
10
|
+
.broadcast { stroke: #4aff4a; stroke-width: 2; fill: none; marker-end: url(#arrowhead-green); }
|
11
|
+
.text { fill: #ffffff; font-family: 'Arial', sans-serif; font-size: 12px; }
|
12
|
+
.title { fill: #4a9eff; font-family: 'Arial', sans-serif; font-size: 18px; font-weight: bold; }
|
13
|
+
.subtitle { fill: #ffaa4a; font-family: 'Arial', sans-serif; font-size: 11px; }
|
14
|
+
.note { fill: #aaaaaa; font-family: 'Arial', sans-serif; font-size: 10px; }
|
15
|
+
.process { fill: #3a2a1a; stroke: #ffaa4a; stroke-width: 1; rx: 4; }
|
16
|
+
</style>
|
17
|
+
<marker id="arrowhead" markerWidth="8" markerHeight="6"
|
18
|
+
refX="8" refY="3" orient="auto">
|
19
|
+
<polygon points="0 0, 8 3, 0 6" fill="#ff4a4a" />
|
20
|
+
</marker>
|
21
|
+
<marker id="arrowhead-green" markerWidth="8" markerHeight="6"
|
22
|
+
refX="8" refY="3" orient="auto">
|
23
|
+
<polygon points="0 0, 8 3, 0 6" fill="#4aff4a" />
|
24
|
+
</marker>
|
25
|
+
</defs>
|
26
|
+
|
27
|
+
<!-- Background -->
|
28
|
+
<rect class="bg" width="700" height="500"/>
|
29
|
+
|
30
|
+
<!-- Title -->
|
31
|
+
<text x="350" y="25" text-anchor="middle" class="title">Fire Emergency Flow</text>
|
32
|
+
<text x="350" y="45" text-anchor="middle" class="subtitle">House Fire Detection and Emergency Response</text>
|
33
|
+
|
34
|
+
<!-- Participants -->
|
35
|
+
<rect x="50" y="80" width="120" height="80" class="house"/>
|
36
|
+
<text x="110" y="105" text-anchor="middle" class="text">🏠 House</text>
|
37
|
+
<text x="110" y="125" text-anchor="middle" class="subtitle">Smoke Detection</text>
|
38
|
+
<text x="110" y="145" text-anchor="middle" class="subtitle">Safety System</text>
|
39
|
+
|
40
|
+
<rect x="290" y="80" width="120" height="80" class="fire-dept"/>
|
41
|
+
<text x="350" y="105" text-anchor="middle" class="text">🚒 Fire</text>
|
42
|
+
<text x="350" y="125" text-anchor="middle" class="text">Department</text>
|
43
|
+
<text x="350" y="145" text-anchor="middle" class="subtitle">Emergency Response</text>
|
44
|
+
|
45
|
+
<rect x="530" y="80" width="120" height="80" class="all-services"/>
|
46
|
+
<text x="590" y="105" text-anchor="middle" class="text">📢 All Services</text>
|
47
|
+
<text x="590" y="125" text-anchor="middle" class="subtitle">City Emergency</text>
|
48
|
+
<text x="590" y="145" text-anchor="middle" class="subtitle">Network</text>
|
49
|
+
|
50
|
+
<!-- Vertical lifelines -->
|
51
|
+
<line x1="110" y1="160" x2="110" y2="450" stroke="#666" stroke-width="2" stroke-dasharray="3,3"/>
|
52
|
+
<line x1="350" y1="160" x2="350" y2="450" stroke="#666" stroke-width="2" stroke-dasharray="3,3"/>
|
53
|
+
<line x1="590" y1="160" x2="590" y2="450" stroke="#666" stroke-width="2" stroke-dasharray="3,3"/>
|
54
|
+
|
55
|
+
<!-- Message Flow -->
|
56
|
+
<!-- 1. Fire emergency detected -->
|
57
|
+
<path d="M 110 200 L 350 200" class="message"/>
|
58
|
+
<text x="230" y="195" text-anchor="middle" class="text">FireEmergencyMessage</text>
|
59
|
+
<rect x="160" y="210" width="140" height="50" fill="#1a1a1a" stroke="#ff4a4a" rx="4"/>
|
60
|
+
<text x="230" y="225" text-anchor="middle" class="note">Smoke detected</text>
|
61
|
+
<text x="230" y="240" text-anchor="middle" class="note">Fire type: electrical/grease</text>
|
62
|
+
<text x="230" y="255" text-anchor="middle" class="note">Severity: critical/high/medium</text>
|
63
|
+
|
64
|
+
<!-- 2. Fire department assessment -->
|
65
|
+
<rect x="290" y="280" width="120" height="30" class="process"/>
|
66
|
+
<text x="350" y="300" text-anchor="middle" class="note">Assess fire severity</text>
|
67
|
+
|
68
|
+
<rect x="290" y="320" width="120" height="40" class="process"/>
|
69
|
+
<text x="350" y="340" text-anchor="middle" class="note">Dispatch equipment:</text>
|
70
|
+
<text x="350" y="355" text-anchor="middle" class="note">Engine-1, Ladder-1, Rescue-1</text>
|
71
|
+
|
72
|
+
<!-- 3. Dispatch broadcast -->
|
73
|
+
<path d="M 350 380 L 590 380" class="broadcast"/>
|
74
|
+
<text x="470" y="375" text-anchor="middle" class="text">FireDispatchMessage</text>
|
75
|
+
<rect x="420" y="390" width="120" height="40" fill="#2a3a2a" stroke="#4aff4a" rx="4"/>
|
76
|
+
<text x="480" y="405" text-anchor="middle" class="note">Equipment dispatched</text>
|
77
|
+
<text x="480" y="420" text-anchor="middle" class="note">ETA: 3-8 minutes</text>
|
78
|
+
|
79
|
+
<!-- 4. Resolution broadcast -->
|
80
|
+
<path d="M 350 440 L 590 440" class="broadcast"/>
|
81
|
+
<text x="470" y="435" text-anchor="middle" class="text">EmergencyResolvedMessage</text>
|
82
|
+
<text x="470" y="455" text-anchor="middle" class="note">Fire suppressed • Scene secure</text>
|
83
|
+
|
84
|
+
<!-- Process flow indicators -->
|
85
|
+
<circle cx="350" cy="320" r="3" fill="#ffaa4a"/>
|
86
|
+
<circle cx="350" cy="350" r="3" fill="#ffaa4a"/>
|
87
|
+
|
88
|
+
<!-- Equipment details -->
|
89
|
+
<rect x="50" y="380" width="200" height="80" fill="#1a1a1a" stroke="#ffaa4a" rx="6"/>
|
90
|
+
<text x="60" y="400" class="text">Fire Department Equipment:</text>
|
91
|
+
<text x="60" y="415" class="note">• Engine-1: Primary response unit</text>
|
92
|
+
<text x="60" y="430" class="note">• Ladder-1: Aerial rescue operations</text>
|
93
|
+
<text x="60" y="445" class="note">• Rescue-1: Emergency medical support</text>
|
94
|
+
<text x="60" y="460" class="note">• Hazmat-1: Hazardous materials unit</text>
|
95
|
+
</svg>
|
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# examples/multi_program_demo/health_department.rb
|
3
|
+
|
4
|
+
HEALTH_CHECK_RATE = 5 # seconds; setting to zero gets overruns and 1300+ msg/sec
|
5
|
+
|
6
|
+
require_relative '../../lib/smart_message'
|
7
|
+
require_relative 'messages/health_check_message'
|
8
|
+
require_relative 'messages/health_status_message'
|
9
|
+
|
10
|
+
require_relative 'common/logger'
|
11
|
+
|
12
|
+
class HealthDepartment
|
13
|
+
include Common::Logger
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@service_name = 'health-department'
|
17
|
+
Messages::HealthCheckMessage.from = @service_name
|
18
|
+
@check_id = 0
|
19
|
+
|
20
|
+
setup_messaging
|
21
|
+
setup_signal_handlers
|
22
|
+
end
|
23
|
+
|
24
|
+
def setup_logging
|
25
|
+
log_file = File.join(__dir__, 'health_department.log')
|
26
|
+
logger = Logger.new(log_file)
|
27
|
+
logger.level = Logger::INFO
|
28
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
29
|
+
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
30
|
+
end
|
31
|
+
logger.info("Health Department logging started")
|
32
|
+
end
|
33
|
+
|
34
|
+
def setup_messaging
|
35
|
+
# Subscribe to all broadcast health status messages
|
36
|
+
Messages::HealthStatusMessage.subscribe(broadcast: true, to: @service_name) do |message|
|
37
|
+
# Process health status message with colored output
|
38
|
+
status_color = case message.status
|
39
|
+
when 'healthy' then "\e[32m" # Green
|
40
|
+
when 'warning' then "\e[33m" # Yellow
|
41
|
+
when 'critical' then "\e[93m" # Orange
|
42
|
+
when 'failed' then "\e[31m" # Red
|
43
|
+
else "\e[0m" # Reset
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "#{status_color}#{message.service_name}: #{message.status.upcase}\e[0m #{message.details}"
|
47
|
+
logger.info("Received health status: #{message.service_name} = #{message.status} (#{message.details})")
|
48
|
+
end
|
49
|
+
|
50
|
+
puts "🏥 Health Department started"
|
51
|
+
puts " Monitoring city services health status..."
|
52
|
+
puts " Will send HealthCheck broadcasts every 5 seconds"
|
53
|
+
puts " Logging to: health_department.log"
|
54
|
+
puts " Press Ctrl+C to stop\n\n"
|
55
|
+
logger.info("Health Department started monitoring city services")
|
56
|
+
end
|
57
|
+
|
58
|
+
def setup_signal_handlers
|
59
|
+
%w[INT TERM].each do |signal|
|
60
|
+
Signal.trap(signal) do
|
61
|
+
puts "\n🏥 Health Department shutting down..."
|
62
|
+
logger.info("Health Department shutting down")
|
63
|
+
exit(0)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def start_monitoring
|
69
|
+
loop do
|
70
|
+
send_health_check
|
71
|
+
sleep(HEALTH_CHECK_RATE)
|
72
|
+
end
|
73
|
+
rescue => e
|
74
|
+
puts "🏥 Error in health monitoring: #{e.message}"
|
75
|
+
logger.error("Error in health monitoring: #{e.message}")
|
76
|
+
retry
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def send_health_check
|
82
|
+
@check_id += 1
|
83
|
+
|
84
|
+
health_check = Messages::HealthCheckMessage.new(check_id: @check_id)
|
85
|
+
|
86
|
+
puts
|
87
|
+
puts "🏥 Broadcasting health check ##{@check_id} (#{health_check.uuid[0..7]}...)"
|
88
|
+
logger.info("Broadcasting health check ##{@check_id}")
|
89
|
+
|
90
|
+
health_check.publish
|
91
|
+
rescue => e
|
92
|
+
puts "🏥 Error sending health check: #{e.message}"
|
93
|
+
logger.error("Error sending health check: #{e.message}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if __FILE__ == $0
|
98
|
+
health_dept = HealthDepartment.new
|
99
|
+
health_dept.start_monitoring
|
100
|
+
end
|