001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package org.wpilib.hardware.expansionhub;
006
007import static org.wpilib.units.Units.Degrees;
008import static org.wpilib.units.Units.Microseconds;
009
010import org.wpilib.hardware.hal.util.AllocationException;
011import org.wpilib.networktables.BooleanPublisher;
012import org.wpilib.networktables.IntegerPublisher;
013import org.wpilib.networktables.NetworkTableInstance;
014import org.wpilib.networktables.PubSubOption;
015import org.wpilib.system.SystemServer;
016import org.wpilib.units.measure.Angle;
017import org.wpilib.units.measure.Time;
018
019/** This class controls a specific servo in positional/servo mode hooked up to an ExpansionHub. */
020public class ExpansionHubServo implements AutoCloseable {
021  private ExpansionHub m_hub;
022  private final int m_channel;
023
024  private boolean m_reversed;
025
026  private final IntegerPublisher m_pulseWidthPublisher;
027  private final IntegerPublisher m_framePeriodPublisher;
028  private final BooleanPublisher m_enabledPublisher;
029
030  private double m_maxServoAngle = 180.0;
031  private double m_minServoAngle;
032
033  private int m_minPwm = 600;
034  private int m_maxPwm = 2400;
035
036  /**
037   * Constructs a servo at the requested channel on a specific USB port.
038   *
039   * @param usbId The USB port ID the hub is connected to
040   * @param channel The servo channel
041   */
042  public ExpansionHubServo(int usbId, int channel) {
043    m_hub = new ExpansionHub(usbId);
044    m_channel = channel;
045
046    if (!m_hub.checkServoChannel(channel)) {
047      m_hub.close();
048      throw new IllegalArgumentException("Channel " + channel + " out of range");
049    }
050
051    if (!m_hub.checkAndReserveServo(channel)) {
052      m_hub.close();
053      throw new AllocationException("ExpansionHub Servo already allocated");
054    }
055
056    m_hub.reportUsage("ExHubServo[" + channel + "]", "ExHubServo");
057
058    NetworkTableInstance systemServer = SystemServer.getSystemServer();
059
060    PubSubOption[] options =
061        new PubSubOption[] {
062          PubSubOption.SEND_ALL, PubSubOption.KEEP_DUPLICATES, PubSubOption.periodic(0.005)
063        };
064
065    m_pulseWidthPublisher =
066        systemServer
067            .getIntegerTopic("/rhsp/" + usbId + "/servo" + channel + "/pulseWidth")
068            .publish(options);
069
070    m_pulseWidthPublisher.set(1500);
071
072    m_framePeriodPublisher =
073        systemServer
074            .getIntegerTopic("/rhsp/" + usbId + "/servo" + channel + "/framePeriod")
075            .publish(options);
076
077    m_framePeriodPublisher.set(20000);
078
079    m_enabledPublisher =
080        systemServer
081            .getBooleanTopic("/rhsp/" + usbId + "/servo" + channel + "/enabled")
082            .publish(options);
083  }
084
085  /**
086   * Set the servo position.
087   *
088   * <p>Servo values range from 0.0 to 1.0 corresponding to the range of full left to full right.
089   *
090   * @param value Position from 0.0 to 1.0.
091   */
092  public void setPosition(double value) {
093    value = Math.clamp(value, 0.0, 1.0);
094
095    if (m_reversed) {
096      value = 1.0 - value;
097    }
098
099    int rawValue = (int) ((value * getFullRangeScaleFactor()) + m_minPwm);
100
101    setEnabled(true);
102    m_pulseWidthPublisher.set(rawValue);
103  }
104
105  /**
106   * Sets the servo angle.
107   *
108   * <p>Servo angles range defaults to 0 to 180 degrees, but can be changed with setAngleRange().
109   *
110   * @param angle Position in angle units. Will be clamped to be within the current angle range.
111   */
112  public void setAngle(Angle angle) {
113    double dAngle = angle.in(Degrees);
114    if (dAngle < m_minServoAngle) {
115      dAngle = m_minServoAngle;
116    } else if (dAngle > m_maxServoAngle) {
117      dAngle = m_maxServoAngle;
118    }
119
120    setPosition((dAngle - m_minServoAngle) / getServoAngleRange());
121  }
122
123  private double getFullRangeScaleFactor() {
124    return m_maxPwm - m_minPwm;
125  }
126
127  private double getServoAngleRange() {
128    return m_maxServoAngle - m_minServoAngle;
129  }
130
131  /**
132   * Sets the raw pulse width output on the servo.
133   *
134   * @param pulseWidth Pulse width
135   */
136  public void setPulseWidth(Time pulseWidth) {
137    setEnabled(true);
138    m_pulseWidthPublisher.set((long) pulseWidth.in(Microseconds));
139  }
140
141  /**
142   * Sets if the servo output is enabled or not. Defaults to false.
143   *
144   * @param enabled True to enable, false to disable
145   */
146  public void setEnabled(boolean enabled) {
147    m_enabledPublisher.set(enabled);
148  }
149
150  /**
151   * Sets the frame period for the servo. Defaults to 20ms.
152   *
153   * @param framePeriod The frame period
154   */
155  public void setFramePeriod(Time framePeriod) {
156    m_framePeriodPublisher.set((long) framePeriod.in(Microseconds));
157  }
158
159  /**
160   * Gets if the underlying ExpansionHub is connected.
161   *
162   * @return True if hub is connected, otherwise false
163   */
164  public boolean isHubConnected() {
165    return m_hub.isHubConnected();
166  }
167
168  /**
169   * Sets whether the servo is reversed.
170   *
171   * <p>This will reverse both setPosition() and setAngle().
172   *
173   * @param reversed True to reverse, false for normal
174   */
175  public void setReversed(boolean reversed) {
176    m_reversed = reversed;
177  }
178
179  /**
180   * Sets the PWM range for the servo. By default, this is 600 to 2400 microseconds.
181   *
182   * <p>Maximum must be greater than minimum.
183   *
184   * @param minPwm Minimum PWM
185   * @param maxPwm Maximum PWM
186   */
187  public void setPWMRange(int minPwm, int maxPwm) {
188    if (maxPwm <= minPwm) {
189      throw new IllegalArgumentException("Maximum PWM must be greater than minimum PWM");
190    }
191    m_minPwm = minPwm;
192    m_maxPwm = maxPwm;
193  }
194
195  /**
196   * Sets the angle range for the setAngle call. By default, this is 0 to 180 degrees.
197   *
198   * <p>Maximum angle must be greater than minimum angle.
199   *
200   * @param minAngle Minimum angle
201   * @param maxAngle Maximum angle
202   */
203  public void setAngleRange(double minAngle, double maxAngle) {
204    if (maxAngle <= minAngle) {
205      throw new IllegalArgumentException("Maximum angle must be greater than minimum angle");
206    }
207    m_minServoAngle = minAngle;
208    m_maxServoAngle = maxAngle;
209  }
210
211  /** Closes a servo so another instance can be constructed. */
212  @Override
213  public void close() {
214    m_hub.unreserveServo(m_channel);
215    m_hub.close();
216    m_hub = null;
217
218    m_pulseWidthPublisher.close();
219    m_framePeriodPublisher.close();
220    m_enabledPublisher.close();
221  }
222}