Extra — แจ้งเตือนตรวจสั่น/ล้ม (shake)

บอร์ดหนึ่งล้ม → บอร์ดทุกตัวรู้ทันที

มินิโปรเจกต์เสริม · WiFi + MQTT + เซนเซอร์ความเร่ง
โค้ดเดียว ทำทั้งฝั่งส่งและฝั่งรับ · ใช้ได้ตั้งแต่ 1 บอร์ดขึ้นไป

วันนี้เราจะเอาสามอย่างที่เคยฝึกแยกกัน — accelerometer, MQTT, LED/เสียง — มาต่อกันเป็นระบบเตือนภัยจริง ๆ

เป้าหมาย + use case จริง

เราอยากให้บอร์ด เฝ้าดูการสั่นสะเทือน ถ้าเกินระดับที่ตั้งไว้ ให้รีบส่งสัญญาณเตือนออกไปหาทุกบอร์ดในกลุ่มทันที

ของจริงที่ใช้แนวคิดนี้:

  • เครื่องจักรในโรงงาน (Industry 4.0) — สั่นผิดปกติ = ตลับลูกปืนกำลังจะพัง แจ้งก่อนเสียหาย
  • เซนเซอร์ติดผู้สูงอายุ — ตรวจจับการล้ม (fall detection) แล้วเตือนคนดูแล
  • กล่องขนส่งของเปราะ — ถูกกระแทกแรงระหว่างทาง บันทึกไว้เป็นหลักฐาน

ที่มา: "SparkFun 6DoF-Breakout Gyroscope+Accelerometer LSM6DS3" — SparkFun, CC BY 2.0, Wikimedia Commons

ตลับลูกปืนที่เซนเซอร์แบบนี้คอยเฝ้าฟัง — ที่มา: "Rolling-element bearing 60 mm" — Massimiliano Lincetto, CC BY-SA 3.0, Wikimedia Commons

ชิปสี่เหลี่ยมเล็ก ๆ ในรูปคือ accelerometer + gyroscope แบบ 6 แกน รุ่นญาติกับ BMI270 ที่ฝังอยู่บนบอร์ดเรา ข้างในมีโครงสร้าง MEMS จิ๋วที่ขยับได้ตามแรง แล้วแปลงเป็นเลข ax, ay, az ให้เราอ่าน

หัวใจ: การเขย่า/ล้ม ทำให้ "ขนาดความเร่งรวม" พุ่งเกินค่าปกติชั่วขณะ เราจับจังหวะนั้นให้ได้

ฟิสิกส์: ดู "ขนาดรวม" ไม่ใช่ทีละแกน

แทนที่จะดู ax, ay, az แยกกัน เรารวมทั้งสามแกนเป็นเลขก้อนเดียว — ขนาดความเร่งรวม (เวกเตอร์ความเร่งยาวเท่าไร ไม่สนว่าชี้ทางไหน)

a=ax2+ay2+az2|a| = \sqrt{a_x^2 + a_y^2 + a_z^2}

นี่คือทฤษฎีบทพีทาโกรัสในสามมิติ ลองวางบอร์ดนิ่ง ๆ แล้วเอียงไปมา: แรงโน้มถ่วงกระจายไป 3 แกนต่างกัน แต่ "ความยาวรวม" คงที่เสมอ เช่น เอียง 45° แกนหนึ่งได้ ax=az=12ga_x{=}a_z{=}\tfrac{1}{\sqrt2}g รวมกลับมา (12)2+(12)2g=1g\sqrt{(\tfrac{1}{\sqrt2})^2 + (\tfrac{1}{\sqrt2})^2}\,g = 1g เป๊ะ

  • วางนิ่ง: a1g|a| \approx 1g เสมอ ไม่ว่าจะเอียงมุมไหน (มีแค่แรงโน้มถ่วง)
  • เขย่า/กระแทกแรง: มีแรงอื่นบวกเข้ามา a|a| พุ่งเกิน ~1.5–2g ชั่วขณะ — จุดนี้คือสิ่งที่เราจับ
  • ตกอิสระ (free fall): ของกำลังตก ไม่มีพื้นรับ a|a| ดิ่งลงใกล้ 0

เพราะ a|a| ไม่ขึ้นกับว่าเอียงทางไหน เราจึงตั้ง threshold ค่าเดียวจับได้ทุกทิศ — นี่คือเหตุผลที่ใช้ "ขนาดรวม" ไม่ใช่แกนเดียว

เหตุการณ์เกิดแค่ "แวบเดียว" — ดูภาพเคลื่อนไหว

ถ้าเราพล็อต a|a| ตามเวลา จะเห็นว่าตอนเขย่ามันพุ่งทะลุเส้น 1.8g แค่ไม่กี่เฟรมแล้วตกกลับ ตอนตกอิสระก็ดิ่งใกล้ 0 ชั่วขณะเดียวกัน

  • จุดที่วิ่งคือค่าที่บอร์ดอ่านได้ "เดี๋ยวนี้" สีเปลี่ยนตามแถบ: แดง = เกิน threshold, น้ำเงิน = ตกอิสระ, เขียว = ปกติ
  • เพราะ spike อยู่แค่แวบเดียว เราต้อง อ่านเซนเซอร์ถี่พอ (ลูปเรา ~20 ครั้ง/วินาที) ไม่งั้นพลาดจังหวะ
  • และเพราะมันพุ่งทีเดียวอาจข้ามเส้นหลายเฟรมติดกัน เราจึงมี COOLDOWN กันไม่ให้ส่งเตือนรัว ๆ จากเหตุเดียว

ไดอะแกรม pub/sub: topic เดียว เตือนถึงกันหมด

บอร์ดที่ล้มทำหน้าที่ publisher, บอร์ดที่เหลือเป็น subscriber — แต่ในโค้ดของเรา บอร์ดเดียวเป็นทั้งคู่พร้อมกัน

บอร์ดที่ล้ม ตรวจ mag เกิน publish Broker test.mosquitto.org:1883 alert/board01 บอร์ดอื่น LED + เสียงเตือน บอร์ดเราเอง ก็ได้ยินตัวเอง "SHAKE board01" ทุกบอร์ดที่ subscribe topic เดียวกัน ได้รับข้อความเตือนพร้อมกัน

payload สั้น ๆ: "SHAKE board01" หรือ "FALL board01" — คำแรกบอกชนิดเหตุ (เขย่า/ตกอิสระ) ตามด้วยชื่อบอร์ดที่เกิดเหตุ

กฎการตัดสิน: ค่า mag ตกแถบไหน ทำอะไร

ก่อนเข้าโค้ด ขอวางกฎให้เห็นภาพก่อน เรามีแค่เลขก้อนเดียว (mag) แล้วเทียบกับเส้นบน/ล่างสองเส้น

โค้ดในหน้าถัดไปคือกฎสามแถบนี้เป๊ะ ๆ: if mag > THRESHOLD (แดง) · elif mag < FALL_LOW (น้ำเงิน) · ที่เหลือปล่อยผ่าน (เขียว)

เดินโค้ด (1) — วัด "ขนาด" ความเร่งแล้วส่งเตือน

ขนาดรวมพุ่งสูง = เขย่า/กระแทก · ดิ่งใกล้ 0 = ตกอิสระ เราเลือกชนิดเหตุจาก mag แล้ว publish

โค้ดข้างล่างคือสายงานนี้เป๊ะ ๆ: อ่าน 3 แกน → หาร 9.81 เป็น g → รวมเป็น mag ก้อนเดียว → เทียบสองเส้น → ถ้ามีเหตุและพ้น COOLDOWN ก็ publish

    ax, ay, az = sensors.bmi270.acceleration()        # เซนเซอร์คืน m/s²
    ax, ay, az = ax/9.81, ay/9.81, az/9.81    # แปลงเป็น g (วางนิ่ง mag รวม ≈ 1.0)
    mag = math.sqrt(ax * ax + ay * ay + az * az)
    now = time.time()

    event = None                          # เลือกชนิดเหตุจากขนาดรวม
    if mag > THRESHOLD:
        event = "SHAKE"                   # พุ่งเกิน ~1.8g = เขย่า/กระแทก
    elif mag < FALL_LOW:
        event = "FALL"                    # ดิ่งใกล้ 0 = ตกอิสระ

    if event is not None and (now - last_fire) > COOLDOWN:
        payload = "%s %s" % (event, BOARD_ID)   # "SHAKE board01" / "FALL board01"
        mqtt.publish(TOPIC, payload)
        print("ตรวจพบ %s mag=%.2f -> ส่ง '%s'" % (event, mag, payload))
        last_fire = now
  • THRESHOLD = 1.8 / FALL_LOW = 0.4 ลองปรับดู เขย่าเบา ๆ ยังไม่ถึง แต่กระแทกแรงต้องเกิน
  • COOLDOWN กันส่งซ้ำรัว ๆ — เกิดเหตุครั้งเดียวควรเตือนครั้งเดียว ไม่ใช่ทุกเฟรม

เดินโค้ด (2) — ฟังเตือนแล้วกระพริบ + ส่งเสียง

get_message() ไม่บล็อก ไม่มีของก็คืน None เราจึง poll สลับกับการอ่านเซนเซอร์ในลูปเดียวได้

    msg = mqtt.get_message()        # คืน (topic, bytes) หรือ None
    if msg is not None:
        topic, data = msg
        text = data.decode()        # payload เป็น bytes ต้อง decode
        print("ได้แจ้งเตือน:", text)
        if text.startswith("SHAKE") or text.startswith("FALL"):
            blink_alert()           # game.sfx("deny") + led.toggle วน 6 รอบ
    time.sleep_ms(50)
def blink_alert():
    game.sfx("deny")                # เสียงเตือนจากเอนจินเสียงบนบอร์ด
    for _ in range(6):
        led.toggle()
        time.sleep_ms(120)
    led.off()

โค้ดเดียวเป็นทั้ง publisher และ subscriber บอร์ดที่ล้มจึงได้ยินเสียงเตือนตัวเองด้วย

เสียงในคาบนี้ — คาบนี้ไม่ใช่เกม แต่ game.sfx("deny") ในตัวเตือน (blink_alert) คือเสียง ui_deny ตัวเดียวกับที่เกมใช้ตอนกดผิด เอามาทำ "เสียงเตือนภัย"; ถ้าต่อยอดเป็นหลายระดับความรุนแรง game_over ใช้เป็นเสียงเหตุหนักได้ (กดเล่นฟังได้จริง):

ui_deny

game_over

ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer

เชื่อมโยงรากฐาน · เกมเตือนสั่นนี้สอนวิศวกรรมอะไรเราบ้าง

มินิโปรเจกต์ที่ดูเหมือนแค่ "เขย่าแล้วไฟกระพริบ" จริง ๆ คือต้นแบบย่อ ๆ ของระบบ condition monitoring ในงานอุตสาหกรรม ลองดูว่าแต่ละบรรทัดแตะรากฐานอะไร

ฝั่ง Embedded / สัญญาณ

  • อ่านค่าจาก IMU (BMI270) ผ่านบัส I2C/I3C แล้วแปลงหน่วยดิบ → g คืองาน sensor interfacing พื้นฐาน
  • ลูป poll ทุก ~50 ms คือการเลือก polling แทน interrupt ของจริงจะผูก threshold ไว้กับ interrupt ของชิปให้ปลุก CPU เองตอนกระแทก ประหยัดไฟกว่า

ฝั่ง Python / Algorithm

  • a=ax2+ay2+az2|a|=\sqrt{a_x^2+a_y^2+a_z^2} คือ เวกเตอร์นอร์ม เทคนิคเดียวกับที่ DSP ใช้หา "พลังงาน" ของสัญญาณ
  • การเทียบ threshold + COOLDOWN คือ debounce/hysteresis กันสัญญาณรบกวนสั่งงานซ้ำ ใช้ได้ทั้งปุ่มกด เซนเซอร์ และระบบควบคุม
  • โครง pub/sub บน MQTT คือสถาปัตยกรรม event-driven ที่ scale จาก 2 บอร์ดไปเป็นโรงงานทั้งโรงได้ด้วยโค้ดชุดเดิม

Bosch MEMS sensors: Working principle of an accelerometer — Bosch Sensortec · Bosch คือผู้ผลิต BMI270 บนบอร์ดเรา คลิปนี้โชว์ว่าข้างในชิปมีโครงสร้างจิ๋วขยับยังไง ค่า ax/ay/az ถึงเกิดขึ้นได้

วิธีรัน + ต่อยอดสู่โลกจริง

วิธีรัน: เปิด codes/ex_b3_alert.py ใน BENTO IDE แล้วกด Program to Device

  • 1 บอร์ดก็ทดสอบได้ — เขย่าบอร์ดเดียว มันจะเตือนตัวเอง (เห็นครบทั้งวงจร)
  • 2 บอร์ดขึ้นไปสนุกกว่า — ตั้ง TOPIC ตัวเดียวกัน เขย่าตัวหนึ่ง อีกตัวเตือน
  • ทางลัด: ต่อ WiFi ผ่านเมนูบนจอครั้งเดียว แล้ว wifi.connect() จะผ่านทันที

ต่อยอดเลือก 1 อย่าง:

  • เพิ่มระดับความรุนแรง: ส่ง "FALL board01 2.7" แนบค่า mag แล้วฝั่งรับ split(" ") มาแสดง
  • ตั้ง threshold สองชั้น: สั่นเบา = เตือนเหลือง, กระแทกแรง = เตือนแดง
  • เปลี่ยนเป็นระบบกริ่งประตู/นับการกระแทกกล่องพัสดุ — โครง pub/sub เดิมใช้ซ้ำได้เลย

นี่คือแก่นของ IoT จริง ๆ: เซนเซอร์ตัวเล็ก ๆ คุยกันผ่านเครือข่าย แล้วกลายเป็นระบบที่ทำงานร่วมกันได้

fit-css

← Extra Index