001package jmri.jmrix.pi;
002
003import com.pi4j.context.Context;
004import com.pi4j.io.gpio.digital.DigitalInput;
005import com.pi4j.io.gpio.digital.DigitalState;
006
007import jmri.Sensor;
008import jmri.implementation.AbstractSensor;
009import jmri.jmrix.pi.simulator.GpioPinDigitalInputSimulator;
010import jmri.jmrix.pi.simulator.GpioSimulator;
011
012/**
013 * Sensor interface for RaspberryPi GPIO pins.
014 * <p>
015 * Uses Pi4J 3.x on real hardware, or the JMRI-internal
016 * {@link GpioSimulator} when in simulator mode.
017 * <p>
018 * <b>Pin numbering:</b> the numeric part of the system name is the BCM
019 * (Broadcom) GPIO number. For example, system name {@code "PS4"} addresses
020 * BCM GPIO 4.
021 *
022 * @author Paul Bender Copyright (C) 2003-2017
023 */
024public class RaspberryPiSensor extends AbstractSensor {
025
026    /** Pi4J digital input — non-null only in production (non-simulator) mode. */
027    private DigitalInput pi4jPin = null;
028
029    /** JMRI simulator input pin — non-null only in simulator mode. */
030    private GpioPinDigitalInputSimulator simPin = null;
031
032    /** Pi4J registry key for this pin, used to shut it down individually. */
033    private String pinId = null;
034
035    private com.pi4j.io.gpio.digital.PullResistance pi4jPull = com.pi4j.io.gpio.digital.PullResistance.PULL_DOWN;
036    private jmri.Sensor.PullResistance pull = jmri.Sensor.PullResistance.PULL_DOWN;
037
038    public RaspberryPiSensor(String systemName, String userName) {
039        super(systemName, userName);
040        init(systemName, jmri.Sensor.PullResistance.PULL_DOWN);
041    }
042
043    public RaspberryPiSensor(String systemName, String userName, jmri.Sensor.PullResistance p) {
044        super(systemName, userName);
045        init(systemName, p);
046    }
047
048    public RaspberryPiSensor(String systemName) {
049        super(systemName);
050        init(systemName, jmri.Sensor.PullResistance.PULL_DOWN);
051    }
052
053    public RaspberryPiSensor(String systemName, jmri.Sensor.PullResistance p) {
054        super(systemName);
055        init(systemName, p);
056    }
057
058    /**
059     * Common initialisation for all constructors.
060     * <p>
061     * Compare {@link RaspberryPiTurnout}
062     */
063    private void init(String systemName, jmri.Sensor.PullResistance pRes) {
064        log.debug("Provisioning sensor {}", systemName);
065        pull = pRes;
066        pi4jPull = toPi4JPull(pRes);
067        int address = Integer.parseInt(systemName.substring(systemName.lastIndexOf("S") + 1));
068        pinId = "jmri-rpi-sensor-" + address;
069
070        if (RaspberryPiAdapter.isSimulator()) {
071            simPin = GpioSimulator.getInstance().provisionDigitalInputPin(address, systemName);
072        } else {
073            Context ctx = RaspberryPiAdapter.getSharedContext();
074            if (ctx == null) {
075                String msg = Bundle.getMessage("PinNameNotValid", "GPIO " + address, systemName);
076                log.error(msg);
077                throw new IllegalArgumentException(msg);
078            }
079            try {
080                pi4jPin = ctx.create(
081                    DigitalInput.newConfigBuilder(ctx)
082                        .id(pinId)
083                        .name(systemName)
084                        .address(address)
085                        .pull(pi4jPull)
086                        .build()
087                );
088                pi4jPin.addListener(event -> setStateBeforeInvert(event.state().isHigh()));
089            } catch (RuntimeException re) {
090                log.error("Provisioning sensor {} failed with: {}", systemName, re.getMessage());
091                throw new IllegalArgumentException(re.getMessage());
092            }
093        }
094        requestUpdateFromLayout();
095    }
096
097    private static com.pi4j.io.gpio.digital.PullResistance toPi4JPull(jmri.Sensor.PullResistance pr) {
098        if (pr == jmri.Sensor.PullResistance.PULL_UP)   return com.pi4j.io.gpio.digital.PullResistance.PULL_UP;
099        if (pr == jmri.Sensor.PullResistance.PULL_DOWN) return com.pi4j.io.gpio.digital.PullResistance.PULL_DOWN;
100        return com.pi4j.io.gpio.digital.PullResistance.OFF;
101    }
102
103    /**
104     * Request an update on status by reading the current pin state.
105     */
106    @Override
107    public void requestUpdateFromLayout() {
108        if (pi4jPin != null) {
109            setStateBeforeInvert(pi4jPin.state() == DigitalState.HIGH);
110        } else if (simPin != null) {
111            setStateBeforeInvert(simPin.isHigh());
112        }
113    }
114
115    private void setStateBeforeInvert(boolean high) {
116        if (high) {
117            setOwnState(!getInverted() ? Sensor.ACTIVE : Sensor.INACTIVE);
118        } else {
119            setOwnState(!getInverted() ? Sensor.INACTIVE : Sensor.ACTIVE);
120        }
121    }
122
123    /**
124     * Set the pull resistance.
125     * <p>
126     * Note: Pi4J 3.x does not support changing pull resistance after a pin is
127     * provisioned. This call updates the cached value but has no effect on
128     * already-provisioned hardware pins.
129     *
130     * @param r the new PullResistance value
131     */
132    @Override
133    public void setPullResistance(jmri.Sensor.PullResistance r) {
134        pull = r;
135        pi4jPull = toPi4JPull(r);
136        if (!RaspberryPiAdapter.isSimulator()) {
137            log.warn("Changing pull resistance after pin provisioning is not supported in Pi4J 3.x");
138        }
139    }
140
141    /**
142     * Get the pull resistance.
143     *
144     * @return the currently configured pull resistance
145     */
146    @Override
147    public jmri.Sensor.PullResistance getPullResistance() {
148        return pull;
149    }
150
151    @Override
152    public void dispose() {
153        if (pi4jPin != null) {
154            try {
155                Context ctx = RaspberryPiAdapter.getSharedContext();
156                if (ctx != null) {
157                    ctx.shutdown(pinId);
158                }
159            } catch (Exception ex) {
160                log.trace("Pin {} not found during dispose — already removed?", pinId);
161            }
162            pi4jPin = null;
163        } else if (simPin != null) {
164            int address = Integer.parseInt(
165                getSystemName().substring(getSystemName().lastIndexOf("S") + 1));
166            GpioSimulator.getInstance().unprovisionInputPin(address);
167            simPin = null;
168        }
169        super.dispose();
170    }
171
172    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(RaspberryPiSensor.class);
173
174}