Is there a more elegant way to write this?

I’ve got predicates which handle the initialization of a specific device so the declaration of that predicate includes the device ID (happens to be a MAC address). So while the predicate declaration line hardcodes the id it feels very redundant to keep writing out that id in subsequent lines. Is there a better way of coding this?

neopixel_init('5c:cf:7f:00:1c:87'):-
   send_neopixel_setspeed('5c:cf:7f:00:1c:87', 5000),
  send_neopixel_setbrightness('5c:cf:7f:00:1c:87',50),
  send_neopixel_mode('5c:cf:7f:00:1c:87', 14),
  send_neopixel_cmd('5c:cf:7f:00:1c:87','start').

Not sure what the various objects are, but roughly I’d think about something along these lines:

init :-
    neopixel(MacAdr),
    something_init(MacAdr).

something_init(MacAdr) :-
    something_setspeed(MacAdr, 5000),
    something_setbrightness(MacAdr,50),
    somethingo_mode(MacAdr, 14),
    something_cmd(MacAdr,'start').

neopixel('5c:cf:7f:00:1c:87').

How about this:

neopixel_device_id('5c:cf:7f:00:1c:87').

neopixel_init(DeviceId):-
  send_neopixel_setspeed(DeviceId, 5000),
  send_neopixel_setbrightness(DeviceId, 50),
  send_neopixel_mode(DeviceId, 14),
  send_neopixel_cmd(DeviceId, 'start').

and invoke it by: neopixel_device_id(DeviceId), neopixel_init(DeviceId).

Seems we agree quite closely :slight_smile:

I might have misstated the requirement. That list of neopixel initialization instructions is specific to that particular device id, the instructions for a different id would be completely different and I’d create a different predicate for them with their device id as the matching value. I’ve used the approach in the replies in other places, but it’s not what I’m going for here.

Only when I’m correct. :wink:

A more complete view of what I’m trying to do is here. The entry point is the message predicate which gets called as a result of an incoming MQTT message and the first parameter is a list of the topic segments and the second is the actual message body (not used here). So I’m trying to code the unique initialization requirements of that specific device.

message(['Smarthome','Notify', 'neopixel', 'start', NeopixelId],_):-
   neopixel_init(NeopixelId).

neopixel_init('cc:50:e3:17:6a:1e'):-
    send_neopixel_setbrightness('cc:50:e3:17:6a:1e',50),
    send_neopixel_setsegment('cc:50:e3:17:6a:1e',0,0,0,11,0xffff00,8000,'F'),
    send_neopixel_setsegment('cc:50:e3:17:6a:1e',1,1,8,42,0x0000ff,1000,'F'),
    send_neopixel_setsegment('cc:50:e3:17:6a:1e',2,9,20,42,0x00ffff,1500,'T'),
    send_neopixel_setsegment('cc:50:e3:17:6a:1e',3,21,36,42,0x00ff00,2000,'F'),
    send_neopixel_setsegment('cc:50:e3:17:6a:1e',4,37,60,42,0xffff00,4000,'T'),
    send_neopixel_setsegment('cc:50:e3:17:6a:1e',5,61,92,42,0xff0000,6000,'F'),
    send_neopixel_cmd('cc:50:e3:17:6a:1e','start').

neopixel_init('5c:cf:7f:00:1c:87'):-
   send_neopixel_setspeed('5c:cf:7f:00:1c:87', 5000),
   send_neopixel_setbrightness('5c:cf:7f:00:1c:87',50),
   send_neopixel_mode('5c:cf:7f:00:1c:87', 14),
   send_neopixel_cmd('5c:cf:7f:00:1c:87','start').
neopixel_init(DeviceId) :- 
   DeviceId = '5c:cf:7f:00:1c:87',
   !,
   send_neopixel_setspeed(DeviceId, 5000),
   send_neopixel_setbrightness(DeviceId, 50),
   send_neopixel_mode(DeviceId, 14),
   send_neopixel_cmd(DeviceId,'start').

?

This has the disadvantage that there’s no argument indexing, and it needs the ugly cut to avoid a choicepoint, but performance probably isn’t an issue for you during initialization unless you have thousands of these devices.

OK thanks. I guess I wasn’t missing anything obvious.
BTW, the initialization for ‘cc:50:e3:17:6a:1e’ is the code for the animated Christmas tree I built:

3 Likes

The loss of clause indexing for this code is a missing feature of SWI-Prolog. In the end, the original code is the most efficient, just not comfortable to write. The trick with Prolog is then to invent any cute term you really would like to write and a rule for term_expansion/2 to generate the efficient code. That could be something like

init('5c:cf:7f:00:1c:87', [set_speed(5000), setbrightness(50), mode(14), cmd(start)]).

Note that you generally can also interpret the above fact. For your purposes that is probably efficient enough.

neopixel_init('5c:cf:7f:00:1c:87') :- 
    maplist(
        cmd(  '5c:cf:7f:00:1c:87'),
        [send_neopixel_setspeed(5000),
         send_neopixel_setbrightness(50),
         send_neopixel_mode(14),
         send_neopixel_cmd('start')]).

cmd(DeviceId, CmdAndArgs) :-
    CmdAndArgs =.. [Cmd|Args],
    Call =.. [Cmd,DeviceId|Args],
    Call.

Jan and I almost agree, but Jan’s answer is better. :grin:

The funny thing is that we’re kinda going in circles because what those send_neopixel* predicates actually do is to create a Prolog dictionary and then convert that to JSON which is the payload of the MQTT message:

send_neopixel_setspeed(NeoPixelId, S):-
   send_neopixel_cmd(NeoPixelId,"setSpeed",_{s:S}).

send_neopixel_cmd(NeoPixelId, Function, Args):-
   atom_json_dict(Json, [_{f:Function, args:Args}] ,[]),
   publish(['Smarthome','neopixel',NeoPixelId], Json).
1 Like

The code for my LCD and OLED displays is similar, but combines multiple operations into one message like this:

display_oled_vehicle_json(Json, Name, Timestamp):-
   atom_json_dict(Json,[
                     _{f:"clear"},
                     _{f:"setTextAlignment", args:_{textAlignment:"CENTER"}},
                     _{f:"setColor", args:_{color:"WHITE"}},
                     _{f:"setFont", args:_{font:"ArialMT_Plain_16"}},
                     _{f:"drawString", args:_{x:64, y:0, text:"Recognized"}},
                     _{f:"drawString", args:_{x:64, y:20, text:Name}},
                     _{f:"drawString", args:_{x:64, y:40, text:Timestamp}},
                     _{f:"display"},
                     _{f:"setBlankTime", args:_{seconds:18000}}
                   ],[]).

This makes this possible:

2 Likes

How about replacing each of the 5 ‘…’ constants with the same variable?

(Also, I don’t understand why you have such a constant in the head pattern.)

@barb, I think your question is addressed in the reply above that starts " A more complete view of what I’m trying to do is here."

Cute! The overall way of thinking in Prolog is to first model your domain using Prolog terms in such a way that it feels comfortable and neat. That is, without worrying about how to process all this. So, you describe your devices and their properties such as their name, mac address, etc. You typically organise them in classes, so you can describe common properties if you have more (almost) identical devices.

Only after that you start worrying how you send the messages around and make it all work. In many cases you can just write predicates that use the data. That may be slow (for IoT rarely an issue), in which case you compile the data to an efficient program. If that too is too slow you compile it to a C program and link that, etc.

1 Like

It is not clear to me if all devices are unique or if there are also several instances of the same device. But in both cases, I usually start by defining a protocol for the devices interface. In this case, one of the main predicates would be init/0:

:- protocol(device).

    :- public(init/0).

:- end_protocol.

If a device is unique, it can be represented by a prototype that implements the device protocol. For example:

:- object('5c:cf:7f:00:1c:87', implements(device)).

    init :-
        this(MAC),  % this goal is inlined
        neopixel::(
            setspeed(MAC, 5000),
            setbrightness(MAC, 50),
            mode(MAC, 14),
            cmd(MAC, 'start')
        ).

:- end_object.

Assuming that all neopixel public predicates take a MAC address would allow us to simplify to:

:- object('5c:cf:7f:00:1c:87', implements(device)).

    init :-
        this(MAC),  % this goal is inlined
        neopixel(MAC)::(
            setspeed(5000),
            setbrightness(50),
            mode(14),
            cmd('start')
        ).

:- end_object.

If there are several instances of the same device, let’s say, lamp, that only differ on the parameters, I would use instead a parametric object:

:- object(lamp(_MAC_,_Speed_,_Brightness_,_Mode_,_Cmd_), implements(device)).

    init :-
        neopixel(_MAC_)::(
            setspeed(_Speed_),
            setbrightness(_Brightness_),
            mode(_Mode_),
            cmd(_Cmd_)
        ).

:- end_object.

And then define a Prolog table with facts (parametric object proxies) for each lamp:

% lamp(Mac, Speed, Brightness, Mode, Cmd)
lamp('5c:cf:7f:00:1c:87', 5000, 50, 14, 'start').
lamp('cc:50:e3:17:6a:1e', 7500, 35, 42, 'start').
...

I can then e.g. initialize all lamps by calling:

?- {lamp(_,_,_,_,_)}::init, fail; true.

Alternatively:

?- forall(lamp(MAC,S,B,M,C), lamp(MAC,S,B,M,C)::init).

We can also initialize just a specific device given its MAC address:

?- {lamp('5c:cf:7f:00:1c:87',_,_,_,_)}::init.

If multiple devices share all but a few of the parameterizations, we can also due for example:

:- object(lamp_mode(MAC, Mode), extends(lamp(MAC,5000,50,Mode,'start')).

:- end_object.


lamp_mode('5c:cf:7f:00:1c:87', 14).
lamp_mode('cc:50:e3:17:6a:1e', 42).
...

By now, you can see there are several common points between this structured solution and other suggestions on this discussion thread. Some of the advantages of this structured approach is that it easily scales to a large number of devices where we can have a mix of unique devices and multiple instances of the same devices with just different parameterizations and taxonomies of devices where features are inherited.

How about replacing each of the 5 ‘…’ constants with the same variable?

(Also, I don’t understand why you have such a constant in the head pattern.)

I would try to compress problem syntax, just a ‘primary key’ packing of code, from left to right:

neopixel_ID('5c:cf:7f:00:1c:87').
neopixel_init(ID):- neopixel_ID(ID), maplist({ID}/[F=V]>>call(F,ID,V),[
  send_neopixel_setspeed=5000,
  send_neopixel_setbrightness=50,
  send_neopixel_mode=14,
  send_neopixel_cmd='start']).

maplist compresses a lot the statement, common arguments are abstracted away.
And a bit more:

neopixel_init(ID):- neopixel_ID(ID), maplist({ID}/[F=V]>>(
   atom_concat('send_neopixel_',F,G),call(G,ID,V)
 ),[
  setspeed=5000,
  setbrightness=50,
  mode=14,
  cmd='start']).

Or, up to point on the request:

neopixel_init('5c:cf:7f:00:1c:87'):- maplist([F=V]>>(
   atom_concat(send_neopixel_,F,G),
   ( is_list(V) -> As = V ; As = [V] ),
   C=..[G,'5c:cf:7f:00:1c:87'|As],
   call(C)
 ),[
  setspeed=5000,
  setbrightness=50,
  mode=14,
  setsegment=[1,1,8,42,0x0000ff,1000,'F'],
  cmd=start]).

To debug, simply wrap last call in writeln, like this

   C=..[G,'5c:cf:7f:00:1c:87'|As],
   writeln(call(C))

and ‘run’ it:

?- uty:neopixel_init('5c:cf:7f:00:1c:87').
call(send_neopixel_setspeed(5c:cf:7f:00:1c:87,5000))
call(send_neopixel_setbrightness(5c:cf:7f:00:1c:87,50))
call(send_neopixel_mode(5c:cf:7f:00:1c:87,14))
call(send_neopixel_setsegment(5c:cf:7f:00:1c:87,1,1,8,42,255,1000,F))
call(send_neopixel_cmd(5c:cf:7f:00:1c:87,start))

Make reusable, just a bit of cleanup to generalize:

neopixel_init('5c:cf:7f:00:1c:87'):-
    maplist(pfix_call_addr_args(send_neopixel_,'5c:cf:7f:00:1c:87'),
           [setspeed=5000
           ,setbrightness=50
           ,mode=14
           ,setsegment=[1,1,8,42,0x0000ff,1000,'F']
           ,cmd=start]).

the uty itself:

:- module(uty,
          [mapdict/2
          ,pfix_call_addr_args/3
          ]).

% use only if keys order is unimportant
mapdict(F,D) :-
    dict_pairs(D,Tag,Ps), % Tag is useless here
    maplist({F,Tag}/[K-V]>>call(F,Tag,K,V), Ps).

% generalize and make simpler moving here all args business
pfix_call_addr_args(Pfix,Addr,F=V) :-
    atom_concat(Pfix,F,G),
    ( is_list(V) -> As = V ; As = [V] ),
    C=..[G,Addr|As],
    writeln(call(C)).

Ciao, Carlo