venerdì 30 novembre 2018

Testare Lambda NodeJS in locale per Alexa Skills

Per testare una Lambda function per Alexa Skills non e' strettamente necessario utilizzare Amazon AWS con l'editor on line

per esempio si parte dallo scaricare il progetto Basic Starter da SkillTemplates (ask new --template --url https://skilltemplates.com/templates.json)

Si crea quindi nella root del progetto (dove e' contenuta la directory Lambda per capirsi) una directory test e si inseriscono i seguenti tre files

test.js
-------------------------------------------------------------------
var lambda = require('../lambda/custom/index.js');
var context = require('./context.js');
var mockEvent = require('./event.json');

var mockContext = new context();

function callback(error, data) {
  if(error) {
      console.log('error: ' + error);
  } else {
      console.log(data);
  }
}

lambda.handler(mockEvent, mockContext, callback);
-------------------------------------------------------------------


context.js
-------------------------------------------------------------------
module.exports = function() {
    return {
        succeed: function(result) {
            console.log(JSON.stringify(result, null,'\t') );
        },
        fail: function(err) {
            console.log(JSON.stringify(err, null,'\t') );
        },
        done: function(err, result) {
            //console.log("CONTEXT DONE:", err, result);
        },
        functionName: 'local_functionName',
        awsRequestId: 'local_awsRequestId',
        logGroupName: 'local_logGroupName',
        logStreamName: 'local_logStreamName',
        clientContext: 'local_clientContext',
        identity: {
            cognitoIdentityId: 'local_cognitoIdentityId'
        }
    };
};
-------------------------------------------------------------------

event.json
-------------------------------------------------------------------
{
  "session": {
    "new": false,
    "sessionId": "mock_sessionId",
    "application": {
      "applicationId": "mock_applicationId"
    },
    "attributes": {},
    "user": {
      "userId": "mock_userId"
    }
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "mock_requestId",
    "locale": "en-US",
    "timestamp": "2017-09-19T11:46:23Z"
  },
  "context": {
    "AudioPlayer": {
      "playerActivity": "IDLE"
    },
    "System": {
      "application": {
        "applicationId": "mock_applicationId"
      },
      "user": {
        "userId": "mock_userId"
      },
      "device": {
        "supportedInterfaces": {}
      }
    }
  },
  "version": "1.0"
}
-------------------------------------------------------------------

se si lancia il file test.js con

node.js

si ha come riposta
-------------------------------------------------------------------

{ version: '1.0',
  response:
   { outputSpeech:
      { type: 'SSML',
        ssml: '<speak>Hello there. What is your name?</speak>' },
     reprompt: { outputSpeech: [Object] },
     shouldEndSession: false,
     card:
      { type: 'Simple',
        title: 'Example Card Title',
        content: 'Example card body content.' } },
  userAgent: 'ask-node/2.0.5 Node/v10.4.1',
  sessionAttributes: {} }
-------------------------------------------------------------------

questo e' il file che normalmente Lambda passa a Alexa per poi essere trasformato in messaggio vocale ed spedito al dispositivo Alexa
In pratica viene generata l'intent LaunchRequest (event.json) della funzione Lambda che corrisponde a 

-------------------------------------------------------------------
const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  handle(handlerInput) {
    const speechText = 'Hello there. What is your name?';
    const repromptText = 'Can you tell me your name?';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(repromptText)
      .withSimpleCard('Example Card Title', "Example card body content.")
      .getResponse();
  },
};
-------------------------------------------------------------------

giovedì 29 novembre 2018

Vi yank and paste

Piccolo promemoria ...visto che me lo scordo sempre

Nell'editor vi per selezionare e copiare testo si deve, in modalita' comando, premere v (comparira' la scritta -- VISUAL --  in basso) e si evidenzia il testo da copiare con i tasti freccia

A questo punto si puo' copiare (yank) con il tasto y o cancellare con d
Per incollare il testo precedentemente copiato usa p (paste)

Alexa Ask Cli ed Address API

Per installare l'interfaccia di programmazione di Alexa a linea di comando (senza utilizzare gli editor Web online di Alexa ed Aws Lambda) si puo' usare Ask Cli che si installa tramite npm con

npm install -g ask-cli

verra' al termine richiesto di loggardi con la propria user e password su Amazon tramite un finestra di autenticazione web

Nel caso in cui ask risponda con un messaggio di errore del tipo


InvalidClientTokenId: The security token included in the request is invalid.

vuol dire che l'interfaccia Ask Cli non riesce ad auntenticarsi sui server Amazon.
La soluzione e' riprocedere con il comando 
ask init

(attenzione se non gia' configurato a questo punto verra' chiesto di inserire le proprie credenziali.
Su alcuni SO la procedura funziona senza problemi...in altri casi si deve usare ask init --no-browser a causa di un errore di Unhandled promise rejection)

A questo punto si inizia con l'esempio 

ask new --template --url https://skilltemplates.com/templates.json

(altrimenti  se si crea una skill ex novo
ask new --skill-name test --lambda-name test
)

Dal menu si sceglie Device Location API Starter


si entra quindi nella nuova directory del template e si inserisce

ask deploy

ogni volta che e' stato modificato il codice in locale per effettuare l'upload della skill su Amazon si deve effettuare il deploy. Si puo' effettuare anche un deploy parziale della sola funzione Lambda con 

ask lambda upload

Le Address API richiedono una autorizzazione esplicita da parte dell'utente per l'utilizzo. Si deve quindi andare su https://alexa.amazon.it cliccare Le tue Skill, Skill attivate e cercare device-location-api-starter attivare la Skill con l'autorizzazione


a questo punto si e' presentato un problema. La skill era priva di endpoint perche' non e' stata creata la corrispondente funziona Lambda. Ho quindi proceduto a creare una Lambda copiandoci poi sopra il file index.js che ho trovato nel template

----------------------------------------------
/* eslint-disable  func-names */
/* eslint-disable  no-console */

const Alexa = require('ask-sdk-core');

const APP_NAME = "Location Starter"
const messages = {
  NOTIFY_MISSING_PERMISSIONS: 'Please enable device location permissions in the Amazon Alexa app.',
  NO_ADDRESS: 'It looks like you don\'t have an address set. You can set your address from the companion app.',
  ERROR: 'Uh Oh. Looks like something went wrong.'
};
const DEVICE_LOCATION_PERMISSION = 'read::alexa:device:all:address';

const LaunchRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
  },
  handle(handlerInput) {
    const speechText = `Hello, welcome to ${APP_NAME}. You can ask about device address by saying what is my location`;
    const repromptText = 'What would you like to know?';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(repromptText)
      .withSimpleCard(APP_NAME, speechText)
      .getResponse();
  },
};

const DeviceLocationIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'DeviceLocationIntent';
  },
  async handle(handlerInput) {
    const { requestEnvelope, serviceClientFactory, responseBuilder } = handlerInput;
    try {
      const { deviceId } = requestEnvelope.context.System.device;
      const deviceAddressServiceClient = serviceClientFactory.getDeviceAddressServiceClient();
      const address = await deviceAddressServiceClient.getFullAddress(deviceId);
      let response;
      if (address == undefined || (address.addressLine1 === null && address.stateOrRegion === null)) {
        response = responseBuilder.speak(messages.NO_ADDRESS).getResponse();
        return response;
      } else {
        const completeAddress = `${address.addressLine1}, ${address.stateOrRegion}, ${address.postalCode}`;
        const response = `Your complete address is, ${completeAddress}`;
        return handlerInput.responseBuilder
            .speak(response)
            .withSimpleCard(APP_NAME, response)
            .getResponse();
      }
    } catch (error) {
      console.log(JSON.stringify(error));
      if (error.statusCode == 403) {
        return responseBuilder
        .speak(messages.NOTIFY_MISSING_PERMISSIONS)
        .withAskForPermissionsConsentCard([DEVICE_LOCATION_PERMISSION])
        .getResponse();
      }
      console.log(JSON.stringify(error));
      const response = responseBuilder.speak(messages.ERROR).getResponse();
      return response;
    }
  },
};


const HelpIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
  },
  handle(handlerInput) {
    const speechText = 'You can ask for the address';

    return handlerInput.responseBuilder
      .speak(speechText)
      .reprompt(speechText)
      .getResponse();
  },
};

const CancelAndStopIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
        || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
  },
  handle(handlerInput) {
    const speechText = 'Goodbye!';

    return handlerInput.responseBuilder
      .speak(speechText)
      .getResponse();
  },
};

const SessionEndedRequestHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
  },
  handle(handlerInput) {
    console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`);

    return handlerInput.responseBuilder.getResponse();
  },
};

const ErrorHandler = {
  canHandle() {
    return true;
  },
  handle(handlerInput, error) {
    console.log(`Error handled: ${error.message}`);

    return handlerInput.responseBuilder
      .speak('Sorry, I can\'t understand the command. Please say again.')
      .reprompt('Sorry, I can\'t understand the command. Please say again.')
      .getResponse();
  },
};


const skillBuilder = Alexa.SkillBuilders.custom();

exports.handler = skillBuilder
  .addRequestHandlers(
    LaunchRequestHandler,
    DeviceLocationIntentHandler,
    HelpIntentHandler,
    CancelAndStopIntentHandler,
    SessionEndedRequestHandler
  )
  .addErrorHandlers(ErrorHandler)
  .withApiClient(new Alexa.DefaultApiClient())
  .lambda();
 ----------------------------------------------

questo e' il file skill.json. Evidenziato in giallo la richiesta dei permessi
 ----------------------------------------------
{
  "manifest": {
    "publishingInformation": {
      "locales": {
        "en-US": {
          "summary": "Sample Short Description",
          "examplePhrases": [
            "Alexa open location starter",
            "Alexa ask location starter about my adrees"
          ],
          "keywords": [
            "keyword1",
            "keyword2",
            "keyword3"
          ],
          "name": "device-location-api-starter",
          "description": "Sample Full Description",
          "smallIconUri": "https://s3.amazonaws.com/cdn.dabblelab.com/img/skill-template-default-sm.png",
          "largeIconUri": "https://s3.amazonaws.com/cdn.dabblelab.com/img/skill-template-default-lg.png"
        }
      },
      "isAvailableWorldwide": true,
      "testingInstructions": "No special instructions.",
      "category": "EDUCATION_AND_REFERENCE",
      "distributionCountries": []
    },
    "apis": {
      "custom": {
        "endpoint": {
          "sourceDir": "lambda/custom"
        }
      }
    },
    "manifestVersion": "1.0",
    "permissions": [
      {
        "name": "alexa::devices:all:address:full:read"
      }
    ],
    "privacyAndCompliance": {
      "allowsPurchases": false,
      "locales": {
        "en-US": {
          "termsOfUseUrl": "http://dabblelab.com/terms",
          "privacyPolicyUrl": "http://dabblelab.com/privacy"
        }
      },
      "isExportCompliant": true,
      "containsAds": false,
      "isChildDirected": false,
      "usesPersonalInfo": false
    }
  }
}
 ----------------------------------------------





Di default non viene assunto l'indirizzo del dispositivo con quello configurato nel profilo Amazon. Si deve settare in modo esplicito la posizione nella app sul telefono Alexa Amazon per ogni dispositivo



venerdì 23 novembre 2018

Problemi di Alexa

un po' di problemi con Alexa...alcuni esempi




"funzionale" viene interpretato come "funziona le"
"cfr" (pronunciato ciefferre) viene interpretato come "c'è fede"
"centro" alcune volte viene interpretato come "cento"
"sesto" viene intepretato come "6o"


questi problemi sono debuggabili solo attraverso la Alexa Developer Console tramite il file JSON che traduce il parlato nel file interpretato

giovedì 22 novembre 2018

Bash script come cgi in Apache

Cosa succede quando scrivi uno script bash che funziona bene e non vuoi convertirlo in Php? Semplice...ritorni indietro di venti anni e lo fai diventare un cgi script

Per prima cosa si devono abilitare gli script CGI in Apache

a2enmod cgi

service apache2 restart

in Debian la directory di default per i CGI e' in /usr/lib/cgi-bin...si copia quindi lo script e lo si rende eseguibile
Per prima cosa c'e' da dire che lo script non deve rispondere in modo arbitrario (per esempio con un semplice testo) ma con un file html altrimenti verranno generati errori del tipo 500 di output non correttamente formattato

In Bash si possono leggere anche le variabile GET passate sulla URL e cio' mediante IFS
Nell'esempio sottostante e' prevista una sola variabile (che corrisponde ad un id numerico) e che e' stata salvata nell'array parm all'indice 1
----------------------------------------------------
#!/bin/bash

echo "Content-type: text/html"
echo ""

echo '<html>'
echo '<head>'
echo '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">'
PATH="/bin:/usr/bin:/usr/opt/bin"
export $PATH

saveIFS=$IFS
IFS='=&'
parm=($QUERY_STRING)
IFS=$saveIFS


stringa="$(xidel --silent --extract "//img/@src" "http://xxxxxxxxxxxx.it/?utf8=true&size=micro&mode=double&idc=${parm[1]}" | grep idraulico | awk 'FNR==1')"


echo '</head>'
echo '<body>'
#echo ${parm[1]}
file="$(basename $stringa)"

echo $file

echo '</body>'
echo '</html>'

exit 0
----------------------------------------------------

Xidel

Xidel e' un comodo programma a linea di comando che permette di estrarre informazioni da un file HTML. Non esiste nei repository di Debian ma il pacchetto .deb puo' essere comodo scaricato da questo link http://www.videlibri.de/xidel.html

Nell'esempio sottostante viene richiamata una pagina html da cui estrarre un link (che varia in modo non predefinito) in modo automatico

in pratica xidel richiama il file dall'indirizzo xxxxx.wp, estrae tutti gli HRef incontrati, si estraggono solo quelli che hanno la stringa "filename", poi con awk si prende solo il primo della lista e con sed si eliminano dei caratteri (in questo caso ..)


#!/bin/bash
cd "$(dirname "$0")";

stringa="$(xidel --silent --extract "//a/@href" xxxxxxxxxxxx.wp | grep filename | awk 'FNR==1' | sed 's/..//')"
echo "${stringa}"

martedì 20 novembre 2018

Estrarre dati da un XML a linea di comando

Per estrarre dati in modo semplice da un XML una soluzione puo' essere xml_grep
Questo comando e' incluso nel pacchetto Debian xml-twig-tools

si procede quindi con

apt-get install xml-twig-tools

supponiamo di avere un XML tipo

---------------
<?xml version='1.0' encoding='UTF-8'?>
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
  <identifier>XXXXXX</identifier>
  <sender>XXXXXX</sender>
  <sent>2018-11-14T15:24:00+01:00</sent>
  <status>Actual</status>
  <msgType>Alert</msgType>
  <scope>Public</scope>
  <note>XXXXXXX</note>
  <info>
    <category>Met</category>
    <event>XXXXX</event>
............
---------------
e che si voglia estrarre il contenuto di <note>
si puo' usare la semplice sintassi

xml_grep 'note' test.xml --text_only

lunedì 19 novembre 2018

Scheletro di progetto NodeJS

I comandi di base per creare un progetto NodeJS

mkdir test
cd test
npm init --yes
npm install request --save (viene installato via npm il modulo "request" nella subdir /node_modules)

si crea quindi il file del programma (per esempio app.js) e si lo si mette in esecuzione con 
node app.js

HTTP Async request in Alexa

Per fare interagire Alexa con server esterni la cosa piu' semplice e' usare il modulo http perche' e' un built-in e non necessita di supporto esterno. Si dichiara l'uso all'inizio della Lambda function

--------------------------------------------------------------------
const Alexa = require('ask-sdk');
var http = require('http'); 
--------------------------------------------------------------------

si crea poi una funzione di richiesta http. Attenzione: questa funzione e' di tipo asincrono e si deve inserire la Promise per fare in modo che il risultato entri nello scope della funzione principale
--------------------------------------------------------------------
function httpGet() {
  return new Promise(((resolve, reject) => {
    var options = {
        host: '80.211.xxxx,xxxx',  //solo ip o nome dominio ..niente http od https prima
        port: 80,
        path: '/dpc.php',
        method: 'GET',
    };
    
    const request = http.request(options, (response) => {
      response.setEncoding('utf8');
      let returnData = '';

      response.on('data', (chunk) => {
        returnData += chunk;
      });

      response.on('end', () => {
        resolve(returnData);
        console.log("returnData "+returnData);
      });

      response.on('error', (error) => {
        reject(error);
      });
    });
    request.end();
  }));
}
--------------------------------------------------------------------

Il custom intent che deve gestire la funzione asincrona sopra indicata deve essere modificato ponendo async nell'handler e deve aspettare il ritorno della funzione httpGet prima di procedere in modo da popolare in modo corretto la variabile response
--------------------------------------------------------------------
const geo = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (request.type === 'IntentRequest'
        && request.intent.name === 'geo');
  },
  
  
  async handle(handlerInput) {
  const response = await httpGet();

  const { intent } = handlerInput.requestEnvelope.request;
  const activity = intent.slots.luogo.value;
  var speechOutput = response;


  if (activity == "firenze")
      {
        //speechOutput = "firenze si trova in toscana";
      }
  
  if (activity == "milano")
      {
        speechOutput = "milano si trova in lombardia"
      }
  

    return handlerInput.responseBuilder
      .speak(speechOutput)
      .getResponse();
  },
};
--------------------------------------------------------------------

mercoledì 14 novembre 2018

Alexa Skills



Per usare Alexa Skill in Europa si deve usare la zona AWS denominata Ireland

Per creare un custom intent in lambda NodeJS che legga le variabili passate da Alexa ASK si procede cosi

il modello prevede un custom intent di nome geo che ha uno slot  (ovvero una variabile) di nome luogo
---------------------------------------------------------------------------
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "geografia",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": [
                        "aiuto "
                    ]
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "geo",
                    "slots": [
                        {
                            "name": "luogo",
                            "type": "AMAZON.City"
                        }
                    ],
                    "samples": [
                        "dimmi dove si trova {luogo}",
                        "dimmi dove è {luogo}"
                    ]
                }
            ],
            "types": []
        }
    }
}
---------------------------------------------------------------------------

il custom intent deve intercettare una richiesta di tipo IntentRequest e di nome geo
le righe in azzurro servono a leggere la variabile luogo che e' passata da ASK (via JSON) e la mettono nella variabile JS ac
---------------------------------------------------------------------------
const geo = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (request.type === 'IntentRequest'
        && request.intent.name === 'geo');
  },
  handle(handlerInput) {
    

  const { intent } = handlerInput.requestEnvelope.request;
  const ac = intent.slots.luogo.value;
  var speechOutput = "  ";
  
  if (ac == "firenze")
      {
        speechOutput = "firenze si trova in toscana"
      }
  
  if (ac == "milano")
      {
        speechOutput = "milano si trova in lombardia"
      }
  

  console.log(ac);

    //const speechOutput = activity;

    return handlerInput.responseBuilder
      .speak(speechOutput)
      .getResponse();
  },
};
---------------------------------------------------------------------------

i messaggi di console.log si leggono si Amazon CloudWatch Logs.Dalla finestra della funzione di Lambda in alto ci sono i tab Configurazione e Monitoraggio..si clicca Monitoraggio e poi visualizza log in CloudWatch



Per fare interagire Alexa con informazioni derivanti da un server esterno mediante richieste http
(lo script test.php e' semplicemente un echo("stringa");

---------------------------------------------------------------------------

var http = require('http'); 

http.get('http://80.211.xxx,xxx/test.php', (resp) => {
  let data = '';

  resp.on('data', (chunk) => {
    data += chunk;
  });

  // The whole response has been received. Print out the result.
  resp.on('end', () => {
    //console.log(JSON.parse(data).explanation);
    console.log(data);
  });

}).on("error", (err) => {
  console.log("Error: " + err.message);
});
---------------------------------------------------------------------------

martedì 13 novembre 2018

OpenWRT su GL.Inet MT300N-V2

Rispetto al GL.Inet AR150  montare OpenWRT su  MT300N-V2 e' leggermente differente


Si parte impostantdo il portatile su 192.168.1.2. Si inserisce il cavo Ethernet su  LAN del M300, si preme in tasto reset e si connette l'alimentazione USB. Dopo 3 secondi (o quando il led rosso non lampeggia piu') si lascia il tasto reset e ci si connette al 192.168.1.1 e si effettua l'upload del firmware openwrt-18.06.0-ramips-mt76x8-gl-mt300n-v2-squashfs-sysupgrade.bin

A questo punto c'e' la differenza con AR150. Non viene attivata di default l'access point per cui si deve continuare la configurazione mantenendo collegato il cavo Ethernet sulla porta LAN e dopo il reboot si deve usare LuCi all'indirizzo 192.168.1.1