<> <> <> <> DIRECTORY BasicTime USING [ Now, nullGMT, OutOfRange, Update ], Commander USING [ CommandProc, Register ], FinchSmarts, IO, Lark USING [ KeyTable, SHHH ], LupineRuntime USING [ BindingError ], NamesGV USING [ GVGetAttribute ], NamesGVImpExp USING [ GVImport, UnGVImport ], NamesRPC USING [ StartConversation ], Nice, PrincOpsUtils USING [ IsBound ], Process USING [ Detach, SecondsToTicks, SetTimeout, Ticks ], RefQ USING [ Dequeue, Enqueue, Map, MapType, Queue ], Rope, RPC USING [ AuthenticateFailed, CallFailed, EncryptionKey, ImportFailed, VersionRange ], RuntimeError USING [ UNCAUGHT ], Synthesizer USING [ BreakText ], ThParty USING [ Advance, Alert, ConversationsForParty, CreateParty, Deregister, DescribeInterval, DescribeParty, GetNumbersForRName, GetParty, GetPartyFromNumber, OtherParty, Register, RegisterKey, SetIntervals, SetProse ], ThPartyRpcControl, Thrush USING[ CallUrgency, ConversationHandle, ConvEvent, Disposition, EncryptionKey, IntervalSpec, IntervalSpecs, IntervalSpecBody, IntID, IntSpecType, NB, NetAddress, 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], VoiceUtils USING [ CurrentPasskey, CurrentRName, InstanceFromNetAddress, MakeRName, NetAddress, NetAddressFromRope, OwnNetAddress, Problem, Report ] ; FinchSmartsImpl: CEDAR MONITOR IMPORTS BasicTime, Commander, IO, FinchSmarts, LupineRuntime, NamesGV, NamesGVImpExp, NamesRPC, Nice, PrincOpsUtils, Process, RefQ, RPC, Rope, RuntimeError, Synthesizer, ThParty, ThPartyRpcControl, ThSmartsRpcControl, ThVersions, UserProfile, VoiceUtils 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, -- ideally, should be a multiple of the LarkOut packet length intvReq: CARDINAL _ 177777B ]; 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: "cP;z:cp0\\P;z:pp0\\P;z:ra180\\P;z:np\\", direction: record, queueIt: FALSE] ]; <> <> dectalkEndSpec: Thrush.ProseSpec = NEW[Thrush.ProseSpecBody_[ prose: " \033P;z+\033\\", direction: play, queueIt: TRUE] ]; -- paragraph pause Report: PROC[what: ROPE] = { IF NOT pd.doReports THEN RETURN; VoiceUtils.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; };>> }; nb: NB_success; trans: Transition; info.apprise _ FALSE; FOR conversations: RefQ.Queue _ info.conversations, conversations.rest WHILE conversations#NIL DO cDesc: ConvDesc = NARROW[conversations.first]; 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; <> SELECT stateNow FROM idle => IF ours THEN info.currentConvID _ nullConvHandle; parsing, reserved, initiating, maybe, ringing, canActivate, active, inactive => { ours_TRUE; IF cDesc.ultimateState cDesc.ultimateState _ pending; any => NULL; ENDCASE => NULL; <> SELECT (trans_transForStates[stateNow][cDesc.desiredState]) FROM noop => NULL; elim => { cDesc.requestedIntervals_cDesc.pendingIntervals_NIL; cDesc.bluejayConnection _ cDesc.proseConnection _ FALSE; cDesc.requestedProses_cDesc.pendingProses_NIL; }; 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 { <> 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 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; IF stateNow=idle THEN { info.conversations _ RefQ.Dequeue[info.conversations, conversations]; cDesc.clientData _ NIL; }; ENDLOOP; IF info.conversations = NIL 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^]]]; }; 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; <<<< ??? No word ??? >>>> [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; }; spec _ NEW[Thrush.IntervalSpecBody _ [ tune: tune, interval: interval, direction: play ] ]; [, exists, specs] _ ThParty.DescribeInterval[ shhh: info.shh, credentials: cDesc.cState.credentials, targetInterval: spec, 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=NIL THEN specs _ LIST[spec]; spec _ specs.first; FOR sL: Thrush.IntervalSpecs _ specs, sL.rest WHILE sL#NIL DO <> IF sL#specs THEN spec.interval.length _ (sL.first.interval.start+sL.first.interval.length) - spec.interval.start; ENDLOOP; 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; 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 proseText: Rope.ROPE _ NIL; [packet: proseText, remainder: text] _ Synthesizer.BreakText[text, pd.maxProseLength]; ourSpec _ NEW[Thrush.ProseSpecBody _ [ type: request, direction: play, queueIt: queueIt, prose: proseText ]]; EnqueueProses[cDesc, LIST[ourSpec]]; queueIt _ TRUE; ENDLOOP; ourSpec _ NEW[Thrush.ProseSpecBody _ [ type: request, direction: play, queueIt: queueIt, prose: text ]]; EnqueueProses[cDesc, LIST[ourSpec, dectalkEndSpec]]; <> <> }; <> 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; hostHint: VoiceUtils.NetAddress; smartsNetAddress: Thrush.NetAddress = VoiceUtils.OwnNetAddress[]; UninitFinchSmarts[NIL]; info _ NEW[FinchSmarts.FinchInfoBody]; -- Dump any old one! info.conversations _ NIL; info.thProcess _ NIL; info.currentConvID _ nullConvHandle; info.apprise _ FALSE; info.ReportSystemState_ReportSystemState; info.ReportConversationState_ReportConversationState; thrushInstance _ VoiceUtils.MakeRName[style: rName, name: IF thrushInstance#NIL THEN thrushInstance ELSE UserProfile.Token[key: "ThrushClientServerInstance", default: "Strowger.Lark"]]; info.myName _ [ type: "ThSmarts.Lark", instance: VoiceUtils.InstanceFromNetAddress[netAddress: smartsNetAddress, suffix: "0"], version: ThVersions.FinchVR]; info.myRName _ VoiceUtils.CurrentRName[]; info.myPassword _ VoiceUtils.CurrentPasskey[]; ThSmartsRpcControl.ExportInterface[ interfaceName: info.myName, user: info.myRName, password: info.myPassword]; pd.smartsIsExported_TRUE; <> namesGVInstance _ UserProfile.Token[key: "NamesGVInstance", default: "Strowger.lark"]; IF ~(PrincOpsUtils.IsBound[LOOPHOLE[NamesGVImpExp.GVImport]] AND NamesGVImpExp.GVImport[namesGVInstance]) 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}]; hostHint _ VoiceUtils.NetAddressFromRope[NamesGV.GVGetAttribute[thrushInstance, $connect, NIL]]; ThPartyRpcControl.ImportInterface[ interfaceName: [type: "ThParty.Lark", instance: thrushInstance, version: thVR], hostHint: hostHint! 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[netAddress: [smartsNetAddress.net, smartsNetAddress.host]]]] ; 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]; { ClearConvs: RefQ.MapType = { cDesc: ConvDesc=NARROW[subqueue.first]; cDesc.clientData _ NIL; }; []_RefQ.Map[info.conversations, ClearConvs]; info.conversations _ NIL; }; }; pd.finchOn_FALSE; IF PrincOpsUtils.IsBound[LOOPHOLE[NamesGVImpExp.UnGVImport]] THEN NamesGVImpExp.UnGVImport[]; 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-- { IsConv: RefQ.MapType = { cDesc _ NARROW[subqueue.first]; IF cDesc.cState.credentials.convID = convID THEN RETURN[TRUE]; }; IF convID#nullConvHandle AND RefQ.Map[info.conversations, IsConv] THEN RETURN; <> cDesc _ NEW[FinchSmarts.ConvDescBody_[]]; cDesc.otherPartyDesc _ "unknown party"; cDesc.descValid _ validIfNew; cDesc.startTime _ BasicTime.Now[]; cDesc.desiredPartyID _ nullHandle; cDesc.cState.credentials _ [ convID: convID, smartsID: info.smartsID, partyID: info.partyID, stateID: 0]; info.conversations _ RefQ.Enqueue[info.conversations, cDesc]; 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 ] = { <> IsConv: RefQ.MapType = { cDesc _ NARROW[subqueue.first]; IF cDesc.cState.credentials.convID = convID THEN RETURN[TRUE]; }; IF convID=nullConvHandle THEN RETURN[NIL]; RETURN[IF RefQ.Map[info.conversations, IsConv] AND cDesc.descValid THEN cDesc ELSE NIL]; }; GetCurrentConvID: PUBLIC PROC RETURNS [convID: ConversationHandle] = { <> RETURN[info.currentConvID]; }; 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: CARDINAL = eS.intID.reqID; FOR rSs: IntervalSpecs _ cDesc.requestedIntervals, rSs.rest WHILE rSs#NIL DO rS: IntervalSpec = rSs.first; IF eSID # rS.intID.reqID THEN LOOP; 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: CARDINAL = eS.intID.reqID; FOR rSs: ProseSpecs _ cDesc.requestedProses, rSs.rest WHILE rSs#cDesc.pendingProses AND rSs#NIL DO rS: ProseSpec = rSs.first; IF eSID # rS.intID.reqID THEN LOOP; IF rS.type # eS.type THEN 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; }; EnqueueIntervals: INTERNAL PROC[cDesc: ConvDesc, int: IntervalSpecs] = { <> FOR iSs: IntervalSpecs _ int, iSs.rest WHILE iSs#NIL DO iSs.first.intID _ [0, pd.intvReq _ pd.intvReq+1]; <> ENDLOOP; 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] = { <> FOR pSs: ProseSpecs _ prose, pSs.rest WHILE pSs#NIL DO pSs.first.intID _ [0, pd.intvReq _ pd.intvReq+1]; <> ENDLOOP; 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 { VoiceUtils.Report["Your Finch is not running", $Finch]; RETURN; }; on_TRUE; cDesc _ GetConvDesc[info.currentConvID]; 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 VoiceUtils.Report[remark, $Finch]; sameBool_bool; }; Problem: PROC[remark: ROPE_NIL] = { VoiceUtils.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"]; <<>> <> FinchSmarts.Register[NEW[FinchSmarts.ProcsRecord _ [ playbackTune: PlaybackTune, recordTune: RecordTune, stopTune: StopTune, textToSpeech: TextToSpeech, registerTranslateProc: RegisterTranslateProc, stopSpeech: StopSpeech, finchIsRunning: FinchIsRunning ]]]; }. <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <> <<>> <> <> <> <> <> <> <> <> <<>> <> <> <> <> <> <> <<>> <> <> <> <> <> <> <> <> <> <<>> <> <> <> <> <> <> <> <> <<>>