Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.pi4j.plugin.ffm.common.file.FileDescriptorNative;
import com.pi4j.plugin.ffm.common.file.FileFlag;
import com.pi4j.plugin.ffm.common.gpio.PinFlag;
import com.pi4j.plugin.ffm.common.gpio.enums.LineAttributeId;
import com.pi4j.plugin.ffm.common.gpio.structs.*;
import com.pi4j.plugin.ffm.common.ioctl.Command;
import com.pi4j.plugin.ffm.common.ioctl.IoctlNative;
Expand All @@ -20,6 +21,7 @@
import java.nio.file.Path;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;

public class FFMDigitalOutput extends DigitalOutputBase implements DigitalOutput {
private static final Logger logger = LoggerFactory.getLogger(FFMDigitalOutput.class);
Expand Down Expand Up @@ -59,7 +61,20 @@ public DigitalOutput initialize(Context context) throws InitializeException {
}
logger.trace("{}-{} - DigitalOutput BCM line info: {}", deviceName, bcm, lineInfo);
var flags = PinFlag.OUTPUT.getValue();
var lineConfig = new LineConfig(flags, 0, new LineConfigAttribute[]{});
var attributes = new ArrayList<LineConfigAttribute>();
var initialState = config().initialState();
if (initialState != null) {
// Pass the configured initial state to the kernel as part of the line request so the
// pin is driven to it the moment the line is requested. Without this the kernel drives
// a newly requested output low by default and the pin is only switched to the initial
// state afterwards, producing a transient low-then-high glitch (issue #654).
var values = initialState.isHigh() ? 1L : 0L;
var outputValues = new LineAttribute(LineAttributeId.GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES.getValue(), 0, values, 0);
// The single requested line lives at index 0 of the offsets array, so its mask bit is 0.
attributes.add(new LineConfigAttribute(outputValues, 1L));
logger.trace("{}-{} - DigitalOutput BCM initial state: {}", deviceName, bcm, initialState);
}
var lineConfig = new LineConfig(flags, attributes.size(), attributes.toArray(new LineConfigAttribute[0]));
var lineRequest = new LineRequest(new int[]{bcm}, ("pi4j." + getClass().getSimpleName()).getBytes(), lineConfig, 1, 0, 0);
var result = ioctl.call(fd, Command.getGpioV2GetLineIoctl(), lineRequest);
this.chipFileDescriptor = result.fd();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,22 @@ public void testOutputCustomConfig() {
assertEquals(DigitalState.HIGH, output.config().initialState());
assertEquals(6, output.bcm());
}


@Test
public void testOutputInitialStateHigh() {
// Requesting an output with initial state HIGH must succeed through the real character
// device ABI: the kernel validates the output-values attribute we attach to the line
// request and rejects a malformed one with EINVAL. A successful creation therefore proves
// the initial state is passed to the kernel at request time (issue #654).
var config = DigitalOutputConfigBuilder.newInstance()
.bus(97)
.bcm(7)
.initial(DigitalState.HIGH)
.build();
var output = pi4j0.digitalOutput().create(config);
assertEquals(7, output.bcm());
assertEquals(DigitalState.HIGH, output.config().initialState());
assertEquals(DigitalState.HIGH, output.state());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ public record IoctlTestData(Class<? extends Pi4JLayout> objClass, Function<Invoc

public static MockedConstruction<IoctlNative> setup(IoctlTestData... data) {
return mockConstruction(IoctlNative.class, (mock, _) -> {
for (IoctlTestData testData : data) {
when(mock.call(anyInt(), anyLong(), isA(testData.objClass))).thenAnswer(testData.callback::apply);
}
// Register the defaults first so that test-provided callbacks for the same struct
// (e.g. LineRequest) take precedence over them.
when(mock.call(anyInt(), anyLong(), isA(LineValues.class))).thenAnswer((answer) -> answer.<LineValues>getArgument(2));
when(mock.call(anyInt(), anyLong(), isA(LineRequest.class))).thenAnswer((answer) -> {
LineRequest lineRequest = answer.getArgument(2);
return new LineRequest(lineRequest.offsets(), lineRequest.consumer(), lineRequest.config(), lineRequest.numLines(), lineRequest.eventBufferSize(), 42);
});
for (IoctlTestData testData : data) {
when(mock.call(anyInt(), anyLong(), isA(testData.objClass))).thenAnswer(testData.callback::apply);
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import com.pi4j.plugin.ffm.common.FFMPermissionHelper;
import com.pi4j.plugin.ffm.common.gpio.PinEvent;
import com.pi4j.plugin.ffm.common.gpio.PinFlag;
import com.pi4j.plugin.ffm.common.gpio.enums.LineAttributeId;
import com.pi4j.plugin.ffm.common.gpio.structs.LineAttribute;
import com.pi4j.plugin.ffm.common.gpio.structs.LineEvent;
import com.pi4j.plugin.ffm.common.gpio.structs.LineInfo;
import com.pi4j.plugin.ffm.common.gpio.structs.LineRequest;
import com.pi4j.plugin.ffm.common.poll.PollFlag;
import com.pi4j.plugin.ffm.common.poll.structs.PollingData;
import com.pi4j.plugin.ffm.mocks.*;
Expand Down Expand Up @@ -393,6 +395,117 @@ public void testOutputChangeState() {
}
}

@Test
public void testOutputInitialStateHigh() {
var capturedRequest = new java.util.concurrent.atomic.AtomicReference<LineRequest>();
var lineInfoTestData = new IoctlNativeMock.IoctlTestData(LineInfo.class, (answer) -> {
LineInfo lineInfo = answer.getArgument(2);
return new LineInfo(("Test").getBytes(), ("FFM-Test").getBytes(),
lineInfo.offset(), 0,
PinFlag.OUTPUT.getValue(),
new LineAttribute[0]);
});
var lineRequestTestData = new IoctlNativeMock.IoctlTestData(LineRequest.class, (answer) -> {
LineRequest lineRequest = answer.getArgument(2);
capturedRequest.set(lineRequest);
return new LineRequest(lineRequest.offsets(), lineRequest.consumer(), lineRequest.config(),
lineRequest.numLines(), lineRequest.eventBufferSize(), 42);
});
try (var _ = FileDescriptorNativeMock.setup(GPIOCHIP_FILE);
var _ = IoctlNativeMock.setup(lineInfoTestData, lineRequestTestData)) {

var builder = DigitalOutputConfigBuilder.newInstance()
.bus(-1)
.bcm(9)
.initial(DigitalState.HIGH)
.build();
var pin = pi4j0.digitalOutput().create(builder);

assertEquals(DigitalState.HIGH, pin.state());

var request = capturedRequest.get();
assertNotNull(request, "LineRequest was not sent to the kernel");
var config = request.config();
assertEquals(1, config.numAttrs(), "Initial state should add a single output-values attribute");
var attribute = config.attrs()[0];
assertEquals(LineAttributeId.GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES.getValue(), attribute.attr().id());
// bit 0 corresponds to the single requested line at index 0 in the offsets array
assertEquals(1L, attribute.mask());
assertEquals(1L, attribute.attr().values(), "HIGH initial state should set the line's value bit");
}
}

@Test
public void testOutputInitialStateLowPassedToKernel() {
var capturedRequest = new java.util.concurrent.atomic.AtomicReference<LineRequest>();
var lineInfoTestData = new IoctlNativeMock.IoctlTestData(LineInfo.class, (answer) -> {
LineInfo lineInfo = answer.getArgument(2);
return new LineInfo(("Test").getBytes(), ("FFM-Test").getBytes(),
lineInfo.offset(), 0,
PinFlag.OUTPUT.getValue(),
new LineAttribute[0]);
});
var lineRequestTestData = new IoctlNativeMock.IoctlTestData(LineRequest.class, (answer) -> {
LineRequest lineRequest = answer.getArgument(2);
capturedRequest.set(lineRequest);
return new LineRequest(lineRequest.offsets(), lineRequest.consumer(), lineRequest.config(),
lineRequest.numLines(), lineRequest.eventBufferSize(), 42);
});
try (var _ = FileDescriptorNativeMock.setup(GPIOCHIP_FILE);
var _ = IoctlNativeMock.setup(lineInfoTestData, lineRequestTestData)) {

var builder = DigitalOutputConfigBuilder.newInstance()
.bus(-1)
.bcm(10)
.initial(DigitalState.LOW)
.build();
var pin = pi4j0.digitalOutput().create(builder);

assertEquals(DigitalState.LOW, pin.state());

var request = capturedRequest.get();
assertNotNull(request, "LineRequest was not sent to the kernel");
var config = request.config();
assertEquals(1, config.numAttrs(), "Initial state should add a single output-values attribute");
var attribute = config.attrs()[0];
assertEquals(LineAttributeId.GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES.getValue(), attribute.attr().id());
assertEquals(1L, attribute.mask());
assertEquals(0L, attribute.attr().values(), "LOW initial state should clear the line's value bit");
}
}

@Test
public void testOutputWithoutInitialStateHasNoAttributes() {
var capturedRequest = new java.util.concurrent.atomic.AtomicReference<LineRequest>();
var lineInfoTestData = new IoctlNativeMock.IoctlTestData(LineInfo.class, (answer) -> {
LineInfo lineInfo = answer.getArgument(2);
return new LineInfo(("Test").getBytes(), ("FFM-Test").getBytes(),
lineInfo.offset(), 0,
PinFlag.OUTPUT.getValue(),
new LineAttribute[0]);
});
var lineRequestTestData = new IoctlNativeMock.IoctlTestData(LineRequest.class, (answer) -> {
LineRequest lineRequest = answer.getArgument(2);
capturedRequest.set(lineRequest);
return new LineRequest(lineRequest.offsets(), lineRequest.consumer(), lineRequest.config(),
lineRequest.numLines(), lineRequest.eventBufferSize(), 42);
});
try (var _ = FileDescriptorNativeMock.setup(GPIOCHIP_FILE);
var _ = IoctlNativeMock.setup(lineInfoTestData, lineRequestTestData)) {

var builder = DigitalOutputConfigBuilder.newInstance()
.bus(-1)
.bcm(11)
.build();
pi4j0.digitalOutput().create(builder);

var request = capturedRequest.get();
assertNotNull(request, "LineRequest was not sent to the kernel");
assertEquals(0, request.config().numAttrs(),
"Without an initial state no output-values attribute should be sent");
}
}

@Test
public void testApi() {
var lineInfoTestData = new IoctlNativeMock.IoctlTestData(LineInfo.class, (answer) -> {
Expand Down
Loading