From 844b23c20cda476d618a5b41bc5fc92eda90d22b Mon Sep 17 00:00:00 2001 From: marcel Date: Sat, 8 Jul 2023 17:06:59 +0200 Subject: [PATCH] Added poll functions --- CHANGELOG.md | 3 ++ README.md | 8 +++- aprs-mqtt-bridge.py | 105 ++++++++++++++++++++++++++++++++++++++++++- aprs-mqtt-bridge.yml | 57 ++++++++++++++++++++++- 4 files changed, 169 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caf0713..e9a27b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,6 @@ First working version. ## [1.0.1] - 2023-01-14 Changed: aprs_status (published on the MQTT broker) now returns actual state of transmision (sending, retrying, send or failed) instead if just 'ready' and 'busy'. + +## [1.1.0] - 2023-07-08 +Added: APRS nodes can now be polled to get there actual status. An extra section in the YAML configuration file is added for this functionality. diff --git a/README.md b/README.md index 5a6d4f8..eda6be1 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,14 @@ global: retry: 3 # Try this often before giving up destination: APRX29 # Destination or program ID digi_path: WIDE2-1 # Digi path of APRS messages + poll_rate: 300 # Number of second between polling #beacon_program: /usr/sbin/beacon # The external AX.25 beacon program => obsolete +# Poll the status af these clients (IMPORTANT: the calls must also be defined in the topics section) +poll: + - call: PE1RXF-5 + - call: PE1RXF-6 + - call: PE1RXF-8 topics: # MQTT topic: 5GHz dish at workshop (must be unique name) @@ -54,7 +60,7 @@ topics: ``` The configuration entry 'global:retry' sets the amount of APRS message retries we attempt before giving up. The configuration entry 'global:transmit_rate' sets the time between retries. -For now, the state of the outputs of the APRS nodes is not published to the MQTT broker. In the future, this could be implemented in the software. It is possible to poll the nodes via command '06' to get the current state of the outputs. +When a node is added to the poll: call: entry its status will be polled on a regular base. The poll rate is set in the global entry. This is the poll interval between every transmision. When three nodes are defined, a node is polled every 3*poll_rate seconds. ## Requirements diff --git a/aprs-mqtt-bridge.py b/aprs-mqtt-bridge.py index 7ea02f2..4eb192b 100755 --- a/aprs-mqtt-bridge.py +++ b/aprs-mqtt-bridge.py @@ -2,7 +2,7 @@ """ A bridge between APRS messaging and MQTT, designed to control my lora_aprs_node_pico - (C)2022 M.T. Konstapel https://meezenest.nl/mees + (C)2022-2023 M.T. Konstapel https://meezenest.nl/mees This file is part of aprs-mqtt-bridge. @@ -40,6 +40,7 @@ class mqtt_settings: #transmit_rate #retry #topics + #poll state = 'ready' aprs_state = 'idle' pass @@ -201,6 +202,7 @@ def read_config(): cfg = yaml.load(f, Loader=SafeLoader) mqtt.topics = cfg['topics'] + mqtt.poll = cfg['poll'] #print(mqtt.topics) #for topic in mqtt.topics: # print(topic['name']) @@ -240,6 +242,11 @@ def read_config(): except: print ("Error in configuration file: no retry defined.") sys.exit(1) + try: + mqtt.poll_rate = cfg['global']['poll_rate'] + except: + print ("Error in configuration file: no poll_rate defined.") + sys.exit(1) mqtt.client_id = f'{mqtt.topic_root}-{random.randint(0, 1000)}' @@ -255,6 +262,76 @@ def add_subscribtions_from_configfile(client): subscribe(client,current_topic) print('Topic ' + topics['name'] + ' added') +# Loop through poll list and activate then +def poll_clients(count=[0]): + # How many clients do we have to poll? + nr_of_clients = len(mqtt.poll) + + current_call = mqtt.poll[count[0]]['call'] + print ('Polling ' + current_call) + + # Now we have to figure out the other paramters of the client. These are defined in the topics section of the configuration file + for topics in mqtt.topics: + if current_call == topics['call']: + current_port = topics['port'] + source_call = topics['server'] + #print ('Server: ' + source_call) + #print ('AX.25 port: ' + current_port) + break + + # Find call of ax25 port + for position in range(len(axdevice)): + if axdevice[position] == current_port: + port_call = axaddress[position] + #print (port_call) + + message = ':' + topics['call'].ljust(9) + ':' + '06' + + #print (port_call + ' ' + source_call + ' ' + mqtt.destination + ' ' + message) + send_ax25(port_call, source_call, mqtt.destination, 0, message) + + # Every time this functtion is called it moves up one call and wraps around + count[0] += 1 + + if count[0] >= nr_of_clients: + count[0] = 0 + +# Decode response of clients to sensible MQTT messages +def process_polling(call, data, mqtt_client): + print ('Received status: ' + call + " " + data) + + if len(data) != 5: + return + + for cnt in range(len(data)): + if data[-1*cnt] != '0' and data[-1*cnt] != '1': + print('Invalid response') + return + + # Now we have to figure out the other paramters of the client. These are defined in the topics section of the configuration file + for topics in mqtt.topics: + if call == topics['call']: + current_name = topics['name'] + current_poll_bit = topics['poll_bit'] + print ('MQTT topic: ' + current_name) + #print (current_poll_bit) + + inverted_poll_bit = -1*current_poll_bit + status_bit = data[inverted_poll_bit] + if status_bit == '1': + print ('ON') + state = 'ON' + else: + print('OFF') + state = 'OFF' + + topic = mqtt.topic_root + '/' + current_name + '/state' + + publish(mqtt_client,topic,state) + + + + def process_message(data, payload): #print ('Payload: '+ payload) #print (data['call']) @@ -298,6 +375,7 @@ def run(): add_subscribtions_from_configfile(client) #topic = mqtt.topic_root + '/set' #subscribe(client,topic) + client.loop_start() # Send ready to MQTT broker to indicate we are meaning business @@ -307,6 +385,8 @@ def run(): mqtt.state = 'busy' aprs.time_out_timer = time.time() + aprs.poll_timer = time.time() + while True: if aprs.request_to_send == 1: @@ -334,7 +414,22 @@ def run(): #print(digipeaters) #print("Payload = %s"%payload) #print("") + + # Test if received packet is from a polled station + for poll in mqtt.poll: + if poll['call'] == source: + #print ('Received packet from polled station: ' + payload) + # split payload at colon. If it is a valid reply, we should get three + # substrings: the first in empty, the second with the call of the ax25 + # interface and the thirth with the status of the outputs + split_message=payload.split(":") + if len(split_message) == 3: + if split_message[1].replace(" ", "") == axaddress[port]: + if len(split_message[2]) == 5: + #print ('Received status: ' + source + " " + split_message[2]) + process_polling(source, split_message[2], client) + # Waiting for acknowledge... if aprs.wait_for_ack != 0: if source == aprs.call_of_wait_for_ack: # split payload at colon. If it is a valid acknowledge, we should get three @@ -385,6 +480,14 @@ def run(): publish(client,topic,mqtt.aprs_state) mqtt.state = 'ready' + if time.time() - aprs.poll_timer > mqtt.poll_rate: + poll_clients() + # Reset poll timer + aprs.poll_timer = time.time() + #print('Poll clients') + + + if __name__ == '__main__': #sys.stdout = sys.stderr = open('debug.log', 'w') diff --git a/aprs-mqtt-bridge.yml b/aprs-mqtt-bridge.yml index 03d4b88..fa50425 100644 --- a/aprs-mqtt-bridge.yml +++ b/aprs-mqtt-bridge.yml @@ -7,8 +7,14 @@ global: retry: 3 # Try this often before giving up destination: APRX29 # Destination or program ID digi_path: WIDE2-1 # Digi path of APRS messages + poll_rate: 300 # Number of second between polling #beacon_program: /usr/sbin/beacon # The external AX.25 beacon program => obsolete +# Poll the status af these clients (IMPORTANT: the calls must also be defined in the topics section) +poll: + - call: PE1RXF-5 + - call: PE1RXF-6 + - call: PE1RXF-8 topics: # MQTT topic: 5GHz dish at workshop (must be unique name) @@ -16,6 +22,7 @@ topics: call: PE1RXF-6 # Call of node to which commands below are send server: PE1RXF-3 # Call of APRS server sending the commands port: ax2 # Name of AX.25 port to use + poll_bit: 2 # Bit in status response of client (1=b00001, 2=b00010, 3=b00100, 4=b01000, 5=b10000) command: - payload: 'ON' # This is the payload we have to receive cmd: 33{33 # This command is send to the node @@ -29,6 +36,7 @@ topics: call: PE1RXF-6 server: PE1RXF-3 port: ax2 + poll_bit: 1 command: - payload: 'ON' cmd: 31{31 @@ -37,11 +45,12 @@ topics: cmd: 30{30 response: ack30 - # MQTT topic: 5GHz dish at tiny house - - name: ubiquity_dish_ptp_tiny_house + # Server at tiny house + - name: server_tiny_house call: PE1RXF-5 server: PE1RXF-3 port: ax2 + poll_bit: 3 command: - payload: 'ON' cmd: 35{35 @@ -50,11 +59,26 @@ topics: cmd: 34{34 response: ack34 + # MQTT topic: 5GHz dish at tiny house + - name: ubiquity_dish_ptp_tiny_house + call: PE1RXF-5 + server: PE1RXF-3 + port: ax2 + poll_bit: 1 + command: + - payload: 'ON' + cmd: 31{31 + response: ack31 + - payload: 'OFF' + cmd: 30{30 + response: ack30 + # MQTT topic: 5GHz dish in orchard - name: ubiquity_dish_ptmp_orchard call: PE1RXF-8 server: PE1RXF-3 port: ax2 + poll_bit: 1 command: - payload: 'ON' cmd: 31{31 @@ -68,6 +92,7 @@ topics: call: PE1RXF-8 server: PE1RXF-3 port: ax2 + poll_bit: 2 command: - payload: 'ON' cmd: 33{33 @@ -75,3 +100,31 @@ topics: - payload: 'OFF' cmd: 32{32 response: ack32 + + # MQTT topic: QRP-lab QDX transceiver at tiny house + - name: qdx_transceiver_tiny_house + call: PE1RXF-5 + server: PE1RXF-3 + port: ax2 + poll_bit: 2 + command: + - payload: 'ON' + cmd: 33{33 + response: ack33 + - payload: 'OFF' + cmd: 32{32 + response: ack32 + + # MQTT topic: Switched 12V output at tiny house + - name: switched_12v_tiny_house + call: PE1RXF-5 + server: PE1RXF-3 + port: ax2 + poll_bit: 4 + command: + - payload: 'ON' + cmd: 37{37 + response: ack37 + - payload: 'OFF' + cmd: 36{36 + response: ack36