WIP: kw1fox-2/software #13

Draft
Kenwood wants to merge 27 commits from kw1fox-2/software into master
40 changed files with 535441 additions and 397 deletions

3
.gitignore vendored
View File

@ -10,3 +10,6 @@ __pycache__
*.jpg *.jpg
*.wav *.wav
*.pdf *.pdf
# Dev
.vscode

BIN
CAD/kw1fox-2/Assembly.FCStd Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
Resources/KitSuneAero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

BIN
Resources/RadioFont.ttf Normal file

Binary file not shown.

View File

@ -1,70 +0,0 @@
### DISCLAIMER
### This is an example Makefile and it MUST be configured to suit your needs.
### For detailed explanations about all the available options,
### please refer to https://github.com/sudar/Arduino-Makefile/blob/master/arduino-mk-vars.md
### PROJECT_DIR
### This is the path to where you have created/cloned your project
PROJECT_DIR = $(shell dirname $(shell pwd))
### ARDMK_DIR
### Path to the Arduino-Makefile directory.
ARDMK_DIR = $(PROJECT_DIR)/Arduino-Makefile
### ARDUINO_DIR
### Path to the Arduino application and resources directory.
ARDUINO_DIR = /usr/share/arduino
### USER_LIB_PATH
### Path to where the your project's libraries are stored.
USER_LIB_PATH := $(realpath $(PROJECT_DIR)/lib)
### BOARD_TAG & BOARD_SUB
### For Arduino IDE 1.0.x
### Only BOARD_TAG is needed. It must be set to the board you are currently using. (i.e uno, mega2560, etc.)
# BOARD_TAG = mega2560
### For Arduino IDE 1.6.x
### Both BOARD_TAG and BOARD_SUB are needed. They must be set to the board you are currently using. (i.e BOARD_TAG = uno, mega, etc. & BOARD_SUB = atmega2560, etc.)
### Note: for the Arduino Uno, only BOARD_TAG is mandatory and BOARD_SUB can be equal to anything
BOARD_TAG = mega
BOARD_SUB = atmega2560
### MONITOR_PORT
### The port your board is connected to. Using an '*' tries all the ports and finds the right one. Choose one of the two.
MONITOR_PORT = /dev/ttyUSB*
# MONITOR_PORT = /dev/ttyACM*
### MONITOR_BAUDRATE
### It must be set to Serial baudrate value you are using.
MONITOR_BAUDRATE = 115200
### AVR_TOOLS_DIR
### Path to the AVR tools directory such as avr-gcc, avr-g++, etc.
AVR_TOOLS_DIR = /usr
### AVRDUDE
### Path to avrdude directory.
AVRDUDE = /usr/bin/avrdude
### CFLAGS_STD
CFLAGS_STD = -std=gnu11
### CXXFLAGS_STD
### You can choose wich ever you like
# CXXFLAGS_STD = -std=gnu++11
CXXFLAGS_STD = -std=gnu++17
### CPPFLAGS
### Flags you might want to set for debugging purpose. Comment to stop.
CXXFLAGS += -pedantic -Wall -Wextra
LDFLAGS += -fdiagnostics-color
### OBJDIR
### Don't touch this!
### This is were you put the binaries you just compile using 'make'
CURRENT_DIR = $(shell basename $(CURDIR))
OBJDIR = $(PROJECT_DIR)/build/$(CURRENT_DIR)/$(BOARD_TAG)
### path to Arduino.mk, inside the ARDMK_DIR, don't touch.
include $(ARDMK_DIR)/Arduino.mk

View File

@ -1,63 +0,0 @@
/* Crawler Slave
*
* This code runs on the crawler i2c network
* and provides a cleaner, less CPU intensive control over PWM devices.
*/
#include <Wire.h>
#include <Servo.h>
// This servo is used to wipe and clean the camera lens
Servo windowWiperServo;
// Variables populated over i2c from master
int id;
int val;
void setup() {
// For debugging
//Serial.begin(115200);
// Attach the wiper servo to pin 9
windowWiperServo.attach(9);
// This is the address the pi will speak to us at
Wire.begin(0x4);
// Call receiveEvent when data received
Wire.onReceive(receiveEvent);
// Setup LED
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
//Serial.println("Started");
}
// Just loop to keep the running code alive, and wait for events to happen.
void loop() {
delay(50);
}
// This method runs when we receive a message
void receiveEvent(int n) {
Wire.read(); // Remove smbus trash
if (true) { // Dont do anything if this is not true
id = Wire.read(); // ID of the servo/device to access
val = Wire.read(); // Value to assign
//Serial.println(id);
//Serial.println(val);
switch(id) {
case 1:
windowWiperServo.write(val);
break;
}
}
// Prevents a bug where if bytes are left in buffer, arduino crashes.
while (Wire.available()) {
Wire.read();
}
}

View File

@ -0,0 +1,8 @@
# Joe wrote this
install:
sudo cp qsstv.service /etc/systemd/system/qsstv.service
sudo cp robotstreamer.service /etc/systemd/system/robotstreamer.service
sudo systemctl daemon-reload
sudo systemctl enable qsstv.service
sudo systemctl enable robotstreamer.service

12
Software/houston/Pipfile Normal file
View File

@ -0,0 +1,12 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
xlib = "*"
pillow = "*"
[dev-packages]
[requires]

78
Software/houston/Pipfile.lock generated Normal file
View File

@ -0,0 +1,78 @@
{
"_meta": {
"hash": {
"sha256": "d38f5cc73695023ab5e5dc66a552a8d0bb9d3842a276ca547364ce1d6baaf5b9"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"pillow": {
"hashes": [
"sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e",
"sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595",
"sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512",
"sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c",
"sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477",
"sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a",
"sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4",
"sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e",
"sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5",
"sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378",
"sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a",
"sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652",
"sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7",
"sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a",
"sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a",
"sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6",
"sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165",
"sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160",
"sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331",
"sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b",
"sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458",
"sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033",
"sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8",
"sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481",
"sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58",
"sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7",
"sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3",
"sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea",
"sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34",
"sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3",
"sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8",
"sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581",
"sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244",
"sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef",
"sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0",
"sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2",
"sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97",
"sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"
],
"index": "pypi",
"version": "==9.1.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"version": "==1.16.0"
},
"xlib": {
"hashes": [
"sha256:60b7cd5d90f5d5922d9ce27b61589c07d970796558d417461db7b66e366bc401",
"sha256:8eee67dad83ef4b82bbbfa85d51eeb20c79d12b119fe25aa1d27bd602ff82212"
],
"index": "pypi",
"version": "==0.21"
}
},
"develop": {}
}

View File

@ -0,0 +1,148 @@
import time
import subprocess as sp
from Xlib import display, X
from PIL import Image, ImageFont, ImageDraw
class Streamer:
def __init__(self):
self.x1, self.y1 = 580, 620
# Create display
self.dsp = display.Display(":0")
# This is the root of the screen
self.root = self.dsp.screen().root
# ffmpeg command
# ffmpeg --stream_loop -1 -re -i ~/INPUT_FILE -vcodec libx264 -profile:v main -preset:v medium -r 20 -g 60 -keyint_min 60 -sc_threshold 0 -b:v 2500k -maxrate 2500k -bufsize 2500k -sws_flags lanczos+accurate_rnd -acodec aac -b:a 96k -ar 48000 -ac 2 -f flv rtmp://rtmp.robotstreamer.com/live/123?key=123"
cmd_out = [
"ffmpeg",
"-f",
"image2pipe",
"-vcodec",
"png",
"-r",
"5", # FPS
"-i",
"-", # Indicated input comes from pipe
"-vcodec",
"libx264",
"-profile:v",
"main",
"-pix_fmt",
"yuv420p",
"-preset:v",
"medium",
"-r",
"30",
"-g",
"60",
"-keyint_min",
"60",
"-sc_threshold",
"0",
"-b:v",
"2500k",
"-maxrate",
"1500k",
"-bufsize",
"1500k",
"-sws_flags",
"lanczos+accurate_rnd",
"-acodec",
"aac",
"-b:a",
"96k",
"-r",
"6",
"-ar",
"48000",
"-ac",
"2",
"-f",
"flv",
"rtmp://rtmp.robotstreamer.com/live/topkek",
]
# This is the ffmpeg pipe streamer!
self.pipe = sp.Popen(cmd_out, stdin=sp.PIPE)
# Graphics and resources
self.font = ImageFont.truetype(r"../../Resources/RadioFont.ttf", 20)
def getFrame(self):
"""
Returns a single cropped sstv video frame
"""
raw = self.root.get_image(0, 0, self.x1, self.y1, X.ZPixmap, 0xFFFFFFFF)
image = Image.frombytes("RGB", (self.x1, self.y1), raw.data, "raw", "BGRX")
image = image.crop((0, 182, self.x1, self.y1))
return image
def drawGraphics(self, image: Image):
"""
Draws graphics and sprites over
a frame
"""
frame = Image.new(mode="RGB", size=(1280, 720))
# Scale and paste
image = image.resize((960, 720), Image.ANTIALIAS)
frame.paste(image)
# Populate text, normally this should poll or use shared vars!
info = f"""
KW1FOX-1
Online!
Volt: N/A
Last comm: {int(time.time())}
KW1FOX-2
Offline.
Volt: N/A
Last comm: N/A
KW1FOX-3
Offline.
Volt: N/A
Last comm: N/A
Currently Showing:
KW1FOX-1
NOCOM
NOMETA
"""
# Draw text
draw = ImageDraw.Draw(frame)
draw.text((960 + 10, 10), info, font=self.font, align="left")
return frame
def stream(self):
"""
Actually streams!
"""
while True:
self.drawGraphics(self.getFrame()).save(self.pipe.stdin, "PNG")
def __del__(self):
self.dsp.close()
self.pipe.stdin.close()
self.pipe.wait()
if __name__ == "__main__":
myStreamer = Streamer()
# myStreamer.drawGraphics(Image.open("../../Resources/KW1FOX-1_320x240.png")).show()
myStreamer.stream()

13
Software/houston/notes.md Normal file
View File

@ -0,0 +1,13 @@
# Notes
Taking some inspiration from this repo: https://github.com/xssfox/sstv-skimmer/blob/main/Dockerfile
## Installing qsstv and pulseaudio on server
Requires `houston` user
as well as qsstv (obvio)
and i should prolly install `pulseaudio` as well
Also, i edited the xorg.conf so it never times out.

View File

@ -0,0 +1,8 @@
[Unit]
Description=Runs a qsstv instance as houston with houston's config
[Service]
ExecStart=sudo su -l houston -c "/opt/radio/lewis-crawler/Software/houston/sstv.sh"
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,199 @@
[CW]
cwWPM=12
cwtext=
cwtone=800
[DIRECTORIES]
audioPath=/home/parallels/qsstv/audio/
docURL=http://users.telenet.be/on4qz/qsstv/manual
rxDRMImagesPath=/tmp/rx_images
rxSSTVImagesPath=/tmp/rx_images
saveTXimages=false
templatesPath=/home/parallels/qsstv/templates/
txDRMImagesPath=/home/parallels/qsstv/tx_drm/
txSSTVImagesPath=/home/parallels/qsstv/tx_sstv/
txStockImagesPath=/home/parallels/qsstv/tx_stock/
[DRMPROFILE]
drmPF1Bandwidth=0
drmPF1Interleave=0
drmPF1Mode=0
drmPF1Name=Profile 1
drmPF1Protection=0
drmPF1QAM=0
drmPF1ReedSolomon=0
drmPF2Bandwidth=0
drmPF2Interleave=0
drmPF2Mode=0
drmPF2Name=Profile 2
drmPF2Protection=0
drmPF2QAM=0
drmPF2ReedSolomon=0
drmPF3Bandwidth=0
drmPF3Interleave=0
drmPF3Mode=0
drmPF3Name=Profile 3
drmPF3Protection=0
drmPF3QAM=0
drmPF3ReedSolomon=0
[FREQSELECT]
additionalCommand=
additionalCommandHex=false
frequencyList=@Invalid()
modeList=@Invalid()
passBandList=@Invalid()
sbModeList=@Invalid()
[FTPCONFIG]
addExtension=false
enableFTP=false
ftpDefaultImageFormat=png
ftpLogin=
ftpNumImages=30
ftpPassword=
ftpPort=21
ftpRemoteDRMDirectory=
ftpRemoteHost=
ftpRemoteSSTVDirectory=
ftpSaveFormat=0
[GUI]
backGroundColor=@Variant(\0\0\0\x43\x1\xff\xff\0\0VV\xe6\xe6\0\0)
confirmClose=true
confirmDeletion=true
galleryColumns=4
galleryRows=4
imageBackGroundColor=@Variant(\0\0\0\x43\x1\xff\xff\x80\x80\x80\x80\x80\x80\0\0)
imageStretch=true
lowRes=false
slowCPU=false
[HYBRID]
enableHybridNotify=true
enableHybridRx=true
hybridFtpHybridFilesDirectory=HybridFiles1
hybridFtpLogin=
hybridFtpPassword=
hybridFtpPort=21
hybridFtpRemoteDirectory=
hybridFtpRemoteHost=
hybridNotifyDir=RxOkNotifications1
onlineStatusDir=OnlineCallsigns1
[MAIN]
transmissionModeIndex=0
windowHeight=768
windowWidth=1024
windowX=0
windowY=0
[PERSONAL]
callsign=NOCALL
firstname=NOFIRSTNAME
lastname=NONAME
locator=NOLOCATOR
onlinestatusenabled=true
onlinestatustext=
qth=NOWHERE
[REPEATER]
repeaterAcknowledge=
repeaterEnabled=false
repeaterIdleTemplate=
repeaterIdleTxMode=0
repeaterImage1=
repeaterImage2=
repeaterImage3=
repeaterImage4=
repeaterImageInterval=10
repeaterImageSize=10
repeaterTemplate=
repeaterTxDelay=5
repeaterTxMode=0
[RX]
autoSave=true
autoSlantAdjust=true
defaultImageFormat=png
minCompletion=50
sensitivity=2
sstvModeIndexRx=10
[SOUND]
alsaSelected=false
inputAudioDevice=default -- Playback/recording through the PulseAudio sound server
outputAudioDevice=default -- Playback/recording through the PulseAudio sound server
pttToneOtherChannel=false
pulseSelected=true
recordingSize=100
rxclock=48000
soundRoutingInput=0
soundRoutingOutput=0
swapChannel=false
txclock=48000
[SPECTRUM]
avg=0.9
maxdb=-25
range=35
[TX]
compressedSize=5000
drmBandWith=0
drmInterLeaver=0
drmProtection=0
drmQAM=0
drmReedSolomon=0
drmRobMode=0
sstvModeIndexTx=0
templateIndex=-1
useCW=false
useHybrid=false
useTemplate=false
useVOX=false
[WATERFALL]
bsrWF=BSR
endBinWF=END BIN
endPicWF=END PIC
endRepeaterWF=END RPT
fixWF=FIX
sampleString=Sample Text
startBinWF=START BIN
startPicWF=START PIC
startRepeaterWF=START RPT
wfBold=false
wfFont=Aharoni CLM
wfFontSize=12
[Waterfall]
text1=rest
text2=
text3=
text4=
[logging]
deduplicate=true
maskBA="@Variant(\0\0\0\r\0\0\0,\0\0\0\0\0\0)"
[radio1]
XMLRPCPort=7362
activeDTR=false
activeRTS=true
baudrate=9600
civAddress=
databits=8
enableCAT=false
enableSerialPTT=false
enableXMLRPC=false
handshake=None
nactiveDTR=false
nactiveRTS=false
parity=None
pttSerialPort=/dev/ttyS0
pttType=1
radioModel="29001 ADAT www.adat.ch,ADT-200A"
serialPort=/dev/ttyS0
stopbits=1
txOnDelay=0

View File

@ -0,0 +1,9 @@
[Unit]
Description=Runs the robotstreamer stream
[Service]
ExecStart=sudo su -l houston -c "/opt/radio/lewis-crawler/Software/houston/stream.sh"
Restart=on-failure
[Install]
WantedBy=multi-user.target

12
Software/houston/sstv.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# Runs all the required fluff for spawning qsstv with notifing and stuff.
# Make tempdirs
mkdir -p /tmp/sstv_images
# Copy config file
cp /opt/radio/lewis-crawler/Software/houston/qsstv_9.0.conf ~/.config/ON4QZ/qsstv_9.0.conf
# Spawn qsstv
xinit -geometry =1280x960+0+0 -fn 8x13 -j -fg white -bg black qsstv -- -nocursor

5
Software/houston/stream.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
# Runs the robotstream
cd /opt/radio/lewis-crawler/Software/houston/ && pipenv run python RobotStreamer.py

15
Software/kw1fox-2/Pipfile Normal file
View File

@ -0,0 +1,15 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
pysstv = "*"
pillow = "*"
pyaudio = "*"
pi-ina219 = "*"
[dev-packages]
[requires]
python_version = "3.10"

121
Software/kw1fox-2/Pipfile.lock generated Normal file
View File

@ -0,0 +1,121 @@
{
"_meta": {
"hash": {
"sha256": "377233137977bb1db670b363317a6a2619ddd8544f91531a5224826022fa3e23"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"adafruit-gpio": {
"hashes": [
"sha256:d6465b92c866c51ca8f3bc1e8f2ec36f5ccdb46d0fd54101c1109756d4a2dcd0"
],
"version": "==1.0.3"
},
"adafruit-pureio": {
"hashes": [
"sha256:2caf22fb07c7f771d83267f331a76cde314723f884a9570ea6f768730c87a879"
],
"markers": "python_full_version >= '3.5.0'",
"version": "==1.1.9"
},
"mock": {
"hashes": [
"sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62",
"sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"
],
"markers": "python_version >= '3.6'",
"version": "==4.0.3"
},
"pi-ina219": {
"hashes": [
"sha256:29524cc308c56a5c483f551187923d3e167f2d39a29dc64c731ba7a8e04d022b"
],
"index": "pypi",
"version": "==1.4.0"
},
"pillow": {
"hashes": [
"sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e",
"sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595",
"sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512",
"sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c",
"sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477",
"sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a",
"sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4",
"sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e",
"sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5",
"sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378",
"sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a",
"sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652",
"sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7",
"sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a",
"sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a",
"sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6",
"sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165",
"sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160",
"sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331",
"sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b",
"sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458",
"sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033",
"sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8",
"sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481",
"sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58",
"sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7",
"sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3",
"sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea",
"sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34",
"sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3",
"sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8",
"sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581",
"sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244",
"sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef",
"sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0",
"sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2",
"sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97",
"sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"
],
"index": "pypi",
"version": "==9.1.0"
},
"pyaudio": {
"hashes": [
"sha256:0d92f6a294565260a282f7c9a0b0d309fc8cc988b5ee5b50645634ab9e2da7f7",
"sha256:259bb9c1363be895b4f9a97e320a6017dd06bc540728c1a04eb4a7b6fe75035b",
"sha256:2a19bdb8ec1445b4f3e4b7b109e0e4cec1fd1f1ce588592aeb6db0b58d4fb3b0",
"sha256:51b558d1b28c68437b53218279110db44f69f3f5dd3d81859f569a4a96962bdc",
"sha256:589bfad2c615dd4b5d3757e763019c42ab82f06fba5cae64ec02fd7f5ae407ed",
"sha256:8f89075b4844ea94dde0c951c2937581c989fabd4df09bfd3f075035f50955df",
"sha256:93bfde30e0b64e63a46f2fd77e85c41fd51182a4a3413d9edfaf9ffaa26efb74",
"sha256:cf1543ba50bd44ac0d0ab5c035bb9c3127eb76047ff12235149d9adf86f532b6",
"sha256:f78d543a98b730e64621ebf7f3e2868a79ade0a373882ef51c0293455ffa8e6e"
],
"index": "pypi",
"version": "==0.2.11"
},
"pysstv": {
"hashes": [
"sha256:a0306ff80bb25f28c4455c9dd75dd8cebbb3fa7d77b87af18e6cf2b86ade6b47"
],
"index": "pypi",
"version": "==0.5.4"
},
"spidev": {
"hashes": [
"sha256:8a7f5c289f161ea2ac4697fa8a10918232c990678dd0053084b3c43b1363910d"
],
"version": "==3.5"
}
},
"develop": {}
}

View File

@ -0,0 +1,3 @@
# KW1FOX-2 Software
This is the software for KW1FOX-2, the APRS/SSTV relay station.

View File

View File

@ -0,0 +1,29 @@
from PIL import Image
from pysstv.color import Robot36
from tools.pyaudio import PyAudioSSTV
class kw1fox2:
def __init__(self, sim=False):
# Lets us automate more testing locally by having a program-wide sim mode.
self.sim = sim
img = Image.open("../../Resources/KW1FOX-1_320x240.png")
self.sstv = Robot36(img, 44100, 16)
self.sstv.vox_enabled = True
self.sstv.write_wav("Mysstv.wav")
def run(self):
PyAudioSSTV(self.sstv).execute()
def take_snapshot(self):
if not self.sim:
raise NotImplementedError
if __name__ == "__main__":
station = kw1fox2()
station.run()

View File

View File

@ -0,0 +1,43 @@
#!/usr/bin/env python
"""
Example code from here: https://github.com/dnet/pySSTV/blob/master/pysstv/examples/pyaudio_sstv.py
"""
from __future__ import division
from pysstv.sstv import SSTV
from time import sleep
from itertools import islice
import struct, pyaudio
class PyAudioSSTV(object):
def __init__(self, sstv):
self.pa = pyaudio.PyAudio()
self.sstv = sstv
self.fmt = "<" + SSTV.BITS_TO_STRUCT[sstv.bits]
def __del__(self):
self.pa.terminate()
def execute(self):
self.sampler = self.sstv.gen_samples()
stream = self.pa.open(
format=self.pa.get_format_from_width(self.sstv.bits // 8),
channels=1,
rate=self.sstv.samples_per_sec,
output=True,
stream_callback=self.callback,
)
stream.start_stream()
while stream.is_active():
sleep(0.5)
stream.stop_stream()
stream.close()
def callback(self, in_data, frame_count, time_info, status):
frames = "".join(
struct.pack(self.fmt, b) for b in islice(self.sampler, frame_count)
)
return frames, pyaudio.paContinue

View File

@ -1,5 +0,0 @@
wiper_servo:
pin: 17
min_pulse: 0.000544
max_pulse: 0.0024

View File

@ -1,11 +0,0 @@
[Unit]
Description=Crawler service overseer, manages running main crawler software
After=multi-user.target
[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/python /srv/crawler/crawler.py
[Install]
WantedBy=multi-user.target

View File

@ -1,2 +0,0 @@
PySSTV
picamera

View File

@ -1,87 +0,0 @@
from shutil import copyfile
from PIL import Image, ImageFont, ImageDraw
from pysstv import color
import logging as log
try:
from picamera import PiCamera
except ModuleNotFoundError:
log.info("Running in simulator mode")
def take_photo():
# This def is meant to take a photograph from the robot,
# it should include all steps and error checking to raise the mast
# Take the photo, and put the mast down.
# Copy in the test pattern png (if photo process errors out, this will be used instead)
log.debug('Copying test pattern.')
copyfile('photos/TEST_PATTERN.jpg', 'working/working.jpg')
# Software to take the photo should be here
#copyfile('photos/camera_latest.jpg', 'working/working.jpg')
log.debug('Initalizing camera.')
try:
camera = PiCamera()
log.info('Saving photo.')
camera.capture('working/working.jpg')
except NameError:
log.info("Running in simulator mode, not replacing test pattern")
def mark_photo():
log.info('Opening photo for viewing.')
raw_img = Image.open("working/working.jpg") # Open the current working image
log.info('Resizing image.')
img = raw_img.resize((320, 240), Image.ANTIALIAS) # resize it for the radio
log.info('Drawing on image.')
if False:
TINT_COLOR = (255, 255, 255) # White text bg
TEXT_COLOR = (0,0,0)
else:
TINT_COLOR = (0, 0, 0) # Black text bg
TEXT_COLOR = (255,255,255)
TRANSPARENCY = .25 # Degree of transparency, 0-100%
OPACITY = int(255 * TRANSPARENCY)
overlay = Image.new('RGBA', img.size, TINT_COLOR+(0,))
draw = ImageDraw.Draw(overlay)
#bigfont = ImageFont.truetype(r'C:\Users\System-Pc\Desktop\arial.ttf', 20)
#smallfont = ImageFont.truetype(r'C:\Users\System-Pc\Desktop\arial.ttf', 17)
draw.rectangle(((0, 0), (90, 20)), fill=TINT_COLOR+(OPACITY,))
#draw.text((0, 0),"KW1FOX",TEXT_COLOR,font=bigfont) # Draw KW1FOX in the top left
draw.rectangle(((0, 40), (83, 80)), fill=TINT_COLOR+(OPACITY,))
#draw.text((0, 40),"day: 25.2",TEXT_COLOR,font=smallfont)
#draw.text((0, 60),"Volt: 13.8",TEXT_COLOR,font=smallfont)
#draw.text((0, 80),"Miles: 1.02",TEXT_COLOR,font=smallfont)
log.info('Converting image color.')
img = img.convert("RGBA")
img = Image.alpha_composite(img, overlay)
img = img.convert("RGB")
img.save('working/working.jpg') # Save the working image
return img
if __name__ == "__main__":
log.basicConfig(level=log.DEBUG)
# Take photograph.
log.info('Taking Photograph')
take_photo() # Saves a photograph to the working/working.jpg location
log.info('Done taking photo.')
# Draw neccicary text on photo
log.info('Drawing on photo.')
radio_photo = mark_photo() # draws text on working/working.jpg and returns a PIL image
log.info('Done drawing on photo.')
log.info('Creating slowscan.')
slowscan = color.Robot36(radio_photo, 48000, 16) # Image, rate, bits
#slowscan = color.MartinM1(radio_photo, 48000, 16) # Image, rate, bits
log.info('Saving out slowscan.')
slowscan.write_wav('working/working.wav')
#sstv('working/working.png', 'working/radio.wav', mode='Robot36')

View File

@ -1,73 +0,0 @@
from discord_webhook import DiscordWebhook
from picamera import PiCamera
from time import sleep
from gps import *
from smbus import SMBus
import time
addr = 0x4 # bus address
bus = SMBus(1) # indicates /dev/ic2-1
numb = 1
def writeData(value):
byteValue = StringToBytes(value)
bus.write_i2c_block_data(addr,0x00,byteValue) #first byte is 0=command byte.. just is.
return -1
def StringToBytes(val):
retVal = []
for c in val:
retVal.append(ord(c))
return retVal
try:
for _x in range (0, 2):
for i in range(78, 130):
writeData("WIPE-" + str(i))
time.sleep(0.02)
for i in range(130, 78, -1):
writeData("WIPE-" + str(i))
time.sleep(0.02)
except OSError:
print("Could not speak to ardujmemo")
def get_uptime():
with open('/proc/uptime', 'r') as f:
uptime_seconds = float(f.readline().split()[0])
return uptime_seconds
def getPositionData(gps):
location = [None]
while(location[0] == None):
print("Trying again")
nx = gpsd.next()
# For a list of all supported classes and fields refer to:
# https://gpsd.gitlab.io/gpsd/gpsd_json.html
if nx['class'] == 'TPV':
latitude = getattr(nx,'lat', "Unknown")
longitude = getattr(nx,'lon', "Unknown")
#print "Your position: lon = " + str(longitude) + ", lat = " + str(latitude)
location = [latitude, longitude]
return location
gpsd = gps(mode=WATCH_ENABLE|WATCH_NEWSTYLE)
loc = getPositionData(gpsd)
webhookURL = "https://discord.com/api/webhooks/856609966404534272/TR9tnLq2sIGZoOeADNswmGRNlzBcqM5aKihfU6snVTP9WhSSoVVvi7nT6i-ZfZS7Hcqm"
print(loc[0])
print(loc[1])
webhook = DiscordWebhook(url=webhookURL, content="Uptime: " + str( round( ((get_uptime() / 60) / 60 ), 2 )) + " hours. Lat is " + str(loc[0]) + ", long is " + str(loc[1]))
camera = PiCamera()
sleep(3) # let iso settle out
camera.capture('still.jpg')
with open("still.jpg", "rb") as f:
webhook.add_file(file=f.read(), filename='still.jpg')
response = webhook.execute() # Hit send

View File

@ -1,19 +0,0 @@
import RPi.GPIO as GPIO
import time
servoPIN = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(servoPIN, GPIO.OUT)
p = GPIO.PWM(servoPIN, 50) # GPIO 17 for PWM with 50Hz
p.start(2.5) # Initialization
try:
while True:
p.ChangeDutyCycle(10)
time.sleep(2)
p.ChangeDutyCycle(2.5)
time.sleep(2)
except KeyboardInterrupt:
p.stop()
GPIO.cleanup()

View File

@ -1,20 +0,0 @@
import smbus
import time
import struct
# for RPI version 1, use bus = smbus.SMBus(0)
bus = smbus.SMBus(1)
# This is the address we setup in the Arduino Program
address = 0x04
try:
for _x in range (0, 2):
for i in range(78, 130):
bus.write_i2c_block_data(address, 0, [1, i])
time.sleep(0.02)
for i in range(130, 78, -1):
bus.write_i2c_block_data(address, 0, [1, i])
time.sleep(0.02)
except OSError:
print("Could not speak to ardujmemo")

View File

@ -1,35 +0,0 @@
# Raspberry Pi Master for Arduino Slave
# i2c_master_pi.py
# Connects to Arduino via I2C
# DroneBot Workshop 2019
# https://dronebotworkshop.com
from smbus import SMBus
import time
addr = 0x8 # bus address
bus = SMBus(1) # indicates /dev/ic2-1
numb = 1
print ("Enter num")
for _x in range (0, 4):
for i in range(76, 130):
bus.write_byte(addr, i)
time.sleep(0.02)
for i in range(130, 76, -1):
bus.write_byte(addr, i)
time.sleep(0.02)
#while numb == 1:
#
# ledstate = input(">>>> ")
#
# if ledstate == "1":
# bus.write_byte(addr, 0x1) # switch it on
# elif ledstate == "0":
# bus.write_byte(addr, 0x0) # switch it on
# else:
# numb = 0

View File

@ -1,12 +0,0 @@
import RPi.GPIO as GPIO # Import Raspberry Pi GPIO library
from time import sleep # Import the sleep function from the time module
GPIO.setwarnings(False) # Ignore warning for now
GPIO.setmode(GPIO.BOARD) # Use physical pin numbering
GPIO.setup(12, GPIO.OUT, initial=GPIO.LOW) # Set pin 8 to be an output pin and set initial value to low (off)
while True: # Run forever
GPIO.output(12, GPIO.HIGH) # Turn on
sleep(1) # Sleep for 1 second
GPIO.output(12, GPIO.LOW) # Turn off
sleep(1) # Sleep for 1 second