Project - SAM TTS Announcer

A Network Text-To-Speech Voice Announcer which allows any networked UDP nodes to send text messages for speaking by this SAM TTS node.

A small and neat self-contained 'dual-core' device can be made using a Wemos dual base adapter, allowing two D1 Mini's to sit side by side.
You will need to cut the interconnecting tracks for TX, RX and Reset

The trick is to ensure both devices do not output on the same gpio pins, therefore those pins can remain interconnected without interfering with the other device.
Only the SAM Speaker firmware needs to use gpio2 for its speaker output, so the Annex gpio2 is ignored.

And SAM's gpio0 Busy output signal can conveniently be interconnected directly to the Annex gpio0 Busy interrupt input - so no wire is even needed.

We cannot prevent ESP8266 devices from outputting serial debug info at bootup.
So rather than feed SAM RX input from our Annex TX output (which would generate unwanted bootup noise) it is better to send to SAM RX using Annex serial2 gpio5.
This (purple) wire link is the only actual wiring mod needed on the dual base adapter, and allows us to send only those messages we wish to speak, when we want it to speak.
It also leaves the Annex hardware serial free to be used as a voice test input - whereby any text sent from a serial console connected to the Annex USB is received on the hardware serial port and passed straight out through software serial2 gpio5 to be read on SAM RX and spoken as audio output on SAM gpio2.

The script includes a circular FIFO (First In, First Out) array to ensure new incoming messages are queued to prevent loss of previous messages.
It uses gpio0 as a Busy signal from SAM so it only sends the next message from the queue when SAM signals it is no longer busy speaking.

The Annex Network Node module uses EasyNet to receive UDP text messages sent to it by other network nodes, which it then forwards over serial2 to be spoken by the SAM Speaker module. 
The EasyNet message format is: TargetNodeName  Instruction  optional data   (name and instruction are not case sensitive, data remains as-is).
This application uses nodename SAM  to recognise the instruction SAY followed by "the message string within guotes"
The node name is easily changed, and so can instructions names in the local instructionslist$ if their corresponding branch is similarly renamed.
In this case, SAM will also recognise the instruction  IP  which will cause it to branch to the IP subroutine to announce its local IP address.
It will also recognise and respond to the REPLY instruction, eg: ALL  REPLY  will cause EasyNet nodes to return a response message.
So it is not actually essential to address the node by its unique node name, because EasyNet nodes will also respond to the collective name of "All", or any of the names that are present in their Groupname" (which can contain multiple group names), or it will also respond to its local IP address.

To test the dual device for correct operation, use the Toolkit UDP console and send a message, eg:
(don't forget to ensure the UDP console is set to the correct subnet and port)

All Say "Hello world"   or   SAM SAY "Hi, I am Sam"
SAM SAY "Overheating"   or   All Say "Movement outside"

Any networked nodes can then send similar appropriate messages whenever their sensors are triggered by something which needs to be verbally announced.

'SAM Voice Announcer - Networked Text 2 Speech using EasyNet and Circular FIFO queue for SAM Speaker TTS firmware installed on a 2nd module nodename$  = "SAM"
groupname$ = "Voice\Announcer\Speakers\TTS\Speech"
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$
onudp udpRX
udpmsg$ = ""  'variable to hold incoming UDP message
instructionslist$ = ucase$("Reply Say IP ")  'List of Subdir branches available to action as remote instructions
instruction$ = ""
buffersize = 20                          'needs to be one more than the max message queue size
dim q$(buffersize)
qitem$ = ""                                 'queue data variable
qsize = 0                                    'size of queue
qfront = 0
qback = 0
serial.mode 115200                    'Hardware serial
serial2.mode 115200, 5, 4          'Software serial
led_pin = 13                                'Optional Busy/Ready status LED
pin.mode led_pin, output
ready = 0                                     'Ready state of Busy pin signal
busy_pin = 0                                'modules hardware Busy signal
pin.mode busy_pin, input, pullup
interrupt busy_pin, busy
onserial serialin
qitem$ = "SAM, Text 2 Speech Voice Announcer."
gosub say
'pause 3000
gosub IP
timer0 2000, dQ

gosub dQ

qitem$ = word$(ip$,1)
qitem$ = "This I P address is " + replace$(qitem$,"."," dot ")
gosub say

'if pin(busy_pin) = ready then
 qitem$ = ""
 if qsize > 0 then qpull
 if qitem$ <> "" then
  print2 qitem$

udpmsg$ =$
target$ = ucase$(word$(udpmsg$,1)) 'Target is always first word of message - may be NodeName or GroupName or "ALL" or localIP addr
if (target$=localIP$) OR (target$=ucase$(nodename$)) OR (target$=ucase$(groupname$)) OR (instr(ucase$(groupname$),target$)>0) OR (target$="ALL") then      '"Target Name or IP matched"
 instruction$ = trim$(ucase$(word$(udpmsg$,2)))   'Instruction is always second word of message
wlog udpmsg$
 if word.find(ucase$(instructionslist$),instruction$) > 0 then  'check if a recognised as valid on local instructionslist$
  qitem$ = word$(udpmsg$,2,|"|)
  gosub instruction$       'branch to action the corresponding instruction subroutine
  udp.reply udpmsg$ + " INSTRUCTION NOT RECOGNISED"
 endif 'word.find instruction$

print2 serial.input$

if pin(busy_pin) = ready then
 pin(led_pin) = 0
 gosub dQ
 pin(led_pin) = 1

sub qpush
if qsize + 1 >= buffersize then
 print2 "ERROR: Queue is full"
 q$(qback) = qitem$                          '  Push item to back of q   
 qback = (qback + 1) mod buffersize          '  Adjust the back pointer
qsize = (qback - qfront) mod buffersize
end sub

sub qpull
if qsize < 1 then
 print2 "ERROR: Queue is empty"
 qitem$ = q$(qfront)                         '  Pull item from front of q
 qfront = (qfront + 1) mod (buffersize)      '  Adjust the front pointer
qsize = (qback - qfront) mod buffersize
end sub

udp.reply "Reply from " + Nodename$

---------------- end -----------------

SAM translates English text into audio speech, plus it understands four punctuation marks: the hyphen (-), comma (,), period (.), and question mark (?).
The hyphen (-) serves to mark clause boundaries by inserting a short pause in the speech.
The comma marks phrase boundaries and inserts a longer pause approximately double that of the hyphen.
The period adds a pause and also causes the pitch to fall.
The question-mark also adds a pause, but it causes the pitch to rise.

You can sprinkle those punctuation marks liberally in your text to make it pronounce better and sound more to your liking.