Mapping JSON to Prolog compounds

I’m using: SWI-Prolog version 8.1.17

I’m trying to accept JSON and convert it to Prolog terms in a convenient form.

sample_json('{
   "device_location":"st7735",
   "hostname":"LCDst7735-d1e72",
   "ip_addr":"192.168.0.148",
   "version":"Dec 24 2019 13:58:08",
   "mac_addr":"4c:11:ae:0d:1e:72",
   "arduino_board":"ESP8266_WEMOS_D1MINI",
   "mqtt":{
      "mqtt_server":"mqtt.geekster.com",
      "mqtt_port":1883,
      "mqtt_sub_topic":"Smarthome/lcd/4c:11:ae:0d:1e:72",
      "mqtt_log_topic":"Smarthome/Log/mqtt"
   },
   "lcd":{
      "display_type":"st7735",
      "width":128,
      "height":128,
      "rotation":0,
      "color_depth":"5:6:5",
      "fonts":[
         {"index":9,"fontname":"FreeSans9pt7b"},
         {"index":12,"fontname":"FreeSans12pt7b"},
         {"index":18,"fontname":"FreeSans18pt7b"},
         {"index":24,"fontname":"FreeSans24pt7b"}
      ]
   }}
').

When I use atom_json_dict(Json,Dict,[]) on the above, the fonts section translates to:

sample_fontslist([
      _{fontname:"FreeSans9pt7b",index:9},
      _{fontname:"FreeSans12pt7b",index:12},
      _{fontname:"FreeSans18pt7b",index:18},
      _{fontname:"FreeSans24pt7b",index:24}
   ]).

And if I do:

sample_fontslist(L), dicts_to_compounds(L, [index, fontname], dict_fill(null),C).

I get:

C = [row(9, "FreeSans9pt7b"), row(12, "FreeSans12pt7b"), row(18, "FreeSans18pt7b"), row(24, "FreeSans24pt7b")].

This is REALLY close to what I want, but instead of “row” as the name I’d like “font”:

[font(9, "FreeSans9pt7b"), font(12, "FreeSans12pt7b"), font(18, "FreeSans18pt7b"), font(24, "FreeSans24pt7b")]

When I look at the source code for dicts_to_compounds there is no option to specify the default_tag as font instead of row.

So I’m wondering what’s the most appropriate way of fixing this. Should I do all of the above and then do a translation from row to font at the end, or was there something I could have done at the atom_json_dict stage to map the anonymous objects to fonts?

2 Likes

Quick answer, did not test, have never done JSON to Prolog, however as a general case of loading data that is close but not quite there, which is what you have, I would not tinker with the code that is working but take the option to just change the data as needed with something like

font(N,Font_name) :-
   row(N,Font_name),
   font_name(Font_name).

font_name("FreeSans9pt7b").
font_name("FreeSans12pt7b").
...

The reason for adding the line

font_name(Font_name)

is to ensure that only rows that have valid fonts are converted.

Again, not tested, but should give you an idea.

Have a nice holiday. :smiley:

Just noticed, how nice.

1 Like

More info on what I’m up to. I’ve got ESP8266/ESP32 devices controlling LCD, OLED, and Neopixel “display” devices and broadcasting their config via MQTT. I’ve got Prolog listening to those messages and trying to keep a dynamic configuration of the available devices and storing it using the Prolog persistence library.
So later when Prolog needs to display something at a location it’ll be able to match against the asserted display facts:

display(mac:string, hostname:string, device_location:string, mqtt_topic:string, ip_addr:string, tech:any)

And adjust for display type, color capabilities, resolution, and available fonts, then send out the appropriate JSON message via MQTT to display the message. The only reason I actually have ip_addr in there is because to display images I use code that does a HTTP POST to send the image data, for text and drawing I use MQTT.

I thought about doing a data transform like you mentioned, but in this case I’m really trying to transform a list of componds which will then be asserted as part of the persistence library.

So the ultimate fact in the database might look like:

display(
   "4c:11:ae:0d:1e:72",
   "LCDst7735-d1e72",
   "st7735",
   "Smarthome/lcd/4c:11:ae:0d:1e:72",
   "192.168.0.148",
   lcd(128,128, [
      font(9,"FreeSans9pt7b"),
      font(12,"FreeSans12pt7b"),
      font(18, "FreeSans18pt7b"),
      font(24,"FreeSans24pt7b")
   ])
).

@sprior I’ve edited the below because I figured out the solution without using maplist, which is to add default_tag(font) to atom_json_dict’s Options list as in:

...
atom_json_dict(Json, Dict, [default_tag(font)]),
...

This is my first encounter with dicts_to_compounds/4, so thanks for broadening my SWI Prolog vocabulary.

The documentation provides a clue: “When converting from dict to row and the dict has no tag, the functor row is used.”

Here’s how I’ve done it

:- use_module(library(http/json)).

sample_json('{
   "device_location":"st7735",
   "hostname":"LCDst7735-d1e72",
   "ip_addr":"192.168.0.148",
   "version":"Dec 24 2019 13:58:08",
   "mac_addr":"4c:11:ae:0d:1e:72",
   "arduino_board":"ESP8266_WEMOS_D1MINI",
   "mqtt":{
      "mqtt_server":"mqtt.geekster.com",
      "mqtt_port":1883,
      "mqtt_sub_topic":"Smarthome/lcd/4c:11:ae:0d:1e:72",
      "mqtt_log_topic":"Smarthome/Log/mqtt"
   },
   "lcd":{
      "display_type":"st7735",
      "width":128,
      "height":128,
      "rotation":0,
      "color_depth":"5:6:5",
      "fonts":[
         {"index":9,"fontname":"FreeSans9pt7b"},
         {"index":12,"fontname":"FreeSans12pt7b"},
         {"index":18,"fontname":"FreeSans18pt7b"},
         {"index":24,"fontname":"FreeSans24pt7b"}
      ]
   }}
').

getfonts :-
    sample_json(Json), 
    atom_json_dict(Json, Dict, [default_tag(font)]),
    dicts_to_compounds(Dict.lcd.fonts, [index, fontname], dict_fill(null), Fonts),
    print(Fonts).

Which produces

[font(9, "FreeSans9pt7b"), font(12, "FreeSans12pt7b"), 
 font(18, "FreeSans18pt7b"), font(24, "FreeSans24pt7b")].
2 Likes

Thanks! I wasn’t aware of that option for atom_json_dict. It’s possible that this isn’t the best place to fix this because it changes all the anonymous compounds generated from the json. The result of atom_json_dict is now:

Dict = 
font{
   arduino_board:"ESP8266_WEMOS_D1MINI",
   device_location:"st7735",
   hostname:"LCDst7735-d1e72",
   ip_addr:"192.168.0.148",
   lcd:font{
      color_depth:"5:6:5",
      display_type:"st7735",
      fonts:[
         font{fontname:"FreeSans9pt7b", index:9},
         font{fontname:"FreeSans12pt7b", index:12},
         font{fontname:"FreeSans18pt7b", index:18},
         font{fontname:"FreeSans24pt7b", index:24}],
      height:128,
      rotation:0,
      width:128},
   mac_addr:"4c:11:ae:0d:1e:72",
   mqtt:font{
      mqtt_log_topic:"Smarthome/Log/mqtt",
      mqtt_port:1883,
      mqtt_server:"mqtt.geekster.com",
      mqtt_sub_topic:"Smarthome/lcd/4c:11:ae:0d:1e:72"},
   version:"Dec 24 2019 13:58:08"
}.

Now these aren’t that much of a problem in terms of getting the code to work because I was ignoring those other names anyway, but it does feel incorrect. It also solves the font list problem, but if in the future I needed to add another list to the structure I’d be back to basically the original problem.

So I think a more correct approach might be to do the intial atom_json_dict, get the fonts list, then run a transform on the list, or run a transform on the list generated by dicts_to_compounds .

2 Likes

It’s probably clearest to set default_tag(json) and then convert the functor name on a case-by-case basis as in:

json2font(json(Index, Fontname), font(Index, Fontname)).

getfonts :-
    sample_json(Json), 
    atom_json_dict(Json, Dict, [default_tag(json)]),
    dicts_to_compounds(Dict.lcd.fonts, [index, fontname], 
                       dict_fill(null), Compounds),
    maplist(json2font, Compounds, Fonts),
    print(Fonts).

Leaving the Options list empty and using the default row has the slight drawback of creating extra work for the garbage collector, I recall reading in a different discussion in this forum.

(I edited a typo, forgot to change default_tag(font) to default_tag(json)).

2 Likes

OK thanks, maplist was the piece I wasn’t familiar with.

Thanks both, it’s working now and I’ve started to collect device inventory in a db file.