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}