packages feed

arduino-copilot 1.4.0 → 1.5.0

raw patch · 19 files changed

+1026/−248 lines, 19 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Copilot.Arduino: infixr 4 ||
- Copilot.Arduino.Internals: instance Control.Monad.Writer.Class.MonadWriter [(Copilot.Language.Spec.Spec, Copilot.Arduino.Internals.Framework)] Copilot.Arduino.Internals.Sketch
+ Copilot.Arduino: TypedBehavior :: Behavior t -> TypedBehavior p t
+ Copilot.Arduino: class IfThenElse t a
+ Copilot.Arduino: instance Copilot.Arduino.IfThenElse Copilot.Arduino.Internals.Sketch ()
+ Copilot.Arduino: instance Copilot.Core.Type.Typed a => Copilot.Arduino.IfThenElse (Copilot.Arduino.Internals.TypedBehavior p) a
+ Copilot.Arduino: instance Copilot.Core.Type.Typed a => Copilot.Arduino.IfThenElse Copilot.Arduino.Internals.Sketch (Copilot.Arduino.Internals.Behavior a)
+ Copilot.Arduino: instance Copilot.Core.Type.Typed a => Copilot.Arduino.IfThenElse Copilot.Language.Stream.Stream a
+ Copilot.Arduino: liftB :: (Behavior a -> Behavior r) -> TypedBehavior t a -> Behavior r
+ Copilot.Arduino: liftB2 :: (Behavior a -> Behavior b -> Behavior r) -> TypedBehavior t a -> TypedBehavior t b -> Behavior r
+ Copilot.Arduino: scheduleB :: (Typed t, Eq t) => Behavior t -> [(t, Sketch ())] -> Sketch ()
+ Copilot.Arduino: whenB :: Behavior Bool -> Sketch t -> Sketch t
+ Copilot.Arduino.Internals: CChunk :: [CLine] -> CChunk
+ Copilot.Arduino.Internals: NoTriggerLimit :: TriggerLimit
+ Copilot.Arduino.Internals: TriggerLimit :: Behavior Bool -> TriggerLimit
+ Copilot.Arduino.Internals: UniqueId :: Integer -> UniqueId
+ Copilot.Arduino.Internals: addTriggerLimit :: TriggerLimit -> Behavior Bool -> Behavior Bool
+ Copilot.Arduino.Internals: data TriggerLimit
+ Copilot.Arduino.Internals: defineTriggerAlias' :: String -> String -> Framework -> Sketch (Framework, String)
+ Copilot.Arduino.Internals: getTriggerLimit :: TriggerLimit -> Behavior Bool
+ Copilot.Arduino.Internals: input :: Input o t => o -> Sketch (Behavior t)
+ Copilot.Arduino.Internals: instance Control.Monad.Writer.Class.MonadWriter [(Copilot.Arduino.Internals.TriggerLimit -> Copilot.Language.Spec.Spec, Copilot.Arduino.Internals.TriggerLimit -> Copilot.Arduino.Internals.Framework)] Copilot.Arduino.Internals.Sketch
+ Copilot.Arduino.Internals: instance GHC.Base.Monoid Copilot.Arduino.Internals.CChunk
+ Copilot.Arduino.Internals: instance GHC.Base.Monoid Copilot.Arduino.Internals.TriggerLimit
+ Copilot.Arduino.Internals: instance GHC.Base.Semigroup Copilot.Arduino.Internals.CChunk
+ Copilot.Arduino.Internals: instance GHC.Base.Semigroup Copilot.Arduino.Internals.TriggerLimit
+ Copilot.Arduino.Internals: instance GHC.Classes.Eq Copilot.Arduino.Internals.CChunk
+ Copilot.Arduino.Internals: instance GHC.Classes.Eq Copilot.Arduino.Internals.CLine
+ Copilot.Arduino.Internals: instance GHC.Classes.Ord Copilot.Arduino.Internals.CChunk
+ Copilot.Arduino.Internals: instance GHC.Classes.Ord Copilot.Arduino.Internals.CLine
+ Copilot.Arduino.Internals: instance GHC.Show.Show Copilot.Arduino.Internals.CChunk
+ Copilot.Arduino.Internals: instance GHC.Show.Show Copilot.Arduino.Internals.CLine
+ Copilot.Arduino.Internals: mkCChunk :: [CLine] -> [CChunk]
+ Copilot.Arduino.Internals: newtype CChunk
+ Copilot.Arduino.Internals: newtype UniqueId
+ Copilot.Arduino.Internals: uniqueName :: String -> UniqueId -> String
+ Copilot.Arduino.Internals: uniqueName' :: String -> UniqueId -> String
+ Copilot.Arduino.Internals: whenB :: Behavior Bool -> Sketch t -> Sketch t
+ Copilot.Arduino.Library.EEPROMex: RangeReads :: Range t -> RangeIndex -> Behavior RangeIndex -> RangeReads t
+ Copilot.Arduino.Library.EEPROMex: data RangeReads t
+ Copilot.Arduino.Library.EEPROMex: instance (Copilot.Arduino.Internals.ShowCType t, Copilot.Arduino.Library.EEPROMex.EEPROMable t) => Copilot.Arduino.Internals.Input (Copilot.Arduino.Library.EEPROMex.RangeReads t) t
+ Copilot.Arduino.Library.EEPROMex: scanRange :: Range t -> RangeIndex -> RangeReads t
- Copilot.Arduino: ifThenElse :: Typed a => Stream Bool -> Stream a -> Stream a -> Stream a
+ Copilot.Arduino: ifThenElse :: IfThenElse t a => Behavior Bool -> t a -> t a -> t a
- Copilot.Arduino: infixr 1 ++
+ Copilot.Arduino: infixr 1 =<<
- Copilot.Arduino.Internals: Framework :: [CLine] -> [CLine] -> [CLine] -> Map PinId (Set PinMode) -> [CLine] -> Framework
+ Copilot.Arduino.Internals: Framework :: [CChunk] -> [CChunk] -> [CChunk] -> Map PinId (Set PinMode) -> [CChunk] -> Framework
- Copilot.Arduino.Internals: InputSource :: [CLine] -> [CLine] -> Map PinId PinMode -> [CLine] -> Stream t -> InputSource t
+ Copilot.Arduino.Internals: InputSource :: [CChunk] -> [CChunk] -> Map PinId PinMode -> [CChunk] -> Stream t -> InputSource t
- Copilot.Arduino.Internals: Sketch :: WriterT [(Spec, Framework)] (State UniqueIds) t -> Sketch t
+ Copilot.Arduino.Internals: Sketch :: WriterT [(TriggerLimit -> Spec, TriggerLimit -> Framework)] (State UniqueIds) t -> Sketch t
- Copilot.Arduino.Internals: UniqueIds :: [Integer] -> UniqueIds
+ Copilot.Arduino.Internals: UniqueIds :: Map String Integer -> UniqueIds
- Copilot.Arduino.Internals: [defineVar] :: InputSource t -> [CLine]
+ Copilot.Arduino.Internals: [defineVar] :: InputSource t -> [CChunk]
- Copilot.Arduino.Internals: [defines] :: Framework -> [CLine]
+ Copilot.Arduino.Internals: [defines] :: Framework -> [CChunk]
- Copilot.Arduino.Internals: [earlySetups] :: Framework -> [CLine]
+ Copilot.Arduino.Internals: [earlySetups] :: Framework -> [CChunk]
- Copilot.Arduino.Internals: [loops] :: Framework -> [CLine]
+ Copilot.Arduino.Internals: [loops] :: Framework -> [CChunk]
- Copilot.Arduino.Internals: [readInput] :: InputSource t -> [CLine]
+ Copilot.Arduino.Internals: [readInput] :: InputSource t -> [CChunk]
- Copilot.Arduino.Internals: [setupInput] :: InputSource t -> [CLine]
+ Copilot.Arduino.Internals: [setupInput] :: InputSource t -> [CChunk]
- Copilot.Arduino.Internals: [setups] :: Framework -> [CLine]
+ Copilot.Arduino.Internals: [setups] :: Framework -> [CChunk]
- Copilot.Arduino.Internals: defineTriggerAlias :: String -> String -> Framework -> (Framework, String)
+ Copilot.Arduino.Internals: defineTriggerAlias :: String -> Framework -> Sketch (Framework, String)
- Copilot.Arduino.Internals: getUniqueId :: Sketch Integer
+ Copilot.Arduino.Internals: getUniqueId :: String -> Sketch UniqueId
- Copilot.Arduino.Library.EEPROMex: allocRange :: forall t. EEPROMable t => Word16 -> Sketch (Range t)
+ Copilot.Arduino.Library.EEPROMex: allocRange :: EEPROMable t => Word16 -> Sketch (Range t)

Files

CHANGELOG view
@@ -1,3 +1,30 @@+arduino-copilot (1.5.0) unstable; urgency=medium++  * Added whenB and scheduleB sketch combinators.+  * Replaced Copolot's ifThenElse with a polymorphic one that, as well+    as choosing between two Streams, can choose between two Sketches+    or two TypedBehaviors.+  * Added robot example, a simple line-follower that is an example+    of using ifB to compose simple Sketches into a more complex one.+  * Added liftB and liftB2, which let Copilot DSL expressions operate on+    TypedBehavior.+  * Added waterheater example, which shows how to keep units of measurement+    separated using TypedBehavior.+  * Copolot.Arduino now exports IsDigitalIOPin, IsAnalogInputPin, IsPWMPin,+    and the TypedBehavior constructor.+  * Reading from the same input in different parts of the same program+    no longer generates C that fails to compile. Instead, a single read+    is done per iteration.+  * Writing to the same output in different parts of a sketch no longer+    generates C code that fails to compile. However, if different values+    are written in the same iteration, it's undefined what order the writes+    will happen in.+  * Add EEPROMex.scanRange to read from EEPROM.+  * Fix bug in serial str's handling of special characters in the string.+  * Copilot.Arduino.Internals API changed.++ -- Joey Hess <id@joeyh.name>  Fri, 07 Feb 2020 14:44:02 -0800+ arduino-copilot (1.4.0) unstable; urgency=medium    * New Copilot.Arduino.Library.EEPROMex module.
README view
@@ -8,7 +8,7 @@ 	import Copilot.Arduino 	main = arduino $ do 		led =: blinking-		delay =: constant 100+		delay =: MilliSeconds (constant 100)  This and other examples are included in the examples/ directory, each with their own README explaining how to build and use them.
TODO view
@@ -1,65 +1,26 @@--* Currently all Inputs run each time through the loop, which could be a-  problem when an input is slow. It would be good if the @: operator could-  also rate limit when Inputs occur.--  Copilot does not let `extern` be limited, but the actual input operation-  is done by ardino-copilot's readInput, and extern then just uses the-  value it sets. So, make each Input add a trigger, that sets a flag-  variable, and in readInput just check if the flag is set.--  It should be possible to implement this w/o adding overhead when @: is-  not used on an Input. Either, avoid checking the flag in this case,-  or check that the C compiler can optimise away a check of a flag that-  always succeeds.--  (Note that, if @: makes the first read be skipped, or all of them,-  there would need to be a default value produced. Or, to avoid default-  values, it could force the first read to always occur.)--  An Input returns a Behavior; with @: it should really return an Event.-  But, that would need boilerplate to get the Stream out to use.-- 	- foo <- input pin4- 	+ Event foo _ <- input pin4 @: longer_and_longer-	  pin5 =: streamfunc foo--  Seems not worth the distinction of Behavior vs Event here.-  -* If @: could somehow be applied to a whole Sketch () action, it would be-  possible to write some interesting combinators based on it, eg:--  ifM :: Stream Bool -> Sketch a -> Sketch a -> Sketch a--  schedule :: Stream Int -> [(Int, Sketch a] -> Sketch a--  Although, it may be that these would cause some explosion of the binary-  size, since each trigger in a Sketch's guard would, presumably, turn-  into a C function that duplicates the same code. Perhaps the C compiler-  could notice the common expressions and optimise them?--  Anyway, this would need the Sketch () to contain not a Spec, which I-  think is too opaque, but a data structure that allows adjusting the-  guards of `trigger` and an equivilant guard be added for `extern`.-  (See also, previous item.) And, it pushes things in the direction of-  needing to support multiple calls to Serial.display and wait with -  different parameters, and generally never hardcode the name of the C-  function called by a trigger.--  Now, I like the way that @: currently turns a behavior into an event, in-  FRP style. If @: is applied to a whole Sketch, it would stop doing that.-  That feels like a loss. So, perhaps @: stays as-is, and there's a new-  combinator to do the same thing to a Sketch. Internally, they would both-  adjust the guards of triggers. A bit redundant, but seems worth it to-  keep @: FRP style. (Making @: more polymorphic is also a possibility.)+* liftB and liftB2 imply liftB[3..5] at least ought to exist. -  Another combinator, that I considered but rejected is:+  Really, want something like Applicative. But apparently TypedBehavior+  is not a Functor, and so cannot be Applicative. There might be some+  way to use type classes to do arbitrary arity lifting. Take a look+  at QuickCheck and printf for ideas. Also see+  https://www.seas.upenn.edu/~sweirich/papers/aritygen -  (|:) :: Sketch a -> Sketch a -> Sketch a +  Alternative might be to wrap all of Copilot DSL's functions like (>)+  with ones that operate on TypedBehavior and Event. +  (Then rename TypedBehavior to Behavior and make everything produce+  and consume that, rather than the underlying Stream?) -  The problem with this is that in foo |: bar |: baz, one of the-  three never runs, because both combinators must branch on the same-  Stream Bool.+* using whenB with an input' makes the interpretation see+  each value from the list, even when the whenB is supposed to prevent the+  input' from having run, and the value should be whatever was input+  previously. This may not be fixable w/o Copilot DSL support+  for limiting when inputs happen, at least as far as what the interpreter+  displays as values of the input goes. But, it should be fixable+  as far as the value that is input, at least in theory, by ignoring+  the input values that should not have been input. The  Copilot+  DSL code used to do that may increase the size of the C program+  unncessarily though.  * The way serial output works is somewhat unsatisfying, because   it does not take a Stream FormatOutput. So the Copilot DSL can't be used@@ -72,6 +33,9 @@   seems like it would probably get in the way of actually constructing such   a stream: https://github.com/Copilot-Language/copilot/issues/36 +  The workaround, for now, is to use ifM_ etc to switch between different+  sketches for the different output formats.+ * analogReference()   Takes one of a set of defines, which vary depending on board,   so how to pass that through Copilot? Could write a C@@ -88,5 +52,8 @@ * EEPROMex supports writing individial bits of a byte, but the haskell   library does not expose it. What would be a good haskell interface to   that, a Stream (Array 8 Bool)?-* reading from EEPROMex ranges * Factor out Range from EEPROMex, and use it to also implement RAM ranges.+  Maybe redundant with Copilot's support for Array, but Copilot's+  facilities for operating on Array are limited so it could be useful to+  have a Range interface too.+
arduino-copilot.cabal view
@@ -1,5 +1,5 @@ Name: arduino-copilot-Version: 1.4.0+Version: 1.5.0 Cabal-Version: >= 1.8 License: BSD3 Maintainer: Joey Hess <id@joeyh.name>@@ -53,6 +53,14 @@   examples/eepromrange/README   examples/eepromrange/demo.hs   examples/eepromrange/pre-build-hook.sh+  examples/robot/Makefile+  examples/robot/README+  examples/robot/demo.hs+  examples/robot/pre-build-hook.sh+  examples/waterheater/Makefile+  examples/waterheater/README+  examples/waterheater/demo.hs+  examples/waterheater/pre-build-hook.sh   test.hs  Library
examples/eepromrange/demo.hs view
@@ -34,7 +34,14 @@ 	range <- EEPROM.allocRange sz :: Sketch (EEPROM.Range ADC) 	v <- input' a1 ([10, 20..] :: [ADC]) 	range =: EEPROM.sweepRange 0 v @: frequency 3+	oldv <- input' (EEPROM.scanRange range 1) [11..] :: Sketch (Behavior ADC) 	led =: frequency 3-	Serial.device =: [ Serial.show v, Serial.char '\n']+	Serial.device =:+		[ Serial.str "a1:"+		, Serial.show v+		, Serial.str " old EEPROM value:"+		, Serial.show oldv+		, Serial.char '\n'+		] 	delay =: MilliSeconds (constant 10000) 	Serial.baud 9600
+ examples/robot/Makefile view
@@ -0,0 +1,127 @@+# Arduino Make file. Refer to https://github.com/sudar/Arduino-Makefile+BOARD_TAG    = uno+include /usr/share/arduino/Arduino.mk++# --- leonardo (or pro micro w/leo bootloader)+#BOARD_TAG    = leonardo+#MONITOR_PORT = /dev/ttyACM0+#include /usr/share/arduino/Arduino.mk++# --- mega2560 ide 1.0+#BOARD_TAG    = mega2560+#ARDUINO_PORT = /dev/ttyACM0+#include /usr/share/arduino/Arduino.mk++# --- mega2560 ide 1.6+#BOARD_TAG    = mega+#BOARD_SUB    = atmega2560+#MONITOR_PORT = /dev/ttyACM0+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- nano ide 1.0+#BOARD_TAG    = nano328+#MONITOR_PORT = /dev/ttyUSB0+#include /usr/share/arduino/Arduino.mk++# --- nano ide 1.6+#BOARD_TAG   = nano+#BOARD_SUB   = atmega328+#ARDUINO_DIR = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- pro mini+#BOARD_TAG    = pro5v328+#MONITOR_PORT = /dev/ttyUSB0+#include /usr/share/arduino/Arduino.mk++# --- sparkfun pro micro+#BOARD_TAG         = promicro16+#ALTERNATE_CORE    = promicro+#BOARDS_TXT        = $(HOME)/arduino/hardware/promicro/boards.txt+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/promicro/bootloaders+#BOOTLOADER_PATH   = caterina+#BOOTLOADER_FILE   = Caterina-promicro16.hex+#ISP_PROG     	   = usbasp+#AVRDUDE_OPTS 	   = -v+#include /usr/share/arduino/Arduino.mk++# --- chipkit+#BOARD_TAG = mega_pic32+#MPIDE_DIR = /where/you/installed/mpide-0023-linux64-20130817-test+#include /usr/share/arduino/chipKIT.mk++# --- pinoccio+#BOARD_TAG         = pinoccio256+#ALTERNATE_CORE    = pinoccio+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/pinoccio/bootloaders+#BOOTLOADER_PATH   = STK500RFR2/release_0.51+#BOOTLOADER_FILE   = boot_pinoccio.hex+#CFLAGS_STD        = -std=gnu99+#CXXFLAGS_STD      = -std=gnu++11+#include /usr/share/arduino/Arduino.mk++# --- fio+#BOARD_TAG = fio+#include /usr/share/arduino/Arduino.mk++# --- atmega-ng ide 1.6+#BOARD_TAG    = atmegang+#BOARD_SUB    = atmega168+#MONITOR_PORT = /dev/ttyACM0+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- arduino-tiny ide 1.0+#ISP_PROG     	    = usbasp+#BOARD_TAG          = attiny85at8+#ALTERNATE_CORE     = tiny+#ARDUINO_VAR_PATH   = $(HOME)/arduino/hardware/tiny/cores/tiny+#ARDUINO_CORE_PATH  = $(HOME)/arduino/hardware/tiny/cores/tiny+#AVRDUDE_OPTS 	    = -v+#include /usr/share/arduino/Arduino.mk++# --- arduino-tiny ide 1.6+#ISP_PROG       = usbasp+#BOARD_TAG      = attiny85at8+#ALTERNATE_CORE = tiny+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- damellis attiny ide 1.0+#ISP_PROG       = usbasp+#BOARD_TAG      = attiny85+#ALTERNATE_CORE = attiny-master+#AVRDUDE_OPTS   = -v+#include /usr/share/arduino/Arduino.mk++# --- damellis attiny ide 1.6+#ISP_PROG       = usbasp+#BOARD_TAG      = attiny+#BOARD_SUB      = attiny85+#ALTERNATE_CORE = attiny+#F_CPU          = 16000000L+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- teensy3+#BOARD_TAG 	 = teensy31+#ARDUINO_DIR = /where/you/installed/the/patched/teensy/arduino-1.0.6+#include /usr/share/arduino/Teensy.mk++# --- mighty 1284p+#BOARD_TAG         = mighty_opt+#BOARDS_TXT        = $(HOME)/arduino/hardware/mighty-1284p/boards.txt+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/mighty-1284p/bootloaders+#BOOTLOADER_PATH   = optiboot+#BOOTLOADER_FILE   = optiboot_atmega1284p.hex+#ISP_PROG     	   = usbasp+#AVRDUDE_OPTS 	   = -v+#include /usr/share/arduino/Arduino.mk++# --- atmega328p on breadboard+#BOARD_TAG    = atmega328bb+#ISP_PROG     = usbasp+#AVRDUDE_OPTS = -v+#BOARDS_TXT   = $(HOME)/arduino/hardware/breadboard/boards.txt+#include /usr/share/arduino/Arduino.mk
+ examples/robot/README view
@@ -0,0 +1,24 @@+This is a demo program using arduino-copilot. ++To build the C code:++	runghc demo.hs++The resulting `.ino` sketch can be loaded into the Arduino IDE and flashed+to an Arduino Uno board using the IDE.++## Arduino-Makefile integration++The `Makefile` and `pre-build-hook.sh` show how to integrate arduino-copilot+with <https://github.com/sudar/Arduino-Makefile>. This automates generating+the C code, compiling that, and flashing it onto an Arduino Uno board,+with a single command++	make upload++Note that you will need to manually build the C code once, as shown above+, since the Arduino-Makefile expects to find a `.ino` file.++The path in the `Makefile` to `Arduino.mk` may need to be adjusted (the+default is the location where on a Debian system, `apt-get install+arduino-mk` will install it).
+ examples/robot/demo.hs view
@@ -0,0 +1,90 @@+{- A simple line-following robot.+ -+ - This is not the most efficient or best way to implement it+ - (it compiles to around 500 lines of C code), but+ - it is a good example of building up a Sketch by combining+ - simpler Sketches.+ -}++{-# LANGUAGE RebindableSyntax #-}++import Copilot.Arduino.Uno+import qualified Copilot.Arduino.Library.Serial.XBee as XBee++main :: IO ()+main = arduino $ do+	XBee.configure pin2 pin3 (XBee.Baud 9600)+	emergencystop <- input pin4+	if emergencystop+		then stop+		else do+			ll <- leftLineSensed+			rl <- rightLineSensed+			if ll && rl+				then stop+				else if ll+					then turnLeft+					else if rl+						then turnRight+						else goForward++stop :: Sketch ()+stop = do+	led =: true+	XBee.device =: [XBee.str "stopped\n"]+	rightWheelMotor true (constant 0)+	leftWheelMotor true (constant 0)++goForward :: Sketch ()+goForward = do+	led =: false+	XBee.device =: [XBee.str "forward\n"]+	leftWheelMotor true (constant 255)+	rightWheelMotor true (constant 255)++turnLeft :: Sketch ()+turnLeft = do+	led =: blinking+	XBee.device =: [XBee.str "turn left\n"]+	rightWheelMotor true (constant 128)+	leftWheelMotor false (constant 128)++turnRight :: Sketch ()+turnRight = do+	led =: blinking+	XBee.device =: [XBee.str "turn right\n"]+	rightWheelMotor false (constant 128)+	leftWheelMotor true (constant 128)++leftLineSensed :: Sketch (Behavior Bool)+leftLineSensed = lineSensed a1 "left"++rightLineSensed :: Sketch (Behavior Bool)+rightLineSensed = lineSensed a2 "right"++lineSensed :: IsAnalogInputPin t => Pin t -> [Char] -> Sketch (Stream Bool)+lineSensed pin desc = do+	v <- input pin :: Sketch (Behavior ADC)+	XBee.device =: +		[ XBee.str (desc <> " line sensor:")+		, XBee.show v+		, XBee.char '\n'+		]+	return (v > 50)++leftWheelMotor :: Behavior Bool -> Behavior Word8 -> Sketch ()+leftWheelMotor = wheelMotor pin5 pin6++rightWheelMotor :: Behavior Bool -> Behavior Word8 -> Sketch ()+rightWheelMotor = wheelMotor pin9 pin10++wheelMotor+	:: (IsPWMPin t1, IsPWMPin t2)+	=> Pin t1 -- ^ pin to drive motor forward+	-> Pin t2 -- ^ pin to drive motor backward+	-> Behavior Bool -- ^ True when driving forward+	-> Behavior Word8 -- ^ How hard to drive the motor.+	-> Sketch ()+wheelMotor forwardpin backwardpin forward power = do+	forwardpin =: pwm (if forward then power else constant 0)+	backwardpin =: pwm (if forward then constant 0 else power)
+ examples/robot/pre-build-hook.sh view
@@ -0,0 +1,2 @@+#!/bin/sh+exec runghc demo.hs
+ examples/waterheater/Makefile view
@@ -0,0 +1,127 @@+# Arduino Make file. Refer to https://github.com/sudar/Arduino-Makefile+BOARD_TAG    = uno+include /usr/share/arduino/Arduino.mk++# --- leonardo (or pro micro w/leo bootloader)+#BOARD_TAG    = leonardo+#MONITOR_PORT = /dev/ttyACM0+#include /usr/share/arduino/Arduino.mk++# --- mega2560 ide 1.0+#BOARD_TAG    = mega2560+#ARDUINO_PORT = /dev/ttyACM0+#include /usr/share/arduino/Arduino.mk++# --- mega2560 ide 1.6+#BOARD_TAG    = mega+#BOARD_SUB    = atmega2560+#MONITOR_PORT = /dev/ttyACM0+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- nano ide 1.0+#BOARD_TAG    = nano328+#MONITOR_PORT = /dev/ttyUSB0+#include /usr/share/arduino/Arduino.mk++# --- nano ide 1.6+#BOARD_TAG   = nano+#BOARD_SUB   = atmega328+#ARDUINO_DIR = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- pro mini+#BOARD_TAG    = pro5v328+#MONITOR_PORT = /dev/ttyUSB0+#include /usr/share/arduino/Arduino.mk++# --- sparkfun pro micro+#BOARD_TAG         = promicro16+#ALTERNATE_CORE    = promicro+#BOARDS_TXT        = $(HOME)/arduino/hardware/promicro/boards.txt+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/promicro/bootloaders+#BOOTLOADER_PATH   = caterina+#BOOTLOADER_FILE   = Caterina-promicro16.hex+#ISP_PROG     	   = usbasp+#AVRDUDE_OPTS 	   = -v+#include /usr/share/arduino/Arduino.mk++# --- chipkit+#BOARD_TAG = mega_pic32+#MPIDE_DIR = /where/you/installed/mpide-0023-linux64-20130817-test+#include /usr/share/arduino/chipKIT.mk++# --- pinoccio+#BOARD_TAG         = pinoccio256+#ALTERNATE_CORE    = pinoccio+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/pinoccio/bootloaders+#BOOTLOADER_PATH   = STK500RFR2/release_0.51+#BOOTLOADER_FILE   = boot_pinoccio.hex+#CFLAGS_STD        = -std=gnu99+#CXXFLAGS_STD      = -std=gnu++11+#include /usr/share/arduino/Arduino.mk++# --- fio+#BOARD_TAG = fio+#include /usr/share/arduino/Arduino.mk++# --- atmega-ng ide 1.6+#BOARD_TAG    = atmegang+#BOARD_SUB    = atmega168+#MONITOR_PORT = /dev/ttyACM0+#ARDUINO_DIR  = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- arduino-tiny ide 1.0+#ISP_PROG     	    = usbasp+#BOARD_TAG          = attiny85at8+#ALTERNATE_CORE     = tiny+#ARDUINO_VAR_PATH   = $(HOME)/arduino/hardware/tiny/cores/tiny+#ARDUINO_CORE_PATH  = $(HOME)/arduino/hardware/tiny/cores/tiny+#AVRDUDE_OPTS 	    = -v+#include /usr/share/arduino/Arduino.mk++# --- arduino-tiny ide 1.6+#ISP_PROG       = usbasp+#BOARD_TAG      = attiny85at8+#ALTERNATE_CORE = tiny+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- damellis attiny ide 1.0+#ISP_PROG       = usbasp+#BOARD_TAG      = attiny85+#ALTERNATE_CORE = attiny-master+#AVRDUDE_OPTS   = -v+#include /usr/share/arduino/Arduino.mk++# --- damellis attiny ide 1.6+#ISP_PROG       = usbasp+#BOARD_TAG      = attiny+#BOARD_SUB      = attiny85+#ALTERNATE_CORE = attiny+#F_CPU          = 16000000L+#ARDUINO_DIR    = /where/you/installed/arduino-1.6.5+#include /usr/share/arduino/Arduino.mk++# --- teensy3+#BOARD_TAG 	 = teensy31+#ARDUINO_DIR = /where/you/installed/the/patched/teensy/arduino-1.0.6+#include /usr/share/arduino/Teensy.mk++# --- mighty 1284p+#BOARD_TAG         = mighty_opt+#BOARDS_TXT        = $(HOME)/arduino/hardware/mighty-1284p/boards.txt+#BOOTLOADER_PARENT = $(HOME)/arduino/hardware/mighty-1284p/bootloaders+#BOOTLOADER_PATH   = optiboot+#BOOTLOADER_FILE   = optiboot_atmega1284p.hex+#ISP_PROG     	   = usbasp+#AVRDUDE_OPTS 	   = -v+#include /usr/share/arduino/Arduino.mk++# --- atmega328p on breadboard+#BOARD_TAG    = atmega328bb+#ISP_PROG     = usbasp+#AVRDUDE_OPTS = -v+#BOARDS_TXT   = $(HOME)/arduino/hardware/breadboard/boards.txt+#include /usr/share/arduino/Arduino.mk
+ examples/waterheater/README view
@@ -0,0 +1,24 @@+This is a demo program using arduino-copilot. ++To build the C code:++	runghc demo.hs++The resulting `.ino` sketch can be loaded into the Arduino IDE and flashed+to an Arduino Uno board using the IDE.++## Arduino-Makefile integration++The `Makefile` and `pre-build-hook.sh` show how to integrate arduino-copilot+with <https://github.com/sudar/Arduino-Makefile>. This automates generating+the C code, compiling that, and flashing it onto an Arduino Uno board,+with a single command++	make upload++Note that you will need to manually build the C code once, as shown above+, since the Arduino-Makefile expects to find a `.ino` file.++The path in the `Makefile` to `Arduino.mk` may need to be adjusted (the+default is the location where on a Debian system, `apt-get install+arduino-mk` will install it).
+ examples/waterheater/demo.hs view
@@ -0,0 +1,68 @@+{- Controlling a hot water heater.+ -+ - This shows how additional types can be used to eg, keep track of units+ - of measurement, while using the Copilot DSL.+ -}++{-# LANGUAGE RebindableSyntax #-}++import Copilot.Arduino.Uno+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as L++-- For use as phantom types in TypedBehavior+data PSI+data Celsius++maxSafePSI :: TypedBehavior PSI Float+maxSafePSI = TypedBehavior (constant 45)++maxWaterTemp :: TypedBehavior Celsius Float+maxWaterTemp = TypedBehavior (constant 35)++warmWaterTemp :: TypedBehavior Celsius Float+warmWaterTemp = TypedBehavior (constant 30)++isSafeTemp :: TypedBehavior Celsius Float -> Behavior Bool+isSafeTemp t = liftB2 (<) t maxWaterTemp++isSafePSI :: TypedBehavior PSI Float -> Behavior Bool+isSafePSI p = liftB2 (<) p maxSafePSI++-- Avoid explosions and scalding water.+isSafeToHeat :: TypedBehavior PSI Float -> NonEmpty (TypedBehavior Celsius Float) -> Behavior Bool+isSafeToHeat p cs = foldl (&&) (isSafePSI p) (L.map isSafeTemp cs)++-- Convert a raw value read from an ADC into a temperature in Celsius.+--+-- Assumes the ADC value is 0 at 0F, and 1024 at 200F.+adcToCelsius :: Behavior ADC -> TypedBehavior Celsius Float+adcToCelsius v = TypedBehavior $ v' * (constant 200 / constant 1024)+  where+	v' :: Behavior Float+	v' = unsafeCast v++-- Convert a raw value read from an ADC into a PSI.+--+-- Assumes the ADCI value is 0 at 0 PSI, and 1024 at 150 PSI.+adcToPSI :: Behavior ADC -> TypedBehavior PSI Float+adcToPSI v = TypedBehavior $ v' * (constant 150 / constant 1024)+  where+	v' :: Behavior Float+	v' = unsafeCast v++main :: IO ()+main = arduino $ do+	uppertemp <- adcToCelsius <$> input a1+	lowertemp <- adcToCelsius <$> input a2+	pressure <- adcToPSI <$> input a3+	let upperheatingelement = pin4+	let lowerheatingelement = pin5+	whenB (isSafeToHeat pressure (uppertemp :| [lowertemp])) $ do+		-- Run the upper heating element if the upper water temp is+		-- low, to provide hot water on demand. Run the lower+		-- heating element only when the upper is off.+		let runupper = liftB2 (<) uppertemp warmWaterTemp+		upperheatingelement =: runupper+		lowerheatingelement =: not runupper+	delay =: MilliSeconds 1000
+ examples/waterheater/pre-build-hook.sh view
@@ -0,0 +1,2 @@+#!/bin/sh+exec runghc demo.hs
src/Copilot/Arduino.hs view
@@ -11,6 +11,7 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Copilot.Arduino ( 	-- * Arduino sketch generation@@ -19,7 +20,7 @@ 	Pin, 	-- * Functional reactive programming 	Behavior,-	TypedBehavior,+	TypedBehavior(..), 	Event, 	(@:), 	-- * Inputs@@ -45,17 +46,27 @@ 	MicroSeconds(..), 	ClockMillis, 	ClockMicros,+	IsDigitalIOPin,+	IsAnalogInputPin,+	IsPWMPin, 	-- * Utilities 	blinking, 	firstIteration, 	frequency, 	sketchSpec,+	-- * Combinators+	liftB,+	liftB2,+	whenB,+	scheduleB,+	ifThenElse,+	IfThenElse, 	-- * Copilot DSL 	---	-- | The Copilot.Language module is re-exported here, including-	-- a version of the Prelude modified for it. You should enable-	-- the RebindableSyntax language extension in your program-	-- to use the Copilot DSL.+	-- | Most of the Copilot.Language module is re-exported here,+	-- including a version of the Prelude modified for it. You+	-- should enable the RebindableSyntax language extension in+	-- your program to use the Copilot DSL. 	-- 	-- > {-# LANGUAGE RebindableSyntax #-} 	--@@ -65,8 +76,9 @@ 	module X, ) where -import Language.Copilot as X hiding (Stream)+import Language.Copilot as X hiding (Stream, ifThenElse) import Language.Copilot (Stream)+import qualified Language.Copilot import Copilot.Arduino.Internals import Copilot.Arduino.Main import Control.Monad.Writer@@ -116,13 +128,24 @@ data Delay = Delay  instance Output Delay MilliSeconds where-	Delay =: (MilliSeconds n) = tell-		[(trigger "delay" true [arg n], mempty)]+	Delay =: (MilliSeconds n) = do+		(f, triggername) <- defineTriggerAlias "delay" mempty+		tell [(go triggername, \_ -> f)]+	  where+		go triggername tl =+			let c = getTriggerLimit tl+			in trigger triggername c [arg n]  instance Output Delay MicroSeconds where-	Delay =: (MicroSeconds n) = tell-		[(trigger "delayMicroseconds" true [arg n], mempty)]+	Delay =: (MicroSeconds n) = do+		(f, triggername) <- defineTriggerAlias "delayMicroseconds" mempty+		tell [(go triggername, \_ -> f)]+	  where+		go triggername tl = +			let c = getTriggerLimit tl+			in trigger triggername c [arg n] + -- | Number of MillisSeconds since the Arduino booted. -- -- > n <- input millis@@ -151,9 +174,11 @@ inputClock :: [Char] -> [Word32] -> Sketch (Behavior Word32) inputClock src interpretvalues = mkInput $ InputSource 	{ setupInput = []-	, defineVar = [CLine $ showCType (Proxy @Word32) <> " " <> varname <>";"]+	, defineVar = mkCChunk+		[CLine $ showCType (Proxy @Word32) <> " " <> varname <>";"] 	, inputPinmode = mempty-	, readInput = [CLine $ varname <> " = " <> src <> "();"]+	, readInput = mkCChunk+		[CLine $ varname <> " = " <> src <> "();"] 	, inputStream = extern varname interpretvalues' 	}   where@@ -162,23 +187,6 @@ 		| null interpretvalues = Nothing 		| otherwise = Just interpretvalues --- | Use this to read a value from a component of the Arduino.------ For example, to read a digital value from pin12 and turn on the --- led when the pin is high:------ > buttonpressed <- input pin12--- > led =: buttonpressed------ Some pins support multiple types of reads, for example pin a0--- supports a digital read (`Bool`), and an analog to digital converter--- read (`ADC`). In such cases you may need to specify the type of--- data to read:------ > v <- input a0 :: Sketch (Behavior ADC)-input :: Input o t => o -> Sketch (Behavior t)-input o = input' o []- -- | Normally when a digital value is read from a `Pin`, it is configured -- without the internal pullup resistor being enabled. Use this to enable -- the pullup register for all reads from the `Pin`.@@ -188,7 +196,7 @@ -- -- > pullup pin12 pullup :: Pin t -> Sketch ()-pullup (Pin p) = tell [(return (), f)]+pullup (Pin p) = tell [(\_ -> return (), \_ -> f)]   where 	f = mempty 		{ pinmodes = M.singleton p (S.singleton InputPullupMode)@@ -207,9 +215,59 @@ led :: Pin '[ 'DigitalIO ] led = Pin (PinId 13) +class IfThenElse t a where+	-- | This allows "if then else" expressions to be written+	-- that choose between two Streams, or Behaviors, or TypedBehaviors,+	-- or Sketches, when the RebindableSyntax language extension is+	-- enabled.+	--+	-- > {-# LANGUAGE RebindableSyntax #-}+	-- > buttonpressed <- input pin3+	-- > if buttonpressed then ... else ...+	ifThenElse :: Behavior Bool -> t a -> t a -> t a++instance Typed a => IfThenElse Stream a where+	ifThenElse = Language.Copilot.ifThenElse++instance Typed a => IfThenElse (TypedBehavior p) a where+	ifThenElse c (TypedBehavior a) (TypedBehavior b) =+		TypedBehavior (ifThenElse c a b)++instance IfThenElse Sketch () where+	ifThenElse c a b = do+		whenB c a+		whenB (not c) b++instance Typed a => IfThenElse Sketch (Behavior a) where+	ifThenElse c a b = do+		ra <- whenB c a+		rb <- whenB (not c) b+		return $ Language.Copilot.ifThenElse c ra rb++-- | Schedule when to perform different Sketches.+scheduleB :: (Typed t, Eq t) => Behavior t -> [(t, Sketch ())] -> Sketch ()+scheduleB b = sequence_ . map go+  where+	go (v, s) = whenB (b == constant v) s+ -- | Extracts a copilot `Spec` from a `Sketch`. -- -- This can be useful to intergrate with other libraries  -- such as copilot-theorem. sketchSpec :: Sketch a -> Spec sketchSpec = fromMaybe (return ()) . fst . evalSketch++-- | Apply a Copilot DSL function to a `TypedBehavior`.+liftB+	:: (Behavior a -> Behavior r)+	-> TypedBehavior t a+	-> Behavior r+liftB f (TypedBehavior b) = f b++-- | Apply a Copilot DSL function to two `TypedBehavior`s.+liftB2+	:: (Behavior a -> Behavior b -> Behavior r)+	-> TypedBehavior t a+	-> TypedBehavior t b+	-> Behavior r+liftB2 f (TypedBehavior a) (TypedBehavior b) = f a b
src/Copilot/Arduino/Internals.hs view
@@ -49,16 +49,16 @@ -- Arduino at any point in time. -- -- Under the hood, the `Sketch` is run in a loop. On each iteration, it first--- reads all inputs and then updates outputs as needed.+-- reads inputs and then updates outputs as needed. -- -- While it is a monad, a Sketch's outputs are not updated in any -- particular order, because Copilot does not guarantee any order.-newtype Sketch t = Sketch (WriterT [(Spec, Framework)] (State UniqueIds) t)+newtype Sketch t = Sketch (WriterT [(TriggerLimit -> Spec, TriggerLimit -> Framework)] (State UniqueIds) t) 	deriving  		( Monad 		, Applicative 		, Functor-		, MonadWriter [(Spec, Framework)]+		, MonadWriter [(TriggerLimit -> Spec, TriggerLimit -> Framework)] 		, MonadState UniqueIds 		) @@ -68,45 +68,103 @@ instance Semigroup (Sketch t) where 	(Sketch a) <> (Sketch b) = Sketch (a >> b) -newtype UniqueIds = UniqueIds [Integer]+newtype UniqueIds = UniqueIds (M.Map String Integer) +newtype UniqueId = UniqueId Integer++data TriggerLimit+	= TriggerLimit (Behavior Bool)+	| NoTriggerLimit++getTriggerLimit :: TriggerLimit -> Behavior Bool+getTriggerLimit (TriggerLimit b) = b+getTriggerLimit NoTriggerLimit = true++addTriggerLimit :: TriggerLimit -> Behavior Bool -> Behavior Bool+addTriggerLimit tl c = getTriggerLimit (tl <> TriggerLimit c)++instance Monoid TriggerLimit where+	mempty = NoTriggerLimit++instance Semigroup TriggerLimit where+	TriggerLimit a <> TriggerLimit b =+		TriggerLimit (a Language.Copilot.&& b)+	a <> NoTriggerLimit = a+	NoTriggerLimit <> b = b+ evalSketch :: Sketch a -> (Maybe Spec, Framework)-evalSketch (Sketch s) = (spec, mconcat fs)+evalSketch (Sketch s) = (spec, f)   where 	(is, fs) = unzip $ -		runIdentity $ evalStateT (execWriterT s) (UniqueIds [1..])+		runIdentity $ evalStateT (execWriterT s) (UniqueIds mempty)+	f = mconcat (map (\f' -> f' NoTriggerLimit) fs) 	-- Copilot will throw an ugly error if given a spec that does 	-- nothing at all, so return Nothing to avoid that.+	spec :: Maybe Spec 	spec = if null is 		then Nothing-		else Just (sequence_ is)+		else Just $ sequence_ $ map (\i -> i NoTriggerLimit) is -getUniqueId :: Sketch Integer-getUniqueId = do-	v <- get-	case v of-		UniqueIds (u:us) -> do-			put (UniqueIds us)-			return u-		UniqueIds [] -> error "somehow UniqueIds ran out"+-- | Limit the effects of a `Sketch` to times when a `Behavior` `Bool` is True.+--+-- When applied to `=:`, this does the same thing as `@:` but without+-- the FRP style conversion the input `Behavior` into an `Event`. So `@:`+-- is generally better to use than this.+--+-- But, this can also be applied to `input`, to limit how often input+-- gets read. Useful to avoid performing slow input operations on every+-- iteration of a Sketch.+--+-- > v <- whenB (frequency 10) $ input pin12+--+-- (It's best to think of the value returned by that as an Event,+-- but it's currently represented as a Behavior, since the Copilot DSL+-- cannot operate on Events.)+whenB :: Behavior Bool -> Sketch t -> Sketch t+whenB c (Sketch s) = do+	ids <- get+	let ((r, w), ids') = runIdentity $ runStateT (runWriterT s) ids+	put ids'+	let (is, fs) = unzip w+	let spec = combinetl $ \c' -> sequence_ (map (\i -> i c') is)+	tell [(spec, mempty)]+	forM_ fs $ \f -> tell [(const (return ()), combinetl f)]+	return r+  where+	combinetl :: (TriggerLimit -> a) -> TriggerLimit -> a+	combinetl g tl = g (TriggerLimit c <> tl) +-- | Gets a unique id.+getUniqueId :: String -> Sketch UniqueId+getUniqueId s = do+	UniqueIds m <- get+	let u = maybe 1 succ (M.lookup s m)+	put $ UniqueIds $ M.insert s u m+	return (UniqueId u)++-- | Generates a unique name.+uniqueName :: String -> UniqueId -> String+uniqueName s (UniqueId i)+	| i Prelude.== 1 = s+	| otherwise = s <> "_" <>  show i++uniqueName' :: String -> UniqueId -> String+uniqueName' s (UniqueId i) = s <> "_" <>  show i+ -- | The framework of an Arduino sketch. data Framework = Framework-	{ defines :: [CLine]+	{ defines :: [CChunk] 	-- ^ Things that come before the C code generated by Copilot.-	, setups :: [CLine]+	, setups :: [CChunk] 	-- ^ Things to do at setup, not including configuring pins.-	, earlySetups :: [CLine]+	, earlySetups :: [CChunk] 	-- ^ Things to do at setup, before the setups. 	, pinmodes :: M.Map PinId (S.Set PinMode) 	-- ^ How pins are used.-	, loops :: [CLine]+	, loops :: [CChunk] 	-- ^ Things to run in `loop`. 	} --- | A line of C code.-newtype CLine = CLine { fromCLine :: String }- instance Semigroup Framework where 	a <> b = Framework 		{ defines = defines a <> defines b@@ -119,43 +177,88 @@ instance Monoid Framework where 	mempty = Framework mempty mempty mempty mempty mempty +-- | A line of C code.+newtype CLine = CLine { fromCLine :: String }+	deriving (Eq, Show, Ord)++-- | A chunk of C code. Identical chunks get deduplicated.+newtype CChunk = CChunk [CLine]+	deriving (Eq, Show, Ord, Semigroup, Monoid)++mkCChunk :: [CLine] -> [CChunk]+mkCChunk l = [CChunk l]+ -- | Copilot only supports calling a trigger with a given name once -- per Spec; the generated C code will fail to build if the same name is--- used in two triggers. This generates a name from a suffix, which should--- be somehow unique.-defineTriggerAlias :: String -> String -> Framework -> (Framework, String)-defineTriggerAlias suffix cfuncname f = -	(f { defines = define : defines f }, triggername)-  where-	triggername = cfuncname <> "_" <> suffix-	define = CLine $ "#define " <> triggername <> " " <> cfuncname+-- used in two triggers. This generates a unique alias that can be +-- used in a trigger.+defineTriggerAlias :: String -> Framework -> Sketch (Framework, String)+defineTriggerAlias = defineTriggerAlias' "" +defineTriggerAlias' :: String -> String -> Framework -> Sketch (Framework, String)+defineTriggerAlias' suffix cfuncname f = do+	let basetname = if null suffix +		then cfuncname+		else cfuncname <> "_" <> suffix+	u <- getUniqueId basetname+	let triggername = uniqueName basetname u+	let define = if cfuncname Prelude./= triggername+		then mkCChunk [ CLine $ "#define " <> triggername <> " " <> cfuncname	]+		else mempty+	return (f { defines = define <> defines f }, triggername)+ data InputSource t = InputSource-	{ defineVar :: [CLine]+	{ defineVar :: [CChunk] 	-- ^ Added to the `Framework`'s `defines`, this typically 	-- defines a C variable.-	, setupInput :: [CLine]+	, setupInput :: [CChunk] 	-- ^ How to set up the input, not including pin mode. 	, inputPinmode :: M.Map PinId PinMode-	, readInput :: [CLine]+	-- ^ How pins are used by the input.+	, readInput :: [CChunk] 	-- ^ How to read a value from the input, this typically 	-- reads a value into a C variable. 	, inputStream :: Stream t+	-- ^ How to use Copilot's extern to access the input values. 	}  mkInput :: InputSource t -> Sketch (Behavior t) mkInput i = do-	tell [(return (), f)]+	u <- getUniqueId "input"+	tell [(mkspec u, f u)] 	return (inputStream i)   where-	f = Framework-		{ defines = defineVar i+	f u ratelimited = Framework+		{ defines = defineVar i <> mkdefine u ratelimited 		, setups = setupInput i 		, earlySetups = mempty 		, pinmodes = M.map S.singleton (inputPinmode i)-		, loops = readInput i+		, loops = mkloops u ratelimited (readInput i) 		} +	varname = uniqueName "update_input"+	triggername = uniqueName "input"+	+	mkdefine _ NoTriggerLimit = []+	mkdefine u (TriggerLimit _) = mkCChunk $ map CLine+		[ "bool " <> varname u <> " = true;"+		, "void " <> triggername u <> " (bool v) {"+		, "  " <> varname u <> " = v;"+		, "}"+		]+	+	mkloops _ NoTriggerLimit reader = reader+	mkloops u (TriggerLimit _) reader = mkCChunk $ concat+		[ [ CLine $ "if (" <> varname u <> ") {" ]+		, map (\(CLine l) -> CLine $ "  " <> l ) readerlines+		, [ CLine "}" ]+		]+	  where+		readerlines = concatMap (\(CChunk l) -> l) reader++	mkspec _ NoTriggerLimit = return ()+	mkspec u (TriggerLimit c) = trigger (triggername u) true [arg c]+ -- | A pin on the Arduino board. -- -- For definitions of pins like `Copilot.Arduino.Uno.pin12`, @@ -211,25 +314,26 @@ class Output o t where 	(=:) :: o -> t -> Sketch () 	-- ^ Connect a `Behavior` or `Event` to an `Output`-	--+	--  	-- > led =: blinking-	--+	--  	-- When a `Behavior` is used, its current value is written on each 	-- iteration of the `Sketch`. -	--+	--  	-- For example, this constantly turns on the LED, even though it will 	-- already be on after the first iteration, because `true` 	-- is a `Behavior` (that is always True).-	--+	--  	-- > led =: true-	--+	--  	-- To avoid unncessary work being done, you can use an `Event` 	-- instead. Then the write only happens at the points in time-	-- when the `Event` occurs.+	-- when the `Event` occurs. To turn a `Behavior` into an `Event`,+	-- use `@:` 	--  	-- So to make the LED only be turned on in the first iteration, 	-- and allow it to remain on thereafter without doing extra work:-	--	+	--  	-- > led =: true @: firstIteration  -- Same fixity as =<<@@ -254,7 +358,7 @@ type instance BehaviorToEvent (TypedBehavior p v) = Event p (Stream v)  class IsBehavior behavior where-	-- | Generate an event, from some type of behavior,+	-- | Generate an Event, from some type of behavior, 	-- that only occurs when the `Behavior` Bool is True. 	(@:) :: behavior -> Behavior Bool -> BehaviorToEvent behavior @@ -265,30 +369,53 @@ 	(@:) (TypedBehavior b) c = Event b c  instance IsDigitalIOPin t => Output (Pin t) (Event () (Stream Bool)) where-	(Pin p@(PinId n)) =: (Event b c) = tell [(go, f)]+	(Pin p@(PinId n)) =: (Event b c) = do+		(f, triggername) <- defineTriggerAlias' ("pin_" <> show n) "digitalWrite" $+			mempty { pinmodes = M.singleton p (S.singleton OutputMode) }+		tell [(go triggername, const f)] 	  where-		go = trigger triggername c [arg (constant n), arg b]-		(f, triggername) = -			defineTriggerAlias (show n) "digitalWrite" $-				mempty { pinmodes = M.singleton p (S.singleton OutputMode) }+		go triggername tl = +			let c' = addTriggerLimit tl c+			in trigger triggername c' [arg (constant n), arg b]  instance IsPWMPin t => Output (Pin t) (Event 'PWM (Stream Word8)) where-	(Pin (PinId n)) =: (Event v c) = tell [(go, f)]+	(Pin (PinId n)) =: (Event v c) = do+		(f, triggername) <- defineTriggerAlias' ("pin_" <> show n) "analogWrite" mempty+		tell [(go triggername, const f)] 	  where-		go = trigger triggername c [arg (constant n), arg v]+		go triggername tl = +			let c' = addTriggerLimit tl c+			in trigger triggername c' [arg (constant n), arg v] 		-- analogWrite does not need any pinmodes set up-		(f, triggername) = defineTriggerAlias (show n) "analogWrite" mempty  class Input o t where 	-- | The list is input to use when simulating the Sketch. 	input' :: o -> [t] -> Sketch (Behavior t) +-- | Use this to read a value from a component of the Arduino.+--+-- For example, to read a digital value from pin12 and turn on the +-- led when the pin is high:+--+-- > buttonpressed <- input pin12+-- > led =: buttonpressed+--+-- Some pins support multiple types of reads, for example pin a0+-- supports a digital read (`Bool`), and an analog to digital converter+-- read (`ADC`). In such cases you may need to specify the type of+-- data to read:+--+-- > v <- input a0 :: Sketch (Behavior ADC)+input :: Input o t => o -> Sketch (Behavior t)+input o = input' o []+ instance IsDigitalIOPin t => Input (Pin t) Bool where 	input' (Pin p@(PinId n)) interpretvalues = mkInput $ InputSource-		{ defineVar = [CLine $ "bool " <> varname <> ";"]-		, setupInput = []+		{ defineVar = mkCChunk [CLine $ "bool " <> varname <> ";"]+		, setupInput = mempty 		, inputPinmode = M.singleton p InputMode-		, readInput = [CLine $ varname <> " = digitalRead(" <> show n <> ");"]+		, readInput = mkCChunk+			[CLine $ varname <> " = digitalRead(" <> show n <> ");"] 		, inputStream = extern varname interpretvalues' 		} 	  where@@ -302,10 +429,11 @@  instance IsAnalogInputPin t => Input (Pin t) ADC where 	input' (Pin (PinId n)) interpretvalues = mkInput $ InputSource-		{ defineVar = [CLine $ "int " <> varname <> ";"]-		, setupInput = []+		{ defineVar = mkCChunk [CLine $ "int " <> varname <> ";"]+		, setupInput = mempty 		, inputPinmode = mempty-		, readInput = [CLine $ varname <> " = analogRead(" <> show n <> ");"]+		, readInput = mkCChunk+			[CLine $ varname <> " = analogRead(" <> show n <> ");"] 		, inputStream = extern varname interpretvalues' 		} 	  where
src/Copilot/Arduino/Library/EEPROMex.hs view
@@ -32,12 +32,15 @@ 	sweepRange, 	sweepRange', 	RangeWrites(..),+	scanRange,+	RangeReads(..), ) where  import Copilot.Arduino import Copilot.Arduino.Internals import Control.Monad.Writer import Data.Proxy+import qualified Prelude  -- | Set the maximum number of writes to EEPROM that can be made while -- the Arduino is running. When too many writes have been made,@@ -59,17 +62,17 @@ -- 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)]+maxAllowedWrites n = tell [(\_ -> return (), \_ -> f)]   where 	f = mempty-		{ earlySetups = +		{ earlySetups = mkCChunk 			[ 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 ]+		, defines = mkCChunk [ includeCLine ] 		}  -- | The address of the first byte of the EEPROM that will be allocated@@ -94,19 +97,20 @@ -- -- > EEPROM.memPool 0 (EndAddress sizeOfEEPROM) memPool :: StartAddress -> EndAddress -> Sketch ()-memPool (StartAddress start) (EndAddress end) = tell [(return (), f)]+memPool (StartAddress start) (EndAddress end) =+	tell [(\_ -> return (), \_ -> f)]   where 	f = mempty 		-- setMemPool() has to come before any 		-- getAddress(), so do it in earlySetups.-		{ earlySetups = +		{ earlySetups = mkCChunk 			[ CLine $ "EEPROM.setMemPool(" 				<> show start 				<> ", " 				<> show end 				<> ");" 			]-		, defines = [ includeCLine ]+		, defines = mkCChunk [ includeCLine ] 		}  -- | Allocates a location in the EEPROM. @@ -152,23 +156,24 @@ -- 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+	i <- getUniqueId "eeprom"+	let addrvarname = uniqueName "eeprom_address" i+	let bootvarname = uniqueName "eeprom_boot_val" 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 "}"+			[ CChunk [includeCLine]+			, CChunk+				[ CLine $ "int " <> addrvarname <> ";"+				, CLine $ showCType proxy <> " " <> bootvarname <> ";"+				, CLine $ "void " <> eepromWriterName i+					<> "(" <> showCType proxy <> " value) {"+				, CLine $ "  EEPROM." <> writeValue proxy+					<> "(" <> addrvarname <> ", value);"+				, CLine "}"+				] 			]-		, setupInput =+		, setupInput = mkCChunk 			[ CLine $ addrvarname <>  				" = EEPROM.getAddress(sizeof("  				<> showCType proxy <> "));"@@ -180,17 +185,25 @@ 		, inputStream = extern bootvarname (Just (repeat interpretval)) 		, inputPinmode = mempty 		}-	return (bootval, Location writername)+	return (bootval, Location i) -data Location t = Location String+eepromWriterName :: UniqueId -> String+eepromWriterName = uniqueName' "eeprom_write" +data Location t = Location UniqueId+ instance EEPROMable t => Output (Location t) (Event () (Stream t)) where-	Location writername =: (Event v c) =-		tell [(trigger writername c [arg v], mempty)]+	Location i =: (Event v c) = do+		(f, triggername) <- defineTriggerAlias (eepromWriterName i) mempty+		tell [(go triggername, \_ -> f)]+	  where+		go triggername tl =+			let c' = addTriggerLimit tl c+			in trigger triggername c' [arg v]  -- | A range of values in the EEPROM. data Range t = Range-	{ rangeStart :: Location (Range t)+	{ rangeLocation :: Location (Range t) 	, rangeSize :: Word16 -- ^ number of `t` values in the Range 	} @@ -212,40 +225,10 @@ -- -- 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 :: (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)+	i <- getUniqueId "eeprom"+	return (Range (Location i) sz)  -- | Description of writes made to a Range. --@@ -286,15 +269,53 @@  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)]+writeRange :: forall t. EEPROMable t => Behavior Bool -> Range t -> RangeWrites t -> Sketch()+writeRange c range (RangeWrites idx v) = do+	(f', triggername) <- defineTriggerAlias writername f+	tell [(spec triggername, \_ -> f')]   where-	Location writername = rangeStart range+	Location i = rangeLocation range 	idx' = idx c `mod` constant (rangeSize range)+	startaddrvarname = eepromRangeStartAddrName i+	writername = uniqueName "eeprom_range_write" i+	proxy = Proxy @t+	f = Framework+		{ defines = +			[ CChunk [includeCLine]+			, CChunk+				[ 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 = mkCChunk+			[ CLine $ startaddrvarname <> " = EEPROM.getAddress"+				<> "(sizeof(" <> showCType proxy <> ")" +				<> " * " <> show (rangeSize range)+				<> ");"+			]+		, earlySetups = []+		, pinmodes = mempty+		, loops = mempty+		}+	spec triggername tl =+		let c' = addTriggerLimit tl c+		in trigger triggername c' [arg idx', arg v] --- | Treat the range as a ring buffer, and starting with the specified--- `RangeIndex`, sweep over the range writing values from the+eepromRangeStartAddrName :: UniqueId -> String+eepromRangeStartAddrName = uniqueName "eeprom_range_address"++-- | 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)@@ -307,6 +328,80 @@   where 	cnt = [start] ++ rest 	rest = if c then cnt + 1 else cnt++-- | Description of how to read a` Range`.+--+-- The first read is made at the specified RangeIndex, and the location+-- to read from subsequently comes from the Behavior RangeIndex.+data RangeReads t = RangeReads (Range t) RangeIndex (Behavior RangeIndex)++-- | Scan through the `Range`, starting with the specified `RangeIndex`.+--+-- > range <- EEPROM.allocRange 100 :: Sketch (EEPROM.Range ADC)+-- > v <- input $ scanRange range 0+-- +-- Once the end of the `Range` is reached, input continues+-- from the start of the `Range`.+--+-- It's fine to write and read from the same range in the same Sketch,+-- but if the RangeIndex being read and written is the same, it's not+-- defined in what order the two operations will happen.+--+-- Also, when interpreting a Sketch that both reads and writes to a range,+-- the input from that range won't reflect the writes made to it,+-- but will instead come from the list of values passed to `input'`.+scanRange :: Range t -> RangeIndex -> RangeReads t+scanRange r startidx = RangeReads r startidx cnt+  where+	cnt = [startidx+1] ++ rest+	rest = cnt + 1++instance (ShowCType t, EEPROMable t) => Input (RangeReads t) t where+	input' (RangeReads range startidx idx) interpretvalues = do+		-- This trigger writes value of idx+		-- to indexvarname. The next time through the loop,+		-- the extern uses that to determine where to read from.+		(f, triggername) <- defineTriggerAlias indexvarupdatername mempty+		let t tl = +			let c = getTriggerLimit tl+			in trigger triggername c [arg idx']+		tell [(t, \_ -> f)]+		mkInput $ InputSource+			{ defineVar = mkCChunk+				[ CLine $ showCType proxy <> " " <> valname <> ";"+				, CLine $ "int " <> indexvarname <> ";"+				, CLine $ "void " <> indexvarupdatername <> " (int idx) {"+				, CLine $ "  " <> indexvarname <> " = idx;"+				, CLine $ "}"+				]+			, setupInput =  mkCChunk+				-- Prime with startidx on the first time+				-- through the loop.+				[ CLine $ indexvarname <> " = " <> show startidx' <> ";" ]+			, readInput = mkCChunk+				[ CLine $ valname <> " = EEPROM." <> readValue proxy+					<> "("+						<> eepromRangeStartAddrName i+						<> " + " <> indexvarname +						<> "*sizeof("+						<> showCType proxy+						<> ")"+					<> ");"+				]+			, inputStream = extern valname interpretvalues'+			, inputPinmode = mempty+			}+	  where+		Location i = rangeLocation range+		idx' = idx `mod` constant (rangeSize range)+		startidx' = startidx `Prelude.mod` rangeSize range+		proxy = Proxy @t+		indexvarname = uniqueName "eeprom_range_read_index" i+		indexvarupdatername = uniqueName "eeprom_range_read" i+		valname = uniqueName "eeprom_range_val" i+		interpretvalues'+			| null interpretvalues = Nothing+			| otherwise = Just interpretvalues  class (ShowCType t, Typed t) => EEPROMable t where 	readValue :: Proxy t -> String
src/Copilot/Arduino/Library/Serial.hs view
@@ -38,8 +38,7 @@ -- | Use this to communicate with the serial port, both input and output. -- -- To output to the serial port, simply connect this to a [`FormatOutput`]--- that describes the serial output. Note that you can only do this once--- in a Sketch.+-- that describes the serial output. -- -- > main = arduino $ do -- > 	Serial.baud 9600@@ -49,6 +48,14 @@ -- > 		, Serial.show b -- > 		, Serial.char '\n' -- > 		]+-- +-- You can output different things to a serial port at different times,+-- eg using `whenB`, but note that if multiple outputs are sent at the+-- same time, the actual order is not defined. This example may output+-- "world" before "hello"+--+-- > Serial.device =: [Serial.str "hello "]+-- > Serial.device =: [Serial.str "world"] -- -- To input from the serial port, use this with `input`. --
src/Copilot/Arduino/Library/Serial/Device.hs view
@@ -29,10 +29,11 @@ newtype SerialDeviceName = SerialDeviceName String  baudD :: SerialDeviceName -> Int -> Sketch ()-baudD (SerialDeviceName devname) n = tell [(return (), f)]+baudD (SerialDeviceName devname) n = tell [(\_ -> return (), \_ -> f)]   where 	f = mempty-		{ setups = [CLine $ devname <> ".begin(" <> Prelude.show n <> ");"]+		{ setups = mkCChunk+			[CLine $ devname <> ".begin(" <> Prelude.show n <> ");"] 		}  newtype Baud = Baud Int@@ -47,18 +48,20 @@ 	-> Sketch () configureD d@(SerialDeviceName devname) (Pin (PinId rxpin)) (Pin (PinId txpin)) (Baud n) = do 	baudD d n-	tell [(return (), f)]+	tell [(\_ -> return (), \_ -> f)]   where 	f = mempty-		{ defines = -			[ CLine $ "#include <SoftwareSerial.h>"-			, CLine $ "SoftwareSerial " <> devname-				<> " = SoftwareSerial"-				<> "("-				<> Prelude.show rxpin-				<> ", "-				<> Prelude.show txpin-				<> ");"+		{ defines =+			[ CChunk [ CLine $ "#include <SoftwareSerial.h>" ]+			, CChunk+				[ CLine $ "SoftwareSerial " <> devname+					<> " = SoftwareSerial"+					<> "("+					<> Prelude.show rxpin+					<> ", "+					<> Prelude.show txpin+					<> ");"+				] 			] 		} @@ -69,10 +72,12 @@ 		mkInput s 	  where 		s = InputSource-			{ defineVar = [CLine $ "int " <> varname <> ";"]+			{ defineVar = mkCChunk+				[CLine $ "int " <> varname <> ";"] 			, setupInput = [] 			, inputPinmode = mempty-			, readInput = [CLine $ varname <> " = " <> devname <> ".read();"]+			, readInput = mkCChunk+				[CLine $ varname <> " = " <> devname <> ".read();"] 			, inputStream = extern varname interpretvalues' 			} 		varname = "input_" <> devname@@ -88,16 +93,19 @@ 	sdn =: l = sdn =: (Event l true :: Event () [FormatOutput])  instance Output SerialDevice (Event () [FormatOutput]) where-	SerialDevice sdn@(SerialDeviceName devname) =: (Event l c) =-		tell [(go, f)]+	SerialDevice sdn@(SerialDeviceName devname) =: (Event l c) = do+		u <- getUniqueId "serial"+		let outputfuncname = uniqueName ("output_" <> devname) u+		let f = mempty { defines = printer outputfuncname }+		(f', triggername) <- defineTriggerAlias outputfuncname f+		tell [(go triggername, \_ -> f')] 	  where-		go = trigger triggername c (mapMaybe formatArg l)-		f = mempty { defines = printer }--		triggername = "output_" <> devname+		go triggername tl = +			let c' = addTriggerLimit tl c+			in trigger triggername c' (mapMaybe formatArg l) 	-		printer = concat-			[ [CLine $ "void " <> triggername <> "("+		printer outputfuncname = mkCChunk $ concat+			[ [CLine $ "void " <> outputfuncname <> "(" 				<> intercalate ", " arglist <> ") {"] 			, map (\(fmt, n) -> CLine ("  " <> fromCLine (fmt n))) 				(zip (map (\fo -> formatCLine fo sdn) l) argnames)@@ -134,9 +142,9 @@ quoteString :: String -> String quoteString s = '"' : concatMap esc s <> "\""   where-	esc '"' = "\""-	esc '\\' = "\\"-	esc '\n' = "\\\n"+	esc '"' = "\\\""+	esc '\\' = "\\\\"+	esc '\n' = "\\n" 	esc c = [c]  class OutputString t where
src/Copilot/Arduino/Main.hs view
@@ -107,19 +107,20 @@ 		, "#include <stdint.h>" 		, blank 		]-	, map fromCLine (defines f)+	, map fromCLine (fromchunks (defines f)) 	, [blank] 	, map fromCLine ccode 	, [blank] 	, 		[ "void setup()" 		]-	, codeblock $ map fromCLine (earlySetups f <> setups f)+	, codeblock $ map fromCLine+		(fromchunks (earlySetups f) <> fromchunks (setups f)) 	, [blank] 	, 		[ "void loop()" 		]-	, codeblock $ map fromCLine (loops f) <>+	, codeblock $ map fromCLine (fromchunks (loops f)) <> 		[ "step();" 		] 	]@@ -127,10 +128,18 @@ 	blank = "" 	indent l = "  " <> l 	codeblock l = ["{"] <> map indent l <> ["}"]+	-- If two CChunks are identical, only include it once.+	-- This can happen when eg, the same setup code is generated+	-- to use a resource that's accessed more than once in a program.+	-- Note: Does not preserve order of chunks in the list.+	fromchunks :: [CChunk] -> [CLine]+	fromchunks cl = concatMap (\(CChunk l) -> l) $+		S.toList $ S.fromList cl  finalizeFramework :: Framework -> Framework finalizeFramework f = -	let pinsetups = mapMaybe setuppinmode (M.toList $ pinmodes f)+	let pinsetups = concat $+		mapMaybe setuppinmode (M.toList $ pinmodes f) 	in f { setups = pinsetups <> setups f }   where 	setuppinmode (PinId n, s)@@ -151,5 +160,5 @@ 			unwords (map show (S.toList s)) ++ "). " ++ 			"This is not currently supported by arduino-copilot." 	-	setmode n v = Just $ CLine $-		"pinMode(" <> show n <> ", " ++ v ++ ");"+	setmode n v = Just $ mkCChunk+		[ CLine $ "pinMode(" <> show n <> ", " ++ v ++ ");" ]