Hexa's Blog

Tản mạn về doanh nghiệp

11/11/2026 @ Saigon etc

Tản màn về việc tạo dựng doanh nghiệp, những dòng tôi viết dưới đây là những suy nghĩ rời rạc từ chính những thứ tôi nhìn thấy.

  • Về mặt lý tưởng, chủ doanh nghiệp / công tyọ là những người nhìn thấy vấn đề của những người xung quanh, hay to tát hơn là cả xả hội. Họ tìm cách giải quyết vấn đề đó. Cuối cùng là họ được trả thưởng cho giải pháp mà họ đưa ra.
  • Lợi nhuận và tiền bạc là những thứ có được ở điểm cuối, hoặc ít nhất là gần cuối của quá trình trình:
    • Nhìn ra vấn đề
    • Tìm ra giải pháp khi này giải pháp vẫn trên giấy
    • Tạo ra giải pháp khi này giải pháp không còn trên giấy, nó đã có tính thực tiễn
    • Nhận phần thưởng

Tuy nhiên, ta cần phải làm rõ 5 điểm sau:

  • [1] Nhìn ra vấn đề, chưa chắc đã tìm ra giải pháp.

  • [2] Tìm ra giải pháp trên giấy rồi đó, nhưng chưa chắc đã tạo ra được giải pháp, hay làm nó trở nên thực tiễn hơn

  • [3] Tạo ra giải pháp rồi, đúng là nó đã có tính thực tiễn rồi, nhưng chưa chắc đã nhận được phần thưởng.

  • [4] Giá trị phần thưởng không tỷ lệ thuận, lũy tiến theo độ khó của vấn đề, hay nỗ lực để giải quyết vấn đề đó.

  • [5] Sau cùng, đây là ý quan trọng nhất. Việc tạo ra doanh nghiệp ‌/ công ty KHÔNG CÓ NGHĨA là đi giải quyết vấn đề của xã hội. Có thể, nó rất đơn giản là đi kiếm tiền. Hoàn toàn chả có gì nhân bản, nhân loại hay cao siêu ở đây cả.

    Họ kiếm tiền, mục đích tối thượng ở đây là kiếm tiền, tạo lợi nhuận, tối đa hóa lợi nhuận bằng mọi cách! Chấm hết!

    Đây là mô hình mà cá nhân tôi chán ghét nhất.


Khi tôi tạo doanh nghiệp, chắc chắn là tôi không chọn con đường này!

Cách build Monero GUI

09/11/2026 @ Saigon Cryptocurrency Node

I. Mở đầu

Nối tiếp bài viết Cách build Monero, bài viết này tập trung nói về cách build Monero-GUI, release v0.18.5.4. Sau khi build xong, bạn sẽ sử dụng Monero GUI dễ dàng hơn. Ở phiên bản này, vẫn đang sử dụng là QT5 cho giao diện GUI. Ở phiên bản kế tiếp sẽ là QT6

II. Nội dung

Bước 1: Cài đặt các thư viện liên quan

$ sudo dnf install make automake cmake ninja gcc-c++ boost-devel graphviz doxygen unbound-devel pkgconfig openssl-devel libcurl-devel hidapi-devel libusb-devel zeromq-devel libgcrypt-devel

Cái này dành cho qt5, khi monero-gui chuyển qua qt6, sẽ không chạy command sau:

$ sudo dnf install qt5-qtdeclarative-devel qt5-qtsvg-devel

Bước 2: Clone source code & repository phụ

$ git clone --recursive git@github.com:monero-project/monero-gui.git
$ cd monero-gui
$ git checkout v0.18.5.4
$ git submodule update --init --recursive

Ở lần build tới, để update các repository phụ, chạy command sau:

$ git submodule update --init --recursive

Bước 3: Build

$ make release

Và đây là kết quả thu được.

$ tree build/release/bin
build/release/bin
├── monerod
└── monero-wallet-gui

Khi bạn chạy monero-wallet-gui, mọi config của bạn bao gồm nơi chứa wallet và node nằm ở ~/.config/monero-project/monero-core.conf. Mỗi khi chạy lại, monero-wallet-gui sẽ đọc file này.

Bài viết đến đây là kết thúc! Nhân tiện thì giả sử bạn đang cần giao dịch ngay mà không muốn mất thời gian đồng bộ hóa giữ liệu. Bạn có thể sử dụng node của tôi: monero.hexalink.xyz:18081.

Node: monero.hexalink.xyz || Port: 18081
Node: monero.hexalink.xyz || Port: 18081

Nếu bạn sử dụng node monero.hexalink.xyz:18081 này, tôi khẳng định là tôi mới giúp bạn tiết kiệm được ít nhất là 3-5 ngày đồng bộ hóa dữ liệu rồi đó!

III. Lời cảm ơn

Tôi đã gặp khó khăn trong lúc build monero-gui, trong release v0.18.5.4, source code sử dụng QT5, tuy nhiên hướng dẫn mà tôi đọc lại thuộc branch master, lại là dành cho QT6. Do đó mà thiếu qt5-qtdeclarative-devel qt5-qtsvg-devel.

Mọi thứ đã có thể lâu hơn rất nhiều nếu không có sự hỗ trợ của ChatGPT. Cảm ơn rất nhiều!

IV. Trích dẫn

Cách build Monero

08/11/2026 @ Saigon Cryptocurrency Node

I. Mở đầu

Bài viết này tập trung vào việc build monero từ source code trên Fedora, mọi thông tin trên bài viết này được lấy từ Repo Monero, file Readme.md.

Lưu ý là Monero hiện tại không phải là monorepo. Nó tách riêng 2 phiên bản:

Bài viết này tập trung vào chỉ dẫn build cái đầu tiên: Monero

II. Nội dung

Bước 1: Cài đặt các thư viện liên quan

$ sudo dnf install gcc gcc-c++ cmake pkgconf boost-devel openssl-devel zeromq-devel unbound-devel \
           libsodium-devel libunwind-devel ccache doxygen graphviz hidapi-devel libusb1-devel \
           protobuf-devel protobuf-compiler rust cargo

Bước 2: Clone source code & repository phụ

$ git clone --recursive https://github.com/monero-project/monero
$ git submodule init && git submodule update

Lần sau, bạn chỉ cần

  • $ git fetch origin: để kéo mã nguồn mới về
  • $ git checkout TAG: để chọn tag cho mã nguồn
  • $ git submodule init && git submodule update: để cài đặt và update các repository phụ

Bước 3: Build

$ make -j $(nproc)

Đây là kết quả của tôi sau khi build phiên bản v0.18.5.4.

➜ monero (85f036ba7) ✗  tree build/Linux/_HEAD_detached_at_v0.18.5.4_/release/bin
build/Linux/_HEAD_detached_at_v0.18.5.4_/release/bin
├── monero-blockchain-ancestry
├── monero-blockchain-depth
├── monero-blockchain-export
├── monero-blockchain-import
├── monero-blockchain-mark-spent-outputs
├── monero-blockchain-prune
├── monero-blockchain-prune-known-spent-data
├── monero-blockchain-stats
├── monero-blockchain-usage
├── monerod
├── monero-gen-ssl-cert
├── monero-gen-trusted-multisig
├── monero-wallet-cli
└── monero-wallet-rpc

Bài viết đến đây là kết thúc. Lần tới khi tôi build monero, tôi sẽ tiết kiệm được 2 phút cuộc đời.

III. Trích dẫn

Cách test tốc độ mạng nội bộ với iperf3

05/11/2026 @ Saigon Linux

I. Mở đầu

Để test tốc độ mạng giữa các máy tính nội bộ, chúng ta hoàn toàn có thể cân nhắc sử dụng iperf3.

Tôi biết đến công cụ này khi đang tìm cách backup blockchain bitcoin. Thực sự mà nói, tốc độ copy data từ máy tính cá nhân (client) đến máy NAS (server) khá là chậm.

Điều này khiển tôi phải thực sự kiểm tra network nội bộ của mình. Công cụ mà tôi sử dụng là iperf3.

Nó có sẵn trên máy NAS server (Unraid OS) và cả trên (Fedora). Thực sự là tôi chả cần phải cài gì thêm cả. Khá là tiện. 😙

II. Nội dung

[1] Mô hình truyền dữ liệu
[1] Mô hình truyền dữ liệu

Bước 1: Trên máy NAS Server, chạy iperf3 ở chế độ server

Dựa theo hình vẽ trên, máy server có ip là 192.168.1.3, chạy command sau:

$ iperf3 -s
# Command outputs
-----------------------------------------------------------
Server listening on 5201 (test #1)
-----------------------------------------------------------

Bước 2: Trên máy client, chạy iperf3 ở chế độ client và kết nối đến server

Ở trên máy client, có ip là 192.168.1.2, chạy command sau:

$ iperf3 -c 192.168.1.3
# Command outputs
Connecting to host 192.168.1.3, port 5201
[  5] local 192.168.2.5 port 60954 connected to 192.168.1.3 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  57.5 MBytes   482 Mbits/sec    0    317 KBytes
[  5]   1.00-2.00   sec   112 MBytes   936 Mbits/sec    0    445 KBytes
[  5]   2.00-3.00   sec   112 MBytes   936 Mbits/sec    0    469 KBytes
[  5]   3.00-4.00   sec   112 MBytes   941 Mbits/sec    0    492 KBytes
[  5]   4.00-5.00   sec   112 MBytes   938 Mbits/sec    0    492 KBytes
[  5]   5.00-6.00   sec   112 MBytes   940 Mbits/sec    0    492 KBytes
[  5]   6.00-7.00   sec  72.2 MBytes   606 Mbits/sec    0    492 KBytes
[  5]   7.00-8.00   sec  54.9 MBytes   460 Mbits/sec    0    492 KBytes
[  5]   8.00-9.00   sec  81.2 MBytes   682 Mbits/sec    0    492 KBytes
[  5]   9.00-10.00  sec   111 MBytes   933 Mbits/sec    0    519 KBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec   936 MBytes   785 Mbits/sec    0    sender
[  5]   0.00-10.00  sec   934 MBytes   784 Mbits/sec         receiver

Dựa vào command trên, ta thấy được tốc độ upload dữ liệu lên server là 785 Mbits/sec, tương đương với 98MB/sec.

Bài viết kết thúc, hi vọng giúp ai đó tiết kiệm 2 phút cuộc đời!

3. Trích dẫn

Lưu trữ dữ liệu với rsync

04/11/2026 @ Saigon Linux

1. Mở đầu

Tôi cần phải nói là tôi không bao giờ nghĩ đến việc mình sẽ viết blog về chủ đề này.

Rsync - Nó quá đơn giản, nó quá dễ dàng.

Nhưng mà mỗi lần đụng đến rsync, tôi đều phải rsync --help hoặc tệ hơn là dùng chatgpt. Tôi luôn đặt ra những ranh giới cho chính mình khi nào cần và khi nào không cần dùng đến chatgpt. Và tôi cảm giác thật là con người khi làm như vậy.

Nói sơ qua một chút thì rsync là một command dùng để sao lưu dữ liệu. Tôi đang có nhu cầu sao lưu dữ liệu của bitcoin knots (Bitcoin Blake2B). Việc này sẽ giúp tôi giảm thời gian khi triển khai node mới.

[1] Sao lưu bitcoin
[1] Sao lưu bitcoin

2. Nội dung

$ rsync --archive --human-readable --info=progress2 \
  /mnt/disk_2/CryptoCurrency/Bitcoin_knots/ \
  /mnt/disk_2/CryptoCurrency/Bitcoin_knots_height_980396

Tôi sẽ giải thích các flag:

  • --archive: chế độ lưu trữ, nó là tổng hợp của rất nhiều flag. (xem kỹ ở đây)
  • --human-readable: khi show log, dữ liệu dễ đọc hơn
  • --info=progress2: hiển thị progress với phần trăm

Lưu ý:

  • Path của thư mục gốc SRC /mnt/disk_2/CryptoCurrency/Bitcoin_knots/ sẽ cần / ở cuối nếu mục đích là copy toàn bộ nội dung của nó đến thư mục đích đến DEST.
  • Nếu mà không có / ở cuối của SRC, rsync sẽ tạo thư mục con có tên là Bitcoin_knots nằm bên trong DEST.

So sánh các loại HDD của Western Digital(repost)

12/10/2026 etc

[1] danh sách HDD của Western Digital
[1] danh sách HDD của Western Digital

Chú thích:

  • CMR (Conventional Magnetic Recording): Ghi từ tính thông thường
  • SMR (Shingled Magnetic Recording): Ghi từ tính kiểu lợp ngói
Tiêu chí CMR SMR
Tên đầy đủ Conventional Magnetic Recording Shingled Magnetic Recording
Cách bố trí track Không chồng lấn Chồng lấn một phần
Mật độ lưu trữ Thấp hơn Cao hơn
Ghi tuần tự Tốt Tốt
Ghi ngẫu nhiên Tốt Chậm hơn
Ghi đè dữ liệu Tốt Chậm hơn
Giá trên mỗi TB Cao hơn Rẻ hơn
Ứng dụng điển hình NAS, máy chủ, workload hỗn hợp Backup, lưu trữ lạnh, dữ liệu ít thay đổi

SMR do thiết kế theo kiểu lợp ngói, các rãnh từ bị chồng lấn(1), đầu ghi thì lớn(2). Hai nguyên nhân trên gây ảnh hưởng không tốt đến quá trình ghi dữ liệu.

Khi ghi dữ liệu trên một rãnh từ tính, việc này hoàn toàn có thể làm sai dữ liệu ở các rãnh từ tính lân cận.

Rãnh từ tính xếp lớp, đầu ghi thì lại to. Chỉ cần ghi dữ liệu trên một rãnh từ tính, HDD thực tế phải ghi dữ liệu trên hai rãnh lân cận, nhằm đảm bảo độ chính xác 🥲!

Tóm tắt lại thì nếu mà mua ổ đĩa làm NAS, 100% là nên chọn CMR - Conventional Magnetic Recording 👨🏻‍🔧.

References

Cách chọn first-child trong mối quan hệ one-many trong database postgresql

08/10/2026 @ Saigon SQL

Như mọi khi, trước khi đi sâu hơn vào mặt kỹ thuật, tôi cần phải miêu tả vấn đề tôi đang gặp phải. Đây là database diagram.

[1] Database diagram
[1] Database diagram

Tôi có hai table products và product_variants. Một product sẽ có nhiều product_variants. Các product_variants được sắp xếp theo thứ tự display_order.

Tôi muốn khai thác một số field của product và một số field của product_variants đầu tiên (first-child).

Table products (Đã tối giản)

id name
1 Vàng miếng SJC
2 Vàng nhẫn DOJI Quý-Hưng-Thịnh-Vượng-Vinh

Table product_variants(Đã tối giản)

id product_id name price display_order
1 1 miếng 10 chỉ 150,000,000 VND 1
2 1 miếng 5 chỉ 75,000,000 VND 2
3 1 miếng 2 chỉ 30,000,000 VND 3
4 1 miếng 1 chỉ 15,000,000 VND 4
         
5 2 nhẫn Vinh - 10 chỉ 140,000,000 1
6 2 nhẫn Vượng - 5 chỉ 70,000,000 2
7 2 nhẫn Thịnh - 2 chỉ 28,000,000 VND 3
8 2 nhẫn Hưng - 1 chỉ 14,000,000 VND 4
9 2 nhẫn Quý - 0.5 chỉ 7,000,000 VND 5

Và tôi muốn sau khi query sẽ có kết quả như sau, bạn nhìn kỹ nhé, tôi chỉ lấy product_variant có display_order nhỏ nhất:

product_id product_name product_variant_id product_variant_name product_variant_price
1 Vàng miếng SJC 1 miếng 10 chỉ 150,000,000
2 Vàng nhẫn DOJI Quý-Hưng-Thịnh-Vượng-Vinh 5 nhẫn Vinh - 10 chỉ 140,000,000

Câu hỏi đã là rõ ràng, bây giờ là câu query, lưu ý là bạn sẽ cần phải sử dụng distinct on và order.

  • order nhằm giúp sắp xếp dữ liệu theo display order
  • distinct on là để loại trừ dữ liệu trùng lặp product_id
  • Các bươc sẽ như sau:
    • [1] Chắc chắn là phải có left join
    • [2] Sắp xếp theo thứ tự product_id asc & display_order asc
    • [3] Dùng distinct on (product_id) để chỉ giữ lại row đầu tiên của mỗi product_id
select distinct on (p.id)
    p.id as product_id, p.name as product_name,
    pv.id as product_variant_id, pv.name as product_variant_name, pv.price as product_variant_price
from products p
left join product_variants pv on p.id = pv.product_id
order by p.id asc, pv.display_order asc;

Lưu ý, đây không phải là cách query nhanh nhất. Tôi thấy có sequence scan trong Explain Execution Plan. Hiện tại thì data của tôi vẫn chưa đủ lớn để tối ưu hóa. Nó nên dành cho bài post khác chứ không phải là ngay bây giờ.

[2] Explain Execution Plan
[2] Explain Execution Plan

Đây không phải là cách nhanh nhất. Tuy nhiên, đây chắc chắn là câu query dễ chịu nhất để viết và đọc.

Cách thay đổi thứ tự record entry với database postgresql

25/09/2026 @ Saigon SQL

Tôi sẽ mở đầu bài viết này từ vấn đề tôi đã gặp phải!

Trong dự án Dolphin Shopkit, mô hình dữ liệu bao gồm Collection (Bộ sưu tập) và Product (Sản phẩm). Đây là mối quan hệ many-many, Một bộ sưu tập có nhiều sản phẩm, một sản phẩm thuộc nhiều bộ sưu tập.

Nhu cầu của tôi là: thay đổi thứ tự hiển thị các sản phẩm này trong bộ sưu tập. Tâm sự một chút thì đây là lần đầu tiên tôi làm tính năng kiểu như thế này! Và tôi tin là trong tương lai tôi sẽ còn gặp lại nó.

Bài post như là cách để tôi tra cứu trong tương lai khi cần thiết.

Để dễ hình dung, đây là database schema!

Product & Product Variant schema
Product & Product Variant schema

Hãy chú ý cột product_display_order trong bảng collections_products.


Cách làm sẽ là như sau, tôi sẽ chỉ phương hướng, chút nữa sẽ là mã nguồn để tham khảo.

  • Phía UI sẽ có các thao tác drag-n-drop (kéo thả), client sẽ gửi lên server danh sách collection_product_id_list với thứ tự mới.
    • ví dụ: collection_product_id_list: 1004, 1003, 1002, 1001, 1000.
  • Dưới database, sẽ chạy sql query cập nhật tất cả các collections_products entry record với giá trị cột product_display_order mới.
collection_product_id product_display_order(Cũ) product_display_order(Mới)
1000 1 5
1001 2 4
1002 3 3
1003 4 2
1004 5 1

Keyword để tra cứu ở đây là ORDINALITY (Postgresql). Mục tiêu là từ danh sách collection_product_id với trật tự mới, ta sẽ thêm cột dữ liệu tuần tự product_display_order.

SELECT * FROM unnest(array[1004, 1003, 1002, 1001, 1000]::bigint[])
WITH ORDINALITY AS t(collection_product_id, product_display_order)
collection_product_id product_display_order
1004 1
1003 2
1002 3
1003 4
1004 5

Tiếp theo ta sẽ update bảng collections_products, dựa vào dữ liệu ở bước trên.

UPDATE collections_products cp
SET product_display_order = x.product_display_order
FROM (SELECT * FROM unnest(array[1004, 1003, 1002, 1001, 1000]::bigint[])
      WITH ORDINALITY AS t(collection_product_id, product_display_order)) as x
WHERE cp.id = x.collection_product_id;

Bạn thấy đó, kết quả là chỉ cần duy nhất một câu SQL là giải quyết được bài toán thay đổi thứ tự.


Hey, nhưng nếu bạn nghĩ rằng bài viết này vậy là xong thì chưa đâu nhé! Sẽ còn vài điều quan trọng nữa, ít nhất là cho tôi - người dùng Phoenix Webframework, LiveView và SortableJS để thay đổi vị trí Product trong Collection

[1] Về phía frontend (HTML), đây là code sample, chú ý phx-hook

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Name</th>
      <th>Action</th>
    </tr>
  </thead>
  <tbody id="collection_product_list" phx-hook="AdminCollectionSortableProductHook">
    <tr id={row_id} sortable-collection-product-id={collection_product.id}>
      <td>{collection_product.product_id}</td>
      <td>{collection_product.product_name}</td>
      <td>
        <div class="btn btn-square btn-outline btn-sm border-transparent">
          <span class="iconify lucide--grip-vertical size-4"></span>
        </div>
      </td>
    </tr>
  </tbody>
</table>

[2] Về phía frontend (Javascript), tôi sử dụng SortableJS đây là code sample.

import Sortable from 'sortablejs';
const AdminCollectionSortableProductHook = {
  mounted() {
    this.sortable = new Sortable(this.el, this.sortableOptions());
  },
  sortableOptions() {
    const hook = this;
    const element = this.el;
    return {
      dataIdAttr: "sortable-collection-product-id",
      handle: ".lucide--grip-vertical",
      onEnd: function(event) {
        const collectionProductIdList = hook.sortable.toArray();
        hook.pushEventTo(element, "reorder_collection_product_list", {
          collection_product_id_list: collectionProductIdList
        });
      }
    }
  },

  destroyed() {
    this.sortable.destroy();
  }

}

export default AdminCollectionSortableProductHook;

[3] Phía LiveView khi handle_event/3 - reorder_collection_product_list

def handle_event("reorder_collection_product_list", %{"collection_product_id_list" => collection_product_id_list}, socket) do
  Collections.reorder_collection_product_list(collection_product_id_list)
  {:noreply, socket}
end

[4] Phía Ecto (Database Libary) khi chạy sql query

  def reorder_collection_product_list(collection_product_id_list) do
    mod_collection_product_id_list = collection_product_id_list
    |> Enum.map(&String.to_integer/1)
    query = """
     UPDATE collections_products cp
     SET product_display_order = x.product_display_order
     FROM (SELECT * FROM unnest($1::bigint[])
           WITH ORDINALITY AS t(collection_product_id, product_display_order)) as x
     WHERE cp.id = x.collection_product_id;
    """
    Ecto.Adapters.SQL.query(Repo, query, [mod_collection_product_id_list])
  end

Credit: để giải quyết bài toán này, tôi đã dùng ChatGPT để hỗ trợ. Nếu không có ChatGPT, khả năng cao là tôi sẽ mở database session, sau đó update từng dòng một, rồi commit session. Phương pháp sẽ không thể nào gọn gàng như hiện tại nếu thiếu ORDINALITY.

Nhân tiện, từ khi tôi triển khai tính năng này đến thời điểm viết bài cũng cả tháng rồi, tuy nhiên, rất khó nhớ. Tôi viết blog cũng vì lý do này, mỗi lần viết là lại thêm một lần nhớ, ORDINALITY này rất thú vị, để quên thì thật đáng tiếc.

Reference List

How to record video for K1 Max with ffmpeg?

08/05/2026 @ Saigon 3D Print

Step 1: Find K1 Max livestream url

  • stream: http://K1_MAX_IP:8080/?action=stream
  • snapshot: http://K1_MAX_IP:8080/?action=snapshot

For more details, go to http://K1_MAX_IP:8080

Step 2: Use ffmpeg command to take stream input (mjpg) and create video

Find supportive encoder on your machine, ffmpeg -encoders | grep 264

I use encoder libvpx-vp9 to create

ffmpeg -i http://K1_MAX_IP:8080/?action=stream \
       -c:v libvpx-vp9 -vf fps=12 \
       -crf 30 -b:v 0 \
       -f segment \
       -segment_time 3600 \
       -reset_timestamps 1 \
       -strftime 1 \
       "%Y-%m-%dT%H%M%S.webm"
  • -c:v: Use video encoder named libvpx-vp9
  • fps=12: Use 12 frame per second for output video
  • -crf 30: Constant Rate Factor. It refers to video quality. 0 means the best quality, 40+ means super compressed, worse quality. (tbh, I only test Constant Rate Factor 30)
  • -b:v 0: Do not configure fixed video bitrate.
  • -f segment -segment_time 3600: For each one hour, create a video file.
  • -reset_timestamps 1: when create video, start timestamps at 1 (by default, it starts from the end timestamps of previous video)
  • -strftime 1 "%Y-%m-%dT%H%M%S.webm": Output with filename format.

Credit

ChatGPT helps me so much with ffmpeg, without it, I can’t understand all the ffmpeg options quickyly.

ffmpeg -i http://192.168.2.253:8080/\?action\=stream \
       -c:v h264_nvenc -vf fps=12 -cq 28  -preset p5 -pix_fmt yuv420p \
       -f segment -segment_time 3600 -reset_timestamps 1 \
       -strftime 1 \
       "%Y-%m-%dT%H%M%S.mp4"

Kỹ thuật sử dụng cảm biến hiệu điện thế ZMPT101B

11/04/2026 @ Saigon Arduino

Bài viết này miêu tả những kỹ thuật và lưu ý mà tôi đã học được để sử dụng cảm biển ZMPT101B.

  1. Ngay khi mua cảm biến ZMPT101B, cần tinh chỉnh triết áp với Serial Plotter, xung không nên bị bè ở đầu, mất góc ở đỉnh.
  2. Tiếp theo, tìm giá trị tham chiếu zero point ở 0 voltage AC.
  3. Cuối cùng là tìm hệ số chuyển đổi Scale.
  4. Mỗi cảm biến lại có những sai số khác nhau, hoàn toàn không thể sử dụng hệ số chuyển đổi (SCALE)của cảm biến này áp dụng cho cảm biển khác, mặc dù là cùng một model.

1. Vặn triết áp và theo dõi trên Serial Plotter

Trước khi điều chỉnh, bắt buộc phải cắm dây điện xoay chiều vào cảm biến ZMPT101B. Bên cạnh đó, phải có phần mềm Arduino-IDE, mở công cụ Serial Plotter. Arduino-CLI không có tính năng này.

Việc điều chỉnh triết áp dựa trên các tiêu chí sau:

  • Giá trị / biên độ là lớn nhất khi xem trên Serial
  • Đỉnh của sóng không được phép bè ra, mất góc nhọn.
void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.println(analogRead(A0));
  delayMicroseconds(1000);
}
[1] Serial Plotter sau khi tinh chỉnh triết áp
[1] Serial Plotter sau khi tinh chỉnh triết áp

2. Tìm kiếm vị trí tham chiếu (Zero Point)

Định nghĩa vị trí tham chiếu (Zero Point) là vị trí mà ở đó không ghi nhận giá trị hiệu điện thế điện xoay chiều.

Để tim giá trị này, ta tháo toàn bộ dây AC cần đo ra khỏi cảm biến và chạy đoạn code sau. Ở phía bên tôi, tôi thu được giá trị là 511.

const int WINDOW_TIME = 1000;
const int ZMPT101B_PIN = A0;

unsigned long sumOfSampleData = 0;
unsigned long sampleCounter = 0;
unsigned long startTime;
void setup() {
  Serial.begin(115200);
  startTime = millis();
}

void loop() {

  int sampleData = analogRead(ZMPT101B_PIN);
  sumOfSampleData += sampleData;
  sampleCounter += 1;

  if(millis() - startTime >= WINDOW_TIME) {
    int sampleAvg = sumOfSampleData / sampleCounter;
    Serial.print("Zero Point: ");
    Serial.println(sampleAvg);
    sumOfSampleData = 0;
    sampleCounter = 0;
    startTime = millis();
  }
}
14:11:15.109 -> Zero Point: 511
14:11:16.106 -> Zero Point: 511
...
14:11:22.113 -> Zero Point: 511
14:11:23.109 -> Zero Point: 511

Bên cảm biến của tôi, kết quả là 511.

3. Tìm hệ số chuyển đổi scale

Ở đoạn này bắt buộc phải có đồng hồ chính xác bên ngoài, đo trước hiệu điện thế điện xoay chiều AC. Sau đó, gài giá trị vào EXPECTED_VOLTAGE ở đoạn code dưới đây và chạy. Khi chạy code, đảm bảo dây AC cần đo đã gắn vào cảm biến ZMPT101B chắc chắn.

Ở bên nhà tôi, hiệu điện thế đo được là 233V (Hoàn toàn không phải là 220V đâu nhé). Giá trị scale tôi thu được là 1.38

#include <math.h>

const int ZMPT101B_PIN       = A0;
const float EXPECTED_VOLTAGE = 233.0;
const unsigned int zeroPoint = 511;
const int WINDOW_TIME        = 1000;

unsigned long  sumOfSquareOfSampleDataMinusZeroPoint = 0;
unsigned long  sampleCounter                         = 0;
unsigned long  startTime;
float          rootMeanSquare = 0.0f;

void setup() {
  Serial.begin(115200);
  startTime = millis();
}

void loop() {
  int sampleData = analogRead(ZMPT101B_PIN);
  sumOfSquareOfSampleDataMinusZeroPoint += (sampleData - zeroPoint) * (sampleData - zeroPoint);
  sampleCounter += 1;

  if (millis() - startTime >= WINDOW_TIME) {
    rootMeanSquare = sqrt((float) sumOfSquareOfSampleDataMinusZeroPoint / sampleCounter);
    float scale = EXPECTED_VOLTAGE / rootMeanSquare;
    Serial.print("Root Mean Square: ");
    Serial.print(rootMeanSquare);
    Serial.print(" (in ");
    Serial.print(sampleCounter);
    Serial.print(" samples). Scale: ");
    Serial.println(scale);

    sumOfSquareOfSampleDataMinusZeroPoint = 0;
    sampleCounter = 0;
    startTime = millis();
  }
}
14:04:15.083 -> Voltage: 233.00 | Root Mean Square: 168.56 (in 17856 samples) | Scale: 1.38
14:04:17.108 -> Voltage: 233.00 | Root Mean Square: 168.53 (in 17855 samples) | Scale: 1.38
14:04:19.099 -> Voltage: 233.00 | Root Mean Square: 168.69 (in 17855 samples) | Scale: 1.38
14:04:21.088 -> Voltage: 233.00 | Root Mean Square: 168.73 (in 17855 samples) | Scale: 1.38

Phương pháp như sau:

  • Tính toán giá trị hiệu dụng (Root Mean Square - RMS) của giá trị sensor với giá trị tham chiếu (zero point) trong một khoảng thời gian (WINDOW_TIME)
  • Kết hợp với giá trị hiệu điện thế AC đo đạc từ một đồng hồ đo điện khác (EXPECTED_VOLTAGE)
  • Tồn tại sự tương quan giữa giá trị hiệu dụng của sensor và hiệu điện thế EXPECTED_VOLTAGE, tỷ lệ này ta gọi là SCALE(Hệ số chuyển đổi)
  • Tính toán tỷ lệ SCALE = EXPECTED_VOLTAGE / RMS

4. Tìm giá trị hiệu điện thế tức thời

Sau khi tìm được giá trị SCALE - hệ số chuyển đổi, ví dụ cảm biến cho ta giá trị là X, muốn tìm ra hiệu điện thế tức thời, ta làm như sau:

Vtức_thời = (X - ZeroPoint) * Scale

5. Lưu ý

  • Một khi đã tinh chỉnh xong, không được phép vặn triết áp. Nếu mà lỡ vặn, phải làm lại từ đầu.