diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,14 @@
+arduino-copilot (1.3.0) unstable; urgency=medium
+
+  * New polymorphic "input" action that can be used to read digital and
+    analog pins, as well as read from serial ports, and replaces
+    readfrom, readvoltage, etc.
+  * Replaced PWMDutyCycle with pwm.
+  * Changed the Serial and XBee modules to work with the =: operator
+    and "input".
+
+ -- Joey Hess <id@joeyh.name>  Thu, 30 Jan 2020 22:33:20 -0400
+
 arduino-copilot (1.2.0) unstable; urgency=medium
 
   * Bugfix: Doing the same kind of write to two different pins in the
diff --git a/TODO b/TODO
--- a/TODO
+++ b/TODO
@@ -12,29 +12,64 @@
   or check that the C compiler can optimise away a check of a flag that
   always succeeds.
 
-* Naming could be more FRP-like. readfrom and readvoltage
-  are too imperative sounding. (Outputs were already fixed to be
-  FRP-style.)
+  (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.)
 
-  A polymorphic read operation could work, assuming type inference is
-  able to usually determine what kind of value is wanted to be read.
+  An Input returns a Behavior; with @: it should really return an Event.
+  But, that would need boilerplate to get the Stream out to use.
 
-	a <- stream a1
-	pin1 := a
+ 	- foo <- input pin4
+ 	+ Event foo _ <- input pin4 @: longer_and_longer
+	  pin5 =: streamfunc foo
 
-  Looks like this would work: While the type of a could be Bool or Voltage,
-  going to a pin that only supports digital IO implies it must be a Bool.
-  (Need to check if the type checker is able to work this out!)
+  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.)
+
+  Another combinator, that I considered but rejected is:
+
+  (|:) :: Sketch a -> Sketch a -> Sketch a 
+
+  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.
+
 * analogReference()
   Takes one of a set of defines, which vary depending on board,
   so how to pass that through Copilot? Could write a C
   switch statement, on an int, and have the boade modules define
   effectively enums.
-* readvoltage gets a raw ADC value; add a stream function to convert that
+* Voltage is a raw ADC value; add a stream function to convert that
   to a floating point actual voltage, given also a stream that contains
   the reference voltage.
-* delayMicroseconds
 * pulses
 * random numbers
 * Wire library
+* test.hs is not wired into the cabal file because it depends on runghc
+  having the library available
diff --git a/arduino-copilot.cabal b/arduino-copilot.cabal
--- a/arduino-copilot.cabal
+++ b/arduino-copilot.cabal
@@ -1,5 +1,5 @@
 Name: arduino-copilot
-Version: 1.2.0
+Version: 1.3.0
 Cabal-Version: >= 1.8
 License: BSD3
 Maintainer: Joey Hess <id@joeyh.name>
@@ -41,6 +41,11 @@
   examples/button/README
   examples/button/demo.hs
   examples/button/pre-build-hook.sh
+  examples/serialport/Makefile
+  examples/serialport/README
+  examples/serialport/demo.hs
+  examples/serialport/pre-build-hook.sh
+  test.hs
 
 Library
   GHC-Options: -Wall -fno-warn-tabs
diff --git a/examples/button/demo.hs b/examples/button/demo.hs
--- a/examples/button/demo.hs
+++ b/examples/button/demo.hs
@@ -17,6 +17,6 @@
 
 main :: IO ()
 main = arduino $ do
-	buttonpressed <- readfrom' pin12 [False, False, False, True, True]
+	buttonpressed <- input' pin12 [False, False, False, True, True]
 	led =: buttonpressed || blinking
 	delay =: MilliSeconds (longer_and_longer * 2)
diff --git a/examples/serialport/Makefile b/examples/serialport/Makefile
new file mode 100644
--- /dev/null
+++ b/examples/serialport/Makefile
@@ -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
diff --git a/examples/serialport/README b/examples/serialport/README
new file mode 100644
--- /dev/null
+++ b/examples/serialport/README
@@ -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).
diff --git a/examples/serialport/demo.hs b/examples/serialport/demo.hs
new file mode 100644
--- /dev/null
+++ b/examples/serialport/demo.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE RebindableSyntax #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+
+import Copilot.Arduino.Uno
+import qualified Copilot.Arduino.Library.Serial as Serial
+import qualified Copilot.Arduino.Library.Serial.XBee as XBee
+import Control.Monad
+
+-- Should it use the onboard serial port?
+useSerial :: Bool
+useSerial = True
+
+-- Should it use the XBee wireless serial port?
+-- (it can use more than one serial port at the same time.)
+useXBee :: Bool
+useXBee = True
+
+userIsTyping :: Stream Int8 -> Stream Bool
+userIsTyping userinput = userinput /= constant Serial.noInput
+
+getSerial :: Input o Int8 => Bool -> o -> Sketch (Behavior Int8)
+getSerial True i = input i
+getSerial False _ = return (constant Serial.noInput)
+
+main :: IO ()
+main = arduino $ do
+	when useSerial $ Serial.baud 9600
+	when useXBee $ XBee.configure pin2 pin3 (XBee.Baud 9600)
+
+	-- Measure the voltage of an analog input, and display it to the
+	-- serial port. This is a handy demo because with nothing connected
+	-- to a0, just moving your hand close to the arduino will probably
+	-- cause some noise to be read.
+	n <- input a0 :: Sketch (Behavior Voltage)
+	let msg = 
+		[ Serial.str "analog input:"
+		, Serial.showFormatted n Serial.HEX
+		, Serial.char '\n'
+		]
+	when useSerial $ Serial.device =: msg
+	when useXBee $ XBee.device =: msg
+
+	-- Light the led whenever the user types on the serial port.
+	userinput <- getSerial useSerial Serial.device
+	userinputxbee <- getSerial useXBee XBee.device
+	led =: userIsTyping userinput || userIsTyping userinputxbee
+
+	delay =: MilliSeconds (constant 100)
diff --git a/examples/serialport/pre-build-hook.sh b/examples/serialport/pre-build-hook.sh
new file mode 100644
--- /dev/null
+++ b/examples/serialport/pre-build-hook.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec runghc demo.hs
diff --git a/src/Copilot/Arduino.hs b/src/Copilot/Arduino.hs
--- a/src/Copilot/Arduino.hs
+++ b/src/Copilot/Arduino.hs
@@ -8,11 +8,7 @@
 
 {-# LANGUAGE RebindableSyntax #-}
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE PolyKinds #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE UndecidableInstances #-}
 
 module Copilot.Arduino (
 	-- * Arduino sketch generation
@@ -21,16 +17,15 @@
 	Pin,
 	-- * Functional reactive programming
 	Behavior,
+	TypedBehavior,
 	Event,
 	(@:),
 	-- * Inputs
 	Input,
-	readfrom,
-	readfrom',
+	input,
+	input',
 	pullup,
 	Voltage,
-	readvoltage,
-	readvoltage',
 	-- * Outputs
 	--
 	-- | Only a few common outputs are included in this module.
@@ -39,7 +34,7 @@
 	Output,
 	led,
 	(=:),
-	PWMDutyCycle(..),
+	pwm,
 	delay,
 	MilliSeconds(..),
 	MicroSeconds(..),
@@ -109,37 +104,31 @@
 instance Output Delay MicroSeconds where
 	Delay =: (MicroSeconds n) = tell
 		[(trigger "delayMicroseconds" true [arg n], mempty)]
-
--- | Reading a Bool from a `Pin`.
+	
+-- | Use this to read a value from a component of the Arduino.
 --
--- > buttonpressed <- readfrom pin12
+-- For example, to read a digital value from pin12 and turn on the 
+-- led when the pin is high:
+--
+-- > buttonpressed <- input pin12
 -- > led =: buttonpressed
-readfrom :: IsDigitalIOPin t => Pin t -> Input Bool
-readfrom p = readfrom' p []
-
--- | The list is used as simulated input when interpreting the program.
-readfrom' :: IsDigitalIOPin t => Pin t -> [Bool] -> Input Bool
-readfrom' (Pin p@(PinId n)) interpretvalues = mkInput $ InputSource
-	{ defineVar = [CLine $ "bool " <> varname <> ";"]
-	, setupInput = []
-	, inputPinmode = M.singleton p InputMode
-	, readInput = [CLine $ varname <> " = digitalRead(" <> show n <> ");"]
-	, inputStream = extern varname interpretvalues'
-	}
-  where
-	varname = "arduino_digital_pin_input" <> show n
-	interpretvalues'
-		| null interpretvalues = Nothing
-		| otherwise = Just interpretvalues
+--
+-- Some pins support multiple types of reads, for example pin a0
+-- supports a digital read (`Bool`), and an analog read (`Voltage`).
+-- In such cases you may need to specify the type of data to read:
+--
+-- > v <- input a0 :: Sketch (Behavior Voltage)
+input :: Input o t => o -> Sketch (Behavior t)
+input o = input' o []
 
--- | Normally when a `Pin` is read with `readfrom`, it is configured
+-- | 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`.
 --
 -- Bear in mind that enabling the pullup resistor inverts the value that
 -- will be read from the pin.
 --
--- > pullup pin3
+-- > pullup pin12
 pullup :: Pin t -> Sketch ()
 pullup (Pin p) = tell [(return (), f)]
   where
@@ -147,44 +136,14 @@
 		{ pinmodes = M.singleton p (S.singleton InputPullupMode)
 		}
 
--- | Voltage read from an Arduino's ADC. Ranges from 0-1023.
-type Voltage = Int16
-
--- | Measuring the voltage of a `Pin`.
-readvoltage :: IsAnalogInputPin t => Pin t -> Input Voltage
-readvoltage p = readvoltage' p []
-
-readvoltage' :: IsAnalogInputPin t => Pin t -> [Int16] -> Input Voltage
-readvoltage' (Pin (PinId n)) interpretvalues = mkInput $ InputSource
-	{ defineVar = [CLine $ "int " <> varname <> ";"]
-	, setupInput = []
-	, inputPinmode = mempty
-	, readInput = [CLine $ varname <> " = analogRead(" <> show n <> ");"]
-	, inputStream = extern varname interpretvalues'
-	}
-  where
-	varname = "arduino_analog_pin_input" <> show n
-	interpretvalues'
-		| null interpretvalues = Nothing
-		| otherwise = Just interpretvalues
-
 -- | Use this to do PWM output to a pin.
 --
--- > pin3 =: PWMDutyCycle (constant 128)
+-- > pin3 =: pwm (constant 128)
 -- 
--- Each Word8 from the Stream describes a PWM square wave.
+-- Each Word8 of the Behavior describes a PWM square wave.
 -- 0 is always off and 255 is always on.
-data PWMDutyCycle = PWMDutyCycle (Behavior Word8)
-
-instance IsPWMPin t => Output (Pin t) (Event PWMDutyCycle) where
-	(Pin (PinId n)) =: (Event (PWMDutyCycle v) c) = tell [(go, f)]
-	  where
-		go = trigger triggername c [arg (constant n), arg v]
-		-- analogWrite does not need any pinmodes set up
-		(f, triggername) = defineTriggerAlias (show n) "analogWrite" mempty
-
-instance IsPWMPin t => Output (Pin t) PWMDutyCycle where
-	(=:) o s = o =: alwaysEvent s
+pwm :: Behavior Word8 -> TypedBehavior 'PWM Word8
+pwm = TypedBehavior
 
 -- | The on-board LED.
 led :: Pin '[ 'DigitalIO ]
diff --git a/src/Copilot/Arduino/Internals.hs b/src/Copilot/Arduino/Internals.hs
--- a/src/Copilot/Arduino/Internals.hs
+++ b/src/Copilot/Arduino/Internals.hs
@@ -30,6 +30,16 @@
 -- <https://copilot-language.github.io/>
 type Behavior t = Stream t
 
+-- | A Behavior with an additional phantom type `p`.
+--
+-- The Compilot DSL only lets a Stream contain basic C types,
+-- a limitation that `Behavior` also has. When more type safely
+-- is needed, this can be used.
+data TypedBehavior p t = TypedBehavior (Behavior t)
+
+-- | A discrete event, that occurs at particular points in time.
+data Event p v = Event v (Stream Bool)
+
 -- | An Arduino sketch, implemented using Copilot.
 --
 -- It's best to think of the `Sketch` as a description of the state of the
@@ -75,10 +85,16 @@
 instance Monoid Framework where
 	mempty = Framework mempty mempty mempty mempty
 
--- | A source of a `Stream` of values input from the Arduino.
---
--- Runs in the `Sketch` monad.
-type Input t = Sketch (Stream t)
+-- | 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
 
 data InputSource t = InputSource
 	{ defineVar :: [CLine]
@@ -93,7 +109,7 @@
 	, inputStream :: Stream t
 	}
 
-mkInput :: InputSource t -> Input t
+mkInput :: InputSource t -> Sketch (Behavior t)
 mkInput i = do
 	tell [(return (), f)]
 	return (inputStream i)
@@ -159,7 +175,7 @@
 -- | Things that can have a `Behavior` or `Event` output to them.
 class Output o t where
 	(=:) :: o -> t -> Sketch ()
-	-- ^ Conneact a `Behavior` or `Event` to an `Output`
+	-- ^ Connect a `Behavior` or `Event` to an `Output`
 	--
 	-- > led =: blinking
 	--
@@ -173,7 +189,8 @@
 	-- > led =: true
 	--
 	-- To avoid unncessary work being done, you can use an `Event`
-	-- instead. Then only new values of the `Event` will be written.
+	-- instead. Then the write only happens at the points in time
+	-- when the `Event` occurs.
 	-- 
 	-- So to make the LED only be turned on in the first iteration,
 	-- and allow it to remain on thereafter without doing extra work:
@@ -183,25 +200,36 @@
 -- Same fixity as =<<
 infixr 1 =:
 
--- | A discrete event, that occurs at particular points in time.
-data Event v = Event v (Stream Bool)
+instance Output o (Event () (Stream v)) => Output o (Behavior v) where
+	(=:) o b = o =: te
+	  where
+	  	te :: Event () (Stream v)
+		te = Event b true
 
-instance Output o (Event (Behavior v)) => Output o (Behavior v) where
-	(=:) o s = o =: alwaysEvent s
+instance Output o (Event p (Stream v)) => Output o (TypedBehavior p v) where
+	(=:) o (TypedBehavior b) = o =: te
+	  where
+		te :: Event p (Stream v)
+		te = Event b true
 
-alwaysEvent :: v -> Event v
-alwaysEvent s = Event s true
+-- | This type family is open, so it can be extended when adding other data
+-- types to the IsBehavior class.
+type family BehaviorToEvent a
+type instance BehaviorToEvent (Behavior v) = Event () (Stream v)
+type instance BehaviorToEvent (TypedBehavior p v) = Event p (Stream v)
 
--- | Generate an event, that only occurs when the `Behavior` Bool is True.
---
--- While `v` is usually some type of `Behavior`, this can also be used with
--- some other data types that contain a `Behavior`. For example:
---
--- > pin3 := PWMDutyCycle (constant 128) @: firstIteration
-(@:) :: v -> Behavior Bool -> Event v
-(@:) = Event
+class IsBehavior behavior where
+	-- | Generate an event, from some type of behavior,
+	-- that only occurs when the `Behavior` Bool is True.
+	(@:) :: behavior -> Behavior Bool -> BehaviorToEvent behavior
 
-instance IsDigitalIOPin t => Output (Pin t) (Event (Behavior Bool)) where
+instance IsBehavior (Behavior v) where
+	b @: c = Event b c
+
+instance IsBehavior (TypedBehavior p v) where
+	(@:) (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)]
 	  where
 		go = trigger triggername c [arg (constant n), arg b]
@@ -209,13 +237,45 @@
 			defineTriggerAlias (show n) "digitalWrite" $
 				mempty { pinmodes = M.singleton p (S.singleton OutputMode) }
 
--- | 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
+instance IsPWMPin t => Output (Pin t) (Event 'PWM (Stream Word8)) where
+	(Pin (PinId n)) =: (Event v c) = tell [(go, f)]
+	  where
+		go = 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)
+
+instance IsDigitalIOPin t => Input (Pin t) Bool where
+	input' (Pin p@(PinId n)) interpretvalues = mkInput $ InputSource
+		{ defineVar = [CLine $ "bool " <> varname <> ";"]
+		, setupInput = []
+		, inputPinmode = M.singleton p InputMode
+		, readInput = [CLine $ varname <> " = digitalRead(" <> show n <> ");"]
+		, inputStream = extern varname interpretvalues'
+		}
+	  where
+		varname = "arduino_digital_pin_input" <> show n
+		interpretvalues'
+			| null interpretvalues = Nothing
+			| otherwise = Just interpretvalues
+
+-- | Voltage read from an Arduino's ADC. Ranges from 0-1023.
+type Voltage = Int16
+
+instance IsAnalogInputPin t => Input (Pin t) Voltage where
+	input' (Pin (PinId n)) interpretvalues = mkInput $ InputSource
+		{ defineVar = [CLine $ "int " <> varname <> ";"]
+		, setupInput = []
+		, inputPinmode = mempty
+		, readInput = [CLine $ varname <> " = analogRead(" <> show n <> ");"]
+		, inputStream = extern varname interpretvalues'
+		}
+	  where
+		varname = "arduino_analog_pin_input" <> show n
+		interpretvalues'
+			| null interpretvalues = Nothing
+			| otherwise = Just interpretvalues
+
diff --git a/src/Copilot/Arduino/Library/Serial.hs b/src/Copilot/Arduino/Library/Serial.hs
--- a/src/Copilot/Arduino/Library/Serial.hs
+++ b/src/Copilot/Arduino/Library/Serial.hs
@@ -7,15 +7,15 @@
 
 module Copilot.Arduino.Library.Serial (
 	baud,
-	output,
+	device,
 	char,
 	str,
 	show,
 	showFormatted,
 	byte,
-	input,
-	input',
 	noInput,
+	SerialDevice,
+	FormatOutput,
 	ShowableType,
 	FormatableType,
 	Base(..),
@@ -34,37 +34,27 @@
 baud :: Int -> Sketch ()
 baud = baudD dev
 
--- | Output to the serial port.
+-- | Use this to communicate with the serial port, both input and output.
 --
--- Note that this can only be used once in a Sketch.
+-- 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.
 --
--- > main = arduino $ do
+-- > main = arduino $ doa
 -- > 	Serial.baud 9600
--- > 	b <- readfrom pin3
--- > 	n <- readvoltage a1
--- > 	Serial.output true
--- > 		[ Serial.str "pin3:"
+-- > 	b <- input pin4
+-- > 	Serial.device =:
+-- > 		[ Serial.str "pin4:"
 -- > 		, Serial.show b
--- > 		, Serial.str " a1:"
--- > 		, Serial.show n
 -- > 		, Serial.char '\n'
 -- > 		]
-output
-	:: Stream Bool
-	-- ^ This Stream controls when output is sent to the serial port.
-	-> [FormatOutput]
-	-> Sketch ()
-output = outputD dev
-
--- | Input from the serial port.
 --
--- Reads one byte on each iteration of the sketch. When there is no
--- serial input available, reads `noInput`.
+-- To input from the serial port, use this with `input`.
 --
--- > userinput <- Serial.input
-input :: Input Int8
-input = inputD dev
-
--- | The list is used to simulate serial input when interpreting the program.
-input' :: [Int8] -> Input Int8
-input' = input'D dev
+-- > userinput <- input Serial.device
+--
+-- The resulting `Behavior Int8` will be updated on each iteration
+-- of the sketch. When there is no new serial input available, it will
+-- contain `noInput`.
+device :: SerialDevice
+device = SerialDevice dev
diff --git a/src/Copilot/Arduino/Library/Serial/Device.hs b/src/Copilot/Arduino/Library/Serial/Device.hs
--- a/src/Copilot/Arduino/Library/Serial/Device.hs
+++ b/src/Copilot/Arduino/Library/Serial/Device.hs
@@ -25,7 +25,7 @@
 import Data.Proxy
 import qualified Prelude
 
--- | Eg "Serial" or "Serial2"
+-- | Eg \"Serial\" or \"Serial2\"
 newtype SerialDeviceName = SerialDeviceName String
 
 baudD :: SerialDeviceName -> Int -> Sketch ()
@@ -38,14 +38,14 @@
 newtype Baud = Baud Int
 	deriving (Show, Eq)
 
-deviceD
+configureD
 	:: (IsDigitalIOPin rx, IsDigitalIOPin tx)
 	=> SerialDeviceName
 	-> Pin rx
 	-> Pin tx
 	-> Baud
 	-> Sketch ()
-deviceD d@(SerialDeviceName devname) (Pin (PinId rxpin)) (Pin (PinId txpin)) (Baud n) = do
+configureD d@(SerialDeviceName devname) (Pin (PinId rxpin)) (Pin (PinId txpin)) (Baud n) = do
 	baudD d n
 	tell [(return (), f)]
   where
@@ -62,32 +62,58 @@
 			]
 		}
 
-outputD
-	:: SerialDeviceName
-	-> Stream Bool
-	-- ^ This Stream controls when output is sent to the serial port.
-	-> [FormatOutput]
-	-> Sketch ()
-outputD sdn@(SerialDeviceName devname) c l = tell [(go, f)]
-  where
-	go = trigger triggername c (mapMaybe formatArg l)
-	f = mempty { defines = printer }
+newtype SerialDevice = SerialDevice SerialDeviceName
 
-	triggername = "arduino_serial_" <> devname <> "_output"
+instance Input SerialDevice Int8 where
+	input' (SerialDevice (SerialDeviceName devname)) interpretvalues =
+		mkInput s
+	  where
+		s = InputSource
+			{ defineVar = [CLine $ "int " <> varname <> ";"]
+			, setupInput = []
+			, inputPinmode = mempty
+			, readInput = [CLine $ varname <> " = " <> devname <> ".read();"]
+			, inputStream = extern varname interpretvalues'
+			}
+		varname = "arduino_serial_" <> devname <> "_read"
+		interpretvalues'
+			| null interpretvalues = Nothing
+			| otherwise = Just interpretvalues
 
-	printer = concat
-		[ [CLine $ "void " <> triggername <> "("
-			<> intercalate ", " arglist <> ") {"]
-		, map (\(fmt, n) -> CLine ("  " <> fromCLine (fmt n)))
-			(zip (map (\fo -> formatCLine fo sdn) l) argnames)
-		, [CLine "}"]
-		]
+-- | Value that is read from serial port when there is no input available.
+noInput :: Int8
+noInput = -1
+
+instance Output SerialDevice [FormatOutput] where
+	sdn =: l = sdn =: (Event l true :: Event () [FormatOutput])
+
+instance Output SerialDevice (Event () [FormatOutput]) where
+	SerialDevice sdn@(SerialDeviceName devname) =: (Event l c) =
+		tell [(go, f)]
+	  where
+		go = trigger triggername c (mapMaybe formatArg l)
+		f = mempty { defines = printer }
+
+		triggername = "arduino_serial_" <> devname <> "_output"
 	
-	argnames = map (\n -> "arg" <> Prelude.show n) ([1..] :: [Integer])
-	arglist = mapMaybe mkarg (zip (map formatCType l) argnames)
-	mkarg (Just ctype, argname) = Just (ctype <> " " <> argname)
-	mkarg (Nothing, _) = Nothing
+		printer = concat
+			[ [CLine $ "void " <> triggername <> "("
+				<> intercalate ", " arglist <> ") {"]
+			, map (\(fmt, n) -> CLine ("  " <> fromCLine (fmt n)))
+				(zip (map (\fo -> formatCLine fo sdn) l) argnames)
+			, [CLine "}"]
+			]
+		
+		argnames = map (\n -> "arg" <> Prelude.show n) ([1..] :: [Integer])
+		arglist = mapMaybe mkarg (zip (map formatCType l) argnames)
+		mkarg (Just ctype, argname) = Just (ctype <> " " <> argname)
+		mkarg (Nothing, _) = Nothing
 
+instance IsBehavior [FormatOutput] where
+	(@:) = Event
+
+type instance BehaviorToEvent [FormatOutput] = Event () [FormatOutput]
+
 data FormatOutput = FormatOutput
 	{ formatArg :: Maybe Arg
 	, formatCType :: Maybe String
@@ -183,26 +209,3 @@
 
 data Base = BIN | OCT | DEC | HEX
 	deriving (Show)
-
-inputD :: SerialDeviceName -> Input Int8
-inputD d = input'D d []
-
--- | The list is used to simulate serial input when interpreting the program.
-input'D :: SerialDeviceName -> [Int8] -> Input Int8
-input'D (SerialDeviceName devname) interpretvalues = mkInput $ InputSource
-	{ defineVar = [CLine $ "int " <> varname <> ";"]
-	, setupInput = []
-	, inputPinmode = mempty
-	, readInput = [CLine $ varname <> " = " <> devname <> ".read();"]
-	, inputStream = extern varname interpretvalues'
-	}
-  where
-	varname = "arduino_serial_" <> devname <> "_read"
-	interpretvalues'
-		| null interpretvalues = Nothing
-		| otherwise = Just interpretvalues
-
--- | Value that is read from serial port when there is no input available.
-noInput :: Int8
-noInput = -1
-
diff --git a/src/Copilot/Arduino/Library/Serial/XBee.hs b/src/Copilot/Arduino/Library/Serial/XBee.hs
--- a/src/Copilot/Arduino/Library/Serial/XBee.hs
+++ b/src/Copilot/Arduino/Library/Serial/XBee.hs
@@ -7,16 +7,16 @@
 
 module Copilot.Arduino.Library.Serial.XBee (
 	Baud(..),
+	configure,
 	device,
-	output,
 	char,
 	str,
 	show,
 	showFormatted,
 	byte,
-	input,
-	input',
 	noInput,
+	SerialDevice,
+	FormatOutput,
 	ShowableType,
 	FormatableType,
 	Base(..),
@@ -33,46 +33,36 @@
 --
 -- This must be included in your sketch if it uses XBee.
 --
--- > XBee.device pin2 pin3 (XBee.Baud 9600)
-device
+-- > XBee.configure pin2 pin3 (XBee.Baud 9600)
+configure
 	:: (IsDigitalIOPin rx, IsDigitalIOPin tx)
 	=> Pin rx -- ^ pin on which to receive serial data
-	-> Pin tx -- ^ pin on which to transfer serial data
+	-> Pin tx -- ^ pin on which to send serial data
 	-> Baud
 	-> Sketch ()
-device = deviceD dev
+configure = configureD dev
 
--- | Output to XBee.
+-- | Use this to communicate with the XBee, both input and output.
 --
--- Note that this can only be used once in a Sketch.
+-- To output to the XBee, simply connect this to a [`FormatOutput`]
+-- that describes the serial output. Note that you can only do this once
+-- in a Sketch.
 --
 -- > main = arduino $ doa
--- >    XBee.device pin2 pin3 (XBee.Baud 9600)
--- > 	b <- readfrom pin4
--- > 	n <- readvoltage a1
--- > 	XBee.output true
+-- >    XBee.configure pin2 pin3 (XBee.Baud 9600)
+-- > 	b <- input pin4
+-- > 	XBee.device =:
 -- > 		[ Serial.str "pin4:"
 -- > 		, Serial.show b
--- > 		, Serial.str " a1:"
--- > 		, Serial.show n
 -- > 		, Serial.char '\n'
 -- > 		]
-output
-	:: Stream Bool
-	-- ^ This Stream controls when output is sent to the XBee.
-	-> [FormatOutput]
-	-> Sketch ()
-output = outputD dev
-
--- | Input from the XBee.
 --
--- Reads one byte on each iteration of the sketch. When there is no
--- serial input available, reads `noInput`.
+-- To input from the XBee, use this with `input`.
 --
--- > userinput <- XBee.input
-input :: Input Int8
-input = inputD dev
-
--- | The list is used to simulate Xbee input when interpreting the program.
-input' :: [Int8] -> Input Int8
-input' = input'D dev
+-- > userinput <- input XBee.device
+--
+-- The resulting `Behavior Int8` will be updated on each iteration
+-- of the sketch. When there is no new serial input available, it will
+-- contain `noInput`.
+device :: SerialDevice
+device = SerialDevice dev
diff --git a/src/Copilot/Arduino/Nano.hs b/src/Copilot/Arduino/Nano.hs
--- a/src/Copilot/Arduino/Nano.hs
+++ b/src/Copilot/Arduino/Nano.hs
@@ -3,7 +3,6 @@
 {-# LANGUAGE DataKinds #-}
 
 module Copilot.Arduino.Nano (
-	-- * General Arduino programming infrastructure
 	module Copilot.Arduino
 	-- * Pins
 	, pin2
@@ -26,9 +25,6 @@
 	, a5
 	, a6
 	, a7
-	-- * UART
-	-- Not currently supported!
-	--, uart
 ) where
 
 import Copilot.Arduino
diff --git a/src/Copilot/Arduino/Uno.hs b/src/Copilot/Arduino/Uno.hs
--- a/src/Copilot/Arduino/Uno.hs
+++ b/src/Copilot/Arduino/Uno.hs
@@ -3,7 +3,6 @@
 {-# LANGUAGE DataKinds #-}
 
 module Copilot.Arduino.Uno (
-	-- * General Arduino programming infrastructure
 	module Copilot.Arduino
 	-- * Pins
 	, pin2
@@ -24,9 +23,6 @@
 	, a3
 	, a4
 	, a5
-	-- * UART
-	-- Not currently supported!
-	--, uart
 ) where
 
 import Copilot.Arduino
diff --git a/test.hs b/test.hs
new file mode 100644
--- /dev/null
+++ b/test.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE LambdaCase #-}
+
+import Control.Monad
+import System.FilePath
+import System.Directory
+import System.Process
+import System.Exit
+
+main = do
+	examples <- filterM doesFileExist
+		=<< (map (</> "demo.hs") . map ("examples" </>))
+		<$> getDirectoryContents "examples"
+	if null examples
+		then error "did not find any examples to test"
+		else mapM_ shouldCompile examples
+
+shouldCompile :: FilePath -> IO ()
+shouldCompile f = do
+	let p = (proc "runghc" ["-Wall", "-fno-warn-tabs", takeFileName f])
+		{ cwd = Just (takeDirectory f) }
+	readCreateProcessWithExitCode p "" >>= \case
+		(ExitSuccess, "", "") -> return ()
+		v -> error $ "runghc " ++ f ++
+			" produced unexpected result: " ++ show v
