Concurrent Programming in Java
© 1996 Doug Lea

Pull Flow Example: Home Heating System

This is an uncommon design solution for a common design exercise. The following Applet implements a simulation of a Home Heat System with four rooms:

The requirements for the system it implements are described in the following lightly edited excerpts from Booch's Object-Oriented Design (first edition) text. (Booch's version was in turn taken from other sources. They allegedly originated from an actual set of requirements for a real system. The implementation is fairly faithful even to some of the stranger-sounding aspects of the requirements.)

The basic function of the system is to regulate the flow of heat to individual rooms in a home in an attempt to maintain a working temperature tw established for each room. A given room needs heat to be turned on whenever the room temperature is <= tw-2 degrees, and does not need heat when the room temperature is >= tw+2 degrees. The working temperature for each room is calculated by the system as a function of a desired temperature, td (set by the user through a manual input device) and whether or not the room is occupied. If the room is occupied, the working temperature is set to the desired temperature. If the room is vacant, the working temperature is set to td-5 degrees Fahrenheit. Additionally, the system maintains a weekly living pattern, and attempts to raise room temperatures thirty minutes before occupancy is anticipated for a given room. The weekly living pattern is updated when variations to the established patterns occur two weeks in a row.

... The following input devices exist: Main switch, Desired temperature input, and Fault Reset. ... The following display devices are provided: Furnace Status, Fault indicator.

Heat is provided to each room of the home by hot water, which is heated by the furnace. Each room is equipped with a valve that controls the flow of hot water into the room. The valve can only be commanded to be fully open or fully closed.

The furnace consists of a boiler, an oil valve, an ignator, a blower, and a water temperature sensor. The furnace activates when the main switch is on and at least one room needs heat. The activation procedure is:

The furnace deactivates on a fault or when no rooms need heat. The deactivation procedure is: A fuel flow status sensor and an optical combustion sensor signal the system if abnormalities occur, in which case the system deactivates the furnace. The minimum time for the furnace to restart after prior operation is five minutes.
There are lots of ways to design such a system. (See, for example the versions in Booch's book, as well as DeChampeaux's OO Development Process and Metrics). We'll take a pull-driven approach. Pull-driven designs are uncommon for control systems, but work pretty well here, because most of the information flow across stages takes a function-like transformational form, that can be computed in a demand-driven manner.

The overall flow is illustrated in the following diagram, where solid lines represent (most of the) information flow (not control flow) implemented via software connections, and dashed lines represent (only some of the) ``external'' feedback in the system; for example turning on a RoomValve switch should cause a Room Valve Sensor to report that the valve is on. Since this is a simulation, all of these are also implemented as software connections.

Note: All source code described in this example can be obtained as the single file HHS.java.

From a pull-driven approach, the principal functioning of the system is for Room Valve Switches to decide whether to turn on or off. So the Room Valve Switches (one per room) play the role of sinks in this design, pulling information from other components. In the process of doing so, information passes through and is transformed by a lot of other components.

Representations and Interfaces

Only a few kinds of quantities are passed around by the components of this system, and all of them are simple values, not objects: Additionally, all of the possible stages report out only single values. All together this allows some economy in defining the main take-style pull interfaces:
interface BoolStage     { public boolean boolValue(); }
interface RealStage     { public float   realValue(); }
interface Timer         { public long    timeValue(); }

While most components in this system are pull-driven value-reporters, there are also some purely passive Effectors serving as push-driven sinks activated as by-products of other actions. Plus a few (like the Furnace Indicator) that both accept and report values, so act like ``variables''. These can all be captured by adding a few more interfaces:
interface BoolEffector  { public void    set(boolean newval); }
interface BoolVar extends BoolStage,  BoolEffector {}
interface RealEffector  { public void    set(float newval); }
interface RealVar extends RealStage, RealEffector {}

Simulated Stages

Because this program is a simulation, we'll need some strictly internal default implementations of each of the main interfaces. The following suffice:

class Bool implements BoolVar {
  protected boolean value_;
  public Bool(boolean initial)                 { value_ = initial; }
  public Bool()                                { value_ = false; }
  public synchronized boolean boolValue()      { return value_; }
  public synchronized void set(boolean newVal) { value_ = newVal; }
}

class Real implements RealVar {
  protected float value_;
  public Real(float initial)                  { value_ = initial; }
  public Real()                               { value_ = 0.0f; }
  public synchronized float realValue()       { return value_; }
  public synchronized void set(float newVal)  { value_ = newVal; }
}

class SystemTimer implements Timer {
  public long timeValue() { return System.currentTimeMillis(); }
};
For example, a Bool is used instead of an actual physical Valve Sensor. Rather than physically sensing whether a valve is open, we simply arrange that Room Valve Switches set the Bool to true or false. (Depending on the exact hardware available, we might have to do this in a real system. If there were not any way to sense the valves, this would be the best we could do. And even if there were, we might want to use a Bool as well, and build a stage that dealt with differences between assumed versus sensed status.)

Similarly we need a few arbitrary classes to simulate sensed values. Among other unshown classes, here is one that randomly reports true or false:

class RandomBoolStage implements BoolStage {
  protected float prob_;
  public  RandomBoolStage(float prob) { prob_ = prob; }
  public boolean boolValue() { return Math.random() <= prob_; }
};

Several other classes dress up these raw simulated stages using AWT widgets, for viewing in the Applet. For example, among other similar (unshown) classes, here is a wrapper around a BoolVar:

class LabelledBoolVar  extends java.awt.Label implements BoolVar {

  static final Color offColor = Color.lightGray;
  static final Color onColor = Color.green;
  protected BoolVar s_;

  public LabelledBoolVar(String label, BoolVar s) {
    super(label);
    s_ = s;
    display();
  }

  public LabelledBoolVar(String label) { this(label, new Bool()); }

  protected void display() {
    if (s_.boolValue()) setBackground(onColor); else setBackground(offColor);
  }

  public synchronized void set(boolean newVal) {
    boolean old = s_.boolValue();
    s_.set(newVal);
    if (newVal != old) display();
  }
  public synchronized boolean boolValue() { return s_.boolValue();  }
}

Function Stages

Many stages in this system serve as ``functoids'' -- objects that compute functions on their inputs only upon demand.

Most of these functions are highly specialized to this system, but we do need a few generic, reusable components. For example, we need ``OR-gates'', that report true if any of their inputs report true. These are used when OR'ing the Valve Sensors from the rooms to determine whether any of them are on. While most stages in this system have fixed links to predecessors, ORers can be connected to just about anything, using a list of attachable BoolStages:

class Orer implements BoolStage {
 protected collections.UpdatableBag inputs_;

 public Orer() { inputs_ = new collections.LinkedBuffer(); }

 public synchronized void attach(BoolStage s) { inputs_.addIfAbsent(s); }
 public synchronized void detach(BoolStage s) { inputs_.exclude(s); }

 public synchronized boolean  boolValue() {
   for (Enumeration e = inputs_.elements(); e.hasMoreElements(); ) 
     if (((BoolStage)(e.nextElement())).boolValue()) return true;
   return false;
 }

};
A more typical stage is the RoomHeatNeedCalculator, that combines information about the current actual temperature, desired temperature, (actual or expected) current occupancy, and current valve status (which is used to prevent hysteresis effects) to decide whether a room needs heat. Like several other Stages, this component is immutable, simply computing this function whenever asked, without ever changing its state. (Among other consequences, no methods need be synchronized.) Also, like most components of control systems, the details are slightly messy:
class RoomHeatNeedCalculator implements BoolStage {

  // Room update parameters
  static final float offsetWhenUnoccupied = 5.0f;
  static final float roomHeatThreshold = 2.0f;
  
  // Safety overrides for desired temperature settings

  static final float minimumDesiredRoomTemperature = 40.0f;
  static final float maximumDesiredRoomTemperature = 120.0f;

  protected BoolStage  valveSensor_;
  protected BoolStage  occupancyReporter_; 
  protected RealStage actualTemperatureSensor_;
  protected RealStage desiredTemperatureSensor_;

  public RoomHeatNeedCalculator(BoolStage  valveSensor, 
                                BoolStage  occupancyReporter,
                                RealStage actualTemperatureSensor,
                                RealStage desiredTemperatureSensor) {
    valveSensor_ = valveSensor; 
    occupancyReporter_ = occupancyReporter;
    actualTemperatureSensor_ = actualTemperatureSensor;
    desiredTemperatureSensor_ = desiredTemperatureSensor;
  }

  public boolean boolValue() {
    float actual = actualTemperatureSensor_.realValue();
    float desired = desiredTemperatureSensor_.realValue();

    // Offset desired temp if not occupied or expected
    boolean consideredOccupied = occupancyReporter_.boolValue();
    if (!consideredOccupied) desired -= offsetWhenUnoccupied;
    
    // check if desired is safe/sensible:
    
    if (desired < minimumDesiredRoomTemperature) 
      desired = minimumDesiredRoomTemperature;
    else if (desired > maximumDesiredRoomTemperature) 
      desired = maximumDesiredRoomTemperature;
    
    boolean currentlyHeating = valveSensor_.boolValue();
    if (!currentlyHeating && actual <= desired - roomHeatThreshold) 
      return true;
    else if (currentlyHeating && actual >= desired + roomHeatThreshold)
      return false;
    else
      return currentlyHeating;
  }
};

Sinks

As discussed above, the main sinks in this system are the RoomValve switches. Because each switch is a sink in a continuously executing system, it uses a (single) thread running as a continuous loop to constantly redetermine whether to be on or off. All methods are stateless and unsynchronized. (The update rate is set to be a fairly slow once per second just for demo purposes. Also, since this is a simulation, this class does not actually cause any physical action, its only effect is to turn on a simulated Valve Sensor):
class RoomValve implements Runnable {

  static final int updateInterval = 1000;

  protected BoolStage    heatNeed_;
  protected BoolStage    furnaceOn_;
  protected BoolEffector valveSensor_;
  protected Thread       me_;

  public RoomValve(BoolStage       heatNeed,
                   BoolStage      furnaceOn,
                   BoolEffector   valveSensor) {
    heatNeed_ = heatNeed;
    furnaceOn_ = furnaceOn;
    valveSensor_ = valveSensor;

    me_ = new Thread(this);
    me_.start();
  }

  protected void update() {
    boolean wantHeat = heatNeed_.boolValue();
    boolean canHeat = furnaceOn_.boolValue();
    valveSensor_.set(wantHeat && canHeat);
  }

  public void run() {
    if (Thread.currentThread() != me_) return; // only one thread!
    for (;;) {
      update();
      try { Thread.sleep(updateInterval); }
      catch (InterruptedException ex) { return; }
    }
  }
}

Stateful Stages

A TimeOutSensor is a generic Stage component that maintains enough internal state to tell a client (in all cases in this system, the Furnace) whether a certain time period has elapsed:
class TimeOutSensor implements BoolStage {
  protected long startTime_;
  protected long duration_;
  protected Timer time_;
  
  public TimeOutSensor(Timer time, long duration) {
    time_ = time;
    duration_ = duration;
    startTime_ = time_.timeValue();
  }
  public synchronized boolean boolValue() { 
    return time_.timeValue() - startTime_ >= duration_; 
  }
  public synchronized void reset() { startTime_ = time_.timeValue(); }
}
The OccupancyReporter is a Stage that is attached to a raw OccupancySensor, but ``transforms'' this value into one describing whether a room is either currently occupied or is expected to be occupied soon. To do this, it maintains occupancy history and smooths it into the required living pattern.

Because occupancy histories must be updated periodically whether or not the Reporter is asked about its current value, this update is done in its own Thread, running independently of it being asked. (It just so happens that under the parameters of the rest of the system, OccupancyReporter.boolValue() is called frequently enough for such purposes, but adding a thread here guarantees proper functionality without having to make any further assumptions about the rest of the system.)

class OccupancyReporter implements BoolStage, Runnable {

  // Main time granularity constant.
  // Occupancy is tracked in 5-minute (300 second) blocks;

  static final int occBlockSize = 300 * 1000; 

  // Occupancy is checked every 1 second
  static final int updateInterval = 1000; 

  // Derived constants:
  
  // One week has 7 days * 24 hrs * 60 min * 60 sec / occBlockSize blocks 
  static final  int blocksPerWeek  = 7 * 24 * 60 * 60 * 1000 / occBlockSize; 
  static final  int blocksPerTwoWeeks = 2 * blocksPerWeek;
  
  // A half hour has 30 min * 60 sec / occBlockSize blocks
  static final  int blocksPer30Minutes = 30 * 60 * 1000 / occBlockSize;

  // Utilities for indexing bit vectors recording occupancy
  static int convertTimeToHistoryIndex(long t) {
    return (int)((t / occBlockSize) % blocksPerTwoWeeks);
  }

  // index of livingPattern block corresponding to a history block
  static int livingPatternIndexOf(int historyIndex) {
    return historyIndex % blocksPerWeek;
  }

  // index of history block representing same time during other week
  static int otherWeekIndexOf(int historyIndex) {
    return (historyIndex + blocksPerWeek) % blocksPerTwoWeeks;
  }

  // offset a livingPattern index by a constant
  static int livingPatternOffset(int base, int offset) {
    return (base + offset + blocksPerWeek) % blocksPerWeek;
  }

  protected BoolStage occSensor_;
  protected Timer time_;
  protected BitSet     history_;
  protected BitSet     livingPattern_; 
  protected int        lastUpdatedPatternIndex_;
  protected Thread     me_;

  public OccupancyReporter(BoolStage occSensor, Timer time) {
    occSensor_ = occSensor;
    time_ = time;
    livingPattern_ = new BitSet(blocksPerWeek);
    history_ = new BitSet(blocksPerTwoWeeks);
    lastUpdatedPatternIndex_ = 0;
    me_ = new Thread(this);
    me_.start();
  }

  public void run() {
    if (Thread.currentThread() != me_) return;

    for (;;) {
      update();
      try { Thread.sleep(updateInterval); }
      catch (InterruptedException ex) { return; }
    }
  }

  // Report true if occupied or expected in 30 minutes
  public synchronized boolean boolValue() {

    // check current occupancy
    int historyIndex = convertTimeToHistoryIndex(time_.timeValue());
    if (history_.get(historyIndex)) 
      return true;

    // check livingPattern for expected occupancy within the next 30 minutes
     
    int livingPatternIndex = livingPatternIndexOf(historyIndex);
    for(int i = 0; i < blocksPer30Minutes; ++i)
      if (livingPattern_.get(livingPatternOffset(livingPatternIndex, i))) 
        return true;
    
    return false;
  }


  // Check occupancy; update history and livingPattern
  protected synchronized void update() {

    int historyIndex = convertTimeToHistoryIndex(time_.timeValue());
    
    // Clear the current block, but only if this is the first 
    // of possibly many successive update calls during the same block.
    // This way we end up OR'ing occupancy for the block.
    // (This might be a bad choice if occupancy sensors  false alarm.)


    if (historyIndex != lastUpdatedPatternIndex_) {
      history_.clear(historyIndex);
      lastUpdatedPatternIndex_ = historyIndex;

      //  Only bother to update livingPattern when cross blocks.

      int otherWeekIndex = otherWeekIndexOf(historyIndex);
      int livingPatternIndex = livingPatternIndexOf(historyIndex);
      
      boolean current = history_.get(historyIndex);
      boolean otherWeek = history_.get(otherWeekIndex);
      
      if (current == otherWeek) {
        if (current) livingPattern_.set(livingPatternIndex);
        else livingPattern_.clear(livingPatternIndex);
      }
      
      // Apply a simple smoother to get rid of strays.
      // The following changes the middle of the previous group of 5 blocks
      // if it is different from all its neighbors. (We cannot apply
      // this to current index since we don't know future yet.) 
      
      int t1 = livingPatternOffset(livingPatternIndex, -4);
      int t2 = livingPatternOffset(livingPatternIndex, -3);
      int t3 = livingPatternOffset(livingPatternIndex, -2);
      int t4 = livingPatternOffset(livingPatternIndex, -1);
      int t5 = livingPatternIndex;
      
      if (livingPattern_.get(t1) && livingPattern_.get(t2) && 
          livingPattern_.get(t4) && livingPattern_.get(t5))
        livingPattern_.set(t3);
      else if (!livingPattern_.get(t1) && !livingPattern_.get(t2) && 
               !livingPattern_.get(t4) && !livingPattern_.get(t5))
        livingPattern_.clear(t3);
      
    }
    
    if (occSensor_.boolValue()) history_.set(historyIndex);
    
  }
}

Furnace

While from the point of view of the main flow of this system, the Furnace's entire role is to tell the Room Valves whether it is on or off, to do so it must coordinate a large number of ``dumb'' devices. In fact, all it does is coordinate those devices; oddly enough, it has only one bit of local state of its own; tracking whether it is on (ready for heating). It is otherwise a functional stage that inspects the state of all of its connected inputs, and then as ``byproducts'' of deciding whether it is off or on, turns on other switches. Even so, the basic design is a state machine, which distills all of its inputs into a few logical states, and then acts accordingly. Because some of the states are time-dependent, the main state update loop is done in a Thread; in the same way and for the same reasons that this was done in the OccupancyReporter. (Note: Several time values have been shortened from the requirements to make this more interesting to watch.)

The code for this is very long, but not very interesting except as an illustration of how to build a mostly-humanly comprehensible state update function from such a messy set of inputs and transitions, and also including many more Fault states, covering cases that should never happen, than described in the original requirements.

class Furnace implements BoolStage, Runnable {

  // Bounds on water temperature in boiler
  static final float minWaterTemp = 150.0f;
  static final float maxWaterTemp = 200.0f;

  // Time-out values for Furnace
  static final long blowerTimeOutPeriod = 60000; // 1 minute
  static final long roomValveTimeOutPeriod = 60000; // 1 minute
  static final long blowerDelayPeriod = 5000; // 5 seconds
  static final long waterTimeOutPeriod =  30 * 1000; // 30 sec
  static final long furnaceRecyclePeriod = 10 * 1000; // 10 sec
  
  // Threshhold value for Furnace blower
  static final float  minBlowerRPM = 300.0f;

  // State update rate
  static final int updateInterval = 1000;

  // State to report in boolValue
  protected boolean on_;

  // Components
  protected BoolStage     blowerSensor_;
  protected BoolStage     oilValveSensor_;
  protected BoolStage     fuelFlowFaultSensor_;
  protected BoolStage     combustionFaultSensor_;
  protected BoolStage     heatRequest_;
  protected BoolStage     roomValveStatus_;
  protected BoolStage     mainSwitch_;
  protected RealStage     rpmSensor_;
  protected RealStage     waterTempSensor_;
  protected Timer         time_;
  protected BoolEffector  oilValveSwitch_;
  protected BoolEffector  blowerSwitch_;
  protected BoolEffector  ignatorSwitch_;
  protected BoolEffector  furnaceIndicator_;
  protected BoolVar       faultLight_;
  protected BoolVar       faultClearRequested_;
  protected TimeOutSensor recycleTimeOut_;
  protected TimeOutSensor roomValveTimeOut_;
  protected TimeOutSensor blowerInitTimeOut_;
  protected TimeOutSensor waterTimeOut_;
  protected TimeOutSensor blowerDelayTimeOut_;

  protected Thread me_;

  public Furnace(BoolEffector   blowerSwitch,
                 BoolStage      blowerSensor,
                 BoolEffector   oilValveSwitch,
                 BoolStage      oilValveSensor,
                 BoolEffector   ignatorSwitch,
                 BoolStage      fuelFlowFaultSensor,
                 BoolStage      combustionFaultSensor,
                 RealStage      rpmSensor,
                 RealStage      waterTempSensor,
                 Timer          time,
                 BoolStage      mainSwitch,
                 BoolVar        faultLight,
                 BoolEffector   furnaceIndicator,
                 BoolVar        faultClearRequested,
                 BoolStage      heatRequest,
                 BoolStage      roomValveStatus) {
    blowerSwitch_ = blowerSwitch;
    blowerSensor_ = blowerSensor;
    oilValveSwitch_ = oilValveSwitch;
    oilValveSensor_ = oilValveSensor;
    ignatorSwitch_ = ignatorSwitch;
    fuelFlowFaultSensor_ = fuelFlowFaultSensor;
    combustionFaultSensor_ = combustionFaultSensor;
    rpmSensor_ = rpmSensor;
    waterTempSensor_ = waterTempSensor;
    time_ = time;
    mainSwitch_ = mainSwitch;
    faultLight_ = faultLight;
    furnaceIndicator_ = furnaceIndicator;
    faultClearRequested_ = faultClearRequested;
    heatRequest_ = heatRequest;
    roomValveStatus_ = roomValveStatus;

    blowerSwitch_.set(false);
    oilValveSwitch_.set(false);
    ignatorSwitch_.set(false);
    furnaceIndicator_.set(false);
    on_ = false;

    recycleTimeOut_ = new TimeOutSensor(time_, furnaceRecyclePeriod);
    roomValveTimeOut_ = new TimeOutSensor(time_, roomValveTimeOutPeriod);
    blowerInitTimeOut_ = new TimeOutSensor(time_, blowerTimeOutPeriod);
    waterTimeOut_ = new TimeOutSensor(time_, waterTimeOutPeriod);
    blowerDelayTimeOut_ = new TimeOutSensor(time_, blowerDelayPeriod);

    me_ = new Thread(this);
    me_.start();
  }
 

  // better names for Sensed States

  boolean faulted()             { return faultLight_.boolValue(); }
  boolean systemOn()            { return mainSwitch_.boolValue(); }
  boolean clearRequested()      { return faultClearRequested_.boolValue(); }
  boolean wantHeat()            { return heatRequest_.boolValue(); }
  boolean blowerOn()            { return blowerSensor_.boolValue(); }
  boolean oilOn()               { return oilValveSensor_.boolValue(); }
  boolean roomValvesOpen()      { return roomValveStatus_.boolValue(); }
  boolean recycleTimedOut()     { return recycleTimeOut_.boolValue(); }
  boolean roomValvesTimedOut()  { return roomValveTimeOut_.boolValue(); }
  boolean blowerInitTimedOut()  { return blowerInitTimeOut_.boolValue(); }
  boolean waterTimedOut()       { return waterTimeOut_.boolValue(); }
  boolean blowerDelayTimedOut() { return blowerDelayTimeOut_.boolValue(); }
  boolean combustionFault()     { return combustionFaultSensor_.boolValue(); }
  boolean fuelFlowFault()       { return fuelFlowFaultSensor_.boolValue(); }
  boolean rpmOK()            { return rpmSensor_.realValue() >= minBlowerRPM; }
  boolean waterHotEnough() { return waterTempSensor_.realValue()>=minWaterTemp;}
  boolean waterTooHot() { return waterTempSensor_.realValue() > maxWaterTemp; }
  
  //  Action States -- aggregations of above

  
  boolean readyForBlowerOn() {
    return recycleTimedOut() && !blowerOn() && !oilOn();
  }
  
  boolean readyForOilOn() {
    return blowerOn() && rpmOK() && !oilOn();
  }
  
  boolean readyToHeat() {
    return blowerOn() && rpmOK() && oilOn() && waterHotEnough();
  }
  
  boolean readyForBlowerOff() {
    return !on_ && (!roomValvesOpen() || roomValvesTimedOut()) && 
      !oilOn() && blowerOn() && blowerDelayTimedOut();
  }
  
  boolean readyForOilOff() {
    return !on_ &&  (!roomValvesOpen() || roomValvesTimedOut()) && oilOn();
  }

  boolean blowerTimeOutFault() {
    return blowerOn() && !rpmOK() && blowerInitTimedOut();
  }

  boolean blowerFailureFault() { 
    return oilOn() && !rpmOK();  
  }
  
  boolean roomValveTimeOutFault() {
    return !on_ && oilOn() && 
      roomValvesOpen() && roomValvesTimedOut();
  }
  
  boolean waterTimeOutFault() {
    return oilOn() &&  !waterHotEnough() && waterTimedOut();
  }
  
  boolean abnormality() {
    return combustionFault() || 
      fuelFlowFault() ||
        waterTooHot() ||
          blowerTimeOutFault() ||
            waterTimeOutFault() ||
              roomValveTimeOutFault() ||
                blowerFailureFault() ;
  }
  
  
  // Actions corresponding to above states
  
  void setFurnaceOn()  { on_ = true; furnaceIndicator_.set(true);  }
  
  void setFurnaceOff() {
    if (on_) {
      on_ = false;
      furnaceIndicator_.set(false);
      recycleTimeOut_.reset();
      roomValveTimeOut_.reset();
    }
  }
  
  void setOilOn() {
    if (!oilOn()) {
      oilValveSwitch_.set(true);
      ignatorSwitch_.set(true);
      waterTimeOut_.reset();
    }
  }
  
  void setOilOff() {
    if (oilOn()) {
      oilValveSwitch_.set(false);
      blowerDelayTimeOut_.reset();
    }
  }
  
  void setBlowerOn() {
    if (!blowerOn()) {
      blowerSwitch_.set(true);
      blowerInitTimeOut_.reset();
    }
  }
  
  void setBlowerOff() { blowerSwitch_.set(false); }
  void setFault()     { faultLight_.set(true); }
  void clearFault()   { 
    faultLight_.set(false);  
    faultClearRequested_.set(false);
  }
  

  // Check state; perform an action
  synchronized void update() {
    
    if (faulted() && (!systemOn() || clearRequested())) clearFault();
    
    if (abnormality()) setFault();
    
    if (!faulted() && systemOn() && wantHeat()) {
      if (readyToHeat())            setFurnaceOn();
      else if (readyForOilOn())     setOilOn();
      else if (readyForBlowerOn())  setBlowerOn();
    }
    else {
      if (on_)                      setFurnaceOff(); 
      else if (readyForOilOff())    setOilOff();
      else if (readyForBlowerOff()) setBlowerOff();
    }
  }

  public void run() {

    if (Thread.currentThread() != me_) return; // only one thread!

    for (;;) {
      update();
      try { Thread.sleep(updateInterval); }
      catch (InterruptedException ex) { return; }
    }
  }

  public synchronized boolean boolValue() { return on_; }
};

HHS

As is normally the case in such designs, the main system (in this case an Applet) simply instantiates the components and links them together:
public class HHS extends Applet {

  static final int numberOfRooms = 4;

  public void init() {

    SystemTimer timer = 
      new SystemTimer();
    BoolButton mainSwitch = 
      new BoolButton("Main");
    BoolButton faultResetButton = 
      new BoolButton("Reset");
    LabelledBoolVar faultLight =  
      new LabelledBoolVar("Fault");
    LabelledBoolVar furnaceLight = 
      new LabelledBoolVar("Furnace");
    LabelledBoolVar blower = 
      new LabelledBoolVar("Blower");
    LabelledBoolVar oilValve = 
      new LabelledBoolVar("Oil"); 
    PulsedLabelledBoolVar ignator =  
      new PulsedLabelledBoolVar("Ignator", 500);
    Orer requestGate = 
      new Orer();
    Orer valveGate = 
      new Orer();
    RandomBoolStage fuelFlowFaultSensor = 
      new RandomBoolStage(0.00001f);
    RandomBoolStage combustionFaultSensor = 
      new RandomBoolStage(0.00001f);
    SimPhysSensor rpmSensor = 
      new SimPhysSensor(blower,
                        timer,
                        1.0f,
                        10.0f,
                        0.0f,
                        400.0f,
                        0.0f);
    SimPhysSensor boilerSensor = 
      new SimPhysSensor(oilValve,
                        timer,
                        0.02f,
                        1.0f,
                        30.0f,
                        200.0f,
                        60.0f);
    Furnace furnace = 
      new Furnace(blower,
                  blower,
                  oilValve,
                  oilValve,
                  ignator,
                  fuelFlowFaultSensor,
                  combustionFaultSensor,
                  rpmSensor,
                  boilerSensor,
                  timer,
                  mainSwitch,
                  faultLight,
                  furnaceLight,
                  faultResetButton,
                  requestGate,
                  valveGate);

    Panel ctlPanel = new Panel();
    ctlPanel.add(mainSwitch);
    ctlPanel.add(faultResetButton);
    ctlPanel.add(faultLight);
    ctlPanel.add(furnaceLight);
    ctlPanel.add(blower);
    ctlPanel.add(oilValve);
    ctlPanel.add(ignator);
    add(ctlPanel);
    
    for (int i = 0; i < numberOfRooms; ++i) {

      LabelledBoolVar valveSensor = 
        new LabelledBoolVar("valve"); 
      BoolButton occSensor = 
        new BoolButton("Occupy");
      OccupancyReporter occReporter = 
        new OccupancyReporter(occSensor, timer);
      Real dt = 
        new Real(72.0f - (float)i);
      RealAdjustorButton dtUp = 
        new RealAdjustorButton(dt, true);
      RealAdjustorButton dtDn = 
        new RealAdjustorButton(dt, false);
      LabelledRealStage desiredTemp  = 
        new LabelledRealStage("Desired:",dt);
      SimPhysSensor at = 
        new SimPhysSensor(valveSensor,
                          timer,
                          0.002f,
                          0.0004f,
                          0.0f,
                          100.0f,
                          (float)(60.0f + 3.0f * (float)i));
      LabelledRealStage actualTemp = 
        new LabelledRealStage("Actual:", at);
      RoomHeatNeedCalculator heatNeedCalculator =  
        new RoomHeatNeedCalculator(valveSensor, 
                                   occReporter, 
                                   actualTemp, 
                                   desiredTemp);
      RoomValve roomValve = 
        new RoomValve(heatNeedCalculator,
                      furnaceLight,
                      valveSensor);

      requestGate.attach(heatNeedCalculator);
      valveGate.attach(valveSensor);

      Panel room = new Panel();
      room.add(occSensor);
      room.add(dtUp);
      room.add(dtDn);
      room.add(desiredTemp);
      room.add(actualTemp);
      room.add(valveSensor);
      add(room);
    };

    mainSwitch.set(true);
  }

  public boolean action(Event evt, Object arg) {
    if (evt.target instanceof BoolButton) {
      BoolButton b = (BoolButton)(evt.target);
      b.toggle();
      return true;
    }
    else if (evt.target instanceof RealAdjustorButton) {
      RealAdjustorButton b = (RealAdjustorButton)(evt.target);
      b.adjust();
      return true;
    }
    return false;
  }
}


Doug Lea
Last modified: Mon Apr 7 10:38:58 EDT 1997