M5Stackをプリントサーバーにする

# はじめに

[前回](https://qiita.com/fukuebiz/items/cfad81fac76888abd27a) プリンターに接続できることが確認できたので、プリントサーバーとして動作させてみました。下記のように、印刷したいファイルのURLをトピックに入れて発行したら、M5Stackが受け取ってファイルを印刷するイメージです。

![m5stack_print_overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555397/0ae0d12e-6c44-e273-1b2a-7010878c3885.png)

# 検証

課題がいくつかありました。本当はAmazon FreeRTOSを使いたかったけど、まだAmazon FreeRTOSがBLEのClient部分がまだ実装されていないとか、M5StackのBLEがメモリをたくさん使うといったところです。次のページを参考にさせて頂きました。

 - [BLE環境センサー・ゲートウェイ(ESP32編)](https://ambidata.io/samples/m5stack/ble_gw/)
 - [ESP32 DeveloperでAWS IoTを動かそうとしてハマったメモ](https://www.1ft-seabass.jp/memo/2017/07/31/esp32-developer-aws-iot-tips/)
 - [BLEセンサデータをM5stackで取得しGASに投げる](https://qiita.com/notitle420/items/68ea5dda32fba427670b) 


テストコードはこちら。

```cpp:main.cpp
/**
 * Test print for BlueTooth Printer
 */
#include <M5Stack.h>
#include <BLEDevice.h>

#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#include "BLERemoteDescriptor.h"

#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>


#define PRINTER_NAME "BlueTooth Printer" // Printer BD Name
static esp_bd_addr_t peer_bd_addr = {0x00,0x11,0x22,0x33,0x44,0x55}; // Printer BD Adress

static String printer_service_uuid = "";
static String printer_characteristic_uuid = "";
BLEClient* pClient;
std::map<std::string, BLERemoteService*> *pRemoteServices;
BLERemoteService *printerService;
std::map<std::string, BLERemoteCharacteristic*> *pRemoteCharacteristics;
BLERemoteCharacteristic *printerCharacteristic;
std::map<std::string, BLERemoteDescriptor*> *pRemoteDescriptors;
BLERemoteDescriptor *printerDescriptor;

volatile boolean deviceBleConnected = false; // flag BLE conneted



char *ssid = "<SSID>";
char *password = "<パスワード>";
  
WiFiClient client;
WiFiClientSecure httpsClient;
PubSubClient mqttClient(httpsClient);

const char *endpoint = "<エンドポイント>"; // Example: xxxxxxxxxxxxxx.iot.ap-northeast-1.amazonaws.com
const int port = 8883;
String pubTopic = "/sample123/complete";
String subTopic = "/sample123";

#define THING_NAME "ESP32_printer"

long messageSentAt = 0;
int dummyValue = 0;
char pubMessage[128];
  
const char* rootCA = "-----BEGIN CERTIFICATE-----\n" \
"...\n" \
"-----END CERTIFICATE-----\n";
  
const char* certificate = "-----BEGIN CERTIFICATE-----\n" \
"...\n" \
"-----END CERTIFICATE-----\n";
  
const char* privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" \
"...\n" \
"-----END RSA PRIVATE KEY-----\n";


/*
 * Connect To Wi-Fi
 */
void connectWiFi(void)
{
    // Start WiFi
    Serial.print("Connecting to Wi-Fi: ");
    Serial.print(ssid);
    WiFi.begin(ssid, password);
  
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nConnected.");
    delay(500);
}

/*
 * Disconnect from Wi-Fi
 */
void disconnectWiFi()
{
    WiFi.disconnect();
}

void mqttCallback (char* topic, byte* payload, unsigned int length);

/*
 * Connect To MQTT
 */
void connectMQTT()
{
    // Configure MQTT Client
    httpsClient.setCACert(rootCA);
    httpsClient.setCertificate(certificate);
    httpsClient.setPrivateKey(privateKey);
    mqttClient.setServer(endpoint, port);
    mqttClient.setCallback(mqttCallback);
}

/*
 * Disconnect from MQTT
 */
void disconnectMQTT()
{
    mqttClient.disconnect();
}

/*
 * Connect to AWS IoT
 */
void connectAWSIoT()
{
    if (WiFi.status() != WL_CONNECTED)
    {
        connectWiFi();
    }

    Serial.println("Connecting to MQTT");
    while (!mqttClient.connected())
    {
        if (mqttClient.connect(THING_NAME))
        {
            Serial.println("Connected.");
            int qos = 0;
            mqttClient.subscribe((subTopic + "/#").c_str(), qos);
            Serial.println("Subscribed.");
        }
        else
        {
            Serial.print("Failed. Error state=");
            Serial.println(mqttClient.state());
            // Wait 5 seconds before retrying
            delay(5000);
            // re-connect
            disconnectMQTT();
            connectMQTT();
        }
    }
}

/*
 * Client callback
 */
class PrinterClientCallbacks : public BLEClientCallbacks
{
    void onConnect(BLEClient *pclient)
    {
        deviceBleConnected = true; // set ble connected flag
        Serial.printf("%s connected\n", PRINTER_NAME);
    }

    void onDisconnect(BLEClient *pclient)
    {
        pclient->disconnect();
        deviceBleConnected = false; // clear ble connected flag
        Serial.printf("%s disconnected\n", PRINTER_NAME);
    }
};

/*
 * Notify Callback (but seems not called)
 */
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic,
    uint8_t *pData, size_t length, bool isNotify)
{
    Serial.printf("notifyCallbacklength=%d, isNofity=%d\n", length, isNotify);
    Serial.printf("uuid: %s\n", pBLERemoteCharacteristic->getUUID().toString().c_str());
}

/*
 * Connect to printer
 */
bool connectToServer(BLEAddress pAddress)
{
    Serial.printf("Create a connection to addr: %s\n", pAddress.toString().c_str());
    BLEDevice::init("M5Stack");
    pClient = BLEDevice::createClient();

    Serial.printf(" – Client created\n");
    pClient->setClientCallbacks(new PrinterClientCallbacks());

    Serial.printf(" – Connecting to server…\n");
    if (pClient->connect(pAddress)) // connect to the remove BLE Server.
    {
        Serial.printf(" - Connected to printer\n");
    }
    else
    {
        Serial.printf(" - Connection failed\n");
        return false;
    }

    Serial.printf("Retrieving service UUIDs...\n");
    pRemoteServices = pClient->getServices();
    for (const auto &p : *pRemoteServices)
    {
        String uuid = p.first.c_str();
        Serial.printf("UUID: %s\n", uuid.c_str());
        if (!uuid.endsWith("-0000-1000-8000-00805f9b34fb"))
        {
            printer_service_uuid = uuid;
        }
    }
    if (printer_service_uuid == "")
    {
        Serial.printf(" – Printer service not found\n");
        return false;
    }

    printerService = pRemoteServices->at(printer_service_uuid.c_str());
    if (printerService == nullptr)
    {
        Serial.printf(" – Service not found (UUID: %s)\n", printer_service_uuid.c_str());
        return false;
    }
    else
    {
        Serial.printf(" – Service found (UUID: %s)\n", printer_service_uuid.c_str());
    }

    // printer characteristic
    Serial.printf("Retrieving printer characteristics...");
    pRemoteCharacteristics = printerService->getCharacteristics();
    for (const auto &p : *pRemoteCharacteristics)
    {
        String uuid = p.first.c_str();
        Serial.printf("UUID: %s\n", uuid.c_str());
    }
    if (pRemoteCharacteristics->size() > 0)
    {
        printerCharacteristic = printerService->getCharacteristic(pRemoteCharacteristics->begin()->first.c_str());
        if (printerCharacteristic == nullptr)
        {
            Serial.printf(" – Notify characteristic not found (UUID: %s)\n", pRemoteCharacteristics->begin()->first.c_str());
            return false;
        }
        else
        {
            Serial.printf(" – Notify characteristic found (UUID: %s)\n", pRemoteCharacteristics->begin()->first.c_str());
        }
    }
    else
    {
        Serial.printf(" – Notify characteristic not found\n");
        return false;
    }
    printerCharacteristic->registerForNotify(notifyCallback, true); //register callback

  return true;
}

/*
 * Scan device & log
 */
void scan()
{
    Serial.printf("UUID, Name, RSSI, Manufacturer\n");
//    Serial.printf("UUID, Name, RSSI\n");

    // disconnect Wi-Fi
    disconnectMQTT();
    disconnectWiFi();

    // connect to BLE
    BLEDevice::init("M5Stack");

    BLEScan* pBLEScan = BLEDevice::getScan();
    pBLEScan->setActiveScan(false);
    BLEScanResults foundDevices = pBLEScan->start(3);
    int count = foundDevices.getCount();
    for (int i = 0; i < count; i++)
    {
        BLEAdvertisedDevice d = foundDevices.getDevice(i);
        Serial.printf(d.getAddress().toString().c_str());
        Serial.printf(", ");
        Serial.printf(d.getName().c_str());
        Serial.printf(", ");
        Serial.printf("%d", d.getRSSI());
        Serial.printf(", ");
        if (d.haveManufacturerData())
        {
            std::string data = d.getManufacturerData();
            int manu = data[1] << 8 | data[0];
            Serial.printf("%d", manu);
        }
        Serial.printf("\n");

    }

    pBLEScan->clearResults();
    // disconnect BLE
    BLEDevice::deinit(true);
    deviceBleConnected = false;

    ESP.restart();
}

void testCharacter()
{
    printerCharacteristic->writeValue(0x1b); // NORMAL TEXT
    printerCharacteristic->writeValue(0x21);
    printerCharacteristic->writeValue(0x00);
    printerCharacteristic->writeValue("[Font A]\n");
    printerCharacteristic->writeValue("!\"#$%&\'()*+,-./01234567890:;<=>?\n");
    printerCharacteristic->writeValue("@ABCDEFGHIJKLMNOPQRSTUVWXYZ\n");
}

void testJapanese()
{
    printerCharacteristic->writeValue(0x1b); // NORMAL TEXT
    printerCharacteristic->writeValue(0x21);
    printerCharacteristic->writeValue(0x00);
    printerCharacteristic->writeValue("[Japanese Hiragana]\n");
    printerCharacteristic->writeValue({'\xA4', '\xA2', '\xA4', '\xA4', '\xA4', '\xA6', '\xA4', '\xA8', '\xA4', '\xAA', '\x0a', '\0'}); // あいうえお
    printerCharacteristic->writeValue({'\xA3', '\xB1', '\xA3', '\xB2', '\xA3', '\xB3', '\xA3', '\xB4', '\xA3', '\xB5'});
    printerCharacteristic->writeValue({'\xA3', '\xB6', '\xA3', '\xB7', '\xA3', '\xB8', '\xA3', '\xB9', '\xA3', '\xB0', '\x0a', '\0'}); // １２３４５６７８９０
}

/*
 * Test print
 */
void testPrint()
{
    // disconnect Wi-Fi
    disconnectMQTT();
    disconnectWiFi();

    // connect to BLE
    BLEDevice::init("M5Stack");

    for (int i = 0; i < 3; i++)
    {
        if (deviceBleConnected)
        {
            testCharacter();
            testJapanese();
            break;
        }
        connectToServer(peer_bd_addr);
        sleep(3);
    }

    // disconnect BLE
    client.flush();
    pRemoteCharacteristics->clear();
    pRemoteServices->clear();
    pClient->disconnect();
//    pClient->setClientCallbacks(NULL);
    BLEDevice::deinit(true);
    deviceBleConnected = false;

    ESP.restart();
}

// Utility to extract header value from headers
String getHeaderValue(String header, String headerName)
{
    return header.substring(strlen(headerName.c_str()));
}

/*
 * Print from S3
 */
void printFromS3(String& url)
{
    String host = "";
    int s3port = 80;
    String bin = "";

    Serial.println("printFromS3: " + url);
    long contentLength = 0;
    bool isValidContentType = false;

    if (url.startsWith("http://"))
    {
        host = url.substring(7);
    }
    else if (url.startsWith("https://")) // not supported yet
    {
        host = url.substring(8);
        s3port = 443;
    }
    else
    {
        host = url;
    }
    if (url.indexOf("/") > -1)
    {
        bin = host.substring(host.indexOf("/"));
        host = host.substring(0, host.indexOf("/"));
    }

    Serial.println("Connecting to: " + String(host));
    // Connect to S3
    if (client.connect(host.c_str(), s3port))
    {
        Serial.println("Fetching Bin: " + String(bin));

        client.print(String("GET ") + bin + " HTTP/1.1\r\n" +
                    "Host: " + host + "\r\n" +
                    "Cache-Control: no-cache\r\n" +
                    "Connection: close\r\n\r\n");

        unsigned long timeout = millis();
        while (client.available() == 0)
        {
            if (millis() - timeout > 5000)
            {
                Serial.println("Client Timeout !");
                client.stop();
                return;
            }
        }

        while (client.available())
        {
            String line = client.readStringUntil('\n');
            line.trim();

            if (!line.length())
            {
                //headers ended
                break;
            }

            if (line.startsWith("HTTP/1.1"))
            {
                if (line.indexOf("200") < 0)
                {
                    Serial.println("Got a non 200 status code from server. Exiting OTA Update.");
                    break;
                }
            }

            if (line.startsWith("Content-Length: "))
            {
                contentLength = atol((getHeaderValue(line, "Content-Length: ")).c_str());
                Serial.println("Got " + String(contentLength) + " bytes from server");
            }

            if (line.startsWith("Content-Type: "))
            {
                String contentType = getHeaderValue(line, "Content-Type: ");
                Serial.println("Got " + contentType + " payload.");
                if (contentType == "application/octet-stream" || contentType == "text/plain")
                {
                    isValidContentType = true;
                }
            }
        }
    } else {
        // Connect to S3 failed
        Serial.println("Connection to " + String(host) + " failed. Please check your setup");
    }

    // get data
    if (contentLength && isValidContentType)
    {
        Serial.println("Start printing");
        // get print data
        String text = client.readString();
        // diconnect Wi-Fi
        client.flush();
        disconnectWiFi();
        // connect to BLE
        for (int i = 0; i < 3; i++)
        {
            if (deviceBleConnected)
            {
                break;
            }
            connectToServer(peer_bd_addr);
            sleep(3);
        }
        // print datas
        printerCharacteristic->writeValue(0x1b); // NORMAL TEXT
        printerCharacteristic->writeValue(0x21);
        printerCharacteristic->writeValue(0x00);
        for (int i = 0; i < text.length(); i++)
        {
            printerCharacteristic->writeValue((uint8_t *)text.substring(i, i + 1).c_str(), 1, true);
        }
        printerCharacteristic->writeValue(0x1b); // NORMAL TEXT
        printerCharacteristic->writeValue(0x21);
        printerCharacteristic->writeValue(0x00);
    }

    // disconnect BLE
    client.flush();
    pRemoteCharacteristics->clear();
    pRemoteServices->clear();
    pClient->disconnect();
    BLEDevice::deinit(true);
    deviceBleConnected = false;
    Serial.println("Finished printing");

    // publish print result
    connectAWSIoT();
    mqttClient.publish(pubTopic.c_str(), "{}");

    ESP.restart();
}

/*
 * print data url
 */
/*
String getUrl(char* topic, byte* payload, unsigned int length)
{
    StaticJsonDocument<200> doc;
    DeserializationError error = deserializeJson(doc, payload);

    if (error)
    {
        Serial.print(F("deserializeJson() failed: "));
        Serial.println(error.c_str());
        const char* err = "http://xxx";
        return err;
    }

    String url = doc["url"];

    return url;
}
*/

/*
 * MQTT Callback (Receive)
 */
void mqttCallback (char* topic, byte* payload, unsigned int length)
{
    Serial.print("Received. topic=");
    Serial.println(topic);
    // payload
//    for (int i = 0; i < length; i++)
//    {
//        Serial.print((char)payload[i]);
//    }
//    Serial.print("\n");

    // get url from payload
//    String url = getUrl(topic, payload, length);
    // get url from topic
    String url = String(topic);
    if (url.startsWith(subTopic))
    {
        url = url.substring(subTopic.length() + 1);
    }
    printFromS3(url);

}
  
/*
 * Setup
 */
void setup()
{
    Serial.begin(115200);
    M5.begin();
//    dacWrite(25, 0); // Speaker OFF
    M5.Lcd.setTextFont(2);
    M5.Lcd.println("Please wait...");

    connectWiFi();

    connectAWSIoT();

    M5.Lcd.clearDisplay(0);
    M5.Lcd.setTextFont(2);
    M5.Lcd.println("BLE print test.");
    M5.Lcd.println();
    M5.Lcd.println("A button: scan BLE & log.");
    M5.Lcd.println("B button: test print.");

//    BLEDevice::init("M5Stack");
}

/*
 * main loop
 */
void loop()
{
    if (!mqttClient.connected())
    {
        connectAWSIoT();
    }
    mqttClient.loop();

    if(M5.BtnA.wasPressed())
    {
        scan();
    }
    if(M5.BtnB.wasPressed())
    {
        testPrint();
    }
    M5.update();
}

```

# まとめ

課題はありますが、うまく動くようになれば、こんなことにも使えそうです。

![m5stack_print_usecase1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555397/e486f3d1-510e-d57d-5768-459a98715f29.png)