diff options
| -rw-r--r-- | MainLoop.cpp | 24 | ||||
| -rw-r--r-- | MainLoop.h | 7 | ||||
| -rw-r--r-- | Makefile | 26 | ||||
| -rw-r--r-- | click-fcgi.cpp | 129 | ||||
| -rw-r--r-- | debian/control | 4 | ||||
| -rw-r--r-- | debian/nginx-sites-available | 22 | ||||
| -rw-r--r-- | html/favicon.ico | bin | 0 -> 2238 bytes | |||
| -rw-r--r-- | html/index.html | 263 | 
8 files changed, 470 insertions, 5 deletions
diff --git a/MainLoop.cpp b/MainLoop.cpp index b6331a2..ba026f2 100644 --- a/MainLoop.cpp +++ b/MainLoop.cpp @@ -22,6 +22,8 @@  using namespace std::chrono_literals;  using namespace std::string_literals; +namespace bp = boost::process; +  MainLoop::MainLoop(int argc, char** argv):    m_status{},    m_config{argc, argv}, @@ -34,6 +36,28 @@ MainLoop::MainLoop(int argc, char** argv):  {    m_status.add(LED{"/sys/class/leds/ACT", "/sys/class/leds/PWR"});    m_status.add(LED{"/sys/class/leds/thingm1:green:led1", "/sys/class/leds/thingm1:red:led1"}); + +  start_fcgi(); +} + +MainLoop::~MainLoop() +{ +  stop_fcgi(); +} + +void MainLoop::start_fcgi() +{ +  m_child_fcgi = bp::child("spawn-fcgi -a 127.0.0.1 -p 9090 -n -- ./click-fcgi"); +  if (!m_child_fcgi.valid() || !m_child_fcgi.running()) { +    throw std::runtime_error("click-fcgi not started"); +  } +} + +void MainLoop::stop_fcgi() +{ +  if (m_child_fcgi.valid()) { +    m_child_fcgi.terminate(); +  }  }  bool run_flag = true; @@ -10,16 +10,23 @@  #include "PIDFile.h"  #include <boost/signals2.hpp> +#include <boost/process.hpp>  class MainLoop  {  public:    MainLoop(int argc, char** argv); +  ~MainLoop();    int run();  private: +  void start_fcgi(); +  void stop_fcgi(); +    void reconfigure_mode(); +  boost::process::child m_child_fcgi; +    boost::signals2::connection m_click_connection;    StatusLED m_status; @@ -1,6 +1,7 @@  TARGET=click +FCGI_TARGET=click-fcgi -default: $(TARGET) +default: $(TARGET) $(FCGI_TARGET)  SRCS= \    MainLoop.cpp \ @@ -26,6 +27,14 @@ HEADERS=$(SRCS:.cpp=.h)  OBJS=$(SRCS:.cpp=.o) +FCGI_SRCS=\ +  click-fcgi.cpp \ +  config.cpp \ +  log.cpp \ +  debug.cpp \ + +FCGI_OBJS=$(FCGI_SRCS:.cpp=.o) +  CXX=clang++  ifeq ($(CXXFLAGS),) @@ -37,22 +46,33 @@ CXXFLAGS+=-std=c++20 -Wall -Wpedantic  CXXFLAGS+=-gdwarf-4  CXXFLAGS+=-I/usr/include/libevdev-1.0 -CXXLIBS=$(shell pkg-config --libs alsa) -lreichwein -lfmt -lasound -levdev +CXXLIBS=$(shell pkg-config --libs alsa) -lreichwein -lfmt -lasound -levdev -lfcgi  $(TARGET): $(OBJS)  	$(CXX) $^ -o $@ $(CXXLIBS) +$(FCGI_TARGET): $(FCGI_OBJS) +	$(CXX) $^ -o $@ $(CXXLIBS) +  %.o: %.cpp  	$(CXX) $(CXXFLAGS) -o $@ -c $< +run-fcgi: +	spawn-fcgi -a 127.0.0.1 -p 9090 -n -- ./click-fcgi +  install:  	mkdir -p $(DESTDIR)/usr/bin  	cp $(TARGET) $(DESTDIR)/usr/bin  	mkdir -p $(DESTDIR)/usr/lib/click/media  	cp media/click.s16le $(DESTDIR)/usr/lib/click/media +	mkdir -p $(DESTDIR)/usr/lib/click +	cp $(FCGI_TARGET) $(DESTDIR)/usr/lib/click +	cp -r html $(DESTDIR)/usr/lib/click +	mkdir -p $(DESTDIR)/etc/nginx/sites-available +	cp debian/nginx-sites-available $(DESTDIR)/etc/nginx/sites-available/click  clean: -	rm -f $(TARGET) $(OBJS) +	rm -f $(TARGET) $(FCGI_TARGET) $(OBJS) $(FCGI_OBJS)  sound:  	ffmpeg -i media/click.wav -f s16le media/click.s16le diff --git a/click-fcgi.cpp b/click-fcgi.cpp new file mode 100644 index 0000000..8b422ca --- /dev/null +++ b/click-fcgi.cpp @@ -0,0 +1,129 @@ +#include "config.h" + +#include <stdexcept> +#include <string> +#include <iostream> + +#include <fcgiapp.h> + +#include <boost/property_tree/ptree.hpp> +#include <boost/property_tree/xml_parser.hpp> +#include <fmt/format.h> + +namespace pt = boost::property_tree; + +using namespace std::string_literals; + +namespace { + +class PostData +{ +public: +  PostData(FCGX_Request& request) { +    std::string result; +    std::string contentLengthString(FCGX_GetParam("CONTENT_LENGTH", request.envp)); +    int contentLength = std::stoul(contentLengthString); + +    result.resize(contentLength); + +    unsigned int status = FCGX_GetStr(result.data(), result.size(), request.in); +    if (status != result.size()) { +      throw std::runtime_error(fmt::format("Read error: {}/{}", status, result.size())); +    } + +    m_data = result; +  } + +  std::string getData() +  { +    return m_data; +  } + +  // path: xml path, e.g. data.value +  std::string getXMLElement(const std::string& path) +  { +    pt::ptree tree{}; +    std::istringstream iss{m_data}; +    pt::read_xml(iss, tree, pt::xml_parser::trim_whitespace); + +    return tree.get<std::string>(path); +  } + +private: +  std::string m_data; +}; + +std::string getCommand(FCGX_Request& request) +{ +  std::string query = FCGX_GetParam("QUERY_STRING", request.envp); +  size_t pos = query.find("command="); +  if (pos != query.npos) { +    return query.substr(pos + 8); +  } else { +    return {}; +  } +} + +std::string to_xml() +{ +  std::string result{"<data><status>ok</status><ui>"}; + +  result += "ui1</ui>"; +  return result + "</data>"; +} + +} // namespace + +int main(int argc, char* argv[]) { +  try { +    Config config{argc, argv}; + +    std::string ok_data{"<data><status>ok</status><message>OK</message></data>"}; +    std::string error_data{"<data><status>error</status><message>General Error</message></data>"}; + +    int result = FCGX_Init(); +    if (result != 0) { +      return 1; // error on init +    } + +    FCGX_Request request; + +    if (FCGX_InitRequest(&request, 0, 0) != 0) { +      return 1; // error on init +    } + +    while (FCGX_Accept_r(&request) == 0) { +      std::string method = FCGX_GetParam("REQUEST_METHOD", request.envp); + +      FCGX_PutS("Content-Type: text/xml\r\n\r\n", request.out); + +      try { +        if (method == "POST") { +          PostData data{request}; +          std::string command {getCommand(request)}; +          if (command == "start") { +            FCGX_PutS(ok_data.c_str(), request.out); +          } else if (command == "stop") { +            FCGX_PutS(ok_data.c_str(), request.out); +          } else if (command == "getui") { +            FCGX_PutS(to_xml().c_str(), request.out); +          } else if (command == "setfile") { +            std::string filename = data.getXMLElement("data.value"); +            FCGX_PutS(ok_data.c_str(), request.out); +          } else { +            FCGX_PutS(error_data.c_str(), request.out); +          } +        } else { +          throw std::runtime_error(fmt::format("Bad request method: POST expected, got {}", method).c_str()); +        } +      } catch (const std::exception& ex) { +        FCGX_PutS(("<data><status>error</status><message>Error: "s + ex.what() + "</message></data>").c_str(), request.out); +      } +    } +  } catch (const std::exception& ex) { +    std::cerr << "Error: " << ex.what() << std::endl; +  } + +  return 0; +} + diff --git a/debian/control b/debian/control index de141e8..b95f2c8 100644 --- a/debian/control +++ b/debian/control @@ -2,13 +2,13 @@ Source: click  Section: sound  Priority: optional  Maintainer: Roland Reichwein <mail@reichwein.it> -Build-Depends: debhelper, clang, libc++-dev, libreichwein-dev, libasound2-dev, libfmt-dev, libboost-all-dev, libevdev-dev +Build-Depends: debhelper, clang, libc++-dev, libreichwein-dev, libasound2-dev, libfmt-dev, libboost-all-dev, libevdev-dev, libfcgi-dev  Standards-Version: 4.5.0  Homepage: http://www.reichwein.it/click/  Package: click  Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends} +Depends: ${shlibs:Depends}, ${misc:Depends}, spawn-fcgi, nginx, alsa-utils  Homepage: http://www.reichwein.it/click/  Description: Software system for MIDI Click   MIDI Click is a combined hardware-software solution to generate an audio diff --git a/debian/nginx-sites-available b/debian/nginx-sites-available new file mode 100644 index 0000000..f1ca5a1 --- /dev/null +++ b/debian/nginx-sites-available @@ -0,0 +1,22 @@ +server { +	listen 80 default_server; +	listen [::]:80 default_server; + +	root /usr/lib/clock/html; + +	# Add index.php to the list if you are using PHP +	index index.html index.htm index.nginx-debian.html; + +	server_name _; + +	location ~ \.fcgi { +		include fastcgi_params; +		fastcgi_pass 127.0.0.1:9090; +	} + +	location / { +		# First attempt to serve request as file, then +		# as directory, then fall back to displaying a 404. +		try_files $uri $uri/ =404; +	} +} diff --git a/html/favicon.ico b/html/favicon.ico Binary files differnew file mode 100644 index 0000000..e8cbddb --- /dev/null +++ b/html/favicon.ico diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..2fd9898 --- /dev/null +++ b/html/index.html @@ -0,0 +1,263 @@ +<!DOCTYPE html> +<html> +	<head> +		<title>CLICK</title> + +<style> +:root{ +  background-color:#000000; +  color:#FFFFFF; +  font-family: "sans-serif"; +} + +body { +  /* +  background-color:#808080; +  */ +} + +#headline{ +  color:#808080; +  text-align: center; +  font-size: 12pt; +} + +#statusdiv{ +  color:#808080; +  text-align: center; +} + +.button{ +  width: 200px; +  height: 150px; + +  background-color: #04AA04; +  border: none; +  color: white; +  padding: 20px 50px; +  text-align: center; +  text-decoration: none; +  display: inline-block; +  margin: 4px 2px; +  border-radius: 8px; +} + +#buttoncontainer{ +  text-align: center; +  width: 100%; +} + +.selected{ +  color: #FF8080; +  cursor: pointer; +  font-size: 500%; +} + +.normal{ +  color: #FFFFFF; +  cursor: pointer; +  font-size: 500%; +} + +#ui{ +  height: calc(100vh - 300px); +  width: 100%; +  white-space: pre; +  overflow-x: auto; +  overflow-y: auto; +  font-family: monospace; +} + +</style> +<script type="text/javascript"> +  var play_state = "stopped"; + +  function draw_play() +  { +    var canvas = document.getElementById("playcanvas"); + +    var height = canvas.height; +    var width = canvas.width; + +    var c = canvas.getContext("2d"); + +    c.clearRect(0, 0, width, height); +    c.fillStyle = "#FFFFFF"; +    c.beginPath(); +    c.moveTo(30,30); +    c.lineTo(70,50); +    c.lineTo(30,70); +    c.lineTo(30,30); +    c.fill(); +  } + +  function draw_stop() +  { +    var canvas = document.getElementById("stopcanvas"); + +    var height = canvas.height; +    var width = canvas.width; + +    var c = canvas.getContext("2d"); + +    c.clearRect(0, 0, width, height); +    c.fillStyle = "#FFFFFF"; +    c.beginPath(); +    c.moveTo(30,30); +    c.lineTo(70,30); +    c.lineTo(70,70); +    c.lineTo(30,70); +    c.lineTo(30,30); +    c.fill(); +  } + +  function draw_symbols() +  { +    draw_play(); +    draw_stop(); +  } + +  function show_play() +  { +    document.getElementById("playcanvas").style.display = 'block'; +    document.getElementById("stopcanvas").style.display = 'none'; +  } + +  function show_stop() +  { +    document.getElementById("stopcanvas").style.display = 'block'; +    document.getElementById("playcanvas").style.display = 'none'; +  } + +  function show_status(text) { +    document.getElementById("status").innerHTML = text; +  } + +  function remove_extension(filename) +  { +    if (filename.endsWith(".midi")) { +      return filename.substring(0, filename.length - 5); +    } +    if (filename.endsWith(".mid")) { +      return filename.substring(0, filename.length - 4); +    } +    return filename; +  } + +  function get_ui(){ +    var xhr = new XMLHttpRequest(); + +    xhr.onreadystatechange = function() { +      if (this.readyState != 4) { +        return; +      } +      if (this.status != 200) { +        show_status("HTTP error"); +      } else { +        var xml = xhr.responseXML; +        var ui = xml.getElementsByTagName("ui")[0].childNodes[0].nodeValue; + +        document.getElementById("ui").innerHTML = ui; +        show_status("OK"); +      } +    } + +    xhr.open("POST", "click.fcgi" + "?command=getui", true); +    xhr.setRequestHeader("Content-type", "text/xml"); +    xhr.send(""); +  } + +  function playbutton_clicked() +  { +    var action = "start"; +    var element = document.getElementById("playbutton"); +    if (play_state == "stopped") { +      show_stop(); +      play_state = "playing"; +    } else { +      show_play(); +      play_state = "stopped"; +      action = "stop"; +    } + +    var xhr = new XMLHttpRequest(); + +    xhr.onreadystatechange = function() { +      if (this.readyState != 4) { +        return; +      } +      if (this.status != 200) { +        show_status("HTTP error"); +      } else { +        var xml = xhr.responseXML; +        var message = xml.getElementsByTagName("message")[0].childNodes[0].nodeValue; + +        show_status(message); +      } +    } + +    xhr.open("POST", "click.fcgi" + "?command=" + action, true); +    xhr.setRequestHeader("Content-type", "text/xml"); +    xhr.send(""); +  } + +  function ui_clicked(index) +  { +    var xhr = new XMLHttpRequest(); + +    xhr.onreadystatechange = function() { +      if (this.readyState != 4) { +        return; +      } +      if (this.status != 200) { +        show_status("HTTP error"); +      } else { +        var xml = xhr.responseXML; +        var message = xml.getElementsByTagName("message")[0].childNodes[0].nodeValue; + +        show_status(message); + +        get_ui(); // trigger reload +      } +    } + +    xhr.open("POST", "click.fcgi" + "?command=setfile", true); +    xhr.setRequestHeader("Content-type", "text/xml"); +    xhr.send("<data><value>" + "</value></data>"); +  } + +  function startup() { +    draw_symbols(); +    document.getElementById("playbutton").onclick = playbutton_clicked; +    show_play(); + +    get_ui(); +  } + +</script> +</head> +<body onload="startup();"> +  <div id="headline">CLICK</div> + +<br/> +<div id="ui"> +(Loading UI...) +</div> +<br/> +<div id="buttoncontainer"> +<button class="button">-</button> +<button class="button">+</button> +<button id="playbutton" class="button"> +<canvas id="playcanvas" width="100" height="100"></canvas> +<canvas id="stopcanvas" width="100" height="100"></canvas> +</button> +<button class="button">Mode</button> +<button class="button">Note</button> +</div> +<br/> +<div id="statusdiv"> +  Status: <span id="status">(unknown)</span> +</div> + +	</body> +</html>  | 
