diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e2c12a4..77863ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -38,9 +38,9 @@ jobs: strategy: matrix: cabal: - - '3.12' + - '3.16' ghc: - - '9.8.2' + - '9.8.4' os: - "ubuntu-latest" name: Haskell CI diff --git a/CHANGELOG.md b/CHANGELOG.md index c17169d..97f2541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# Version [0.2.0.0](https://github.com/DistRap/network-can/compare/0.1.0.0...0.2.0.0) (2026-04-29) + +* Split `slcan` and `socketcan` into public sublibraries +* Migrate to `io-classes` and switch from `MonadCAN` typeclass + to `CAN` handle (record of functions style): + + ``` + data CAN m = CAN + { canSend :: CANMessage -> m () + , canRecv :: m CANMessage + } + ``` +* Runners renamed + * `runSocketCAN` is now `withSocketCAN` + * `runSLCAN` is now `withSLCAN` + to reflect the handle change + # Version [0.1.0.0](https://github.com/DistRap/network-can/compare/d50564...0.1.0.0) (2025-05-19) * Initial release diff --git a/README.lhs b/README.lhs new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/README.lhs @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/README.md b/README.md index d6661a2..dfc4cdb 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,22 @@ CAN bus networking using Linux SocketCAN or SLCAN backends. ```haskell import qualified Control.Monad -import qualified Control.Monad.IO.Class import qualified Network.CAN import qualified Network.SocketCAN main :: IO () main = do - Network.SocketCAN.runSocketCAN + Network.SocketCAN.withSocketCAN (Network.SocketCAN.mkCANInterface "vcan0") - $ do + $ \can -> do Network.CAN.send + can $ Network.CAN.standardMessage 0x123 [0xDE, 0xAD] Control.Monad.forever $ Network.CAN.recv - >>= Control.Monad.IO.Class.liftIO . print + can + >>= putStrLn . Network.CAN.prettyCANMessage ``` diff --git a/app/CANBridge.hs b/app/CANBridge.hs index cec255c..8ba8099 100644 --- a/app/CANBridge.hs +++ b/app/CANBridge.hs @@ -1,8 +1,8 @@ {-# LANGUAGE LambdaCase #-} module Main where -import Control.Monad.Trans (MonadTrans(lift)) -import Data.Default.Class (Default(def)) +import Control.Monad.Class.MonadAsync (race_) +import Data.Default (Default(def)) import Network.SLCAN (Transport(..)) import System.Hardware.Serialport (CommSpeed(..), SerialPortSettings(..)) @@ -11,7 +11,6 @@ import qualified Network.CAN import qualified Network.SocketCAN import qualified Network.SLCAN import qualified System.Hardware.Serialport -import qualified UnliftIO.Async -- | Bridge vcan0 to slcan over /dev/can4discouart serial port main :: IO () @@ -21,12 +20,12 @@ main = do (System.Hardware.Serialport.defaultSerialSettings { commSpeed = CS115200 } ) - Network.SLCAN.runSLCAN (Transport_Handle h) def $ do - Network.SocketCAN.runSocketCAN (Network.SocketCAN.mkCANInterface "vcan0") $ do - UnliftIO.Async.race_ + Network.SLCAN.withSLCAN (Transport_Handle h) def $ \slcan -> do + Network.SocketCAN.withSocketCAN (Network.SocketCAN.mkCANInterface "vcan0") $ \socketcan -> do + race_ (Control.Monad.forever - $ Network.CAN.recv >>= lift . Network.CAN.send + $ Network.CAN.recv slcan >>= Network.CAN.send socketcan ) (Control.Monad.forever - $ lift Network.CAN.recv >>= Network.CAN.send + $ Network.CAN.recv socketcan >>= Network.CAN.send slcan ) diff --git a/app/CANDump.hs b/app/CANDump.hs index f96068e..fdfecfc 100644 --- a/app/CANDump.hs +++ b/app/CANDump.hs @@ -1,22 +1,16 @@ module Main where import qualified Control.Monad -import qualified Control.Monad.IO.Class import qualified Network.CAN import qualified Network.SocketCAN main :: IO () main = do - Network.SocketCAN.runSocketCAN + Network.SocketCAN.withSocketCAN (Network.SocketCAN.mkCANInterface "vcan0") - (Control.Monad.forever - $ Network.CAN.recv - >>= Control.Monad.IO.Class.liftIO . print - ) - --- needs Network.CAN.Pretty or Builder or smthing --- that does the same ID formatting as SLCAN.Builder:78 --- a la --- $ candump -e vcan0 --- vcan0 001237E5 [2] 4C EE --- vcan0 7E5 [2] 4C EE + $ \can -> + (Control.Monad.forever + $ Network.CAN.recv + can + >>= putStrLn . Network.CAN.prettyCANMessage + ) diff --git a/app/SLCANSerial.hs b/app/SLCANSerial.hs index dc5a4c4..f66c376 100644 --- a/app/SLCANSerial.hs +++ b/app/SLCANSerial.hs @@ -1,9 +1,9 @@ module Main where -import Control.Monad.IO.Class -import Data.Default.Class (Default(def)) +import Control.Monad.Class.MonadSay (MonadSay(say)) +import Data.Default (Default(def)) import System.Hardware.Serialport (CommSpeed(..), SerialPortSettings(..)) -import Network.CAN (MonadCAN) +import Network.CAN (CAN) import Network.SLCAN (Transport(..)) import qualified Control.Monad @@ -23,18 +23,18 @@ main = do { commSpeed = CS115200 } ) - Network.SLCAN.runSLCAN + Network.SLCAN.withSLCAN (Transport_Handle h) def act act - :: ( MonadCAN m - , MonadIO m - ) - => m () -act = do + :: MonadSay m + => CAN m + -> m () +act can = do Network.CAN.send + can $ Network.CAN.standardMessage -- vendorID SDO 0x601 @@ -42,4 +42,5 @@ act = do Control.Monad.forever $ Network.CAN.recv - >>= Control.Monad.IO.Class.liftIO . print + can + >>= say . Network.CAN.prettyCANMessage diff --git a/app/SLCANUDP.hs b/app/SLCANUDP.hs index 11fbde0..e01a3db 100644 --- a/app/SLCANUDP.hs +++ b/app/SLCANUDP.hs @@ -1,8 +1,8 @@ module Main where -import Control.Monad.IO.Class -import Data.Default.Class (Default(def)) -import Network.CAN (MonadCAN) +import Control.Monad.Class.MonadSay (MonadSay(say)) +import Data.Default (Default(def)) +import Network.CAN (CAN) import Network.SLCAN (Transport(..)) import Network.Socket (AddrInfo(..), SocketType(Datagram)) @@ -36,7 +36,7 @@ main = do sock (addrAddress ourAddrinfo) - Network.SLCAN.runSLCAN + Network.SLCAN.withSLCAN (Transport_UDP sock (addrAddress targetAddrinfo)) def act @@ -44,16 +44,17 @@ main = do (_, _) -> error "getAddrInfo fail" act - :: ( MonadCAN m - , MonadIO m - ) - => m () -act = do + :: MonadSay m + => CAN m + -> m () +act can = do Network.CAN.send + can $ Network.CAN.standardMessage 0x7E5 [0x4C] Control.Monad.forever $ Network.CAN.recv - >>= Control.Monad.IO.Class.liftIO . print + can + >>= say . Network.CAN.prettyCANMessage diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..78b421a --- /dev/null +++ b/cabal.project @@ -0,0 +1,3 @@ +multi-repl: true + +packages: . diff --git a/cabal.project.local.ci b/cabal.project.local.ci index 16c31aa..e71265a 100644 --- a/cabal.project.local.ci +++ b/cabal.project.local.ci @@ -1 +1 @@ -flags: +build-apps +flags: +build-apps +build-readme diff --git a/network-can.cabal b/network-can.cabal index c2958a1..6de0b11 100644 --- a/network-can.cabal +++ b/network-can.cabal @@ -1,6 +1,6 @@ -cabal-version: 2.2 +cabal-version: 3.0 name: network-can -version: 0.1.0.0 +version: 0.2.0.0 synopsis: CAN bus networking description: Talk to CAN buses using Linux SocketCAN and SLCAN homepage: https://github.com/DistRap/network-can @@ -25,97 +25,141 @@ flag build-apps description: Build example applications +flag build-readme + default: + False + description: + Build readme example + +common commons + ghc-options: -Wall -Wunused-packages + default-language: Haskell2010 + +common execs + ghc-options: -threaded + -rtsopts + "-with-rtsopts -N" + library - ghc-options: -Wall + import: commons hs-source-dirs: src exposed-modules: Network.CAN - Network.CAN.Class Network.CAN.Types - Network.SLCAN + build-depends: base >= 4.7 && < 5 + , QuickCheck + +library slcan + import: commons + visibility: public + hs-source-dirs: src-slcan + exposed-modules: Network.SLCAN Network.SLCAN.Builder Network.SLCAN.Parser Network.SLCAN.Types - Network.SocketCAN - Network.SocketCAN.Bindings - Network.SocketCAN.Example - Network.SocketCAN.LowLevel - Network.SocketCAN.Translate - build-depends: base >= 4.7 && < 5 , attoparsec >= 0.14 , bytestring , containers - , data-default-class - , mtl + , data-default + , io-classes , network >= 3.1 + , network-can , QuickCheck - , transformers - , unliftio +library socketcan + import: commons + visibility: public + hs-source-dirs: src-socketcan + exposed-modules: Network.SocketCAN + Network.SocketCAN.Bindings + Network.SocketCAN.Example + Network.SocketCAN.LowLevel + Network.SocketCAN.Translate + build-depends: base >= 4.7 && < 5 + , io-classes + , network >= 3.1 + , network-can build-tool-depends: hsc2hs:hsc2hs - default-language: Haskell2010 test-suite pure type: exitcode-stdio-1.0 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N hs-source-dirs: test main-is: Spec.hs - other-modules: Samples + other-modules: CANSpec + Samples SLCANSpec SocketCANSpec build-tool-depends: hspec-discover:hspec-discover build-depends: base >= 4.7 && < 5 , hspec , network-can + , network-can:slcan + , network-can:socketcan default-language: Haskell2010 executable hcandump + import: commons, execs if !flag(build-apps) buildable: False build-depends: base >=4.7 && <5 , network-can - default-language: Haskell2010 + , network-can:socketcan main-is: CANDump.hs hs-source-dirs: app - ghc-options: -Wall -threaded -rtsopts "-with-rtsopts -N" executable hcanbridge + import: commons, execs if !flag(build-apps) buildable: False build-depends: base >=4.7 && <5 , network-can - , data-default-class - , mtl + , network-can:slcan + , network-can:socketcan + , data-default + , io-classes , serialport >= 0.5.5 - , unliftio - default-language: Haskell2010 main-is: CANBridge.hs hs-source-dirs: app - ghc-options: -Wall -threaded -rtsopts "-with-rtsopts -N" executable hslcanserial + import: commons, execs if !flag(build-apps) buildable: False build-depends: base >=4.7 && <5 , network-can - , data-default-class + , network-can:slcan + , data-default + , io-classes , serialport >= 0.5.5 - default-language: Haskell2010 main-is: SLCANSerial.hs hs-source-dirs: app - ghc-options: -Wall -threaded -rtsopts "-with-rtsopts -N" executable hslcanudp + import: commons, execs if !flag(build-apps) buildable: False build-depends: base >=4.7 && <5 , network , network-can - , data-default-class - default-language: Haskell2010 + , network-can:slcan + , data-default + , io-classes main-is: SLCANUDP.hs hs-source-dirs: app - ghc-options: -Wall -threaded -rtsopts "-with-rtsopts -N" + +executable readme + import: commons, execs + if !flag(build-readme) + buildable: False + build-depends: + base >=4.7 && <5 + , network-can + , network-can:socketcan + build-tool-depends: + markdown-unlit:markdown-unlit + main-is: README.lhs + ghc-options: -pgmL markdown-unlit -Wall source-repository head type: git diff --git a/src/Network/SLCAN.hs b/src-slcan/Network/SLCAN.hs similarity index 58% rename from src/Network/SLCAN.hs rename to src-slcan/Network/SLCAN.hs index 50694ad..7af0d27 100644 --- a/src/Network/SLCAN.hs +++ b/src-slcan/Network/SLCAN.hs @@ -1,7 +1,6 @@ -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-} + module Network.SLCAN ( Transport(..) , withSLCANTransport @@ -10,45 +9,41 @@ module Network.SLCAN , recvSLCANMessage , sendCANMessage , module Network.SLCAN.Types - , SLCANT(..) , SLCANException(..) - , runSLCAN + , withSLCAN ) where -import Control.Exception (Exception) +import Control.Monad.Class.MonadThrow (Exception(..), MonadThrow(throwIO), finally) import Control.Monad.IO.Class (MonadIO(..)) -import Control.Monad.Reader (MonadReader, ask) -import Control.Monad.Trans (MonadTrans(..)) -import Control.Monad.Trans.Reader (ReaderT(..)) import Network.Socket (Socket, SockAddr) -import Network.CAN (CANMessage, MonadCAN(..)) +import Network.CAN (CANMessage, CAN(..)) import Network.SLCAN.Types import System.IO (Handle) -import UnliftIO (MonadUnliftIO) import qualified Control.Monad -import qualified Control.Exception import qualified Data.ByteString import qualified Data.ByteString.Char8 import qualified System.IO import qualified Network.SLCAN.Builder import qualified Network.SLCAN.Parser import qualified Network.Socket.ByteString -import qualified UnliftIO data Transport = Transport_Handle Handle | Transport_UDP Socket SockAddr withSLCANTransport - :: Transport + :: ( MonadIO m + , MonadThrow m + ) + => Transport -> SLCANConfig - -> (Transport -> IO a) - -> IO a + -> (Transport -> m a) + -> m a withSLCANTransport transport SLCANConfig{..} act = do let sendC = sendSLCANControl transport - Control.Exception.finally + finally (do sendC SLCANControl_Close sendC (SLCANControl_Bitrate slCANConfigBitrate) @@ -66,26 +61,29 @@ withSLCANTransport transport SLCANConfig{..} act = do (sendC SLCANControl_Close) sendSLCANMessage - :: Transport + :: MonadIO m + => Transport -> SLCANMessage - -> IO () -sendSLCANMessage (Transport_Handle handle) msg = do + -> m () +sendSLCANMessage (Transport_Handle handle) msg = liftIO $ do Control.Monad.void $ Data.ByteString.hPutStr handle $ Network.SLCAN.Builder.buildSLCANMessage msg System.IO.hFlush handle -sendSLCANMessage (Transport_UDP socket target) msg = do - Network.Socket.ByteString.sendAllTo - socket - (Network.SLCAN.Builder.buildSLCANMessage msg) - target +sendSLCANMessage (Transport_UDP socket target) msg = + liftIO + $ Network.Socket.ByteString.sendAllTo + socket + (Network.SLCAN.Builder.buildSLCANMessage msg) + target sendSLCANControl - :: Transport + :: MonadIO m + => Transport -> SLCANControl - -> IO () + -> m () sendSLCANControl t = sendSLCANMessage t . SLCANMessage_Control @@ -128,66 +126,43 @@ sendCANMessage t = sendSLCANMessage t . SLCANMessage_Data -newtype SLCANT m a = SLCANT - { _unSLCANT :: ReaderT Transport m a } - deriving - ( Functor - , Applicative - , Monad - , MonadReader Transport - , MonadIO - , MonadUnliftIO - ) - -instance MonadTrans SLCANT where - lift = SLCANT . lift - --- | Run SLCANT transformer -runSLCANT - :: Monad m - => Transport - -> SLCANT m a - -> m a -runSLCANT t = - (`runReaderT` t) - . _unSLCANT - data SLCANException = SLCANException_ParseError String deriving Show instance Exception SLCANException -runSLCAN +withSLCAN :: ( MonadIO m - , MonadUnliftIO m + , MonadThrow m ) => Transport -> SLCANConfig - -> SLCANT m a + -> (CAN m -> m a) -> m a -runSLCAN transport config act = do - UnliftIO.withRunInIO $ \runInIO -> - withSLCANTransport - transport - config - (\t -> runInIO (runSLCANT t act)) - -instance MonadIO m => MonadCAN (SLCANT m) where - send cm = do - ask >>= liftIO . flip sendCANMessage cm - recv = do - transport <- ask - liftIO - (recvSLCANMessage transport) - >>= \case - Left e -> - UnliftIO.throwIO $ SLCANException_ParseError e - Right (SLCANMessage_Data cm) -> - pure cm - Right _other -> - -- TODO: do something with - -- SLCANMessage_Error - -- and SLCANMessage_State - -- like allow registering handlers for these - -- or throwIO on _Error one - recv +withSLCAN transport config act = do + withSLCANTransport + transport + config + $ \t -> + act + CAN + { canSend = liftIO . sendCANMessage t + , canRecv = + let + recv = + liftIO + (recvSLCANMessage t) + >>= \case + Left e -> + throwIO $ SLCANException_ParseError e + Right (SLCANMessage_Data cm) -> + pure cm + Right _other -> + -- TODO: do something with + -- SLCANMessage_Error + -- and SLCANMessage_State + -- like allow registering handlers for these + -- or throwIO on _Error one + recv + in recv + } diff --git a/src/Network/SLCAN/Builder.hs b/src-slcan/Network/SLCAN/Builder.hs similarity index 100% rename from src/Network/SLCAN/Builder.hs rename to src-slcan/Network/SLCAN/Builder.hs diff --git a/src/Network/SLCAN/Parser.hs b/src-slcan/Network/SLCAN/Parser.hs similarity index 100% rename from src/Network/SLCAN/Parser.hs rename to src-slcan/Network/SLCAN/Parser.hs diff --git a/src/Network/SLCAN/Types.hs b/src-slcan/Network/SLCAN/Types.hs similarity index 98% rename from src/Network/SLCAN/Types.hs rename to src-slcan/Network/SLCAN/Types.hs index 88b4877..01617ac 100644 --- a/src/Network/SLCAN/Types.hs +++ b/src-slcan/Network/SLCAN/Types.hs @@ -10,7 +10,7 @@ module Network.SLCAN.Types , SLCANConfig(..) ) where -import Data.Default.Class (Default(def)) +import Data.Default (Default(def)) import Data.Set (Set) import Data.Word (Word16) import Network.CAN.Types (CANMessage) diff --git a/src-socketcan/Network/SocketCAN.hs b/src-socketcan/Network/SocketCAN.hs new file mode 100644 index 0000000..e2a379b --- /dev/null +++ b/src-socketcan/Network/SocketCAN.hs @@ -0,0 +1,96 @@ +module Network.SocketCAN + ( withSocket + , sendCANMessage + , recvCANMessage + , Network.Socket.ifNameToIndex + , CANInterface + , mkCANInterface + , NoSuchInterface(..) + , withSocketCAN + ) where + +import Network.CAN (CANMessage, CAN(..)) +import Network.Socket (Socket) +import Network.SocketCAN.Bindings (SockAddrCAN(..)) + +import Control.Monad.Class.MonadThrow (Exception(..), MonadThrow(bracket, throwIO)) +import Control.Monad.IO.Class (MonadIO(..)) + +import qualified Network.Socket (ifNameToIndex) +import qualified Network.SocketCAN.LowLevel +import qualified Network.SocketCAN.Translate + +withSocket + :: ( MonadIO m + , MonadThrow m + ) + => Int + -> (Socket -> m a) + -> m a +withSocket ifaceIdx act = do + bracket + (liftIO Network.SocketCAN.LowLevel.socket) + (liftIO . Network.SocketCAN.LowLevel.close) + (\canSock -> do + liftIO + $ Network.SocketCAN.LowLevel.bind + canSock + $ Network.SocketCAN.Bindings.SockAddrCAN + $ fromIntegral ifaceIdx + act canSock + ) + +sendCANMessage + :: Socket + -> CANMessage + -> IO () +sendCANMessage canSock cm = + Network.SocketCAN.LowLevel.send + canSock + (Network.SocketCAN.Translate.toSocketCANFrame cm) + +recvCANMessage + :: Socket + -> IO CANMessage +recvCANMessage canSock = + Network.SocketCAN.LowLevel.recv canSock + >>= pure . Network.SocketCAN.Translate.fromSocketCANFrame + +newtype CANInterface = CANInterface + { unCANInterface :: String } + deriving Eq + +instance Show CANInterface where + show = unCANInterface + +mkCANInterface :: String -> CANInterface +mkCANInterface = CANInterface + +data NoSuchInterface = NoSuchInterface + deriving Show + +instance Exception NoSuchInterface + +withSocketCAN + :: ( MonadIO m + , MonadThrow m + ) + => CANInterface + -> (CAN m -> m a) + -> m a +withSocketCAN interface act = do + mIdx <- + liftIO + $ Network.Socket.ifNameToIndex (unCANInterface interface) + + case mIdx of + Nothing -> throwIO NoSuchInterface + Just idx -> + withSocket + idx + $ \sock -> + act + CAN + { canSend = liftIO . sendCANMessage sock + , canRecv = liftIO $ recvCANMessage sock + } diff --git a/src/Network/SocketCAN/Bindings.hsc b/src-socketcan/Network/SocketCAN/Bindings.hsc similarity index 100% rename from src/Network/SocketCAN/Bindings.hsc rename to src-socketcan/Network/SocketCAN/Bindings.hsc diff --git a/src/Network/SocketCAN/Example.hs b/src-socketcan/Network/SocketCAN/Example.hs similarity index 96% rename from src/Network/SocketCAN/Example.hs rename to src-socketcan/Network/SocketCAN/Example.hs index 8a1e818..a9dfc95 100644 --- a/src/Network/SocketCAN/Example.hs +++ b/src-socketcan/Network/SocketCAN/Example.hs @@ -14,7 +14,7 @@ example = do case mIdx of Nothing -> error $ "Interface " <> interface <> " not found" Just idx -> - withSocketCAN idx act + withSocket idx act act :: Socket -> IO () act sock = do diff --git a/src/Network/SocketCAN/LowLevel.hs b/src-socketcan/Network/SocketCAN/LowLevel.hs similarity index 100% rename from src/Network/SocketCAN/LowLevel.hs rename to src-socketcan/Network/SocketCAN/LowLevel.hs diff --git a/src/Network/SocketCAN/Translate.hs b/src-socketcan/Network/SocketCAN/Translate.hs similarity index 100% rename from src/Network/SocketCAN/Translate.hs rename to src-socketcan/Network/SocketCAN/Translate.hs diff --git a/src/Network/CAN.hs b/src/Network/CAN.hs index 8364b86..b9138ee 100644 --- a/src/Network/CAN.hs +++ b/src/Network/CAN.hs @@ -1,7 +1,24 @@ module Network.CAN - ( module Network.CAN.Class + ( CAN(..) + , send + , recv , module Network.CAN.Types ) where -import Network.CAN.Class import Network.CAN.Types + +data CAN m = CAN + { canSend :: CANMessage -> m () + , canRecv :: m CANMessage + } + +send + :: CAN m + -> CANMessage + -> m () +send = canSend + +recv + :: CAN m + -> m CANMessage +recv = canRecv diff --git a/src/Network/CAN/Class.hs b/src/Network/CAN/Class.hs deleted file mode 100644 index 2c713a6..0000000 --- a/src/Network/CAN/Class.hs +++ /dev/null @@ -1,39 +0,0 @@ -{-# LANGUAGE DefaultSignatures #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TypeOperators #-} -module Network.CAN.Class - ( MonadCAN(..) - ) where - -import Control.Monad.Trans (MonadTrans, lift) -import Control.Monad.Trans.Except (ExceptT) -import Control.Monad.Trans.Reader (ReaderT) -import Control.Monad.Trans.State (StateT) -import Network.CAN.Types (CANMessage(..)) - -class Monad m => MonadCAN m where - - send :: CANMessage -> m () - default send - :: ( MonadTrans t - , MonadCAN m' - , m ~ t m' - ) - => CANMessage - -> m () - send = lift . send - - recv :: m CANMessage - default recv - :: ( MonadTrans t - , MonadCAN m' - , m ~ t m' - ) - => m CANMessage - recv = lift recv - -instance MonadCAN m => MonadCAN (ExceptT e m) -instance MonadCAN m => MonadCAN (ReaderT r m) -instance MonadCAN m => MonadCAN (StateT s m) diff --git a/src/Network/CAN/Types.hs b/src/Network/CAN/Types.hs index cd17a20..0166530 100644 --- a/src/Network/CAN/Types.hs +++ b/src/Network/CAN/Types.hs @@ -8,19 +8,21 @@ module Network.CAN.Types -- * Message , CANMessage(..) , standardMessage + , prettyCANMessage ) where import Data.Word (Word8, Word16, Word32) import Test.QuickCheck (Arbitrary(..)) import qualified Test.QuickCheck +import qualified Text.Printf -- * Arbitration data CANArbitrationField = CANArbitrationField - { canArbitrationFieldID :: Word32 -- ^ CAN ID - , canArbitrationFieldExtended :: Bool -- ^ Extended CAN ID - , canArbitrationFieldRTR :: Bool -- ^ Remote transmission request + { canArbitrationFieldID :: Word32 -- ^ CAN ID + , canArbitrationFieldExtended :: Bool -- ^ Extended CAN ID + , canArbitrationFieldRTR :: Bool -- ^ Remote transmission request } deriving (Eq, Ord, Show) instance Arbitrary CANArbitrationField where @@ -89,3 +91,40 @@ standardMessage cid cdata = CANMessage { canMessageArbitrationField = standardID cid , canMessageData = cdata } + +-- | Pretty print @CANMessage@ similar to candump output +-- +-- > prettyCANMessage (standardMessage 123 [0x13, 0x37]) +-- " 07B [2] 13 37" +-- > prettyCANMessage (CANMessage (extendedID 123) [0x13, 0x37]) +-- "0000007B [2] 13 37" +prettyCANMessage + :: CANMessage + -> String +prettyCANMessage msg = + unwords + $ [ prettyArb + $ canMessageArbitrationField msg + , " [" <> show (length $ canMessageData msg) <> "] " + ] + ++ prettyData + (canArbitrationFieldRTR $ canMessageArbitrationField msg) + (canMessageData msg) + where + prettyArb arb | canArbitrationFieldExtended arb = + hexFixed + 8 + $ canArbitrationFieldID arb + prettyArb arb | otherwise = + replicate 5 ' ' + <> hexFixed + 3 + (canArbitrationFieldID arb) + + prettyData :: Bool -> [Word8] -> [String] + prettyData True _ = pure "remote request" + prettyData _ x = map (hexFixed 2) x + + hexFixed width = + Text.Printf.printf + $ "%0" <> show (width :: Int) <> "X" diff --git a/src/Network/SocketCAN.hs b/src/Network/SocketCAN.hs deleted file mode 100644 index d1ce974..0000000 --- a/src/Network/SocketCAN.hs +++ /dev/null @@ -1,122 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -module Network.SocketCAN - ( withSocketCAN - , sendCANMessage - , recvCANMessage - , Network.Socket.ifNameToIndex - , SocketCANT - , CANInterface - , mkCANInterface - , NoSuchInterface(..) - , runSocketCAN - ) where - -import Network.CAN (CANMessage, MonadCAN(..)) -import Network.Socket (Socket) -import Network.SocketCAN.Bindings (SockAddrCAN(..)) - -import Control.Monad.Reader (MonadReader, ask) -import Control.Monad.Trans (MonadTrans(..)) -import Control.Monad.Trans.Reader (ReaderT(..)) -import UnliftIO - -import qualified Control.Exception -import qualified Network.Socket (ifNameToIndex) -import qualified Network.SocketCAN.LowLevel -import qualified Network.SocketCAN.Translate - -withSocketCAN - :: Int - -> (Socket -> IO a) - -> IO a -withSocketCAN ifaceIdx act = do - Control.Exception.bracket - Network.SocketCAN.LowLevel.socket - Network.SocketCAN.LowLevel.close - (\canSock -> do - Network.SocketCAN.LowLevel.bind - canSock - $ Network.SocketCAN.Bindings.SockAddrCAN - $ fromIntegral ifaceIdx - act canSock - ) - -sendCANMessage - :: Socket - -> CANMessage - -> IO () -sendCANMessage canSock cm = - Network.SocketCAN.LowLevel.send - canSock - (Network.SocketCAN.Translate.toSocketCANFrame cm) - -recvCANMessage - :: Socket - -> IO CANMessage -recvCANMessage canSock = - Network.SocketCAN.LowLevel.recv canSock - >>= pure . Network.SocketCAN.Translate.fromSocketCANFrame - -newtype SocketCANT m a = SocketCANT - { _unSocketCANT :: ReaderT Socket m a } - deriving - ( Functor - , Applicative - , Monad - , MonadReader Socket - , MonadIO - , MonadUnliftIO - ) - -instance MonadTrans SocketCANT where - lift = SocketCANT . lift - --- | Run SocketCANT transformer -runSocketCANT - :: Monad m - => Socket - -> SocketCANT m a - -> m a -runSocketCANT sock = - (`runReaderT` sock) - . _unSocketCANT - -newtype CANInterface = CANInterface - { unCANInterface :: String } - deriving Eq - -instance Show CANInterface where - show = unCANInterface - -mkCANInterface :: String -> CANInterface -mkCANInterface = CANInterface - -data NoSuchInterface = NoSuchInterface - deriving Show - -instance Exception NoSuchInterface - -runSocketCAN - :: ( MonadIO m - , MonadUnliftIO m - ) - => CANInterface - -> SocketCANT m a - -> m a -runSocketCAN interface act = do - mIdx <- - liftIO - $ Network.Socket.ifNameToIndex (unCANInterface interface) - - case mIdx of - Nothing -> throwIO NoSuchInterface - Just idx -> withRunInIO $ \runInIO -> - withSocketCAN idx (\s -> runInIO (runSocketCANT s act)) - -instance MonadIO m => MonadCAN (SocketCANT m) where - send cm = do - canSock <- ask - liftIO $ sendCANMessage canSock cm - recv = do - canSock <- ask - liftIO $ recvCANMessage canSock diff --git a/test/CANSpec.hs b/test/CANSpec.hs new file mode 100644 index 0000000..a975759 --- /dev/null +++ b/test/CANSpec.hs @@ -0,0 +1,21 @@ +module CANSpec where + +import Test.Hspec (Spec, describe, it, shouldBe) +import Samples + +import qualified Network.CAN + +spec :: Spec +spec = do + describe "CAN" $ do + it "pretty prints samples" $ + map Network.CAN.prettyCANMessage samples + `shouldBe` + [ " 000 [0] " + , " FFF [0] " + , " 123 [2] DE AD" + , " 123 [0] remote request" + , "00000000 [0] " + , "00123456 [1] EE" + , "00123456 [0] remote request" + ]