PosBox trở thành IoT Box, và tầng driver bị dựng lại hai lần: một lần vì bảo trì ở v12, một lần vì bảo mật ở v19. Bản đồ phiên bản cho biết nên bắt đầu debug từ đâu.
Trên một hệ thống POS v16, chúng tôi mất cả buổi chiều lần theo một lỗi máy in nhiệt qua hw_escpos rồi mới nhận ra module đó không còn tồn tại từ v11. Câu trả lời nằm trong hw_drivers. Kiểu lệch pha đó tránh được, một khi bạn nắm bản đồ theo phiên bản, và bản đồ đó chỉ hợp lý khi bạn hiểu vì sao kiến trúc đã bị dựng lại hai lần.
PosBox/IoT Box không phải một sản phẩm. Nó là một khuôn mẫu kiến trúc đã được dựng lại dưới những ràng buộc thật: bảo trì ở v12, bảo mật ở v19. Mỗi lần dựng lại đều làm đổi module bạn cần dùng. Tài liệu chính thức coi mỗi phiên bản như một khởi đầu mới; bài này lần theo mạch từ v8 tới v19.
Chúng ta đi qua bốn thời kỳ kiến trúc: v8 đến v11 (PosBox với module driver riêng cho từng thiết bị), v12 (đổi tên cộng gom module), v13 đến v18 (IoT Box ổn định), và từ v19 trở đi (viết lại từ đầu). Bài kết thúc bằng một bản đồ phiên bản gọn, cho bạn biết nên bắt đầu debug từ đâu trên bất kỳ phiên bản Odoo nào còn được hỗ trợ.
Các phương án thay thế cho IoT Box chính thức (cắm USB trực tiếp, iot_oca của OCA, các tích hợp phần cứng của bên thứ ba) nằm ngoài phạm vi bài này; chúng được nói trong một bài riêng.
Vì sao cần một proxy phần cứng ngay từ đầu
Trước khi đi vào chi tiết của từng phiên bản, cần làm rõ cái ràng buộc đã khiến PosBox trở nên cần thiết. Mọi thứ khác đều theo sau nó.
Bức tường bảo mật của trình duyệt
POS của Odoo là một ứng dụng web một trang. Ứng dụng web chạy trong môi trường sandbox của trình duyệt, không truy cập được cổng USB, cổng serial hay bất kỳ giao diện phần cứng nào ở tầng hệ điều hành. Mọi tương tác với phần cứng đều phải đi qua HTTP.
WebUSB và WebSerial nay đã có trên các trình duyệt hiện đại, nhưng năm 2013 khi PosBox ra đời thì chưa. Kể cả hôm nay, chúng vẫn đòi người dùng cấp quyền tường minh cho từng thiết bị và không phủ hết mọi lớp thiết bị (cân dùng cổng serial, máy in tài chính và các thiết bị USB HID đời cũ là những chỗ thường thiếu). Bức tường bảo mật của trình duyệt không phải một hạn chế tạm thời; nó là một đặc tính thiết kế của nền tảng web.
Vì sao phải đặt tại chỗ, không đặt trên cloud
Đưa từng lượt quét mã vạch và từng lệnh in qua máy chủ Odoo sẽ tạo ra độ trễ không chấp nhận được ngay tại quầy. Một thu ngân đang quét hàng không thể chờ một vòng đi về tới máy chủ ở xa cho mỗi lần quét. Proxy phải chạy trên mạng nội bộ, sát với phần cứng.
Chạy proxy như một dịch vụ phần mềm ngay trên máy trạm POS (một PC bán lẻ chạy Windows hoặc Mac) lại sinh ra vấn đề riêng: xung đột với phần mềm diệt virus, cách liệt kê USB không nhất quán giữa các hệ điều hành, và sự phụ thuộc vào việc người vận hành không đụng vào máy. Một thiết bị chuyên dụng, cụ thể là Raspberry Pi, tránh được tất cả những thứ đó. Pi chạy một môi trường Linux có kiểm soát, hành vi USB đoán trước được, và đủ rẻ để dành hẳn cho vai trò này.
Ràng buộc định hình tất cả
Một ràng buộc duy nhất đã sinh ra kiến trúc PosBox: truy cập phần cứng cần quyền ở tầng hệ điều hành; trình duyệt không có quyền đó. Phải có một tiến trình cục bộ có đặc quyền bắc cầu qua khoảng cách ấy.
Mọi lần viết lại sau này chỉ là mở rộng phạm vi hoặc cải thiện khả năng bảo trì trong cùng ràng buộc đó. Bản thân ràng buộc không đổi qua bất kỳ phiên bản Odoo nào. Điều này đáng nói thẳng, vì nó giải thích vì sao kiến trúc nhìn ở mức tổng quan vẫn na ná nhau ngay cả khi tên module đổi hoàn toàn.
Thời kỳ 1: PosBox, v8 đến v11
Trong kiến trúc PosBox nguyên bản, proxy phần cứng chia làm hai tầng: một module controller lõi (hw_proxy) và một nhóm module driver riêng cho từng thiết bị (hw_escpos, hw_scanner, hw_scale và các module khác).
hw_proxy: vòng lặp controller
hw_proxy là phần lõi proxy HTTP và RPC [2]. Nó phơi ra các endpoint trạng thái và các trình xử lý lời gọi RPC mà POS chạy trên trình duyệt trao đổi cùng. Vòng lặp controller làm hai việc: thăm dò cổng USB tìm thiết bị HID lớp 7 qua PyUSB, và quét cổng serial tìm thiết bị RS-232 [3].
Khi phát hiện một thiết bị, vòng lặp tìm driver tương ứng trong một dictionary drivers dùng chung. Mỗi module driver tự đăng ký mình vào dictionary này lúc import và cài đặt một phương thức get_status(). Proxy gọi get_status() để đưa trạng thái phần cứng lên giao diện POS. Khuôn mẫu registry này đơn giản và tường minh: proxy không cần biết gì về phần cứng; driver mới cần biết.
Các module driver theo từng thiết bị
Mỗi lớp phần cứng có module riêng [1]:
hw_escposlo máy in nhiệt ESC/POS và ngăn kéo tiền. Đường truyền: USB HID và RS-232. Phụ thuộc Python:usb.core,serial,qrcode[4].hw_scannerlo máy quét mã vạch. Đường truyền: USB HID quaevdev, coi máy quét như đầu vào bàn phím [5].hw_scalelo cân, gồm cả Mettler Toledo Ariva. Đường truyền: cổng serial RS-232 với một vòng lặp thăm dò đọc các khung dữ liệu trọng lượng [6].hw_screenlo màn hình hiển thị hướng về phía khách.hw_blackbox_belo máy in tài chính của Bỉ và chịu các yêu cầu tuân thủ pháp lý với nhịp phát hành riêng.hw_posbox_upgradelo cập nhật phần mềm từ xa cho image Raspberry Pi [7].
Đường truyền quan trọng hơn tên module. hw_escpos dùng USB HID và gửi các lệnh byte ESC/POS; hw_scale mở một cổng serial và đọc các khung dữ liệu có độ rộng cố định. Đó là hai kiểu hỏng khác nhau và hai đường debug khác nhau.
Vì sao tách module là hợp lý vào thời điểm đó
Mỗi lớp thiết bị có phụ thuộc Python khác nhau. PyUSB, evdev và pyserial không cần cùng tồn tại trong một đường import nếu các module tách rời. Một driver cân bị hỏng không nên làm chết máy in. hw_blackbox_be bám theo quy định tài chính của Bỉ chứ không theo lịch phát hành POS của Odoo, nên tách riêng nghĩa là nó cập nhật được độc lập.
Tách riêng từng module là quyết định đúng ở quy mô nhỏ. Nó trở thành gánh nặng bảo trì khi số thiết bị được hỗ trợ tăng lên, vì mỗi thiết bị mới lại cần một module mới với manifest, phụ thuộc và lịch phát hành riêng.
Hệ điều hành PosBox
Thiết bị chạy một image PosBox OS chuyên dụng trên Raspberry Pi [1]. Image này đóng gói sẵn toàn bộ phụ thuộc Python cho các module driver. Việc cập nhật image từ xa do hw_posbox_upgrade lo, cho phép cập nhật phần mềm của Pi mà không cần chạm tay vào thiết bị [7].
Thời kỳ 2: v12, việc đổi tên chỉ là chú thích, việc gom module thì không
Hai chuyện xảy ra ở v12: thiết bị được đổi tên từ PosBox thành IoT Box, và các module driver riêng từng thiết bị được gom vào một module hw_drivers duy nhất. Người ta nhớ chuyện đổi tên; nhưng phần quan trọng với lập trình viên lại là chuyện gom module.
Lần đổi thương hiệu gọn trong hai dòng
Trong hw_posbox_homepage/__manifest__.py, trường name của module đổi từ 'PosBox Homepage' thành 'IoT Box Homepage' [9]. URL website chuyển từ /page/point-of-sale sang /page/point-of-sale-hardware. Điều này báo hiệu một định vị rộng hơn, vượt ra ngoài POS bán lẻ, nhưng nó chỉ là một thay đổi trong manifest. Ngay tại thời điểm đổi tên, kiến trúc bên dưới không hề bị đụng tới.
hw_drivers: thay đổi thật sự của v12
Các module driver hw_* riêng lẻ bị gỡ khỏi repository chính của Odoo và gom vào một module hw_drivers duy nhất [10]. Một module, một chu kỳ phát hành, một manifest phụ thuộc. Gánh nặng quản lý phiên bản theo từng module biến mất.
Khuôn mẫu registry cho driver vẫn tồn tại bên trong hw_drivers. Dictionary drivers dùng chung và giao diện get_status() vẫn còn; chỉ là được đóng gói khác đi. Nếu ở v11 bạn dùng hw_escpos, giờ hãy nhìn vào hw_drivers. Kiến trúc y như cũ; chỉ cách đóng gói đổi.
Tương thích ngược: hw_proxy vẫn còn
hw_proxy vẫn còn với vai trò là phụ thuộc của hw_drivers cho tới v18 [10][11]. Tầng proxy HTTP không đổi. Proxy vẫn phơi ra đúng các endpoint đó cho trình duyệt. Đây là thay đổi về đóng gói, không phải thay đổi giao thức. Việc chuyển từ v11 sang v12 không làm hỏng các cấu hình POS sẵn có.
“IoT” báo hiệu điều gì
Việc đổi URL sang /page/point-of-sale-hardware và cái tên IoT Box thể hiện một ý đồ. Thiết bị nay được định vị cho những trường hợp dùng vượt ra ngoài POS: máy thanh toán, trạm cân, và ở các phiên bản sau là cả I/O công nghiệp. Kiến trúc ở v12 chưa phản ánh đầy đủ điều đó. hw_drivers vẫn lấy phần cứng POS làm trung tâm. Nhưng lời tuyên bố về phạm vi ở v12 đã gieo mầm cho lần viết lại ở v19, khi xác lập rằng thiết bị này là để tích hợp phần cứng nói chung, không chỉ POS bán lẻ.
Thời kỳ 3: v13 đến v18, IoT Box ổn định
Đặc trưng của thời kỳ này là những gì Odoo đã không đổi. hw_drivers đi suốt từ v13 tới v18 mà không thay đổi kiến trúc.
Quãng bình nguyên dài
Nếu bạn mở addons/hw_drivers/__manifest__.py trên bất kỳ cây mã nguồn nào từ v13 tới v18, nó giống hệt v12 về cấu trúc [11]. Sáu phiên bản lớn của Odoo trên cùng một kiến trúc module. Đây là thời kỳ mà phần lớn hệ thống Odoo đang chạy thuộc về: v14, v16 và v17 đều là những phiên bản production phổ biến.
Phạm vi nở ra, kiến trúc thì không
Suốt thời kỳ này, IoT Box được tiếp thị cho phần cứng ngoài POS: máy thanh toán, trạm cân, cảm biến I/O công nghiệp. Các loại thiết bị mới dần được thêm vào hw_drivers. Không có tầng trừu tượng mới nào được đưa vào. Không giao thức mới. Mỗi loại thiết bị mới là một driver mới đăng ký vào đúng cái dictionary drivers ấy, bên trong đúng module hw_drivers ấy. Khuôn mẫu từ v8 vẫn đang chạy.
Việc cố ý không viết lại
Một quãng bình nguyên sáu phiên bản trên cùng một kiến trúc là một quyết định, không phải sự bỏ bê. Tầng HTTP hw_proxy chạy đủ tốt cho nhu cầu bán lẻ. Chi phí viết lại không chính đáng chừng nào kiến trúc còn đáp ứng được yêu cầu.
Ràng buộc rốt cuộc buộc phải viết lại là bảo mật. Xác thực dựa trên chứng chỉ cho thiết bị IoT đòi một mô hình giao tiếp khác với proxy HTTP không xác thực mà hw_proxy cung cấp. Việc nhét xác thực bằng chứng chỉ vào hw_proxy đủ xâm lấn về mặt kiến trúc để một lần viết lại sạch sẽ trở thành lựa chọn tốt hơn.
Ý nghĩa thực tế cho lập trình viên trên v13 đến v18
Hãy bắt đầu từ hw_drivers cho mọi lần debug phần cứng trên các phiên bản này. hw_proxy vẫn hiện diện như một phụ thuộc nhưng không phải nơi chứa logic driver. Ranh giới module rất rõ: hw_drivers chứa toàn bộ mã driver; hw_proxy là phần ống dẫn HTTP bên dưới.
Đừng đi tìm hw_escpos hay hw_scanner trên các phiên bản này. Chúng không tồn tại dưới dạng module độc lập. Mã cho máy in ESC/POS và máy quét mã vạch nằm bên trong hw_drivers.
Thời kỳ 4: v19, viết lại từ đầu
v19 là một lần viết lại cắt đứt hẳn. Tên module đổi, mô hình giao tiếp đổi, và câu chuyện bảo mật cũng đổi. Đây không phải một lần nâng cấp tiệm tiến từ v18.
Điều gì buộc phải viết lại
Nguyên nhân trực tiếp là bảo mật. Việc hỗ trợ một cơ quan cấp chứng chỉ SSH, cụ thể là sinh cặp khóa và xác thực bằng chứng chỉ cho thiết bị IoT, đòi một mô hình giao tiếp khác với tầng HTTP của hw_proxy [15]. Mô hình cũ không có câu chuyện xác thực nào cả. Thêm nó vào là xâm lấn về mặt kiến trúc: các endpoint HTTP trong hw_proxy vốn được thiết kế cho việc dùng trong mạng nội bộ không cần xác thực. Gắn thêm xác thực bằng chứng chỉ lên tầng đó sẽ cho ra thứ còn tệ hơn một lần viết lại sạch sẽ.
Động lực thứ hai là phần mở rộng phạm vi đã tuyên bố ở v12 nhưng chưa bao giờ làm tới nơi. Lần viết lại ở v19 là điểm mà kiến trúc bắt kịp định vị “IoT vượt ra ngoài POS”.
iot_drivers: tầng driver mới
iot_drivers thay cho hw_drivers [12]. Vai trò về mặt khái niệm vẫn thế: driver phần cứng cho các thiết bị kết nối. Phần cài đặt thì được dựng lại từ đầu để hỗ trợ mô hình xác thực mới.
hw_posbox_homepage vẫn còn ở v19 với vai trò điểm vào giao diện web của thiết bị, nhưng nay nó phục vụ một frontend IoT riêng của Odoo chứ không còn là một lớp giao diện web phủ lên trên proxy [12]. Giao diện người dùng nhìn thấy thì như cũ; backend nó kết nối tới thì khác.
iot_base: tiện ích mạng và bộ điều khiển thiết bị bằng JavaScript
iot_base là một module mới, không có thứ tương đương trực tiếp ở v8-v18 [13]. Nó bổ sung các tiện ích mạng dùng cho hệ điều hành của IoT Box và cho tầng giao tiếp với máy chủ Odoo. Quan trọng hơn về mặt kiến trúc: nó mang theo một thư viện JavaScript điều khiển thiết bị để quản lý tương tác với thiết bị ngay trên trình duyệt.
Đây là một chuyển dịch đáng kể. Ở mọi thời kỳ trước, logic tương tác với thiết bị nằm trọn trong proxy phía máy chủ. Với iot_base, một phần logic đó dịch sang phía client. Trình duyệt nay tham gia được vào việc quản lý thiết bị, thay vì chỉ là bên tiêu thụ thuần túy các endpoint của proxy.
iot_box_image: dựng image tự động
iot_box_image tự động hóa việc dựng một image Raspberry Pi OS khởi động được [14][15]. Việc này ở các phiên bản trước là một quy trình chắp vá; v19 biến nó thành một quá trình dựng tự động, có đánh phiên bản. Image dùng cách đánh phiên bản YY.MM (năm và tháng), nên các bản phát hành đánh theo thời gian chứ không gắn với phiên bản lớn của Odoo [14]. Nhờ vậy, hệ điều hành của IoT Box có thể phát hành các bản cập nhật bảo mật và cập nhật driver độc lập với chu kỳ phát hành của Odoo.
Script dựng (build_image.sh) lo phần tạo image [15]. Các script chạy sau khởi tạo (overwrite_before_init, overwrite_after_init) cấu hình các thiết lập ở tầng hệ điều hành. Bài này không đi vào chi tiết bên trong các script đó; điểm kiến trúc cần nắm là việc dựng image nay đã thành một hạng mục chính thức, tự động và được đánh phiên bản độc lập.
Cơ quan cấp chứng chỉ SSH: câu chuyện bảo mật
Thiết bị IoT ở v19 được xác thực trước khi bắt đầu giao tiếp. Việc sinh cặp khóa và xác thực bằng chứng chỉ được xử lý ở tầng hệ điều hành và hạ tầng [15]. Điều này thay cho mô hình HTTP không xác thực mà hw_proxy dùng từ v8 tới v18. Với các hệ thống quy mô doanh nghiệp và nhiều điểm bán, đây là một cải thiện bảo mật đáng kể. Với một cửa hàng bán lẻ đơn lẻ, nó diễn ra trong suốt nhưng cho một thế đứng dễ bảo vệ hơn.
Bản đồ phiên bản: bạn đang ở tầng driver nào?
Ba thời kỳ, ba tầng driver. Bản đồ rất gọn.
v8 đến v11:
- Controller:
hw_proxy - Driver: các module
hw_*riêng lẻ (hw_escpos,hw_scanner,hw_scale,hw_screen,hw_blackbox_be,hw_posbox_upgrade) - Bắt đầu debug từ đúng module driver của loại phần cứng liên quan
v12 đến v18:
- Controller:
hw_proxy(chỉ là phụ thuộc, phần ống dẫn) - Driver:
hw_drivers(gom về một module duy nhất) - Bắt đầu debug từ
hw_driverscho mọi vấn đề phần cứng [10][11]
v19 trở đi:
- Driver:
iot_drivers(thay chohw_drivers) [12] - Mạng và phía client:
iot_base[13] - Image hệ điều hành:
iot_box_image(đánh phiên bản theo thời gian, dựng tự động) [14] - Bắt đầu debug từ
iot_driverscho vấn đề phần cứng; xemiot_basecho vấn đề mạng và bộ điều khiển thiết bị phía trình duyệt
Một quy tắc thực dụng
Với mọi buổi debug phần cứng, hãy bắt đầu từ tầng driver tương ứng với phiên bản Odoo đang cài. Đừng tìm logic driver trong hw_proxy ở v12 trở đi. Đừng tìm hw_escpos ở v12 trở đi. Đừng tìm trong hw_drivers ở v19. Tên module không tương thích ngược giữa các thời kỳ.
Những gì không đổi qua mọi thời kỳ
Ba thứ không đổi qua bất kỳ phiên bản Odoo nào:
- Ràng buộc nền tảng: trình duyệt không truy cập phần cứng trực tiếp được; phải có một tiến trình cục bộ có đặc quyền làm proxy. Điều này đúng ở v19 y như ở v8.
- Raspberry Pi là thiết bị đích. Pi không bắt buộc ở mọi thời kỳ (thiết bị Linux nào cũng chạy được phần mềm này), nhưng các image chính thức đều nhắm tới Pi xuyên suốt.
hw_posbox_homepagelà điểm vào giao diện web của thiết bị. Cái tên này tồn tại từ v8 tới v19, dù backend nó kết nối tới đã đổi hoàn toàn [1][9][12].
Kết luận
Câu chuyện PosBox và IoT Box là ba quyết định kiến trúc do ba sức ép khác nhau thúc đẩy. Thiết kế nguyên bản ở v8 giải quyết khoảng cách giữa trình duyệt và phần cứng bằng một proxy HTTP đơn giản cùng các module driver tách biệt. Lần gom về hw_drivers ở v12 giải quyết một bài toán bảo trì. Lần viết lại ở v19 giải quyết một bài toán bảo mật.
Việc đổi tên từ PosBox sang IoT Box chỉ là một thay đổi hai dòng trong manifest. Các lần viết lại mới là câu chuyện.
Với bất kỳ hệ thống Odoo nào, bản đồ phiên bản ở trên cho bạn biết cần nhìn vào đâu. Một cái tên module, tra đúng một lần, tiết kiệm được cả buổi chiều.
Nguồn
Toàn bộ file nằm trong repository mã nguồn mở odoo/odoo, dưới nhánh của phiên bản tương ứng (ví dụ 10.0, 12.0, 17.0).
Thời kỳ 1 — v8 đến v11 (PosBox):
[1] addons/hw_posbox_homepage/__openerp__.py (v8): manifest và mô tả module
[2] addons/hw_proxy/__manifest__.py (v10): phụ thuộc và mô tả module
[3] addons/hw_proxy/controllers/main.py (v10): vòng lặp controller, quét USB và serial
[4] addons/hw_escpos/__manifest__.py (v10): phụ thuộc của driver ESC/POS
[5] addons/hw_scanner/__manifest__.py (v10): driver máy quét mã vạch qua evdev
[6] addons/hw_scale/__manifest__.py (v10): driver cân qua RS-232
[7] addons/hw_posbox_upgrade/__manifest__.py (v10): module cập nhật từ xa
[8] addons/hw_posbox_homepage/__manifest__.py (v11): manifest trước khi đổi tên
Thời kỳ 2 — v12 (đổi tên IoT Box + gom module):
[9] addons/hw_posbox_homepage/__manifest__.py (v12): đổi tên thành IoT Box Homepage, đổi URL
[10] addons/hw_drivers/__manifest__.py (v12): module driver đã gom lại
Thời kỳ 3 — v13 đến v18 (IoT Box ổn định):
[11] addons/hw_drivers/__manifest__.py (v17): cấu trúc không đổi suốt thời kỳ ổn định
Thời kỳ 4 — v19 (viết lại từ đầu):
[12] addons/iot_drivers/__manifest__.py (v19): module driver mới thay cho hw_drivers
[13] addons/iot_base/__manifest__.py (v19): tiện ích mạng và bộ điều khiển thiết bị bằng JavaScript
[14] addons/iot_box_image/__manifest__.py (v19): module dựng image Raspberry Pi tự động
[15] addons/iot_box_image/build_image.sh (v19): script dựng image và cấu hình sau khởi tạo