/*
* Copyright 2013 Ytai Ben-Tsvi. All rights reserved.
*
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied.
*/
package ioio.lib.impl;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList;
import java.util.List;
import ioio.lib.api.DigitalInput;
import ioio.lib.api.DigitalOutput;
import ioio.lib.api.Sequencer;
import ioio.lib.api.exception.ConnectionLostException;
import ioio.lib.impl.IncomingState.SequencerEventListener;
import ioio.lib.impl.InterruptibleQueue.Nudged;
import ioio.lib.impl.ResourceManager.Resource;
import ioio.lib.impl.ResourceManager.ResourceType;
import ioio.lib.spi.Log;
public class SequencerImpl extends AbstractResource implements Sequencer, SequencerEventListener {
private static final String TAG = "SequencerImpl";
private enum RemoteState {
IDLE_OR_MANUAL, RUNNING, STALLED, CLOSED
}
private enum LocalState {
IDLE, RUNNING, MANUAL, CLOSED
}
// A mapping between the ChannelConfig sub-types to their respective
// Channel sub-types.
private static class ClassMapEntry {
public final Class<? extends ChannelConfig> configClass;
public final Class<? extends Channel> channelClass;
public ClassMapEntry(Class<? extends ChannelConfig> configClass,
Class<? extends Channel> channelClass) {
this.configClass = configClass;
this.channelClass = channelClass;
}
}
private static final ClassMapEntry[] CLASS_MAP = {
new ClassMapEntry(ChannelConfigBinary.class, ChannelBinary.class),
new ClassMapEntry(ChannelConfigPwmPosition.class, ChannelPwmPosition.class),
new ClassMapEntry(ChannelConfigPwmSpeed.class, ChannelPwmSpeed.class),
new ClassMapEntry(ChannelConfigFmSpeed.class, ChannelFmSpeed.class),
new ClassMapEntry(ChannelConfigSteps.class, ChannelSteps.class)};
private final Channel[] channels_;
private final List<Resource> pins_ = new LinkedList<ResourceManager.Resource>();
private final List<Resource> ocs_ = new LinkedList<ResourceManager.Resource>();
private final Resource sequencer_ = new Resource(ResourceType.SEQUENCER);
int availableSlots_ = 0;
int numCuesStarted_ = 0;
private byte[] serializedBuf_ = new byte[68];
private LocalState localState_ = LocalState.IDLE;
private RemoteState remoteState_ = RemoteState.CLOSED;
private InterruptibleQueue<Event> eventQueue_ = new InterruptibleQueue<Sequencer.Event>(32);
private Event lastEvent_ = new Event(Event.Type.CLOSED, 0);
public SequencerImpl(IOIOImpl ioio, ChannelConfig[] config) throws ConnectionLostException {
super(ioio);
if (config.length > 32) {
throw new IllegalArgumentException("Up to 32 channels are supported.");
}
// Traverse config, create channels and an array of resources to
// allocate.
channels_ = new Channel[config.length];
for (int i = 0; i < config.length; ++i) {
channels_[i] = createChannel(config[i]);
}
// Allocate resources.
ioio_.resourceManager_.alloc(pins_, ocs_, sequencer_);
// Register for events, before they start coming.
ioio_.incomingState_.addSequencerEventListener(this);
ioio_.incomingState_.addDisconnectListener(this);
// Protocol (pins, sequencer).
for (Channel ch : channels_) {
ch.openPins();
}
openSequencer();
}
@Override
public synchronized void push(ChannelCue[] cues, int duration) throws ConnectionLostException,
InterruptedException {
checkState();
if (duration < 2 || duration > (1 << 16)) {
throw new IllegalArgumentException("Duration must be in the range [2..65536]");
}
while (availableSlots_ == 0) {
wait();
}
try {
final int size = serializeCues(cues, serializedBuf_);
ioio_.protocol_.sequencerPush(duration - 1, serializedBuf_, size);
--availableSlots_;
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
@Override
public synchronized void manualStart(ChannelCue[] cues) throws ConnectionLostException {
checkState();
if (localState_ == LocalState.RUNNING) {
throw new IllegalStateException("manualStart() may not be called when running.");
}
try {
final int size = serializeCues(cues, serializedBuf_);
ioio_.protocol_.sequencerManualStart(serializedBuf_, size);
localState_ = LocalState.MANUAL;
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
@Override
public synchronized void manualStop() throws ConnectionLostException {
checkState();
if (localState_ == LocalState.RUNNING) {
throw new IllegalStateException("manualStop() may not be called when running.");
}
if (localState_ == LocalState.MANUAL) {
try {
ioio_.protocol_.sequencerManualStop();
localState_ = LocalState.IDLE;
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
}
@Override
public synchronized void start() throws ConnectionLostException {
checkState();
if (localState_ != LocalState.IDLE) {
throw new IllegalStateException("start() may only be called when idle.");
}
try {
ioio_.protocol_.sequencerStart();
localState_ = LocalState.RUNNING;
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
@Override
public synchronized void stop() throws ConnectionLostException {
checkState();
try {
ioio_.protocol_.sequencerStop();
localState_ = LocalState.IDLE;
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
@Override
public synchronized void pause() throws ConnectionLostException {
checkState();
if (localState_ != LocalState.RUNNING) {
throw new IllegalStateException("pause() may only be called when running.");
}
try {
ioio_.protocol_.sequencerPause();
localState_ = LocalState.IDLE;
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
@Override
public synchronized int available() {
return availableSlots_;
}
@Override
public synchronized void close() {
checkClose();
try {
// Send the protocol message to close the sequencer.
closeSequencer();
// Send the protocol messages to close the pins.
for (Resource pin : pins_) {
ioio_.protocol_.setPinDigitalIn(pin.id, DigitalInput.Spec.Mode.FLOATING);
}
// Wait until the IOIO acknowledges the close. Unlike most other interfaces, this is
// important, since the close command send to the IOIO here is asynchronous, so we do
// not want to free the resources or exit the call before the close is complete.
waitRemoteState(RemoteState.CLOSED);
// Free resources.
ioio_.resourceManager_.free(pins_, ocs_, sequencer_);
} catch (Exception e) {
} finally {
super.close();
}
}
@Override
public synchronized void opened(int arg) {
availableSlots_ = arg;
remoteState_ = RemoteState.IDLE_OR_MANUAL;
pushEvent(Event.Type.STOPPED);
notifyAll();
}
@Override
public synchronized void stalled() {
remoteState_ = RemoteState.STALLED;
pushEvent(Event.Type.STALLED);
notifyAll();
}
@Override
public synchronized void nextCue() {
++availableSlots_;
++numCuesStarted_;
pushEvent(Event.Type.CUE_STARTED);
notifyAll();
}
@Override
public synchronized void paused() {
remoteState_ = RemoteState.IDLE_OR_MANUAL;
pushEvent(Event.Type.PAUSED);
notifyAll();
}
@Override
public synchronized void stopped(int arg) {
availableSlots_ += arg;
numCuesStarted_ = 0;
remoteState_ = RemoteState.IDLE_OR_MANUAL;
pushEvent(Event.Type.STOPPED);
notifyAll();
}
@Override
public synchronized void closed() {
remoteState_ = RemoteState.CLOSED;
pushEvent(Event.Type.CLOSED);
notifyAll();
}
private void closeSequencer() throws ConnectionLostException {
try {
ioio_.protocol_.sequencerClose();
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
private void openSequencer() throws ConnectionLostException {
try {
int offset = 0;
for (Channel ch : channels_) {
offset = ch.serializeConfig(serializedBuf_, offset);
}
ioio_.protocol_.sequencerOpen(serializedBuf_, offset);
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
private synchronized void waitRemoteState(RemoteState target) throws InterruptedException,
ConnectionLostException {
while (remoteState_ != target) {
safeWait();
}
}
// /////////////////////////////////////////////////////////////////////////
// Configuration wrappers (serializers).
private static int convertClock(Clock clk) {
switch (clk) {
case CLK_16M:
return 0;
case CLK_2M:
return 1;
case CLK_250K:
return 2;
case CLK_62K5:
return 3;
default:
return -1;
}
}
private int serializeCues(ChannelCue[] cues, byte[] buf) {
if (cues.length != channels_.length) {
throw new IllegalArgumentException("Wrong number of channels.");
}
int offset = 0;
int i = 0;
for (Channel ch : channels_) {
offset = ch.serializeCue(cues[i++], buf, offset);
}
return offset;
}
private Channel createChannel(ChannelConfig config) throws ConnectionLostException {
for (ClassMapEntry mapping : CLASS_MAP) {
if (mapping.configClass.isInstance(config)) {
Constructor<?> ctor;
try {
ctor = mapping.channelClass.getConstructor(SequencerImpl.class,
mapping.configClass);
return (Channel) ctor.newInstance(this, config);
} catch (InvocationTargetException e) {
try {
throw e.getCause();
} catch (RuntimeException cause) {
throw cause;
} catch (ConnectionLostException cause) {
throw cause;
} catch (Throwable cause) {
Log.e(TAG, "Unexpected exception caught.", e);
}
} catch (Exception e) {
Log.e(TAG, "Unexpected exception caught.", e);
}
}
}
throw new IllegalArgumentException("Unsupported config type: "
+ config.getClass().getName());
}
private static interface Channel {
int serializeConfig(byte[] buf, int offset);
int serializeCue(ChannelCue cue, byte[] buf, int offset);
void openPins() throws ConnectionLostException;
}
private abstract class ChannelOutCompare implements Channel {
protected final Resource oc_;
private final DigitalOutput.Spec[] specs_;
protected ChannelOutCompare(DigitalOutput.Spec[] specs) {
specs_ = specs;
oc_ = new Resource(ResourceType.OUTCOMPARE);
ocs_.add(oc_);
for (DigitalOutput.Spec spec : specs) {
ioio_.hardware_.checkSupportsPeripheralOutput(spec.pin);
pins_.add(new Resource(ResourceType.PIN, spec.pin));
}
}
@Override
public final void openPins() throws ConnectionLostException {
try {
for (DigitalOutput.Spec spec : specs_) {
ioio_.protocol_.setPinDigitalOut(spec.pin, false, spec.mode);
ioio_.protocol_.setPinPwm(spec.pin, oc_.id, true);
}
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
}
private class ChannelPwmPosition extends ChannelOutCompare {
private final ChannelConfigPwmPosition cfg_;
public ChannelPwmPosition(ChannelConfigPwmPosition cfg) {
super(cfg.pinSpec);
cfg_ = cfg;
}
@Override
public int serializeConfig(byte[] buf, int offset) {
final int encPulseWidth = cfg_.initialPulseWidth == 0 ? 0 : cfg_.initialPulseWidth - 1;
final int encPeriod = cfg_.period - 1;
buf[offset++] = 0;
buf[offset++] = (byte) ((oc_.id & 0x0F) | (convertClock(cfg_.clk) << 4));
buf[offset++] = (byte) ((encPeriod >> 0) & 0xFF);
buf[offset++] = (byte) ((encPeriod >> 8) & 0xFF);
buf[offset++] = (byte) ((encPulseWidth >> 0) & 0xFF);
buf[offset++] = (byte) ((encPulseWidth >> 8) & 0xFF);
return offset;
}
@Override
public int serializeCue(ChannelCue cue, byte[] buf, int offset) {
if (!(cue instanceof ChannelCuePwmPosition)) {
throw new IllegalArgumentException("Wrong cue type.");
}
ChannelCuePwmPosition pwm = (ChannelCuePwmPosition) cue;
if (pwm.pulseWidth != 0 && (pwm.pulseWidth < 2 || pwm.pulseWidth > (1 << 16))) {
throw new IllegalArgumentException("Pulse width must be 0 or between [2..65536]");
}
final int encPw = pwm.pulseWidth == 0 ? 0 : pwm.pulseWidth - 1;
buf[offset++] = (byte) ((encPw >> 0) & 0xFF);
buf[offset++] = (byte) ((encPw >> 8) & 0xFF);
return offset;
}
}
private class ChannelPwmSpeed extends ChannelOutCompare {
private final ChannelConfigPwmSpeed cfg_;
public ChannelPwmSpeed(ChannelConfigPwmSpeed cfg) {
super(cfg.pinSpec);
cfg_ = cfg;
}
@Override
public int serializeConfig(byte[] buf, int offset) {
final int encPulseWidth = cfg_.initialPulseWidth == 0 ? 0 : cfg_.initialPulseWidth - 1;
final int encPeriod = cfg_.period - 1;
buf[offset++] = 1;
buf[offset++] = (byte) ((oc_.id & 0x0F) | (convertClock(cfg_.clk) << 4));
buf[offset++] = (byte) ((encPeriod >> 0) & 0xFF);
buf[offset++] = (byte) ((encPeriod >> 8) & 0xFF);
buf[offset++] = (byte) ((encPulseWidth >> 0) & 0xFF);
buf[offset++] = (byte) ((encPulseWidth >> 8) & 0xFF);
return offset;
}
@Override
public int serializeCue(ChannelCue cue, byte[] buf, int offset) {
if (!(cue instanceof ChannelCuePwmSpeed)) {
throw new IllegalArgumentException("Wrong cue type.");
}
ChannelCuePwmSpeed pwm = (ChannelCuePwmSpeed) cue;
if (pwm.pulseWidth != 0 && (pwm.pulseWidth < 2 || pwm.pulseWidth > (1 << 16))) {
throw new IllegalArgumentException("Pulse width must be 0 or between [2..65536]");
}
final int encPw = pwm.pulseWidth == 0 ? 0 : pwm.pulseWidth - 1;
buf[offset++] = (byte) ((encPw >> 0) & 0xFF);
buf[offset++] = (byte) ((encPw >> 8) & 0xFF);
return offset;
}
}
private class ChannelFmSpeed extends ChannelOutCompare {
private final ChannelConfigFmSpeed cfg_;
public ChannelFmSpeed(ChannelConfigFmSpeed cfg) {
super(cfg.pinSpec);
cfg_ = cfg;
}
@Override
public int serializeConfig(byte[] buf, int offset) {
final int encPulseWidth = cfg_.pulseWidth - 1;
buf[offset++] = 2;
buf[offset++] = (byte) ((oc_.id & 0x0F) | (convertClock(cfg_.clk) << 4));
buf[offset++] = (byte) ((encPulseWidth >> 0) & 0xFF);
buf[offset++] = (byte) ((encPulseWidth >> 8) & 0xFF);
return offset;
}
@Override
public int serializeCue(ChannelCue cue, byte[] buf, int offset) {
if (!(cue instanceof ChannelCueFmSpeed)) {
throw new IllegalArgumentException("Wrong cue type.");
}
ChannelCueFmSpeed fm = (ChannelCueFmSpeed) cue;
if (fm.period < 0 || fm.period > (1 << 16)) {
throw new IllegalArgumentException("Period must be between [0..65536]");
}
final int encPeriod = fm.period < 2 ? fm.period : fm.period - 1;
buf[offset++] = (byte) ((encPeriod >> 0) & 0xFF);
buf[offset++] = (byte) ((encPeriod >> 8) & 0xFF);
return offset;
}
}
private class ChannelSteps extends ChannelOutCompare {
public ChannelSteps(ChannelConfigSteps cfg) {
super(cfg.pinSpec);
}
@Override
public int serializeConfig(byte[] buf, int offset) {
buf[offset++] = 3;
buf[offset++] = (byte) ((oc_.id & 0x0F));
return offset;
}
@Override
public int serializeCue(ChannelCue cue, byte[] buf, int offset) {
if (!(cue instanceof ChannelCueSteps)) {
throw new IllegalArgumentException("Wrong cue type.");
}
ChannelCueSteps steps = (ChannelCueSteps) cue;
if (steps.pulseWidth < 0 || steps.pulseWidth > steps.period / 2) {
throw new IllegalArgumentException(
"Pulse width must be 0 or between [0..floor(period/2)]");
}
if (steps.period < 3 || steps.period > (1 << 16)) {
throw new IllegalArgumentException("Period must be between [3..65536]");
}
final int pwEnc = steps.pulseWidth; // No -1 here on purpose!
final int periodEnd = steps.period - 1;
buf[offset++] = (byte) convertClock(steps.clk);
buf[offset++] = (byte) ((pwEnc >> 0) & 0xFF);
buf[offset++] = (byte) ((pwEnc >> 8) & 0xFF);
buf[offset++] = (byte) ((periodEnd >> 0) & 0xFF);
buf[offset++] = (byte) ((periodEnd >> 8) & 0xFF);
return offset;
}
}
private class ChannelBinary implements Channel {
private final ChannelConfigBinary cfg_;
private final Resource pin_;
@SuppressWarnings("unused")
public ChannelBinary(ChannelConfigBinary cfg) {
cfg_ = cfg;
pin_ = new Resource(ResourceType.PIN, cfg.pinSpec.pin);
pins_.add(pin_);
}
@Override
public int serializeConfig(byte[] buf, int offset) {
buf[offset++] = 0x04;
buf[offset++] = (byte) ((cfg_.pinSpec.pin & 0x3F) | (cfg_.initialValue ? (1 << 6) : 0) | (cfg_.initWhenIdle ? (1 << 7)
: 0));
return offset;
}
@Override
public int serializeCue(ChannelCue cue, byte[] buf, int offset) {
if (!(cue instanceof ChannelCueBinary)) {
throw new IllegalArgumentException("Wrong cue type.");
}
ChannelCueBinary bin = (ChannelCueBinary) cue;
buf[offset++] = (byte) (bin.value ? 0x01 : 0x00);
return offset;
}
@Override
public void openPins() throws ConnectionLostException {
try {
ioio_.protocol_.setPinDigitalOut(cfg_.pinSpec.pin, cfg_.initialValue,
cfg_.pinSpec.mode);
} catch (IOException e) {
throw new ConnectionLostException(e);
}
}
}
@Override
public Event getLastEvent() throws ConnectionLostException {
checkState();
return lastEvent_;
}
@Override
public Event waitEvent() throws ConnectionLostException, InterruptedException {
checkState();
while (true) {
try {
return eventQueue_.pull();
} catch (Nudged e) {
checkState();
}
}
}
@Override
public void waitEventType(Event.Type type) throws ConnectionLostException, InterruptedException {
while (waitEvent().type != type)
;
}
@Override
public synchronized void setEventQueueSize(int size) throws ConnectionLostException {
checkState();
if (size <= 0) {
throw new IllegalArgumentException("Event queue size must be positive.");
}
InterruptibleQueue<Event> prevQueue = eventQueue_;
eventQueue_ = new InterruptibleQueue<Event>(size);
prevQueue.nudge();
}
private void pushEvent(Event.Type t) {
lastEvent_ = new Event(t, numCuesStarted_);
eventQueue_.pushDiscardingOld(lastEvent_);
}
}