Site icon R-bloggers

Getting My Feet Wet With `Plumber` and JavaScript

[This article was first published on r on Everyday Is A School Day, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Tried out plumber and a bit of JavaScript to build a simple local API for logging migraine events šŸ§ šŸ’». Just a quick tap on my phone now records the time to a CSV—pretty handy! šŸ“±āœ…

Motivation < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

After our previous blog on barometric pressure monitoring, my friend Alec Wong said ā€˜Won’t it be great if we can just hit a button and it will record an event?".

In this case the reason for recording barometric pressure is to see if there is a link between migraine event and barometric pressure values/change etc. And yes, it would be great if we can create an app of something sort to make recording much easier!

There are many ways to do this. The way where we can maximize learning within R environment is to use plumber to create an API for us to interact and record event! Our use case is actually quite straight forward. We just need something that record a current timestamp when a button is clicked. Simple!

But since I’ve never used plumber before, this is a great opportunity to explore it! And also a bit of JavaScript too. Again, this blog is more for my benefit where it serves as a note for myself. Here we go!

Objectives: < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

Big Picture < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

As the image above shows, we want an app on our phone that once clicked will somehow change a csv dataframe. All these can be done by plumber setting an API to the csv. Since I just want to be able to do this on a local network of a different device (e.g. raspberrypi), we don’t need to deploy this to digital ocean or a server per se. We can run it in the background and set systemctl in case rpi restarts, point it to 0.0.0.0 and we can GET/POST via the device’s IP.

Yes, unfortunately this will not work if we’re no longer on local network, which at least from my utility, it will be just fine. No need to expose port forwarding. The safer way would be to use digital ocean droplet to do this, so you’re not exposing your own IP and open port to the public. That also means, you may have to pay some šŸ’° (e.g. ~$5/month). May someday when it can incorporate the barometric pressure and/or other metrics then

plumber.R < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

library(plumber)
library(readr)

file <- "migraine.csv"

if (file.exists(file)) {
  df <- read_csv(file)
} else {
df <- tibble(date=as.POSIXct(character()))
}

#* @apiTitle Migraine logger
#* @apiDescription A simple API to log migraine events

#* Return HTML content
#* @get /
#* @serializer html
function() {
  
  # Return HTML code with the log button
  html_content <- '
     <!DOCTYPE html>
     <html>
     <head>
       <title>Migraine Logger</title>
     </head>
     <body>
       <h1>Migraine Logger</h1>
       <button id="submit">Oh No, Migraine Today!</button>
       <div id="result" style="display: none;"></div>
       
      <script>
       document.getElementById("submit").onclick = function() {
          fetch("/log", {
            method : "post"
          })
          .then(response => response.json())
          .then(data => {
            const resultDiv = document.getElementById("result");
            resultDiv.textContent = data[0];
            resultDiv.style.display = "block";
          })
          .catch(error => {
            const resultDiv = document.getElementById("result");
            resultDiv.textContent = error.message
          })
       };
      </script>
      
     </body>
     </html>
     '
  return(html_content)
}

#* logging 
#* @post /log
function(){
  date_now <- tibble(date=Sys.time())
  df <<- rbind(df,date_now)
  write_csv(df, "migraine.csv")
  list(paste0("you have logged ", date_now$date[1], " to migraine.csv"))
}

#* download data
#* @get /download
#* @serializer csv
function(){
  df
}

Alright, let’s explore the code one by one. Again, as a note for my benefit.

Load libraries, load data, metadata

library(plumber)
library(readr)

file <- "migraine.csv"

if (file.exists(file)) {
  df <- read_csv(file)
} else {
df <- tibble(date=as.POSIXct(character()))
}

#* @apiTitle Migraine logger
#* @apiDescription A simple API to log migraine events

The above is quite self-explainatory. Point to a file, if it exists, read it, if not create an empty dataframe. The title and description of this API is described as such.

Let’s Write Out HTML & Javascript

#* Return HTML content
#* @get /
#* @serializer html
function() {
  
  # Return HTML code with the log button
  html_content <- '
     <!DOCTYPE html>
     <html>
     <head>
       <title>Migraine Logger</title>
     </head>
     <body>
       <h1>Migraine Logger</h1>
       <button id="submit">Oh No, Migraine Today!</button>
       <div id="result" style="display: none;"></div>
       
      <script>
       document.getElementById("submit").onclick = function() {
          fetch("/log", {
            method : "post"
          })
          .then(response => response.json())
          .then(data => {
            const resultDiv = document.getElementById("result");
            resultDiv.textContent = data[0];
            resultDiv.style.display = "block";
          })
          .catch(error => {
            const resultDiv = document.getElementById("result");
            resultDiv.textContent = error.message
          })
       };
      </script>
      
     </body>
     </html>
     '
  return(html_content)
}
  1. The skeleton #*, first is comment, 2nd is GET / (HTTP method), 3rd is Turn this function into HTML output Serializer. Basically means if we go to http://localhost:8000/, it will return this HTML. Now if we set GET /hello, then html will also show if you go to http://localhost:8000/hello
  2. Next is the HTML (without the Javascript, which is between ). Basically, write a heading, create a button, and a div to return result.
  3. The Javascript:

The interesting thing I’ve not come across is the arrow function. response => response.json() means function(response) { return response.json() }.

More Plumber API functions:

#* logging 
#* @post /log
function(){
  date_now <- tibble(date=Sys.time())
  df <<- rbind(df,date_now)
  write_csv(df, "migraine.csv")
  list(paste0("you have logged ", date_now$date[1], " to migraine.csv"))
}

#* download data
#* @get /download
#* @serializer csv
function(){
  df
}
  1. The POST /log function is where the magic happens. When the button is clicked, it will run this function. It will create a new row with the current timestamp and append it to the dataframe. Then write it out to migraine.csv. The <<- operator is used to assign a value to a variable in the parent environment (in this case, the global environment). This allows us to modify the df variable defined outside of the function. The list(paste0("you have logged ", date_now$date[1], " to migraine.csv")) will return a message to the user that the event has been logged. This is what will be displayed in the div with ID ā€œresultā€ in the HTML.

  2. The GET /download function is to download the data. It will return the dataframe as a CSV file when you go to http://localhost:8000/download. The @serializer csv line tells plumber to serialize the output as a CSV file.

OK, Let’s Check It Out! Click that Run API button for A Test Run!

We should see something like this. You can test it via Swagger UI or you can go to the address without __doc__ to get to the html directly.

Hurray! It works, locally… now let’s see if it works if it’s on a different device.

How To Run It? < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

  1. Transfer plumber.R or whatever file you saved to, to your device of choice.
  2. Install packages, of course
  3. Then run the following code
Rscript -e "pr <- plumber::plumb('plumber.R'); pr |> pr_run(port=8000,host='0.0.0.0')"

What it does it it’ll run the plumber API. And use a different device in the same network, then go to http://your-local-device-ip:8000/ and you should see something like the following

Hurray! It works! Now, let’s make sure we run it in the background and if rpi restarts, it will re-run the script by using systemctl. All of the code below are to be run in bash

sudo nano /etc/systemd/system/migraine-logger.service

Paste this in the migraine-logger.service

[Unit]
Description=Migraine Logger Plumber API
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/path/to/your/app
ExecStart=/usr/bin/Rscript -e "pr <- plumber::plumb('plumber.R'); pr |> pr_run(port=8000, host='0.0.0.0')"
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable, Start, Check Status

sudo systemctl enable migraine-logger.service
sudo systemctl start migraine-logger.service
sudo systemctl status migraine-logger.service

Hurray !!!

One Click On iOS? < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

Use your browser on iOS to go to your device’s IP and port e.g. http://192.168.1.11:8000 , then click on share and create shortcut homescreen, like so

Then you can have a shortcut on your iOS device that will open the app and click the button for you!

Opportunities For Improvement < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

Lessons Learnt < svg class="anchor-symbol" aria-hidden="true" height="26" width="26" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> < path d="M0 0h24v24H0z" fill="currentColor"> < path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76.0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71.0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71.0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76.0 5-2.24 5-5s-2.24-5-5-5z">

If you like this article:

To leave a comment for the author, please follow the link and comment on their blog: r on Everyday Is A School Day.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Exit mobile version