├── .gitignore ├── sbpd_commands.cfg ├── Makefile ├── LICENSE ├── servercomm.h ├── discovery.h ├── sbpd.h ├── control.h ├── GPIO.h ├── README.md ├── servercomm.c ├── GPIO.c ├── control.c ├── discovery.c └── sbpd.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.a 3 | sbpd* 4 | -------------------------------------------------------------------------------- /sbpd_commands.cfg: -------------------------------------------------------------------------------- 1 | # 2 | # sbpd_commands.cfg - Custom commands definition 3 | # 4 | # = 5 | # 6 | # CODE - MUST be a 4 character code, to be reference on command line when defining buttons 7 | # 8 | # For commands reference the LMS cli documentation, commands are to be JSON formatted. 9 | # 10 | # Default commands 11 | PLAY=["pause"] 12 | VOL-=["button","voldown"] 13 | VOL+=["button","volup"] 14 | PREV=["button","rew"] 15 | NEXT=["button","fwd"] 16 | POWR=["button","power"]" 17 | MIX+=["mixer","volume","+5"] 18 | MIX-=["mixer","volume","-5"] 19 | 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | CFLAGS = -Wall -fPIC -std=gnu99 -s -O3 -I/usr/local/include -Wl,-rpath,/usr/local/lib 3 | LDFLAGS = -L./lib -Wl,-rpath,/usr/local/lib -lcurl -lwiringPi 4 | #STATIC_LDFLAGS = -lpthread -ldl -lwiringPi ./libs/libcurl.a /usr/local/lib/libssl.a /usr/local/lib/libcrypto.a /usr/lib/libz.a 5 | STATIC_LDFLAGS = -lpthread -ldl -lwiringPi ./libs/libcurl.a -L/usr/local/lib -lcrypto -lssl -lz 6 | 7 | 8 | EXECUTABLE = sbpd 9 | EXECUTABLE-STATIC_CURL = sbpd-static 10 | 11 | SOURCES = control.c discovery.c GPIO.c sbpd.c servercomm.c 12 | DEPS = control.h discovery.h GPIO.h sbpd.h servercomm.h 13 | 14 | OBJECTS = $(SOURCES:.c=.o) 15 | 16 | all: $(EXECUTABLE) 17 | 18 | static: $(EXECUTABLE-STATIC_CURL) 19 | 20 | $(EXECUTABLE): $(OBJECTS) 21 | $(CC) $(OBJECTS) $(LDFLAGS) -o $@ 22 | 23 | $(EXECUTABLE-STATIC_CURL): $(OBJECTS) 24 | $(CC) $(OBJECTS) $(STATIC_LDFLAGS) -o $@ 25 | strip --strip-unneeded $(EXECUTABLE-STATIC_CURL) 26 | 27 | $(OBJECTS): $(DEPS) 28 | 29 | .c.o: 30 | $(CC) $(CFLAGS) $< -c -o $@ 31 | 32 | clean: 33 | rm -f $(OBJECTS) $(EXECUTABLE) $(EXECUTABLE-STATIC_CURL) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | sbpd SqueezeButtonPi Daemon 2 | 3 | Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | * Neither the name of ickStream nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 27 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /servercomm.h: -------------------------------------------------------------------------------- 1 | // 2 | // servercomm.h 3 | // SqueezeButtonPi 4 | // 5 | // Created by Jörg Schwieder on 02.02.17. 6 | // 7 | // 8 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 9 | // All rights reserved. 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright 15 | // notice, this list of conditions and the following disclaimer. 16 | // * Redistributions in binary form must reproduce the above copyright 17 | // notice, this list of conditions and the following disclaimer in the 18 | // documentation and/or other materials provided with the distribution. 19 | // * Neither the name of ickStream nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 32 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | // 34 | 35 | #ifndef servercomm_h 36 | #define servercomm_h 37 | 38 | #include "sbpd.h" 39 | 40 | // 41 | // 42 | // Initialize CURL for server communication and set MAC address 43 | // 44 | // 45 | int init_comm(char * use_mac); 46 | 47 | // 48 | // 49 | // Shutdown CURL 50 | // 51 | // 52 | void shutdown_comm(); 53 | 54 | // 55 | // 56 | // Send CLI command fragment to Logitech Media Server/Squeezebox Server 57 | // This command blocks (synchronously communicates". 58 | // In timing critical situations this should be called from a separate thread 59 | // 60 | // Parameters: 61 | // server: the server information structure defining host, port etc. 62 | // frament: the command fragment to be sent as JSON array 63 | // e.g. "[\"mixer\”,\"volume\",\"+2\"]" 64 | // Returns: success flag 65 | // 66 | // 67 | bool send_command(struct sbpd_server * server, int command, char * fragment); 68 | 69 | #endif /* servercomm_h */ 70 | -------------------------------------------------------------------------------- /discovery.h: -------------------------------------------------------------------------------- 1 | // 2 | // discovery.h 3 | // SqueezeButtonPi 4 | // 5 | // Created by Jörg Schwieder on 02.02.17. 6 | // 7 | // 8 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 9 | // All rights reserved. 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright 15 | // notice, this list of conditions and the following disclaimer. 16 | // * Redistributions in binary form must reproduce the above copyright 17 | // notice, this list of conditions and the following disclaimer in the 18 | // documentation and/or other materials provided with the distribution. 19 | // * Neither the name of ickStream nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 32 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | // 34 | 35 | #ifndef discovery_h 36 | #define discovery_h 37 | 38 | #include "sbpd.h" 39 | 40 | // 41 | // Polling function for server discovery 42 | // Call from main loop 43 | // Handles internal scheduling between different 44 | // 45 | // Parameters: 46 | // config: defines which parameters are preconfigured and will not be discovered 47 | // discovered: the discovered parameters 48 | // server: server configuration 49 | // 50 | void poll_discovery(sbpd_config_parameters_t config, 51 | sbpd_config_parameters_t *discovered, 52 | struct sbpd_server * server); 53 | 54 | 55 | // 56 | // MAC address search 57 | // 58 | // mac address. From SqueezeLite so should match that behaviour. 59 | // search first 4 interfaces returned by IFCONF 60 | // 61 | // Parameters: 62 | // config: defines which parameters are preconfigured and will not be discovered 63 | // discovered: the discovered parameters 64 | // server: server configuration 65 | // 66 | char * find_mac(); 67 | 68 | #endif /* discovery_h */ 69 | -------------------------------------------------------------------------------- /sbpd.h: -------------------------------------------------------------------------------- 1 | // 2 | // sbpd.h 3 | // SqueezeButtonPi 4 | // 5 | // General state and data structure definitions 6 | // 7 | // Created by Jörg Schwieder on 02.02.17. 8 | // 9 | // 10 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 11 | // All rights reserved. 12 | // 13 | // Redistribution and use in source and binary forms, with or without 14 | // modification, are permitted provided that the following conditions are met: 15 | // 16 | // * Redistributions of source code must retain the above copyright 17 | // notice, this list of conditions and the following disclaimer. 18 | // * Redistributions in binary form must reproduce the above copyright 19 | // notice, this list of conditions and the following disclaimer in the 20 | // documentation and/or other materials provided with the distribution. 21 | // * Neither the name of ickStream nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software 23 | // without specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 28 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 29 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 30 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 32 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 33 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 34 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | #ifndef sbpd_h 38 | #define sbpd_h 39 | 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | 46 | #define USER_AGENT "SqueezeButtonPi" 47 | #define VERSION "2.1" 48 | 49 | // configuration parameters. 50 | // used to flag pre-configured or detected parameters 51 | typedef enum { 52 | // server 53 | SBPD_cfg_host = 0x1, 54 | SBPD_cfg_port = 0x2, 55 | SBPD_cfg_user = 0x4, 56 | SBPD_cfg_password = 0x8, 57 | SBPD_cfg_config = 0x10, 58 | 59 | // player 60 | SBPD_cfg_MAC = 0x1000, 61 | } sbpd_config_parameters_t; 62 | 63 | // 64 | // Command types 65 | // 66 | #define LMS 0 67 | #define SCRIPT 1 68 | #define NOTUSED 2 69 | 70 | // supported commands 71 | // need to be configured 72 | /* Not in Use 73 | typedef enum { 74 | SBPD_cmd_volume = 0x1, 75 | SBPD_cmd_pause = 0x2, 76 | SBPD_cmd_next = 0x4, 77 | SBPD_cmd_prev = 0x8, 78 | SBPD_cmd_power = 0x10, 79 | } sbpd_commands_t; 80 | */ 81 | 82 | // server configuration data structure 83 | // contains address and user/password 84 | struct sbpd_server { 85 | char * host; 86 | uint32_t port; 87 | char * user; 88 | char * password; 89 | char * config_file; 90 | }; 91 | 92 | // 93 | // Define scheduling behavior 94 | // We use usleep so this is in µs 95 | // 96 | #define SCD_SLEEP_TIMEOUT 100000 97 | #define SCD_SECOND 1000000 98 | 99 | // 100 | // Helpers 101 | // 102 | #define STRTOU32(x) (*((uint32_t *)x)) // make a 32 bit integer from a 4 char string 103 | 104 | void parse_config(); 105 | char * trim (char * s); 106 | 107 | // 108 | // Logging 109 | // 110 | #define logerr( args... ) _mylog( __FILE__, __LINE__, LOG_ERR, args ) 111 | #define logwarn( args... ) _mylog( __FILE__, __LINE__, LOG_WARNING, args ) 112 | #define lognotice( args... ) _mylog( __FILE__, __LINE__, LOG_NOTICE, args ) 113 | #define loginfo( args... ) _mylog( __FILE__, __LINE__, LOG_INFO, args ) 114 | #define logdebug( args... ) _mylog( __FILE__, __LINE__, LOG_DEBUG, args ) 115 | void _mylog( const char *file, int line, int prio, const char *fmt, ... ); 116 | int loglevel(); 117 | long long ms_timer(void); 118 | #endif /* sbpd_h */ 119 | -------------------------------------------------------------------------------- /control.h: -------------------------------------------------------------------------------- 1 | // 2 | // control.h 3 | // SqueezeButtonPi 4 | // 5 | // Created by Jörg Schwieder on 02.02.17. 6 | // 7 | // 8 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 9 | // All rights reserved. 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright 15 | // notice, this list of conditions and the following disclaimer. 16 | // * Redistributions in binary form must reproduce the above copyright 17 | // notice, this list of conditions and the following disclaimer in the 18 | // documentation and/or other materials provided with the distribution. 19 | // * Neither the name of ickStream nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 32 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | // 34 | 35 | #ifndef control_h 36 | #define control_h 37 | 38 | #include "sbpd.h" 39 | #include "GPIO.h" 40 | 41 | // 42 | // Store command parameters for each button used 43 | // 44 | struct button_ctrl 45 | { 46 | struct button * gpio_button; 47 | volatile bool waiting; 48 | char * shortfragment; 49 | char * longfragment; 50 | bool presstype; 51 | int cmdtype; 52 | int cmd_longtype; 53 | }; 54 | 55 | // 56 | // Setup button control 57 | // Parameters: 58 | // cmd: Command type LMS. One of 59 | // PLAY - play/pause 60 | // VOL+ - increment volume 61 | // VOL- - decrement volume 62 | // PREV - previous track 63 | // NEXT - next track 64 | // Command type SCRIPT. 65 | // SCRIPT:/path/to/shell/script.sh 66 | // pin: the GPIO-Pin-Number 67 | // edge: one of 68 | // 1 - falling edge 69 | // 2 - rising edge 70 | // 0, 3 - both 71 | // 72 | int setup_button_ctrl(char * cmd, int pin, int resist, int pressed, char * cmd_long, int long_time); 73 | 74 | // 75 | // Polling function: handle button commands 76 | // Parameters: 77 | // server: the server to send commands to 78 | // 79 | void handle_buttons(struct sbpd_server * server); 80 | 81 | // 82 | // Store command parameters for each button used 83 | // 84 | struct encoder_ctrl 85 | { 86 | struct encoder * gpio_encoder; 87 | volatile long last_value; 88 | char * fragment; 89 | int limit; 90 | volatile long long last_time; 91 | int min_time; 92 | }; 93 | // 94 | // Setup encoder control 95 | // Parameters: 96 | // cmd: Command. Currently only 97 | // VOLU - volume 98 | // Can be NULL for volume or actually anything since it's ignored 99 | // pin1: the GPIO-Pin-Number for the first pin used 100 | // pin2: the GPIO-Pin-Number for the second pin used 101 | // edge: one of 102 | // 1 - falling edge 103 | // 2 - rising edge 104 | // 0, 3 - both 105 | // 106 | int setup_encoder_ctrl(char * cmd, int pin1, int pin2, int edge); 107 | 108 | // 109 | // Polling function: handle encoders 110 | // Parameters: 111 | // server: the server to send commands to 112 | // 113 | void handle_encoders(struct sbpd_server * server); 114 | 115 | 116 | // 117 | // Set of commands to send to LMS server 118 | // 119 | #define MAXLEN 255 120 | #define MAX_COMMANDS 80 121 | struct lms_command { 122 | int code; 123 | char fragment[MAXLEN]; 124 | }; 125 | 126 | int add_lms_command_frament ( char * name, char * value ); 127 | 128 | 129 | #endif /* control_h */ 130 | -------------------------------------------------------------------------------- /GPIO.h: -------------------------------------------------------------------------------- 1 | // 2 | // GPIO.h 3 | // SqueezeButtonPi 4 | // 5 | // Created by Jörg Schwieder on 02.02.17. 6 | // 7 | // 8 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 9 | // All rights reserved. 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright 15 | // notice, this list of conditions and the following disclaimer. 16 | // * Redistributions in binary form must reproduce the above copyright 17 | // notice, this list of conditions and the following disclaimer in the 18 | // documentation and/or other materials provided with the distribution. 19 | // * Neither the name of ickStream nor the names of its contributors 20 | // may be used to endorse or promote products derived from this software 21 | // without specific prior written permission. 22 | // 23 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 32 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | // 34 | 35 | #ifndef GPIO_h 36 | #define GPIO_h 37 | 38 | #include "sbpd.h" 39 | #include "time.h" 40 | 41 | 42 | // 43 | // 44 | // Init GPIO functionality 45 | // Essentially just initialized WiringPi to use GPIO pin numbering 46 | // 47 | // 48 | void init_GPIO(); 49 | 50 | // 51 | // Buttons and Rotary Encoders 52 | // Rotary Encoder taken from https://github.com/astine/rotaryencoder 53 | // http://theatticlight.net/posts/Reading-a-Rotary-Encoder-from-a-Raspberry-Pi/ 54 | // 55 | 56 | //17 pins / 2 pins per encoder = 8 maximum encoders 57 | #define max_encoders 8 58 | //17 pins / 1 pins per button = 17 maximum buttons 59 | #define max_buttons 17 60 | 61 | struct button; 62 | 63 | #define SHORTPRESS 0 64 | #define LONGPRESS 1 65 | 66 | // 67 | // A callback executed when a button gets triggered. Button struct and change returned. 68 | // Note: change might be "0" indicating no change, this happens when buttons chatter 69 | // Value in struct already updated. 70 | // 71 | typedef void (*button_callback_t)(const struct button * button, int change, bool presstype); 72 | 73 | struct button { 74 | int pin; 75 | volatile bool value; 76 | button_callback_t callback; 77 | uint32_t timepressed; 78 | bool pressed; 79 | int long_press_time; 80 | }; 81 | 82 | // 83 | // 84 | // Configuration function to define a button 85 | // Should be run for every button you want to control 86 | // For each button a button struct will be created 87 | // 88 | // Parameters: 89 | // pin: GPIO-Pin used in BCM numbering scheme 90 | // callback: callback function to be called when button state changed 91 | // edge: edge to be used for trigger events, 92 | // one of INT_EDGE_RISING, INT_EDGE_FALLING or INT_EDGE_BOTH (the default) 93 | // Returns: pointer to the new button structure 94 | // The pointer will be NULL is the function failed for any reason 95 | // 96 | // 97 | struct button *setupbutton(int pin, 98 | button_callback_t callback, 99 | int resist, 100 | bool pressed, 101 | int long_press_time); 102 | 103 | 104 | struct encoder; 105 | 106 | // 107 | // A callback executed when a rotary encoder changes it's value. 108 | // Encoder struct and change returned. 109 | // Value in struct already updated. 110 | // 111 | typedef void (*rotaryencoder_callback_t)(const struct encoder * encoder, long change); 112 | 113 | struct encoder 114 | { 115 | int pin_a; 116 | int pin_b; 117 | volatile long value; 118 | volatile int lastEncoded; 119 | rotaryencoder_callback_t callback; 120 | }; 121 | 122 | // 123 | // 124 | // Configuration function to define a rotary encoder 125 | // Should be run for every rotary encoder you want to control 126 | // For each encoder a button struct will be created 127 | // 128 | // Parameters: 129 | // pin_a, pin_b: GPIO-Pins used in BCM numbering scheme 130 | // callback: callback function to be called when encoder state changed 131 | // edge: edge to be used for trigger events, 132 | // one of INT_EDGE_RISING, INT_EDGE_FALLING or INT_EDGE_BOTH (the default) 133 | // Returns: pointer to the new encoder structure 134 | // The pointer will be NULL is the function failed for any reason 135 | // 136 | // 137 | struct encoder *setupencoder(int pin_a, 138 | int pin_b, 139 | rotaryencoder_callback_t callback, 140 | int edge); 141 | 142 | 143 | 144 | 145 | 146 | #endif /* GPIO_h */ 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # SqeezeButtonPi Daemon 3 | 4 | SqueezeButtonPi Daemon or sbpd is a controller tool to use buttons and rotary encoders to control an instance of SqueezeLite or SqueezePlay running on the same Raspberry Pi device in connection with a Logitech Media Server/Squeezebox Server instance. 5 | 6 | Rotary encoders or rotary-push-encoders can be used for volume, buttons (and the push-function of a rotary encoder) can be used for play/pause, skip forward, skip back or toggle the power state. 7 | 8 | ## COPYRIGHT / LICENSE 9 | 10 | Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 11 | All rights reserved. 12 | 13 | Redistribution and use in source and binary forms, with or without 14 | modification, are permitted provided that the following conditions are met: 15 | 16 | * Redistributions of source code must retain the above copyright 17 | notice, this list of conditions and the following disclaimer. 18 | * Redistributions in binary form must reproduce the above copyright 19 | notice, this list of conditions and the following disclaimer in the 20 | documentation and/or other materials provided with the distribution. 21 | * Neither the name of ickStream nor the names of its contributors 22 | may be used to endorse or promote products derived from this software 23 | without specific prior written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 28 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 29 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 30 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 32 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 33 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 34 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | 36 | ## Dependencies 37 | 38 | SqueezeButtonPi uses [WiringPi](http://wiringpi.com "WiringPi") and libCurl 39 | 40 | ## Configuration 41 | 42 | Usage: 43 | `sbpd [OPTION...] [e,pin1,pin2,CMD,edge] [b,pin,CMD,edge...]` 44 | 45 | Options arguments: 46 | 47 | -A, --address=Server-Address Set server address. Default: autodetect 48 | -f, --conf_file= 49 | Full path to command configuration file 50 | -M, --mac=MAC-Address Set MAC address of player. Deafult: autodetect 51 | -p, --password=password Set password for server. Default: none 52 | -P, --port=xxxx Set server control port. Default: autodetect 53 | -u, --username=user name Set user name for server. Default: none 54 | -d, --daemonize Daemonize 55 | -s, --silent Don't produce output 56 | -v, --verbose Produce verbose output 57 | -z, --debug Produce debug output 58 | -?, --help Give this help list 59 | --usage Give a short usage message 60 | -V, --version Print program version 61 | 62 | Non-Option arguments. 63 | At least one needs to be specified for the daemon to do anything useful 64 | Arguments are a comma-separated list of configuration parameters: 65 | 66 | For rotary encoders (one, volume only): 67 | e,pin1,pin2,CMD[,edge] 68 | "e" for "Encoder" 69 | p1, p2: GPIO PIN numbers in BCM-notation 70 | CMD: Command. Unused for encoders, always VOLM for Volume 71 | edge: Optional. one of 72 | 1 - falling edge 73 | 2 - rising edge 74 | 0, 3 - both 75 | For buttons: 76 | b,pin,CMD[,resist,pressed,CMD_LONG,long_time] 77 | "b" for "Button" 78 | pin: GPIO PIN numbers in BCM-notation 79 | CMD: Command. One of: 80 | PLAY: Play/pause 81 | PREV: Jump to previous track 82 | NEXT: Jump to next track 83 | VOL+: Increase volume 84 | VOL-: Decrease volume 85 | POWR: Toggle power state 86 | Commands can be defined in config file 87 | use -f option, ref:sbpd_commands.cfg 88 | Command type SCRIPT. 89 | SCRIPT:/path/to/shell/script.sh 90 | resist: Optional. one of 91 | 0 - Internal resistor off 92 | 1 - pull down - input puts 3v on GPIO pin 93 | 2 - pull up (default) - input pulls GPIO pin to ground 94 | pressed: Optional GPIO pinstate for button to read pressed 95 | 0 - state is 0 (default) 96 | 1 - state is 1 97 | CMD_LONG: Command to be used for a long button push, see above command list 98 | long_time: Number of milliseconds to define a long press 99 | 100 | ## Command configuration file 101 | 102 | # 103 | # sbpd_commands.cfg - Custom commands definition 104 | # 105 | # = 106 | # 107 | # CODE - MUST be a 4 character code, to be reference on command line when defining buttons 108 | # 109 | # For commands reference the LMS cli documentation, commands are to be JSON formatted. 110 | # 111 | # Default commands 112 | PLAY=["pause"] 113 | VOL-=["button","voldown"] 114 | VOL+=["button","volup"] 115 | PREV=["button","rew"] 116 | NEXT=["button","fwd"] 117 | POWR=["button","power"]" 118 | MIX+=["mixer","volume","+5"] 119 | MIX-=["mixer","volume","-5"] 120 | 121 | ## Security 122 | 123 | One issue with this code is that since it uses WiringPi it needs to be run with root privileges. 124 | This is not a particularly good idea given that it also communicates over the network so if you run this on the Raspberry Pi controlling your nuclear powerplant in the backyard I would at least advice against exposing it to the internet. 125 | A better architecture would probably be to fork a separate process running with more limited user rights for the networking stuff. 126 | 127 | ## Limitations 128 | 129 | ### IPv6 130 | In the current state, this will probably not work in IPv6-only networks. 131 | 132 | ### Server Switching 133 | 134 | The controller will follow the player if you switch the player to a new server. 135 | This might not work with a remote server but should be reliable in an IPv4 network 136 | 137 | ### Encoder Speed 138 | 139 | Server commands are fed to the server using a very simple scheduler on the main thread. Up to 10 commands per second can be sent but since all requests are being sent synchronously this depends on the reaction speed of the server. 140 | The result of this is that very fast command sequences can result in jumping volume levels and delayed volume changes. 141 | 142 | ### Multiple Players 143 | 144 | Probably not a limitation on a Pi. Only a single instance of SqueezeLite should be running if autodetection is being used since the code only looks for the first connection on port 3483. 145 | With more than one player the server being found will be random. In such a setup, manual server configuration will be required. 146 | 147 | ### Multiple Network Interfaces 148 | 149 | The MAC address detection is borrowed from SqueezeLite so when running automatically the MAC found should be the same used by SqueezeLite. 150 | If that's not the case in setups with more than one MAC or SqueezeLite uses a manually configured MAC manual MAC configuration should be used. 151 | 152 | ### MySqueezebox.com 153 | 154 | This tool will only work with Logitech Media Server, it doesn't authenticate against MySqueezebox.com. While authentication against MySqueezebox.com should generally be possible for a controller it should be unneeded in this case because DIY and 3rd party players are not supposed to directly connect to MySqueezebox.com anyway and the design of this tool is to control a player running on the same device as this controller. 155 | -------------------------------------------------------------------------------- /servercomm.c: -------------------------------------------------------------------------------- 1 | // 2 | // servercomm.c 3 | // SqueezeButtonPi 4 | // 5 | // Send CLI commands to the server 6 | // 7 | // Created by Jörg Schwieder on 02.02.17. 8 | // 9 | // 10 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 11 | // All rights reserved. 12 | // 13 | // Redistribution and use in source and binary forms, with or without 14 | // modification, are permitted provided that the following conditions are met: 15 | // 16 | // * Redistributions of source code must retain the above copyright 17 | // notice, this list of conditions and the following disclaimer. 18 | // * Redistributions in binary form must reproduce the above copyright 19 | // notice, this list of conditions and the following disclaimer in the 20 | // documentation and/or other materials provided with the distribution. 21 | // * Neither the name of ickStream nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software 23 | // without specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 28 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 29 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 30 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 31 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 32 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 33 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 34 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | #include "servercomm.h" 38 | #include "sbpd.h" 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | // 45 | // lock for asynchronous sending of commands - we don't do this right now 46 | // 47 | /*#include 48 | 49 | static volatile bool commLock; 50 | static pthread_mutex_t lock;*/ 51 | 52 | static CURL *curl; 53 | static char * MAC = NULL; 54 | static struct curl_slist * headerList = NULL; 55 | 56 | #define JSON_CALL_MASK "{\"id\":%ld,\"method\":\"slim.request\",\"params\":[\"%s\",%s]}" 57 | #define SERVER_ADDRESS_TEMPLATE "http://localhost/jsonrpc.js" 58 | 59 | // 60 | // 61 | // Send CLI command fragment to Logitech Media Server/Squeezebox Server 62 | // This command blocks (synchronously communicates". 63 | // In timing critical situations this should be called from a separate thread 64 | // 65 | // Parameters: 66 | // server: the server information structure defining host, port etc. 67 | // frament: the command fragment to be sent as JSON array 68 | // e.g. "[\"mixer\”,\"volume\",\"+2\"]" 69 | // optionally: some CLI commands can take parameter hashes as "params:{}" 70 | // Returns: success flag 71 | // 72 | // 73 | bool send_command(struct sbpd_server * server, int command, char * fragment) { 74 | loginfo("Send Command:%d, Fragment:%s", command, fragment); 75 | if ( command == LMS ) { 76 | if (!curl) 77 | return false; 78 | 79 | // 80 | // Sending commands asynchronously? Secure with a mutex 81 | // But right now we are actually not doing that, so comment out 82 | // 83 | /*pthread_mutex_lock(&lock); 84 | if (commLock) { 85 | pthread_mutex_unlock(&lock); 86 | return false; 87 | } 88 | commLock = true; 89 | pthread_mutex_unlock(&lock);*/ 90 | 91 | // 92 | // target setup. We call an IPv4 ip so we need to replace a default host 93 | // 94 | struct curl_slist * targetList = NULL; 95 | 96 | curl_easy_setopt(curl, CURLOPT_URL, SERVER_ADDRESS_TEMPLATE); 97 | char target[100]; 98 | snprintf(target, sizeof(target), "::%s:%d", server->host, server->port); 99 | //logdebug("Command Target: %s", target); 100 | targetList = curl_slist_append(targetList, target); 101 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); 102 | curl_easy_setopt(curl, CURLOPT_CONNECT_TO, targetList); 103 | 104 | // Setup an error buffer to log errors 105 | char errbuf[CURL_ERROR_SIZE]; 106 | curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); 107 | errbuf[0] = 0; 108 | // 109 | // username/password? 110 | // 111 | char secret[255]; 112 | if (server->user && server->password) { 113 | snprintf(secret, sizeof(secret), "%s:%s", server->user, server->password); 114 | curl_easy_setopt(curl, CURLOPT_USERPWD, secret); 115 | } 116 | 117 | // 118 | // setup payload (JSON/RPC CLI command) for POST command 119 | // 120 | char jsonFragment[256]; 121 | snprintf(jsonFragment, sizeof(jsonFragment), JSON_CALL_MASK, 1l, MAC, fragment); 122 | logdebug("Server %s command: %s", target, jsonFragment); 123 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonFragment); 124 | if (headerList) 125 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); 126 | 127 | // 128 | // Send command and clean up 129 | // Note: one could retrieve a result here since all communication is synchronous! 130 | // 131 | CURLcode res = curl_easy_perform(curl); 132 | if(res != CURLE_OK) { 133 | size_t len = strlen(errbuf); 134 | loginfo("Curl Error: (%d) ", res); 135 | if(len) 136 | loginfo( "%s%s", errbuf,((errbuf[len - 1] != '\n') ? "\n" : "")); 137 | else 138 | loginfo( "%s\n", curl_easy_strerror(res)); 139 | } 140 | curl_slist_free_all(targetList); 141 | targetList = NULL; 142 | 143 | //commLock = false; 144 | return true; 145 | } else if ( command == SCRIPT ) { 146 | char * cmdline = (char *) malloc(strlen(fragment)); 147 | int err; 148 | strcpy( cmdline, fragment); 149 | loginfo("Sending commandline: %s\n", cmdline); 150 | if ((err = system(cmdline)) != 0){ 151 | loginfo ("%s exit status = %d\n", cmdline, err); 152 | return false; 153 | } 154 | free (cmdline); 155 | } 156 | return true; 157 | } 158 | 159 | // 160 | // Curl reply callback 161 | // Replies from the server go here. 162 | // We could handle return values here but we just log. 163 | // 164 | size_t write_data(char *buffer, size_t size, size_t nmemb, void *userp) { 165 | if (size) 166 | logdebug("Server reply %s", buffer); 167 | return size * nmemb; 168 | } 169 | 170 | // 171 | // 172 | // Initialize CURL for server communication and set MAC address 173 | // 174 | // 175 | int init_comm(char * use_mac) { 176 | loginfo("Initializing CURL"); 177 | MAC = use_mac; 178 | // 179 | // Setup mutex lock for comm - unused 180 | // 181 | /*if (pthread_mutex_init(&lock, NULL) != 0) { 182 | logerr("comm lock mutex init failed"); 183 | return -1; 184 | }*/ 185 | 186 | // 187 | // Initialize curl comm 188 | // 189 | curl_global_init(CURL_GLOBAL_ALL); 190 | curl = curl_easy_init(); 191 | if (!curl) { 192 | curl_global_cleanup(); 193 | return -1; 194 | } 195 | 196 | // 197 | // Set verbose mode for communication debugging 198 | // 199 | if (loglevel() == LOG_DEBUG) 200 | curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); 201 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); 202 | headerList = curl_slist_append(headerList, "Content-Type: application/json"); 203 | char userAgent[50]; 204 | snprintf(userAgent, sizeof(userAgent), "User-Agent: %s/%s)", USER_AGENT, VERSION); 205 | headerList = curl_slist_append(headerList, userAgent); 206 | // 207 | // Add session-ID? Only needed for MySB which is not supported 208 | // 209 | //headerList = curl_slist_append(headerList, "x-sdi-squeezenetwork-session: ...") 210 | return 0; 211 | } 212 | 213 | // 214 | // 215 | // Shutdown CURL 216 | // 217 | // 218 | void shutdown_comm() { 219 | curl_slist_free_all(headerList); 220 | curl_easy_cleanup(curl); 221 | curl_global_cleanup(); 222 | } 223 | 224 | -------------------------------------------------------------------------------- /GPIO.c: -------------------------------------------------------------------------------- 1 | // 2 | // GPIO.c 3 | // SqueezeButtonPi 4 | // 5 | // Low-Level code to configure and read buttons and rotary encoders. 6 | // Calls callbacks on activity 7 | // 8 | // Created by Jörg Schwieder on 02.02.17. 9 | // 10 | // 11 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 12 | // All rights reserved. 13 | // 14 | // Redistribution and use in source and binary forms, with or without 15 | // modification, are permitted provided that the following conditions are met: 16 | // 17 | // * Redistributions of source code must retain the above copyright 18 | // notice, this list of conditions and the following disclaimer. 19 | // * Redistributions in binary form must reproduce the above copyright 20 | // notice, this list of conditions and the following disclaimer in the 21 | // documentation and/or other materials provided with the distribution. 22 | // * Neither the name of ickStream nor the names of its contributors 23 | // may be used to endorse or promote products derived from this software 24 | // without specific prior written permission. 25 | // 26 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 27 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 28 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 29 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 30 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 31 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 34 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 35 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | // 37 | 38 | #include "GPIO.h" 39 | #include "sbpd.h" 40 | 41 | #include 42 | 43 | // 44 | // Configured buttons 45 | // 46 | static int numberofbuttons = 0; 47 | // 48 | // Pre-allocate encoder objects on the stack so we don't have to 49 | // worry about freeing them 50 | // 51 | static struct button buttons[max_buttons]; 52 | 53 | // 54 | // GetTime function 55 | // 56 | uint32_t gettime_ms(void) { 57 | struct timespec ts; 58 | if (!clock_gettime(CLOCK_REALTIME, &ts)) { 59 | return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; 60 | } 61 | return 0; 62 | } 63 | 64 | // 65 | // number of milliseconds to debounce the button 66 | // 67 | #define NOPRESSTIME 50 68 | // 69 | // 70 | // Button handler function 71 | // Called by the GPIO interrupt when a button is pressed or released 72 | // Depends on edge configuration. 73 | // Checks all configred buttons for status changes and 74 | // calls callback if state change detected. 75 | // 76 | // 77 | void updateButtons() 78 | { 79 | uint32_t now; 80 | now = gettime_ms(); 81 | struct button *button = buttons; 82 | for (; button < buttons + numberofbuttons; button++) 83 | { 84 | bool bit = digitalRead(button->pin); 85 | bool presstype; 86 | logdebug("%lu - %lu= %i Pin Value=%i Stored Value=%i", (unsigned long)now, (unsigned long)button->timepressed, (signed int)(now - button->timepressed), bit, button->value); 87 | 88 | int increment = 0; 89 | if ( (bit == button->pressed) && (button->timepressed == 0) ){ 90 | button->timepressed = now; 91 | increment = 0; 92 | } else if (button->timepressed != 0){ 93 | if ((signed int)(now - button->timepressed) < (signed int)NOPRESSTIME ) { 94 | logdebug("No PRESS: %i", (signed int)(now - button->timepressed)); 95 | increment = 0; 96 | } else if ((signed int)(now - button->timepressed) > (signed int)button->long_press_time ) { 97 | loginfo("Long PRESS: %i", (signed int)(now - button->timepressed)); 98 | button->value = bit; 99 | presstype = LONGPRESS; 100 | increment = 1; 101 | } else { 102 | loginfo("Short PRESS: %i", (signed int)(now - button->timepressed)); 103 | button->value = bit; 104 | presstype = SHORTPRESS; 105 | increment = 1; 106 | } 107 | button->timepressed = 0; 108 | } 109 | if (button->callback && increment) 110 | button->callback(button, increment, presstype); 111 | } 112 | } 113 | 114 | // 115 | // 116 | // Configuration function to define a button 117 | // Should be run for every button you want to control 118 | // For each button a button struct will be created 119 | // 120 | // Parameters: 121 | // pin: GPIO-Pin used in BCM numbering scheme 122 | // callback: callback function to be called when button state changed 123 | // edge: edge to be used for trigger events, 124 | // one of INT_EDGE_RISING, INT_EDGE_FALLING or INT_EDGE_BOTH (the default) 125 | // Returns: pointer to the new button structure 126 | // The pointer will be NULL is the function failed for any reason 127 | // 128 | // 129 | struct button *setupbutton(int pin, button_callback_t callback, int resist, bool pressed, int long_press_time) 130 | { 131 | if (numberofbuttons > max_buttons) 132 | { 133 | logerr("Maximum number of buttons exceded: %i", max_buttons); 134 | return NULL; 135 | } 136 | 137 | int edge = INT_EDGE_BOTH; //Need to see both directions for button depressed time. 138 | 139 | struct button *newbutton = buttons + numberofbuttons++; 140 | newbutton->pin = pin; 141 | newbutton->value = 0; 142 | newbutton->callback = callback; 143 | newbutton->timepressed = 0; 144 | newbutton->pressed = pressed; 145 | newbutton->long_press_time = long_press_time; 146 | pinMode(pin, INPUT); 147 | pullUpDnControl(pin, resist); 148 | wiringPiISR(pin,edge, updateButtons); 149 | 150 | return newbutton; 151 | } 152 | 153 | // 154 | // 155 | // Encoders 156 | // Rotary Encoder taken from https://github.com/astine/rotaryencoder 157 | // http://theatticlight.net/posts/Reading-a-Rotary-Encoder-from-a-Raspberry-Pi/ 158 | // 159 | // 160 | // Configured encoders 161 | // 162 | static int numberofencoders = 0; 163 | // 164 | // Pre-allocate encoder objects on the stack so we don't have to 165 | // worry about freeing them 166 | // 167 | static struct encoder encoders[max_encoders]; 168 | 169 | // 170 | // 171 | // Encoder handler function 172 | // Called by the GPIO interrupt when encoder is rotated 173 | // Depends on edge configuration 174 | // 175 | // 176 | void updateEncoders() 177 | { 178 | struct encoder *encoder = encoders; 179 | for (; encoder < encoders + numberofencoders; encoder++) 180 | { 181 | int MSB = digitalRead(encoder->pin_a); 182 | int LSB = digitalRead(encoder->pin_b); 183 | 184 | int encoded = (MSB << 1) | LSB; 185 | int sum = (encoder->lastEncoded << 2) | encoded; 186 | 187 | int increment = 0; 188 | 189 | if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) increment = 1; 190 | if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) increment = -1; 191 | 192 | encoder->value += increment; 193 | 194 | encoder->lastEncoded = encoded; 195 | if (encoder->callback) 196 | encoder->callback(encoder, increment); 197 | } 198 | } 199 | 200 | // 201 | // 202 | // Configuration function to define a rotary encoder 203 | // Should be run for every rotary encoder you want to control 204 | // For each encoder a button struct will be created 205 | // 206 | // Parameters: 207 | // pin_a, pin_b: GPIO-Pins used in BCM numbering scheme 208 | // callback: callback function to be called when encoder state changed 209 | // edge: edge to be used for trigger events, 210 | // one of INT_EDGE_RISING, INT_EDGE_FALLING or INT_EDGE_BOTH (the default) 211 | // Returns: pointer to the new encoder structure 212 | // The pointer will be NULL is the function failed for any reason 213 | // 214 | // 215 | struct encoder *setupencoder(int pin_a, 216 | int pin_b, 217 | rotaryencoder_callback_t callback, 218 | int edge) 219 | { 220 | if (numberofencoders > max_encoders) 221 | { 222 | logerr("Maximum number of encodered exceded: %i", max_encoders); 223 | return NULL; 224 | } 225 | 226 | if (edge != INT_EDGE_FALLING && edge != INT_EDGE_RISING) 227 | edge = INT_EDGE_BOTH; 228 | 229 | struct encoder *newencoder = encoders + numberofencoders++; 230 | newencoder->pin_a = pin_a; 231 | newencoder->pin_b = pin_b; 232 | newencoder->value = 0; 233 | newencoder->lastEncoded = 0; 234 | newencoder->callback = callback; 235 | 236 | pinMode(pin_a, INPUT); 237 | pinMode(pin_b, INPUT); 238 | pullUpDnControl(pin_a, PUD_UP); 239 | pullUpDnControl(pin_b, PUD_UP); 240 | wiringPiISR(pin_a,edge, updateEncoders); 241 | wiringPiISR(pin_b,edge, updateEncoders); 242 | 243 | return newencoder; 244 | } 245 | 246 | // 247 | // 248 | // Init GPIO functionality 249 | // Essentially just initialized WiringPi to use GPIO pin numbering 250 | // 251 | // 252 | void init_GPIO() { 253 | loginfo("Initializing GPIO"); 254 | wiringPiSetupGpio() ; 255 | } 256 | 257 | 258 | 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /control.c: -------------------------------------------------------------------------------- 1 | // 2 | // control.c 3 | // SqueezeButtonPi 4 | // 5 | // The actual control code 6 | // - Configure buttons and rotary encoders 7 | // - Send commands when buttons and rotary encoders are used 8 | // 9 | // Created by Jörg Schwieder on 02.02.17. 10 | // 11 | // 12 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 13 | // All rights reserved. 14 | // 15 | // Redistribution and use in source and binary forms, with or without 16 | // modification, are permitted provided that the following conditions are met: 17 | // 18 | // * Redistributions of source code must retain the above copyright 19 | // notice, this list of conditions and the following disclaimer. 20 | // * Redistributions in binary form must reproduce the above copyright 21 | // notice, this list of conditions and the following disclaimer in the 22 | // documentation and/or other materials provided with the distribution. 23 | // * Neither the name of ickStream nor the names of its contributors 24 | // may be used to endorse or promote products derived from this software 25 | // without specific prior written permission. 26 | // 27 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 28 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 29 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 30 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 31 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 32 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 34 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 35 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 36 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | // 38 | 39 | #include "sbpd.h" 40 | #include "control.h" 41 | #include "servercomm.h" 42 | #include 43 | #include 44 | #include 45 | #include 46 | 47 | // 48 | // Pre-allocate encoder and button objects on the stack so we don't have to 49 | // worry about freeing them 50 | // 51 | static struct button_ctrl button_ctrls[max_buttons]; 52 | static struct encoder_ctrl encoder_ctrls[max_encoders]; 53 | static int numberofbuttons = 0; 54 | static int numberofencoders = 0; 55 | 56 | // 57 | // Command fragments 58 | // 59 | // Buttons 60 | // 61 | /*#define FRAGMENT_PAUSE "[\"pause\"]" 62 | #define FRAGMENT_VOLUME_UP "[\"button\",\"volup\"]" 63 | #define FRAGMENT_VOLUME_DOWN "[\"button\",\"voldown\"]" 64 | #define FRAGMENT_PREV "[\"button\",\"rew\"]" 65 | #define FRAGMENT_NEXT "[\"button\",\"fwd\"]" 66 | #define FRAGMENT_POWER "[\"button\",\"power\"]" 67 | */ 68 | // 69 | // Encoder 70 | // 71 | #define FRAGMENT_VOLUME "[\"mixer\",\"volume\",\"%s%d\"]" 72 | #define FRAGMENT_TRACK "[\"playlist\",\"jump\",\"%s%d\"]" 73 | 74 | // 75 | // LMS Command structure 76 | // 77 | static struct lms_command lms_commands[MAX_COMMANDS]; 78 | static int numberofcommands = 0; 79 | 80 | int add_lms_command_frament ( char * name, char * value ) { 81 | loginfo("Adding Command %s: Fragment %s", name, value); 82 | lms_commands[numberofcommands].code = STRTOU32(name); 83 | strncpy (lms_commands[numberofcommands].fragment, value, MAXLEN); 84 | numberofcommands ++; 85 | if (numberofcommands > MAX_COMMANDS ) 86 | return 1; 87 | return 0; 88 | } 89 | 90 | char * get_lms_command_fragment ( int code ) { 91 | for (int i = 0; i < numberofcommands; i++) { 92 | if ( code == lms_commands[i].code ) 93 | return lms_commands[i].fragment; 94 | } 95 | return NULL; 96 | } 97 | 98 | // 99 | // Button press callback 100 | // Sets the flag for "button pressed" 101 | // 102 | void button_press_cb(const struct button * button, int change, bool presstype) { 103 | for (int cnt = 0; cnt < numberofbuttons; cnt++) { 104 | if (button == button_ctrls[cnt].gpio_button) { 105 | button_ctrls[cnt].presstype = presstype; 106 | button_ctrls[cnt].waiting = true; 107 | loginfo("Button CB set for button #:%d, gpio pin %d", cnt, button_ctrls[cnt].gpio_button->pin); 108 | return; 109 | } 110 | } 111 | } 112 | 113 | // 114 | // Setup button control 115 | // Parameters: 116 | // pin: the GPIO-Pin-Number 117 | // cmd: Command. One of 118 | // PLAY - play/pause 119 | // VOL+ - increment volume 120 | // VOL- - decrement volume 121 | // PREV - previous track 122 | // NEXT - next track 123 | // Command type SCRIPT. 124 | // SCRIPT:/path/to/shell/script.sh 125 | // resist: Optional. one of 126 | // 0 - Internal resistor off 127 | // 1 - pull down - input puts 3v on GPIO pin 128 | // 2 - pull up (default) - input pulls GPIO pin to ground 129 | // pressed: Optional GPIO pinstate for button to read pressed 130 | // 0 - state is 0 (default) 131 | // 1 - state is 1 132 | // cmd_long Command to be used for a long button push, see above command list 133 | // long_time: Number of milliseconds to define a long press 134 | 135 | int setup_button_ctrl(char * cmd, int pin, int resist, int pressed, char * cmd_long, int long_time) { 136 | char * fragment = NULL; 137 | char * fragment_long = NULL; 138 | char * script; 139 | char * script_long; 140 | char * separator = ":"; 141 | int cmdtype; 142 | int cmd_longtype; 143 | 144 | // 145 | // Select fragment for short press parameter 146 | // 147 | if (strlen(cmd) == 4) { 148 | fragment = get_lms_command_fragment(STRTOU32(cmd)); 149 | cmdtype = LMS; 150 | } else if (strncmp("SCRIPT:", cmd, 7) == 0) { 151 | cmdtype = SCRIPT; 152 | strtok( cmd, separator ); 153 | script = strtok( NULL, "" ); 154 | fragment = script; 155 | } 156 | if (!fragment){ 157 | loginfo("Command %s, not found in defined commands", cmd); 158 | return -1; 159 | } 160 | 161 | // 162 | // Select fragment for long press parameter 163 | // 164 | if ( cmd_long == NULL ) { 165 | cmd_longtype = NOTUSED; 166 | } else if ( strlen(cmd_long) == 4 ) { 167 | fragment_long = get_lms_command_fragment(STRTOU32(cmd_long)); 168 | cmd_longtype = LMS; 169 | } else if (strncmp("SCRIPT:", cmd_long, 7) == 0) { 170 | cmd_longtype = SCRIPT; 171 | strtok( cmd_long, separator ); 172 | script_long = strtok( NULL, "" ); 173 | fragment_long = script_long; 174 | } 175 | if ( (cmd_long != NULL) & (!fragment_long) ){ 176 | loginfo("Command %s, not found in defined commands", cmd_long); 177 | cmd_longtype = NOTUSED; 178 | } 179 | 180 | // Make sure resistor setting makes sense, or reset to default 181 | if ( (resist != PUD_OFF) && (resist != PUD_DOWN) && (resist == PUD_UP) ) 182 | resist = PUD_UP; 183 | 184 | struct button * gpio_b = setupbutton(pin, button_press_cb, resist, (bool)(pressed == 0) ? 0 : 1, long_time); 185 | 186 | button_ctrls[numberofbuttons].cmdtype = cmdtype; 187 | button_ctrls[numberofbuttons].shortfragment = fragment; 188 | button_ctrls[numberofbuttons].cmd_longtype = cmd_longtype; 189 | button_ctrls[numberofbuttons].longfragment = fragment_long; 190 | button_ctrls[numberofbuttons].waiting = false; 191 | button_ctrls[numberofbuttons].gpio_button = gpio_b; 192 | numberofbuttons++; 193 | loginfo("Button defined: Pin %d, BCM Resistor: %s, Short Type: %s, Short Fragment: %s , Long Type: %s, Long Fragment: %s, Long Press Time: %i", 194 | 195 | pin, 196 | (resist == PUD_OFF) ? "both" : 197 | (resist == PUD_DOWN) ? "down" : "up", 198 | (cmdtype == LMS) ? "LMS" : 199 | (cmdtype == SCRIPT) ? "Script" : "unused", 200 | fragment, 201 | (cmd_longtype == LMS) ? "LMS" : 202 | (cmd_longtype == SCRIPT) ? "Script" : "unused", 203 | fragment_long, 204 | long_time); 205 | return 0; 206 | } 207 | 208 | // 209 | // Polling function: handle button commands 210 | // Parameters: 211 | // server: the server to send commands to 212 | // 213 | void handle_buttons(struct sbpd_server * server) { 214 | //logdebug("Polling buttons"); 215 | for (int cnt = 0; cnt < numberofbuttons; cnt++) { 216 | if (button_ctrls[cnt].waiting) { 217 | loginfo("Button pressed: Pin: %d, Press Type:%s", button_ctrls[cnt].gpio_button->pin, 218 | (button_ctrls[cnt].presstype == LONGPRESS) ? "Long" : "Short" ); 219 | if ( button_ctrls[cnt].presstype == SHORTPRESS ) { 220 | if ( button_ctrls[cnt].shortfragment != NULL ) { 221 | send_command(server, button_ctrls[cnt].cmdtype, button_ctrls[cnt].shortfragment); 222 | } 223 | } 224 | if ( button_ctrls[cnt].presstype == LONGPRESS ) { 225 | if ( button_ctrls[cnt].longfragment != NULL ) { 226 | send_command(server, button_ctrls[cnt].cmd_longtype, button_ctrls[cnt].longfragment); 227 | } else { 228 | loginfo("No Long Press command configured"); 229 | } 230 | } 231 | button_ctrls[cnt].waiting = false; // clear waiting 232 | } 233 | } 234 | } 235 | 236 | 237 | // 238 | // Encoder interrupt callback 239 | // Currently does nothing since we poll for volume changes 240 | // 241 | void encoder_rotate_cb(const struct encoder * encoder, long change) { 242 | logdebug("Interrupt: encoder value: %d change: %d", encoder->value, change); 243 | } 244 | 245 | // 246 | // Setup encoder control 247 | // Parameters: 248 | // cmd: Command. Currently only 249 | // VOLU - volume 250 | // TRAC - previous or next track 251 | // Can be NULL for volume 252 | // pin1: the GPIO-Pin-Number for the first pin used 253 | // pin2: the GPIO-Pin-Number for the second pin used 254 | // edge: one of 255 | // 1 - falling edge 256 | // 2 - rising edge 257 | // 0, 3 - both 258 | // 259 | // 260 | int setup_encoder_ctrl(char * cmd, int pin1, int pin2, int edge) { 261 | char * fragment = NULL; 262 | if (strlen(cmd) > 4) 263 | return -1; 264 | // 265 | // Select fragment for parameter 266 | // Would love to "switch" here but that's not portable... 267 | // 268 | uint32_t code = STRTOU32(cmd); 269 | if (code == STRTOU32("VOLU")) { 270 | fragment = FRAGMENT_VOLUME; 271 | encoder_ctrls[numberofencoders].limit = 100; 272 | encoder_ctrls[numberofencoders].min_time = 0; 273 | } else if (code == STRTOU32("TRAC")) { 274 | fragment = FRAGMENT_TRACK; 275 | encoder_ctrls[numberofencoders].limit = 1; 276 | encoder_ctrls[numberofencoders].min_time = 500; 277 | } 278 | if ( fragment == NULL ) { 279 | return -1; 280 | } 281 | 282 | struct encoder * gpio_e = setupencoder(pin1, pin2, encoder_rotate_cb, edge); 283 | encoder_ctrls[numberofencoders].fragment = fragment; 284 | encoder_ctrls[numberofencoders].gpio_encoder = gpio_e; 285 | encoder_ctrls[numberofencoders].last_value = 0; 286 | encoder_ctrls[numberofencoders].last_time = 0; 287 | numberofencoders++; 288 | loginfo("Rotary encoder defined: Pin %d, %d, Edge: %s, Fragment: \n%s", 289 | pin1, pin2, 290 | ((edge != INT_EDGE_FALLING) && (edge != INT_EDGE_RISING)) ? "both" : 291 | (edge == INT_EDGE_FALLING) ? "falling" : "rising", 292 | fragment); 293 | return 0; 294 | } 295 | 296 | // 297 | // Polling function: handle encoder commands 298 | // Parameters: 299 | // server: the server to send commands to 300 | // 301 | void handle_encoders(struct sbpd_server * server) { 302 | // 303 | // chatter filter set duration in encoder setup. 304 | // - volume set to 0... 305 | // - track change set to 500ms 306 | // We poll every 100ms anyway plus wait for network action to complete 307 | // 308 | long long time = ms_timer(); 309 | 310 | //logdebug("Polling encoders"); 311 | 312 | int command = LMS; 313 | 314 | for (int cnt = 0; cnt < numberofencoders; cnt++) { 315 | // 316 | // build volume delta 317 | // ignore if > 100: overflow 318 | // 319 | int delta = (int)(encoder_ctrls[cnt].gpio_encoder->value - encoder_ctrls[cnt].last_value); 320 | if (delta > 100) 321 | delta = 0; 322 | if (delta != 0) { 323 | //Check if change happened before minimum delay, clear out data. 324 | if ( encoder_ctrls[cnt].last_time + encoder_ctrls[cnt].min_time > time ) { 325 | loginfo("Encoder on GPIO %d, %d value change: %d, before %d ms ellapsed not sending lms command.", 326 | encoder_ctrls[cnt].gpio_encoder->pin_a, 327 | encoder_ctrls[cnt].gpio_encoder->pin_b, 328 | delta, 329 | (encoder_ctrls[cnt].min_time) ); 330 | encoder_ctrls[cnt].last_value = encoder_ctrls[cnt].gpio_encoder->value; 331 | return; 332 | } 333 | 334 | loginfo("Encoder on GPIO %d, %d value change: %d", 335 | encoder_ctrls[cnt].gpio_encoder->pin_a, 336 | encoder_ctrls[cnt].gpio_encoder->pin_b, 337 | delta); 338 | 339 | char fragment[50]; 340 | char * prefix = (delta > 0) ? "+" : "-"; 341 | if ( abs(delta) > encoder_ctrls[cnt].limit ) { 342 | delta = encoder_ctrls[cnt].limit; 343 | } 344 | snprintf(fragment, sizeof(fragment), 345 | encoder_ctrls[cnt].fragment, prefix, abs(delta)); 346 | 347 | if (send_command(server, command, fragment)) { 348 | encoder_ctrls[cnt].last_value = encoder_ctrls[cnt].gpio_encoder->value; 349 | encoder_ctrls[cnt].last_time = time; // chatter filter 350 | } 351 | } 352 | } 353 | } 354 | 355 | 356 | 357 | 358 | 359 | -------------------------------------------------------------------------------- /discovery.c: -------------------------------------------------------------------------------- 1 | // 2 | // discovery.c 3 | // SqueezeButtonPi 4 | // 5 | // Find server connection 6 | // Identify the address of the server the player is talking to 7 | // Find the communication port 8 | // 9 | // Created by Jörg Schwieder on 02.02.17. 10 | // 11 | // 12 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 13 | // All rights reserved. 14 | // 15 | // Redistribution and use in source and binary forms, with or without 16 | // modification, are permitted provided that the following conditions are met: 17 | // 18 | // * Redistributions of source code must retain the above copyright 19 | // notice, this list of conditions and the following disclaimer. 20 | // * Redistributions in binary form must reproduce the above copyright 21 | // notice, this list of conditions and the following disclaimer in the 22 | // documentation and/or other materials provided with the distribution. 23 | // * Neither the name of ickStream nor the names of its contributors 24 | // may be used to endorse or promote products derived from this software 25 | // without specific prior written permission. 26 | // 27 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 28 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 29 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 30 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 31 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 32 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 34 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 35 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 36 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | // 38 | 39 | #include "discovery.h" 40 | #include "sbpd.h" 41 | 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | 53 | 54 | // 55 | // Prototypes 56 | // 57 | void update_server(sbpd_config_parameters_t * discovered, 58 | struct sbpd_server * server); 59 | void update_port(); 60 | void _write_server_string(struct sbpd_server * server, in_addr_t s_addr); 61 | bool get_serverIPv4(uint32_t *ip); 62 | void send_discovery(uint32_t address); 63 | uint32_t read_discovery(uint32_t address); 64 | 65 | static bool get_mac(uint8_t mac[]); 66 | 67 | 68 | #define IP_SEARCH_TIMEOUT 3 // every 3 s 69 | 70 | // 71 | // Polling function for server discovery 72 | // Call from main loop 73 | // Handles internal scheduling between different 74 | // 75 | 76 | // 77 | // counter for search scheduling 78 | // 79 | static uint32_t search_timer = 0; 80 | // 81 | // Helper variable; don't want to convert back and forth between string and net-addr 82 | // 83 | static in_addr_t foundAddr = 0; 84 | // 85 | // Parameters: 86 | // config: defines which parameters are preconfigured and will not be discovered 87 | // discovered: the discovered parameters 88 | // server: server configuration 89 | // 90 | void poll_discovery(sbpd_config_parameters_t config, 91 | sbpd_config_parameters_t *discovered, 92 | struct sbpd_server * server) { 93 | //logdebug("Polling server discovery"); 94 | // 95 | // search for server 96 | // 97 | if (!(config & SBPD_cfg_host)) { 98 | if (!search_timer--) { 99 | search_timer = IP_SEARCH_TIMEOUT * SCD_SECOND / SCD_SLEEP_TIMEOUT; 100 | in_addr_t addr = 0; 101 | if (server->host) 102 | addr = inet_addr(server->host); 103 | bool change = get_serverIPv4(&addr); 104 | logdebug("New or changed server address %s", (change) ? "found" : "not found"); 105 | if (change) { 106 | // 107 | // found server but not port 108 | // 109 | *discovered |= SBPD_cfg_host; 110 | *discovered &= ~SBPD_cfg_port; 111 | foundAddr = addr; 112 | 113 | // we don't update server struct, yet, if we also look for the port. 114 | if (config & SBPD_cfg_port) 115 | _write_server_string(server, addr); 116 | // otherwise: look for port 117 | else 118 | send_discovery(addr); 119 | } 120 | } 121 | } 122 | // 123 | // only search if configured to do so and port is not yet found 124 | // 125 | if (!(config & SBPD_cfg_port) && 126 | !(*discovered & SBPD_cfg_port)) { 127 | logdebug("Looking for port"); 128 | uint32_t foundPort = read_discovery(foundAddr); 129 | if (foundPort) { 130 | loginfo("Squeezebox control port found: %d", foundPort); 131 | if (!(config & SBPD_cfg_host)) 132 | _write_server_string(server, foundAddr); 133 | server->port = foundPort; 134 | *discovered |= SBPD_cfg_port; 135 | } 136 | 137 | } 138 | } 139 | 140 | // 141 | // Helper function to convert server address to string 142 | // 143 | void _write_server_string(struct sbpd_server * server, in_addr_t s_addr) { 144 | struct in_addr addr; 145 | addr.s_addr = s_addr; 146 | static char foundServer[16]; // only one server, so we can do this statically 147 | 148 | char * aAddr = inet_ntoa(addr); 149 | loginfo("Server address found: %s", aAddr); 150 | strncpy(foundServer, aAddr, 16); 151 | server->host = foundServer; 152 | } 153 | 154 | // 155 | // Define TCP states here to not have to include kernel header 156 | // 157 | enum { 158 | TCP_ESTABLISHED = 1, 159 | TCP_SYN_SENT, 160 | TCP_SYN_RECV, 161 | TCP_FIN_WAIT1, 162 | TCP_FIN_WAIT2, 163 | TCP_TIME_WAIT, 164 | TCP_CLOSE, 165 | TCP_CLOSE_WAIT, 166 | TCP_LAST_ACK, 167 | TCP_LISTEN, 168 | TCP_CLOSING, // Now a valid state 169 | 170 | TCP_MAX_STATES // Leave at the end! 171 | }; 172 | // 173 | // 174 | // Get server IP from /proc/net/tcp 175 | // 176 | // returns true if server IP was found and changed 177 | // 178 | // 179 | bool get_serverIPv4(uint32_t *ip) { 180 | uint32_t foundIp; 181 | FILE * procTcp = fopen("/proc/net/tcp", "r"); 182 | if (!procTcp) 183 | return false; 184 | char line[256]; 185 | if (!fgets(line, 255, procTcp)) { 186 | fclose(procTcp); 187 | return false; 188 | } 189 | bool found = false; 190 | while (fgets(line, 255, procTcp)) { 191 | logdebug("/proc/net/tcp line: %s", line); 192 | strtok(line, " "); // line number 193 | strtok(NULL, " "); // source address 194 | char * target = strtok(NULL, " "); // target address 195 | char * socketState = strtok(NULL, " "); // socket state 196 | logdebug("target: %s\n", target); 197 | if (!target) { 198 | logwarn("no tcp target found"); 199 | fclose(procTcp); 200 | return false; 201 | } 202 | char * ipString = strtok(target, ":"); 203 | char * portString = strtok(NULL, ":"); 204 | if (!ipString || !portString || !socketState) { 205 | if (!ipString) 206 | logwarn("no ipString found"); 207 | if (!portString) 208 | logwarn("no portString found"); 209 | if (!socketState) 210 | logwarn("no socketState found"); 211 | fclose(procTcp); 212 | return false; 213 | } 214 | char * portComp = "0D9B"; 215 | // 216 | // just be sure the port is upper case 217 | // 218 | char * sTemp = portString; 219 | while ((*sTemp = toupper(*sTemp))) 220 | sTemp++; 221 | // 222 | // state 223 | // 224 | unsigned long uSocketState = strtol(socketState, NULL, 16); 225 | // 226 | // port 3483 and socket state == TCP_ESTABLISHED? 227 | // 228 | if ((*((uint32_t *)portComp) == *((uint32_t *)portString)) && 229 | (uSocketState == TCP_ESTABLISHED)) { 230 | foundIp = (uint32_t)strtoul(ipString, NULL, 16); 231 | if (foundIp != *ip) { 232 | if (!found) // only change once. The rest is for logging only 233 | *ip = foundIp; 234 | loginfo("Found server %s. A new address", ipString); 235 | // no logging: we're done 236 | if (loglevel() < LOG_NOTICE) { 237 | fclose(procTcp); 238 | return true; 239 | } 240 | found = true; 241 | } 242 | logdebug("Found server %s. Same as before"); 243 | // no logging? we're done 244 | if (loglevel() < LOG_NOTICE) { 245 | fclose(procTcp); 246 | return false; 247 | } 248 | } 249 | } 250 | fclose(procTcp); 251 | return found; 252 | } 253 | 254 | static int udpSocket = 0; 255 | static uint32_t udpAddress; 256 | # define SIZE_SERVER_DISCOVERY_LONG 23 257 | # define SBS_UDP_PORT 3483 258 | 259 | // 260 | // get port through server discovery 261 | // 262 | 263 | // 264 | // send server discovery 265 | // 266 | void send_discovery(uint32_t address) { 267 | if (udpSocket) 268 | close(udpSocket); 269 | // create discovery socket 270 | udpSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); 271 | 272 | int yes = 1; 273 | setsockopt(udpSocket, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(int)); 274 | setsockopt(udpSocket, SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes)); 275 | 276 | // send packet 277 | udpAddress = address; 278 | struct sockaddr_in addr4; 279 | memset(&addr4, 0, sizeof(addr4)); 280 | addr4.sin_family = AF_INET; 281 | addr4.sin_port = htons(SBS_UDP_PORT); 282 | addr4.sin_addr.s_addr = address; 283 | 284 | char * data = "eIPAD\0NAME\0JSON\0UUID\0\0\0"; 285 | 286 | size_t error; 287 | error = sendto(udpSocket, data, SIZE_SERVER_DISCOVERY_LONG, 0, (struct sockaddr*)&addr4, sizeof(addr4)); 288 | if (error == -1) 289 | loginfo("Error sending discovery packet"); 290 | } 291 | 292 | 293 | #define BUFSIZE 1600 294 | // 295 | // poll udp port for discovery reply 296 | // 297 | uint32_t read_discovery(uint32_t address) { 298 | char buffer[BUFSIZE]; 299 | static struct sockaddr_in returnAddr; 300 | memset(&returnAddr, 0, sizeof(returnAddr)); 301 | returnAddr.sin_family = AF_INET; 302 | //returnAddr.sin_port = htons(SBS_UDP_PORT); 303 | returnAddr.sin_addr.s_addr = address; 304 | //returnAddr.sin_len = sizeof(returnAddr); 305 | socklen_t addrSize = sizeof(returnAddr); 306 | 307 | ssize_t size = recvfrom(udpSocket, 308 | (void *)buffer, 309 | sizeof(buffer), 310 | MSG_DONTWAIT, 311 | (struct sockaddr *)&returnAddr, 312 | &addrSize); 313 | 314 | if ((size <= 0) || 315 | (buffer[0] != 'E')) { 316 | logdebug("Server discovery: no reply, yet"); 317 | return 0; 318 | } 319 | 320 | logdebug("Server discovery: packet found"); 321 | unsigned int pos = 1; 322 | // char code[5]; 323 | // code[4] = 0; 324 | // this is the whole packet interpretation for debug purposes 325 | // not needed in final code 326 | char port[6]; 327 | strncpy(port, "9000\0", 6); 328 | if (loglevel() >= LOG_NOTICE) { 329 | char name[256]; 330 | memset(name, 0, sizeof(name)); 331 | char UUID[256]; 332 | memset(UUID, 0, sizeof(UUID)); 333 | while (pos < (size - 5)) { 334 | unsigned int fieldLen = buffer[pos + 4]; 335 | uint32_t * selector = (uint32_t *)(buffer + pos); 336 | if (*selector == STRTOU32("NAME")) { 337 | strncpy(name, buffer + pos + 5, MIN(fieldLen, sizeof(name))); 338 | } else if (*selector == STRTOU32("JSON")) { 339 | strncpy(port, buffer + pos + 5, MIN(fieldLen, sizeof(port))); 340 | } else if (*selector == STRTOU32("UUID")) { 341 | strncpy(UUID, buffer + pos + 5, MIN(fieldLen, sizeof(UUID))); 342 | } 343 | pos += fieldLen + 5; 344 | } 345 | loginfo("discovery packet: port: %s, name: %s, uuid: %s", port, name, UUID); 346 | } else { 347 | while (pos < (size - 5)) { 348 | unsigned int fieldLen = buffer[pos + 4]; 349 | uint32_t * selector = (uint32_t *)(buffer + pos); 350 | if (*selector == STRTOU32("JSON")) { 351 | strncpy(port, buffer + pos + 5, MIN(fieldLen, sizeof(port))); 352 | } 353 | pos += fieldLen + 5; 354 | } 355 | loginfo("discovery packet: port: %s", port); 356 | } 357 | close(udpSocket); 358 | 359 | return (uint32_t)strtoul(port, NULL, 10); 360 | } 361 | 362 | 363 | // 364 | // MAC address search 365 | // 366 | // mac address. From SqueezeLite so should match that behaviour. 367 | // search first 4 interfaces returned by IFCONF 368 | // 369 | // Parameters: 370 | // config: defines which parameters are preconfigured and will not be discovered 371 | // returns: MAC string 372 | // 373 | char * find_mac() { 374 | // Find MAC 375 | uint8_t mac[6]; 376 | bool ret = get_mac(mac); 377 | if (!ret) 378 | return NULL; 379 | static char macBuf[18]; 380 | sprintf(macBuf, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); 381 | loginfo("MAC address found: %s", macBuf); 382 | return macBuf; 383 | } 384 | 385 | 386 | // 387 | // Actualy MAC address search 388 | // 389 | // mac address. From SqueezeLite so should match that behaviour. 390 | // search first 4 interfaces returned by IFCONF 391 | // 392 | // Returns 6 bytes MAC. 393 | // 394 | static bool get_mac(uint8_t mac[]) { 395 | #ifdef __unix__ // just to silence errors on Mac while developing 396 | char *utmac; 397 | struct ifconf ifc; 398 | struct ifreq *ifr, *ifend; 399 | struct ifreq ifreq; 400 | struct ifreq ifs[4]; 401 | 402 | utmac = getenv("UTMAC"); 403 | if (utmac) 404 | { 405 | if ( strlen(utmac) == 17 ) 406 | { 407 | if (sscanf(utmac,"%2hhx:%2hhx:%2hhx:%2hhx:%2hhx:%2hhx", 408 | &mac[0],&mac[1],&mac[2],&mac[3],&mac[4],&mac[5]) == 6) 409 | { 410 | return true; 411 | } 412 | } 413 | 414 | } 415 | 416 | mac[0] = mac[1] = mac[2] = mac[3] = mac[4] = mac[5] = 0; 417 | 418 | int s = socket(AF_INET, SOCK_DGRAM, 0); 419 | 420 | ifc.ifc_len = sizeof(ifs); 421 | ifc.ifc_req = ifs; 422 | 423 | if (ioctl(s, SIOCGIFCONF, &ifc) == 0) { 424 | ifend = ifs + (ifc.ifc_len / sizeof(struct ifreq)); 425 | 426 | for (ifr = ifc.ifc_req; ifr < ifend; ifr++) { 427 | if (ifr->ifr_addr.sa_family == AF_INET) { 428 | 429 | strncpy(ifreq.ifr_name, ifr->ifr_name, sizeof(ifreq.ifr_name)); 430 | if (ioctl (s, SIOCGIFHWADDR, &ifreq) == 0) { 431 | memcpy(mac, ifreq.ifr_hwaddr.sa_data, 6); 432 | if (mac[0]+mac[1]+mac[2] != 0) { 433 | close(s); 434 | return true; 435 | } 436 | } 437 | } 438 | } 439 | } 440 | 441 | close(s); 442 | #endif 443 | return false; 444 | } 445 | 446 | 447 | 448 | 449 | -------------------------------------------------------------------------------- /sbpd.c: -------------------------------------------------------------------------------- 1 | // 2 | // main.c 3 | // SqueezeButtonPi 4 | // 5 | // Squeezebox Button Control for Raspberry Pi - Daemon. 6 | // 7 | // Use buttons and rotary encoders to control 8 | // SqueezeLite or SqueezePlay running on a Raspberry Pi 9 | // Works with Logitech Media Server 10 | // 11 | // Created by Jörg Schwieder on 02.02.17. 12 | // 13 | // 14 | // Copyright (c) 2017, Joerg Schwieder, PenguinLovesMusic.com 15 | // All rights reserved. 16 | // 17 | // Redistribution and use in source and binary forms, with or without 18 | // modification, are permitted provided that the following conditions are met: 19 | // 20 | // * Redistributions of source code must retain the above copyright 21 | // notice, this list of conditions and the following disclaimer. 22 | // * Redistributions in binary form must reproduce the above copyright 23 | // notice, this list of conditions and the following disclaimer in the 24 | // documentation and/or other materials provided with the distribution. 25 | // * Neither the name of ickStream nor the names of its contributors 26 | // may be used to endorse or promote products derived from this software 27 | // without specific prior written permission. 28 | // 29 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 30 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 31 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 32 | // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 33 | // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 34 | // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 35 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 36 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 37 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 38 | // EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | // 40 | 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include "sbpd.h" 51 | #include "discovery.h" 52 | #include "servercomm.h" 53 | #include "control.h" 54 | 55 | // 56 | // Server configuration 57 | // 58 | static sbpd_config_parameters_t configured_parameters = 0; 59 | static sbpd_config_parameters_t discovered_parameters = 0; 60 | static struct sbpd_server server; 61 | static char * MAC; 62 | 63 | // 64 | // signal handling 65 | // 66 | static volatile int stop_signal; 67 | static void sigHandler( int sig, siginfo_t *siginfo, void *context ); 68 | 69 | // 70 | // Logging 71 | // 72 | static int streamloglevel = LOG_NOTICE; 73 | static int sysloglevel = LOG_ALERT; 74 | 75 | // 76 | // Argument Parsing 77 | // 78 | const char *argp_program_version = USER_AGENT " " VERSION; 79 | const char *argp_program_bug_address = ""; 80 | static error_t parse_opt(int key, char *arg, struct argp_state *state); 81 | static error_t parse_arg(); 82 | // 83 | // OPTIONS. Field 1 in ARGP. 84 | // Order of fields: {NAME, KEY, ARG, FLAGS, DOC, GROUP}. 85 | // 86 | static struct argp_option options[] = 87 | { 88 | { "conf_file", 'f', "", 0, "Full path to command configuration file", 0}, 89 | { "mac", 'M', "MAC-Address", 0, 90 | "Set MAC address of player. Default: autodetect", 0 }, 91 | { "address", 'A', "Server-Address", 0, 92 | "Set server address. Default: autodetect", 0 }, 93 | { "port", 'P', "xxxx", 0, "Set server control port. Default: autodetect", 0 }, 94 | { "username", 'u', "user name", 0, "Set user name for server. Default: none", 0 }, 95 | { "password", 'p', "password", 0, "Set password for server. Default: none", 0 }, 96 | { "verbose", 'v', 0, 0, "Produce verbose output", 1 }, 97 | { "silent", 's', 0, 0, "Don't produce output", 1 }, 98 | { "daemonize", 'd', 0, 0, "Daemonize", 1 }, 99 | // { "kill", 'k', 0, 0, "Kill daemon", 1 }, 100 | { "debug", 'z', 0, 0, "Produce debug output", 1 }, 101 | {0} 102 | }; 103 | // 104 | // ARGS_DOC. Field 3 in ARGP. 105 | // Non-Option arguments. 106 | static char args_doc[] = "[e,pin1,pin2,CMD,edge] [b,pin,CMD,resist,pressed...]"; 107 | // 108 | // 109 | // DOC. Field 4 in ARGP. 110 | // Program documentation. 111 | // 112 | static char doc[] = "sbpd - SqueezeButtinPiDaemon is a button and rotary encoder handling daemon for Raspberry Pi and a Squeezebox player software.\nsbpd connects to a Squeezebox server and sends the configured control commands on behalf of a player running on the RPi.\n2017 Joerg Schwieder/PenguinLovesMusic.com\n\n\ 113 | At least one needs to be specified for the daemon to do anything useful\n\ 114 | Arguments are a comma-separated list of configuration parameters:\n\ 115 | For rotary encoders (one, volume only):\n\ 116 | e,pin1,pin2,CMD[,edge]\n\ 117 | \"e\" for \"Encoder\"\n\ 118 | p1, p2: GPIO PIN numbers in BCM-notation\n\ 119 | CMD: Command. one of. \n\ 120 | VOLU for Volume\n\ 121 | TRAC for Prev/Next track\n\ 122 | edge: Optional. one of\n\ 123 | 1 - falling edge\n\ 124 | 2 - rising edge\n\ 125 | 0, 3 - both\n\ 126 | For buttons:\n\ 127 | b,pin,CMD[,resist,pressed]\n\ 128 | \"b\" for \"Button\"\n\ 129 | pin: GPIO PIN numbers in BCM-notation\n\ 130 | CMD: Command. One of.\n\ 131 | PLAY - play/pause\n\ 132 | VOL+ - increment volume\n\ 133 | VOL- - decrement volume\n\ 134 | PREV - previous track\n\ 135 | NEXT - next track\n\ 136 | Commands can be defined in config file\n\ 137 | use -f option, ref:sbpd_commands.cfg \n\ 138 | Command type SCRIPT.\n\ 139 | SCRIPT:/path/to/shell/script.sh\n\ 140 | resist: Optional. one of\n\ 141 | 0 - Internal resistor off\n\ 142 | 1 - pull down - input puts 3v on GPIO pin\n\ 143 | 2 - pull up (default) - input pulls GPIO pin to ground\n\ 144 | pressed: Optional GPIO pinstate for button to read pressed\n\ 145 | 0 - state is 0 (default)\n\ 146 | 1 - state is 1\n\ 147 | CMD_LONG: Command to be used for a long button push, see above list\n\ 148 | long_time: Number of milliseconds for a long button press\n"; 149 | // 150 | // ARGP parsing structure 151 | // 152 | static struct argp argp = {options, parse_opt, args_doc, doc}; 153 | static bool arg_daemonize = false; 154 | static char *arg_elements[max_buttons + max_encoders]; 155 | static int arg_element_count = 0; 156 | 157 | int main(int argc, char * argv[]) { 158 | // 159 | // Parse Arguments 160 | // 161 | 162 | argp_parse (&argp, argc, argv, 0, 0, 0); 163 | // 164 | // Parse command config file 165 | // 166 | parse_config(); 167 | 168 | // 169 | // Daemonize 170 | // 171 | if (arg_daemonize) { 172 | int cpid = fork(); 173 | if( cpid == -1 ) { 174 | logerr("Could not fork sbpd"); 175 | return -1; 176 | } 177 | if (cpid) // Parent process exits ... 178 | return 0; 179 | if( setsid() == -1 ) { 180 | logerr("Could not create new session"); 181 | return -2; 182 | } 183 | int fd = open( "/dev/null", O_RDWR, 0 ); 184 | if( fd!=-1) { 185 | dup2(fd, STDIN_FILENO); 186 | dup2(fd, STDOUT_FILENO); 187 | dup2(fd, STDERR_FILENO); 188 | } 189 | } 190 | 191 | // 192 | // Init GPIO 193 | // Done after daemonization becasue child process needs to have GPIO initilized 194 | // 195 | init_GPIO(); 196 | 197 | // 198 | // Now parse GPIO elements 199 | // Needed to initialize GPIO first 200 | // 201 | error_t arg_err = parse_arg(); 202 | 203 | if ( arg_err != 0 ) { 204 | return -2; 205 | } 206 | // 207 | // Configure signal handling 208 | // 209 | struct sigaction act; 210 | memset( &act, 0, sizeof(act) ); 211 | act.sa_sigaction = &sigHandler; 212 | act.sa_flags = SA_SIGINFO; 213 | sigaction( SIGINT, &act, NULL ); 214 | sigaction( SIGTERM, &act, NULL ); 215 | 216 | 217 | // 218 | // Find MAC 219 | // 220 | if (!(configured_parameters & SBPD_cfg_MAC)) { 221 | MAC = find_mac(); 222 | if (!MAC) 223 | return -1; // no MAC, no control 224 | discovered_parameters |= SBPD_cfg_MAC; 225 | } 226 | 227 | // 228 | // Initialize server communication 229 | // 230 | init_comm(MAC); 231 | 232 | // 233 | // 234 | // Main Loop 235 | // 236 | // 237 | loginfo("Starting main loop polling"); 238 | while( !stop_signal ) { 239 | // 240 | // Poll the server discovery 241 | // 242 | poll_discovery(configured_parameters, 243 | &discovered_parameters, 244 | &server); 245 | handle_buttons(&server); 246 | handle_encoders(&server); 247 | // 248 | // Just sleep... 249 | // 250 | usleep( SCD_SLEEP_TIMEOUT ); // 0.1s 251 | 252 | } // end of: while( !stop_signal ) 253 | 254 | // 255 | // Shutdown server communication 256 | // 257 | shutdown_comm(); 258 | 259 | return 0; 260 | } 261 | 262 | // 263 | // 264 | // Argument parsing 265 | // 266 | // 267 | // 268 | // PARSER. Field 2 in ARGP. 269 | // Order of parameters: KEY, ARG, STATE. 270 | // 271 | static error_t 272 | parse_opt (int key, char *arg, struct argp_state *state) 273 | { 274 | switch (key) 275 | { 276 | // 277 | // Output Modes and Misc. 278 | // 279 | // Verbose mode 280 | case 'v': 281 | streamloglevel = LOG_INFO; 282 | loginfo("Options parsing: Set verbose mode"); 283 | break; 284 | // Debug mode 285 | case 'z': 286 | streamloglevel = LOG_DEBUG; 287 | loginfo("Options parsing: Set debug mode"); 288 | break; 289 | // Silent Mode 290 | case 's': 291 | streamloglevel = 0; 292 | loginfo("Options parsing: Set quiet mode"); 293 | break; 294 | // Daemonize 295 | case 'd': 296 | loginfo("Options parsing: Daemonize"); 297 | // don't daemonize here, parse all args first 298 | arg_daemonize = true; 299 | break; 300 | 301 | // 302 | // Player parameter 303 | // 304 | // MAC Address 305 | case 'M': 306 | MAC = arg; 307 | loginfo("Options parsing: Manually set MAC: %s", MAC); 308 | configured_parameters |= SBPD_cfg_MAC; 309 | break; 310 | 311 | // 312 | // Server Parameters 313 | // 314 | // Server address 315 | case 'A': 316 | server.host = arg; 317 | loginfo("Options parsing: Manually set http address %s", server.host); 318 | configured_parameters |= SBPD_cfg_host; 319 | break; 320 | // Server port 321 | case 'P': 322 | server.port = (uint32_t)strtoul(arg, NULL, 10); 323 | loginfo("Options parsing: Manually set http port %s", server.port); 324 | configured_parameters |= SBPD_cfg_port; 325 | break; 326 | // Server user name 327 | case 'u': 328 | server.user = arg; 329 | loginfo("Options parsing: Manually set user name"); 330 | configured_parameters |= SBPD_cfg_user; 331 | break; 332 | // Server password 333 | case 'p': 334 | server.password = arg; 335 | loginfo("Options parsing: Manually set http password"); 336 | configured_parameters |= SBPD_cfg_password; 337 | break; 338 | // Server Configuration file for button commands 339 | case 'f': 340 | server.config_file = arg; 341 | loginfo("Options parsing: Setting command config file to %s", server.config_file); 342 | configured_parameters |= SBPD_cfg_config; 343 | break; 344 | case ARGP_KEY_ARG: 345 | if (arg_element_count == (max_encoders + max_buttons)) { 346 | logerr("Too many control elements defined"); 347 | return ARGP_ERR_UNKNOWN; 348 | } 349 | arg_elements[arg_element_count] = arg; 350 | arg_element_count++; 351 | break; 352 | case ARGP_KEY_END: 353 | break; 354 | default: 355 | return ARGP_ERR_UNKNOWN; 356 | } 357 | return 0; 358 | } 359 | // 360 | // Parse non-option arguments 361 | // 362 | // GPIO devices 363 | // 364 | // Arguments are a comma-separated list of configuration parameters: 365 | // For rotary encoders (one, volume only): 366 | // e,pin1,pin2,CMD[,edge] 367 | // "e" for "Encoder" 368 | // p1, p2: GPIO PIN numbers in BCM-notation 369 | // CMD: VOLU for Volume 370 | // TRAC for Playlist previous/next 371 | // edge: Optional. one of 372 | // 1 - falling edge 373 | // 2 - rising edge 374 | // 0, 3 - both 375 | // For buttons: 376 | // b,pin,CMD[,resist,pressed,CMD_LONG] 377 | // "b" for "Button" 378 | // pin: GPIO PIN numbers in BCM-notation 379 | // CMD: Command. One of 380 | // PLAY - play/pause 381 | // VOL+ - increment volume 382 | // VOL- - decrement volume 383 | // PREV - previous track 384 | // NEXT - next track 385 | // Command type SCRIPT. 386 | // SCRIPT:/path/to/shell/script.sh 387 | // resist: Optional. one of 388 | // 0 - Internal resistor off 389 | // 1 - pull down - input puts 3v on GPIO pin 390 | // 2 - pull up (default) - input pulls GPIO pin to ground 391 | // pressed: Optional GPIO pinstate for button to read pressed 392 | // 0 - state is 0 (default) 393 | // 1 - state is 1 394 | // CMD_LONG: Command to be used for a long button push, see above command list 395 | // long_time: Number of millivoid seconds to define a long press 396 | // 397 | static error_t parse_arg() { 398 | for (int arg_num = 0; arg_num < arg_element_count; arg_num++) { 399 | char * arg = arg_elements[arg_num]; 400 | { 401 | char * code = strtok(arg, ","); 402 | if (strlen(code) != 1) 403 | return ARGP_ERR_UNKNOWN; 404 | switch (code[0]) { 405 | case 'e': { 406 | char * string = strtok(NULL, ","); 407 | int p1 = 0; 408 | if (string) 409 | p1 = (int)strtol(string, NULL, 10); 410 | string = strtok(NULL, ","); 411 | int p2 = 0; 412 | if (string) 413 | p2 = (int)strtol(string, NULL, 10); 414 | char * cmd = strtok(NULL, ","); 415 | string = strtok(NULL, ","); 416 | int edge = 0; 417 | if (string) 418 | edge = (int)strtol(string, NULL, 10); 419 | if ( (p1 == 0) | (p2 == 0) | (cmd == NULL) ) { 420 | logerr("Encoder argument error"); 421 | return ARGP_ERR_UNKNOWN; 422 | } 423 | setup_encoder_ctrl(cmd, p1, p2, edge); 424 | } 425 | break; 426 | case 'b': { 427 | char * string = strtok(NULL, ","); 428 | int pin = 0; 429 | if (string) 430 | pin = (int)strtol(string, NULL, 10); 431 | char * cmd = strtok(NULL, ","); 432 | int resist = 2; 433 | string = strtok(NULL, ","); 434 | if (string) 435 | resist = (int)strtol(string, NULL, 10); 436 | bool pressed = 0; 437 | string = strtok(NULL, ","); 438 | if (string) 439 | pressed = (int)strtol(string, NULL, 10); 440 | char * cmd_long = NULL; 441 | if (string) 442 | cmd_long = strtok(NULL, ","); 443 | string = strtok(NULL, ","); 444 | uint32_t long_time=3000; 445 | if (string) 446 | long_time = (int)strtol(string, NULL, 10); 447 | if ( (pin == 0) | (cmd == NULL) ) { 448 | logerr("Button argument error"); 449 | return ARGP_ERR_UNKNOWN; 450 | } 451 | setup_button_ctrl(cmd, pin, resist, pressed, cmd_long, long_time); 452 | } 453 | break; 454 | 455 | default: 456 | break; 457 | } 458 | } 459 | } 460 | return 0; 461 | } 462 | 463 | void parse_config() { 464 | char *s, buff[256]; 465 | FILE *fp = NULL; 466 | if (configured_parameters & SBPD_cfg_config) { 467 | fp = fopen ( server.config_file, "r"); 468 | if (fp == NULL) loginfo("Config file %s : not found", server.config_file); 469 | } 470 | if (fp == NULL) { 471 | loginfo("Using builtin button configuration"); 472 | add_lms_command_frament ( "PLAY", "[\"pause\"]" ); 473 | add_lms_command_frament ( "VOL+", "[\"button\",\"volup\"]" ); 474 | add_lms_command_frament ( "VOL-", "[\"button\",\"voldown\"]" ); 475 | add_lms_command_frament ( "PREV", "[\"button\",\"rew\"]" ); 476 | add_lms_command_frament ( "NEXT", "[\"button\",\"fwd\"]" ); 477 | add_lms_command_frament ( "POWR", "[\"button\",\"power\"]" ); 478 | return; 479 | } 480 | //Start reading file, line by line 481 | while ((s = fgets (buff, sizeof buff, fp)) != NULL) { 482 | //Skip blank lines and comments 483 | if (buff[0] == '\n' || buff[0] == '#') 484 | continue; 485 | 486 | //Parse name/value pair from line 487 | // names are 4 characters only. 488 | char name[MAXLEN] = ""; 489 | char value[MAXLEN] = ""; 490 | s = strtok (buff, "="); 491 | if (s==NULL) 492 | continue; 493 | else 494 | strncpy (name, s, 4); 495 | s = strtok (NULL, "="); 496 | if (s==NULL) 497 | continue; 498 | else 499 | strncpy (value, s, MAXLEN); 500 | // Remove beginning and trailing whitespace 501 | trim (value); 502 | 503 | loginfo ("name=%s, value=%s", name, value); 504 | if (strlen(name) == 4) { 505 | if ( add_lms_command_frament ( name, value ) != 0 ) { 506 | loginfo ("Too many commands in config file, reduce the number of commands"); 507 | continue; 508 | } 509 | } else { 510 | loginfo ("Invalid or missing commands in config file."); 511 | } 512 | } 513 | fclose (fp); 514 | } 515 | 516 | // 517 | // 518 | // Misc. code 519 | // 520 | // 521 | 522 | // 523 | // trim: get rid of trailing and leading whitespace, including the trailing "\n" from fgets() 524 | // 525 | char * trim (char * s) { 526 | // Initialize start, end pointers 527 | char *s1 = s, *s2 = &s[strlen (s) - 1]; 528 | // Trim and delimit right side 529 | while ( (isspace (*s2)) && (s2 >= s1) ) 530 | s2--; 531 | *(s2+1) = '\0'; 532 | 533 | // Trim left side 534 | while ( (isspace (*s1)) && (s1 < s2) ) 535 | s1++; 536 | 537 | // Copy finished string 538 | strcpy (s, s1); 539 | return s; 540 | } 541 | 542 | // 543 | // Handle signals 544 | // 545 | static void sigHandler( int sig, siginfo_t *siginfo, void *context ) 546 | { 547 | // 548 | // What sort of signal is to be processed ? 549 | // 550 | switch( sig ) { 551 | // 552 | // A normal termination request 553 | // 554 | case SIGINT: 555 | case SIGTERM: 556 | stop_signal = sig; 557 | break; 558 | // 559 | // Ignore broken pipes 560 | // 561 | case SIGPIPE: 562 | break; 563 | } 564 | } 565 | 566 | // 567 | // Logging facility 568 | // 569 | void _mylog( const char *file, int line, int prio, const char *fmt, ... ) 570 | { 571 | // 572 | // Init arguments, lock mutex 573 | // 574 | va_list a_list; 575 | va_start( a_list, fmt ); 576 | 577 | // 578 | // Log to stream 579 | // 580 | if( prio <= streamloglevel) { 581 | 582 | // select stream due to priority 583 | FILE *f = (prio < LOG_INFO) ? stderr : stdout; 584 | //FILE *f = stderr; 585 | 586 | // print timestamp, prio and thread info 587 | struct timeval tv; 588 | gettimeofday( &tv, NULL ); 589 | double time = tv.tv_sec+tv.tv_usec*1E-6; 590 | fprintf( f, "%.4f %d", time, prio); 591 | 592 | // prepend location to message (if available) 593 | if( file ) 594 | fprintf( f, " %s,%d: ", file, line ); 595 | else 596 | fprintf( f, ": " ); 597 | 598 | // the message itself 599 | vfprintf( f, fmt, a_list ); 600 | 601 | // New line and flush stream buffer 602 | fprintf( f, "\n" ); 603 | fflush( f ); 604 | } 605 | 606 | // 607 | // use syslog facility, hide debugging messages from syslog 608 | // 609 | if( prio <= sysloglevel && prio < LOG_DEBUG) 610 | vsyslog( prio, fmt, a_list ); 611 | 612 | // 613 | // Clean variable argument list, unlock mutex 614 | // 615 | va_end ( a_list ); 616 | } 617 | 618 | int loglevel() { 619 | return MAX(sysloglevel, streamloglevel); 620 | } 621 | 622 | long long ms_timer(void) { 623 | struct timespec tv; 624 | 625 | clock_gettime(CLOCK_REALTIME, &tv); 626 | return (((long long)tv.tv_sec)*1000)+(tv.tv_nsec/1e6); 627 | } 628 | --------------------------------------------------------------------------------