Project - Voice Announcer

Project - Text 2 Speech Voice Announcer

A network TTS Voice Announcer which uses the same hardware as   Chapter 6 - Text 2 Speech Project   

UsageTargetname   SPEAK   any following text

Speaks any Alerts or Announcements sent to it by any other EasyNet nodes (or the UDP Console).

The hardware is identical to the standalone project, so follow those details, then simply load this EasyNet network version of the script afterwards.
Instead of a circular FIFO array, this version uses a text variable called voiceq$ as its FIFO queue, which is still hardware-controlled by busypin.
It works in a similar may to the EasyNet hand-shaking retryq$ which provides some robustness for the UDP broadcast messages.
The default nodename$ is 'Voice1' but can be changed to whatever you prefer - don't forget to configure in Config to connect to your router.
Assuming you have an amplified speaker connected, the Voice Announcer will speak its IP address at startup.
Any EasyNet nodes on the same subnet can send text-to-speech messages targeted for a Voice Announcer to speak (could be more than one).
Embed "SendQ VOICE1 SPEAK your text to be spoken here" into your appropriate nodes event trigger code to announce it at the target location.
Use "SendQ Announcers Speak global text announcement" to send the message to multiple announcers at multiple locations using groupname.
Enter 'voice1 speak sound316 [m52] alert, front door visitor' in Toolkits 'Message to Send' window to send manually from UDP Console.

Sound Notes:
Some punctuation such as " , . - ? " affects pronounciation, eg: an inserted comma can make a big difference, and also a full stop at the end.
Items enclosed in square brackets are interpreted as behaviour flags and not taken literally (if lower-case), ie:
  • [pmillis]  inserts a pause of the specified millis duration.
  • [mnumber]  selects the specified voice  (3=female1, 51=male1, 52=male2, 53=female2, 54=DonaldDuck, 55-MinnieMouse)
  • [snumber]  changes speed of speech  0 to 10  (0=slow, 10=fast)
  • [tnumber]  changes voice tone (pitch)  0 to 10  (0-low, 10=high)
  • [vnumber]  changes audio volume  0 to 10  (0=quiet, 10=loud)
Also not taken literally (even though not enclosed in square brackets) are 3 groups of sound effects, denoted by  "sound" + 3 digit number.
  sound101 to sound125 are "Sound effects",  sound201 to sound225 are "Ringtones", sound301 to sound330 are "Alarms"
All the items above should be entered lower-case, but they can all be inserted anywhere in the text to affect how it is sounds after that point.

The script stores the assigned default parameters in opt$ - these default parameter options are included with every message... they can be temporarily over-ridden per-message by parameters embedded within the message, but change the defaults in opt$ to make permanent changes.

Basic:
title$ = "EasyNet Voice Announcer, by Electroguard"
nodename$  = "Voice1"       'Assign a unique node name of your choice
groupname$ = "Speech\Announcers"  'concatenated group names are searched for a partial match
localIP$   = WORD$(IP$,1)
netIP$     = WORD$(localIP$,1,".") + "." + WORD$(localIP$,2,".") + "." + WORD$(localIP$,3,".") + "."
nodeIP$    = WORD$(localIP$,4,".")
udpport    = 5001                       'change to suit your own preference, but don't forget to do the same for all nodes
if nodename$ = "" then nodename$ = "Node" + nodeIP$   '
instructionslist$ = ucase$("Reply Rename Load Save Restart Relay1ON Relay1OFF Relay1Toggle Speak") 'local shared subdirs
filename$ = word$(BAS.FILENAME$,1,".") + ".ini" 'comment this and next line if you don't want to load/save settings to ini file
gosub Load                               'reads nodename$ from file if available
serial2.mode 115200,4,5            'Software serial2 TX & RX
RXmsg$ = ""                              'variable to hold incoming message
instruction$ = ""                          'variable to hold incoming instruction
data$ = ""                                   'variable to hold any incoming data after the instruction
retryq$ = ""                                 'variable to hold all unexpired messages still waiting to be acknowledged
qdelimiter$ = "|"                          'separates messages in the retryq
time2live = 1 * 60                       'sent-message unacknowledged lifetime in seconds
ID$ = ""                                       'unique msg ID consists of send date+time + time2live - also acts as msg 'expire' time flag
ledpin = 13: pin.mode ledpin, output: ledoff = 1: pin(ledpin) = ledoff
relay1pin = 12: pin.mode relay1pin, output: pin(relay1pin) = 0     'using active high qpio12 for relay1
buttonpin = 0: pin.mode buttonpin, input, pullup                           'using active low gpio0 button
interrupt buttonpin, pressed
start=0: stop=0                            'used by button-pressed subroutine to differentiate between short and long presses
busypin = 14                               'modules hardware Busy signal
ready = 0                                     'Ready state of Busy pin signal
pin.mode busypin, input, pullup
interrupt busypin, busy
onserial2 serial2in
voiceq$ = ""
qitem$ = ""
strt = 253                                     '&hFD start byte for TTL module
lenhi = 0                                       'hi byte of data length         
lenlo = 0                                       'lo byte of data length
synth = 1                                      'synthesise talk instruction byte
stat = 33                                       '&h21 status query byte
enc = 0                                         'encoder type byte
ctl$ = ""
opt$ = "[d][x1][t2][s6][m51][g2][h2][n1][y1][v7]"      'optional parameter defaults for adjusting speech and text interpretation
msg$ = "sound218 [p500] [m51][t2] Easy net network Voice Announcer, by Electro guard,[p1500]"
gosub speak
msg$ = word$(ip$,1)
msg$ = "[t5][p1000] [m3] This node name is called, " + nodename$ + ", and the I P address is [y1][i1][n1]" + replace$(msg$,"."," dot ")
gosub speak
timer0 2000, dQ                             'periodic timer to ensure no unspoken words remain in the queue
timer1 2 * 1000, Retry                    'periodic timer to keep resending unACKed msgs until they expire                        
udp.begin(udpport)
onudp udpRX
wlog "OK"
wait


Speak:              'adds new messages into the voice queue
L = len(opt$) + len(msg$) + 2
if L >4000 then
 msg$ = "[p400][m53] ATTENTION. WARNING, the message was ignored because it was too long."
endif
if data$ <> "" then msg$ = data$
gosub prepack
voiceq$ = voiceq$ + qitem$ + qdelimiter$
gosub dQ
return

prepack:             'pre-loads the necessary pre-amble and pre-set options to the message
msg$ = msg$ + "  "
L = len(opt$) + len(msg$) + 2
lenhi = L >> 8
lenlo = L and 255
ctl$ = str$(strt) + "," + str$(lenhi) + "," + str$(lenlo)+","+str$(synth)+","+str$(enc) + chr$(10)
'wlog "before prepack, opt$=" + opt$ + ", msg$=" + msg$
qitem$ = ctl$ + "opt=" + opt$ + chr$(10) + "msg=" + msg$ + chr$(10)
'wlog "after prepack, opt$=" + opt$ + ", msg$=" + msg$ + ",  qitem$=" + qitem$
return

dQ:                     'de-queue the next word and send to the speech chip if it is not already busy speaking
if pin(busypin) = ready then
 qitem$ = ""
 if word.count(voiceq$, qdelimiter$) > 0 then
  qitem$ = word$(voiceq$,1,qdelimiter$)               'grab first unACKed  msg in the queue
  voiceq$ = word.delete$(voiceq$,1,qdelimiter$)  'chop msg off front of queue
  if qitem$ <> "" then
   for pos = 1 to 5
    serial2.byte val(word$(qitem$,pos,","))
   next pos
   wlog "dQ, opt$=" + word.getparam$(qitem$,"opt") + ", msg$=" + word.getparam$(qitem$,"msg")
   print2 word.getparam$(qitem$,"opt")
   print2 word.getparam$(qitem$,"msg")
  endif  'qitem <>
 endif  'wordcount
endif  'busypin
return

serial2in:            'grabs any responses from the speech chip and shows them on wlog console
temp$ = serial2.input$
wlog "Response=" + hex$(asc(temp$))
return

busy:                   'speech chip busy/ready interrupt
if pin(busypin) = ready then
 pin(ledpin) = 0
 gosub dQ
else
 pin(ledpin) = 1
endif
return

udpRX:
RXmsg$ = udp.read$
if ucase$(word$(RXmsg$,1)) = "ACK" then
 gosub ACK   'echoed reply from successfully received message, original msg can be removed from queue  
else
 target$ = ucase$(word$(RXmsg$,1))          'Target may be NodeName or GroupName or "ALL" or localIP address
 if (target$=localIP$) OR (target$=ucase$(nodename$)) OR (instr(ucase$(groupname$),target$)>0) OR (target$="ALL") then
  instruction$ = trim$(ucase$(word$(RXmsg$,2)))  'Instruction is second word of message
  data$ = "": getdata data$,RXmsg$," ",2      'extract any data that follows the instruction
  if word.find(ucase$(instructionslist$),instruction$) > 0 then
   if (ucase$(instruction$) <> "ACK") and (instr(ucase$(data$),"ID=") > 0) then
    udp.reply "ACK " + RXmsg$                      'ACKnowledge the incoming msg
   endif
   gosub instruction$                                       'branch to action the corresponding instruction subroutine
  else
   udp.reply RXmsg$ + " INSTRUCTION NOT RECOGNISED"
  endif  'word.find
 endif   '(target$=localIP$)
endif 'ACK
return

ACK:
msg$ = "": getdata msg$, RXmsg$, " ", 1
wlog "Ack recvd for " + msg$
pos = word.find(retryq$,msg$,qdelimiter$)
if pos > 0 then retryq$ = word.delete$(retryq$,pos,qdelimiter$)
return

RETRY:
if word.count(retryq$, qdelimiter$) > 0 then
if retryq$ <> "" then wlog "queue=" + retryq$
 msg$ = word$(retryq$,1,qdelimiter$)               'grab first unACKed  msg in the queue
 retryq$ = word.delete$(retryq$,1,qdelimiter$)  'chop msg off front of queue
 expire$ = ""
 WordParse expire$, msg$, "ID=", " "                'parse out ID= expire time
 if msg$ <> "" then                                             'compare expire time to current unix time
  if dateunix(date$) + timeunix(time$) > val(expire$) then
   Send "LOG ERROR: Node " + Nodename$ + " FAILED SEND - " + msg$ + " not ACKnowledged"
  else
   retryq$ = retryq$ + msg$ + qdelimiter$
   udp.write netip$ + "255", udpport, msg$
   wlog "retry " + msg$
  endif
 endif
endif
return

RENAME:
if data$ <> "" then nodename$ = data$
return

LOAD: 'Load settings from file, by default is only looking for nodename, add your own saved parameters to look for if wished
a$ = ""
if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$)
if WORD.GETPARAM$(a$,"nodename$") <> "" then nodename$ = WORD.GETPARAM$(a$,"nodename$")
return

SAVE:  'Save settings to file, by default will only save nodename, add your own parameters to save if wished
a$ = ""
if FILE.EXISTS(filename$) > 0 then a$ = FILE.READ$(filename$)
WORD.SETPARAM  a$, "nodename$", nodename$
FILE.SAVE filename$, a$
return

RESTART:
reboot 'causes a device restart
return

sub SendQ(sendmsg$)      
sendmsg$ = sendmsg$ + " ID=" + str$(dateunix(date$) + timeunix(time$) + time2live, "%10d", 1)
retryq$ = retryq$ + sendmsg$ + qdelimiter$
udp.write netip$ + "255", udpport, sendmsg$
end sub

sub Send(sendmsg$)
udp.write netip$ + "255", udpport, sendmsg$
end sub

sub GetData(ret$, v$, sep$, pos)  'extracts everything from the msg after the Instruction and puts into data$ (thanks cicciocb)
local i, p, q
p = 1
for i = 1  to pos
 p = instr(p + 1, v$, sep$)
 if p > 0 then p = p + len(sep$)
next i
if p = 0 then
 ret$ = ""
else
 q = instr(p+1, v$, sep$)
 if q = 0 then q = 999
 ret$ = mid$(v$, p)
end if  
end sub

sub WordParse(ret$, full$, search$, sep$)  'extracts value from option=value (thanks cicciocb)
local p, b$
p = instr(full$, search$)
if p <> 0 then
 b$ = mid$(full$, p + len(search$))
 ret$ = word$(b$, 1, sep$)
else
 ret$ = ""
end if
end sub

REPLY:
udp.reply "Reply from " + Nodename$ + " Ramfree=" + str$(ramfree) + " Flashfree=" + str$(flashfree)
return

Relay1ON:
pin(relay1pin) = 1
return

Relay1OFF:
pin(relay1pin) = 0
return

Relay1Toggle:
if pin(relay1pin) = 1 then pin(relay1pin) = 0 else pin(relay1pin) = 1
return

PRESSED:
if pin(buttonpin) = 0 then start = millis else stop = millis
if stop > start then
 if stop - start < 2000 then
  sendq "All relay1toggle"       'short press
 else  
  send "ALL Reply"            'long press
 endif
endif   
return

END   '-------------------- End ---------------------