Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
src/Functions/TimingUtils.st \
src/FunctionBlocks/TrafficLight.st \
src/FunctionBlocks/PedestrianLight.st \
src/FunctionBlocks/TurnSignalLight.st \
src/FunctionBlocks/VehicleDetector.st \
src/FunctionBlocks/TrafficLogger.st \
src/Programs/IntersectionController.st \
Expand Down Expand Up @@ -92,3 +93,12 @@ jobs:
src/Interfaces/IControllable.st \
src/FunctionBlocks/TrafficLight.st \
--test tests/test_interface.st

- name: Run turn signal tests
run: |
strucpp \
src/DataTypes/TrafficTypes.st \
src/Interfaces/IControllable.st \
src/FunctionBlocks/TrafficLight.st \
src/FunctionBlocks/TurnSignalLight.st \
--test tests/test_turn_signal.st
60 changes: 60 additions & 0 deletions src/FunctionBlocks/TurnSignalLight.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
(* TurnSignalLight Function Block
Extends TrafficLight with a dedicated turn signal arrow.
The turn arrow activates during the RED-to-GREEN transition. *)

FUNCTION_BLOCK TurnSignalLight EXTENDS TrafficLight
VAR_INPUT
turnEnabled : BOOL := TRUE;
turnDuration : TIME := T#10s;
END_VAR

VAR_OUTPUT
turnArrowOn : BOOL := FALSE;
turnDirection : INT := 0; (* 0=none, 1=left, 2=right *)
END_VAR

VAR
turnTimer : TON;
turnActive : BOOL := FALSE;
prevState : TrafficState := TrafficState.RED;
END_VAR

METHOD PUBLIC SetLeftTurn : BOOL
turnDirection := 1;
SetLeftTurn := TRUE;
END_METHOD

METHOD PUBLIC SetRightTurn : BOOL
turnDirection := 2;
SetRightTurn := TRUE;
END_METHOD

METHOD PUBLIC IsTurnActive : BOOL
IsTurnActive := turnArrowOn;
END_METHOD

(* Run parent TrafficLight state machine first *)
SUPER^();

(* Detect RED-to-GREEN transition to activate turn signal *)
IF prevState = TrafficState.RED AND currentState = TrafficState.GREEN THEN
IF turnEnabled AND turnDirection > 0 THEN
turnActive := TRUE;
END_IF;
END_IF;

(* Run turn signal timer *)
turnTimer(IN := turnActive, PT := turnDuration);
IF turnTimer.Q THEN
turnActive := FALSE;
turnTimer(IN := FALSE, PT := turnDuration);
END_IF;

(* Turn off arrow when not in GREEN phase *)
IF currentState <> TrafficState.GREEN THEN
turnActive := FALSE;
END_IF;

turnArrowOn := turnActive;
prevState := currentState;
END_FUNCTION_BLOCK
86 changes: 86 additions & 0 deletions tests/test_turn_signal.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
(* Tests for the TurnSignalLight function block *)

TEST 'TurnSignalLight starts with arrow off'
VAR ts : TurnSignalLight; END_VAR

ts();
ASSERT_FALSE(ts.turnArrowOn, 'Arrow should be off initially');
ASSERT_EQ(ts.turnDirection, 0, 'No direction set');
END_TEST

TEST 'TurnSignalLight SetLeftTurn sets direction'
VAR ts : TurnSignalLight; END_VAR

ts.SetLeftTurn();
ASSERT_EQ(ts.turnDirection, 1, 'Should be left turn');
END_TEST

TEST 'TurnSignalLight SetRightTurn sets direction'
VAR ts : TurnSignalLight; END_VAR

ts.SetRightTurn();
ASSERT_EQ(ts.turnDirection, 2, 'Should be right turn');
END_TEST

TEST 'TurnSignalLight activates on RED to GREEN transition'
VAR
ts : TurnSignalLight;
timing : PhaseTiming;
END_VAR

timing.redDuration := T#100ms;
timing.greenDuration := T#200ms;
timing.yellowDuration := T#50ms;
ts.timing := timing;
ts.turnDuration := T#80ms;
ts.SetLeftTurn();

(* Start in RED *)
ts();
ASSERT_EQ(ts.currentState, TrafficState.RED, 'Start in RED');
ASSERT_FALSE(ts.turnArrowOn, 'Arrow off during RED');

(* Transition to GREEN *)
ADVANCE_TIME(T#50ms);
ts();
ADVANCE_TIME(T#50ms);
ts();
ASSERT_EQ(ts.currentState, TrafficState.GREEN, 'Now GREEN');
ASSERT_TRUE(ts.turnArrowOn, 'Arrow on during GREEN after RED');
END_TEST

TEST 'TurnSignalLight arrow turns off after duration'
VAR
ts : TurnSignalLight;
timing : PhaseTiming;
END_VAR

timing.redDuration := T#100ms;
timing.greenDuration := T#300ms;
timing.yellowDuration := T#50ms;
ts.timing := timing;
ts.turnDuration := T#80ms;
ts.SetLeftTurn();

(* Get to GREEN *)
ts();
ADVANCE_TIME(T#50ms);
ts();
ADVANCE_TIME(T#50ms);
ts();
ASSERT_TRUE(ts.turnArrowOn, 'Arrow should be on');

(* Wait for turn duration to expire *)
ADVANCE_TIME(T#40ms);
ts();
ADVANCE_TIME(T#40ms);
ts();
ASSERT_FALSE(ts.turnArrowOn, 'Arrow should be off after duration');
END_TEST

TEST 'TurnSignalLight IsTurnActive method'
VAR ts : TurnSignalLight; END_VAR

ts();
ASSERT_FALSE(ts.IsTurnActive(), 'Not active initially');
END_TEST