libcw/cwAvahiSurface.cpp

476 lines
12 KiB
C++
Raw Permalink Normal View History

//| Copyright: (C) 2020-2024 Kevin Larke <contact AT larke DOT org>
//| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
#include <avahi-client/client.h>
#include <avahi-client/publish.h>
#include <avahi-common/alternative.h>
#include <avahi-common/thread-watch.h>
#include <avahi-common/malloc.h>
#include <avahi-common/error.h>
#include <avahi-common/timeval.h>
#include <avahi-common/strlst.h>
#include "cwCommon.h"
#include "cwLog.h"
#include "cwCommonImpl.h"
#include "cwTime.h"
#include "cwThread.h"
#include "cwTcpSocket.h"
using namespace cw;
using namespace cw::net;
#define INSTANCE_NAME "MC Mix"
#define SERVICE_NAME "_EuConProxy._tcp"
#define SERVICE_PORT 49168
#define SERVICE_HOST nullptr //"Euphonix-MC-38C9863744E7.local"
#define ENET_INTERFACE "ens9"
#define SERVICE_TXT_0 "lmac=38-C9-86-37-44-E7"
#define SERVICE_TXT_1 "dummy=0"
#define HOST_MAC "hmac=00-E0-4C-A9-A4-8D" // mbp19 enet MAC
typedef struct app_str
{
AvahiEntryGroup *group = nullptr;
AvahiThreadedPoll *poll = nullptr;
char *name = nullptr;
unsigned instanceId = 0;
socket::handle_t tcpH;
thread::handle_t tcpThreadH;
unsigned recvBufByteN = 4096;
unsigned protocolState = 0;
unsigned txtXmtN = 0;
time::spec_t t0;
} app_t;
static void create_services(app_t* app, AvahiClient *c);
void errorv( app_t* app, const char* fmt, va_list vl )
{
vprintf(fmt,vl);
}
void logv( app_t* app, const char* fmt, va_list vl )
{
vprintf(fmt,vl);
}
void error( app_t* app, const char* fmt, ... )
{
va_list vl;
va_start(vl,fmt);
errorv( app, fmt, vl );
va_end(vl);
}
void rpt( app_t* app, const char* fmt, ... )
{
va_list vl;
va_start(vl,fmt);
logv( app, fmt, vl );
va_end(vl);
}
void choose_new_service_name( app_t* app )
{
char buf[255];
app->instanceId += 1;
snprintf(buf,sizeof(buf),"%s - %i", INSTANCE_NAME, app->instanceId);
char* n = avahi_strdup(buf);
avahi_free(app->name);
app->name = n;
rpt(app,"Service name collision, renaming service to '%s'\n", app->name);
}
static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata)
{
app_t* app = (app_t*)userdata;
assert(g == app->group || app->group == nullptr);
app->group = g;
// Called whenever the entry group state changes
switch (state)
{
case AVAHI_ENTRY_GROUP_ESTABLISHED :
// The entry group has been established successfully
rpt( app, "Service '%s' successfully established.\n", app->name);
break;
case AVAHI_ENTRY_GROUP_COLLISION :
{
// A service name collision with a remote service happened. Let's pick a new name.
choose_new_service_name(app);
// And recreate the services
create_services(app,avahi_entry_group_get_client(g));
break;
}
case AVAHI_ENTRY_GROUP_FAILURE :
error(app,"Entry group failure: %s\n", avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
/* Some kind of failure happened while we were registering our services */
avahi_threaded_poll_quit(app->poll);
break;
case AVAHI_ENTRY_GROUP_UNCOMMITED:
case AVAHI_ENTRY_GROUP_REGISTERING:
;
}
}
static void create_services(app_t* app, AvahiClient *c)
{
int ret;
AvahiPublishFlags flags = (AvahiPublishFlags)0;
assert(c);
// If this is the first time we're called, create a new entry group
if (!app->group)
{
if (!(app->group = avahi_entry_group_new(c, entry_group_callback, app)))
{
error(app,"avahi_entry_group_new() failed: %s\n", avahi_strerror(avahi_client_errno(c)));
goto fail;
}
}
// If the group is empty (either because it was just created, or because it was reset previously, add our entries.
if (avahi_entry_group_is_empty(app->group))
{
rpt(app,"Adding service '%s'\n", app->name);
// Add the service to the group
if ((ret = avahi_entry_group_add_service(app->group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, flags, app->name, SERVICE_NAME, nullptr, SERVICE_HOST, SERVICE_PORT, SERVICE_TXT_0, SERVICE_TXT_1, nullptr)) < 0)
{
if (ret == AVAHI_ERR_COLLISION)
goto collision;
error(app, "Failed to add _ipp._tcp service: %s\n", avahi_strerror(ret));
goto fail;
}
// Tell the server to register the service
if ((ret = avahi_entry_group_commit(app->group)) < 0)
{
error(app,"Failed to commit entry group: %s\n", avahi_strerror(ret));
goto fail;
}
}
return;
collision:
// A service name collision with a local service happened. Pick a new name.
choose_new_service_name(app);
avahi_entry_group_reset(app->group);
create_services(app,c);
return;
fail:
avahi_threaded_poll_quit(app->poll);
}
static void client_callback(AvahiClient *c, AvahiClientState state, void * userdata)
{
assert(c);
app_t* app = (app_t*)userdata;
// Called whenever the client or server state changes
switch (state)
{
case AVAHI_CLIENT_S_RUNNING:
// The server has startup successfully and registered its host
// name on the network, so it's time to create our services
create_services(app,c);
break;
case AVAHI_CLIENT_FAILURE:
error(app,"Client failure: %s\n", avahi_strerror(avahi_client_errno(c)));
avahi_threaded_poll_quit(app->poll);
break;
case AVAHI_CLIENT_S_COLLISION:
// Let's drop our registered services. When the server is back
// in AVAHI_SERVER_RUNNING state we will register them
// again with the new host name.
rpt(app,"S Collision\n");
case AVAHI_CLIENT_S_REGISTERING:
rpt(app,"S Registering\n");
// The server records are now being established. This
// might be caused by a host name change. We need to wait
// for our own records to register until the host name is
// properly esatblished.
if (app->group)
avahi_entry_group_reset(app->group);
break;
case AVAHI_CLIENT_CONNECTING:
;
}
}
rc_t _send_response( app_t* app, const unsigned char* buf, unsigned bufByteN )
{
rc_t rc;
if((rc = socket::send( app->tcpH, buf, bufByteN )) != kOkRC )
{
error(app,"Send failed.");
}
return rc;
}
rc_t send_response1( app_t* app )
{
// wifi: 98 5A EB 89 BA AA
// enet: 38 C9 86 37 44 E7
unsigned char buf[] =
{ 0x0b,0x00,0x00,0x00,0x00,0x00,0x00,0x50,0x00,0x02,0x03,0xfc,0x01,0x05,
0x06,0x00,
0x38,0xc9,0x86,0x37,0x44,0xe7,
0x01,0x00,
0xc0,0xa8,0x00,0x44,
0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x03,0xff,0x00,0x30,0x08,0x00,0x00,0x80,0x00,0x40,0x01,0x01,0x00,0x00,0x00,0x00,
0x00,0x00
};
return _send_response(app,buf,sizeof(buf));
}
rc_t send_response2( app_t* app )
{
unsigned char buf[] = { 0x0d,0x00,0x00,0x00,0x00,0x00,0x00,0x08 };
return _send_response(app,buf,sizeof(buf));
}
rc_t send_heart_beat( app_t* app )
{
unsigned char buf[] = { 0x03,0x00,0x00,0x00 };
return _send_response(app,buf,sizeof(buf));
}
rc_t send_txt( app_t* app, bool updateFl=true )
{
rc_t rc = kOkRC;
int ret;
const char* array[] =
{
"lmac=38-C9-86-37-44-E7",
"host=mbp19",
"hmac=BE-BD-EA-31-F9-88",
"dummy=1"
};
AvahiStringList* list = avahi_string_list_new_from_array(array,4);
if( updateFl )
ret = avahi_entry_group_update_service_txt_strlst(app->group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, (AvahiPublishFlags)0, app->name, SERVICE_NAME, nullptr, list);
else
ret = avahi_entry_group_add_service_strlst(app->group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, (AvahiPublishFlags)0, app->name, SERVICE_NAME, nullptr, nullptr, SERVICE_PORT, list);
if(ret < 0)
{
error(app,"Failed to %s entry group text: %s\n", updateFl ? "update" : "add", avahi_strerror(ret));
goto fail;
}
avahi_string_list_free(list);
fail:
return rc;
}
bool tcpReceiveCallback( void* arg )
{
app_t* app = static_cast<app_t*>(arg);
socket::handle_t sockH = app->tcpH;
char buf[ app->recvBufByteN ];
unsigned readByteN = 0;
rc_t rc = kOkRC;
time::spec_t t1;
if( !socket::isConnected(sockH) )
{
if((rc = socket::accept( sockH )) == kOkRC )
{
rpt(app,"TCP connected.\n");
}
}
else
{
if((rc = socket::receive( sockH, buf, app->recvBufByteN, &readByteN, nullptr )) == kOkRC || rc == kTimeOutRC )
{
if( rc == kTimeOutRC )
{
}
else
if( readByteN > 0 )
{
unsigned* h = (unsigned*)buf;
unsigned id = h[0];
switch( app->protocolState )
{
case 0:
if( id == 10 )
{
send_response1(app);
sleepMs(20);
send_heart_beat(app);
app->protocolState+=1;
}
break;
case 1:
{
if( buf[0] == 0x0c )
{
send_response2(app);
app->protocolState+=1;
time::get(app->t0);
}
}
break;
case 2:
{
time::get(t1);
if( time::elapsedMs( &app->t0, &t1 ) >= 4000 )
{
send_heart_beat(app);
app->t0 = t1;
}
}
break;
}
}
}
}
return true;
}
int main( int argc, const char* argv[] )
{
AvahiClient *client = nullptr;
int err_code;
int ret = 1;
const unsigned sbufN = 31;
char sbuf[ sbufN+1 ];
app_t app;
rc_t rc;
unsigned tcpTimeOutMs = 50;
cw::log::createGlobal();
// create the TCP socket
if((rc = socket::create(
app.tcpH,
SERVICE_PORT,
socket::kTcpFl | socket::kBlockingFl | socket::kStreamFl | socket::kListenFl,
tcpTimeOutMs,
NULL,
socket::kInvalidPortNumber )) != kOkRC )
{
rc = cwLogError(rc,"mDNS TCP socket create failed.");
goto errLabel;
}
unsigned char mac[6];
socket::get_mac( app.tcpH, mac, nullptr, ENET_INTERFACE );
for(int i=0; i<6; ++i)
printf("%02x:",mac[i]);
// create the TCP listening thread
if((rc = thread::create( app.tcpThreadH, tcpReceiveCallback, &app, "avahi_suf" )) != kOkRC )
goto errLabel;
// Allocate Avahi thread
if (!(app.poll = avahi_threaded_poll_new()))
{
error(&app,"Failed to create simple poll object.\n");
goto errLabel;
}
// Assign the service name
app.name = avahi_strdup(INSTANCE_NAME);
// Allocate a new client
if((client = avahi_client_new(avahi_threaded_poll_get(app.poll), (AvahiClientFlags)0, client_callback, &app, &err_code)) == nullptr )
{
error(&app,"Failed to create client: %s\n", avahi_strerror(err_code));
goto errLabel;
}
// start the tcp thread
if((rc = thread::unpause( app.tcpThreadH )) != kOkRC )
goto errLabel;
// start the avahi thread
avahi_threaded_poll_start(app.poll);
while( true )
{
printf("? ");
if( std::fgets(sbuf,sbufN,stdin) == sbuf )
{
if( strcmp(sbuf,"quit\n") == 0)
break;
}
}
avahi_threaded_poll_stop(app.poll);
ret = 0;
errLabel:
//if (client)
// avahi_client_free(client);
thread::destroy(app.tcpThreadH);
socket::destroy(app.tcpH);
if (app.poll)
avahi_threaded_poll_free(app.poll);
avahi_free(app.name);
cw::log::destroyGlobal();
return ret;
}