zephyr-copilot-1.0.0: src/Copilot/Zephyr/Main.hs
{-# LANGUAGE BangPatterns #-}
module Copilot.Zephyr.Main (zephyr) where
import Language.Copilot (Spec, interpret, reify)
import Copilot.Compile.C99 (compile)
import Copilot.Zephyr.Internals
import System.IO
import System.Directory
import System.IO.Temp (withSystemTempDirectory, createTempDirectory)
import System.FilePath
import System.Exit
import Data.List (isInfixOf, intercalate)
import Data.Maybe
import qualified Data.Map as M
import qualified Data.Set as S
import Options.Applicative
-- | Typically your program's main will be implemented using this.
-- For example:
--
-- > {-# LANGUAGE RebindableSyntax #-}
-- >
-- > import Copilot.Zephyr
-- >
-- > main = zephyr $ do
-- > led0 =: flashing
-- > delay =: MilliSeconds (constant 100)
--
-- Running this program compiles the `Sketch` into C code using copilot
-- and writes it to a file. That can be built and uploaded to your board
-- using Zephyr.
--
-- This also supports interpreting a `Sketch`, without loading it onto a
-- board. Run the program with parameters "-i 4" to display what it
-- would do on the first 4 iterations. The output will look something like
-- this:
--
-- > delay: digitalWrite:
-- > (100) (13,false)
-- > (100) (13,true)
-- > (100) (13,false)
-- > (100) (13,true)
zephyr :: Sketch () -> IO ()
zephyr s = go =<< execParser opts
where
opts = info (parseCmdLine <**> helper)
( fullDesc
<> progDesc "Run this program with no options to generate an Zephyr program."
)
go o = case (mspec, interpretSteps o) of
(Nothing, _) -> do
hPutStrLn stderr "This Sketch does not do anything."
exitFailure
(Just spec, Just n) -> interpret n spec
(Just spec, Nothing) -> do
let name = "generated"
writeMain name spec f'
writePrjConf name kconfigs
writeAppOverlay name devicetree
writeCMakeLists name
(mspec, f) = evalSketch s
-- Strict evaluation because this may throw errors,
-- and we want to throw them before starting compilation.
!(f', kconfigs, devicetree) = finalizeFramework f
data CmdLine = CmdLine
{ interpretSteps :: Maybe Integer
}
parseCmdLine :: Parser CmdLine
parseCmdLine = CmdLine
<$> optional (option auto
( long "interpret"
<> short 'i'
<> help "use copilot to interpret the program, displaying what it would do"
<> metavar "NUM"
))
writeMain :: String -> Spec -> Framework -> IO ()
writeMain name spec framework = do
-- This could be a lot prettier, unfortunately copilot only exports
-- an interface that writes the generated code to a file.
-- And, the .c file includes a .h file that will make it fail to
-- build, so that include has to be filtered out.
withSystemTempDirectory "copilot" $ \toptmpdir -> do
mytmpdir <- createTempDirectory toptmpdir "copilot"
reify spec >>= compile (mytmpdir </> "copilot")
c <- lines <$> readFile (mytmpdir </> "copilot.c")
let c' = filter (Prelude.not . isInfixOf "#include \"") c
createDirectoryIfMissing False name
writeFile (name </> "main.c") $
unlines $ map fromCLine $
sketchFramework framework (map CLine c')
writeCMakeLists :: String -> IO ()
writeCMakeLists name = do
createDirectoryIfMissing False name
writeFile (name </> "CMakeLists.txt") $ unlines
[ "cmake_minimum_required(VERSION 3.20.0)"
, "find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})"
, "project(" ++ name ++ ")"
, ""
, "target_sources(app PRIVATE main.c)"
]
data KConfig = KConfig String
writePrjConf :: String -> [KConfig] -> IO ()
writePrjConf name kconfigs = do
createDirectoryIfMissing False name
writeFile (name </> "prj.conf") $ unlines $
map (\(KConfig s) -> s) kconfigs
newtype DeviceTree = DeviceTree String
writeAppOverlay :: String -> DeviceTree -> IO ()
writeAppOverlay name (DeviceTree s) = do
createDirectoryIfMissing False name
writeFile (name </> "app.overlay") s
-- Makes an Zephyr C program, using a Framework, and a list of lines of C
-- code generated by Copilot.
sketchFramework :: Framework -> [CLine] -> [CLine]
sketchFramework f ccode = map CLine $ concat
[
[ "/* automatically generated, do not edit */"
, blank
, "#include <zephyr.h>"
, blank
]
, map fromCLine (fromchunks (defines f))
, [blank]
, map fromCLine ccode
, [blank]
,
[ "void main(void)"
]
, codeblock $ concat
[ map fromCLine (fromchunks (earlySetups f))
, map fromCLine (fromchunks (setups f))
, ["while (1)"]
, codeblock $ concat
[ map fromCLine (fromchunks (loops f))
, [ "step();" ]
]
]
]
where
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, [KConfig], DeviceTree)
finalizeFramework f =
let (pindefines, pinsetups) = unzip $ mapMaybe setuppin
(M.toList (pinmodes f))
includes = if not (null (concat pindefines))
then mkCChunk
[ CLine "#include <device.h>"
, CLine "#include <devicetree.h>"
, CLine "#include <drivers/gpio.h>"
]
else []
f' = f
{ defines = includes <> concat pindefines <> defines f
, setups = concat pinsetups <> setups f
}
kconfigs = if not (null (concat pindefines))
then [KConfig "CONFIG_GPIO=y"]
else []
devicetree = DeviceTree $ unlines $
concatMap mkdevicetree (M.keys (pinmodes f))
in (f', kconfigs, devicetree)
where
setuppin (Zephyr (GPIOAlias n) _, s)
| s == S.singleton OutputMode = Just $
( definepin n
, setmode n (ored ["GPIO_OUTPUT", gpioflags n])
)
| s == S.singleton InputMode = Just $
( definepin n
, setmode n (ored ["GPIO_INPUT", gpioflags n])
)
| s == S.singleton InputPullupMode = Just $
( definepin n
, setmode n (ored ["GPIO_PULL_UP", gpioflags n])
)
| s == S.fromList [InputMode, InputPullupMode] = Just $
( definepin n
, setmode n (ored ["GPIO_INPUT", "GPIO_PULL_UP", gpioflags n])
)
| S.null s = Nothing
| otherwise = error $
"The program uses pin " ++ show n ++
" in multiple ways in different places (" ++
unwords (map show (S.toList s)) ++ "). " ++
"This is not currently supported by zephyr-copilot."
definepin n = mkCChunk
[ CLine $ "#define " <> gpionode n <> " DT_ALIAS(" <> n <> ")"
, CLine $ "#if DT_NODE_HAS_STATUS(" <> gpionode n <> ", okay)"
, CLine $ "#define " <> gpiolabel n
<> " DT_GPIO_LABEL(" <> gpionode n <> ", gpios)"
, CLine $ "#define " <> gpiopin n
<> " DT_GPIO_PIN(" <> gpionode n <> ", gpios)"
, CLine $ "#define " <> gpioflags n
<> " DT_GPIO_FLAGS(" <> gpionode n <> ", gpios)"
, CLine $ "#else"
, CLine $ "#error \"Unsupported board: devicetree alias is not"
<> " defined for " <> n <> "\""
, CLine $ "#define " <> gpiolabel n <> ""
, CLine $ "#define " <> gpiopin n <> " 0"
, CLine $ "#define " <> gpioflags n <> " 0"
, CLine $ "#endif"
, CLine $ "const struct device *" <> pinDevVar n <> ";"
]
setmode n v = mkCChunk
[ CLine $ pinDevVar n <> " = device_get_binding("
<> gpiolabel n <> ");"
, CLine $ "gpio_pin_configure("
<> pinDevVar n <> ", "
<> gpiopin n <> ", "
<> v <> ");"
]
gpiopin n = pinDevDef n
gpiolabel n = pinDevDef (n <> "_LABEL")
gpioflags n = pinDevDef (n <> "_FLAGS")
gpionode n = pinDevNode n
ored = intercalate " | "
mkdevicetree (Zephyr _ GPIOAddressBuiltIn) = []
mkdevicetree (Zephyr (GPIOAlias n) (GPIOAddress addr)) =
[ "/ {"
, "\tmypins {"
, "\t\tcompatible = \"gpio-keys\";"
, "\t\t" ++ n ++ ": " ++ n ++ "{"
, "\t\t\tgpios = <&" ++ addr ++ " 0>;"
, "\t\t\tlabel = \"" ++ n ++ "\";"
, "\t\t};"
, "\t};"
, "\taliases {"
, "\t\t" ++ map toalias n ++ " = &" ++ n ++ ";"
, "\t};"
, "};"
]
where
toalias '_' = '-'
toalias c = c