PhSmartsImpl.mesa
Copyright Ó 1990, 1992 by Xerox Corporation. All rights reserved.
Vin, December 20, 1990 0:21 am PST
Swinehart, October 23, 1992 8:44 am PDT
Notes: could fold in PhPrivateImpl, since this is so much smaller now.
state variables such as doingAudio should be in cDesc; check action vs. progress problem.
DIRECTORY
Atom,
Commander,
CommanderOps,
IO,
LoganBerryEntry,
MacawProc,
PhSmarts,
PhSwitch,
Process,
RefID,
Rope,
ThParty,
Thrush,
ThSmarts,
VoiceUtils
;
PhSmartsImpl: CEDAR MONITOR LOCKS PhSmarts.lock
IMPORTS Atom, Commander, CommanderOps, IO, LoganBerryEntry, --MacawProc,-- PhSmarts, PhSwitch, Process, Rope, ThParty, VoiceUtils
EXPORTS ThSmarts, PhSmarts = {
OPEN pd: PhSmarts.pd, phoenixInfo: PhSmarts.phoenixInfo;
Definitions
ROPE: TYPE = Thrush.ROPE;
ConvDesc: TYPE = PhSmarts.ConvDesc;
ConvEvent: TYPE = Thrush.ConvEvent;
ConversationID: TYPE = Thrush.ConversationID;
nullConvID: ConversationID=Thrush.nullConvID;
NB : TYPE = Thrush.NB;
OpenConversations: TYPE = PhSmarts.OpenConversations;
PhoenixInfo: TYPE = PhSmarts.PhoenixInfo;
SmartsID: TYPE = Thrush.SmartsID;
StateInConv: TYPE = Thrush.StateInConv;
PartyInfo: TYPE ~ ThParty.PartyInfo;
PartyID: TYPE = Thrush.PartyID;
nullID: RefID.ID = Thrush.nullID;
PD: TYPE = PhSmarts.PD;
serverInstance: Rope.ROPE ¬ NIL;
WhatNeedsDoing: TYPE = ATOM;
whatNeedsDoingIf: ARRAY Thrush.StateInConv OF ARRAY Thrush.StateInConv OF WhatNeedsDoing ¬ [
If we're in the state identified by the row, and someone else in the conversation reports a transition onto the state identified by the column, what should we do?
never idle failed resrv pars init notif rback ring canAc activ inact
[ $imp, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt], --neverWas
(Clip this table to view without these comments.)
This situation arises when we've forgotten about the conversation that somebody else is still reporting on.
[ $imp, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $ntiy ], -- idle
[ $imp, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $ntiy ], -- failed
[ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- reserved
The actions of other parties are not of interest to us yet, since they're not in this conv.
[ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- parsing
Ditto.
[ $imp, $idle, $noop, $invl, $invl, $invl, $xrep, $xrep, $rback,$actvd, $actvd,$actvd ], -- initiating
They can either enter ringing to indicate interest, or go active without ringing
[ $imp, $idlerg,$noop,$invl, $invl, $invl, $xrep, $noop, $noop, $ntiy, $noop, $ntiy ], -- notified
We don't expect to hear from others while we're deciding whether to play
[ $imp, $idle, $noop, $invl, $invl, $invl, $noop, $xrep, $noop, $ntiy, $actv, $actvd ], -- ringback
They have earlier expressed interest noopringing), and are now joining the fray
[ $imp, $idlerg,$noop,$invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $ckrg, $ntiy ], -- ringing
The only thing that interests us here is everybody else quitting.
[ $imp, $idle, $noop, $invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $ckrg, $ntiy ], -- canActivate
[ $imp, $idle, $noop, $invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $reac, $deac ], -- active
[ $imp, $idle, $noop, $invl, $invl, $noop, $noop, $noop, $noop, $ntiy, $reac, $deac ], -- active
[ $imp, $idle, $invl, $invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $ntiy, $ntiy ] -- inactive (current ­)
];
Unique ID for actions
actionID: INT ¬ 0;
Substitution: PUBLIC ENTRY PROC [shh: Thrush.SHHH, convEvent: Thrush.ConvEvent, oldPartyID: Thrush.PartyID, newPartyID: Thrush.PartyID] = {};
CheckIn: PUBLIC ENTRY PROC[
shh: Thrush.SHHH, credentials: Thrush.Credentials,
voicePath: BOOL, reason: Thrush.Reason, remark: ROPE, nextScheduledCheck: INT ] = {
ENABLE UNWIND => NULL;
localRemark: ROPE¬NIL;
connected: BOOL ¬ TRUE;
enabled: BOOL ¬ phoenixInfo.enabled;
phoenixInfo.nextScheduledCheck ¬ nextScheduledCheck;
SELECT reason FROM
$goodbye => {
enabled ¬ connected ¬ FALSE;
localRemark ¬ "Permanent disconnect requested by server";
};
$trylater => {
localRemark ¬ "Temporary disconnect requested by server";
connected ¬ FALSE;
};
$welcome, $hello => connected ¬ TRUE;
ENDCASE;
[]¬PhSmarts.RecordSystemStateFromSmartsReport[
remark: localRemark, connected: connected, enabled: enabled, voicePath: voicePath, remoteRemark: remark];
NOTIFY phoenixInfo.pollCondition;
};
Progress: PUBLIC ENTRY PROC [shh: Thrush.SHHH, convEvent: Thrush.ConvEvent] = {
Some party has changed state in a conversation we know about.
Three cases:
Another smarts initiated a state change for this party, and we're not receptive.
(For now, when we don't know about that conversation and are already in another)
Another smarts initiated a change for this party, and we're receptive.
(New conversation and we're idle or it's a conversation we're in already)
Another party changed state in a conversation we know about.
ENABLE UNWIND => NULL;
partyInfo: ThParty.PartyInfo;
whatNeedsDoing: WhatNeedsDoing;
nb: Thrush.NB;
reason: Thrush.Reason;
refAny: REF ANY;
cInfo: ThParty.ConversationInfoRec;
numParties: INT;
numActive: INT;
convID: ConversationID = convEvent.self.convID;
cDesc: ConvDesc;
info: PhoenixInfo ¬ PhSmarts.phoenixInfo;
VoiceUtils.ReportFR["Progress Called: myState=%g, otherState=%g, %g", $Smarts, info, [rope[stateRope[convEvent.self.state]]], [rope[stateRope[convEvent.other.state]]], [rope[IF convEvent.self.partyID=convEvent.other.partyID THEN "(me)" ELSE "(somebody else)"]]];
IF info=NIL THEN { Problem["No Smarts for SmartsID %g", NIL, IO.card[convEvent.self.smartsID]]; RETURN; };
[nb, cDesc] ¬ GetInfo[convEvent.self];
IF nb#$success THEN { IF cDesc#NIL THEN NoteNewState[cDesc, convEvent]; RETURN; };
Not at all sure what the implications of doing this are. Hope GetInfo doesn't fail.
cInfo ¬ cDesc.cInfo;
partyInfo ¬ cDesc.partyInfo;
numParties ¬ cInfo.numParties;
numActive ¬ cInfo.numActive;
cDesc.convMedia ¬ FetchAtom[cInfo.convAttributes, $Media];
cDesc.doingAudio ¬ cDesc.convMedia#$Video;
cDesc.doingVideo ¬ cDesc.convMedia#$Audio;
cDesc.expectedMedia ¬ FetchAtom[cInfo.convAttributes, $ExpectedMedia];
cDesc.actualMedia ¬ FetchAtom[cInfo.convAttributes, $ActualMedia];
cDesc.isConference ¬ numParties > 2;
Added temporarily *****
Here we should also check whether there is already a video conversation in progress. If it is, then we have to decide the actual media of the call or reject the call.
IF cDesc.doingVideo THEN
IF ~PhSmarts.VideoHardwareInUse[cDesc] THEN {
MacawProc.Initialize[];
MacawProc.InitializeMacaw[partyInfo: partyInfo, isConference: cDesc.isConference];
}
ELSE -- Reject the call --;
IF convEvent.self.partyID = convEvent.other.partyID THEN { -- own state changed
NoteNewState[cDesc, convEvent]; -- Existing conv. or notif. of new one.
RETURN;
};
Someone else's state changed in a conv. we're interested in; see if it means anything to us!
whatNeedsDoing ¬ whatNeedsDoingIf[convEvent.self.state][convEvent.other.state];
SELECT whatNeedsDoing FROM
$noop, $ntiy => NULL;
$imp => ERROR;
$xrep => Problem["Didn't expect state change report", info];
We don't expect reports of state changes like $notified and $ringback to be reported to us unless it's us.
$frgt => PhSmarts.ForgetConv[cDesc];
$invl => {
Problem["Invalid State Transition", info];
ChangeState[cDesc: cDesc, desiredState: $failed, reason: $error, comment: "System Error: Invalid State Transition" ];
};
$idle => {
Somebody just quit. If everybody else has quit, or if the moderator of the meeting conversation has quit, then quit too. Otherwise, if the conversation is not of meeting type, then remove that participant from the list of senders maintained in VoiceProtocol.
reason ¬ IF convEvent.reason # NIL THEN convEvent.reason ELSE $terminating;
IF (cInfo.numParties - cInfo.numIdle) <= 1 OR (cInfo.convType = $meeting AND convEvent.other.partyID = cInfo.moderator) THEN
This means that we are the last party to leave the conversation. Quit, too.
ChangeState[cDesc: cDesc, desiredState: IF reason=$terminating THEN $idle ELSE $failed, reason: reason, reportToAll: reason=$terminating];
};
$idlerg => {
We are ringing, and someone just quit. If it was the originator, quit too.
Otherwise, ignore the event.
reason ¬ IF convEvent.reason#NIL THEN convEvent.reason ELSE $terminating;
IF cInfo.originator = convEvent.other.partyID THEN
ChangeState[cDesc: cDesc, desiredState: IF reason=$terminating THEN $idle ELSE $failed, reason: reason, comment: "Originator left", reportToAll: reason=$terminating];
};
$rback => ChangeState[cDesc: cDesc, desiredState: $ringback];
$actv, $actvd => ChangeState[cDesc: cDesc, desiredState: $active, reportToAll:TRUE];
$ckrg => NULL;
We are ringing and someone just went into an active state. Check whether we should continue ringing or stop. This should be implemented - but later.
$reac =>
Someone has become active, we're already active. If not a meeting, add new party.
IF convEvent.other.partyID # partyInfo.parties[partyInfo.ixOriginator].partyID THEN {
NoteNewState[cDesc, convEvent]; -- make sure we're up to date in compliance.
};
$deac =>
Someone has gone into an inactive (held) state, we're active. Not clear what we should do.
NoteNewState[cDesc, convEvent]; -- assure compliance.
IF cDesc.doingVideo THEN MacawProc.DeActiveProc[];
ENDCASE => ERROR;
};
CheckInitiation: ENTRY PROC [cDesc: ConvDesc] ~ TRUSTED {
ENABLE UNWIND => NULL;
nb: NB;
c: CONDITION;
Process.SetTimeout[@c, Process.SecondsToTicks[pd.timeoutInitiating]];
WAIT c;
IF cDesc.situation.self.state # $initiating THEN RETURN;
If no response has been received in the timeout period, then terminate the request - since something must be wrong.
ChangeState[cDesc: cDesc, desiredState: $idle, comment: "Initial Connection Timeout: Party can't be reached", reportToAll: TRUE];
};
ReportAction: PUBLIC ENTRY PROC [shh: Thrush.SHHH, report: Thrush.ActionReport] = {
ENABLE UNWIND => NULL;
nb: NB;
cDesc: ConvDesc;
myName: Rope.ROPE;
IF PhSmarts.phoenixInfo=NIL THEN { Problem["No Smarts for SmartsID %g", NIL, IO.card[report.self.smartsID]]; RETURN; };
[nb, cDesc] ¬ GetInfo[credentials: report.self];
IF nb#$success OR cDesc=NIL THEN RETURN;
SELECT report.actionClass FROM
$conferenceParticipation => {
IF nb=$success THEN SELECT report.actionType FROM
$conferenceEstablished =>
If the report is received from a participant other than the originator, ignore it.
IF report.requestingParty = cDesc.cInfo.originator AND
report.self.state # $active THEN
If we are not active, go idle. Otherwise, ignore report.
ChangeState[cDesc: cDesc, desiredState: $idle, comment: "Conference Established, and we're not in it", reportToAll:TRUE];
$requestToLeave => {
myName ¬ cDesc.partyInfo[cDesc.partyInfo.ixSelf].intendedName;
IF report.requestingParty = cDesc.cInfo.originator AND Rope.Equal[myName, report.actionInfo] THEN
We are being asked to leave the conversation. Take appropriate actions here.
ChangeState[cDesc: cDesc, desiredState: $idle, reportToAll:TRUE];
};
ENDCASE;
};
$PriorityChange =>
The participant has requested a change in priority. The changed priority is available in ActionReport.actionInfo. Set this priority value in the conversation information being maintained by Phoenix.
cDesc.myPriority ¬ report.actionInfo;
$ParticipationChange => NULL; -- Perhaps must manually ensure realworld compliance
ENDCASE;
};
Call Management Utilities
ChangeState: INTERNAL PROC [cDesc: ConvDesc, desiredState: StateInConv,
reason: Thrush.Reason¬NIL, comment: ROPE¬NIL, reportToAll: BOOL ¬ FALSE] ~ {
nb: NB;
convEvent: Thrush.ConvEvent;
[nb, convEvent] ¬ ThParty.Advance[shhh: phoenixInfo.shh, credentials: cDesc.situation.self, state: desiredState, reason: reason, comment: comment, reportToAll: reportToAll ];
VoiceUtils.ReportFR["ChangeState: desired=%g, actual=%g, nb=%g", $Smarts, PhSmarts.phoenixInfo, [rope[stateRope[desiredState]]], [rope[stateRope[convEvent.self.state]]], [atom[nb]]];
WHILE TRUE DO -- loop permits stateMismatch to repeat.
SELECT nb FROM
$success => { NoteNewState[cDesc, convEvent]; EXIT; };
$stateMismatch => {
The basic idea here is to prevent peculiarities of RPC timeout from destroying the PhSmarts. If there is a state mismatch, it is possible that the party received the state transition request twice. There are two possible scenarios:
-- Two progress reports were received by the smarts.
-- The reply to ThParty.Advance was lost and the smarts timed out and retransmitted the request.
currentState: StateInConv ¬ convEvent.self.state;
Report["State mismatch reported.", PhSmarts.phoenixInfo];
NoteNewState[cDesc, convEvent];
IF currentState = desiredState THEN RETURN;
[nb, convEvent] ¬ ThParty.Advance[shhh: phoenixInfo.shh, credentials: convEvent.self, state: desiredState --, checkConflict: TRUE --];
};
ENDCASE => {
Something is seriously wrong here. Inform the user of the anamoly and quit. Change the state to Idle and forget the conversation. The forced transition to idle will clear all local non-idle behaviors.
Problem["Unexpected party state-advance failure, nb=%g", cDesc.info, IO.atom[nb]];
convEvent.self.state ¬ $idle;
NoteNewState[cDesc, convEvent];
RETURN;
};
ENDLOOP;
};
NoteNewState: INTERNAL PROC[cDesc:ConvDesc, convEvent:Thrush.ConvEvent] ~ {
nb: NB;
nextState: StateInConv;
state: StateInConv ¬ convEvent.self.state;
previousState: StateInConv ¬ cDesc.situation.self.state;
cDesc (conversation descriptor) has not been updated yet. Hence, comparison of
previous state and current state (obtained from the convEvent) are possible here.
cDesc.situation ¬ convEvent­;
IF state = previousState THEN RETURN;
SELECT state FROM
$notified => {
videoDeviceInUse: BOOL ¬ FALSE;
videoDeviceInUse ¬ PhSmarts.VideoHardwareInUse[cDesc];
nextState ¬ $ringing;
IF cDesc.convMedia = $AudioVisual AND videoDeviceInUse AND
cDesc.expectedMedia = $BestEffort THEN {
We can currently only establish a conversation in the audio domain. Hence, set the actual media being used to "Audio" and go to ringing state.
[] ¬ ThParty.AddAttributes[credentials: convEvent.self, convAttributes: LIST[[$ActualMedia, "Audio"]]]; -- Where else is ActualMedia set? Tested?
cDesc.convMedia ¬ $Audio; cDesc.doingVideo ¬ FALSE;
};
IF cDesc.doingVideo AND videoDeviceInUse THEN {
ChangeState[cDesc: cDesc, desiredState: $idle, comment: "VideoDevice Busy", reportToAll: TRUE];
RETURN;
};
IF cDesc.doingAudio THEN
nextState ← IF PhSmarts.DBInfo[cDesc.situation.self.partyID, $autoanswer, rNAtom].value.Equal["true", FALSE] THEN $active ELSE $ringing;
nextState ¬ $ringing;
ChangeState[cDesc: cDesc, desiredState: nextState, reportToAll: TRUE];
RETURN;
};
$initiating => {
Fork a process that will set a timeout, sleep for that duration, then if no response has been received from the callee, go idle and report.
Process.Detach[FORK CheckInitiation[cDesc]];
IF PhSmarts.VideoHardwareInUse[cDesc] AND
~MacawProc.InitiatingProc[cDesc.isConference] AND cDesc.doingVideo THEN {
ChangeState[cDesc: cDesc, desiredState: $idle, reason: reason, comment: "Cannot create the video connection", reportToAll:TRUE];
}
IF cDesc.doingVideo THEN [] ¬ MacawProc.InitiatingProc[cDesc.isConference];
};
$ringing, $reserved, $ringback, $failed, $active, $inactive => NULL;
$idle, $neverWas => PhSmarts.ForgetConv[cDesc];
ENDCASE;
IF cDesc.doingAudio THEN []¬PhSwitch.Comply[cDesc]; -- Make physical world agree with logical.
};
GetInfo: PROC[credentials: Thrush.Credentials]
RETURNS [nb: NB¬$NoSuchConv, cDesc: PhSmarts.ConvDesc¬NIL] ~ {
Set up. Find local knowledge of conversation, then obtain additional information from the server. The need for the ConvInfo suggests that Thrush should include it in all progress reports (perhaps with an optimization for the attributes). Similarly, some way to further limit, perhaps from the source, the number of times that party info must be shared is needed. We use the algorithm from FinchSmarts to determine when to get new party info. It may not be adequate. DCS August 11, 1992 10:24:39 am PDT
cInfo: ThParty.ConversationInfo;
cDesc ¬ PhSmarts.GetConv[PhSmarts.phoenixInfo, credentials, credentials.state >= $failed];
This call to GetConv will create a conversation descriptor record, if it already does not exist, and then add it to the list of OpenConversations. It returns the conversation descriptor for this conversation (for which an event is received).
IF cDesc = NIL THEN RETURN;
[nb, cInfo] ¬
ThParty.GetConversationInfo[shh: phoenixInfo.shh, convID: credentials.convID];
IF nb = $success THEN {
partyInfo: ThParty.PartyInfo;
cDesc.myPriority ¬ LoganBerryEntry.GetAttr[cInfo.convAttributes, $Priority];
IF cDesc.partyInfo=NIL OR cDesc.situation.self.state#credentials.state OR
cDesc.cInfo.numParties#cInfo.numParties OR
cDesc.cInfo.numActive#cInfo.numActive OR cDesc.cInfo.numIdle#cInfo.numIdle THEN {
[nb, partyInfo] ¬ ThParty.GetPartyInfo[shh: phoenixInfo.shh, credentials: credentials, allParties: TRUE]; -- expensive on every transition!
cDesc.partyInfo ¬ MergeAttributes[cDesc.partyInfo, -- into -- partyInfo];
};
cDesc.cInfo ¬ cInfo­;
}
ELSE {
msg: Rope.ROPE = IO.PutFR1["Unexpected Server Information failure, nb=%g", IO.atom[nb]];
Problem[msg, cDesc.info];
};
};
MergeAttributes: PROC[oldInfo: PartyInfo, newInfo: PartyInfo]
RETURNS [mergedInfo: PartyInfo] ~ {
Side effects on newInfo, which is returned. This is n­gazzilion, but assert n is small.
mergedInfo ¬ newInfo;
IF oldInfo=NIL THEN RETURN;
IF newInfo=NIL THEN ERROR;
FOR ixNew: NAT IN [1..newInfo.numParties] DO
FOR ixOld:NAT IN [1..oldInfo.numParties] DO IF newInfo[ixNew].partyID=oldInfo[ixOld].partyID THEN {
newAttrs: ThParty.Attributes ¬ newInfo[ixNew].partyAttributes;
FOR attributes: ThParty.Attributes ¬ oldInfo[ixOld].partyAttributes,
attributes.rest WHILE attributes#NIL DO
IF LoganBerryEntry.GetAttr[newAttrs, attributes.first.type]=NIL THEN
newInfo[ixNew].partyAttributes ¬ newAttrs ¬ CONS[attributes.first, newAttrs];
ENDLOOP;
};
ENDLOOP;
ENDLOOP;
};
Added for sending action reports
SendActionReport: INTERNAL PROC [myCredentials: Thrush.Credentials, actionClass: Thrush.ActionClass, actionType: Thrush.ActionType, actionInfo: Rope.ROPE ¬ NIL, reportToAll: BOOL ¬ TRUE] ~ {
actionReport: Thrush.ActionReport ¬ [other: myCredentials, requestingParty: myCredentials.partyID, actionID: actionID, actionClass: actionClass, actionType: actionType, actionInfo: actionInfo];
actionID ¬ actionID + 1;
[] ¬ ThParty.ReportAction[shhh: phoenixInfo.shh, report: actionReport, reportToAll: reportToAll];
};
General Utility Procedures
Problem: PUBLIC PROC[comment: ROPE, info: PhoenixInfo, v1: IO.Value ¬ [null[]]] = {
IF v1=[null[]] THEN
VoiceUtils.Problem[Rope.Concat["PhoenixSmarts: ", comment], $Smarts, info]
ELSE
VoiceUtils.ProblemFR[Rope.Concat["PhoenixSmarts(%g): ", comment], $Smarts, info, v1];
};
Report: PUBLIC PROC[comment: ROPE, info: PhoenixInfo¬PhSmarts.phoenixInfo, v1: IO.Value ¬ [null[]], where: ATOM¬$Smarts] = {
IF v1=[null[]] THEN
VoiceUtils.Report[Rope.Concat["PhoenixSmarts: ", comment], $Smarts, info]
ELSE
VoiceUtils.ReportFR[Rope.Concat["PhoenixSmarts(%g): ", comment], $Smarts, info, v1];
};
FetchAtom: PROC[attributes: ThParty.Attributes, attribute: ATOM, default: ROPE¬NIL]
RETURNS[atom: ATOM ¬ NIL] ~ {
value: ROPE ¬ LoganBerryEntry.GetAttr[attributes, attribute];
IF value = NIL THEN value ¬ default;
IF value # NIL THEN atom ¬ Atom.MakeAtom[value];
};
stateRope: ARRAY Thrush.StateInConv OF ROPE ¬ [
"$neverWas", "$idle", "$failed", "$reserved", "$parsing", "$initiating", "$pending", "$ringback", "$ringing", "$canActivate", "$active", "$inactive" ];
Initialization and Registration Procedures
PhoenixCmd: Commander.CommandProc ~ {
serverInstance ¬ CommanderOps.NextArgument[cmd];
StartPhoenix[];
};
UnPhoenixCmd: Commander.CommandProc = { StopPhoenix[]; };
StartPhoenix: PROC ~ {
enabled, connected: BOOL;
[enabled, connected] ¬ PhSmarts.PhoenixIsRunning[];
SELECT TRUE FROM
connected => Report["Already running . . ."];
enabled => { Report["Connecting . . ."]; PhSmarts.Poke[]; };
ENDCASE => {
Report[ comment: "Connecting . . ."];
Report[ comment: "Phoenix Initialization in progress ... ", where: $DefaultWindow];
PhSmarts.InitPhoenixSmarts[serverInstance];
Report[ comment: "done", where: $DefaultWindow];
Report[ comment: "done"];
};
};
StopPhoenix: PROC[disable: BOOL¬TRUE] = {
Report[ comment: "Disconnecting . . ."];
Report[ comment: "Phoenix Termination in progress ... ", where: $DefaultWindow];
PhSmarts.UnInitPhoenixSmarts[disable];
Report[ comment: "Done"];
Report[ comment: "done", where: $DefaultWindow];
};
Commander.Register["Phoenix", PhoenixCmd];
Commander.Register["UnPhoenix", UnPhoenixCmd];
}.
October 1, 1992 11:25:32 am PDT, DCS, major simplifications based on experience, simplified underpinnings.