DIRECTORY
Basics USING [ Comparison ],
BasicTime USING [ Now, nullGMT, OutOfRange, Update ],
Commander USING [ CommandProc, Register ],
Convert USING [ IntFromRope ],
FinchSmarts,
IO,
Lark USING [ KeyTable, SHHH ],
Log USING [Problem, Report ],
LupineRuntime USING [ BindingError ],
Names USING [ CurrentPasskey, CurrentRName, InstanceFromNetAddress, OwnNetAddress ],
NamesGV USING [ AttributeSeq, GVGetAttribute, GVGetAttributeSeq ],
NamesGVImpExp USING [ GVImport, UnGVImport ],
NamesRPC USING [ StartConversation ],
Nice,
PrincOpsUtils USING [ IsBound ],
Process USING [ Detach, SecondsToTicks, SetTimeout, Ticks ],
RefTab USING [Create, Fetch, Ref, Store ],
Rope,
RPC USING [ AuthenticateFailed, CallFailed, EncryptionKey, ImportFailed, VersionRange ],
RuntimeError USING [ UNCAUGHT ],
ThParty USING [ Advance, Alert, ConversationsForParty, CreateParty, Deregister, DescribeInterval, DescribeParty, GetNumbersForRName, GetParty, GetPartyFromNumber, OtherParty, Register, RegisterKey, SameConvClass, SetIntervals, SetProse ],
ThPartyRpcControl,
Thrush
USING[
CallUrgency, ConversationHandle, ConvEvent, Disposition, EncryptionKey, IntervalSpec, IntervalSpecs, IntervalSpecBody, IntID, IntSpecType, NB, none, nullConvHandle, nullHandle, nullKey, nullTune, PartyHandle, ProseSpec, ProseSpecs, ProseSpecBody, Reason, ROPE, SmartsHandle, StateID, StateInConv, ThHandle, Tune, unencrypted, VoiceDirection, VoiceInterval ],
ThSmarts,
ThSmartsRpcControl,
ThVersions USING [ GetThrushVR, FinchVersion, FinchVR ],
UserProfile USING [ Token]
;
FinchSmartsImpl:
CEDAR
MONITOR
IMPORTS BasicTime, Commander, Convert, IO, FinchSmarts, Log, LupineRuntime, Names, NamesGV, NamesGVImpExp, NamesRPC, Nice, PrincOpsUtils, Process, RefTab, RPC, Rope, RuntimeError, ThParty, ThPartyRpcControl, ThSmartsRpcControl, ThVersions, UserProfile
EXPORTS FinchSmarts, ThSmarts = {
OPEN IO;
Declarations
CallUrgency: TYPE = Thrush.CallUrgency;
ConvDesc: TYPE = FinchSmarts.ConvDesc;
ConversationHandle:
TYPE = Thrush.ConversationHandle;
nullConvHandle: ConversationHandle = Thrush.nullConvHandle;
Disposition: TYPE = Thrush.Disposition;
FinchInfo: TYPE = FinchSmarts.FinchInfo;
IntervalSpec: TYPE = Thrush.IntervalSpec;
IntervalSpecs: TYPE = Thrush.IntervalSpecs;
ProseSpec: TYPE = Thrush.ProseSpec;
ProseSpecs: TYPE = Thrush.ProseSpecs;
NB: TYPE = Thrush.NB;
none: SHHH = Thrush.none;
nullHandle: Thrush.ThHandle = Thrush.nullHandle;
PartyHandle: TYPE = Thrush.PartyHandle;
Reason: TYPE = Thrush.Reason;
ROPE: TYPE = Thrush.ROPE;
SHHH: TYPE = Lark.SHHH;
SmartsHandle: TYPE = Thrush.SmartsHandle;
StateInConv: TYPE = Thrush.StateInConv;
PD:
TYPE =
RECORD [
doReports: BOOL←FALSE,
doNice: BOOL←FALSE,
timeoutNoAction: INTEGER ← 30,
noActionTicks: Process.Ticks ← Process.SecondsToTicks[30],
timeoutJayConnect: INTEGER ← 1,
noJayTicks: Process.Ticks ← Process.SecondsToTicks[1],
encryptionRequested: BOOLEAN←TRUE,
interfacesAreImported: BOOLEAN←FALSE,
smartsIsExported: BOOLEAN←FALSE,
waitsForConnect: NAT𡤆,
queueIt: BOOL←FALSE,
finchOn: BOOLEAN←FALSE,
maxProseLength: INT
];
info: FinchInfo;
pd: REF PD ← NEW[PD←[]];
stopIntervalSpec: Thrush.IntervalSpec =
NEW[Thrush.IntervalSpecBody←[
interval: [length: 0], direction: play, queueIt: FALSE] ];
stopProseSpec: Thrush.ProseSpec =
NEW[Thrush.ProseSpecBody←[
prose: "", direction: play, queueIt: FALSE] ];
resetProseSpec: Thrush.ProseSpec =
NEW[Thrush.ProseSpecBody←[
prose: "\022", direction: record, queueIt:
FALSE] ];
-- \022 is ControlR
Would prefer BOOLEAN filter field to control whether resets can be sent, this hack encodes it in direction=record. August 20, 1985
Report:
PROC[what:
ROPE] = {
IF NOT pd.doReports THEN RETURN;
Log.Report[what, $Finch];
};
BeNice:
PROC[r:
REF, d:
INT] = {
IF NOT pd.doNice THEN RETURN;
Nice.BeNice[r, d, $Finch, NIL];
};
Client Functions
GetHistory: PUBLIC PROC[ convID: Thrush.ConversationHandle, toState: INT𡤀 -- i.e., all-- ]
RETURNS [ s: Thrush.EventSequence ] = {
s←ThParty.GetHistory[shhh: info.shh, convID: convID, firstState: 1, lastState: toState];
};
GetRname:
PUBLIC
PROC[partyID: Thrush.PartyHandle]
RETURNS [rName:
ROPE] = {
RETURN[IF info=NIL THEN NIL ELSE ThParty.DescribeParty[shh: info.shh, partyID: partyID]];
};
DisconnectCall:
PUBLIC
ENTRY
PROC[
convID: Thrush.ConversationHandle, -- not used yet
reason: Thrush.Reason←terminating,
comment: ROPE←NIL] = {
ENABLE UNWIND => NULL;
cDesc: ConvDesc; state: StateInConv;
IF NOT(([,cDesc, state]𡤏inchOn[]).on) THEN RETURN;
cDesc.bluejayConnection ← cDesc.proseConnection ← FALSE;
SELECT state
FROM
idle, any => {
info.ReportConversationState[convNotActive, NIL, "No conversation to disconnect"];
RETURN;
};
ringing, pending => IF reason=terminating THEN reason ← busy;
ENDCASE;
Request[info, cDesc, idle, reason, comment];
};
AnswerCall:
PUBLIC
ENTRY
PROC[convID: Thrush.ConversationHandle
-- not used yet -- ] = {
ENABLE UNWIND => NULL;
cDesc: ConvDesc; state: StateInConv;
IF NOT(([,cDesc, state]𡤏inchOn[]).on) THEN RETURN;
SELECT state
FROM
ringing, pending =>
NULL;
IF convID#info.currentConvID THEN {
info.ReportConversationState[noSuchConv, NIL, "Can't answer that call"]; RETURN; };
ENDCASE=>RETURN;
Request[info, cDesc, active];
};
PlaceCall:
PUBLIC
ENTRY
PROC [
convID: Thrush.ConversationHandle, -- not used yet --
rName: ROPE, -- rName or description
number: ROPE, -- telephone number, if present
urgency: Thrush.CallUrgency←normal,
useNumber: BOOL←FALSE] = {
ENABLE { UNWIND=>NULL; RPC.CallFailed => { UninitFinchSmarts["Communication Failure"]; CONTINUE; }; };
Get fone for dest
calledPartyID: PartyHandle;
fullRName: ROPE;
IF NOT (FinchOn[].on) THEN RETURN;
calledPartyID ←
SELECT
TRUE
FROM
~useNumber
AND
(calledPartyID ← ThParty.GetParty[shh: info.shh, partyID: info.partyID, rName: rName]) #nullHandle =>
calledPartyID,
number#
NIL =>
ThParty.GetPartyFromNumber[shh: info.shh, partyID: info.partyID, phoneNumber: number, description: rName, trunkOK: TRUE],
rName=NIL OR useNumber => nullHandle,
([fullRName, number,] ← ThParty.GetNumbersForRName[shh: info.shh, rName: rName]).number #
NIL =>
ThParty.GetPartyFromNumber[
shh: info.shh, partyID: info.partyID, phoneNumber: number,
description: fullRName, trunkOK: TRUE],
ENDCASE => nullHandle;
IF calledPartyID = nullHandle
THEN {
info.ReportConversationState[noSuchParty2,
NIL,
IF rName=NIL THEN "You did not supply a call destination"
ELSE "No telephone number could be found for called party"];
};
[]𡤌onnect[calledPartyID, FALSE, FALSE, urgency];
};
RecordTune:
PUBLIC
ENTRY
PROC [
useTune: Thrush.Tune,
useInterval: Thrush.VoiceInterval,
queueIt: BOOL←FALSE
]
RETURNS[
reason: FinchSmarts.RecordReason←hopeless,
tune: Thrush.Tune←Thrush.nullTune,
interval: Thrush.VoiceInterval←[],
key: Thrush.EncryptionKey←Thrush.nullKey
] = {
ENABLE UNWIND=>NULL;
inst: ThMessageDB.MessageInstance = ThMessageDB.GetMessage["Beep.Lark", 1];
Out of luck, for a while . . . no way to talk about it here!!!!
IF inst#NIL THEN PlBaMe[tune, key: extract from equiv. of inst.];
<<If useTune#newTune, probably won't work -- no key. Nuthatch will solve.>>
<<<Won't work (key troubles) if new tunes are supplied. Vestigial>>>
ourSpec: Thrush.IntervalSpec;
cDesc: ConvDesc; state: StateInConv;
started: BOOL←FALSE;
IF
NOT (([,cDesc,state]←JayConnection[
TRUE]).jayOpen)
THEN
RETURN;
That's all, folks . . . for now.
ourSpec ←
NEW[Thrush.IntervalSpecBody ← [
type: request, direction: record, queueIt: queueIt,
tune: useTune, interval: useInterval, keyIndex: 1
]];
Wait for recording to (fail to) begin.
EnqueueIntervals[cDesc, LIST[ourSpec]];
started ← WaitForStartOrFail[cDesc, ourSpec];
state ← cDesc.cState.state;
IF ~started
OR cDesc.cState.keyTable=
NIL
THEN {
Recording didn't start or can't be decrypted; return with error value.
IF state#idle THEN Complain[info, cDesc, error, "Voice server connection failed"];
RETURN[hopeless, Thrush.nullTune, , ];
};
Wait for recording or conversation to end.
WHILE cDesc.cState.state#idle
AND ourSpec.type#finished
DO
WAIT info.thAction;
ENDLOOP;
RETURN[ok, ourSpec.tune, ourSpec.interval, cDesc.cState.keyTable[ourSpec.keyIndex]];
If state is idle, possibly the interval doesn't know its actual duration.
};
WaitForStartOrFail:
INTERNAL
PROC[cDesc: ConvDesc, spec: Thrush.IntervalSpec]
RETURNS[started: BOOL←FALSE] = {
ENABLE UNWIND => NULL;
TRUSTED { Process.SetTimeout[@info.thAction, pd.noJayTicks]; };
FOR i:
NAT
IN [0..pd.waitsForConnect)
DO
WAIT info.thAction; -- <<The supervisor condition vbl; will this work?>>
IF cDesc.cState.state=idle OR (started←(spec.type#request)) THEN EXIT;
ENDLOOP;
TRUSTED { Process.SetTimeout[@info.thAction, pd.noActionTicks]; };
};
StopTune:
PUBLIC
ENTRY
PROC [reason: FinchSmarts.RecordReason←ok] = {
ENABLE UNWIND=>NULL;
cDesc: ConvDesc;
IF NOT (([,cDesc,]←JayConnection[FALSE]).jayOpen) THEN RETURN; -- Stop only if started
EnqueueIntervals[cDesc, LIST[NEW[Thrush.IntervalSpecBody←stopIntervalSpec^]]];
};
StopSpeech:
PUBLIC
ENTRY
PROC [reason: FinchSmarts.RecordReason←ok] = {
ENABLE UNWIND=>NULL;
cDesc: ConvDesc;
IF NOT (([,cDesc,]←ProseConnection[FALSE]).proseOpen) THEN RETURN; -- Stop only if started
ClearPendingProses[cDesc];
EnqueueProses[cDesc, LIST[NEW[Thrush.ProseSpecBody←stopProseSpec^]]];
};
ResetProse:
PUBLIC
ENTRY
PROC [reason: FinchSmarts.RecordReason←ok] = {
ENABLE UNWIND=>NULL;
cDesc: ConvDesc;
IF NOT (([,cDesc,]←ProseConnection[FALSE]).proseOpen) THEN RETURN; -- Stop only if started
ClearPendingProses[cDesc];
EnqueueProses[cDesc, LIST[NEW[Thrush.ProseSpecBody←resetProseSpec^]]];
};
NoiseSpec: TYPE = REF NoiseSpecRec;
NoiseSpecRec:
TYPE =
RECORD [
tune: Thrush.Tune←-1,
key: Thrush.EncryptionKey← Thrush.nullKey
];
PlayNoise:
PUBLIC
PROC [
convID: Thrush.ConversationHandle ← Thrush.nullConvHandle, --vestigial until I figger it out
noiseName: ATOM,
queueIt: BOOL,
serverInstance: ROPE,
failOK: BOOL, -- playing is optional; leave connection open if tune doesn't exist.
wait: BOOL -- wait for noise to begin playing or the connection to fail.
] RETURNS [ started: BOOL←FALSE ] = TRUSTED {
ENABLE ANY => CONTINUE;
noiseSpec: NoiseSpec;
IF info=NIL THEN RETURN[FALSE];
noiseSpec ← NARROW[RefTab.Fetch[info.sysNoises, noiseName].val];
testInterval: Thrush.IntervalSpec;
IF noiseSpec =
NIL
THEN {
key: Thrush.EncryptionKey;
cardKey: LONG POINTER TO ARRAY[0..2) OF LONG CARDINAL = LOOPHOLE[LONG[@key]];
val: NamesGV.AttributeSeq;
s: IO.STREAM;
noiseSpec ← NEW[NoiseSpecRec ← []];
IF serverInstance =
NIL
THEN
serverInstance ← UserProfile.Token["ThrushClientServerInstance", "Strowger.lark"];
val ← NamesGV.GVGetAttributeSeq[serverInstance, noiseName];
IF val=NIL OR val.length#2 THEN RETURN;
noiseSpec.tune ← Convert.IntFromRope[val[0].attributeValue];
IF noiseSpec.tune=-1 THEN RETURN;
s←IO.RIS[val[1].attributeValue];
cardKey[0] ← s.GetCard[]; cardKey[1] ← s.GetCard[];
noiseSpec.key ← key;
[]←RefTab.Store[info.sysNoises, noiseName, noiseSpec];
};
started ← PlaybackTune[tune: noiseSpec.tune, interval: [], key: noiseSpec.key, queueIt: queueIt, failOK: failOK, wait: wait];
FlushNoiseCacheCmd: Commander.CommandProc = {
info.sysNoises ← RefTab.Create[];
};
PlaybackTune:
PUBLIC
ENTRY
PROC [
tune: Thrush.Tune,
interval: Thrush.VoiceInterval,
key: Thrush.EncryptionKey,
queueIt: BOOL←FALSE,
failOK: BOOL, -- playing is optional; leave connection open if tune doesn't exist.
wait: BOOL←FALSE
] RETURNS[ started: BOOL←FALSE ] = { -- FALSE if failed or if didn't wait to find out
ENABLE UNWIND=>NULL;
cDesc: ConvDesc; state: StateInConv;
nb: NB; keyIndex: [0..17B];
exists: BOOL;
specs: Thrush.IntervalSpecs;
spec: Thrush.IntervalSpec;
IF tune<=Thrush.nullTune
THEN {
info.ReportConversationState[convNotActive, NIL, "No such tune"]; RETURN; }; -- UGH!
IF NOT (([,cDesc,state]←JayConnection[TRUE]).jayOpen) THEN RETURN;
[nb, keyIndex] ←
ThParty.RegisterKey[shh: info.shh, credentials: cDesc.cState.credentials, key: key];
IF nb#success
AND nb#stateMismatch
THEN {
IF ~failOK THEN Complain[info, cDesc, error, "Could not encode encryption key"];
RETURN;
};
[, exists, specs] ← ThParty.DescribeInterval[shhh: info.shh, credentials: cDesc.cState.credentials,
targetInterval: NEW[Thrush.IntervalSpecBody ← [ tune: tune, interval: interval, direction: play ] ],
minSilence: LAST[INT]]; -- Just trim off surrounding silences.
IF ~exists
THEN {
IF ~failOK THEN Complain[info, cDesc, error, "Could not play utterance"];
RETURN;
};
IF specs.rest#NIL THEN ERROR;
spec ← specs.first;
spec.keyIndex ← keyIndex; spec.type ← request; spec.direction ← play; spec.queueIt ← queueIt;
EnqueueIntervals[cDesc, specs ];
IF wait THEN started ← WaitForStartOrFail[cDesc, spec];
};
defaultTranslateProc: FinchSmarts.ProseTranslateProc←NIL;
RegisterTranslateProc:
PUBLIC
PROC [translate: FinchSmarts.ProseTranslateProc←
NIL] ~ {
defaultTranslateProc ← translate;
};
TextToSpeech:
PUBLIC
ENTRY
PROC [
text: Rope.ROPE, queueIt: BOOL←TRUE,
proseTranslateProc: FinchSmarts.ProseTranslateProc←NIL]
RETURNS [reason: FinchSmarts.RecordReason←ok] = {
ENABLE UNWIND => NULL;
Modelled after RecordTune, but it breaks text up into manageable chunks so as to allow speech to start sooner and flush quicker...
ourSpec: Thrush.ProseSpec;
cDesc: ConvDesc; state: StateInConv;
started: BOOL←FALSE;
IF proseTranslateProc#NIL THEN text ← proseTranslateProc[text]
ELSE IF defaultTranslateProc#NIL THEN text ← defaultTranslateProc[text];
IF
NOT (([,cDesc,state]←ProseConnection[
TRUE]).proseOpen)
THEN
RETURN[hopeless];
IF ~queueIt THEN ClearPendingProses[cDesc]; -- need to generate reports??
WHILE text.Length[] > pd.maxProseLength
DO
Scan backward from the end of the text for a place to break between words. The Prose treats an index marker as a word terminator. Keep punctuation with its preceding word for correct prosodics. Except - need to handle $, Dr., ft. specially both here and in LarkOutImpl. Where to put a common procedure? They run on different machines!
index: INT ← pd.maxProseLength;
WHILE IO.TokenProc[Rope.Fetch[text, index-1]] = other
DO
index ← index-1;
IF index=0
THEN {index ← pd.maxProseLength;
EXIT};
Too long between break chars; just make progress somehow.
ENDLOOP;
ourSpec ←
NEW[Thrush.ProseSpecBody ← [
type: request, direction: play, queueIt: queueIt, prose: text.Substr[len: index]
]];
text ← text.Substr[start: index];
EnqueueProses[cDesc, LIST[ourSpec]];
queueIt ← TRUE;
ENDLOOP;
ourSpec ←
NEW[Thrush.ProseSpecBody ← [
type: request, direction: play, queueIt: queueIt, prose: text
]];
EnqueueProses[cDesc, LIST[ourSpec]];
For now, assume that everything will work okay from here.
};
Registration
InitFinchSmarts:
PUBLIC
PROC [
thrushInstance: Thrush.ROPE←NIL,
ReportSystemState: PROC[ on: BOOL ],
ReportConversationState: PROC[ nb: NB, cDesc: ConvDesc, remark: Rope.ROPE ]
] = {
problem: ROPE←NIL; {
ENABLE RPC.CallFailed => { problem ← "Communication Failure"; GOTO InitFailed; };
partyID: Thrush.PartyHandle;
smartsID: Thrush.SmartsHandle;
thVR: RPC.VersionRange = ThVersions.GetThrushVR;
problem: Thrush.ROPE ← NIL;
namesGVInstance: Thrush.ROPE←NIL;
UninitFinchSmarts[NIL];
IF info=NIL THEN info ← NEW[FinchSmarts.FinchInfoBody];
info.conversations ← NIL;
info.thProcess ← NIL;
info.currentConvID ← nullConvHandle;
info.apprise ← FALSE;
info.ReportSystemState←ReportSystemState;
info.ReportConversationState←ReportConversationState;
IF info.sysNoises=NIL THEN info.sysNoises ← RefTab.Create[];
IF thrushInstance =
NIL
THEN thrushInstance ← UserProfile.Token[
key: "ThrushClientServerInstance", default: "Strowger.Lark"];
info.myName ← [
type: "ThSmarts.Lark",
instance: Names.InstanceFromNetAddress[netAddress: Names.OwnNetAddress[], suffix: "0"],
version: ThVersions.FinchVR];
info.myRName ← Names.CurrentRName[];
info.myPassword ← Names.CurrentPasskey[];
info.serverInstanceName ← thrushInstance;
ThSmartsRpcControl.ExportInterface[
interfaceName: info.myName,
user: info.myRName,
password: info.myPassword];
pd.smartsIsExported←
TRUE;
For the purpose of importing NamesGV, use prior binding of serverInstance if there is one and the server name is the same as before.
namesGVInstance ← UserProfile.Token[key: "NamesGVInstance", default: "Strowger.lark"];
IF info.namesGVInstance=NIL OR ~namesGVInstance.Equal[s2: info.namesGVInstanceName, case: FALSE] THEN info.namesGVInstance ← namesGVInstance;
info.namesGVInstanceName ← namesGVInstance;
IF PrincOpsUtils.IsBound[
LOOPHOLE[NamesGVImpExp.GVImport]]
AND ~NamesGVImpExp.GVImport[info.namesGVInstance]
AND
~NamesGVImpExp.GVImport[info.namesGVInstanceName] THEN {
problem ← "Couldn't import Grapevine Package"; GOTO InitFailed; };
info.shh ← IF NOT pd.encryptionRequested THEN Thrush.unencrypted
ELSE NamesRPC.StartConversation [
caller: info.myRName,
callee: thrushInstance,
key: info.myPassword,
level: --<<ECB>>--CBCCheck !
RPC.AuthenticateFailed=> { problem←"Could not authenticate"; GOTO InitFailed}];
Implement the GV lookup portion of importing the Thrush instance explicitly, in order to get the benefit of the local Grapevine cache.
IF (info.namesGVInstance ← NamesGV.GVGetAttribute[rName: info.namesGVInstanceName, attribute: $connect, default: NIL]) = NIL THEN { problem←"Telephone server not found"; GOTO InitFailed};
IF (info.serverInstance ← NamesGV.GVGetAttribute[rName: info.serverInstanceName, attribute: $connect, default: NIL]) = NIL THEN { problem←"Telephone server not found"; GOTO InitFailed};
ThPartyRpcControl.ImportInterface[
interfaceName: [type: "ThParty.Lark", instance: info.serverInstance, version: thVR] ! --
RPC.ImportFailed=> {
IF why=wrongVersion THEN
problem ← IO.PutFR["Finch version %d too old; import failed",
card[ThVersions.FinchVersion]];
GOTO InitFailed;
}];
pd.interfacesAreImported←TRUE;
partyID←ThParty.CreateParty[shh: info.shh, type: individual, rName: info.myRName];
IF partyID=Thrush.nullHandle THEN { problem←"Can't register with server"; GOTO InitFailed; };
smartsID←ThParty.Register[
shh: info.shh, partyID: partyID, interface: NEW[ThSmartsRpcControl.InterfaceName←info.myName],
properties: [x: manager[machine: Names.OwnNetAddress[]]]] ;
IF smartsID=Thrush.nullHandle THEN { problem←"Can't register with server"; GOTO InitFailed; }; -- <<try to delete the Party?>>
info.smartsID ← smartsID;
info.partyID ← partyID;
ThParty.ConversationsForParty[shh: info.shh, partyID: partyID];
Will shortly update our knowledge of what's already going on!!
pd.finchOn←TRUE;
info.ReportSystemState[pd.finchOn];
EXITS
InitFailed => UninitFinchSmarts[problem];
};};
UninitFinchSmarts:
PUBLIC
PROC[problem:
ROPE←
NIL] = {
<<Must eliminate participation in any conversation that only we know enough about to get rid of, and eliminate any evidence of our partipation in any others. For now, do only the latter thing>>
ENABLE RPC.CallFailed => GOTO Failed;
IF problem#NIL THEN Problem[problem];
IF info#
NIL
THEN {
IF pd.finchOn
AND info.partyID#nullHandle
AND info.smartsID#nullHandle
THEN
ThParty.Deregister[info.shh, info.smartsID!RPC.CallFailed=>CONTINUE];
info.shh←none;
info.ReportSystemState[FALSE];
WHILE info.conversations#
NIL
DO
-- a bug?!
cDesc: ConvDesc=info.conversations;
info.conversations ← cDesc.next;
cDesc.nextsc.prev←NIL;
cDesc.clientData←NIL;
ENDLOOP;
};
pd.finchOn←FALSE;
IF PrincOpsUtils.IsBound[
LOOPHOLE[NamesGVImpExp.UnGVImport]]
THEN
NamesGVImpExp.UnGVImport[];
IF pd.interfacesAreImported THEN -- We don't do this any more.
ThPartyRpcControl.UnimportInterface[!LupineRuntime.BindingError=>CONTINUE];
pd.interfacesAreImported←FALSE;
IF pd.smartsIsExported
THEN
ThSmartsRpcControl.UnexportInterface[!LupineRuntime.BindingError=>CONTINUE];
pd.smartsIsExported←FALSE;
EXITS
Failed => pd.interfacesAreImported ← pd.smartsIsExported ← FALSE;
FinchIsRunning:
PUBLIC
PROC RETURNS [finchIsRunning:
BOOL] = {
RETURN[pd.finchOn]; };
Utilities
GetConv:
PUBLIC INTERNAL
PROC[convID: ConversationHandle, validIfNew:
BOOL
] RETURNS [ cDesc: ConvDesc←NIL ] = --INLINE-- {
pDesc: ConvDesc←NIL;
IF convID#nullConvHandle THEN
FOR cDesc ← info.conversations, cDesc.next
WHILE cDesc#
NIL
DO
descID: ConversationHandle = cDesc.cState.credentials.convID;
IF descID = convID THEN RETURN;
IF cDesc.cState.state=idle AND ThParty.SameConvClass[convID, descID] THEN EXIT;
pDescsc; ENDLOOP;
IF cDesc=
NIL
THEN {
-- If not, we're reusing a related entry.
cDesc ← NEW[FinchSmarts.ConvDescBody←[]];
cDesc.prev ← pDesc;
IF pDesc#NIL THEN pDesc.next ← cDesc ELSE info.conversations ← cDesc;
cDesc.otherPartyDesc ← "unknown party";
};
cDesc.descValid ← validIfNew;
cDesc.startTime ← BasicTime.Now[];
cDesc.desiredState ← any;
cDesc.desiredPartyID ← nullHandle;
cDesc.desiredReason ← wontSay;
cDesc.desiredComment←NIL;
cDesc.requestedIntervals ← cDesc.pendingIntervals ← NIL;
cDesc.requestedProses ← cDesc.pendingProses ← NIL;
cDesc.conference← cDesc.completed ← cDesc.failed ←
FALSE;
Other fields are intentionally retained.
cDesc.cState.credentials ← [
convID: convID, smartsID: info.smartsID, partyID: info.partyID, stateID: 0];
IF pd.doReports
THEN
Report[IO.PutFR[" ** NewConv %g %g, vl=%g\n", PutFTime[convID], rope[info.myRName], bool[validIfNew]]];
};
PutFTime:
PROC[convID: ConversationHandle]
RETURNS [
IO.Value] = {
ehsMemorialBool: BOOL←FALSE; -- wouldn't need if could return from catch phrase.
IF convID=nullConvHandle
OR convID=BasicTime.nullGMT
THEN RETURN[rope["(not assigned)"]];
[]sicTime.Update[convID, 0!BasicTime.OutOfRange=> {
ehsMemorialBool←TRUE; CONTINUE; }];
IF ehsMemorialBool THEN RETURN[int[LOOPHOLE[convID, INT]]]
ELSE RETURN[time[convID]];
};
GetConvDesc:
PUBLIC PROC[convID: ConversationHandle]
RETURNS[cDesc: ConvDesc←
NIL ] = {
Not at present protected by monitor -- FinchToolImpl back-pointer problem.
FOR cDesc ← info.conversations, cDesc.next
WHILE cDesc#
NIL
DO
IF cDesc.cState.credentials.convID = convID THEN EXIT; ENDLOOP;
RETURN[IF cDesc#NIL AND cDesc.descValid THEN cDesc ELSE NIL];
};
GetCDesc:
PROC[]
RETURNS [cDesc: ConvDesc←
NIL ] = {
convID: ConversationHandle=info.currentConvID;
IF convID=nullConvHandle THEN RETURN;
RETURN[GetConvDesc[convID]];
};
Apprise:
PUBLIC INTERNAL
PROC[info: FinchInfo] =
--INLINE-- {
IF info.thProcess=
NIL
THEN
TRUSTED { Process.Detach[info.thProcess ← FORK Supervise[info]]; };
info.apprise ← TRUE;
BROADCAST info.thAction;
};
ReportIntervals:
INTERNAL
PROC[cDesc: ConvDesc, event: Thrush.ConvEvent] = {
n^2 match (worst-case) of reported interval information matched against the requests that we have made. This gives us needed progress information.
FOR eSs: IntervalSpecs ← event.intervalSpecs, eSs.rest
WHILE eSs#
NIL
DO
eS: IntervalSpec = eSs.first;
eSID: Thrush.IntID = eS.intID;
FOR rSs: IntervalSpecs ← cDesc.requestedIntervals, rSs.rest
WHILE rSs#
NIL
DO
rS: IntervalSpec = rSs.first;
SELECT CompareIntID[eSID, rS.intID] FROM
less => EXIT; -- Possibly an error: interval exists that we didn't request.
greater => LOOP;
ENDCASE => NULL; -- Equal, deal with it.
rS^ ← eS^; -- Update status.
cDesc.requestedIntervals records all those intervals that have been requested but not denoted finished. They must finish in order. If an embedded entry in this list finishes, then the finish notifications for any preceding entries have been lost; here we simulate them. <<Consider various abnormal finish codes augmenting finish>>
IF rS.type=finished
THEN
-- Truncate requests
FOR fSs: IntervalSpecs ← cDesc.requestedIntervals, fSs.rest
WHILE fSs#rSs
DO
IF fSs.first.type # finished
THEN {
fSs.first.type ← finished; fSs.first.changeNoted←FALSE; };
ENDLOOP;
EXIT;
ENDLOOP;
ENDLOOP;
};
ReportProses:
INTERNAL
PROC[cDesc: ConvDesc, event: Thrush.ConvEvent] = {
n^2 match (worst-case) of reported prose information matched against the requests that we have made. This gives us needed progress information.
FOR eSs: ProseSpecs ← event.proseSpecs, eSs.rest
WHILE eSs#
NIL
DO
eS: ProseSpec = eSs.first;
eSID: Thrush.IntID = eS.intID;
FOR rSs: ProseSpecs ← cDesc.requestedProses, rSs.rest
WHILE rSs#cDesc.pendingProses
AND rSs#
NIL
DO
rS: ProseSpec = rSs.first;
SELECT CompareIntID[eSID, rS.intID] FROM
less => EXIT; -- Possibly an error: prose exists that we didn't request.
greater => LOOP;
ENDCASE => NULL; -- Equal, deal with it.
IF rS.type = eS.type
THEN
NULL
Avoids duplicate request reports during debugging (caused by a process race)
ELSE rS^ ← eS^; -- Update status.
cDesc.requestedProses records all those intervals that have been requested but not denoted finished. They must finish in order. If an embedded entry in this list finishes, then the finish notifications for any preceding entries have been lost; here we simulate them. <<Consider various abnormal finish codes augmenting finish>>
NOTE: ALL ProseSpecs should be reported explicitly (except ones that were flushed)!!!
IF rS.type=finished
THEN
-- Truncate requests
FOR fSs: ProseSpecs ← cDesc.requestedProses, fSs.rest
WHILE fSs#rSs
DO
IF fSs.first.type # finished
THEN {
rS.queueIt should be FALSE; all of these requests have actually been flushed.
fSs.first.type ← finished; fSs.first.changeNoted←FALSE; };
ENDLOOP;
EXIT;
ENDLOOP;
ENDLOOP;
};
CompareIntID:
PROC[i1, i2: Thrush.IntID]
RETURNS [ Basics.Comparison ] = {
RETURN[
IF i1=i2 THEN equal
ELSE
SELECT i1.stateID
FROM
<i2.stateID => less,
>i2.stateID => greater,
ENDCASE =>
SELECT i1.reqID
FROM
<i2.reqID => less,
>i2.reqID => greater,
ENDCASE => ERROR
];
};
EnqueueIntervals:
INTERNAL
PROC[cDesc: ConvDesc, int: IntervalSpecs] = {
Splice int to the end of cDesc.requestedIntervals, which is guaranteed to be a superset of pendingIntervals. When pendingIntervals is a trivial subset (NIL), it needs special attention.
IF cDesc.pendingIntervals=NIL THEN cDesc.pendingIntervals←int;
IF cDesc.requestedIntervals=NIL THEN cDesc.requestedIntervals←int
ELSE
FOR iSs: IntervalSpecs ← cDesc.requestedIntervals, iSs.rest
WHILE iSs#
NIL
DO
IF iSs.rest=NIL THEN { iSs.rest ← int; EXIT; };
ENDLOOP;
Apprise[info];
};
EnqueueProses:
INTERNAL
PROC[cDesc: ConvDesc, prose: ProseSpecs] = {
Splice prose to the end of cDesc.requestedProses, which is guaranteed to be a superset of pendingProses. When pendingProses is a trivial subset (NIL), it needs special attention.
IF cDesc.pendingProses=NIL THEN cDesc.pendingProses←prose;
IF cDesc.requestedProses=NIL THEN cDesc.requestedProses←prose
ELSE
FOR pSs: ProseSpecs ← cDesc.requestedProses, pSs.rest
WHILE pSs#
NIL
DO
cDesc.pendingProses is closer to the end of the list than cDesc.requestedProses is...
Yeah, but maybe it's a different list.
IF pSs.rest=NIL THEN { pSs.rest ← prose; EXIT; };
ENDLOOP;
Apprise[info];
};
ClearPendingProses:
INTERNAL
PROC [cDesc: ConvDesc] ~ {
FOR pSs: ProseSpecs ← cDesc.requestedProses, pSs.rest
WHILE pSs#
NIL
DO
IF pSs.rest=cDesc.pendingProses THEN { pSs.rest ← NIL; EXIT; };
ENDLOOP;
cDesc.pendingProses←NIL;
};
Complain:
INTERNAL
PROC[info: FinchInfo, cDesc: ConvDesc, reason: Reason←wontSay, comment:
ROPE←
NIL] = {
cDesc.desiredState ← IF cDesc.cState.state#any THEN idle ELSE reserved;
cDesc.desiredReason ← reason;
cDesc.desiredComment ← comment;
cDesc.desiredPartyID ← nullHandle;
cDesc.pendingIntervals ← NIL;
ClearPendingProses[cDesc];
Apprise[info]; -- BROADCAST is sometimes meaningless (running monitor.)
};
FinchOn:
INTERNAL
PROC
RETURNS [
on: BOOL←FALSE, cDesc: ConvDesc←NIL, state: StateInConv←idle] = {
IF ~pd.finchOn
OR info=
NIL
THEN {
Log.Report["Your Finch is not running", $Finch]; RETURN; };
on←TRUE;
cDesc ← GetCDesc[];
state←IF cDesc=NIL THEN idle ELSE cDesc.cState.state;
};
Request:
INTERNAL
PROC[
info: FinchInfo,
cDesc: ConvDesc,
state: StateInConv,
reason: Reason←wontSay,
comment: ROPE←NIL
] = {
cDesc.desiredState ← state;
cDesc.desiredReason ← reason;
cDesc.desiredComment ← comment;
Apprise[info];
};
Connect:
INTERNAL
PROC [
calledPartyID: PartyHandle, bluejayConnection: BOOL, proseConnection: BOOL, urgency: Thrush.CallUrgency, waitForActive: BOOL←FALSE]
RETURNS [ cDesc: ConvDesc ] = {
state: StateInConv;
[,cDesc, state]𡤏inchOn[];
SELECT state
FROM
idle => { cDesc ← GetConv[nullConvHandle, TRUE]; cDesc.cState.state𡤊ny; };
reserved => NULL;
ENDCASE => {
info.ReportConversationState[convStillActive, NIL, "Conversation already in progress"];
RETURN;
};
cDesc.bluejayConnection ← bluejayConnection;
cDesc.proseConnection ← proseConnection;
cDesc.desiredPartyID ← calledPartyID;
cDesc.pendingIntervals ← NIL;
ClearPendingProses[cDesc];
Request[info, cDesc, active];
IF ~waitForActive THEN RETURN;
FOR i:
NAT
IN [0..pd.waitsForConnect)
DO
TRUSTED { Process.SetTimeout[@info.thAction, pd.noJayTicks]; };
WAIT info.thAction; -- <<The supervisor condition vbl; will this work?>>
TRUSTED { Process.SetTimeout[@info.thAction, pd.noActionTicks]; };
SELECT cDesc.cState.state
FROM
active => RETURN;
idle => EXIT;
ENDCASE;
ENDLOOP;
Complain[info, cDesc, error, "Finch failed to connect to voice service."];
};
JayConnection:
INTERNAL
PROC[newConn:
BOOL←
TRUE]
RETURNS [
jayOpen: BOOLLSE,
cDesc: ConvDesc←NIL,
state: StateInConv←idle
] = {
calledPartyID: PartyHandle;
IF
NOT (([,cDesc, state]𡤏inchOn[]).on)
THEN {
jayOpen←RepRet[FALSE, noSuchParty, "Your Finch is not running"]; RETURN; };
SELECT state
FROM
idle, reserved =>
IF newConn
THEN {
calledPartyID ← ThParty.GetParty[shh: info.shh, partyID: info.partyID, rName: "Recording", type: service];
IF calledPartyID # Thrush.nullHandle
THEN
cDesc ← Connect[calledPartyID, TRUE, FALSE, normal, TRUE];
};
ENDCASE;
jayOpen←RepRet[IF cDesc=NIL THEN FALSE ELSE cDesc.bluejayConnection, noSuchConv, "Connection to voice server failed"];
};
ProseConnection:
INTERNAL
PROC[newConn:
BOOL←
TRUE]
RETURNS [
proseOpen: BOOLLSE,
cDesc: ConvDesc←NIL,
state: StateInConv←idle
] = {
calledPartyID: PartyHandle;
IF
NOT (([,cDesc, state]𡤏inchOn[]).on)
THEN {
proseOpen←RepRet[FALSE, noSuchParty, "Your Finch is not running"]; RETURN; };
SELECT state
FROM
idle, reserved =>
IF newConn
THEN {
calledPartyID ← ThParty.GetParty[shh: info.shh, partyID: info.partyID, rName: "Text-to-Speech", type: service];
Would really like to be able to distinguish between service dead and service busy for client reports (return param from TextToSpeech). Also true for JayConnection above.
IF calledPartyID # Thrush.nullHandle
THEN
cDesc ← Connect[calledPartyID, FALSE, TRUE, normal, TRUE];
};
ENDCASE;
proseOpen←RepRet[IF cDesc=NIL THEN FALSE ELSE cDesc.proseConnection, noSuchConv, "Connection to text-to-speech server failed"];
};
RepRet:
PROC[bool:
BOOL, nb:
NB, remark: Rope.
ROPE]
RETURNS[sameBool: BOOL] = {
IF bool=
FALSE
THEN
IF info#NIL THEN info.ReportConversationState[nb, NIL, remark]
ELSE Log.Report[remark, $Finch];
sameBool𡤋ool;
};
Problem:
PROC[remark:
ROPE←NIL] = { Log.Problem[remark, $Finch]; };
Feep:
PUBLIC
ENTRY PROC[feepString:
ROPE] = {
cDesc: ConvDesc;
IF info=
NIL
OR info.conversations=
NIL
OR info.currentConvID=nullConvHandle
THEN RETURN;
cDesc ← GetConv[info.currentConvID, FALSE];
EnqueueProses[cDesc,
LIST[NEW[Thrush.ProseSpecBody←[prose: feepString, direction: play]]]];
};
State Transition Tables
Transition: TYPE = {
Just codes to dispatch on in Supervisor; explained there
noop, elim, idle, alrt, actv, spvs, invl, ntiy
};
transForStates:
ARRAY StateInConv
OF
ARRAY StateInConv
OF Transition ← [
idle resrv pars init pend mayb ring canAc activ inact any -- desired
[ elim, invl, invl, invl, invl, invl, invl, invl, elim, invl, elim ], -- idle (current)
[ idle, noop, invl, invl, invl, invl, invl, invl, alrt, ntiy, noop ], -- reserved
[ idle, invl, invl, invl, invl, invl, invl, invl, invl, ntiy, noop ], -- parsing
[ idle, invl, invl, invl, invl, invl, invl, invl, noop, ntiy, noop ], -- initiating
[ idle, invl, invl, invl, invl, invl, invl, invl, actv, ntiy, noop ], -- pending
[ idle, invl, invl, invl, invl, invl, invl, invl, noop, ntiy, noop ], -- maybe
[ idle, invl, invl, invl, invl, invl, invl, invl, actv, ntiy, noop ], -- ringing
[ idle, invl, invl, invl, invl, invl, invl, invl, ntiy, ntiy, ntiy ], -- canActivate
[ idle, invl, invl, invl, invl, invl, invl, invl, spvs, ntiy, spvs ], -- active
[ idle, invl, invl, invl, invl, invl, invl, invl, ntiy, invl, ntiy ], -- inactive
[ elim, alrt, invl, invl, invl, invl, invl, invl, alrt, ntiy, invl ] -- any (nonex)
];
NB: examine relationship to validity table in PartyOpsImpl someday.
}.
Swinehart, August 6, 1985 5:43:08 pm PDT
Merge PTZ prose changes
changes to: DIRECTORY, NB, PD, stopIntervalSpec, stopProseSpec, Report, Progress, Supervise, DisconnectCall, PlaceCall, StopTune, NoiseSpec, PlaybackTune, TextToSpeech, InitFinchSmarts, GetConv, ReportProses, CompareIntID, EnqueueProses, Complain, Connect, JayConnection, ProseConnection, RepRet, FinchSmarts