Asynchronous processing with Arduino (Asynchronous processing of processing requests from Linux)

I wrote [Correct the display of the 16x8 LED matrix] in another article. Using that LED Matrix, if you send a message display request from Linux, we will create an Arduino that scrolls the message and returns a response. The point of this article is "Asynchronous (strictly speaking, pseudo-asynchronous) request reception and message display on Arduino".

What to use.


Linux <-> arduino USB serial connection check

Kernel Message when Arduino NANO is connected to Linux
usb 1-12: new full-speed USB device number 3 using xhci_hcd
usb 1-12: New USB device found, idVendor=0403, idProduct=6001
usb 1-12: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-12: Product: FT232R USB UART
usb 1-12: Manufacturer: FTDI
usb 1-12: SerialNumber: ALxxxxxx
usbcore: registered new interface driver usbserial_generic
usbserial: USB Serial support registered for generic
usbcore: registered new interface driver ftdi_sio
usbserial: USB Serial support registered for FTDI USB Serial Device
ftdi_sio 1-12:1.0: FTDI USB Serial Device converter detected
usb 1-12: Detected FT232RL
usb 1-12: FTDI USB Serial Device converter now attached to ttyUSB0
Kernel Message when Arduino NANO Every is connected to Linux
usb 1-6: new full-speed USB device number 4 using xhci_hcd
usb 1-6: New USB device found, idVendor=2341, idProduct=0058
usb 1-6: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-6: Product: Arduino Nano Every
usb 1-6: Manufacturer: Arduino LLC
usb 1-6: SerialNumber: FAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
cdc_acm 1-6:1.0: ttyACM0: USB ACM device
usbcore: registered new interface driver cdc_acm
cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters

serial_test.ino


#include <Wire.h>
 
void setup()
{
  Wire.begin();
  Serial.begin(115200);
  Serial.println("serial test");
}

void loop()
{
  String str;  
  if(Serial.available()>0) {
    str = Serial.readString();
    Serial.println("[" + str + "]");
  }
}

screen&nbsp;Operation check with


$ screen /dev/ttyUSB0 115200
serial test
hogehoge
[hogehoge]

Try scrolling the text

When you pass a request from Linux to an arduino, it will return a response as the content is executed. For the time being, implement the following functions as a starting point. Data is passed in JSON.

function request response
Scroll display of text {"textscr":"hello world !"} {"status":200, "msg":"..."}
Arduino health check {"status":{}} {"status":200, "msg":"..."}

Below is an excerpt of the main Arduino code

test.ino


int sendResponse(int status, String msg) {
  StaticJsonDocument<100> response;
  
  response["status"] = status;
  response["msg"] = msg;

  serializeJson(response, Serial);
  Serial.println();
}

int ExecCmd(String cmd, JsonVariant value) {
  if(cmd == "status") {
    sendResponse(100, "alive");
   
  } else if(cmd == "textscr") {
    JsonObject obj2 = value.as<JsonObject>();
    
    String msg = obj2["msg"];
    int size = obj2["size"];

    if(size < 1 || size > 2) {
      size = 1;
    }
    matrix.setTextSize(size);
    matrix.setTextWrap(false);
    matrix.setTextColor(LED_ON);

    int l = msg.length();
    for(int16_t x = 7; x >= -6*l; x--) {
      matrix.clear();
      matrix.setCursor(x,0);
      matrix.print(msg);
      matrix.writeDisplay();
      delay(100);
    }

    sendResponse(200, "msg:" + msg + ",size:" + (String)size);

  } else { // "status" : 404
    sendResponse(404, "command not found:" + cmd);
  }
}

StaticJsonDocument<200> request;

void loop() {
  if(Serial.available() > 0) {
    DeserializationError error = deserializeJson(request, Serial);
    if (error) {
      sendResponse(500, error.c_str());
      return;
    }

    JsonObject obj = request.as<JsonObject>();
    for (JsonPair p : obj) {
      ExecCmd((String)p.key().c_str(), p.value());
    }
  }
}
The entire Arduino code is here

test.ino


#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_LEDBackpack.h>
#include <ArduinoJson.h>

#ifndef _swap_int16_t
#define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; }
#endif

class aitendo_KLED1608K33D_8x16matrix : public Adafruit_LEDBackpack, public Adafruit_GFX {
 public:
  aitendo_KLED1608K33D_8x16matrix(void);

  void drawPixel(int16_t x, int16_t y, uint16_t color);

 private:
};

aitendo_KLED1608K33D_8x16matrix::aitendo_KLED1608K33D_8x16matrix(void) : Adafruit_GFX(16, 8) {
}

void aitendo_KLED1608K33D_8x16matrix::drawPixel(int16_t x, int16_t y, uint16_t color) {
  if ((y < 0) || (x < 0)) return;
  if ((getRotation() % 2 == 0) && ((x >= 16) || (y >= 8))) return;
  if ((getRotation() % 2 == 1) && ((y >= 16) || (x >= 8))) return;

 // check rotation, move pixel around if necessary
  switch (getRotation()) {
  case 0:
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 1:
    y = 16 - y - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  case 2:
    x = 16 - x - 1;
    y = 8 - y - 1;
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 3:
    x = 8 - x - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  }

  if (color) {
    displaybuffer[x] |= 1 << y;
  } else {
    displaybuffer[x] &= ~(1 << y);
  }
}

aitendo_KLED1608K33D_8x16matrix matrix = aitendo_KLED1608K33D_8x16matrix();

void setup() {
  Serial.begin(115200);
  Serial.println("16x8 LED Matrix");

  matrix.begin(0x70);
  matrix.setBrightness(5);
  matrix.setRotation(0);
  matrix.clear();
  matrix.writeDisplay();
}

int sendResponse(int status, String msg) {
  StaticJsonDocument<100> response;
  
  response["status"] = status;
  response["msg"] = msg;

  serializeJson(response, Serial);
  Serial.println();
}

int ExecCmd(String cmd, JsonVariant value) {
  if(cmd == "status") {
    sendResponse(100, "alive");
   
  } else if(cmd == "textscr") {
    JsonObject obj2 = value.as<JsonObject>();
    
    String msg = obj2["msg"];
    int size = obj2["size"];

    if(size < 1 || size > 2) {
      size = 1;
    }
    matrix.setTextSize(size);
    matrix.setTextWrap(false);
    matrix.setTextColor(LED_ON);

    int l = msg.length();
    for(int16_t x = 7; x >= -6*l; x--) {
      matrix.clear();
      matrix.setCursor(x,0);
      matrix.print(msg);
      matrix.writeDisplay();
      delay(100);
    }

    sendResponse(200, "msg:" + msg + ",size:" + (String)size);

  } else { // "status" : 404
    sendResponse(404, "command not found:" + cmd);
  }
}

StaticJsonDocument<200> request;

void loop() {
  if(Serial.available() > 0) {
    DeserializationError error = deserializeJson(request, Serial);
    if (error) {
      sendResponse(500, error.c_str());
      return;
    }

    JsonObject obj = request.as<JsonObject>();
    for (JsonPair p : obj) {
      ExecCmd((String)p.key().c_str(), p.value());
    }
  }
}

Below Linux side code First of all, I finally checked the USB connection between the Linux box and Arduino, but here I am using Ubuntu, which is the test environment Windows Linux Subsystem.

test.py


import serial
import time
import json

s = serial.Serial()
s.port = "/dev/ttyS4" #Connect to COM4 arduino
s.baudrate = 115200
s.timeout = 1
s.dtr = False   #Prevents arduino from being reset when connecting serially
s.open()
time.sleep(1)   #Wait for feelings

s.reset_input_buffer()  #Cleaning the receive buffer of the serial port

def request(data):
    print("request:", data)
    s.write(json.dumps(data).encode())  # encode()Must be a binary string in
    while True: #Wait for the response to come back
        msg = s.readline().decode()
        if(len(msg) > 0):
            print("response:", msg)
            break

request({"textscr" : {"msg":"Hello World !!!"}})
request({"status" : {}})

Execution result led.gif It is correct that the response of "textscr" request is {"status": 200, "msg": Hello World !!! "}, but it seems that the first character was spilled at this time. Consider error handling. Is it?


Manage processing blocking during text scroll display

As you can see from the execution result, the response is not returned until the textscr display is completed, and another command is not accepted while waiting. It's a little difficult to handle. The text scroll on the Arduino side uses a for loop to wait 100ms for displaying characters and then shift it horizontally by 1 dot, so every time the text is shifted by 1 dot, the process is returned to loop () and the next scroll is performed. Allows you to receive commands. In addition, if a new textscr is sent during scrolling, the existing display execution should be discarded and overwritten.

Arduino modified version The whole code is here
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_LEDBackpack.h>
#include <ArduinoJson.h>

#ifndef _swap_int16_t
#define _swap_int16_t(a, b) { int16_t t = a; a = b; b = t; }
#endif

class aitendo_KLED1608K33D_8x16matrix : public Adafruit_LEDBackpack, public Adafruit_GFX {
 public:
  aitendo_KLED1608K33D_8x16matrix(void);

  void drawPixel(int16_t x, int16_t y, uint16_t color);

 private:
};

aitendo_KLED1608K33D_8x16matrix::aitendo_KLED1608K33D_8x16matrix(void) : Adafruit_GFX(16, 8) {
}

void aitendo_KLED1608K33D_8x16matrix::drawPixel(int16_t x, int16_t y, uint16_t color) {
  if ((y < 0) || (x < 0)) return;
  if ((getRotation() % 2 == 0) && ((x >= 16) || (y >= 8))) return;
  if ((getRotation() % 2 == 1) && ((y >= 16) || (x >= 8))) return;

 // check rotation, move pixel around if necessary
  switch (getRotation()) {
  case 0:
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 1:
    y = 16 - y - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  case 2:
    x = 16 - x - 1;
    y = 8 - y - 1;
    if (x >= 8) {
      x -= 8;
      y += 8; 
    }
    break;
  case 3:
    x = 8 - x - 1;
    if(y >= 8) {
      y -= 8;
      x += 8;
    }
    _swap_int16_t(x, y);
    break;
  }

  if (color) {
    displaybuffer[x] |= 1 << y;
  } else {
    displaybuffer[x] &= ~(1 << y);
  }
}


aitendo_KLED1608K33D_8x16matrix matrix = aitendo_KLED1608K33D_8x16matrix();

#define DR_UNRELATED 0
#define DR_STOP 1
#define DR_OVERRIDE 2
#define DR_NEW 10
#define DR_CONTINUE 11

typedef struct {
  const char *cmd;
  int (*drawfunc)(int, JsonVariant);
} Cmds;
Cmds *drawing;

void setup() {
  drawing = NULL;

  Serial.begin(115200);
  Serial.println("16x8 LED Matrix");

  matrix.begin(0x70);
  matrix.setBrightness(5);
  matrix.setRotation(0);
  matrix.clear();
  matrix.writeDisplay();
}

int sendResponse(int status, String msg) {
  StaticJsonDocument<100> response;
  
  response["status"] = status;
  response["msg"] = msg;

  serializeJson(response, Serial);
  Serial.println();
}

int cmdStatus(int stat, JsonVariant value) {
  sendResponse((drawing ? 102 : 100), (drawing ? "drawing" : "free time"));
  return(DR_UNRELATED);
}

int cmdTextscr(int stat, JsonVariant value) {
  static String sMsg;
  static int16_t l, x;

  if(stat == DR_NEW) {
    JsonObject obj = value.as<JsonObject>();

    String msg = obj["msg"];

    sMsg = msg;
    l = msg.length();
    x = 7;

    matrix.setTextSize(1);
    matrix.setTextWrap(false);
    matrix.setTextColor(LED_ON);
  }

  if(x >= -6*l) {
    matrix.clear();
    matrix.setCursor(x,0);
    matrix.print(sMsg);
    matrix.writeDisplay();
    delay(100);
    x--;
  } else {
    //sendResponse(200, "finish textscr msg:" + msg);
    return(DR_STOP);
  }

  if(stat == DR_NEW) {
    sendResponse(200, "textscr msg:" + sMsg);
    return(DR_OVERRIDE);
  }
  return(DR_UNRELATED);
}

Cmds cmds[] = {
  {"status", cmdStatus},
  {"textscr", cmdTextscr},
  {"", NULL},
};

StaticJsonDocument<200> request;
JsonVariant JVNULL = JsonVariant();

void loop() {
  if(Serial.available() > 0) {
    DeserializationError error = deserializeJson(request, Serial);
    if (error) {
      sendResponse(500, error.c_str());
      return;
    }

    JsonObject obj = request.as<JsonObject>();
    for (JsonPair p : obj) {
      String cmd = (String)p.key().c_str();
      int i;
      for(i = 0; cmds[i].cmd != ""; i++) {
        if((String)cmds[i].cmd == cmd) {
          int r = (*cmds[i].drawfunc)(DR_NEW, p.value());
          switch(r) {
            case DR_OVERRIDE:
              drawing = &cmds[i];
              break;
            case DR_STOP:
              drawing = NULL;
              break;
          }
          break;
        }
      }
      if(cmds[i].cmd == "") {
        sendResponse(404, "command not found:" + cmd);
      }
    }

  } else {
    if(drawing) {
      int r = drawing->drawfunc(DR_CONTINUE, JVNULL);
      switch(r) {
        case DR_STOP:
          drawing = NULL;
          break;
      }
    }
  }
}

Excerpt below.

test2.ino


#define DR_UNRELATED 0  //Returned if cmdxxxx processing does not overwrite other running requests
#define DR_STOP 1       //Returned when cmdxxxx processing is complete
#define DR_OVERRIDE 2   //Returned when cmdxxxx processing overwrites another running request
#define DR_NEW 10       //Pass to cmdxxxx when a new request comes in
#define DR_CONTINUE 11  //Passed when calling cmdxxxx in subsequent processing

int cmdStatus(int stat, JsonVariant value) {
  ...
}

int cmdTextscr(int stat, JsonVariant value) {
  ...
}

typedef struct {
  const char *cmd;
  int (*drawfunc)(int, JsonVariant);
} Cmds;

Cmds *drawing;

Cmds cmds[] = {
  {"status", cmdStatus},
  {"textscr", cmdTextscr},
  {"", NULL},
};

void loop() {
  if(Serial.available() > 0) {
    ...
    for (JsonPair p : obj) {
      String cmd = (String)p.key().c_str(); //Store the received request string in cmd
      int i;
      for(i = 0; cmds[i].cmd != ""; i++) { //cmds that match cmd[]Loop to find and call members
        if((String)cmds[i].cmd == cmd) {
          int r = (*cmds[i].drawfunc)(DR_NEW, p.value());
          switch(r) {
            case DR_OVERRIDE:
              drawing = &cmds[i]; //Remember the function you just called because it will continue to process.
              break;
            case DR_STOP:
              drawing = NULL; //Since the function just called is already completed, there is no function to continue processing
              break;
          }
          break;
        }
      }
      ...
    }

  } else {
    if(drawing) { //If there is a request function running, call that function
      int r = drawing->drawfunc(DR_CONTINUE, JVNULL);
      ...
    }
  }
}
  1. Prepare the LED display function cmdxxxx for each request command (currently only two, "textscr" and "status"), and put it in cmds [], which is a list of Cmds structures, together with the command name.
  2. For example, if you send request "textscr" from Linux, cmdTextscr will be called from within loop () with stat = DR_NEW to start scrolling.
  3. cmdTextscr displays text, waits 100ms, and returns DR_OVERRIDE or DR_UNRELATED if it hasn't reached the end of scrolling. Scroll display Returns DR_STOP when it is completed to the end.
  4. On the loop () side that received the reply, remember drawing = & cmds [1] because drawing = NULL if DR_STOP and running if DR_OVERRIDE.
  5. loop () repeats the above in a for loop, and if drawing! = NULL, it calls the process being executed, and if a new request comes, it repeats execution.

Now, on the Linux side, you don't have to wait after sending a request, and you can send a request at any time.

test2.py


import serial
import time
import datetime
import json

s = serial.Serial()
s.port = "/dev/ttyS4"
s.baudrate = 115200
s.timeout = 1
s.dtr = False   #Prevents arduino from being reset when connecting serially
s.open()
time.sleep(1)   #Wait for feelings

s.reset_input_buffer()  #Cleaning the receive buffer of the serial port

def request(data):
    print(datetime.datetime.now().strftime('%H:%M:%S'), "request:", data)
    s.write(json.dumps(data).encode())  # encode()Must be a binary string in
    while True: #Wait for the response to come back
        msg = s.readline().decode()
        if(len(msg) > 0):
            print(datetime.datetime.now().strftime('%H:%M:%S' ), "response:", msg)
            break

request({"textscr" : {"msg":"Hello World !!!"}})
time.sleep(3)
request({"status" : {}})
time.sleep(1)
request({"textscr" : {"msg":"\\(^_^)/"}})
time.sleep(6)
request({"status" : {}})

test2.py execution result


ubuntu:~$ python3 test.py
00:25:39 request: {'textscr': {'msg': 'Hello World !!!'}}
00:25:39 response: {"status":200,"msg":"textscr msg:Hello World !!!"}

00:25:42 request: {'status': {}}
00:25:42 response: {"status":102,"msg":"drawing"}

00:25:43 request: {'textscr': {'msg': '\\(^_^)/'}}
00:25:43 response: {"status":200,"msg":"textscr msg:\\(^_^)/"}

00:25:49 request: {'status': {}}
00:25:49 response: {"status":100,"msg":"free time"}

The time is added before request and response for easy understanding. It will return a response as soon as you start scrolling, and if you send another textscr request while scrolling, it will execute the new request. Perfect. end.

Recommended Posts

Asynchronous processing with Arduino (Asynchronous processing of processing requests from Linux)
Parallel processing with Parallel of scikit-learn
Basics of binarized image processing with Python
Drawing with Matrix-Reinventor of Python Image Processing-
Example of efficient data processing with PANDAS
Install Windows 10 from a Linux server with PXE
Image processing from scratch with python (5) Fourier transform
Image processing from scratch with python (4) Contour extraction
Back up from QNAP to Linux with rsync
Learn Nim with Python (from the beginning of the year).
The story of replacing Nvidia GTX 1650 with Linux Mint 20.1.
Open Chrome version of LINE from the command line [Linux]
[Chapter 5] Introduction to Python with 100 knocks of language processing
[Chapter 6] Introduction to scikit-learn with 100 knocks of language processing
[Chapter 3] Introduction to Python with 100 knocks of language processing
[Chapter 2] Introduction to Python with 100 knocks of language processing
Python asynchronous processing ~ Full understanding of async and await ~
Try to tamper with requests from iphone with Burp Suite
Learn Python asynchronous processing / coroutines by comparing with Node.js
[Basics of data science] Collecting data from RSS with python
Easy learning of 100 language processing knock 2020 with "Google Colaboratory"
[Chapter 4] Introduction to Python with 100 knocks of language processing
ODBC access to SQL Server from Linux with Python