summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2025-01-20 21:56:53 +0100
committerRoland Reichwein <mail@reichwein.it>2025-01-20 21:56:53 +0100
commit2c5684482c14764cec4fb32b2ec07dd3f77fd4bf (patch)
tree9ff9c5c04a20c30568f4ce1c466fe55818a0b45b
parent5fd637644c7529bfdc5291215f3f8ee1edd304c4 (diff)
Add click-fcgi (WIP)HEADmaster
-rw-r--r--MainLoop.cpp24
-rw-r--r--MainLoop.h7
-rw-r--r--Makefile26
-rw-r--r--click-fcgi.cpp129
-rw-r--r--debian/control4
-rw-r--r--debian/nginx-sites-available22
-rw-r--r--html/favicon.icobin0 -> 2238 bytes
-rw-r--r--html/index.html263
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;
diff --git a/MainLoop.h b/MainLoop.h
index b732c32..4d08f5e 100644
--- a/MainLoop.h
+++ b/MainLoop.h
@@ -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;
diff --git a/Makefile b/Makefile
index 91ee0c8..6b1121e 100644
--- a/Makefile
+++ b/Makefile
@@ -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
new file mode 100644
index 0000000..e8cbddb
--- /dev/null
+++ b/html/favicon.ico
Binary files differ
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>