1 /* 2 * Copyright 2014 Intel Corporation All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.intel.thermal; 18 19 import android.os.UEventObserver; 20 import android.util.Log; 21 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.Hashtable; 25 import java.util.Iterator; 26 27 /** 28 * The ThermalZone class contains attributes of a Thermal zone. A Thermal zone 29 * can have one or more sensors associated with it. Whenever the temperature of a 30 * thermal zone crosses the thresholds configured, actions are taken. 31 */ 32 public class ThermalZone { 33 34 private static final String TAG = "ThermalZone"; 35 36 protected int mZoneID; /* ID of the Thermal zone */ 37 protected int mCurrThermalState; /* Current thermal state of the zone */ 38 protected int mCurrEventType; /* specifies thermal event type, HIGH or LOW */ 39 protected String mZoneName; /* Name of the Thermal zone */ 40 protected int mMaxStates; 41 /* List of sensors under this thermal zone */ 42 protected ArrayList<ThermalSensor> mThermalSensors = null; 43 // sensor name - sensorAttrib object hash to improve lookup performace 44 // during runtime thermal monitoring like re-programming sensor thresholds 45 // calculating weighted zone temp. 46 protected Hashtable<String, ThermalSensorAttrib> mThermalSensorsAttribMap = null; 47 protected int mZoneTemp; /* Temperature of the Thermal Zone */ 48 protected boolean mSupportsEmulTemp = true; 49 private int mDebounceInterval; /* Debounce value to avoid thrashing of throttling actions */ 50 private Integer mPollDelay[]; /* Delay between sucessive polls in milli seconds */ 51 protected boolean mSupportsUEvent; /* Determines if Sensor supports Uevents */ 52 private String mZoneLogic; /* Logic to be used to determine thermal state of zone */ 53 private boolean mIsZoneActive = false; 54 private boolean mMaxThreshExceeded = false; 55 private int mTripMax; 56 private int mOffset = 0; 57 protected Integer mZoneTempThresholds[]; /* Array containing temperature thresholds */ 58 // mZoneTempThresholdsRaw contains the Raw thresholds (as specified in xml). 59 // mZoneTempThresholds contsins the calibrated thresholds that are used 60 // to detect zone state change at runtime. 61 protected Integer mZoneTempThresholdsRaw[]; 62 63 /* MovingAverage related declarations */ 64 private int mRecordedValuesHead = -1; /* Index pointing to the head of past values of sensor */ 65 private int mRecordedValues[]; /* Recorded values of sensor */ 66 private int mNumberOfInstances[]; /* Number of recorded instances to be considered */ 67 private ArrayList<Integer> mWindowList = null; 68 private boolean mIsMovingAverage = false; /* By default false */ 69 70 // override this method in ModemZone to limit num states to default setMaxStates(int state)71 public void setMaxStates(int state) { 72 mMaxStates = state; 73 } 74 updateMaxStates(int state)75 public void updateMaxStates(int state) { 76 setMaxStates(state); 77 } 78 getMaxStates()79 public int getMaxStates() { 80 return mMaxStates; 81 } 82 getMovingAverageFlag()83 public boolean getMovingAverageFlag() { 84 return mIsMovingAverage; 85 } 86 setMovingAvgWindow(ArrayList<Integer> windowList)87 public void setMovingAvgWindow(ArrayList<Integer> windowList) { 88 int maxValue = Integer.MIN_VALUE; // -2^31 89 90 if (windowList == null || mPollDelay == null) { 91 Log.i(TAG, "setMovingAvgWindow input is null"); 92 mIsMovingAverage = false; 93 return; 94 } 95 mNumberOfInstances = new int[windowList.size()]; 96 if (mNumberOfInstances == null) { 97 Log.i(TAG, "failed to create poll windowlist"); 98 mIsMovingAverage = false; 99 return; 100 } 101 mIsMovingAverage = true; 102 for (int i = 0; i < windowList.size(); i++) { 103 if (mPollDelay[i] == 0) { 104 mIsMovingAverage = false; 105 Log.i(TAG, "Polling delay is zero, WMA disabled\n"); 106 return; 107 } 108 mNumberOfInstances[i] = windowList.get(i) / mPollDelay[i]; 109 if (mNumberOfInstances[i] <= 0) { 110 mIsMovingAverage = false; 111 Log.i(TAG, "Polling delay greater than moving average window, WMA disabled\n"); 112 return; 113 } 114 maxValue = Math.max(mNumberOfInstances[i], maxValue); 115 } 116 mRecordedValues = new int[maxValue]; 117 } 118 movingAverageTemp()119 public int movingAverageTemp() { 120 int index, calIndex; 121 int predictedTemp = 0; 122 123 mRecordedValuesHead = (mRecordedValuesHead + 1) % mRecordedValues.length; 124 mRecordedValues[mRecordedValuesHead] = mZoneTemp; 125 126 // Sensor State starts with -1, InstancesList starts with 0 127 for (index = 0; index < mNumberOfInstances[mCurrThermalState + 1]; index++) { 128 calIndex = mRecordedValuesHead - index; 129 if (calIndex < 0) { 130 calIndex = mRecordedValues.length + calIndex; 131 } 132 predictedTemp += mRecordedValues[calIndex]; 133 } 134 return predictedTemp / index; 135 } 136 printAttrs()137 public void printAttrs() { 138 Log.i(TAG, "mZoneID:" + Integer.toString(mZoneID)); 139 Log.i(TAG, "mDBInterval: " + Integer.toString(mDebounceInterval)); 140 Log.i(TAG, "mZoneName:" + mZoneName); 141 Log.i(TAG, "mSupportsUEvent:" + Boolean.toString(mSupportsUEvent)); 142 Log.i(TAG, "mZoneLogic:" + mZoneLogic); 143 Log.i(TAG, "mOffset:" + mOffset); 144 Log.i(TAG, "mPollDelay[]:" + Arrays.toString(mPollDelay)); 145 Log.i(TAG, "mZoneTempThresholds[]: " + Arrays.toString(mZoneTempThresholds)); 146 Log.i(TAG, "mNumberOfInstances[]: " + Arrays.toString(mNumberOfInstances)); 147 Log.i(TAG, "mEmulTempFlag:" + mSupportsEmulTemp); 148 Log.i(TAG, "MaxStates:" + getMaxStates()); 149 printStateThresholdMap(); 150 printSensors(); 151 printSensorAttribList(); 152 } 153 printStateThresholdMap()154 public void printStateThresholdMap() { 155 if (mZoneTempThresholds == null 156 || mZoneTempThresholds.length < ThermalManager.DEFAULT_NUM_ZONE_STATES) return; 157 StringBuilder s = new StringBuilder(); 158 s.append("[" + "State0" + "<" + mZoneTempThresholds[1] + "];"); 159 for (int index = 2; index < getMaxStates(); index++) { 160 int curstate = index - 1; 161 s.append("[" + mZoneTempThresholds[index - 1] + "<=" + "State" 162 + curstate + "<" + mZoneTempThresholds[index] + "];"); 163 } 164 Log.i(TAG, "states-threshold map:" + s.toString()); 165 } 166 printSensors()167 private void printSensors() { 168 if (mThermalSensors == null) return; 169 StringBuilder s = new StringBuilder(); 170 for (ThermalSensor ts : mThermalSensors) { 171 if (ts != null) { 172 s.append(ts.getSensorName()); 173 s.append(","); 174 } 175 } 176 Log.i(TAG, "zoneID: " + mZoneID + " sensors mapped:" + s.toString()); 177 } 178 printSensorAttribList()179 private void printSensorAttribList() { 180 if (mThermalSensorsAttribMap == null) return; 181 Iterator it = (Iterator) mThermalSensorsAttribMap.keySet().iterator(); 182 if (it == null) return; 183 ThermalSensorAttrib sensorAttrib = null; 184 while (it.hasNext()) { 185 sensorAttrib = mThermalSensorsAttribMap.get((String) it.next()); 186 if (sensorAttrib != null) sensorAttrib.printAttrs(); 187 } 188 } 189 ThermalZone()190 public ThermalZone() { 191 mCurrThermalState = ThermalManager.THERMAL_STATE_OFF; 192 mZoneTemp = ThermalManager.INVALID_TEMP; 193 } 194 getStateAsString(int index)195 public static String getStateAsString(int index) { 196 if (index < -1 || index > 3) 197 return "Invalid"; 198 return ThermalManager.STATE_NAMES[index + 1]; 199 } 200 getEventTypeAsString(int type)201 public static String getEventTypeAsString(int type) { 202 return type == 0 ? "LOW" : "HIGH"; 203 } 204 setSensorList(ArrayList<ThermalSensorAttrib> sensorAtribList)205 public void setSensorList(ArrayList<ThermalSensorAttrib> sensorAtribList) { 206 if (sensorAtribList == null || ThermalManager.sSensorMap == null) return; 207 for (ThermalSensorAttrib sa : sensorAtribList) { 208 // since each object of sensor attrib list is already validated during 209 // parsing it is gauranteed that 'sa != null' and a valid sensor object 's' 210 // will be returned. Hence skipping null check.. 211 if (mThermalSensors == null) { 212 // first time allocation 213 mThermalSensors = new ArrayList<ThermalSensor>(); 214 if (mThermalSensors == null) { 215 // allocation failure. return 216 return; 217 } 218 } 219 if (mThermalSensorsAttribMap == null) { 220 // first time allocation 221 mThermalSensorsAttribMap = new Hashtable<String, ThermalSensorAttrib>(); 222 if (mThermalSensorsAttribMap == null) return; 223 } 224 mThermalSensors.add(ThermalManager.getSensor(sa.getSensorName())); 225 mThermalSensorsAttribMap.put(sa.getSensorName(), sa); 226 } 227 } 228 getThermalSensorList()229 public ArrayList<ThermalSensor> getThermalSensorList() { 230 return mThermalSensors; 231 } 232 getZoneState()233 public int getZoneState() { 234 return mCurrThermalState; 235 } 236 setZoneState(int state)237 public void setZoneState(int state) { 238 mCurrThermalState = state; 239 } 240 getEventType()241 public int getEventType() { 242 return mCurrEventType; 243 } 244 setEventType(int type)245 public void setEventType(int type) { 246 mCurrEventType = type; 247 } 248 setZoneTemp(int temp)249 public void setZoneTemp(int temp) { 250 mZoneTemp = temp; 251 } 252 getZoneTemp()253 public int getZoneTemp() { 254 return mZoneTemp; 255 } 256 setZoneId(int id)257 public void setZoneId(int id) { 258 mZoneID = id; 259 } 260 getZoneId()261 public int getZoneId() { 262 return mZoneID; 263 } 264 setZoneName(String name)265 public void setZoneName(String name) { 266 mZoneName = name; 267 } 268 getZoneName()269 public String getZoneName() { 270 return mZoneName; 271 } 272 setSupportsUEvent(int flag)273 public void setSupportsUEvent(int flag) { 274 mSupportsUEvent = (flag == 1); 275 } 276 isUEventSupported()277 public boolean isUEventSupported() { 278 return mSupportsUEvent; 279 } 280 isMaxThreshExceed()281 public boolean isMaxThreshExceed() { 282 return mMaxThreshExceeded; 283 } 284 setZoneLogic(String type)285 public void setZoneLogic(String type) { 286 mZoneLogic = type; 287 } 288 getZoneLogic()289 public String getZoneLogic() { 290 return mZoneLogic; 291 } 292 setDBInterval(int interval)293 public void setDBInterval(int interval) { 294 mDebounceInterval = interval; 295 } 296 getDBInterval()297 public int getDBInterval() { 298 return mDebounceInterval; 299 } 300 getOffset()301 public int getOffset() { 302 return mOffset; 303 } 304 setOffset(int offset)305 public void setOffset(int offset) { 306 mOffset = offset; 307 } 308 setPollDelay(ArrayList<Integer> delayList)309 public void setPollDelay(ArrayList<Integer> delayList) { 310 if (delayList != null) { 311 mPollDelay = new Integer[delayList.size()]; 312 if (mPollDelay != null) { 313 mPollDelay = delayList.toArray(mPollDelay); 314 } 315 } 316 } 317 getPollDelay()318 public Integer[] getPollDelay() { 319 return mPollDelay; 320 } 321 322 /** 323 * In polldelay array, index of TOFF = 0, Normal = 1, Warning = 2, Alert = 324 * 3, Critical = 4. Whereas a ThermalZone states are enumerated as TOFF = 325 * -1, Normal = 0, Warning = 1, Alert = 2, Critical = 3. Hence we add 1 326 * while querying poll delay 327 */ getPollDelay(int index)328 public int getPollDelay(int index) { 329 index++; 330 331 // If poll delay is requested for an invalid state, return the delay 332 // corresponding to normal state 333 if (index < 0 || index >= mPollDelay.length) 334 index = 1; 335 336 return mPollDelay[index]; 337 } 338 setZoneTempThreshold(ArrayList<Integer> thresholdList)339 public void setZoneTempThreshold(ArrayList<Integer> thresholdList) { 340 if (mZoneName.contains("CPU") || mZoneName.contains("SoC")) 341 mTripMax = ThermalManager.sTjMaxTemp; 342 else 343 mTripMax = ThermalManager.sMaxSkinTrip; 344 345 if (thresholdList != null ) { 346 // In Uevent mode, if any threshold specified for a particular 347 // zone exceeds the max threshold temp, we de-activate that zone. 348 if (mSupportsUEvent) { 349 for (int i = 0; i < thresholdList.size(); i++) { 350 if (thresholdList.get(i) <= mTripMax) 351 continue; 352 else 353 mMaxThreshExceeded = true; 354 } 355 } 356 if (mMaxThreshExceeded == false) { 357 mZoneTempThresholds = new Integer[thresholdList.size()]; 358 mZoneTempThresholdsRaw = new Integer[thresholdList.size()]; 359 if (mZoneTempThresholds != null) { 360 mZoneTempThresholds = thresholdList.toArray(mZoneTempThresholds); 361 } 362 if (mZoneTempThresholdsRaw != null) { 363 mZoneTempThresholdsRaw = thresholdList.toArray(mZoneTempThresholdsRaw); 364 } 365 } 366 } 367 } 368 getZoneTempThreshold(int index)369 public int getZoneTempThreshold(int index) { 370 if (index < 0 || index >= mZoneTempThresholds.length) 371 return -1; 372 return mZoneTempThresholds[index]; 373 } 374 getZoneTempThreshold()375 public Integer[] getZoneTempThreshold() { 376 return mZoneTempThresholds; 377 } 378 getZoneActiveStatus()379 public boolean getZoneActiveStatus() { 380 return mIsZoneActive; 381 } 382 computeZoneActiveStatus()383 public void computeZoneActiveStatus() { 384 // init again. needed because of a profile change 385 mIsZoneActive = false; 386 if (mSupportsUEvent) { 387 // Zone de-activated when any threshold for that zone is 388 // above the allowed Max threshold. 389 if (mMaxThreshExceeded == true) { 390 Log.i(TAG, "deactivate zone:" + mZoneName + 391 ". Zone Threshold exceeds max trip temp:" + mTripMax); 392 mIsZoneActive = false; 393 return; 394 } 395 } 396 if (mZoneTempThresholds == null) { 397 Log.i(TAG, "deactivate zone:" + getZoneName() + " threshold list is NULL! "); 398 mIsZoneActive = false; 399 return; 400 } 401 // 1. minimum number of states supported must be DEFAULT NUM STATES 402 // 2. if sensor list null disable zone 403 if (mMaxStates < ThermalManager.DEFAULT_NUM_ZONE_STATES) { 404 // substract by 1 since TOFF is transparent to USER 405 int minStateSupport = ThermalManager.DEFAULT_NUM_ZONE_STATES - 1; 406 Log.i(TAG, "deactivate zone:" + getZoneName() + " supports < " 407 + minStateSupport + " states"); 408 mIsZoneActive = false; 409 return; 410 } 411 if (mThermalSensors == null) { 412 Log.i(TAG, "deactivate zone:" + getZoneName() + " sensor list null! "); 413 mIsZoneActive = false; 414 return; 415 } 416 417 if (mSupportsUEvent) { 418 // if uevent just check the first sensor 419 ThermalSensor s = mThermalSensors.get(0); 420 if (s != null && s.getSensorActiveStatus()) { 421 mIsZoneActive = true; 422 return; 423 } 424 } else { 425 if (mPollDelay == null) { 426 Log.i(TAG, "deactivate zone:" + getZoneName() 427 + " polldelay list null in poll mode! "); 428 mIsZoneActive = false; 429 return; 430 } 431 if (mZoneTempThresholds.length != mPollDelay.length) { 432 Log.i(TAG, "deactivate zone:" + getZoneName() 433 + " mismatch of polldelay and threshold list in polling mode!"); 434 mIsZoneActive = false; 435 return; 436 } 437 for (ThermalSensor ts : mThermalSensors) { 438 if (ts != null && ts.getSensorActiveStatus()) { 439 mIsZoneActive = true; 440 return; 441 } 442 } 443 } 444 } 445 setEmulTempFlag(int flag)446 public void setEmulTempFlag(int flag) { 447 mSupportsEmulTemp = (flag == 1); 448 } 449 getEmulTempFlag()450 public boolean getEmulTempFlag() { 451 return mSupportsEmulTemp; 452 } 453 454 // override in Specific zone class which inherit ThermalZone startMonitoring()455 public void startMonitoring() { 456 } 457 458 // override in Specific zone class which inherit ThermalZone stopMonitoring()459 public void stopMonitoring() { 460 } 461 462 // override in ModemZone to unregister Modem specific intents 463 // override in VirtualThermalZone to stop UEvent observers unregisterReceiver()464 public void unregisterReceiver() { 465 if (isUEventSupported()) { 466 mUEventObserver.stopObserving(); 467 } 468 } 469 470 // override in VirtualThermalZone class startEmulTempObserver()471 public void startEmulTempObserver() { 472 } 473 474 // override in VirtualThermalZone class calibrateThresholds()475 public void calibrateThresholds() { 476 } 477 478 /** 479 * Function that calculates the state of the Thermal Zone after reading 480 * temperatures of all sensors in the zone. This function is used when a 481 * zone operates in polling mode. 482 */ isZoneStateChanged()483 public boolean isZoneStateChanged() { 484 for (int i = 0; i < mThermalSensors.size(); i++) { 485 if (mThermalSensors.get(i).getSensorActiveStatus()) { 486 mThermalSensors.get(i).updateSensorTemp(); 487 } 488 } 489 return updateZoneParams(); 490 } 491 492 /** 493 * Function that calculates the state of the Thermal Zone after reading 494 * temperatures of all sensors in the zone. This is an overloaded function 495 * used when a zone supports UEvent notifications from kernel. When a 496 * sensor sends an UEvent, it also sends its current temperature as a 497 * parameter of the UEvent. 498 */ isZoneStateChanged(ThermalSensor s, int temp)499 public boolean isZoneStateChanged(ThermalSensor s, int temp) { 500 if (s == null) return false; 501 s.setCurrTemp(temp); 502 setZoneTemp(temp); 503 return updateZoneParams(); 504 } 505 506 /** 507 * Method to update Zone Temperature and Zone Thermal State 508 */ updateZoneParams()509 public boolean updateZoneParams() { 510 int newZoneState; 511 int prevZoneState = mCurrThermalState; 512 513 if (!updateZoneTemp()) { 514 return false; 515 } 516 517 newZoneState = ThermalUtils.calculateThermalState(mZoneTemp, mZoneTempThresholds); 518 if (newZoneState == prevZoneState) { 519 return false; 520 } 521 522 if (newZoneState == ThermalManager.THERMAL_STATE_OFF) { 523 setZoneState(newZoneState); 524 return true; 525 } 526 527 int threshold = ThermalUtils.getLowerThresholdTemp(prevZoneState, mZoneTempThresholds); 528 // For Interrupt based zones, HW (should) takes care of the debounce. 529 if (!isUEventSupported()) { 530 if (newZoneState < prevZoneState && getZoneTemp() > (threshold - getDBInterval())) { 531 Log.i(TAG, " THERMAL_LOW_EVENT for zone:" + getZoneName() 532 + " rejected due to debounce interval"); 533 return false; 534 } 535 } 536 537 setZoneState(newZoneState); 538 setEventType(newZoneState > prevZoneState 539 ? ThermalManager.THERMAL_HIGH_EVENT 540 : ThermalManager.THERMAL_LOW_EVENT); 541 return true; 542 } 543 updateZoneTemp()544 public boolean updateZoneTemp() { 545 return false; 546 } 547 registerUevent()548 public void registerUevent() { 549 int indx; 550 551 if (mThermalSensors == null) return; 552 if (mThermalSensors.size() > 1) { 553 Log.i(TAG, "for zone:" + getZoneName() + " in uevent mode only first sensor used!"); 554 } 555 ThermalSensor sensor = mThermalSensors.get(0); 556 if (sensor == null) return; 557 String path = sensor.getUEventDevPath(); 558 if (path.equalsIgnoreCase("invalid")) return; 559 // first time update of sensor temp and zone temp 560 sensor.updateSensorTemp(); 561 setZoneTemp(sensor.getCurrTemp()); 562 if (updateZoneParams()) { 563 // first intent after initialization 564 sendThermalEvent(); 565 } 566 mUEventObserver.startObserving(path); 567 programThresholds(sensor); 568 } 569 570 public UEventObserver mUEventObserver = new UEventObserver() { 571 @Override 572 public void onUEvent(UEventObserver.UEvent event) { 573 String sensorName; 574 int sensorTemp, errorVal, eventType = -1; 575 ThermalZone zone; 576 if (mThermalSensors == null) return; 577 578 // Name of the sensor and current temperature are mandatory parameters of an UEvent 579 sensorName = event.get("NAME"); 580 sensorTemp = Integer.parseInt(event.get("TEMP")); 581 582 // eventType is an optional parameter. so, check for null case 583 if (event.get("EVENT") != null) 584 eventType = Integer.parseInt(event.get("EVENT")); 585 586 if (sensorName != null) { 587 Log.i(TAG, "UEvent received for sensor:" + sensorName + " temp:" + sensorTemp); 588 // check the name against the first sensor 589 ThermalSensor sensor = mThermalSensors.get(0); 590 if (sensor != null && sensor.getSensorName() != null 591 && sensor.getSensorName().equalsIgnoreCase(sensorName)) { 592 // Adjust the sensor temperature based on the 'error correction' temperature. 593 // For 'LOW' event, debounce interval will take care of this. 594 errorVal = sensor.getErrorCorrectionTemp(); 595 if (eventType == ThermalManager.THERMAL_HIGH_EVENT) 596 sensorTemp += errorVal; 597 598 if (isZoneStateChanged(sensor, sensorTemp)) { 599 sendThermalEvent(); 600 // reprogram threshold 601 programThresholds(sensor); 602 } 603 } 604 } 605 } 606 }; 607 programThresholds(ThermalSensor s)608 public void programThresholds(ThermalSensor s) { 609 if (s == null) return; 610 int zoneState = getZoneState(); 611 if (zoneState == ThermalManager.THERMAL_STATE_OFF) return; 612 int lowerTripPoint = ThermalUtils.getLowerThresholdTemp(zoneState, getZoneTempThreshold()); 613 int upperTripPoint = ThermalUtils.getUpperThresholdTemp(zoneState, getZoneTempThreshold()); 614 if (lowerTripPoint != ThermalManager.INVALID_TEMP 615 && upperTripPoint != ThermalManager.INVALID_TEMP) { 616 if (ThermalUtils.writeSysfs(s.getSensorLowTempPath(), lowerTripPoint) == -1) { 617 Log.i(TAG, "error while programming lower trip point:" + lowerTripPoint 618 + "for sensor:" + s.getSensorName()); 619 } 620 if (ThermalUtils.writeSysfs(s.getSensorHighTempPath(), upperTripPoint) == -1) { 621 Log.i(TAG, "error while programming upper trip point:" + upperTripPoint 622 + "for sensor:" + s.getSensorName()); 623 } 624 } 625 } 626 sendThermalEvent()627 public void sendThermalEvent() { 628 ThermalEvent event = new ThermalEvent(mZoneID, mCurrEventType, 629 mCurrThermalState, mZoneTemp, mZoneName, 630 ThermalManager.getCurProfileName()); 631 ThermalManager.addThermalEvent(event); 632 } 633 } 634