-- | EEPROMex library for arduino-copilot. -- -- This module is designed to be imported qualified. -- -- This is an interface to the C EEPROMex library, which will need to be -- installed in your Arduino development environment. -- https://playground.arduino.cc/Code/EEPROMex/ {-# LANGUAGE RebindableSyntax #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeFamilies #-} module Copilot.Arduino.Library.EEPROMex ( -- * Configuration maxAllowedWrites, memPool, StartAddress(..), EndAddress(..), EEPROMable, -- * Single values alloc, alloc', Location, -- * Ranges Range, RangeIndex, allocRange, sweepRange, sweepRange', RangeWrites(..), ) where import Copilot.Arduino import Copilot.Arduino.Internals import Control.Monad.Writer import Data.Proxy -- | Set the maximum number of writes to EEPROM that can be made while -- the Arduino is running. When too many writes have been made, -- it will no longer write to the EEPROM, and will send warning -- messages to the serial port. -- -- This is a strongly encouraged safety measure to use, because the -- Arduino's EEPROM can only endure around 100,000 writes, and -- a Sketch that's constantly writing to EEPROM, without a well chosen -- `:@` rate limit or `delay`, could damage your hardware in just a few -- minutes. -- -- Note that this module uses EEPROMex's update facility for all writes -- to EEPROM. That avoids writing a byte when its current value is -- the same as what needed to be written. That prevents excessive wear -- in some cases, but you should still use maxAllowedWrites too. -- -- For this to work, CPPFLAGS needs to include "-D_EEPROMEX_DEBUG" when the -- EEPROMex C library gets built. When you use this, it generates C -- code that makes sure that is enabled. maxAllowedWrites :: Word16 -> Sketch () maxAllowedWrites n = tell [(return (), f)] where f = mempty { earlySetups = [ CLine "#ifdef _EEPROMEX_DEBUG" , CLine $ "EEPROM.setMaxAllowedWrites(" <> show n <> ");" , CLine "#else" , CLine "#error \"maxAllowedWrites cannot be checked because _EEPROMEX_DEBUG is not set.\"" , CLine "#endif" ] , defines = [ includeCLine ] } -- | The address of the first byte of the EEPROM that will be allocated -- by `alloc`. The default is to start allocation from 0. -- -- Picking a different StartAddress can avoid overwriting the -- part of the EEPROM that is used by some other program. newtype StartAddress = StartAddress Word16 deriving (Num, Eq, Ord, Enum, Show, Read, Bounded, Integral, Real) -- | The address of the last byte of the EEPROM that can be allocated -- by `alloc`. The default is to allow allocating 512 bytes. -- -- Modules for particular boards, such as Copilot.Arduino.Library.Uno, -- each define a sizeOfEEPROM value, so to use the entire EEPROM, use: -- -- > EndAddress sizeOfEEPROM newtype EndAddress = EndAddress Word16 deriving (Num, Eq, Ord, Enum, Show, Read, Bounded, Integral, Real) -- | Configure the EEPROM memory pool that the program can use. -- -- > EEPROM.memPool 0 (EndAddress sizeOfEEPROM) memPool :: StartAddress -> EndAddress -> Sketch () memPool (StartAddress start) (EndAddress end) = tell [(return (), f)] where f = mempty -- setMemPool() has to come before any -- getAddress(), so do it in earlySetups. { earlySetups = [ CLine $ "EEPROM.setMemPool(" <> show start <> ", " <> show end <> ");" ] , defines = [ includeCLine ] } -- | Allocates a location in the EEPROM. -- -- Two things are returned; first a `Behavior` which contains a value -- read from the EEPROM at boot; and secondly a `Location` that can be -- written to later on. -- -- Here's an example of using it, to remember the maximum value read from -- a1, persistently across power cycles. -- -- > EEPROM.maxAllowedWrites 100 -- > (bootval, eepromloc) <- EEPROM.alloc -- > currval <- input a1 :: Sketch (Behavior ADC) -- > let maxval = [maxBound] ++ if currval > bootval then currval else bootval -- > eepromloc =: currval @: (currval > maxval) -- > delay =: MilliSeconds (constant 10000) -- -- Of course, the EEPROM could already contain any value at the allocated -- location to start with (either some default value or something written -- by another program). So you'll need to first boot up a sketch that zeros -- it out before using the sketch above. Here's a simple sketch that zeros -- two values: -- -- > EEPROM.maxAllowedWrites 100 -- > (_, eepromloc1) <- EEPROM.alloc -- > (_, eepromloc2) <- EEPROM.alloc -- > eepromloc1 =: constant (0 :: ADC) :@ firstIteration -- > eepromloc2 =: constant (0 :: Word16) :@ firstIteration -- -- `alloc` can be used as many times as you want, storing multiple values -- in the EEPROM, up to the limit of the size of the EEPROM. -- (Which is not statically checked.) -- -- Do note that the EEPROM layout is controlled by the order of calls to -- `alloc`, so take care when reordering or deleting calls. alloc :: forall t. (EEPROMable t) => Sketch (Behavior t, Location t) alloc = alloc' (factoryValue (Proxy @t)) -- | Same as `alloc'`, but with a value which will be used -- as the EEPROM's boot value when interpreting the Sketch, -- instead of the default of acting as if all bits of the EEPROM are -- set. alloc' :: forall t. (EEPROMable t) => t -> Sketch (Behavior t, Location t) alloc' interpretval = do i <- getUniqueId let addrvarname = "eeprom_address" <> show i let bootvarname = "eeprom_boot_val" <> show i let writername = "eeprom_write" <> show i let proxy = Proxy @t bootval <- mkInput $ InputSource { defineVar = [ includeCLine , CLine $ "int " <> addrvarname <> ";" , CLine $ showCType proxy <> " " <> bootvarname <> ";" , CLine $ "void " <> writername <> "(" <> showCType proxy <> " value) {" , CLine $ " EEPROM." <> writeValue proxy <> "(" <> addrvarname <> ", value);" , CLine "}" ] , setupInput = [ CLine $ addrvarname <> " = EEPROM.getAddress(sizeof(" <> showCType proxy <> "));" , CLine $ bootvarname <> " = EEPROM." <> readValue proxy <> "(" <> addrvarname <> ");" ] , readInput = [] , inputStream = extern bootvarname (Just (repeat interpretval)) , inputPinmode = mempty } return (bootval, Location writername) data Location t = Location String instance EEPROMable t => Output (Location t) (Event () (Stream t)) where Location writername =: (Event v c) = tell [(trigger writername c [arg v], mempty)] -- | A range of values in the EEPROM. data Range t = Range { rangeStart :: Location (Range t) , rangeSize :: Word16 -- ^ number of `t` values in the Range } -- | Allocates a Range in the EEPROM, which stores the specified number of -- items of some type. -- -- This is an example of using a range of the EEPROM as a ring buffer, -- to store the last 100 values read from a1. -- -- > EEPROM.maxAllowedWrites 1000 -- > range <- EEPROM.allocRange 100 :: Sketch (EEPROM.Range ADC) -- > currval <- input a1 :: Sketch (Behavior ADC) -- > range =: EEPROM.sweepRange 0 currval -- > delay =: MilliSeconds (constant 10000) -- -- `allocRange` can be used as many times as you want, and combined with -- uses of `alloc`, up to the size of the EEPROM. -- (Which is not statically checked.) -- -- Do note that the EEPROM layout is controlled by the order of calls to -- `allocRange` and `alloc`, so take care when reordering or deleting calls. allocRange :: forall t. (EEPROMable t) => Word16 -> Sketch (Range t) allocRange sz = do i <- getUniqueId let startaddrvarname = "eeprom_range_address" <> show i let writername = "eeprom_range_write" <> show i let proxy = Proxy @t let f = Framework { defines = [ includeCLine , CLine $ "int " <> startaddrvarname <> ";" , CLine $ "void " <> writername <> "(" <> showCType proxy <> " value" <> ", " <> showCType (Proxy @Word16) <> " offset" <> ") {" , CLine $ " EEPROM." <> writeValue proxy <> "(" <> startaddrvarname <> " + offset*sizeof(" <> showCType proxy <> ")" <> ", value);" , CLine "}" ] , setups = [ CLine $ startaddrvarname <> " = EEPROM.getAddress" <> "(sizeof(" <> showCType proxy <> ")" <> " * " <> show sz <> ");" ] , earlySetups = [] , pinmodes = mempty , loops = mempty } tell [(return (), f)] return (Range (Location writername) sz) -- | Description of writes made to a Range. -- -- This can be turned into an `Event` by using `@:` with it, and that's -- what the Behavior Bool is needed for. Consider this example, -- that ignores the Behavior Bool, and just uses a counter for the -- `RangeIndex`: -- -- > range =: EEPROM.RangeWrites (\_ -> counter) (constant 0) @: blinking -- -- The use of `@:` `blinking` makes an `Event` that only occurs on every other -- iteration of the Sketch. But, the counter increases on every -- iteration of the Sketch. So that will only write to every other value -- in the `Range`. -- -- The Behavior Bool is only True when an event occurs, and so -- it can be used to avoid incrementing the counter otherwise. See --`sweepRange'` for an example. data RangeWrites t = RangeWrites (Behavior Bool -> Behavior RangeIndex) (Behavior t) -- | An index into a Range. 0 is the first value in the Range. -- -- Indexes larger than the size of the Range will not overflow it, -- instead they loop back to the start of the Range. type RangeIndex = Word16 instance EEPROMable t => Output (Range t) (RangeWrites t) where (=:) = writeRange true instance EEPROMable t => Output (Range t) (Event () (RangeWrites t)) where range =: Event ws c = writeRange c range ws instance EEPROMable t => IsBehavior (RangeWrites t) where (@:) = Event type instance BehaviorToEvent (RangeWrites t) = Event () (RangeWrites t) writeRange :: EEPROMable t => Behavior Bool -> Range t -> RangeWrites t -> Sketch() writeRange c range (RangeWrites idx v) = tell [(trigger writername c [arg idx', arg v], mempty)] where Location writername = rangeStart range idx' = idx c `mod` constant (rangeSize range) -- | Treat the range as a ring buffer, and starting with the specified -- `RangeIndex`, sweep over the range writing values from the -- `Behavior`. sweepRange :: RangeIndex -> Behavior t -> RangeWrites t sweepRange start = RangeWrites (sweepRange' start) -- | This is actually just a simple counter that increments on each write -- to the range. That's sufficient, because writes that overflow the -- end of the range wrap back to the start. sweepRange' :: RangeIndex -> Behavior Bool -> Behavior RangeIndex sweepRange' start c = cnt where cnt = [start] ++ rest rest = if c then cnt + 1 else cnt class (ShowCType t, Typed t) => EEPROMable t where readValue :: Proxy t -> String writeValue :: Proxy t -> String factoryValue :: Proxy t -> t -- ^ The EEPROM comes from the factory with all bits set, -- this follows suite. Eg, maxBound for unsigned ints, -- minBound for signed ints since the sign bit being set -- makes it negative, and NaN for floats. -- | This instance is not efficient; a whole byte is read/written -- rather than a single bit. instance EEPROMable Bool where readValue _ = "readByte" writeValue _ = "updateByte" factoryValue _ = True instance EEPROMable Int8 where readValue _ = "readByte" writeValue _ = "updateByte" factoryValue _ = minBound instance EEPROMable Int16 where readValue _ = "readInt" writeValue _ = "updateInt" factoryValue _= minBound instance EEPROMable Int32 where readValue _ = "readLong" writeValue _ = "updateLong" factoryValue _= minBound instance EEPROMable Word8 where readValue _ = "readByte" writeValue _ = "updateByte" factoryValue _ = maxBound instance EEPROMable Word16 where readValue _ = "readInt" writeValue _ = "updateInt" factoryValue _ = maxBound instance EEPROMable Word32 where readValue _ = "readLong" writeValue _ = "updateLong" factoryValue _ = maxBound instance EEPROMable Float where readValue _ = "readFloat" writeValue _ = "updateFloat" factoryValue _ = 0/0 -- NaN instance EEPROMable Double where readValue _ = "readDouble" writeValue _ = "updateDOuble" factoryValue _ = 0/0 -- NaN includeCLine :: CLine includeCLine = CLine "#include <EEPROMex.h>"