Hi all
I’m working on a library for KNX, a building automation protocol. In KNX, devices exchange binary encoded messages on a bus. The messages contain a payload, and the specific payload has to be encoded/decoded based on its type (called datapoint type, or DPT for short). DPTs are identified by their ID, which might look like “1.001”, “9.020”, etc.
The KNX spec itself has 300+ DPTs, and it allows for vendors to create their own proprietary DPTs. Because of this, one of the design goals of my library is to make it possible for users of the library to define their own DPTs.
When a message comes in from the bus, it’s adressed to some logical address (e.g. “2/2/10”). Depending on the bus configuration, that address might use DPT 9.020 for its payload. To decode it, I need to lookup the decode function for DPT 9.020. And since the user may have defined the type for this DPT, the lookup must be dynamic.
I have a solution for this which works, but I’d like to check with the community whether there are better ideas out there. My solution looks like this:
-
All DPTs have their own module, which must implement the KNX.Datapoint.Type behaviour. It looks like this:
defmodule KNX.Datapoint.Type do @callback id() :: String.t() # returns the DPT ID, e.g. "9.020" @callback decode(bitstring()) :: {:ok, any()} @callback encode(any()) :: {:ok, bitstring()} @callback to_string(any()) :: String.t() end
-
A special compiler runs after all the other compilers. The compiler will
- Loop through all .beam files and find all modules implementing the KNX.Datapoint.Type behaviour (using this code from bitwalker)
- Define a module KNX.Datapoint.Types, and for each implementation found, generate a function clause for
by_id(String.t()) :: atom()
, e.g.def by_id("9.020"), do: KNX.Datapoint.Type.Value_Temp
This works, but since it has its own compiler, it means the users of the library will have to add this compiler to their mix.exs file:
defmodule MyApp.Mixfile do
...
def project() do
[
...
compilers: Mix.compilers() ++ [:knx],
...
]
end
...
end
I find that sort of clunky. I’ve never had to do it before (I’ve noticed Phoenix uses it, but they have their own generator which means users don’t have to think about it). I’ve tried adding the compiler in the library, but that makes compiling fail if users define their own types.
I guess another option is to compile the lookup table when the app boots. That would hide it for the user, but when things don’t work that would be a horrible user experience.
So what do you think? Is this a crazy idea? Is there something more straight-forward that could be done?