{-# LANGUAGE CPP #-} module Robotics.NXT.Protocol ( -- * Initialization withNXT, defaultDevice, -- * Motors setOutputState, setOutputStateConfirm, getOutputState, resetMotorPosition, -- * Sensors setInputMode, setInputModeConfirm, getInputValues, resetInputScaledValue, -- * Miscellaneous getVersion, getDeviceInfo, getBatteryLevel, isBatteryRechargeable, keepAlive, keepAliveConfirm, getSleepTimeout, getLastKeepAliveTime, stopEverything, shutdown, -- * Remote Programs -- | It is possible to remotely run and control (with messages) programs on the NXT brick. Those here are low-level functions -- but check also high-level "Robotics.NXT.Remote" and "Robotics.NXT.MotorControl" modules. startProgram, startProgramConfirm, stopProgram, stopProgramConfirm, stopProgramExisting, ensureStartProgram, getCurrentProgramName, -- * Messages -- | It is possible to control programs on the NXT brick with messages. Those here are low-level functions -- but check also high-level "Robotics.NXT.Remote" and "Robotics.NXT.MotorControl" modules. messageWrite, messageWriteConfirm, messageRead, maybeMessageRead, ensureMessageRead, -- * Sounds playSoundFile, playSoundFileConfirm, playTone, stopSoundPlayback, stopSoundPlaybackConfirm, -- * Low Speed (I2C) -- | With those low-level functions it is possible to communicate with digital sensors attached to the NXT brick. But check -- also high-level "Robotics.NXT.Sensor.Ultrasonic" and "Robotics.NXT.Sensor.Compass" modules. lowspeedGetStatus, lowspeedWrite, lowspeedWriteConfirm, lowspeedRead, -- * Filesystem openWrite, openWriteLinear, write, writeConfirm, close, closeConfirm, delete, deleteConfirm, deleteExisting, -- * IO Map -- | Interface to NXT firmware is based on internal IO map interface. All commands are in fact just pretty wrappers to this -- interface, but it is possible to use it directly and thus gain some additional possibilities which are not -- available otherwise (some of those are already wrapped in this interface's additional functions and feel free to suggest -- more if you need them). getModuleID, listModules, requestFirstModule, requestNextModule, closeModuleHandle, closeModuleHandleConfirm, readIOMap, writeIOMap, writeIOMapConfirm, -- * Internals -- | Be careful when using those functions as you have to assure your program is well-behaved: you should see 'NXTInternals' as a -- token you have to pass around in order, not reusing or copying values. (The only exception is that you can reuse the token -- initally returned by 'initialize' in 'terminate' call, even if you have used it in-between.) They are exposed so that you can -- decouple initalization, execution and termination phase. If you do not need that use 'withNXT'. initialize, terminate, runNXT, execNXT ) where import qualified Data.ByteString as B import Control.Exception import Control.Monad.State import Data.Bits import Data.Char import Data.List hiding (delete) import Data.Maybe import Data.Ratio import Data.Time.Clock.POSIX import Data.Word import System.IO import qualified System.Hardware.Serialport as S #if (!defined(mingw32_HOST_OS) && !defined(windows_HOST_OS)) import System.Posix.Signals #endif import Text.Printf import Robotics.NXT.Data import Robotics.NXT.Errors import Robotics.NXT.Types import Robotics.NXT.Internals -- Described in Lego Mindstorms NXT Bluetooth Developer Kit: -- Appendix 1 - Communication protocol -- Appendix 2 - Direct commands -- TODO: All functions which requests ModuleInfo could populate module ID cache along the way -- TODO: Add an optional warning if direction of communication changes -- TODO: Implement all missing "confirm" versions of functions {-| Default Bluetooth serial device filename for current operating system. Currently always @\/dev\/rfcomm0@. -} defaultDevice :: FilePath defaultDevice = "/dev/rfcomm0" debug :: Bool debug = False {-| Opens and intializes a Bluetooth serial device communication. -} initialize :: FilePath -> IO NXTInternals initialize device = do #if (!defined(mingw32_HOST_OS) && !defined(windows_HOST_OS)) -- we have to block signals from interrupting openFd system call (fixed in GHC versions after 6.12.1) let signals = foldl (flip addSignal) emptySignalSet [virtualTimerExpired] blockSignals signals #endif h <- S.openSerial device S.defaultSerialSettings { S.commSpeed = S.CS115200, S.timeout = 1000 } #if (!defined(mingw32_HOST_OS) && !defined(windows_HOST_OS)) unblockSignals signals #endif when debug $ hPutStrLn stderr "initialized" return $ NXTInternals h Nothing [] Nothing Nothing {-| Stops all NXT activities (by calling 'stopEverything') and closes the Bluetooth serial device communication. 'NXTInternals' token must not be used after that anymore. -} terminate :: NXTInternals -> IO () terminate i = do i' <- execNXT stopEverything i let h = nxthandle i' S.closeSerial h when debug $ hPutStrLn stderr "terminated" {-| Function which initializes and terminates Bluetooth connection to the NXT brick (using 'initialize' and 'terminate') and in-between runs given computation. It terminates Bluetooth connection on an exception, too, rethrowing it afterwards. -} withNXT :: FilePath -> NXT a -> IO a withNXT device action = mask $ \restore -> do i <- initialize device (r, i') <- restore (runNXT action i) `onException` terminate i terminate i' return r -- Main function for sending data to NXT -- It calculates the length and prepends it to the message sendData :: [Word8] -> NXT () sendData message = do h <- getsNXT nxthandle let len = toUWord . length $ message packet = len ++ message n <- liftIO . S.send h . B.pack $ packet when (n /= length packet) $ liftIO $ failNXT' "not all data has been send" when debug $ liftIO . hPutStrLn stderr $ "sent: " ++ show packet -- Main function for receiving data from NXT receiveData :: NXT [Word8] receiveData = do h <- getsNXT nxthandle len <- liftIO $ S.recv h 2 let len' = fromUWord . B.unpack $ len packet <- liftIO $ S.recv h len' let unpacket = B.unpack packet when debug $ liftIO . hPutStrLn stderr $ "received: " ++ show unpacket return unpacket {-| Gets firmware and protocol versions of the NXT brick. -} getVersion :: NXT Version getVersion = do when debug $ liftIO . hPutStrLn stderr $ "getversion" let send = [0x01, 0x88] sendData send receive <- receiveData case receive of [0x02, 0x88, 0x00, pMinor, pMajor, fMinor, fMajor] -> return $ Version (FirmwareVersion fMajor' fMinor') (ProtocolVersion pMajor' pMinor') where fMajor' = fromIntegral fMajor fMinor' = fromIntegral fMinor pMajor' = fromIntegral pMajor pMinor' = fromIntegral pMinor _:_:e:_ -> liftIO $ failNXT "getVersion" e _ -> liftIO $ failNXT' "getVersion" {-| Gets device (the NXT brick) information: name, Bluetooth 48 bit address in the string format, strength of Bluetooth signal (not implemented in current NXT firmware versions, use 'bluetoothRSSI' or 'bluetoothLinkQuality' as an alternative), free space on flash. -} getDeviceInfo :: NXT DeviceInfo getDeviceInfo = do when debug $ liftIO . hPutStrLn stderr $ "getdeviceinfo" let send = [0x01, 0x9B] sendData send receive <- receiveData case receive of 0x02:0x9B:0x00:info | length info == 30 -> do modifyNXT (\s -> s { address = Just btaddress }) -- we cache it return $ DeviceInfo name' btaddress btstrength flashfree where (name, info') = splitAt 15 info name' = dataToString0 name btaddress = map toUpper . intercalate ":" . map (printf "%02x") . take 6 $ info' -- 7th byte not used? btstrength = fromULong . take 4 . drop 7 $ info' flashfree = fromULong . take 4 . drop 11 $ info' _:_:e:_ -> liftIO $ failNXT "getDeviceInfo" e _ -> liftIO $ failNXT' "getDeviceInfo" {-| Starts a given program on the NXT brick. -} startProgram :: FileName -> NXT () startProgram = startProgram' False {-| Same as 'startProgram' but also request a confirmation. Useful to assure the command was really accepted, but this does not assure that the program has really started successfully (especially not that it is already running when the confirmation is received). Use 'ensureStartProgram' for that. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} startProgramConfirm :: FileName -> NXT () startProgramConfirm = startProgram' True startProgram' :: Bool -> FileName -> NXT () startProgram' confirm filename = do when debug $ liftIO . hPutStrLn stderr $ "startprogram" let send = [request confirm, 0x00] ++ nameToData filename sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x00, 0x00] -> return () [_, _, e] -> liftIO $ failNXT "startProgram" e _ -> liftIO $ failNXT' "startProgram" {-| Stops a currently running program. -} stopProgram :: NXT () stopProgram = stopProgram' False False {-| Same as 'stopProgram' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} stopProgramConfirm :: NXT () stopProgramConfirm = stopProgram' True False {-| Same as 'stopProgramConfirm' but it also requires that the program was really running. It throws a 'NXTException' otherwise. -} stopProgramExisting :: NXT () stopProgramExisting = stopProgram' True True stopProgram' :: Bool -> Bool -> NXT () stopProgram' confirm running = do when debug $ liftIO . hPutStrLn stderr $ "stopprogram" let send = [request confirm, 0x01] sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x01, 0x00] -> return () [0x02, 0x01, 0xEC] -> when running $ liftIO $ failNXT "stopProgram" 0xEC [_, _, e] -> liftIO $ failNXT "stopProgram" e _ -> liftIO $ failNXT' "stopProgram" -- TODO: Could probably loop infinitely in some strange situation? Some timeout could be useful? {-| Helper function which first ensures that no other program is running and then ensures that a given program is really running before it returns. -} ensureStartProgram :: FileName -> NXT () ensureStartProgram filename = do stopAndWait startAndWait where stopAndWait = do stopProgramConfirm name <- getCurrentProgramName unless (isNothing name) stopAndWait startAndWait = do startProgramConfirm filename name <- getCurrentProgramName unless (isJust name) startAndWait {-| Plays a given sound file. -} playSoundFile :: LoopPlayback -> FileName -> NXT () playSoundFile = playSoundFile' False {-| Same as 'playSoundFile' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} playSoundFileConfirm :: LoopPlayback -> FileName -> NXT () playSoundFileConfirm = playSoundFile' True playSoundFile' :: Bool -> LoopPlayback -> FileName -> NXT () playSoundFile' confirm loop filename = do when debug $ liftIO . hPutStrLn stderr $ "playsoundfile" let send = [request confirm, 0x02, fromIntegral . fromEnum $ loop] ++ nameToData filename sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x02, 0x00] -> return () [_, _, e] -> liftIO $ failNXT "playSoundFile" e _ -> liftIO $ failNXT' "playSoundFile" {-| Plays a tone with a given frequency (in hertz) for a given duration (in seconds). -} playTone :: Frequency -> Duration -> NXT () playTone frequency duration = do when debug $ liftIO . hPutStrLn stderr $ "playtone" let send = [0x80, 0x03] ++ toUWord frequency ++ toUWord (toMilliseconds duration) sendData send where toMilliseconds :: Duration -> Integer -- duration is in seconds, but NXT requires milliseconds toMilliseconds d = floor (d * 1000) {-| Sets output port (motor) state. This is the main function for controlling a motor. -} setOutputState :: OutputPort -> OutputPower -> [OutputMode] -> RegulationMode -> TurnRatio -> RunState -> TachoLimit -> NXT () setOutputState = setOutputState' False {-| Same as 'setOutputState' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} setOutputStateConfirm :: OutputPort -> OutputPower -> [OutputMode] -> RegulationMode -> TurnRatio -> RunState -> TachoLimit -> NXT () setOutputStateConfirm = setOutputState' True setOutputState' :: Bool -> OutputPort -> OutputPower -> [OutputMode] -> RegulationMode -> TurnRatio -> RunState -> TachoLimit -> NXT () setOutputState' confirm output power mode regulation turn runstate tacholimit | power >= -100 && power <= 100 && turn >= -100 && turn <= 100 = do when debug $ liftIO . hPutStrLn stderr $ "setoutputstate" let send = [request confirm, 0x04, fromIntegral . fromEnum $ output] ++ toSByte power ++ [modebyte, regulation'] ++ toSByte turn ++ [runstate'] ++ toULong tacholimit sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x04, 0x00] -> return () [_, _, e] -> liftIO $ failNXT "setOutputState" e _ -> liftIO $ failNXT' "setOutputState" | otherwise = liftIO . throwIO $ PatternMatchFail "setOutputState" where modebyte = foldl (.|.) 0x00 . map convmode $ mode where convmode m = case m of MotorOn -> 0x01 Brake -> 0x02 Regulated -> 0x04 regulation' = case regulation of RegulationModeIdle -> 0x00 RegulationModeMotorSpeed -> 0x01 RegulationModeMotorSync -> 0x02 runstate' = case runstate of MotorRunStateIdle -> 0x00 MotorRunStateRampUp -> 0x10 MotorRunStateRunning -> 0x20 MotorRunStateRampDown -> 0x40 MotorRunStateHold -> 0x60 {-| Gets output port (motor) current state. In additional to values used with 'setOutputState' also 'TachoCount', 'BlockTachoCount' and 'RotationCount' values are available which tell you current position of a motor. -} getOutputState :: OutputPort -> NXT OutputState getOutputState output = do when debug $ liftIO . hPutStrLn stderr $ "getoutputstate" let send = [0x00, 0x06, fromIntegral . fromEnum $ output] sendData send receive <- receiveData case receive of 0x02:0x06:0x00:port:power:modebyte:regulation:turn:runstate:values | length values == 16 && fromEnum output == fromIntegral port -> return $ OutputState output (fromSByte [power]) mode regulation' (fromSByte [turn]) runstate' tacholimit tachocount blocktachocount rotationcount where mode = motoron ++ brake ++ regulated motoron = [MotorOn | testBit modebyte 0] brake = [Brake | testBit modebyte 1] regulated = [Regulated | testBit modebyte 2] regulation' = case regulation of 0x00 -> RegulationModeIdle 0x01 -> RegulationModeMotorSpeed 0x02 -> RegulationModeMotorSync _ -> throw $ PatternMatchFail "getOutputState" runstate' = case runstate of 0x00 -> MotorRunStateIdle 0x10 -> MotorRunStateRampUp 0x20 -> MotorRunStateRunning 0x40 -> MotorRunStateRampDown 0x60 -> MotorRunStateHold _ -> throw $ PatternMatchFail "getOutputState" tacholimit = fromULong . take 4 $ values tachocount = fromSLong . take 4 . drop 4 $ values blocktachocount = fromSLong . take 4 . drop 8 $ values rotationcount = fromSLong . take 4 . drop 12 $ values _:_:e:_ -> liftIO $ failNXT "getOutputState" e _ -> liftIO $ failNXT' "getOutputState" {-| Sets input port (sensor) type and mode. -} setInputMode :: InputPort -> SensorType -> SensorMode -> NXT () setInputMode = setInputMode' False {-| Same as 'setInputMode' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} setInputModeConfirm :: InputPort -> SensorType -> SensorMode -> NXT () setInputModeConfirm = setInputMode' True setInputMode' :: Bool -> InputPort -> SensorType -> SensorMode -> NXT () setInputMode' confirm input sensortype sensormode = do when debug $ liftIO . hPutStrLn stderr $ "setinputmode" let send = [request confirm, 0x05, fromIntegral . fromEnum $ input, sensortype', sensormode'] sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x05, 0x00] -> return () [_, _, e] -> liftIO $ failNXT "setInputMode" e _ -> liftIO $ failNXT' "setInputMode" where sensortype' = case sensortype of NoSensor -> 0x00 Switch -> 0x01 Temperature -> 0x02 Reflection -> 0x03 Angle -> 0x04 LightActive -> 0x05 LightInactive -> 0x06 SoundDB -> 0x07 SoundDBA -> 0x08 Custom -> 0x09 Lowspeed -> 0x0A Lowspeed9V -> 0x0B NoOfSensorTypes -> 0x0C sensormode' = case sensormode of RawMode -> 0x00 BooleanMode -> 0x20 TransitionCntMode -> 0x40 PeriodCounterMode -> 0x60 PctFullScaleMode -> 0x80 CelsiusMode -> 0xA0 FahrenheitMode -> 0xC0 AngleStepsMode -> 0xE0 -- two modes unnecessary? -- SlopeMask -> 0x1F -- ModeMask -> 0xE0 {-| Gets input port (sensor) values. This is the main function for reading a sensor. -} getInputValues :: InputPort -> NXT InputValue getInputValues input = do when debug $ liftIO . hPutStrLn stderr $ "getinputvalues" let send = [0x00, 0x07, fromIntegral . fromEnum $ input] sendData send receive <- receiveData case receive of 0x02:0x07:0x00:port:valid:calibrated:sensortype:sensormode:values | length values == 8 && fromEnum input == fromIntegral port -> return $ InputValue input valid' calibrated' sensortype' sensormode' raw normalized scaled calibratedv where valid' = valid /= 0x00 calibrated' = calibrated /= 0x00 sensortype' = case sensortype of 0x00 -> NoSensor 0x01 -> Switch 0x02 -> Temperature 0x03 -> Reflection 0x04 -> Angle 0x05 -> LightActive 0x06 -> LightInactive 0x07 -> SoundDB 0x08 -> SoundDBA 0x09 -> Custom 0x0A -> Lowspeed 0x0B -> Lowspeed9V 0x0C -> NoOfSensorTypes _ -> throw $ PatternMatchFail "getInputValues" sensormode' = case sensormode of 0x00 -> RawMode 0x20 -> BooleanMode 0x40 -> TransitionCntMode 0x60 -> PeriodCounterMode 0x80 -> PctFullScaleMode 0xA0 -> CelsiusMode 0xC0 -> FahrenheitMode 0xE0 -> AngleStepsMode -- two modes unnecessary? -- 0x1F -> SlopeMask -- 0xE0 -> ModeMask _ -> throw $ PatternMatchFail "getInputValues" raw = fromUWord . take 2 $ values normalized = fromUWord . take 2 . drop 2 $ values scaled = fromSWord . take 2 . drop 4 $ values calibratedv = fromSWord . take 2 . drop 6 $ values _:_:e:_ -> liftIO $ failNXT "getInputValues" e _ -> liftIO $ failNXT' "getInputValues" {-| Resets input port (sensor) scaled value. -} resetInputScaledValue :: InputPort -> NXT () resetInputScaledValue input = do when debug $ liftIO . hPutStrLn stderr $ "resetinputscaledvalue" let send = [0x80, 0x08, fromIntegral . fromEnum $ input] sendData send {-| Writes a message to the given inbox queue of the running remote program. A message length is limited to 58 characters/bytes. A queue is limited to 5 messages. -} messageWrite :: Inbox -> String -> NXT () messageWrite = messageWrite' False {-| Same as 'messageWrite' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} messageWriteConfirm :: Inbox -> String -> NXT () messageWriteConfirm = messageWrite' True messageWrite' :: Bool -> Inbox -> String -> NXT () messageWrite' confirm inbox message | length message <= 58 = do when debug $ liftIO . hPutStrLn stderr $ "messagewrite" let message' = messageToData message send = [request confirm, 0x09, fromIntegral . fromEnum $ inbox] ++ (toUByte . length $ message') ++ message' sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x09, 0x00] -> return () [_, _, e] -> liftIO $ failNXT "messageWrite" e _ -> liftIO $ failNXT' "messageWrite" | otherwise = liftIO . throwIO $ PatternMatchFail "messageWrite" {-| Resets one of three position counters for a given output port. -} resetMotorPosition :: OutputPort -> MotorReset -> NXT () resetMotorPosition output reset = do when debug $ liftIO . hPutStrLn stderr $ "resetmotorposition" case reset of InternalPosition -> do mid <- getModuleID "Output.mod" writeIOMap (fromJust mid) (fromEnum output * 32 + 18) [0x08] -- flags field is at offset 18, output block is 32 bytes long, UPDATE_RESET_COUNT is 0x08 _ -> do let send = [0x80, 0x0A, fromIntegral . fromEnum $ output, fromIntegral . fromEnum $ reset] sendData send {-| Gets current battery level (in volts). -} getBatteryLevel :: NXT Voltage getBatteryLevel = do when debug $ liftIO . hPutStrLn stderr $ "getbatterylevel" let send = [0x00, 0x0B] sendData send receive <- receiveData case receive of [0x02, 0x0B, 0x00, v1, v2] -> return $ fromUWord [v1, v2] % 1000 -- voltage is in millivolts _:_:e:_ -> liftIO $ failNXT "getBatteryLevel" e _ -> liftIO $ failNXT' "getBatteryLevel" {-| Is battery used in the NXT brick rechargeable? -} isBatteryRechargeable :: NXT Bool isBatteryRechargeable = do when debug $ liftIO . hPutStrLn stderr $ "isbatteryrechargeable" mid <- getModuleID "Ui.mod" r <- readIOMap (fromJust mid) 35 1 return $ (/=) 0 (head r) {-| Stops current sound file playback. -} stopSoundPlayback :: NXT () stopSoundPlayback = stopSoundPlayback' False {-| Same as 'stopSoundPlayback' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} stopSoundPlaybackConfirm :: NXT () stopSoundPlaybackConfirm = stopSoundPlayback' True stopSoundPlayback' :: Bool -> NXT () stopSoundPlayback' confirm = do when debug $ liftIO . hPutStrLn stderr $ "stopsoundplayback" let send = [request confirm, 0x0C] sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x0C, 0x00] -> return () [_, _, e] -> liftIO $ failNXT "stopSoundPlayback" e _ -> liftIO $ failNXT' "stopSoundPlayback" {-| Sends a keep alive (turned on) packet. It prevents the NXT brick from automatically powering off. Other commands do not prevent that from hapenning so it is useful to send this packet from time to time if you want to prevent powering off. -} keepAlive :: NXT () keepAlive = keepAlive' False >> return () {-| Same as 'keepAlive' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} keepAliveConfirm :: NXT () keepAliveConfirm = keepAlive' True >> return () keepAlive' :: Bool -> NXT Duration keepAlive' confirm = do when debug $ liftIO . hPutStrLn stderr $ "keepalive" current <- liftIO getPOSIXTime modifyNXT (\s -> s { lastkeepalive = Just current }) let send = [request confirm, 0x0D] sendData send if confirm then do receive <- receiveData case receive of 0x02:0x0D:0x00:limit -> do let l = fromRational $ fromULong limit % 1000 -- limit is in milliseconds modifyNXT (\s -> s { sleeptime = Just l }) return l _:_:e:_ -> liftIO $ failNXT "keepAlive" e _ -> liftIO $ failNXT' "keepAlive" else return 0 {-| Gets current sleep timeout setting (in seconds) after which the NXT brick automatically powers off if not prevented with a keep alive packet (use 'keepAlive' to send one). This setting is cached. -} getSleepTimeout :: NXT Duration getSleepTimeout = do sleep <- getsNXT sleeptime case sleep of Just s -> return s Nothing -> keepAlive' True {-| When was a last keep alive packet send? -} getLastKeepAliveTime :: NXT (Maybe POSIXTime) getLastKeepAliveTime = getsNXT lastkeepalive {-| Gets number of bytes available to read. -} lowspeedGetStatus :: InputPort -> NXT Int lowspeedGetStatus input = do when debug $ liftIO . hPutStrLn stderr $ "lowspeedgetstatus" let send = [0x00, 0x0E, fromIntegral . fromEnum $ input] sendData send receive <- receiveData case receive of [0x02, 0x0E, 0x00, bytes] -> return $ fromUByte [bytes] 0x02:0x10:0x20:_ -> lowspeedGetStatus input -- pending communication transaction in progress, retrying _:_:e:_ -> liftIO $ failNXT "lowSpeedGetStatus" e _ -> liftIO $ failNXT' "lowSpeedGetStatus" {-| Writes data. At most 16 bytes can be written at a time. Reply data length must be specified in the write command since reading from the device is done on a master-slave basis. -} lowspeedWrite :: InputPort -> RxDataLength -> TxData -> NXT () lowspeedWrite = lowspeedWrite' False {-| Same as 'lowspeedWrite' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} lowspeedWriteConfirm :: InputPort -> RxDataLength -> TxData -> NXT () lowspeedWriteConfirm = lowspeedWrite' True lowspeedWrite' :: Bool -> InputPort -> RxDataLength -> TxData -> NXT () lowspeedWrite' confirm input rx txdata | length txdata <= 16 && rx <= 16 = do when debug $ liftIO . hPutStrLn stderr $ "lowspeedwrite" let send = [request confirm, 0x0F, fromIntegral . fromEnum $ input] ++ (toUByte . length $ txdata) ++ toUByte rx ++ txdata sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x0F, 0x00] -> return () [_, _, e] -> liftIO $ failNXT "lowspeedWrite" e _ -> liftIO $ failNXT' "lowspeedWrite" | otherwise = liftIO . throwIO $ PatternMatchFail "lowspeedWrite" {-| Reads data. The protocol does not support variable-length return packages so the response always contains 16 data bytes with invalid data padded with zeros. -} lowspeedRead :: InputPort -> NXT RxData lowspeedRead input = do when debug $ liftIO . hPutStrLn stderr $ "lowspeedread" let send = [0x00, 0x10, fromIntegral . fromEnum $ input] sendData send receive <- receiveData case receive of 0x02:0x10:0x00:rx:rxdata | length rxdata == 16 && rx <= 16 -> return $ take (fromUByte [rx]) rxdata 0x02:0x10:0x20:_ -> lowspeedRead input -- pending communication transaction in progress, retrying _:_:e:_ -> liftIO $ failNXT "lowSpeedRead" e _ -> liftIO $ failNXT' "lowSpeedRead" {-| Gets the name of the currently running program, if any. -} getCurrentProgramName :: NXT (Maybe String) getCurrentProgramName = do when debug $ liftIO . hPutStrLn stderr $ "getcurrentprogramname" let send = [0x00, 0x11] sendData send receive <- receiveData case receive of 0x02:0x11:0x00:filename | length filename == 20 -> return $ Just $ dataToString0 filename 0x02:0x11:0xEC:_ -> return Nothing _:_:e:_ -> liftIO $ failNXT "getCurrentProgramName" e _ -> liftIO $ failNXT' "getCurrentProgramName" {-| Reads a message from the currently running program from a given remote inbox queue. A queue is limited to 5 messages. It throws a 'NXTException' if there is no message in a remote inbox queue. -} messageRead :: RemoteInbox -> RemoveMessage -> NXT String messageRead inbox remove = do m <- maybeMessageRead inbox remove case m of Just m' -> return m' Nothing -> liftIO $ failNXT "messageRead" 0x40 -- TODO: Could probably loop infinitely? Some timeout could be useful? {-| Same as 'messageWrite' but if there is no message in a given remote inbox queue it retries until there is. -} ensureMessageRead :: RemoteInbox -> RemoveMessage -> NXT String ensureMessageRead inbox remove = do m <- maybeMessageRead inbox remove case m of Just m' -> return m' Nothing -> ensureMessageRead inbox remove {-| Same as 'messageWrite' but returns 'Nothing' if there is no message in a given remote inbox queue. -} maybeMessageRead :: RemoteInbox -> RemoveMessage -> NXT (Maybe String) maybeMessageRead inbox remove = do when debug $ liftIO . hPutStrLn stderr $ "messageRead" let inbox' = fromIntegral . fromEnum $ inbox send = [0x00, 0x13, inbox', fromIntegral . fromEnum $ Inbox0, fromIntegral . fromEnum $ remove] -- local inbox number does not matter for PC, it is used only when master NXT reads from slave NXT sendData send receive <- receiveData case receive of 0x02:0x13:0x00:inbox'':size:message | inbox'' == inbox' && length message == 59 && size <= 59 -> return $ Just $ dataToString0 message 0x02:0x13:0x40:_ -> return Nothing _:_:e:_ -> liftIO $ failNXT "messageRead" e _ -> liftIO $ failNXT' "messageRead" {-| Helper function which stops all NXT brick activities: stops motors and disables sensors. -} stopEverything :: NXT () stopEverything = do when debug $ liftIO . hPutStrLn stderr $ "stopeverything" mapM_ stopMotor [A ..] mapM_ stopSensor [One ..] where stopMotor x = setOutputState x 0 [] RegulationModeIdle 0 MotorRunStateIdle 0 stopSensor x = setInputMode x NoSensor RawMode {-| Shutdowns (powers off) the NXT brick. You have to manually turn it on again. -} shutdown :: NXT () shutdown = do when debug $ liftIO . hPutStrLn stderr $ "shutdown" mid <- getModuleID "IOCtrl.mod" writeIOMap (fromJust mid) 0 [0x00, 0x5A] {-| Opens a given file for writing as a linked list of flash sectors. -} openWrite :: FileName -> FileSize -> NXT FileHandle openWrite filename filesize = do when debug $ liftIO . hPutStrLn stderr $ "openwrite" let send = [0x01, 0x81] ++ nameToData filename ++ toULong filesize sendData send receive <- receiveData case receive of [0x02, 0x81, 0x00, h] -> return $ fromUByte [h] _:_:e:_ -> liftIO $ failNXT "openWrite" e _ -> liftIO $ failNXT' "openWrite" {-| Opens a given file for writing as a linear contiguous block of flash memory (required for user programs and certain data files). -} openWriteLinear :: FileName -> FileSize -> NXT FileHandle openWriteLinear filename filesize = do when debug $ liftIO . hPutStrLn stderr $ "openwritelinear" let send = [0x01, 0x89] ++ nameToData filename ++ toULong filesize sendData send receive <- receiveData case receive of [0x02, 0x89, 0x00, h] -> return $ fromUByte [h] _:_:e:_ -> liftIO $ failNXT "openWriteLinear" e _ -> liftIO $ failNXT' "openWriteLinear" {-| Writes data to a file. At most 61 bytes can be written at a time. -} write :: FileHandle -> FileData -> NXT () write = write' False {-| Same as 'write' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} writeConfirm :: FileHandle -> FileData -> NXT () writeConfirm = write' True write' :: Bool -> FileHandle -> FileData -> NXT () write' confirm filehandle filedata | length filedata <= 61 = do when debug $ liftIO . hPutStrLn stderr $ "write" let send = [request' confirm, 0x83] ++ toUByte filehandle ++ filedata sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x83, 0x00, h, bw1, bw2] | fromUByte [h] == filehandle && length filedata == fromUWord [bw1, bw2] -> return () _:_:e:_ -> liftIO $ failNXT "write" e _ -> liftIO $ failNXT' "write" | otherwise = liftIO . throwIO $ PatternMatchFail "write" {-| Closes a file. -} close :: FileHandle -> NXT () close = close' False {-| Same as 'close' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} closeConfirm :: FileHandle -> NXT () closeConfirm = close' True close' :: Bool -> FileHandle -> NXT () close' confirm filehandle = do when debug $ liftIO . hPutStrLn stderr $ "close" let send = [request' confirm, 0x84] ++ toUByte filehandle sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x84, 0x00, h] | fromUByte [h] == filehandle -> return () _:_:e:_ -> liftIO $ failNXT "close" e _ -> liftIO $ failNXT' "close" {-| Deletes a given file. -} delete :: FileName -> NXT () delete = delete' False False {-| Same as 'delete' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} deleteConfirm :: FileName -> NXT () deleteConfirm = delete' True False {-| Same as 'deleteConfirm' but it also requires that the file exists before deletion. It throws a 'NXTException' otherwise. -} deleteExisting :: FileName -> NXT () deleteExisting = delete' True True delete' :: Bool -> Bool -> FileName -> NXT () delete' confirm existence filename = do when debug $ liftIO . hPutStrLn stderr $ "delete" let send = [request' confirm, 0x85] ++ nameToData filename sendData send when confirm $ do receive <- receiveData case receive of 0x02:0x85:0x00:f | dataToString0 f == filename -> return () 0x02:0x85:0x87:_ -> when existence $ liftIO $ failNXT "delete" 0x87 _:_:e:_ -> liftIO $ failNXT "delete" e _ -> liftIO $ failNXT' "delete" -- TODO: Populate cache here? {-| Requests information about the first module matching a given module name (which can be a wild card). Returned module handle can be used for followup requests and has to be closed when not needed anymore. -} requestFirstModule :: ModuleName -> NXT (ModuleHandle, Maybe ModuleInfo) requestFirstModule modulename = do when debug $ liftIO . hPutStrLn stderr $ "requestfirstmodule" let send = [0x01, 0x90] ++ nameToData modulename sendData send receive <- receiveData case receive of 0x02:0x90:0x00:h:values | length values == 30 -> return (fromUByte [h], Just $ ModuleInfo name moduleid size iomapsize) where name = dataToString0 . take 20 $ values moduleid = fromULong . take 4 . drop 20 $ values size = fromULong . take 4 . drop 24 $ values iomapsize = fromUWord . take 2 . drop 28 $ values 0x02:0x90:0x90:h:_ -> return (fromUByte [h], Nothing) -- module not found _:_:e:_ -> liftIO $ failNXT "requestFirstModule" e _ -> liftIO $ failNXT' "requestFirstModule" -- TODO: Populate cache here? {-| Requests information about the next module matching previously requested module name (which can be a wild card). Returned module handle can be used for followup requests and has to be closed when not needed anymore. -} requestNextModule :: ModuleHandle -> NXT (ModuleHandle, Maybe ModuleInfo) requestNextModule modulehandle = do when debug $ liftIO . hPutStrLn stderr $ "requestnextmodule" let send = [0x01, 0x91] ++ toUByte modulehandle sendData send receive <- receiveData case receive of 0x02:0x91:0x00:h:values | length values == 30 -> return (fromUByte [h], Just $ ModuleInfo name moduleid size iomapsize) where name = dataToString0 . take 20 $ values moduleid = fromULong . take 4 . drop 20 $ values size = fromULong . take 4 . drop 24 $ values iomapsize = fromUWord . take 2 . drop 28 $ values 0x02:0x91:0x90:h:_ -> return (fromUByte [h], Nothing) -- module not found _:_:e:_ -> liftIO $ failNXT "requestNextModule" e _ -> liftIO $ failNXT' "requestNextModule" {-| Closes module handle of previously requested module information. -} closeModuleHandle :: ModuleHandle -> NXT () closeModuleHandle = closeModuleHandle' False {-| Same as 'closeModuleHandle' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} closeModuleHandleConfirm :: ModuleHandle -> NXT () closeModuleHandleConfirm = closeModuleHandle' True closeModuleHandle' :: Bool -> ModuleHandle -> NXT () closeModuleHandle' confirm modulehandle = do when debug $ liftIO . hPutStrLn stderr $ "closemodulehandle" let send = [request' confirm, 0x92] ++ toUByte modulehandle sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x92, 0x00, h] | fromUByte [h] == modulehandle -> return () _:_:e:_ -> liftIO $ failNXT "closeModuleHandle" e _ -> liftIO $ failNXT' "closeModuleHandle" -- TODO: Populate cache here? -- TODO: Use bracket to ensure closed module handle? {-| Helper function to get information about all modules matching a given module name (which can be a wild card). -} listModules :: ModuleName -> NXT [ModuleInfo] listModules modulename = do (h, first) <- requestFirstModule modulename case first of Nothing -> do closeModuleHandle h return [] Just first' -> do (h', other) <- next h closeModuleHandle h' return (first':other) where next h = do (h', mi) <- requestNextModule h case mi of Just mi' -> do (h'', mi'') <- next h' return (h'', mi':mi'') Nothing -> return (h', []) {-| Reads data from an IO map of a given module. At most 119 bytes can be read at a time. You probably have to know what different values at different positions mean and control. The best way is to check NXT firmware source code. -} readIOMap :: ModuleID -> IOMapOffset -> IOMapLength -> NXT IOMapData readIOMap moduleid offset len | offset >= 0 && len <= 119 = do when debug $ liftIO . hPutStrLn stderr $ "readiomap" let send = [0x01, 0x94] ++ toULong moduleid ++ toUWord offset ++ toUWord len sendData send receive <- receiveData case receive of 0x02:0x94:0x00:mid1:mid2:mid3:mid4:r1:r2:values | fromULong [mid1, mid2, mid3, mid4] == moduleid && fromUWord [r1, r2] == len -> return values _:_:e:_ -> liftIO $ failNXT "readIOMap" e _ -> liftIO $ failNXT' "readIOMap" | otherwise = liftIO . throwIO $ PatternMatchFail "readIOMap" {-| Writes data to an IO map of a given module. At most 54 bytes can be written at a time. You probably have to know what different values at different positions mean and control. The best way is to check NXT firmware source code. -} writeIOMap :: ModuleID -> IOMapOffset -> IOMapData -> NXT () writeIOMap = writeIOMap' False {-| Same as 'writeIOMap' but also request a confirmation. Useful to assure the command was really accepted. In a case of an error it throws a 'NXTException'. Confirmation requires a change of the direction of NXT Bluetooth communication which takes around 30 ms. -} writeIOMapConfirm :: ModuleID -> IOMapOffset -> IOMapData -> NXT () writeIOMapConfirm = writeIOMap' True writeIOMap' :: Bool -> ModuleID -> IOMapOffset -> IOMapData -> NXT () writeIOMap' confirm moduleid offset mapdata | offset >= 0 && length mapdata <= 54 = do when debug $ liftIO . hPutStrLn stderr $ "writeiomap" let send = [request' confirm, 0x95] ++ toULong moduleid ++ toUWord offset ++ toUWord (length mapdata) ++ mapdata sendData send when confirm $ do receive <- receiveData case receive of [0x02, 0x95, 0x00, mid1, mid2, mid3, mid4, w1, w2] | fromULong [mid1, mid2, mid3, mid4] == moduleid && fromUWord [w1, w2] == length mapdata -> return () _:_:e:_ -> liftIO $ failNXT "writeIOMap" e _ -> liftIO $ failNXT' "writeIOMap" | otherwise = liftIO . throwIO $ PatternMatchFail "writeIOMap" {-| Helper function to get an ID of a module matching a given module name. Each module encompass some firmware functionality. Function caches IDs so it hopefully retrieves it from a cache of previous requests. -} getModuleID :: ModuleName -> NXT (Maybe ModuleID) getModuleID modulename | '*' `elem` modulename = return Nothing -- we do not allow wild cards | otherwise = do mods <- getsNXT modules let modulename' = map toLower modulename case modulename' `lookup` mods of Just (ModuleInfo _ mid _ _) -> return $ Just mid Nothing -> do (h, mi) <- requestFirstModule modulename' closeModuleHandle h case mi of Just mi'@(ModuleInfo _ mid _ _) -> do modifyNXT (\s -> s { modules = (modulename', mi'):mods }) return $ Just mid Nothing -> return Nothing request :: Bool -> Word8 request confirm | confirm = 0x00 | otherwise = 0x80 request' :: Bool -> Word8 request' confirm | confirm = 0x01 | otherwise = 0x81