4

My work is mostly in embedded systems programming in C, and the proper class structure to pull this off eludes me. Currently we communicate via C# and Visual Basic with a large collection of servos, pumps, and sensors via a USB-to-CAN hid device.

Right now, it is quite cumbersome to communicate with the devices. To read the firmware version of controller number 1 you would use:

SendCan(Controller,1,ReadFirmwareVersion) or
SendCan(8,1,71)

This sends three bytes on the CAN bus: (8,1,71)

Connected to controllers are various sensors.

SendCan(Controller,1,PassThroughCommand,O2Sensor,2,ReadO2)

would tell Controller number 1 to pass a command to O2 Sensor number 2 to read O2 by sending the bytes 8,1,200,16,2,0

I would like to develop a domain-specific language for this setup. Instead of commands issued like they are currently, commands would be written like this:

Controller1.SendCommand.O2Sensor2.ReadO2

to send the bytes 8,1,200,16,0

What's the best way to do this? Some machines have 20 O2 Sensors, others have 5 controllers, so the numbers and types of controllers and sensors, pumps, etc. aren't static.

5 Answers 5

2

I would prefer to write

machine1.Controllers[1].O2Sensors[2].ReadO2();

There is no need to have a SendCommand in there.


UPDATE

Here is my suggestion for a controller class

public class Controller
{
    private const int ReadFirmwareVersionCmd = 71;
    private const int PassThroughCommandCmd = 200;

    private int _controllerNumber;

    public Controller(int controllerNumber)
    {
        _controllerNumber = controllerNumber;
        O2Sensors = new List<O2Sensor>();
    }

    public List<O2Sensor> O2Sensors { get; private set; }

    private void SendControllerCommand(int controllerCommand)
    {
        Controller.SendCan(8, _controllerNumber, controllerCommand);
    }

    public void SendSensorCommand(int sensorType, int sensorNo, int sensorCommand)
    {
        Controller.SendCan(8, _controllerNumber, PassThroughCommandCmd, sensorType, sensorNo, sensorCommand);
    }

    private static void SendCan(int deviceType, int deviceNo, int commandNo)
    {
        // ...
    }

    private static void SendCan(int deviceType, int deviceNo, int commandNo,
                                int sensorType, int sensorNo, int sensorCommand)
    {
        // ...
    }

    public void ReadFirmwareVersion( )
    {
        SendControllerCommand(ReadFirmwareVersionCmd);
    }
}

I would derive all the sensor types from a common base

public abstract class SensorBase
{
    protected Controller _controller;
    protected int _sensorNo;

    public SensorBase(Controller controller, int sensorNo)
    {
        _controller = controller;
        _sensorNo = sensorNo;
    }

    public abstract void Read();
}

An O2-sensor as an example for a sensor

public class O2Sensor : SensorBase
{
    public O2Sensor(Controller controller, int sensorNo)
        : base(controller, sensorNo)
    {
    }

    public override void Read()
    {
        _controller.SendSensorCommand(16, _sensorNo, 0);
    }
}

You can initialize a controller like this

var controller1 = new Controller(1);
controller1.O2Sensors.Add(new O2Sensor(controller1, 1));
controller1.O2Sensors.Add(new O2Sensor(controller1, 2));

Now you can read information like this

controller1.ReadFirmwareVersion();
controller1.O2Sensors[1].Read();
5

Your thinking is still too low-level. You're looking for a better way to say things like "What's the value of O2 detected by O2 Sensor #2?" But why do you need to know that value? What are you going to do on the basis of the value?

If you just want a wrapper that's a bit less cumbersome, sure, write that. But if you want a genuine domain-specific language, you need to start by thinking much more high-level. You also need to consider making objects that are more capable.

Let's say you need to show an alert if the O2 level is too low. Simple:

if (new Controller().O2Sensors[1].Value < 30)
    MessageBox.Show("The canary has died.");

Simple, but not good. To get the most flexible OO design, you should be telling objects what to do rather than querying them:

new Controller().ShowAlertIfMainOxygenLevelTooLow();

Then the Controller itself knows which oxygen sensor is the main one, and it knows what "too low" means.

But what if "too low" differs by environment? Maybe your controller might be used on a space station, and the O2 environment is richer, and even a small drop indicates trouble. In that case you'd need to construct your Controller with an appropriate strategy, something like:

public class Controller
{
    public Controller(IOxygenLevelDetector oxygenLevelDetector)
    {
        [...]
    }
}

Your IOxygenLevelDetector interface might have a method that takes the O2 sensor, reads the current O2 level, and returns a bool indicating whether the level is too low, or whether the level is in a success or failure state...whatever makes the most sense to you. You'd construct your Controller with the appropriate environment:

new Controller(new SpaceStation())

or

new Controller(new RoomOnEarth())

Of course, at some point you're still reading the oxygen level and then doing something. How do you know when it's the wrong place to do that? A good rule of thumb is to minimize dots. Something like this

new ServiceBoard().Controllers[5].O2Sensors[1].OxygenGauge.Value

is too many dots. The ServiceBoard has too much visibility into the internals of a lot of other objects. All it should do is go one level: Tell a Controller to do something, and wait for the reply. The Controller, in turn, should tell an O2Sensor to do something, and should wait for the reply. In this way, even when you have to make changes, you avoid having them ripple through the entire program.

3
  • +1! for "..you should be telling objects what to do ..". And "..thinking too low level.." Finally, kudos for the "minimize dots" heuristic. Nicely explained.
    – radarbob
    Apr 6, 2012 at 20:30
  • +1 for suggesting he think outside the low level box he's in and for the "why" and "what" to guide the process. Apr 6, 2012 at 22:13
  • +1 for the higher level idea. Probably it would be the best to have a low level wrapper for the communication stuff PLUS an interface that defines high-level logic. These are two abstraction levels that deserve to be treated separately. Apr 7, 2012 at 21:22
3

Why not do something like this:

Controller.At(1).SendCommand().O2Sensor.At(2).ReadO2();

That way you can accommodate for all of the controllers, all of the O2 sensors and it is still pretty clear.

5
  • Even better, you can use indexers: Controllers[1].SendCommand().O2Sensors[2].ReadO2().
    – svick
    Apr 6, 2012 at 16:53
  • @svick it is a valid option, but to me, it hurts the "flow". I guess I just prefer the smoothness of a parenthesis rather than the angling of a bracket
    – Jetti
    Apr 6, 2012 at 16:56
  • If you put your SendCommand() at the end, you can collect the information you need before you actually send the command. Your fluent interface, as written, seems like it will be difficult to implement, since your SendCommand() doesn't yet know what it's supposed to do. Apr 6, 2012 at 18:02
  • @RobertHarvey, I think SendCommand() corresponds to the PassThroughCommand constant (200). I think it wouldn't be hard to implement this way: ReadO2() would actually send the command, because it knows nothing can follow it.
    – svick
    Apr 6, 2012 at 18:20
  • @RobertHarvey I was just following what the OP had, since the OP knows the domain and I don't. It may indeed make sense to put SendCommand() at the end but I don't know.
    – Jetti
    Apr 6, 2012 at 19:53
3

That's still too verbose for my taste, because of the lack of encapsulation. I don't know much about your topology, or the overlap of commands between sensors, but my first choice for an API for your examples would simply be:

ReadFirmwareVersion(1);
ReadO2(1, 2);

If you needed it to be more object-oriented, and not have to pass around the controller and device addresses all the time, I would make it look something like this:

controller = new Controller(1);
controller.readFirmwareVersion();
o2Sensor = new O2Sensor(controller, 2);
o2Sensor.readO2();

No need for a DSL here. You should be able to simplify the API within the confines of your existing language.

1
  • Thanks. Quite helpful. I think this is a long term goal to reach. Right now I'm looking at something that technicians can use... I want them to know they are talking to O2 sensor 2 on controller 1 and be able to easily script simple functions. For the overall, autonomous function of the machine I agree this would be a good direction to go. Apr 7, 2012 at 0:11
1

They way most folks implement fluent-style interfaces is to first implement a standard object sort of interface then wrap things in interfaces with extension methods to get to fluent.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.