Skip to content

Commit 491de8c

Browse files
committed
service/oss: detect headphones
1 parent 73e97de commit 491de8c

2 files changed

Lines changed: 314 additions & 2 deletions

File tree

src/services/oss/oss.cpp

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
#include <utility>
88

99
#include <QFile>
10+
#include <QFileDevice>
11+
#include <QHash>
1012
#include <QProcess>
13+
#include <QRegularExpression>
1114
#include <QSocketNotifier>
1215
#include <QTextStream>
16+
#include <Qt>
17+
#include <QtTypes>
1318
#include <fcntl.h>
1419
#include <qalgorithms.h>
1520
#include <qlist.h>
@@ -19,7 +24,9 @@
1924
#include <qtimer.h>
2025
#include <qtmetamacros.h>
2126
#ifdef __FreeBSD__
27+
#include <sys/event.h>
2228
#include <sys/ioccom.h>
29+
#include <sys/queue.h>
2330
#include <sys/socket.h>
2431
#include <sys/sysctl.h>
2532
#include <sys/types.h>
@@ -341,12 +348,30 @@ OSS::OSS(QObject* parent): QObject(parent), mRescanTimer(new QTimer(this)) {
341348
});
342349

343350
this->connectToDevd();
351+
this->initializeJackStates();
352+
// clang-format off
353+
#ifdef __FreeBSD__
354+
this->setupJackDetection();
355+
#endif
356+
// clang-format on
344357
} else {
345358
qCWarning(logOSS) << "OSS sound system is not available";
346359
}
347360
}
348361

349362
OSS::~OSS() {
363+
if (this->mKqueueNotifier) {
364+
this->mKqueueNotifier->setEnabled(false);
365+
}
366+
367+
if (this->mKqueue >= 0) {
368+
close(this->mKqueue);
369+
}
370+
371+
if (this->mLogFileDescriptor >= 0) {
372+
close(this->mLogFileDescriptor);
373+
}
374+
350375
if (this->mDevdSocket >= 0) {
351376
close(this->mDevdSocket);
352377
}
@@ -369,6 +394,7 @@ bool OSS::isAvailable() const { return this->mAvailable; }
369394
void OSS::connectToDevd() {
370395
// clang-format off
371396
#ifdef __FreeBSD__
397+
// clang-format on
372398
// Try seqpacket first
373399
const char* pipePaths[] = {
374400
"/var/run/devd.seqpacket.pipe",
@@ -415,6 +441,7 @@ void OSS::connectToDevd() {
415441
}
416442

417443
qCWarning(logOSS) << "Failed to connect to a devd pipe—device monitoring disabled";
444+
// clang-format off
418445
#else
419446
qCInfo(logOSS) << "devd monitoring only available on FreeBSD";
420447
#endif
@@ -473,6 +500,265 @@ void OSS::handleDevdEvent() {
473500
}
474501
}
475502

503+
#ifdef __FreeBSD__
504+
void OSS::handleKqueueEvent() {
505+
struct kevent event;
506+
struct timespec timeout = {.tv_sec = 0, .tv_nsec = 0};
507+
508+
auto nev = kevent(this->mKqueue, nullptr, 0, &event, 1, &timeout);
509+
510+
if (nev > 0 && (event.fflags & (NOTE_WRITE | NOTE_EXTEND))) {
511+
this->readNewLogLines();
512+
}
513+
}
514+
515+
void OSS::setupJackDetection() {
516+
this->mLogFileDescriptor = open("/var/log/messages", O_RDONLY);
517+
if (this->mLogFileDescriptor < 0) {
518+
qCWarning(logOSS) << "Failed to open /var/log/messages for jack detection";
519+
return;
520+
}
521+
522+
// Seek to end to only catch new messages
523+
this->mLogFilePosition = lseek(this->mLogFileDescriptor, 0, SEEK_END); // NOLINT
524+
525+
this->mKqueue = kqueue(); // NOLINT
526+
if (this->mKqueue < 0) {
527+
qCWarning(logOSS) << "Failed to create kqueue for jack detection";
528+
close(this->mLogFileDescriptor);
529+
this->mLogFileDescriptor = -1;
530+
return;
531+
}
532+
533+
struct kevent change;
534+
EV_SET(
535+
&change,
536+
this->mLogFileDescriptor,
537+
EVFILT_VNODE,
538+
EV_ADD | EV_ENABLE | EV_CLEAR,
539+
NOTE_WRITE | NOTE_EXTEND,
540+
0,
541+
nullptr
542+
);
543+
544+
if (kevent(this->mKqueue, &change, 1, nullptr, 0, nullptr) < 0) {
545+
qCWarning(logOSS) << "Failed to register kqueue event:" << qt_error_string(errno);
546+
close(this->mKqueue);
547+
close(this->mLogFileDescriptor);
548+
549+
this->mKqueue = -1;
550+
this->mLogFileDescriptor = -1;
551+
return;
552+
}
553+
554+
this->mKqueueNotifier = new QSocketNotifier(this->mKqueue, QSocketNotifier::Read, this);
555+
connect(this->mKqueueNotifier, &QSocketNotifier::activated, this, &OSS::handleKqueueEvent);
556+
this->mKqueueNotifier->setEnabled(true);
557+
558+
qCInfo(logOSS) << "Jack detection monitoring enabled";
559+
}
560+
#endif
561+
562+
void OSS::identifyJackType(int nid, bool connected) {
563+
// Read sysctl to identify what this nid is
564+
const QString sysctlPath = QString("dev.hdaa.0.nid%1").arg(nid);
565+
566+
QProcess process;
567+
process.start("sysctl", QStringList() << "-n" << sysctlPath);
568+
process.waitForFinished(100);
569+
570+
const QString output = QString::fromUtf8(process.readAllStandardOutput()).trimmed();
571+
572+
if (output.contains("Headphones", Qt::CaseInsensitive)) {
573+
qCInfo(logOSS) << "Headphones" << (connected ? "connected" : "disconnected");
574+
emit this->headphonesChanged(connected);
575+
} else if (output.contains("Mic", Qt::CaseInsensitive)) {
576+
qCInfo(logOSS) << "Microphone" << (connected ? "connected" : "disconnected");
577+
emit this->microphoneChanged(connected);
578+
}
579+
}
580+
581+
void OSS::syncJackProperties() {
582+
for (auto it = this->mJackStates.constBegin(); it != this->mJackStates.constEnd(); ++it) {
583+
const int nid = it.key();
584+
const bool connected = it.value();
585+
586+
// Identify what type of jack this is and update properties
587+
const QString sysctlPath = QString("dev.hdaa.0.nid%1").arg(nid);
588+
589+
QProcess process;
590+
process.start("sysctl", QStringList() << "-n" << sysctlPath);
591+
process.waitForFinished(100);
592+
593+
const QString output = QString::fromUtf8(process.readAllStandardOutput()).trimmed();
594+
595+
if (output.contains("Headphones", Qt::CaseInsensitive)) {
596+
if (this->mHeadphonesConnected != connected) {
597+
this->mHeadphonesConnected = connected;
598+
qCInfo(logOSS) << "Initial headphone state:" << (connected ? "connected" : "disconnected");
599+
emit this->headphonesChanged(connected);
600+
}
601+
}
602+
}
603+
}
604+
605+
void OSS::initializeJackStates() {
606+
QProcess sysctl;
607+
sysctl.start("sysctl", QStringList() << "-a");
608+
if (!sysctl.waitForFinished(500)) {
609+
qCWarning(logOSS) << "sysctl timeout";
610+
return;
611+
}
612+
613+
const QString output = QString::fromUtf8(sysctl.readAllStandardOutput());
614+
const QStringList lines = output.split('\n');
615+
616+
for (const QString& line: lines) {
617+
// Look for jack pins
618+
if (line.contains("dev.hdaa.") && line.contains(": pin:") && line.contains("Jack")) {
619+
const QRegularExpression regex(R"(dev\.hdaa\.\d+\.nid(\d+):\s+pin:\s+(.+))");
620+
auto match = regex.match(line);
621+
622+
if (match.hasMatch()) {
623+
const int nid = match.captured(1).toInt();
624+
const QString pinDesc = match.captured(2);
625+
626+
// Skip disabled or internal (Fixed/None) pins
627+
if (pinDesc.contains("DISABLED") || pinDesc.contains("(None)")
628+
|| pinDesc.contains("(Fixed)"))
629+
{
630+
continue;
631+
}
632+
633+
qCDebug(logOSS) << "Found jack: nid=" << nid << "desc=" << pinDesc;
634+
635+
// Initialize as disconnected
636+
this->mJackStates[nid] = false;
637+
638+
if (pinDesc.contains("Headphones", Qt::CaseInsensitive)) {
639+
this->mHeadphonesConnected = false;
640+
qCInfo(logOSS) << "Found headphone jack at nid" << nid;
641+
}
642+
}
643+
}
644+
}
645+
646+
qCDebug(logOSS) << "Initialized" << this->mJackStates.size() << "jacks";
647+
648+
this->readRecentLogMessages();
649+
this->syncJackProperties();
650+
}
651+
652+
void OSS::readNewLogLines() {
653+
QFile logFile;
654+
if (!logFile.open(
655+
this->mLogFileDescriptor,
656+
QIODevice::ReadOnly | QIODevice::Text,
657+
QFileDevice::DontCloseHandle
658+
))
659+
{
660+
return;
661+
}
662+
663+
logFile.seek(this->mLogFilePosition);
664+
665+
QTextStream in(&logFile);
666+
while (!in.atEnd()) {
667+
const QString line = in.readLine();
668+
this->parseLogLine(line);
669+
}
670+
671+
// Update position for the next read
672+
this->mLogFilePosition = logFile.pos();
673+
}
674+
675+
void OSS::parseLogLine(const QString& line) {
676+
if (!line.contains("Pin sense:")) {
677+
return;
678+
}
679+
680+
// Extract nid number
681+
const QRegularExpression nidRegex(R"(nid=(\d+))");
682+
auto nidMatch = nidRegex.match(line);
683+
684+
if (!nidMatch.hasMatch()) {
685+
return;
686+
}
687+
688+
const int nid = nidMatch.captured(1).toInt();
689+
690+
bool connected = false;
691+
if (line.contains("(connected)")) {
692+
connected = true;
693+
} else if (line.contains("(disconnected)")) {
694+
connected = false;
695+
} else {
696+
return; // Unknown state
697+
}
698+
699+
if (this->mJackStates.value(nid, !connected) != connected) {
700+
this->mJackStates[nid] = connected;
701+
702+
qCInfo(logOSS) << "Jack state changed: nid=" << nid
703+
<< (connected ? "connected" : "disconnected");
704+
705+
emit this->jackStateChanged(nid, connected);
706+
this->identifyJackType(nid, connected);
707+
}
708+
}
709+
710+
void OSS::readRecentLogMessages() {
711+
QFile logFile("/var/log/messages");
712+
if (!logFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
713+
qCWarning(logOSS) << "Cannot read /var/log/messages for initial state";
714+
return;
715+
}
716+
717+
// Read last 8KB for recent events
718+
const qint64 size = logFile.size();
719+
if (size > 8192) {
720+
logFile.seek(size - 8192);
721+
}
722+
723+
QTextStream in(&logFile);
724+
const QString content = in.readAll();
725+
logFile.close();
726+
727+
const QStringList lines = content.split('\n');
728+
QHash<int, bool> lastLoggedState;
729+
730+
for (const QString& line: lines) {
731+
if (!line.contains("Pin sense:")) {
732+
continue;
733+
}
734+
735+
const QRegularExpression nidRegex(R"(nid=(\d+))");
736+
auto nidMatch = nidRegex.match(line);
737+
738+
if (!nidMatch.hasMatch()) {
739+
continue;
740+
}
741+
742+
const int nid = nidMatch.captured(1).toInt();
743+
const bool connected = line.contains("(connected)");
744+
745+
lastLoggedState[nid] = connected;
746+
}
747+
748+
for (auto it = lastLoggedState.constBegin(); it != lastLoggedState.constEnd(); ++it) {
749+
const int nid = it.key();
750+
const bool connected = it.value();
751+
752+
if (this->mJackStates.contains(nid)) {
753+
this->mJackStates[nid] = connected;
754+
}
755+
}
756+
757+
qCDebug(logOSS) << "After reading log, jack states:" << this->mJackStates;
758+
}
759+
760+
bool OSS::headphonesConnected() const { return this->mHeadphonesConnected; }
761+
476762
void OSS::scanDevices() {
477763
if (this->mDefaultDevice != nullptr) {
478764
this->mDefaultDevice = nullptr;

0 commit comments

Comments
 (0)