001package jmri.jmrix.dccpp;
002
003import jmri.InstanceManager;
004import jmri.JmriException;
005import jmri.Meter;
006import jmri.MeterManager;
007import jmri.implementation.DefaultMeter;
008import jmri.implementation.MeterUpdateTask;
009
010import java.util.HashMap;
011
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015/**
016 * Provide access to current meters from the DCC++ Base Station
017 *   Creates meters based on values sent from command station
018 *
019 * @author Mark Underwood    Copyright (C) 2015
020 * @author Daniel Bergqvist  Copyright (C) 2020
021 * @author mstevetodd        Copyright (C) 2025
022 */
023public class DCCppPredefinedMeters implements DCCppListener {
024
025    private DCCppTrafficController tc = null;
026    private final MeterUpdateTask updateTask;
027    private String systemPrefix = null;
028    private char beanType;
029    private HashMap<String, Meter> meters = new HashMap<String, Meter>(2); //keep track of defined meters
030
031    public DCCppPredefinedMeters(DCCppSystemConnectionMemo memo) {
032        log.debug("Constructor called");
033
034        systemPrefix = memo.getSystemPrefix();
035        beanType = InstanceManager.getDefault(MeterManager.class).typeLetter();
036        tc = memo.getDCCppTrafficController();
037
038        updateTask = new MeterUpdateTask(0, 10000) {
039            @Override
040            public void requestUpdateFromLayout() {
041                if (tc.getCommandStation().isCurrentListSupported()) {
042                    tc.sendDCCppMessage(DCCppMessage.makeCurrentValuesMsg(), DCCppPredefinedMeters.this);
043                } else {
044                    tc.sendDCCppMessage(DCCppMessage.makeReadTrackCurrentMsg(), DCCppPredefinedMeters.this);
045                }
046            }
047        };
048
049        // TODO: For now this is OK since the traffic controller
050        // ignores filters and sends out all updates, but
051        // at some point this will have to be customized.
052        tc.addDCCppListener(DCCppInterface.CS_INFO, this);
053
054        //request one 'c' reply to set up the meters
055        if (!tc.getCommandStation().isCurrentListSupported()) {
056            tc.sendDCCppMessage(DCCppMessage.makeReadTrackCurrentMsg(), DCCppPredefinedMeters.this);
057        }
058
059        // send <JG> to get current maximums, response used to build list of Meters (no check here as version might not be ready yet)
060        tc.sendDCCppMessage(DCCppMessage.makeCurrentMaxesMsg(), DCCppPredefinedMeters.this);
061
062        updateTask.initTimer();
063        
064    }
065
066    public void setDCCppTrafficController(DCCppTrafficController controller) {
067        tc = controller;
068    }
069
070    /* handle new Meter replies and original current replies
071     *   creates meters if first time this name is encountered
072     *   uses new MeterReply message format from DCC-EX
073     *   also supports original "current percent" meter from
074     *   older DCC++                                           */
075    @Override
076    public void message(DCCppReply r) {
077
078        if (r.isCurrentMaxesReply()) {
079            //create a meter for each Track           
080            for (int t = 0; t <= r.getCurrentMaxesList().size()-1; t++) {
081                Integer maxValue = r.getCurrentMaxesList().get(t);
082                Meter newMeter;
083                String sysName = systemPrefix + beanType + t;
084                if (meters.get(sysName) == null) {
085                    String mode = tc.getCommandStation().getTrackMode(t);
086                    String userName = "Track " + String.valueOf((char)('A'+t)) + " " + mode + " (" + systemPrefix + ")";
087                    log.debug("Adding new current meter {} ({})", sysName, userName);
088                    newMeter = new DefaultMeter.DefaultCurrentMeter(
089                            sysName, jmri.Meter.Unit.Milli, -5.0, maxValue, 1.0, updateTask);
090                    newMeter.setUserName(userName);
091                    //store and register new Meter
092                    meters.put(sysName, newMeter);
093                    InstanceManager.getDefault(MeterManager.class).register(newMeter);
094                } else {
095                    log.debug("not creating duplicate meter '{}'", sysName);
096                }
097            }            
098            return;
099        }
100        
101        if (r.isCurrentValuesReply()) {
102            //update the meter for each Track
103            for (int t = 0; t <= r.getCurrentValuesList().size()-1; t++) {
104                String sysName = systemPrefix + beanType + t;
105                //set the newValue for the meter
106                Meter meter = meters.get(sysName);
107                Integer meterValue = Math.max(r.getCurrentValuesList().get(t), 0); //get the value, ignore negative values
108                log.debug("Setting value for '{}' to {}" , sysName, meterValue);
109                try {
110                    meter.setCommandedAnalogValue(meterValue);
111                } catch (JmriException e) {
112                    log.error("exception thrown when setting meter '{}' to value {}", sysName, meterValue, e);
113                }
114            }            
115            return;
116        }
117        
118        if (r.isTrackManagerReply()) {
119            //recalculate the username since mode may have changed 
120            int trackNum = r.getTrackManagerLetter() - 'A'; //get track number from track letter
121            String userName = "Track " + r.getTrackManagerLetter() + " " + r.getTrackManagerMode() + " (" + systemPrefix + ")";
122            String sysName = systemPrefix + beanType + trackNum;
123            Meter meter = meters.get(sysName);
124            if (meter != null) {
125                log.debug("Updating username for current meter {} to '{}'", sysName, userName);
126                meter.setUserName(userName); //TODO: fix Meter to redraw title for this change
127            }
128            return;
129        }
130        
131        //bail if other message types received
132        if (!r.isCurrentReply() && !r.isMeterReply()) return;
133
134        //also stop processing the older replies if the newer lists are supported
135        if (tc.getCommandStation().isCurrentListSupported()) return;
136
137        log.debug("Handling reply: '{}'", r);
138
139        //assume old-style current message and default name and settings
140        String meterName = "CurrentPct";
141        double meterValue = 0.0;
142        String meterType = DCCppConstants.CURRENT;
143        Meter.Unit meterUnit = Meter.Unit.Percent;
144        double minValue = 0.0;
145        double maxValue = 100.0;
146        double resolution = 0.1;
147        double warnValue = 100.0; //TODO: use when Meter updated to take advantage of it
148
149        //use settings from message if Meter reply
150        if (r.isMeterReply()) {
151            meterName = r.getMeterName();
152            meterValue= r.getMeterValue();
153            meterType = r.getMeterType();
154            minValue  = r.getMeterMinValue();
155            maxValue  = r.getMeterMaxValue();
156            resolution= r.getMeterResolution();
157            meterUnit = r.getMeterUnit();
158            warnValue = r.getMeterWarnValue();
159        }
160
161        //create, store and register the meter if not yet defined
162        if (!meters.containsKey(meterName)) {
163            log.debug("Adding new meter '{}' of type '{}' with unit '{}' {}",
164                    meterName, meterType, meterUnit, warnValue);
165            Meter newMeter;
166            String sysName = systemPrefix + beanType + meterType + "_" + meterName;
167            if (meterType.equals(DCCppConstants.VOLTAGE)) {
168                newMeter = new DefaultMeter.DefaultVoltageMeter(
169                        sysName, meterUnit, minValue, maxValue, resolution, updateTask);
170            } else {
171                newMeter = new DefaultMeter.DefaultCurrentMeter(
172                        sysName, meterUnit, minValue, maxValue, resolution, updateTask);
173            }
174            //store meter by incoming name for lookup later
175            meters.put(meterName, newMeter);
176            InstanceManager.getDefault(MeterManager.class).register(newMeter);
177        }
178
179        //calculate percentage meter value if original current reply message type received
180        if (r.isCurrentReply()) {
181            meterValue = ((r.getCurrentInt() * 1.0f) / (DCCppConstants.MAX_CURRENT * 1.0f)) * 100.0f ;
182        }
183
184        //set the newValue for the meter
185        Meter meter = meters.get(meterName);
186        log.debug("Setting value for '{}' to {}" , meterName, meterValue);
187        try {
188            meter.setCommandedAnalogValue(meterValue);
189        } catch (JmriException e) {
190            log.error("exception thrown when setting meter '{}' value {}", meterName, meterValue, e);
191        }
192    }
193
194    @Override
195    public void message(DCCppMessage m) {
196        // Do nothing
197    }
198
199    /* dispose of all defined meters             */
200    /* NOTE: I don't know if this is ever called */
201    public void dispose() {
202        meters.forEach((k, v) -> {
203            log.debug("disposing '{}'", k);
204            updateTask.disable(v);
205            InstanceManager.getDefault(MeterManager.class).deregister(v);
206            updateTask.dispose(v);
207        });
208    }
209
210    // Handle message timeout notification, no retry
211    @Override
212    public void notifyTimeout(DCCppMessage msg) {
213        log.debug("Notified of timeout on message '{}', not retrying", msg);
214    }
215
216    private final static Logger log = LoggerFactory.getLogger(DCCppPredefinedMeters.class);
217
218}