<> <> <> <> 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; <> 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_6, queueIt: BOOL_FALSE, finchOn: BOOLEAN_FALSE, maxProseLength: INT_1000 ]; 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 <> 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]; }; <> <<>> Progress: PUBLIC ENTRY PROC[ shh: SHHH, smartsID: Thrush.SmartsHandle, event: Thrush.ConvEvent, yourParty: BOOL, latestEvent: BOOL, informationOnly: BOOL ] RETURNS [ d: Thrush.Disposition ] = { ENABLE UNWIND => NULL; cDesc: ConvDesc; IF info=NIL THEN RETURN[pass]; d_pass; <> IF pd.doReports THEN Report[ Rope.Concat[ IO.PutFR["---- FnProg: %g(%d) %g %g yr=%g ", PutFTime[event.credentials.convID], int[event.credentials.stateID], refAny[NEW[StateInConv_event.state]], rope[info.myRName], bool[yourParty]], IO.PutFR["lt=%g in=%g, pr=%g, ky=%g\n", bool[latestEvent], bool[event.intervalSpecs#NIL], bool[event.proseSpecs#NIL], bool[event.keyTable#NIL]]]]; cDesc _ GetConv[event.credentials.convID, FALSE]; IF event.credentials.stateID <= cDesc.cState.credentials.stateID THEN RETURN[pass]; -- Old news! <<>> <> cDesc.cState.credentials.smartsID _ smartsID; cDesc.cState.credentials.stateID _ event.credentials.stateID; <<<> <>>> <> IF event.keyTable#NIL THEN { cDesc.cState.keyTable _ event.keyTable; cDesc.newKeys_TRUE; }; IF event.intervalSpecs#NIL THEN ReportIntervals[cDesc, event]; IF event.proseSpecs#NIL THEN ReportProses[cDesc, event]; IF event.address#NIL THEN { cDesc.cState.address_event.address; cDesc.newAddress_TRUE; }; <<>> <> IF yourParty THEN { Other: PROC={ oD: ROPE; IF (~latestEvent) OR cDesc.otherPartyID#nullHandle THEN RETURN; [,cDesc.otherPartyID, oD, cDesc.conference] _ ThParty.OtherParty[shhh: info.shh, credentials: cDesc.cState.credentials]; IF oD#NIL THEN cDesc.otherPartyDesc _ oD; }; cDesc.descValid _ TRUE; cDesc.cState.credentials.partyID _ event.credentials.partyID; cDesc.cState.state _ event.state; cDesc.cState.comment _ event.comment; cDesc.cState.reason _ event.reason; IF event.spec#NIL THEN { cDesc.cState.spec _ event.spec; cDesc.newSpec_TRUE; }; <> IF event.comment#NIL THEN cDesc.cState.comment _ event.comment; IF event.urgency#normal THEN cDesc.cState.urgency _ event.urgency; IF event.alertKind#standard THEN cDesc.cState.alertKind _ event.alertKind; SELECT event.state FROM reserved, parsing => cDesc.originator _ us; initiating => { cDesc.originator _ us; Other[]; }; pending => { cDesc.originator _ them; Other[]; }; maybe, ringing, active, canActivate, inactive => Other[]; ENDCASE; }; BeNice[event, 4]; BeNice[cDesc, 6]; <<>> cDesc.newEvent_TRUE; IF latestEvent THEN Apprise[info]; -- Wait for the last report to wake process. }; Supervise: PUBLIC ENTRY PROC[info: FinchInfo ] = { problem: ROPE_NIL; TRUSTED { Process.SetTimeout[@info.thAction, pd.noActionTicks]; }; IF info.apprise THEN DO ENABLE { UNWIND => NULL; < GOTO Failing;>> RuntimeError.UNCAUGHT => Problem["Unknown Finch Smarts Supervisor Failure"]; <> < { problem _ "Unknown Finch Smarts Supervisor Failure"; GOTO Failing; };>> }; cDesc: ConvDesc; prevL: ConvDesc _ NIL; nb: NB_success; trans: Transition; numC, numDead: NAT_0; info.apprise _ FALSE; FOR cDesc _ info.conversations, cDesc.next WHILE cDesc#NIL DO stateNow: StateInConv = cDesc.cState.state; newEvent: BOOL=cDesc.newEvent; ours: BOOL _ (info.currentConvID = cDesc.cState.credentials.convID); cDesc.newEvent_FALSE; -- For things that must be done once-only per new state. IF newEvent OR stateNow#idle THEN { IF pd.doReports THEN Report[ Rope.Concat[ IO.PutFR["**** FnSup: %g(%d) %g %g->%g", PutFTime[cDesc.cState.credentials.convID], int[cDesc.cState.credentials.stateID], rope[info.myRName], refAny[NEW[StateInConv_stateNow]], refAny[NEW[StateInConv_cDesc.desiredState]]], IO.PutFR[" %g%g\n", refAny[NEW[Transition_transForStates[stateNow][cDesc.desiredState]]], rope[IF ours THEN " (ours)" ELSE ""]]]]; BeNice[cDesc, 6]; }; <<>> IF NOT cDesc.descValid THEN LOOP; <> numC_numC+1; SELECT stateNow FROM idle => IF ours THEN info.currentConvID _ nullConvHandle; parsing, reserved, initiating, maybe, ringing, canActivate, active, inactive => { ours_TRUE; info.currentConvID _ cDesc.cState.credentials.convID; }; pending, any => NULL; ENDCASE => NULL; <> SELECT (trans_transForStates[stateNow][cDesc.desiredState]) FROM noop => NULL; elim => { IF newEvent THEN cDesc.bluejayConnection _ cDesc.proseConnection _ FALSE ELSE numDead_numDead+1; cDesc.requestedIntervals_cDesc.pendingIntervals_NIL; cDesc.requestedProses_cDesc.pendingProses_NIL; cDesc.cState.credentials.stateID_0; -- In case re-used. }; alrt => { -- placing call, or reserving conversation convID: ConversationHandle; IF NOT ours THEN Problem["What to do in a race?"]; IF cDesc.desiredState#reserved THEN cDesc.weOriginated _ TRUE; [nb, convID] _ ThParty.Alert [ credentials: cDesc.cState.credentials, calledPartyID: cDesc.desiredPartyID, state: IF cDesc.desiredState=reserved THEN reserved ELSE initiating, reason: cDesc.desiredReason, comment: cDesc.desiredComment ]; IF nb=success THEN { cDesc.cState.credentials.convID _ info.currentConvID _ convID; ours_TRUE; }; }; idle, actv => -- Simple transitions nb _ ThParty.Advance [ shhh: info.shh, credentials: cDesc.cState.credentials, state: cDesc.desiredState, reason: cDesc.desiredReason, comment: cDesc.desiredComment ]; spvs => { IF cDesc.pendingIntervals#NIL THEN { -- Issue any new requests, then record the resulting stateID so that later comparisons will work. stateID: Thrush.StateID=cDesc.cState.credentials.stateID+1; reqID: CARDINAL _ 177777B; FOR iSL: IntervalSpecs _ cDesc.pendingIntervals, iSL.rest WHILE iSL#NIL DO iSL.first.intID _ [stateID, reqID_reqID+1]; ENDLOOP; nb _ ThParty.SetIntervals[ shhh: info.shh, credentials: cDesc.cState.credentials, intervalSpecs: cDesc.pendingIntervals ]; IF nb=success THEN cDesc.pendingIntervals_NIL; }; IF cDesc.pendingProses#NIL THEN { <> nextPSL, prevPSL: ProseSpecs _ NIL; stateID: Thrush.StateID=cDesc.cState.credentials.stateID+1; reqID: CARDINAL _ 177777B; textLen: INT _ 0; FOR pSL: ProseSpecs _ cDesc.pendingProses, pSL.rest WHILE pSL#NIL DO IF (textLen _ textLen+pSL.first.prose.Length[]) <= pd.maxProseLength THEN { pSL.first.intID _ [stateID, reqID_reqID+1]; prevPSL _ pSL; } ELSE { nextPSL _ pSL; prevPSL.rest _ NIL; EXIT; }; ENDLOOP; nb _ ThParty.SetProse[ shhh: info.shh, credentials: cDesc.cState.credentials, proseSpecs: cDesc.pendingProses ]; prevPSL.rest _ nextPSL; -- put the list back together so that cDesc.requestedProses is still connected IF nb=success THEN { cDesc.pendingProses _ nextPSL; IF ~cDesc.proseConnection THEN cDesc.requestedProses _ NIL; <> }; }; }; invl => { Problem["FinchSmarts: Invalid state transition request."]; info.apprise_TRUE; cDesc.desiredState _ idle; cDesc.desiredReason _ error; cDesc.desiredComment _ "Invalid state transition"; }; ntiy => { Problem["FinchSmarts: State transition not yet implemented."]; info.apprise_TRUE; cDesc.desiredState _ idle; cDesc.desiredReason _ error; cDesc.desiredComment _ "Unimplemented state transition"; }; ENDCASE => ERROR; <> IF nb#success AND pd.doReports THEN Report[IO.PutFR[" ** Results: nb=%g\n", refAny[NEW[Thrush.NB_nb]]]]; <<>> SELECT nb FROM success, stateMismatch => NULL; noSuchParty2 => -- Have to get to error tone here. No such party. Complain[info, cDesc, notFound, "Called party not found"]; narcissism => -- Have to get to error tone here. No such party. Complain[info, cDesc, notFound, "Attempt to call self rejected on philosophical grounds"]; partyNotEnabled => { info.ReportConversationState[noSuchSmarts, cDesc, "Your Etherphone is not connected to the telephone server"]; <<<< How to disarm the cDesc, which will continue to generate noise?>>>> }; invalidTransition, convNotActive, convStillActive => { <> comment: ROPE="Party-level detected invalid state transition request"; Problem[comment]; Complain[info, cDesc, error, comment]; }; notInConv, noSuchConv => { -- Complain, zap, go idle and repeat. comment: ROPE="NotInConv or NoSuchConv"; Problem[comment]; IF ours THEN info.currentConvID _ nullConvHandle; cDesc.cState.credentials.convID _ nullConvHandle; cDesc.cState.credentials.stateID _ 0; Complain[info, cDesc, error, comment]; }; noSuchParty, noSuchSmarts => { -- Complain, deregister, we gone! Needs tuning. problem _ "NoSuchParty or NoSuchSmarts reported, must try to go away"; GOTO Failing; }; ENDCASE => ERROR; <> IF newEvent THEN info.ReportConversationState[success, cDesc, NIL]; <> FOR iSs: IntervalSpecs _ cDesc.requestedIntervals, iSs.rest WHILE iSs#NIL DO IF iSs.first.type=finished AND iSs.first.changeNoted THEN cDesc.requestedIntervals _ iSs.rest ELSE EXIT; ENDLOOP; FOR pSs: ProseSpecs _ cDesc.requestedProses, pSs.rest WHILE pSs#NIL DO IF pSs.first.type=finished AND pSs.first.changeNoted THEN cDesc.requestedProses _ pSs.rest ELSE EXIT; ENDLOOP; prevL _ cDesc; ENDLOOP; IF info.conversations = NIL OR numC=numDead THEN EXIT; IF NOT info.apprise THEN WAIT info.thAction; IF info.conversations = NIL THEN EXIT; REPEAT Failing => UninitFinchSmarts[problem]; -- now Failed ENDLOOP; info.thProcess _ NIL; }; <> <<>> <> <> <> <<};>> <<>> 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]_FinchOn[]).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]_FinchOn[]).on) THEN RETURN; SELECT state FROM ringing, pending => NULL; <> <> 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; }; }; <> 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"]; }; []_Connect[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; <> <> <> <<<>>> <<<<>>>> <<>> ourSpec: Thrush.IntervalSpec; cDesc: ConvDesc; state: StateInConv; started: BOOL_FALSE; IF NOT (([,cDesc,state]_JayConnection[TRUE]).jayOpen) THEN RETURN; <> ourSpec _ NEW[Thrush.IntervalSpecBody _ [ type: request, direction: record, queueIt: queueIt, tune: useTune, interval: useInterval, keyIndex: 1 ]]; <<>> <> EnqueueIntervals[cDesc, LIST[ourSpec]]; started _ WaitForStartOrFail[cDesc, ourSpec]; state _ cDesc.cState.state; IF ~started OR cDesc.cState.keyTable=NIL THEN { <> IF state#idle THEN Complain[info, cDesc, error, "Voice server connection failed"]; RETURN[hopeless, Thrush.nullTune, , ]; }; <<>> <> 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]]; <> }; 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; -- <> 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 [ <> 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]; <> 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; <<>> <> 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 <> 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}; <> 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]]; <> }; <> 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; <> 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: --<>--CBCCheck ! RPC.AuthenticateFailed=> { problem_"Could not authenticate"; GOTO InitFailed}]; <> 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; }; -- <> info.smartsID _ smartsID; info.partyID _ partyID; ThParty.ConversationsForParty[shh: info.shh, partyID: partyID]; <> pd.finchOn_TRUE; info.ReportSystemState[pd.finchOn]; EXITS InitFailed => UninitFinchSmarts[problem]; };}; UninitFinchSmarts: PUBLIC PROC[problem: ROPE_NIL] = { <<<>>> 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.next_cDesc.prev_NIL; cDesc.clientData_NIL; ENDLOOP; }; pd.finchOn_FALSE; IF PrincOpsUtils.IsBound[LOOPHOLE[NamesGVImpExp.UnGVImport]] THEN NamesGVImpExp.UnGVImport[]; <> <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]; }; <> 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; pDesc_cDesc; 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; <> 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)"]]; []_BasicTime.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 ] = { <> 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] = { <> 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. <>>> 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] = { <> 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 <> ELSE rS^ _ eS^; -- Update status. <>>> <> IF rS.type=finished THEN -- Truncate requests FOR fSs: ProseSpecs _ cDesc.requestedProses, fSs.rest WHILE fSs#rSs DO IF fSs.first.type # finished THEN { <> 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 less, >i2.stateID => greater, ENDCASE => SELECT i1.reqID FROM less, >i2.reqID => greater, ENDCASE => ERROR ]; }; EnqueueIntervals: INTERNAL PROC[cDesc: ConvDesc, int: IntervalSpecs] = { <> 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] = { <> 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 <> <> 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]_FinchOn[]; SELECT state FROM idle => { cDesc _ GetConv[nullConvHandle, TRUE]; cDesc.cState.state_any; }; 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; -- <> 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: BOOL_FALSE, cDesc: ConvDesc_NIL, state: StateInConv_idle ] = { calledPartyID: PartyHandle; IF NOT (([,cDesc, state]_FinchOn[]).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: BOOL_FALSE, cDesc: ConvDesc_NIL, state: StateInConv_idle ] = { calledPartyID: PartyHandle; IF NOT (([,cDesc, state]_FinchOn[]).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]; <> 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_bool; }; 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]]]]; }; <<>> <> Transition: TYPE = { <> 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) ]; <> <> ViewCmd: Commander.CommandProc = TRUSTED { Nice.View[pd, "Finch PD"]; }; Commander.Register["VuFinch", ViewCmd, "Program Management variables Finch"]; Commander.Register["FlushNoiseCache", FlushNoiseCacheCmd, "Forget cached SysNoises"]; <<>> <> FinchSmarts.Register[NEW[FinchSmarts.ProcsRecord _ [ playbackTune: PlaybackTune, playNoise: PlayNoise, recordTune: RecordTune, stopTune: StopTune, textToSpeech: TextToSpeech, registerTranslateProc: RegisterTranslateProc, stopSpeech: StopSpeech, finchIsRunning: FinchIsRunning ]]]; }. <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <<>>