Hexa's Blog

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.

Tìm kiếm I2C address cho board Arduino

09/04/2026 @ Saigon Arduino

Tôi không sử dụng Arduino-IDE mà sử dụng arduino-cli để tạo project ($ arduino-cli sketch new i2c_address_finder). Sau khi tạo xong project, hãy thay đổi nội dung của file .ino giống như dưới đây, trước khi copy-paste, hay để tôi giải thích về quy trình:

  • Thư viện Wire dành cho giao thức với I2C. Method Wire.begin() dùng để xác lập board arduino chúng ta đang sử dụng sẽ đóng vai trò là MAIN. Các linh kiện sensor, màn hình bên ngoài sẽ đóng vai trò là SUB.
  • Chúng ta muốn là arduino khi tìm được address I2C sẽ in ra trên màn hình monitor cho chúng ta xem. Giai đoạn này sẽ phụ thuộc vào Serial
    • Serial.begin(__baudrate__): Xác định truyền tín hiệu với giao thức Serial cùng baudrate. Trong code là 9600 (Liên quan đến bước )
    • Serial.print("__text__"): In text trên màn hình monitor
  • Địa chỉ I2C được miêu tả bởi 7 bit, có số lượng tối đa là 128 giá trị. Theo như tài liệu, trong số 128 giá trị khả dĩ, sẽ có các địa chỉ đặc biệt, không được sử dụng cho thiết bị SUB. Trong code dưới đây, chúng ta vẫn sẽ quét qua. Và sử dụng hai method sau:
    • Wire.beginTransmission(__byte_address__): Khởi tạo kết nối đến SUB với địa chỉ __byte_address__
    • Wire.endTransmission(): Đóng kết nối, nếu giá trị trả về bằng 0 thì nghĩa là đã kết nối và đóng thành công. Ta tìm được địa chỉ I2C hợp lệ.
    • Serial.println(i, HEX): In giá trị address ở dạng hexadecimal
#include <Wire.h>

void setup() {
  Wire.begin();
  Serial.begin(9600);
  delay(1000);
  Serial.println("Hexalink.xyz");
  Serial.println("Scanning...");
}

void loop() {
  for (byte i = 0; i < 128; i++) {
    Wire.beginTransmission(i);
    if (Wire.endTransmission() == 0) {
      Serial.print("Found at 0x");
      Serial.println(i, HEX);
    }
  }
  delay(2000);
}

Bước 1: Find board & port

$ arduino-cli board list

Port         Protocol Type              Board Name  FQBN            Core
/dev/ttyACM0 serial   Serial Port (USB) Arduino UNO arduino:avr:uno arduino:avr
  • Fully Qualified Board Name (FQBN): arduino:avr:uno
  • Port: /dev/ttyACM0

Bước 2: Compile

$ arduino-cli compile --fqbn arduino:avr:uno

Sketch uses 3704 bytes (11%) of program storage space. Maximum is 32256 bytes.
Global variables use 428 bytes (20%) of dynamic memory, leaving 1620 bytes for local variables. Maximum is 2048 bytes.

Bước 3: Upload

$ arduino-cli upload --fqbn arduino:avr:uno --port /dev/ttyACM0

New upload port: /dev/ttyACM0 (serial)

Bước 4: Monitor

$ arduino-cli monitor -p /dev/ttyACM0 --config  9600

Monitor port settings:
  baudrate=9600
  bits=8
  dtr=on
  parity=none
  rts=on
  stop_bits=1

Connecting to /dev/ttyACM0. Press CTRL-C to exit.
Scat 0x27
Scanning...
Found at 0x27
Found at 0x27

Xem trên monitor, địa chỉ I2C 0x27 đã tìm được.

Credit

Kiến thức này là tôi lấy được từ nhiều nguồn, bao gồm cả ChatGPT để hiểu I2C và Serial trong bài viết này. Cảm ơn ChatGPT.