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.

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                        
onudp udpRX
wlog "OK"

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."
if data$ <> "" then msg$ = data$
gosub prepack
voiceq$ = voiceq$ + qitem$ + qdelimiter$
gosub dQ

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$

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

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

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

RXmsg$ =$
if ucase$(word$(RXmsg$,1)) = "ACK" then
 gosub ACK   'echoed reply from successfully received message, original msg can be removed from queue  
 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
   gosub instruction$                                       'branch to action the corresponding instruction subroutine
  endif  'word.find
 endif   '(target$=localIP$)
endif '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$)

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"
   retryq$ = retryq$ + msg$ + qdelimiter$
   udp.write netip$ + "255", udpport, msg$
   wlog "retry " + msg$

if data$ <> "" then nodename$ = data$

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$")

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$

reboot 'causes a device restart

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$ = ""
 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$)
 ret$ = ""
end if
end sub

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

pin(relay1pin) = 1

pin(relay1pin) = 0

if pin(relay1pin) = 1 then pin(relay1pin) = 0 else pin(relay1pin) = 1

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

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