+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hardware/src/main/kotlin/dev/nextftc/hardware/actuators/NextFeedbackCRServo.kt b/hardware/src/main/kotlin/dev/nextftc/hardware/actuators/NextFeedbackCRServo.kt
index d60f1ff..0e60a92 100644
--- a/hardware/src/main/kotlin/dev/nextftc/hardware/actuators/NextFeedbackCRServo.kt
+++ b/hardware/src/main/kotlin/dev/nextftc/hardware/actuators/NextFeedbackCRServo.kt
@@ -71,7 +71,13 @@ class NextFeedbackCRServo(
feedbackName: String,
cacheTolerance: Double = 0.01,
) : this(
- { CRServoImplEx(LynxServoController(RobotController.appContext, module), port, ServoConfigurationType.getStandardServoType()) },
+ {
+ CRServoImplEx(
+ LynxServoController(RobotController.appContext, module),
+ port,
+ ServoConfigurationType.getStandardServoType(),
+ )
+ },
feedbackName,
cacheTolerance,
)
diff --git a/hardware/src/main/kotlin/dev/nextftc/hardware/sensors/NextDigitalSensor.kt b/hardware/src/main/kotlin/dev/nextftc/hardware/sensors/NextDigitalSensor.kt
new file mode 100644
index 0000000..cc54591
--- /dev/null
+++ b/hardware/src/main/kotlin/dev/nextftc/hardware/sensors/NextDigitalSensor.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2026 NextFTC Team
+ *
+ * Use of this source code is governed by an BSD-3-clause
+ * license that can be found in the LICENSE.md file at the root of this repository or at
+ * https://opensource.org/license/bsd-3-clause.
+ */
+
+package dev.nextftc.hardware.sensors
+
+import com.qualcomm.robotcore.hardware.DigitalChannel
+import dev.nextftc.hardware.LazyHardware
+import dev.nextftc.hardware.RobotController
+
+/**
+ * Lightweight wrapper around a [DigitalChannel] for reading digital sensors
+ * like limit switches, magnetic switches, and beam breaks.
+ *
+ * Most digital sensors are "active low" — they read `false` when triggered
+ * (switch pressed, magnet present, beam broken) and `true` when idle. This
+ * wrapper handles that inversion via [inverted] so [isTriggered] always
+ * means what you'd expect.
+ *
+ * Example:
+ * ```
+ * val beamBreak = NextDigitalSensor("beamBreak")
+ * if (beamBreak.isTriggered) { stopMotor() }
+ * ```
+ *
+ * @param initializer Lazily resolves the backing [DigitalChannel].
+ * @param inverted If true, [isTriggered] returns the opposite of the raw
+ * sensor state — i.e. triggered when the channel reads low. For example, a
+ * touch sensor reads `false` while it's being pressed, so inverting makes
+ * [isTriggered] read `true` when pressed, which is what you'd expect.
+ * Defaults to true, matching most FTC digital sensors, which are active-low.
+ *
+ * @author 28shettr
+ */
+
+class NextDigitalSensor(initializer: () -> DigitalChannel, private val inverted: Boolean = true) {
+ /**
+ * @param name Hardware map name to resolve the [DigitalChannel] from.
+ * @param inverted If true, [isTriggered] is the opposite of the raw state. Defaults to true.
+ */
+ @JvmOverloads
+ constructor(name: String, inverted: Boolean = true) : this(
+ {
+ RobotController.hardwareMap[name] as DigitalChannel
+ },
+ inverted,
+ )
+
+ private val sensor by LazyHardware(initializer).also {
+ it.applyAfterInit { channel -> channel.mode = DigitalChannel.Mode.INPUT }
+ }
+
+ /** Raw state of the digital channel */
+ val rawState: Boolean
+ get() = sensor.state
+
+ /** True if the sensor is currently triggered (accounting for [inverted]). */
+ val isTriggered: Boolean
+ get() = if (inverted) {
+ !sensor.state
+ } else {
+ sensor.state
+ }
+
+ /** Returns a string of the sensor's current state for telemetry or logging. */
+ fun debug(): String = "Sensor State: $isTriggered, Raw State: $rawState, Inverted: $inverted"
+}