Many changes to tx code to better handle radio upsets. Move to libcamera2 as the default

pull/16/head
Mark Jessop 2024-09-14 21:12:27 +09:30
rodzic 9959be4ccb
commit 3828ff03b5
11 zmienionych plików z 542 dodań i 512 usunięć

Wyświetl plik

@ -1,9 +1,9 @@
#!/bin/bash
#
# Wenet TX-side Initialisation Script
# 2022-03-19 Mark Jessop <vk5qi@rfhead.net>
# Wenet TX-side Initialisation Script - Systemd Unit Version
# 2024-07-21 Mark Jessop <vk5qi@rfhead.net>
#
# Run this to set up an attached RFM22B/RFM98W and start transmitting!
# Run this to set up an attached RFM98W and start transmitting!
# Replace the transmit frequency and callsign with your own.
#
@ -11,31 +11,92 @@
# This MUST be <= 6 characters long.
MYCALL=N0CALL
# The centre frequency of the Wenet transmission.
# The centre frequency of the Wenet transmission, in MHz.
TXFREQ=443.500
# Transmit power, in dBm
# Allowed values are from 2 through 17 dBm.
TXPOWER=17
# GPS Port
# Note that we only support uBlox GPS units
# set this to none to disable GPS support
GPSPORT=/dev/ttyACM0
# Image settings
# Image scaling - Scale the 'native' image resolution of the attached camera by this much
# before transmitting.
TX_IMAGE_SCALING=0.5
# White Balance settings
# Allowed Values: Auto, Daylight, Cloudy, Incandescent, Tungesten, Fluorescent, Indoor
WHITEBALANCE=Auto
# Refer near the end of this file for image flipping and overlay options
# Baud Rate
# Known working transmit baud rates are 115200 (the preferred default).
# Lower baud rates *may* work, but will need a lot of testing on the receiver
# chain to be sure they perform correctly.
BAUDRATE=115200
# GPS Port
# Note that we only support uBlox GPS units
GPSPORT=/dev/ttyACM0
# RFM98W SPI Device
# SPI device number of your RFM98W chip
# This will either be 0 or 1 on a RPi.
SPIDEVICE=0
# Modulation UART
# The UART used to modulate the RFM98W with our Wenet transmission
# We want to be using the PL011 UART, *not* the Mini-UART
# On a Pi Zero W, you may need to disable bluetooth. See here for more info:
# https://www.raspberrypi.com/documentation/computers/configuration.html#uarts-and-device-tree
SERIALPORT=/dev/ttyAMA0
# CHANGE THE FOLLOWING LINE TO REFLECT THE ACTUAL PATH TO THE TX FOLDER.
# i.e. it may be /home/username/dev/wenet/tx/
cd /home/pi/wenet/tx/
#Uncomment to initialise a RFM22B (untested with Python 3)
#python init_rfm22b.py $TXFREQ
# Uncomment for use with a RFM98W
python3 init_rfm98w.py --frequency $TXFREQ --baudrate $BAUDRATE
# Wait here until the SPI devices are available.
# This can take a few tens of seconds after boot.
timeout=20
echo "Checking that the SPI devices exist..."
while : ; do
[[ -e "/dev/spidev0.0" ]] && break
if [ "$timeout" == 0 ]; then
echo "Did not find SPI device in timeout period!"
exit 1
# At this point this script exits, and systemd should restart us anyway.
fi
echo "Waiting another 2 seconds for SPI to be available."
sleep 2
((timeout--))
done
echo "Waiting another 10 seconds before startup."
sleep 10
# Start the main TX Script.
# Note that you can also add --logo /path/to/logo.png to this to add a logo overlay.
python3 tx_picam_gps.py --baudrate $BAUDRATE --gps $GPSPORT $MYCALL &
#
# Additional configuration lines you may wish to add or remove before the $CALLSIGN line may include:
# Flip the image vertically and horizontally (e.g. if the camera is mounted upside down)
# --vflip --hflip \
# Add a logo overlay in the bottom right of the image. This must be a transparent PNG file.
# --logo yourlogo.png \
# Set a fixed focus position on a PiCam v3 (NOTE: The Picamv3 focus drifts with temperature - beware!!!)
# --lensposition 0.5 \
# If you don't want any GPS overlays, you can comment the above line and run:
# python WenetPiCam.py --baudrate $BAUDRATE $MYCALL &
python3 tx_picamera2_gps.py \
--rfm98w $SPIDEVICE \
--baudrate $BAUDRATE \
--frequency $TXFREQ \
--serial_port $SERIALPORT \
--tx_power $TXPOWER \
--gps $GPSPORT \
--resize $TX_IMAGE_SCALING \
--whitebalance $WHITEBALANCE \
--vflip --hflip \
$MYCALL

Wyświetl plik

@ -1,65 +0,0 @@
#!/bin/bash
#
# Wenet TX-side Initialisation Script - Systemd Unit Version
# 2024-07-21 Mark Jessop <vk5qi@rfhead.net>
#
# Run this to set up an attached RFM22B/RFM98W and start transmitting!
# Replace the transmit frequency and callsign with your own.
#
# A callsign which will be included in the Wenet Packets.
# This MUST be <= 6 characters long.
MYCALL=VK5ARG
# The centre frequency of the Wenet transmission.
TXFREQ=443.500
# Baud Rate
# Known working transmit baud rates are 115200 (the preferred default).
# Lower baud rates *may* work, but will need a lot of testing on the receiver
# chain to be sure they perform correctly.
BAUDRATE=115200
# GPS Port
# Note that we only support uBlox GPS units
GPSPORT=/dev/ttyACM0
# CHANGE THE FOLLOWING LINE TO REFLECT THE ACTUAL PATH TO THE TX FOLDER.
# i.e. it may be /home/username/dev/wenet/tx/
cd /home/pi/wenet/tx/
# Wait here until the SPI devices are available.
# This can take a few tens of seconds after boot.
timeout=20
while : ; do
[[ -e "/dev/spidev0.0" ]] && break
if [ "$timeout" == 0 ]; then
echo "Did not find SPI device in timeout period!"
exit 1
# At this point this script exits, and systemd should restart us anyway.
fi
echo "Waiting another 2 seconds for SPI to be available."
sleep 2
((timeout--))
done
echo "Waiting another 10 seconds before startup."
sleep 10
#Uncomment to initialise a RFM22B (untested with Python 3)
#python init_rfm22b.py $TXFREQ
# Uncomment for use with a RFM98W
python3 init_rfm98w.py --frequency $TXFREQ --baudrate $BAUDRATE
# Start the main TX Script.
# Do not add a & on the end of these lines when running via systemd!
# Note that you can also add --logo /path/to/logo.png to this to add a logo overlay.
# If using a Picam HQ, add a --picamhq argument into this line before the --gps argument
python3 tx_picam_gps.py --baudrate $BAUDRATE --gps $GPSPORT $MYCALL
# If you don't want any GPS overlays, you can comment the above line and run:
# python WenetPiCam.py --baudrate $BAUDRATE $MYCALL

Wyświetl plik

@ -28,6 +28,7 @@ from time import sleep
from threading import Thread
import numpy as np
from ldpc_encoder import *
from radio_wrappers import *
from queue import Queue
class PacketTX(object):
@ -75,23 +76,16 @@ class PacketTX(object):
# WARNING: 115200 baud is ACTUALLY 115386.834 baud, as measured using a freq counter.
def __init__(self,
serial_port="/dev/ttyAMA0",
serial_baud=115200,
# Radio wrapper, for radio setup and modulation.
radio,
# User callsign, should be used in the idle sequence, but currently isn't...
callsign="N0CALL",
payload_length=256,
fec=True,
debug = False,
callsign="N0CALL",
udp_listener = None,
log_file = None):
# Instantiate our low-level transmit interface, be it a serial port, or the BinaryDebug class.
if debug == True:
self.s = BinaryDebug()
self.debug = True
else:
self.debug = False
self.s = serial.Serial(serial_port,serial_baud)
self.radio = radio
self.payload_length = payload_length
self.callsign = callsign.encode('ascii')
@ -159,19 +153,16 @@ class PacketTX(object):
while self.transmit_active:
if self.telemetry_queue.qsize()>0:
packet = self.telemetry_queue.get_nowait()
self.s.write(packet)
self.radio.transmit_packet(packet)
elif self.ssdv_queue.qsize()>0:
packet = self.ssdv_queue.get_nowait()
self.s.write(packet)
self.radio.transmit_packet(packet)
else:
if not self.debug:
self.s.write(self.idle_message)
else:
# TODO: Tune this value.
sleep(0.05)
self.radio.transmit_packet(self.idle_message)
time.sleep(0.1)
print("Closing Thread")
self.s.close()
self.radio.shutdown()
def close(self):
@ -534,16 +525,39 @@ if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--txport", default="/dev/ttyAMA0", type=str, help="Transmitter serial port. Defaults to /dev/ttyAMA0")
parser.add_argument("--baudrate", default=115200, type=int, help="Transmitter baud rate. Defaults to 115200 baud.")
parser.add_argument("--rfm98w", default=None, type=int, help="If set, configure a RFM98W on this SPI device number.")
parser.add_argument("--frequency", default=443.500, type=float, help="Transmit Frequency (MHz). (Default: 443.500 MHz)")
parser.add_argument("--baudrate", default=115200, type=int, help="Wenet TX baud rate. (Default: 115200).")
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("-v", "--verbose", action='store_true', default=False, help="Show additional debug info.")
args = parser.parse_args()
debug_output = False # If True, packet bits are saved to debug.bin as one char per bit.
if args.verbose:
logging_level = logging.DEBUG
else:
logging_level = logging.INFO
# Set up logging
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging_level)
radio = None
if args.rfm98w is not None:
radio = RFM98W_Serial(
spidevice = args.rfm98w,
frequency = args.frequency,
baudrate = args.baudrate,
serial_port = args.serial_port,
tx_power_dbm = args.tx_power
)
# Other radio options would go here.
else:
logging.critical("No radio type specified! Exiting")
sys.exit(1)
tx = PacketTX(
debug=debug_output,
serial_port=args.txport,
serial_baud=args.baudrate,
radio=radio,
udp_listener=55674)
tx.start_tx()
@ -556,4 +570,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
tx.close()
print("Closing")
logging.info("Closing")

Wyświetl plik

@ -72,7 +72,7 @@ def setter(register_address):
############################################### Definition of the LoRa class ###########################################
class LoRa(object):
class LoRaRFM98W(object):
BOARD = None
spi = None

Wyświetl plik

@ -35,6 +35,17 @@ class WenetPiCamera2(object):
"""
# White balance text to enum lookup
wb_lookup = {
"auto": controls.AwbModeEnum.Auto,
"incandescent": controls.AwbModeEnum.Incandescent,
"fluorescent": controls.AwbModeEnum.Fluorescent,
"tungsten": controls.AwbModeEnum.Tungsten,
"indoor": controls.AwbModeEnum.Indoor,
"daylight": controls.AwbModeEnum.Daylight,
"cloudy": controls.AwbModeEnum.Cloudy
}
def __init__(self,
callsign = "N0CALL",
tx_resolution=(1936,1088),
@ -42,7 +53,7 @@ class WenetPiCamera2(object):
image_delay=0.0,
vertical_flip = False,
horizontal_flip = False,
greyworld = False,
whitebalance = 'auto',
lens_position = 0.0,
temp_filename_prefix = 'picam_temp',
debug_ptr = None
@ -64,7 +75,7 @@ class WenetPiCamera2(object):
vertical_flip: Flip captured images vertically.
horizontal_flip: Flip captured images horizontally.
Used to correct for picam orientation.
greyworld: Use Greyworld AWB setting, for IR-filtered images.
whitebalance: White balance mode - allowed values: Auto, Incandescent, Tungesten, Fluorescent, Indoor, Daylight, Cloudy
lens_position: Lens Position setting (float), 0.0 = Infinity, 10 = very close.
Only usable on Pi Camera v3 modules.
@ -81,12 +92,17 @@ class WenetPiCamera2(object):
self.num_images = num_images
self.image_delay = image_delay
self.callsign = callsign
self.tx_resolution = tx_resolution
self.tx_resolution_init = tx_resolution
self.horizontal_flip = horizontal_flip
self.vertical_flip = vertical_flip
self.greyworld = greyworld
self.lens_position = lens_position
if whitebalance.lower() in self.wb_lookup:
self.whitebalance = self.wb_lookup[whitebalance.lower()]
else:
self.whitebalance = self.wb_lookup['auto']
self.init_camera()
@ -96,7 +112,18 @@ class WenetPiCamera2(object):
self.camera_properties = self.cam.camera_properties
self.debug_ptr("Camera Resolution: " + str(self.camera_properties['PixelArraySize']))
self.debug_ptr("Camera Native Resolution: " + str(self.camera_properties['PixelArraySize']))
# If the user has explicitly specified the transmit image resolution, use it.
if type(self.tx_resolution_init) == tuple:
self.tx_resolution = self.tx_resolution_init
self.debug_ptr(f"Transmit Resolution set to {str(self.tx_resolution)}")
# Otherwise, has the user provided a floating point scaling factor?
elif type(self.tx_resolution_init) == float:
res_x = 16*int(self.camera_properties['PixelArraySize'][0]*self.tx_resolution_init/16)
res_y = 16*int(self.camera_properties['PixelArraySize'][1]*self.tx_resolution_init/16)
self.tx_resolution = (res_x, res_y)
self.debug_ptr(f"Transmit Resolution set to {str(self.tx_resolution)}, scaled {self.tx_resolution_init} from native.")
# Configure camera, including flip settings.
capture_config = self.cam.create_still_configuration(
@ -105,14 +132,15 @@ class WenetPiCamera2(object):
self.cam.configure(capture_config)
# Set other settings, White Balance, exposure metering, etc.
self.cam.set_controls(
{'AwbMode': controls.AwbModeEnum.Daylight,
{'AwbMode': self.whitebalance,
'AeMeteringMode': controls.AeMeteringModeEnum.Matrix,
'NoiseReductionMode': controls.draft.NoiseReductionModeEnum.Off}
)
# Set Pi Camera 3 lens position
if 'LensPosition' in self.cam.camera_controls:
if 'LensPosition' in self.cam.camera_controls and self.lens_position>0:
self.debug_ptr("Configured lens position to " + str(self.lens_position))
self.cam.set_controls({"AfMode": controls.AfModeEnum.Manual, "LensPosition": self.lens_position})
@ -153,13 +181,13 @@ class WenetPiCamera2(object):
# Set other settings, White Balance, exposure metering, etc.
self.cam.set_controls(
{'AwbMode': controls.AwbModeEnum.Daylight,
{'AwbMode': self.whitebalance,
'AeMeteringMode': controls.AeMeteringModeEnum.Matrix,
'NoiseReductionMode': controls.draft.NoiseReductionModeEnum.Off}
)
# Set Pi Camera 3 lens position
if 'LensPosition' in self.cam.camera_controls:
if 'LensPosition' in self.cam.camera_controls and self.lens_position>0:
self.debug_ptr("Configured lens position to " + str(self.lens_position))
self.cam.set_controls({"AfMode": controls.AfModeEnum.Manual, "LensPosition": self.lens_position})

Wyświetl plik

@ -1,279 +0,0 @@
#!/usr/bin/env python
#
# RFM22B Initialisation for Horus High Speed Telemetry
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
# A mash together of a few different libraries, including:
# https://github.com/MaxBaex/raspi_rfm22
# https://github.com/omcaree/rfm22b/blob/master/src/rfm22b.cpp
#
# Uses spidev for SPI comms.
#
# Note: This is fairly limited, and was only intended to get a RFM22B
# into direct-async TX mode.
# Lots of functions (i.e. anything to do with packets) are unimplemented.
#
# Electrical Connections:
# GPIO0 <-> TX_ANT
# GPIO1 <-> RX_ANT
# SDN <-> GND (Not sure if this is a good thing, not being about to hard reset it..)
# GPIO2 <-> UART TXD (for direct mode TX)
# SPI - connected to CE1 (can change this when instantiating the object.)
#
import spidev, sys, time
class REG:
DEVICE_TYPE = 0x00
DEVICE_VERSION = 0x01
DEVICE_STATUS = 0x02
INTERRUPT_STATUS_1 = 0x03
INTERRUPT_STATUS_2 = 0x04
INTERRUPT_ENABLE_1 = 0x05
INTERRUPT_ENABLE_2 = 0x06
OPERATING_FUNCTION_CONTROL_1 = 0x07
OPERATING_FUNCTION_CONTROL_2 = 0x08
CRYSTAL_OSCILLATOR_LOAD = 0x09
MICROCONTROLLER_CLOCK_OUTPUT = 0x0A
GPIO0_CONFIGURATION = 0x0B
GPIO1_CONFIGURATION = 0x0C
GPIO2_CONFIGURATION = 0x0D
I_O_PORT_CONFIGURATION = 0x0E
ADC_CONFIGURATION = 0x0F
ADC_SENSOR_AMPLIFIER_OFFSET = 0x10
ADC_VALUE = 0x11
TEMPERATURE_SENSOR_CONTROL = 0x12
TEMPERATURE_VALUE_OFFSET = 0x13
WAKE_UP_TIMER_PERIOD_1 = 0x14
WAKE_UP_TIMER_PERIOD_2 = 0x15
WAKE_UP_TIMER_PERIOD_3 = 0x16
WAKE_UP_TIMER_VALUE_1 = 0x17
WAKE_UP_TIMER_VALUE_2 = 0x18
LOW_DUTY_CYCLE_MODE_DURATION = 0x19
LOW_BATTERY_DETECTION_THRESHOLD = 0x1A
BATTERY_VOLTAGE_LEVEL = 0x1B
IF_FILTER_BANDWIDTH = 0x1C
AFC_LOOP_GEARSHIFT_OVERRIDE = 0x1D
AFC_TIMING_CONTROL = 0x1E
CLOCK_RECOVERY_GEARSHIFT_OVERRIDE = 0x1F
CLOCK_RECOVERY_OVERSAMPLING_RATIO = 0x20
CLOCK_RECOVERY_OFFSET_2 = 0x21
CLOCK_RECOVERY_OFFSET_1 = 0x22
CLOCK_RECOVERY_OFFSET_0 = 0x23
CLOCK_RECOVERY_TIMING_LOOP_GAIN_1 = 0x24
CLOCK_RECOVERY_TIMING_LOOP_GAIN_0 = 0x25
RECEIVED_SIGNAL_STRENGTH_INDICATOR = 0x26
RSSI_THRESSHOLF_FOR_CLEAR_CHANNEL_INDICATOR = 0x27
ANTENNA_DIVERSITY_REGISTER_1 = 0x28
ANTENNA_DIVERSITY_REGISTER_2 = 0x29
AFC_LIMITER = 0x2A
AFC_CORRECTION_READ = 0x2B
OOK_COUNTER_VALUE_1 = 0x2C
OOK_COUNTER_VALUE_2 = 0x2D
SLICER_PEAK_HOLD = 0x2E
# Register 0x2F reserved
DATA_ACCESS_CONTROL = 0x30
EzMAC_STATUS = 0x31
HEADER_CONTROL_1 = 0x32
HEADER_CONTROL_2 = 0x33
PREAMBLE_LENGTH = 0x34
PREAMBLE_DETECTION_CONTROL = 0x35
SYNC_WORD_3 = 0x36
SYNC_WORD_2 = 0x37
SYNC_WORD_1 = 0x38
SYNC_WORD_0 = 0x39
TRANSMIT_HEADER_3 = 0x3A
TRANSMIT_HEADER_2 = 0x3B
TRANSMIT_HEADER_1 = 0x3C
TRANSMIT_HEADER_0 = 0x3D
TRANSMIT_PACKET_LENGTH = 0x3E
CHECK_HEADER_3 = 0x3F
CHECK_HEADER_2 = 0x40
CHECK_HEADER_1 = 0x41
CHECK_HEADER_0 = 0x42
HEADER_ENABLE_3 = 0x43
HEADER_ENABLE_2 = 0x44
HEADER_ENABLE_1 = 0x45
HEADER_ENABLE_0 = 0x46
RECEIVED_HEADER_3 = 0x47
RECEIVED_HEADER_2 = 0x48
RECEIVED_HEADER_1 = 0x49
RECEIVED_HEADER_0 = 0x4A
RECEIVED_PACKET_LENGTH = 0x4B
# Registers 0x4C-4E reserved
ADC8_CONTROL = 0x4F
# Registers 0x50-5F reserved
CHANNEL_FILTER_COEFFICIENT_ADDRESS = 0x60
# Register 0x61 reserved
CRYSTAL_OSCILLATOR_CONTROL_TEST = 0x62
# Registers 0x63-68 reserved
AGC_OVERRIDE_1 = 0x69
# Registers 0x6A-0x6C reserved
TX_POWER = 0x6D
TX_DATA_RATE_1 = 0x6E
TX_DATA_RATE_0 = 0x6F
MODULATION_MODE_CONTROL_1 = 0x70
MODULATION_MODE_CONTROL_2 = 0x71
FREQUENCY_DEVIATION = 0x72
FREQUENCY_OFFSET_1 = 0x73
FREQUENCY_OFFSET_2 = 0x74
FREQUENCY_BAND_SELECT = 0x75
NOMINAL_CARRIER_FREQUENCY_1 = 0x76
NOMINAL_CARRIER_FREQUENCY_0 = 0x77
# Register 0x78 reserved
FREQUENCY_HOPPING_CHANNEL_SELECT = 0x79
FREQUENCY_HOPPING_STEP_SIZE = 0x7A
# Register 0x7B reserved
TX_FIFO_CONTROL_1 = 0x7C
TX_FIFO_CONTROL_2 = 0x7D
RX_FIFO_CONTROL = 0x7E
FIFO_ACCESS = 0x7F
class MODE:
IDLE = 0
RX = 1
TX = 2
class TXPOW:
TXPOW_1DBM = 0x00
TXPOW_2DBM = 0x01
TXPOW_5DBM = 0x02
TXPOW_8DBM = 0x03
TXPOW_11DBM = 0x04
TXPOW_14DBM = 0x05
TXPOW_17DBM = 0x06
TXPOW_20DBM = 0x07
class CONFIGS:
# Register: 1c, 1f, 20, 21, 22, 23, 24, 25, 2c, 2d, 2e, 58, 69, 6e, 6f, 70, 71, 72
REGISTERS = [0x1C,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x2C,0x2D,0x2E,0x58,0x69,0x6E,0x6F,0x70,0x71,0x72]
DIRECT_120K = [0x8a, 0x03, 0x60, 0x01, 0x55, 0x55, 0x02, 0xad, 0x40, 0x0a, 0x50, 0x80, 0x60, 0x20, 0x00, 0x00, 0x02, 0x60]
class RFM22B(object):
def __init__(self,device=1):
self.spi = spidev.SpiDev()
self.spi.open(0,device)
self.spi.max_speed_hz = 1000000
if not self.check_connection():
print("Init Failed!")
self.spi.close()
sys.exit(1)
self.setup_basic()
print("Init Complete!")
def close(self):
self.spi.close()
def read_register(self,address):
return self.spi.xfer([address,0])[1]
def write_register(self,address,data):
return self.spi.xfer([address | 0x80, data])
def check_connection(self):
device_type = self.read_register(REG.DEVICE_TYPE)
device_version = self.read_register(REG.DEVICE_VERSION)
print("Device Type:\t %d" % device_type)
print("Device Version:\t %d" % device_version)
if (device_type != 8) or (device_version != 6):
print("Wrong Device?!")
return False
else:
return True
# 441.2E6 = HBSEL: 0 FB: 20 FBS: 84 FC: 7679 NCF1: 29, NCF0: 255
def set_frequency(self, freq_hz):
if (freq_hz<240E6) or (freq_hz>960E6):
print("Invalid Frequency!")
return False
hbsel = 1 if (freq_hz>= 480E6) else 0
fb = int(freq_hz/10E6/(hbsel+1) - 24)
fbs = (1<<6) | (hbsel<<5) | fb
fc = int((freq_hz/(10.0E6 * (hbsel+1)) - fb - 24) * 64000)
ncf1 = fc>>8
ncf0 = fc & 0xFF
print("hbsel: %d, fb: %d, fbs: %d, fc: %d, ncf1: %d, ncf0: %d" % (hbsel,fb,fbs,fc,ncf1,ncf0))
self.write_register(REG.FREQUENCY_BAND_SELECT,fbs)
self.write_register(REG.NOMINAL_CARRIER_FREQUENCY_1,ncf1)
self.write_register(REG.NOMINAL_CARRIER_FREQUENCY_0,ncf0)
# Read back registers.
fbs_read = self.read_register(REG.FREQUENCY_BAND_SELECT)
ncf1_read = self.read_register(REG.NOMINAL_CARRIER_FREQUENCY_1)
ncf0_read = self.read_register(REG.NOMINAL_CARRIER_FREQUENCY_0)
if (fbs == fbs_read) and (ncf1 == ncf1_read) and (ncf0 == ncf0_read):
print("Frequency set OK!")
return True
else:
print("Frequency not set correctly!")
return False
def setup_basic(self):
# Software reset.
self.write_register(REG.OPERATING_FUNCTION_CONTROL_1,0x80)
time.sleep(0.1)
# Interrupts
self.write_register(REG.INTERRUPT_ENABLE_1,0x87)
# Switch to ready mode.
self.write_register(REG.OPERATING_FUNCTION_CONTROL_1,0x01)
self.write_register(REG.CRYSTAL_OSCILLATOR_LOAD,0x7f)
# Configure GPIO 0 and 1 settings.
self.write_register(REG.GPIO0_CONFIGURATION, 0x12) # TX State, physically wired into TX_ANT pin.
self.write_register(REG.GPIO1_CONFIGURATION, 0x15) # RX State, physically wired into RX_ANT pin.
# Program a bulk set of registers.
# I'm using this to set a bunch of different registers at one time, to load up pre-calculated configs.
def set_bulk(self,registers,data):
if len(registers) != len(data):
print("Array length mismatch!")
return False
for k in xrange(len(registers)):
self.write_register(registers[k],data[k])
def set_tx_power(self,power):
self.write_register(REG.TX_POWER,power)
def set_mode(self,mode):
self.write_register(REG.OPERATING_FUNCTION_CONTROL_1,mode)
if __name__ == '__main__':
# Attempt to extract a frequency from the first argument:
if len(sys.argv)<2:
print("Usage: \tpython init_rfm22b.py <TX Frequency in MHz>")
print("Example: \tpython init_rfm22b.py 441.200")
sys.exit(1)
try:
tx_freq = float(sys.argv[1])
except:
print("Unable to parse input.")
if tx_freq>450.0 or tx_freq<430.00:
print("Frequency out of 70cm band, using default.")
tx_freq = 441.200*1e6
else:
tx_freq = tx_freq*1e6
rfm = RFM22B()
rfm.set_tx_power(TXPOW.TXPOW_17DBM | 0x08)
rfm.set_frequency(tx_freq)
rfm.write_register(REG.GPIO2_CONFIGURATION,0x30) # TX Data In
rfm.set_bulk(CONFIGS.REGISTERS,CONFIGS.DIRECT_120K) # Direct Asynchronous mode, ~120KHz tone spacing.
rfm.set_mode(0x09)
rfm.close()

Wyświetl plik

@ -1,91 +0,0 @@
#!/usr/bin/env python
#
# RFM98W Initialisation for Wenet Telemetry
#
# Copyright (C) 2018 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
# Requires pySX127x:
# https://github.com/darksidelemm/pySX127x
# Copy the SX127x directory into this directory...
#
# Uses spidev for comms with a RFM98W module.
#
# Note: As with the RFM22B version, all this script does
# is get the RFM98W onto the right frequency, and into the right mode.
#
# SPI: Connected to CE0 (like most of my LoRa shields)
# RPi TXD: Connected to RFM98W's DIO2 pin.
#
import sys
import argparse
from SX127x.LoRa import *
from SX127x.hardware_piloragateway import HardwareInterface
def setup_rfm98w(frequency=441.200, spi_device=0, shutdown=False, deviation = 71797):
# Set this to 1 if your RFM98W is on CE1
hw = HardwareInterface(spi_device)
# Start talking to the module...
lora = LoRa(hw)
# Set us into FSK mode, and set continuous mode on
lora.set_register(0x01,0x00) # Standby Mode
# If we have been asked to shutdown the RFM98W, then exit here.
if shutdown:
sys.exit(0)
# Otherwise, proceed.
lora.set_register(0x31,0x00) # Set Continuous Mode
# Set TX Frequency
lora.set_freq(frequency)
# Set Deviation (~70 kHz). Signals ends up looking a bit wider than the RFM22B version.
_dev_lsbs = int(deviation / 61.03)
_dev_msb = _dev_lsbs >> 8
_dev_lsb = _dev_lsbs % 256
lora.set_register(0x04,_dev_msb)
lora.set_register(0x05,_dev_lsb)
# Set Transmit power to 50mW.
# NOTE: If you're in another country you'll probably want to modify this value to something legal...
lora.set_register(0x09,0x8F)
# Go into TX mode.
lora.set_register(0x01,0x02) # .. via FSTX mode (where the transmit frequency actually gets set)
lora.set_register(0x01,0x03) # Now we're in TX mode...
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--frequency", default=441.200, type=float, help="Transmit Frequency (MHz). Default = 441.200 MHz.")
parser.add_argument("--spidevice", default=0, type=int, help="LoRa SPI Device number. Default = 0.")
parser.add_argument("--baudrate", default=115200, type=int, help="Wenet TX baud rate. We chose a TX deviation based on this. Default=115200.")
parser.add_argument("--shutdown",action="store_true", help="Shutdown Transmitter instead of activating it.")
args = parser.parse_args()
tx_freq = args.frequency
if tx_freq>450.0 or tx_freq<430.00:
print("Frequency out of 70cm band, using default.")
tx_freq = 441.200
if args.baudrate == 9600:
deviation = 4800
elif args.baudrate == 4800:
deviation = 2400
else:
deviation = 71797
print("Using deviation of %d Hz for Baud Rate %d bd." % (deviation, args.baudrate))
setup_rfm98w(frequency=tx_freq, spi_device=args.spidevice, shutdown=args.shutdown, deviation=deviation)
print("Frequency set to: %.3f MHz" % tx_freq)
sys.exit(0)

Wyświetl plik

@ -0,0 +1,285 @@
#!/usr/bin/env python
#
# Radio Wrappers for Wenet Transmissions
#
# Copyright (C) 2024 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
# For RFM98W support, requires pySX127x:
# https://github.com/darksidelemm/pySX127x
# (This is included with the Wenet repository)
#
# Uses spidev for comms with a RFM98W module.
#
# Note: As with the RFM22B version, all this script does
# is get the RFM98W onto the right frequency, and into the right mode.
#
# SPI: Connected to CE0 (like most of my LoRa shields)
# RPi TXD: Connected to RFM98W's DIO2 pin.
#
import sys
import argparse
import logging
import serial
import time
import numpy as np
from SX127x.LoRa import *
from SX127x.hardware_piloragateway import HardwareInterface
class RFM98W_Serial(object):
"""
RFM98W Wrapper for Wenet Transmission, using 2-FSK Direct-Asynchronous Modulation via a UART.
"""
def __init__(
self,
spidevice=0,
frequency=443.500,
baudrate=115200,
serial_port=None,
tx_power_dbm=17,
reinit_count=5000
):
self.spidevice = spidevice
self.frequency = frequency
self.baudrate = baudrate
self.serial_port = serial_port
self.tx_power_dbm = tx_power_dbm
self.reinit_count = reinit_count
self.hw = None
self.lora = None
self.tx_packet_count = 0
self.start()
def start(self):
"""
Initialise (or re-initialise) both the RFM98W and Serial connections.
Configure the RFM98W into direct asynchronous FSK mode, with the appropriate power, deviation, and transmit frequency.
"""
self.hw = HardwareInterface(self.spidevice)
self.lora = LoRaRFM98W(self.hw, verbose=False)
logging.debug(f"RFM98W - SX127x Register Dump: {self.lora.backup_registers}")
logging.debug(f"RFM98W - SX127x device version: {hex(self.lora.get_version())}")
if not self.comms_ok():
logging.critical("RFM98W - No communication with RFM98W IC!")
self.shutdown()
return
# Deviation selection.
if self.baudrate == 9600:
deviation = 4800
elif self.baudrate == 4800:
deviation = 2400
else:
# Default deviation, for 115200 baud
deviation = 71797
# Refer https://cdn.sparkfun.com/assets/learn_tutorials/8/0/4/RFM95_96_97_98W.pdf
self.lora.set_register(0x01,0x00) # FSK Sleep Mode
self.lora.set_register(0x31,0x00) # Set Continuous Transmit Mode
self.lora.set_freq(self.frequency)
logging.info(f"RFM98W - Frequency set to: {self.frequency} MHz.")
# Set Deviation (~70 kHz). Signals ends up looking a bit wider than the RFM22B version.
_dev_lsbs = int(deviation / 61.03)
_dev_msb = _dev_lsbs >> 8
_dev_lsb = _dev_lsbs % 256
self.lora.set_register(0x04,_dev_msb)
self.lora.set_register(0x05,_dev_lsb)
# Set Transmit power
tx_power_lookup = {0:0x80, 1:0x80, 2:0x80, 3:0x81, 4:0x82, 5:0x83, 6:0x84, 7:0x85, 8:0x86, 9:0x87, 10:0x88, 11:0x89, 12:0x8A, 13:0x8B, 14:0x8C, 15:0x8D, 16:0x8E, 17:0x8F}
if self.tx_power_dbm in tx_power_lookup:
self.lora.set_register(0x09, tx_power_lookup[self.tx_power_dbm])
logging.info(f"RFM98W - TX Power set to {self.tx_power_dbm} dBm ({hex(tx_power_lookup[self.tx_power_dbm])}).")
else:
# Default to low power, 1.5mW or so
self.lora.set_register(0x09, 0x80)
logging.info(f"RFM98W - Unknown TX power, setting to 2 dBm (0x80).")
# Go into TX mode.
self.lora.set_register(0x01,0x02) # .. via FSTX mode (where the transmit frequency actually gets set)
self.lora.set_register(0x01,0x03) # Now we're in TX mode...
# Seems we need to briefly sleep before we can read the register correctly.
time.sleep(0.1)
# Confirm we've gone into transmit mode.
if self.lora.get_register(0x01) == 0x03:
logging.info("RFM98W - Radio initialised!")
else:
logging.critical("RFM98W - TX Mode not set correctly!")
# Now initialise the Serial port for modulation
if self.serial_port:
try:
self.serial = serial.Serial(self.serial_port, self.baudrate)
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)}")
self.serial = None
else:
# If no serial port info provided, write out to a binary debug file.
self.serial = BinaryDebug()
logging.info("No serial port provided - using Binary Debug output (binary_debug.bin)")
def shutdown(self):
"""
Shutdown the RFM98W, and close the SPI and Serial connections.
"""
try:
# Set radio into FSK sleep mode
self.lora.set_register(0x01,0x00)
logging.info("RFM98W - Set radio into sleep mode.")
self.lora = None
except:
pass
try:
# Shutdown SPI device
self.hw.teardown()
logging.info("RFM98W - Disconnected from SPI.")
self.hw = None
except:
pass
try:
# Close the serial connection
self.serial.close()
logging.info("RFM98W - Closed Serial Port")
self.serial = None
except:
pass
return
def comms_ok(self):
"""
Test SPI communications with the RFM98W and return true if ok.
"""
try:
_ver = self.lora.get_version()
if _ver == 0x00 or _ver == 0xFF or _ver == None:
return False
else:
return True
except Exception as e:
logging.critical("RFM98W - Could not read device version!")
return False
return False
def transmit_packet(self, packet):
"""
Modulate serial data, using a UART.
"""
if self.serial:
self.serial.write(packet)
# Increment transmit packet counter
self.tx_packet_count += 1
# If we have a reinitialisation count set, reinitialise the radio.
if self.reinit_count:
if self.tx_packet_count % self.reinit_count == 0:
logging.info(f"RFM98W - Reinitialising Radio at {self.tx_packet_count} packets.")
self.start()
class BinaryDebug(object):
""" Debug binary 'transmitter' Class
Used to write packet data to a file in one-bit-per-char (i.e. 0 = 0x00, 1 = 0x01)
format for use with codec2-dev's fsk modulator.
Useful for debugging, that's about it.
"""
def __init__(self):
self.f = open("binary_debug.bin",'wb')
def write(self,data):
# TODO: Add in RS232 framing
raw_data = np.array([],dtype=np.uint8)
for d in data:
d_array = np.unpackbits(np.fromstring(d,dtype=np.uint8))
raw_data = np.concatenate((raw_data,[0],d_array[::-1],[1]))
self.f.write(raw_data.astype(np.uint8).tostring())
def close(self):
self.f.close()
if __name__ == '__main__':
# Test code for the above. Allows enabling a radio and (optionally) sending some test packets.
import time
parser = argparse.ArgumentParser()
parser.add_argument("--rfm98w", default=None, type=int, help="If set, configure a RFM98W on this SPI device number.")
parser.add_argument("--frequency", default=443.500, type=float, help="Transmit Frequency (MHz). (Default: 443.500 MHz)")
parser.add_argument("--baudrate", default=115200, type=int, help="Wenet TX baud rate. (Default: 115200).")
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("--shutdown", default=False, action="store_true", help="Shutdown Transmitter after configuration.")
parser.add_argument("--test_modulation", default=False, action="store_true", help="Transmit a sequence of dummy packets as a test.")
parser.add_argument("-v", "--verbose", action='store_true', default=False, help="Show additional debug info.")
args = parser.parse_args()
if args.verbose:
logging_level = logging.DEBUG
else:
logging_level = logging.INFO
# Set up logging
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging_level)
radio = None
if args.rfm98w is not None:
radio = RFM98W_Serial(
spidevice = args.rfm98w,
frequency = args.frequency,
baudrate = args.baudrate,
serial_port = args.serial_port,
tx_power_dbm = args.tx_power
)
# Other radio options would go here.
else:
logging.critical("No radio type specified! Exiting")
sys.exit(1)
if args.test_modulation:
# Transmit a canned text message a few times
time.sleep(1)
test_packet = b'UUUUUUUUUUUUUUUU\xab\xcd\xef\x01\x00\x1d\x00\x01This is a Wenet test message!UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU5\x89\xad5\xff\xfbgX\x96\xaa\x10\xb9\x05,\x8co\xf7\xf0\xdd\x19\x1bs2\xd9$\x85\xa2\xc2\xd5\xc9\x15\xef\xac\x06\xb6\x11H\xb0;\xc3\xae\x1b\xe0_\x8cC\x13L*\x04\x17(\x9a\xa6\x95\x84\xf1UB{\xf5\x96\xb9\x14\x05\xa8@'
logging.info("Sending 100 test packets.")
for x in range(100):
radio.transmit_packet(test_packet)
time.sleep(0.1)
if args.shutdown:
logging.info("Sleeping 5 seconds before shutdown")
time.sleep(5)
radio.shutdown()
sys.exit(0)

Wyświetl plik

@ -3,7 +3,7 @@
# PiCamera2 Library Transmitter Script - with GPS Data and Logo Overlay.
# Capture images from the PiCam, and transmit them.
#
# Copyright (C) 2023 Mark Jessop <vk5qi@rfhead.net>
# Copyright (C) 2024 Mark Jessop <vk5qi@rfhead.net>
# Released under GNU GPL v3 or later
#
@ -11,19 +11,40 @@ import PacketTX
import WenetPiCamera2
import ublox
import argparse
import logging
import time
import os
import subprocess
import traceback
from radio_wrappers import *
parser = argparse.ArgumentParser()
parser.add_argument("callsign", default="N0CALL", help="Payload Callsign")
parser.add_argument("--gps", default="/dev/ttyACM0", help="uBlox GPS Serial port. Defaults to /dev/ttyACM0")
parser.add_argument("--logo", default="none", help="Optional logo to overlay on image.")
parser.add_argument("--txport", default="/dev/ttyAMA0", type=str, help="Transmitter serial port. Defaults to /dev/ttyAMA0")
parser.add_argument("--baudrate", default=115200, type=int, help="Transmitter baud rate. Defaults to 115200 baud.")
parser.add_argument("--rfm98w", default=0, type=int, help="If set, configure a RFM98W on this SPI device number.")
parser.add_argument("--frequency", default=443.500, type=float, help="Transmit Frequency (MHz). (Default: 443.500 MHz)")
parser.add_argument("--baudrate", default=115200, type=int, help="Wenet TX baud rate. (Default: 115200).")
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.")
parser.add_argument("--hflip", action='store_true', default=False, help="Flip captured image horizontally.")
parser.add_argument("--resize", type=float, default=0.5, help="Resize raw image from camera by this factor before transmit (in both X/Y, to nearest multiple of 16 pixels). Default=0.5")
parser.add_argument("--whitebalance", type=str, default='daylight', help="White Balance setting: Auto, Daylight, Cloudy, Incandescent, Tungesten, Fluorescent, Indoor")
parser.add_argument("--lensposition", type=float, default=-1, help="For PiCam v3, set the lens position. Default: -1 = Autofocus")
parser.add_argument("-v", "--verbose", action='store_true', default=False, help="Show additional debug info.")
args = parser.parse_args()
if args.verbose:
logging_level = logging.DEBUG
else:
logging_level = logging.INFO
# Set up logging
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging_level)
callsign = args.callsign
# Truncate callsign if it's too long.
if len(callsign) > 6:
@ -31,9 +52,22 @@ if len(callsign) > 6:
print("Using Callsign: %s" % callsign)
if args.rfm98w is not None:
radio = RFM98W_Serial(
spidevice = args.rfm98w,
frequency = args.frequency,
baudrate = args.baudrate,
serial_port = args.serial_port,
tx_power_dbm = args.tx_power
)
# Other radio options would go here.
else:
logging.critical("No radio type specified! Exiting")
sys.exit(1)
# Start up Wenet TX.
tx = PacketTX.PacketTX(serial_port=args.txport, serial_baud=args.baudrate, callsign=callsign, log_file="debug.log", udp_listener=55674)
tx = PacketTX.PacketTX(radio=radio, callsign=callsign, log_file="debug.log", udp_listener=55674)
tx.start_tx()
# Sleep for a second to let the transmitter fire up.
@ -46,11 +80,12 @@ system_time_set = False
# Disable Systemctl NTP synchronization so that we can set the system time on first GPS lock.
# This is necessary as NTP will refuse to sync the system time to the information we feed it via ntpshm unless
# the system clock is already within a few seconds.
ret_code = os.system("timedatectl set-ntp 0")
if ret_code == 0:
tx.transmit_text_message("GPS Debug: Disabled NTP Sync until GPS lock.")
else:
tx.transmit_text_message("GPS Debug: Could not disable NTP sync.")
if args.gps.lower() != 'none':
ret_code = os.system("timedatectl set-ntp 0")
if ret_code == 0:
tx.transmit_text_message("GPS Debug: Disabled NTP Sync until GPS lock.")
else:
tx.transmit_text_message("GPS Debug: Could not disable NTP sync.")
def handle_gps_data(gps_data):
""" Handle GPS data passed to us from the UBloxGPS instance """
@ -87,14 +122,19 @@ def handle_gps_data(gps_data):
# Try and start up the GPS rx thread.
try:
gps = ublox.UBloxGPS(port=args.gps,
dynamic_model = ublox.DYNAMIC_MODEL_AIRBORNE1G,
update_rate_ms = 1000,
debug_ptr = tx.transmit_text_message,
callback = handle_gps_data,
log_file = 'gps_data.log'
)
if args.gps.lower() != 'none':
gps = ublox.UBloxGPS(port=args.gps,
dynamic_model = ublox.DYNAMIC_MODEL_AIRBORNE1G,
update_rate_ms = 1000,
debug_ptr = tx.transmit_text_message,
callback = handle_gps_data,
log_file = 'gps_data.log'
)
else:
tx.transmit_text_message("No GPS configured. No GPS data will be overlaid.")
gps = None
except Exception as e:
tx.transmit_text_message("ERROR: Could not Open GPS - %s" % str(e), repeats=5)
gps = None
@ -152,12 +192,14 @@ def post_process_image(filename):
# Finally, initialise the PiCam capture object.
picam = WenetPiCamera2.WenetPiCamera2(
tx_resolution=(1936,1088),
tx_resolution=args.resize,
callsign=callsign,
num_images=5,
debug_ptr=tx.transmit_text_message,
vertical_flip=False,
horizontal_flip=False)
vertical_flip=args.vflip,
horizontal_flip=args.hflip,
whitebalance=args.whitebalance,
lens_position=args.lensposition)
# .. and start it capturing continuously.
picam.run(destination_directory="./tx_images/",
tx = tx,
@ -177,7 +219,8 @@ except KeyboardInterrupt:
print("Closing")
picam.stop()
tx.close()
gps.close()
if gps:
gps.close()

Wyświetl plik

@ -7,7 +7,8 @@
# Released under GNU GPL v3 or later
#
import PacketTX, sys, os, argparse
import PacketTX, sys, os, argparse, logging
from radio_wrappers import *
# Set to whatever resolution you want to test.
file_path = "../test_images/%d_raw.bin" # _raw, _800x608, _640x480, _320x240
@ -35,13 +36,46 @@ def transmit_file(filename, tx_object):
tx_object.wait()
parser = argparse.ArgumentParser()
parser.add_argument("--baudrate", default=115200, type=int, help="Transmitter baud rate. Defaults to 115200 baud.")
parser.add_argument("--rfm98w", default=None, type=int, help="If set, configure a RFM98W on this SPI device number.")
parser.add_argument("--frequency", default=443.500, type=float, help="Transmit Frequency (MHz). (Default: 443.500 MHz)")
parser.add_argument("--baudrate", default=115200, type=int, help="Wenet TX baud rate. (Default: 115200).")
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("-v", "--verbose", action='store_true', default=False, help="Show additional debug info.")
args = parser.parse_args()
if args.verbose:
logging_level = logging.DEBUG
else:
logging_level = logging.INFO
# Set up logging
logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", level=logging_level)
radio = None
if args.rfm98w is not None:
radio = RFM98W_Serial(
spidevice = args.rfm98w,
frequency = args.frequency,
baudrate = args.baudrate,
serial_port = args.serial_port,
tx_power_dbm = args.tx_power
)
# Other radio options would go here.
else:
logging.critical("No radio type specified! Exiting")
sys.exit(1)
tx = PacketTX.PacketTX(
radio=radio,
udp_listener=55674)
tx = PacketTX.PacketTX(debug=debug_output, serial_baud=args.baudrate)
tx.start_tx()
print("TX Started. Press Ctrl-C to stop.")
try:
for img in image_numbers:

Wyświetl plik

@ -3,8 +3,8 @@ Description=wenet_tx
After=basic.target
[Service]
# Update this path if not running as the pi user!
ExecStart=/home/pi/wenet/start_tx_systemd.sh
# Update this path if the wenet files are not in the pi user's home directory!
ExecStart=/home/pi/wenet/start_tx.sh
Restart=always
RestartSec=30
# This one too!