001package jmri.jmrix.tams; 002 003import java.util.Hashtable; 004import javax.annotation.Nonnull; 005import jmri.JmriException; 006import jmri.Sensor; 007import org.slf4j.Logger; 008import org.slf4j.LoggerFactory; 009 010/** 011 * Implement sensor manager for Tams systems. The Manager handles all the state\ 012 * changes Requires v1.4.7 or higher of TAMS software to work correctly 013 * <p> 014 * System names are "TSnnn:yy", where T is the user configurable system prefix, 015 * nnn is the Tams Object Number for a given S88 Bus Module and 016 * yy is the port on that module. 017 * 018 * @author Kevin Dickerson Copyright (C) 2009 019 * @author Jan Boen and Sergiu Costan 020 * 021 * Rework Poll for status using binary commands send xEvtSen (78 CB)h this 022 * returns multiple bytes first byte address of the S88 sensor, second and third 023 * bytes = values of that sensor this repeats for each sensor with changes the 024 * last byte contains 00h this means all reports have been received 025 * 026 * xEvtSen reports sensor changes 027 */ 028public class TamsSensorManager extends jmri.managers.AbstractSensorManager implements TamsListener { 029 030 public int maxSE; //Will hold the highest value of board number x 2 and we use this value to determine to tell the Tams MC how many S88 half-modules to poll 031 032 public TamsSensorManager(TamsSystemConnectionMemo memo) { 033 super(memo); 034 init(); 035 } 036 037 private void init() { 038 TamsTrafficController tc = getMemo().getTrafficController(); 039 //Connect to the TrafficManager 040 tc.addTamsListener(this); 041 TamsMessage tm = TamsMessage.setXSR();//auto reset after reading S88 042 tc.sendTamsMessage(tm, this); 043 log.debug("Sending TamsMessage = {} , isBinary = {} and replyType = {}", 044 tm.toString(), tm.isBinary(), tm.getReplyType()); 045 //Add polling for sensor state changes 046 tm = TamsMessage.getXEvtSen(); //reports only sensors with changed states 047 //tc.sendTamsMessage(tm, this); 048 tc.addPollMessage(tm, this); 049 log.debug("TamsMessage added to poll queue = {} {} and replyType = {}", 050 jmri.util.StringUtil.appendTwoHexFromInt(tm.getElement(0) & 0xFF, ""), 051 jmri.util.StringUtil.appendTwoHexFromInt(tm.getElement(1) & 0xFF, ""), 052 tm.getReplyType()); 053 } 054 055 //The hash table simply holds the object number against the TamsSensor ref. 056 private final Hashtable<Integer, Hashtable<Integer, TamsSensor>> _ttams = new Hashtable<>(); // stores known Tams Obj 057 058 /** 059 * {@inheritDoc} 060 */ 061 @Override 062 @Nonnull 063 public TamsSystemConnectionMemo getMemo() { 064 return (TamsSystemConnectionMemo) memo; 065 } 066 067 /** 068 * {@inheritDoc} 069 * <p> 070 * System name is normalized to ensure uniqueness. 071 * @throws IllegalArgumentException when SystemName can't be converted 072 */ 073 @Override 074 @Nonnull 075 protected Sensor createNewSensor(@Nonnull String systemName, String userName) throws IllegalArgumentException { 076 TamsTrafficController tc = getMemo().getTrafficController(); 077 TamsSensor s = new TamsSensor(systemName, userName); 078 log.debug("Creating new TamsSensor: {}", systemName); 079 if (systemName.contains(":")) { 080 int board; 081 int channel; 082 083 String curAddress = systemName.substring(getSystemPrefix().length() + 1); 084 int seperator = curAddress.indexOf(':'); 085 try { 086 board = Integer.parseInt(curAddress.substring(0, seperator)); 087 log.debug("Creating new TamsSensor with board: {}", board); 088 if (!_ttams.containsKey(board)) { 089 _ttams.put(board, new Hashtable<>()); 090 //log.debug("_ttams: {}", _ttams.toString()); 091 } 092 } catch (NumberFormatException ex) { 093 throw new IllegalArgumentException("Unable to convert " + // NOI18N 094 systemName.substring(getSystemPrefix().length() + 1) + 095 " into the Module and port format of nn:xx"); // NOI18N 096 } 097 Hashtable<Integer, TamsSensor> sensorList = _ttams.get(board); 098 try { 099 channel = Integer.parseInt(curAddress.substring(seperator + 1)); 100 if (!sensorList.containsKey(channel)) { 101 sensorList.put(channel, s); 102 } 103 } catch (NumberFormatException ex) { 104 throw new IllegalArgumentException("Unable to convert " + // NOI18N 105 systemName.substring(getSystemPrefix().length() + 1) + 106 " into the Module and port format of nn:xx"); // NOI18N 107 } 108 if ((board * 2) > maxSE) {//Check if newly defined board number is higher than what we know 109 maxSE = board * 2;//adjust xSE and inform Tams MC 110 log.debug("Changed xSE to {}", maxSE); 111 TamsMessage tm = new TamsMessage("xSE " + Integer.toString(maxSE)); 112 tm.setBinary(false); 113 tm.setReplyType('S'); 114 tc.sendTamsMessage(tm, this); 115 } 116 } 117 //Probably sending the status check 16 times but should work... 118 //Get initial status of sensors 119 TamsMessage tm = TamsMessage.setXSensOff(); //force report from sensors with at least 1 port set 120 tc.sendTamsMessage(tm, this); 121 tm = TamsMessage.getXEvtSen(); //reports only sensors with changed states 122 tc.sendTamsMessage(tm, this); 123 log.debug("Returning this sensor: {}", s.toString()); 124 return s; 125 } 126 127 @Override 128 @Nonnull 129 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 130 if (!curAddress.contains(":")) { 131 throw new JmriException("Hardware Address passed should be past in the form 'Module:port', was " + curAddress); 132 } 133 134 //Address format passed is in the form of board:channel or T:turnout address 135 int seperator = curAddress.indexOf(':'); 136 try { 137 board = Integer.parseInt(curAddress.substring(0, seperator)); 138 } catch (NumberFormatException ex) { 139 throw new JmriException("First part of "+curAddress+" in front of : should be a number"); 140 } 141 try { 142 port = Integer.parseInt(curAddress.substring(seperator + 1)); 143 } catch (NumberFormatException ex) { 144 throw new JmriException("Port Address, Second part of "+curAddress+" after : should be a number"); 145 } 146 147 if (port == 0 || port > 16) { 148 throw new JmriException("Port number in "+curAddress+" must be between 1 and 16"); 149 } 150 StringBuilder sb = new StringBuilder(); 151 sb.append(getSystemPrefix()); 152 sb.append("S"); 153 sb.append(board); 154 sb.append(":"); 155 //Little work around to pad single digit address out. 156 padPortNumber(port, sb); 157 return sb.toString(); 158 } 159 160 /** 161 * Validates to contain at least 1 number . . . 162 * <p> 163 * TODO: add custom TamsSensor validation. 164 * {@inheritDoc} 165 */ 166 @Override 167 @Nonnull 168 public String validateSystemNameFormat(@Nonnull String name, @Nonnull java.util.Locale locale) throws jmri.NamedBean.BadSystemNameException { 169 return validateTrimmedMin1NumberSystemNameFormat(name,locale); 170 } 171 172 int board = 0; 173 int port = 0; 174 175 void padPortNumber(int portNo, StringBuilder sb) { 176 if (portNo < 10) { 177 sb.append("0"); 178 } 179 sb.append(portNo); 180 } 181 182 /** 183 * Determine if it is possible to add a range of sensors in numerical order 184 * eg 1 to 16, primarily used to enable/disable the add range box in the add 185 * sensor panel. 186 * 187 * @return true 188 */ 189 @Override 190 public boolean allowMultipleAdditions(@Nonnull String systemName) { 191 return true; 192 } 193 194 // to listen for status changes from Tams system 195 @Override 196 public void reply(TamsReply r) { 197 //log.debug("ReplyType = " + tm.getReplyType() + ", Binary? = " + tm.isBinary()+ ", OneByteReply = " + tm.getReplyOneByte()); 198 if (getMemo().getTrafficController().replyType == 'S') {//Only handle Sensor events 199 log.debug("*** Tams Sensor Reply ***"); 200 if ( getMemo().getTrafficController().replyBinary ) { 201 log.debug("Reply to binary command = {}", r ); 202 if ((r.getNumDataElements() > 1) && (r.getElement(0) > 0x00)) { 203 // Here we break up a long sensor related TamsReply into individual S88 module status' 204 int numberOfReplies = r.getNumDataElements() / 3; 205 //log.debug("Incoming Reply = "); 206 //for (int i = 0; i < r.getNumDataElements(); i++) { 207 //log.debug("Byte " + i + " = " + jmri.util.StringUtil.appendTwoHexFromInt(r.getElement(i) & 0xFF, "")); 208 //} 209 //log.debug("length of reply = " + r.getNumDataElements() + " & number of replies = " + numberOfReplies); 210 for (int i = 0; i < numberOfReplies; i++) { 211 //create a new TamsReply and pass it to the decoder 212 TamsReply tr = new TamsReply(); 213 tr.setBinary(true); 214 tr.setElement(0, r.getElement(3 * i)); 215 tr.setElement(1, r.getElement(3 * i + 1)); 216 tr.setElement(2, r.getElement(3 * i + 2)); 217 log.debug("Going to pass this to the decoder = {} {} {}", 218 tr.getElement(0), tr.getElement(1), tr.getElement(2)); 219 //The decodeSensorState will do the actual decoding of each individual S88 port 220 decodeSensorState(tr); 221 } 222 } 223 } 224 } 225 } 226 227 @Override 228 public void message(TamsMessage m) { 229 // messages are ignored 230 } 231 232 private void decodeSensorState(TamsReply r) { 233 //reply to XEvtSen consists of 3 bytes per S88 module 234 //byte 1 = S88 board number 1 to 52 in binary format 235 //byte 2 = bits 1 to 8 236 //byte 3 = bits 9 to 16 237 String sensorprefix = getSystemPrefix() + "S" + r.getElement(0) + ":"; 238 log.debug("Decoding sensor: {}", sensorprefix); 239 log.debug("Lower Byte: {}", r.getElement(1)); 240 log.debug("Upper Byte: {}", r.getElement(2)); 241 Hashtable<Integer, TamsSensor> sensorList = _ttams.get(board); 242 int i = (r.getElement(1) & 0xff) << 8;//first 8 ports in second element of the reply 243 log.debug("i after loading first byte= {}", Integer.toString(i,2)); 244 i = i + (r.getElement(2) & 0xff);//first 8 ports in third element of the reply 245 log.debug("i after loading second byte= {}", Integer.toString(i,2)); 246 int mask = 0b1000000000000000; 247 for (int j = 1; j <= 16; j++) { 248 int result = i & mask; 249 //log.debug("mask= {}", Integer.toString(mask, 2)); 250 //log.debug("result= {}", Integer.toString(result, 2)); 251 if (sensorList != null) { 252 TamsSensor ms = sensorList.get(j); 253 log.debug("ms: {}", ms); 254 if (ms == null) { 255 log.debug("ms = NULL!"); 256 StringBuilder sb = new StringBuilder(); 257 sb.append(sensorprefix); 258 //Little work around to pad single digit address out. 259 padPortNumber(j, sb); 260 try { 261 ms = (TamsSensor) provideSensor(sb.toString()); 262 } catch (Exception e){ 263 log.warn("Could not provide Sensor {}: {}",sb.toString(),e.getLocalizedMessage()); 264 } 265 } 266 if (ms != null) { 267 log.debug("ms = exists and is not null"); 268 if (result == 0) { 269 ms.setOwnState(Sensor.INACTIVE); 270 log.debug("{}{} INACTIVE", sensorprefix, j); 271 } else { 272 log.debug("{}{} ACTIVE", sensorprefix, j); 273 ms.setOwnState(Sensor.ACTIVE); 274 } 275 } 276 } 277 mask = mask / 2; 278 } 279 log.debug("sensor decoding is done"); 280 } 281 282 @Override 283 public void dispose() { 284 getMemo().getTrafficController().removePollMessage(TamsMessage.getXEvtSen(), this); 285 getMemo().getTrafficController().removeTamsListener(this); 286 super.dispose(); 287 } 288 289 private final static Logger log = LoggerFactory.getLogger(TamsSensorManager.class); 290}