001package jmri.jmrix.bidib;
002
003import java.util.LinkedHashMap;
004import java.util.Map;
005
006import org.bidib.jbidibc.messages.BidibLibrary; //new
007import org.bidib.jbidibc.messages.AccessoryState;
008import org.bidib.jbidibc.messages.AccessoryStateOptions;
009import org.bidib.jbidibc.messages.AddressData;
010import org.bidib.jbidibc.messages.BidibPort;
011import org.bidib.jbidibc.core.DefaultMessageListener;
012import org.bidib.jbidibc.messages.LcConfig;
013import org.bidib.jbidibc.messages.LcConfigX;
014import org.bidib.jbidibc.messages.Node;
015import org.bidib.jbidibc.messages.ProtocolVersion;
016import org.bidib.jbidibc.messages.enums.AccessoryAcknowledge;
017import org.bidib.jbidibc.messages.enums.ActivateCoilEnum;
018import org.bidib.jbidibc.messages.enums.AddressTypeEnum;
019import org.bidib.jbidibc.messages.enums.LcOutputType;
020import org.bidib.jbidibc.messages.enums.PortModelEnum;
021import org.bidib.jbidibc.messages.enums.TimeBaseUnitEnum;
022import org.bidib.jbidibc.messages.enums.TimingControlEnum;
023import org.bidib.jbidibc.messages.message.AccessoryGetMessage;
024import org.bidib.jbidibc.messages.message.AccessorySetMessage;
025import org.bidib.jbidibc.messages.message.BidibCommandMessage;
026import org.bidib.jbidibc.messages.message.BidibRequestFactory;
027import org.bidib.jbidibc.messages.message.CommandStationAccessoryMessage;
028import org.bidib.jbidibc.messages.message.FeedbackGetRangeMessage;
029import org.bidib.jbidibc.messages.port.ReconfigPortConfigValue;
030import org.bidib.jbidibc.messages.utils.ByteUtils;
031import org.bidib.jbidibc.messages.utils.NodeUtils;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * This class handles output to:
037 * - BiDiB Accessories
038 * - DCC Accessories via command station
039 * - BiDiB LC Ports
040 * 
041 * Output value is sent to the type according to the address type.
042 * Incoming messages a are catched by the BiDiB Message listener, then some common
043 * processing takes place and the new value is sent back to the listener of this class instance.
044 * 
045 * @author Eckart Meyer Copyright (C) 2020-2023
046 */
047public class BiDiBOutputMessageHandler extends DefaultMessageListener {
048
049    private final BiDiBNamedBeanInterface nb;
050    protected BiDiBTrafficController tc = null;
051    protected String type;// for log output only, e.g. "TURNOUT"
052    protected LcConfigX portConfigx;
053    protected LcOutputType lcType; //cached type from ConfigX or fixed in type based address
054    protected BidibRequestFactory requestFactory = null;
055    final Object portConfigLock = new Object();
056
057    // the configLock is used to synchronize config messages
058    //private final Object configLock = new Object();
059    
060    // internal cs accessory request aspect table since MSG_CS_ACCESSORY_ACK does not return the accessory state
061    private final Map<BiDiBAddress, Integer> csAccessoryAspectMap = new LinkedHashMap<>();
062
063    BiDiBOutputMessageHandler(BiDiBNamedBeanInterface nb, String type, BiDiBTrafficController tc) {
064        this.type = type;
065        this.nb = nb;
066        this.tc = tc;
067        BiDiBAddress addr = nb.getAddr();
068        if (addr.isValid()  &&  tc != null) {
069            lcType = addr.getPortType();
070            requestFactory = tc.getBidib().getNode(addr.getNode()).getRequestFactory();
071        }
072    }
073    
074    /**
075     * Get the port configuration if output is a BiDiB port
076     * 
077     * @return port ConfigX or null if not a BiDiB port
078     */
079    public LcConfigX getConfigX() {
080        return portConfigx;
081    }
082    
083    /**
084     * Get the port output type if output is a BiDiB port
085     * 
086     * @return port output type or null if not a BiDiB port
087     */
088    public LcOutputType getLcType() {
089        return lcType;
090    }
091    
092    /**
093     * Send output request to traffic controller
094     * Send new port value or aspect value
095     * 
096     * @param portstat BiDiB output value (see protocol description for valid values)
097     */
098    public void sendOutput(int portstat) {
099        BiDiBAddress addr = nb.getAddr();
100        log.trace("sendOutput: portstat: {}", portstat);
101        if (addr.isValid()) {
102            log.info("send output message to BiDiB: addr: {}, state: {}", addr, portstat);
103            Node node = addr.getNode();
104            if (addr.isPortAddr()) {
105                if (portExists()  &&  requestFactory != null) {
106                    waitQueryConfig();
107                    BidibCommandMessage m = requestFactory.createLcOutputMessage(tc.getPortModel(node), lcType, addr.getAddr(), portstat);
108                    tc.sendBiDiBMessage(m, node);
109                }
110            }
111            else if (addr.isAccessoryAddr()) {
112                if (accessoryExists()) {
113                    tc.sendBiDiBMessage(new AccessorySetMessage(addr.getAddr(), portstat), node);
114                }
115            }
116            else if (addr.isTrackAddr()) { //send a CS Accessory Message
117                // can't check address
118                tc.sendBiDiBMessage(new CommandStationAccessoryMessage(addr.getAddr(), AddressTypeEnum.ACCESSORY,
119                            TimingControlEnum.COIL_ON_OFF, ActivateCoilEnum.COIL_ON, portstat & 0x1F, TimeBaseUnitEnum.UNIT_100MS, 0), node);
120                // remember the requested aspect since the ACK messgae does not contain any state and we also cannot query the state
121                csAccessoryAspectMap.put(addr, portstat & 0x1F);
122            }
123            else {
124                log.error("sending output message not supported for address type");
125            }
126        }
127        else {
128            log.warn("node is not available, UID: {}", ByteUtils.formatHexUniqueId(addr.getNodeUID()));
129        }
130    }
131    
132    public void sendQueryConfig() {
133        BiDiBAddress addr = nb.getAddr();
134        log.trace("queryOutput for addr: {}", addr);
135        if (addr.isValid()) {
136            log.debug("send query config message to BiDiB: addr: {}", addr);
137            Node node = addr.getNode();
138            if (addr.isPortAddr()  &&  portExists()  &&  requestFactory != null) {
139                log.info("send port query config message to BiDiB: addr: {}", addr);
140                // only ports have configurations
141                BidibCommandMessage m;
142                if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_6)) { //ConfigX is available since V0.6
143                    m = requestFactory.createLcConfigXGet(tc.getPortModel(node), lcType, addr.getAddr());
144                }
145                else {
146                    m = requestFactory.createLcConfigGet(tc.getPortModel(node), lcType, addr.getAddr());
147                }
148                portConfigx = null;// invalidate
149                tc.sendBiDiBMessage(m, node);
150            }
151        }
152        else {
153            log.warn("node is not available, UID: {}", ByteUtils.formatHexUniqueId(addr.getNodeUID()));
154        }
155    }
156    
157    public void waitQueryConfig() {
158        BiDiBAddress addr = nb.getAddr();
159        if (addr.isValid()) {
160            if (addr.isPortAddr()) {
161                synchronized (portConfigLock) {
162                    while (portConfigx == null) {// "while" instead of "if" - see Doku of Java Object()
163                        try {
164                            log.debug("wait for config message from BiDiB: addr: {}", addr);
165                            // wait will relinquish synchronization claim and wait for notifyAll()
166                            portConfigLock.wait(500L);
167                            // on return the synchronization claim is re-established
168                        }
169                        catch (InterruptedException ie) {
170                            log.warn("Wait for port config was interrupted.", ie);
171                        }
172                    }
173                }
174            }
175        }
176    }
177    
178    /**
179     * Send output query request to traffic controller
180     */
181    public void sendQuery() {
182        BiDiBAddress addr = nb.getAddr();
183        log.trace("queryOutput for addr: {}", addr);
184        if (addr.isValid()) {
185            log.info("send query output message to BiDiB: addr: {}", addr);
186            Node node = addr.getNode();
187            if (addr.isPortAddr()) {
188                if (portExists()  &&  requestFactory != null) {
189                    waitQueryConfig();
190                    BidibCommandMessage m;
191                    if (node.getProtocolVersion().isHigherThan(ProtocolVersion.VERSION_0_6)  ||  lcType.getType() <= 7) {
192                        m = requestFactory.createLcPortQuery(tc.getPortModel(node), lcType, addr.getAddr());
193                    }
194                    else {
195                        // this is only used for input ports on nodes with older firmware (<= 0.6)
196                        m = requestFactory.createLcKey(addr.getAddr());
197                    }
198                    tc.sendBiDiBMessage(m, node);
199                }
200            }
201            else if (addr.isAccessoryAddr()) {
202                if (accessoryExists()) {
203                    tc.sendBiDiBMessage(new AccessoryGetMessage(addr.getAddr()), node);
204                }
205            }
206            else if (addr.isTrackAddr()) {
207                // can't check and can't request
208                // no warning/error level here please - would break LightManager test unit (some test expect specific warn/error)
209                log.info("query of a CS accessory is not possible.");
210            }
211            else if (addr.isFeedbackAddr()) {
212                if (feedbackExists()) {
213                    int a = (addr.getAddr() / 8) * 8;
214                    int b = ((addr.getAddr() + 8) / 8) * 8; //exclusive end address
215                    log.debug("  requesting feedback from {} to {}", a, b);
216                    tc.sendBiDiBMessage(new FeedbackGetRangeMessage(a, b), node);
217                }
218            }
219            else {
220                log.error("sending query output message not supported for address type, addr: {}", addr);
221            }
222        }
223        else {
224            log.warn("node is not available, UID: {}", ByteUtils.formatHexUniqueId(addr.getNodeUID()));
225        }
226    }
227
228    /**
229     * Get the number of ports for a node by type.
230     * For type based addressing this is the real number of ports of this type, addresses starting from 0.
231     * For flat addressing this is only a hint how many ports of this type exists in the flat address range.
232     * In the latter case this feature may not been implemented and will return 0.
233     * 
234     * @param node to check
235     * @param type to look for
236     * @return number of ports for this type.
237     */
238    private int getPortTypeCount(Node node, LcOutputType type) {
239        int id;
240        switch (type) {
241            case SWITCHPORT:
242            case SWITCHPAIRPORT:
243                id = BidibLibrary.FEATURE_CTRL_SWITCH_COUNT;
244                break;
245            case LIGHTPORT:
246                id = BidibLibrary.FEATURE_CTRL_LIGHT_COUNT;
247                break;
248            case SERVOPORT:
249                id = BidibLibrary.FEATURE_CTRL_SERVO_COUNT;
250                break;
251            case SOUNDPORT:
252                id = BidibLibrary.FEATURE_CTRL_SOUND_COUNT;
253                break;
254            case MOTORPORT:
255                id = BidibLibrary.FEATURE_CTRL_MOTOR_COUNT;
256                break;
257            case ANALOGPORT:
258                id = BidibLibrary.FEATURE_CTRL_ANALOGOUT_COUNT;
259                break;
260            case BACKLIGHTPORT:
261                id = BidibLibrary.FEATURE_CTRL_BACKLIGHT_COUNT;
262                break;
263            case INPUTPORT:
264                id = BidibLibrary.FEATURE_CTRL_INPUT_COUNT;
265                break;
266            default:
267                return 0;
268        }
269        return tc.getNodeFeature(node, id);
270    }
271
272    private boolean portExists() {
273        BiDiBAddress addr = nb.getAddr();
274        if (addr.isPortAddr()) {
275            Node node = addr.getNode();
276            if (addr.isPortTypeBasedModel()) {
277                if (addr.getAddr() >= 0  &&  addr.getAddr() < getPortTypeCount(addr.getNode(), lcType)) {
278                    return true;
279                }
280            }
281            else {
282                if (addr.getAddr() >= 0  &&  addr.getAddr() < node.getPortFlatModel()) {
283                    return true;
284                }
285            }
286        }
287        return false;
288    }
289    
290    private boolean accessoryExists() {
291        BiDiBAddress addr = nb.getAddr();
292        if (addr.isAccessoryAddr()) {
293            if (addr.getAddr() >= 0  &&  addr.getAddr() < tc.getNodeFeature(addr.getNode(), BidibLibrary.FEATURE_ACCESSORY_COUNT)) {
294                return true;
295            }
296        }
297        return false;
298    }
299    
300    private boolean feedbackExists() {
301        BiDiBAddress addr = nb.getAddr();
302        if (addr.isFeedbackAddr()) {
303            if (addr.getAddr() >= 0  &&  addr.getAddr() < tc.getNodeFeature(addr.getNode(), BidibLibrary.FEATURE_BM_SIZE)) {
304                return true;
305            }
306        }
307        return false;
308    }
309
310// Overridable methods for notifications
311    
312    /**
313     * Notify output state
314     * @param state desired state from NamedBean list
315     */
316    public void newOutputState(int state) {
317    }
318    
319    /**
320     * Notify error state
321     * @param err - BiDiB error number
322     */
323    public void errorState(int err) {
324    }
325    
326    /**
327     * Notify output will change later
328     * @param time in msec
329     */
330    public void outputWait(int time) {
331    }
332    
333    /**
334     * Notify LC port ConfigX
335     * 
336     * @param lcConfigX input
337     * @param lcType input
338     */
339    public void newLcConfigX(LcConfigX lcConfigX, LcOutputType lcType) {
340    }
341    
342        
343// Accessory related received messages
344    
345// - BiDiB Accessories
346    
347    @Override
348    public void accessoryState(byte[] address, int messageNum, final AccessoryState accessoryState, final AccessoryStateOptions accessoryStateOptions) {
349        BiDiBAddress addr = nb.getAddr();
350        //log.trace("node UID: {}, node addr: {}, msg node addr: {}, state: {}", addr.getNodeUID(), addr.getNodeAddr(), address, accessoryState);
351        if (addr.isAccessoryAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.getAddr() == accessoryState.getAccessoryNumber()) {
352            log.info("{} accessory state was signalled, state: {}, opts: {}, node: {}",
353                    type, accessoryState, accessoryStateOptions, addr);
354            if (accessoryState.hasError()) {
355                log.warn("Accessory state error: {}", accessoryState.getErrorInformation());
356                errorState(accessoryState.getErrorCode());
357            }
358            else {
359                if (accessoryState.getWait() == 0) {
360                    newOutputState(accessoryState.getActiveAspect());
361                }
362                else {
363                    outputWait(accessoryState.getWait());
364                }
365            }
366        }
367    }
368
369// - DCC Accessories
370
371    @Override
372    public void csAccessoryAcknowledge(byte[] address, int messageNum, int decoderAddress, AccessoryAcknowledge acknowledge) {
373        BiDiBAddress addr = nb.getAddr();
374        //log.trace("node UID: {}, node addr: {}, msg node addr: {}", addr.getNodeUID(), addr.getNodeAddr(), address);
375        if (addr.isTrackAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.getAddr() == decoderAddress) {
376            log.info("{} CS accessory ackn was signalled, acknowledge: {}, decoderAddress: {}, node: {}", type, acknowledge, decoderAddress, addr);
377            if (acknowledge == AccessoryAcknowledge.NOT_ACKNOWLEDGED) {
378                log.warn("NOT acknowledged!");
379                errorState(csAccessoryAspectMap.get(addr));
380            }
381            else {
382                if (acknowledge == AccessoryAcknowledge.DELAYED) {
383                    outputWait(0);
384                }
385                else {
386                    // since the message does not contain an aspect, we just return the requested aspect from the map
387                    newOutputState(csAccessoryAspectMap.get(addr));
388                }
389            }
390        }
391    }
392    @Override
393    public void csAccessoryManual(byte[] address, int messageNum, AddressData decoderAddress, ActivateCoilEnum activate, int aspect) {
394        BiDiBAddress addr = nb.getAddr();
395        //log.trace("node UID: {}, node addr: {}, msg node addr: {}, decoder address", addr.getNodeUID(), addr.getNodeAddr(), address, decoderAddress);
396        if (addr.isAccessoryAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.getAddr() == decoderAddress.getAddress()) {
397            log.info("{} accessory manual was signalled, activate coil: {}, aspect: {}, decoder address: {}, node: {}",
398                    type, activate.getType(), aspect, decoderAddress.getAddress(), addr);
399                newOutputState(aspect);
400        }
401    }
402
403// LightControl related received messages
404    
405    private void notifyLcConfigX(LcConfigX lcConfigX) {
406        BiDiBAddress addr = nb.getAddr();
407        log.trace("portConfigx: {}", lcConfigX);
408        if (addr.getNode().isPortFlatModelAvailable()) {
409            ReconfigPortConfigValue p = (ReconfigPortConfigValue)lcConfigX.getPortConfig().get(BidibLibrary.BIDIB_PCFG_RECONFIG);
410            log.warn("reconfig: {}, type: {}", p, p.getCurrentOutputType());
411            if (lcType != p.getCurrentOutputType()) {
412                log.warn("** reconfig: {}, type changed: {} -> {}", p, lcType, p.getCurrentOutputType());
413            }
414            lcType = p.getCurrentOutputType();
415        }
416        else {
417            lcType = lcConfigX.getOutputType(PortModelEnum.type);
418        }
419        newLcConfigX(lcConfigX, lcType);
420    }
421    
422    @Override
423    public void lcStat(byte[] address, int messageNum, BidibPort bidibPort, int portStatus) {
424        BiDiBAddress addr = nb.getAddr();
425        //log.trace("lcStat: node UID: {}, node addr: {}, address: {}, port: {}, stat: {}", addr.getNodeUID(), addr.getNodeAddr(), address, bidibPort, portStatus);
426        if (addr.isPortAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.isAddressEqual(bidibPort)) {
427            log.info("{} LC status was signalled, state: {}, type: {}, node: {}", type, portStatus, lcType, addr);
428            newOutputState(portStatus);
429        }
430    }
431    @Override
432    public void lcWait(byte[] address, int messageNum, BidibPort bidibPort, int time) {
433        BiDiBAddress addr = nb.getAddr();
434        //log.trace("lcStat: node UID: {}, node addr: {}, address: {}, port: {}, time: {}", addr.getNodeUID(), addr.getNodeAddr(), address, bidibPort, time);
435        if (addr.isPortAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.isAddressEqual(bidibPort)) {
436            log.info("{} LC Wait was signalled, wait: {}, type: {}, node: {}", type, time, lcType, addr);
437            outputWait(time);
438        }
439    }
440    @Override
441    public void lcNa(byte[] address, int messageNum, BidibPort bidibPort, Integer errorCode) {
442        BiDiBAddress addr = nb.getAddr();
443        //log.trace("lcNa: node UID: {}, node addr: {}, address: {}, port: {}, errorCode: {}", addr.getNodeUID(), addr.getNodeAddr(), address, bidibPort, errorCode);
444        if (addr.isPortAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.isAddressEqual(bidibPort)) {
445            log.info("{} LC NA was signalled, error: {}, type: {}, node: {}", type, errorCode, lcType, addr);
446            errorState(errorCode);
447        }
448    }
449    @Override
450    public void lcConfig(byte[] address, int messageNum, LcConfig lcConfig) {
451        BiDiBAddress addr = nb.getAddr();
452        //log.trace("lcConfig: node addr: {}, config: {}", address, lcConfig);
453        if (addr.isPortAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.isAddressEqual(lcConfig)) {
454            log.info("{} LC Config was signalled, config: {}, node: {}", type, lcConfig, addr);
455            synchronized (portConfigLock) {
456                portConfigx = tc.convertConfig2ConfigX(addr.getNode(), lcConfig);
457                notifyLcConfigX(portConfigx);
458                portConfigLock.notifyAll();
459            }
460        }
461    }
462    @Override
463    public void lcConfigX(byte[] address, int messageNum, LcConfigX lcConfigX) {
464        BiDiBAddress addr = nb.getAddr();
465        //log.trace("lcConfigX: node addr: {}, configx: {}", address, lcConfigX);
466        if (addr.isPortAddr()  &&  NodeUtils.isAddressEqual(addr.getNodeAddr(), address)  &&  addr.isAddressEqual(lcConfigX)) {
467            log.info("{} LC ConfigX was signalled, configx: {}, node: {}", type, lcConfigX, addr);
468            synchronized (portConfigLock) {
469                portConfigx = new LcConfigX(addr.makeBidibPort(), new LinkedHashMap<>() );
470                portConfigx.getPortConfig().putAll(lcConfigX.getPortConfig());
471                notifyLcConfigX(portConfigx);
472                portConfigLock.notifyAll();
473            }
474        }
475    }
476    
477    private final static Logger log = LoggerFactory.getLogger(BiDiBOutputMessageHandler.class);
478}