Compare commits

..

No commits in common. "main" and "UIRefinementsSL" have entirely different histories.

24 changed files with 185 additions and 753 deletions

22
LICENSE
View File

@ -1,22 +0,0 @@
MIT License
Copyright (c) 2023 Severin Memmishofer
Copyright (c) 2023 Sebastian Lenzlinger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,6 +1,2 @@
# RippleChat
RippleChat is a meant to be a TinySSB compatible chat client. It is started as a university project as a proof of concept for p2p messaging in a Secure Scuttlebut fashion on iOS.
At this stage it communicates via BLE.
It is not currently compatible with TinySSB conforming apps like [Tremola](https://github.com/cn-uofbasel/tremola).
Future work includes switching to the tinySSB protocol and heavily beautifying th UI.
2023 © Severin Memmishofer, Sebastian Lenzlinger

View File

@ -15,22 +15,20 @@
96454F362A558EBE0040BEBD /* RippleChatUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96454F352A558EBE0040BEBD /* RippleChatUITests.swift */; };
96454F382A558EBE0040BEBD /* RippleChatUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96454F372A558EBE0040BEBD /* RippleChatUITestsLaunchTests.swift */; };
96454F452A5593900040BEBD /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 96454F442A5593900040BEBD /* .gitignore */; };
96BD330E2A5C254B007A6E53 /* TextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BD330D2A5C254B007A6E53 /* TextApp.swift */; };
96BD33102A5C27B0007A6E53 /* NewFeedEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BD330F2A5C27B0007A6E53 /* NewFeedEntryView.swift */; };
96BD33132A5C400B007A6E53 /* FeedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BD33122A5C400B007A6E53 /* FeedListView.swift */; };
96BD33162A5C403C007A6E53 /* DiscoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BD33152A5C403C007A6E53 /* DiscoveryView.swift */; };
F581F59B2A5AE72F0081C383 /* BTCentral.swift in Sources */ = {isa = PBXBuildFile; fileRef = F581F59A2A5AE72F0081C383 /* BTCentral.swift */; };
F5847B622A599BF4009E28D4 /* Bodyy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5847B612A599BF4009E28D4 /* Bodyy.swift */; };
96BD33162A5C403C007A6E53 /* PeeringView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BD33152A5C403C007A6E53 /* PeeringView.swift */; };
F581F59B2A5AE72F0081C383 /* BluetoothController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F581F59A2A5AE72F0081C383 /* BluetoothController.swift */; };
F5847B622A599BF4009E28D4 /* Body.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5847B612A599BF4009E28D4 /* Body.swift */; };
F5847B642A599CC3009E28D4 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5847B632A599CC3009E28D4 /* LogEntry.swift */; };
F5847B662A599EA4009E28D4 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5847B652A599EA4009E28D4 /* Feed.swift */; };
F5847B6A2A59AB24009E28D4 /* FeedStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5847B692A59AB24009E28D4 /* FeedStore.swift */; };
F58EB2D02A5590E800E22DA6 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = F58EB2CF2A5590E800E22DA6 /* README.md */; };
F59375722A5FF344001FA46A /* FeedDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59375712A5FF344001FA46A /* FeedDetailView.swift */; };
F5A4B1212A5D4D1F00F5AE01 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A4B1202A5D4D1F00F5AE01 /* SettingsView.swift */; };
F5A4B1232A5D5F8B00F5AE01 /* BTPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A4B1222A5D5F8B00F5AE01 /* BTPeripheral.swift */; };
F5A4B1252A5D7A8D00F5AE01 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A4B1242A5D7A8D00F5AE01 /* DataStore.swift */; };
F5A4B1272A5D861E00F5AE01 /* SettingsEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A4B1262A5D861E00F5AE01 /* SettingsEditView.swift */; };
F5F1419C2A5EFA3600C81B1A /* LogEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F1419B2A5EFA3600C81B1A /* LogEntryView.swift */; };
F5F1419E2A5EFA4700C81B1A /* FeedCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F1419D2A5EFA4700C81B1A /* FeedCardView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -62,22 +60,20 @@
96454F352A558EBE0040BEBD /* RippleChatUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RippleChatUITests.swift; sourceTree = "<group>"; };
96454F372A558EBE0040BEBD /* RippleChatUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RippleChatUITestsLaunchTests.swift; sourceTree = "<group>"; };
96454F442A5593900040BEBD /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
96BD330D2A5C254B007A6E53 /* TextApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextApp.swift; sourceTree = "<group>"; };
96BD330F2A5C27B0007A6E53 /* NewFeedEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewFeedEntryView.swift; sourceTree = "<group>"; };
96BD33122A5C400B007A6E53 /* FeedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListView.swift; sourceTree = "<group>"; };
96BD33152A5C403C007A6E53 /* DiscoveryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryView.swift; sourceTree = "<group>"; };
F581F59A2A5AE72F0081C383 /* BTCentral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCentral.swift; sourceTree = "<group>"; };
F5847B612A599BF4009E28D4 /* Bodyy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bodyy.swift; sourceTree = "<group>"; };
96BD33152A5C403C007A6E53 /* PeeringView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeeringView.swift; sourceTree = "<group>"; };
F581F59A2A5AE72F0081C383 /* BluetoothController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothController.swift; sourceTree = "<group>"; };
F5847B612A599BF4009E28D4 /* Body.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Body.swift; sourceTree = "<group>"; };
F5847B632A599CC3009E28D4 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
F5847B652A599EA4009E28D4 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
F5847B692A59AB24009E28D4 /* FeedStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedStore.swift; sourceTree = "<group>"; };
F58EB2CF2A5590E800E22DA6 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
F59375712A5FF344001FA46A /* FeedDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDetailView.swift; sourceTree = "<group>"; };
F5A4B1202A5D4D1F00F5AE01 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
F5A4B1222A5D5F8B00F5AE01 /* BTPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPeripheral.swift; sourceTree = "<group>"; };
F5A4B1242A5D7A8D00F5AE01 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
F5A4B1262A5D861E00F5AE01 /* SettingsEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsEditView.swift; sourceTree = "<group>"; };
F5F1419B2A5EFA3600C81B1A /* LogEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntryView.swift; sourceTree = "<group>"; };
F5F1419D2A5EFA4700C81B1A /* FeedCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCardView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -133,14 +129,15 @@
96454F1A2A558EBC0040BEBD /* RippleChatApp.swift */,
96454F1C2A558EBC0040BEBD /* ContentView.swift */,
96BD33112A5C3FFC007A6E53 /* Views */,
F581F59A2A5AE72F0081C383 /* BTCentral.swift */,
F581F59A2A5AE72F0081C383 /* BluetoothController.swift */,
F5A4B1222A5D5F8B00F5AE01 /* BTPeripheral.swift */,
96454F1E2A558EBD0040BEBD /* Assets.xcassets */,
96454F202A558EBD0040BEBD /* Preview Content */,
F5847B612A599BF4009E28D4 /* Bodyy.swift */,
F5847B612A599BF4009E28D4 /* Body.swift */,
F5847B632A599CC3009E28D4 /* LogEntry.swift */,
F5847B652A599EA4009E28D4 /* Feed.swift */,
F5847B692A59AB24009E28D4 /* FeedStore.swift */,
96BD330D2A5C254B007A6E53 /* TextApp.swift */,
F5A4B1242A5D7A8D00F5AE01 /* DataStore.swift */,
);
path = RippleChat;
@ -176,12 +173,9 @@
children = (
96BD330F2A5C27B0007A6E53 /* NewFeedEntryView.swift */,
96BD33122A5C400B007A6E53 /* FeedListView.swift */,
96BD33152A5C403C007A6E53 /* DiscoveryView.swift */,
96BD33152A5C403C007A6E53 /* PeeringView.swift */,
F5A4B1202A5D4D1F00F5AE01 /* SettingsView.swift */,
F5A4B1262A5D861E00F5AE01 /* SettingsEditView.swift */,
F5F1419B2A5EFA3600C81B1A /* LogEntryView.swift */,
F5F1419D2A5EFA4700C81B1A /* FeedCardView.swift */,
F59375712A5FF344001FA46A /* FeedDetailView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -318,22 +312,20 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F5847B622A599BF4009E28D4 /* Bodyy.swift in Sources */,
96BD33162A5C403C007A6E53 /* DiscoveryView.swift in Sources */,
F5847B622A599BF4009E28D4 /* Body.swift in Sources */,
96BD33162A5C403C007A6E53 /* PeeringView.swift in Sources */,
F5847B662A599EA4009E28D4 /* Feed.swift in Sources */,
96BD33132A5C400B007A6E53 /* FeedListView.swift in Sources */,
F5847B642A599CC3009E28D4 /* LogEntry.swift in Sources */,
96BD33102A5C27B0007A6E53 /* NewFeedEntryView.swift in Sources */,
F5A4B1232A5D5F8B00F5AE01 /* BTPeripheral.swift in Sources */,
F5A4B1252A5D7A8D00F5AE01 /* DataStore.swift in Sources */,
F5F1419C2A5EFA3600C81B1A /* LogEntryView.swift in Sources */,
F5847B6A2A59AB24009E28D4 /* FeedStore.swift in Sources */,
F5A4B1272A5D861E00F5AE01 /* SettingsEditView.swift in Sources */,
F59375722A5FF344001FA46A /* FeedDetailView.swift in Sources */,
F5F1419E2A5EFA4700C81B1A /* FeedCardView.swift in Sources */,
F581F59B2A5AE72F0081C383 /* BTCentral.swift in Sources */,
F581F59B2A5AE72F0081C383 /* BluetoothController.swift in Sources */,
96454F1D2A558EBC0040BEBD /* ContentView.swift in Sources */,
F5A4B1212A5D4D1F00F5AE01 /* SettingsView.swift in Sources */,
96BD330E2A5C254B007A6E53 /* TextApp.swift in Sources */,
96454F1B2A558EBC0040BEBD /* RippleChatApp.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -524,7 +516,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"RippleChat/Preview Content\"";
DEVELOPMENT_TEAM = GN2B48NJ47;
DEVELOPMENT_TEAM = B5S58UWR64;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Allow for bluetooth use";
@ -539,7 +531,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = unibas.inetsec.RippleChat1;
PRODUCT_BUNDLE_IDENTIFIER = unibas.inetsec.RippleChat;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;

View File

@ -1,120 +0,0 @@
//
// BluetoothController.swift
// RippleChat
//
// Created by Severin Memmishofer on 09.07.23.
//
import SwiftUI
import CoreBluetooth
class BTCentral: NSObject, ObservableObject {
private var centralManager: CBCentralManager?
private var peripherals: [CBPeripheral] = []
@Published var peripheralNames: [String] = []
var writeCharacteristics: [CBCharacteristic] = []
let BLE_SERVICE_UUID = CBUUID(string: "6e400001-7646-4b5b-9a50-71becce51558")
let BLE_CHARACTERISTIC_UUID_RX = CBUUID(string: "6e400002-7646-4b5b-9a50-71becce51558")
override init() {
super.init()
self.centralManager = CBCentralManager(delegate: self, queue: .main)
}
// func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
// }
}
extension BTCentral: CBCentralManagerDelegate, CBPeripheralDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("Device is powered on...")
self.centralManager?.scanForPeripherals(withServices: [BLE_SERVICE_UUID])
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if !peripherals.contains(peripheral) {
peripheral.delegate = self
centralManager!.connect(peripheral)
self.peripherals.append(peripheral)
self.peripheralNames.append(peripheral.name ?? "unnamed device")
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
peripheral.discoverServices([BLE_SERVICE_UUID])
print("Connected to device \(String(describing: peripheral.name))")
if(centralManager?.delegate == nil) {
print("central is nil")
} else {
print("central is not nil")
}
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
// Not implemented yet
}
// func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
// print("Discovering services...")
// peripheral.discoverCharacteristics(BLE_CHARACTERISTIC_UUID_RX)
// }
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
print("*******************************************************")
if ((error) != nil) {
print("Error discovering services: \(error!.localizedDescription)")
return
}
guard let services = peripheral.services else {
return
}
//We need to discover the all characteristic
for service in services {
peripheral.discoverCharacteristics(nil, for: service)
}
print("Discovered Services: \(services)")
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard let characteristics = service.characteristics else {
print("No characteristics found for service \(service.uuid)")
return
}
for characteristic in characteristics {
if characteristic.uuid.isEqual(BLE_CHARACTERISTIC_UUID_RX) {
self.writeCharacteristics.append(characteristic)
peripheral.setNotifyValue(true, for: characteristic)
peripheral.readValue(for: characteristic)
print("Characteristic: \(characteristic.uuid)")
}
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
print("updating characteristic value...")
print(characteristic.value ?? "Characteristic is nil")
}
func writeToCharacteristics(message: String) {
guard let messageData = message.data(using: .utf8) else {
print("Could not convert message to data.")
return
}
print("Writing to Characteristic...")
// Go through
for characteristic in writeCharacteristics {
// Go through connected peripherals and write to their characteristic
for peripheral in peripherals {
peripheral.writeValue(messageData, for: characteristic, type: .withoutResponse)
}
}
}
}

View File

@ -10,8 +10,6 @@ import CoreBluetooth
class BluetoothPeripheral: NSObject, ObservableObject {
@Published var incomingMsg: String = ""
@Published var wantVector: WantMessage = WantMessage()
private var peripheralManager: CBPeripheralManager?
let BLE_SERVICE_UUID = CBUUID(string: "6e400001-7646-4b5b-9a50-71becce51558")
@ -23,101 +21,64 @@ class BluetoothPeripheral: NSObject, ObservableObject {
}
}
// TODO: Change variable names, etc...
extension BluetoothPeripheral: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .unknown:
print("BT Device is UNKNOWN")
print("Bluetooth Device is UNKNOWN")
case .unsupported:
print("BT Device is UNSUPPORTED")
print("Bluetooth Device is UNSUPPORTED")
case .unauthorized:
print("BT Device is UNAUTHORIZED")
print("Bluetooth Device is UNAUTHORIZED")
case .resetting:
print("BT Device is RESETTING")
print("Bluetooth Device is RESETTING")
case .poweredOff:
print("BT Device is POWERED OFF")
print("Bluetooth Device is POWERED OFF")
case .poweredOn:
print("BT Device is POWERED ON")
addBTService()
print("Bluetooth Device is POWERED ON")
addServices()
@unknown default:
fatalError()
}
}
func addBTService() {
let myCharacteristic = CBMutableCharacteristic(type: BLE_CHARACTERISTIC_UUID_RX, properties: [.read, .write, .notify, .writeWithoutResponse], value: nil, permissions: [.readable, .writeable])
func addServices() {
let myCharacteristic = CBMutableCharacteristic(type: BLE_CHARACTERISTIC_UUID_RX, properties: [.read, .write, .notify], value: nil, permissions: [.readable])
// 2. Create instance of CBMutableService
let myService = CBMutableService(type: BLE_SERVICE_UUID, primary: true)
// 3. Add characteristics to the service
myService.characteristics = [myCharacteristic]
// 4. Add service to peripheralManager
peripheralManager!.add(myService)
}
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
peripheralManager!.startAdvertising([CBAdvertisementDataLocalNameKey : "RippleChat", CBAdvertisementDataServiceUUIDsKey: [BLE_SERVICE_UUID]])
// 5. Start advertising
peripheralManager!.startAdvertising([CBAdvertisementDataLocalNameKey : "RippleChat", CBAdvertisementDataServiceUUIDsKey : BLE_SERVICE_UUID])
print("Started Advertising")
if(peripheralManager?.delegate == nil) {
print("peripheral is nil")
} else {
print("peripheral is not nil")
}
}
func discoverServices(_ serviceUUIDs: [CBUUID]?) {
print("test Peripheral")
print("Discovering services... \(String(describing: serviceUUIDs))")
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
print("Discovering Services Peripheral")
// print("*******************************************************")
// func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
//
// if ((error) != nil) {
// print("Error discovering services: \(error!.localizedDescription)")
// return
// }
// guard let services = peripheral.services else {
// return
// }
// //We need to discover the all characteristic
// for service in services {
// peripheral.discoverCharacteristics(nil, for: service)
// }
// print("Discovered Services: \(services)")
}
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
for request in requests {
if let value = request.value {
// Handle the received data
let receivedData = Data(value)
print(receivedData)
// Decode the received JSON string into your data structure
let decoder = JSONDecoder()
do {
let receivedObject = try decoder.decode(WantMessage.self, from: receivedData)
// Use the received object to update your app state as needed
print("Received Write")
self.incomingMsg = ""
self.incomingMsg = receivedObject.printMsg()
self.wantVector = receivedObject
print(receivedObject.printMsg())
} catch {
print("Failed to decode JSON: \(error)")
}
// If you want to write back to the central
// if let central = request.central {
// let dataToWrite = // some data you want to send back
// let writeType: CBCharacteristicWriteType = // choose .withResponse or .withoutResponse
// central.writeValue(dataToWrite, for: request.characteristic, type: writeType)
// }
}
// Respond to the write request
peripheral.respond(to: request, withResult: .success)
}
}
// messageLabel.text = "Data getting Read"
// readValueLabel.text = value
//
// // Perform your additional operations here
//
// }
//
// func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
//
// messageLabel.text = "Writing Data"
//
// if let value = requests.first?.value {
// writeValueLabel.text = value.hexEncodedString()
// //Perform here your additional operations on the data you get
// }
// }
}

View File

@ -0,0 +1,38 @@
//
// BluetoothController.swift
// RippleChat
//
// Created by Severin Memmishofer on 09.07.23.
//
import SwiftUI
import CoreBluetooth
class BluetoothController: NSObject, ObservableObject {
private var centralManager: CBCentralManager?
private var peripherals: [CBPeripheral] = []
@Published var peripheralNames: [String] = []
let BLE_SERVICE_UUID = CBUUID(string: "6e400001-7646-4b5b-9a50-71becce51558")
let BLE_CHARACTERISTIC_UUID_RX = CBUUID(string: "6e400002-7646-4b5b-9a50-71becce51558")
override init() {
super.init()
self.centralManager = CBCentralManager(delegate: self, queue: .main)
}
}
extension BluetoothController: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
self.centralManager?.scanForPeripherals(withServices: [BLE_SERVICE_UUID])
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if !peripherals.contains(peripheral) {
self.peripherals.append(peripheral)
self.peripheralNames.append(peripheral.name ?? "unnamed device")
}
}
}

View File

@ -5,10 +5,9 @@
// Created by Severin Memmishofer on 08.07.23.
//
import SwiftUI
import Foundation
public struct Bodyy: Codable {
struct Body: Codable {
let tag: String
let value: String

View File

@ -10,35 +10,23 @@ import CoreBluetooth
struct ContentView: View {
@State var currentView = 0
@EnvironmentObject var dataStore: DataStore
@StateObject private var bluetoothController = BTCentral()
@StateObject private var bluetoothPeripheral = BluetoothPeripheral()
@Environment(\.scenePhase) private var scenePhase
let saveAction: ()->Void
@StateObject var dataStore = DataStore()
var body: some View {
VStack {
switch self.currentView {
case 0:
DiscoveryView()
PeeringView()
.environmentObject(dataStore)
.environmentObject(bluetoothController)
.environmentObject(bluetoothPeripheral)
.navigationTitle("Peering")
case 1:
FeedListView()
FeedListView(feeds: [])
.environmentObject(dataStore)
.environmentObject(bluetoothController)
.environmentObject(bluetoothPeripheral)
.navigationTitle("Feeds")
case 2:
SettingsView()
.environmentObject(dataStore)
.environmentObject(bluetoothController)
.environmentObject(bluetoothPeripheral)
.navigationTitle("Settings")
default:
FeedListView()
FeedListView(feeds: [])
.environmentObject(dataStore)
}
HStack {
@ -73,16 +61,14 @@ struct ContentView: View {
}
.frame(height: UIScreen.main.bounds.height * 0.05)
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(saveAction: {})
ContentView()
.environmentObject(DataStore())
}
}

View File

@ -8,128 +8,16 @@
import SwiftUI
import Foundation
@MainActor
class DataStore: ObservableObject {
typealias FID = String
typealias SEQ = Int
@Published var personalID: String
@Published var personalFeed: Feed
@Published var friends: [FID:SEQ]
@Published var feedStores: [FeedStore]
@Published var friends: [String]
@Published var feeds: [Feed]
init(personalID: String = "", personalFeed: Feed = Feed(), friends: [String:Int] = [:], feedStores: [FeedStore] = []) {
init(personalID: String = "", friends: [String] = [], feeds: [Feed] = []) {
self.personalID = personalID
self.friends = friends
self.feedStores = feedStores
self.personalFeed = personalFeed
self.feeds = feeds
}
private func fileURL(for filename: String) throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("\(filename).json")
}
func savePersonalID() async throws {
let task = Task {
let data = try JSONEncoder().encode(personalID)
let outfile = try fileURL(for: "personalID")
try data.write(to: outfile)
}
_ = try await task.value
}
func saveFriends() async throws {
let task = Task {
let data = try JSONEncoder().encode(friends)
let outfile = try fileURL(for: "friends")
try data.write(to: outfile)
}
_ = try await task.value
}
func loadPersonalID() async throws {
let task = Task<String, Error> {
let fileURL = try self.fileURL(for: "personalID")
guard let data = try? Data(contentsOf: fileURL) else {
return ""
}
let personalID = try JSONDecoder().decode(String.self, from: data)
return personalID
}
let personalID = try await task.value
self.personalID = personalID
}
func loadFriends() async throws {
let task = Task<[String:Int], Error> {
let fileURL = try self.fileURL(for: "friends")
guard let data = try? Data(contentsOf: fileURL) else {
return [:]
}
let friends = try JSONDecoder().decode([String:Int].self, from: data)
return friends
}
let friends = try await task.value
self.friends = friends
}
func loadFeedStores() async throws {
let task = Task<[FeedStore], Error> {
var feedStores: [FeedStore] = []
for feedStore in self.feedStores {
try await feedStore.load()
feedStores.append(feedStore)
}
return feedStores
}
let feedStores = try await task.value
self.feedStores = feedStores
}
func saveFeedStores() async throws {
let task = Task {
for feedStore in self.feedStores {
try await feedStore.save(feed: feedStore.feed)
}
}
_ = try await task.value
}
func savePersonalFeed() async throws {
let task = Task {
let data = try JSONEncoder().encode(personalFeed)
let outfile = try fileURL(for: "personalFeed")
try data.write(to: outfile)
}
_ = try await task.value
}
func loadPersonalFeed() async throws {
let task = Task<Feed, Error> {
let fileURL = try self.fileURL(for: "personalFeed")
guard let data = try? Data(contentsOf: fileURL) else {
return Feed(feedID: self.personalID)
}
let personalFeed = try JSONDecoder().decode(Feed.self, from: data)
return personalFeed
}
let personalFeed = try await task.value
self.personalFeed = personalFeed
}
}
extension DataStore {
static let sampleDataStore = DataStore(personalID: "BOB", friends: SettingsView_Previews.friends, feedStores: [FeedStore(feed: Feed.sampleFeed), FeedStore(feed: Feed.sampleFeed2)])
}

View File

@ -10,42 +10,30 @@ import Foundation
struct Feed: Codable {
let feedID: String
var feed: [LogEntry]
let feed: [LogEntry]
init(feedID: String = "", feed: [LogEntry] = []) {
self.feedID = feedID
self.feed = feed
}
func getLastLogEntry() -> LogEntry {
if self.feed.isEmpty {
return LogEntry()
} else {
return self.feed.last!
}
}
mutating func appendLogEntry(log: LogEntry) {
self.feed.append(log)
}
}
extension Feed {
static let sampleData: [LogEntry] =
[
LogEntry(feedid: "BOB", sequenceNumber: 1, body: Bodyy(tag: Apps.nam, value: "Bob")),
LogEntry(feedid: "BOB", sequenceNumber: 2, body: Bodyy(tag: Apps.txt, value: "My first post!")),
LogEntry(feedid: "BOB", sequenceNumber: 3, body: Bodyy(tag: Apps.txt, value: "Welcome Alice"))
LogEntry(feedid: "BOB", sequenceNumber: 1, body: Body(tag: Apps.nam, value: "Bob")),
LogEntry(feedid: "BOB", sequenceNumber: 2, body: Body(tag: Apps.txt, value: "My first post!")),
LogEntry(feedid: "BOB", sequenceNumber: 3, body: Body(tag: Apps.txt, value: "Welcome Alice"))
]
static let sampleData2: [LogEntry] =
[
LogEntry(feedid: "ALI", sequenceNumber: 1, body: Bodyy(tag: Apps.nam, value: "Alice")),
LogEntry(feedid: "ALI", sequenceNumber: 2, body: Bodyy(tag: Apps.txt, value: "Alice' first post!")),
LogEntry(feedid: "ALI", sequenceNumber: 3, body: Bodyy(tag: Apps.txt, value: "Welcome Bob")),
LogEntry(feedid: "ALI", sequenceNumber: 4, body: Bodyy(tag: Apps.txt, value: "Whaddup DAWG"))
LogEntry(feedid: "ALI", sequenceNumber: 1, body: Body(tag: Apps.nam, value: "Alice")),
LogEntry(feedid: "ALI", sequenceNumber: 2, body: Body(tag: Apps.txt, value: "Alice' first post!")),
LogEntry(feedid: "ALI", sequenceNumber: 3, body: Body(tag: Apps.txt, value: "Welcome Bob")),
LogEntry(feedid: "ALI", sequenceNumber: 4, body: Body(tag: Apps.txt, value: "Whaddup DAWG"))
]
static let sampleFeed: Feed = Feed(feedID: "BOB", feed: sampleData)

View File

@ -8,9 +8,8 @@
import SwiftUI
@MainActor
class FeedStore: ObservableObject, Identifiable {
class FeedStore: ObservableObject {
let id: UUID = UUID()
@Published var feed: Feed
init(feed: Feed) {

View File

@ -1,32 +0,0 @@
//
// File.swift
// RippleChat
//
// Created by Severin Memmishofer on 13.07.23.
//
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error {
print("Error discovering services: \(error.localizedDescription)")
return
}
for service in peripheral.services ?? [] {
if service.uuid == CBUUID(string: "YourServiceUUIDHere") {
peripheral.discoverCharacteristics([CBUUID(string: "YourCharacteristicUUIDHere")], for: service)
}
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let error = error {
print("Error discovering characteristics: \(error.localizedDescription)")
return
}
for characteristic in service.characteristics ?? [] {
if characteristic.uuid == CBUUID(string: "YourCharacteristicUUIDHere") {
// You've found your characteristic!
}
}
}

View File

@ -7,22 +7,16 @@
import Foundation
struct LogEntry: Codable, Identifiable {
var id: UUID = UUID()
struct LogEntry: Codable {
let feedid: String
let sequenceNumber: Int
let body: Bodyy
let body: Body
init(feedid: String = "", sequenceNumber: Int = 0, body: Bodyy = Bodyy()) {
init(feedid: String, sequenceNumber: Int, body: Body) {
self.feedid = feedid
self.sequenceNumber = sequenceNumber
self.body = body
}
}
extension LogEntry {
static let sampleLogEntry = LogEntry(feedid: "BOB", sequenceNumber: 2, body: Bodyy(tag: Apps.txt, value: "My first post!"))
}

View File

@ -9,34 +9,9 @@ import SwiftUI
@main
struct RippleChatApp: App {
@StateObject private var dataStore = DataStore()
var body: some Scene {
WindowGroup {
ContentView() {
Task {
do {
try await dataStore.savePersonalID()
try await dataStore.savePersonalFeed()
try await dataStore.saveFriends()
try await dataStore.saveFeedStores()
} catch {
fatalError(error.localizedDescription)
}
}
}
.environmentObject(dataStore)
.task {
do {
try await dataStore.loadPersonalID()
try await dataStore.loadPersonalFeed()
try await dataStore.loadFriends()
try await dataStore.loadFeedStores()
} catch {
// Handle the error
print("Error loading data: \(error)")
fatalError(error.localizedDescription)
}
}
ContentView()
}
}

View File

@ -1,80 +0,0 @@
//
// PeeringView.swift
// RippleChat
//
// Created by Sebastian Lenzlinger on 10.07.23.
//
import SwiftUI
struct DiscoveryView: View {
@EnvironmentObject var dataStore: DataStore
@EnvironmentObject var btCentral: BTCentral
@EnvironmentObject var btPeripheral: BluetoothPeripheral
var body: some View {
NavigationStack {
List(btCentral.peripheralNames, id: \.self) { peripheral in
Text(peripheral)
}
.navigationTitle("Peer Discovery")
.navigationViewStyle(StackNavigationViewStyle())
List {
ForEach(btPeripheral.wantVector.friends.keys.sorted(), id: \.self) { friend in
Text("Feed: \(friend.description), SEQ: \(btPeripheral.wantVector.friends[friend] ?? -1)")
}
}
Text("Incoming msg: \(btPeripheral.incomingMsg)")
.onChange(of: btPeripheral.incomingMsg) { newValue in
compareWithSavedFeeds(newVector: btPeripheral.wantVector)
}
Button(action: {
do {
var combinedDict = dataStore.friends
combinedDict[dataStore.personalFeed.feedID] = dataStore.personalFeed.feed.count
let WANT_msg = WantMessage(friends: combinedDict)
let encoded_msg = try JSONEncoder().encode(WANT_msg)
btCentral.writeToCharacteristics(message: String(data: encoded_msg, encoding: .utf8)!)
//btController.writeToCharacteristics(message: "Test")
print("Pressed Button")
} catch {
fatalError(error.localizedDescription)
}
}) {
Text("Send WANT-Vector")
}
.padding()
}
}
func compareWithSavedFeeds(newVector: WantMessage) {
print("comparing with saved Feeds...")
for friend in newVector.friends.keys.sorted() {
if(dataStore.friends.keys.contains(friend.description)) {
print("Found friend \(friend.description) in own Feeds!")
var missingFeedEntries: Int = -1
if let ownCount = dataStore.friends[friend] {
missingFeedEntries = newVector.friends[friend]! - ownCount
}
print("You are \(missingFeedEntries) behind on the feed of \(friend.description)")
}
}
}
}
struct DiscoveryView_Previews: PreviewProvider {
static var previews: some View {
DiscoveryView()
.environmentObject(BluetoothPeripheral())
.environmentObject(BTCentral())
}
}
struct WantMessage: Codable {
var command = "WANT"
var friends = [String:Int]()
func printMsg() -> String {
return ("{\(command) : \(friends.description)}")
}
}

View File

@ -1,40 +0,0 @@
//
// FeedDetailView.swift
// RippleChat
//
// Created by Severin Memmishofer on 12.07.23.
//
import SwiftUI
struct FeedCardView: View {
@EnvironmentObject var dataStore: DataStore
var feed: Feed
private var lastLogEntry: LogEntry {
if feed.feed.isEmpty {
return LogEntry()
} else {
return feed.feed.last!
}
}
var body: some View {
VStack (alignment: .leading) {
HStack {
Text("feedID: \(feed.feedID)")
Spacer()
Text("SEQ: \(lastLogEntry.sequenceNumber)")
}
Text("Last: \(lastLogEntry.body.value)")
}
.padding()
}
}
struct FeedCardView_Previews: PreviewProvider {
static var previews: some View {
FeedCardView(feed: Feed.sampleFeed)
.environmentObject(DataStore.sampleDataStore)
}
}

View File

@ -1,33 +0,0 @@
//
// FeedDetailView.swift
// RippleChat
//
// Created by Severin Memmishofer on 13.07.23.
//
import SwiftUI
struct FeedDetailView: View {
@EnvironmentObject var dataStore: DataStore
var feed: Feed
init(feed: Feed) {
self.feed = feed
}
var body: some View {
NavigationStack {
List(feed.feed) { logEntry in
LogEntryView(logEntry: logEntry)
}
}
.navigationTitle("Feed: \(feed.feedID)")
}
}
struct FeedDetailView_Previews: PreviewProvider {
static var previews: some View {
FeedDetailView(feed: Feed.sampleFeed)
.environmentObject(DataStore.sampleDataStore)
}
}

View File

@ -8,33 +8,35 @@
import SwiftUI
struct FeedListView: View {
@State var feeds: [Feed]
@StateObject var store = FeedStore(feed: Feed.sampleFeed)
var feedStores = [FeedStore(feed: Feed.sampleFeed), FeedStore(feed: Feed.sampleFeed2)]
@EnvironmentObject var dataStore: DataStore
var body: some View {
NavigationStack {
Form {
Section(header: Text("Your own Feed:")) {
NavigationLink(destination: FeedDetailView(feed: dataStore.personalFeed)) {
FeedCardView(feed: dataStore.personalFeed)
}
}
Section(header: Text("Feeds of your Firends")) {
List(dataStore.feedStores) { feedStore in
NavigationLink(destination: FeedDetailView(feed: feedStore.feed)) {
FeedCardView(feed: feedStore.feed)
}
Text("FeedListView")
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
Button("Save Feed") {
Task {
do {
for feed in feedStores {
try await feed.save(feed: feed.feed)
}
} catch {
fatalError(error.localizedDescription)
}
}
.navigationTitle("Feeds")
NewFeedEntryView()
}
Spacer()
NewFeedEntryView()
}
}
struct FeedListView_Previews: PreviewProvider {
static var previews: some View {
FeedListView()
.environmentObject(DataStore.sampleDataStore)
FeedListView(feeds: [])
}
}

View File

@ -1,20 +0,0 @@
//
// FeedView.swift
// RippleChat
//
// Created by Severin Memmishofer on 12.07.23.
//
import SwiftUI
struct FeedView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct FeedView_Previews: PreviewProvider {
static var previews: some View {
FeedView()
}
}

View File

@ -1,39 +0,0 @@
//
// LogEntryView.swift
// RippleChat
//
// Created by Severin Memmishofer on 12.07.23.
//
import SwiftUI
struct LogEntryView: View {
var logEntry: LogEntry
init(logEntry: LogEntry) {
self.logEntry = logEntry
}
var body: some View {
HStack {
VStack {
HStack {
Text("SEQ: \(logEntry.sequenceNumber)")
Spacer()
Text("Tag: \(logEntry.body.tag)")
}
HStack {
Text("Value: \(logEntry.body.value)")
Spacer()
}
}
}
.padding()
}
}
struct LogEntryView_Previews: PreviewProvider {
static var previews: some View {
LogEntryView(logEntry: LogEntry.sampleLogEntry)
}
}

View File

@ -9,31 +9,16 @@ import SwiftUI
struct NewFeedEntryView: View {
@State private var newEntry: String = ""
@EnvironmentObject var dataStore: DataStore
var body: some View {
VStack(alignment: .leading) {
HStack {
TextField("Enter your new feed message:", text: $newEntry)
Button(action: {
let nextSeq = dataStore.personalFeed.getLastLogEntry().sequenceNumber + 1
let newBody = Bodyy(tag: Apps.txt, value: newEntry)
let newLogEntry = LogEntry(feedid: dataStore.personalID, sequenceNumber: nextSeq, body: newBody)
dataStore.personalFeed.appendLogEntry(log: newLogEntry)
newEntry = ""
}) {
Button(action: {}) {
Text("Send")
}
.task {
do {
try await dataStore.savePersonalFeed()
} catch {
// Handle the error
print("Error loading data: \(error)")
fatalError(error.localizedDescription)
}
}
}
Text("New entry: \(newEntry)")
}
.padding()
}
@ -42,6 +27,5 @@ struct NewFeedEntryView: View {
struct NewFeedEntryView_Previews: PreviewProvider {
static var previews: some View {
NewFeedEntryView()
.environmentObject(DataStore.sampleDataStore)
}
}

View File

@ -0,0 +1,31 @@
//
// PeeringView.swift
// RippleChat
//
// Created by Sebastian Lenzlinger on 10.07.23.
//
import SwiftUI
struct PeeringView: View {
@ObservedObject private var bluetoothController = BluetoothController()
@ObservedObject private var bluetoothPeripheral = BluetoothPeripheral()
@EnvironmentObject var dataStore: DataStore
var body: some View {
Text("Peering View")
NavigationStack {
List(bluetoothController.peripheralNames, id: \.self) { peripheral in
Text(peripheral)
}
.navigationTitle("Peripherals")
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct PeeringView_Previews: PreviewProvider {
static var previews: some View {
PeeringView()
}
}

View File

@ -17,35 +17,23 @@ struct SettingsEditView: View {
//Label(dataStore.personalID, systemImage: "person.crop.circle")
HStack {
TextField(dataStore.personalID, text: $dataStore.personalID)
}
}
Section(header: Text("Friends")) {
ForEach(dataStore.friends.keys.sorted(), id: \.self) { friend in
if let seq = dataStore.friends[friend] {
Label("\(friend) - SEQ: \(seq)", systemImage: "person")
}
}
.onDelete { indexSet in do {
indexSet.forEach { index in
let key = dataStore.friends.keys.sorted()[index]
dataStore.friends.removeValue(forKey: key)
}
dataStore.feedStores.remove(atOffsets: indexSet)
ForEach(dataStore.friends) { friend in
Label(friend, systemImage: "person")
}
.onDelete {indices in
dataStore.friends.remove(atOffsets: indices)
}
HStack {
TextField("New Feed", text: $newFeedID)
Button(action: {
let newFeed = Feed(feedID: newFeedID)
let newFeedStore = FeedStore(feed: newFeed)
dataStore.feedStores.append(newFeedStore)
withAnimation {
dataStore.friends[newFeedID] = 0
let feedid = newFeedID
dataStore.friends.append(feedid)
newFeedID = ""
}
}) {
Image(systemName: "plus.circle.fill")
}

View File

@ -22,10 +22,8 @@ struct SettingsView: View {
Label(dataStore.personalID, systemImage: "person.crop.circle")
}
Section(header: Text("Friends")) {
ForEach(dataStore.friends.keys.sorted(), id: \.self) { friend in
if let seq = dataStore.friends[friend] {
Label("\(friend) - SEQ: \(seq)", systemImage: "person")
}
ForEach(dataStore.friends) { friend in
Label(friend, systemImage: "person")
}
}
}
@ -51,7 +49,6 @@ struct SettingsView: View {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
isPresentingEditView = false
dataStore.personalFeed = Feed(feedID: dataStore.personalID)
}
}
}
@ -65,9 +62,9 @@ struct SettingsView: View {
struct SettingsView_Previews: PreviewProvider {
static var friends = [
"BOS":1,
"ALI":2,
"CYN":3
"BOS",
"ALI",
"CYN"
]
static var previews: some View {
SettingsView()