Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/emoji-picker-element-1.21.0

This commit is contained in:
DJ2LS 2024-01-28 09:25:37 +01:00 committed by GitHub
commit ce87464d23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 941 additions and 2246 deletions

View file

@ -32,7 +32,7 @@ jobs:
repository: DJ2LS/FreeDATA
- name: Set up Python 3.11
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.11"

View file

@ -1,6 +1,7 @@
# FreeDATA - Protocols
## ARQ Sessions
An ARQ Session represents a reliable data transmission session from a sending station (A) to a receiving station (B). It uses automatic repeat request on top of different codec2 modes according to the transmission channel conditions.
So lets say A wants to send some data to B. A typical scenario would be like this:
@ -22,22 +23,19 @@ ISS->(1)IRS:BURST (ID, offset, payload),(ID, offset, payload),(ID, offset, paylo
IRS->(1)ISS:DATA ACK NACK (ID, next_offset, speed level, frames, snr)
```
### Frame details
#### SESSION_OPEN_REQ
ISS sends this first
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|origin|6|
|destination_crc|3|
| field | bytes |
| --------------- | ----- |
| session id | 1 |
| origin | 6 |
| destination_crc | 3 |
#### SESSION_OPEN_ACK
@ -45,14 +43,13 @@ Sent by the IRS in response to a SESSION_OPEN_REQ
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|origin|6|
|destination_crc|3|
|protocol version|1|
|snr|1|
| field | bytes |
| ---------------- | ----- |
| session id | 1 |
| origin | 6 |
| destination_crc | 3 |
| protocol version | 1 |
| snr | 1 |
#### SESSION_INFO
@ -60,13 +57,12 @@ ISS sends this in response to a SESSION_OPEN_ACK
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|total bytes|4|
|total crc|4|
|snr|1|
| field | bytes |
| ----------- | ----- |
| session id | 1 |
| total bytes | 4 |
| total crc | 4 |
| snr | 1 |
#### SESSION_INFO_ACK
@ -74,14 +70,13 @@ IRS sends this in response to a SESSION_INFO
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|total crc|4|
|snr|1|
|speed level|1|
|frames per burst|1|
| field | bytes |
| ---------------- | ----- |
| session id | 1 |
| total crc | 4 |
| snr | 1 |
| speed level | 1 |
| frames per burst | 1 |
#### Data Burst
@ -92,50 +87,49 @@ Mode according to handshake speed level
Frames per burst according to handshake
##### Modulation
Each burst is composed of frames_per_burst frames:
|preamble|f1|f2|f3|...|postamble|
##### Each data frame
|field|bytes|
|-|-|
|session id|1|
|offset|4|
|payload|(the remaining payload length)|
| field | bytes |
| ---------- | ------------------------------ |
| session id | 1 |
| offset | 4 |
| payload | (the remaining payload length) |
#### DATA_BURST_ACK
Sent by the IRS following successful decoding of burst.
|field|bytes|
|-|-|
|session id|1|
|next offset|4|
|next speed level|1|
|next frames per burst|1|
|snr|1|
| field | bytes |
| --------------------- | ----- |
| session id | 1 |
| next offset | 4 |
| next speed level | 1 |
| next frames per burst | 1 |
| snr | 1 |
#### DATA_BURST_NACK
Sent by the IRS following unsuccessful decoding of burst or timeout.
|field|bytes|
|-|-|
|session id|1|
|next offset|4|
|next speed level|1|
|next frames per burst|1|
|snr|1|
| field | bytes |
| --------------------- | ----- |
| session id | 1 |
| next offset | 4 |
| next speed level | 1 |
| next frames per burst | 1 |
| snr | 1 |
#### DATA ACK NACK
Sent by the IRS after receiving data with a state information.
| field |bytes|
|------------|-|
| session id |1|
| state |1|
| snr |1|
| field | bytes |
| ---------- | ----- |
| session id | 1 |
| state | 1 |
| snr | 1 |

View file

@ -19,22 +19,8 @@
"files": [
"dist",
"dist-electron",
"../modem/server.dist/",
],
"extraResources": [
{
"from": "../modem/server.dist/",
"to": "modem",
"filter": [
"**/*",
"!**/.git"
]
}
],
"mac": {
"target": [

View file

@ -2,7 +2,7 @@
"name": "FreeDATA",
"description": "FreeDATA",
"private": true,
"version": "0.11.1-alpha.3",
"version": "0.12.1-alpha",
"main": "dist-electron/main/index.js",
"scripts": {
"start": "vite",
@ -32,7 +32,6 @@
},
"homepage": "https://freedata.app",
"dependencies": {
"@electron/asar": "3.2.7",
"@electron/notarize": "2.2.0",
"@electron/universal": "2.0.0",
"@popperjs/core": "2.11.8",
@ -53,14 +52,9 @@
"mime": "4.0.1",
"nconf": "^0.12.1",
"pinia": "2.1.7",
"pouchdb": "8.0.1",
"pouchdb-browser": "8.0.1",
"pouchdb-find": "8.0.1",
"pouchdb-upsert": "2.2.0",
"qth-locator": "2.1.0",
"sass": "1.66.1",
"socket.io": "4.7.2",
"uuid": "9.0.1",
"uuid": "^9.0.1",
"vue": "3.3.12",
"vue-chartjs": "5.3.0",
"vuemoji-picker": "0.2.0"
@ -68,7 +62,7 @@
"devDependencies": {
"@types/nconf": "^0.10.6",
"@typescript-eslint/eslint-plugin": "6.17.0",
"@vitejs/plugin-vue": "4.5.2",
"@vitejs/plugin-vue": "5.0.3",
"electron": "28.1.3",
"electron-builder": "24.9.1",
"eslint": "8.56.0",
@ -80,7 +74,7 @@
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.20.1",
"typescript": "5.3.3",
"vite": "5.0.10",
"vite": "5.0.12",
"vite-plugin-electron": "0.28.0",
"vite-plugin-electron-renderer": "0.14.5",
"vitest": "1.0.2",

View file

@ -6,14 +6,9 @@ setActivePinia(pinia);
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
import {
getNewMessagesByDXCallsign,
resetIsNewMessage,
} from "../js/chatHandler";
function chatSelected(callsign) {
chat.selectedCallsign = callsign.toUpperCase();
// scroll message container to bottom
var messageBody = document.getElementById("message-container");
if (messageBody != null) {
@ -21,27 +16,6 @@ function chatSelected(callsign) {
messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
}
if (getNewMessagesByDXCallsign(callsign)[1] > 0) {
let messageArray = getNewMessagesByDXCallsign(callsign)[2];
console.log(messageArray);
for (const key in messageArray) {
resetIsNewMessage(messageArray[key].uuid, false);
}
}
try {
chat.beaconLabelArray = Object.values(
chat.sorted_beacon_list[chat.selectedCallsign].timestamp,
);
chat.beaconDataArray = Object.values(
chat.sorted_beacon_list[chat.selectedCallsign].snr,
);
} catch (e) {
console.log("beacon data not fetched: " + e);
chat.beaconLabelArray = [];
chat.beaconDataArray = [];
}
}
</script>
<template>
@ -50,33 +24,30 @@ function chatSelected(callsign) {
id="chat-list-tab"
role="chat-tablist"
>
<template v-for="(item, key) in chat.callsign_list" :key="item.dxcallsign">
<template v-for="(details, callsign, key) in chat.callsign_list" :key="callsign">
<a
class="list-group-item list-group-item-action list-group-item-dark rounded-2 border-0 mb-2"
:class="{ active: key == 0 }"
:id="`list-chat-list-${item}`"
:id="`list-chat-list-${callsign}`"
data-bs-toggle="list"
:href="`#list-${item}-messages`"
:href="`#list-${callsign}-messages`"
role="tab"
aria-controls="list-{{item}}-messages"
@click="chatSelected(item)"
aria-controls="list-{{callsign}}-messages"
@click="chatSelected(callsign)"
>
<div class="row">
<div class="col-9">
<strong>{{ item }}</strong>
<span
class="badge rounded-pill bg-danger"
v-if="getNewMessagesByDXCallsign(item)[1] > 0"
>
{{ getNewMessagesByDXCallsign(item)[1] }} new messages
</span>
<strong>{{ callsign }}</strong>
<!--<small> {{details.timestamp}} </small>-->
</div>
<div class="col-3">
<button
class="btn btn-sm btn-outline-secondary ms-2 border-0"
data-bs-target="#deleteChatModal"
data-bs-toggle="modal"
@click="chatSelected(item)"
@click="chatSelected(callsign)"
>
<i class="bi bi-three-dots-vertical"></i>
</button>

View file

@ -30,7 +30,7 @@ function getDateTime(timestampRaw) {
<template>
<div class="tab-content" id="nav-tabContent-chat-messages">
<template v-for="(callsign, key) in chat.callsign_list">
<template v-for="(details, callsign, key) in chat.callsign_list" :key="callsign">
<div
class="tab-pane fade show"
:class="{ active: key == 0 }"
@ -38,38 +38,44 @@ function getDateTime(timestampRaw) {
role="tabpanel"
:aria-labelledby="`list-chat-list-${callsign}`"
>
<template
v-for="item in chat.sorted_chat_list[callsign]"
:key="item._id"
>
<!--
<div v-if="prevChatMessageDay !== getDateTime(item.timestamp)">
<div class="separator my-2">
{{ (prevChatMessageDay = getDateTime(item.timestamp)) }}
</div>
</div>
-->
<div v-if="item.type === 'beacon' && item.status === 'received'">
<!-- {{ item }} -->
{{item}}
<hr>
<div v-if="item.direction === 'transmit'">
{{ console.log('Item direction:', item.direction) }}
<!--<sent-message :message="item" />-->
</div>
<div v-if="item.type === 'ping'">{{ item.snr }} dB ping received</div>
<div v-if="item.type === 'ping-ack'">
{{ item.snr }} dB ping-ack received
</div>
<div v-if="item.type === 'transmit'">
<sent-message :message="item" />
</div>
<div v-else-if="item.type === 'received'">
<received-message :message="item" />
<div v-else-if="item.direction === 'receive'">
{{ console.log('Item direction:', item.direction) }}
<!--<received-message :message="item" />-->
</div>
<!--
<div v-if="item.type === 'broadcast_transmit'">
<sent-broadcast-message :message="item" />
</div>
<div v-else-if="item.type === 'broadcast_received'">
<received-broadcast-message :message="item" />
</div>
-->
</template>
</div>
</template>

View file

@ -1,4 +1,5 @@
<template>
<div class="row justify-content-start mb-2">
<div :class="messageWidthClass">
<div class="card bg-light border-0 text-dark">
@ -11,7 +12,7 @@
</div>
<div class="card-body">
<p class="card-text">{{ message.msg }}</p>
<p class="card-text">{{ message.body }}</p>
</div>
<div class="card-footer p-0 bg-light border-top-0">
@ -70,20 +71,20 @@ export default {
},
methods: {
showMessageInfo() {
requestMessageInfo(this.message._id);
requestMessageInfo(this.message.id);
//let infoModal = Modal.getOrCreateInstance(document.getElementById('messageInfoModal'))
//console.log(this.infoModal)
//this.infoModal.show()
},
deleteMessage() {
deleteMessageFromDB(this.message._id);
deleteMessageFromDB(this.message.id);
},
async downloadAttachment() {
try {
// reset file store
chat.downloadFileFromDB = [];
const attachment = await getMessageAttachment(this.message._id);
const attachment = await getMessageAttachment(this.message.id);
const blob = new Blob([atob_FD(attachment[2])], {
type: `${attachment[1]};charset=utf-8`,
});
@ -121,6 +122,8 @@ export default {
},
getDateTime() {
console.log(this.message.timestamp)
var datetime = new Date(this.message.timestamp * 1000).toLocaleString(
navigator.language,
{

View file

@ -108,14 +108,11 @@ function newChat() {
let callsign = this.newChatCall.value;
callsign = callsign.toUpperCase().trim();
if (callsign === "") return;
startChatWithNewStation(callsign);
//startChatWithNewStation(callsign);
//updateAllChat(false);
this.newChatCall.value = "";
}
function syncWithModem() {
getRxBuffer();
}
</script>
<template>
@ -123,26 +120,15 @@ function syncWithModem() {
<div class="container">
<div class="row w-100">
<div class="col-3 p-0 me-2">
<div class="input-group bottom-0 m-0 ms-1">
<input
class="form-control"
maxlength="9"
style="text-transform: uppercase"
placeholder="callsign"
@keypress.enter="newChat()"
ref="newChatCall"
/>
<button
class="btn btn-sm btn-outline-success"
id="createNewChatButton"
type="button"
title="Start a new chat (enter dx call sign first)"
@click="newChat()"
<button
class="btn btn-outline-primary"
data-bs-target="#newChatModal"
data-bs-toggle="modal"
>
new chat
<i class="bi bi-pencil-square" style="font-size: 1.2rem"></i>
<i class="bi bi-pencil-square"> Start a new chat</i>
</button>
</div>
</div>
<div class="col-5 ms-2 p-0">
<!-- right side of chat nav bar-->
@ -164,18 +150,6 @@ function syncWithModem() {
</div>
</div>
</div>
<div class="col-2 ms-2 p-0">
<div class="input-group mb-0 p-0">
<button
type="button"
class="btn btn-outline-secondary"
@click="syncWithModem()"
>
Modem Sync {{ state.rx_buffer_length }}
</button>
</div>
</div>
</div>
</div>
</nav>

View file

@ -17,8 +17,7 @@ import chat_navbar from './chat_navbar.vue'
import chat_conversations from './chat_conversations.vue'
import chat_messages from './chat_messages.vue'
import {updateAllChat, newMessage, newBroadcast} from '../js/chatHandler'
import { newMessage } from '../js/messagesHandler.ts'
import {
Chart as ChartJS,
@ -54,10 +53,11 @@ function transmitNewMessage(){
return;
if (chat.selectedCallsign.startsWith("BC-")) {
newBroadcast(chat.selectedCallsign, chat.inputText)
return "new broadcast"
} else {
newMessage(chat.selectedCallsign, chat.inputText, chat.inputFile, chat.inputFileName, chat.inputFileSize, chat.inputFileType)
//newMessage(chat.selectedCallsign, chat.inputText, chat.inputFile, chat.inputFileName, chat.inputFileSize, chat.inputFileType)
newMessage(chat.selectedCallsign, chat.inputText)
}
// finally do a cleanup
//chatModuleMessage.reset();
@ -195,9 +195,11 @@ const speedChartData = computed(() => ({
<!-- trigger file selection modal -->
<!--
<button type="button" class="btn btn-outline-secondary border-0 rounded-pill me-1" data-bs-toggle="modal" data-bs-target="#fileSelectionModal">
<i class="bi bi-paperclip" style="font-size: 1.2rem"></i>
</button>
-->
<textarea

View file

@ -9,19 +9,16 @@ import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { sendModemTestFrame } from "../js/api";
import {
deleteChatByCallsign,
getNewMessagesByDXCallsign,
} from "../js/chatHandler";
import main_startup_check from "./main_startup_check.vue";
import { newMessage } from '../js/messagesHandler.ts'
function newChat(){
newMessage(chat.newChatCallsign, chat.newChatMessage)
}
function deleteChat() {
//console.log(chat.selectedCallsign)
deleteChatByCallsign(chat.selectedCallsign);
return
}
import {
@ -181,16 +178,12 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
<span class="input-group-text" id="basic-addon1"
>Total Messages</span
>
<span class="input-group-text" id="basic-addon1">{{
getNewMessagesByDXCallsign(chat.selectedCallsign)[0]
}}</span>
<span class="input-group-text" id="basic-addon1">...</span>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1">New Messages</span>
<span class="input-group-text" id="basic-addon1">{{
getNewMessagesByDXCallsign(chat.selectedCallsign)[1]
}}</span>
<span class="input-group-text" id="basic-addon1">...</span>
</div>
</div>
<div class="modal-footer">
@ -316,6 +309,75 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
</div>
</div>
<div
class="modal fade"
ref="modalEle"
id="newChatModal"
tabindex="-1"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteChatModalLabel">
Start a new chat
</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="alert alert-info" role="alert">
1. Enter destination callsign
<br>
2. Enter a first message
<br>
3. Pressing "START NEW CHAT"
</div>
<div class="form-floating mb-3">
<input type="text" class="form-control" id="floatingInputDestination" placeholder="dxcallsign / destination" maxlength="9"
style="text-transform: uppercase" @keypress.enter="newChat()" v-model="chat.newChatCallsign">
<label for="floatingInputDestination">dxcallsign / destination</label>
</div>
<div class="form-floating">
<textarea class="form-control" placeholder="Your first message" id="floatingTextareaNewChatMessage" style="height: 100px" v-model="chat.newChatMessage"></textarea>
<label for="floatingTextareaNewChatMessage">First message</label>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
<button
class="btn btn-sm btn-outline-success"
id="createNewChatButton"
type="button"
data-bs-dismiss="modal"
title="Start a new chat (enter dx call sign first)"
@click="newChat()"
>
START NEW CHAT
<i class="bi bi-pencil-square" style="font-size: 1.2rem"></i>
</button>
</div>
</div>
</div>
</div>
<!-- HELP MODALS AUDIO -->
<div
class="modal fade"

View file

@ -4,6 +4,10 @@ import {
validateCallsignWithoutSSID,
} from "./freedata";
import {
processFreedataMessages
} from "./messagesHandler"
function buildURL(params, endpoint) {
const url = "http://" + params.host + ":" + params.port + endpoint;
return url;
@ -15,8 +19,7 @@ async function apiGet(endpoint) {
if (!response.ok) {
throw new Error(`REST response not ok: ${response.statusText}`);
}
const data = await response.json();
return data;
return await response.json();
} catch (error) {
console.error("Error getting from REST:", error);
}
@ -127,3 +130,15 @@ export function setRadioParameters(frequency, mode, rf_level) {
export function getRadioStatus() {
return apiGet("/radio");
}
export async function getFreedataMessages(){
let res = await apiGet("/freedata/messages")
processFreedataMessages(res)
}
export async function sendFreedataMessage(dxcall, body) {
return await apiPost("/freedata/messages", {
dxcall: dxcall,
body: body,
});
}

File diff suppressed because it is too large Load diff

View file

@ -1,886 +0,0 @@
var net = require("net");
import { atob_FD, btoa_FD, sortByPropertyDesc } from "./freedata";
import { addDataToWaterfall } from "../js/waterfallHandler.js";
import {
newMessageReceived,
newBeaconReceived,
updateTransmissionStatus,
setStateSuccess,
setStateFailed,
} from "./chatHandler";
import { displayToast } from "./popupHandler";
// ----------------- init pinia stores -------------
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const stateStore = useStateStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
var client = new net.Socket();
var socketchunk = ""; // Current message, per connection.
// split character
//const split_char = "\0;\1;";
const split_char = "0;1;";
// global to keep track of Modem connection error emissions
var modemShowConnectStateError = 1;
var setTxAudioLevelOnce = true;
var setRxAudioLevelOnce = true;
// network connection Timeout
//setTimeout(connectModem, 2000);
function connectModem() {
//exports.connectModem = function(){
//console.log('connecting to Modem...')
//clear message buffer after reconnecting or initial connection
socketchunk = "";
client.connect(settings.modem_port, settings.modem_host);
}
client.on("connect", function () {
console.log("Modem connection established");
stateStore.modem_running_state = "running";
stateStore.busy_state = "-";
stateStore.arq_state = "-";
stateStore.frequency = "-";
stateStore.mode = "-";
stateStore.bandwidth = "-";
stateStore.dbfs_level = 0;
stateStore.updateTncState(client.readyState);
modemShowConnectStateError = 1;
});
client.on("error", function (err) {
if (modemShowConnectStateError == 1) {
console.log("Modem connection error");
console.log(err);
modemShowConnectStateError = 0;
stateStore.modem_running_state = "stopped";
}
setTimeout(connectModem, 500);
client.destroy();
stateStore.busy_state = "-";
stateStore.arq_state = "-";
stateStore.frequency = "-";
stateStore.mode = "-";
stateStore.bandwidth = "-";
stateStore.dbfs_level = 0;
stateStore.updateTncState(client.readyState);
});
/*
client.on('close', function(data) {
console.log(' Modem connection closed');
setTimeout(connectModem, 2000)
});
*/
client.on("end", function (data) {
console.log("Modem connection ended");
console.log(data);
stateStore.busy_state = "-";
stateStore.arq_state = "-";
stateStore.frequency = "-";
stateStore.mode = "-";
stateStore.bandwidth = "-";
stateStore.dbfs_level = 0;
stateStore.updateTncState(client.readyState);
client.destroy();
stateStore.modem_running_state = "stopped";
setTimeout(connectModem, 500);
});
function writeTncCommand(command) {
console.log(command);
// we use the writingCommand function to update our TCPIP state because we are calling this function a lot
// if socket opened, we are able to run commands
if (client.readyState == "open") {
client.write(command + "\n");
}
if (client.readyState == "closed") {
console.log("Modem SOCKET CONNECTION CLOSED!");
}
if (client.readyState == "opening") {
console.log("connecting to Modem...");
}
}
client.on("data", function (socketdata) {
stateStore.updateTncState(client.readyState);
/*
inspired by:
stackoverflow.com questions 9070700 nodejs-net-createserver-large-amount-of-data-coming-in
*/
socketdata = socketdata.toString("utf8"); // convert data to string
socketchunk += socketdata; // append data to buffer so we can stick long data together
// check if we received begin and end of json data
if (socketchunk.startsWith('{"') && socketchunk.endsWith('"}\n')) {
var data = "";
// split data into chunks if we received multiple commands
socketchunk = socketchunk.split("\n");
//don't think this is needed anymore
//data = JSON.parse(socketchunk[0])
// search for empty entries in socketchunk and remove them
for (let i = 0; i < socketchunk.length; i++) {
if (socketchunk[i] === "") {
socketchunk.splice(i, 1);
}
}
//iterate through socketchunks array to execute multiple commands in row
for (let i = 0; i < socketchunk.length; i++) {
//check if data is not empty
if (socketchunk[i].length > 0) {
//try to parse JSON
try {
data = JSON.parse(socketchunk[i]);
} catch (e) {
console.log("Throwing away data!!!!\n" + e); // "SyntaxError
//console.log(e); // "SyntaxError
console.log(socketchunk[i]);
socketchunk = "";
//If we're here, I don't think we want to process any data that may be in data variable
continue;
}
}
//console.log(data)
if (data["command"] == "modem_state") {
//console.log(data)
stateStore.rx_buffer_length = data["rx_buffer_length"];
stateStore.frequency = data["frequency"];
stateStore.busy_state = data["modem_state"];
stateStore.arq_state = data["arq_state"];
stateStore.mode = data["mode"];
stateStore.bandwidth = data["bandwidth"];
stateStore.tx_audio_level = data["tx_audio_level"];
stateStore.rx_audio_level = data["rx_audio_level"];
// if audio level is different from config one, send new audio level to modem
//console.log(parseInt(stateStore.tx_audio_level))
//console.log(parseInt(settings.tx_audio_level))
if (
parseInt(stateStore.tx_audio_level) !==
parseInt(settings.tx_audio_level) &&
setTxAudioLevelOnce === true
) {
setTxAudioLevelOnce = false;
console.log(setTxAudioLevelOnce);
setTxAudioLevel(settings.tx_audio_level);
}
if (
parseInt(stateStore.rx_audio_level) !==
parseInt(settings.rx_audio_level) &&
setRxAudioLevelOnce === true
) {
setRxAudioLevelOnce = false;
console.log(setRxAudioLevelOnce);
setRxAudioLevel(settings.rx_audio_level);
}
stateStore.dbfs_level = data["audio_dbfs"];
stateStore.ptt_state = data["ptt_state"];
stateStore.speed_level = data["speed_level"];
stateStore.fft = JSON.parse(data["fft"]);
stateStore.channel_busy = data["channel_busy"];
stateStore.channel_busy_slot = data["channel_busy_slot"];
addDataToWaterfall(JSON.parse(data["fft"]));
if (data["scatter"].length > 0) {
stateStore.scatter = data["scatter"];
}
// s meter strength
stateStore.s_meter_strength_raw = data["strength"];
if (stateStore.s_meter_strength_raw == "") {
stateStore.s_meter_strength_raw = "Unsupported";
stateStore.s_meter_strength_percent = 0;
} else {
// https://www.moellerstudios.org/converting-amplitude-representations/
stateStore.s_meter_strength_percent = Math.round(
Math.pow(10, stateStore.s_meter_strength_raw / 20) * 100,
);
}
stateStore.dbfs_level_percent = Math.round(
Math.pow(10, stateStore.dbfs_level / 20) * 100,
);
stateStore.dbfs_level = Math.round(stateStore.dbfs_level);
stateStore.arq_total_bytes = data["total_bytes"];
stateStore.heard_stations = data["stations"].sort(
sortByPropertyDesc("timestamp"),
);
stateStore.dxcallsign = data["dxcallsign"];
stateStore.beacon_state = data["beacon_state"];
stateStore.audio_recording = data["audio_recording"];
stateStore.hamlib_status = data["hamlib_status"];
stateStore.alc = data["alc"];
stateStore.rf_level = data["rf_level"];
stateStore.is_codec2_traffic = data["is_codec2_traffic"];
stateStore.arq_session_state = data["arq_session"];
stateStore.arq_state = data["arq_state"];
stateStore.arq_transmission_percent = data["arq_transmission_percent"];
stateStore.arq_seconds_until_finish = data["arq_seconds_until_finish"];
stateStore.arq_seconds_until_timeout =
data["arq_seconds_until_timeout"];
stateStore.arq_seconds_until_timeout_percent =
(stateStore.arq_seconds_until_timeout / 180) * 100;
if (data["speed_list"].length > 0) {
prepareStatsDataForStore(data["speed_list"]);
}
// TODO: Remove ported objects
/*
let Data = {
mycallsign: data["mycallsign"],
mygrid: data["mygrid"],
//channel_state: data['CHANNEL_STATE'],
info: data["info"],
rx_msg_buffer_length: data["rx_msg_buffer_length"],
tx_n_max_retries: data["tx_n_max_retries"],
arq_tx_n_frames_per_burst: data["arq_tx_n_frames_per_burst"],
arq_tx_n_bursts: data["arq_tx_n_bursts"],
arq_tx_n_current_arq_frame: data["arq_tx_n_current_arq_frame"],
arq_tx_n_total_arq_frames: data["arq_tx_n_total_arq_frames"],
arq_rx_frame_n_bursts: data["arq_rx_frame_n_bursts"],
arq_rx_n_current_arq_frame: data["arq_rx_n_current_arq_frame"],
arq_n_arq_frames_per_data_frame:
data["arq_n_arq_frames_per_data_frame"],
arq_bytes_per_minute: data["arq_bytes_per_minute"],
arq_compression_factor: data["arq_compression_factor"],
routing_table: data["routing_table"],
mesh_signalling_table: data["mesh_signalling_table"],
listen: data["listen"],
//speed_table: [{"bpm" : 5200, "snr": -3, "timestamp":1673555399},{"bpm" : 2315, "snr": 12, "timestamp":1673555500}],
};
*/
//continue to next for loop iteration, nothing else needs to be done here
continue;
}
// ----------- catch modem messages START -----------
//init message variable
var message = "";
if (data["freedata"] == "modem-message") {
// break early if we received a dummy callsign
// thats a kind of hotfix, as long as the modem isnt handling this better
if (
data["dxcallsign"] == "AA0AA-0" ||
data["dxcallsign"] == "ZZ9YY-0"
) {
break;
}
console.log(data);
switch (data["fec"]) {
case "is_writing":
// RX'd FECiswriting
break;
case "broadcast":
// RX'd FEC BROADCAST
var encoded_data = atob_FD(data["data"]);
var splitted_data = encoded_data.split(split_char);
var messageArray = [];
if (splitted_data[0] == "m") {
messageArray.push(data);
console.log(data);
}
break;
}
switch (data["cq"]) {
case "transmitting":
// CQ TRANSMITTING
displayToast(
"success",
"bi-arrow-left-right",
"Transmitting CQ",
5000,
);
break;
case "received":
// CQ RECEIVED
message = "CQ from " + data["dxcallsign"];
displayToast("success", "bi-person-arms-up", message, 5000);
break;
}
switch (data["qrv"]) {
case "transmitting":
// QRV TRANSMITTING
displayToast(
"info",
"bi-person-raised-hand",
"Transmitting QRV ",
5000,
);
break;
case "received":
// QRV RECEIVED
message = "QRV from " + data["dxcallsign"] + " | " + data["dxgrid"];
displayToast("success", "bi-person-raised-hand", message, 5000);
break;
}
switch (data["beacon"]) {
case "transmitting":
// BEACON TRANSMITTING
displayToast(
"success",
"bi-broadcast-pin",
"Transmitting beacon",
5000,
);
break;
case "received":
// BEACON RECEIVED
newBeaconReceived(data);
message =
"Beacon from " + data["dxcallsign"] + " | " + data["dxgrid"];
displayToast("info", "bi-broadcast", message, 5000);
break;
}
switch (data["ping"]) {
case "transmitting":
// PING TRANSMITTING
message = "Sending ping to " + data["dxcallsign"];
displayToast("success", "bi-arrow-right", message, 5000);
break;
case "received":
// PING RECEIVED
message =
"Ping request from " +
data["dxcallsign"] +
" | " +
data["dxgrid"];
displayToast("success", "bi-arrow-right-short", message, 5000);
break;
case "acknowledge":
// PING ACKNOWLEDGE
message =
"Received ping-ack from " +
data["dxcallsign"] +
" | " +
data["dxgrid"];
displayToast("success", "bi-arrow-left-right", message, 5000);
break;
}
// ARQ SESSION && freedata == modem-message
if (data["arq"] == "session") {
switch (data["status"]) {
case "connecting":
// ARQ Open
break;
case "connected":
// ARQ Opening
break;
case "waiting":
// ARQ Opening
break;
case "close":
// ARQ Closing
break;
case "failed":
// ARQ Failed
break;
}
}
// ARQ TRANSMISSION && freedata == modem-message
if (data["arq"] == "transmission") {
switch (data["status"]) {
case "opened":
// ARQ Open
message = "ARQ session opened: " + data["dxcallsign"];
displayToast("success", "bi-arrow-left-right", message, 5000);
break;
case "opening":
// ARQ Opening IRS/ISS
if (data["irs"] == "False") {
message = "ARQ session opening: " + data["dxcallsign"];
displayToast("info", "bi-arrow-left-right", message, 5000);
break;
} else {
message = "ARQ sesson request from: " + data["dxcallsign"];
displayToast("success", "bi-arrow-left-right", message, 5000);
break;
}
case "waiting":
// ARQ waiting
message = "Channel busy | ARQ protocol is waiting";
displayToast("warning", "bi-hourglass-split", message, 5000);
break;
case "receiving":
// ARQ RX
break;
case "failed":
// ARQ TX Failed
if (data["reason"] == "protocol version missmatch") {
message = "Protocol version mismatch!";
displayToast("danger", "bi-chevron-bar-expand", message, 5000);
setStateFailed();
break;
} else {
message = "Transmission failed";
displayToast("danger", "bi-x-octagon", message, 5000);
updateTransmissionStatus(data);
setStateFailed();
break;
}
switch (data["irs"]) {
case "True":
updateTransmissionStatus(data);
break;
default:
updateTransmissionStatus(data);
break;
}
break;
case "received":
// ARQ data received
console.log(data);
// we need to encode here to do a deep check for checking if file or message
//var encoded_data = atob(data['data'])
var encoded_data = atob_FD(data["data"]);
var splitted_data = encoded_data.split(split_char);
// new message received
if (splitted_data[0] == "m") {
console.log(splitted_data);
newMessageReceived(splitted_data, data);
}
break;
case "transmitting":
// ARQ transmitting
updateTransmissionStatus(data);
break;
case "transmitted":
// ARQ transmitted
message = "Data transmitted";
displayToast("success", "bi-check-sqaure", message, 5000);
updateTransmissionStatus(data);
setStateSuccess();
break;
}
}
}
}
//finally delete message buffer
socketchunk = "";
}
});
// Send Ping
//exports.sendPing = function (dxcallsign) {
export function sendPing(dxcallsign) {
var command =
'{"type" : "ping", "command" : "ping", "dxcallsign" : "' +
dxcallsign +
'"}';
writeTncCommand(command);
}
// Send Mesh Ping
//exports.sendMeshPing = function (dxcallsign) {
function sendMeshPing(dxcallsign) {
var command =
'{"type" : "mesh", "command" : "ping", "dxcallsign" : "' +
dxcallsign +
'"}';
writeTncCommand(command);
}
// Send CQ
//exports.sendCQ = function () {
export function sendCQ() {
var command = '{"type" : "broadcast", "command" : "cqcqcq"}';
writeTncCommand(command);
}
// Set AUDIO Level
export function setTxAudioLevel(value) {
var command =
'{"type" : "set", "command" : "tx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command);
}
export function setRxAudioLevel(value) {
var command =
'{"type" : "set", "command" : "rx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command);
}
// Send Message
export function sendMessage(obj) {
let dxcallsign = obj.dxcallsign;
let checksum = obj.checksum;
let uuid = obj.uuid;
let command = obj.command;
let filename = Object.keys(obj._attachments)[0];
//let filetype = filename.split(".")[1]
let filetype = obj._attachments[filename].content_type;
let file = obj._attachments[filename].data;
//console.log(obj._attachments)
//console.log(filename)
//console.log(filetype)
//console.log(file)
let data_with_attachment =
obj.timestamp +
split_char +
obj.msg +
split_char +
filename +
split_char +
filetype +
split_char +
file;
let data = btoa_FD(
"m" +
split_char +
command +
split_char +
checksum +
split_char +
uuid +
split_char +
data_with_attachment,
);
// TODO: REMOVE mode and frames from Modem!
var mode = 255;
var frames = 5;
command =
'{"type" : "arq", "command" : "send_raw", "uuid" : "' +
uuid +
'", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
console.log(command);
writeTncCommand(command);
}
/*
// Send Request message
//It would be then „m + split + request + split + request-type“
function sendRequest(dxcallsign, mode, frames, data, command) {
data = btoa_FD("m" + split_char + command + split_char + data);
command =
'{"type" : "arq", "command" : "send_raw", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
console.log(command);
console.log("--------------REQ--------------------");
writeTncCommand(command);
}
// Send Response message
//It would be then „m + split + request + split + request-type“
function sendResponse(dxcallsign, mode, frames, data, command) {
data = btoa_FD("m" + split_char + command + split_char + data);
command =
'{"type" : "arq", "command" : "send_raw", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
console.log(command);
console.log("--------------RES--------------------");
writeTncCommand(command);
}
//Send station info request
//exports.sendRequestInfo = function (dxcallsign) {
function sendRequestInfo(dxcallsign) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "0", "req");
}
//Send shared folder file list request
//exports.sendRequestSharedFolderList = function (dxcallsign) {
function sendRequestSharedFolderList(dxcallsign) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "1", "req");
}
//Send shared file request
//exports.sendRequestSharedFile = function (dxcallsign, file) {
function sendRequestSharedFile(dxcallsign, file) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "2" + file, "req");
}
//Send station info response
//exports.sendResponseInfo = function (dxcallsign, userinfo) {
function sendResponseInfo(dxcallsign, userinfo) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, userinfo, "res-0");
}
//Send shared folder response
//exports.sendResponseSharedFolderList = function (dxcallsign, sharedFolderList) {
function sendResponseSharedFolderList(dxcallsign, sharedFolderList) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, sharedFolderList, "res-1");
}
//Send shared file response
//exports.sendResponseSharedFile = function (
function sendResponseSharedFile(dxcallsign, sharedFile, sharedFileData) {
console.log(
"In sendResponseSharedFile",
dxcallsign,
sharedFile,
sharedFileData,
);
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, sharedFile + "/" + sharedFileData, "res-2");
}
*/
// Get RX BUffer
export function getRxBuffer() {
var command = '{"type" : "get", "command" : "rx_buffer"}';
writeTncCommand(command);
}
// OPEN ARQ SESSION
export function connectARQ(dxcallsign) {
var command =
'{"type" : "arq", "command" : "connect", "dxcallsign": "' +
dxcallsign +
'", "attempts": "10"}';
writeTncCommand(command);
}
// CLOSE ARQ SESSION
export function disconnectARQ() {
var command = '{"type" : "arq", "command" : "disconnect"}';
writeTncCommand(command);
}
// SEND TEST FRAME
export function sendTestFrame() {
var command = '{"type" : "set", "command" : "send_test_frame"}';
writeTncCommand(command);
}
// SEND FEC
export function sendFEC(mode, payload) {
var command =
'{"type" : "fec", "command" : "transmit", "mode" : "' +
mode +
'", "payload" : "' +
payload +
'"}';
writeTncCommand(command);
}
// SEND FEC IS WRITING
export function sendFecIsWriting(mycallsign) {
var command =
'{"type" : "fec", "command" : "transmit_is_writing", "mycallsign" : "' +
mycallsign +
'"}';
writeTncCommand(command);
}
// SEND FEC TO BROADCASTCHANNEL
//export function sendBroadcastChannel(channel, data_out, uuid) {
export function sendBroadcastChannel(obj) {
//let checksum = obj.checksum;
let command = obj.command;
let uuid = obj.uuid;
let channel = obj.dxcallsign;
let data_out = obj.msg;
let data = btoa_FD(
"m" +
split_char +
channel +
//split_char +
//checksum +
split_char +
uuid +
split_char +
data_out,
);
console.log(data.length);
let payload = data;
command =
'{"type" : "fec", "command" : "transmit", "mode": "datac4", "wakeup": "True", "payload" : "' +
payload +
'"}';
writeTncCommand(command);
}
// RECORD AUDIO
export function record_audio() {
var command = '{"type" : "set", "command" : "record_audio"}';
writeTncCommand(command);
}
// SET FREQUENCY
export function set_frequency(frequency) {
var command =
'{"type" : "set", "command" : "frequency", "frequency": ' + frequency + "}";
writeTncCommand(command);
}
// SET MODE
export function set_mode(mode) {
var command = '{"type" : "set", "command" : "mode", "mode": "' + mode + '"}';
writeTncCommand(command);
}
// SET rf_level
export function set_rf_level(rf_level) {
var command =
'{"type" : "set", "command" : "rf_level", "rf_level": "' + rf_level + '"}';
writeTncCommand(command);
}
// https://stackoverflow.com/a/50579690
// crc32 calculation
//console.log(crc32('abc'));
//console.log(crc32('abc').toString(16).toUpperCase()); // hex
/*
function crc32(r) {
for (var a, o = [], c = 0; c < 256; c++) {
a = c;
for (var f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1;
o[c] = a;
}
for (var n = -1, t = 0; t < r.length; t++)
n = (n >>> 8) ^ o[255 & (n ^ r.charCodeAt(t))];
return (-1 ^ n) >>> 0;
};
*/
// TODO Maybe moving this to another module
function prepareStatsDataForStore(data) {
// dummy data
//state.arq_speed_list = [{"snr":0.0,"bpm":104,"timestamp":1696189769},{"snr":0.0,"bpm":80,"timestamp":1696189778},{"snr":0.0,"bpm":70,"timestamp":1696189783},{"snr":0.0,"bpm":58,"timestamp":1696189792},{"snr":0.0,"bpm":52,"timestamp":1696189797},{"snr":"NaN","bpm":42,"timestamp":1696189811},{"snr":0.0,"bpm":22,"timestamp":1696189875},{"snr":0.0,"bpm":21,"timestamp":1696189881},{"snr":0.0,"bpm":17,"timestamp":1696189913},{"snr":0.0,"bpm":15,"timestamp":1696189932},{"snr":0.0,"bpm":15,"timestamp":1696189937},{"snr":0.0,"bpm":14,"timestamp":1696189946},{"snr":-6.1,"bpm":14,"timestamp":1696189954},{"snr":-6.1,"bpm":14,"timestamp":1696189955},{"snr":-5.5,"bpm":28,"timestamp":1696189963},{"snr":-5.5,"bpm":27,"timestamp":1696189963}]
var speed_listSize = 0;
if (typeof data == "undefined") {
speed_listSize = 0;
} else {
speed_listSize = data.length;
}
var speed_list_bpm = [];
for (let i = 0; i < speed_listSize; i++) {
speed_list_bpm.push(data[i].bpm);
}
var speed_list_timestamp = [];
for (let i = 0; i < speed_listSize; i++) {
let timestamp = data[i].timestamp * 1000;
let h = new Date(timestamp).getHours();
let m = new Date(timestamp).getMinutes();
let s = new Date(timestamp).getSeconds();
let time = h + ":" + m + ":" + s;
speed_list_timestamp.push(time);
}
var speed_list_snr = [];
for (let i = 0; i < speed_listSize; i++) {
let snr = NaN;
if (data[i].snr !== 0) {
snr = data[i].snr;
} else {
snr = NaN;
}
speed_list_snr.push(snr);
}
stateStore.arq_speed_list_bpm = speed_list_bpm;
stateStore.arq_speed_list_timestamp = speed_list_timestamp;
stateStore.arq_speed_list_snr = speed_list_snr;
}

View file

@ -1,3 +1,4 @@
/*
import {
newMessageReceived,
newBeaconReceived,
@ -5,7 +6,10 @@ import {
setStateSuccess,
setStateFailed,
} from "./chatHandler";
*/
import { displayToast } from "./popupHandler";
import { getFreedataMessages } from "./api"
import { processFreedataMessages } from "./messagesHandler.ts"
// ----------------- init pinia stores -------------
import { setActivePinia } from "pinia";
@ -58,6 +62,14 @@ export function eventDispatcher(data) {
return;
}
switch (data["message-db"]) {
case "changed":
console.log("fetching new messages...")
var messages = getFreedataMessages()
processFreedataMessages(messages)
return;
}
switch (data["ptt"]) {
case true:
case false:
@ -97,6 +109,9 @@ export function eventDispatcher(data) {
message = "Connected to server";
displayToast("success", "bi-ethernet", message, 5000);
stateStore.modem_connection = "connected";
getFreedataMessages()
return;
case "arq":

View file

@ -1,3 +1,6 @@
const os = require("os");
const path = require("path");
/**
* Binary to ASCII replacement
* @param {string} data in normal/usual utf-8 format
@ -97,3 +100,31 @@ export function validateCallsignWithoutSSID(callsign: string) {
}
return true;
}
export function getAppDataPath() {
const platform = os.platform();
let appDataPath;
// Check if running in GitHub Actions
const isGitHubActions = process.env.GITHUB_ACTIONS === "true";
if (isGitHubActions) {
return "/home/runner/work/FreeDATA/FreeDATA/gui/config";
}
switch (platform) {
case "darwin": // macOS
appDataPath = path.join(os.homedir(), "Library", "Application Support");
break;
case "win32": // Windows
appDataPath =
process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
break;
case "linux": // Linux
appDataPath = path.join(os.homedir(), ".config");
break;
default:
throw new Error("Unsupported platform");
}
return appDataPath;
}

View file

@ -0,0 +1,66 @@
// pinia store setup
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useChatStore } from "../store/chatStore.js";
const chatStore = useChatStore(pinia);
import { sendFreedataMessage } from "./api"
interface Message {
id: string;
timestamp: string;
origin: string;
destination: string;
direction: string;
body: string;
attachments: any[];
status: any;
statistics: any;
}
export function processFreedataMessages(data){
let jsondata = JSON.parse(data);
chatStore.callsign_list = createCallsignListFromAPI(jsondata)
chatStore.sorted_chat_list = createSortedMessagesList(jsondata)
}
function createCallsignListFromAPI(data: { total_messages: number, messages: Message[] }): {[key: string]: {timestamp: string, body: string}} {
const callsignList: {[key: string]: {timestamp: string, body: string}} = {};
data.messages.forEach(message => {
let callsign = message.direction === 'receive' ? message.origin : message.destination;
if (!callsignList[callsign] || callsignList[callsign].timestamp < message.timestamp) {
callsignList[callsign] = { timestamp: message.timestamp, body: message.body };
}
});
return callsignList;
}
function createSortedMessagesList(data: { total_messages: number, messages: Message[] }): {[key: string]: Message[]} {
const callsignMessages: {[key: string]: Message[]} = {};
data.messages.forEach(message => {
let callsign = message.direction === 'receive' ? message.origin : message.destination;
if (!callsignMessages[callsign]) {
callsignMessages[callsign] = [];
}
callsignMessages[callsign].push(message);
});
return callsignMessages;
}
export function newMessage(dxcall, body){
sendFreedataMessage(dxcall, body)
}

View file

@ -2,6 +2,16 @@ import { defineStore } from "pinia";
import { ref } from "vue";
export const useChatStore = defineStore("chatStore", () => {
var callsign_list = ref();
var sorted_chat_list = ref();
var newChatCallsign = ref();
var newChatMessage = ref();
/* ------------------------------------------------ */
var chat_filter = ref([
{ type: "newchat" },
{ type: "received" },
@ -46,9 +56,6 @@ export const useChatStore = defineStore("chatStore", () => {
var inputFileType = ref("-");
var inputFileSize = ref("-");
var callsign_list = ref();
var sorted_chat_list = ref();
var unsorted_chat_list = ref([]);
var sorted_beacon_list = ref({});
var unsorted_beacon_list = ref({});
@ -68,12 +75,13 @@ export const useChatStore = defineStore("chatStore", () => {
return {
selectedCallsign,
newChatCallsign,
newChatMessage,
selectedMessageObject,
inputText,
chat_filter,
callsign_list,
sorted_chat_list,
unsorted_chat_list,
inputFileName,
inputFileSize,
inputFileType,

View file

@ -1,9 +1,27 @@
import { reactive, ref, watch } from "vue";
import { getConfig, setConfig } from "../js/api";
import { getAppDataPath } from "../js/freedata";
import fs from "fs";
const path = require("path");
const nconf = require("nconf");
var nconf = require("nconf");
nconf.file({ file: "config/config.json" });
var appDataPath = getAppDataPath();
var configFolder = path.join(appDataPath, "FreeDATA");
let configFile = "config.json";
const isGitHubActions = process.env.GITHUB_ACTIONS === "true";
if (isGitHubActions) {
configFile = "example.json";
configFolder = appDataPath;
}
var configPath = path.join(configFolder, configFile);
console.log("AppData Path:", appDataPath);
console.log(configFolder);
console.log(configPath);
nconf.file({ file: configPath });
// +++
//GUI DEFAULT SETTINGS........

View file

@ -3,3 +3,10 @@ import re
def validate_freedata_callsign(callsign):
regexp = "^[a-zA-Z]+\d+\w+-\d{1,2}$"
return re.compile(regexp).match(callsign) is not None
def validate_message_attachment(attachment):
for field in ['name', 'type', 'data']:
if field not in attachment:
raise ValueError(f"Attachment missing '{field}'")
if len(attachment[field]) < 1:
raise ValueError(f"Attachment has empty '{field}'")

View file

@ -0,0 +1,86 @@
# File: arq_data_type_handler.py
import structlog
import lzma
import gzip
from message_p2p import message_received
class ARQDataTypeHandler:
def __init__(self, event_manager):
self.logger = structlog.get_logger(type(self).__name__)
self.event_manager = event_manager
self.handlers = {
"raw": {
'prepare': self.prepare_raw,
'handle': self.handle_raw
},
"raw_lzma": {
'prepare': self.prepare_raw_lzma,
'handle': self.handle_raw_lzma
},
"raw_gzip": {
'prepare': self.prepare_raw_gzip,
'handle': self.handle_raw_gzip
},
"p2pmsg_lzma": {
'prepare': self.prepare_p2pmsg_lzma,
'handle': self.handle_p2pmsg_lzma
},
}
def dispatch(self, type_byte: int, data: bytearray):
endpoint_name = list(self.handlers.keys())[type_byte]
if endpoint_name in self.handlers and 'handle' in self.handlers[endpoint_name]:
return self.handlers[endpoint_name]['handle'](data)
else:
self.log(f"Unknown handling endpoint: {endpoint_name}", isWarning=True)
def prepare(self, data: bytearray, endpoint_name="raw" ):
if endpoint_name in self.handlers and 'prepare' in self.handlers[endpoint_name]:
return self.handlers[endpoint_name]['prepare'](data), list(self.handlers.keys()).index(endpoint_name)
else:
self.log(f"Unknown preparation endpoint: {endpoint_name}", isWarning=True)
def log(self, message, isWarning=False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def prepare_raw(self, data):
self.log(f"Preparing uncompressed data: {len(data)} Bytes")
return data
def handle_raw(self, data):
self.log(f"Handling uncompressed data: {len(data)} Bytes")
return data
def prepare_raw_lzma(self, data):
compressed_data = lzma.compress(data)
self.log(f"Preparing LZMA compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data
def handle_raw_lzma(self, data):
decompressed_data = lzma.decompress(data)
self.log(f"Handling LZMA compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
return decompressed_data
def prepare_raw_gzip(self, data):
compressed_data = gzip.compress(data)
self.log(f"Preparing GZIP compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data
def handle_raw_gzip(self, data):
decompressed_data = gzip.decompress(data)
self.log(f"Handling GZIP compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
return decompressed_data
def prepare_p2pmsg_lzma(self, data):
compressed_data = lzma.compress(data)
self.log(f"Preparing LZMA compressed P2PMSG data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data
def handle_p2pmsg_lzma(self, data):
decompressed_data = lzma.decompress(data)
self.log(f"Handling LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
message_received(self.event_manager, decompressed_data)
return decompressed_data

View file

@ -5,6 +5,8 @@ import structlog
from event_manager import EventManager
from modem_frametypes import FRAME_TYPE
import time
from arq_data_type_handler import ARQDataTypeHandler
class ARQSession():
@ -44,6 +46,7 @@ class ARQSession():
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
self.event_frame_received = threading.Event()
self.arq_data_type_handler = ARQDataTypeHandler(self.event_manager)
self.id = None
self.session_started = time.time()
self.session_ended = 0
@ -88,10 +91,13 @@ class ARQSession():
if self.state in self.STATE_TRANSITION:
if frame_type in self.STATE_TRANSITION[self.state]:
action_name = self.STATE_TRANSITION[self.state][frame_type]
getattr(self, action_name)(frame)
received_data, type_byte = getattr(self, action_name)(frame)
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
self.arq_data_type_handler.dispatch(type_byte, received_data)
return
self.log(f"Ignoring unknow transition from state {self.state.name} with frame {frame['frame_type']}")
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
def is_session_outdated(self):
session_alivetime = time.time() - self.session_max_age

View file

@ -5,6 +5,7 @@ from modem_frametypes import FRAME_TYPE
from codec2 import FREEDV_MODE
from enum import Enum
import time
class IRS_State(Enum):
NEW = 0
OPEN_ACK_SENT = 1
@ -68,6 +69,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.state = IRS_State.NEW
self.state_enum = IRS_State # needed for access State enum from outside
self.type_byte = None
self.total_length = 0
self.total_crc = ''
self.received_data = None
@ -114,6 +116,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort:
self.set_state(IRS_State.OPEN_ACK_SENT)
return None, None
def send_info_ack(self, info_frame):
# Get session info from ISS
@ -121,6 +124,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.total_length = info_frame['total_length']
self.total_crc = info_frame['total_crc']
self.dx_snr.append(info_frame['snr'])
self.type_byte = info_frame['type']
self.log(f"New transfer of {self.total_length} bytes")
self.event_manager.send_arq_session_new(False, self.id, self.dxcall, self.total_length, self.state.name)
@ -134,7 +138,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.launch_transmit_and_wait(info_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort:
self.set_state(IRS_State.INFO_ACK_SENT)
return None, None
def process_incoming_data(self, frame):
if frame['offset'] != self.received_bytes:
@ -174,7 +178,7 @@ class ARQSessionIRS(arq_session.ARQSession):
# self.transmitted_acks += 1
self.set_state(IRS_State.BURST_REPLY_SENT)
self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling)
return
return None, None
if self.final_crc_matches():
self.log("All data received successfully!")
@ -192,6 +196,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics())
return self.received_data, self.type_byte
else:
ack = self.frame_factory.build_arq_burst_ack(self.id,
@ -207,7 +212,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.set_state(IRS_State.FAILED)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
return False, False
def calibrate_speed_settings(self):
self.speed_level = 0 # for now stay at lowest speed level
@ -230,4 +235,5 @@ class ARQSessionIRS(arq_session.ARQSession):
self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(IRS_State.ABORTED)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
return None, None

View file

@ -53,13 +53,13 @@ class ARQSessionISS(arq_session.ARQSession):
}
}
def __init__(self, config: dict, modem, dxcall: str, data: bytearray, state_manager):
def __init__(self, config: dict, modem, dxcall: str, state_manager, data: bytearray, type_byte: bytes):
super().__init__(config, modem, dxcall)
self.state_manager = state_manager
self.data = data
self.total_length = len(data)
self.data_crc = ''
self.type_byte = type_byte
self.confirmed_bytes = 0
self.state = ISS_State.NEW
@ -119,11 +119,13 @@ class ARQSessionISS(arq_session.ARQSession):
info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
helpers.get_crc_32(self.data),
self.snr[0])
self.snr[0], self.type_byte)
self.launch_twr(info_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.INFO_SENT)
return None, None
def send_data(self, irs_frame):
self.set_speed_and_frames_per_burst(irs_frame)
@ -137,15 +139,15 @@ class ARQSessionISS(arq_session.ARQSession):
# check if we received an abort flag
if irs_frame["flag"]["ABORT"]:
self.transmission_aborted(irs_frame)
return
return None, None
if irs_frame["flag"]["FINAL"]:
if self.confirmed_bytes == self.total_length and irs_frame["flag"]["CHECKSUM"]:
self.transmission_ended(irs_frame)
return
else:
self.transmission_failed()
return
return None, None
payload_size = self.get_data_payload_size()
burst = []
@ -158,6 +160,7 @@ class ARQSessionISS(arq_session.ARQSession):
burst.append(data_frame)
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto')
self.set_state(ISS_State.BURST_SENT)
return None, None
def transmission_ended(self, irs_frame):
# final function for sucessfully ended transmissions
@ -166,6 +169,7 @@ class ARQSessionISS(arq_session.ARQSession):
self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics())
self.state_manager.remove_arq_iss_session(self.id)
return None, None
def transmission_failed(self, irs_frame=None):
# final function for failed transmissions
@ -173,6 +177,7 @@ class ARQSessionISS(arq_session.ARQSession):
self.set_state(ISS_State.FAILED)
self.log(f"Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics())
return None, None
def abort_transmission(self, irs_frame=None):
# function for starting the abort sequence
@ -202,4 +207,5 @@ class ARQSessionISS(arq_session.ARQSession):
self.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
self.state_manager.remove_arq_iss_session(self.id)
return None, None

View file

@ -3,6 +3,8 @@ import queue
from codec2 import FREEDV_MODE
import structlog
from state_manager import StateManager
from arq_data_type_handler import ARQDataTypeHandler
class TxCommand():
@ -13,6 +15,7 @@ class TxCommand():
self.event_manager = event_manager
self.set_params_from_api(apiParams)
self.frame_factory = DataFrameFactory(config)
self.arq_data_type_handler = ARQDataTypeHandler(event_manager)
def set_params_from_api(self, apiParams):
pass

View file

@ -13,13 +13,20 @@ class ARQRawCommand(TxCommand):
if not api_validations.validate_freedata_callsign(self.dxcall):
self.dxcall = f"{self.dxcall}-0"
try:
self.type = apiParams['type']
except KeyError:
self.type = "raw"
self.data = base64.b64decode(apiParams['data'])
def run(self, event_queue: Queue, modem):
self.emit_event(event_queue)
self.logger.info(self.log_message())
iss = ARQSessionISS(self.config, modem, self.dxcall, self.data, self.state_manager)
prepared_data, type_byte = self.arq_data_type_handler.prepare(self.data, self.type)
iss = ARQSessionISS(self.config, modem, self.dxcall, self.state_manager, prepared_data, type_byte)
if iss.id:
self.state_manager.register_arq_iss_session(iss)
iss.start()

View file

@ -0,0 +1,38 @@
from command import TxCommand
import api_validations
import base64
from queue import Queue
from arq_session_iss import ARQSessionISS
from message_p2p import MessageP2P
from arq_data_type_handler import ARQDataTypeHandler
from message_system_db_manager import DatabaseManager
class SendMessageCommand(TxCommand):
"""Command to send a P2P message using an ARQ transfer session
"""
def set_params_from_api(self, apiParams):
origin = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}"
self.message = MessageP2P.from_api_params(origin, apiParams)
def transmit(self, modem):
# Convert JSON string to bytes (using UTF-8 encoding)
DatabaseManager(self.event_manager).add_message(self.message.to_dict())
payload = self.message.to_payload().encode('utf-8')
json_bytearray = bytearray(payload)
data, data_type = self.arq_data_type_handler.prepare(json_bytearray, 'p2pmsg_lzma')
iss = ARQSessionISS(self.config,
modem,
self.message.destination,
self.state_manager,
data,
data_type
)
self.state_manager.register_arq_iss_session(iss)
iss.start()

View file

@ -15,7 +15,6 @@ class DataFrameFactory:
'FINAL': 0, # Bit-position for indicating the FINAL state
'ABORT': 1, # Bit-position for indicating the ABORT request
'CHECKSUM': 2, # Bit-position for indicating the CHECKSUM is correct or not
'ENABLE_COMPRESSION': 3 # Bit-position for indicating compression is enabled
}
def __init__(self, config):
@ -118,6 +117,7 @@ class DataFrameFactory:
"total_crc": 4,
"snr": 1,
"flag": 1,
"type": 1,
}
self.template_list[FR_TYPE.ARQ_SESSION_INFO_ACK.value] = {
@ -218,7 +218,7 @@ class DataFrameFactory:
elif key in ["session_id", "speed_level",
"frames_per_burst", "version",
"offset", "total_length", "state"]:
"offset", "total_length", "state", "type"]:
extracted_data[key] = int.from_bytes(data, 'big')
elif key in ["snr"]:
@ -350,10 +350,8 @@ class DataFrameFactory:
}
return self.construct(FR_TYPE.ARQ_SESSION_OPEN_ACK, payload)
def build_arq_session_info(self, session_id: int, total_length: int, total_crc: bytes, snr, flag_compression=False):
def build_arq_session_info(self, session_id: int, total_length: int, total_crc: bytes, snr, type):
flag = 0b00000000
if flag_compression:
flag = helpers.set_flag(flag, 'ENABLE_COMPRESSION', True, self.ARQ_FLAGS)
payload = {
"session_id": session_id.to_bytes(1, 'big'),
@ -361,6 +359,7 @@ class DataFrameFactory:
"total_crc": total_crc,
"snr": helpers.snr_to_bytes(1),
"flag": flag.to_bytes(1, 'big'),
"type": type.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.ARQ_SESSION_INFO, payload)
@ -377,7 +376,6 @@ class DataFrameFactory:
}
return self.construct(FR_TYPE.ARQ_STOP_ACK, payload)
def build_arq_session_info_ack(self, session_id, total_crc, snr, speed_level, frames_per_burst, flag_final=False, flag_abort=False):
flag = 0b00000000
if flag_final:

View file

@ -89,4 +89,7 @@ class EventManager:
def modem_failed(self):
event = {"modem": "failed"}
self.broadcast(event)
self.broadcast(event)
def freedata_message_db_change(self):
self.broadcast({"message-db": "changed"})

View file

@ -31,7 +31,6 @@ class FrameHandler():
def is_frame_for_me(self):
call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid'])
ft = self.details['frame']['frame_type']
print(self.details)
valid = False
# Check for callsign checksum
if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK']:

82
modem/message_p2p.py Normal file
View file

@ -0,0 +1,82 @@
import datetime
import api_validations
import base64
import json
from message_system_db_manager import DatabaseManager
def message_received(event_manager, data):
decompressed_json_string = data.decode('utf-8')
received_message_obj = MessageP2P.from_payload(decompressed_json_string)
received_message_dict = MessageP2P.to_dict(received_message_obj, received=True)
DatabaseManager(event_manager).add_message(received_message_dict)
class MessageP2P:
def __init__(self, origin: str, destination: str, body: str, attachments: list) -> None:
self.timestamp = datetime.datetime.now().isoformat()
self.origin = origin
self.destination = destination
self.body = body
self.attachments = attachments
@classmethod
def from_api_params(cls, origin: str, params: dict):
dxcall = params['dxcall']
if not api_validations.validate_freedata_callsign(dxcall):
dxcall = f"{dxcall}-0"
if not api_validations.validate_freedata_callsign(dxcall):
raise ValueError(f"Invalid dxcall given ({params['dxcall']})")
body = params['body']
if len(body) < 1:
raise ValueError(f"Body cannot be empty")
attachments = []
if 'attachments' in params:
for a in params['attachments']:
api_validations.validate_message_attachment(a)
attachments.append(cls.__decode_attachment__(a))
return cls(origin, dxcall, body, attachments)
@classmethod
def from_payload(cls, payload):
payload_message = json.loads(payload)
attachments = list(map(cls.__decode_attachment__, payload_message['attachments']))
return cls(payload_message['origin'], payload_message['destination'],
payload_message['body'], attachments)
def get_id(self) -> str:
return f"{self.origin}_{self.destination}_{self.timestamp}"
def __encode_attachment__(self, binary_attachment: dict):
encoded_attachment = binary_attachment.copy()
encoded_attachment['data'] = str(base64.b64encode(binary_attachment['data']), 'utf-8')
return encoded_attachment
def __decode_attachment__(encoded_attachment: dict):
decoded_attachment = encoded_attachment.copy()
decoded_attachment['data'] = base64.b64decode(encoded_attachment['data'])
return decoded_attachment
def to_dict(self, received=False):
"""Make a dictionary out of the message data
"""
return {
'id': self.get_id(),
'origin': self.origin,
'destination': self.destination,
'body': self.body,
'direction': 'receive' if received else 'transmit',
'attachments': list(map(self.__encode_attachment__, self.attachments)),
}
def to_payload(self):
"""Make a byte array ready to be sent out of the message data"""
json_string = json.dumps(self.to_dict())
return json_string

View file

@ -0,0 +1,141 @@
# database_manager.py
import sqlite3
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from threading import local
from message_system_db_model import Base, Station, Status, Attachment, P2PMessage
from datetime import datetime
import json
import structlog
class DatabaseManager:
def __init__(self, event_manger, uri='sqlite:///freedata-messages.db'):
self.event_manager = event_manger
self.engine = create_engine(uri, echo=False)
self.thread_local = local()
self.session_factory = sessionmaker(bind=self.engine)
Base.metadata.create_all(self.engine)
self.logger = structlog.get_logger(type(self).__name__)
def initialize_default_values(self):
session = self.get_thread_scoped_session()
try:
statuses = [
"transmitting",
"transmitted",
"received",
"failed",
"failed_checksum",
"aborted"
]
# Add default statuses if they don't exist
for status_name in statuses:
existing_status = session.query(Status).filter_by(name=status_name).first()
if not existing_status:
new_status = Status(name=status_name)
session.add(new_status)
session.commit()
self.log("Initialized database")
except Exception as e:
session.rollback()
self.log(f"An error occurred while initializing default values: {e}", isWarning=True)
finally:
session.remove()
def log(self, message, isWarning=False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def get_thread_scoped_session(self):
if not hasattr(self.thread_local, "session"):
self.thread_local.session = scoped_session(self.session_factory)
return self.thread_local.session
def get_or_create_station(self, session, callsign):
station = session.query(Station).filter_by(callsign=callsign).first()
if not station:
station = Station(callsign=callsign)
session.add(station)
session.flush() # To get the callsign immediately
return station
def get_or_create_status(self, session, status_name):
status = session.query(Status).filter_by(name=status_name).first()
if not status:
status = Status(name=status_name)
session.add(status)
session.flush() # To get the ID immediately
return status
def add_message(self, message_data):
session = self.get_thread_scoped_session()
try:
# Create and add the origin and destination Stations
origin = self.get_or_create_station(session, message_data['origin'])
destination = self.get_or_create_station(session, message_data['destination'])
# Create and add Status if provided
status = None
if 'status' in message_data:
status = self.get_or_create_status(session, message_data['status'])
# Parse the timestamp from the message ID
timestamp = datetime.fromisoformat(message_data['id'].split('_')[2])
# Create the P2PMessage instance
new_message = P2PMessage(
id=message_data['id'],
origin_callsign=origin.callsign,
destination_callsign=destination.callsign,
body=message_data['body'],
timestamp=timestamp,
direction=message_data['direction'],
status_id=status.id if status else None
)
# Process and add attachments
for attachment_data in message_data.get('attachments', []):
attachment = Attachment(
name=attachment_data['name'],
data_type=attachment_data['type'],
data=attachment_data['data']
)
new_message.attachments.append(attachment)
session.add(new_message)
session.commit()
self.log(f"Added data to database: {new_message.id}")
self.event_manager.freedata_message_db_change()
return new_message.id
except Exception as e:
session.rollback()
self.log(f"error adding new message to databse with error: {e}", isWarning=True)
self.log(f"---> please delete or update existing database", isWarning=True)
finally:
session.remove()
def get_all_messages(self):
session = self.get_thread_scoped_session()
try:
messages = session.query(P2PMessage).all()
return [message.to_dict() for message in messages]
except Exception as e:
self.log(f"error fetching database messages with error: {e}", isWarning=True)
self.log(f"---> please delete or update existing database", isWarning=True)
return []
finally:
session.remove()
def get_all_messages_json(self):
messages_dict = self.get_all_messages()
messages_with_header = {'total_messages' : len(messages_dict), 'messages' : messages_dict}
return json.dumps(messages_with_header) # Convert to JSON string

View file

@ -0,0 +1,61 @@
# models.py
from sqlalchemy import Column, String, Integer, JSON, ForeignKey, DateTime
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class Station(Base):
__tablename__ = 'station'
callsign = Column(String, primary_key=True)
location = Column(JSON, nullable=True)
info = Column(JSON, nullable=True)
class Status(Base):
__tablename__ = 'status'
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
class P2PMessage(Base):
__tablename__ = 'p2p_message'
id = Column(String, primary_key=True)
origin_callsign = Column(String, ForeignKey('station.callsign'))
via_callsign = Column(String, ForeignKey('station.callsign'), nullable=True)
destination_callsign = Column(String, ForeignKey('station.callsign'))
body = Column(String, nullable=True)
attachments = relationship('Attachment', backref='p2p_message')
timestamp = Column(DateTime)
status_id = Column(Integer, ForeignKey('status.id'), nullable=True)
status = relationship('Status', backref='p2p_messages')
direction = Column(String)
statistics = Column(JSON, nullable=True)
def to_dict(self):
return {
'id': self.id,
'timestamp': self.timestamp.isoformat() if self.timestamp else None,
'origin': self.origin_callsign,
'via': self.via_callsign,
'destination': self.destination_callsign,
'direction': self.direction,
'body': self.body,
'attachments': [attachment.to_dict() for attachment in self.attachments],
'status': self.status.name if self.status else None,
'statistics': self.statistics
}
class Attachment(Base):
__tablename__ = 'attachment'
id = Column(Integer, primary_key=True)
name = Column(String)
data_type = Column(String)
data = Column(String)
message_id = Column(String, ForeignKey('p2p_message.id'))
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'data_type': self.data_type,
'data': self.data # Be cautious with large binary data
}

View file

@ -190,7 +190,6 @@ class radio:
try:
mode, bandwidth = response.split('\n', 1) # Split the response into mode and bandwidth
except ValueError:
print(response)
mode = 'err'
bandwidth = 'err'

View file

@ -16,13 +16,16 @@ import command_ping
import command_feq
import command_test
import command_arq_raw
import command_message_send
import event_manager
from message_system_db_manager import DatabaseManager
app = Flask(__name__)
CORS(app)
CORS(app, resources={r"/*": {"origins": "*"}})
sock = Sock(app)
MODEM_VERSION = "0.12.0-alpha"
MODEM_VERSION = "0.13.0-alpha"
# set config file to use
def set_config():
@ -234,6 +237,16 @@ def get_post_radio():
elif request.method == 'GET':
return api_response(app.state_manager.get_radio_status())
@app.route('/freedata/messages', methods=['POST', 'GET'])
def get_post_freedata_message():
if request.method in ['GET']:
result = DatabaseManager(app.event_manager).get_all_messages_json()
return api_response(result)
if enqueue_tx_command(command_message_send.SendMessageCommand, request.json):
return api_response(request.json)
else:
api_abort('Error executing command...', 500)
# @app.route('/modem/arq_connect', methods=['POST'])
# @app.route('/modem/arq_disconnect', methods=['POST'])
# @app.route('/modem/send_raw', methods=['POST'])
@ -283,6 +296,8 @@ if __name__ == "__main__":
app.service_manager = service_manager.SM(app)
# start modem service
app.modem_service.put("start")
# initialize databse default values
DatabaseManager(app.event_manager).initialize_default_values()
wsm.startThreads(app)
app.run()

View file

@ -27,4 +27,5 @@ pytest-cov
pytest-cover
pytest-coverage
pytest-rerunfailures
pick
pick
sqlalchemy

View file

@ -126,12 +126,13 @@ class TestARQSession(unittest.TestCase):
def testARQSessionSmallPayload(self):
# set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 50
self.loss_probability = 0
self.establishChannels()
params = {
'dxcall': "XX1XXX-1",
'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")),
'type': "raw_lzma"
}
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
cmd.run(self.iss_event_queue, self.iss_modem)
@ -146,6 +147,7 @@ class TestARQSession(unittest.TestCase):
params = {
'dxcall': "XX1XXX-1",
'data': base64.b64encode(np.random.bytes(1000)),
'type': "raw_lzma"
}
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
cmd.run(self.iss_event_queue, self.iss_modem)

View file

@ -0,0 +1,42 @@
import sys
sys.path.append('modem')
import unittest
import queue
from arq_data_type_handler import ARQDataTypeHandler
from event_manager import EventManager
class TestDispatcher(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.event_queue = queue.Queue()
cls.event_manager = EventManager([cls.event_queue])
cls.arq_data_type_handler = ARQDataTypeHandler(cls.event_manager)
def testDataTypeHevent_managerandlerRaw(self):
# Example usage
example_data = b"Hello FreeDATA!"
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, "raw")
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
self.assertEqual(example_data, dispatched_data)
def testDataTypeHandlerLZMA(self):
# Example usage
example_data = b"Hello FreeDATA!"
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, "raw_lzma")
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
self.assertEqual(example_data, dispatched_data)
def testDataTypeHandlerGZIP(self):
# Example usage
example_data = b"Hello FreeDATA!"
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, "raw_gzip")
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
self.assertEqual(example_data, dispatched_data)
if __name__ == '__main__':
unittest.main()

69
tests/test_message_p2p.py Executable file
View file

@ -0,0 +1,69 @@
import sys
sys.path.append('modem')
import numpy as np
import unittest
from config import CONFIG
from message_p2p import MessageP2P
from message_system_db_manager import DatabaseManager
from event_manager import EventManager
import queue
class TestDataFrameFactory(unittest.TestCase):
@classmethod
def setUpClass(cls):
config_manager = CONFIG('modem/config.ini.example')
cls.config = config_manager.read()
cls.event_queue = queue.Queue()
cls.event_manager = EventManager([cls.event_queue])
cls.mycall = f"{cls.config['STATION']['mycall']}-{cls.config['STATION']['myssid']}"
cls.database_manager = DatabaseManager(cls.event_manager, uri='sqlite:///:memory:')
def testFromApiParams(self):
api_params = {
'dxcall': 'DJ2LS-3',
'body': 'Hello World!',
}
message = MessageP2P.from_api_params(self.mycall, api_params)
self.assertEqual(message.destination, api_params['dxcall'])
self.assertEqual(message.body, api_params['body'])
def testToPayloadWithAttachment(self):
attachment = {
'name': 'test.gif',
'type': 'image/gif',
'data': np.random.bytes(1024)
}
message = MessageP2P(self.mycall, 'DJ2LS-3', 'Hello World!', [attachment])
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
self.assertEqual(message.origin, received_message.origin)
self.assertEqual(message.destination, received_message.destination)
self.assertCountEqual(message.attachments, received_message.attachments)
self.assertEqual(attachment['data'], received_message.attachments[0]['data'])
def testToPayloadWithAttachmentAndDatabase(self):
attachment = {
'name': 'test.gif',
'type': 'image/gif',
'data': np.random.bytes(1024)
}
message = MessageP2P(self.mycall, 'DJ2LS-3', 'Hello World!', [attachment])
payload = message.to_payload()
received_message = MessageP2P.from_payload(payload)
received_message_dict = MessageP2P.to_dict(received_message, received=True)
self.database_manager.add_message(received_message_dict)
self.assertEqual(message.origin, received_message.origin)
self.assertEqual(message.destination, received_message.destination)
self.assertCountEqual(message.attachments, received_message.attachments)
self.assertEqual(attachment['data'], received_message.attachments[0]['data'])
result = self.database_manager.get_all_messages()
self.assertEqual(result[0]["destination"], message.destination)
if __name__ == '__main__':
unittest.main()