001package jmri.jmrix.rps;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.io.File;
005import java.io.IOException;
006import javax.vecmath.Point3d;
007import jmri.CommandStation;
008import jmri.jmrit.roster.Roster;
009import jmri.jmrit.roster.RosterEntry;
010import org.jdom2.JDOMException;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014/**
015 * Engine does basic computations of RPS system.
016 * <p>
017 * Holds all the alignment info. Receivers are indexed by their RPS receiver
018 * number in all cases.
019 * <p>
020 * Gets a reading from the Distributor and passes back a Measurement
021 * <p>
022 * Bound properties:
023 * <ul>
024 * <li>vSound - velocity of sound, in whatever units are in use
025 * </ul>
026 * <p>
027 * This class maintains a collection of "Transmitter" objects representing the
028 * RPS-equipped rolling stock (usually engines) on the layout. This is an
029 * extension to the common Roster, and every entry in this class's collection
030 * must be present in the Roster.
031 *
032 * @author Bob Jacobsen Copyright (C) 2006, 2008
033 */
034public class Engine implements ReadingListener {
035
036    public Engine() {
037    }
038
039    void loadValues() {
040        // load dummy contents
041        setInitialAlignment();
042        loadInitialTransmitters();
043    }
044
045    public void dispose() {
046    }
047
048    public void setVSound(double v) {
049        double oldVal = vsound;
050        vsound = v;
051        log.info("change vsound from {} to {}", oldVal, v);
052        prop.firePropertyChange("vSound", Double.valueOf(oldVal), Double.valueOf(v));
053    }
054
055    public double getVSound() {
056        return vsound;
057    }
058    private double vsound = 0.013544;  // 0.013544 inches/usec, .000345 m/usec,
059    private int offset = 0;
060
061    public void setOffset(int offset) {
062        this.offset = offset;
063    }
064
065    public int getOffset() {
066        return offset;
067    }
068
069    Measurement lastPoint = null;
070
071    Receiver[] receivers;
072
073    /**
074     * Set the maximum receiver number expected.
075     * <p>
076     * If the highest value in the hardware is 5, that's what's needed here.
077     * @param n max receivers.
078     */
079    public void setMaxReceiverNumber(int n) {
080        log.debug("setReceiverCount to {}", n);
081        if ((receivers != null) && (n == receivers.length + 1)) {
082            return;
083        }
084        Receiver[] oldReceivers = receivers;
085        receivers = new Receiver[n + 1];  // n is highest address, so need n+1
086        if (oldReceivers == null) {
087            return;
088        }
089        // clear new array
090        for (int i = 0; i < receivers.length; i++) {
091            receivers[i] = null;
092        }
093        // copy the existing receivers
094        for (int i = 0; i < Math.min(n + 1, oldReceivers.length); i++) {
095            receivers[i] = oldReceivers[i];
096        }
097    }
098
099    public int getMaxReceiverNumber() {
100        if (receivers == null) {
101            return 0;
102        }
103        return receivers.length - 1;
104    }
105
106    /**
107     * Set a particular receiver by address (starting at 1).
108     * @param address the receiver address.
109     * @param receiver the receiver.
110     */
111    public void setReceiver(int address, Receiver receiver) {
112        if (receivers == null) {
113            throw new IllegalArgumentException("Must initialize first");
114        }
115        if (address >= receivers.length) {
116            throw new IllegalArgumentException("Index " + address + " is larger than expected " + receivers.length);
117        }
118        log.debug("store receiver {} in {}", address, this);
119        receivers[address] = receiver;
120    }
121
122    public Receiver getReceiver(int i) {
123        return receivers[i];
124    }
125
126    public void setReceiverPosition(int i, Point3d p) {
127        receivers[i].setPosition(p);
128    }
129
130    public Point3d getReceiverPosition(int i) {
131        if (receivers[i] == null) {
132            log.debug("getReceiverPosition of null receiver index i={}", i);
133            return null;
134        }
135        return receivers[i].getPosition();
136    }
137
138    public void setAlgorithm(String algorithm) {
139        this.algorithm = algorithm;
140    }
141
142    public String getAlgorithm() {
143        return algorithm;
144    }
145
146    String algorithm = "Ash 2.1";  // default value, configured separately
147
148    @Override
149    public void notify(Reading r) {
150        // This implementation creates a new Calculator
151        // each time to ensure that the most recent
152        // receiver positions are used; this should be
153        // replaced with some notification system
154        // to reduce the work done.
155
156        // ok to send next poll
157        log.debug("po false {}", r.getId());
158        pollOutstanding = false;
159
160        // make a list of receiver positions to provide
161        // to the new Calculator.  Missing/unconfigured receivers
162        // are null.
163        Point3d[] list = new Point3d[receivers.length];
164        for (int i = 0; i < receivers.length; i++) {
165
166            if (receivers[i] == null) {
167                list[i] = null;
168                continue;  // skip receivers not present
169            }
170
171            Point3d p = getReceiverPosition(i);
172            if (p != null) {
173                receivers[i].setLastTime((int) r.getValue(i));  // receivers numbered from 1
174                log.debug("    {}th value min {} < time {} < max {} at {}",
175                        i, receivers[i].getMinTime(), r.getValue(i), receivers[i].getMaxTime(), p);
176                if (receivers[i].isActive() && (receivers[i].getMinTime() <= r.getValue(i))
177                        && (r.getValue(i) <= receivers[i].getMaxTime())) {
178                    list[i] = p;
179                } else {
180                    list[i] = null;
181                }
182            } else {
183                list[i] = null;
184                log.error("Unexpected null position from receiver {}", i);
185            }
186        }
187
188        Calculator c = Algorithms.newCalculator(list, getVSound(),
189                getOffset(), getAlgorithm());
190
191        Measurement m = c.convert(r, lastPoint);
192
193        saveLastMeasurement(r.getId(), m);
194
195        lastPoint = m;
196        Distributor.instance().submitMeasurement(m);
197    }
198
199    // Store the lastMeasurement
200    void saveLastMeasurement(String id, Measurement m) {
201        for (int i = 0; i < getNumTransmitters(); i++) {
202            if (getTransmitter(i).getId().equals(id) && getTransmitter(i).isPolled()) {
203                getTransmitter(i).setLastMeasurement(m);
204                // might be more than one, so don't end here
205            }
206        }
207    }
208
209    // Store alignment info
210    public void storeAlignment(File file) throws IOException {
211        PositionFile pf = new PositionFile();
212        pf.prepare();
213        pf.setConstants(getVSound(), getOffset(), getAlgorithm());
214
215        for (int i = 1; i <= getMaxReceiverNumber(); i++) {
216            if (getReceiver(i) == null) {
217                continue;
218            }
219            pf.setReceiver(i, getReceiver(i));
220        }
221        pf.store(file);
222    }
223
224    public void loadAlignment(File file) throws org.jdom2.JDOMException, IOException {
225        // start by getting the file
226        PositionFile pf = new PositionFile();
227        pf.loadFile(file);
228
229        // get VSound
230        setVSound(pf.getVSound());
231
232        // get offset
233        setOffset(pf.getOffset());
234
235        // get algorithm
236        setAlgorithm(pf.getAlgorithm());
237
238        // get receivers
239        setMaxReceiverNumber(pf.maxReceiver());  // count from 1
240        Point3d p;
241        boolean a;
242        int min;
243        int max;
244        for (int i = 1; i <= getMaxReceiverNumber(); i++) {
245            p = pf.getReceiverPosition(i);
246            if (p == null) {
247                continue;
248            }
249
250            a = pf.getReceiverActive(i);
251            min = pf.getReceiverMin(i);
252            max = pf.getReceiverMax(i);
253
254            log.debug("load {} with {}", i, p);
255            Receiver r = new Receiver(p);
256            r.setActive(a);
257            r.setMinTime(min);
258            r.setMaxTime(max);
259            setReceiver(i, r);
260        }
261
262    }
263
264    protected void setInitialAlignment() {
265        File defaultFile = new File(PositionFile.defaultFilename());
266        try {
267            loadAlignment(defaultFile);
268        } catch (Exception e) {
269            log.debug("load exception ", e);
270            // load dummy values
271            setDefaultAlignment();
272        }
273    }
274
275    protected void setDefaultAlignment() {
276        setMaxReceiverNumber(2);
277        setReceiver(1, new Receiver(new Point3d(0.0, 0.0, 72.0)));
278        setReceiver(2, new Receiver(new Point3d(72.0, 0.0, 72.0)));
279    }
280
281    //**************************************
282    // Methods to handle polling
283    //**************************************
284    public void setPollingInterval(int pollingInterval) {
285        this.pollingInterval = pollingInterval;
286    }
287    int pollingInterval = 500;
288
289    public int getPollingInterval() {
290        return pollingInterval;
291    }
292
293    boolean polling = false;
294
295    public void setPolling(boolean polling) {
296        this.polling = polling;
297        if (polling) {
298            startPoll();
299        } else {
300            stopPoll();
301        }
302    }
303
304    public boolean getPolling() {
305        return polling;
306    }
307
308    java.util.ArrayList<Transmitter> transmitters;
309
310    void loadInitialTransmitters() {
311        transmitters = new java.util.ArrayList<Transmitter>();
312        // load transmitters from the JMRI roster
313        java.util.List<RosterEntry> l = Roster.getDefault().matchingList(null, null, null, null, null, null, null);
314        log.debug("Got {} roster entries", l.size());
315        for (int i = 0; i < l.size(); i++) {
316            RosterEntry r = null;
317            try {
318                r = l.get(i);
319                int address = Integer.parseInt(r.getDccAddress());
320                Transmitter t = new Transmitter(r.getId(), false, address, r.isLongAddress());
321                t.setRosterName(r.getId());
322                transmitters.add(t);
323            } catch (NumberFormatException e) {
324                // just skip this entry
325                if (r != null) {
326                    log.warn("Skip roster entry: {}", r.getId());
327                } else {
328                    log.warn("Failed roster entry skipped");
329                }
330            }
331        }
332
333        // load the polling status, custom IDs, etc, from file if possible
334        try {
335            loadPollConfig(new File(PollingFile.defaultFilename()));
336        } catch (IOException | JDOMException e) {
337            log.error("Unable to load {}", PollingFile.defaultFilename(), e);
338        }
339    }
340
341    // Store polling info
342    public void storePollConfig(File file) throws IOException {
343        PollingFile pf = new PollingFile();
344        pf.prepare();
345        pf.setPoll();
346
347        for (int i = 0; i < getNumTransmitters(); i++) {
348            pf.setTransmitter(i);
349        }
350        pf.store(file);
351    }
352
353    public void loadPollConfig(File file) throws org.jdom2.JDOMException, IOException {
354        if (file.exists()) {
355            PollingFile pf = new PollingFile();
356            pf.loadFile(file);
357            // first make sure transmitters defined
358            pf.getTransmitters(this);
359            // and possibly start polling
360            pf.getPollValues();
361        }
362    }
363
364    public Transmitter getTransmitterByAddress(int addr) {
365        if (addr < 0) {
366            return null;
367        }
368        if (transmitters == null) {
369            return null;
370        }
371        for (int i = 0; i < getNumTransmitters(); i++) {
372            if (getTransmitter(i).getAddress() == addr) {
373                return getTransmitter(i);
374            }
375        }
376        return null;
377    }
378
379    public Transmitter getTransmitter(int i) {
380        if (i < 0) {
381            return null;
382        }
383        if (transmitters == null) {
384            return null;
385        }
386        return transmitters.get(i);
387    }
388
389    public int getNumTransmitters() {
390        if (transmitters == null) {
391            return 0;
392        }
393        return transmitters.size();
394    }
395
396    public String getPolledID() {
397        Transmitter t = getTransmitter(pollIndex);
398        if (t == null) {
399            return "";
400        }
401        return t.getId();
402    }
403
404    public int getPolledAddress() {
405        Transmitter t = getTransmitter(pollIndex);
406        if (t == null) {
407            return -1;
408        }
409        return t.getAddress();
410    }
411
412    /**
413     * The real core of the polling, this selects the next one to poll. -1 means
414     * none selected, try again later.
415     * @return index to poll next
416     */
417    int selectNextPoll() {
418        int startindex = pollIndex;
419        while (++pollIndex < getNumTransmitters()) {
420            if (getTransmitter(pollIndex).isPolled()) {
421                return pollIndex;
422            }
423        }
424        // here, we got to the end without finding somebody to poll
425        // try the start
426        pollIndex = -1; // will autoincrement to 0
427        while (++pollIndex <= startindex) {
428            if (getTransmitter(pollIndex).isPolled()) {
429                return pollIndex;
430            }
431        }
432        // no luck, say so
433        return -1;
434    }
435
436    int pollIndex = -1; // left at last one done
437    boolean bscPoll = false;
438    boolean throttlePoll = false;
439
440    public void setBscPollMode() {
441        bscPoll = true;
442        throttlePoll = false;
443    }
444
445    public void setDirectPollMode() {
446        bscPoll = false;
447        throttlePoll = false;
448    }
449
450    public void setThrottlePollMode() {
451        bscPoll = false;
452        throttlePoll = true;
453    }
454
455    public boolean getBscPollMode() {
456        return bscPoll;
457    }
458
459    public boolean getThrottlePollMode() {
460        return throttlePoll;
461    }
462
463    public boolean getDirectPollMode() {
464        return !(bscPoll || throttlePoll);
465    }
466
467    void startPoll() {
468        // time to start operation
469        pollThread = new Thread() {
470            @Override
471            public void run() {
472                log.debug("Polling starts");
473                while (true) {
474                    try {
475                        int i = selectNextPoll();
476                        log.debug("Poll {}", i);
477                        setOn(i);
478                        log.debug("po true {}", i);
479                        pollOutstanding = true;
480                        synchronized (this) {
481                            wait(20);
482                        }
483                        setOff(i);
484                        log.debug("start wait");
485                        waitBeforeNextPoll(pollingInterval);
486                        log.debug("end wait");
487                    } catch (InterruptedException e) {
488                        // cancel whatever is happening
489                        log.debug("Polling stops");
490                        Thread.currentThread().interrupt(); // retain if needed later
491                        return; // end operation
492                    }
493                }
494            }
495        };
496        pollThread.start();
497    }
498
499    Thread pollThread;
500    boolean pollOutstanding;
501
502    /**
503     * Wait before sending next poll.
504     * <p>
505     * Waits specified time, and then checks to see if response has been
506     * returned. If not, it waits again (twice) by 1/2 the interval, then
507     * finally polls anyway.
508     * @param pollingInterval in milliseconds
509     * @throws InterruptedException in theory, but not in practice.
510     */
511    void waitBeforeNextPoll(int pollingInterval) throws InterruptedException {
512        synchronized (this) {
513            wait(pollingInterval);
514        }
515        if (!pollOutstanding) {
516            return;
517        }
518        log.debug("--- extra wait");
519        for (int i = 0; i < 20; i++) {
520            synchronized (this) {
521                wait(pollingInterval / 4);
522            }
523            log.debug("-------------extra wait");
524            if (!pollOutstanding) {
525                return;
526            }
527        }
528    }
529
530    void stopPoll() {
531        if (pollThread != null) {
532            pollThread.interrupt();
533        }
534    }
535
536    void setOn(int i) {
537        Transmitter t = getTransmitter(i);
538        byte[] packet;
539        if (bscPoll) {
540            // poll using BSC instruction
541            packet = jmri.NmraPacket.threeBytePacket(
542                    t.getAddress(), t.isLongAddress(),
543                    (byte) 0xC0, (byte) 0xA5, (byte) 0xFE);
544            if (jmri.InstanceManager.getNullableDefault(CommandStation.class) != null) {
545                jmri.InstanceManager.getDefault(CommandStation.class).sendPacket(packet, 1);
546            }
547        } else {
548            // poll using F2
549            if (throttlePoll) {
550                // use throttle; first, get throttle
551                if (t.checkInit()) {
552                    // now send F2
553                    t.getThrottle().setFunction(2, true);
554                } else {
555                    return;  // bail if not ready
556                }
557            } else {
558                // send packet direct
559                packet = jmri.NmraPacket.function0Through4Packet(
560                        t.getAddress(), t.isLongAddress(),
561                        false, false, true, false, false);
562                if (jmri.InstanceManager.getNullableDefault(CommandStation.class) != null) {
563                    jmri.InstanceManager.getDefault(CommandStation.class).sendPacket(packet, 1);
564                }
565            }
566        }
567    }
568
569    void setOff(int i) {
570        if (!bscPoll) {
571            // have to turn off F2 since not using BSC
572            Transmitter t = getTransmitter(i);
573            if (throttlePoll) {
574                // use throttle; first, get throttle
575                if (t.checkInit()) {
576                    // now send F2
577                    t.getThrottle().setFunction(2, false);
578                } else {
579                    return;  // bail if not ready
580                }
581            } else {
582                // send direct
583                byte[] packet = jmri.NmraPacket.function0Through4Packet(
584                        t.getAddress(), t.isLongAddress(),
585                        false, false, false, false, false);
586                if (jmri.InstanceManager.getNullableDefault(CommandStation.class) != null) {
587                    jmri.InstanceManager.getDefault(CommandStation.class).sendPacket(packet, 1);
588                }
589            }
590        }
591    }
592
593    // for now, we only allow one Engine
594    @SuppressFBWarnings(value = "MS_PKGPROTECT") // for tests
595    static volatile protected Engine _instance = null;
596
597    @SuppressFBWarnings(value = "LI_LAZY_INIT_UPDATE_STATIC") // see comment in method
598    static public Engine instance() {
599        if (_instance == null) {
600            // NOTE: _instance has to be initialized before loadValues()
601            // is called, because it invokes instance() indirectly.
602            _instance = new Engine();
603            _instance.loadValues();
604        }
605        return _instance;
606    }
607
608    // handle outgoing parameter notification
609    java.beans.PropertyChangeSupport prop = new java.beans.PropertyChangeSupport(this);
610
611    public void removePropertyChangeListener(java.beans.PropertyChangeListener p) {
612        prop.removePropertyChangeListener(p);
613    }
614
615    public void addPropertyChangeListener(java.beans.PropertyChangeListener p) {
616        prop.addPropertyChangeListener(p);
617    }
618
619    private final static Logger log = LoggerFactory.getLogger(Engine.class);
620
621}