| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ExitGateController.h" | ||
| 2 | #include "esp_log.h" | ||
| 3 | |||
| 4 | static const char* TAG = "ExitGateController"; | ||
| 5 | static const char* exitGateStateToString(ExitGateState state); | ||
| 6 | |||
| 7 | 6 | ExitGateController::ExitGateController( | |
| 8 | IEventBus& eventBus, | ||
| 9 | IGate& gate, | ||
| 10 | ITicketService& ticketService, | ||
| 11 | uint32_t barrierTimeoutMs, | ||
| 12 | 6 | uint32_t validationTimeMs) | |
| 13 | 6 | : m_eventBus(eventBus) | |
| 14 | 6 | , m_gate(&gate) | |
| 15 | 6 | , m_ticketService(ticketService) | |
| 16 | 6 | , m_state(ExitGateState::Idle) | |
| 17 | 6 | , m_barrierTimeoutMs(barrierTimeoutMs) | |
| 18 | 6 | , m_validationTimeMs(validationTimeMs) | |
| 19 | 6 | , m_currentTicketId(0) | |
| 20 | 6 | , m_barrierTimer(nullptr) | |
| 21 | 6 | , m_validationTimer(nullptr) { | |
| 22 | // Subscribe to events | ||
| 23 |
1/2✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
|
6 | m_eventBus.subscribe(EventType::ExitLightBarrierBlocked, |
| 24 | 8 | [this](const Event& e) { onLightBarrierBlocked(e); }); | |
| 25 |
1/2✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
|
6 | m_eventBus.subscribe(EventType::ExitLightBarrierCleared, |
| 26 | 7 | [this](const Event& e) { onLightBarrierCleared(e); }); | |
| 27 | |||
| 28 | // Create timers | ||
| 29 | 12 | m_barrierTimer = xTimerCreate( | |
| 30 | "ExitBarrierTimer", | ||
| 31 | 6 | pdMS_TO_TICKS(m_barrierTimeoutMs), | |
| 32 | pdFALSE, | ||
| 33 | this, | ||
| 34 | barrierTimerCallback); | ||
| 35 | |||
| 36 | 12 | m_validationTimer = xTimerCreate( | |
| 37 | "ExitValidationTimer", | ||
| 38 | 6 | pdMS_TO_TICKS(m_validationTimeMs), | |
| 39 | pdFALSE, | ||
| 40 | this, | ||
| 41 | validationTimerCallback); | ||
| 42 | |||
| 43 | 6 | ESP_LOGI(TAG, "ExitGateController initialized"); | |
| 44 | 6 | } | |
| 45 | |||
| 46 | 6 | ExitGateController::~ExitGateController() { | |
| 47 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | if (m_barrierTimer) { |
| 48 | 6 | xTimerDelete(m_barrierTimer, 0); | |
| 49 | } | ||
| 50 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | if (m_validationTimer) { |
| 51 | 6 | xTimerDelete(m_validationTimer, 0); | |
| 52 | } | ||
| 53 | 6 | } | |
| 54 | |||
| 55 | ✗ | void ExitGateController::setupGpioInterrupts() { | |
| 56 | ✗ | ESP_LOGI(TAG, "Exit gate GPIO interrupts configured"); | |
| 57 | ✗ | } | |
| 58 | |||
| 59 | ✗ | void ExitGateController::reset() { | |
| 60 | // Stop timers if running | ||
| 61 | ✗ | if (m_barrierTimer && xTimerIsTimerActive(m_barrierTimer)) { | |
| 62 | ✗ | xTimerStop(m_barrierTimer, 0); | |
| 63 | } | ||
| 64 | ✗ | if (m_validationTimer && xTimerIsTimerActive(m_validationTimer)) { | |
| 65 | ✗ | xTimerStop(m_validationTimer, 0); | |
| 66 | } | ||
| 67 | |||
| 68 | // Reset state | ||
| 69 | ✗ | m_state = ExitGateState::Idle; | |
| 70 | ✗ | m_currentTicketId = 0; | |
| 71 | |||
| 72 | // Ensure barrier is closed | ||
| 73 | ✗ | m_gate->close(); | |
| 74 | |||
| 75 | ✗ | ESP_LOGI(TAG, "ExitGateController reset to Idle"); | |
| 76 | ✗ | } | |
| 77 | |||
| 78 | 16 | const char* ExitGateController::getStateString() const { | |
| 79 |
7/8✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
16 | switch (m_state) { |
| 80 | 4 | case ExitGateState::Idle: | |
| 81 | 4 | return "Idle"; | |
| 82 | 4 | case ExitGateState::ValidatingTicket: | |
| 83 | 4 | return "ValidatingTicket"; | |
| 84 | 2 | case ExitGateState::OpeningBarrier: | |
| 85 | 2 | return "OpeningBarrier"; | |
| 86 | 1 | case ExitGateState::WaitingForCarToPass: | |
| 87 | 1 | return "WaitingForCarToPass"; | |
| 88 | 1 | case ExitGateState::CarPassing: | |
| 89 | 1 | return "CarPassing"; | |
| 90 | 2 | case ExitGateState::WaitingBeforeClose: | |
| 91 | 2 | return "WaitingBeforeClose"; | |
| 92 | 2 | case ExitGateState::ClosingBarrier: | |
| 93 | 2 | return "ClosingBarrier"; | |
| 94 | ✗ | default: | |
| 95 | ✗ | return "Unknown"; | |
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | 13 | void ExitGateController::setState(ExitGateState newState) { | |
| 100 |
1/2✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
|
13 | if (m_state != newState) { |
| 101 | 13 | ESP_LOGI(TAG, "State: %s -> %s", getStateString(), exitGateStateToString(newState)); | |
| 102 | 13 | m_state = newState; | |
| 103 | } | ||
| 104 | 13 | } | |
| 105 | |||
| 106 | 2 | void ExitGateController::onLightBarrierBlocked(const Event& event) { | |
| 107 | (void) event; | ||
| 108 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
2 | if (m_state == ExitGateState::WaitingForCarToPass) { |
| 109 | 1 | ESP_LOGI(TAG, "Car entering exit barrier"); | |
| 110 | 1 | setState(ExitGateState::CarPassing); | |
| 111 | } | ||
| 112 | // Note: Idle state does not react to light barrier | ||
| 113 | // Exit sequence must be started manually via ticket_validate command | ||
| 114 | 2 | } | |
| 115 | |||
| 116 | 1 | void ExitGateController::onLightBarrierCleared(const Event& event) { | |
| 117 | (void) event; | ||
| 118 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (m_state == ExitGateState::CarPassing) { |
| 119 | 1 | ESP_LOGI(TAG, "Car exited parking, waiting %u ms before closing barrier", (unsigned) m_barrierTimeoutMs); | |
| 120 |
1/2✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
|
1 | m_eventBus.publish(Event(EventType::CarExitedParking, 0, m_currentTicketId)); |
| 121 | |||
| 122 | // Wait before closing barrier (uses configured timeout) | ||
| 123 | 1 | setState(ExitGateState::WaitingBeforeClose); | |
| 124 | |||
| 125 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (m_barrierTimer) { |
| 126 | 1 | xTimerChangePeriod(m_barrierTimer, pdMS_TO_TICKS(m_barrierTimeoutMs), 0); | |
| 127 | 1 | xTimerReset(m_barrierTimer, 0); | |
| 128 | } | ||
| 129 | } | ||
| 130 | 1 | } | |
| 131 | |||
| 132 | ✗ | void ExitGateController::onValidationTimeout() { | |
| 133 | // This function is not used in manual validation mode | ||
| 134 | // Validation must be triggered manually via validateTicketManually() | ||
| 135 | ✗ | ESP_LOGW(TAG, "Validation timeout - should not happen in manual mode"); | |
| 136 | ✗ | } | |
| 137 | |||
| 138 | 4 | bool ExitGateController::validateTicketManually(uint32_t ticketId) { | |
| 139 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (m_state != ExitGateState::Idle) { |
| 140 | ✗ | ESP_LOGW(TAG, "Cannot validate manually - must be in Idle state (current: %s)", getStateString()); | |
| 141 | ✗ | return false; | |
| 142 | } | ||
| 143 | |||
| 144 |
1/2✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
|
4 | ESP_LOGI(TAG, "Starting manual ticket validation for ID=%lu", (unsigned long) ticketId); |
| 145 |
1/2✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
|
4 | setState(ExitGateState::ValidatingTicket); |
| 146 | 4 | m_currentTicketId = ticketId; | |
| 147 | |||
| 148 | // Check if ticket is paid | ||
| 149 | 4 | Ticket ticket; | |
| 150 |
3/4✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 1 times.
|
4 | if (m_ticketService.getTicketInfo(ticketId, ticket)) { |
| 151 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
|
3 | if (!ticket.isPaid) { |
| 152 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | ESP_LOGW(TAG, "Ticket not paid: ID=%lu - use 'ticket_pay %lu' command first!", |
| 153 | (unsigned long) ticketId, (unsigned long) ticketId); | ||
| 154 |
1/2✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
|
1 | m_eventBus.publish(Event(EventType::TicketRejected)); |
| 155 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | setState(ExitGateState::Idle); |
| 156 | 1 | return false; | |
| 157 | } | ||
| 158 | |||
| 159 |
2/4✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
|
2 | if (m_ticketService.validateAndUseTicket(ticketId)) { |
| 160 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | ESP_LOGI(TAG, "Ticket validation successful: ID=%lu", (unsigned long) ticketId); |
| 161 |
1/2✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
|
2 | m_eventBus.publish(Event(EventType::TicketValidated, 0, ticketId)); |
| 162 | |||
| 163 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | setState(ExitGateState::OpeningBarrier); |
| 164 |
1/2✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
|
2 | m_gate->open(); |
| 165 |
1/2✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
|
2 | m_eventBus.publish(Event(EventType::ExitBarrierOpened)); |
| 166 | 2 | startBarrierTimer(); | |
| 167 | 2 | return true; | |
| 168 | } | ||
| 169 | } | ||
| 170 | |||
| 171 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | ESP_LOGW(TAG, "Ticket validation failed: ID=%lu", (unsigned long) ticketId); |
| 172 |
1/2✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
|
1 | m_eventBus.publish(Event(EventType::TicketRejected)); |
| 173 |
1/2✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
|
1 | setState(ExitGateState::Idle); |
| 174 | 1 | return false; | |
| 175 | } | ||
| 176 | |||
| 177 | 3 | void ExitGateController::onBarrierTimeout() { | |
| 178 | 3 | ESP_LOGD(TAG, "Barrier timeout in state: %s", getStateString()); | |
| 179 | |||
| 180 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
|
3 | if (m_state == ExitGateState::OpeningBarrier) { |
| 181 | 1 | setState(ExitGateState::WaitingForCarToPass); | |
| 182 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
2 | } else if (m_state == ExitGateState::WaitingBeforeClose) { |
| 183 | // Wait period finished, now close barrier | ||
| 184 | 1 | ESP_LOGI(TAG, "Wait period finished, closing barrier"); | |
| 185 | 1 | setState(ExitGateState::ClosingBarrier); | |
| 186 | 1 | m_gate->close(); | |
| 187 |
1/2✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
|
1 | m_eventBus.publish(Event(EventType::ExitBarrierClosed)); |
| 188 | |||
| 189 | // Start timer with normal barrier timeout | ||
| 190 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (m_barrierTimer) { |
| 191 | 1 | xTimerChangePeriod(m_barrierTimer, pdMS_TO_TICKS(m_barrierTimeoutMs), 0); | |
| 192 | 1 | xTimerReset(m_barrierTimer, 0); | |
| 193 | } | ||
| 194 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | } else if (m_state == ExitGateState::ClosingBarrier) { |
| 195 | 1 | setState(ExitGateState::Idle); | |
| 196 | 1 | m_currentTicketId = 0; | |
| 197 | } | ||
| 198 | 3 | } | |
| 199 | |||
| 200 | 2 | void ExitGateController::startBarrierTimer() { | |
| 201 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | if (m_barrierTimer) { |
| 202 | 2 | xTimerReset(m_barrierTimer, 0); | |
| 203 | } | ||
| 204 | 2 | } | |
| 205 | |||
| 206 | ✗ | void ExitGateController::stopBarrierTimer() { | |
| 207 | ✗ | if (m_barrierTimer) { | |
| 208 | ✗ | xTimerStop(m_barrierTimer, 0); | |
| 209 | } | ||
| 210 | ✗ | } | |
| 211 | |||
| 212 | ✗ | void ExitGateController::startValidationTimer() { | |
| 213 | ✗ | if (m_validationTimer) { | |
| 214 | ✗ | xTimerReset(m_validationTimer, 0); | |
| 215 | } | ||
| 216 | ✗ | } | |
| 217 | |||
| 218 | ✗ | void ExitGateController::stopValidationTimer() { | |
| 219 | ✗ | if (m_validationTimer) { | |
| 220 | ✗ | xTimerStop(m_validationTimer, 0); | |
| 221 | } | ||
| 222 | ✗ | } | |
| 223 | |||
| 224 | ✗ | void ExitGateController::barrierTimerCallback(TimerHandle_t xTimer) { | |
| 225 | ✗ | auto* controller = static_cast<ExitGateController*>(pvTimerGetTimerID(xTimer)); | |
| 226 | ✗ | if (controller) { | |
| 227 | ✗ | controller->onBarrierTimeout(); | |
| 228 | } | ||
| 229 | ✗ | } | |
| 230 | |||
| 231 | ✗ | void ExitGateController::validationTimerCallback(TimerHandle_t xTimer) { | |
| 232 | ✗ | auto* controller = static_cast<ExitGateController*>(pvTimerGetTimerID(xTimer)); | |
| 233 | ✗ | if (controller) { | |
| 234 | ✗ | controller->onValidationTimeout(); | |
| 235 | } | ||
| 236 | ✗ | } | |
| 237 | |||
| 238 | // Helper function to convert state to string (avoids stack-heavy lambda) | ||
| 239 | 13 | static const char* exitGateStateToString(ExitGateState state) { | |
| 240 |
7/8✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
|
13 | switch (state) { |
| 241 | 3 | case ExitGateState::Idle: | |
| 242 | 3 | return "Idle"; | |
| 243 | 4 | case ExitGateState::ValidatingTicket: | |
| 244 | 4 | return "ValidatingTicket"; | |
| 245 | 2 | case ExitGateState::OpeningBarrier: | |
| 246 | 2 | return "OpeningBarrier"; | |
| 247 | 1 | case ExitGateState::WaitingForCarToPass: | |
| 248 | 1 | return "WaitingForCarToPass"; | |
| 249 | 1 | case ExitGateState::CarPassing: | |
| 250 | 1 | return "CarPassing"; | |
| 251 | 1 | case ExitGateState::WaitingBeforeClose: | |
| 252 | 1 | return "WaitingBeforeClose"; | |
| 253 | 1 | case ExitGateState::ClosingBarrier: | |
| 254 | 1 | return "ClosingBarrier"; | |
| 255 | ✗ | default: | |
| 256 | ✗ | return "Unknown"; | |
| 257 | } | ||
| 258 | } | ||
| 259 |