A network TTS Voice Announcer which uses the same hardware as Chapter 6 - Text 2 Speech Project
Usage: Targetname 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:
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 --------------------- |