diff --git a/tx/PacketTX.py b/tx/PacketTX.py index c486ee7..3ba5c86 100644 --- a/tx/PacketTX.py +++ b/tx/PacketTX.py @@ -85,11 +85,25 @@ class PacketTX(object): fec=True, udp_listener = None, log_file = None): + + self.min_radio_rotation_time_seconds = 60 # only when queues empty + self.last_rotate = time.monotonic() - self.radio = radio + if type(radio) == list: # handle multiple radios + self.radios = radio + self.radio = radio.pop(0) + self.radios.append(self.radio) # rotate since we've used the first one + else: + self.radios = None + self.radio = radio self.payload_length = payload_length - self.callsign = callsign.encode('ascii') + if type(callsign) == list: + self.callsigns = [x.encode('ascii') for x in callsign] + self.callsign = self.callsigns.pop(0) + self.callsigns.append(self.callsign) + else: + self.callsign = callsign.encode('ascii') self.fec = fec self.crc16 = crcmod.predefined.mkCrcFun('crc-ccitt-false') @@ -157,10 +171,23 @@ class PacketTX(object): packet = self.telemetry_queue.get_nowait() self.radio.transmit_packet(packet) elif self.ssdv_queue.qsize()>0: - packet = self.ssdv_queue.get_nowait() - self.radio.transmit_packet(packet) + (packet, callsign) = self.ssdv_queue.get_nowait() + if callsign == self.callsign: + self.radio.transmit_packet(packet) + else: + logging.info("packet not intended for this callsign - dropping") else: self.radio.transmit_packet(self.idle_message) + if self.radios: + if time.monotonic() > self.min_radio_rotation_time_seconds + self.last_rotate: + logging.info("Rotating radio") + logging.info(self.radios) + self.last_rotate = time.monotonic() + self.callsign = self.callsigns.pop(0) + self.callsigns.append(self.callsign) + self.radio = self.radios.pop(0) + self.radios.append(self.radio) + self.radio.start() # time.sleep(0.1) # commented this out as we should probably always be sending something # this can cause gaps in i2s, which while won't hurt the performance can be annoying @@ -192,11 +219,11 @@ class PacketTX(object): # New packet queueing and queue querying functions (say that 3 times fast) - def queue_image_packet(self,packet): - self.ssdv_queue.put(self.frame_packet(packet, self.fec)) + def queue_image_packet(self,packet,callsign): + self.ssdv_queue.put([self.frame_packet(packet, self.fec),callsign]) - def queue_image_file(self, filename): + def queue_image_file(self, filename, callsign): """ Read in and transmit it, 256 bytes at a time. Intended for transmitting SSDV images. """ @@ -205,7 +232,7 @@ class PacketTX(object): f = open(filename,'rb') for x in range(file_size//256): data = f.read(256) - self.queue_image_packet(data) + self.queue_image_packet(data,callsign) f.close() return True except: diff --git a/tx/WenetPiCamera2.py b/tx/WenetPiCamera2.py index 02286b4..c3c751f 100644 --- a/tx/WenetPiCamera2.py +++ b/tx/WenetPiCamera2.py @@ -389,7 +389,7 @@ class WenetPiCamera2(object): return True - def ssdvify(self, filename="output.jpg", image_id=0, quality=6): + def ssdvify(self, filename="output.jpg", image_id=0, quality=6, tx=None): """ Convert a supplied JPEG image to SSDV. Returns the filename of the converted SSDV image. @@ -417,6 +417,8 @@ class WenetPiCamera2(object): file_basename = filename[:-4] # Construct SSDV command-line. + if tx: + self.callsign = tx.callsign.decode() ssdv_command = "ssdv -e -n -q %d -c %s -i %d picam_temp.jpg picam_temp.ssdv" % (quality, self.callsign, image_id) print(ssdv_command) # Update debug message. @@ -503,7 +505,13 @@ class WenetPiCamera2(object): self.debug_message("Image Post-Processing Failed: %s" % error_str) # SSDV'ify the image. - ssdv_filename = self.ssdvify(capture_filename, image_id=image_id) + # Wait until the transmit queue is empty before pushing in packets. + self.debug_message("Waiting for SSDV TX queue to empty.") + while tx.image_queue_empty() == False: + sleep(1) # Sleep for a short amount of time to allow switching of radio + if self.auto_capture_running == False: + return + ssdv_filename = self.ssdvify(capture_filename, image_id=image_id, tx=tx) # Check the SSDV Conversion has completed properly. If not, continue if ssdv_filename == "FAIL": @@ -514,18 +522,11 @@ class WenetPiCamera2(object): # Otherwise, read in the file and push into the TX buffer. file_size = os.path.getsize(ssdv_filename) - # Wait until the transmit queue is empty before pushing in packets. - self.debug_message("Waiting for SSDV TX queue to empty.") - while tx.image_queue_empty() == False: - sleep(0.05) # Sleep for a short amount of time. - if self.auto_capture_running == False: - return - # Inform ground station we are about to send an image. self.debug_message("Transmitting %d PiCam SSDV Packets." % (file_size//256)) # Push SSDV file into transmit queue. - tx.queue_image_file(ssdv_filename) + tx.queue_image_file(ssdv_filename, self.callsign.encode('ascii')) # Increment image ID. image_id = (image_id + 1) % 256 diff --git a/tx/radio_wrappers.py b/tx/radio_wrappers.py index d6d9668..10fae39 100644 --- a/tx/radio_wrappers.py +++ b/tx/radio_wrappers.py @@ -228,7 +228,7 @@ class RFM98W_Serial(RFM98W): self.serial_port = serial_port - super().__init__(spidevice,frequency,baudrate,tx_power_dbm,reinit_count) + super().__init__(spidevice,frequency,baudrate,tx_power_dbm,reinit_count,led=5) self.start() @@ -242,6 +242,7 @@ class RFM98W_Serial(RFM98W): if self.serial_port: try: self.serial = serial.Serial(self.serial_port, self.baudrate) + self.serial.break_condition=True # Set UART to low when we aren't using it. Used for dual tx mode logging.info(f"RFM98W - Opened Serial port {self.serial_port} for modulation.") except Exception as e: logging.critical(f"Could not open serial port! Error: {str(e)}") @@ -274,8 +275,10 @@ class RFM98W_Serial(RFM98W): """ Modulate serial data, using a UART. """ + self.serial.break_condition=False if self.serial: self.serial.write(packet) + self.serial.break_condition=True super().transmit_packet(packet) # used to reinit the radio occasionally diff --git a/tx/tx_picamera2_gps.py b/tx/tx_picamera2_gps.py index 20337f7..20c116d 100644 --- a/tx/tx_picamera2_gps.py +++ b/tx/tx_picamera2_gps.py @@ -20,15 +20,15 @@ from radio_wrappers import * parser = argparse.ArgumentParser() -parser.add_argument("callsign", default="N0CALL", help="Payload Callsign") +parser.add_argument("callsign", nargs="+", action="extend", help="Payload Callsign") parser.add_argument("--gps", default="none", help="uBlox GPS Serial port. Defaults to /dev/ttyACM0") parser.add_argument("--gpsbaud", default=115200, type=int, help="uBlox GPS Baud rate. (Default: 115200)") parser.add_argument("--logo", default="none", help="Optional logo to overlay on image.") parser.add_argument("--rfm98w", default=None, type=int, help="If set, configure a RFM98W on this SPI device number.") parser.add_argument("--rfm98w-i2s", default=None, type=int, help="If set, configure a RFM98W on this SPI device number. Using I2S") parser.add_argument("--audio-device", default="hw:CARD=i2smaster,DEV=0", type=str, help="Alsa device string. Sets the audio device for rfm98w-i2s mode. (Default: hw:CARD=i2smaster,DEV=0)") -parser.add_argument("--frequency", default=443.500, type=float, help="Transmit Frequency (MHz). (Default: 443.500 MHz)") -parser.add_argument("--baudrate", default=None, type=int, help="Wenet TX baud rate. (Default: 115200 for uart and 96000 for I2S). Known working I2S baudrates: 8000, 24000, 48000, 96000.") +parser.add_argument("--frequency", nargs="+", type=float, help="Transmit Frequency (MHz). (Default: 443.500 MHz)",action="extend") +parser.add_argument("--baudrate", nargs="+", type=int,action="extend", help="Wenet TX baud rate. (Default: 115200 for uart and 96000 for I2S). Known working I2S baudrates: 8000, 24000, 48000, 96000.") parser.add_argument("--serial_port", default="/dev/ttyAMA0", type=str, help="Serial Port for modulation.") parser.add_argument("--tx_power", default=17, type=int, help="Transmit power in dBm (Default: 17 dBm, 50mW. Allowed values: 2-17)") parser.add_argument("--vflip", action='store_true', default=False, help="Flip captured image vertically.") @@ -45,12 +45,6 @@ parser.add_argument("--image_delay", type=float, default=1.0, help="Delay time b parser.add_argument("-v", "--verbose", action='store_true', default=False, help="Show additional debug info.") args = parser.parse_args() -if args.baudrate == None: - if args.rfm98w: - args.baudrate = 115200 - elif args.rfm98w_i2s: - args.baudrate = 96000 - if args.verbose: logging_level = logging.DEBUG else: @@ -62,36 +56,35 @@ logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=loggi callsign = args.callsign # Truncate callsign if it's too long. -if len(callsign) > 6: - callsign = callsign[:6] +callsign = [x[:6] for x in args.callsign] print("Using Callsign: %s" % callsign) +radios = [] if args.rfm98w is not None: - radio = RFM98W_Serial( + radios.append(RFM98W_Serial( spidevice = args.rfm98w, - frequency = args.frequency, - baudrate = args.baudrate, + frequency = args.frequency.pop(0), + baudrate = args.baudrate.pop(0), serial_port = args.serial_port, tx_power_dbm = args.tx_power - ) -elif args.rfm98w_i2s is not None: - radio = RFM98W_I2S( + )) +if args.rfm98w_i2s is not None: + radios.append(RFM98W_I2S( spidevice = args.rfm98w_i2s, - baudrate = args.baudrate, - frequency = args.frequency, + baudrate = args.baudrate.pop(0), + frequency = args.frequency.pop(0), audio_device= args.audio_device, tx_power_dbm = args.tx_power - ) + )) # Other radio options would go here. -else: +if not radios: logging.critical("No radio type specified! Exiting") sys.exit(1) - # Start up Wenet TX. picam = None -tx = PacketTX.PacketTX(radio=radio, callsign=callsign, log_file="debug.log", udp_listener=55674) +tx = PacketTX.PacketTX(radio=radios, callsign=callsign, log_file="debug.log", udp_listener=55674) tx.start_tx() # Sleep for a second to let the transmitter fire up.