Lick Guide

In this guide we'll write a pair of simple apps to demonstrate how Lick works. One will be a Gall agent called licker.hoon, and the other a Python script called licker.py.

The Gall agent will create a socket through Lick and the Python script will connect to it. When the Gall agent is poked with a message of %ping, it'll send it through the socket to the Python script. The Python script will print ping!, then send a %pong message back through the socket to the Gall agent, which will print pong! to the Dojo.

First, we'll look at these two files.

licker.hoon

licker.hoon
/+  default-agent
|%
+$  card  card:agent:gall
--
^-  agent:gall
|_  =bowl:gall
+*  this  .
    def   ~(. (default-agent this %|) bowl)
::
++  on-init
  ^-  (quip card _this)
  :_  this
  [%pass /lick %arvo %l %spin /'licker.sock']~
::
++  on-poke
  |=  [=mark =vase]
  ^-  (quip card _this)
  ?>  ?=([%noun %ping] [mark !<(@tas vase)])
  :_  this
  [%pass /spit %arvo %l %spit /'licker.sock' %noun %ping]~
::
++  on-arvo
  |=  [=wire sign=sign-arvo]
  ^-  (quip card _this)
  ?.  ?=([%lick %soak *] sign)  (on-arvo:def +<)
  ?+    [mark noun]:sign        (on-arvo:def +<)
    [%connect ~]     ((slog 'socket connected' ~) `this)
    [%disconnect ~]  ((slog 'socket disconnected' ~) `this)
    [%error *]       ((slog leaf+"socket {(trip ;;(@t noun.sign))}" ~) `this)
    [%noun %pong]    ((slog 'pong!' ~) `this)
  ==
::
++  on-save   on-save:def
++  on-load   on-load:def
++  on-watch  on-watch:def
++  on-leave  on-leave:def
++  on-peek   on-peek:def
++  on-agent  on-agent:def
++  on-fail   on-fail:def
--

Our Gall agent is extremely simple and has no state. It only uses three agent arms: +on-init, +on-poke and +on-arvo.

+on-init

++  on-init
  ^-  (quip card _this)
  :_  this
  [%pass /lick %arvo %l %spin /'licker.sock']~

All +on-init does is pass Lick a %spin task to create a new licker.sock socket.

+on-poke

When +on-poke receives a poke with a $mark of %noun and data of %ping, it passes Lick a %spit task with the same data. Lick will send it on through to our licker.sock socket for our Python script. This lets us poke our agent from the Dojo like:

+on-arvo

+on-arvo expects a %soak gift from Lick. A %soak is primarily a message coming in from the socket, though connection status is also communicated in %soaks. The four cases we handle are:

  • %connect: An external process has connected to the socket.

  • %disconnect: An external process has disconnected from the socket.

  • %error: An error has occurred. The error message is a $cord in the $noun. The only time you'll get this is if you tried to %spit a message to the socket but there was nothing connected to it. In that case, the error message will be 'not connected'.

  • [%noun %pong]: This is the successful response we expect from the Python script.

In all cases we just +slog a message to the terminal.


licker.py

licker.py

Our Python script is also quite simple. We'll walk through it piece by piece.

First, we import the socket library and noun.py.

This function takes some data from the socket, decodes it, and returns a pair of the mark and noun. The data initially has the following format:

The version is always 0 (though this may change in the future). The cue_data function just strips off the the version and size headers, but you may wish to verify these.

After that, cue_data converts the jam to an integer and passes it to the cue function in noun.py to decode. It converts the mark to a string, then returns it along with the raw noun.

This function takes a mark string and msg string, converts them to integers, forms a cell and jams them with the jam function in noun.py. It's used to produce the jam when sending something back to the socket.

Once jam_result has been run, make_output calculates the length of the jam, sets the version number, and puts it all together so it can be sent off to the socket.

Here we specify the path to the socket and open the connection. Lick sockets live in:

You'll need to change sock_path to your pier location.

This is the main loop of our script. It listens for a message from the socket, calls cue_data to decode it, checks it's an expected ping, prints it, produces a pong in response and sends it back to the socket.


Setup

Create the folders for the project:

In the Dojo of a fakezod, mount the %base desk:

Copy across some dependencies (change the pier path if necessary):

Add a desk.bill sys.kelvin files:

Open a licker.hoon app in an editor, paste in the licker.hoon code above, and save it:

Open a licker.py file in an editor, paste in the licker.py code above, and save it:

Download the noun.py dependency from the urbit/tools repo:

Install additional python dependencies bitstream, mmh3 and numpy:

NOTE: At the time of writing, bitstream doesn't build against python>3.10. If you have 3.11 or newer, you may need to install a separate python3.10 (how your distro packages it may vary).

Create and mount the %licker desk in the Dojo:

Delete the existing files and copy in the new ones:

In the Dojo, commit the files and install the desk:


Try it out

First, run the Python script:

You should see the following in the Dojo:

Now, try poking the %licker agent with %ping:

In the terminal running the Python script, you should see:

And in the Dojo, you should see the response:

Now try closing the Python script. You should see the following in the Dojo:

If you try :licker %ping again, you'll see this error message:


Last updated