org.kegbot.core.hardware.KegboardController.java Source code

Java tutorial

Introduction

Here is the source code for org.kegbot.core.hardware.KegboardController.java

Source

/*
 * Copyright 2014 Bevbot LLC <info@bevbot.com>
 *
 * This file is part of the Kegtab package from the Kegbot project. For
 * more information on Kegtab or Kegbot, see <http://kegbot.org/>.
 *
 * Kegtab is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 2.
 *
 * Kegtab is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with Kegtab. If not, see <http://www.gnu.org/licenses/>.
 */

package org.kegbot.core.hardware;

import android.os.SystemClock;
import android.util.Log;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.util.HexDump;

import org.kegbot.core.FlowMeter;
import org.kegbot.core.ThermoSensor;
import org.kegbot.kegboard.KegboardHelloMessage;
import org.kegbot.kegboard.KegboardMessage;
import org.kegbot.kegboard.KegboardMessageFactory;
import org.kegbot.kegboard.KegboardMeterStatusMessage;
import org.kegbot.kegboard.KegboardPingCommand;
import org.kegbot.kegboard.KegboardSetOutputCommand;
import org.kegbot.kegboard.KegboardTemperatureReadingMessage;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A directly-attached Kegboard (Arduino or Pro Mini) USB controller.
 */
public class KegboardController implements Controller {

    private static final String TAG = KegboardController.class.getSimpleName();

    private static final Pattern SERIAL_RE = Pattern
            .compile("^KB-([0-9a-fA-F]{4})-([0-9a-fA-F]{4})-([0-9a-fA-F]{4,8})$");

    static final String METER_0 = "flow0";
    static final String METER_1 = "flow1";

    private static final String DEFAULT_BOARD_NAME = "kegboard";

    private static final long OUTPUT_REFRESH_INTERVAL = TimeUnit.SECONDS.toMillis(5);

    private static final Pattern PORT_NAME_PATTERN = Pattern.compile("[a-zA-Z]+(\\d+)");

    private final UsbSerialPort mSerialPort;

    private String mStatus = STATUS_UNKNOWN;
    private String mName = DEFAULT_BOARD_NAME;
    private String mSerialNumber = "";

    private final byte[] mReadBuffer = new byte[128];

    private final KegboardMessageFactory mReader = new KegboardMessageFactory();

    private final Map<String, FlowMeter> mFlowMetersByName = Maps.newLinkedHashMap();

    private final Map<String, ThermoSensor> mThermoSensors = Maps.newLinkedHashMap();

    /**
     * Map of toggle number to next desired "enable" refresh time.
     */
    private final Map<Integer, Long> mEnabledOutputsToRefreshUptimeMillis = Maps.newLinkedHashMap();

    /** Keys that were present in #mLastEnabledOutputs during last refreshOutputs call. */
    private final Set<Integer> mLastEnabledOutputs = Sets.newLinkedHashSet();

    public KegboardController(UsbSerialPort port) {
        mSerialPort = port;
    }

    @Override
    public String toString() {
        return String.format("<Kegboard '%s': port=%s serial_number=%s status=%s>", getName(), mSerialPort,
                mSerialNumber, mStatus);
    }

    @Override
    public String getSerialNumber() {
        return Strings.nullToEmpty(mSerialNumber);
    }

    @Override
    public String getDeviceType() {
        if (getSerialNumber().startsWith("KB-01")) {
            return TYPE_KBPM;
        }
        return TYPE_UNKNOWN;
    }

    @Override
    public String getStatus() {
        return mStatus;
    }

    void setStatus(String status) {
        mStatus = status;
    }

    @Override
    public synchronized String getName() {
        return mName;
    }

    @Override
    public Collection<FlowMeter> getFlowMeters() {
        return ImmutableList.copyOf(mFlowMetersByName.values());
    }

    @Override
    public FlowMeter getFlowMeter(final String meterName) {
        return mFlowMetersByName.get(meterName);
    }

    @Override
    public Collection<ThermoSensor> getThermoSensors() {
        return mThermoSensors.values();
    }

    @Override
    public ThermoSensor getThermoSensor(String sensorName) {
        return mThermoSensors.get(sensorName);
    }

    UsbSerialPort getPort() {
        return mSerialPort;
    }

    boolean scheduleToggleOutput(final int outputId, final boolean enable) {
        Log.d(TAG, "scheduleToggleOutput: outputId=" + outputId + " enable=" + enable);
        if (outputId < 0 || outputId >= 4) {
            Log.w(TAG, "Unknown output number: " + outputId);
            return false;
        }
        final Integer outputInteger = Integer.valueOf(outputId);

        synchronized (mEnabledOutputsToRefreshUptimeMillis) {
            if (!enable) {
                mEnabledOutputsToRefreshUptimeMillis.remove(outputInteger);
            } else {
                final Long nextRefresh = Long.valueOf(SystemClock.uptimeMillis() + OUTPUT_REFRESH_INTERVAL);
                mEnabledOutputsToRefreshUptimeMillis.put(outputInteger, nextRefresh);
            }
        }

        return true;
    }

    private void toggleOutput(final int outputId, final boolean enable) throws IOException {
        if (outputId < 0 || outputId >= 4) {
            throw new IOException("Illegal output id.");
        }
        final KegboardSetOutputCommand enableCmd = new KegboardSetOutputCommand(outputId, enable);
        mSerialPort.write(enableCmd.toBytes(), 500);
    }

    void refreshOutputs() throws IOException {
        final long now = SystemClock.uptimeMillis();
        synchronized (mEnabledOutputsToRefreshUptimeMillis) {
            for (final Map.Entry<Integer, Long> entry : mEnabledOutputsToRefreshUptimeMillis.entrySet()) {
                final int outputId = entry.getKey().intValue();
                final long deadline = entry.getValue().longValue();
                if (deadline >= now) {
                    toggleOutput(outputId, true);
                    final Long nextRefresh = Long.valueOf(SystemClock.uptimeMillis() + OUTPUT_REFRESH_INTERVAL);
                    mEnabledOutputsToRefreshUptimeMillis.put(entry.getKey(), nextRefresh);
                }
            }

            final Set<Integer> missing = Sets.newLinkedHashSet(mLastEnabledOutputs);
            missing.removeAll(mEnabledOutputsToRefreshUptimeMillis.keySet());
            if (!missing.isEmpty()) {
                for (final Integer outputId : missing) {
                    Log.d(TAG, "refreshOutputs: missing id " + outputId);
                    toggleOutput(outputId.intValue(), false);
                }
            }

            mLastEnabledOutputs.clear();
            mLastEnabledOutputs.addAll(mEnabledOutputsToRefreshUptimeMillis.keySet());
        }
    }

    void ping() throws IOException {
        final KegboardPingCommand cmd = new KegboardPingCommand();
        mSerialPort.write(cmd.toBytes(), 500);
    }

    synchronized void setSerialNumber(final String serialNumber) {
        if (mSerialNumber.equals(serialNumber)) {
            return;
        }
        if (!mSerialNumber.isEmpty()) {
            Log.w(TAG, String.format("Ignoring serial number change from=%s to=%s.", mSerialNumber, serialNumber));
            return;
        }

        final Matcher matcher = SERIAL_RE.matcher(serialNumber);
        if (matcher.matches()) {
            final String g1 = matcher.group(1);
            final String g2 = matcher.group(2);
            final String g3 = Strings.padStart(matcher.group(3), 8, '0');

            Log.d(TAG, "setSerialNumber: " + mSerialNumber);
            mSerialNumber = String.format("KB-%s-%s-%s", g1, g2, g3);
            mName = getShortNameFromSerialNumber(mSerialNumber);
        } else {
            Log.w(TAG, "Unknown serial number, ignoring: " + serialNumber);
        }
    }

    private static String getShortNameFromSerialNumber(final String serialNumber) {
        final int dashPos = serialNumber.lastIndexOf('-');
        if (dashPos != serialNumber.length() - 9) {
            return DEFAULT_BOARD_NAME;
        }
        return String.format("%s-%s", DEFAULT_BOARD_NAME,
                serialNumber.substring(dashPos + 1).toLowerCase(Locale.US));
    }

    /**
     * Reads and returns a single message.
     */
    KegboardMessage readMessage() {
        final KegboardMessage message = mReader.getMessage();
        if (message != null) {
            handleMessage(message);
        }
        return message;
    }

    List<KegboardMessage> readMessages() {
        final List<KegboardMessage> result = Lists.newArrayList();
        while (true) {
            final KegboardMessage message = readMessage();
            if (message == null) {
                break;
            }
            result.add(message);
        }
        return result;
    }

    void blockingRead() throws IOException {
        final int amtRead = mSerialPort.read(mReadBuffer, Integer.MAX_VALUE);
        if (amtRead < 0) {
            Log.w(TAG, "Error!");
            throw new IOException("Device closed.");
        }
        Log.d(TAG, "Read bytes: " + HexDump.dumpHexString(mReadBuffer, 0, amtRead));
        mReader.addBytes(mReadBuffer, amtRead);
    }

    private void handleMessage(KegboardMessage message) {
        if (message instanceof KegboardHelloMessage) {
            // Update cached serial number.
            final String serialNumber = ((KegboardHelloMessage) message).getSerialNumber();
            if (!serialNumber.isEmpty()) {
                Log.d(TAG, "Updating serial number");
                setSerialNumber(serialNumber);
            }
        } else if (message instanceof KegboardMeterStatusMessage) {
            final KegboardMeterStatusMessage meterStatus = (KegboardMeterStatusMessage) message;

            final String portName = meterStatus.getMeterName();
            final String meterName = String.format("%s.%s", getName(), portName);

            final FlowMeter meter;
            if (mFlowMetersByName.containsKey(meterName)) {
                meter = mFlowMetersByName.get(meterName);
            } else {
                meter = new FlowMeter(meterName);
                mFlowMetersByName.put(meterName, meter);
            }
            meter.setTicks(meterStatus.getMeterReading());
        } else if (message instanceof KegboardTemperatureReadingMessage) {
            final KegboardTemperatureReadingMessage tempMessage = (KegboardTemperatureReadingMessage) message;

            final String name = tempMessage.getName();
            if (!mThermoSensors.containsKey(name)) {
                mThermoSensors.put(name, new ThermoSensor(name));
            }

            final ThermoSensor sensor = mThermoSensors.get(name);
            sensor.setTemperatureC(tempMessage.getValue());
        }
    }

}