Mod bus 가 이드 튜 토 리 얼 : grid - x / mod bus 에서 0 으로 프로젝트 전투 까지

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

Mod bus 가 이드 튜 토 리 얼 : grid - x / mod bus 에서 0 으로 프로젝트 전투 까지

grid-x/modbus是Go语言工业Modbus通讯开发的首选库,以简洁API、完善协议サポート和优秀并发性能著称。本記事从零基础出发,経由完全的项目実践案例,详细讲解grid-x/modbus的环境搭建、TCP/RTU通讯実装、データ型処理、エラートラブルシューティング和パフォーマンス最適化,帮助Go開発者たちは快速上手工业Modbus开发。

一、No licensing fees, open standardgrid-x/modbus

1.1 Go语言在工业控制中的优势

  • 部署简单:编译为单个二进制文件,无需运行时环境,适合エンベデッド·イン和工业现场部署
  • 并发原生:goroutine天然サポート多デバイス并发采集,无需复杂的线程管理
  • 性能优秀:编译型语言,运行速度接近C,适合高频データの収集
  • 跨プラットフォーム:Windows、Linux、ARM、MIPS均可编译运行
  • 生态丰富:HTTP、MQTT、データベースの種類、序列化等標準库齐全
  • 内存セキュリティ:自动垃圾回收,避免C语言的内存泄漏和指针エラー

1.2 grid-x/modbus库的核心设计

grid-x/modbus采用Handler + Client的分层架构:Handler负责底层传输(TCPconnect、Serial port configuration、CRC check),Client负责协议层(function code、データ编码)。这种设计使得切换伝送の方法只需更换Handler,业务代码无需修改。

// 架构示意
Handler(传输层)  →  Client(协议层)  →  业务代码
  TCP/RTU/ASCII       function code01-16         レジスタ読み書き
  连接管理            データ编码/解码       业务逻辑
  タイムアウト控制            異常処理            データストア

二、环境搭建与第一1つの手続き

2.1 インストールGo开发环境

# チェックGo版本(需要Go 1.18+)
go version

# 作成项目目录
mkdir modbus_demo && cd modbus_demo

# 初期化Goモジュール
go mod init modbus_demo

# インストールgrid-x/modbus
go get github.com/grid-x/modbus

2.2 第一个Modbus TCP程序

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/grid-x/modbus"
)

func main() {
    // 步骤1:作成TCPクライアント側は
    // 简洁モード:每次リクエスト自动建立和接続を閉じる
    client := modbus.TCPClient("192.168.1.100:502")

    ctx := context.Background()

    // 步骤2:Read and hold register(function code03)
    // 住所から。0开始,read101つのレジスタ
    // 戻り値は是[]byte,每2バイト为一1つのレジスタ(前の高さ。)
    data, err := client.ReadHoldingRegisters(ctx, 0, 10)
    if err != nil {
        log.Fatalf("Read failed:%v", err)
    }

    // 步骤3:解析データ
    fmt.Println("=== 読み取り结果 ===")
    for i := 0; i < len(data); i += 2 {
        regAddr := i / 2
        regValue := uint16(data[i])<<8 | uint16(data[i+1])
        fmt.Printf("register%d(4%04d):%d\n", regAddr, regAddr+1, regValue)
    }
}

2.3 运行与测试

# 运行程序
go run main.go

# もし没実際のデバイス,可以用Modbus Slave模拟
# 下载アドレス:https://www.modbustools.com/modbus_slave.html
# 設定:TCP Slave,ポート502,slave address1,レジスタを保持する。10个

三、Modbus TCP深入実践

3.1 连接管理与パフォーマンス最適化

简洁モード(modbus.TCPClient)每次リクエスト都会建立新的TCPconnect,高频リクエスト时性能较差。生产环境应使用Handlerモード复用连接。

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/grid-x/modbus"
)

func main() {
    // 作成Handler(可設定タイムアウト、駅からの住所等)
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.Timeout = 5 * time.Second  // タイムアウト5秒
    handler.SlaveID = 1                 // slave address1

    ctx := context.Background()

    // 建立连接(一次建立,多次使用)
    if err := handler.Connect(ctx); err != nil {
        log.Fatalf("接続の失敗:%v", err)
    }
    defer handler.Close()  // 程序结束时关闭

    client := modbus.NewClient(handler)

    // 连续読み取り100次(复用同一连接)
    start := time.Now()
    for i := 0; i < 100; i++ {
        _, err := client.ReadHoldingRegisters(ctx, 0, 10)
        if err != nil {
            log.Printf("No.%d次読み取り失敗:%v", i, err)
        }
    }
    fmt.Printf("100次読み取り耗时:%v\n", time.Since(start))
}

3.2 完全機能コード実践

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/grid-x/modbus"
)

func main() {
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveID = 1
    ctx := context.Background()
    handler.Connect(ctx)
    defer handler.Close()
    client := modbus.NewClient(handler)

    // ====== リードコイルを読む(function code01)======
    // リードコイル0-7的ステータス
    coilData, err := client.ReadCoils(ctx, 0, 8)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("=== Coil status ===")
    for i := 0; i < 8; i++ {
        state := (coilData[i/8] >> uint(i%8)) & 0x01
        fmt.Printf("コイル%d:%s\n", i, map[byte]string{0: "OFF", 1: "ON"}[state])
    }

    // ====== write single coil(function code05)======
    // 0xFF00 = ON,0x0000 = OFF
    _, err = client.WriteSingleCoil(ctx, 0, 0xFF00) // コイル0置ON
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("コイル0已置ON")

    // ====== write multiple coils(function code0F)======
    // 一次設定コイル0-7:0x55 = 01010101 = コイル0,2,4,6为ON
    _, err = client.WriteMultipleCoils(ctx, 0, 8, []byte{0x55})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("コイル0-7已批量設定")

    // ====== read discrete inputs(function code02)======
    inputData, err := client.ReadDiscreteInputs(ctx, 0, 4)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("=== ショップ型入力 ===")
    for i := 0; i < 4; i++ {
        state := (inputData[i/8] >> uint(i%8)) & 0x01
        fmt.Printf("入力%d:%d\n", i, state)
    }

    // ====== read holding registers(function code03)======
    regData, err := client.ReadHoldingRegisters(ctx, 0, 5)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("=== レジスタを保持する。 ===")
    for i := 0; i < len(regData); i += 2 {
        value := uint16(regData[i])<<8 | uint16(regData[i+1])
        fmt.Printf("register%d:%d\n", i/2, value)
    }

    // ====== read input registers(function code04)======
    analogData, err := client.ReadInputRegisters(ctx, 0, 2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("=== 入力レジスタ。(模拟量)===")
    for i := 0; i < len(analogData); i += 2 {
        value := uint16(analogData[i])<<8 | uint16(analogData[i+1])
        fmt.Printf("模拟量%d:%d(元の值)\n", i/2, value)
    }

    // ====== write single register(function code06)======
    _, err = client.WriteSingleRegister(ctx, 10, 500)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("register10書き込み済み。500")

    // ====== write multiple registers(function code10)======
    // 写入レジスタ20-22:值为100, 200, 300
    writeData := []byte{
        0x00, 0x64, // 100
        0x00, 0xC8, // 200
        0x01, 0x2C, // 300
    }
    _, err = client.WriteMultipleRegisters(ctx, 20, 3, writeData)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("register20-22已一括書き込みき込み入")
}

四、データ型処理実践

4.1 16ビット整数(記号付き。/符号なし。)

// Parse16ビット符号なし整数(0-65535)
func parseUint16(data []byte, index int) uint16 {
    return uint16(data[index*2])<<8 | uint16(data[index*2+1])
}

// Parse16ビット符号付き整数(-32768到32767)
func parseInt16(data []byte, index int) int16 {
    return int16(uint16(data[index*2])<<8 | uint16(data[index*2+1]))
}

// 使用例の例
data, _ := client.ReadHoldingRegisters(ctx, 0, 4)
temp := parseInt16(data, 0)      // temperature(記号付き。,可能为负)
humidity := parseUint16(data, 1) // 湿度(符号なし。)
fmt.Printf("temperature:%d°C,湿度:%d%%\n", temp, humidity)

4.2 32ビット浮動小数点数(最も一般的な)

import "math"

// 32ビット浮動小数点数有4种バイトオーダー,必须機器の確認使用哪种
// 最も一般的な的是ABCD(ビッグエンディアン序)和CDAB(Word exchange)

// ABCDビッグエンディアン序:reg[0]存高16位,reg[1]存低16位
func parseFloat32ABCD(data []byte) float32 {
    bits := uint32(data[0])<<24 | uint32(data[1])<<16 |
        uint32(data[2])<<8 | uint32(data[3])
    return math.Float32frombits(bits)
}

// CDABWord exchange:reg[0]存低16位,reg[1]存高16位
func parseFloat32CDAB(data []byte) float32 {
    bits := uint32(data[2])<<24 | uint32(data[3])<<16 |
        uint32(data[0])<<8 | uint32(data[1])
    return math.Float32frombits(bits)
}

// 写入32ビット浮動小数点数(ABCDビッグエンディアン序)
func float32ToBytesABCD(f float32) []byte {
    bits := math.Float32bits(f)
    return []byte{
        byte(bits >> 24), byte(bits >> 16),
        byte(bits >> 8), byte(bits),
    }
}

// 実践:読み取り温度センサー(浮動小数点数,占21つのレジスタ)
data, err := client.ReadHoldingRegisters(ctx, 100, 2) // 读レジスタ100-101
if err != nil {
    log.Fatal(err)
}
temperature := parseFloat32ABCD(data)
fmt.Printf("当前温度:%.2f°C\n", temperature)

// 写入温度を設定する。
setTemp := float32(25.5)
_, err = client.WriteMultipleRegisters(ctx, 200, 2, float32ToBytesABCD(setTemp))
if err != nil {
    log.Fatal(err)
}
fmt.Printf("温度を設定する。書き込み済み。:%.1f°C\n", setTemp)

4.3 32ビット整数与64ビット·データ

// 32ビット符号付き整数(占21つのレジスタ)
func parseInt32(data []byte) int32 {
    return int32(uint32(data[0])<<24 | uint32(data[1])<<16 |
        uint32(data[2])<<8 | uint32(data[3]))
}

// 32ビット符号なし整数
func parseUint32(data []byte) uint32 {
    return uint32(data[0])<<24 | uint32(data[1])<<16 |
        uint32(data[2])<<8 | uint32(data[3])
}

// 64ビット整数(占41つのレジスタ)
func parseInt64(data []byte) int64 {
    return int64(uint64(data[0])<<56 | uint64(data[1])<<48 |
        uint64(data[2])<<40 | uint64(data[3])<<32 |
        uint64(data[4])<<24 | uint64(data[5])<<16 |
        uint64(data[6])<<8 | uint64(data[7]))
}

// 64ビット浮動小数点数(double,占41つのレジスタ)
func parseFloat64(data []byte) float64 {
    bits := uint64(data[0])<<56 | uint64(data[1])<<48 |
        uint64(data[2])<<40 | uint64(data[3])<<32 |
        uint64(data[4])<<24 | uint64(data[5])<<16 |
        uint64(data[6])<<8 | uint64(data[7])
    return math.Float64frombits(bits)
}

4.4 ストリング·ストリング与ビット操作。

// 読み取りストリング·ストリング(すべてのレジスタ。存2个ASCIIcharacters)
func readString(client modbus.Client, ctx context.Context, addr, regCount uint16) (string, error) {
    data, err := client.ReadHoldingRegisters(ctx, addr, regCount)
    if err != nil {
        return "", err
    }
    // 去除末尾的空バイト
    result := string(data)
    for len(result) > 0 && result[len(result)-1] == 0 {
        result = result[:len(result)-1]
    }
    return result, nil
}

// ビット操作。:読み取りレジスタ中特ポジショニング的ステータス
func readBit(data []byte, regIndex, bitIndex int) bool {
    regValue := uint16(data[regIndex*2])<<8 | uint16(data[regIndex*2+1])
    return (regValue>>uint(bitIndex))&0x01 == 1
}

// ビット操作。:設定レジスタ中特ポジショニング
func setBit(regValue uint16, bitIndex int, value bool) uint16 {
    if value {
        return regValue | (1 << uint(bitIndex))
    }
    return regValue &^ (1 << uint(bitIndex))
}

五、Modbus RTUシリアルポート実践

5.1 シリアルポート設定与连接

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/grid-x/modbus"
)

func main() {
    // 作成RTU Handler
    handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    // Windows: handler := modbus.NewRTUClientHandler("COM3")

    // シリアルパラメータ設定(必须与スレーブデバイス一致)
    handler.BaudRate = 9600   // Baud rate:common9600, 19200, 38400, 115200
    handler.DataBits = 8      // data bit:通常为8
    handler.Parity = "N"      // check digit:N=无,E=偶,O=奇
    handler.StopBits = 1      // stop bit:1或2
    handler.SlaveID = 1       // slave address:1-247
    handler.Timeout = 5 * time.Second

    ctx := context.Background()

    // Open the serial port
    if err := handler.Connect(ctx); err != nil {
        log.Fatalf("シリアルポート打开失敗:%v", err)
    }
    defer handler.Close()

    client := modbus.NewClient(handler)

    // Read and hold register
    data, err := client.ReadHoldingRegisters(ctx, 0, 10)
    if err != nil {
        log.Fatalf("Read failed:%v", err)
    }

    for i := 0; i < len(data); i += 2 {
        value := uint16(data[i])<<8 | uint16(data[i+1])
        fmt.Printf("register%d:%d\n", i/2, value)
    }
}

5.2 RTUよくある質問トラブルシューティング

問題可能原因解決方法
シリアルポート打不开デバイス名エラー/Insufficient permissionsチェック/dev/ttyUSB0存在するか;Linux执行sudo usermod -aG dialout $USER
応答なし。A/Bライン·リバース/アドレスエラー交換A/B线;駅住所の確認拨码設定
CRCErrorBaud rate/チェックビット不マッチング確認マスター·マスター駅シリアルパラメータ完全一致
偶尔丢包干渉/終端抵抗バス两端加120Ω終端抵抗;シールドツイストペアの使用
レスポンス·タイムアウトスレーブ処理慢增大handler.Timeoutto5-10秒

六、完全项目実践:環境モニタリング系统

以下是一个完全的工业環境モニタリング系统,経由Modbus TCP読み取り温湿度センサーデータ,サポート多デバイス并发采集、データストア和異常告警。

package main

import (
    "context"
    "encoding/binary"
    "fmt"
    "log"
    "math"
    "sync"
    "time"

    "github.com/grid-x/modbus"
)

// SensorConfig センサーは設定
type SensorConfig struct {
    Name     string
    Address  string
    SlaveID  byte
    TempReg  uint16 // 温度レジスター·アドレス
    HumReg   uint16 // 湿度レジスター·アドレス
    TempType string // "int16" 或 "float32"
}

// SensorData センサーはデータ
type SensorData struct {
    Name      string
    Timestamp time.Time
    Temp      float64
    Humidity  float64
    Error     error
}

// ModbusReader 封装Modbusread
type ModbusReader struct {
    handler *modbus.TCPClientHandler
    client  modbus.Client
}

func NewModbusReader(address string, slaveID byte) (*ModbusReader, error) {
    handler := modbus.NewTCPClientHandler(address)
    handler.SlaveID = slaveID
    handler.Timeout = 3 * time.Second

    ctx := context.Background()
    if err := handler.Connect(ctx); err != nil {
        return nil, err
    }

    return &ModbusReader{
        handler: handler,
        client:  modbus.NewClient(handler),
    }, nil
}

func (r *ModbusReader) Close() {
    r.handler.Close()
}

// ReadFloat32 read32ビット浮動小数点数(ABCDビッグエンディアン序)
func (r *ModbusReader) ReadFloat32(ctx context.Context, addr uint16) (float32, error) {
    data, err := r.client.ReadHoldingRegisters(ctx, addr, 2)
    if err != nil {
        return 0, err
    }
    bits := uint32(data[0])<<24 | uint32(data[1])<<16 |
        uint32(data[2])<<8 | uint32(data[3])
    return math.Float32frombits(bits), nil
}

// ReadInt16 read16ビット符号付き整数
func (r *ModbusReader) ReadInt16(ctx context.Context, addr uint16) (int16, error) {
    data, err := r.client.ReadHoldingRegisters(ctx, addr, 1)
    if err != nil {
        return 0, err
    }
    return int16(binary.BigEndian.Uint16(data)), nil
}

// readSensor 読み取り单个センサーは
func readSensor(ctx context.Context, cfg SensorConfig, wg *sync.WaitGroup, results chan<- SensorData) {
    defer wg.Done()

    reader, err := NewModbusReader(cfg.Address, cfg.SlaveID)
    if err != nil {
        results <- SensorData{Name: cfg.Name, Error: err}
        return
    }
    defer reader.Close()

    data := SensorData{
        Name:      cfg.Name,
        Timestamp: time.Now(),
    }

    // 読み取り温度
    if cfg.TempType == "float32" {
        temp, err := reader.ReadFloat32(ctx, cfg.TempReg)
        if err != nil {
            data.Error = fmt.Errorf("温度読み取り失敗:%w", err)
            results <- data
            return
        }
        data.Temp = float64(temp)
    } else {
        temp, err := reader.ReadInt16(ctx, cfg.TempReg)
        if err != nil {
            data.Error = fmt.Errorf("温度読み取り失敗:%w", err)
            results <- data
            return
        }
        data.Temp = float64(temp) / 10.0 // 假设分辨率0.1°C
    }

    // 読み取り湿度
    hum, err := reader.ReadInt16(ctx, cfg.HumReg)
    if err != nil {
        data.Error = fmt.Errorf("湿度読み取り失敗:%w", err)
        results <- data
        return
    }
    data.Humidity = float64(hum) / 10.0 // 假设分辨率0.1%

    results <- data
}

func main() {
    sensors := []SensorConfig{
        {Name: "车间A", Address: "192.168.1.100:502", SlaveID: 1, TempReg: 0, HumReg: 2, TempType: "float32"},
        {Name: "车间B", Address: "192.168.1.101:502", SlaveID: 1, TempReg: 0, HumReg: 2, TempType: "float32"},
        {Name: "仓库", Address: "192.168.1.102:502", SlaveID: 1, TempReg: 0, HumReg: 2, TempType: "int16"},
    }

    ctx := context.Background()
    ticker := time.NewTicker(10 * time.Second) // 每10秒采集一次
    defer ticker.Stop()

    fmt.Println("環境モニタリング系统启动,每10秒采集一次...")

    for range ticker.C {
        var wg sync.WaitGroup
        results := make(chan SensorData, len(sensors))

        // 并发読み取りすべてのセンサーは
        for _, cfg := range sensors {
            wg.Add(1)
            go readSensor(ctx, cfg, &wg, results)
        }

        wg.Wait()
        close(results)

        // 結果の処理
        fmt.Printf("\n=== %s ===\n", time.Now().Format("2006-01-02 15:04:05"))
        for data := range results {
            if data.Error != nil {
                fmt.Printf("[%s] Error:%v\n", data.Name, data.Error)
                continue
            }
            fmt.Printf("[%s] temperature:%.1f°C,湿度:%.1f%%\n", data.Name, data.Temp, data.Humidity)

            // 異常告警
            if data.Temp > 35 {
                fmt.Printf("  ⚠️  温度过高告警:%.1f°C\n", data.Temp)
            }
            if data.Humidity > 80 {
                fmt.Printf("  ⚠️  湿度过高告警:%.1f%%\n", data.Humidity)
            }
        }
    }
}

七、エラー処理与異常コード。

7.1 ModbusException Code

Exception CodeName含义常见原因
0x01違法な機能コードスレーブはこの機能をサポートしません码デバイスサポートなし。書き込み操作。或特定功能
0x02Illegal data address要求アドレスが範囲外ですレジスター·アドレスエラー或数量制限超過
0x03Illegal data value値の書き込み不合法写入超出量程的值
0x04Substation equipment malfunctionスレーブ処理时出错デバイス硬件故障或内部エラー
0x05Confirmスレーブ已接受但需时间処理写入操作需要较长时间
0x06駅から忙しい。スレーブ処理中です。其他リクエストリクエスト过于频繁

7.2 带重试的健壮読み取り

// ReadWithRetry 带重试的レジスタの読み取り
func ReadWithRetry(client modbus.Client, ctx context.Context, addr, qty uint16, maxRetries int) ([]byte, error) {
    var lastErr error
    for attempt := 0; attempt < maxRetries; attempt++ {
        data, err := client.ReadHoldingRegisters(ctx, addr, qty)
        if err == nil {
            return data, nil
        }
        lastErr = err
        log.Printf("No.%d次読み取り失敗:%v,重试中...", attempt+1, err)
        time.Sleep(time.Duration(attempt+1) * 500 * time.Millisecond) // 退避重试
    }
    return nil, fmt.Errorf("重试%d次后仍失敗:%w", maxRetries, lastErr)
}

// 使用例の例
data, err := ReadWithRetry(client, ctx, 0, 10, 3) // 最多重试3次
if err != nil {
    log.Printf("最终読み取り失敗:%v", err)
    // 记录告警、切换备用デバイス等
}

八、デバッグ与日志

package main

import (
    "context"
    "log"
    "os"
    "time"

    "github.com/grid-x/modbus"
)

func main() {
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveID = 1
    handler.Timeout = 5 * time.Second

    // 有効日志(出力元の通讯データ)
    handler.Logger = log.New(os.Stdout, "[MODBUS] ", log.LstdFlags|log.Lmicroseconds)

    ctx := context.Background()
    handler.Connect(ctx)
    defer handler.Close()

    client := modbus.NewClient(handler)

    // 此时すべてのリクエスト和レスポンス都会打印到コンソール·コンソール
    // サンプル出力:
    // [MODBUS] 2024/01/01 12:00:00. 00 000 1 Tx : [ 00 01 00 00 00 06 01 03 00 00 00 00 0 A ]
    [ MO DB US ] 2024 / 01 / 01 12: 00 : 00 . 00 5000 R x : [ 00 01 00 00 17 01 03 14 00 64 00 C 8 ...]

    client . Read H old ing Reg ister s (ct x , 0, 10)
}

IX . 성 능 최적 화 기술

  • バッチ読み取り한 번에 10 개의 레 지 스터 를 읽는 것은 1 개의 레 지 스터 를 10 번 읽는 것보다 10 배 빠 릅니다 .
  • 连接复用
  • 合理タイムアウト
  • 并发采集
  • 减少書き込み操作。
  • アドレス对齐
  • 避免频繁切换SlaveID

Gr id - x / Mod bus 학습

阶段
入門
基础
上級
エキスパート

grid - x / mod bus 는 간 결 한 API 와 완벽한 프로토 콜 지원 으로 Go 언어 의 Mod bus 개발 을위한 최고의 선택 입니다 .이 기 사는 제 로 기반 에서 완전한 프로젝트 실 전에 이르 기까지 개발 중에 만나는 모든 일반적인 시 나 리오 를 다 룹 니다 .먼저 시 뮬 레이 션 환경에서 코드를 검 증 한 다음 실제 장비 에 접속 하여 개발 과정에서 로 그 기능을 잘 활용 하여 통신 문제를 해결 하는 것이 좋습니다 .

Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support

Leave a Reply

Your email address will not be published. Required fields are marked *.