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:
Alexander Heinrich
2021-03-09 23:57:28 +01:00
committed by GitHub
co-authored by Frank Hessel
parent f88663f5e7
commit 898563ca0b
46 changed files with 2955 additions and 270 deletions
@@ -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)
}
}