Java tutorial
/*** Java-ERI Java-based Embedded Railroad Interfacing. *** Copyright (C) 2014 in USA by Brian Witt , bwitt@value.net *** *** Licensed under the Apache License, Version 2.0 ( the "License" ) ; *** you may not use this file except in compliance with the License. *** You may obtain a copy of the License at: *** http://www.apache.org/licenses/LICENSE-2.0 *** *** Unless required by applicable law or agreed to in writing, software *** distributed under the License is distributed on an "AS IS" BASIS, *** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *** See the License for the specific languatge governing permissions and *** limitations under the License. ***/ package org.embeddedrailroad.eri.layoutio.cmri; import com.crunchynoodles.util.SynchronizedByteBuffer; import java.lang.Thread.UncaughtExceptionHandler; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.concurrent.Future; import com.google.common.collect.Queues; import gnu.io.SerialPort; import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; import gnu.io.UnsupportedCommOperationException; import org.embeddedrailroad.eri.layoutio.LayoutTimeoutManager; /*** * Provides a state-machine to poll units on a CMRI bank via some comms-channel. * Model provides initialize strings for units, and a place to dump input-data updates. * Instance has a little worker-thread running in a local-class-instance. * <p> * Note: Almost all the public methods are {@code synchronized}. * Happily, these methods can call other {@code synchronized} methods of the same class. * That said, please don't use instances of {@link CmriPollMachine} for locking in a client class. * The {@code synchronized} methods already to that! * * @author brian */ public class CmriPollMachine implements UncaughtExceptionHandler { /*** * Create a polling machine that contains a little worker thread to do the actual work. * The port has already been setup for baud, stop bits, parity, etc. * * @param serial Serial port object , the channel * @param baudRate baud rate, information only. * @param model "Layout Model" to hold unit IO states. */ public CmriPollMachine(SerialPort serial, int baudRate, CmriLayoutModelImpl model) { this.m_model = model; this.m_recovery_rate = CmriSerialLayoutTransport.DEFAULT_DISCOVERY_RATE; this.m_port = serial; this.m_baud_rate = baudRate; this.m_consecutive_missed_polls = new int[CMRI_HIGHEST_POLL_ADDR + 1]; // ``"Synchronized" [collection] classes can be useful when you need to prevent all access to // a collection via a single lock, at the expense of poorer scalability.'' this.m_active_queue = Queues.synchronizedQueue(new java.util.LinkedList<Integer>()); this.m_revive_queue = Queues.synchronizedQueue(new java.util.LinkedList<Integer>()); } //---------------------------- BEAN THINGS ------------------------------ /*** * Maximum time, in milliseconds, to wait for first char in response from a unit after * a query message has been sent out. * If the first char isn't received in said microseconds, the unit is declared "non-responding." * * @param millisecs max wait time after transmitting query. */ public void setRxTurnaroundTimeout(int millisecs) { m_response_wait = millisecs; } /*** * Time to wait for first char of response to appear from unit after the entire request * message has been sent out the serial port. * @return milliseconds to wait after request has been fully transmitted. */ public int getRxTurnaroundTimeout() { return this.m_response_wait; } /*** * Return the timeout in milliseconds to wait for first byte of a response starting * from when the first byte is sent out. * Takes size of TX packet, which assumes it is buffered by OS and is right now * being sent out. * In modern OSes, the serial or USB driver has buffered the packet to send. * * @param sizeSent how many bytes will be sent. * @return milliseconds to wait, based on {@link #setRxTurnaroundTimeout(int) } value. */ public int getTxToFirstRxTimeout(int sizeSent) { int wait_xmit = (int) (((sizeSent + 2) / (m_baud_rate / 10.0)) * 1000); int wait_rec = getRxTurnaroundTimeout(); return wait_xmit + wait_rec; } /*** * Set recovery rate to re-try units that are not responding. * After every poll cycle, the recovery rate is added to an accumulator. * Once 1.0 is reached ( or exceeded ), communication with a non-communicating unit is attempted. * * @param rr value more than 0.0 but not above 1.0. * @throws IllegalArgumentException if {@link rr} less-than or equal to 0, or above 1.0 */ public void setRecoveryRate(double rr) { if (rr <= 0.0 || rr > 1.0) throw new IllegalArgumentException("recovery rate must be (0.0 .. 1.0]"); this.m_recovery_rate = (float) rr; } /*** * Return recovery rate for how often to attempt and revive a non-communicating unit. * * @return value more than 0.0 but not above 1.0. * @see CmriSerialLayoutTransport.DEFAULT_DISCOVERY_RATE */ public float getRecoveryRate() { return this.m_recovery_rate; } /*** * Put some unit on the revival work list. * If currently polling, then unit is demoted to re-initialization. * E.g. adding the same unit-address twice, will first put it on the poll list, * and then move to the "revive list" on the second call here. * * @param unitAddr */ public synchronized void addUnitToPollingList(int unitAddr) { if (m_active_queue.contains(unitAddr)) { m_active_queue.remove(unitAddr); } if (!m_revive_queue.contains(unitAddr)) { m_revive_queue.add(unitAddr); } } /*** * Stop talking with some unit in the bank. * Removes unit-address from both the active polling list and the revive list. * * @param unitAddr which to remove */ public synchronized void removeUnitFromPollingList(int unitAddr) { m_active_queue.remove(unitAddr); m_revive_queue.remove(unitAddr); } /*** * Return set of known unit addresses. * Normally, list is just units that are actively communicating. * * @param includeNonResponding include those not responding * @return Set of unit addresses. */ public synchronized Set<Integer> getKnownUnits(boolean includeNonResponding) { HashSet<Integer> known = new HashSet<Integer>(); known.addAll(m_active_queue); if (includeNonResponding) { known.addAll(m_revive_queue); } return (known); } /*** * Count another missed response from a unit ; too many and try to re-INIT unit * @param addr polling address, 0 to 255 */ private void _missedPoll(int addr) { m_consecutive_missed_polls[addr] += 1; if (m_consecutive_missed_polls[addr] > PROP_GONE_TOO_LONG_REINIT) { LOG.log(Level.WARNING, "Unit #{0} hasn't responded in a while, will re-INIT.", addr); m_consecutive_missed_polls[addr] = 0; // Since unit it on polling list, call addUnitToReviveList() will demote to revive list. addUnitToPollingList(addr); } } //------------------------ WORKER THREAD SUPPORT ------------------------ /*** * Check is polling is enabled. There is a slight delay between "start now" request * and when the worker thread does its first poll. * @return {@code true} if ought to be polling, {@code false} otherwise. */ public boolean isPolling() { return this.m_polling; } /*** * Start or stop polling, on first start creates the worker thread. * Creation is automatic; however, for worker-thread exit-join, the client must call {@link #shutdown()} * explicitly. * * @param goPoll true to start, false to stop. */ public synchronized void setPolling(boolean goPoll) { if (goPoll != m_polling) { // On change, either start or stop the worker thread that does the polling. if (goPoll) { if (m_worker == null) { m_worker = new CmriSerialPollingWorker(); } if (m_thread == null) { m_thread = new Thread(m_worker); m_thread.setDaemon(true); m_thread.setName(m_worker.getClass().getSimpleName() + " on " + m_port.getName()); m_thread.setUncaughtExceptionHandler(this); m_thread.start(); } m_worker.setOkToPoll(true); } else { m_worker.setOkToPoll(false); } // It changed, so update... m_polling = goPoll; } } /*** * Stop polling and kill the polling thread, which shuts down this transport. * Stop any worker thread from polling and then do {@link Thread.join()} to reclaim it. * Will wait just a few seconds for worker-thread to exit itself before returning to caller. */ public synchronized void shutdown() { // Stop polling, just in case... if (m_polling == true) { setPolling(false); } if (m_thread != null) { try { m_worker.stopWorkerThread(); m_thread.join(3500); // 3500 = 3.5 seconds. if (m_thread.isAlive()) { // Hmmm... it didn't stop for use gently, so send INTR and wait again. // See http://docs.oracle.com/javase/tutorial/essential/concurrency/simple.html m_thread.interrupt(); m_thread.join(3000); } } catch (InterruptedException ex) { m_thread.interrupt(); } m_worker = null; m_thread = null; } } /*** * Catch-before-falling our worker thread. * Method invoked when the given thread terminates due to the * given uncaught exception. * * <p>"Any exception thrown by this method will be ignored by the * Java Virtual Machine." * * @param t the thread * @param e the exception that was not caught by the thread itself. */ @Override public void uncaughtException(Thread t, Throwable e) { if (t == m_thread) { shutdown(); } } //--------------------- POLLING MACHINE (INTERNAL) ---------------------- /*** * Class-type that runs state-machine to poll units in a bank. * * <p> Note: By Java semantics, inner-class cannot have {@code static} methods. * * <p> {@link Runnable} : See http://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html */ class CmriSerialPollingWorker implements SerialPortEventListener, Runnable, LayoutTimeoutManager.TimeoutAction { public CmriSerialPollingWorker() { m_in_mesg = new SynchronizedByteBuffer(4000 * 2); m_timeout_mgr = LayoutTimeoutManager.getInstance(); } //-------------------- SerialPortEventListener ---------------------- /*** * Handle rxtx serial events. Dispatches the event to event-specific methods. * * <p> See: http://www.codeproject.com/Questions/450480/How-communicate-with-serial-port-in-Java * * @param event The serial event */ @Override public void serialEvent(SerialPortEvent event) { switch (event.getEventType()) { case SerialPortEvent.DATA_AVAILABLE: try { while (m_instr.available() > 0) { m_in_mesg.add((byte) m_instr.read()); } } catch (IOException ex) { LOG.log(Level.INFO, "CmriSerialPollingWorker#serialEvent() found EOF."); m_in_mesg.add(CH_ERROR_BYTE); } break; // "OUTPUT_BUFFER_EMPTY is an optional event type. Well hidden in the // documentation Sun states that not all JavaComm implementations // support generating events of this type." case SerialPortEvent.OUTPUT_BUFFER_EMPTY: break; case SerialPortEvent.FE: case SerialPortEvent.OE: case SerialPortEvent.PE: // reception trouble.... m_in_mesg.add(CH_ERROR_BYTE); m_cntr_bad_bytes_in += 1; break; default: // modem change, ring indicator, etc... Ignored for CMRI. break; } } //---------------------- interface Runnable ------------------------- /*** * Run state machine to communicate with CMRI units, until told to stop or is interrupted. * * <p> * See http://stackoverflow.com/questions/12916580/why-arent-my-threads-timing-out-when-they-fail?rq=1 * for good practices about using {@code Thread.currentThread().isInterrupted()}. */ @Override public void run() { LOG.log(Level.INFO, "Thread #{0} starting on " + m_port.toString() + " ...", Long.toString(Thread.currentThread().getId())); m_in_mesg.setMatchFirstByte(CMRI_CH_STX); // // "Open the input Reader and output stream. The choice of a // Reader and Stream are arbitrary and need to be adapted to // the actual application. Typically one would use Streams in // both directions, since they allow for binary data transfer, // not only character data transfer. // // "Since the main operation when using a modem is to transfer data // unaltered, the communication with the modem should be handled via // InputStream/OutputStream, and not a Reader/Writer." // try { m_instr = m_port.getInputStream(); m_outstr = m_port.getOutputStream(); } catch (IOException ex) { throw new RuntimeException("Cannot get inputStream or outputStream on COM-port, not expected.", ex); } try { m_port.addEventListener(this); } catch (TooManyListenersException ex) { throw new RuntimeException("Listener already installed, not expected.", ex); } // We're interested in receive-type errors: m_port.notifyOnFramingError(true); m_port.notifyOnOverrunError(true); m_port.notifyOnParityError(true); // // Enable the events we are interested in // m_port.notifyOnDataAvailable(true); m_port.notifyOnOutputEmpty(true); try { m_port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); } catch (UnsupportedCommOperationException ex) { } // With accumulation, once level reaches 1.0, then try one revival. float recovering = 0.0f; // We're open for business! m_port.setDTR(true); m_port.setRTS(true); try { while (!m_do_thread_exit) { Thread.sleep(PROP_INTER_UNIT_SILENCE); if (!m_OK_to_poll_units) continue; // 1. Check for reviving units. if (recovering <= 1.0f && m_revive_queue.size() > 0) { recovering += m_recovery_rate; if (recovering >= 1.0f) { int addr = m_revive_queue.remove(); if (recoverUnit(addr)) { // Yup, got it going. Move to active queue. m_active_queue.add(addr); Thread.sleep(PROP_INTER_UNIT_SILENCE); } else { // Sadness, no response. Put on end of non-responding queue // for another query in a little bit. m_revive_queue.add(addr); } // We tried one. Reset accumulator, which gives duration between attempts. recovering -= 1.0f; } } // 2. Poll entire active list. int len = m_active_queue.size(); while (--len >= 0 && !m_do_thread_exit) { int addr = m_active_queue.remove(); if (queryResponseUnit(addr)) { // Still communicating, keep it around. m_active_queue.add(addr); // Slight pause between units of the poll cycle. Thread.sleep(PROP_INTER_UNIT_SILENCE); } else { // Sadness, it went silent on us. m_revive_queue.add(addr); Thread.yield(); } } // while active units to deal with.. } // while running... } catch (InterruptedException ex) { // we are done, OK to exit thread. LOG.log(Level.INFO, "worker thread done", ex); } finally { LOG.log(Level.INFO, "Thread #{0} exiting...", Long.toString(Thread.currentThread().getId())); } } /*** * Attempt to recover a unit, which for CMRI means programming in its configuration * and trying to get a response of some sort. * Sometimes there is no way to validate the existence of a unit, e.g. it has no inputs to query. * In that case, we assume the unit is functioning. * * @param addr unit's poll address, typically 0 to 127 , but not range checked. * @return true if unit communicated back, else false when no positive response. */ protected boolean recoverUnit(int addr) { ArrayList<byte[]> inits = m_model.getUnitInitializationStrings(addr); if (inits == null) { LOG.log(Level.WARNING, "Want to revive unit #{0} but have no init-message.", addr); return false; } try { drainReceivePort(); for (byte[] m : inits) { // There is no response for CMRI INIT message, so use sendCmriMessage() // method directly. sendCmriMessage(addr, m); } } catch (IOException ex) { throw new RuntimeException("COM-port fail during recoverUnit().", ex); } return false; } /**** * Query a unit for changed inputs; if unit has no inputs then CMRI has no "idling response" so * just assume the unit is functioning. * * @param addr unit's poll address, typically 0 to 127 , but not range checked. * @return true if unit communicated back or none expected, else false when no positive response. */ protected boolean queryResponseUnit(int addr) throws InterruptedException { Future timeout_future = null; try { m_in_mesg.setEnabled(true); int chars_sent = sendCmriMessage(addr, null); // Await first two bytes back from unit, which must match STX and unit's poll address // Compute timeou assuming whole TX packet has been buffered by OS, so must wait // for it to be fully sent, then a pause while th eunit interprets it, then // time for 4 bytes to be sent back: FF, FF, STX, "addr" if (m_in_mesg.awaitCountAtLeast(2, getTxToFirstRxTimeout(chars_sent + 2))) { // Timeout waiting for first byte of response. LOG.log(Level.FINE, "Timeout waiting for STX + poll-address response."); _missedPoll(addr); return false; } m_rx_timeout = false; timeout_future = this.m_timeout_mgr.timeoutMillis(getTxToFirstRxTimeout(chars_sent), this, null); // TODO: we got STX + UA back from unit. Must probe the message to determine // its length. Might have to actually parse as we go for fastest response. try { synchronized (timeout_future) { timeout_future.wait(); } } catch (InterruptedException ex) { // Oh dear, timeout before whole packet received. } m_consecutive_missed_polls[addr] = 0; return true; } catch (IOException ex) { } finally { m_in_mesg.setEnabled(false); if (timeout_future != null && !timeout_future.isDone()) timeout_future.cancel(true); } _missedPoll(addr); return false; } @Override public void onTimeout(Object anchor) { m_rx_timeout = true; } //---------------------- MESSAGES METHODS ------------------------ /*** * Send out a CMRI message to some unit; framing, checksum generation and escaping are done here. * * @param addr unit poll address * @param mesg bytes of message, at least 1 byte which holds the command byte. * * @return Number of bytes sent out. * @throws IOException for general output issues. * @throws IllegalArgumentException if {@link mesg} is null or has zero size. */ private int sendCmriMessage(int addr, byte[] mesg) throws IOException, IllegalArgumentException { if (mesg == null || mesg.length < 1) throw new IllegalArgumentException("CMRI message must be at least 1 byte: the command."); m_outstr.write(CMRI_HEADER_BYTES); // UA is sent as an ASCII letter on wire. m_outstr.write((byte) (addr + CMRI_ADDR_OFFSET)); int cntr_escapes = 0; // Ecape any bytes as they are sent. UA is not escaped. for (byte b : mesg) { if (b == CMRI_CH_STX || b == CMRI_CH_ETX || b == CMRI_CH_ESCAPE) { cntr_escapes++; m_outstr.write(CMRI_CH_ESCAPE); } m_outstr.write(b); } // Drain any input just before sending ETX. This ensures we're ready to RX! drainReceivePort(); m_outstr.write(CMRI_TRAILER_BYTES); int count = CMRI_HEADER_BYTES.length + 1 + mesg.length + cntr_escapes + CMRI_TRAILER_BYTES.length; m_cntr_good_bytes_out += count; return (count); } private void receiveMessage(int bytesSent) { } /*** * Make any pending RX bytes go away, but does so at at most a few seconds. * @throws IOException when stream has been closed. */ private void drainReceivePort() throws IOException { // wait at most 'PROP_DRAIN_PORT_MAX_WAIT_SECONDS' seconds, in 5 micro-second units. int max_waits = (int) (200 * 1000 * PROP_DRAIN_PORT_MAX_WAIT_SECONDS); while (--max_waits >= 0 && m_instr.available() > 0) { // do a short sleep first. try { Thread.sleep(0, 5 * 1000); // sleep 5 microseconds, in nanos. } catch (InterruptedException ex) { Thread.currentThread().interrupt(); return; } m_instr.read(); } m_in_mesg.clear(); } //-------------------------- HELPER METHODS ------------------------- /*** * Sleep a short time, in milliseconds, with quick return if interrupted which causes * the worker thread to exit. * Positive value does a sleep ; zero will yield the thread's time slice. * * <p> If the thread is interrupted, then that exception is re-thrown. * * @param milliseconds milliseconds of sleep, or 0 to just give up thread's time slice. */ public void shortSleep(int milliseconds) { if (m_do_thread_exit) { // Time to exit thread, signal interrupt to start the duty. Thread.currentThread().interrupt(); return; } if (milliseconds > 0) { try { Thread.sleep(milliseconds); } catch (InterruptedException ex) { // re-signal, so maybe thread will exit. Thread.currentThread().interrupt(); } } else if (milliseconds == 0) { Thread.yield(); } } //------------------------- CONTROL METHODS ------------------------- /*** * Turn on or off polling. * When off, then worker thread will sleep with periodic checks back here. * Stop can be either after done with unit, or between poll cycles. * * @param ok true to start polling */ public void setOkToPoll(boolean ok) { m_OK_to_poll_units = ok; } /*** * Ask the worker thread to exit. * Afterwards use {@code thread.join()} for cleanup. */ public void stopWorkerThread() { m_do_thread_exit = true; } //------------------------- INSTANCE VARS --------------------------- protected volatile boolean m_OK_to_poll_units = false; protected volatile boolean m_do_thread_exit = false; protected InputStream m_instr; protected OutputStream m_outstr; /*** Offset added to unit address before sending on the wire. */ public final byte CMRI_ADDR_OFFSET = (byte) 0x41; /*** CMRI protocol escape byte. */ public final byte CMRI_CH_ESCAPE = (byte) 0x10; /*** CMRI protocol "start of packet" byte. */ public final byte CMRI_CH_STX = (byte) 0x02; /*** CMRI protocol "end of packet" byte. */ public final byte CMRI_CH_ETX = (byte) 0x03; /*** CMRI protocol "line in use" byte. */ public final byte CMRI_CH_FRAME = (byte) 0xff; /*** Indicates an RX error occurred and is now cleared. */ public final byte CH_ERROR_BYTE = (byte) CMRI_CH_FRAME; /*** Prefix (header) for CMRI packets. */ public final byte[] CMRI_HEADER_BYTES = new byte[] { CMRI_CH_FRAME, CMRI_CH_FRAME, CMRI_CH_STX }; /*** Suffix (trailer) for CMRI packets. */ public final byte[] CMRI_TRAILER_BYTES = new byte[] { CMRI_CH_ETX /*, CMRI_CH_FRAME */ }; /*** * Where current in-message is collected. If disabled, then no message * is expected. */ private final SynchronizedByteBuffer m_in_mesg; private final LayoutTimeoutManager m_timeout_mgr; private boolean m_rx_timeout; } /*** * Milliseconds of quiet time between unit poll-response. * If we wait for a non-responding unit, that wait time is considered part of * the inter-unit poll-response quiet time. */ public int PROP_INTER_UNIT_SILENCE = 30; /*** * Max time to wait when draining the RX port. Prevents live-lock if a slave unit * is babbling. Value is {@code double}, so max ease of fine tuning... */ public double PROP_DRAIN_PORT_MAX_WAIT_SECONDS = 0.5; /*** * After missing this many polls, send an INIT message to unit to try and re-enroll. * In case it lost its configuration, e.g. the unit was power-cycled. */ public int PROP_GONE_TOO_LONG_REINIT = 5; /** Smallest logical polling address. */ public final int CMRI_LOWEST_POLL_ADDR = 0; /** Maximum logical polling address. */ public final int CMRI_HIGHEST_POLL_ADDR = 255; //--------------------------- INSTANCE VARS ----------------------------- /*** Statistics: count of bytes in messages that were not accepted. */ public volatile long m_cntr_bad_bytes_in; /*** Statistics: count of bytes in messages that were accepted. */ public volatile long m_cntr_good_bytes_in; /*** Statistics: count of bytes in messages sent out. */ public volatile long m_cntr_good_bytes_out; /*** Model we feed sensor changes into. */ protected final CmriLayoutModelImpl m_model; /*** Is polling desired? Read by worker {@link #m_worker}. */ private boolean m_polling; /*** Fractional addition until reaching 1.0, then a revival will occur. */ protected volatile float m_recovery_rate; /*** Time to wait for a first char of response before giving up, in milliseconds. */ protected int m_response_wait; /*** Synchronized queue of units actively responding. */ protected Queue<Integer> m_active_queue; /*** Synchronized queue of known units that are NOT responding. */ protected Queue<Integer> m_revive_queue; /*** * Counter of missed polls for some unit poll address, reset to zero when * a poll-response is successful. */ transient private int[] m_consecutive_missed_polls; /*** Serial port to use. Requires {@link gnu.io.SerialPort} to inter-operate with host OS. */ transient protected SerialPort m_port; /*** Baud rate of serial port with baud rate already set. */ transient protected int m_baud_rate; /*** Little worker object. */ transient protected CmriSerialPollingWorker m_worker; /*** Thread inside the little worker object {@link #m_worker}. */ transient protected Thread m_thread; /*** Logging output spigot. */ transient private static final Logger LOG = Logger.getLogger(CmriPollMachine.class.getName()); }