async 資料工程 await Serial Port 


Jason​

今天為什麼想來聽?

  • 工業 4.0 的資料工程

今天為什麼想來聽?

  • 工業 4.0 的資料工程
  • Python & Serial port 

今天為什麼想來聽?

  • 工業 4.0 的資料工程
  • Python & Serial port
  • Asyncio & Serial port

今天為什麼想來聽?

  • 工業 4.0 的資料工程
  • Python & Serial port
  • Asyncio & Serial port
  • 來看老朋友

今天為什麼想來聽?

  • 工業 4.0 的資料工程
  • Python & Serial port
  • Asyncio & Serial port
  • 來看老朋友
  • 來看 Jason

今天為什麼想來聽?

  • 工業 4.0 的資料工程
  • Python & Serial port
  • Asyncio & Serial port
  • 來看老朋友
  • 來看 Jason
  • 其他

說在一開始

工業 3.0, 4.0?

自動化生產?資訊化?

數據分析?機器學習?深度學習?人工智慧?

美好的工業4.0

  • 自動化機器
  • 機械手臂

美好的工業4.0

  • 人力降至最低
  • 生產資訊與機械、機台狀態與能源耗用的即時監控
  • 智慧生產

實際的工業樣貌

  • 傳統機械與自動化機械混雜
  • 機台多為半自動生產
  • 資訊孤島

就是開不了口讓你知道

  • 要達到上述意味很多都需要客製化的設計(軟硬體)
  • 半導體、系統廠、軟體公司剩下的都是?佛系工程師!
  • 外包、外包都是外包(合理但...外包商佛系?)
  • 醒醒吧,你根本沒有工業 4.0
  • 哈摟,老闆!

現況

  • 自動化產線
  • 視覺檢測
  • 但無資訊系統,手抄?
  • 分析資料?囧

機器學習 == ?? + ?? + ?? + ?? + ...

  • 所謂全端,就是碰到什麼都卡關
  • 包山包海就是全端的工作,樣樣不精就是全端的專長,咪啾
  • 承認吧,是隻社畜(問題不是老闆要面對,是阿宅你要面對)

如果要規劃一個系統架構,希望是?

本篇討論的部分

  • 序列資料通訊的介面標準

Python 使用 pyserial 模組讀寫 Serial port 

>>> import serial
>>> # Open named port at “19200,8,N,1”, 1s timeout:
>>> with serial.Serial('/dev/ttyS1', 19200, timeout=1) as ser:
...     x = ser.read()          # read one byte
...     s = ser.read(10)        # read up to ten bytes (timeout)
...     line = ser.readline()   # read a '\n' terminated line


>>> # Open port at “38400,8,E,1”, non blocking HW handshaking:
>>> ser = serial.Serial('COM3', 38400, timeout=0,
...                     parity=serial.PARITY_EVEN, rtscts=1)
>>> s = ser.read(100)       # read up to one hundred bytes
...                         # or as much is in the buffer

開始動手開幹前

  • 即時性?
  • 資料型態(big5, utf-8)?程式崩潰的 handle?
  • 網路穩不穩定?資料是否會掉?
  • File Base 定時 Parser? 或是走 TCP/IP
  • 多個裝置一個 Gateway 程式?

Data Flow

  • 視覺機器產生資料,Gateway 透過 RS-232 取得資料
  • Gateway 接收訊號接著與 web service 取得 id 再透過 API 寫入資料庫

Vision

Gateway

Django

Serial Port

TCP/IP

Sequence 的寫法

  • While 迴圈等待 serial port 資料?
  • 1 + 1 = 1??
  • 多執行緒?(Python GIL 👍)
  • 想要更潮一點

非同步?策略

By Chetan Giridhar, PyCon India 2014

Program Flow

  • Gateway 裡的 Producer() 不斷讀 serial port,資料產生就放入 Queue 中。Gateway 的 Consumer() 會不斷檢查,當 Queue 有資料就執行 API 將資料存到 Server
  • 非同步架構?Asyncio!

Producer

Queue

Consumer

想像的總是美好

現實卻是殘酷

def consumer():
    with serial.Serial(com, port, timeout=0) as ser:
        buffer = ''
        while True:
            buffer += ser.readline().decode()
            if '\r' in buffer:
                buf = buffer.split('\r')
                last_received, buffer = buffer.split('\r')[-2:]
                yield last_received


async def async_producer(session, q):
    start = time.time()
    while len(q):
        data = q.popleft()
        status, result = await async_post(session, url_seq, url_films, data)
        
        if status != 201:
            q.append(data)
        
        if time.time() - start >= 1:
            break

專業養蟲師開始

專業養蟲師開始

q = deque()
com = 'COM5'
port = 9600
last_received = ''

async def async_main():
    global q
    async with aiohttp.ClientSession() as session:
        for data in consumer():
            q.append(data)
            
            d = q.popleft()
            status, result = await async_post(session, url_seq, url_films, data=d) # post data
            if status != 201:
                q.append(d)
            
            if len(q) >= 2:
                await async_producer(session, q)

loop = asyncio.get_event_loop()
loop.run_until_complete(async_main())
loop.close()

是位擅長產生 bug 的朋友呢

錯誤在那呢?

  • 會 blocking
  • 不是非同步,是同步
  • Event loop, coroutine, Future, Task
  • 錯誤的範例程式說明
  • 正確的範例程式說明
async def async_main():
    global q  # --> 錯誤 (1)
    async with aiohttp.ClientSession() as session:
        for data in consumer():  # --> 錯誤 (2) 
            q.append(data)
            
            d = q.popleft()
            status, result = await async_post(session, url_seq, url_films, data=d) # post data
            if status != 201:
                q.append(d)
            
            if len(q) >= 2:
                await async_producer(session, q)

loop = asyncio.get_event_loop()
loop.run_until_complete(async_main())
loop.close()

死路了嗎?

快快樂樂的讓 serial port 非同步

選擇原因與理由

  • 因為用 asyncio library
  • 因為 pyserial 底下專案
  • 廢廢的理由:協定太複雜

官方教學

一頁,簡潔有力啊!我喜歡

官方教學

可能會讓人直接放棄的一頁官方教學

看得懂嗎?

不懂 asyncio

手把手時間

程式碼

多執行緒做法

import threading
import serial
import queue

port = '/dev/cu.usbmodem1411'
baud = 9600

ser = serial.Serial(port, baud, timeout=0)

def produce(ser, queue):
    while True:
        buffers = ''
        while True:
            buffers += ser.readline().decode()
            if '\r' in buffers:
                last_received, buffers = buffers.split('\r')[-2:]
                data = last_received.strip()
                print(f"prodcue: {id(data)}")
                queue.put(data)

def consume(queue):
    while True:
        data = queue.get()
        print(f"consume: {id(data)}")
        queue.task_done()

q = queue.Queue()
workers = [
    threading.Thread(target=produce, args=(ser, q, )),
    threading.Thread(target=consume, args=(q,))
]

for w in workers:
    w.start()

q.join()

pyserial-asyncio API

  • Protocol (Low level api)
  • Stream (High level api)

Protocol 說明

class Reader(asyncio.Protocol):
    def __init__(self, queue):
        """Store the queue here."""
        super().__init__()
        self.transport = None
        self.buf = None
        self.queue = queue        

    def connection_made(self, transport):
        """Store the serial transport and prepare to receive data."""
        self.transport = transport
        self.buf = bytes()
        print('port opend', transport)

    def data_received(self, data):
        """Store characters until a newline is received."""
        self.buf += data
        if b'\r' in self.buf:
            lines = self.buf.split(b'\r')
            recv, self.buf = lines[-2:]  # whatever was left over
            data = recv.strip()
            asyncio.ensure_future(self.queue.put(data))
            self.buf.strip()
            print(f'producing: {id(data)}')

    def connection_lost(self, exc):
        print('Reader closed')

Asyncio

transport

  • represent connections such as sockets, SSL connection, and pipes
  • Async socket operations
    • Usually, frameworks implement e.g. Tornado

protocols

  • represent applications such as HTTP client/server, SMTP, and FTP
  • Async HTTP operation
  • [loop.create_connection()] ​

Stream 說明

import serial_asyncio


async def produce(queue, **kwargs):
    """get serial data use recv() define format with non-blocking 
    """
    reader, writer = await serial_asyncio.open_serial_connection(url=url, **kwargs)
    buffers = recv(reader)
    async for buf in buffers:
        # TODO: can handle data format here
        print(f"produce id: {id(buf)}")

async def recv(r):
    """
    Handle stream data with different StreamReader: 
    'read', 'readexactly', 'readuntil', or 'readline'
    """
    while True:
        msg = await r.readuntil(b'\r')
        yield msg.rstrip().decode('utf-8')

Asyncio

StreamReader

  • Represents a reader object that provides APIs to read data from the IO stream

serial port output 的字串,需要處理

# 'read', 'readexactly', 'readuntil', or 'readline'
msg = await r.readuntil(b'\r')
async def consume(queue):
    """Get serail data with async"""
    while True:
        data = await queue.get()
        print(f'consuming: {id(data)}')
        """handle data from here"""
        await asyncio.sleep(random.random())
        queue.task_done()


loop = asyncio.get_event_loop()
queue = asyncio.Queue(loop=loop)

produce = partial(Reader, queue)
producer_coro = serial_asyncio.create_serial_connection(
    loop, produce, com, baudrate
)
consumer_coro = consume(queue)
loop.run_until_complete(asyncio.gather(producer_coro, consumer_coro))
loop.close()

資料處理

一切都完美了嗎?

>>> buf = buf.rstrip().decode('utf-8')
>>> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe0 in position 0: invalid 
continuation byte

loop.stop()

Task was destroyed but it is pending!
task: <Task pending coro=<consume() running at xxxxx.py:207> 
wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x03144F30>()]>>
2018-09-04 08:24:31 INFO  [xxxxx:216] Event loop is closed
Exception ignored in: <coroutine object consume at 0x03255660>
RuntimeError: coroutine ignored GeneratorExit

decode error

這是啥 à?'À'?可以吃嗎?

無法控制設備丟出任何奇怪的編碼訊息

or

裝置?win 的 big5

Python3.x 的編碼

  • str
  • bytes
try:
    buf = buf.rstrip().decode('utf-8')
except Exception as e:
    wlog(buf)
    buf = buf.rstrip().decode('utf-8', 'ignore')

還有些細節:

就是資料庫的時間

import pytz


# 要查詢資料庫的時間記得要加上時區
tp = pytz.timezone('Asia/Taipei')
dt = datetime.datetime.strptime(start, '%Y-%m-%d %H:%M').astimezone(tp)

Django 為例,如果 settings.py 設定 USE_TZ = True,那預設存進 DB 的時間都會自動轉成 UTC,做查詢時,記得要轉換

Demo 時間

後端大概是這樣

前端網頁大概就4這樣

前端 js 可以設定幾秒後動態去更新資料(Demo 手按的 XD)

如果要漂亮一點就是多加一些統計量,也是動態的

資料科學家

也歡迎挖角 XD

我是超全端(樣樣都卡關)

Thanks

async 資料工程 await Serial Port

By Jason

async 資料工程 await Serial Port

  • 1,410