From 360d8fe6b1faeea134f7a18d45ebbe69fd685be6 Mon Sep 17 00:00:00 2001 From: Klaus Schmidinger Date: Sat, 10 Jun 2017 11:53:39 +0200 Subject: [PATCH] Implemented CAM auto responses --- HISTORY | 3 + camresponses.conf | 29 +++++++ ci.c | 190 +++++++++++++++++++++++++++++++++++++++++++--- ci.h | 4 +- vdr.5 | 41 +++++++++- vdr.c | 3 +- 6 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 camresponses.conf diff --git a/HISTORY b/HISTORY index 62fed8c0..1711490f 100644 --- a/HISTORY +++ b/HISTORY @@ -9113,3 +9113,6 @@ Video Disk Recorder Revision History - Increased SLL_LENGTH in thread.c to better handle long caller lines, and enclosed logCaller with DEBUG_LOCKCALL to preserve memory in normal operation. - Fixed a typo in CAMMENURETYTIMEOUT and added logging CAM enquiries. +- The new configuration file 'camresponses.conf' can be used to define automatic + responses to CAM menus, for instance to avoid annyoing popup messages or entering + the parental rating PIN. See vdr.5 for details. diff --git a/camresponses.conf b/camresponses.conf new file mode 100644 index 00000000..f3cc56df --- /dev/null +++ b/camresponses.conf @@ -0,0 +1,29 @@ +# CAM responses for VDR +# +# Format: +# +# nr text action +# +# nr: the number of the CAM this action applies to (0 = all CAMs) +# text: the text in the CAM menu to react on (must be quoted with '"' if it contains +# blanks, escape '"' with '\') +# action: the action to take if the given text is encountered +# +# Possible actions are: +# +# - DISCARD: simply discard the menu (equivalent to pressing 'Back' on the RC) +# - CONFIRM: confirm the menu (equivalent to pressing 'OK' without selecting a +# particular item) +# - SELECT: select the menu item containing the text (equivalent to positioning +# the cursor on the item and pressing 'OK') +# - : the given number is sent to the CAM as if it were typed in by the user +# (provided this is an input field). +# +# Note that the text given in a rule must match exactly, including any leading or +# trailing blanks. If in doubt, you can get the exact text from the log file. +# Action keywords are case insensitive. +# +# Examples: + +# * "Hello! This is your annoying \"nag\" message!" DISCARD +# 3 "Please enter your PIN" 1234 diff --git a/ci.c b/ci.c index 55324535..4c3cb8fa 100644 --- a/ci.c +++ b/ci.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: ci.c 4.16 2017/05/18 09:05:46 kls Exp $ + * $Id: ci.c 4.17 2017/06/10 11:53:39 kls Exp $ */ #include "ci.h" @@ -78,17 +78,18 @@ static char *CopyString(int Length, const uint8_t *Data) ///< Copies the string at Data. ///< Returns a pointer to a newly allocated string. { - // Some CAMs send funny characters at the beginning of strings. - // Let's just skip them: - while (Length > 0 && (*Data == ' ' || *Data == 0x05 || *Data == 0x96 || *Data == 0x97)) { + char *s = MALLOC(char, Length + 1); + char *p = s; + while (Length > 0) { + char c = *Data; + if (isprint(c)) // some CAMs send funny characters in their strings, let's just skip them + *p++ = c; + else if (c == 0x8A) // the character 0x8A is used as newline, so let's put a real '\n' in there + *p++ = '\n'; Length--; Data++; } - char *s = MALLOC(char, Length + 1); - strncpy(s, (char *)Data, Length); - s[Length] = 0; - // The character 0x8A is used as newline, so let's put a real '\n' in there: - strreplace(s, 0x8A, '\n'); + *p = 0; return s; } @@ -290,6 +291,137 @@ void cCaActivationReceiver::Receive(const uchar *Data, int Length) } } +// --- cCamResponse ---------------------------------------------------------- + +// CAM Response Actions: + +#define CRA_NONE 0 +#define CRA_DISCARD -1 +#define CRA_CONFIRM -2 +#define CRA_SELECT -3 + +class cCamResponse : public cListObject { +private: + int camNumber; + char *text; + int action; +public: + cCamResponse(void); + ~cCamResponse(); + bool Parse(const char *s); + int Matches(int CamNumber, const char *Text) const; + }; + +cCamResponse::cCamResponse(void) +{ + camNumber = -1; + text = NULL; + action = CRA_NONE; +} + +cCamResponse::~cCamResponse() +{ + free(text); +} + +bool cCamResponse::Parse(const char *s) +{ + // Number: + s = skipspace(s); + if (*s == '*') { + camNumber = 0; // all CAMs + s++; + } + else { + char *e; + camNumber = strtol(s, &e, 10); + if (e == s || camNumber <= 0) + return false; + s = e; + } + // Text: + s = skipspace(s); + char *t = const_cast(s); // might have to modify it + char *q = NULL; // holds a copy in case of backslashes + bool InQuotes = false; + while (*t) { + if (*t == '"') { + if (t == s) { // opening quotes + InQuotes = true; + s++; + } + else if (InQuotes) // closing quotes + break; + } + else if (*t == '\\') { + if (!q) { // need to make a copy in order to strip backslashes + q = strdup(s); + t = q + (t - s); + s = q; + } + memmove(t, t + 1, strlen(t)); + } + else if (*t == ' ') { + if (!InQuotes) + break; + } + t++; + } + free(text); // just for safety + text = NULL; + if (t != s) { + text = strndup(s, t - s); + s = t + 1; + } + free(q); + if (!text) + return false; + // Action: + s = skipspace(s); + if (strcasecmp(s, "DISCARD") == 0) action = CRA_DISCARD; + else if (strcasecmp(s, "CONFIRM") == 0) action = CRA_CONFIRM; + else if (strcasecmp(s, "SELECT") == 0) action = CRA_SELECT; + else if (isnumber(s)) action = atoi(s); + else + return false; + return true; +} + +int cCamResponse::Matches(int CamNumber, const char *Text) const +{ + if (!camNumber || camNumber == CamNumber) { + if (strcmp(text, Text) == 0) + return action; + } + return CRA_NONE; +} + +// --- cCamResponses -------------------------------------------------------- + +class cCamResponses : public cConfig { +public: + int GetMatch(int CamNumber, const char *Text) const; + }; + +int cCamResponses::GetMatch(int CamNumber, const char *Text) const +{ + for (const cCamResponse *cr = First(); cr; cr = Next(cr)) { + int Action = cr->Matches(CamNumber, Text); + if (Action != CRA_NONE) { + dsyslog("CAM %d: auto response %4d to '%s'\n", CamNumber, Action, Text); + return Action; + } + } + return CRA_NONE; +} + +cCamResponses CamResponses; + +bool CamResponsesLoad(const char *FileName, bool AllowComments, bool MustExist) +{ + return CamResponses.Load(FileName, AllowComments, MustExist); +} + // --- cTPDU ----------------------------------------------------------------- #define MAX_TPDU_SIZE 4096 @@ -1292,15 +1424,41 @@ void cCiMMI::Process(int Length, const uint8_t *Data) if (l > 0) menu->titleText = GetText(l, &d); if (l > 0) menu->subTitleText = GetText(l, &d); if (l > 0) menu->bottomText = GetText(l, &d); + int Action = CRA_NONE; + int Select = -1; + int Item = 0; while (l > 0) { char *s = GetText(l, &d); if (s) { if (!menu->AddEntry(s)) free(s); + else if (Action == CRA_NONE) { + Action = CamResponses.GetMatch(CamSlot()->SlotNumber(), s); + if (Action == CRA_SELECT) + Select = Item; + } } else break; + Item++; } + if (Action != CRA_NONE) { + delete menu; + menu = NULL; + cCondWait::SleepMs(100); + if (Action == CRA_DISCARD) { + SendCloseMMI(); + dsyslog("CAM %d: DISCARD", CamSlot()->SlotNumber()); + } + else if (Action == CRA_CONFIRM) { + SendMenuAnswer(1); + dsyslog("CAM %d: CONFIRM", CamSlot()->SlotNumber()); + } + else if (Action == CRA_SELECT) { + SendMenuAnswer(Select + 1); + dsyslog("CAM %d: SELECT %d", CamSlot()->SlotNumber(), Select + 1); + } + } } } break; @@ -1319,6 +1477,19 @@ void cCiMMI::Process(int Length, const uint8_t *Data) l--; // I really wonder why there is no text length field here... enquiry->text = CopyString(l, d); + int Action = CamResponses.GetMatch(CamSlot()->SlotNumber(), enquiry->text); + if (Action > CRA_NONE) { + char s[enquiry->expectedLength * 2]; + snprintf(s, sizeof(s), "%d", Action); + if (int(strlen(s)) == enquiry->expectedLength) { + delete enquiry; + enquiry = NULL; + SendAnswer(s); + dsyslog("CAM %d: PIN", CamSlot()->SlotNumber()); + } + else + esyslog("CAM %d: ERROR: unexpected PIN length %d, expected %d", CamSlot()->SlotNumber(), int(strlen(s)), enquiry->expectedLength); + } } } break; @@ -1449,6 +1620,7 @@ void cCiMenu::Abort(void) cCiEnquiry::cCiEnquiry(cCiMMI *MMI) { mmi = MMI; + mutex = NULL; text = NULL; blind = false; expectedLength = 0; diff --git a/ci.h b/ci.h index e0cfa588..90267911 100644 --- a/ci.h +++ b/ci.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: ci.h 4.9 2017/05/18 09:05:46 kls Exp $ + * $Id: ci.h 4.10 2017/06/10 11:53:39 kls Exp $ */ #ifndef __CI_H @@ -529,4 +529,6 @@ public: extern cChannelCamRelations ChannelCamRelations; +bool CamResponsesLoad(const char *FileName, bool AllowComments = false, bool MustExist = false); + #endif //__CI_H diff --git a/vdr.5 b/vdr.5 index 8375aa4a..cf6eca71 100644 --- a/vdr.5 +++ b/vdr.5 @@ -8,7 +8,7 @@ .\" License as specified in the file COPYING that comes with the .\" vdr distribution. .\" -.\" $Id: vdr.5 4.2 2017/04/02 11:41:51 kls Exp $ +.\" $Id: vdr.5 4.3 2017/06/10 11:53:39 kls Exp $ .\" .TH vdr 5 "19 Feb 2015" "2.2" "Video Disk Recorder Files" .SH NAME @@ -918,6 +918,45 @@ in this file, other CAMs will be tried just as well. The main purpose of this file is to speed up channel switching in systems with more than one CAM. This file will be read at program startup and saved when the program ends. +.SS CAM AUTO RESPONSE +If your CAM keeps popping up annoying messages or you want to make sure VDR +can record programmes with parental rating without having to enter the PIN +(in case you can't turn that off in your CAM), you can set up auto responses +in the file \fIcamresponses.conf\fR. + +Each line in this file specifies one rule to apply to texts received from +the CAM. If the CAM's menu text matches the text in one of these rules, +the given action is taken and sent to the CAM as an automatic response, +without any menu appearing on the screen. The first match wins. + +The format of these rules is: + +nr text action + +where +.TS +tab (@); +l l. +nr @is the number of the CAM this action applies to (0 = all CAMs) +text @is the text in the CAM menu to react on (must be quoted with '"' if it contains blanks, escape '"' with '\\') +action @is the action to take if the given text is encountered +.TE + +Possible actions are: +.TS +tab (@); +l l. +DISCARD @simply discard the menu (equivalent to pressing 'Back' on the RC) +CONFIRM @confirm the menu (equivalent to pressing 'OK' without selecting a particular item) +SELECT @select the menu item containing the text (equivalent to positioning the cursor on the item and pressing 'OK') + @the given number is sent to the CAM as if it were tyed in by the user (provided this is an input field). +.TE + +Note that the text given in a rule must match exactly, including any leading or +trailing blanks. If in doubt, you can get the exact text from the log file. +Action keywords are case insensitive. + +Everything following (and including) a '#' character is considered to be comment. .SS COMMANDLINE OPTIONS If started without any options, vdr tries to read any files in the directory /etc/vdr/conf.d with names that do not begin with a '.' and that end with '.conf'. diff --git a/vdr.c b/vdr.c index fe2a7297..d92c6297 100644 --- a/vdr.c +++ b/vdr.c @@ -22,7 +22,7 @@ * * The project's page is at http://www.tvdr.de * - * $Id: vdr.c 4.17 2017/06/06 10:53:44 kls Exp $ + * $Id: vdr.c 4.18 2017/06/10 11:53:39 kls Exp $ */ #include @@ -763,6 +763,7 @@ int main(int argc, char *argv[]) Keys.Load(AddDirectory(ConfigDirectory, "remote.conf")); KeyMacros.Load(AddDirectory(ConfigDirectory, "keymacros.conf"), true); Folders.Load(AddDirectory(ConfigDirectory, "folders.conf")); + CamResponsesLoad(AddDirectory(ConfigDirectory, "camresponses.conf"), true); if (!*cFont::GetFontFileName(Setup.FontOsd)) { const char *msg = "no fonts available - OSD will not show any text!";