DIRECTORY Atom USING [ DottedPairNode, GetPName ], Commander USING [ CommandProc, Register ], Convert USING [ IntFromRope ], IO, LarkPlay USING [ MergeToneSpecs, PlayString, ToneSpec, ToneSpecRec ], LarkSmartsMonitorImpl, MBQueue USING [ QueueClientAction ], NameDB USING [ GetAttribute, SetAttribute ], Nice, Process USING [ Detach, SecondsToTicks, SetTimeout ], RefID USING [ ID, Reseal, Unseal ], RefTab USING [ Create, Fetch, Ref, Store ], Rope USING [ Concat, Equal, Fetch, Length, ROPE ], ThNet USING [ pd ], ThParty USING [ Advance, ConversationInfo, DescribeParty, GetConversationInfo, GetKeyTable, GetPartyInfo, PartyInfo ], ThPartyPrivate USING [ GetCurrentParty ], Thrush USING [ ActionReport, ConvEvent, ConversationID, Credentials, PartyID, PartyType, NB, notReallyInConv, nullConvID, nullID, Reason, ROPE, SHHH, SmartsID, StateID, StateInConv ], ThSmartsPrivate USING [ ConvDesc, ConvDescBody, ConvRequest, ConvRequestBody, EnterLarkState, Fail, LarkInfo, LarkState, OpenConversations, ParseState, QueueLarkAction, SmartsInfo ], ThSmartsRpcControl, Triples USING [ Select ], TU, VoiceUtils USING [ MakeAtom, ProblemFR, ReportFR ] ; LarkSmartsSupImpl: CEDAR MONITOR LOCKS root IMPORTS Atom, Commander, Convert, IO, LarkPlay, root: LarkSmartsMonitorImpl, MBQueue, NameDB, Nice, Process, RefID, RefTab, Rope, ThNet, ThParty, ThPartyPrivate, ThSmartsPrivate, Triples, TU, VoiceUtils EXPORTS ThSmartsPrivate SHARES LarkSmartsMonitorImpl = { OPEN IO; ConversationID: TYPE = Thrush.ConversationID; nullConvID: ConversationID=Thrush.nullConvID; ConvDesc: TYPE = ThSmartsPrivate.ConvDesc; ConvEvent: TYPE = Thrush.ConvEvent; Reseal: PROC[r: REF] RETURNS[RefID.ID] = INLINE {RETURN[RefID.Reseal[r]]; }; NB: TYPE = Thrush.NB; OpenConversations: TYPE = ThSmartsPrivate.OpenConversations; PartyID: TYPE = Thrush.PartyID; nullID: RefID.ID = Thrush.nullID; ROPE: TYPE = Thrush.ROPE; SmartsID: TYPE = Thrush.SmartsID; SmartsInfo: TYPE = ThSmartsPrivate.SmartsInfo; StateInConv: TYPE = Thrush.StateInConv; LORA: TYPE = LIST OF REF ANY; PD: TYPE = RECORD [ timeoutNoAction: INTEGER _ 5, doReports: BOOL_FALSE, doNice: BOOL_FALSE ]; pd: REF PD _ NEW[PD_[]]; LarkProgress: PUBLIC ENTRY PROC[ interface: ThSmartsRpcControl.InterfaceRecord, shh: Thrush.SHHH, convEvent: Thrush.ConvEvent ] = { ENABLE UNWIND => NULL; -- RestoreInvariant; convID: ConversationID = convEvent.self.convID; info: SmartsInfo = GetSmartsInfo[smartsID: convEvent.self.smartsID]; cDesc: ConvDesc; reason: Thrush.Reason; cInfo: REF ThParty.ConversationInfo; whatNeedsDoing: WhatNeedsDoing; IF info=NIL THEN { Problem["No Smarts at LarkProgress for SmartsID %g", NIL, card[convEvent.self.smartsID]]; RETURN; }; cDesc _ GetConv[ info, convEvent.self, TRUE ]; IF convEvent.self.partyID = convEvent.other.partyID THEN { -- own state changed NoteNewState[cDesc, convEvent]; -- Report on existing conv. or notification of new one. RETURN; }; whatNeedsDoing _ whatNeedsDoingIf[cDesc.situation.self.state][convEvent.other.state]; SELECT whatNeedsDoing FROM $noop, $ntiy => NULL; -- No action is needed, or we don't know what one to take. $imp => ERROR; -- This is supposed to be impossible! $xrep => Problem["Didn't expect state change report", info]; $frgt => ForgetConv[cDesc]; $invl => { Problem["Invalid state transition", info]; []_ChangeState[cDesc: cDesc, state: $failed, reason: $error, comment: "System Error: Invalid state transition"]; }; $idle => { IF (cInfo _ GetConversationInfo[cDesc]) = NIL THEN RETURN; -- have already complained reason _ IF convEvent.reason#NIL THEN convEvent.reason ELSE $terminating; IF (cInfo.numParties-cInfo.numIdle) <= 1 THEN []_ChangeState[cDesc, IF reason=$terminating THEN $idle ELSE $failed, reason] ELSE IF cDesc.situation.self.state=$active THEN ResetLarkState[cDesc]; }; $idlerg => { IF (cInfo _ GetConversationInfo[cDesc]) = NIL THEN RETURN; -- have already complained reason _ IF convEvent.reason # NIL THEN convEvent.reason ELSE $terminating; IF cInfo.originator = convEvent.other.partyID THEN []_ChangeState[cDesc, IF reason=$terminating THEN $idle ELSE $failed, reason]; }; $rback => IF ~DBInfo[cDesc.situation.self.partyID, $deferringback].value.Equal["true", FALSE] THEN []_ChangeState[cDesc, $ringback]; -- Provide ring-back signal $actv => []_ChangeState[cDesc, $active]; -- Go active (else wait for timeout) $actvd => IF ~DBInfo[cDesc.situation.self.partyID, $deferringback].value.Equal["true", FALSE] THEN []_ChangeState[cDesc, $active]; -- Go active (else wait for timeout) $ckrg => CheckRing[cDesc]; -- We're ringing; Some other party that was ringing has now become active; determine if we should continue ringing. $reac, -- Someone else has entered or returned to active state, while we are active $deac -- Someone else has entered inactive state. => ResetLarkState[cDesc]; ENDCASE => ERROR; }; LarkSubstitution: PUBLIC ENTRY PROC[ interface: ThSmartsRpcControl.InterfaceRecord, shh: Thrush.SHHH, convEvent: Thrush.ConvEvent, oldPartyID: Thrush.PartyID, newPartyID: Thrush.PartyID ] = { ENABLE UNWIND => NULL; -- RestoreInvariant; info: SmartsInfo = GetSmartsInfo[smartsID: convEvent.self.smartsID]; cDesc: ConvDesc; IF info=NIL THEN { Problem["No Smarts at LarkSubstitution for SmartsID %g", NIL, card[convEvent.self.smartsID]]; RETURN; }; IF (cDesc _ GetConv[ info, convEvent.self ]) = NIL THEN RETURN; NoteNewState[cDesc, convEvent]; }; LarkReportAction: PUBLIC ENTRY PROC[ interface: ThSmartsRpcControl.InterfaceRecord, shh: Thrush.SHHH, report: Thrush.ActionReport ] = { ENABLE UNWIND => NULL; nb: NB; info: SmartsInfo = GetSmartsInfo[smartsID: report.self.smartsID]; cDesc: ConvDesc; IF info=NIL THEN { Problem["No Smarts at LarkSubstitution for SmartsID %g", NIL, card[report.self.smartsID]]; RETURN; }; IF (cDesc _ GetConv[ info, report.self ]) = NIL THEN RETURN; SELECT report.actionClass FROM $keyDistribution => { SELECT report.actionType FROM $newKeys => NULL; ENDCASE => RETURN; [nb, cDesc.keyTable] _ ThParty.GetKeyTable[credentials: report.self]; IF nb#$success THEN { cDesc.keyTable _ NIL; RETURN; }; SetLarkState[cDesc, LIST[cDesc.keyTable]]; }; $recording, $playback, $synthesizer => SELECT report.actionType FROM $scheduled => cDesc.lastActionID _ report.actionID; $started => NULL; -- always follows $scheduled $finished, $flushed => { IF cDesc.lastActionID = report.actionID AND cDesc.situation.self.state > Thrush.notReallyInConv AND (SELECT cDesc.info.larkInfo.switchState FROM speaker, monitor =>TRUE, ENDCASE =>FALSE) THEN [] _ ChangeState[cDesc, $idle] }; ENDCASE; ENDCASE; }; nextScheduledCheckRef: REF INT _ NEW[INT_0]; LarkCheckIn: PUBLIC ENTRY PROC[ interface: ThSmartsRpcControl.InterfaceRecord, shh: Thrush.SHHH, credentials: Thrush.Credentials, reason: Thrush.Reason, nextScheduledCheck: INT ] = { ENABLE UNWIND => NULL; info: SmartsInfo _ GetSmartsInfo[credentials.smartsID]; -- That's us. IF info=NIL OR info.failed THEN RETURN; nextScheduledCheckRef^ _ nextScheduledCheck; ThSmartsPrivate.QueueLarkAction[info.larkInfo, nextScheduledCheckRef]; }; QdNotification: ENTRY PROC [r: REF] ~ { ENABLE UNWIND => NULL; -- RestoreInvariant; cDesc: ConvDesc = NARROW[r]; { OPEN now: cDesc.situation.self; rNAtom: ATOM; rName: ROPE; val: ROPE; IF cDesc.info.failed THEN { Problem["Notification abandoned", cDesc.info]; RETURN; }; SELECT now.state FROM $notified => NULL; $initiating => { []_ChangeState[cDesc, $failed, $error, "Party remained in initiating state too long"]; RETURN; }; ENDCASE => RETURN; -- Nothing to do any more [rName, rNAtom, val] _ DBInfo[now.partyID, $deferanswer]; IF val.Equal["true", FALSE] THEN TRUSTED { Process.Detach[FORK DeferSignalling[cDesc]]; RETURN; }; IF HardwareInUse[cDesc] THEN []_ChangeState[cDesc, $idle, $busy] ELSE IF ChangeState[cDesc, IF DBInfo[now.partyID, $autoanswer, rNAtom].value.Equal["true", FALSE] THEN $active ELSE $ringing] = $success AND now.state=$ringing THEN CheckRing[cDesc]; }; }; QdIdle: ENTRY PROC [r: REF] ~ { ENABLE UNWIND => NULL; -- RestoreInvariant; cDesc: ConvDesc = NARROW[r]; [] _ ChangeState[cDesc, $idle, cDesc.situation.reason]; }; DeferSignalling: ENTRY PROC [cDesc: ConvDesc] ~ TRUSTED { OPEN now: cDesc.situation.self; c: CONDITION; attribute: ATOM _ IF cDesc.situation.self.state = $notified THEN $deferanswer ELSE $deferringback; Process.SetTimeout[@c, Process.SecondsToTicks[pd.timeoutNoAction*(IF attribute=$deferanswer THEN 1 ELSE 2)]]; WAIT c; SELECT cDesc.situation.self.state FROM $notified, $initiating => NULL; ENDCASE=>RETURN; NameDB.SetAttribute[DBInfo[now.partyID].rName, attribute, NIL]; cDesc.info.requests.QueueClientAction[QdNotification, cDesc]; }; DoAdvance: INTERNAL PROC[convRequest: ThSmartsPrivate.ConvRequest, secondTry: BOOL_FALSE] RETURNS [nb: NB] = { cDesc: ConvDesc = convRequest.cDesc; DO -- for possible stateMismatch loop OPEN now: cDesc.situation.self, desired: convRequest.desiredSituation.self; convEvent: Thrush.ConvEvent; IF now.state=$idle OR now.state = desired.state THEN RETURN[$success]; [nb, convEvent] _ ThParty.Advance[ credentials: now, state: desired.state, reason: convRequest.desiredSituation.reason, comment: convRequest.desiredSituation.comment, reportToAll: shouldReportToAll[desired.state], bilateral: convRequest.bilateral, checkConflict: requiresHardware[desired.state] ]; SELECT nb FROM $success => cDesc.info.NoteNewStateP[cDesc, convEvent]; $stateMismatch => { IF secondTry THEN ERROR; -- Don't loop forever cDesc.info.NoteNewStateP[cDesc, convEvent]; secondTry _ TRUE; IF now.state # $idle THEN LOOP; -- Attempt the transition, based on new information. }; $partyAlreadyActive, $conferenceConv, $voiceTerminalBusy, $bilateralConv, $voiceTerminalUnavailable, $interfaceError => { IF secondTry THEN ERROR; -- Don't loop forever IF nb=$interfaceError THEN Problem["Party state-advance yields interface error", cDesc.info]; IF convEvent#NIL THEN cDesc.info.NoteNewStateP[cDesc, convEvent]; convRequest.desiredSituation.reason _ $error; convRequest.desiredSituation.comment _ Atom.GetPName[nb]; desired.state _ $idle; IF now.state # $idle THEN [] _ DoAdvance[convRequest, TRUE]; }; ENDCASE => { -- $convIdle, $noSuchSmarts, $noSuchParty, $noSuchConv, $notInConv msg: Rope.ROPE = IO.PutFR["Unexpected party state-advance failure, nb=%g", atom[nb]]; IF cDesc#NIL AND cDesc.info#NIL AND cDesc.info.larkInfo#NIL THEN ThSmartsPrivate.Fail[cDesc.info.larkInfo, msg, TRUE] ELSE { Problem[msg, cDesc.info]; ForgetConv[cDesc]; }; }; EXIT; ENDLOOP; }; NoteNewState: PUBLIC INTERNAL PROC[cDesc: ConvDesc, convEvent: ConvEvent] ~ { OPEN now: cDesc.situation.self; nb: NB; larkStateData: LORA _NIL; state: StateInConv _ convEvent.self.state; previousState: StateInConv = cDesc.situation.self.state; cDesc.situation _ convEvent^; IF state = previousState THEN RETURN; -- No conceivable value in acting. SELECT state FROM $notified => { -- Somebody else did the notifying. We have to respond. cDesc.info.requests.QueueClientAction[QdNotification, cDesc]; }; $initiating => TRUSTED { -- Set timers Process.Detach[FORK DeferSignalling[cDesc]]; }; $ringing, $ringback, $reserved => { larkStateData _ LIST[SetupRingTunes[cDesc]]; IF larkStateData.first=NIL THEN larkStateData_NIL; }; $idle, $neverWas => ForgetConv[cDesc]; $failed => IF HardwareInUse[cDesc] THEN { cDesc.info.requests.QueueClientAction[QdIdle, cDesc]; -- Go away quietly. RETURN; }; $active => { value: ROPE; rNAtom: ATOM; larkStateData _ LIST[ComputeConnection[cDesc]]; [, rNAtom, value] _ DBInfo[now.partyID, $audiosource]; larkStateData _ CONS[NEW[Atom.DottedPairNode_[$audioSource, VoiceUtils.MakeAtom[value]]], larkStateData]; --[[$audioSource,audioSource],...] value _ DBInfo[now.partyID, $transmitonly, rNAtom].value; larkStateData _ CONS[NEW[Atom.DottedPairNode_[$transmitOnly, VoiceUtils.MakeAtom[value]]], larkStateData]; }; ENDCASE; IF state > Thrush.notReallyInConv AND cDesc.keyTable=NIL THEN { [nb, cDesc.keyTable] _ ThParty.GetKeyTable[credentials: now]; IF nb # $success THEN cDesc.keyTable_NIL; }; SetLarkState[cDesc, larkStateData]; }; CheckRing: PUBLIC INTERNAL PROC [cDesc: ConvDesc] ~ { nb: NB; cInfo: ThParty.ConversationInfo; [nb, cInfo] _ ThParty.GetConversationInfo[convID: cDesc.situation.self.convID]; IF nb # $success THEN { []_ChangeState[cDesc, $failed, $error, "System Error: can't monitor ringing situation"]; RETURN; }; IF cDesc.ringCheckProcess#NIL OR -- Already handling cInfo.numActive = 0 THEN RETURN; -- Not in the interesting situation yet. TRUSTED { Process.Detach[FORK RingProcess[cDesc]]; }; }; RingProcess: ENTRY PROC[cDesc: ConvDesc] = { ENABLE UNWIND => { cDesc.ringCheckProcess _ NIL; }; nb: NB; cInfo: ThParty.ConversationInfo; numWaited: INT_0; numToWait: INT_0; condition: CONDITION; TRUSTED { Process.SetTimeout[@condition, Process.SecondsToTicks[1]]; }; WHILE TRUE DO value: ROPE_NIL; valueAtom: ATOM; IF cDesc.info.failed OR cDesc.situation.self.state # $ringing THEN EXIT; [nb, cInfo] _ ThParty.GetConversationInfo[convID: cDesc.situation.self.convID]; IF nb#$success OR cInfo.bilateralConv AND cInfo.numActive>=2 THEN GOTO Idle; value _ DBInfo[cDesc.situation.self.partyID, $multiring].value; valueAtom _ VoiceUtils.MakeAtom[value]; SELECT TRUE FROM valueAtom=NIL, value.Length[]=0, valueAtom=$false => numToWait_numWaited; -- quit valueAtom=$true => { numToWait_numWaited+1; }; -- forever value.Fetch[0] IN ['0..'9] => numToWait_Convert.IntFromRope[value]; ENDCASE => numToWait _ numWaited; -- invalid database IF numWaited >= numToWait THEN GOTO Idle; WAIT condition; numWaited _ numWaited+1; REPEAT Idle => []_ChangeState[cDesc, $idle]; -- We're a third wheel. ENDLOOP; cDesc.ringCheckProcess _ NIL; }; ChangeState: PUBLIC INTERNAL PROC[ cDesc: ConvDesc, state: StateInConv _ $idle, reason: Thrush.Reason _ NIL, -- $wontSay comment: ROPE_NIL, bilateral: BOOL_FALSE ] RETURNS [nb: NB _ $noSuchConv] = { convRequest: ThSmartsPrivate.ConvRequest; IF cDesc = NIL THEN { Problem["No cDesc supplied at ChangeState", NIL]; RETURN; }; IF state=$failed THEN { IF DBInfo[cDesc.situation.self.partyID, $service].value.Length[]#0 THEN state _ $idle; SELECT cDesc.info.larkInfo.switchState FROM $onhook, $speaker, sPEAKER => { cInfo: REF ThParty.ConversationInfo _ GetConversationInfo[cDesc]; IF cInfo=NIL OR cInfo.originator#cDesc.situation.self.partyID THEN state _ $idle; }; ENDCASE; }; IF requiresHardware[state] AND HardwareInUse[cDesc] THEN { state _ $idle; reason _ $error; comment _ "Conflicting use of Lark requested"; }; convRequest _ NEW[ThSmartsPrivate.ConvRequestBody _ [cDesc: cDesc, bilateral: bilateral]]; convRequest.desiredSituation.self.state _ state; convRequest.desiredSituation.reason _ reason; convRequest.desiredSituation.comment _ comment; nb _ DoAdvance[convRequest]; }; GetConv: PUBLIC INTERNAL PROC[ info: SmartsInfo, credentials: Thrush.Credentials, createOK: BOOL_FALSE ] RETURNS [ cDesc: ConvDesc_NIL ] = --INLINE-- { nb: NB_$success; partyID: PartyID_credentials.partyID; convID: ConversationID _ credentials.convID; SELECT credentials.smartsID FROM nullID, info.smartsID => NULL; ENDCASE=>ERROR; FOR convs: OpenConversations _ info.conversations, convs.rest WHILE convs#NIL DO IF convs.first.situation.self.convID = convID THEN RETURN[convs.first]; ENDLOOP; IF ~createOK THEN { VoiceUtils.ProblemFR["Couldn't find referenced conversation, id= %g", $System, NIL, time[convID]]; RETURN; }; IF partyID=nullID THEN [nb, partyID] _ ThPartyPrivate.GetCurrentParty[smartsID: info.smartsID]; SELECT nb FROM $success => NULL; $noSuchSmarts, $noSuchParty => { VoiceUtils.ProblemFR["Party has apparently failed; damage control.", $System, NIL]; RETURN; }; ENDCASE => ERROR; cDesc _ NEW[ThSmartsPrivate.ConvDescBody_[]]; cDesc.situation.self _ [ convID: convID, smartsID: info.smartsID, partyID: partyID ]; cDesc.info _ info; info.conversations _ CONS[cDesc, info.conversations]; IF pd.doReports THEN ReportFR[" ** NewConv %t %g\n", info, time[convID], TU.RefAddr[info] ]; }; ForgetConv: PUBLIC INTERNAL PROC[cDesc: ConvDesc] = { info: SmartsInfo = cDesc.info; convs: OpenConversations _ info.conversations; IF convs=NIL THEN RETURN ELSE IF convs.first=cDesc THEN info.conversations _ convs.rest ELSE FOR cS: OpenConversations _ convs, cS.rest WHILE cS.rest#NIL DO IF cS.rest.first=cDesc THEN { cS.rest _ cS.rest.rest; EXIT; }; ENDLOOP; }; ComputeConnection: PUBLIC INTERNAL PROC[cDesc: ConvDesc] RETURNS [pInfo: ThParty.PartyInfo _ NIL] = { nb: NB; [nb, pInfo] _ ThParty.GetPartyInfo[credentials: cDesc.situation.self, nameReq: $none, allParties: TRUE]; IF nb # $success OR pInfo[0].partyID=0 THEN { Problem["No conversation info, or incomplete", cDesc.info]; pInfo_NIL; RETURN; }; -- This is really bad! }; GetSmartsInfo: PUBLIC PROC[smartsID: SmartsID] RETURNS [info: SmartsInfo_NIL] = { sd: REF _ RefID.Unseal[smartsID]; IF sd#NIL THEN RETURN[NARROW[Triples.Select[$SmartsData, sd, --info--]]]; }; DBInfo: PUBLIC PROC[partyID: Thrush.PartyID, attribute: ATOM_NIL, prevRNAtom: ATOM_NIL] RETURNS [rName: ROPE_NIL, rNAtom: ATOM_NIL, value: ROPE_NIL] = { nb: NB _ $success; rNAtom _ prevRNAtom; IF rNAtom#NIL THEN rName _ Atom.GetPName[rNAtom] ELSE IF partyID#nullID THEN [nb, rName] _ ThParty.DescribeParty[partyID: partyID, nameReq: $current]; IF nb # $success OR rName=NIL THEN RETURN; IF rNAtom=NIL THEN rNAtom _ VoiceUtils.MakeAtom[rName: rName, case: FALSE]; IF attribute#NIL THEN value _ NameDB.GetAttribute[rName, attribute]; }; GetConversationInfo: INTERNAL PROC[cDesc: ConvDesc] RETURNS [cInfo: REF ThParty.ConversationInfo] = { nb: NB; cInfoRecord: ThParty.ConversationInfo; [nb, cInfoRecord] _ ThParty.GetConversationInfo[convID: cDesc.situation.self.convID]; SELECT nb FROM $success => NULL; $noSuchConv => { Problem["Conversation disappeared, can't get ConversationInfo", cDesc.info]; ForgetConv[cDesc]; RETURN[NIL]; }; ENDCASE => ERROR; RETURN[NEW[ThParty.ConversationInfo _ cInfoRecord]]; }; Problem: PROC[comment: ROPE, info: SmartsInfo, v1: IO.Value _ [null[]]] = { VoiceUtils.ProblemFR[Rope.Concat["LarkSmarts(%g): ", comment], $Smarts, info, TU.RefAddr[info], v1]; }; ReportFR: PROC[what: ROPE, info: SmartsInfo, a1, a2, a3: IO.Value_rope[NIL]] = { IF NOT pd.doReports THEN RETURN; VoiceUtils.ReportFR[what, $Lark, info.larkInfo, a1, a2, a3]; }; BeNice: PROC[r: REF, d: INT, info: SmartsInfo] = { IF NOT pd.doNice THEN RETURN; Nice.BeNice[r, d, $Lark, info.larkInfo]; }; ringTunes: RefTab.Ref_RefTab.Create[59]; ringTone: LarkPlay.ToneSpec _ NIL; subduedRingTone: LarkPlay.ToneSpec _ NIL; outsideRingTone: LarkPlay.ToneSpec _ NIL; outsideSubduedRingTone: LarkPlay.ToneSpec _ NIL; outsideRingTune: PUBLIC LarkPlay.ToneSpec _ NIL; outsideRingTuneRope: ROPE _ "@300;G%>G<%G%>G<%G%>G<%G%>G<%G%>*C"; ringTuneDelay: NAT _ 2400; -- ms. delay between tune starts. SetupRingTunes: PROC[cDesc: ConvDesc] RETURNS [toneSpec: LarkPlay.ToneSpec] = { OPEN self: cDesc.situation.self; partyID: Thrush.PartyID = self.partyID; rName: ROPE; originatingPartyID: Thrush.PartyID_nullID; otherRName: ROPE; otherTune: LarkPlay.ToneSpec_NIL; otherType: Thrush.PartyType_$service; -- default value: not telephone, individual, trunk defaultSpec: LarkPlay.ToneSpec_NIL; -- outside or inside ringing nb: NB; pInfo: ThParty.PartyInfo; divisor: NAT _ 1; rope: ROPE; now: BasicTime.GMT = BasicTime.Now[]; ringMode: ATOM; ringDo: ATOM; IF ThNet.pd.ringsInvalid THEN MakeDefaultRingTunes[]; [nb, pInfo] _ ThParty.GetPartyInfo[credentials: [convID: self.convID, partyID: partyID], allParties: self.state#$reserved, nameReq: $current]; IF nb#$success OR pInfo.numParties=0 THEN RETURN[ringTone]; -- not worth messing about rName _ pInfo[0].intendedName; -- self SELECT self.state FROM $reserved => { ringMode _ $r; rope _ NameDB.GetAttribute[rName, $dialtonetune]; ringDo _ IF rope=NIL THEN $false ELSE VoiceUtils.MakeAtom[rName: rope, case: FALSE]; IF ringDo=$false THEN RETURN[NIL] ELSE ringDo _ $tune; }; $ringing => originatingPartyID _ pInfo.conversationInfo.originator; $ringback => NULL; ENDCASE => RETURN[NIL]; IF self.state#$reserved THEN FOR i: NAT IN [1..pInfo.numParties) DO SELECT self.state FROM ringback => SELECT pInfo[i].state FROM ringing, notified => { toneSpec _ GetRingTune[pInfo[i].intendedName]; IF toneSpec=NIL THEN RETURN[ringTone]; toneSpec _ NEW[LarkPlay.ToneSpecRec _ toneSpec^]; toneSpec.volume _ ThNet.pd.tonesVolume+2; toneSpec.repeatIndefinitely _ TRUE; RETURN[toneSpec]; }; ENDCASE; ringing => IF pInfo[i].partyID = originatingPartyID THEN { otherType _ pInfo[i].type; otherRName _ pInfo[i].intendedName; EXIT; }; ENDCASE=>ERROR; ENDLOOP; SELECT self.state FROM $ringback => RETURN[ringTone]; -- Didn't find another party above. $ringing => { defaultSpec _ SELECT otherType FROM $trunk => outsideRingTone, ENDCASE => ringTone; rope _ NameDB.GetAttribute[rName, $ringmode]; ringMode _ IF rope=NIL THEN $r ELSE VoiceUtils.MakeAtom[rName: rope, case: FALSE]; rope _ NameDB.GetAttribute[rName, $dotune]; ringDo _ IF rope=NIL THEN $standard ELSE VoiceUtils.MakeAtom[rName: rope, case: FALSE]; }; ENDCASE; SELECT ringMode FROM -- $o, $s, $r $o => RETURN[NIL]; $s => RETURN[ IF otherType=$trunk THEN outsideSubduedRingTone ELSE subduedRingTone]; $r => NULL; -- largest case continues below ENDCASE => RETURN[defaultSpec]; -- unknown case; act vanilla SELECT ringDo FROM $false, $standard => RETURN[defaultSpec]; $both => { SELECT otherType FROM $individual, $telephone => { divisor _ 2; rope _ NameDB.GetAttribute[otherRName, $dotune]; ringDo _ IF rope=NIL THEN $standard ELSE VoiceUtils.MakeAtom[rName: rope, case: FALSE]; IF ringDo=$both THEN otherTune _ GetRingTune[otherRName]; }; $trunk => otherTune _ outsideRingTune; ENDCASE; }; $true, $tune => NULL; -- continue below ENDCASE => RETURN[defaultSpec]; toneSpec _ GetRingTune[rName]; IF toneSpec=NIL THEN RETURN[defaultSpec]; IF otherTune#NIL THEN toneSpec _ LarkPlay.MergeToneSpecs[toneSpec, otherTune, divisor, ringTuneDelay]; }; GetRingTune: PROC[name: ROPE] RETURNS [ringTune: LarkPlay.ToneSpec_NIL] = { nameAtom: ATOM; ringTuneRope: ROPE; IF name=NIL THEN RETURN; ringTuneRope _ NameDB.GetAttribute[name, $ringtune]; IF ringTuneRope=NIL THEN RETURN; nameAtom _ VoiceUtils.MakeAtom[name]; ringTune _ NARROW[ringTunes.Fetch[nameAtom].val]; IF ringTune#NIL AND ringTune.asRope=ringTuneRope THEN RETURN; ringTune _ LarkPlay.PlayString[music: ringTuneRope, file: FALSE, volume: ThNet.pd.defaultRingVolume]; []_ringTunes.Store[nameAtom, ringTune] }; MakeDefaultRingTunes: PROC = { ringTone _ NEW[LarkPlay.ToneSpecRec _ [ repeatIndefinitely: TRUE, volume: ThNet.pd.defaultRingVolume, tones: LIST[LIST[ [f1: 440, f2: 480, on: 2000, off: 4000], [f1: 440, f2: 480, on: 2000, off: 4000]]]]]; outsideRingTone _ NEW[LarkPlay.ToneSpecRec _ [ repeatIndefinitely: TRUE, volume: ThNet.pd.defaultRingVolume, tones: LIST[LIST[ [f1: 440, f2: 480, on: 500, off: 500], [f1: 440, f2: 480, on: 500, off: 4500]]]]]; subduedRingTone _ NEW[LarkPlay.ToneSpecRec _ [ repeatIndefinitely: FALSE, volume: ThNet.pd.defaultRingVolume+ThNet.pd.subduedVolumeInterval, tones: LIST[LIST[[f1: 440, f2: 480, on: 500, off: 0]]] ]]; outsideSubduedRingTone _ NEW[LarkPlay.ToneSpecRec _ [ repeatIndefinitely: FALSE, volume: ThNet.pd.defaultRingVolume+ThNet.pd.subduedVolumeInterval, tones: LIST[LIST[[f1: 440, f2: 480, on: 500, off: 500], [f1: 440, f2: 480, on: 500, off: 0]]] ]]; outsideRingTune _ LarkPlay.PlayString[outsideRingTuneRope, FALSE, ThNet.pd.defaultRingVolume]; ThNet.pd.ringsInvalid _ FALSE; }; larkStateForState: ARRAY StateInConv OF ThSmartsPrivate.LarkState _ [ silence, idle, errorTone, dialTone, silence, silence, silence, ringBack, ringing, silence, talking, silence ]; SetLarkState: INTERNAL PROC[cDesc: ConvDesc, larkStateData: LORA ] = { state: StateInConv _ cDesc.situation.self.state; larkState: ThSmartsPrivate.LarkState _ larkStateForState[state]; larkInfo: ThSmartsPrivate.LarkInfo _ cDesc.info.larkInfo; IF larkInfo.forwardedCall THEN RETURN; -- don't even heed data IF HardwareInUse[cDesc] THEN { IF requiresHardware[state] THEN Problem["Conflicting use of Lark at SetLarkState: ", cDesc.info]; RETURN; }; IF requiresHardware[state] AND cDesc.keyTable#cDesc.info.larkInfo.keyTable AND cDesc.keyTable#NIL THEN larkStateData _ CONS[cDesc.keyTable, larkStateData]; IF larkState # larkInfo.larkState THEN SELECT larkState FROM $talking => { spec: ThParty.PartyInfo; FOR dL: LORA _ larkStateData, dL.rest WHILE dL#NIL DO WITH dL.first SELECT FROM pI: ThParty.PartyInfo => spec _ pI; ENDCASE; ENDLOOP; IF spec=NIL THEN ERROR; IF spec.conversationInfo.numActive < 2 OR (spec.conversationInfo.bilateralConv AND spec[0].socket.host=spec[1].socket.host) THEN larkState _ larkInfo.larkState; -- don't change }; $errorTone => larkState _ SELECT cDesc.situation.reason FROM $busy, $notImportantEnough => $busyTone, $absent, $noCircuits, $noParticular, $notFound, $error => $errorTone, ENDCASE => $errorTone; ENDCASE; ThSmartsPrivate.EnterLarkState[cDesc.info.larkInfo, larkState, larkStateData]; }; ResetLarkState: INTERNAL PROC[cDesc: ConvDesc] = { SetLarkState[cDesc, LIST[ComputeConnection[cDesc]]]; }; HardwareRequired: PUBLIC PROC[cDesc: ConvDesc] RETURNS [required: BOOL] = { RETURN[IF cDesc=NIL THEN FALSE ELSE requiresHardware[cDesc.situation.self.state]]; }; HardwareInUse: PUBLIC PROC[cDesc: ConvDesc] RETURNS [inUse: BOOL] = { convID: ConversationID _ cDesc.situation.self.convID; info: SmartsInfo _ cDesc.info; IF info=NIL OR convID = nullConvID THEN RETURN[FALSE]; FOR convs: OpenConversations _ info.conversations, convs.rest WHILE convs#NIL DO IF (~convs.first.situation.self.convID = convID) AND requiresHardware[convs.first.situation.self.state] THEN RETURN[TRUE]; ENDLOOP; RETURN[FALSE]; }; requiresHardware: ARRAY StateInConv OF BOOL _ [ FALSE, FALSE,TRUE, TRUE, TRUE, FALSE, FALSE, TRUE, TRUE, FALSE, TRUE, FALSE ]; shouldReportToAll: ARRAY StateInConv OF BOOL _ [ FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, TRUE, TRUE ]; WhatNeedsDoing: TYPE = ATOM; -- { whatNeedsDoingIf: ARRAY StateInConv OF ARRAY StateInConv OF WhatNeedsDoing _ [ [ $imp, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt, $frgt], --neverWas [ $imp, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $ntiy ], -- idle [ $imp, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $noop, $ntiy ], -- failed [ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- reserved [ $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp, $imp ], -- parsing [ $imp, $idle, $noop, $invl, $invl, $invl, $xrep, $xrep, $rback,$actvd, $actvd,$actvd ], -- initiating [ $imp, $idlerg,$noop,$invl, $invl, $invl, $xrep, $noop, $noop, $ntiy, $noop, $ntiy ], -- notified [ $imp, $idle, $noop, $invl, $invl, $invl, $noop, $xrep, $noop, $ntiy, $actv, $ntiy ], -- ringback [ $imp, $idlerg,$noop,$invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $ckrg, $ntiy ], -- ringing [ $imp, $idle, $noop, $invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $ckrg, $ntiy ], -- canActivate [ $imp, $idle, $noop, $invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $reac, $deac ], -- active [ $imp, $idle, $invl, $invl, $invl, $invl, $xrep, $xrep, $noop, $ntiy, $ntiy, $ntiy ] -- inactive (current ^) ]; ViewCmd: Commander.CommandProc = TRUSTED { Nice.View[pd, "Lark PD"]; }; Commander.Register["VuLarkSmarts", ViewCmd, "Program Management variables for Lark Smarts"]; }. 5όLarkSmartsSupImpl.mesa Copyright Σ 1985, 1986, 1987 by Xerox Corporation. All rights reserved. Last modified by D. Swinehart, July 19, 1987 11:09:19 pm PDT Polle Zellweger (PTZ) August 27, 1985 9:07:50 pm PDT Definitions Party-invoked actions Some party has changed state in a conversation we know about. Three cases: Another smarts initiated a state change for this party, and we're not receptive. (For now, when we don't know about that conversation and are already in another) Another smarts initiated a change for this party, and we're receptive. (New conversation and we're idle or it's a conversation we're in already) Another party changed state in a conversation we know about. Someone else's state changed in a conv. we're interested in; see if it means anything to us! We don't expect reports of state changes like $notified and $ringback to be reported to us unless it's us. We were told something about a conversation we didn't particularly care to know about. Forget it. We don't know a valid interpretation of this transition on someone else's part when we're in the state we're in. Somebody just quit. If everybody else has quit, quit too. We're ringing. Somebody just quit. If it was the originator, quit too. A poacher has substituted for a poachee, or the other way around. Update state so that if it's us, we know who we are. No conversation state has changed, so just update our notion of our state and quit. Assume problem analyzed elsewhere If the handset and speaker switches are in their "off" positions, terminate the connection when all recording or synthesis activity has completed. N.B. There's a faint possibility that the $recording service and the $synthesizer service will generate non-disjoint ID's. This could cause the connection to terminate prematurely. Queue up a request to change Lark serverTimeout values, if they have changed, then ping. Queued procedures  via request or party->smarts report All have prefix "Qd" (queued request) The only ones left are those that avoid recursive calls on ThParty.Advance and on NoteNewState. Even those could probably be eliminated with a bit more thought. Like maybe Loop with the resulting ConvEvent.  We have been invited to join a conversation, and we were not otherwise occupied. Decide how to deal with it. r is the updated cDesc The only question is whether to ring or to accept right away. Now test whether Manager (workstation program) has assumed the responsibility for determining the incoming calls. This works even if what we're planning to do is to reject due to multiple simultaneous conversations! Manager can thus override the prohibition. Only current use: transition from $failed to $idle when signalling $failed is inadvisable due to conflicting hardware use. We were told not to automatically accept this call or to issue ringback signals, but no one has done anything about it for some time; $notified and $initiating are supposed to be transitory states. Cancel the restraining order and let the call ring through or whatever. Whoever issued the restraining order will have to issue another one. Action procedures: change state, queue up interval request, and so on. All have prefix "Do" We and some other smarts acting on our behalf requested something at the same time. The other one got there first. Try the philosophy that the last one in wins. Unless the previous transition was to the $idle state, now attempt to attain the one requested here. (Not sure about active => ringing, but let's see) A return of stateMismatch from this routine indicates a failure based on the party having become idle during the attempt, or having achieved two stateMismatches in a row. The transition didn't occur. These things can happen. We're postulating that the proper response is always to attempt to reach an idle state, before returning the original error code. Caller must take any additional cleanup action, but usually none is required. We requested a state later than the current state. This is fatal. Try to get out cleanly. This isn't expected to change anything, but it is current information. Return the original error code. Callers should attempt minimal activity after one of these major errors. It would probably be acceptable to crash on these errors, since they should have been reported to us under normal Lark failure conditions. DoAdvance.nb~$success -- questionable, but most errors have been dealt with Record a change in our own state; if another action is required, queue it, since the caller of this routine may have to take some actions on the basis of this state transition before the state changes again. cDesc hasn't been updated with convEvent, yet, so comparisons of previous and current state are possible here if desirable. This is the only time a change in our own state has to be reacted to. Still queued up because otherwise the state-management is confusing. Or maybe it isn't. Earliest opportunity to get keys Change Lark state if appropriate for this conversation. Call is in progress, but we haven't joined it yet. Decide how long to stay around. It won't work anyway. Connection Management Utilities The $failed state is intended for Larks serving individuals who are in some sense known to be present. The idea is to enter a variant of the $idle state that makes noise. We do it only if the phone doesn't represent a service, and only if either the handset is offhook or our party is the originating party. That's a heuristic, so it will have to be monitored. Don't send services into failed state. ChangeState.nb~$ a variety of nb-errors. On return, all errors have been handled as well as possible, though; callers should just do minimum additional work when errors have occurred. Just remove its cDesc from info.conversations. Socket assignment: Each transmitting host gets an assigned socket (ThParty makes them) and ThParty.GetPartyInfo returns a list of them (self first). One transmits voice to a socket comprised of the other host's address combined with one's OWN socket number. In a conference, the other host is the multicast host. One listens to some number of sockets, comprised of the other host's (or multicast) address, and their own socket numbers. This makes the multicast case consistent. A ConnectionSpec, when sent to the lark, includes: buffer: {in1, in2, out1, out2, out3}. A buffer to which analog-to-ethernet (ini, corresponding to txi) or ethernet-to-analog (outj, corresponding to rxj) samples for this connection will be queued. When running the standard program O3I1 (1 in, 3 out) only in1 can be used, and any ethernet-to-analog connection can be assigned to any of the outj buffers. This is the conference mode. When running O2I2 (2 in, 2 out), in1 and out1 are paired, as are in2 and out2; out3 is not used. This is the "simultaneous back door and front door call" mode. localSocket: for ini (txi) buffers, this is ignored by the Lark Pup code. For outj (rxj) buffers, this specifies the socket on which to listen -- see above. remoteSocket: ignored for outj (rxj) buffers. For in1 (tx1) buffers, specifies the socket to send to. This call now simply gets the PartyInfo from ThParty; LarkOutImpl produces the actual connection specs and sends them on to the lark. Other Utilities Ring Tune Stuff  but for GFI's, would move to another file. ringback: choose original called party's ring tune for ringback tune. Or if more than one party is being called, use the first one that's still ringing or notified. ringing: find the originator's party, in case we're going to play a duet. We are being called. Figure out how to respond. Obtain the ringing mode. We're ringing or dialtoning. Figure out how to do it. This produces a second ring tune, compatible with the callee's; they will be played together. Returns the ring tune, if any, associated with the party. If the caller wants to change anything, it must copy the results, since GetRingTune doesn't. State Transition Tables neverWas, idle, failed, reserved, parsing, initiating, pending, maybe, ringing, canActivate, active, inActive This should guarantee that the current key table for the conversation that "owns" the hardware is distributed whenever necessary. If connection is to the back door on the same machine, Trunk smarts has already set state to trunkTalking, or is about to. Don't change it. See the discussion of sockets  they'll be equal only when sender and receiver is same host. We're disallowing conferences in this case. TRUE iff our hardware is in use already by some other conversation. No need to include trunk use here, since the trunk/lark interlock will take care of that. Something's wrong, but we needn't be the bad news bearers Should we report this transition to everyone, or just to our own smartses? never idle error reserved parsing init notified ringback ringing canAc active inactive never idle error reserved parsing init notified ringback ringing canAc active inactive Just codes to dispatch on in Supervisor; explained there $noop, $idle, $idlerg, $actv, $rback, $reac, $deac, $ckrg, $frgt, -- cases explained in code $actvd, -- same as active, except no-op if database is controlling "ringback" $invl, -- considered an invalid request $xrep, -- we got a report we feel we shouldn't have got $ntiy, -- not implemented yet $imp -- this situation should not arise even in the face of invalid requests }; If we're in the state identified by the row, and someone else in the conversation reports a transition onto the state identified by the column, what should we do? never idle failed resrv pars init notif rback ring canAc activ inact -- _ (other) (Clip this table to view without these comments.) This situation arises when we've forgotten about the conversation that somebody else is still reporting on. The actions of other parties are not of interest to us yet, since they're not in this conv. Ditto. They can either enter ringing to indicate interest, or go active without ringing We don't expect to hear from others while we're deciding whether to play They have earlier expressed interest noopringing), and are now joining the fray The only thing that interests us here is everybody else quitting. Debugging nonsense Swinehart, May 15, 1985 11:05:22 am PDT Cedar 6.0, add Prose stuff as direct copy of Interval stuff. changes to: IntervalSpecs, ProseSpec, ProseSpecs, LarkState, LarkProgress, LarkSupervise, EnqueueProses Swinehart, May 22, 1985 12:15:11 pm PDT hotLine => autoAnswer changes to: LarkSupervise Swinehart, July 2, 1985 10:09:14 am PDT Fixing up trunkTalking stuff changes to: LarkSupervise, LarkStateForState Polle Zellweger (PTZ) July 11, 1985 6:15:01 pm PDT changes to: DIRECTORY, EnqueueProses, ReportProseDone, LarkSupervise Polle Zellweger (PTZ) July 16, 1985 9:42:51 pm PDT changes to: LarkProgress, LarkSupervise, ReportProseDone Polle Zellweger (PTZ) July 30, 1985 4:41:39 pm PDT Handle copying of newProses correctly - give Lark its own copy. changes to: LarkSupervise Swinehart, August 6, 1985 2:26:39 pm PDT Merge PTZ ProseSpec changes changes to: DIRECTORY, LarkProgress, LarkSupervise, EnqueueProses Polle Zellweger (PTZ) August 14, 1985 6:12:13 pm PDT Prose flushing, also fix undesired speakerphone hangup for intervals, proseSpecs. changes to: LarkProgress, LarkSupervise Polle Zellweger (PTZ) August 22, 1985 2:04:45 pm PDT changes to: LarkProgress -- comment out some optimizing queue flushing Swinehart, October 28, 1985 9:58:08 am PST Handle => ID, H => Reseal, Log => VoiceUtils changes to: DIRECTORY, LarkSmartsSupImpl, ConversationID, Reseal, IntervalSpec, NB, nullID, PartyID, SmartsID, LarkProgress, LarkSupervise, GetConv, GetSmartsInfo Swinehart, November 9, 1985 5:03:39 pm PST Major revision: MBQueue replaces supervisor process. More responsibility for own state transitions, socket calculations, key maintenance, ... changes to: LarkProgress, NoteProgress, LarkSupervise, GetConv, larkStateForState, transForStates, ConvEvent, NoteProgress, RejectCall, NoteNewState, LarkSupervise, whatNeedsDoingIf, pairedState, WhatNeedsDoing, ] Swinehart, June 1, 1986 7:33:23 pm PDT Test hypothesis that Smarts supervisory actions don't have to be queued, since progress reports are queued at the Party level. Will vastly simplify Smartses. Keep the MBQueue around for things that might take a while and that can be done out of sequence. In fact, can keep LarkSmartsImpl's use of the MBQueue around for a while. It's harmless, says here. Left in also to help with QdNotification, although that's considered eliminatable. changes to: LarkProgress, LarkSubstitution, QdLarkProgress, QdSubstitution, QdNotification, NoteNewState, LarkReportAction Swinehart, January 1, 1987 9:54:27 pm PST Still working on FD/BD conflict stuff. Pushing it back to Party level. changes to: DIRECTORY, LarkProgress, QdNotification, NoteNewState Swinehart, January 27, 1987 5:05:49 pm PST Updating error management (NB-handling) changes to: QdNotification, DoAdvance, ChangeState, GetConversationInfo, SetupRingTunes Swinehart, April 6, 1987 8:07:48 am PDT Cedar 7.0; accommodate NamesGV => NameDB changes to: DIRECTORY, LarkSmartsSupImpl, PD, DeferAnswer, DBInfo, SetupRingTunes, GetRingTune changes to: QdNotification, NoteNewState Swinehart, June 11, 1987 2:04:27 pm PDT GetConv now accepts credentials instead of convID, extracts partyID from credentials instead of guessing it, when credentials provides for that. changes to: LarkProgress, LarkSubstitution, LarkReportAction, GetConv, convEvent, actionReport, CheckConvID, CheckSmartsID, DIRECTORY Swinehart, June 21, 1987 9:10:30 pm PDT Add ThSmarts.CheckIn implementation, for keep-alive polling changes to: LarkReportAction, LarkCheckin Κ η˜šœ™IcodešœH™HJšœ<™œœ˜SKšœ˜——Kšœœ˜-K˜—K˜K˜—šžœœœœ˜K™zKšœœœŸ˜.Kšœœ˜K˜7K˜K˜—šžœœœœ˜9KšœΤ™ΤKšœ˜Kšœ œ˜ šœ œ˜Kšœ(œœ˜P—šœ˜Kšœ+œœœ˜V—Kšœ˜Kšœœœœ˜WKšœ:œ˜?Kšœ=˜=K˜J˜——™FJ™J˜š ž œœœ6œœ˜YKšœœ˜K˜$KšœŸ"˜%KšœG˜KK˜Kšœœœœ ˜Fšœ"˜"Kšœ˜Kšœ˜Kšœ,˜,Kšœ.˜.Kšœ.˜.K˜!K˜.Kšœ˜—šœ˜K˜7˜K™sK™ΖKšœ œœŸ˜.K˜+Kšœ œ˜KšœœœŸ4˜TK™ͺK˜—Kšœ9˜9šœ*˜*Kšœˆ™ˆ—šœ˜K™[Kšœ œœŸ˜.KšœœC˜]šœ œœ,˜AK™F—K˜-K˜9Kšœ˜Kšœœœ˜š œœ(œ œ˜DJšœœœ˜>Jšœ˜—J˜J˜—šžœœœ˜8Jšœœ˜,J™β™2Jš œOΟdœ’œ’œ’œΑ’œΙ™€Jš œ’œ’œ9’œ’œE™Jšœ’œ’œC™f—J™…Jšœœ˜šœ ˜ JšœTœ˜Z—šœœœ˜-JšœBœœŸ˜h—J˜—J™—™J˜š ž œœœœœ˜QJšœœ˜!Jš œœœœœ!Ÿœ˜IJšœ˜J˜—šžœœœ%œœœœ˜WJš œ œœ œ œœ˜@Jšœœ ˜Jšœ˜Jšœœœ˜0šœœ˜JšœI˜I—Jš œœœœœ˜*Jšœœœ2œ˜KJšœ œœ/˜DJšœ˜J˜—šžœœœ˜3Jšœ œ˜1Jšœœ˜J˜&JšœU˜Ušœ˜Kšœ œ˜šœ˜KšœL˜LJšœ˜Jšœœ˜ J˜—Kšœœ˜—Kšœœ*˜4K˜—J˜šžœ˜Jšœ œ0˜BJšœNœ˜dJ˜J˜—šžœœœ œ˜PJšœœœœ˜ Jšœ<˜šœœ˜šœ˜JšœA˜A—Jšœ˜Jšœ˜—š œœ-œœ˜fKšœœ ˜4K™—šœ œœ ˜<šœ ˜ J™ŒJšœ˜šœœœœœœ œ˜PJšœ#˜#Jšœœ˜—Jšœœœœ˜šœ%˜)Jšœ%œ*˜VJ™ŠJšœ Ÿ˜/—J˜—šœœ˜¦€ §˜iJš€¦€>¦€¦€§ ˜dJš€¦€M§˜qJ˜J˜™šœ!œ˜*J˜Jšœ˜—Jšœ\˜\—J˜™'K™