mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-08-20 03:56:14 +00:00
Supporting ESP32 as tags for OpenHaystack (#19)
* Moving microbit firmware to a subfolder in /Firmware to prepare integration of ESP32 * Add firmware for ESP32 and update workflows * Integrated ESP32 firmware from @fhessel to OpenHaystack App Co-authored-by: Frank Hessel <fhessel@seemoo.tu-darmstadt.de>
This commit is contained in:
co-authored by
Frank Hessel
parent
f88663f5e7
commit
898563ca0b
@@ -6,14 +6,22 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
class AccessoryController: ObservableObject {
|
||||
static let shared = AccessoryController()
|
||||
|
||||
@Published var accessories: [Accessory]
|
||||
|
||||
var accessoryObserver: AnyCancellable?
|
||||
|
||||
init() {
|
||||
self.accessories = KeychainController.loadAccessoriesFromKeychain()
|
||||
self.accessoryObserver = self.accessories.publisher
|
||||
.sink { _ in
|
||||
try? self.save()
|
||||
}
|
||||
}
|
||||
|
||||
init(accessories: [Accessory]) {
|
||||
@@ -46,4 +54,30 @@ class AccessoryController: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func delete(accessory: Accessory) throws {
|
||||
var accessories = self.accessories
|
||||
guard let idx = accessories.firstIndex(of: accessory) else { return }
|
||||
|
||||
accessories.remove(at: idx)
|
||||
|
||||
withAnimation {
|
||||
self.accessories = accessories
|
||||
}
|
||||
try self.save()
|
||||
}
|
||||
|
||||
func addAccessory(with name: String, color: Color, icon: String) throws -> Accessory {
|
||||
let accessory = try Accessory(name: name, color: color, iconName: icon)
|
||||
|
||||
let accessories = self.accessories + [accessory]
|
||||
|
||||
withAnimation {
|
||||
self.accessories = accessories
|
||||
}
|
||||
|
||||
try self.save()
|
||||
|
||||
return accessory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// ESP32Controller.swift
|
||||
// OpenHaystack
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ESP32Controller {
|
||||
static var espFirmwareDirectory: URL? {
|
||||
Bundle.main.resourceURL?.appendingPathComponent("ESP32")
|
||||
}
|
||||
|
||||
/// Tries to find the port / path at which the ESP32 module is attached
|
||||
static func findPort() -> [URL] {
|
||||
// List all ports
|
||||
let ports = try? FileManager.default.contentsOfDirectory(atPath: "/dev").filter({$0.contains("cu.")})
|
||||
|
||||
let portURLs = ports?.map({URL(fileURLWithPath: "/dev/\($0)")})
|
||||
|
||||
return portURLs ?? []
|
||||
}
|
||||
|
||||
/// Runs the script to flash the firmware on an ESP32
|
||||
static func flashToESP32(accessory: Accessory, port: URL, completion: @escaping (Result<Void, Error>) -> Void) throws {
|
||||
|
||||
// Copy firmware to a temporary directory
|
||||
let temp = NSTemporaryDirectory() + "OpenHaystack"
|
||||
let urlTemp = URL(fileURLWithPath: temp)
|
||||
try? FileManager.default.removeItem(at: urlTemp)
|
||||
|
||||
try? FileManager.default.createDirectory(atPath: temp, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
guard let espDirectory = espFirmwareDirectory else {return}
|
||||
|
||||
try FileManager.default.copyFolder(from: espDirectory, to: urlTemp)
|
||||
let scriptPath = urlTemp.appendingPathComponent("flash_esp32.sh")
|
||||
|
||||
let key = try accessory.getAdvertisementKey().base64EncodedString()
|
||||
let arguments = ["-p", "\(port.path)", key]
|
||||
|
||||
let task = try NSUserUnixTask(url: scriptPath)
|
||||
task.execute(withArguments: arguments) { e in
|
||||
DispatchQueue.main.async {
|
||||
if let error = e {
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
// Delete the temporary folder
|
||||
try? FileManager.default.removeItem(at: urlTemp)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
enum FirmwareFlashError: Error {
|
||||
/// Missing files for flashing
|
||||
case notFound
|
||||
/// Flashing / writing failed
|
||||
case flashFailed
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// FileManager.swift
|
||||
// OpenHaystack
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
|
||||
/// Copy a folder recursively.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: Folder source
|
||||
/// - to: Folder destination
|
||||
/// - Throws: An error if copying or acessing files fails
|
||||
func copyFolder(from: URL, to: URL) throws {
|
||||
// Create the folder
|
||||
try? FileManager.default.createDirectory(at: to, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
let files = try FileManager.default.contentsOfDirectory(atPath: from.path)
|
||||
for file in files {
|
||||
// Check if file is a folder
|
||||
var isDir: ObjCBool = .init(booleanLiteral: false)
|
||||
let fileURL = from.appendingPathComponent(file)
|
||||
FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDir)
|
||||
|
||||
if isDir.boolValue == true {
|
||||
try self.copyFolder(from: fileURL, to: to.appendingPathComponent(file))
|
||||
} else {
|
||||
// Copy file
|
||||
try FileManager.default.copyItem(at: fileURL, to: to.appendingPathComponent(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
(directory will be populated in CI release workflow)
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Directory of this script
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
|
||||
# Defaults: Directory for the virtual environment
|
||||
VENV_DIR="$SCRIPT_DIR/venv"
|
||||
|
||||
# Defaults: Serial port to access the ESP32
|
||||
PORT=/dev/ttyS0
|
||||
|
||||
# Defaults: Fast baud rate
|
||||
BAUDRATE=921600
|
||||
|
||||
# Parameter parsing
|
||||
while [[ $# -gt 0 ]]; do
|
||||
KEY="$1"
|
||||
case "$KEY" in
|
||||
-p|--port)
|
||||
PORT="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-s|--slow)
|
||||
BAUDRATE=115200
|
||||
shift
|
||||
;;
|
||||
-v|--venvdir)
|
||||
VENV_DIR="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
echo "flash_esp32.sh - Flash the OpenHaystack firmware onto an ESP32 module"
|
||||
echo ""
|
||||
echo " This script will create a virtual environment for the required tools."
|
||||
echo ""
|
||||
echo "Call: flash_esp32.sh [-p <port>] [-v <dir>] [-s] PUBKEY"
|
||||
echo ""
|
||||
echo "Required Arguments:"
|
||||
echo " PUBKEY"
|
||||
echo " The base64-encoded public key"
|
||||
echo ""
|
||||
echo "Optional Arguments:"
|
||||
echo " -h, --help"
|
||||
echo " Show this message and exit."
|
||||
echo " -p, --port <port>"
|
||||
echo " Specify the serial interface to which the device is connected."
|
||||
echo " -s, --slow"
|
||||
echo " Use 115200 instead of 921600 baud when flashing."
|
||||
echo " Might be required for long/bad USB cables or slow USB-to-Serial converters."
|
||||
echo " -v, --venvdir <dir>"
|
||||
echo " Select Python virtual environment with esptool installed."
|
||||
echo " If the directory does not exist, it will be created."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$PUBKEY" ]]; then
|
||||
PUBKEY="$1"
|
||||
shift
|
||||
else
|
||||
echo "Got unexpected parameter $1"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Sanity check: Pubkey exists
|
||||
if [[ -z "$PUBKEY" ]]; then
|
||||
echo "Missing public key, call with --help for usage"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sanity check: Port
|
||||
if [[ ! -e "$PORT" ]]; then
|
||||
echo "$PORT does not exist, please specify a valid serial interface with the -p argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Setup the virtual environment
|
||||
if [[ ! -d "$VENV_DIR" ]]; then
|
||||
# Create the virtual environment
|
||||
PYTHON="$(which python3)"
|
||||
if [[ -z "$PYTHON" ]]; then
|
||||
PYTHON="$(which python)"
|
||||
fi
|
||||
if [[ -z "$PYTHON" ]]; then
|
||||
echo "Could not find a Python installation, please install Python 3."
|
||||
exit 1
|
||||
fi
|
||||
if ! ($PYTHON -V 2>&1 | grep "Python 3" > /dev/null); then
|
||||
echo "Executing \"$PYTHON\" does not run Python 3, please make sure that python3 or python on your PATH points to Python 3"
|
||||
exit 1
|
||||
fi
|
||||
if ! ($PYTHON -c "import venv" &> /dev/null); then
|
||||
echo "Python 3 module \"venv\" was not found."
|
||||
exit 1
|
||||
fi
|
||||
$PYTHON -m venv "$VENV_DIR"
|
||||
if [[ $? != 0 ]]; then
|
||||
echo "Creating the virtual environment in $VENV_DIR failed."
|
||||
exit 1
|
||||
fi
|
||||
source "$VENV_DIR/bin/activate"
|
||||
pip install --upgrade pip
|
||||
pip install esptool
|
||||
if [[ $? != 0 ]]; then
|
||||
echo "Could not install Python 3 module esptool in $VENV_DIR";
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
source "$VENV_DIR/bin/activate"
|
||||
fi
|
||||
|
||||
# Prepare the key
|
||||
KEYFILE="$SCRIPT_DIR/tmp.key"
|
||||
if [[ -f "$KEYFILE" ]]; then
|
||||
echo "$KEYFILE already exists, stopping here not to override files..."
|
||||
exit 1
|
||||
fi
|
||||
echo "$PUBKEY" | python3 -m base64 -d - > "$KEYFILE"
|
||||
if [[ $? != 0 ]]; then
|
||||
echo "Could not parse the public key. Please provide valid base64 input"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Call esptool.py. Errors from here on are critical
|
||||
set -e
|
||||
|
||||
# Clear NVM
|
||||
esptool.py --after no_reset \
|
||||
erase_region 0x9000 0x5000
|
||||
esptool.py --before no_reset --baud $BAUDRATE \
|
||||
write_flash 0x1000 "$SCRIPT_DIR/build/bootloader/bootloader.bin" \
|
||||
0x8000 "$SCRIPT_DIR/build/partition_table/partition-table.bin" \
|
||||
0xe000 "$KEYFILE" \
|
||||
0x10000 "$SCRIPT_DIR/build/openhaystack.bin"
|
||||
rm "$KEYFILE"
|
||||
@@ -17,7 +17,7 @@ struct KeychainController {
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecMatchLimit: kSecMatchLimitOne,
|
||||
kSecReturnData: true,
|
||||
kSecReturnData: true
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -49,7 +49,7 @@ struct KeychainController {
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories),
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories)
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -63,7 +63,7 @@ struct KeychainController {
|
||||
var query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecAttrService: "SEEMOO-FINDMY"
|
||||
]
|
||||
|
||||
if test {
|
||||
|
||||
@@ -63,7 +63,7 @@ struct MailPluginManager {
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
try self.copyFolder(from: localPluginURL, to: pluginURL)
|
||||
try FileManager.default.copyFolder(from: localPluginURL, to: pluginURL)
|
||||
|
||||
self.openAppleMail()
|
||||
}
|
||||
@@ -73,32 +73,6 @@ struct MailPluginManager {
|
||||
|
||||
}
|
||||
|
||||
/// Copy a folder recursively.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: Folder source
|
||||
/// - to: Folder destination
|
||||
/// - Throws: An error if copying or acessing files fails
|
||||
func copyFolder(from: URL, to: URL) throws {
|
||||
// Create the folder
|
||||
try? FileManager.default.createDirectory(at: to, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
let files = try FileManager.default.contentsOfDirectory(atPath: from.path)
|
||||
for file in files {
|
||||
// Check if file is a folder
|
||||
var isDir: ObjCBool = .init(booleanLiteral: false)
|
||||
let fileURL = from.appendingPathComponent(file)
|
||||
FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDir)
|
||||
|
||||
if isDir.boolValue == true {
|
||||
try self.copyFolder(from: fileURL, to: to.appendingPathComponent(file))
|
||||
} else {
|
||||
// Copy file
|
||||
try FileManager.default.copyItem(at: fileURL, to: to.appendingPathComponent(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallMailPlugin() throws {
|
||||
try FileManager.default.removeItem(at: pluginURL)
|
||||
}
|
||||
@@ -115,7 +89,7 @@ struct MailPluginManager {
|
||||
|
||||
let downloadsPluginURL = downloadsFolder.appendingPathComponent(mailBundleName + ".mailbundle")
|
||||
|
||||
try self.copyFolder(from: localPluginURL, to: downloadsPluginURL)
|
||||
try FileManager.default.copyFolder(from: localPluginURL, to: downloadsPluginURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,6 +72,22 @@ struct MicrobitController {
|
||||
return patchedFirmware
|
||||
}
|
||||
|
||||
static func deploy(accessory: Accessory) throws {
|
||||
let microbits = try MicrobitController.findMicrobits()
|
||||
guard let microBitURL = microbits.first,
|
||||
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
|
||||
else {
|
||||
throw FirmwareFlashError.notFound
|
||||
}
|
||||
|
||||
let firmware = try Data(contentsOf: firmwareURL)
|
||||
let pattern = "OFFLINEFINDINGPUBLICKEYHERE!".data(using: .ascii)!
|
||||
let publicKey = try accessory.getAdvertisementKey()
|
||||
let patchedFirmware = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: publicKey)
|
||||
|
||||
try MicrobitController.deployToMicrobit(microBitURL, firmwareFile: patchedFirmware)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum PatchingError: Error {
|
||||
|
||||
@@ -41,8 +41,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
|
||||
if var colorComponents = try? container.decode([CGFloat].self, forKey: .colorComponents),
|
||||
let spaceName = try? container.decode(String.self, forKey: .colorSpaceName),
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents)
|
||||
{
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents) {
|
||||
self.color = Color(cgColor)
|
||||
} else {
|
||||
self.color = Color.white
|
||||
@@ -58,8 +57,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
try container.encode(self.icon, forKey: .icon)
|
||||
|
||||
if let colorComponents = self.color.cgColor?.components,
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name
|
||||
{
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name {
|
||||
try container.encode(colorComponents, forKey: .colorComponents)
|
||||
try container.encode(colorSpace as String, forKey: .colorSpaceName)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,5 @@ struct AccessoryMapView: NSViewControllerRepresentable {
|
||||
nsViewController.addLastLocations(from: accessories)
|
||||
|
||||
nsViewController.changeMapType(mapType)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// ESP32InstallSheet.swift
|
||||
// OpenHaystack
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
struct ESP32InstallSheet: View {
|
||||
@Binding var accessory: Accessory?
|
||||
@Binding var alertType: OpenHaystackMainView.AlertType?
|
||||
@State var detectedPorts: [URL] = []
|
||||
|
||||
@State var isFlashing = false
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
self.portSelectionView
|
||||
.padding()
|
||||
.overlay(self.loadingOverlay)
|
||||
.frame(minWidth: 640, minHeight: 480, alignment: .center)
|
||||
}
|
||||
.onAppear {
|
||||
self.detectedPorts = ESP32Controller.findPort()
|
||||
}
|
||||
}
|
||||
|
||||
var portSelectionView: some View {
|
||||
VStack {
|
||||
Text("Flash your ESP32")
|
||||
.font(.title2)
|
||||
|
||||
Text("Select the serial port that belongs to your ESP32 module")
|
||||
.foregroundColor(.gray)
|
||||
|
||||
self.portList
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button("Reload ports", action: {
|
||||
self.detectedPorts = ESP32Controller.findPort()
|
||||
})
|
||||
|
||||
Button("Cancel", action: {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var portList: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 4) {
|
||||
ForEach(0..<self.detectedPorts.count, id: \.self) { portIdx in
|
||||
Button(action: {
|
||||
if let accessory = self.accessory {
|
||||
// Flash selected module
|
||||
self.deployAccessoryToESP32(accessory: accessory, to: self.detectedPorts[portIdx])
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Text(self.detectedPorts[portIdx].path)
|
||||
.padding(4)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
|
||||
})
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var loadingOverlay: some View {
|
||||
ZStack {
|
||||
if isFlashing {
|
||||
Rectangle()
|
||||
.fill(Color.gray)
|
||||
.opacity(0.5)
|
||||
|
||||
VStack {
|
||||
ActivityIndicator(size: .large)
|
||||
Text("This can take up to 3min")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deployAccessoryToESP32(accessory: Accessory, to port: URL) {
|
||||
do {
|
||||
self.isFlashing = true
|
||||
try ESP32Controller.flashToESP32(accessory: accessory, port: port, completion: { result in
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
self.isFlashing = false
|
||||
switch result {
|
||||
case .success(_):
|
||||
self.alertType = .deployedSuccessfully
|
||||
case .failure(let error):
|
||||
os_log(.error, "Flashing to ESP32 failed %@", String(describing: error))
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
self.alertType = .deployFailed
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
os_log(.error, "Execution of script failed %@", String(describing: error))
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
self.alertType = .deployFailed
|
||||
self.isFlashing = false
|
||||
|
||||
}
|
||||
|
||||
self.accessory = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct ESP32InstallSheet_Previews: PreviewProvider {
|
||||
@State static var acc: Accessory? = try! Accessory(name: "Sample")
|
||||
|
||||
@State static var alert: OpenHaystackMainView.AlertType?
|
||||
|
||||
static var previews: some View {
|
||||
ESP32InstallSheet(accessory: $acc, alertType: $alert)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// ManageAccessoriesView.swift
|
||||
// OpenHaystack
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ManageAccessoriesView: View {
|
||||
|
||||
@ObservedObject var accessoryController = AccessoryController.shared
|
||||
var accessories: [Accessory] {
|
||||
return self.accessoryController.accessories
|
||||
}
|
||||
|
||||
// MARK: Bindings from main View
|
||||
@Binding var alertType: OpenHaystackMainView.AlertType?
|
||||
@Binding var focusedAccessory: Accessory?
|
||||
@Binding var accessoryToDeploy: Accessory?
|
||||
@Binding var showESP32DeploySheet: Bool
|
||||
|
||||
// MARK: View State
|
||||
@State var keyName: String = ""
|
||||
@State var accessoryColor: Color = Color.white
|
||||
@State var selectedIcon: String = "briefcase.fill"
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Create a new tracking accessory")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
Text("A BBC Microbit can be used to track anything you care about. Connect it over USB, name the accessory (e.g. Backpack) generate the key and deploy it")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
HStack {
|
||||
TextField("Name", text: self.$keyName)
|
||||
ColorPicker("", selection: self.$accessoryColor)
|
||||
.frame(maxWidth: 50, maxHeight: 20)
|
||||
IconSelectionView(selectedImageName: self.$selectedIcon)
|
||||
}
|
||||
|
||||
Button(
|
||||
action: self.addAccessory,
|
||||
label: {
|
||||
Text("Generate key and deploy")
|
||||
}
|
||||
)
|
||||
.disabled(self.keyName.isEmpty)
|
||||
.padding(.bottom)
|
||||
|
||||
Divider()
|
||||
|
||||
Text("Your accessories")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
if self.accessories.isEmpty {
|
||||
Spacer()
|
||||
Text("No accessories have been added yet. Go ahead and add one above")
|
||||
.multilineTextAlignment(.center)
|
||||
} else {
|
||||
self.accessoryList
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.sheet(isPresented: self.$showESP32DeploySheet, content: {
|
||||
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
|
||||
})
|
||||
}
|
||||
|
||||
/// Accessory List view.
|
||||
var accessoryList: some View {
|
||||
List(self.accessories) { accessory in
|
||||
AccessoryListEntry(
|
||||
accessory: accessory,
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deploy(accessory:),
|
||||
zoomOn: { self.focusedAccessory = $0 })
|
||||
}
|
||||
.background(Color.clear)
|
||||
.cornerRadius(15.0)
|
||||
}
|
||||
|
||||
/// Delete an accessory from the list of accessories.
|
||||
func delete(accessory: Accessory) {
|
||||
do {
|
||||
try self.accessoryController.delete(accessory: accessory)
|
||||
} catch {
|
||||
self.alertType = .deletionFailed
|
||||
}
|
||||
}
|
||||
|
||||
func deploy(accessory: Accessory) {
|
||||
self.accessoryToDeploy = accessory
|
||||
self.alertType = .selectDepoyTarget
|
||||
}
|
||||
|
||||
/// Add an accessory with the provided details.
|
||||
func addAccessory() {
|
||||
let keyName = self.keyName
|
||||
self.keyName = ""
|
||||
|
||||
do {
|
||||
let accessory = try self.accessoryController.addAccessory(with: keyName, color: self.accessoryColor, icon: self.selectedIcon)
|
||||
self.deploy(accessory: accessory)
|
||||
|
||||
} catch {
|
||||
self.alertType = .keyError
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct ManageAccessoriesView_Previews: PreviewProvider {
|
||||
|
||||
@State static var accessories = PreviewData.accessories
|
||||
@State static var alertType: OpenHaystackMainView.AlertType?
|
||||
@State static var focussed: Accessory?
|
||||
@State static var deploy: Accessory?
|
||||
@State static var showESPSheet: Bool = true
|
||||
|
||||
static var previews: some View {
|
||||
ManageAccessoriesView(alertType: self.$alertType, focusedAccessory: self.$focussed, accessoryToDeploy: self.$deploy, showESP32DeploySheet: self.$showESPSheet)
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,6 @@ import SwiftUI
|
||||
|
||||
struct OpenHaystackMainView: View {
|
||||
|
||||
@State var keyName: String = ""
|
||||
@State var accessoryColor: Color = Color.white
|
||||
@State var selectedIcon: String = "briefcase.fill"
|
||||
|
||||
@State var loading = false
|
||||
@ObservedObject var accessoryController = AccessoryController.shared
|
||||
var accessories: [Accessory] {
|
||||
@@ -30,14 +26,20 @@ struct OpenHaystackMainView: View {
|
||||
@State var mapType: MKMapType = .standard
|
||||
@State var isLoading = false
|
||||
@State var focusedAccessory: Accessory?
|
||||
@State var accessoryToDeploy: Accessory?
|
||||
|
||||
@State var showESP32DeploySheet = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack {
|
||||
self.accessoryView
|
||||
.frame(width: geo.size.width * 0.5)
|
||||
ManageAccessoriesView(
|
||||
alertType: self.$alertType,
|
||||
focusedAccessory: self.$focusedAccessory,
|
||||
accessoryToDeploy: self.$accessoryToDeploy,
|
||||
showESP32DeploySheet: self.$showESP32DeploySheet)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -92,67 +94,6 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
// MARK: Subviews
|
||||
|
||||
/// Left side of the view. Shows a list of accessories and the possibility to add accessories
|
||||
var accessoryView: some View {
|
||||
VStack {
|
||||
Text("Create a new tracking accessory")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
Text("A BBC Microbit can be used to track anything you care about. Connect it over USB, name the accessory (e.g. Backpack) generate the key and deploy it")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
HStack {
|
||||
TextField("Name", text: self.$keyName)
|
||||
ColorPicker("", selection: self.$accessoryColor)
|
||||
.frame(maxWidth: 50, maxHeight: 20)
|
||||
IconSelectionView(selectedImageName: self.$selectedIcon)
|
||||
}
|
||||
|
||||
Button(
|
||||
action: self.addAccessory,
|
||||
label: {
|
||||
Text("Generate key and deploy")
|
||||
}
|
||||
)
|
||||
.disabled(self.keyName.isEmpty)
|
||||
.padding(.bottom)
|
||||
|
||||
Divider()
|
||||
|
||||
Text("Your accessories")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
if self.accessories.isEmpty {
|
||||
Spacer()
|
||||
Text("No accessories have been added yet. Go ahead and add one above")
|
||||
.multilineTextAlignment(.center)
|
||||
} else {
|
||||
self.accessoryList
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Accessory List view.
|
||||
var accessoryList: some View {
|
||||
List(self.accessories) { accessory in
|
||||
AccessoryListEntry(
|
||||
accessory: accessory,
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
|
||||
zoomOn: { self.focusedAccessory = $0 })
|
||||
}
|
||||
.background(Color.clear)
|
||||
.cornerRadius(15.0)
|
||||
}
|
||||
|
||||
/// Overlay for the map that is gray and shows an activity indicator when loading.
|
||||
var mapOverlay: some View {
|
||||
ZStack {
|
||||
@@ -202,28 +143,25 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an accessory with the provided details.
|
||||
func addAccessory() {
|
||||
let keyName = self.keyName
|
||||
self.keyName = ""
|
||||
func onAppear() {
|
||||
|
||||
do {
|
||||
let accessory = try Accessory(name: keyName, color: self.accessoryColor, iconName: self.selectedIcon)
|
||||
|
||||
let accessories = self.accessories + [accessory]
|
||||
|
||||
withAnimation {
|
||||
self.accessoryController.accessories = accessories
|
||||
}
|
||||
try self.accessoryController.save()
|
||||
|
||||
self.deployAccessoryToMicrobit(accessory: accessory)
|
||||
|
||||
} catch {
|
||||
self.errorDescription = String(describing: error)
|
||||
self.showKeyError = true
|
||||
/// Checks if the search party token can be fetched without the Mail Plugin. If true the plugin is not needed for this environment. (e.g. when SIP is disabled)
|
||||
let reportsFetcher = ReportsFetcher()
|
||||
if let token = reportsFetcher.fetchSearchpartyToken(),
|
||||
let tokenString = String(data: token, encoding: .ascii) {
|
||||
self.searchPartyToken = tokenString
|
||||
return
|
||||
}
|
||||
|
||||
let pluginManager = MailPluginManager()
|
||||
|
||||
// Check if the plugin is installed
|
||||
if pluginManager.isMailPluginInstalled == false {
|
||||
// Install the mail plugin
|
||||
self.alertType = .activatePlugin
|
||||
} else {
|
||||
self.checkPluginIsRunning(nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Download the location reports for all current accessories. Shows an error if something fails, like plug-in is missing
|
||||
@@ -246,75 +184,35 @@ struct OpenHaystackMainView: View {
|
||||
self.isLoading = true
|
||||
}
|
||||
|
||||
let findMyDevices = self.accessories.compactMap({ acc -> FindMyDevice? in
|
||||
do {
|
||||
return try acc.toFindMyDevice()
|
||||
} catch {
|
||||
os_log("Failed getting id for key %@", String(describing: error))
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
FindMyController.shared.devices = findMyDevices
|
||||
FindMyController.shared.fetchReports(with: tokenData) { error in
|
||||
|
||||
let reports = FindMyController.shared.devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
withAnimation {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
FindMyController.shared.fetchReports(for: accessories, with: tokenData) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
|
||||
case .success(let devices):
|
||||
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
withAnimation {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.accessoryController.updateWithDecryptedReports(devices: FindMyController.shared.devices)
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
guard error != nil else { return }
|
||||
os_log("Error: %@", String(describing: error))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Delete an accessory from the list of accessories.
|
||||
func delete(accessory: Accessory) {
|
||||
do {
|
||||
var accessories = self.accessories
|
||||
guard let idx = accessories.firstIndex(of: accessory) else { return }
|
||||
|
||||
accessories.remove(at: idx)
|
||||
|
||||
withAnimation {
|
||||
self.accessoryController.accessories = accessories
|
||||
}
|
||||
try self.accessoryController.save()
|
||||
|
||||
} catch {
|
||||
self.alertType = .deletionFailed
|
||||
}
|
||||
|
||||
func deploy(accessory: Accessory) {
|
||||
self.accessoryToDeploy = accessory
|
||||
self.alertType = .selectDepoyTarget
|
||||
}
|
||||
|
||||
/// Deploy the public key of the accessory to a BBC microbit.
|
||||
func deployAccessoryToMicrobit(accessory: Accessory) {
|
||||
do {
|
||||
let microbits = try MicrobitController.findMicrobits()
|
||||
guard let microBitURL = microbits.first,
|
||||
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
|
||||
else {
|
||||
self.alertType = .deployFailed
|
||||
return
|
||||
}
|
||||
let firmware = try Data(contentsOf: firmwareURL)
|
||||
let pattern = "OFFLINEFINDINGPUBLICKEYHERE!".data(using: .ascii)!
|
||||
let publicKey = try accessory.getAdvertisementKey()
|
||||
let patchedFirmware = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: publicKey)
|
||||
|
||||
try MicrobitController.deployToMicrobit(microBitURL, firmwareFile: patchedFirmware)
|
||||
|
||||
try MicrobitController.deploy(accessory: accessory)
|
||||
} catch {
|
||||
os_log("Error occurred %@", String(describing: error))
|
||||
self.alertType = .deployFailed
|
||||
@@ -322,28 +220,8 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
|
||||
self.alertType = .deployedSuccessfully
|
||||
}
|
||||
|
||||
func onAppear() {
|
||||
|
||||
/// Checks if the search party token can be fetched without the Mail Plugin. If true the plugin is not needed for this environment. (e.g. when SIP is disabled)
|
||||
let reportsFetcher = ReportsFetcher()
|
||||
if let token = reportsFetcher.fetchSearchpartyToken(),
|
||||
let tokenString = String(data: token, encoding: .ascii)
|
||||
{
|
||||
self.searchPartyToken = tokenString
|
||||
return
|
||||
}
|
||||
|
||||
let pluginManager = MailPluginManager()
|
||||
|
||||
// Check if the plugin is installed
|
||||
if pluginManager.isMailPluginInstalled == false {
|
||||
// Install the mail plugin
|
||||
self.alertType = .activatePlugin
|
||||
} else {
|
||||
self.checkPluginIsRunning(nil)
|
||||
}
|
||||
self.accessoryToDeploy = nil
|
||||
}
|
||||
|
||||
/// Ask to install and activate the mail plugin.
|
||||
@@ -402,6 +280,7 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
// MARK: - Alerts
|
||||
|
||||
// swiftlint:disable function_body_length
|
||||
/// Create an alert for the given alert type.
|
||||
///
|
||||
/// - Parameter alertType: current alert type
|
||||
@@ -465,6 +344,17 @@ struct OpenHaystackMainView: View {
|
||||
action: {
|
||||
self.downloadPlugin()
|
||||
}), secondaryButton: .cancel())
|
||||
case .selectDepoyTarget:
|
||||
let microbitButton = Alert.Button.default(Text("Microbit"), action: {self.deployAccessoryToMicrobit(accessory: self.accessoryToDeploy!)})
|
||||
|
||||
let esp32Button = Alert.Button.default(Text("ESP32"), action: {
|
||||
self.showESP32DeploySheet = true
|
||||
})
|
||||
|
||||
return Alert(title: Text("Select target"),
|
||||
message: Text("Please select to which device you want to deploy"),
|
||||
primaryButton: microbitButton,
|
||||
secondaryButton: esp32Button)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +371,7 @@ struct OpenHaystackMainView: View {
|
||||
case noReportsFound
|
||||
case activatePlugin
|
||||
case pluginInstallFailed
|
||||
case selectDepoyTarget
|
||||
}
|
||||
|
||||
}
|
||||
@@ -491,7 +382,7 @@ struct OpenHaystackMainView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
OpenHaystackMainView(accessoryController: AccessoryController(accessories: accessories))
|
||||
.frame(width: 640, height: 480, alignment: .center)
|
||||
.frame(width: 800, height: 600, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user