• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Scala-Robotics-Simulator / PPS-22-srs / #419

28 Aug 2025 10:57AM UTC coverage: 46.919% (+0.9%) from 46.066%
#419

push

github

srs-mate
feat(robot): validate sensors during validation

14 of 15 new or added lines in 5 files covered. (93.33%)

70 existing lines in 6 files now uncovered.

1241 of 2645 relevant lines covered (46.92%)

6.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.4
/src/main/scala/io/github/srs/model/entity/dynamicentity/sensor/Sensor.scala
1
package io.github.srs.model.entity.dynamicentity.sensor
2

3
import cats.Monad
4
import cats.syntax.all.*
5
import io.github.srs.model.entity.dynamicentity.{ DynamicEntity, Robot }
6
import io.github.srs.model.entity.{ Orientation, Point2D, ShapeType }
7
import io.github.srs.model.environment.Environment
8
import io.github.srs.model.illumination.model.ScaleFactor
9
import io.github.srs.utils.Ray.intersectRay
10
import io.github.srs.utils.SimulationDefaults.DynamicEntity.Sensor.ProximitySensor as ProximitySensorDefaults
11

12
/**
13
 * Represents the range of a sensor.
14
 */
15
type Range = Double
16

×
17
/**
18
 * Represents a sensor that can sense the environment for a dynamic entity.
19
 *
20
 * @tparam Entity
21
 *   the type of dynamic entity that the sensor can act upon.
22
 * @tparam Env
23
 *   the type of environment in which the sensor operates.
24
 */
25
trait Sensor[-Entity <: DynamicEntity, -Env <: Environment]:
26
  /**
8✔
27
   * The type of data that the sensor returns. This type can vary based on the specific sensor implementation.
28
   */
29
  type Data
30

31
  /**
32
   * The offset orientation of the sensor relative to the entity's orientation.
33
   *
34
   * @return
35
   *   the orientation offset of the sensor.
36
   */
37
  def offset: Orientation
38

39
  /**
40
   * Senses the environment for the given entity and returns the data collected by the sensor.
41
   *
42
   * @param entity
43
   *   the dynamic entity that the sensor is attached to.
44
   * @param env
45
   *   the environment in which the sensor operates.
46
   * @tparam F
47
   *   the effect type in which the sensing operation is performed.
48
   * @return
49
   *   a monadic effect containing the data sensed by the sensor.
50
   */
51
  def sense[F[_]: Monad](entity: Entity, env: Env): F[Data]
52

53
  private[sensor] def direction(entity: Entity): Point2D =
54
    val globalOrientation = entity.orientation.toRadians + offset.toRadians
4✔
55
    (math.cos(globalOrientation), -math.sin(globalOrientation))
24✔
56

20✔
57
  private[sensor] def origin(entity: Entity): Point2D =
58
    import Point2D.*
4✔
59
    val distance = entity.shape match
60
      case ShapeType.Circle(radius) => radius
7✔
61
      case ShapeType.Rectangle(width, height) => math.min(width, height)
19✔
62
    val origin = entity.position + direction(entity) * distance
1✔
63
    origin
33✔
64
end Sensor
2✔
65

66
/**
67
 * Represents a reading from a sensor. This case class encapsulates the sensor and the value it has sensed.
68
 *
69
 * @param sensor
70
 *   the sensor that has taken the reading.
71
 * @param value
72
 *   the value sensed by the sensor.
73
 * @tparam S
74
 *   the type of sensor, which is a subtype of [[Sensor]].
75
 * @tparam A
76
 *   the type of data sensed by the sensor.
77
 */
78
final case class SensorReading[S <: Sensor[?, ?], A](sensor: S, value: A)
79

60✔
80
/**
81
 * A collection of sensor readings. This type is used to represent multiple sensor readings from a dynamic entity. It is
82
 * a vector of [[SensorReading]] instances, allowing for efficient access and manipulation of sensor data.
83
 */
84
type SensorReadings = Vector[SensorReading[? <: Sensor[?, ?], ?]]
85

86
object SensorReadings:
87

×
88
  extension (readings: SensorReadings)
89

×
90
    def prettyPrint: Vector[String] =
91
      readings
×
92
        .map(r =>
×
93
          r.sensor match
×
94
            case ProximitySensor(offset, range) =>
95
              s"Proximity (offset: ${offset.degrees}°, range: $range m) -> ${r.value}"
96
            case LightSensor(offset) => s"Light (offset: ${offset.degrees}°) -> ${r.value}"
97
            case other => s"${other.getClass.getSimpleName} -> ${r.value}",
98
        )
99

×
100
/**
101
 * A proximity sensor that can sense the distance to other entities in the environment. It calculates the distance to
102
 * the nearest entity within its range and returns a normalized value. The value is normalized to a range between 0.0
103
 * (closest) and 1.0 (farthest).
104
 *
105
 * @param offset
106
 *   the offset orientation of the sensor relative to the entity's orientation.
107
 * @param range
108
 *   the range of the sensor, which defines how far it can sense.
109
 * @tparam Entity
110
 *   the type of dynamic entity that the sensor can act upon.
111
 * @tparam Env
112
 *   the type of environment in which the sensor operates.
113
 */
114
final case class ProximitySensor[Entity <: DynamicEntity, Env <: Environment](
115
    override val offset: Orientation = Orientation(ProximitySensorDefaults.DefaultOffset),
78✔
116
    range: Range = ProximitySensorDefaults.DefaultRange,
21✔
117
) extends Sensor[Entity, Env]:
13✔
118

119
  override type Data = Double
120

121
  private def rayEnd(entity: Entity): Point2D =
122
    import Point2D.*
4✔
123
    origin(entity) + direction(entity) * range
124

35✔
125
  override def sense[F[_]: Monad](entity: Entity, env: Env): F[Data] =
126
    Monad[F].pure:
4✔
127
      val o = origin(entity)
10✔
128
      val end = rayEnd(entity)
12✔
129

8✔
130
      val distances = env.entities
131
        .filter(!_.equals(entity))
5✔
132
        .flatMap(intersectRay(_, o, end))
7✔
133
        .filter(_ <= range)
8✔
134

8✔
135
      distances.minOption.map(_ / range).getOrElse(1.0)
136

29✔
137
end ProximitySensor
138

139
/**
140
 * A light sensor that senses the light intensity in the environment.
141
 *
142
 * @param offset
143
 *   the offset orientation of the sensor relative to the entity's orientation.
144
 * @tparam Entity
145
 *   the type of dynamic entity that the sensor can act upon.
146
 * @tparam Env
147
 *   the type of environment in which the sensor operates.
148
 */
149
final case class LightSensor[Entity <: DynamicEntity, Env <: Environment](
150
    offset: Orientation = Orientation(ProximitySensorDefaults.DefaultOffset),
58✔
151
) extends Sensor[Entity, Env]:
6✔
152

153
  override type Data = Double
154

155
  /**
156
   * Senses the light intensity at the position of the sensor in the environment. It uses a cached light map to compute
157
   * the field of light and samples it at the sensor's position.
158
   *
159
   * @param entity
160
   *   the dynamic entity that the sensor is attached to.
161
   * @param env
162
   *   the environment in which the sensor operates.
163
   * @return
164
   *   a monadic effect containing the light intensity sensed by the sensor.
165
   */
166
  override def sense[F[_]: Monad](entity: Entity, env: Env): F[Data] =
167
    Monad[F].pure:
4✔
168
      val o = origin(entity)
10✔
169
      env.lightField.illuminationAt(o)(using ScaleFactor.default)
12✔
170

20✔
171
end LightSensor
172

173
object Sensor:
UNCOV
174

×
175
  extension (r: Robot)
176

5✔
177
    /**
178
     * Senses all sensors of the robot in the given environment.
179
     *
180
     * @param env
181
     *   the environment in which to sense.
182
     * @return
183
     *   a vector of sensor readings.
184
     */
185
    def senseAll[F[_]: Monad](env: Environment): F[SensorReadings] =
186
      r.sensors.traverse: sensor =>
4✔
187
        sensor.sense(r, env).map(reading => SensorReading(sensor, reading))
17✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc