001package com.pi4j.io.i2c.impl;
002
003/*
004 * #%L
005 * **********************************************************************
006 * ORGANIZATION  :  Pi4J
007 * PROJECT       :  Pi4J :: Java Library (Core)
008 * FILENAME      :  I2CBusImpl.java
009 *
010 * This file is part of the Pi4J project. More information about
011 * this project can be found here:  https://www.pi4j.com/
012 * **********************************************************************
013 * %%
014 * Copyright (C) 2012 - 2021 Pi4J
015 * %%
016 * This program is free software: you can redistribute it and/or modify
017 * it under the terms of the GNU Lesser General Public License as
018 * published by the Free Software Foundation, either version 3 of the
019 * License, or (at your option) any later version.
020 *
021 * This program is distributed in the hope that it will be useful,
022 * but WITHOUT ANY WARRANTY; without even the implied warranty of
023 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
024 * GNU General Lesser Public License for more details.
025 *
026 * You should have received a copy of the GNU General Lesser Public
027 * License along with this program.  If not, see
028 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
029 * #L%
030 */
031
032import java.io.IOException;
033import java.nio.ByteBuffer;
034import java.nio.IntBuffer;
035import java.util.concurrent.Callable;
036import java.util.concurrent.TimeUnit;
037import java.util.concurrent.locks.ReentrantLock;
038import java.util.logging.Level;
039import java.util.logging.Logger;
040
041import com.pi4j.io.file.LinuxFile;
042import com.pi4j.io.i2c.I2CBus;
043import com.pi4j.io.i2c.I2CConstants;
044import com.pi4j.io.i2c.I2CDevice;
045import com.pi4j.io.i2c.I2CFactory;
046
047/**
048 * This is implementation of i2c bus. This class keeps underlying linux file descriptor of particular bus. As all reads and writes from/to i2c bus are blocked I/Os current implementation uses only one file per bus for all devices. Device
049 * implementations use this class file handle.
050 *
051 * Hint: For concurrency-locking the methods lock() and unlock() are provided. This requires that there is exactly one I2CBus-instance per bus-number what is guaranteed by the I2CFactory class. The locking is done by I2CDeviceImpl by using
052 * those methods. The reason for this is to enable other locking-strategies than the simple "lock before and release after access"-strategy.
053 *
054 * @author Daniel Sendula, refactored by <a href="http://raspelikan.blogspot.co.at">RasPelikan</a>
055 *
056 */
057public class I2CBusImpl implements I2CBus {
058
059    private static final Logger logger = Logger.getLogger(I2CBusImpl.class.getCanonicalName());
060
061    /** File handle for this i2c bus */
062    protected LinuxFile file = null;
063
064    protected int lastAddress = -1;
065
066    /** File name of this i2c bus */
067    protected String filename;
068
069    /** Used to identifiy the i2c bus within Pi4J **/
070    protected int busNumber;
071
072    protected long lockAquireTimeout;
073
074    protected TimeUnit lockAquireTimeoutUnit;
075
076    private final ReentrantLock accessLock = new ReentrantLock(true);
077
078    /**
079     * Constructor of i2c bus implementation.
080     *
081     * @param busNumber used to identifiy the i2c bus within Pi4J
082
083     * @throws IOException thrown in case that file cannot be opened
084     */
085    protected I2CBusImpl(final int busNumber, final String fileName, final long lockAquireTimeout, final TimeUnit lockAquireTimeoutUnit) {
086        this.filename = fileName;
087        this.busNumber = busNumber;
088
089        if (lockAquireTimeout < 0) {
090            this.lockAquireTimeout = I2CFactory.DEFAULT_LOCKAQUIRE_TIMEOUT;
091        } else {
092            this.lockAquireTimeout = lockAquireTimeout;
093        }
094
095        if (lockAquireTimeoutUnit == null) {
096            this.lockAquireTimeoutUnit = I2CFactory.DEFAULT_LOCKAQUIRE_TIMEOUT_UNITS;
097        } else {
098            this.lockAquireTimeoutUnit = lockAquireTimeoutUnit;
099        }
100    }
101
102    /**
103     * Returns i2c device implementation ({@link I2CDeviceImpl}).
104     *
105     * @param address address of i2c device
106     *
107     * @return implementation of i2c device with given address
108     *
109     * @throws IOException never in this implementation
110     */
111    @Override
112    public I2CDevice getDevice(int address) throws IOException {
113        return new I2CDeviceImpl(this, address);
114    }
115
116    /**
117     * Opens the bus.
118     *
119     * @throws IOException thrown in case there are problems opening the i2c bus.
120     */
121    protected void open() throws IOException {
122        if (file != null) {
123            return;
124        }
125
126        file = new LinuxFile(filename, "rw");
127
128        lastAddress = -1;
129    }
130
131    /**
132     * Closes this i2c bus. Can be used in a thread safe way during bus operations.
133     *
134     * @throws IOException never in this implementation
135     */
136    @Override
137    public synchronized void close() throws IOException {
138        if (file != null) {
139            file.close();
140            file = null;
141        }
142    }
143
144    public int readByteDirect(final I2CDevice device) throws IOException {
145        return runBusLockedDeviceAction(device, () -> file.readUnsignedByte());
146    }
147
148    public int readBytesDirect(final I2CDevice device, final int size, final int offset, final byte[] buffer) throws IOException {
149        return runBusLockedDeviceAction(device, () -> file.read(buffer, offset, size));
150    }
151
152    public int readByte(final I2CDevice device, final int localAddress) throws IOException {
153        return runBusLockedDeviceAction(device, () -> {
154            file.writeByte(localAddress);
155
156            return file.readUnsignedByte();
157        });
158    }
159
160    public int readBytes(final I2CDevice device, final int localAddress, final int size, final int offset, final byte[] buffer) throws IOException {
161        return runBusLockedDeviceAction(device, () -> {
162            file.writeByte(localAddress);
163
164            return file.read(buffer, offset, size);
165        });
166    }
167
168    public void writeByteDirect(final I2CDevice device, final byte data) throws IOException {
169        runBusLockedDeviceAction(device, () -> {
170            file.writeByte(data & 0xFF);
171
172            return null;
173        });
174    }
175
176    public void writeBytesDirect(final I2CDevice device, final int size, final int offset, final byte[] buffer) throws IOException {
177        runBusLockedDeviceAction(device, () -> {
178            file.write(buffer, offset, size);
179
180            return null;
181        });
182    }
183
184    public void writeByte(final I2CDevice device, final int localAddress, final byte data) throws IOException {
185        runBusLockedDeviceAction(device, () -> {
186            file.write(new byte[] { (byte)localAddress, data });
187
188            return null;
189        });
190    }
191
192    public void writeBytes(final I2CDevice device, final int localAddress, final int size, final int offset, final byte[] buffer) throws IOException {
193        runBusLockedDeviceAction(device, () -> {
194            byte[] buf = new byte[size + 1];
195
196            buf[0] = (byte)localAddress;
197
198            System.arraycopy(buffer, offset, buf, 1, size);
199
200            file.write(buf);
201
202            return null;
203        });
204    }
205
206    public int writeAndReadBytesDirect(final I2CDevice device, final int writeSize, final int writeOffset, final byte[] writeBuffer,
207                                       final int readSize, final int readOffset, final byte[] readBuffer) throws IOException {
208        return runBusLockedDeviceAction(device, () -> {
209            file.write(writeBuffer, writeOffset, writeSize);
210
211            return file.read(readBuffer, readOffset, readSize);
212        });
213    }
214
215    public void ioctl(final I2CDevice device, final long command, final int value) throws IOException {
216        runBusLockedDeviceAction(device, () -> {
217            file.ioctl(command, value);
218
219            return null;
220        });
221    }
222
223    public void ioctl(final I2CDevice device, final long command, final ByteBuffer values, final IntBuffer offsets) throws IOException {
224        runBusLockedDeviceAction(device, () -> {
225            file.ioctl(command, values, offsets);
226
227            return null;
228        });
229    }
230
231    /**
232     * Selects a device on the bus for an action, and locks parallel access around file descriptor operations.
233     * Multiple bus instances may be used in parallel, but a single bus instance must limit parallel access.
234     * <p>
235     * The timeout used for the acquisition of the lock may be defined on getting the I2CBus from I2CFactory.
236     * <p>
237     * The 'run'-method of 'action' may throw an 'IOExceptionWrapperException' to wrap IOExceptions. The wrapped IOException is unwrapped by this method and rethrown as IOException.
238     *
239     * @param <T> The result-type of the method
240     * @param device Device to be selected on the bus
241     * @param action The action to be run
242     * @throws RuntimeException thrown by the custom code
243     * @throws IOException see method description above
244     * @see I2CFactory#getInstance(int, long, java.util.concurrent.TimeUnit)
245     */
246    public <T> T runBusLockedDeviceAction(final I2CDevice device, final Callable<T> action) throws IOException {
247        if (action == null) {
248            throw new NullPointerException("Parameter 'action' is mandatory!");
249        }
250
251        testForProperOperationConditions(device);
252
253        try {
254            if (accessLock.tryLock(lockAquireTimeout, lockAquireTimeoutUnit)) {
255                try {
256                    testForProperOperationConditions(device);
257
258                    selectBusSlave(device);
259
260                    return action.call();
261                } finally {
262                    accessLock.unlock();
263                }
264            }
265        } catch (InterruptedException e) {
266            logger.log(Level.FINER, "Failed locking I2CBusImpl-" + busNumber, e);
267            throw new RuntimeException("Could not obtain an access-lock!", e);
268        } catch (IOException e) { // unwrap IOExceptionWrapperException
269            throw e;
270        } catch (RuntimeException e) {
271            throw e;
272        } catch (Exception e) { // unexpected exceptions
273            throw new RuntimeException(e);
274        }
275        throw new RuntimeException("Could not obtain an access-lock!");
276    }
277
278    /**
279     * Selects the slave device if not already selected on this bus.
280     * Uses SharedSecrets to get the POSIX file descriptor, and runs
281     * the required ioctl's via JNI.
282     *
283     * @param device Device to select
284     */
285    protected void selectBusSlave(final I2CDevice device) throws IOException {
286        final int addr = device.getAddress();
287
288        if (lastAddress != addr) {
289            lastAddress = addr;
290
291            file.ioctl(I2CConstants.I2C_SLAVE, addr & 0xFF);
292        }
293    }
294
295    protected void testForProperOperationConditions(final I2CDevice device) throws IOException {
296        if (file == null) {
297            throw new IOException(toString() + " has already been closed! A new bus has to be acquired.");
298        }
299
300        if (device == null) {
301            throw new NullPointerException("Parameter 'device' is mandatory!");
302        }
303    }
304
305    @Override
306    public int getBusNumber() {
307        return busNumber;
308    }
309
310    @Override
311    public String toString() {
312        return "I2CBus '" + busNumber + "' ('" + filename + "')";
313    }
314}