feat: enhance plugin system with Go helpers and add Modbus plugin

- Register Go helper functions (modbus.crc16, hex.encode/decode,
  util.bytes) into Lua states for Modbus RTU support
- Add plugins/modbus.lua with .modbus read/write commands
- Fix Reload race condition (hold lock across Unload+Load)
- Make App.Close nil-safe for sess
- Restore internal/console/console_test.go

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
JiXieShi
2026-05-24 00:01:22 +08:00
parent 30d6c2bc3c
commit 209ecac2d5
5 changed files with 293 additions and 6 deletions
+116
View File
@@ -0,0 +1,116 @@
package luaplugin
import (
lua "github.com/yuin/gopher-lua"
)
// registerHelpers registers Go utility functions into a Lua state.
func registerHelpers(L *lua.LState) {
modbus := L.NewTable()
L.SetGlobal("modbus", modbus)
L.SetField(modbus, "crc16", L.NewFunction(luaCRC16))
L.SetField(modbus, "validate", L.NewFunction(luaValidateCRC))
hex := L.NewTable()
L.SetGlobal("hex", hex)
L.SetField(hex, "encode", L.NewFunction(luaHexEncode))
L.SetField(hex, "decode", L.NewFunction(luaHexDecode))
util := L.NewTable()
L.SetGlobal("util", util)
L.SetField(util, "bytes", L.NewFunction(luaBytes))
}
// crc16 computes the CRC-16/MODBUS checksum for the given data.
func crc16(data []byte) uint16 {
var crc uint16 = 0xFFFF
for _, b := range data {
crc ^= uint16(b)
for i := 0; i < 8; i++ {
if crc&1 != 0 {
crc = (crc >> 1) ^ 0xA001
} else {
crc >>= 1
}
}
}
return crc
}
func luaCRC16(L *lua.LState) int {
s := L.CheckString(1)
crc := crc16([]byte(s))
L.Push(lua.LNumber(crc))
return 1
}
func luaValidateCRC(L *lua.LState) int {
s := L.CheckString(1)
if len(s) < 2 {
L.Push(lua.LBool(false))
return 1
}
data := []byte(s[:len(s)-2])
crc := crc16(data)
expect := uint16(s[len(s)-2]) | uint16(s[len(s)-1])<<8
L.Push(lua.LBool(crc == expect))
return 1
}
func luaHexEncode(L *lua.LState) int {
s := L.CheckString(1)
buf := make([]byte, len(s)*2)
for i, b := range []byte(s) {
buf[i*2] = hexChar(b >> 4)
buf[i*2+1] = hexChar(b & 0x0F)
}
L.Push(lua.LString(buf))
return 1
}
func luaHexDecode(L *lua.LState) int {
s := L.CheckString(1)
if len(s)%2 != 0 {
L.Push(lua.LNil)
return 1
}
buf := make([]byte, len(s)/2)
for i := 0; i < len(s); i += 2 {
buf[i/2] = unhexChar(s[i])<<4 | unhexChar(s[i+1])
}
L.Push(lua.LString(buf))
return 1
}
func luaBytes(L *lua.LState) int {
// Converts a sequence of numbers to a byte string.
// e.g. util.bytes(0x01, 0x03, 0x00, 0x01, 0x00, 0x01) → "\x01\x03\x00\x01\x00\x01"
top := L.GetTop()
buf := make([]byte, top)
for i := 1; i <= top; i++ {
buf[i-1] = byte(L.CheckInt(i))
}
L.Push(lua.LString(buf))
return 1
}
func hexChar(b byte) byte {
if b < 10 {
return '0' + b
}
return 'A' + (b - 10)
}
func unhexChar(c byte) byte {
switch {
case c >= '0' && c <= '9':
return c - '0'
case c >= 'a' && c <= 'f':
return c - 'a' + 10
case c >= 'A' && c <= 'F':
return c - 'A' + 10
default:
return 0
}
}
+7 -5
View File
@@ -57,6 +57,7 @@ func (m *Manager) Load(path string) (string, error) {
}
state := lua.NewState()
registerHelpers(state)
if err = state.DoFile(abs); err != nil {
state.Close()
return "", err
@@ -110,19 +111,20 @@ func (m *Manager) Disable(name string) error {
return nil
}
// Reload reloads a plugin's file.
// Reload reloads a plugin's file atomically.
func (m *Manager) Reload(name string) error {
m.mu.Lock()
p, ok := m.plugins[name]
m.mu.Unlock()
if !ok {
m.mu.Unlock()
return fmt.Errorf("plugin %s not found", name)
}
path := p.Path
if err := m.Unload(name); err != nil {
return err
}
p.L.Close()
delete(m.plugins, name)
m.mu.Unlock()
_, err := m.Load(path)
return err
}