001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.common.hardware.GPIO.pi;
019
020import static org.photonvision.common.hardware.GPIO.pi.PigpioException.*;
021
022import java.io.IOException;
023import java.nio.ByteBuffer;
024import java.nio.ByteOrder;
025import java.util.ArrayList;
026import org.photonvision.common.logging.LogGroup;
027import org.photonvision.common.logging.Logger;
028
029@SuppressWarnings({"SpellCheckingInspection", "unused"})
030public class PigpioSocket {
031    private static final Logger logger = new Logger(PigpioSocket.class, LogGroup.General);
032    private static final int PIGPIOD_MESSAGE_SIZE = 12;
033
034    private PigpioSocketLock commandSocket;
035    private int activeWaveformID = -1;
036
037    /** Creates and starts a socket connection to a pigpio daemon on localhost */
038    public PigpioSocket() {
039        this("127.0.0.1", 8888);
040    }
041
042    /**
043     * Creates and starts a socket connection to a pigpio daemon on a remote host with the specified
044     * address and port
045     *
046     * @param addr Address of remote pigpio daemon
047     * @param port Port of remote pigpio daemon
048     */
049    public PigpioSocket(String addr, int port) {
050        try {
051            commandSocket = new PigpioSocketLock(addr, port);
052        } catch (IOException e) {
053            logger.error("Failed to create or connect to Pigpio Daemon socket", e);
054        }
055    }
056
057    /**
058     * Reconnects to the pigpio daemon
059     *
060     * @throws PigpioException on failure
061     */
062    public void reconnect() throws PigpioException {
063        try {
064            commandSocket.reconnect();
065        } catch (IOException e) {
066            logger.error("Failed to reconnect to Pigpio Daemon socket", e);
067            throw new PigpioException("reconnect", e);
068        }
069    }
070
071    /**
072     * Terminates the connection to the pigpio daemon
073     *
074     * @throws PigpioException on failure
075     */
076    public void gpioTerminate() throws PigpioException {
077        try {
078            commandSocket.terminate();
079        } catch (IOException e) {
080            logger.error("Failed to terminate connection to Pigpio Daemon socket", e);
081            throw new PigpioException("gpioTerminate", e);
082        }
083    }
084
085    /**
086     * Read the GPIO level
087     *
088     * @param pin Pin to read from
089     * @return Value of the pin
090     * @throws PigpioException on failure
091     */
092    public boolean gpioRead(int pin) throws PigpioException {
093        try {
094            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_READ.value, pin);
095            if (retCode < 0) throw new PigpioException(retCode);
096            return retCode != 0;
097        } catch (IOException e) {
098            logger.error("Failed to read GPIO pin: " + pin, e);
099            throw new PigpioException("gpioRead", e);
100        }
101    }
102
103    /**
104     * Write the GPIO level
105     *
106     * @param pin Pin to write to
107     * @param value Value to write
108     * @throws PigpioException on failure
109     */
110    public void gpioWrite(int pin, boolean value) throws PigpioException {
111        try {
112            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WRITE.value, pin, value ? 1 : 0);
113            if (retCode < 0) throw new PigpioException(retCode);
114        } catch (IOException e) {
115            logger.error("Failed to write to GPIO pin: " + pin, e);
116            throw new PigpioException("gpioWrite", e);
117        }
118    }
119
120    /**
121     * Clears all waveforms and any data added by calls to {@link #waveAddGeneric(ArrayList)}
122     *
123     * @throws PigpioException on failure
124     */
125    public void waveClear() throws PigpioException {
126        try {
127            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCLR.value);
128            if (retCode < 0) throw new PigpioException(retCode);
129        } catch (IOException e) {
130            logger.error("Failed to clear waveforms", e);
131            throw new PigpioException("waveClear", e);
132        }
133    }
134
135    /**
136     * Adds a number of pulses to the current waveform
137     *
138     * @param pulses ArrayList of pulses to add
139     * @return the new total number of pulses in the current waveform
140     * @throws PigpioException on failure
141     */
142    private int waveAddGeneric(ArrayList<PigpioPulse> pulses) throws PigpioException {
143        // pigpio wave message format
144
145        // I p1 0
146        // I p2 0
147        // I p3 pulses * 12
148        // ## extension ##
149        // III on/off/delay * pulses
150
151        if (pulses == null || pulses.isEmpty()) return 0;
152
153        try {
154            ByteBuffer bb = ByteBuffer.allocate(pulses.size() * 12);
155            bb.order(ByteOrder.LITTLE_ENDIAN);
156            for (var pulse : pulses) {
157                bb.putInt(pulse.gpioOn).putInt(pulse.gpioOff).putInt(pulse.delayMicros);
158            }
159
160            int retCode =
161                    commandSocket.sendCmd(
162                            PigpioCommand.PCMD_WVAG.value,
163                            0,
164                            0,
165                            pulses.size() * PIGPIOD_MESSAGE_SIZE,
166                            bb.array());
167            if (retCode < 0) throw new PigpioException(retCode);
168
169            return retCode;
170        } catch (IOException e) {
171            logger.error("Failed to add pulse(s) to waveform", e);
172            throw new PigpioException("waveAddGeneric", e);
173        }
174    }
175
176    /**
177     * Creates pulses and adds them to the current waveform
178     *
179     * @param pulseTimeMillis Pulse length in milliseconds
180     * @param blinks Number of times to pulse. -1 for repeat
181     * @param pinNo Pin to pulse
182     */
183    private void addBlinkPulsesToWaveform(int pulseTimeMillis, int blinks, int pinNo) {
184        boolean repeat = blinks == -1;
185
186        if (blinks == 0) return;
187
188        if (repeat) {
189            blinks = 1;
190        }
191
192        try {
193            ArrayList<PigpioPulse> pulses = new ArrayList<>();
194            var startPulse = new PigpioPulse(pinNo, 0, pulseTimeMillis * 1000);
195            var endPulse = new PigpioPulse(0, pinNo, pulseTimeMillis * 1000);
196
197            for (int i = 0; i < blinks; i++) {
198                pulses.add(startPulse);
199                pulses.add(endPulse);
200            }
201
202            waveAddGeneric(pulses);
203            pulses.clear();
204        } catch (Exception e) {
205            e.printStackTrace();
206        }
207    }
208
209    /**
210     * Generates and sends a waveform to the given pins with the specified parameters.
211     *
212     * @param pulseTimeMillis Pulse length in milliseconds
213     * @param blinks Number of times to pulse. -1 for repeat
214     * @param pins Pins to pulse
215     * @throws PigpioException on failure
216     */
217    public void generateAndSendWaveform(int pulseTimeMillis, int blinks, int... pins)
218            throws PigpioException {
219        if (pins.length == 0) return;
220        boolean repeat = blinks == -1;
221        if (blinks == 0) return;
222
223        // stop any active waves
224        waveTxStop();
225        waveClear();
226
227        if (activeWaveformID != -1) {
228            waveDelete(activeWaveformID);
229            activeWaveformID = -1;
230        }
231
232        for (int pin : pins) {
233            addBlinkPulsesToWaveform(pulseTimeMillis, blinks, pin);
234        }
235
236        int waveformId = waveCreate();
237
238        if (waveformId >= 0) {
239            if (repeat) {
240                waveSendRepeat(waveformId);
241            } else {
242                waveSendOnce(waveformId);
243            }
244        } else {
245            logger.error("Failed to send wave: " + getMessageForError(waveformId));
246        }
247    }
248
249    /**
250     * Stops the transmission of the current waveform
251     *
252     * @return success
253     * @throws PigpioException on failure
254     */
255    public boolean waveTxStop() throws PigpioException {
256        try {
257            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVHLT.value);
258            if (retCode < 0) throw new PigpioException(retCode);
259            return retCode == 0;
260        } catch (IOException e) {
261            logger.error("Failed to stop waveform", e);
262            throw new PigpioException("waveTxStop", e);
263        }
264    }
265
266    /**
267     * Creates a waveform from the data provided by the prior calls to {@link
268     * #waveAddGeneric(ArrayList)} Upon success a wave ID greater than or equal to 0 is returned
269     *
270     * @return ID of the created waveform
271     * @throws PigpioException on failure
272     */
273    public int waveCreate() throws PigpioException {
274        try {
275            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVCRE.value);
276            if (retCode < 0) throw new PigpioException(retCode);
277            return retCode;
278        } catch (IOException e) {
279            logger.error("Failed to create new waveform", e);
280            throw new PigpioException("waveCreate", e);
281        }
282    }
283
284    /**
285     * Deletes the waveform with specified wave ID
286     *
287     * @param waveId ID of the waveform to delete
288     * @throws PigpioException on failure
289     */
290    public void waveDelete(int waveId) throws PigpioException {
291        try {
292            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVDEL.value, waveId);
293            if (retCode < 0) throw new PigpioException(retCode);
294        } catch (IOException e) {
295            logger.error("Failed to delete wave: " + waveId, e);
296            throw new PigpioException("waveDelete", e);
297        }
298    }
299
300    /**
301     * Transmits the waveform with specified wave ID. The waveform is sent once
302     *
303     * @param waveId ID of the waveform to transmit
304     * @return The number of DMA control blocks in the waveform
305     * @throws PigpioException on failure
306     */
307    public int waveSendOnce(int waveId) throws PigpioException {
308        try {
309            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTX.value, waveId);
310            if (retCode < 0) throw new PigpioException(retCode);
311            return retCode;
312        } catch (IOException e) {
313            throw new PigpioException("waveSendOnce", e);
314        }
315    }
316
317    /**
318     * Transmits the waveform with specified wave ID. The waveform cycles until cancelled (either by
319     * the sending of a new waveform or {@link #waveTxStop()}
320     *
321     * @param waveId ID of the waveform to transmit
322     * @return The number of DMA control blocks in the waveform
323     * @throws PigpioException on failure
324     */
325    public int waveSendRepeat(int waveId) throws PigpioException {
326        try {
327            int retCode = commandSocket.sendCmd(PigpioCommand.PCMD_WVTXR.value, waveId);
328            if (retCode < 0) throw new PigpioException(retCode);
329            return retCode;
330        } catch (IOException e) {
331            throw new PigpioException("waveSendRepeat", e);
332        }
333    }
334
335    /**
336     * Starts hardware PWM on a GPIO at the specified frequency and dutycycle
337     *
338     * @param pin GPIO pin to start PWM on
339     * @param pwmFrequency Frequency to run at (1Hz-125MHz). Frequencies above 30MHz are unlikely to
340     *     work
341     * @param pwmDuty Duty cycle to run at (0-1,000,000)
342     * @throws PigpioException on failure
343     */
344    public void hardwarePWM(int pin, int pwmFrequency, int pwmDuty) throws PigpioException {
345        try {
346            ByteBuffer bb = ByteBuffer.allocate(4);
347            bb.order(ByteOrder.LITTLE_ENDIAN);
348            bb.putInt(pwmDuty);
349
350            int retCode =
351                    commandSocket.sendCmd(PigpioCommand.PCMD_HP.value, pin, pwmFrequency, 4, bb.array());
352            if (retCode < 0) throw new PigpioException(retCode);
353        } catch (IOException e) {
354            throw new PigpioException("hardwarePWM", e);
355        }
356    }
357}