# libxcom **Repository Path**: meto475/libxcom ## Basic Information - **Project Name**: libxcom - **Description**: 是一种轻量型数据序列化和反序列化语言。与平台无关,可以自己配置解析函数。目前在串口指令方面使用较成熟。 - **Primary Language**: C++ - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 1 - **Created**: 2023-09-05 - **Last Updated**: 2024-11-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README @[toc] # Xcom 串口语言 Xcom用于串口协议指令(但不限于串口)的封装和解析。简单灵活,支持任意格式的指令和数学表达式运算。在实际运用中通过这种语言可以快速从串口指令序列中提取任意需要的数据并解析出结果。 Xcom 是用c++语言实现的翻译型语言,对外开源,支持交叉编译。 # 快速入门 开发一个modbus协议。 ## 第一步, 协议构建 ```python #modbus协议 def crc16: $crc($1,0x8005,0xffff,0x0000,0) # $addr $len 都是输入的参数 def send: [$addr 03H $1 $itom(2,$len) $crc16($group(0,$pos))] def recv: [$addr 03H $ditch(1,$assign(&dl,$0)) $data($ditch($dl)) $ditch(2,$chk($0,$crc16($group(0,$pos))))] ``` 上面首先是定义了一个crc 16计算的函数, `$crc` 是内置函数,包含5个参数,用于将传入的数据计算出对应的CRC16的值。 然后,是定义了两个函数, `send`,`recv`是函数名,可以自定义。 `send` 是定义了协议的封装逻辑,[] 是指令构建运算符。 在内部,第1个是`$addr` 表示设备地址,这是一个变量,最终会转换成一个字节的16进制数字. 内置的变量还包括`$len`, 表示寄存器长度,由用户自定义。 第2个03H 是固定的功能码,在modbus协议里一般表示读取数据 第3个`$1` 是传入的第一个参数。 比如在代码里调用时使用`$send(3)`, 那么这里`$1` 就是3 第4个是`$itom(2,$len)`, `$itom` 是内置函数,表示把一个数转换成16进制指令,这里的2表示两个字节,即最终会转变成2个字节的指令。 比如`$itom(2, 37) = 00H 25H` 第5个相对复杂。 先看最里面的括号,`$group(0,$pos)` 表示提取[] 内从第0个指令到当前位置之前的(`$pos` 表示位置)指令, 将他作为参数传给`$crc16` 进行CRC16计算。 这样,就完成了`send` 的指令构建 `recv` 跟`send`差不多,但是`recv`是对数据解析的指令。`$ditch` 指令是他的一个特征,`$ditch`是从当前位置挖去N个字节,并进行处理。第一个参数是挖去的字节数,第二个参数表示动作,如果不做任何动作,第二个参数可不填。`$0` 是`$ditch`挖取的数据。`$ditch(1,$assign(&dl,$0))` 表示挖去一个字节,并赋值给`dl`. 最后一个`$ditch` 是挖取2个字节,并与校验码进行比较,判断校验码是否正确。 ## 第二步, 接入代码 ```c++ int main() { setenv("LOG", "stdout", 1); // 将log设置到控制台输出 ID_DEV devId("51H"); const std::string templ = "#modbus协议\n" "def crc16: $crc($1,0x8005,0xffff,0x0000,0)\n" "# $addr $len 都是输入的参数\n" "def send: [$addr 03H $1 $itom(2,$len) $crc16($group(0,$pos))]\n" "def recv: [$addr 03H $ditch(1,$assign(&dl,$0)) $data($ditch($dl)) $ditch(2,$chk($0,$crc16($group(0,$pos))))]"; DataItem ditem(devId, templ); ditem.SetRegLength(2); // 设置寄存器长度, 即$len 的值 ditem.ParseDataExpr("$int($data)", 1); // 最终的结果进行计算,这里是对recv 挖取的$data 转换成整数,1.0 表示比例因子为1, 即将结果再除以1.0 std::vector params; params.push_back("04H"); ditem.ParseParams(params); // 这里传入的是参数列表, 对应着$1, $2, $3 等等。这里只有一个参数 // 生成发生命令 OpValue cmd = ditem.GenerateCmd("send"); cmd.Show(); if (cmd.IsEmpty()) return -1; CmdValue genCmd = cmd.GenCmd(); genCmd.Show(); // 中间省略发送接收过程 // 处理接收指令 OpValue result; uint8_t cmd_buf[] = {0x51,0x03,0x04,0x00,0x00,0x00,0x1E,0x2A,0x3E}; CmdValue recv(cmd_buf, sizeof(cmd_buf)/sizeof(cmd_buf[0])); recv.Show(); if (ditem.ParseRecvCmd(recv, "recv") != RECV_COMPLETE) { return -1; } result = ditem.Result(); result.Show(); if (result != 0x1E) { return -1; } return 0; } ``` ## 1. 基本语法 ### 1.1 数字和运算符 #### 1.1.1 常量和编码 Xcom 语言的常量可以是数和指令序列。数最大可以支持64字节的整型和双精度浮点数,最小可以是单个字节的数。 编码格式只有10进制和16进制,不提供二进制或八进制表示。16进制后面加个H后缀或0x前缀,例如0x13和13H是相等的。 串口最常用的编码有BCD编码,余3编码,IEEE754 规范浮点数等,这些提供了内置函数转换。 #### 1.1.2 运算符 下面是基本的运算符。基本满足各种需求。 - () :小括号 小括号与函数组成了函数调用,括号里面为传入参数列表,括号里如果是表达式将优先运算,这跟其他语言基本都一样。 - []: 中括号 中括号是一种运算符,用于串口指令合成。比如Modbus 指令 01 03 00 00 00 02 C4 0B,用Xcom 语言表示就是 [01H 03H 00H 00H 00H 02H C4H 0BH], 后面跟着H表示16进制,如果没有H则为10进制数。 - {}:大括号 用于定义函数数组或表达式数组。 比如def send[2]:{[01H 03H],[02H 03]}, 定义了两个函数。一般用于需要连续发送多条串口指令的情况下,可以把过程变得很简单。 - `$`: 函数或变量标志。 在表达式中引用函数或变量需要用`$`作前缀。比如`$sum`。如果引用参数则`$1`,`$2` 等分别表示传入的第一个,第二个参数 - &: 函数或变量引用。 这种运算符提供了一种可以将函数或变量的地址作为参数传递。 **下面所有运算可以是数与数,也可以是指令序列与指令序列,指令序列和数(数放后面)。凡是涉及指令序列的运算字节之间不产生进位。这也符合串口特点。** - +: 加法运算。 - -:减法运算 - *: 乘法运算 - /: 除法运算 - &: 与 - |: 或 - !: 非 > 上面不包括 '=' 运算符,可以用$assign(&v,03H) 代替,等价于 v=03H 涉及左移右移运算,目前只能用除法和乘法代替。例如左移3位,相当于乘以8,左移则相当于除以8. ### 1.2 函数定义 #### 1.2.1 def 关键字和函数定义 def 是函数定义的最基本关键字。所有函数的定义都要以def开始,基本格式为: `def 函数名:表达式` 所有的函数都是由表达式构成,可以包含多个表达式。 函数名和表达式之间用冒号隔开。函数名的定义规则和其他语言一样,只能保护字母,数字,下划线,且不能以数字开头。例如func_max4,就是合格的,5func_min 就是不合格的。表达式可以是任意数学表达式的组合。后面会详细说明。 下面定义的是累加求和校验函数: `def chksum:$sum($1)%256` 定义的是名称为chksum 的函数,它需要一个传入参数\$1,\$sum 是内置函数,用于对$1 求和运算。很显然\$1 应该是一个指令序列,否则\$sum 求的结果就是自身。这个函数的功能是对一个指令序列累加求和,然后和256求余运算。这就是串口协议Mbus里最常用的累加求和校验运算。 #### 1.2.2 内置变量 内置变量只有3个,除\$pos是隐含变量 外,其他都是由用户设置的。 - \$addr: 设备地址 设备地址如果是bcd编码,后缀加B即可。例如20190104B, 等价于 [04H 01H 19H 20H], 十六进制地址可以写成 0925H,表示[25H 09H],没有后缀则按十进制处理。 - \$pos: 当前位置 只有在`[]`表达式内部才有效。 例如: `[68H 01H 03H $3 $chk($pos) 16H]` 如果`$3` 是4个字节,那么`$pos`等于`7`,从`0`开始计算。 - \$len: 寄存器字节 表示寄存器长度或个数,现在市场上大多数寄存器都是每个寄存器两个字节,因此寄存器长度如果是2,字节长度就是4。 **从以上可见Xcom语言是既能方便专业人员又给普通用户是提供了极大的方便的,专业人员只需要写好Xcom语句,可能就两三个表达式,普通用户只需要设置三四个变量,基本就能实现数据提取。具体看后面实际案例** 下面是我们的实际案例: ``` # 能量表数据采集协议 def chksum: $sum($1)%256 def send: [68H $1 $addr $reverse($2) 01H 03H $3 $fill(1,$chksum($group(0,$pos))) 16H] def recv: [68H $ditch(1) $ditch(7) 81H $ditch(1,$assign(&dl,$0-3)) $3 $data($ditch($dl)) $ditch(1) 16H] #--------------------下面是用户要求的能量表采集的7个数据点位------------------------------ #机械式热量表 积累冷量 寄存器字节:4 数据域:20H;11H 11H 00H;1FH 90H 01H 数据表达式:$bcd($reverse($data(0,$len))) 公式系数:100 积累热量 寄存器字节:4 数据域:20H;11H 11H 00H;1FH 90H 01H 数据表达式:$bcd($reverse($data(5,$len))) 公式系数:100 功率 寄存器字节:4 数据域:20H;11H 11H 00H;1FH 90H 01H 数据表达式:$bcd($reverse($data(10,$len))) 公式系数:100 瞬时流速 寄存器字节:4 数据域:20H;11H 11H 00H;1FH 90H 01H 数据表达式:$bcd($reverse($data(15,$len))) 公式系数:100 累积流量 寄存器字节:4 数据域:20H;11H 11H 00H;1FH 90H 01H 数据表达式:$bcd($reverse($data(20,$len))) 公式系数:100 供水温度 寄存器字节:3 数据域:20H;11H 11H 00H;1FH 90H 01H 数据表达式:$bcd($reverse($data(25,$len))) 公式系数:100 回水温度 寄存器字节:3 数据域:20H;11H 11H 00H;1FH 90H 01H 数据表达式:$bcd($reverse($data(28,$len))) 公式系数:100 ``` #### 1.2.3 内置函数 内置函数是Xcom 语言最重要的一个部分。它帮助我们完成了许多串口中最基本的运算。比如指令序列求和,将指令序列解析成BCD码,整数,IEEE 754规范浮点数等。 >下面所有内置函数在说明时使用的\$1,\$2 等表示传入的第一个第二个参数,具体讲解时不再重复。 - \$goup:指令序列捕捉,只能用于`[]` 表达式内部 如果两个整型参数,表示从\$1 开始的连续 \$2 个字节重新捕捉。 如果一个整型参数,表示从0 开始的连续 \$1 个字节重新捕捉。 - \$assgn: 赋值运算 两个参数,第一个参数必须是`&` 开头的变量,第二个参数可以是数或指令序列 - \$cmp: 比较是否相等 两个参数。比较\$1 和 \$2 是否相等。相等返回1,否则返回0。 这种比较只是大小的比较,跟具体类型无关。比较特别的是,指令序列`[03H]` 和数字`03H` 是相等的,但是[03H 01H] 和 0301H 不相等,因为指令序列转换成数字还要分大端和小端模式。 - \$chk: 校验确认 跟\$cmp 处理基本相同.不同的是\$chk 专门用于校验确认,并记录校验结果,影响整个指令序列的解析结果。 - \$reverse: 反转指令序列 一个参数,必须是指令序列 - \$crc: CRC 校验码计算 共五个参数,crc 16位MODBUS校验码计算,`$crc($1,0x8005,0xffff,0x0000,0)`, 大多数情况下,这个函数使用记住就行,\$1 是待计算的指令序列,最后一个0,表示高字节在前,低字节在后,如果是1,则反过来。目前基本所有modbus 协议的CRC16 计算用这个都支持,也支持8位和32位的。主要是有后面四个参数决定。下表是常用校验算法说明: 算法| \$2:wcPoly|\$3:wCRCin|\$4: wXor|\$5:大端0小端1 --|--|--|--|-- CRC16_CCIT|0x1021|0x0000|0x0000|0 CRC16_CCIT_FALSE|0x1021|0xffff|0x0000|1 CRC16_XMODEM|0x1021|0x0000|0x0000|1 CRC16_X25|0x1021|0xFFFF|0xFFFF|0 CRC16_MODBUS|0x8005|0xFFFF|0x0000|0 CRC16_IBM|0x8005|0x0000|0x0000|0 CRC16_MAXIM|0x8005|0x0000|0xFFFF|0 CRC16_USB|0x8005|0xFFFF|0xFFFF|0 MODBUS 协议一般使用的就是CRC16_MODBUS,即`$crc($1,0x8005,0xffff,0x0000,0)` - \$eram: 涉及加密的crc modbus校验 - \$itom: 将整数转化成指令序列 两个参数。\$1 为转化的指令长度,1~4;第二个参数\$2 为被转化的整数。高字节在前低字节在后。 - \$bit: 位与运算 共三个参数,后面两个参数如果不用默认都是0。\$1 是指令序列,\$2 是第几个字节,从0开始。\$3 是对第几位进行与运算。例如:\$bit([0FH 09H],0,3), 与 `0x0F&(0x01<<3)`等价 - \$rand: 产生随机序列 一个参数,\$1 表示产生的随机序列长度。 > 下面的函数参数设置一样。有三种参数模式: > 如果只有一个参数,必须是指令序列,则是对整个指令序列进行这种运算。 > 如果有两个参数,第一个参数为指令序列,第二个参数为长度。即表示从第0个字节开始的$2 个字节进行运算 > 如果有三个参数,第一个依然为指令序列,第二个为起始索引,从0开始;第三个参数为长度。 - \$sum: 指令序列求和。 例如: ``` def chksum: $sum($1)%256 def send: [68H $1 $addr $reverse($2) 01H 03H $3 $fill(1,$chksum($group(0,$pos))) 16H] def recv: [68H $ditch(1) $ditch(7) 81H $ditch(1,$assign(&dl,$0-3)) $3 $data($ditch($dl)) $ditch(1) 16H] ``` 上面定义了三个函数,第一个定义的是名称为chksum 的函数,它需要一个参数,即\$1,\$sum 是内置函数,用于对$1 求和运算。很显然\$1 应该是一个指令序列,否则\$sum 求的结果就是自身。这个函数的功能是对一个指令序列累加求和,然后和256求余运算。实质上这就是串口协议Mbus里最常用的累加求和校验运算。 第二个函数send 前面两行def 开头的表示定义函数。使用时可以用冒号前面的command0 代表冒号后面的指令。 第三行的'#' 表示这一行是注释。后面分别注释对应各自的数据项,ID,名称、发送指令、接收指令、表示、单位. 第四行开始为数据指令,数据项之间用空格隔开。 - **ID**: 为了统一定义了不同数据指令的ID,设备地址的指令ID为0000,上面的水表计量数据的ID是0101,高位的01 表示水表,02表示电表,03表示能量表,地位的01是水表计量数据。 - **名称**: 数据指令的名称,可以自己定义,尽量统一。 - **发送指令** : 是指获取这条数据需要发送给仪表设备的指令 - **接收指令**: 是指获取这条数据接收到的指令。上面为方便理解写的是静态指令,实际中比较复杂。具体定义参考后面函数说明。 - **表示**: 是指这条数据实际表示。目前默认都写1。 - **单位**: 这条数据的单位,地址和控制指令没有单位可以写none,也可以不写。 但是仅定义数据项,只能使用静态的指令,这样无法在程序中获取数据,因此必须定义许多关键字和函数来提取其中的有效数据,达到一种协议只需要一个配置文件就可以解决所有问题的目的。 ## 1.2 常量 主要包括16进制数字和10进制数字。16进制数字用0x 开头或H结尾,比如0x86 和 86H 都表示16进制的86。 如果是10进制,直接写成数字即可。10进制可以使用小数,16进制暂时不支持小数。 ## 1.3 运算符号 - **[]** []表示将中间的数字合成指令,比如: [68H 11H 68H 76H 00H 04H 00H 33H 78H 01H 03H 1fH 90H 01H BAH 16H] 就是一条指令。 - **$** 表示后面是函数、变量或参数。例如: def func: (\$1*\$1 )%\$2 上面是定义了一个函数func,\$1、\$2 表示func的第一和第二个参数。在使用func时 需要这样: \$func(16,256),这样\$1 就是16, \$2就是256代入表达式进行运算。 - **&** 与$ 相对,它后面跟的一般是函数或变量。但不立马进行计算,而是整个的作为参数。比如: ``` def acount: $1%256 $data(&acount,255) ``` 其中data 是关键字,它表示把255进行某种运算得出一个结果。这里传入acount 表示把255 作为acount的参数进行运算。 而如果前面是\$,必须这样调用 $acount(255) 得到的结果是一样的。 - **()** 参数列表,比如:\$func(16,256),参数之间用空格或逗号隔开。或者是一个表达式需要优先计算。比如 (2+3)*4 - **+,-,*,/,%** 分别是加减乘除,模运算。 ## 1.4 函数 ### 1.4.1 函数定义和使用 通过上面数据指令可以知道,仅仅靠数据指令有很多东西无法解决。比如设备地址、校验码、接收指令时数据的长度、数据内容都是不确定的,必须写成动态的指令格式。而且考虑到一种型号或同一种协议的设备只需要一个指令配置文件,写成静态指令会导致每个设备需要一个配置指令,会增加工作量。因此必须定义函数。 函数格式 def func: 表达式 函数调用 \$func(参数1,参数2,...) 例如: ``` def func: $bcd(($1)%$2 def command0:[68H 11H $1 33H 78H 01H 07H $2 $func($ditch(4),256) 01H $sum($group(0,$pos)) 16H] $command0($addr,[1fH 90H 01H]) ``` 这个例子看起来比较复杂,但是了解了各个运算符号和内置函数的定义也并不难理解。 **\$command0(\$addr,[1fH 90H 01H]) ** 这是调用了command0 函数,有两个参数分别是\$addr 和 [1fH 90H 01H] ,代入command0中的 \$1 \$2 ,\$addr 是仪表地址,这由系统自动补充。 再来看$command0的定义。 $func 在上面已经定义,它是把第一个参数转化成bcd码与第二个参数求余,\$ditch(4) 是关键字不是函数,后面会讲,\$ditch(4)表示把接收到的指令从当前位置挖出4个字节作为\$func的第一个参数。 后面还有sum,group pos 都是内置函数或变量。 ### 1.4.2 内置函数 函数可以是用def 自定义的函数,也可以使用内置的函数。这些函数目前主要有以下几个: - reverse: 反转指令顺序,比如\$reverse([01H 90H]) 得到的是[90H 01H],一般指令都是高位在前低位在后,但有时候可能是地位在前高位在后,这时候可以使用reverse - assign: 赋值运算 。 比如\$assign(&len,5),表示把5赋值给一个变量len。第一个参数必须是&接的变量。assign 也起到了定义变量的作用。例如: def command: [16H \$addr 78H 83H \$assign(&length,\$ditch(1)) \$data(\$length,&sum) \$ditch(2)] 这里面 \$assign 里定义了length的值,在\$data 里就使用了length 。但要注意定义必须在使用之前。 以下函数有一个或三个参数,第一个参数是多字节指令,第二、三个参数(m,n)分别表示取出指令的从第m位开始的n个字节。 - int: 将指令转换成整数,例如int([01H 90H]),表示将指令[01H 90H] 转换成整数是400(计算01*16*16+9*16) - float: 将指令转换成浮点数,即IEEE 754规范的浮点数,也是计算机本身的浮点数标准,例如float([01H 90H]),转换成浮点数是0.400 - sum: 将指令从第一个字节到最后一个进行累加求和。 - product: 将指令从第一个字节到最后一个进行累乘求积。 - bcd: 指令转换成BCD编码 - rbcd: bcd编码转换成指令 ## 1.3 关键字 ### 1.2.1 **def ** 定义函数关键字,行开头使用def 表示定义函数。格式如下: def func: (\$1*\$1)%\$2 就表示定义了一个名称为func的函数,它至少应该有两个参数,分别用\$1,\$2代替。比如:func(16,256) \$1就是16,\$2就是256,那么,func(16,256)=16*16%256=1 需要注意不要在表达式内部有空格,为避免这个问题可以加上括号。比如 def func: ((\$1*\$1)%\$2) ### 1.2.2 **ditch ** 本意表示挖。它的形式是\$ditch(m,$do), 即挖出m个字节的指令,进行\$do运算,这里面的do可以是任何函数。 ditch用于接收指令分析。比如接收的指令是: ``` [68 11 68 76 00 04 00 33 78 81 16 1F 90 01 00 01 00 00 2C 00 00 00 00 2C 00 00 00 00 00 00 00 00 00 A6 16] ``` 根据协议知道,第一个16 是它的数据长度,1F 90 01 后面的13个字节是其数据部分,我们需要把前4个字节提取出来转化成我们需要的计量数值,那么解析指令可以这样定义: ``` def measure: $bcd($1,0,4)/100.0 def command1: [68H 11H $ditch(8) $ditch(1,$assign(&len,$1-3)) $ditch(3) $ditch(4,$data(&measure,$1)) $ditch($len-4+2)] ``` command1里用到了多个ditch,第一个ditch(8),表示挖出8个字节的指令,但不做任何处理,即[68 76 00 04 00 33 78 81] 相当于被忽视。第二个ditch带两个参数,第一个参数是1,第二个嵌套了一个函数assign, 它表示挖出一个字节,即16H,然后将16H 减去3 赋值给len, 这时得到的len=13H。 紧接着又是ditch(3),即忽视接下来的三个字节。 再后面是挖出4个字节[00 01 00 00]交给data并作为$measure的参数进行运算就可以得到我们需要的结果。具体见data部分。 ### 1.2.3 **data ** data 只能用于接收指令提取数据,比如水表的计量数据。它的第一个参数是一个&接的函数,第二个参数是第一个参数的参数。例如: \$ditch(4,\$data(&measure,\$1)) 表示挖出4个字节的指令,作为data的第二个参数,第一个参数是自定义的函数measure,他是把一个多字节的指令转化成bcd码,然后除以100. measure的参数是data的第二个参数,即\$1,也就是\$ditch 挖到的4个字节指令[00 01 00 00]。计算的结果是100.00 **注意: 发送指令不能使用ditch ,data 关键字** ### 1.2.4 **chk** 用于校验。它有两个参数,即比较这两个参数是否相等,如果不相等会返回错误。 ### 1.2.5 **group** 组合指令。是指对现有指令进行组合。在校验码字段进行累加求和时用到比较多。比如对从第0个字节开始到校验码之前所有的指令进行累加和,这时就可以使用\$group(0,\$pos) 得到从0到校验码之前的所有合并的指令。\$pos 表示当前关键字在整个指令中的索引,从0开始计算,所以这里\$pos 也表示指令的长度。即从0开始的连续\$pos 个字节的指令进行合并。 ### 1.2.6 **pos** 当前在指令中的位置。例如: [86H 11H \$group(1,\$pos) 16H] 因为pos 是group的参数,group 在指令中的位置是2,那么pos=2. group(1,\$pos) = [11H] ### 1.2.7 **addr** 仪表地址。 ## 2.1 完整例子 这时hed型号仪表的完整指令配置 ``` #型号HED09E3Y/C def crc16: $crc($1,0x8005,0xffff,0x0000,0) def charge:$bcd($1)*0.1 #读寄存器 :地址 功能号 起始地址 长度 校验(crc16_modbus) def command0: [$1 03H $itom(2,$2) $itom(2,$3) $crc16($group(0,$pos))] #应答 : 地址 功能号 数据长度 数据 校验 def command1: [$addr 03H $ditch(1,$assign(&len,$1)) $data($1,$ditch($len)) $crc16($group(0,$pos))] #01 03 04 00 00 00 00 FA 33 def command2: [01H 03H 02H $ditch(2,$assign(&addr,$int($1))) $crc16($group(0,$pos))] #获取地址应答 0000 设备地址 $command0(01H 45H 1) $command2 1 none 0110 正向有功电度 $command0($addr 36H 2) $command1(&charge) 1 kwh 0111 反向有功电度 $command0($addr 38H 2) $command1(&charge) 1 kwh 0112 正向无功电度 $command0($addr 3CH 2) $command1(&charge) 1 kvarh 0113 反向无功电度 $command0($addr 3EH 2) $command1(&charge) 1 kvarh ```