Skip to content
13 changes: 13 additions & 0 deletions src/main/java/frc/lib/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package frc.lib;

import org.wpilib.driverstation.Alliance;
import org.wpilib.util.Alert;
import org.wpilib.util.Color;

public interface Util {
Expand All @@ -31,4 +32,16 @@ public static Color allianceToColor(Alliance alliance) {
public static boolean nearlyEqual(double a, double b) {
return Math.abs(a - b) < Math.ulp(1);
}

/**
* Activates or clears an alert, updating its text only while active and only when it changed.
*
* @param alert the alert to update
* @param active whether the alert should be active
* @param text the alert text to show while active
*/
public static void setAlert(Alert alert, boolean active, String text) {
if (active && !text.equals(alert.getText())) alert.setText(text);
alert.set(active);
}
}
125 changes: 125 additions & 0 deletions src/main/java/frc/lib/hardware/CANBusHealth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) 2026 Triple Helix Robotics, FRC Team 2363
// https://github.com/TripleHelixProgramming
//
// Use of this source code is governed by a BSD
// license that can be found in the LICENSE file
// at the root directory of this project.

package frc.lib.hardware;

/**
* Decides whether a CAN bus needs an alert, from one cycle of logged bus status at a time.
*
* <p>The states follow the CAN standard's fault confinement: a controller enters ErrorWarning when
* an error counter reaches 96, ErrorPassive at 128, and BusOff when the transmit counter passes
* 255. A few error frames on a healthy bus raise the counters without reaching those states, so the
* raw counters are logged but do not trigger alerts.
*
* <p>This class uses only strings, numbers and the caller's timestamps, so it gives the same result
* in log replay and can be unit tested without Phoenix or the HAL.
*/
public class CANBusHealth {
/** How urgent a bus problem is. */
public enum Severity {
NONE,
MEDIUM,
HIGH
}

/** How long an alert stays active after the condition was last seen, in seconds. */
public static final double HOLD_SECONDS = 0.5;

/** How long the sample count may stay unchanged before the reader counts as stalled. */
public static final double STALE_SECONDS = 1.5;

private boolean hasPrevious = false;
private long previousSampleCount;
private long previousBusOffCount;
private long previousRestartCount;
private double lastSampleChangeTime;
private double lastHighTime = Double.NEGATIVE_INFINITY;
private double lastMediumTime = Double.NEGATIVE_INFINITY;

/**
* Returns the severity of one bus status sample.
*
* @param status the Phoenix StatusCode name of the status read ("OK" when the read worked)
* @param state the Phoenix CANState name
* @param busOffRose whether the bus-off count rose since the previous sample
* @param restartRose whether the controller restart count rose since the previous sample
* @param stale whether the reader has stopped producing samples
*/
public static Severity severity(
String status, String state, boolean busOffRose, boolean restartRose, boolean stale) {
if (!"OK".equals(status)
|| "ErrorPassive".equals(state)
|| "BusOff".equals(state)
|| "Stopped".equals(state)
|| busOffRose
|| restartRose
|| stale) {
return Severity.HIGH;
}
if ("ErrorWarning".equals(state)) return Severity.MEDIUM;
return Severity.NONE;
}

/**
* Takes one cycle of logged bus status.
*
* <p>A sample count of 0 means no status has been read, as in simulation or when replaying a log
* recorded without a reader. Such cycles never count as stale. Counter increases are measured
* from the first real sample, so counts left from before a code restart do not trigger an alert.
*
* @param now the current timestamp in seconds
* @param sampleCount how many status reads the reader has completed
* @param status the Phoenix StatusCode name of the status read
* @param state the Phoenix CANState name
* @param busOffCount the cumulative bus-off count
* @param restartCount the cumulative controller restart count
*/
public void update(
double now,
long sampleCount,
String status,
String state,
long busOffCount,
long restartCount) {
boolean busOffRose = false;
boolean restartRose = false;
boolean stale = false;
if (sampleCount > 0) {
if (!hasPrevious) {
hasPrevious = true;
previousSampleCount = sampleCount;
lastSampleChangeTime = now;
} else {
busOffRose = busOffCount > previousBusOffCount;
restartRose = restartCount > previousRestartCount;
if (sampleCount != previousSampleCount) {
previousSampleCount = sampleCount;
lastSampleChangeTime = now;
}
stale = now - lastSampleChangeTime > STALE_SECONDS;
}
previousBusOffCount = busOffCount;
previousRestartCount = restartCount;
}

switch (severity(status, state, busOffRose, restartRose, stale)) {
case HIGH -> lastHighTime = now;
case MEDIUM -> lastMediumTime = now;
case NONE -> {}
}
}

/** Returns true while a high-severity problem was seen within the last {@link #HOLD_SECONDS}. */
public boolean isHighActive(double now) {
return now - lastHighTime < HOLD_SECONDS;
}

/** Returns true while a warning was seen within the last {@link #HOLD_SECONDS}. */
public boolean isMediumActive(double now) {
return now - lastMediumTime < HOLD_SECONDS;
}
}
165 changes: 165 additions & 0 deletions src/main/java/frc/lib/hardware/CANChain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright (c) 2026 Triple Helix Robotics, FRC Team 2363
// https://github.com/TripleHelixProgramming
//
// Use of this source code is governed by a BSD
// license that can be found in the LICENSE file
// at the root directory of this project.

package frc.lib.hardware;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.wpilib.hardware.bus.CANPort;

/**
* A CAN bus's devices in daisy-chain order, and the logic for locating a break along it.
*
* <p>Declare the chain first, then one device per line, starting with the device wired closest to
* the SystemCore:
*
* <pre>{@code
* public static final CANChain CHAIN = new CANChain("SC1", CANPort.CAN_S1);
* public static final CANChain.Device FRONT_LEFT_DRIVE = CHAIN.add(28, "FrontLeft drive");
* public static final CANChain.Device FRONT_LEFT_TURN = CHAIN.add(29, "FrontLeft turn");
* }</pre>
*
* <p>Java runs static field initializers in the order they are written, and finishes them all
* before any other class can read the chain (JLS §12.4.2). So the order of the {@code add} lines is
* the chain order, and a device's position is its CAN index. Each device carries its bus, so code
* that uses a device never has to name the bus again.
*
* <p>A single break in the cable leaves every device before it connected and every device after it
* disconnected. The static methods find that split and describe where to look. Apart from the
* {@link CANPort} enum, they use no WPILib or Phoenix types.
*/
public class CANChain {
/**
* A device on the chain. A CAN ID is only unique within one bus, so the device carries both.
*
* @param port the SystemCore CAN port the device is wired to
* @param id the device's CAN ID
* @param label a name that tells the pit crew where the device is, e.g. "FrontLeft drive"
*/
public record Device(CANPort port, int id, String label) {}

private final String name;
private final CANPort port;
private final List<Device> devices = new ArrayList<>();

/**
* @param name the bus name used in logs and alerts, e.g. "SC1"
* @param port the SystemCore CAN port this chain starts from
*/
public CANChain(String name, CANPort port) {
this.name = name;
this.port = port;
}

/** Returns the bus name used in logs and alerts. */
public String name() {
return name;
}

/** Returns the SystemCore CAN port this chain starts from. */
public CANPort port() {
return port;
}

/**
* Adds the next device along the chain.
*
* @param id the device's CAN ID
* @param label a name that tells the pit crew where the device is
* @return the device, on this chain's port
*/
public Device add(int id, String label) {
Device device = new Device(port, id, label);
devices.add(device);
return device;
}

/** Returns the devices in chain order, as a read-only view. */
public List<Device> devices() {
return Collections.unmodifiableList(devices);
}

/**
* Finds a single break in the chain.
*
* <p>Returns {@code k} when devices {@code 0..k-1} are connected, devices {@code k..n-1} are
* disconnected, and at least two devices are disconnected. With one device down, a failed device
* and a failed cable look the same, and that device's own alert already names it. Any other
* pattern, such as a single device down in the middle, is not a chain break.
*
* @param connected each device's connection state, in chain order
* @return the index of the first disconnected device, or -1 if the pattern is not a chain break
*/
public static int findBreak(boolean[] connected) {
int n = connected.length;
int k = 0;
while (k < n && connected[k]) k++;
for (int i = k; i < n; i++) {
if (connected[i]) return -1;
}
return n - k >= 2 ? k : -1;
}

/**
* Describes where to look for a break found by {@link #findBreak}.
*
* @param k the index of the first disconnected device
* @param traced when and by whom the chain order was traced from the wiring
*/
public String hint(int k, String traced) {
int last = devices.size() - 1;
String prefix = "CAN chain break on " + name + " (order traced " + traced + "): ";
if (k == 0) {
return prefix
+ "no device responds. Check the SystemCore "
+ name
+ " port and plug, and the cable to "
+ describe(0)
+ ".";
}
return prefix
+ "#0–#"
+ (k - 1)
+ " respond, #"
+ k
+ "–#"
+ last
+ " don't. Check "
+ describe(k - 1)
+ "'s outgoing connector, the cable, and "
+ describe(k)
+ "'s incoming connector.";
}

private String describe(int index) {
Device device = devices.get(index);
return "#" + index + " " + device.label() + " (ID " + device.id() + ")";
}

/**
* Checks that every device on a chain has a connection source.
*
* @param chain the bus's devices in chain order
* @param sources the devices that have a connection source, on any bus
* @return a description of the first problem found, or null if the chain is usable
*/
public static String validate(List<Device> chain, Set<Device> sources) {
Set<Integer> chainIds = new HashSet<>();
for (Device device : chain) {
if (!chainIds.add(device.id())) {
return "CAN ID " + device.id() + " appears twice in the chain";
}
if (!sources.contains(device)) {
return device.label() + " (ID " + device.id() + ") has no connection source";
}
}
return null;
}
}
Loading
Loading