libmodbus-1.1.0: System/Modbus.hsc
{- | Haskell bindings to the C modbus library https://libmodbus.org/ -}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
module System.Modbus (
-- * Equivilance to the C library
-- | Functions in this module are named the same as those in the C
-- library, but without the leading "modbus_". You may wish to import
-- this module qualified as Modbus to make the names match up.
--
-- See the C library documentation for details about the use
-- of any function. https://libmodbus.org/documentation/
--
-- When a function in the C library returns a special value on
-- error, this module will instead throw an exception.
--
-- This module has been tested with version 3.1.4 of the C library.
-- It may also work with other versions.
-- * Example
--
-- | This example reads some of the registers of an Epever solar
-- charge controller. It shows how `Data.Binary` can be used
-- to decode the modbus registers into a haskell data structure.
--
-- > import System.Modbus
-- > import Data.Binary.Get
-- >
-- > main = do
-- > mb <- new_rtu "/dev/ttyS1" (Baud 115200) ParityNone (DataBits 8) (StopBits 1)
-- > set_slave mb (DeviceAddress 1)
-- > connect mb
-- > regs <- mkRegisterVector 5
-- > b <- read_input_registers mb (Addr 0x3100) regs
-- > print $ runGet getEpever b
-- >
-- > data Epever = Epever
-- > { pv_array_voltage :: Float
-- > , pv_array_current :: Float
-- > , pv_array_power :: Float
-- > , battery_voltage :: Float
-- > } deriving (Show)
-- >
-- > getEpever :: Get Epever
-- > getEpever = Epever
-- > <$> epeverfloat -- register 0x3100
-- > <*> epeverfloat -- register 0x3101
-- > <*> epeverfloat2 -- register 0x3102 (low) and 0x3103 (high)
-- > <*> epeverfloat -- register 0x3104
-- > where
-- > epeverfloat = decimals 2 <$> getWord16host
-- > epeverfloat2 = do
-- > l <- getWord16host
-- > h <- getWord16host
-- > return (decimals 2 (l + h*2^16))
-- > decimals n v = fromIntegral v / (10^n)
-- * Core data types
Context,
Addr(..),
-- * RTU Context
Baud(..),
Parity(..),
DataBits(..),
StopBits(..),
new_rtu,
SerialMode(..),
rtu_get_serial_mode,
rtu_set_serial_mode,
RTS(..),
rtu_get_rts,
rtu_set_rts,
rtu_get_rts_delay,
rtu_set_rts_delay,
-- * TCP (IPv4) Context
IPAddress(..),
Port(..),
new_tcp,
-- * TCP PI (IPv4 and IPv6) Context
Node(..),
Service(..),
new_tcp_pi,
-- * Configuration
DeviceAddress(..),
broadcastAddress,
set_slave,
connect,
set_debug,
Timeout(..),
get_byte_timeout,
set_byte_timeout,
get_response_timeout,
set_response_timeout,
-- * Accessing registers
RegisterVector,
mkRegisterVector,
RegisterData(..),
read_registers,
read_input_registers,
write_registers,
write_register,
write_and_read_registers,
-- * Accessing bits/coils
BitVector,
mkBitVector,
BitData(..),
Bit,
boolBit,
bitBool,
read_bits,
read_input_bits,
write_bits,
write_bit,
) where
#include <modbus/modbus.h>
import Foreign
import Foreign.C
import Data.Char
import Data.Default
import qualified Data.Vector.Storable.Mutable as VM
import qualified Data.Vector.Storable as V
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import GHC.IO.Exception
foreign import ccall unsafe "modbus.h &modbus_close" modbus_close
:: FunPtr (Ptr () -> IO ())
foreign import ccall unsafe "modbus.h &modbus_free" modbus_free
:: FunPtr (Ptr () -> IO ())
foreign import ccall unsafe "modbus.h modbus_new_rtu" modbus_new_rtu
:: CString -> Int -> CChar -> Int -> Int -> IO (Ptr ())
foreign import ccall unsafe "modbus.h modbus_rtu_get_serial_mode" modbus_rtu_get_serial_mode
:: Ptr () -> IO Int
foreign import ccall unsafe "modbus.h modbus_rtu_set_serial_mode" modbus_rtu_set_serial_mode
:: Ptr () -> Int -> IO Int
foreign import ccall unsafe "modbus.h modbus_rtu_get_rts" modbus_rtu_get_rts
:: Ptr () -> IO Int
foreign import ccall unsafe "modbus.h modbus_rtu_set_rts" modbus_rtu_set_rts
:: Ptr () -> Int -> IO Int
foreign import ccall unsafe "modbus.h modbus_rtu_get_rts_delay" modbus_rtu_get_rts_delay
:: Ptr () -> IO Int
foreign import ccall unsafe "modbus.h modbus_rtu_set_rts_delay" modbus_rtu_set_rts_delay
:: Ptr () -> Int -> IO Int
foreign import ccall unsafe "modbus.h modbus_new_tcp" modbus_new_tcp
:: CString -> Int -> IO (Ptr ())
foreign import ccall unsafe "modbus.h modbus_new_tcp" modbus_new_tcp_pi
:: CString -> CString -> IO (Ptr ())
foreign import ccall unsafe "modbus.h modbus_set_slave" modbus_set_slave
:: Ptr () -> Int -> IO Int
foreign import ccall unsafe "modbus.h modbus_connect" modbus_connect
:: Ptr () -> IO Int
foreign import ccall unsafe "modbus.h modbus_set_debug" modbus_set_debug
:: Ptr () -> Int -> IO Int
foreign import ccall unsafe "modbus.h modbus_get_byte_timeout" modbus_get_byte_timeout
:: Ptr () -> Ptr Word32 -> Ptr Word32 -> IO Int
foreign import ccall unsafe "modbus.h modbus_set_byte_timeout" modbus_set_byte_timeout
:: Ptr () -> Ptr Word32 -> Ptr Word32 -> IO Int
foreign import ccall unsafe "modbus.h modbus_get_response_timeout" modbus_get_response_timeout
:: Ptr () -> Ptr Word32 -> Ptr Word32 -> IO Int
foreign import ccall unsafe "modbus.h modbus_set_response_timeout" modbus_set_response_timeout
:: Ptr () -> Ptr Word32 -> Ptr Word32 -> IO Int
foreign import ccall unsafe "modbus.h modbus_read_registers" modbus_read_registers
:: Ptr () -> Int -> Int -> Ptr Word16 -> IO Int
foreign import ccall unsafe "modbus.h modbus_read_input_registers" modbus_read_input_registers
:: Ptr () -> Int -> Int -> Ptr Word16 -> IO Int
foreign import ccall unsafe "modbus.h modbus_write_registers" modbus_write_registers
:: Ptr () -> Int -> Int -> Ptr Word16 -> IO Int
foreign import ccall unsafe "modbus.h modbus_write_register" modbus_write_register
:: Ptr () -> Int -> Word16 -> IO Int
foreign import ccall unsafe "modbus.h modbus_write_and_read_registers" modbus_write_and_read_registers
:: Ptr () -> Int -> Int -> Ptr Word16 -> Int -> Int -> Ptr Word16 -> IO Int
foreign import ccall unsafe "modbus.h modbus_read_bits" modbus_read_bits
:: Ptr () -> Int -> Int -> Ptr Word8 -> IO Int
foreign import ccall unsafe "modbus.h modbus_read_input_bits" modbus_read_input_bits
:: Ptr () -> Int -> Int -> Ptr Word8 -> IO Int
foreign import ccall unsafe "modbus.h modbus_write_bits" modbus_write_bits
:: Ptr () -> Int -> Int -> Ptr Word8 -> IO Int
foreign import ccall unsafe "modbus.h modbus_write_bit" modbus_write_bit
:: Ptr () -> Int -> Int -> IO Int
accessVector
:: Storable t
=> Context
-> Addr
-> VM.IOVector t
-> (Ptr () -> Int -> Int -> Ptr t -> IO Int)
-> String
-> IO ()
accessVector h (Addr addr) v action actionname = withContext h $ \ctx -> do
let (fptr, nb) = VM.unsafeToForeignPtr0 v
r <- withForeignPtr fptr $ action ctx addr nb
if r == -1
then throwErrno actionname
else if r /= nb
then ioError $ IOError Nothing OtherError
actionname "short read/write" Nothing Nothing
else return ()
-- | A modbus device context.
--
-- The context will automatically be closed and freed when it is
-- garbage collected.
data Context = Context (ForeignPtr ())
mkContext :: Ptr () -> IO Context
mkContext ctx = do
ptr <- newForeignPtr_ ctx
addForeignPtrFinalizer modbus_free ptr
-- this will run before modbus_free
addForeignPtrFinalizer modbus_close ptr
return (Context ptr)
withContext :: Context -> (Ptr () -> IO a) -> IO a
withContext (Context ptr) = withForeignPtr ptr
newtype Baud = Baud Int
deriving (Show, Eq)
data Parity = ParityNone | ParityEven | ParityOdd
deriving (Show, Eq)
newtype DataBits = DataBits Int
deriving (Show, Eq)
newtype StopBits = StopBits Int
deriving (Show, Eq)
-- | Create a modbus Remote Terminal Unit context.
--
-- The FilePath is the serial device to connect to.
new_rtu :: FilePath -> Baud -> Parity -> DataBits -> StopBits -> IO Context
new_rtu f (Baud b) p (DataBits d) (StopBits s) = do
ctx <- withCString f $ \cf ->
modbus_new_rtu cf b pc d s
if ctx == nullPtr
then throwErrno "modbus_new_rtu"
else mkContext ctx
where
pc = fromIntegral $ ord $ case p of
ParityNone -> 'N'
ParityEven -> 'E'
ParityOdd -> 'O'
-- | IPv4 address to connect to. In server mode, use AnyAddress to listen
-- to any addresses.
data IPAddress = IPAddress String | AnyAddress
deriving (Show, Eq)
newtype Port = Port Int
deriving (Show, Eq)
instance Default Port where
def = Port #const (MODBUS_TCP_DEFAULT_PORT)
-- | Create a modbus TCP/IPv4 context.
new_tcp :: IPAddress -> Port -> IO Context
new_tcp ipaddr (Port port) = do
ctx <- case ipaddr of
IPAddress s ->
withCString s $ \cipaddr ->
modbus_new_tcp cipaddr port
AnyAddress ->
modbus_new_tcp nullPtr port
if ctx == nullPtr
then throwErrno "modbus_new_tcp"
else mkContext ctx
-- | Host name or IP address to connect to. In server mode, use AnyNode
-- to listen to any addresses.
data Node = Node String | AnyNode
deriving (Show, Eq)
-- | Service name/port number to connect to.
newtype Service = Service String
deriving (Show, Eq)
instance Default Service where
def = Service (show p)
where
p :: Int
p = #const (MODBUS_TCP_DEFAULT_PORT)
new_tcp_pi :: Node -> Service -> IO Context
new_tcp_pi node (Service service) = withCString service $ \cservice -> do
ctx <- case node of
Node s ->
withCString s $ \cnode ->
modbus_new_tcp_pi cnode cservice
AnyNode ->
modbus_new_tcp_pi nullPtr cservice
if ctx == nullPtr
then throwErrno "modbus_new_tcp_pi"
else mkContext ctx
data SerialMode = RTU_RS232 | RTU_RS485
deriving (Show, Eq)
rtu_get_serial_mode :: Context -> IO SerialMode
rtu_get_serial_mode h = withContext h $ \ctx -> do
r <- modbus_rtu_get_serial_mode ctx
if r == #const (MODBUS_RTU_RS232)
then return RTU_RS232
else if r == #const (MODBUS_RTU_RS485)
then return RTU_RS485
else throwErrno "modbus_rtu_get_serial_mode"
rtu_set_serial_mode :: Context -> SerialMode -> IO ()
rtu_set_serial_mode h m = withContext h $ \ctx -> do
r <- modbus_rtu_set_serial_mode ctx $ case m of
RTU_RS232 -> #const (MODBUS_RTU_RS232)
RTU_RS485 -> #const (MODBUS_RTU_RS485)
if r == 0
then return ()
else throwErrno "modbus_rtu_set_serial_mode"
data RTS = RTU_RTS_NONE | RTU_RTS_UP | RTU_RTS_DOWN
deriving (Show, Eq)
rtu_get_rts :: Context -> IO RTS
rtu_get_rts h = withContext h $ \ctx -> do
r <- modbus_rtu_get_rts ctx
if r == #const (MODBUS_RTU_RTS_NONE)
then return RTU_RTS_NONE
else if r == #const (MODBUS_RTU_RTS_UP)
then return RTU_RTS_UP
else if r == #const (MODBUS_RTU_RTS_DOWN)
then return RTU_RTS_DOWN
else throwErrno "modbus_rtu_get_serial_mode"
rtu_set_rts :: Context -> RTS -> IO ()
rtu_set_rts h m = withContext h $ \ctx -> do
r <- modbus_rtu_set_rts ctx $ case m of
RTU_RTS_NONE -> #const (MODBUS_RTU_RTS_NONE)
RTU_RTS_UP -> #const (MODBUS_RTU_RTS_UP)
RTU_RTS_DOWN -> #const (MODBUS_RTU_RTS_DOWN)
if r == 0
then return ()
else throwErrno "modbus_rtu_set_rts"
rtu_get_rts_delay :: Context -> IO Int
rtu_get_rts_delay h = withContext h $ \ctx -> do
r <- modbus_rtu_get_rts_delay ctx
if r /= -1
then return r
else throwErrno "modbus_rtu_get_rts_delay"
rtu_set_rts_delay :: Context -> Int -> IO ()
rtu_set_rts_delay h n = withContext h $ \ctx -> do
r <- modbus_rtu_set_rts_delay ctx n
if r == 0
then return ()
else throwErrno "modbus_rtu_set_rts_delay"
-- | The address of a modbus device.
newtype DeviceAddress = DeviceAddress Int
deriving (Show, Eq)
broadcastAddress :: DeviceAddress
broadcastAddress = DeviceAddress #const (MODBUS_BROADCAST_ADDRESS)
-- | Set the address of the modbus device that the Context should
-- communicate with.
set_slave :: Context -> DeviceAddress -> IO ()
set_slave h (DeviceAddress n) = withContext h $ \ctx -> do
r <- modbus_set_slave ctx n
if r == 0
then return ()
else throwErrno "modbus_set_slave"
connect :: Context -> IO ()
connect h = withContext h $ \ctx -> do
r <- modbus_connect ctx
if r == 0
then return ()
else throwErrno "modbus_connect"
set_debug :: Context -> Bool -> IO ()
set_debug h b = withContext h $ \ctx -> do
r <- modbus_set_debug ctx $
if b
then #const (TRUE)
else #const (FALSE)
if r == 0
then return ()
else throwErrno "modbus_set_debug"
data Timeout = Timeout
{ to_sec :: Word32
, to_usec :: Word32
}
deriving (Eq, Show)
get_timeout ::(Ptr () -> Ptr Word32 -> Ptr Word32 -> IO Int) -> String -> Context -> IO Timeout
get_timeout action actionname h =
withContext h $ \ctx ->
alloca $ \secp ->
alloca $ \usecp -> do
r <- action ctx secp usecp
if r == 0
then do
sec <- peek secp
usec <- peek usecp
return $ Timeout sec usec
else throwErrno actionname
set_timeout :: (Ptr () -> Ptr Word32 -> Ptr Word32 -> IO Int) -> String -> Context -> Timeout -> IO ()
set_timeout action actionname h timeout =
withContext h $ \ctx ->
alloca $ \secp ->
alloca $ \usecp -> do
poke secp (to_sec timeout)
poke usecp (to_usec timeout)
r <- action ctx secp usecp
if r == 0
then return ()
else throwErrno actionname
get_byte_timeout :: Context -> IO Timeout
get_byte_timeout = get_timeout
modbus_get_byte_timeout
"modbus_get_byte_timeout"
set_byte_timeout :: Context -> Timeout -> IO ()
set_byte_timeout = set_timeout
modbus_set_byte_timeout
"modbus_set_byte_timeout"
get_response_timeout :: Context -> IO Timeout
get_response_timeout = get_timeout
modbus_get_response_timeout
"modbus_get_response_timeout"
set_response_timeout :: Context -> Timeout -> IO ()
set_response_timeout = set_timeout
modbus_set_response_timeout
"modbus_set_response_timeout"
-- | An address on a modbus device.
newtype Addr = Addr Int
deriving (Show, Eq)
-- | A mutable vector that is used as a buffer when reading or writing
-- registers of a modbus device.
type RegisterVector = VM.IOVector Word16
-- | Allocates a vector holding the contents of a specified number
-- of registers.
--
-- The values are initialized to 0 to start.
mkRegisterVector :: Int -> IO RegisterVector
mkRegisterVector sz = VM.replicate sz 0
-- | Types that can hold modbus register data.
--
-- Of these, `RegisterVector` is the most efficient as it avoids
-- allocating new memory on each read or write. But it can be more useful
-- to get a ByteString and use a library such as cereal or binary to
-- parse the contents of the modbus registers.
class RegisterData t where
fromRegisterVector :: RegisterVector -> IO t
toRegisterVector :: t -> IO RegisterVector
instance RegisterData RegisterVector where
fromRegisterVector = pure
toRegisterVector = pure
instance RegisterData (V.Vector Word16) where
fromRegisterVector = V.freeze
toRegisterVector = V.thaw
instance RegisterData B.ByteString where
fromRegisterVector v =
B.pack . V.toList . castbytes <$> fromRegisterVector v
where
-- Simply interpret the vector as bytes.
castbytes :: V.Vector Word16 -> V.Vector Word8
castbytes = V.unsafeCast
toRegisterVector =
toRegisterVector . castbytes . V.fromList . B.unpack
where
-- If there are an odd number of bytes, the last
-- byte will be omitted.
castbytes :: V.Vector Word8 -> V.Vector Word16
castbytes = V.unsafeCast
instance RegisterData L.ByteString where
fromRegisterVector v = L.fromStrict <$> fromRegisterVector v
toRegisterVector = toRegisterVector . L.toStrict
-- | Reads the holding registers from the modbus device, starting at
-- the Addr, into the RegisterVector buffer.
read_registers :: RegisterData t => Context -> Addr -> RegisterVector -> IO t
read_registers h addr v = do
accessVector h addr v
modbus_read_registers
"modbus_read_registers"
fromRegisterVector v
-- | Reads the input registers from the modbus device, starting at
-- the Addr, into the RegisterVector buffer.
read_input_registers :: RegisterData t => Context -> Addr -> RegisterVector -> IO t
read_input_registers h addr v = do
accessVector h addr v
modbus_read_input_registers
"modbus_read_input_registers"
fromRegisterVector v
-- | Writes the registers to the modbus device, starting at
-- the Addr.
write_registers :: Context -> Addr -> RegisterVector -> IO ()
write_registers h addr v =
accessVector h addr v
modbus_write_registers
"modbus_write_registers"
write_register :: Context -> Addr -> Word16 -> IO ()
write_register h (Addr addr) val = withContext h $ \ctx -> do
r <- modbus_write_register ctx addr val
if r == -1
then throwErrno "modbus_write_register"
else return ()
write_and_read_registers
:: Context
-> Addr
-- ^ address to write to
-> RegisterVector
-- ^ data to write
-> Addr
-- ^ address to read from
-> RegisterVector
-- ^ data to read
-> IO ()
write_and_read_registers h (Addr write_addr) write_v (Addr read_addr) read_v =
withContext h $ \ctx -> do
let (write_fptr, write_nb) = VM.unsafeToForeignPtr0 write_v
let (read_fptr, read_nb) = VM.unsafeToForeignPtr0 read_v
r <- withForeignPtr write_fptr $ \write_ptr ->
withForeignPtr read_fptr $ \read_ptr ->
modbus_write_and_read_registers ctx
write_addr write_nb write_ptr
read_addr read_nb read_ptr
if r == -1
then throwErrno actionname
else if r /= read_nb
then ioError $ IOError Nothing OtherError
actionname "short read" Nothing Nothing
else return ()
where
actionname = "modbus_write_and_read_registers"
-- | A mutable vector that is used as a buffer when reading or writing
-- bits (coils) of a modbus device.
type BitVector = VM.IOVector Bit
-- | Allocates a vector holding the specified number of bits.
--
-- The bits are set to start.
mkBitVector :: Int -> IO BitVector
mkBitVector sz = VM.replicate sz #const (TRUE)
type Bit = Word8
boolBit :: Bit -> Bool
boolBit b = b == #const (TRUE)
bitBool :: Bool -> Bit
bitBool True = #const (TRUE)
bitBool False = #const (FALSE)
-- | Types that can hold modbus bit data.
--
-- Of these, `BitVector` is the most efficient as it avoids
-- allocating new memory on each read or write. But it can be easier
-- to use a Vector of Bool.
class BitData t where
fromBitVector :: BitVector -> IO t
toBitVector :: t -> IO BitVector
instance BitData BitVector where
fromBitVector = pure
toBitVector = pure
instance BitData (V.Vector Word8) where
fromBitVector = V.freeze
toBitVector = V.thaw
instance BitData (V.Vector Bool) where
fromBitVector v = V.map boolBit <$> fromBitVector v
toBitVector = toBitVector . V.map bitBool
-- | Reads the bits (coils) from the modbus device, starting at
-- the Addr, into the BitVector.
read_bits :: BitData t => Context -> Addr -> BitVector -> IO t
read_bits h addr v = do
accessVector h addr v
modbus_read_bits
"modbus_read_bits"
fromBitVector v
-- | Reads the input bits from the modbus device, starting at
-- the Addr, into the BitVector.
read_input_bits :: BitData t => Context -> Addr -> BitVector -> IO t
read_input_bits h addr v = do
accessVector h addr v
modbus_read_input_bits
"modbus_read_input_bits"
fromBitVector v
-- | Writes the bits (coils) of the modbus device, starting at
-- the Addr.
write_bits :: Context -> Addr -> BitVector -> IO ()
write_bits h addr v = accessVector h addr v
modbus_write_bits
"modbus_write_bits"
write_bit :: Context -> Addr -> Bit -> IO ()
write_bit h (Addr addr) val = withContext h $ \ctx -> do
r <- modbus_write_bit ctx addr (fromIntegral val)
if r == -1
then throwErrno "modbus_write_bit"
else return ()