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

Made with Slides.com