diff --git a/.gitignore b/.gitignore index e50a6600..5476800a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ Plugins/VersionGeneratorPlugin # LLM Tools .claude/ +.zread/ opencode.json # Xcode per-user state diff --git a/ASFW.xcodeproj/project.pbxproj b/ASFW.xcodeproj/project.pbxproj index b28710cb..ed718b6b 100644 --- a/ASFW.xcodeproj/project.pbxproj +++ b/ASFW.xcodeproj/project.pbxproj @@ -10,8 +10,6 @@ 3A1693F02E808765000BD368 /* DriverKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A1693EF2E808765000BD368 /* DriverKit.framework */; }; 3A1693F92E808765000BD368 /* net.mrmidi.ASFW.ASFWDriver.dext in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 3A1693ED2E808765000BD368 /* net.mrmidi.ASFW.ASFWDriver.dext */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3A1694002E8087BD000BD368 /* PCIDriverKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A1693FF2E8087BD000BD368 /* PCIDriverKit.framework */; }; - 3A27C5302ECDE045009CA664 /* bump.sh in Resources */ = {isa = PBXBuildFile; fileRef = 3A27C52E2ECDE045009CA664 /* bump.sh */; }; - 3A27C5322ECDE045009CA664 /* bump.sh in Resources */ = {isa = PBXBuildFile; fileRef = 3A27C52E2ECDE045009CA664 /* bump.sh */; }; 3ABA31132EF8564A0046405D /* AudioDriverKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3ABA31122EF8564A0046405D /* AudioDriverKit.framework */; }; /* End PBXBuildFile section */ @@ -51,7 +49,6 @@ 3A1693ED2E808765000BD368 /* net.mrmidi.ASFW.ASFWDriver.dext */ = {isa = PBXFileReference; explicitFileType = "wrapper.driver-extension"; includeInIndex = 0; path = net.mrmidi.ASFW.ASFWDriver.dext; sourceTree = BUILT_PRODUCTS_DIR; }; 3A1693EF2E808765000BD368 /* DriverKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DriverKit.framework; path = System/Library/Frameworks/DriverKit.framework; sourceTree = SDKROOT; }; 3A1693FF2E8087BD000BD368 /* PCIDriverKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PCIDriverKit.framework; path = Platforms/DriverKit.platform/Developer/SDKs/DriverKit25.0.sdk/System/DriverKit/System/Library/Frameworks/PCIDriverKit.framework; sourceTree = DEVELOPER_DIR; }; - 3A27C52E2ECDE045009CA664 /* bump.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = bump.sh; sourceTree = ""; }; 3AB4713F2EE31CF0003A4E2A /* ASFWTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASFWTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3AB471482EE31E7A003A4E2A /* ASFW.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = ASFW.xctestplan; sourceTree = ""; }; 3ABA31122EF8564A0046405D /* AudioDriverKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioDriverKit.framework; path = Platforms/DriverKit.platform/Developer/SDKs/DriverKit25.1.sdk/System/DriverKit/System/Library/Frameworks/AudioDriverKit.framework; sourceTree = DEVELOPER_DIR; }; @@ -129,7 +126,6 @@ 3A1693D92E808727000BD368 /* Products */, ASFWAPPENTITLEMENTS /* App.entitlements */, ASFWDRIVERENTITLEMENTS /* ASFWDriver.entitlements */, - 3A27C52E2ECDE045009CA664 /* bump.sh */, ); sourceTree = ""; }; @@ -283,7 +279,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3A27C5302ECDE045009CA664 /* bump.sh in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -291,7 +286,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3A27C5322ECDE045009CA664 /* bump.sh in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -339,7 +333,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "ls ${SRCROOT}\n$SRCROOT/bump.sh\n"; + shellScript = "\"${SRCROOT}/bump.sh\" refresh\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -584,7 +578,13 @@ DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; - EXCLUDED_SOURCE_FILE_NAMES = "*.md"; + EXCLUDED_SOURCE_FILE_NAMES = ( + "*.md", + "*.bak", + "*.bak2", + "*.sh", + "*.txt", + ); FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SDKROOT)/System/DriverKit/System/Library/Frameworks", @@ -618,7 +618,13 @@ DEVELOPMENT_TEAM = F6YA6B56LR; DRIVERKIT_DEPLOYMENT_TARGET = 25.0; ENABLE_USER_SCRIPT_SANDBOXING = NO; - EXCLUDED_SOURCE_FILE_NAMES = "*.md"; + EXCLUDED_SOURCE_FILE_NAMES = ( + "*.md", + "*.bak", + "*.bak2", + "*.sh", + "*.txt", + ); FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SDKROOT)/System/DriverKit/System/Library/Frameworks", diff --git a/ASFW/ASFWApp.swift b/ASFW/ASFWApp.swift index 236b0b58..c5529967 100644 --- a/ASFW/ASFWApp.swift +++ b/ASFW/ASFWApp.swift @@ -6,11 +6,14 @@ // import SwiftUI + @main struct ASFWApp: App { + private let autoActivateDriverOnLaunch = ProcessInfo.processInfo.arguments.contains("--activate-driver") + var body: some Scene { WindowGroup { - ModernContentView() + ModernContentView(autoActivateDriverOnLaunch: autoActivateDriverOnLaunch) } .defaultSize(width: 1000, height: 700) } diff --git a/ASFW/ASFWDriverConnector.swift b/ASFW/ASFWDriverConnector.swift index 5fc073c7..6331e324 100644 --- a/ASFW/ASFWDriverConnector.swift +++ b/ASFW/ASFWDriverConnector.swift @@ -54,6 +54,20 @@ final class ASFWDriverConnector: ObservableObject { case setIsochTxVerifier = 41 case asyncBlockRead = 44 case asyncBlockWrite = 45 + // SBP2 address space management + case allocateAddressRange = 46 + case deallocateAddressRange = 47 + case readIncomingData = 48 + case writeLocalData = 49 + // SBP-2 session management + case createSBP2Session = 53 + case startSBP2Login = 54 + case getSBP2SessionState = 55 + case submitSBP2Inquiry = 56 + case getSBP2InquiryResult = 57 + case releaseSBP2Session = 58 + case submitSBP2Command = 59 + case getSBP2CommandResult = 60 } // MARK: - Re-exported Models @@ -210,4 +224,4 @@ final class ASFWDriverConnector: ObservableObject { } } - \ No newline at end of file + diff --git a/ASFW/DriverConnector+Discovery.swift b/ASFW/DriverConnector+Discovery.swift index 930fa758..2565ad02 100644 --- a/ASFW/DriverConnector+Discovery.swift +++ b/ASFW/DriverConnector+Discovery.swift @@ -23,11 +23,42 @@ extension ASFWDriverConnector { print("[Connector] 📦 Received \(wireData.count) bytes of wire format data") // Parse wire format - return parseDeviceDiscoveryWire(wireData) + return Self.parseDeviceDiscoveryWire(wireData) } /// Parse wire format data from driver - private func parseDeviceDiscoveryWire(_ data: Data) -> [FWDeviceInfo]? { + static func parseDeviceDiscoveryWire(_ data: Data) -> [FWDeviceInfo]? { + @inline(__always) + func readUInt8(at offset: Int) -> UInt8 { + data[data.startIndex + offset] + } + + @inline(__always) + func readUInt32(at offset: Int) -> UInt32 { + var value: UInt32 = 0 + for i in 0..<4 { + value |= UInt32(data[data.startIndex + offset + i]) << (i * 8) + } + return value + } + + @inline(__always) + func readUInt64(at offset: Int) -> UInt64 { + var value: UInt64 = 0 + for i in 0..<8 { + value |= UInt64(data[data.startIndex + offset + i]) << (i * 8) + } + return value + } + + func readCString(at offset: Int, length: Int) -> String { + let start = data.startIndex + offset + let end = start + length + let bytes = data[start.. SBP2CommandRequest { + SBP2CommandRequest( + cdb: [0x12, 0x00, 0x00, allocationLength, 0x00, 0x00], + direction: .fromTarget, + transferLength: UInt32(allocationLength)) + } + + static func testUnitReady() -> SBP2CommandRequest { + SBP2CommandRequest(cdb: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00], direction: .none) + } + + static func requestSense(allocationLength: UInt8 = 18) -> SBP2CommandRequest { + SBP2CommandRequest( + cdb: [0x03, 0x00, 0x00, allocationLength, 0x00, 0x00], + direction: .fromTarget, + transferLength: UInt32(allocationLength), + captureSenseData: true) + } +} + +struct SBP2CommandResult { + let transportStatus: Int32 + let sbpStatus: UInt8 + let payload: Data + let senseData: Data + + var isSuccess: Bool { + transportStatus == 0 && sbpStatus == 0 + } +} + +extension ASFWDriverConnector { + + private func appendUInt32LE(_ value: UInt32, to data: inout Data) { + var littleEndianValue = value.littleEndian + withUnsafeBytes(of: &littleEndianValue) { rawBuffer in + data.append(contentsOf: rawBuffer) + } + } + + private func appendInt32LE(_ value: Int32, to data: inout Data) { + var littleEndianValue = value.littleEndian + withUnsafeBytes(of: &littleEndianValue) { rawBuffer in + data.append(contentsOf: rawBuffer) + } + } + + private func readUInt32LE(_ data: Data, offset: Int) -> UInt32 { + var value: UInt32 = 0 + for index in 0..<4 { + value |= UInt32(data[data.startIndex + offset + index]) << (index * 8) + } + return value + } + + // MARK: - SBP-2 Address Space Management + + /// Allocate an address range in the driver's SBP-2 address space. + /// - Returns: A handle identifying the allocated range, or nil on failure. + func allocateAddressRange(addressHi: UInt16, + addressLo: UInt32, + length: UInt32) -> UInt64? { + guard isConnected else { + log("allocateAddressRange: Not connected", level: .warning) + return nil + } + + var inputs: [UInt64] = [ + UInt64(addressHi), + UInt64(addressLo), + UInt64(length) + ] + + var output: UInt64 = 0 + var outputCount: UInt32 = 1 + + let kr = inputs.withUnsafeMutableBufferPointer { buffer -> kern_return_t in + IOConnectCallScalarMethod( + connection, + Method.allocateAddressRange.rawValue, + buffer.baseAddress, + UInt32(buffer.count), + &output, + &outputCount) + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "allocateAddressRange failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return nil + } + + log(String(format: "SBP2 address range allocated (handle=0x%llX, len=%u)", output, length), level: .success) + return output + } + + /// Deallocate a previously allocated address range. + /// - Returns: true on success. + func deallocateAddressRange(handle: UInt64) -> Bool { + guard isConnected else { + log("deallocateAddressRange: Not connected", level: .warning) + return false + } + + var inputs: [UInt64] = [handle] + + let kr = inputs.withUnsafeMutableBufferPointer { buffer -> kern_return_t in + IOConnectCallScalarMethod( + connection, + Method.deallocateAddressRange.rawValue, + buffer.baseAddress, + UInt32(buffer.count), + nil, + nil) + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "deallocateAddressRange failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return false + } + + log(String(format: "SBP2 address range deallocated (handle=0x%llX)", handle), level: .success) + return true + } + + /// Read data from an address range that was written by a remote device. + /// - Returns: The data read from the range, or nil on failure. + func readIncomingData(handle: UInt64, + offset: UInt32, + length: UInt32) -> Data? { + guard isConnected else { + log("readIncomingData: Not connected", level: .warning) + return nil + } + + var scalars: [UInt64] = [ + handle, + UInt64(offset), + UInt64(length) + ] + + var outSize: Int = max(Int(length), 1) + var out = Data(count: outSize) + + func doCall() -> kern_return_t { + out.withUnsafeMutableBytes { outPtr in + scalars.withUnsafeMutableBufferPointer { scalarPtr -> kern_return_t in + IOConnectCallMethod( + connection, + Method.readIncomingData.rawValue, + scalarPtr.baseAddress, + UInt32(scalarPtr.count), + nil, + 0, + nil, + nil, + outPtr.baseAddress, + &outSize) + } + } + } + + var kr = doCall() + if kr == kIOReturnNoSpace { + out = Data(count: outSize) + kr = doCall() + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "readIncomingData failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return nil + } + + out.count = outSize + log(String(format: "SBP2 readIncomingData (handle=0x%llX, offset=%u, %zu bytes)", handle, offset, outSize), level: .success) + return out + } + + /// Write data to a local address range for a remote device to read. + /// - Returns: true on success. + func writeLocalData(handle: UInt64, + offset: UInt32, + data: Data) -> Bool { + guard isConnected else { + log("writeLocalData: Not connected", level: .warning) + return false + } + + var scalars: [UInt64] = [ + handle, + UInt64(offset), + UInt64(data.count) + ] + + let kr = data.withUnsafeBytes { dataPtr -> kern_return_t in + scalars.withUnsafeMutableBufferPointer { scalarPtr -> kern_return_t in + IOConnectCallMethod( + connection, + Method.writeLocalData.rawValue, + scalarPtr.baseAddress, + UInt32(scalarPtr.count), + dataPtr.baseAddress, + data.count, + nil, + nil, + nil, + nil) + } + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "writeLocalData failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return false + } + + log(String(format: "SBP2 writeLocalData (handle=0x%llX, offset=%u, %zu bytes)", handle, offset, data.count), level: .success) + return true + } + + // MARK: - SBP-2 Session Management + + /// Create an SBP-2 session for a discovered unit. + /// - Parameters: + /// - guidHi: Upper 32 bits of the device GUID. + /// - guidLo: Lower 32 bits of the device GUID. + /// - romOffset: Config ROM offset of the unit directory. + /// - Returns: Session handle, or nil on failure. + func createSBP2Session(guidHi: UInt32, guidLo: UInt32, romOffset: UInt32) -> UInt64? { + guard isConnected else { + log("createSBP2Session: Not connected", level: .warning) + return nil + } + + var inputs: [UInt64] = [UInt64(guidHi), UInt64(guidLo), UInt64(romOffset)] + var output: UInt64 = 0 + var outputCount: UInt32 = 1 + + let kr = inputs.withUnsafeMutableBufferPointer { buffer -> kern_return_t in + IOConnectCallScalarMethod( + connection, + Method.createSBP2Session.rawValue, + buffer.baseAddress, + UInt32(buffer.count), + &output, + &outputCount) + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "createSBP2Session failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return nil + } + + log(String(format: "SBP2 session created (handle=0x%llX)", output), level: .success) + return output + } + + /// Start SBP-2 login for a session. + /// - Returns: true if login was initiated successfully. + @discardableResult + func startSBP2Login(handle: UInt64) -> Bool { + guard isConnected else { + log("startSBP2Login: Not connected", level: .warning) + return false + } + + var inputs: [UInt64] = [handle] + + let kr = inputs.withUnsafeMutableBufferPointer { buffer -> kern_return_t in + IOConnectCallScalarMethod( + connection, + Method.startSBP2Login.rawValue, + buffer.baseAddress, + UInt32(buffer.count), + nil, + nil) + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "startSBP2Login failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return false + } + + log(String(format: "SBP2 login started (handle=0x%llX)", handle), level: .success) + return true + } + + /// Get the current state of an SBP-2 session. + /// - Returns: Tuple of (loginState, loginID, generation, lastError, reconnectPending). + func getSBP2SessionState(handle: UInt64) -> (loginState: UInt8, loginID: UInt16, generation: UInt16, lastError: Int32, reconnectPending: Bool)? { + guard isConnected else { + log("getSBP2SessionState: Not connected", level: .warning) + return nil + } + + var inputs: [UInt64] = [handle] + var outputs: [UInt64] = [0, 0, 0, 0, 0] + var outputCount: UInt32 = 5 + + let kr = inputs.withUnsafeMutableBufferPointer { inBuf -> kern_return_t in + outputs.withUnsafeMutableBufferPointer { outBuf -> kern_return_t in + IOConnectCallScalarMethod( + connection, + Method.getSBP2SessionState.rawValue, + inBuf.baseAddress, + UInt32(inBuf.count), + outBuf.baseAddress, + &outputCount) + } + } + + guard kr == KERN_SUCCESS else { + return nil + } + + return ( + loginState: UInt8(outputs[0] & 0xFF), + loginID: UInt16(outputs[1] & 0xFFFF), + generation: UInt16(outputs[2] & 0xFFFF), + lastError: Int32(truncatingIfNeeded: outputs[3]), + reconnectPending: outputs[4] != 0 + ) + } + + /// Submit a SCSI INQUIRY command to an SBP-2 session. + /// - Returns: true if inquiry was submitted successfully. + @discardableResult + func submitSBP2Inquiry(handle: UInt64, allocationLength: UInt8 = 96) -> Bool { + submitSBP2Command(handle: handle, request: .inquiry(allocationLength: allocationLength)) + } + + @discardableResult + func submitSBP2Command(handle: UInt64, request: SBP2CommandRequest) -> Bool { + guard isConnected else { + log("submitSBP2Command: Not connected", level: .warning) + return false + } + + var payload = Data() + appendUInt32LE(UInt32(request.cdb.count), to: &payload) + appendUInt32LE(request.transferLength, to: &payload) + appendUInt32LE(UInt32(request.outgoingData.count), to: &payload) + appendUInt32LE(request.timeoutMs, to: &payload) + payload.append(request.direction.rawValue) + payload.append(request.captureSenseData ? 1 : 0) + payload.append(contentsOf: [0, 0]) + payload.append(contentsOf: request.cdb) + payload.append(request.outgoingData) + + var scalars: [UInt64] = [handle] + + let kr = payload.withUnsafeBytes { inputPtr in + scalars.withUnsafeMutableBufferPointer { scalarBuffer -> kern_return_t in + IOConnectCallMethod( + connection, + Method.submitSBP2Command.rawValue, + scalarBuffer.baseAddress, + UInt32(scalarBuffer.count), + inputPtr.baseAddress, + payload.count, + nil, + nil, + nil, + nil) + } + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "submitSBP2Command failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return false + } + + log(String(format: "SBP2 command submitted (handle=0x%llX, cdb=%02X, dir=%u, xfer=%u)", + handle, request.cdb.first ?? 0, request.direction.rawValue, request.transferLength), + level: .success) + return true + } + + /// Get the result of a completed INQUIRY command (destructive read). + /// - Returns: Raw INQUIRY data, or nil if not ready. + func getSBP2InquiryResult(handle: UInt64) -> Data? { + guard let result = getSBP2CommandResult(handle: handle), result.isSuccess else { + return nil + } + + let out = result.payload + + if out.count >= 36 { + let vendor = String(data: out[8..<16], encoding: .ascii)?.trimmingCharacters(in: .controlCharacters) ?? "???" + let product = String(data: out[16..<32], encoding: .ascii)?.trimmingCharacters(in: .controlCharacters) ?? "???" + let revision = String(data: out[32..<36], encoding: .ascii)?.trimmingCharacters(in: .controlCharacters) ?? "???" + log(String(format: "SBP2 INQUIRY result: %@ %@ (rev %@, %zu bytes)", vendor, product, revision, out.count), level: .success) + } + + return out + } + + func getSBP2CommandResult(handle: UInt64) -> SBP2CommandResult? { + guard isConnected else { + log("getSBP2CommandResult: Not connected", level: .warning) + return nil + } + + var scalars: [UInt64] = [handle] + var outSize: Int = 512 + var out = Data(count: outSize) + + func doCall() -> kern_return_t { + out.withUnsafeMutableBytes { outPtr in + scalars.withUnsafeMutableBufferPointer { scalarPtr -> kern_return_t in + IOConnectCallMethod( + connection, + Method.getSBP2CommandResult.rawValue, + scalarPtr.baseAddress, + UInt32(scalarPtr.count), + nil, + 0, + nil, + nil, + outPtr.baseAddress, + &outSize) + } + } + } + + var kr = doCall() + if kr == kIOReturnNoSpace { + out = Data(count: outSize) + kr = doCall() + } + + guard kr == KERN_SUCCESS else { + return nil + } + + out.count = outSize + guard out.count >= 16 else { + return nil + } + + let transportStatus = Int32(bitPattern: readUInt32LE(out, offset: 0)) + let sbpStatus = out[out.startIndex + 4] + let payloadLength = Int(readUInt32LE(out, offset: 8)) + let senseLength = Int(readUInt32LE(out, offset: 12)) + let payloadStart = 16 + let payloadEnd = payloadStart + payloadLength + let senseEnd = payloadEnd + senseLength + guard senseEnd <= out.count else { + return nil + } + + let payload = out.subdata(in: payloadStart.. Bool { + guard isConnected else { + log("releaseSBP2Session: Not connected", level: .warning) + return false + } + + var inputs: [UInt64] = [handle] + + let kr = inputs.withUnsafeMutableBufferPointer { buffer -> kern_return_t in + IOConnectCallScalarMethod( + connection, + Method.releaseSBP2Session.rawValue, + buffer.baseAddress, + UInt32(buffer.count), + nil, + nil) + } + + guard kr == KERN_SUCCESS else { + let errorMsg = "releaseSBP2Session failed: \(interpretIOReturn(kr))" + log(errorMsg, level: .error) + lastError = errorMsg + return false + } + + log(String(format: "SBP2 session released (handle=0x%llX)", handle), level: .success) + return true + } +} diff --git a/ASFW/DriverInstallManager.swift b/ASFW/DriverInstallManager.swift index 7acffcb1..562f9e4f 100644 --- a/ASFW/DriverInstallManager.swift +++ b/ASFW/DriverInstallManager.swift @@ -14,7 +14,6 @@ final class DriverInstallManager: NSObject, OSSystemExtensionRequestDelegate { func activate(completion: @escaping (Result) -> Void) { submit(kind: .activation, request: OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: extensionIdentifier, queue: .main), completion: completion) - logBundleScan() } func deactivate(completion: @escaping (Result) -> Void) { @@ -25,6 +24,7 @@ final class DriverInstallManager: NSObject, OSSystemExtensionRequestDelegate { currentOp = kind request.delegate = self self.completion = completion + logBundleScan() OSSystemExtensionManager.shared.submitRequest(request) } @@ -36,7 +36,7 @@ final class DriverInstallManager: NSObject, OSSystemExtensionRequestDelegate { } func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) { - completion?(.failure(error)) + completion?(.failure(describe(error: error))) completion = nil } @@ -48,6 +48,50 @@ final class DriverInstallManager: NSObject, OSSystemExtensionRequestDelegate { return .replace } + private func describe(error: Error) -> NSError { + let nsError = error as NSError + guard nsError.domain == OSSystemExtensionErrorDomain, + let code = OSSystemExtensionError.Code(rawValue: nsError.code) else { + return nsError + } + + let message: String + switch code { + case .unknown: + message = "Unknown system extension error" + case .missingEntitlement: + message = "Missing required system extension entitlement" + case .unsupportedParentBundleLocation: + message = "App must be launched from a supported bundle location" + case .extensionNotFound: + message = "System extension not found inside the running app bundle" + case .extensionMissingIdentifier: + message = "System extension bundle identifier is missing" + case .duplicateExtensionIdentifer: + message = "Duplicate system extension identifier found in app bundle" + case .unknownExtensionCategory: + message = "Unknown system extension category" + case .codeSignatureInvalid: + message = "System extension code signature is invalid" + case .validationFailed: + message = "System extension validation failed" + case .forbiddenBySystemPolicy: + message = "System policy blocked the system extension" + case .requestCanceled: + message = "System extension request was canceled" + case .requestSuperseded: + message = "System extension request was superseded by a newer request" + case .authorizationRequired: + message = "System extension authorization is required" + @unknown default: + message = nsError.localizedDescription + } + + return NSError(domain: nsError.domain, + code: nsError.code, + userInfo: [NSLocalizedDescriptionKey: "\(message) (\(nsError.domain) error \(nsError.code))"]) + } + private func logBundleScan() { let fm = FileManager.default let appBundleURL = Bundle.main.bundleURL diff --git a/ASFW/Models/DriverConnectorModels.swift b/ASFW/Models/DriverConnectorModels.swift index de70b5cb..c23879a4 100644 --- a/ASFW/Models/DriverConnectorModels.swift +++ b/ASFW/Models/DriverConnectorModels.swift @@ -514,8 +514,13 @@ struct DriverConnectorFWDeviceInfo: Identifiable { let generation: UInt32 let state: DriverConnectorFWDeviceState let units: [DriverConnectorFWUnitInfo] + let deviceKind: UInt8 // DeviceKind enum value from driver var stateString: String { state.description } + var isStorage: Bool { deviceKind == 4 } // DeviceKind::Storage = 4 + var hasSBP2Unit: Bool { units.contains(where: \.isSBP2Unit) } + var sbp2Units: [DriverConnectorFWUnitInfo] { units.filter(\.isSBP2Unit) } + var storageUnits: [DriverConnectorFWUnitInfo] { sbp2Units } } struct DriverConnectorFWUnitInfo: Identifiable { @@ -524,12 +529,23 @@ struct DriverConnectorFWUnitInfo: Identifiable { let swVersion: UInt32 let state: DriverConnectorFWUnitState let romOffset: UInt32 + let managementAgentOffset: UInt32? + let lun: UInt32? + let unitCharacteristics: UInt32? + let fastStart: UInt32? let vendorName: String? let productName: String? + private static let sbp2SpecId: UInt32 = 0x00609E + private static let sbp2SwVersion: UInt32 = 0x010483 + var specIdHex: String { String(format: "0x%06X", specId) } var swVersionHex: String { String(format: "0x%06X", swVersion) } var stateString: String { state.description } + var isSBP2Unit: Bool { + specId == Self.sbp2SpecId && swVersion == Self.sbp2SwVersion + } + var isSBP2Storage: Bool { isSBP2Unit } } // API compatibility aliases (keep existing public names and nested access stable) diff --git a/ASFW/ViewModels/DebugViewModel.swift b/ASFW/ViewModels/DebugViewModel.swift index 331bedd2..b21387e5 100644 --- a/ASFW/ViewModels/DebugViewModel.swift +++ b/ASFW/ViewModels/DebugViewModel.swift @@ -75,8 +75,8 @@ class DebugViewModel: ObservableObject { .store(in: &cancellables) } - func connect() { - connector.connect(forceAttempt: false) + func connect(forceAttempt: Bool = false) { + connector.connect(forceAttempt: forceAttempt) } func disconnect() { @@ -154,9 +154,7 @@ class DebugViewModel: ObservableObject { } func getSubunitCapabilities(guid: UInt64, type: UInt8, id: UInt8) async -> ASFWDriverConnector.AVCMusicCapabilities? { - return await Task.detached { - return self.connector.getSubunitCapabilities(guid: guid, type: type, id: id) - }.value + return await self.connector.getSubunitCapabilities(guid: guid, type: type, id: id) } diff --git a/ASFW/ViewModels/DriverViewModel.swift b/ASFW/ViewModels/DriverViewModel.swift index ad2c34db..cb1faf04 100644 --- a/ASFW/ViewModels/DriverViewModel.swift +++ b/ASFW/ViewModels/DriverViewModel.swift @@ -68,7 +68,7 @@ class DriverViewModel: ObservableObject { } } - func installDriver() { + func installDriver(completion: ((Result) -> Void)? = nil) { isBusy = true activationStatus = "Requesting activation..." log("Activation request submitted", source: .app, level: .info) @@ -85,6 +85,7 @@ class DriverViewModel: ObservableObject { self.activationStatus = "Error: \(error.localizedDescription)" self.log(error.localizedDescription, source: .app, level: .error) } + completion?(result) } } } diff --git a/ASFW/ViewModels/SBP2DebugViewModel.swift b/ASFW/ViewModels/SBP2DebugViewModel.swift new file mode 100644 index 00000000..49ee93bb --- /dev/null +++ b/ASFW/ViewModels/SBP2DebugViewModel.swift @@ -0,0 +1,458 @@ +import Foundation +import Combine + +@MainActor +final class SBP2DebugViewModel: ObservableObject { + struct SessionSnapshot: Equatable { + let loginState: UInt8 + let loginID: UInt16 + let generation: UInt16 + let lastError: Int32 + let reconnectPending: Bool + + var loginStateDescription: String { + switch loginState { + case 0: return "Idle" + case 1: return "LoggingIn" + case 2: return "LoggedIn" + case 3: return "Reconnecting" + case 4: return "LoggingOut" + case 5: return "Suspended" + case 6: return "Failed" + default: return "Unknown(\(loginState))" + } + } + + var isLoggedIn: Bool { loginState == 2 } + } + + struct InquirySummary: Equatable { + let vendor: String + let product: String + let revision: String + } + + struct SenseSummary: Equatable { + let senseKey: UInt8 + let asc: UInt8 + let ascq: UInt8 + } + + @Published var isConnected: Bool = false + @Published var isLoadingDevices: Bool = false + @Published var isBusy: Bool = false + @Published var sbp2Devices: [ASFWDriverConnector.FWDeviceInfo] = [] + @Published var selectedDeviceID: UInt64? { + didSet { + guard selectedDeviceID != oldValue else { return } + selectedUnitROMOffset = selectedDevice?.sbp2Units.first?.romOffset + clearSessionState(releaseSession: true) + } + } + @Published var selectedUnitROMOffset: UInt32? { + didSet { + guard selectedUnitROMOffset != oldValue else { return } + clearSessionState(releaseSession: true) + } + } + @Published var sessionHandle: UInt64? + @Published var sessionState: SessionSnapshot? + @Published var commandResult: SBP2CommandResult? + @Published var lastCommandName: String? + @Published var inquirySummary: InquirySummary? + @Published var senseSummary: SenseSummary? + @Published var rawCDBHex: String = "12 00 00 60 00 00" + @Published var rawDirection: SBP2CommandDataDirection = .fromTarget + @Published var rawTransferLength: String = "96" + @Published var rawOutgoingHex: String = "" + @Published var statusMessage: String? + @Published var errorMessage: String? + @Published var lastDeviceRefresh: Date? + @Published var lastStateRefresh: Date? + + private let connector: ASFWDriverConnector + private let workerQueue = DispatchQueue(label: "net.mrmidi.ASFW.sbp2.debug", qos: .userInitiated) + private var cancellables = Set() + private var stateTimer: Timer? + + init(connector: ASFWDriverConnector) { + self.connector = connector + isConnected = connector.isConnected + + connector.$isConnected + .receive(on: DispatchQueue.main) + .sink { [weak self] connected in + guard let self else { return } + self.isConnected = connected + if connected { + self.refreshDevices() + } else { + self.handleDisconnect() + } + } + .store(in: &cancellables) + } + + var selectedDevice: ASFWDriverConnector.FWDeviceInfo? { + guard let selectedDeviceID else { return nil } + return sbp2Devices.first(where: { $0.guid == selectedDeviceID }) + } + + var selectedUnit: ASFWDriverConnector.FWUnitInfo? { + guard let selectedUnitROMOffset else { return nil } + return selectedDevice?.sbp2Units.first(where: { $0.romOffset == selectedUnitROMOffset }) + } + + var hasSelection: Bool { + selectedDevice != nil && selectedUnit != nil + } + + func refreshDevices(preferredDeviceID: UInt64? = nil) { + guard connector.isConnected else { + handleDisconnect() + return + } + + isLoadingDevices = true + errorMessage = nil + + workerQueue.async { [weak self] in + guard let self else { return } + let devices = self.connector.getDiscoveredDevices()?.filter(\.hasSBP2Unit) ?? [] + + DispatchQueue.main.async { + self.isLoadingDevices = false + self.sbp2Devices = devices + self.lastDeviceRefresh = Date() + + let targetDeviceID = preferredDeviceID ?? self.selectedDeviceID + if let targetDeviceID, devices.contains(where: { $0.guid == targetDeviceID }) { + self.selectedDeviceID = targetDeviceID + } else { + self.selectedDeviceID = devices.first?.guid + } + + if let selectedDevice = self.selectedDevice { + let units = selectedDevice.sbp2Units + if let romOffset = self.selectedUnitROMOffset, + units.contains(where: { $0.romOffset == romOffset }) { + self.selectedUnitROMOffset = romOffset + } else { + self.selectedUnitROMOffset = units.first?.romOffset + } + } else { + self.selectedUnitROMOffset = nil + } + + self.statusMessage = devices.isEmpty + ? "No SBP-2 devices discovered." + : "Found \(devices.count) SBP-2 device\(devices.count == 1 ? "" : "s")." + } + } + } + + func openDevice(_ device: ASFWDriverConnector.FWDeviceInfo) { + refreshDevices(preferredDeviceID: device.guid) + } + + func createSession() { + guard let device = selectedDevice, let unit = selectedUnit else { + errorMessage = "Select an SBP-2 device first." + return + } + + clearSessionState(releaseSession: true) + isBusy = true + errorMessage = nil + statusMessage = "Creating SBP-2 session..." + + let guidHi = UInt32((device.guid >> 32) & 0xFFFF_FFFF) + let guidLo = UInt32(device.guid & 0xFFFF_FFFF) + let romOffset = unit.romOffset + + workerQueue.async { [weak self] in + guard let self else { return } + let handle = self.connector.createSBP2Session(guidHi: guidHi, guidLo: guidLo, romOffset: romOffset) + + DispatchQueue.main.async { + self.isBusy = false + guard let handle else { + self.errorMessage = self.connector.lastError ?? "Failed to create SBP-2 session." + return + } + + self.sessionHandle = handle + self.statusMessage = String(format: "Session created (handle=0x%llX).", handle) + self.refreshSessionState() + self.startStatePolling() + } + } + } + + func startLogin() { + guard let handle = sessionHandle else { + errorMessage = "Create a session before starting login." + return + } + + isBusy = true + errorMessage = nil + statusMessage = "Starting SBP-2 login..." + + workerQueue.async { [weak self] in + guard let self else { return } + let ok = self.connector.startSBP2Login(handle: handle) + + DispatchQueue.main.async { + self.isBusy = false + if ok { + self.statusMessage = "Login started. Polling session state..." + self.refreshSessionState() + self.startStatePolling() + } else { + self.errorMessage = self.connector.lastError ?? "Failed to start SBP-2 login." + } + } + } + } + + func refreshSessionState() { + guard let handle = sessionHandle else { return } + + workerQueue.async { [weak self] in + guard let self else { return } + let state = self.connector.getSBP2SessionState(handle: handle) + + DispatchQueue.main.async { + guard let state else { + self.errorMessage = self.connector.lastError ?? "Failed to read session state." + return + } + + self.sessionState = SessionSnapshot( + loginState: state.loginState, + loginID: state.loginID, + generation: state.generation, + lastError: state.lastError, + reconnectPending: state.reconnectPending) + self.lastStateRefresh = Date() + } + } + } + + func runInquiry() { + submitCommand(.inquiry(), label: "INQUIRY") + } + + func runTestUnitReady() { + submitCommand(.testUnitReady(), label: "TEST UNIT READY") + } + + func runRequestSense() { + submitCommand(.requestSense(), label: "REQUEST SENSE") + } + + func runRawCommand() { + guard let cdb = Self.parseHexBytes(rawCDBHex), !cdb.isEmpty else { + errorMessage = "Raw CDB must contain at least one byte." + return + } + + let transferLength = UInt32(rawTransferLength.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0 + let outgoingData: Data + if rawDirection == .toTarget { + guard let bytes = Self.parseHexBytes(rawOutgoingHex) else { + errorMessage = "Outgoing payload is not valid hex." + return + } + outgoingData = Data(bytes) + } else { + outgoingData = Data() + } + + let request = SBP2CommandRequest( + cdb: cdb, + direction: rawDirection, + transferLength: transferLength, + outgoingData: outgoingData) + submitCommand(request, label: "RAW CDB") + } + + func releaseSession() { + clearSessionState(releaseSession: true) + } + + private func submitCommand(_ request: SBP2CommandRequest, label: String) { + guard let handle = sessionHandle else { + errorMessage = "Create a session before submitting commands." + return + } + guard sessionState?.isLoggedIn == true else { + errorMessage = "Log in to the SBP-2 session before submitting commands." + return + } + + isBusy = true + errorMessage = nil + statusMessage = "Submitting \(label)..." + commandResult = nil + lastCommandName = label + + workerQueue.async { [weak self] in + guard let self else { return } + let ok = self.connector.submitSBP2Command(handle: handle, request: request) + + DispatchQueue.main.async { + self.isBusy = false + if ok { + self.statusMessage = "\(label) submitted. Waiting for result..." + self.pollCommandResult(handle: handle, label: label, remainingAttempts: 15) + } else { + self.errorMessage = self.connector.lastError ?? "Failed to submit \(label)." + } + } + } + } + + private func pollCommandResult(handle: UInt64, label: String, remainingAttempts: Int) { + guard remainingAttempts > 0 else { + errorMessage = "Timed out waiting for \(label) result." + return + } + + workerQueue.asyncAfter(deadline: .now() + 0.4) { [weak self] in + guard let self else { return } + let result = self.connector.getSBP2CommandResult(handle: handle) + + DispatchQueue.main.async { + if let result { + self.applyCommandResult(result, label: label) + return + } + + self.pollCommandResult(handle: handle, label: label, remainingAttempts: remainingAttempts - 1) + } + } + } + + private func applyCommandResult(_ result: SBP2CommandResult, label: String) { + commandResult = result + lastCommandName = label + + if label == "INQUIRY", result.isSuccess { + inquirySummary = Self.parseInquirySummary(result.payload) + } + if label == "REQUEST SENSE", result.isSuccess { + senseSummary = Self.parseSenseSummary(result.senseData.isEmpty ? result.payload : result.senseData) + } + + if result.isSuccess { + statusMessage = "\(label) completed." + } else { + errorMessage = "\(label) failed (transport=\(result.transportStatus), sbp=\(result.sbpStatus))." + } + } + + private func startStatePolling() { + stopStatePolling() + + stateTimer = Timer.scheduledTimer(withTimeInterval: 0.8, repeats: true) { [weak self] _ in + Task { @MainActor [weak self] in + guard let self else { return } + guard self.sessionHandle != nil else { + self.stopStatePolling() + return + } + self.refreshSessionState() + } + } + } + + private func stopStatePolling() { + stateTimer?.invalidate() + stateTimer = nil + } + + private func clearSessionState(releaseSession: Bool) { + let handle = sessionHandle + stopStatePolling() + + if releaseSession, let handle { + workerQueue.async { [weak self] in + guard let self else { return } + _ = self.connector.releaseSBP2Session(handle: handle) + } + } + + sessionHandle = nil + sessionState = nil + commandResult = nil + lastCommandName = nil + inquirySummary = nil + senseSummary = nil + lastStateRefresh = nil + } + + private func handleDisconnect() { + stopStatePolling() + sbp2Devices = [] + selectedDeviceID = nil + selectedUnitROMOffset = nil + sessionHandle = nil + sessionState = nil + commandResult = nil + lastCommandName = nil + inquirySummary = nil + senseSummary = nil + errorMessage = nil + statusMessage = "Driver not connected." + } + + private static func parseInquirySummary(_ data: Data) -> InquirySummary? { + guard data.count >= 36 else { return nil } + + func readASCII(_ range: Range) -> String { + let slice = data[range] + return String(decoding: slice, as: UTF8.self) + .trimmingCharacters(in: .whitespacesAndNewlines.union(.controlCharacters)) + } + + return InquirySummary( + vendor: readASCII(8..<16), + product: readASCII(16..<32), + revision: readASCII(32..<36)) + } + + private static func parseSenseSummary(_ data: Data) -> SenseSummary? { + guard data.count >= 14 else { return nil } + return SenseSummary( + senseKey: data[data.startIndex + 2] & 0x0F, + asc: data[data.startIndex + 12], + ascq: data[data.startIndex + 13]) + } + + private static func parseHexBytes(_ text: String) -> [UInt8]? { + let sanitized = text + .replacingOccurrences(of: ",", with: " ") + .replacingOccurrences(of: "\n", with: " ") + .split(separator: " ") + .map(String.init) + + if sanitized.isEmpty { + return [] + } + + var bytes: [UInt8] = [] + bytes.reserveCapacity(sanitized.count) + for token in sanitized { + let normalized = token.hasPrefix("0x") || token.hasPrefix("0X") + ? String(token.dropFirst(2)) + : token + guard let value = UInt8(normalized, radix: 16) else { + return nil + } + bytes.append(value) + } + return bytes + } +} diff --git a/ASFW/Views/DeviceDiscoveryView.swift b/ASFW/Views/DeviceDiscoveryView.swift index ae9b095f..2185e588 100644 --- a/ASFW/Views/DeviceDiscoveryView.swift +++ b/ASFW/Views/DeviceDiscoveryView.swift @@ -9,6 +9,7 @@ import SwiftUI struct DeviceDiscoveryView: View { @ObservedObject var viewModel: DebugViewModel + var onOpenSBP2Device: ((ASFWDriverConnector.FWDeviceInfo) -> Void)? = nil @State private var devices: [ASFWDriverConnector.FWDeviceInfo] = [] @State private var selectedDeviceId: UInt64? @State private var autoRefreshEnabled = true @@ -71,7 +72,7 @@ struct DeviceDiscoveryView: View { // Right: Device details if let selectedDevice = devices.first(where: { $0.id == selectedDeviceId }) { - DeviceDetailView(device: selectedDevice) + DeviceDetailView(device: selectedDevice, onOpenSBP2Device: onOpenSBP2Device) } else { ContentUnavailableView( "Select a Device", @@ -160,6 +161,7 @@ struct DeviceRowView: View { struct DeviceDetailView: View { let device: ASFWDriverConnector.FWDeviceInfo + var onOpenSBP2Device: ((ASFWDriverConnector.FWDeviceInfo) -> Void)? = nil var body: some View { ScrollView { @@ -207,10 +209,33 @@ struct DeviceDetailView: View { Text(String(format: "0x%06X", device.modelId)) .monospaced() } + GridRow { + Text("Kind:") + .fontWeight(.medium) + Text(device.hasSBP2Unit ? "SBP-2 Device" : (device.isStorage ? "Storage" : "Other")) + } } .padding() } + if device.hasSBP2Unit, let onOpenSBP2Device { + GroupBox("SBP-2 Debug") { + VStack(alignment: .leading, spacing: 8) { + Text("This device exposes at least one SBP-2 unit.") + .foregroundStyle(.secondary) + + Button { + onOpenSBP2Device(device) + } label: { + Label("Open SBP-2 Debug", systemImage: "externaldrive.badge.questionmark") + } + .buttonStyle(.borderedProminent) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } + // Units Section if !device.units.isEmpty { GroupBox("Unit Directories") { @@ -270,6 +295,38 @@ struct UnitCardView: View { Text(String(format: "%d quadlets", unit.romOffset)) .monospaced() } + if let managementAgentOffset = unit.managementAgentOffset { + GridRow { + Text("Mgmt Agent:") + .foregroundStyle(.secondary) + Text(String(format: "0x%08X", managementAgentOffset)) + .monospaced() + } + } + if let lun = unit.lun { + GridRow { + Text("LUN:") + .foregroundStyle(.secondary) + Text(String(format: "0x%02X", lun)) + .monospaced() + } + } + if let unitCharacteristics = unit.unitCharacteristics { + GridRow { + Text("Unit Chars:") + .foregroundStyle(.secondary) + Text(String(format: "0x%08X", unitCharacteristics)) + .monospaced() + } + } + if let fastStart = unit.fastStart { + GridRow { + Text("Fast Start:") + .foregroundStyle(.secondary) + Text(String(format: "0x%08X", fastStart)) + .monospaced() + } + } if let vendorName = unit.vendorName, !vendorName.isEmpty { GridRow { diff --git a/ASFW/Views/MetricsView.swift b/ASFW/Views/MetricsView.swift index 690c39f5..2301ebb7 100644 --- a/ASFW/Views/MetricsView.swift +++ b/ASFW/Views/MetricsView.swift @@ -29,8 +29,9 @@ class MetricsViewModel: ObservableObject { func startPolling() { timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + guard let self else { return } Task { @MainActor in - self?.fetchMetrics() + self.fetchMetrics() } } } diff --git a/ASFW/Views/ModernContentView.swift b/ASFW/Views/ModernContentView.swift index a99dc978..5c0c8f7a 100644 --- a/ASFW/Views/ModernContentView.swift +++ b/ASFW/Views/ModernContentView.swift @@ -9,19 +9,25 @@ import SwiftUI import Foundation struct ModernContentView: View { + let autoActivateDriverOnLaunch: Bool @StateObject private var driverVM = DriverViewModel() @StateObject private var debugVM = DebugViewModel() + @StateObject private var sbp2DebugVM: SBP2DebugViewModel @StateObject private var topologyVM: TopologyViewModel @StateObject private var romExplorerVM: RomExplorerViewModel @State private var selectedSection: SidebarSection? = .overview @State private var loggingPreset: LoggingPreset = .standard + @State private var didTriggerAutoDriverActivation = false - init() { + init(autoActivateDriverOnLaunch: Bool = false) { + self.autoActivateDriverOnLaunch = autoActivateDriverOnLaunch let driverViewModel = DriverViewModel() let debugViewModel = DebugViewModel() + let sbp2ViewModel = SBP2DebugViewModel(connector: debugViewModel.connector) let topologyViewModel = TopologyViewModel(connector: debugViewModel.connector) _driverVM = StateObject(wrappedValue: driverViewModel) _debugVM = StateObject(wrappedValue: debugViewModel) + _sbp2DebugVM = StateObject(wrappedValue: sbp2ViewModel) _topologyVM = StateObject(wrappedValue: topologyViewModel) _romExplorerVM = StateObject(wrappedValue: RomExplorerViewModel( connector: debugViewModel.connector, @@ -32,6 +38,7 @@ struct ModernContentView: View { enum SidebarSection: String, CaseIterable, Identifiable { case overview = "Overview" case devices = "Device Discovery" + case sbp2 = "SBP-2 Debug" case avcUnits = "AV/C Units" case avcCommands = "AV/C Commands" case ping = "Ping" @@ -53,6 +60,7 @@ struct ModernContentView: View { switch self { case .overview: return "info.circle" case .devices: return "externaldrive.connected.to.line.below" + case .sbp2: return "externaldrive.badge.questionmark" case .avcUnits: return "music.note" case .avcCommands: return "command" case .ping: return "waveform.path" @@ -90,7 +98,12 @@ struct ModernContentView: View { case .overview: OverviewView(viewModel: driverVM) case .devices: - DeviceDiscoveryView(viewModel: debugVM) + DeviceDiscoveryView(viewModel: debugVM) { device in + sbp2DebugVM.openDevice(device) + selectedSection = .sbp2 + } + case .sbp2: + SBP2DebugView(viewModel: sbp2DebugVM) case .avcUnits: AVCDebugView(viewModel: debugVM) case .avcCommands: @@ -133,7 +146,7 @@ struct ModernContentView: View { } Button { - driverVM.installDriver() + reinstallDriverAndReconnect() } label: { Label("Install", systemImage: "arrow.down.circle.fill") } @@ -167,10 +180,15 @@ struct ModernContentView: View { } .onAppear { debugVM.setDriverViewModel(driverVM) - debugVM.connect() topologyVM.startAutoRefresh() romExplorerVM.setConnector(debugVM.connector, topologyViewModel: topologyVM) loadLoggingPreset() + if autoActivateDriverOnLaunch && !didTriggerAutoDriverActivation { + didTriggerAutoDriverActivation = true + reinstallDriverAndReconnect() + } else { + debugVM.connect() + } } .onDisappear { debugVM.disconnect() @@ -226,6 +244,18 @@ struct ModernContentView: View { } } } + + private func reinstallDriverAndReconnect() { + debugVM.disconnect() + driverVM.driverVersion = nil + + driverVM.installDriver { _ in + Task { @MainActor in + try? await Task.sleep(nanoseconds: 1_500_000_000) + debugVM.connect(forceAttempt: true) + } + } + } } struct AsyncCommandView: View { diff --git a/ASFW/Views/OverviewView.swift b/ASFW/Views/OverviewView.swift index b7e6d64e..d27b829f 100644 --- a/ASFW/Views/OverviewView.swift +++ b/ASFW/Views/OverviewView.swift @@ -43,6 +43,7 @@ struct OverviewView: View { statusIndicator Text(viewModel.activationStatus) .font(.system(.body, design: .monospaced)) + .textSelection(.enabled) Spacer() } .padding() diff --git a/ASFW/Views/SBP2DebugView.swift b/ASFW/Views/SBP2DebugView.swift new file mode 100644 index 00000000..daea31f2 --- /dev/null +++ b/ASFW/Views/SBP2DebugView.swift @@ -0,0 +1,491 @@ +import SwiftUI + +struct SBP2DebugView: View { + @ObservedObject var viewModel: SBP2DebugViewModel + + var body: some View { + VStack(spacing: 0) { + header + Divider() + content + } + .navigationTitle("SBP-2 Debug") + .onAppear { + viewModel.refreshDevices() + } + } + + private var header: some View { + HStack { + Text("SBP-2 Debug") + .font(.title2.bold()) + + Spacer() + + if let lastRefresh = viewModel.lastDeviceRefresh { + Text("Devices: \(lastRefresh, style: .time)") + .font(.caption) + .foregroundStyle(.secondary) + } + + if let lastStateRefresh = viewModel.lastStateRefresh { + Text("State: \(lastStateRefresh, style: .time)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Button { + viewModel.refreshDevices() + } label: { + Label("Refresh", systemImage: "arrow.clockwise") + } + .buttonStyle(.borderedProminent) + .disabled(!viewModel.isConnected || viewModel.isLoadingDevices) + } + .padding() + } + + @ViewBuilder + private var content: some View { + if !viewModel.isConnected { + ContentUnavailableView( + "Driver Not Connected", + systemImage: "cable.connector.slash", + description: Text("Connect to the driver to debug SBP-2 sessions.") + ) + } else if viewModel.sbp2Devices.isEmpty && !viewModel.isLoadingDevices { + ContentUnavailableView( + "No SBP-2 Devices", + systemImage: "externaldrive.badge.questionmark", + description: Text("Refresh after attaching a FireWire SBP-2 target.") + ) + } else { + HSplitView { + List(viewModel.sbp2Devices, selection: $viewModel.selectedDeviceID) { device in + SBP2DeviceRow(device: device) + .tag(device.guid) + } + .frame(minWidth: 240, idealWidth: 280, maxWidth: 320) + + if let device = viewModel.selectedDevice, let unit = viewModel.selectedUnit { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + devicePanel(device: device, unit: unit) + sessionPanel + probePanel + rawCommandPanel + resultPanel + statusPanel + } + .padding() + } + } else { + ContentUnavailableView( + "Select an SBP-2 Device", + systemImage: "sidebar.left", + description: Text("Choose an SBP-2 device and unit to create a session.") + ) + } + } + } + } + + private func devicePanel(device: ASFWDriverConnector.FWDeviceInfo, + unit: ASFWDriverConnector.FWUnitInfo) -> some View { + GroupBox("Selected Device") { + VStack(alignment: .leading, spacing: 12) { + Text(deviceTitle(device)) + .font(.title3.weight(.semibold)) + + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { + GridRow { + Text("GUID:") + .foregroundStyle(.secondary) + Text(String(format: "0x%016llX", device.guid)) + .monospaced() + } + GridRow { + Text("Node:") + .foregroundStyle(.secondary) + Text(String(format: "%u", device.nodeId)) + .monospaced() + } + GridRow { + Text("Generation:") + .foregroundStyle(.secondary) + Text(String(format: "%u", device.generation)) + .monospaced() + } + GridRow { + Text("Units:") + .foregroundStyle(.secondary) + Text("\(device.sbp2Units.count)") + } + } + + Picker("SBP-2 Unit", selection: $viewModel.selectedUnitROMOffset) { + ForEach(device.sbp2Units) { listedUnit in + Text(unitLabel(listedUnit)) + .tag(Optional(listedUnit.romOffset)) + } + } + .pickerStyle(.menu) + .frame(maxWidth: 420, alignment: .leading) + + Divider() + + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { + GridRow { + Text("ROM Offset:") + .foregroundStyle(.secondary) + Text(String(format: "%u", unit.romOffset)) + .monospaced() + } + GridRow { + Text("Spec ID:") + .foregroundStyle(.secondary) + Text(unit.specIdHex) + .monospaced() + } + GridRow { + Text("LUN:") + .foregroundStyle(.secondary) + Text(unit.lun.map { String(format: "0x%02X", $0) } ?? "n/a") + .monospaced() + } + GridRow { + Text("Mgmt Agent:") + .foregroundStyle(.secondary) + Text(unit.managementAgentOffset.map { String(format: "0x%08X", $0) } ?? "n/a") + .monospaced() + } + GridRow { + Text("Unit Chars:") + .foregroundStyle(.secondary) + Text(unit.unitCharacteristics.map { String(format: "0x%08X", $0) } ?? "n/a") + .monospaced() + } + GridRow { + Text("Fast Start:") + .foregroundStyle(.secondary) + Text(unit.fastStart.map { String(format: "0x%08X", $0) } ?? "n/a") + .monospaced() + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } + + private var sessionPanel: some View { + GroupBox("Session") { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 12) { + Button("Create Session") { + viewModel.createSession() + } + .buttonStyle(.borderedProminent) + .disabled(!viewModel.hasSelection || viewModel.isBusy) + + Button("Start Login") { + viewModel.startLogin() + } + .buttonStyle(.bordered) + .disabled(viewModel.sessionHandle == nil || viewModel.isBusy) + + Button("Release") { + viewModel.releaseSession() + } + .buttonStyle(.bordered) + .disabled(viewModel.sessionHandle == nil) + } + + if let handle = viewModel.sessionHandle { + Text(String(format: "Handle: 0x%llX", handle)) + .font(.callout) + .monospaced() + } + + if let state = viewModel.sessionState { + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { + GridRow { + Text("State:") + .foregroundStyle(.secondary) + Text(state.loginStateDescription) + } + GridRow { + Text("Login ID:") + .foregroundStyle(.secondary) + Text(String(format: "0x%04X", state.loginID)) + .monospaced() + } + GridRow { + Text("Generation:") + .foregroundStyle(.secondary) + Text(String(format: "%u", state.generation)) + .monospaced() + } + GridRow { + Text("Last Error:") + .foregroundStyle(.secondary) + Text("\(state.lastError)") + .monospaced() + } + GridRow { + Text("Reconnect Pending:") + .foregroundStyle(.secondary) + Text(state.reconnectPending ? "Yes" : "No") + } + } + } else { + Text("No active session.") + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } + + private var probePanel: some View { + GroupBox("Standard Probes") { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 12) { + Button("INQUIRY") { + viewModel.runInquiry() + } + .buttonStyle(.borderedProminent) + .disabled(viewModel.sessionState?.isLoggedIn != true || viewModel.isBusy) + + Button("TEST UNIT READY") { + viewModel.runTestUnitReady() + } + .buttonStyle(.bordered) + .disabled(viewModel.sessionState?.isLoggedIn != true || viewModel.isBusy) + + Button("REQUEST SENSE") { + viewModel.runRequestSense() + } + .buttonStyle(.bordered) + .disabled(viewModel.sessionState?.isLoggedIn != true || viewModel.isBusy) + } + + if let summary = viewModel.inquirySummary { + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { + GridRow { + Text("Vendor:") + .foregroundStyle(.secondary) + Text(summary.vendor.isEmpty ? "?" : summary.vendor) + } + GridRow { + Text("Product:") + .foregroundStyle(.secondary) + Text(summary.product.isEmpty ? "?" : summary.product) + } + GridRow { + Text("Revision:") + .foregroundStyle(.secondary) + Text(summary.revision.isEmpty ? "?" : summary.revision) + } + } + } + + if let sense = viewModel.senseSummary { + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { + GridRow { + Text("Sense Key:") + .foregroundStyle(.secondary) + Text(String(format: "0x%02X", sense.senseKey)) + .monospaced() + } + GridRow { + Text("ASC/ASCQ:") + .foregroundStyle(.secondary) + Text(String(format: "0x%02X / 0x%02X", sense.asc, sense.ascq)) + .monospaced() + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } + + private var rawCommandPanel: some View { + GroupBox("Raw CDB") { + VStack(alignment: .leading, spacing: 12) { + TextField("CDB hex", text: $viewModel.rawCDBHex) + .textFieldStyle(.roundedBorder) + + HStack(spacing: 12) { + Picker("Direction", selection: $viewModel.rawDirection) { + ForEach(SBP2CommandDataDirection.allCases) { direction in + Text(direction.displayName).tag(direction) + } + } + .pickerStyle(.segmented) + + TextField("Transfer", text: $viewModel.rawTransferLength) + .textFieldStyle(.roundedBorder) + .frame(width: 100) + } + + TextField("Outgoing payload hex", text: $viewModel.rawOutgoingHex) + .textFieldStyle(.roundedBorder) + + Button("Send Raw CDB") { + viewModel.runRawCommand() + } + .buttonStyle(.borderedProminent) + .disabled(viewModel.sessionState?.isLoggedIn != true || viewModel.isBusy) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } + + private var resultPanel: some View { + GroupBox("Last Result") { + VStack(alignment: .leading, spacing: 12) { + if let commandName = viewModel.lastCommandName, + let result = viewModel.commandResult { + Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { + GridRow { + Text("Command:") + .foregroundStyle(.secondary) + Text(commandName) + } + GridRow { + Text("Transport:") + .foregroundStyle(.secondary) + Text("\(result.transportStatus)") + .monospaced() + } + GridRow { + Text("SBP Status:") + .foregroundStyle(.secondary) + Text(String(format: "0x%02X", result.sbpStatus)) + .monospaced() + } + } + + if !result.payload.isEmpty { + VStack(alignment: .leading, spacing: 6) { + Text("Payload") + .font(.headline) + Text(hexDump(result.payload)) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + .background(Color.secondary.opacity(0.08)) + .cornerRadius(8) + } + } + + if !result.senseData.isEmpty { + VStack(alignment: .leading, spacing: 6) { + Text("Sense Data") + .font(.headline) + Text(hexDump(result.senseData)) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(12) + .background(Color.secondary.opacity(0.08)) + .cornerRadius(8) + } + } + } else { + Text("No command result available yet.") + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } + + private var statusPanel: some View { + GroupBox("Status") { + VStack(alignment: .leading, spacing: 8) { + if let statusMessage = viewModel.statusMessage { + Text(statusMessage) + } + if let errorMessage = viewModel.errorMessage { + Text(errorMessage) + .foregroundStyle(.red) + } + if viewModel.isBusy || viewModel.isLoadingDevices { + ProgressView() + .controlSize(.small) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } + + private func deviceTitle(_ device: ASFWDriverConnector.FWDeviceInfo) -> String { + let title = "\(device.vendorName) \(device.modelName)".trimmingCharacters(in: .whitespaces) + return title.isEmpty ? String(format: "Device 0x%016llX", device.guid) : title + } + + private func unitLabel(_ unit: ASFWDriverConnector.FWUnitInfo) -> String { + if let productName = unit.productName, !productName.isEmpty { + return "\(productName) (\(unit.specIdHex), ROM \(unit.romOffset))" + } + return "\(unit.specIdHex) • ROM \(unit.romOffset)" + } + + private func hexDump(_ data: Data) -> String { + data.map { String(format: "%02X", $0) } + .chunked(into: 16) + .map { $0.joined(separator: " ") } + .joined(separator: "\n") + } +} + +private struct SBP2DeviceRow: View { + let device: ASFWDriverConnector.FWDeviceInfo + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.headline) + + HStack(spacing: 8) { + Label(String(format: "Node %u", device.nodeId), systemImage: "externaldrive") + .font(.caption) + .foregroundStyle(.secondary) + + Text("•") + .foregroundStyle(.secondary) + + Text(String(format: "Gen %u", device.generation)) + .font(.caption) + .foregroundStyle(.secondary) + } + + Text("\(device.sbp2Units.count) SBP-2 unit\(device.sbp2Units.count == 1 ? "" : "s")") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + } + + private var title: String { + let combined = "\(device.vendorName) \(device.modelName)".trimmingCharacters(in: .whitespaces) + return combined.isEmpty ? String(format: "SBP-2 0x%016llX", device.guid) : combined + } +} + +private extension Array { + func chunked(into size: Int) -> [[Element]] { + stride(from: 0, to: count, by: size).map { index in + Array(self[index..unsigned32BitValue() != 0; + } + + if (const auto stringProp = OSDynamicCast(OSString, property)) { + return stringProp->isEqualTo("1") || stringProp->isEqualTo("true") || + stringProp->isEqualTo("TRUE"); + } + + return false; +} } // namespace bool ASFWDriver::init() { @@ -109,22 +130,20 @@ kern_return_t IMPL(ASFWDriver, Start) { ctx.stopping.store(false, std::memory_order_release); DriverWiring::EnsureDeps(this, ctx); bool traceProperty = false; + bool experimentalHostCycleMasterBringup = false; if (OSDictionary* serviceProperties = nullptr; CopyProperties(&serviceProperties) == kIOReturnSuccess && serviceProperties != nullptr) { - if (auto property = serviceProperties->getObject("ASFWTraceDMACoherency")) { - if (auto booleanProp = OSDynamicCast(OSBoolean, property)) { - traceProperty = (booleanProp == kOSBooleanTrue); - } else if (auto numberProp = OSDynamicCast(OSNumber, property)) { - traceProperty = numberProp->unsigned32BitValue() != 0; - } else if (auto stringProp = OSDynamicCast(OSString, property)) { - traceProperty = stringProp->isEqualTo("1") || stringProp->isEqualTo("true") || - stringProp->isEqualTo("TRUE"); - } - } + traceProperty = PropertyIsEnabled(serviceProperties->getObject("ASFWTraceDMACoherency")); + experimentalHostCycleMasterBringup = + PropertyIsEnabled(serviceProperties->getObject("ASFWExperimentalHostCycleMasterBringup")); serviceProperties->release(); } + ctx.config.experimentalHostCycleMasterBringup = experimentalHostCycleMasterBringup; ASFW_LOG(Controller, "ASFWDriver::Start(): ASFWTraceDMACoherency property=%{public}s", traceProperty ? "true" : "false"); + ASFW_LOG(Controller, + "ASFWDriver::Start(): ASFWExperimentalHostCycleMasterBringup property=%{public}s", + experimentalHostCycleMasterBringup ? "true" : "false"); if (auto statusKr = ctx.statusPublisher.Prepare(); statusKr != kIOReturnSuccess) { DriverWiring::CleanupStartFailure(ctx); return statusKr; @@ -246,7 +265,7 @@ kern_return_t IMPL(ASFWDriver, Start) { } return ASFW::Async::ResponseCode::NoResponse; }); - ASFW_LOG(Controller, "✅ FCPResponseRouter wired to PacketRouter (tCode 0x1)"); + ASFW_LOG(Controller, "FCPResponseRouter wired to PacketRouter (tCode 0x1)"); } } diff --git a/ASFWDriver/Async/PacketHelpers.hpp b/ASFWDriver/Async/PacketHelpers.hpp index dc631c90..fbac8b73 100644 --- a/ASFWDriver/Async/PacketHelpers.hpp +++ b/ASFWDriver/Async/PacketHelpers.hpp @@ -20,10 +20,10 @@ namespace ASFW::Async { /// /// Per IEEE 1394-1995 §6.2.1, destination_offset is at bytes 8-13 (48-bit). /// -/// @param header Packet header bytes (big-endian, minimum 16 bytes) +/// @param header Packet header bytes (big-endian, minimum 12 bytes) /// @return Destination offset (48-bit address), or 0 if header too short inline uint64_t ExtractDestOffset(std::span header) { - if (header.size() < 16) { + if (header.size() < 12) { return 0; } @@ -65,20 +65,28 @@ inline uint64_t ExtractDestOffset(std::span header) { return (offset_high << 32) | offset_low; } -/// Extract data length from block write/read packet header +/// Extract data length from an OHCI AR DMA block packet header. /// -/// Per IEEE 1394-1995 §6.2.4, data_length is at bytes 14-15 (16-bit). +/// IEEE 1394 wire format stores Q3 as: +/// [data_length:16][extended_tcode:16] /// -/// @param header Packet header bytes (big-endian, minimum 16 bytes) +/// OHCI AR DMA writes each quadlet to memory in little-endian host order, so +/// Q3 appears in memory as: +/// [extended_tcode_lo][extended_tcode_hi][data_length_lo][data_length_hi] +/// +/// That means data_length lives in header[14:15] as a little-endian 16-bit +/// value, not a big-endian wire-order value. +/// +/// @param header Packet header bytes in OHCI AR DMA memory order /// @return Data length in bytes, or 0 if header too short inline uint16_t ExtractDataLength(std::span header) { if (header.size() < 16) { return 0; } - // Data length: bytes 14-15 (16-bit big-endian) - return (static_cast(header[14]) << 8) | - static_cast(header[15]); + // Q3 is stored little-endian in the AR DMA buffer. + return static_cast(header[14]) | + (static_cast(header[15]) << 8); } /// Extract extended transaction code from packet header diff --git a/ASFWDriver/Async/Rx/PacketRouter.cpp b/ASFWDriver/Async/Rx/PacketRouter.cpp index 00ff7db8..b930fbf9 100644 --- a/ASFWDriver/Async/Rx/PacketRouter.cpp +++ b/ASFWDriver/Async/Rx/PacketRouter.cpp @@ -1,13 +1,43 @@ #include "PacketRouter.hpp" +#include #include +#include "../../Common/FWCommon.hpp" #include "ARPacketParser.hpp" #include "../../Logging/Logging.hpp" #include "../Tx/ResponseSender.hpp" namespace ASFW::Async { +namespace { + +std::span CopyAlignedPayload(std::span source, + std::array& scratch) { + if (source.empty()) { + return {}; + } + + if (source.size() > scratch.size()) { + return {}; + } + + std::size_t index = 0; + for (; index + sizeof(uint32_t) <= source.size(); index += sizeof(uint32_t)) { + uint32_t quadlet = 0; + __builtin_memcpy(&quadlet, source.data() + index, sizeof(uint32_t)); + __builtin_memcpy(scratch.data() + index, &quadlet, sizeof(uint32_t)); + } + + for (; index < source.size(); ++index) { + scratch[index] = source[index]; + } + + return std::span(scratch.data(), source.size()); +} + +} // namespace + void PacketRouter::RegisterRequestHandler(uint8_t tCode, PacketHandler handler) { if (tCode >= 16) { ASFW_LOG(Async, "PacketRouter: invalid request tCode %u", tCode); @@ -56,12 +86,26 @@ void PacketRouter::RoutePacket(ARContextType contextType, std::span payloadScratch{}; // Build zero-copy view over header and payload ARPacketView view; view.tCode = tCode; view.header = std::span(packetStart, headerLen); - view.payload = std::span(packetStart + headerLen, dataLen); + if (dataLen > 0) { + const auto payloadBytes = std::span(packetStart + headerLen, dataLen); + view.payload = CopyAlignedPayload(payloadBytes, payloadScratch); + if (view.payload.empty()) { + ASFW_LOG(Async, + "PacketRouter: payload %zu exceeds aligned scratch buffer for tCode=0x%x", + dataLen, + tCode); + offset += packetInfo.totalLength; + continue; + } + } else { + view.payload = {}; + } // Trailer fields – use low 16 bits for xferStatus/timeStamp view.xferStatus = static_cast(packetInfo.xferStatus & 0xFFFF); diff --git a/ASFWDriver/Async/Rx/RxPath.cpp b/ASFWDriver/Async/Rx/RxPath.cpp index 5b8f622a..b8b3d531 100644 --- a/ASFWDriver/Async/Rx/RxPath.cpp +++ b/ASFWDriver/Async/Rx/RxPath.cpp @@ -417,6 +417,14 @@ void RxPath::ProcessReceivedPacket(ARContextType contextType, if (info.tCode == 0x6) { // kTCodeReadQuadletResponse payloadPtr = info.packetStart + 12; // q3 (offset 12-15) payloadLen = 4; + + // DIAGNOSTIC: Log raw bytes of read quadlet response data + ASFW_LOG(Async, + "[DIAG] RxPath ReadQuadletResp: src=0x%04X tLabel=%u rCode=0x%X " + "raw=[%02X %02X %02X %02X] gen=%u", + sourceID, tLabel, rCode, + payloadPtr[0], payloadPtr[1], payloadPtr[2], payloadPtr[3], + currentGen); } // CRITICAL: DMA buffers are mapped as device memory (kIOMemoryMapCacheModeInhibit). diff --git a/ASFWDriver/Async/Track/TransactionCompletionHandler.hpp b/ASFWDriver/Async/Track/TransactionCompletionHandler.hpp index dc3a346a..f618cb58 100644 --- a/ASFWDriver/Async/Track/TransactionCompletionHandler.hpp +++ b/ASFWDriver/Async/Track/TransactionCompletionHandler.hpp @@ -376,7 +376,23 @@ class TransactionCompletionHandler { Transaction* txn = txnMgr_->FindByMatchKey(key); if (!txn) { - ASFW_LOG(Async, "⚠️ OnARResponse: No transaction for key"); + // DIAGNOSTIC: Log detailed key info for unmatched AR response + ASFW_LOG(Async, + "[DIAG] OnARResponse: NO MATCH — key{node=0x%04X gen=%u tLabel=%u} " + "rcode=0x%X dataLen=%zu", + key.node.value, key.generation.value, key.label.value, + rcode, data.size()); + // Dump all active transactions for comparison + if (txnMgr_) { + txnMgr_->ForEachTransaction([&](const Transaction* t) { + if (!t) return; + ASFW_LOG(Async, + "[DIAG] active: tLabel=%u node=0x%04X gen=%u state=%s", + t->label().value, t->GetMatchKey().node.value, + t->GetMatchKey().generation.value, + ToString(t->state())); + }); + } return; } @@ -388,6 +404,10 @@ class TransactionCompletionHandler { state == TransactionState::Failed || state == TransactionState::Cancelled || state == TransactionState::TimedOut) { + ASFW_LOG(Async, + "[DIAG] OnARResponse: LATE ARRIVAL — tLabel=%u state=%s " + "(AR arrived after terminal state)", + key.label.value, ToString(state)); ASFW_LOG_V3(Async, "OnARResponse: AR for terminal txn (state=%{public}s) – ignoring", ToString(state)); return; @@ -559,6 +579,13 @@ class TransactionCompletionHandler { auto txnPtr = txnMgr_->Extract(label); if (txnPtr) { txnPtr->TransitionTo(TransactionState::TimedOut, "OnTimeout"); + + // DIAGNOSTIC: Log timeout details + ASFW_LOG(Async, + "[DIAG] OnTimeout: tLabel=%u node=0x%04X gen=%u — " + "AR response never arrived, completing with timeout", + label.value, txnPtr->GetMatchKey().node.value, + txnPtr->GetMatchKey().generation.value); // Invoke callback txnPtr->InvokeResponseHandler(kIOReturnTimeout, 0xFF, {}); diff --git a/ASFWDriver/Bus/BusManager.cpp b/ASFWDriver/Bus/BusManager.cpp index 1b3467a1..10af8ff4 100644 --- a/ASFWDriver/Bus/BusManager.cpp +++ b/ASFWDriver/Bus/BusManager.cpp @@ -43,6 +43,23 @@ namespace { }); } +[[nodiscard]] bool IsTwoNodeLocalRootTopology(const TopologySnapshot& topology) { + if (!topology.localNodeId.has_value() || !topology.rootNodeId.has_value()) { + return false; + } + if (*topology.localNodeId != *topology.rootNodeId) { + return false; + } + + uint8_t remoteActiveNodes = 0; + for (const auto& node : topology.nodes) { + if (node.linkActive && node.nodeId != *topology.localNodeId) { + ++remoteActiveNodes; + } + } + return remoteActiveNodes == 1U; +} + } // namespace // ============================================================================ @@ -264,6 +281,12 @@ std::optional BusManager::EvaluateGapPolicy( } if (AnyObservedGapNeedsRetool(observedGaps, gapState_.lastConfirmedGap, targetGap)) { + if (IsTwoNodeLocalRootTopology(topology)) { + ASFW_LOG(BusManager, + "Skipping target gap optimization for two-node local-root topology"); + return std::nullopt; + } + ASFW_LOG(BusManager, "Retooling gap count to %u (confirmed=%u)", targetGap, gapState_.lastConfirmedGap); return GapDecision{targetGap, GapDecisionReason::TargetGap}; diff --git a/ASFWDriver/Bus/BusManager.hpp b/ASFWDriver/Bus/BusManager.hpp index 4cf912d7..030fecf0 100644 --- a/ASFWDriver/Bus/BusManager.hpp +++ b/ASFWDriver/Bus/BusManager.hpp @@ -49,8 +49,11 @@ class BusManager { * @brief Gap-reset state tracked across generations. * * `lastConfirmedGap` is the most recent stable packet-0 gap observed on an - * accepted topology. `inFlight` is populated only after the coordinator has - * successfully dispatched a corrective reset carrying a gap update. + * accepted topology. It remains `0xFF` until the first stable topology is + * accepted so the optimizer can distinguish "unknown previous gap" from a + * real confirmed `gap_count = 63`. `inFlight` is populated only after the + * coordinator has successfully dispatched a corrective reset carrying a gap + * update. */ struct GapState { struct InFlightReset { @@ -58,7 +61,7 @@ class BusManager { GapDecisionReason reason{GapDecisionReason::MismatchForce63}; }; - uint8_t lastConfirmedGap{0x3F}; + uint8_t lastConfirmedGap{0xFF}; std::optional inFlight; }; @@ -73,7 +76,7 @@ class BusManager { RootPolicy rootPolicy = RootPolicy::Delegate; uint8_t forcedRootNodeID = 0xFF; bool delegateCycleMaster = true; - bool enableGapOptimization = false; + bool enableGapOptimization = true; uint8_t forcedGapCount = 0; bool forcedGapFlag = false; }; diff --git a/ASFWDriver/Bus/BusResetCoordinator.cpp b/ASFWDriver/Bus/BusResetCoordinator.cpp index e41b17a5..e33a1465 100644 --- a/ASFWDriver/Bus/BusResetCoordinator.cpp +++ b/ASFWDriver/Bus/BusResetCoordinator.cpp @@ -61,7 +61,10 @@ void LogStateTransition(ASFW::Driver::BusResetCoordinator::State previousState, namespace ASFW::Driver { -BusResetCoordinator::BusResetCoordinator() = default; +std::atomic BusResetCoordinator::nextDiagnosticsInstanceId_{1}; + +BusResetCoordinator::BusResetCoordinator() + : diagnosticsInstanceId_(nextDiagnosticsInstanceId_.fetch_add(1, std::memory_order_relaxed)) {} BusResetCoordinator::~BusResetCoordinator() = default; void BusResetCoordinator::Initialize(HardwareInterface* hw, OSSharedPtr workQueue, @@ -104,6 +107,7 @@ void BusResetCoordinator::OnIrq(uint32_t intEvent, uint64_t timestamp) { cycle_.timing.lastBusResetEdgeNs = timestamp; pendingBusResetEdge_ = true; relevant = true; + ++busResetIrqCount_; LogBusResetEdgeLatched(timestamp); } @@ -134,6 +138,23 @@ void BusResetCoordinator::BindCallbacks(TopologyReadyCallback onTopology) { topologyCallback_ = std::move(onTopology); } +BusResetCoordinator::ResetDiagnostics BusResetCoordinator::Diagnostics() const { + return ResetDiagnostics{ + .driverStartId = diagnosticsInstanceId_, + .resetEpoch = resetEpoch_, + .manualResetEpoch = manualResetEpoch_, + .softwareResetIssuedCount = softwareResetIssuedCount_, + .busResetIrqCount = busResetIrqCount_, + .lastAcceptedGeneration = lastAcceptedGeneration_, + .lastTopologyNodeCount = lastTopologyNodeCount_, + .readyForDiscoveryFailureBits = readyForDiscoveryFailureBits_, + .lastRecoveryReasonCode = lastRecoveryReasonCode_, + .lastResetKind = static_cast(lastResetKind_), + .recoveryResetAttempts = manualRecoveryResetAttempts_, + .discoveryCallbackCount = discoveryCallbackCount_, + }; +} + uint64_t BusResetCoordinator::MonotonicNow() { #ifdef ASFW_HOST_TEST return ASFW::Testing::HostMonotonicNow(); diff --git a/ASFWDriver/Bus/BusResetCoordinator.hpp b/ASFWDriver/Bus/BusResetCoordinator.hpp index 8194f0fb..96474a59 100644 --- a/ASFWDriver/Bus/BusResetCoordinator.hpp +++ b/ASFWDriver/Bus/BusResetCoordinator.hpp @@ -110,6 +110,33 @@ class BusResetCoordinator { const char* StateString() const; static const char* StateString(State state); + enum class RecoveryReasonCode : uint8_t { + None = 0, + SelfIDDecodeFailed = 1, + SelfIDTimeout = 2, + TopologyBuildFailed = 3, + SoftwareResetDispatchFailed = 4, + ReadyForDiscoveryFailed = 5, + ManualResetWatchdog = 6, + }; + + struct ResetDiagnostics { + uint32_t driverStartId{0}; + uint32_t resetEpoch{0}; + uint32_t manualResetEpoch{0}; + uint32_t softwareResetIssuedCount{0}; + uint32_t busResetIrqCount{0}; + uint32_t lastAcceptedGeneration{0}; + uint8_t lastTopologyNodeCount{0}; + uint8_t readyForDiscoveryFailureBits{0}; + RecoveryReasonCode lastRecoveryReasonCode{RecoveryReasonCode::None}; + uint8_t lastResetKind{0}; + uint8_t recoveryResetAttempts{0}; + uint8_t discoveryCallbackCount{0}; + }; + + ResetDiagnostics Diagnostics() const; + /** * Reset delegation retry counter (Linux pattern for emergency bypass). * @@ -236,13 +263,16 @@ class BusResetCoordinator { bool DispatchSoftwareReset(const ResetRequest& request); void ClearDelegationAttempt(); void RecordRecoveryReason(std::string reason); + void RecordRecoveryReasonCode(RecoveryReasonCode code); + void ScheduleManualResetWatchdog(uint32_t manualEpoch, uint32_t resetEpoch); + void MaybeRecoverMissingManualResetIrq(uint32_t manualEpoch, uint32_t resetEpoch); bool G_ATInactive(); bool HasSelfIDCompletion() const; bool CanAttemptSelfIDDecode() const; bool G_NodeIDValid() const; - bool ReadyForDiscovery(Discovery::Generation gen) const; + bool ReadyForDiscovery(Discovery::Generation gen); static uint64_t MonotonicNow(); @@ -290,6 +320,20 @@ class BusResetCoordinator { static constexpr uint32_t kMaxDiscoveryDelayMs = 10000; // 10s cap uint32_t currentDiscoveryDelayMs_{0}; bool previousScanHadBusyNodes_{false}; + + static std::atomic nextDiagnosticsInstanceId_; + uint32_t diagnosticsInstanceId_{0}; + uint32_t resetEpoch_{0}; + uint32_t manualResetEpoch_{0}; + uint32_t softwareResetIssuedCount_{0}; + uint32_t busResetIrqCount_{0}; + uint32_t lastAcceptedGeneration_{0}; + uint8_t lastTopologyNodeCount_{0}; + uint8_t readyForDiscoveryFailureBits_{0}; + RecoveryReasonCode lastRecoveryReasonCode_{RecoveryReasonCode::None}; + ResetRequestKind lastResetKind_{ResetRequestKind::Recovery}; + uint8_t manualRecoveryResetAttempts_{0}; + uint8_t discoveryCallbackCount_{0}; }; } // namespace ASFW::Driver diff --git a/ASFWDriver/Bus/BusResetCoordinatorActions.cpp b/ASFWDriver/Bus/BusResetCoordinatorActions.cpp index 6c697be4..df3f5484 100644 --- a/ASFWDriver/Bus/BusResetCoordinatorActions.cpp +++ b/ASFWDriver/Bus/BusResetCoordinatorActions.cpp @@ -1,7 +1,12 @@ #include "BusResetCoordinator.hpp" +#include #include +#ifndef ASFW_HOST_TEST +#include +#endif + #include "../Async/Interfaces/IAsyncControllerPort.hpp" #include "../ConfigROM/ConfigROMStager.hpp" #include "../ConfigROM/ROMScanner.hpp" @@ -17,6 +22,8 @@ namespace { constexpr uint64_t kRepeatedResetHoldoffNs = 2'000'000'000ULL; constexpr uint8_t kConservativeMismatchGapCount = 0x3FU; +constexpr uint32_t kManualResetWatchdogMs = 500; +constexpr uint8_t kMaxManualRecoveryResetAttempts = 1; void MergePhyConfig(ASFW::Driver::BusManager::PhyConfigCommand& base, const ASFW::Driver::BusManager::PhyConfigCommand& addition) { @@ -162,6 +169,7 @@ bool BusResetCoordinator::BuildTopology() { if (!snapshot) { RecordRecoveryReason(std::string{"Topology build failed: "} + TopologyManager::TopologyBuildErrorCodeString(snapshot.error().code)); + RecordRecoveryReasonCode(RecoveryReasonCode::TopologyBuildFailed); cycle_.acceptedTopology.reset(); RequestSoftwareReset({ResetRequestKind::Recovery, ResetFlavor::Short, std::nullopt, "Invalid Self-ID topology"}); @@ -172,6 +180,9 @@ bool BusResetCoordinator::BuildTopology() { } cycle_.acceptedTopology = *snapshot; + lastAcceptedGeneration_ = snapshot->generation; + lastTopologyNodeCount_ = + static_cast(std::min(snapshot->nodes.size(), 0xFFU)); cycle_.timing.lastSelfIdCompletionNs = timestamp; cycle_.timing.softwareResetBlockedUntilNs = timestamp + kRepeatedResetHoldoffNs; @@ -465,6 +476,7 @@ bool BusResetCoordinator::DispatchSoftwareReset(const ResetRequest& request) { ASFW_LOG(BusReset, "Issuing %{public}s %{public}s software reset (%{public}s)", resetKindString(request.kind), resetFlavorString(request.flavor), request.reason.c_str()); + lastResetKind_ = request.kind; if (request.phyConfig.has_value()) { const auto& command = *request.phyConfig; @@ -487,6 +499,7 @@ bool BusResetCoordinator::DispatchSoftwareReset(const ResetRequest& request) { if (!hardware_->InitiateBusReset(request.flavor == ResetFlavor::Short)) { RecordRecoveryReason(std::string{"Software reset dispatch failed: "} + request.reason); + RecordRecoveryReasonCode(RecoveryReasonCode::SoftwareResetDispatchFailed); if (request.gapDecisionReason.has_value() && busManager_ != nullptr) { busManager_->ClearInFlightGapReset(); } @@ -501,6 +514,11 @@ bool BusResetCoordinator::DispatchSoftwareReset(const ResetRequest& request) { busManager_->NoteGapResetIssued(*request.phyConfig->gapCount, *request.gapDecisionReason); } + ++softwareResetIssuedCount_; + if (request.kind == ResetRequestKind::ManualBusManager) { + ScheduleManualResetWatchdog(manualResetEpoch_, resetEpoch_); + } + return true; } @@ -516,7 +534,58 @@ void BusResetCoordinator::RecordRecoveryReason(std::string reason) { metrics_.lastFailureReason = *cycle_.recoveryReason; } +void BusResetCoordinator::RecordRecoveryReasonCode(RecoveryReasonCode code) { + lastRecoveryReasonCode_ = code; +} + +void BusResetCoordinator::ScheduleManualResetWatchdog(uint32_t manualEpoch, uint32_t resetEpoch) { + if (workQueue_.get() == nullptr) { + return; + } + +#ifdef ASFW_HOST_TEST + if (workQueue_->UsesManualDispatchForTesting()) { + workQueue_->DispatchAsyncAfter(static_cast(kManualResetWatchdogMs) * 1'000'000ULL, + ^{ + MaybeRecoverMissingManualResetIrq(manualEpoch, resetEpoch); + }); + return; + } +#endif + + workQueue_->DispatchAsync(^{ +#ifdef ASFW_HOST_TEST + (void)manualEpoch; + (void)resetEpoch; +#else + IOSleep(kManualResetWatchdogMs); + MaybeRecoverMissingManualResetIrq(manualEpoch, resetEpoch); +#endif + }); +} + +void BusResetCoordinator::MaybeRecoverMissingManualResetIrq(uint32_t manualEpoch, + uint32_t resetEpoch) { + if (manualEpoch != manualResetEpoch_ || resetEpoch != resetEpoch_) { + return; + } + + if (manualRecoveryResetAttempts_ >= kMaxManualRecoveryResetAttempts) { + RecordRecoveryReason("Manual reset watchdog reached bounded recovery limit"); + RecordRecoveryReasonCode(RecoveryReasonCode::ManualResetWatchdog); + return; + } + + ++manualRecoveryResetAttempts_; + RecordRecoveryReason("Manual reset watchdog did not observe busReset IRQ/topology"); + RecordRecoveryReasonCode(RecoveryReasonCode::ManualResetWatchdog); + RequestSoftwareReset({ResetRequestKind::Recovery, ResetFlavor::Short, std::nullopt, + "Manual reset watchdog recovery", std::nullopt}); +} + void BusResetCoordinator::RequestUserReset(bool shortReset) { + ++manualResetEpoch_; + manualRecoveryResetAttempts_ = 0; RequestSoftwareReset({ResetRequestKind::ManualBusManager, shortReset ? ResetFlavor::Short : ResetFlavor::Long, std::nullopt, "UserClient-initiated", std::nullopt}); diff --git a/ASFWDriver/Bus/BusResetCoordinatorDiscoveryDelay.cpp b/ASFWDriver/Bus/BusResetCoordinatorDiscoveryDelay.cpp index 3a087deb..24ed581e 100644 --- a/ASFWDriver/Bus/BusResetCoordinatorDiscoveryDelay.cpp +++ b/ASFWDriver/Bus/BusResetCoordinatorDiscoveryDelay.cpp @@ -6,13 +6,32 @@ namespace ASFW::Driver { -bool BusResetCoordinator::ReadyForDiscovery(Discovery::Generation gen) const { +bool BusResetCoordinator::ReadyForDiscovery(Discovery::Generation gen) { const bool nodeValid = G_NodeIDValid(); const bool genMatch = (gen == lastGeneration_); const bool hasTopo = cycle_.acceptedTopology.has_value(); const bool ready = nodeValid && filtersEnabled_ && atArmed_ && hasTopo && genMatch; + uint8_t failureBits = 0; + if (!nodeValid) { + failureBits |= 1U << 0U; + } + if (!filtersEnabled_) { + failureBits |= 1U << 1U; + } + if (!atArmed_) { + failureBits |= 1U << 2U; + } + if (!hasTopo) { + failureBits |= 1U << 3U; + } + if (!genMatch) { + failureBits |= 1U << 4U; + } + readyForDiscoveryFailureBits_ = failureBits; + if (!ready) { + RecordRecoveryReasonCode(RecoveryReasonCode::ReadyForDiscoveryFailed); ASFW_LOG(BusReset, "ReadyForDiscovery(gen=%u): NOT READY — nodeValid=%d filters=%d at=%d " "topo=%d genMatch=%d(last=%u)", diff --git a/ASFWDriver/Bus/BusResetCoordinatorFSM.cpp b/ASFWDriver/Bus/BusResetCoordinatorFSM.cpp index 35a714af..b130dec6 100644 --- a/ASFWDriver/Bus/BusResetCoordinatorFSM.cpp +++ b/ASFWDriver/Bus/BusResetCoordinatorFSM.cpp @@ -1,9 +1,8 @@ #include "BusResetCoordinator.hpp" -#ifdef ASFW_HOST_TEST -#include -#include -#else +#include + +#ifndef ASFW_HOST_TEST #include #endif @@ -18,6 +17,7 @@ namespace { constexpr uint32_t kDeferredPollMs = 1; constexpr uint32_t kSelfIDTimeoutMs = 1000; +constexpr uint32_t kAppleScanBusDelayMs = 100; } // namespace @@ -30,6 +30,8 @@ void BusResetCoordinator::BeginNewResetCycle() { filtersEnabled_ = false; atArmed_ = false; cycle_.ResetForNewEdge(); + ++resetEpoch_; + readyForDiscoveryFailureBits_ = 0; if ((romScanner_ != nullptr) && (lastGeneration_.value > 0U)) { ++metrics_.abortCount; @@ -75,6 +77,7 @@ BusResetCoordinator::StepResult BusResetCoordinator::StepWaitingSelfID() { const bool decoded = DecodeSelfID(); ClearConsumedSelfIDInterrupts(); if (!decoded) { + RecordRecoveryReasonCode(RecoveryReasonCode::SelfIDDecodeFailed); RequestSoftwareReset( {ResetRequestKind::Recovery, ResetFlavor::Short, std::nullopt, "Self-ID decode failed"}); @@ -86,6 +89,7 @@ BusResetCoordinator::StepResult BusResetCoordinator::StepWaitingSelfID() { const uint64_t waitedNs = MonotonicNow() - stateEntryTime_; if (waitedNs >= static_cast(kSelfIDTimeoutMs) * 1'000'000ULL) { RecordRecoveryReason("Self-ID timeout"); + RecordRecoveryReasonCode(RecoveryReasonCode::SelfIDTimeout); ClearConsumedSelfIDInterrupts(); RequestSoftwareReset( {ResetRequestKind::Recovery, ResetFlavor::Short, std::nullopt, "Self-ID timeout"}); @@ -199,28 +203,31 @@ BusResetCoordinator::StepResult BusResetCoordinator::StepComplete() { if (topologyCallback_ && cycle_.acceptedTopology.has_value() && (workQueue_.get() != nullptr)) { auto topo = *cycle_.acceptedTopology; const Discovery::Generation generation{topo.generation}; + uint32_t delayMs = kAppleScanBusDelayMs; if (previousScanHadBusyNodes_ && currentDiscoveryDelayMs_ > 0U) { - const uint32_t delayMs = currentDiscoveryDelayMs_; - ASFW_LOG(BusReset, "Discovery delayed %ums for generation %u", delayMs, - generation.value); - workQueue_->DispatchAsync(^{ + delayMs = std::max(delayMs, currentDiscoveryDelayMs_); + } + + ASFW_LOG(BusReset, "Discovery delayed %ums for generation %u", delayMs, generation.value); #ifdef ASFW_HOST_TEST - std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); + workQueue_->DispatchAsyncAfter(static_cast(delayMs) * 1'000'000ULL, ^{ + if (ReadyForDiscovery(generation)) { + discoveryCallbackCount_ = static_cast( + std::min(static_cast(discoveryCallbackCount_) + 1U, 0xFFU)); + topologyCallback_(topo); + } + }); #else - IOSleep(delayMs); + workQueue_->DispatchAsync(^{ + IOSleep(delayMs); + if (ReadyForDiscovery(generation)) { + discoveryCallbackCount_ = static_cast( + std::min(static_cast(discoveryCallbackCount_) + 1U, 0xFFU)); + topologyCallback_(topo); + } + }); #endif - if (ReadyForDiscovery(generation)) { - topologyCallback_(topo); - } - }); - } else { - workQueue_->DispatchAsync(^{ - if (ReadyForDiscovery(generation)) { - topologyCallback_(topo); - } - }); - } } return StepResult::Finish; diff --git a/ASFWDriver/Bus/IEEE1394-BusReset.md.bak b/ASFWDriver/Bus/IEEE1394-BusReset.md.bak deleted file mode 100644 index f3c43f3e..00000000 --- a/ASFWDriver/Bus/IEEE1394-BusReset.md.bak +++ /dev/null @@ -1,1624 +0,0 @@ -# IEEE 1394 Bus Reset Specification - -## Overview - -This document provides detailed coverage of **Bus Reset** and **Self-Identification** as defined in IEEE 1394-2008 specification. Bus reset is the fundamental mechanism for topology discovery, arbitration reset, and bus initialization in FireWire networks. - -**References:** -- IEEE 1394-2008: Complete specification (consolidates 1394-1995, 1394a-2000, 1394b-2002)§8.3.2 (Bus Reset) -- IEEE 1394-1995 §8.4.6 (Self-Identification Process) -- IEEE 1394a-2000 §16.4.5 (Bus Reset State Machine) -- OHCI §11 (Self-ID Receive) - -**Related Documentation:** See [README.md](README.md) for implementation details in ASFWDriver. - ---- - -## Table of Contents - -1. [Bus Reset Fundamentals](#bus-reset-fundamentals) -2. [Bus Reset Triggers](#bus-reset-triggers) -3. [Bus Reset State Machine](#bus-reset-state-machine) -4. [Self-Identification Process](#self-identification-process) -5. [Self-ID State Machine (S0-S4)](#self-id-state-machine-s0-s4) -6. [Tree Identification](#tree-identification) -7. [Bus Configuration](#bus-configuration) -8. [Timing Requirements](#timing-requirements) -9. [PHY Configuration Packets](#phy-configuration-packets) -10. [Error Handling](#error-handling) - ---- - -## Bus Reset Fundamentals - -### Purpose - -Bus reset serves three critical functions: - -1. **Topology Discovery**: All nodes broadcast their physical layer capabilities and port connectivity via Self-ID packets -2. **Arbitration Reset**: Clears all pending arbitration state, ensuring fair bus access after topology changes -3. **Node ID Assignment**: Assigns unique 6-bit physical IDs to all nodes based on tree topology - -### Key Concepts - -```mermaid -graph TD - A[Bus Reset Event] --> B[All nodes enter Reset State] - B --> C[Bus Arbitration] - C --> D[Root Node Identified] - D --> E[Self-ID Transmission] - E --> F[Tree ID Complete] - F --> G[Normal Operation] - - style A fill:#ff6b6b - style D fill:#4ecdc4 - style G fill:#95e1d3 -``` - -### Bus Reset Duration - -Per IEEE 1394-2008 §8.3.2.3.2: - -| Parameter | Symbol | Value | Description | -|-----------|--------|-------|-------------| -| **Reset Time** | `RESET_TIME` | ≥166 μs | Minimum duration of BUS_RESET signal | -| **Short Reset** | `SHORT_RESET_TIME` | ≥1.28 μs | Abbreviated reset after arbitration | -| **Reset Wait** | `RESET_WAIT` | ≤10 ms | Maximum wait in R1: Reset Wait state | -| **Arbitration Timeout** | `ARB_STATE_TIMEOUT` | Variable | Based on topology depth | - ---- - -## Bus Reset Triggers - -### Hardware Triggers - -Per IEEE 1394-2008 §8.3.2.3: - -```mermaid -flowchart LR - A[Power-On Reset] --> BR[Bus Reset] - B[Cable Hotplug] --> BR - C[Cable Disconnect] --> BR - D[PHY Register Write] --> BR - E[Senior Port Disconnect] --> BR - - style BR fill:#ff6b6b,color:#fff -``` - -### Software-Initiated Reset - -**Long Reset** (IEEE 1394-2008 §8.4.6): -- Triggered by Link Layer via `PH_CONTROL.request` with long reset parameter -- Forces complete bus re-initialization -- All nodes participate in Self-ID - -**Short Reset** (IEEE 1394-2008 §16.3.2.4): -- Triggered after successful arbitration -- Abbreviated reset sequence -- Only root node sends BUS_RESET -- Faster than long reset (~1.28 μs vs 166 μs) - -### PHY-Level Detection - -Per IEEE 1394-2008 §8.3.2.3.4: - -```cpp -// Transition All:R0a (from IEEE 1394-2008 Figure 16-16) -// Entry point if PHY senses BUS_RESET on any active/resuming port -// or port waiting to attach -``` - -Conditions for `All:R0a` transition: -- BUS_RESET detected on **any** active port -- BUS_RESET on resuming port -- BUS_RESET on port attempting to attach -- **Highest priority** transition (preempts all other state transitions) - ---- - -## Bus Reset State Machine - -### State Definitions - -Based on IEEE 1394-2008 §16.4.5 (Figure 16-16): - -```mermaid -stateDiagram-v2 - [*] --> R0_ResetStart : All:R0a (powerReset)
All:R0b (initiatedReset)
All:R0c (maxArbStateTimeout)
TX:R0 (arbitration success) - - R0_ResetStart : R0: Reset Start - R0_ResetStart : resetStartActions() - R0_ResetStart : Send BUS_RESET signal - R0_ResetStart : Duration = resetDuration - - R0_ResetStart --> R1_ResetWait : R0:R1
arbTimer >= resetDuration - - R1_ResetWait : R1: Reset Wait - R1_ResetWait : resetWaitActions() - R1_ResetWait : Send IDLE or PARENT_NOTIFY - - R1_ResetWait --> R0_ResetStart : R1:R0
arbTimer >= (resetDuration + RESET_WAIT) - R1_ResetWait --> T0_TreeIDStart : R1:T0
resetComplete() && arbTimer = 0 - - T0_TreeIDStart : T0: Tree ID Start - T0_TreeIDStart : page 448 (IEEE 1394-2008) - - style R0_ResetStart fill:#ff6b6b,color:#fff - style R1_ResetWait fill:#ffd93d - style T0_TreeIDStart fill:#95e1d3 -``` - -### State Transitions (Detailed) - -#### All:R0a - Detected Bus Reset - -**Trigger**: PHY detects BUS_RESET on any active or resuming port - -**Actions** (per §16.4.5): -``` -resetDetected() -initiatedReset = FALSE -``` - -**Priority**: **Highest** - preempts any other transition - -#### All:R0b - Initiated Bus Reset (Local) - -**Trigger**: Link layer requests long reset OR PHY detects senior port disconnect - -**Conditions**: -- `SBM makes a PH_CONTROL.request that specifies a long reset`, OR -- `The PHY detects a disconnect on its senior port` - -**Actions**: -``` -ibr&& (!phyResponse || immediatePhyRequest) -initiatedReset = TRUE -resetDuration = RESET_TIME -``` - -**Wait**: Current state's actions must complete before transition - -#### All:R0c - Arbitration State Timeout - -**Trigger**: PHY stays in A0: Idle state with `idleArbStateTimeout` for too long - -**Conditions**: -- In A0: Idle state -- `idleArbStateTimeout = false` -- Stayed idle for `MAX_ARB_STATE_TIME` -- Local request pending (link or PHY) - -**Actions**: -``` -maxArbStateTimeout() -initiatedReset = TRUE -resetDuration = RESET_TIME -if (!timeout) { - timeout = TRUE - PH_EVENT.indication(PH_MAX_ARB_STATE_TIMEOUT, 0, 0) -} -``` - -**Purpose**: Prevents indefinite stalls in arbitration state - -#### TX:R0 - Arbitrated Reset (Short) - -**Trigger**: Node won arbitration and `isbrOk` variable is set - -**Conditions**: -- Arbitration succeeded -- `isbrOk = TRUE` -- No packet exists to transmit - -**Actions**: Short bus reset commences immediately - -**Duration**: `SHORT_RESET_TIME` (significantly shorter than `RESET_TIME`) - -**Note**: Bus already in known state after arbitration, so shorter reset is sufficient - ---- - -### R0: Reset Start State - -**Purpose**: Node sends BUS_RESET signal for a duration governed by `resetDuration` - -**Duration**: -- **Standard reset**: `RESET_TIME` (≥166 μs) - long enough for all bus activity to settle -- **Short reset**: `SHORT_RESET_TIME` (≥1.28 μs) - after arbitration - -**Why RESET_TIME is long**: -- Must exceed worst-case packet transmission time -- Must exceed worst-case bus turnaround time -- Ensures all nodes detect the reset signal - -**Exit Condition**: `arbTimer >= resetDuration` → Transition R0:R1 - ---- - -### R1: Reset Wait State - -**Purpose**: Node sends IDLE signals and waits for all active ports to receive IDLE or PARENT_NOTIFY - -**Signals Sent**: -- **IDLE**: Standard quiescent signal -- **PARENT_NOTIFY**: Indicates connected PHYs have left R0: Reset Start - -**Exit Conditions**: - -1. **R1:T0** - Normal completion: - - All connected ports receiving IDLE or PARENT_NOTIFY - - `resetComplete() = TRUE` - - `arbTimer = 0` - - **Proceeds to Tree ID process** (see §16.4.6) - -2. **R1:R0** - Timeout: - - Waited too long (`arbTimer >= resetDuration + RESET_WAIT`) - - Could be transient condition (multiple nodes being reset) - - **Returns to R0: Reset Start** and tries again - -**Timeout Period**: Slightly longer than R0:R1 timeout to avoid oscillation between two nodes - ---- - -## Self-Identification Process - -Per IEEE 1394-1995 §8.4.6: - -### Overview - -After tree identification completes (T0 → Self-ID states), each node broadcasts its capabilities and port connectivity in ascending node ID order (0 → 62). - -```mermaid -sequenceDiagram - participant Root as Root Node (ID=2) - participant Node1 as Node 1 - participant Node0 as Node 0 - participant Bus as FireWire Bus - - Note over Root,Bus: Tree ID Complete - - Node0->>Bus: Self-ID Packet 0 (ID=0) - Node0->>Bus: Self-ID Packet 1+ (if >3 ports) - - Node1->>Bus: Self-ID Packet 0 (ID=1) - Node1->>Bus: Self-ID Packet 1+ (if >3 ports) - - Root->>Bus: Self-ID Packet 0 (ID=2) - Root->>Bus: Self-ID Packet 1+ (if >3 ports) - - Note over Root,Bus: Self-ID Complete - Note over Root,Bus: Enter A0: Idle State -``` - -### Self-ID Packet Format - -#### Packet 0 (Mandatory) - -Per IEEE 1394-1995 §8.4.6.2.4: - -| Bits | Field | Description | -|------|-------|-------------| -| 31-30 | `10` | Packet identifier (Self-ID) | -| 29-24 | `phy_ID` | Physical node ID (0-62) | -| 23 | `L` | **Link active** bit | -| 22 | `gap_cnt_master` | Gap count master capability | -| 21-16 | `gap_cnt` | Gap count value (0-63) | -| 15-14 | `sp` | Speed capability (00=S100, 01=S200, 10=S400) | -| 13-11 | `000` | Reserved | -| 10 | `c` | **Contender bit** (IRM candidate) | -| 9-8 | `pwr` | Power class | -| 7-6 | `00` | Reserved | -| 5-3 | `p0..p2` | Port status (ports 0-2) | -| 2 | `r` | Reserved | -| 1 | `m` | More packets indicator | -| 0 | `i` | **Initiated reset** flag | - -**Example Packet 0**: -``` -Bits: 10 NNNNNN L G GGGGGG SP 000 C PP 00 PPP R M I -Value: 10 000010 1 0 001000 10 000 1 00 00 011 0 0 0 - ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ - | | | | | | | | | | | | | └─ Initiated: No - | | | | | | | | | | | | └─── More: No - | | | | | | | | | | | └───── Reserved - | | | | | | | | | | └───────── Ports 0-2: 011 - | | | | | | | | | └──────────── Reserved - | | | | | | | | └─────────────── Power: 00 - | | | | | | | └───────────────── Contender: Yes - | | | | | | └───────────────────── Reserved - | | | | | └──────────────────────── Speed: S400 - | | | | └─────────────────────────────── Gap count: 8 - | | | └───────────────────────────────── Gap master: No - | | └─────────────────────────────────── Link active: Yes - | └────────────────────────────────────────── Node ID: 2 - └───────────────────────────────────────────── Self-ID packet -``` - -#### Packet 1+ (Extended Port Info) - -For nodes with >3 ports: - -| Bits | Field | Description | -|------|-------|-------------| -| 31-30 | `11` | More packets identifier | -| 29-24 | `phy_ID` | Physical node ID (matches packet 0) | -| 23-22 | `pa` | Port a status | -| 21-20 | `pb` | Port b status | -| 19-18 | `pc` | Port c status | -| 17-16 | `pd` | Port d status | -| 15-14 | `pe` | Port e status | -| 13-12 | `pf` | Port f status | -| 11-10 | `pg` | Port g status | -| 9-8 | `ph` | Port h status | -| 7-6 | `00` | Reserved | -| 5 | `n` | Sequence number | -| 4-2 | `000` | Reserved | -| 1 | `m` | More packets | -| 0 | `00` | Reserved | - -**Port Status Encoding**: -``` -00 = Not connected / not present -01 = Parent (connected to parent node) -10 = Child (connected to child node) -11 = Connected to another port on this node -``` - -### Self-ID Packet Sequence Example - -3-port hub (node ID 1) with all ports connected: - -``` -Packet 0: 10 000001 1 0 001000 10 000 1 00 00 101010 0 0 0 - Self-ID, ID=1, Link=1, Gap=8, S400, Contender=1, Ports[0-2]=child/parent/child -``` - -16-port switch (node ID 5) requires multiple packets: - -``` -Packet 0: 10 000101 1 ... [ports 0-2] ... 1 (more=1) -Packet 1: 11 000101 [ports 3-10, n=0] ... 1 (more=1) -Packet 2: 11 000101 [ports 11-15, n=1] ... 0 (more=0, last packet) -``` - ---- - -## Self-ID State Machine (S0-S4) - -Per IEEE 1394-2008 §16.4.7 (Figure 16-18): - -### Overview - -After Tree Identification completes, nodes enter the Self-ID state machine to broadcast their physical layer capabilities in ascending node ID order. This distributed protocol ensures deterministic packet transmission without centralized coordination. - -### State Machine Diagram - -```mermaid -stateDiagram-v2 - direction LR - - T2 --> S0: from T2: Parent Handshake
page 448 - - S0: S0: Self-ID Start - S0: self_ID_startActions() - S0: Wait for grant or packet - - S0 --> S1: S0:S1
root || portRArb[parentPort] == SELF_ID_GRANT - S0 --> S2: S0:S2
dataComingOn(parentPort) - S0 --> A0: to A0: Idle
page 453 - - S1: S1: Self-ID Grant - S1: self_ID_grantActions() - S1: Grant to lowest child - - S1 --> S2: S1:S2
dataComingOn(lowestUnidentifiedChild) - S1 --> S0: S1:S0
idleReceivePort - S1 --> S4: S1:S4
allChildPortsIdentified - - S2: S2: Self-ID Receive - S2: self_ID_receiveActions() - S2: Receive Self-ID packets - - S2 --> S0: S2:S0
portRArb[receivePort] == IDLE ||
SELF_ID_GRANT ||
dataComingOn(receivePort) - S2 --> S3: S2:S3
portRArb[receivePort] == IDENT_DONE - - S3: S3: Send Speed Capabilities - S3: Transmit speed signal - S3: arbTimer >= legacyTime(SPEED_SIGNAL_LENGTH) - - S3 --> S0: S3:S0
Timer expired - - S4: S4: Self-ID Transmit - S4: self_ID_transmitActions() - S4: Send own Self-ID packet(s) - - S4 --> A0_ping: S4:A0a
pingResponse - S4 --> A0_normal: S4:A0b
!pingResponse && conditions - - A0_ping: to A0: Idle (ping response)
page 453 - A0_normal: to A0: Idle (normal)
page 453 - - style S0 fill:#e3f2fd - style S1 fill:#fff9c4 - style S2 fill:#f3e5f5 - style S3 fill:#ffe0b2 - style S4 fill:#c8e6c9 - style A0_ping fill:#95e1d3 - style A0_normal fill:#95e1d3 -``` - -### State Descriptions - -#### S0: Self-ID Start - -**Purpose**: PHY waits for a grant from parent OR receives Self-ID packet from another node - -**Entry Conditions**: -- At start of self-identify process -- After finishing receiving a Self-ID packet and all children have not yet finished - -**State Actions**: `self_ID_startActions()` - -**Exit Transitions**: - -1. **S0:S1** - Received SELF_ID_GRANT: - ``` - Condition: root || portRArb[parentPort] == SELF_ID_GRANT - ``` - - If node is root, automatically proceed - - If non-root receives GRANT from parent - -2. **S0:S2** - Self-ID packet incoming from parent: - ``` - Condition: dataComingOn(parentPort) - ``` - - Another node (in different branch) is transmitting Self-ID - -3. **To A0: Idle** - Early termination (error cases) - ---- - -#### S1: Self-ID Grant - -**Purpose**: Node has permission to send Self-ID packet. Grants lowest numbered unidentified child or transmits own packet. - -**State Actions**: `self_ID_grantActions()` - -**Node Behavior**: -- If has unidentified children → send GRANT to lowest numbered child -- If no unidentified children OR is proxy for parent port → transmit own Self-ID -- Other connected ports receive DATA_PREFIX (warning of incoming packet) - -**Exit Transitions**: - -1. **S1:S2** - Receiving Self-ID from lowest child: - ``` - Condition: dataComingOn(lowestUnidentifiedChild)
- receivePort = lowestUnidentifiedChild - ``` - -2. **S1:S0** - Proxy transmission complete: - ``` - Condition: idleReceivePort - ``` - - Transmitted proxy Self-ID, return to S0 - -3. **S1:S4** - All children identified, transmit own packet: - ``` - Condition: allChildPortsIdentified - Action: if (!root && !betaMode[parentPort]) - portSpeed[parentPort] = portRSpeed[parentPort] - ``` - ---- - -#### S2: Self-ID Receive - -**Purpose**: Receive Self-ID packet(s) from bus and pass to link layer - -**State Actions**: `self_ID_receiveActions()` - -**Behavior**: -- Data symbols passed to link layer as PHY data indications -- Multiple Self-ID packets may be received -- Parent PHY monitors received speed signal when IDENT_DONE received from child -- Resynchronization delays may cause parent to miss child's speed signal - - Parent samples for up to 144ns (or more per PHY_DELAY) after IDENT_DONE - - Child sends speed for no more than 120ns from IDENT_DONE start -- If PHY gets IDENT_DONE from receive port: - - Flags port as identified - - If port in DS mode, starts sending speed capabilities signal - - Starts speed signaling timer - -**Exit Transitions**: - -1. **S2:S0** - Port goes idle or new packet starts: - ``` - Condition: portRArb[receivePort] == IDLE || - portRArb[receivePort] == SELF_ID_GRANT || - dataComingOn(receivePort) - ``` - - Continue self-identify with next child - - Guards against failure to observe IDLE signal - -2. **S2:S3** - Received IDENT_DONE: - ``` - Condition: portRArb[receivePort] == IDENT_DONE - Action: child_ID_complete[receivePort] = TRUE - portTSpeedRaw(receivePort, dsPortSpeed[receivePort]) - arbTimer = 0 - ``` - - Child completed Self-ID transmission - ---- - -#### S3: Send Speed Capabilities - -**Purpose**: If node capable of >S100 AND receiving port is DS mode, transmit speed capability signal - -**Transmission**: -- Duration: fixed time `SPEED_SIGNAL_LENGTH` -- Content: Speed capability signals for `SPEED_SIGNAL_LENGTH` -- Parent monitors received speed signal from child - -**Speed Negotiation**: -- Highest indicated speed recorded as `speedCapability` of parent -- After transmit, parent sends only IDLE to children - -**Exit Transition**: - -1. **S3:S0** - Timer expired: - ``` - Condition: arbTimer >= legacyTime(SPEED_SIGNAL_LENGTH) - Action: portTSpeedRaw(receivePort, S100) - if (!betaMode[receivePort]) - portSpeed[receivePort] = portSpeed[receivePort] - arbTimer = 0 - ``` - - Speed signaling complete, continue with next child - - `negotiatedSpeed` field in port register map set for DS-mode operation - ---- - -#### S4: Self-ID Transmit - -**Purpose**: Transmit own Self-ID packet(s) - -**State Actions**: `self_ID_transmitActions()` - -**Entry Scenarios**: -1. Part of self-identify process (all child ports flagged as identified) -2. Receipt of PHY ping packet (cancels pending Alpha link requests) - -**Behavior** (Normal Self-ID): -- All child ports flagged as identified → can send own Self-ID -- **Non-root node**: - - Sends IDENT_DONE to parent while simultaneously: - - Transmitting speed capability signal to parent - - Sending IDLE to children - - Speed signal transmitted for fixed duration `SPEED_SIGNAL_LENGTH` - - Monitors bus for speed capability from parent - - Highest indicated speed recorded as `speedCapability` of parent -- **Root node**: - - Sends only IDLE to children - - Children enter A0: Idle (§16.4.8) - - Children never start arbitration on DS ports until self-identify completes for all nodes - -**Child Behavior During Parent Transmission**: -- While transmitting IDENT_DONE (in S4), child monitors received speed from parent -- Child PHY transitions to A0: Idle when receives DATA_PREFIX from parent -- Parent PHY in S2: Self-ID Receive to receive self-ID packet(s) from child -- When parent receives IDENT_DONE from child, parent transitions to S3: Send Speed Capabilities - - In S3, parent transmits speed signal for 100ns to 120ns to indicate own capability - - Monitors received speed from child - - Highest indicated speed recorded as `speedCapability` of child -- After transmitting own speed signal, parent transitions to S0: Self-ID Start - -**Exit Transitions**: - -1. **S4:A0a** - Ping response: - ``` - Condition: pingResponse - ``` - - Entered A0: Idle as ping packet response - -2. **S4:A0b** - Normal completion: - ``` - Condition: self-ID packet transmitted && - !pingResponse && - (node is root || starts receiving new Self-ID packet) - ``` - - **If node is root**: - - All nodes now sending IDLE signals - - Gap timers eventually large enough to allow normal arbitration - - **If node starts receiving new Self-ID packet**: - - Packet will be Self-ID for parent node or another child of parent - - PHY transitions immediately out of A0: Idle into RX: Receive (§16.4.8) - - - **When parent port will operate in DS mode**: - - `negotiatedSpeed` field in port register map for parent port is set - ---- - -### Timing: Speed Signal Exchange - -Per IEEE 1394-2008 §16.4.7: - -```mermaid -sequenceDiagram - participant Child as Child PHY - participant Parent as Parent PHY - - Note over Child,Parent: Child in S4: Self-ID Transmit - - Child->>Parent: IDENT_DONE signal - Child->>Parent: Speed signal (100-120ns) - Child->>Child: Monitor parent speed - - Note over Parent: Receives IDENT_DONE - Note over Parent: Enter S3: Send Speed Capabilities - - Parent->>Child: Speed signal (100-120ns) - Parent->>Parent: Monitor child speed - - Note over Child,Parent: Record highest indicated speed - Note over Child,Parent: Set negotiatedSpeed in port register - - Parent->>Child: IDLE signal - Child->>Parent: IDLE signal - - Note over Parent: Transition to S0 - Note over Child: Transition to A0: Idle -``` - -**Critical Timing Constraints**: - -| Parameter | Value | Notes | -|-----------|-------|-------| -| **SPEED_SIGNAL_LENGTH** | 100-120 ns | Fixed transmission duration | -| **PHY_DELAY** | ≥144 ns | Parent sampling window | -| **Child signal duration** | ≤120 ns | From IDENT_DONE start | -| **Parent sample window** | ≤144 ns | After ID ENT_DONE | - -**Resynchronization Risk**: -- Delays in repeating packets may cause parent to miss child's speed signal -- Parent samples for extended period (144ns+) -- Child transmits for shorter period (120ns max) -- Ensures parent can capture child's speed capability - ---- - -### Self-ID Packet Transmission Order - -Per IEEE 1394-2008 §8.4.6: - -```mermaid -graph TD - A[S0: Lowest node ID waiting] --> B[S1: Receives GRANT] - B --> C{Has unidentified
children?} - C -->|Yes| D[Grant to lowest child] - C -->|No| E[S4: Transmit own packet] - - D --> F[S2: Receive child packet] - F --> G[S3: Speed exchange] - G --> A - - E --> H[Send IDENT_DONE] - H --> I{Is root?} - I -->|Yes| J[All nodes → A0: Idle] - I -->|No| K[Wait for parent packet] - K --> A - - style E fill:#c8e6c9 - style J fill:#95e1d3 -``` - -**Deterministic Order**: -1. Node 0 (lowest ID) transmits first -2. Node 1 transmits second -3. ... -4. Node N-1 (root, highest ID) transmits last - -**Tree Traversal**: -- Depth-first traversal of tree topology -- Leaves transmit before branches -- Root transmits last -- All nodes maintain ascending ID order - ---- - -### Transition Summary Table - -| From State | To State | Transition | Condition | Notes | -|-----------|---------|-----------|-----------|-------| -| T2 | S0 | - | Parent handshake complete | Entry from Tree ID | -| S0 | S1 | S0:S1 | `root \|\| SELF_ID_GRANT` | Permission to transmit | -| S0 | S2 | S0:S2 | `dataComingOn(parentPort)` | Packet from another branch | -| S0 | A0 | - | Early termination | Error recovery | -| S1 | S2 | S1:S2 | `dataComingOn(lowestChild)` | Receive from child | -| S1 | S0 | S1:S0 | `idleReceivePort` | Proxy packet complete | -| S1 | S4 | S1:S4 | `allChildPortsIdentified` | Ready to transmit | -| S2 | S0 | S2:S0 | `IDLE \|\| GRANT \|\| dataComingOn` | Continue with next | -| S2 | S3 | S2:S3 | `IDENT_DONE` | Child transmission done | -| S3 | S0 | S3:S0 | `arbTimer >= SPEED_SIGNAL_LENGTH` | Speed exchange complete | -| S4 | A0 | S4:A0a | `pingResponse` | Ping packet response | -| S4 | A0 | S4:A0b | Normal completion | Self-ID protocol complete | - ---- - -## Tree Identification - -IEEE 1394-2008 Figure 16-17 - -### Overview - -Tree Identification is the distributed election process that establishes parent-child relationships between nodes and selects the root node. This happens after bus reset (R1: Reset Wait) and before Self-ID. - -### Tree ID State Machine (T0-T3) - -```mermaid -stateDiagram-v2 - direction LR - - R1 --> T0: from R1: Reset Wait
page 446 - - T0: T0: Tree ID Start - T0: tree_ID_startActions() - T0: Wait for PARENT_NOTIFY - - T0 --> T1: T0:T1
forceRoot || arbTimer >= FORCE_ROOT_TIMEOUT
children == NPORT - T0 --> T0_loop: T0:T0
T0_timeout && arbTimer == configTimeout
loop = 1; PH_EVENT(PH_CONFIG_TIMEOUT) - - T1: T1: Child Handshake - T1: childHandshakeActions() - T1: Send CHILD_NOTIFY to parent - - T1 --> T2: T1:T2
childHandshakeComplete() - - T2: T2: Parent Handshake - T2: Wait for PARENT_HANDSHAKE - - T2 --> T3: T2:T3
!root && portRArb[parentPort] == ROOT_CONTENTION - T2 --> S0: T2:S0
root || portRArb[parentPort] == PARENT_HANDSHAKE
to S0: Self-ID Start - - T3: T3: Root Contention - T3: rootContendActions() - T3: Contention resolution - - T3 --> T2: T3:T2
portRArb[contention port] == IDLE
send PARENT_NOTIFY - T3 --> T1: T3:T1
portRArb[contention port] == PARENT_NOTIFY
become root - - style T0 fill:#e3f2fd - style T1 fill:#fff9c4 - style T2 fill:#f3e5f5 - style T3 fill:#ffe0b2 - style S0 fill:#c8e6c9 -``` - -### State Descriptions - -#### T0: Tree ID Start - -**Purpose**: Node waits to receive PARENT_NOTIFY signal from all but one of its active ports - -**Entry**: From R1: Reset Wait when bus reset complete - -**State Actions**: `tree_ID_startActions()` - -**Behavior**: -- When PARENT_NOTIFY is observed on a port, that port is marked as a **child port** - -**Exit Transitions**: - -1. **T0:T1** - Timeout or Force Root: - ``` - Condition: (forceRoot || arbTimer >= FORCE_ROOT_TIMEOUT) && - children == NPORT - ``` - - If loop of active ports exists on bus → configuration timeout occurs - - Sets `T0_timeout` flag - - All active ports in Beta mode forced back to P11: Untested state - - May directly result in bus initialization completion - - May allow loop-free build process to set appropriate Beta ports into P12: Loop Disabled state - - Allows fresh bus reset to complete - -2. **T0:T0** - Configuration Timeout Loop: - ``` - Condition: T0_timeout && arbTimer == configTimeout - Action: loop = 1 - PH_EVENT.indication(PH_CONFIG_TIMEOUT, 0, 0) - ``` - -**Transition T0:T1 Details**: -- Detects PARENT_NOTIFY on all but one port (or on all ports for root nodes) -- Leaf nodes (only one connected port) OR root nodes (PARENT_NOTIFY on all ports) take this transition immediately -- If `forceRoot` flag is set, test for all-but-one-port condition is delayed long enough so all other nodes will have transitioned to T1: Child Handshake -- All ports should be receiving PARENT_NOTIFY signal -- Extra delay happens when `forceRoot` is set (value is `FORCE_ROOT_TIMEOUT`) - ---- - -#### T1: Child Handshake - -**Purpose**: All ports labeled as child ports transmit CHILD_NOTIFY signal - -**State Actions**: `childHandshakeActions()` - -**Behavior**: -- Receipt of CHILD_NOTIFY signal allows nodes attached to this node's child port(s) to transition from T2: Parent Handshake to S0: Self-ID Start -- **Leaf nodes** have no children → exit immediately via T1:T2 transition -- If all ports are labeled child ports → node knows it is the **root** - -**Exit Transition**: - -1. **T1:T2** - Child notification complete: - ``` - Condition: All child ports stop sending PARENT_NOTIFY signals - Action: Wait to receive CHILD_HANDSHAKE signal on child ports - Node can now handshake with own parent - ``` - ---- - -#### T2: Parent Handshake - -**Purpose**: Node waits to receive PARENT_HANDSHAKE signal or handle ROOT_CONTENTION - -**Behavior**: -- Node is waiting to receive PARENT_HANDSHAKE signal from parent -- For DS connections, this is the result of node's parent sending PARENT_NOTIFY and parent's parent sending CHILD_NOTIFY signal -- Another way this state can exit: if node receives ROOT_CONTENTION signal from parent - -**Exit Transitions**: - -1. **T2:S0** - Parent handshake received: - ``` - Condition: root || portRArb[parentPort] == PARENT_HANDSHAKE - Action: Starts self-identify process sending IDLE signal - ``` - - Transition to S0: Self-ID Start state - - Also taken if node is root (doesn't have a parent) - -2. **T2:T3** - Root contention detected: - ``` - Condition: !root && portRArb[parentPort] == ROOT_CONTENTION - ``` - - Node receives PARENT_NOTIFY signal on same port it's sending PARENT_NOTIFY - - Merged signal interpreted as ROOT_CONTENTION - - Can happen for single pair of nodes only if each bids to make the other its parent - ---- - -#### T3: Root Contention - -**Purpose**: Handle root contention when two nodes both try to make each other the parent - -**State Actions**: `rootContendActions()` - -**Behavior**: -- Both nodes back off by sending IDLE signal, starting a timer, and picking a random bit -- If random bit is one, node waits longer than if zero -- When timer expires, node samples contention port once again - -**Exit Transitions**: - -1. **T3:T2** - Lost contention (become child): - ``` - Condition: portRArb[contention port] == IDLE at end of delay - Action: Send PARENT_NOTIFY signal - ``` - - If node took longer delay, it takes this path - - Allows node to exit T2: Parent Handshake state via Self-ID Start path - - Otherwise two nodes see ROOT_CONTENTION again and repeat process with new random bits - -2. **T3:T1** - Won contention (become root): - ``` - Condition: portRArb[contention port] == PARENT_NOTIFY at end of delay - Action: Other node already transitioned to T2: Parent Handshake - First node returns to T1: Child Handshake and becomes root - ``` - ---- - -### Tree ID Signals - -| Signal | Direction | Purpose | -|--------|-----------|---------| -| **PARENT_NOTIFY** | Child → Parent | "I acknowledge you as parent" | -| **CHILD_NOTIFY** | Parent → Child | "I acknowledge you as child" | -| **PARENT_HANDSHAKE** | Parent → Child | "Handshake complete, proceed to Self-ID" | -| **ROOT_CONTENTION** | Bidirectional | Both nodes trying to be children (collision) | -| **IDLE** | Any | Quiescent state, no active signaling | - -### Tree ID Timing Parameters - -| Parameter | Value | Purpose | -|-----------|-------|---------| -| **FORCE_ROOT_TIMEOUT** | Variable | Delay when `forceRoot` flag set | -| **CONFIG_TIMEOUT** | Variable | Loop detection timeout | -| **Contention backoff** | Random | Root contention resolution | - -### Tree Identification Process Flow - -```mermaid -sequenceDiagram - participant Leaf as Leaf Node - participant Branch as Branch Node - participant Root as Root Node (will be elected) - - Note over Leaf,Root: All nodes in T0: Tree ID Start - - Leaf->>Branch: PARENT_NOTIFY (one port only) - Note over Leaf: Enter T1: Child Handshake - - Branch->>Root: PARENT_NOTIFY (to designated parent) - Note over Branch: Waiting for PARENT_NOTIFY from all ports except one - - Root->>Root: Received PARENT_NOTIFY on all ports - Note over Root: Become root, enter T1: Child Handshake - - Root->>Branch: CHILD_NOTIFY - Note over Root: Enter T2: Parent Handshake - - Branch->>Leaf: CHILD_NOTIFY - Note over Branch: Enter T2: Parent Handshake - - Note over Branch: Wait for PARENT_HANDSHAKE - Note over Leaf: Wait for PARENT_HANDSHAKE - - Root->>Branch: PARENT_HANDSHAKE (root becomes parent) - Note over Root: Enter S0: Self-ID Start - - Branch->>Leaf: PARENT_HANDSHAKE - Note over Branch: Enter S0: Self-ID Start - - Note over Leaf: Enter S0: Self-ID Start - Note over Leaf,Root: Tree Identification Complete - Note over Leaf,Root: Proceed to Self-ID Process -``` - -### Node Roles After Tree ID - -**Root Node**: -- No parent port (all ports are children or disconnected) -- Highest physical ID in the topology -- Controls bus arbitration fairness -- Designated as node ID = `nodeCount - 1` - -**Branch Node**: -- One parent port, one or more child ports -- Intermediate in tree hierarchy - -**Leaf Node**: -- One parent port, no child ports -- Endpoints in tree hierarchy - -### Physical ID Assignment - -After tree identification, nodes proceed to Self-ID where physical IDs are assigned: - -| Position | Node ID | Description | -|----------|---------|-------------| -| Root | `nodeCount - 1` | Highest ID | -| Branch/Leaf | `0 ... nodeCount - 2` | Ascending from leaves | - -**Example Topology**: -``` - [Node 2] ← Root (ID assigned during Self-ID) - | - ┌─────┴─────┐ - | | - [Node 1] [Node 0] - -After Self-ID: - Node 0 (leaf) → ID = 0 - Node 1 (leaf) → ID = 1 - Node 2 (root) → ID = 2 -``` - -### Root Contention Example - -```mermaid -sequenceDiagram - participant NodeA as Node A - participant NodeB as Node B - - Note over NodeA,NodeB: Both in T2: Parent Handshake - - NodeA->>NodeB: PARENT_NOTIFY - NodeB->>NodeA: PARENT_NOTIFY - - Note over NodeA,NodeB: Both detect ROOT_CONTENTION - Note over NodeA,NodeB: Enter T3: Root Contention - - NodeA->>NodeA: Random bit = 0 (short delay) - NodeB->>NodeB: Random bit = 1 (long delay) - - NodeA->>NodeB: IDLE (backoff) - NodeB->>NodeA: IDLE (backoff) - - Note over NodeA: Short timer expires - NodeA->>NodeA: Sample port → sees IDLE - NodeA->>NodeB: PARENT_NOTIFY (become child) - Note over NodeA: T3:T2 transition - - Note over NodeB: Long timer expires - NodeB->>NodeB: Sample port → sees PARENT_NOTIFY - Note over NodeB: T3:T1 transition (become root) -``` - ---- - -## Bus Configuration - -### Isochronous Resource Manager (IRM) - -Per IEEE 1394-1995 §8.4.2.3: - -**Selection Criteria**: -1. Node with **contender bit = 1** (capable of being IRM) -2. **Highest physical ID** among contenders -3. If root is contender → root becomes IRM -4. If root is not contender → find highest contender ID - -**IRM Responsibilities**: -- Manage isochronous channel allocation (CSR `CHANNELS_AVAILABLE`) -- Manage isochronous bandwidth allocation (CSR `BANDWIDTH_AVAILABLE`) -- Accept IRM lock requests (compare-and-swap operations) - -**IRM Lock Protocol**: -```cpp -// IEEE 1394-1995 §8.3.2.3.5 -// Lock request to BUS_MANAGER_ID (0xFFC0003F) -transaction = LockRequest( - destination = BUS_MANAGER_ID, - offset = CSR_BUS_MANAGER_ID, - data_value = local_node_id, - arg_value = 0x3F // Bus manager ID -); - -if (response == RESP_COMPLETE && result == local_node_id) { - // Successfully became IRM -} else { - // Another node is IRM -} -``` - -### Bus Manager (BM) - -Per IEEE 1394-1995 §8.4.2.5: - -**Selection**: -- Node that successfully completes IRM lock becomes eligible -- May implement bus optimization (gap count, power management) -- Optional role (not all implementations support BM) - -**Bus Manager Functions**: -1. **Gap Count Optimization**: Adjust `gap_cnt` via PHY configuration packet -2. **Power Management**: Coordinate node power states -3. **Topology Optimization**: Force root node selection for performance - ---- - -## Timing Requirements - -### Critical Timing Parameters - -Per IEEE 1394-1995 Table 5-3 and §8.3.2.3: - -| Parameter | Symbol | Min | Typical | Max | Unit | -|-----------|--------|-----|---------|-----|------| -| **Bus Reset Time** | `RESET_TIME` | 166 | - | - | μs | -| **Short Reset Time** | `SHORT_RESET_TIME` | 1.28 | - | - | μs | -| **Reset Wait** | `RESET_WAIT` | - | - | 10 | ms | -| **Arbitration Reset Gap** | `ARB_RESET_GAP` | 2.173 | - | - | μs | -| **Subaction Gap** | `SUBACTION_GAP` | 10 | - | - | μs | -| **Data Prefix** | `DATA_PREFIX` | 0.48 | 0.64 | 0.80 | μs | -| **Data End** | `DATA_END` | 0.40 | 0.52 | 0.64 | μs | - -### State Timing Diagram - -```mermaid -gantt - title Bus Reset Timing Sequence - dateFormat X - axisFormat %L - - section PHY Layer - BUS_RESET Signal :active, 0, 166 - IDLE Signal :166, 200 - - section State Machine - R0: Reset Start :crit, 0, 166 - R1: Reset Wait :166, 200 - T0: Tree ID Start :200, 250 - Self-ID Process :250, 350 - - section Bus Recovery - A0: Idle Arbitration :350, 400 -``` - -### Gap Count Timing Impact - -Per IEEE 1394a-2000 Annex C (Table C-2): - -**Gap Count Formula**: -``` -gap_time = gap_count × base_rate - -Where: - base_rate = 48.8 ns (per subaction gap) - gap_count = 0-63 (6-bit value) - -Example: - gap_count = 63 → 3.074 μs - gap_count = 8 → 390.4 ns -``` - -**Bandwidth Impact**: -``` -overhead_per_packet = gap_count × 48.8 ns -packet_rate = 8000 packets/sec (isochronous) - -Total overhead = 8000 × (gap_count × 48.8 ns) - -gap_count = 63: 24.6 ms/sec (2.46% overhead) -gap_count = 8: 3.1 ms/sec (0.31% overhead) -``` - ---- - -## PHY Configuration Packets - -Per IEEE 1394-1995 §8.4.6.3: - -### Purpose - -Allow bus manager or IRM to optimize bus parameters after Self-ID. - -### Packet Format - -``` -Bits Field Description -31-30 00 PHY packet identifier -29-24 root_ID Force root node (if R=1) -23 R Force root bit -22 T Gap count valid bit -21-16 gap_cnt Gap count value (0-63) -15-0 reserved Reserved (set to 0) -``` - -**Encoding Example**: -```cpp -uint32_t EncodePhyConfig(uint8_t root_id, uint8_t gap_count) { - uint32_t packet = 0x00000000; // PHY packet ID - - // Set force root - packet |= (1u << 23); // R = 1 - packet |= ((root_id & 0x3F) << 24); // root_ID - - // Set gap count - packet |= (1u << 22); // T = 1 - packet |= ((gap_count & 0x3F) << 16); // gap_cnt - - return packet; -} -``` - -### Transmission Timing - -Per IEEE 1394-1995 §8.4.6.3: - -**Constraints**: -1. Must be sent **after** Self-ID complete -2. Must be sent **before** arbitration begins -3. All nodes must process PHY config before normal traffic - -**Sequence**: -```mermaid -sequenceDiagram - participant IRM - participant Root as Root Node - participant Node as Other Nodes - participant Bus - - Note over IRM,Bus: Self-ID Complete - - IRM->>Bus: PHY Config Packet
(gap_cnt=8, root_ID=2) - - Note over Root,Node: All nodes update gap count - Note over Root: May trigger bus reset if root_ID != self - - Root->>Bus: BUS_RESET (if forced to become root) - - Note over IRM,Bus: Bus Reset (short) - Note over IRM,Bus: Tree ID + Self-ID - Note over IRM,Bus: Normal Traffic Resumes -``` - -### Force Root Behavior - -When `R = 1` in PHY config: - -```cpp -// Node receives PHY config packet -if (packet.R == 1 && packet.root_ID == my_physical_ID) { - // I am designated as root - if (current_role != ROOT) { - // Initiate short bus reset - InitiateBusReset(SHORT_RESET); - - // In next tree ID, this node will win - // (force all ports to be children) - } -} else if (packet.R == 1) { - // Another node is designated root - // Defer in tree ID algorithm -} -``` - -**Effect**: Designated node forces all its ports to be parent ports during next tree ID, guaranteeing it becomes root. - ---- - -## Error Handling - -### Timeout Recovery - -Per IEEE 1394a-2000 §16.4.5: - -#### R1:R0 Timeout - -**Condition**: `arbTimer >= resetDuration + RESET_WAIT` - -**Action**: Return to R0: Reset Start - -**Reason**: -- Possible transient condition (cables being inserted) -- Multiple nodes in reset simultaneously -- Retry with fresh BUS_RESET signal - -**Avoid Oscillation**: `RESET_WAIT` timeout is **longer** than R0:R1 timeout to prevent two nodes from bouncing between R0 and R1. - -#### Arbitration State Timeout (All:R0c) - -**Condition**: Stayed in A0: Idle for `MAX_ARB_STATE_TIME` - -**Trigger**: Local request pending (from link or PHY) - -**Action**: -``` -1. Set initiatedReset = TRUE -2. Set resetDuration = RESET_TIME -3. Generate PH_EVENT.indication(PH_MAX_ARB_STATE_TIMEOUT) -4. Transition to R0: Reset Start -``` - -**Purpose**: Break deadlock in arbitration state - -**Example Scenario**: -- IRM lock failed -- Bus manager trying to send PHY config -- Other nodes not granting bus access -- Timeout ensures forward progress - -### Self-ID CRC Errors - -Per IEEE 1394-1995 §8.4.6.2.4: - -**Detection**: Each Self-ID packet includes CRC-8 - -**Recovery**: -1. Node detects CRC error in received Self-ID -2. Discard corrupted packet -3. Request bus reset (goto R0: Reset Start) -4. Retry topology discovery - -**Implementation** (OHCI): -```cpp -std::optional Decode() { - // Validate CRC for each quadlet - for (auto quad : selfIDQuads) { - uint8_t receivedCRC = quad & 0xFF; - uint8_t calculatedCRC = CalculateCRC8(quad >> 8); - - if (receivedCRC != calculatedCRC) { - result.crcError = true; - return std::nullopt; // Discard - } - } - - result.valid = true; - return result; -} -``` - -### Incomplete Self-ID Sequence - -**Scenario**: selfIDComplete IRQ fires but insufficient packets received - -**Detection**: -```cpp -uint32_t selfIDCountReg = hw.Read(kSelfIDCount); -uint32_t selfIDGeneration = selfIDCountReg & 0xFF; -uint32_t selfIDCount = (selfIDCountReg >> 16) & 0xFF; - -if (selfIDCount == 0) { - // No Self-ID packets - bus reset mid-sequence - return std::nullopt; -} -``` - -**Recovery**: Generation counter mismatch indicates racing reset → retry - ---- - -## IEEE 1394-1995 State Machine (Detailed) - -Based on provided images (Figure 16-16): - -### Complete State Diagram - -```mermaid -stateDiagram-v2 - direction LR - - [*] --> R0: All:R0a (resetDetected)
All:R0b (initiatedReset)
All:R0c (maxArbStateTimeout)
TX:R0 (arbitration) - - state R0 { - [*] --> ResetStart - ResetStart: resetStartActions() - ResetStart: Send BUS_RESET - ResetStart: resetDuration timer - } - - R0 --> R1: R0:R1
arbTimer >= resetDuration - - state R1 { - [*] --> ResetWait - ResetWait: resetWaitActions() - ResetWait: Send IDLE/PARENT_NOTIFY - ResetWait: Wait for all ports - } - - R1 --> R0: R1:R0
arbTimer >= (resetDuration + RESET_WAIT)
Timeout retry - - R1 --> T0: R1:T0
resetComplete() && arbTimer = 0
All ports signaled - - state T0 { - [*] --> TreeIDStart - TreeIDStart: Begin tree identification - TreeIDStart: See IEEE 1394a §16.4.6 - } - - T0 --> A0: Tree ID Complete - - state A0 { - [*] --> Idle - Idle: Normal arbitration - Idle: See IEEE 1394a §16.4.7 - } - - note right of R0 - resetDuration values: - - RESET_TIME (166μs): long reset - - SHORT_RESET_TIME (1.28μs): short reset - end note - - note right of R1 - RESET_WAIT: max 10ms - Prevents oscillation between - R0 and R1 states - end note -``` - -### Critical Transitions Detail - -#### Transition All:R0a - Power/Detected Reset - -**From**: Any state -**Priority**: Highest (preempts all transitions) -**Condition**: `BUS_RESET` signal detected on any active/resuming port - -**Actions**: -``` -arbPowerReset() -``` - -**Implementation**: -```cpp -void arbPowerReset() { - // IEEE 1394a-2000 §16.4.5 - initiatedReset = FALSE; - - // All ports marked disconnected - for (auto& port : ports) { - port.status = DISCONNECTED; - } - - // Enter R0: Reset Start - // Will transition through reset → tree ID → self ID - // Eventually reach A0: Idle as root and proxy_root -} -``` - -**Special Case**: On power-on, solitary node transitions through full sequence and enters A0: Idle as both root and proxy_root. - -#### Transition All:R0b - Local Initiated Reset - -**Triggers**: -- SBM (Serial Bus Management) requests long reset via `PH_CONTROL.request` -- PHY detects disconnect on senior port - -**Condition**: -```cpp -ibr && (!phyResponse || immediatePhyRequest) -``` - -Where: -- `ibr` = initiated bus reset flag -- `phyResponse` = PHY packet response pending -- `immediatePhyRequest` = immediate PHY request - -**Actions**: -``` -initiatedReset = TRUE -resetDuration = RESET_TIME // Full 166μs reset -``` - -**Wait**: Current state's actions must complete first - -#### Transition All:R0c - Arbitration Timeout - -**Trigger**: Stayed in A0: Idle too long with pending request - -**Full Condition**: -```cpp -maxArbStateTimeout() - -bool maxArbStateTimeout() { - return (idleArbStateTimeout == FALSE) && - (stayed_in_A0_for > MAX_ARB_STATE_TIME) && - (local_request_pending == TRUE); -} -``` - -**Actions**: -``` -initiatedReset = TRUE -resetDuration = RESET_TIME - -if (!timeout) { - timeout = TRUE - PH_EVENT.indication(PH_MAX_ARB_STATE_TIMEOUT, 0, 0) -} -``` - -**Purpose**: Recovery from arbitration deadlock - -**Reset Arbitration Timer**: Timer resets on exit from **all states**, including self-transitions (e.g., RX:RX) - -#### Transition TX:R0 - Short Reset After Arbitration - -**Trigger**: Won arbitration, `isbrOk` set, no packet to send - -**Condition**: -```cpp -arbitration_succeeded && isbrOk && !packet_exists -``` - -**Action**: Immediately begin short bus reset - -**resetDuration**: `SHORT_RESET_TIME` (1.28 μs) - -**Rationale**: Bus already in known state from arbitration, so abbreviated reset sufficient - ---- - -## Appendix: Timing Calculations - -### Gap Count Optimization Table - -Per IEEE 1394a-2000 Table C-2 (4.5m cables, 144ns PHY delay): - -| Max Hops | Optimal Gap Count | Gap Time (μs) | Round-Trip Time (μs) | -|----------|-------------------|---------------|----------------------| -| 0 (single node) | 63 | 3.074 | - | -| 1 | 5 | 0.244 | 0.433 | -| 2 | 7 | 0.342 | 0.721 | -| 3 | 8 | 0.390 | 1.009 | -| 4 | 10 | 0.488 | 1.297 | -| 5 | 11 | 0.537 | 1.585 | -| 6 | 13 | 0.634 | 1.873 | -| 7 | 14 | 0.683 | 2.161 | -| 8 | 16 | 0.781 | 2.449 | -| 16 | 32 | 1.562 | 4.897 | -| 25+ | 63 | 3.074 | - | - -### Bus Reset Latency Budget - -Typical reset sequence timing: - -``` -Component Duration Cumulative -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Hardware detects cable insertion ~10 μs 10 μs -PHY enters R0: Reset Start 0 μs 10 μs -BUS_RESET signal (RESET_TIME) 166 μs 176 μs -R0:R1 transition ~1 μs 177 μs -R1: Reset Wait (port settling) 5-50 μs 182-227 μs -Tree ID arbitration 10-100 μs 192-327 μs -Self-ID transmission (3 nodes) ~50 μs 242-377 μs -selfIDComplete IRQ → driver 10-50 μs 252-427 μs -OHCI selfIDComplete2 IRQ 5-20 μs 257-447 μs -Driver decode + topology build 100-200 μs 357-647 μs -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -TOTAL (IRQ → topology ready) ~10-25 ms -``` - -**Dominated By**: Hardware arbitration and BUS_RESET signal duration - ---- - -## Cross-References - -### Implementation Details - -For ASFWDriver implementation of this specification, see: - -- [Bus/README.md](README.md) - Complete implementation architecture -- [BusResetCoordinator](README.md#1-busresetcoordinator) - FSM implementation (9 states) -- [SelfIDCapture](README.md#2-selfidcapture) - DMA buffer management -- [TopologyManager](README.md#3-topologymanager) - Snapshot construction -- [BusManager](README.md#4-busmanager) - PHY config and root delegation -- [GapCountOptimizer](README.md#5-gapcountoptimizer) - Table C-2 implementation - -### IEEE Standards References - -- **IEEE 1394-2008**: Complete FireWire specification (consolidates 1394-1995, 1394a-2000, 1394b-2002) - - §8.3.2: Bus Reset - - §8.4.6: Self-Identification Process - - §8.4.6.2.4: Self-ID Packet Format - - §8.4.6.3: PHY Configuration Packets - - §16.4.5: Bus Reset State Machine (Figure 16-16) - - §16.4.6: Tree Identification - - §16.4.7: Self-identification State Machine (Figure 16-18) - - §16.4.8: Arbitration States - - Annex C: Gap Count Optimization (Table C-2) - -- **OHCI 1.1**: Host Controller Interface - - §11: Self-ID Receive - - §6.1.1: Bus Reset Interrupt Handling - - §7.2.3.2: Context Management - ---- - -## Summary - -Bus reset and self-identification are the foundational synchronization mechanisms in IEEE 1394, providing: - -1. **Topology Discovery**: Self-ID state machine broadcasts physical capabilities in deterministic order -2. **Node Addressing**: Distributed tree identification assigns unique physical IDs (0 to N-1) -3. **Speed Negotiation**: Parent-child speed capability exchange during S3/S4 states -4. **Bus Optimization**: Gap count optimization and root forcing improve performance -5. **Error Recovery**: Timeout mechanisms (R1:R0, All:R0c) ensure forward progress - -**State Machine Progression**: -``` -Power-On → R0: Reset Start → R1: Reset Wait → T0-T2: Tree ID → -S0-S4: Self-ID → A0: Idle (Normal Operation) -``` - -**Key Principle**: Distributed state machines where all nodes cooperate to establish coherent topology without centralized coordination. - -**Implementation Complexity**: Requires precise OHCI register sequencing, DMA management, FSM coordination, and sub-microsecond timing for speed signal exchange. - -**Performance Impact**: -- Gap count optimization: 8x bandwidth improvement in typical topologies -- Speed negotiation: Enables S100/S200/S400/S800 operation per link -- Self-ID overhead: ~50-200μs for typical 3-10 node networks - ---- - -*This documentation is based on **IEEE 1394-2008** specification with implementation details from ASFireWire Driver (ASFWDriver) for macOS DriverKit.* diff --git a/ASFWDriver/Bus/IEEE1394-BusReset.md.bak2 b/ASFWDriver/Bus/IEEE1394-BusReset.md.bak2 deleted file mode 100644 index 28d13f25..00000000 --- a/ASFWDriver/Bus/IEEE1394-BusReset.md.bak2 +++ /dev/null @@ -1,1624 +0,0 @@ -# IEEE 1394 Bus Reset Specification - -## Overview - -This document provides detailed coverage of **Bus Reset** and **Self-Identification** as defined in IEEE 1394-2008 specification. Bus reset is the fundamental mechanism for topology discovery, arbitration reset, and bus initialization in FireWire networks. - -**References:** -- IEEE 1394-2008: Complete specification (consolidates 1394-1995, 1394a-2000, 1394b-2002) - - - - -**Related Documentation:** See [README.md](README.md) for implementation details in ASFWDriver. - ---- - -## Table of Contents - -1. [Bus Reset Fundamentals](#bus-reset-fundamentals) -2. [Bus Reset Triggers](#bus-reset-triggers) -3. [Bus Reset State Machine](#bus-reset-state-machine) -4. [Self-Identification Process](#self-identification-process) -5. [Self-ID State Machine (S0-S4)](#self-id-state-machine-s0-s4) -6. [Tree Identification](#tree-identification) -7. [Bus Configuration](#bus-configuration) -8. [Timing Requirements](#timing-requirements) -9. [PHY Configuration Packets](#phy-configuration-packets) -10. [Error Handling](#error-handling) - ---- - -## Bus Reset Fundamentals - -### Purpose - -Bus reset serves three critical functions: - -1. **Topology Discovery**: All nodes broadcast their physical layer capabilities and port connectivity via Self-ID packets -2. **Arbitration Reset**: Clears all pending arbitration state, ensuring fair bus access after topology changes -3. **Node ID Assignment**: Assigns unique 6-bit physical IDs to all nodes based on tree topology - -### Key Concepts - -```mermaid -graph TD - A[Bus Reset Event] --> B[All nodes enter Reset State] - B --> C[Bus Arbitration] - C --> D[Root Node Identified] - D --> E[Self-ID Transmission] - E --> F[Tree ID Complete] - F --> G[Normal Operation] - - style A fill:#ff6b6b - style D fill:#4ecdc4 - style G fill:#95e1d3 -``` - -### Bus Reset Duration - -IEEE 1394-2008: - -| Parameter | Symbol | Value | Description | -|-----------|--------|-------|-------------| -| **Reset Time** | `RESET_TIME` | ≥166 μs | Minimum duration of BUS_RESET signal | -| **Short Reset** | `SHORT_RESET_TIME` | ≥1.28 μs | Abbreviated reset after arbitration | -| **Reset Wait** | `RESET_WAIT` | ≤10 ms | Maximum wait in R1: Reset Wait state | -| **Arbitration Timeout** | `ARB_STATE_TIMEOUT` | Variable | Based on topology depth | - ---- - -## Bus Reset Triggers - -### Hardware Triggers - -IEEE 1394-2008: - -```mermaid -flowchart LR - A[Power-On Reset] --> BR[Bus Reset] - B[Cable Hotplug] --> BR - C[Cable Disconnect] --> BR - D[PHY Register Write] --> BR - E[Senior Port Disconnect] --> BR - - style BR fill:#ff6b6b,color:#fff -``` - -### Software-Initiated Reset - -**Long Reset**: -- Triggered by Link Layer via `PH_CONTROL.request` with long reset parameter -- Forces complete bus re-initialization -- All nodes participate in Self-ID - -**Short Reset**: -- Triggered after successful arbitration -- Abbreviated reset sequence -- Only root node sends BUS_RESET -- Faster than long reset (~1.28 μs vs 166 μs) - -### PHY-Level Detection - -IEEE 1394-2008: - -```cpp -// Transition All:R0a (from IEEE 1394-2008 Figure 16-16) -// Entry point if PHY senses BUS_RESET on any active/resuming port -// or port waiting to attach -``` - -Conditions for `All:R0a` transition: -- BUS_RESET detected on **any** active port -- BUS_RESET on resuming port -- BUS_RESET on port attempting to attach -- **Highest priority** transition (preempts all other state transitions) - ---- - -## Bus Reset State Machine - -### State Definitions - -Figure 16-16 (Bus Reset State Machine): - -```mermaid -stateDiagram-v2 - [*] --> R0_ResetStart : All:R0a (powerReset)
All:R0b (initiatedReset)
All:R0c (maxArbStateTimeout)
TX:R0 (arbitration success) - - R0_ResetStart : R0: Reset Start - R0_ResetStart : resetStartActions() - R0_ResetStart : Send BUS_RESET signal - R0_ResetStart : Duration = resetDuration - - R0_ResetStart --> R1_ResetWait : R0:R1
arbTimer >= resetDuration - - R1_ResetWait : R1: Reset Wait - R1_ResetWait : resetWaitActions() - R1_ResetWait : Send IDLE or PARENT_NOTIFY - - R1_ResetWait --> R0_ResetStart : R1:R0
arbTimer >= (resetDuration + RESET_WAIT) - R1_ResetWait --> T0_TreeIDStart : R1:T0
resetComplete() && arbTimer = 0 - - T0_TreeIDStart : T0: Tree ID Start - T0_TreeIDStart : page 448 (IEEE 1394-2008) - - style R0_ResetStart fill:#ff6b6b,color:#fff - style R1_ResetWait fill:#ffd93d - style T0_TreeIDStart fill:#95e1d3 -``` - -### State Transitions (Detailed) - -#### All:R0a - Detected Bus Reset - -**Trigger**: PHY detects BUS_RESET on any active or resuming port - -**Actions**: -``` -resetDetected() -initiatedReset = FALSE -``` - -**Priority**: **Highest** - preempts any other transition - -#### All:R0b - Initiated Bus Reset (Local) - -**Trigger**: Link layer requests long reset OR PHY detects senior port disconnect - -**Conditions**: -- `SBM makes a PH_CONTROL.request that specifies a long reset`, OR -- `The PHY detects a disconnect on its senior port` - -**Actions**: -``` -ibr&& (!phyResponse || immediatePhyRequest) -initiatedReset = TRUE -resetDuration = RESET_TIME -``` - -**Wait**: Current state's actions must complete before transition - -#### All:R0c - Arbitration State Timeout - -**Trigger**: PHY stays in A0: Idle state with `idleArbStateTimeout` for too long - -**Conditions**: -- In A0: Idle state -- `idleArbStateTimeout = false` -- Stayed idle for `MAX_ARB_STATE_TIME` -- Local request pending (link or PHY) - -**Actions**: -``` -maxArbStateTimeout() -initiatedReset = TRUE -resetDuration = RESET_TIME -if (!timeout) { - timeout = TRUE - PH_EVENT.indication(PH_MAX_ARB_STATE_TIMEOUT, 0, 0) -} -``` - -**Purpose**: Prevents indefinite stalls in arbitration state - -#### TX:R0 - Arbitrated Reset (Short) - -**Trigger**: Node won arbitration and `isbrOk` variable is set - -**Conditions**: -- Arbitration succeeded -- `isbrOk = TRUE` -- No packet exists to transmit - -**Actions**: Short bus reset commences immediately - -**Duration**: `SHORT_RESET_TIME` (significantly shorter than `RESET_TIME`) - -**Note**: Bus already in known state after arbitration, so shorter reset is sufficient - ---- - -### R0: Reset Start State - -**Purpose**: Node sends BUS_RESET signal for a duration governed by `resetDuration` - -**Duration**: -- **Standard reset**: `RESET_TIME` (≥166 μs) - long enough for all bus activity to settle -- **Short reset**: `SHORT_RESET_TIME` (≥1.28 μs) - after arbitration - -**Why RESET_TIME is long**: -- Must exceed worst-case packet transmission time -- Must exceed worst-case bus turnaround time -- Ensures all nodes detect the reset signal - -**Exit Condition**: `arbTimer >= resetDuration` → Transition R0:R1 - ---- - -### R1: Reset Wait State - -**Purpose**: Node sends IDLE signals and waits for all active ports to receive IDLE or PARENT_NOTIFY - -**Signals Sent**: -- **IDLE**: Standard quiescent signal -- **PARENT_NOTIFY**: Indicates connected PHYs have left R0: Reset Start - -**Exit Conditions**: - -1. **R1:T0** - Normal completion: - - All connected ports receiving IDLE or PARENT_NOTIFY - - `resetComplete() = TRUE` - - `arbTimer = 0` - - **Proceeds to Tree ID process** - -2. **R1:R0** - Timeout: - - Waited too long (`arbTimer >= resetDuration + RESET_WAIT`) - - Could be transient condition (multiple nodes being reset) - - **Returns to R0: Reset Start** and tries again - -**Timeout Period**: Slightly longer than R0:R1 timeout to avoid oscillation between two nodes - ---- - -## Self-Identification Process - - - -### Overview - -After tree identification completes (T0 → Self-ID states), each node broadcasts its capabilities and port connectivity in ascending node ID order (0 → 62). - -```mermaid -sequenceDiagram - participant Root as Root Node (ID=2) - participant Node1 as Node 1 - participant Node0 as Node 0 - participant Bus as FireWire Bus - - Note over Root,Bus: Tree ID Complete - - Node0->>Bus: Self-ID Packet 0 (ID=0) - Node0->>Bus: Self-ID Packet 1+ (if >3 ports) - - Node1->>Bus: Self-ID Packet 0 (ID=1) - Node1->>Bus: Self-ID Packet 1+ (if >3 ports) - - Root->>Bus: Self-ID Packet 0 (ID=2) - Root->>Bus: Self-ID Packet 1+ (if >3 ports) - - Note over Root,Bus: Self-ID Complete - Note over Root,Bus: Enter A0: Idle State -``` - -### Self-ID Packet Format - -#### Packet 0 (Mandatory) - - - -| Bits | Field | Description | -|------|-------|-------------| -| 31-30 | `10` | Packet identifier (Self-ID) | -| 29-24 | `phy_ID` | Physical node ID (0-62) | -| 23 | `L` | **Link active** bit | -| 22 | `gap_cnt_master` | Gap count master capability | -| 21-16 | `gap_cnt` | Gap count value (0-63) | -| 15-14 | `sp` | Speed capability (00=S100, 01=S200, 10=S400) | -| 13-11 | `000` | Reserved | -| 10 | `c` | **Contender bit** (IRM candidate) | -| 9-8 | `pwr` | Power class | -| 7-6 | `00` | Reserved | -| 5-3 | `p0..p2` | Port status (ports 0-2) | -| 2 | `r` | Reserved | -| 1 | `m` | More packets indicator | -| 0 | `i` | **Initiated reset** flag | - -**Example Packet 0**: -``` -Bits: 10 NNNNNN L G GGGGGG SP 000 C PP 00 PPP R M I -Value: 10 000010 1 0 001000 10 000 1 00 00 011 0 0 0 - ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ - | | | | | | | | | | | | | └─ Initiated: No - | | | | | | | | | | | | └─── More: No - | | | | | | | | | | | └───── Reserved - | | | | | | | | | | └───────── Ports 0-2: 011 - | | | | | | | | | └──────────── Reserved - | | | | | | | | └─────────────── Power: 00 - | | | | | | | └───────────────── Contender: Yes - | | | | | | └───────────────────── Reserved - | | | | | └──────────────────────── Speed: S400 - | | | | └─────────────────────────────── Gap count: 8 - | | | └───────────────────────────────── Gap master: No - | | └─────────────────────────────────── Link active: Yes - | └────────────────────────────────────────── Node ID: 2 - └───────────────────────────────────────────── Self-ID packet -``` - -#### Packet 1+ (Extended Port Info) - -For nodes with >3 ports: - -| Bits | Field | Description | -|------|-------|-------------| -| 31-30 | `11` | More packets identifier | -| 29-24 | `phy_ID` | Physical node ID (matches packet 0) | -| 23-22 | `pa` | Port a status | -| 21-20 | `pb` | Port b status | -| 19-18 | `pc` | Port c status | -| 17-16 | `pd` | Port d status | -| 15-14 | `pe` | Port e status | -| 13-12 | `pf` | Port f status | -| 11-10 | `pg` | Port g status | -| 9-8 | `ph` | Port h status | -| 7-6 | `00` | Reserved | -| 5 | `n` | Sequence number | -| 4-2 | `000` | Reserved | -| 1 | `m` | More packets | -| 0 | `00` | Reserved | - -**Port Status Encoding**: -``` -00 = Not connected / not present -01 = Parent (connected to parent node) -10 = Child (connected to child node) -11 = Connected to another port on this node -``` - -### Self-ID Packet Sequence Example - -3-port hub (node ID 1) with all ports connected: - -``` -Packet 0: 10 000001 1 0 001000 10 000 1 00 00 101010 0 0 0 - Self-ID, ID=1, Link=1, Gap=8, S400, Contender=1, Ports[0-2]=child/parent/child -``` - -16-port switch (node ID 5) requires multiple packets: - -``` -Packet 0: 10 000101 1 ... [ports 0-2] ... 1 (more=1) -Packet 1: 11 000101 [ports 3-10, n=0] ... 1 (more=1) -Packet 2: 11 000101 [ports 11-15, n=1] ... 0 (more=0, last packet) -``` - ---- - -## Self-ID State Machine (S0-S4) - -Figure 16-18 (Self-identification State Machine): - -### Overview - -After Tree Identification completes, nodes enter the Self-ID state machine to broadcast their physical layer capabilities in ascending node ID order. This distributed protocol ensures deterministic packet transmission without centralized coordination. - -### State Machine Diagram - -```mermaid -stateDiagram-v2 - direction LR - - T2 --> S0: from T2: Parent Handshake
page 448 - - S0: S0: Self-ID Start - S0: self_ID_startActions() - S0: Wait for grant or packet - - S0 --> S1: S0:S1
root || portRArb[parentPort] == SELF_ID_GRANT - S0 --> S2: S0:S2
dataComingOn(parentPort) - S0 --> A0: to A0: Idle
page 453 - - S1: S1: Self-ID Grant - S1: self_ID_grantActions() - S1: Grant to lowest child - - S1 --> S2: S1:S2
dataComingOn(lowestUnidentifiedChild) - S1 --> S0: S1:S0
idleReceivePort - S1 --> S4: S1:S4
allChildPortsIdentified - - S2: S2: Self-ID Receive - S2: self_ID_receiveActions() - S2: Receive Self-ID packets - - S2 --> S0: S2:S0
portRArb[receivePort] == IDLE ||
SELF_ID_GRANT ||
dataComingOn(receivePort) - S2 --> S3: S2:S3
portRArb[receivePort] == IDENT_DONE - - S3: S3: Send Speed Capabilities - S3: Transmit speed signal - S3: arbTimer >= legacyTime(SPEED_SIGNAL_LENGTH) - - S3 --> S0: S3:S0
Timer expired - - S4: S4: Self-ID Transmit - S4: self_ID_transmitActions() - S4: Send own Self-ID packet(s) - - S4 --> A0_ping: S4:A0a
pingResponse - S4 --> A0_normal: S4:A0b
!pingResponse && conditions - - A0_ping: to A0: Idle (ping response)
page 453 - A0_normal: to A0: Idle (normal)
page 453 - - style S0 fill:#e3f2fd - style S1 fill:#fff9c4 - style S2 fill:#f3e5f5 - style S3 fill:#ffe0b2 - style S4 fill:#c8e6c9 - style A0_ping fill:#95e1d3 - style A0_normal fill:#95e1d3 -``` - -### State Descriptions - -#### S0: Self-ID Start - -**Purpose**: PHY waits for a grant from parent OR receives Self-ID packet from another node - -**Entry Conditions**: -- At start of self-identify process -- After finishing receiving a Self-ID packet and all children have not yet finished - -**State Actions**: `self_ID_startActions()` - -**Exit Transitions**: - -1. **S0:S1** - Received SELF_ID_GRANT: - ``` - Condition: root || portRArb[parentPort] == SELF_ID_GRANT - ``` - - If node is root, automatically proceed - - If non-root receives GRANT from parent - -2. **S0:S2** - Self-ID packet incoming from parent: - ``` - Condition: dataComingOn(parentPort) - ``` - - Another node (in different branch) is transmitting Self-ID - -3. **To A0: Idle** - Early termination (error cases) - ---- - -#### S1: Self-ID Grant - -**Purpose**: Node has permission to send Self-ID packet. Grants lowest numbered unidentified child or transmits own packet. - -**State Actions**: `self_ID_grantActions()` - -**Node Behavior**: -- If has unidentified children → send GRANT to lowest numbered child -- If no unidentified children OR is proxy for parent port → transmit own Self-ID -- Other connected ports receive DATA_PREFIX (warning of incoming packet) - -**Exit Transitions**: - -1. **S1:S2** - Receiving Self-ID from lowest child: - ``` - Condition: dataComingOn(lowestUnidentifiedChild)
- receivePort = lowestUnidentifiedChild - ``` - -2. **S1:S0** - Proxy transmission complete: - ``` - Condition: idleReceivePort - ``` - - Transmitted proxy Self-ID, return to S0 - -3. **S1:S4** - All children identified, transmit own packet: - ``` - Condition: allChildPortsIdentified - Action: if (!root && !betaMode[parentPort]) - portSpeed[parentPort] = portRSpeed[parentPort] - ``` - ---- - -#### S2: Self-ID Receive - -**Purpose**: Receive Self-ID packet(s) from bus and pass to link layer - -**State Actions**: `self_ID_receiveActions()` - -**Behavior**: -- Data symbols passed to link layer as PHY data indications -- Multiple Self-ID packets may be received -- Parent PHY monitors received speed signal when IDENT_DONE received from child -- Resynchronization delays may cause parent to miss child's speed signal - - Parent samples for up to 144ns (or more per PHY_DELAY) after IDENT_DONE - - Child sends speed for no more than 120ns from IDENT_DONE start -- If PHY gets IDENT_DONE from receive port: - - Flags port as identified - - If port in DS mode, starts sending speed capabilities signal - - Starts speed signaling timer - -**Exit Transitions**: - -1. **S2:S0** - Port goes idle or new packet starts: - ``` - Condition: portRArb[receivePort] == IDLE || - portRArb[receivePort] == SELF_ID_GRANT || - dataComingOn(receivePort) - ``` - - Continue self-identify with next child - - Guards against failure to observe IDLE signal - -2. **S2:S3** - Received IDENT_DONE: - ``` - Condition: portRArb[receivePort] == IDENT_DONE - Action: child_ID_complete[receivePort] = TRUE - portTSpeedRaw(receivePort, dsPortSpeed[receivePort]) - arbTimer = 0 - ``` - - Child completed Self-ID transmission - ---- - -#### S3: Send Speed Capabilities - -**Purpose**: If node capable of >S100 AND receiving port is DS mode, transmit speed capability signal - -**Transmission**: -- Duration: fixed time `SPEED_SIGNAL_LENGTH` -- Content: Speed capability signals for `SPEED_SIGNAL_LENGTH` -- Parent monitors received speed signal from child - -**Speed Negotiation**: -- Highest indicated speed recorded as `speedCapability` of parent -- After transmit, parent sends only IDLE to children - -**Exit Transition**: - -1. **S3:S0** - Timer expired: - ``` - Condition: arbTimer >= legacyTime(SPEED_SIGNAL_LENGTH) - Action: portTSpeedRaw(receivePort, S100) - if (!betaMode[receivePort]) - portSpeed[receivePort] = portSpeed[receivePort] - arbTimer = 0 - ``` - - Speed signaling complete, continue with next child - - `negotiatedSpeed` field in port register map set for DS-mode operation - ---- - -#### S4: Self-ID Transmit - -**Purpose**: Transmit own Self-ID packet(s) - -**State Actions**: `self_ID_transmitActions()` - -**Entry Scenarios**: -1. Part of self-identify process (all child ports flagged as identified) -2. Receipt of PHY ping packet (cancels pending Alpha link requests) - -**Behavior** (Normal Self-ID): -- All child ports flagged as identified → can send own Self-ID -- **Non-root node**: - - Sends IDENT_DONE to parent while simultaneously: - - Transmitting speed capability signal to parent - - Sending IDLE to children - - Speed signal transmitted for fixed duration `SPEED_SIGNAL_LENGTH` - - Monitors bus for speed capability from parent - - Highest indicated speed recorded as `speedCapability` of parent -- **Root node**: - - Sends only IDLE to children - - Children enter A0: Idle - - Children never start arbitration on DS ports until self-identify completes for all nodes - -**Child Behavior During Parent Transmission**: -- While transmitting IDENT_DONE (in S4), child monitors received speed from parent -- Child PHY transitions to A0: Idle when receives DATA_PREFIX from parent -- Parent PHY in S2: Self-ID Receive to receive self-ID packet(s) from child -- When parent receives IDENT_DONE from child, parent transitions to S3: Send Speed Capabilities - - In S3, parent transmits speed signal for 100ns to 120ns to indicate own capability - - Monitors received speed from child - - Highest indicated speed recorded as `speedCapability` of child -- After transmitting own speed signal, parent transitions to S0: Self-ID Start - -**Exit Transitions**: - -1. **S4:A0a** - Ping response: - ``` - Condition: pingResponse - ``` - - Entered A0: Idle as ping packet response - -2. **S4:A0b** - Normal completion: - ``` - Condition: self-ID packet transmitted && - !pingResponse && - (node is root || starts receiving new Self-ID packet) - ``` - - **If node is root**: - - All nodes now sending IDLE signals - - Gap timers eventually large enough to allow normal arbitration - - **If node starts receiving new Self-ID packet**: - - Packet will be Self-ID for parent node or another child of parent - - PHY transitions immediately out of A0: Idle into RX: Receive - - - **When parent port will operate in DS mode**: - - `negotiatedSpeed` field in port register map for parent port is set - ---- - -### Timing: Speed Signal Exchange - -IEEE 1394-2008 self-ID timing: - -```mermaid -sequenceDiagram - participant Child as Child PHY - participant Parent as Parent PHY - - Note over Child,Parent: Child in S4: Self-ID Transmit - - Child->>Parent: IDENT_DONE signal - Child->>Parent: Speed signal (100-120ns) - Child->>Child: Monitor parent speed - - Note over Parent: Receives IDENT_DONE - Note over Parent: Enter S3: Send Speed Capabilities - - Parent->>Child: Speed signal (100-120ns) - Parent->>Parent: Monitor child speed - - Note over Child,Parent: Record highest indicated speed - Note over Child,Parent: Set negotiatedSpeed in port register - - Parent->>Child: IDLE signal - Child->>Parent: IDLE signal - - Note over Parent: Transition to S0 - Note over Child: Transition to A0: Idle -``` - -**Critical Timing Constraints**: - -| Parameter | Value | Notes | -|-----------|-------|-------| -| **SPEED_SIGNAL_LENGTH** | 100-120 ns | Fixed transmission duration | -| **PHY_DELAY** | ≥144 ns | Parent sampling window | -| **Child signal duration** | ≤120 ns | From IDENT_DONE start | -| **Parent sample window** | ≤144 ns | After ID ENT_DONE | - -**Resynchronization Risk**: -- Delays in repeating packets may cause parent to miss child's speed signal -- Parent samples for extended period (144ns+) -- Child transmits for shorter period (120ns max) -- Ensures parent can capture child's speed capability - ---- - -### Self-ID Packet Transmission Order - - - -```mermaid -graph TD - A[S0: Lowest node ID waiting] --> B[S1: Receives GRANT] - B --> C{Has unidentified
children?} - C -->|Yes| D[Grant to lowest child] - C -->|No| E[S4: Transmit own packet] - - D --> F[S2: Receive child packet] - F --> G[S3: Speed exchange] - G --> A - - E --> H[Send IDENT_DONE] - H --> I{Is root?} - I -->|Yes| J[All nodes → A0: Idle] - I -->|No| K[Wait for parent packet] - K --> A - - style E fill:#c8e6c9 - style J fill:#95e1d3 -``` - -**Deterministic Order**: -1. Node 0 (lowest ID) transmits first -2. Node 1 transmits second -3. ... -4. Node N-1 (root, highest ID) transmits last - -**Tree Traversal**: -- Depth-first traversal of tree topology -- Leaves transmit before branches -- Root transmits last -- All nodes maintain ascending ID order - ---- - -### Transition Summary Table - -| From State | To State | Transition | Condition | Notes | -|-----------|---------|-----------|-----------|-------| -| T2 | S0 | - | Parent handshake complete | Entry from Tree ID | -| S0 | S1 | S0:S1 | `root \|\| SELF_ID_GRANT` | Permission to transmit | -| S0 | S2 | S0:S2 | `dataComingOn(parentPort)` | Packet from another branch | -| S0 | A0 | - | Early termination | Error recovery | -| S1 | S2 | S1:S2 | `dataComingOn(lowestChild)` | Receive from child | -| S1 | S0 | S1:S0 | `idleReceivePort` | Proxy packet complete | -| S1 | S4 | S1:S4 | `allChildPortsIdentified` | Ready to transmit | -| S2 | S0 | S2:S0 | `IDLE \|\| GRANT \|\| dataComingOn` | Continue with next | -| S2 | S3 | S2:S3 | `IDENT_DONE` | Child transmission done | -| S3 | S0 | S3:S0 | `arbTimer >= SPEED_SIGNAL_LENGTH` | Speed exchange complete | -| S4 | A0 | S4:A0a | `pingResponse` | Ping packet response | -| S4 | A0 | S4:A0b | Normal completion | Self-ID protocol complete | - ---- - -## Tree Identification - -IEEE 1394-2008 Figure 16-17 - -### Overview - -Tree Identification is the distributed election process that establishes parent-child relationships between nodes and selects the root node. This happens after bus reset (R1: Reset Wait) and before Self-ID. - -### Tree ID State Machine (T0-T3) - -```mermaid -stateDiagram-v2 - direction LR - - R1 --> T0: from R1: Reset Wait
page 446 - - T0: T0: Tree ID Start - T0: tree_ID_startActions() - T0: Wait for PARENT_NOTIFY - - T0 --> T1: T0:T1
forceRoot || arbTimer >= FORCE_ROOT_TIMEOUT
children == NPORT - T0 --> T0_loop: T0:T0
T0_timeout && arbTimer == configTimeout
loop = 1; PH_EVENT(PH_CONFIG_TIMEOUT) - - T1: T1: Child Handshake - T1: childHandshakeActions() - T1: Send CHILD_NOTIFY to parent - - T1 --> T2: T1:T2
childHandshakeComplete() - - T2: T2: Parent Handshake - T2: Wait for PARENT_HANDSHAKE - - T2 --> T3: T2:T3
!root && portRArb[parentPort] == ROOT_CONTENTION - T2 --> S0: T2:S0
root || portRArb[parentPort] == PARENT_HANDSHAKE
to S0: Self-ID Start - - T3: T3: Root Contention - T3: rootContendActions() - T3: Contention resolution - - T3 --> T2: T3:T2
portRArb[contention port] == IDLE
send PARENT_NOTIFY - T3 --> T1: T3:T1
portRArb[contention port] == PARENT_NOTIFY
become root - - style T0 fill:#e3f2fd - style T1 fill:#fff9c4 - style T2 fill:#f3e5f5 - style T3 fill:#ffe0b2 - style S0 fill:#c8e6c9 -``` - -### State Descriptions - -#### T0: Tree ID Start - -**Purpose**: Node waits to receive PARENT_NOTIFY signal from all but one of its active ports - -**Entry**: From R1: Reset Wait when bus reset complete - -**State Actions**: `tree_ID_startActions()` - -**Behavior**: -- When PARENT_NOTIFY is observed on a port, that port is marked as a **child port** - -**Exit Transitions**: - -1. **T0:T1** - Timeout or Force Root: - ``` - Condition: (forceRoot || arbTimer >= FORCE_ROOT_TIMEOUT) && - children == NPORT - ``` - - If loop of active ports exists on bus → configuration timeout occurs - - Sets `T0_timeout` flag - - All active ports in Beta mode forced back to P11: Untested state - - May directly result in bus initialization completion - - May allow loop-free build process to set appropriate Beta ports into P12: Loop Disabled state - - Allows fresh bus reset to complete - -2. **T0:T0** - Configuration Timeout Loop: - ``` - Condition: T0_timeout && arbTimer == configTimeout - Action: loop = 1 - PH_EVENT.indication(PH_CONFIG_TIMEOUT, 0, 0) - ``` - -**Transition T0:T1 Details**: -- Detects PARENT_NOTIFY on all but one port (or on all ports for root nodes) -- Leaf nodes (only one connected port) OR root nodes (PARENT_NOTIFY on all ports) take this transition immediately -- If `forceRoot` flag is set, test for all-but-one-port condition is delayed long enough so all other nodes will have transitioned to T1: Child Handshake -- All ports should be receiving PARENT_NOTIFY signal -- Extra delay happens when `forceRoot` is set (value is `FORCE_ROOT_TIMEOUT`) - ---- - -#### T1: Child Handshake - -**Purpose**: All ports labeled as child ports transmit CHILD_NOTIFY signal - -**State Actions**: `childHandshakeActions()` - -**Behavior**: -- Receipt of CHILD_NOTIFY signal allows nodes attached to this node's child port(s) to transition from T2: Parent Handshake to S0: Self-ID Start -- **Leaf nodes** have no children → exit immediately via T1:T2 transition -- If all ports are labeled child ports → node knows it is the **root** - -**Exit Transition**: - -1. **T1:T2** - Child notification complete: - ``` - Condition: All child ports stop sending PARENT_NOTIFY signals - Action: Wait to receive CHILD_HANDSHAKE signal on child ports - Node can now handshake with own parent - ``` - ---- - -#### T2: Parent Handshake - -**Purpose**: Node waits to receive PARENT_HANDSHAKE signal or handle ROOT_CONTENTION - -**Behavior**: -- Node is waiting to receive PARENT_HANDSHAKE signal from parent -- For DS connections, this is the result of node's parent sending PARENT_NOTIFY and parent's parent sending CHILD_NOTIFY signal -- Another way this state can exit: if node receives ROOT_CONTENTION signal from parent - -**Exit Transitions**: - -1. **T2:S0** - Parent handshake received: - ``` - Condition: root || portRArb[parentPort] == PARENT_HANDSHAKE - Action: Starts self-identify process sending IDLE signal - ``` - - Transition to S0: Self-ID Start state - - Also taken if node is root (doesn't have a parent) - -2. **T2:T3** - Root contention detected: - ``` - Condition: !root && portRArb[parentPort] == ROOT_CONTENTION - ``` - - Node receives PARENT_NOTIFY signal on same port it's sending PARENT_NOTIFY - - Merged signal interpreted as ROOT_CONTENTION - - Can happen for single pair of nodes only if each bids to make the other its parent - ---- - -#### T3: Root Contention - -**Purpose**: Handle root contention when two nodes both try to make each other the parent - -**State Actions**: `rootContendActions()` - -**Behavior**: -- Both nodes back off by sending IDLE signal, starting a timer, and picking a random bit -- If random bit is one, node waits longer than if zero -- When timer expires, node samples contention port once again - -**Exit Transitions**: - -1. **T3:T2** - Lost contention (become child): - ``` - Condition: portRArb[contention port] == IDLE at end of delay - Action: Send PARENT_NOTIFY signal - ``` - - If node took longer delay, it takes this path - - Allows node to exit T2: Parent Handshake state via Self-ID Start path - - Otherwise two nodes see ROOT_CONTENTION again and repeat process with new random bits - -2. **T3:T1** - Won contention (become root): - ``` - Condition: portRArb[contention port] == PARENT_NOTIFY at end of delay - Action: Other node already transitioned to T2: Parent Handshake - First node returns to T1: Child Handshake and becomes root - ``` - ---- - -### Tree ID Signals - -| Signal | Direction | Purpose | -|--------|-----------|---------| -| **PARENT_NOTIFY** | Child → Parent | "I acknowledge you as parent" | -| **CHILD_NOTIFY** | Parent → Child | "I acknowledge you as child" | -| **PARENT_HANDSHAKE** | Parent → Child | "Handshake complete, proceed to Self-ID" | -| **ROOT_CONTENTION** | Bidirectional | Both nodes trying to be children (collision) | -| **IDLE** | Any | Quiescent state, no active signaling | - -### Tree ID Timing Parameters - -| Parameter | Value | Purpose | -|-----------|-------|---------| -| **FORCE_ROOT_TIMEOUT** | Variable | Delay when `forceRoot` flag set | -| **CONFIG_TIMEOUT** | Variable | Loop detection timeout | -| **Contention backoff** | Random | Root contention resolution | - -### Tree Identification Process Flow - -```mermaid -sequenceDiagram - participant Leaf as Leaf Node - participant Branch as Branch Node - participant Root as Root Node (will be elected) - - Note over Leaf,Root: All nodes in T0: Tree ID Start - - Leaf->>Branch: PARENT_NOTIFY (one port only) - Note over Leaf: Enter T1: Child Handshake - - Branch->>Root: PARENT_NOTIFY (to designated parent) - Note over Branch: Waiting for PARENT_NOTIFY from all ports except one - - Root->>Root: Received PARENT_NOTIFY on all ports - Note over Root: Become root, enter T1: Child Handshake - - Root->>Branch: CHILD_NOTIFY - Note over Root: Enter T2: Parent Handshake - - Branch->>Leaf: CHILD_NOTIFY - Note over Branch: Enter T2: Parent Handshake - - Note over Branch: Wait for PARENT_HANDSHAKE - Note over Leaf: Wait for PARENT_HANDSHAKE - - Root->>Branch: PARENT_HANDSHAKE (root becomes parent) - Note over Root: Enter S0: Self-ID Start - - Branch->>Leaf: PARENT_HANDSHAKE - Note over Branch: Enter S0: Self-ID Start - - Note over Leaf: Enter S0: Self-ID Start - Note over Leaf,Root: Tree Identification Complete - Note over Leaf,Root: Proceed to Self-ID Process -``` - -### Node Roles After Tree ID - -**Root Node**: -- No parent port (all ports are children or disconnected) -- Highest physical ID in the topology -- Controls bus arbitration fairness -- Designated as node ID = `nodeCount - 1` - -**Branch Node**: -- One parent port, one or more child ports -- Intermediate in tree hierarchy - -**Leaf Node**: -- One parent port, no child ports -- Endpoints in tree hierarchy - -### Physical ID Assignment - -After tree identification, nodes proceed to Self-ID where physical IDs are assigned: - -| Position | Node ID | Description | -|----------|---------|-------------| -| Root | `nodeCount - 1` | Highest ID | -| Branch/Leaf | `0 ... nodeCount - 2` | Ascending from leaves | - -**Example Topology**: -``` - [Node 2] ← Root (ID assigned during Self-ID) - | - ┌─────┴─────┐ - | | - [Node 1] [Node 0] - -After Self-ID: - Node 0 (leaf) → ID = 0 - Node 1 (leaf) → ID = 1 - Node 2 (root) → ID = 2 -``` - -### Root Contention Example - -```mermaid -sequenceDiagram - participant NodeA as Node A - participant NodeB as Node B - - Note over NodeA,NodeB: Both in T2: Parent Handshake - - NodeA->>NodeB: PARENT_NOTIFY - NodeB->>NodeA: PARENT_NOTIFY - - Note over NodeA,NodeB: Both detect ROOT_CONTENTION - Note over NodeA,NodeB: Enter T3: Root Contention - - NodeA->>NodeA: Random bit = 0 (short delay) - NodeB->>NodeB: Random bit = 1 (long delay) - - NodeA->>NodeB: IDLE (backoff) - NodeB->>NodeA: IDLE (backoff) - - Note over NodeA: Short timer expires - NodeA->>NodeA: Sample port → sees IDLE - NodeA->>NodeB: PARENT_NOTIFY (become child) - Note over NodeA: T3:T2 transition - - Note over NodeB: Long timer expires - NodeB->>NodeB: Sample port → sees PARENT_NOTIFY - Note over NodeB: T3:T1 transition (become root) -``` - ---- - -## Bus Configuration - -### Isochronous Resource Manager (IRM) - -IRM Selection: - -**Selection Criteria**: -1. Node with **contender bit = 1** (capable of being IRM) -2. **Highest physical ID** among contenders -3. If root is contender → root becomes IRM -4. If root is not contender → find highest contender ID - -**IRM Responsibilities**: -- Manage isochronous channel allocation (CSR `CHANNELS_AVAILABLE`) -- Manage isochronous bandwidth allocation (CSR `BANDWIDTH_AVAILABLE`) -- Accept IRM lock requests (compare-and-swap operations) - -**IRM Lock Protocol**: -```cpp -// Lock request to BUS_MANAGER_ID -// Lock request to BUS_MANAGER_ID (0xFFC0003F) -transaction = LockRequest( - destination = BUS_MANAGER_ID, - offset = CSR_BUS_MANAGER_ID, - data_value = local_node_id, - arg_value = 0x3F // Bus manager ID -); - -if (response == RESP_COMPLETE && result == local_node_id) { - // Successfully became IRM -} else { - // Another node is IRM -} -``` - -### Bus Manager (BM) - -Bus Manager: - -**Selection**: -- Node that successfully completes IRM lock becomes eligible -- May implement bus optimization (gap count, power management) -- Optional role (not all implementations support BM) - -**Bus Manager Functions**: -1. **Gap Count Optimization**: Adjust `gap_cnt` via PHY configuration packet -2. **Power Management**: Coordinate node power states -3. **Topology Optimization**: Force root node selection for performance - ---- - -## Timing Requirements - -### Critical Timing Parameters - -IEEE 1394-2008 Timing Parameters: - -| Parameter | Symbol | Min | Typical | Max | Unit | -|-----------|--------|-----|---------|-----|------| -| **Bus Reset Time** | `RESET_TIME` | 166 | - | - | μs | -| **Short Reset Time** | `SHORT_RESET_TIME` | 1.28 | - | - | μs | -| **Reset Wait** | `RESET_WAIT` | - | - | 10 | ms | -| **Arbitration Reset Gap** | `ARB_RESET_GAP` | 2.173 | - | - | μs | -| **Subaction Gap** | `SUBACTION_GAP` | 10 | - | - | μs | -| **Data Prefix** | `DATA_PREFIX` | 0.48 | 0.64 | 0.80 | μs | -| **Data End** | `DATA_END` | 0.40 | 0.52 | 0.64 | μs | - -### State Timing Diagram - -```mermaid -gantt - title Bus Reset Timing Sequence - dateFormat X - axisFormat %L - - section PHY Layer - BUS_RESET Signal :active, 0, 166 - IDLE Signal :166, 200 - - section State Machine - R0: Reset Start :crit, 0, 166 - R1: Reset Wait :166, 200 - T0: Tree ID Start :200, 250 - Self-ID Process :250, 350 - - section Bus Recovery - A0: Idle Arbitration :350, 400 -``` - -### Gap Count Timing Impact - -IEEE 1394-2008 Table C-2: - -**Gap Count Formula**: -``` -gap_time = gap_count × base_rate - -Where: - base_rate = 48.8 ns (per subaction gap) - gap_count = 0-63 (6-bit value) - -Example: - gap_count = 63 → 3.074 μs - gap_count = 8 → 390.4 ns -``` - -**Bandwidth Impact**: -``` -overhead_per_packet = gap_count × 48.8 ns -packet_rate = 8000 packets/sec (isochronous) - -Total overhead = 8000 × (gap_count × 48.8 ns) - -gap_count = 63: 24.6 ms/sec (2.46% overhead) -gap_count = 8: 3.1 ms/sec (0.31% overhead) -``` - ---- - -## PHY Configuration Packets - -PHY Configuration Packets: - -### Purpose - -Allow bus manager or IRM to optimize bus parameters after Self-ID. - -### Packet Format - -``` -Bits Field Description -31-30 00 PHY packet identifier -29-24 root_ID Force root node (if R=1) -23 R Force root bit -22 T Gap count valid bit -21-16 gap_cnt Gap count value (0-63) -15-0 reserved Reserved (set to 0) -``` - -**Encoding Example**: -```cpp -uint32_t EncodePhyConfig(uint8_t root_id, uint8_t gap_count) { - uint32_t packet = 0x00000000; // PHY packet ID - - // Set force root - packet |= (1u << 23); // R = 1 - packet |= ((root_id & 0x3F) << 24); // root_ID - - // Set gap count - packet |= (1u << 22); // T = 1 - packet |= ((gap_count & 0x3F) << 16); // gap_cnt - - return packet; -} -``` - -### Transmission Timing - -PHY Configuration Packets: - -**Constraints**: -1. Must be sent **after** Self-ID complete -2. Must be sent **before** arbitration begins -3. All nodes must process PHY config before normal traffic - -**Sequence**: -```mermaid -sequenceDiagram - participant IRM - participant Root as Root Node - participant Node as Other Nodes - participant Bus - - Note over IRM,Bus: Self-ID Complete - - IRM->>Bus: PHY Config Packet
(gap_cnt=8, root_ID=2) - - Note over Root,Node: All nodes update gap count - Note over Root: May trigger bus reset if root_ID != self - - Root->>Bus: BUS_RESET (if forced to become root) - - Note over IRM,Bus: Bus Reset (short) - Note over IRM,Bus: Tree ID + Self-ID - Note over IRM,Bus: Normal Traffic Resumes -``` - -### Force Root Behavior - -When `R = 1` in PHY config: - -```cpp -// Node receives PHY config packet -if (packet.R == 1 && packet.root_ID == my_physical_ID) { - // I am designated as root - if (current_role != ROOT) { - // Initiate short bus reset - InitiateBusReset(SHORT_RESET); - - // In next tree ID, this node will win - // (force all ports to be children) - } -} else if (packet.R == 1) { - // Another node is designated root - // Defer in tree ID algorithm -} -``` - -**Effect**: Designated node forces all its ports to be parent ports during next tree ID, guaranteeing it becomes root. - ---- - -## Error Handling - -### Timeout Recovery - -IEEE 1394-2008: - -#### R1:R0 Timeout - -**Condition**: `arbTimer >= resetDuration + RESET_WAIT` - -**Action**: Return to R0: Reset Start - -**Reason**: -- Possible transient condition (cables being inserted) -- Multiple nodes in reset simultaneously -- Retry with fresh BUS_RESET signal - -**Avoid Oscillation**: `RESET_WAIT` timeout is **longer** than R0:R1 timeout to prevent two nodes from bouncing between R0 and R1. - -#### Arbitration State Timeout (All:R0c) - -**Condition**: Stayed in A0: Idle for `MAX_ARB_STATE_TIME` - -**Trigger**: Local request pending (from link or PHY) - -**Action**: -``` -1. Set initiatedReset = TRUE -2. Set resetDuration = RESET_TIME -3. Generate PH_EVENT.indication(PH_MAX_ARB_STATE_TIMEOUT) -4. Transition to R0: Reset Start -``` - -**Purpose**: Break deadlock in arbitration state - -**Example Scenario**: -- IRM lock failed -- Bus manager trying to send PHY config -- Other nodes not granting bus access -- Timeout ensures forward progress - -### Self-ID CRC Errors - - - -**Detection**: Each Self-ID packet includes CRC-8 - -**Recovery**: -1. Node detects CRC error in received Self-ID -2. Discard corrupted packet -3. Request bus reset (goto R0: Reset Start) -4. Retry topology discovery - -**Implementation** (OHCI): -```cpp -std::optional Decode() { - // Validate CRC for each quadlet - for (auto quad : selfIDQuads) { - uint8_t receivedCRC = quad & 0xFF; - uint8_t calculatedCRC = CalculateCRC8(quad >> 8); - - if (receivedCRC != calculatedCRC) { - result.crcError = true; - return std::nullopt; // Discard - } - } - - result.valid = true; - return result; -} -``` - -### Incomplete Self-ID Sequence - -**Scenario**: selfIDComplete IRQ fires but insufficient packets received - -**Detection**: -```cpp -uint32_t selfIDCountReg = hw.Read(kSelfIDCount); -uint32_t selfIDGeneration = selfIDCountReg & 0xFF; -uint32_t selfIDCount = (selfIDCountReg >> 16) & 0xFF; - -if (selfIDCount == 0) { - // No Self-ID packets - bus reset mid-sequence - return std::nullopt; -} -``` - -**Recovery**: Generation counter mismatch indicates racing reset → retry - ---- - -## Bus Reset State Machine (Detailed) - -Based on provided images (Figure 16-16): - -### Complete State Diagram - -```mermaid -stateDiagram-v2 - direction LR - - [*] --> R0: All:R0a (resetDetected)
All:R0b (initiatedReset)
All:R0c (maxArbStateTimeout)
TX:R0 (arbitration) - - state R0 { - [*] --> ResetStart - ResetStart: resetStartActions() - ResetStart: Send BUS_RESET - ResetStart: resetDuration timer - } - - R0 --> R1: R0:R1
arbTimer >= resetDuration - - state R1 { - [*] --> ResetWait - ResetWait: resetWaitActions() - ResetWait: Send IDLE/PARENT_NOTIFY - ResetWait: Wait for all ports - } - - R1 --> R0: R1:R0
arbTimer >= (resetDuration + RESET_WAIT)
Timeout retry - - R1 --> T0: R1:T0
resetComplete() && arbTimer = 0
All ports signaled - - state T0 { - [*] --> TreeIDStart - TreeIDStart: Begin tree identification - TreeIDStart: See Tree Identification section - } - - T0 --> A0: Tree ID Complete - - state A0 { - [*] --> Idle - Idle: Normal arbitration - Idle: See IEEE 1394a §16.4.7 - } - - note right of R0 - resetDuration values: - - RESET_TIME (166μs): long reset - - SHORT_RESET_TIME (1.28μs): short reset - end note - - note right of R1 - RESET_WAIT: max 10ms - Prevents oscillation between - R0 and R1 states - end note -``` - -### Critical Transitions Detail - -#### Transition All:R0a - Power/Detected Reset - -**From**: Any state -**Priority**: Highest (preempts all transitions) -**Condition**: `BUS_RESET` signal detected on any active/resuming port - -**Actions**: -``` -arbPowerReset() -``` - -**Implementation**: -```cpp -void arbPowerReset() { - // IEEE 1394a-2000 §16.4.5 - initiatedReset = FALSE; - - // All ports marked disconnected - for (auto& port : ports) { - port.status = DISCONNECTED; - } - - // Enter R0: Reset Start - // Will transition through reset → tree ID → self ID - // Eventually reach A0: Idle as root and proxy_root -} -``` - -**Special Case**: On power-on, solitary node transitions through full sequence and enters A0: Idle as both root and proxy_root. - -#### Transition All:R0b - Local Initiated Reset - -**Triggers**: -- SBM (Serial Bus Management) requests long reset via `PH_CONTROL.request` -- PHY detects disconnect on senior port - -**Condition**: -```cpp -ibr && (!phyResponse || immediatePhyRequest) -``` - -Where: -- `ibr` = initiated bus reset flag -- `phyResponse` = PHY packet response pending -- `immediatePhyRequest` = immediate PHY request - -**Actions**: -``` -initiatedReset = TRUE -resetDuration = RESET_TIME // Full 166μs reset -``` - -**Wait**: Current state's actions must complete first - -#### Transition All:R0c - Arbitration Timeout - -**Trigger**: Stayed in A0: Idle too long with pending request - -**Full Condition**: -```cpp -maxArbStateTimeout() - -bool maxArbStateTimeout() { - return (idleArbStateTimeout == FALSE) && - (stayed_in_A0_for > MAX_ARB_STATE_TIME) && - (local_request_pending == TRUE); -} -``` - -**Actions**: -``` -initiatedReset = TRUE -resetDuration = RESET_TIME - -if (!timeout) { - timeout = TRUE - PH_EVENT.indication(PH_MAX_ARB_STATE_TIMEOUT, 0, 0) -} -``` - -**Purpose**: Recovery from arbitration deadlock - -**Reset Arbitration Timer**: Timer resets on exit from **all states**, including self-transitions (e.g., RX:RX) - -#### Transition TX:R0 - Short Reset After Arbitration - -**Trigger**: Won arbitration, `isbrOk` set, no packet to send - -**Condition**: -```cpp -arbitration_succeeded && isbrOk && !packet_exists -``` - -**Action**: Immediately begin short bus reset - -**resetDuration**: `SHORT_RESET_TIME` (1.28 μs) - -**Rationale**: Bus already in known state from arbitration, so abbreviated reset sufficient - ---- - -## Appendix: Timing Calculations - -### Gap Count Optimization Table - -Per IEEE 1394a-2000 Table C-2 (4.5m cables, 144ns PHY delay): - -| Max Hops | Optimal Gap Count | Gap Time (μs) | Round-Trip Time (μs) | -|----------|-------------------|---------------|----------------------| -| 0 (single node) | 63 | 3.074 | - | -| 1 | 5 | 0.244 | 0.433 | -| 2 | 7 | 0.342 | 0.721 | -| 3 | 8 | 0.390 | 1.009 | -| 4 | 10 | 0.488 | 1.297 | -| 5 | 11 | 0.537 | 1.585 | -| 6 | 13 | 0.634 | 1.873 | -| 7 | 14 | 0.683 | 2.161 | -| 8 | 16 | 0.781 | 2.449 | -| 16 | 32 | 1.562 | 4.897 | -| 25+ | 63 | 3.074 | - | - -### Bus Reset Latency Budget - -Typical reset sequence timing: - -``` -Component Duration Cumulative -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Hardware detects cable insertion ~10 μs 10 μs -PHY enters R0: Reset Start 0 μs 10 μs -BUS_RESET signal (RESET_TIME) 166 μs 176 μs -R0:R1 transition ~1 μs 177 μs -R1: Reset Wait (port settling) 5-50 μs 182-227 μs -Tree ID arbitration 10-100 μs 192-327 μs -Self-ID transmission (3 nodes) ~50 μs 242-377 μs -selfIDComplete IRQ → driver 10-50 μs 252-427 μs -OHCI selfIDComplete2 IRQ 5-20 μs 257-447 μs -Driver decode + topology build 100-200 μs 357-647 μs -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -TOTAL (IRQ → topology ready) ~10-25 ms -``` - -**Dominated By**: Hardware arbitration and BUS_RESET signal duration - ---- - -## Cross-References - -### Implementation Details - -For ASFWDriver implementation of this specification, see: - -- [Bus/README.md](README.md) - Complete implementation architecture -- [BusResetCoordinator](README.md#1-busresetcoordinator) - FSM implementation (9 states) -- [SelfIDCapture](README.md#2-selfidcapture) - DMA buffer management -- [TopologyManager](README.md#3-topologymanager) - Snapshot construction -- [BusManager](README.md#4-busmanager) - PHY config and root delegation -- [GapCountOptimizer](README.md#5-gapcountoptimizer) - Table C-2 implementation - -### IEEE Standards References - -- **IEEE 1394-2008**: Complete FireWire specification (consolidates 1394-1995, 1394a-2000, 1394b-2002) - - §8.3.2: Bus Reset - - §8.4.6: Self-Identification Process - - §8.4.6.2.4: Self-ID Packet Format - - §8.4.6.3: PHY Configuration Packets - - §16.4.5: Bus Reset State Machine (Figure 16-16) - - §16.4.6: Tree Identification - - §16.4.7: Self-identification State Machine (Figure 16-18) - - §16.4.8: Arbitration States - - Annex C: Gap Count Optimization (Table C-2) - -- **OHCI 1.1**: Host Controller Interface - - §11: Self-ID Receive - - §6.1.1: Bus Reset Interrupt Handling - - §7.2.3.2: Context Management - ---- - -## Summary - -Bus reset and self-identification are the foundational synchronization mechanisms in IEEE 1394, providing: - -1. **Topology Discovery**: Self-ID state machine broadcasts physical capabilities in deterministic order -2. **Node Addressing**: Distributed tree identification assigns unique physical IDs (0 to N-1) -3. **Speed Negotiation**: Parent-child speed capability exchange during S3/S4 states -4. **Bus Optimization**: Gap count optimization and root forcing improve performance -5. **Error Recovery**: Timeout mechanisms (R1:R0, All:R0c) ensure forward progress - -**State Machine Progression**: -``` -Power-On → R0: Reset Start → R1: Reset Wait → T0-T2: Tree ID → -S0-S4: Self-ID → A0: Idle (Normal Operation) -``` - -**Key Principle**: Distributed state machines where all nodes cooperate to establish coherent topology without centralized coordination. - -**Implementation Complexity**: Requires precise OHCI register sequencing, DMA management, FSM coordination, and sub-microsecond timing for speed signal exchange. - -**Performance Impact**: -- Gap count optimization: 8x bandwidth improvement in typical topologies -- Speed negotiation: Enables S100/S200/S400/S800 operation per link -- Self-ID overhead: ~50-200μs for typical 3-10 node networks - ---- - -*This documentation is based on **IEEE 1394-2008** specification with implementation details from ASFireWire Driver (ASFWDriver) for macOS DriverKit.* diff --git a/ASFWDriver/Bus/README.md b/ASFWDriver/Bus/README.md index ba6df5ce..718a252f 100644 --- a/ASFWDriver/Bus/README.md +++ b/ASFWDriver/Bus/README.md @@ -90,7 +90,8 @@ Gap handling is intentionally two-phase: Gap state is transactional: - `lastConfirmedGap` tracks the last stable packet-0 gap observed on an - accepted topology; + accepted topology, and remains `0xFF` until the first stable generation is + accepted so "unknown previous gap" is distinct from a confirmed `63`; - `inFlight` exists only after the coordinator successfully dispatches a corrective reset carrying a new gap target; - failed corrective dispatch clears `inFlight` and does not advance the diff --git a/ASFWDriver/Common/FWCommon.hpp b/ASFWDriver/Common/FWCommon.hpp index b0f9a125..58f54fc7 100644 --- a/ASFWDriver/Common/FWCommon.hpp +++ b/ASFWDriver/Common/FWCommon.hpp @@ -388,7 +388,10 @@ inline constexpr uint8_t kUnitDependentInfo = 0x14; inline constexpr uint8_t kUnitLocation = 0x15; inline constexpr uint8_t kUnitPollMask = 0x16; inline constexpr uint8_t kModelId = 0x17; -inline constexpr uint8_t kGeneration = 0x38; // Apple-specific +inline constexpr uint8_t kGeneration = 0x38; // Apple-specific (root dir, immediate) +inline constexpr uint8_t kManagementAgentOffset = 0x38; // SBP-2 (unit dir, CSR offset type=1) +inline constexpr uint8_t kUnitCharacteristics = 0x39; // SBP-2 (unit dir, immediate) +inline constexpr uint8_t kFastStart = 0x3A; // SBP-2 (unit dir, leaf) } // namespace ConfigKey // ============================================================================ diff --git a/ASFWDriver/ConfigROM/Parse/ConfigROMParser.cpp b/ASFWDriver/ConfigROM/Parse/ConfigROMParser.cpp index 861a3e23..79c39ec7 100644 --- a/ASFWDriver/ConfigROM/Parse/ConfigROMParser.cpp +++ b/ASFWDriver/ConfigROM/Parse/ConfigROMParser.cpp @@ -140,12 +140,27 @@ void ConfigROMParser::AppendRecognizedEntry(std::vector& entries, uint case 0x38: // Legacy non-standard fallback for Management_Agent_Offset if (keyType == EntryType::kCSROffset) { - entries.push_back(RomEntry{.key = CfgKey::Management_Agent_Offset, - .value = value, - .entryType = keyType}); + entries.push_back( + RomEntry{.key = CfgKey::Management_Agent_Offset, .value = value, .entryType = keyType}); + } + return; + + case 0x39: // Unit_Characteristics (SBP-2) — immediate + if (keyType == EntryType::kImmediate) { + entries.push_back( + RomEntry{.key = CfgKey::Unit_Characteristics, .value = value, .entryType = keyType}); } return; + case 0x3A: // Fast_Start (SBP-2) — leaf + if (keyType == EntryType::kLeaf && targetOffsetQuadlets != 0) { + entries.push_back( + RomEntry{.key = CfgKey::Fast_Start, + .value = value, + .entryType = keyType, + .leafOffsetQuadlets = targetOffsetQuadlets}); + } + return; default: return; } diff --git a/ASFWDriver/ConfigROM/Remote/ROMReader.cpp b/ASFWDriver/ConfigROM/Remote/ROMReader.cpp index 904cbfa3..4bb8ce1b 100644 --- a/ASFWDriver/ConfigROM/Remote/ROMReader.cpp +++ b/ASFWDriver/ConfigROM/Remote/ROMReader.cpp @@ -145,6 +145,28 @@ void ROMReader::ScheduleQuadletReadStep(const std::shared_ptr& ctx, Async::AsyncStatus status, std::span responsePayload) { + // DIAGNOSTIC: Log each quadlet read result for ROM discovery + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(ConfigROM, + "[DIAG] ROMReader node=%u qIdx=%u/%u FAILED status=%u (addr=0x%08x)", + ctx->nodeId, ctx->quadletIndex, ctx->quadletCount, + static_cast(status), + ctx->baseAddress + ctx->quadletIndex * 4); + } else if (IsValidQuadletPayload(responsePayload.size())) { + uint32_t q = 0; + std::memcpy(&q, responsePayload.data(), sizeof(q)); + ASFW_LOG(ConfigROM, + "[DIAG] ROMReader node=%u qIdx=%u/%u OK quadlet=0x%08x (addr=0x%08x)", + ctx->nodeId, ctx->quadletIndex, ctx->quadletCount, + q, ctx->baseAddress + ctx->quadletIndex * 4); + } else { + ASFW_LOG(ConfigROM, + "[DIAG] ROMReader node=%u qIdx=%u/%u BAD payload=%zu (addr=0x%08x)", + ctx->nodeId, ctx->quadletIndex, ctx->quadletCount, + responsePayload.size(), + ctx->baseAddress + ctx->quadletIndex * 4); + } + if (CanTreatAsEOF(ctx->policy, status, responsePayload.size(), ctx->successCount)) { EmitQuadletReadResult(ctx, /*success=*/true, status, ctx->successCount); return; @@ -340,6 +362,11 @@ void ROMReader::ReadRootDirQuadlets(uint8_t nodeId, Generation generation, FwSpe auto entryCount = static_cast((hdr >> 16) & 0xFFFFU); entryCount = ClampHeaderFirstEntryCount(entryCount); + ASFW_LOG(ConfigROM, + "[DIAG] ROMReader root dir header: raw=0x%08x host=0x%08x entryCount=%u " + "(node=%u offset=0x%x)", + hdrBe, hdr, entryCount, nodeId, offsetBytes); + if (entryCount == 0) { ReadResult out{}; out.success = true; diff --git a/ASFWDriver/ConfigROM/Remote/ROMScanNodeStateMachine.hpp b/ASFWDriver/ConfigROM/Remote/ROMScanNodeStateMachine.hpp index b5f1dc6e..a5b956df 100644 --- a/ASFWDriver/ConfigROM/Remote/ROMScanNodeStateMachine.hpp +++ b/ASFWDriver/ConfigROM/Remote/ROMScanNodeStateMachine.hpp @@ -10,6 +10,7 @@ class ROMScanNodeStateMachine { enum class State : uint8_t { Idle, ReadingBIB, + WaitingRepublish, VerifyingIRM_Read, VerifyingIRM_Lock, ReadingRootDir, @@ -34,14 +35,21 @@ class ROMScanNodeStateMachine { [[nodiscard]] State CurrentState() const { return state_; } [[nodiscard]] FwSpeed CurrentSpeed() const { return currentSpeed_; } [[nodiscard]] uint8_t RetriesLeft() const { return retriesLeft_; } + [[nodiscard]] uint8_t SlowPublishRetriesLeft() const { return slowPublishRetriesLeft_; } void SetCurrentSpeed(FwSpeed speed) { currentSpeed_ = speed; } void SetRetriesLeft(uint8_t retries) { retriesLeft_ = retries; } + void SetSlowPublishRetriesLeft(uint8_t retries) { slowPublishRetriesLeft_ = retries; } void DecrementRetries() { if (retriesLeft_ > 0) { --retriesLeft_; } } + void DecrementSlowPublishRetries() { + if (slowPublishRetriesLeft_ > 0) { + --slowPublishRetriesLeft_; + } + } [[nodiscard]] ConfigROM& MutableROM() { return partialROM_; } [[nodiscard]] const ConfigROM& ROM() const { return partialROM_; } @@ -71,15 +79,18 @@ class ROMScanNodeStateMachine { case Idle: return next == ReadingBIB || next == Failed; case ReadingBIB: - return next == VerifyingIRM_Read || next == ReadingRootDir || next == Complete || - next == Idle || next == Failed; + return next == WaitingRepublish || next == VerifyingIRM_Read || + next == ReadingRootDir || next == Complete || next == Idle || next == Failed; + case WaitingRepublish: + return next == Idle || next == ReadingRootDir || next == Failed; case VerifyingIRM_Read: return next == VerifyingIRM_Lock || next == ReadingRootDir || next == Complete || next == Failed; case VerifyingIRM_Lock: return next == ReadingRootDir || next == Complete || next == Failed; case ReadingRootDir: - return next == ReadingDetails || next == Complete || next == Failed || next == Idle; + return next == WaitingRepublish || next == ReadingDetails || next == Complete || + next == Failed || next == Idle; case ReadingDetails: return next == Complete || next == Failed; case Complete: @@ -115,6 +126,7 @@ class ROMScanNodeStateMachine { irmIsBad_ = false; irmBitBucket_ = 0xFFFFFFFF; bibInProgress_ = false; + slowPublishRetriesLeft_ = 0; } private: @@ -131,6 +143,7 @@ class ROMScanNodeStateMachine { uint32_t irmBitBucket_{0xFFFFFFFF}; bool bibInProgress_{false}; + uint8_t slowPublishRetriesLeft_{0}; }; } // namespace ASFW::Discovery diff --git a/ASFWDriver/ConfigROM/Remote/ROMScanSession.cpp b/ASFWDriver/ConfigROM/Remote/ROMScanSession.cpp index 7380df6b..1622de5d 100644 --- a/ASFWDriver/ConfigROM/Remote/ROMScanSession.cpp +++ b/ASFWDriver/ConfigROM/Remote/ROMScanSession.cpp @@ -15,6 +15,26 @@ namespace ASFW::Discovery { namespace { +constexpr uint32_t kNikonOui = 0x0090B5; +constexpr uint8_t kNikonSlowPublishRetryBudget = 4; +constexpr uint64_t kNikonSlowPublishDelayNs = 500ULL * 1'000'000ULL; + +[[nodiscard]] constexpr uint32_t ExtractOui(uint64_t guid) noexcept { + return static_cast((guid >> 40U) & 0xFFFFFFULL); +} + +[[nodiscard]] constexpr bool IsNikonGuid(uint64_t guid) noexcept { + return ExtractOui(guid) == kNikonOui; +} + +[[nodiscard]] constexpr bool IsMinimalROM(const BusInfoBlock& bib) noexcept { + return bib.crcLength <= bib.busInfoLength; +} + +[[nodiscard]] constexpr bool IsNikonMinimalROM(const BusInfoBlock& bib) noexcept { + return IsMinimalROM(bib) && IsNikonGuid(bib.guid); +} + void LogBIBReadFailed(uint8_t nodeId) { ASFW_LOG(ConfigROM, "ROMScanSession: Node %u BIB read failed, retrying", nodeId); } @@ -39,6 +59,32 @@ void LogBIBCRCMismatch(uint8_t nodeId, uint16_t computed, uint16_t expected) { nodeId, computed, expected); } +void LogNikonSlowPublishRetry(uint8_t nodeId, uint8_t retriesLeft) { + ASFW_LOG( + ConfigROM, + "ROMScanSession: Node %u Nikon minimal-ROM compatibility retry scheduled (remaining=%u)", + nodeId, retriesLeft); +} + +void LogNikonRootDirProbe(uint8_t nodeId, uint32_t offsetBytes) { + ASFW_LOG(ConfigROM, + "ROMScanSession: Node %u Nikon minimal-ROM compatibility probing root dir " + "(offset=0x%x)", + nodeId, offsetBytes); +} + +void LogNikonRootDirProbeFailed(uint8_t nodeId, Async::AsyncStatus status, uint8_t retriesLeft) { + ASFW_LOG(ConfigROM, + "ROMScanSession: Node %u Nikon root dir probe failed (status=%u, remaining=%u)", + nodeId, static_cast(status), retriesLeft); +} + +void LogNikonRootDirProbeExhausted(uint8_t nodeId) { + ASFW_LOG(ConfigROM, + "ROMScanSession: Node %u Nikon root dir probe exhausted, completing as minimal ROM", + nodeId); +} + } // namespace ROMScanSession::ROMScanSession(Async::IFireWireBus& bus, SpeedPolicy& speedPolicy, @@ -96,6 +142,8 @@ void ROMScanSession::Start(ROMScanRequest request, ScanCompletionCallback comple session->nodeScans_.emplace_back(node.nodeId, session->gen_, session->params_.startSpeed, session->params_.perStepRetries); + session->nodeScans_.back().SetSlowPublishRetriesLeft( + kNikonSlowPublishRetryBudget); } } else { auto targets = std::move(request.targetNodes); @@ -109,6 +157,8 @@ void ROMScanSession::Start(ROMScanRequest request, ScanCompletionCallback comple } session->nodeScans_.emplace_back(nodeId, session->gen_, session->params_.startSpeed, session->params_.perStepRetries); + session->nodeScans_.back().SetSlowPublishRetriesLeft( + kNikonSlowPublishRetryBudget); } } @@ -155,6 +205,50 @@ void ROMScanSession::DispatchAsync(std::function work) { }); } +void ROMScanSession::DispatchDelayed(std::function work, uint64_t delayNs) { + if (!work) { + return; + } + + if (!dispatchQueue_) { +#ifdef ASFW_HOST_TEST + Post(std::move(work)); + return; +#else + const uint64_t delayMs = delayNs / 1'000'000ULL; + const uint64_t trailingNs = delayNs % 1'000'000ULL; + Post([delayMs, trailingNs, work = std::move(work)]() mutable { + if (delayMs > 0) { + IOSleep(delayMs); + } + if (trailingNs > 0) { + IODelay((trailingNs + 999ULL) / 1000ULL); + } + work(); + }); + return; +#endif + } + +#ifdef ASFW_HOST_TEST + dispatchQueue_->DispatchAsyncAfter(delayNs, std::move(work)); +#else + const uint64_t delayMs = delayNs / 1'000'000ULL; + const uint64_t trailingNs = delayNs % 1'000'000ULL; + auto queue = dispatchQueue_; + auto captured = std::make_shared>(std::move(work)); + queue->DispatchAsync(^{ + if (delayMs > 0) { + IOSleep(delayMs); + } + if (trailingNs > 0) { + IODelay((trailingNs + 999ULL) / 1000ULL); + } + (*captured)(); + }); +#endif +} + void ROMScanSession::Post(std::function task) { if (!task) { return; @@ -366,28 +460,107 @@ void ROMScanSession::HandleBIBComplete(uint8_t nodeId, ROMReader::ReadResult res } void ROMScanSession::ContinueAfterBIBSuccess(ROMScanNodeStateMachine& node, uint8_t nodeId) { + // DIAGNOSTIC: Log BIB header fields to understand ROM scan decisions + const auto& bib = node.ROM().bib; + ASFW_LOG(ConfigROM, + "[DIAG] Node %u BIB: busInfoLength=%u crcLength=%u guid=0x%04x%08x " + "irmc=%d cmc=%d isc=%d max_rec=%u link_spd=%u", + nodeId, bib.busInfoLength, bib.crcLength, + static_cast(bib.guid >> 32), static_cast(bib.guid & 0xFFFFFFFF), + bib.irmc ? 1 : 0, bib.cmc ? 1 : 0, bib.isc ? 1 : 0, + bib.maxRec, bib.linkSpd); + if (params_.doIRMCheck && topology_.irmNodeId.has_value() && *topology_.irmNodeId == nodeId && node.ROM().bib.irmc) { + ASFW_LOG(ConfigROM, "[DIAG] Node %u: entering IRM check branch", nodeId); StartIRMRead(node); return; } - if (node.ROM().bib.crcLength <= node.ROM().bib.busInfoLength) { - if (!TransitionNodeState(node, ROMScanNodeStateMachine::State::Complete, - "BIB minimal ROM complete")) { - Pump(); + if (IsMinimalROM(node.ROM().bib)) { + if (ShouldDelayMinimalROMCompletion(node)) { + ScheduleMinimalROMRetry(node); return; } - completedROMs_.push_back(std::move(node.MutableROM())); - Pump(); + + ASFW_LOG(ConfigROM, + "[DIAG] Node %u: EARLY EXIT — crcLength(%u) <= busInfoLength(%u), " + "marking as minimal ROM (no root directory)", + nodeId, bib.crcLength, bib.busInfoLength); + CompleteMinimalROM(node, "BIB minimal ROM complete"); return; } + ASFW_LOG(ConfigROM, + "[DIAG] Node %u: crcLength(%u) > busInfoLength(%u), proceeding to root dir read", + nodeId, bib.crcLength, bib.busInfoLength); StartRootDirRead(node); } +bool ROMScanSession::ShouldDelayMinimalROMCompletion(const ROMScanNodeStateMachine& node) const { + if (!IsMinimalROM(node.ROM().bib)) { + return false; + } + + if (node.SlowPublishRetriesLeft() == 0) { + return false; + } + + return IsNikonMinimalROM(node.ROM().bib); +} + +void ROMScanSession::ScheduleMinimalROMRetry(ROMScanNodeStateMachine& node) { + if (!TransitionNodeState(node, ROMScanNodeStateMachine::State::WaitingRepublish, + "Nikon minimal ROM compatibility wait")) { + Pump(); + return; + } + + hadBusyNodes_ = true; + node.DecrementSlowPublishRetries(); + LogNikonSlowPublishRetry(node.NodeId(), node.SlowPublishRetriesLeft()); + + const uint8_t nodeId = node.NodeId(); + auto weakSelf = weak_from_this(); + DispatchDelayed( + [weakSelf, nodeId]() { + if (auto self = weakSelf.lock(); self) { + self->DispatchAsync([self = std::move(self), nodeId]() { + if (self->aborted_.load(std::memory_order_relaxed)) { + return; + } + + auto* nodePtr = self->FindNode(nodeId); + if (nodePtr == nullptr) { + return; + } + + auto& delayedNode = *nodePtr; + if (delayedNode.CurrentState() != + ROMScanNodeStateMachine::State::WaitingRepublish) { + return; + } + + self->StartRootDirRead(delayedNode); + }); + } + }, + kNikonSlowPublishDelayNs); +} + +void ROMScanSession::CompleteMinimalROM(ROMScanNodeStateMachine& node, const char* reason) { + if (!TransitionNodeState(node, ROMScanNodeStateMachine::State::Complete, reason)) { + Pump(); + return; + } + + completedROMs_.push_back(std::move(node.MutableROM())); + Pump(); +} + void ROMScanSession::StartRootDirRead(ROMScanNodeStateMachine& node) { const uint8_t nodeId = node.NodeId(); + const uint32_t offsetBytes = ASFW::ConfigROM::RootDirStartBytes(node.ROM().bib); if (!TransitionNodeState(node, ROMScanNodeStateMachine::State::ReadingRootDir, "BIB complete enter root dir read")) { @@ -395,10 +568,13 @@ void ROMScanSession::StartRootDirRead(ROMScanNodeStateMachine& node) { return; } + if (IsNikonMinimalROM(node.ROM().bib)) { + LogNikonRootDirProbe(nodeId, offsetBytes); + } + node.SetRetriesLeft(params_.perStepRetries); ++inflight_; - const uint32_t offsetBytes = ASFW::ConfigROM::RootDirStartBytes(node.ROM().bib); auto weakSelf = weak_from_this(); reader_->ReadRootDirQuadlets( nodeId, gen_, node.CurrentSpeed(), offsetBytes, 0, @@ -433,6 +609,18 @@ void ROMScanSession::HandleRootDirComplete(uint8_t nodeId, ROMReader::ReadResult auto& node = *nodePtr; if (!result.success || result.quadletsBE.empty()) { + if (ShouldDelayMinimalROMCompletion(node)) { + LogNikonRootDirProbeFailed(nodeId, result.status, node.SlowPublishRetriesLeft()); + ScheduleMinimalROMRetry(node); + return; + } + + if (IsNikonMinimalROM(node.ROM().bib)) { + LogNikonRootDirProbeExhausted(nodeId); + CompleteMinimalROM(node, "Nikon root dir probe exhausted"); + return; + } + ASFW_LOG(ConfigROM, "ROMScanSession: Node %u RootDir read failed - marking failed", nodeId); (void)TransitionNodeState(node, ROMScanNodeStateMachine::State::Failed, "RootDir read failed"); diff --git a/ASFWDriver/ConfigROM/Remote/ROMScanSession.hpp b/ASFWDriver/ConfigROM/Remote/ROMScanSession.hpp index 0f6a9241..1fa37d3d 100644 --- a/ASFWDriver/ConfigROM/Remote/ROMScanSession.hpp +++ b/ASFWDriver/ConfigROM/Remote/ROMScanSession.hpp @@ -59,6 +59,9 @@ class ROMScanSession final : public std::enable_shared_from_this void StartBIBRead(ROMScanNodeStateMachine& node); void HandleBIBComplete(uint8_t nodeId, ROMReader::ReadResult result); void ContinueAfterBIBSuccess(ROMScanNodeStateMachine& node, uint8_t nodeId); + [[nodiscard]] bool ShouldDelayMinimalROMCompletion(const ROMScanNodeStateMachine& node) const; + void ScheduleMinimalROMRetry(ROMScanNodeStateMachine& node); + void CompleteMinimalROM(ROMScanNodeStateMachine& node, const char* reason); void StartIRMRead(ROMScanNodeStateMachine& node); void HandleIRMReadComplete(uint8_t nodeId, bool success, uint32_t valueHostOrder); @@ -78,6 +81,7 @@ class ROMScanSession final : public std::enable_shared_from_this void RetryWithFallback(ROMScanNodeStateMachine& node); void DispatchAsync(std::function work); + void DispatchDelayed(std::function work, uint64_t delayNs); Async::IFireWireBus& bus_; SpeedPolicy& speedPolicy_; diff --git a/ASFWDriver/ConfigROM/Store/ConfigROMStore.cpp b/ASFWDriver/ConfigROM/Store/ConfigROMStore.cpp index a180d471..4aa9d310 100644 --- a/ASFWDriver/ConfigROM/Store/ConfigROMStore.cpp +++ b/ASFWDriver/ConfigROM/Store/ConfigROMStore.cpp @@ -9,6 +9,9 @@ namespace ASFW::Discovery { namespace { +constexpr uint32_t kUnitSpecIdSBP2 = 0x00609E; +constexpr uint32_t kUnitSwVersionSBP2 = 0x010483; + class LockGuard { public: explicit LockGuard(IOLock* lock) noexcept : lock_(lock) { @@ -30,6 +33,19 @@ class LockGuard { IOLock* lock_{nullptr}; }; +[[nodiscard]] bool HasSBP2Unit(const ASFW::Discovery::ConfigROM& rom) noexcept { + for (const auto& unit : rom.unitDirectories) { + if (unit.unitSpecId == kUnitSpecIdSBP2 && unit.unitSwVersion == kUnitSwVersionSBP2) { + return true; + } + } + return false; +} + +[[nodiscard]] bool HasParsedUnitProfile(const ASFW::Discovery::ConfigROM& rom) noexcept { + return !rom.unitDirectories.empty(); +} + } // namespace ConfigROMStore::ConfigROMStore() : lock_(IOLockAlloc()) { @@ -75,12 +91,27 @@ void ConfigROMStore::Insert(const ConfigROM& rom) { romsByGenNode_[key] = romCopy; auto it = romsByGuid_.find(romCopy.bib.guid); - if (it == romsByGuid_.end() || it->second.gen.value < romCopy.gen.value) { + const bool shouldUpdateGuid = + it == romsByGuid_.end() || + (it->second.gen.value < romCopy.gen.value && + (HasParsedUnitProfile(romCopy) || !HasParsedUnitProfile(it->second))); + + if (shouldUpdateGuid) { romsByGuid_[romCopy.bib.guid] = romCopy; - ASFW_LOG_V2(ConfigROM, "ConfigROMStore::Insert: GUID=0x%016llx gen=%u node=%u state=%u", + ASFW_LOG_V2(ConfigROM, + "ConfigROMStore::Insert: GUID=0x%016llx gen=%u node=%u state=%u " + "rawQuadlets=%zu rootEntries=%zu unitDirs=%zu hasSBP2=%d", romCopy.bib.guid, romCopy.gen.value, romCopy.nodeId, - static_cast(romCopy.state)); + static_cast(romCopy.state), romCopy.rawQuadlets.size(), + romCopy.rootDirMinimal.size(), romCopy.unitDirectories.size(), + HasSBP2Unit(romCopy) ? 1 : 0); + } else { + ASFW_LOG_V2(ConfigROM, + "ConfigROMStore::Insert: retaining richer GUID cache for GUID=0x%016llx " + "candidateGen=%u candidateUnitDirs=%zu cachedGen=%u cachedUnitDirs=%zu", + romCopy.bib.guid, romCopy.gen.value, romCopy.unitDirectories.size(), + it->second.gen.value, it->second.unitDirectories.size()); } } @@ -112,6 +143,7 @@ const ConfigROM* ConfigROMStore::FindLatestForNode(uint8_t nodeId) const { LockGuard guard(lock_); const ConfigROM* latest = nullptr; + const ConfigROM* latestWithUnitProfile = nullptr; for (const auto& [key, rom] : romsByGenNode_) { if (rom.nodeId != nodeId) { continue; @@ -119,7 +151,21 @@ const ConfigROM* ConfigROMStore::FindLatestForNode(uint8_t nodeId) const { if (latest == nullptr || rom.gen.value > latest->gen.value) { latest = &rom; } + if (HasParsedUnitProfile(rom) && + (latestWithUnitProfile == nullptr || + rom.gen.value > latestWithUnitProfile->gen.value)) { + latestWithUnitProfile = &rom; + } } + + if (latestWithUnitProfile != nullptr && latest != latestWithUnitProfile) { + ASFW_LOG_V2(ConfigROM, + "ConfigROMStore::FindLatestForNode: node=%u using gen=%u with unit profile " + "instead of partial gen=%u", + nodeId, latestWithUnitProfile->gen.value, latest ? latest->gen.value : 0); + return latestWithUnitProfile; + } + return latest; } diff --git a/ASFWDriver/Controller/BringupOverrides.hpp b/ASFWDriver/Controller/BringupOverrides.hpp new file mode 100644 index 00000000..21e76918 --- /dev/null +++ b/ASFWDriver/Controller/BringupOverrides.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "ControllerConfig.hpp" +#include "../Bus/BusManager.hpp" + +namespace ASFW::Driver { + +// Host cycle-master bring-up configuration: +// - enable local contender / cycle-master eligibility (matches Linux/Apple default) +// - delegation controlled by experimentalHostCycleMasterBringup property +// +// Per Linux firewire_ohci: PHY contender bit is always set during init. +// Per Apple IOFireWireController: contender is set for most configurations. +// The host MUST be contender-capable for proper bus management (IRM election, +// cycle-start generation), especially in 2-node topologies with SBP-2 devices. +inline void ApplyBringupOverrides(ControllerConfig& config, BusManager* busManager) { + // Always enable contender — matches Linux/Apple behavior + config.allowCycleMasterEligibility = true; + + if (busManager != nullptr) { + // When experimental flag is set, disable delegation so host keeps + // root/cycle-master. Otherwise use default delegation policy. + busManager->SetDelegateMode(!config.experimentalHostCycleMasterBringup); + } +} + +} // namespace ASFW::Driver diff --git a/ASFWDriver/Controller/ControllerConfig.cpp b/ASFWDriver/Controller/ControllerConfig.cpp index 9e525586..7e5ca38d 100644 --- a/ASFWDriver/Controller/ControllerConfig.cpp +++ b/ASFWDriver/Controller/ControllerConfig.cpp @@ -9,10 +9,10 @@ ControllerConfig ControllerConfig::MakeDefault() { config.vendor.vendorName = "Unknown"; config.localGuid = 0; config.enableVerboseLogging = false; + config.experimentalHostCycleMasterBringup = false; config.allowCycleMasterEligibility = false; config.supportedSpeeds = {100, 200, 400}; return config; } } // namespace ASFW::Driver - diff --git a/ASFWDriver/Controller/ControllerConfig.hpp b/ASFWDriver/Controller/ControllerConfig.hpp index 95ed7134..5bc2495d 100644 --- a/ASFWDriver/Controller/ControllerConfig.hpp +++ b/ASFWDriver/Controller/ControllerConfig.hpp @@ -19,6 +19,7 @@ struct ControllerConfig { VendorInfo vendor; uint64_t localGuid{0}; bool enableVerboseLogging{false}; + bool experimentalHostCycleMasterBringup{false}; bool allowCycleMasterEligibility{false}; std::vector supportedSpeeds; @@ -26,4 +27,3 @@ struct ControllerConfig { }; } // namespace ASFW::Driver - diff --git a/ASFWDriver/Controller/ControllerCore.hpp b/ASFWDriver/Controller/ControllerCore.hpp index b62d6ab3..194abffd 100644 --- a/ASFWDriver/Controller/ControllerCore.hpp +++ b/ASFWDriver/Controller/ControllerCore.hpp @@ -57,6 +57,7 @@ class FCPResponseRouter; namespace ASFW::Protocols::SBP2 { class AddressSpaceManager; +class SBP2SessionRegistry; } namespace ASFW::IRM { @@ -96,6 +97,7 @@ class ControllerCore { std::shared_ptr avcDiscovery; std::shared_ptr fcpResponseRouter; std::shared_ptr sbp2AddressSpaceManager; + std::shared_ptr sbp2SessionRegistry; std::shared_ptr irmClient; @@ -137,9 +139,13 @@ class ControllerCore { Protocols::AVC::IAVCDiscovery* GetAVCDiscovery() const; void SetAVCDiscovery(std::shared_ptr avcDiscovery); void SetFCPResponseRouter(std::shared_ptr fcpResponseRouter); + Protocols::SBP2::AddressSpaceManager* GetSbp2AddressSpaceManager() const; + Protocols::SBP2::SBP2SessionRegistry* GetSBP2SessionRegistry() const; void SetSbp2AddressSpaceManager( std::shared_ptr sbp2AddressSpaceManager); + void SetSBP2SessionRegistry( + std::shared_ptr sbp2SessionRegistry); IRM::IRMClient* GetIRMClient() const; void SetIRMClient(std::shared_ptr client); diff --git a/ASFWDriver/Controller/ControllerCoreDiscovery.cpp b/ASFWDriver/Controller/ControllerCoreDiscovery.cpp index 0c99575c..4e7947fb 100644 --- a/ASFWDriver/Controller/ControllerCoreDiscovery.cpp +++ b/ASFWDriver/Controller/ControllerCoreDiscovery.cpp @@ -16,6 +16,7 @@ #include "../ConfigROM/ROMScanner.hpp" #include "../Diagnostics/DiagnosticLogger.hpp" #include "../Diagnostics/MetricsSink.hpp" +#include "../Discovery/DiscoveryConvergence.hpp" #include "../Discovery/DeviceManager.hpp" #include "../Discovery/DeviceRegistry.hpp" #include "../Discovery/SpeedPolicy.hpp" @@ -29,6 +30,7 @@ #include "../Protocols/AVC/AVCDiscovery.hpp" #include "../Protocols/AVC/CMP/CMPClient.hpp" #include "../Protocols/Audio/DeviceProtocolFactory.hpp" +#include "../Protocols/SBP2/SBP2SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" #include "../Version/DriverVersion.hpp" #include "ControllerStateMachine.hpp" @@ -115,6 +117,18 @@ void ControllerCore::OnTopologyReady(const TopologySnapshot& snap) { // CMP (PCR) operations target a *specific device's* plug registers, not the IRM node. // Device-scoped CMP wiring is done at stream start time (IsochService). } + + if (deps_.sbp2SessionRegistry) { + deps_.sbp2SessionRegistry->OnBusReset(static_cast(snap.generation)); + } + + // NOTE: CSR STATE_SET CMSTR write removed. Apple IOFireWireFamily does NOT write + // CSR STATE_SET via async transactions — it uses the OHCI LinkControl register + // directly (kCycleMaster bit), which ASFWDriver already sets in kDefaultLinkControl + // during controller initialization. Async loopback to the local node does not work + // in ASFWDriver (always returns timeout), so the previous implementation was a no-op. + // OHCI hardware generates cycle-start packets automatically when the node is root + // and kCycleMaster is set in LinkControl. } // NOLINTNEXTLINE(readability-function-cognitive-complexity) @@ -154,6 +168,15 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, deps_.busReset->EscalateDiscoveryDelay(); } } + + bool zeroRomScanInconclusive = false; + if (deps_.topology) { + if (const auto latestTopology = deps_.topology->LatestSnapshot()) { + zeroRomScanInconclusive = Discovery::IsZeroRomScanInconclusive( + gen, roms.size(), *latestTopology); + } + } + std::unordered_set discoveredGuids; discoveredGuids.reserve(roms.size()); @@ -188,7 +211,7 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, deviceRecord.isAudioCandidate ? "YES" : "NO"); } - if (deps_.deviceManager) { + if (deps_.deviceManager && !zeroRomScanInconclusive) { auto devices = deps_.deviceManager->GetAllDevices(); for (const auto& device : devices) { if (!device) { @@ -205,12 +228,25 @@ void ControllerCore::OnDiscoveryScanComplete(Discovery::Generation gen, gen.value, guid); deps_.deviceManager->MarkDeviceLost(guid); } + } else if (deps_.deviceManager && zeroRomScanInconclusive) { + ASFW_LOG(Discovery, + "ROM scan for gen=%u produced 0 ROMs but topology still has remote " + "link-active nodes — keeping existing devices until a conclusive scan", + gen.value); } ASFW_LOG(Discovery, "═══════════════════════════════════════"); ASFW_LOG(Discovery, "Discovery complete: %zu devices processed in gen=%u", roms.size(), gen.value); ASFW_LOG(Discovery, "═══════════════════════════════════════════════════════"); + + if (deps_.sbp2SessionRegistry && !zeroRomScanInconclusive) { + deps_.sbp2SessionRegistry->RefreshTargets(gen); + } else if (deps_.sbp2SessionRegistry) { + ASFW_LOG(Discovery, + "Skipping SBP-2 target refresh for inconclusive zero-ROM scan gen=%u", + gen.value); + } } } // namespace ASFW::Driver diff --git a/ASFWDriver/Controller/ControllerCoreFacades.cpp b/ASFWDriver/Controller/ControllerCoreFacades.cpp index 894c095f..48f2f525 100644 --- a/ASFWDriver/Controller/ControllerCoreFacades.cpp +++ b/ASFWDriver/Controller/ControllerCoreFacades.cpp @@ -104,16 +104,25 @@ void ControllerCore::SetFCPResponseRouter( deps_.fcpResponseRouter = std::move(fcpResponseRouter); } +IRM::IRMClient* ControllerCore::GetIRMClient() const { return deps_.irmClient.get(); } + Protocols::SBP2::AddressSpaceManager* ControllerCore::GetSbp2AddressSpaceManager() const { return deps_.sbp2AddressSpaceManager.get(); } +Protocols::SBP2::SBP2SessionRegistry* ControllerCore::GetSBP2SessionRegistry() const { + return deps_.sbp2SessionRegistry.get(); +} + void ControllerCore::SetSbp2AddressSpaceManager( std::shared_ptr sbp2AddressSpaceManager) { deps_.sbp2AddressSpaceManager = std::move(sbp2AddressSpaceManager); } -IRM::IRMClient* ControllerCore::GetIRMClient() const { return deps_.irmClient.get(); } +void ControllerCore::SetSBP2SessionRegistry( + std::shared_ptr sbp2SessionRegistry) { + deps_.sbp2SessionRegistry = std::move(sbp2SessionRegistry); +} void ControllerCore::SetIRMClient(std::shared_ptr client) { deps_.irmClient = std::move(client); diff --git a/ASFWDriver/Controller/ControllerCoreLifecycle.cpp b/ASFWDriver/Controller/ControllerCoreLifecycle.cpp index 903e2267..345075cc 100644 --- a/ASFWDriver/Controller/ControllerCoreLifecycle.cpp +++ b/ASFWDriver/Controller/ControllerCoreLifecycle.cpp @@ -32,6 +32,7 @@ #include "../Protocols/Audio/DeviceProtocolFactory.hpp" #include "../Scheduling/Scheduler.hpp" #include "../Version/DriverVersion.hpp" +#include "BringupOverrides.hpp" #include "ControllerStateMachine.hpp" #include "Logging.hpp" @@ -81,6 +82,8 @@ kern_return_t ControllerCore::InitializeBusResetAndDiscovery() { return kIOReturnNoResources; } + ApplyBringupOverrides(config_, deps_.busManager.get()); + auto workQueue = deps_.scheduler->Queue(); ASFW_LOG(Controller, "Initializing BusResetCoordinator"); @@ -331,6 +334,12 @@ kern_return_t ControllerCore::InitialiseHardware(IOService* provider) { // auto-allocation behavior if (isOHCI_1_1_OrLater) { hw.WriteAndFlush(Register32::kInitialChannelsAvailableHi, 0xFFFFFFFE); + hw.WriteAndFlush(Register32::kInitialChannelsAvailableLo, 0xFFFFFFFF); + // Initialize BandwidthAvailable to maximum isochronous bandwidth. + // Per Linux ohci_enable(): reg_write(ohci, OHCI1394_BandwidthAvailable, 4915) + // Value 4915 (0x1333) ≈ S1600 full-cycle bandwidth. Without this, the IRM + // reports zero available bandwidth and devices may consider bus management inactive. + hw.WriteAndFlush(Register32::kInitialBandwidthAvailable, 4915); } // Step 4: Clear noByteSwapData - enable byte-swapping for data phases per OHCI spec @@ -385,66 +394,22 @@ kern_return_t ControllerCore::InitialiseHardware(IOService* provider) { ASFW_LOG(Hardware, "PHY probe failed after retry; relying on firmware defaults"); } else { uint8_t reg1Value = phyId.value(); - ASFW_LOG_PHY("PHY probe OK (reg1=0x%02x)", reg1Value); - - // --- FIX START: Force Gap Count to 0x3F --- - // Problem: Some PHYs report the strapped value over the register interface - // but require a write to latch it into the active core after reset. - // Fix: Always write register 1 so the latch is triggered even if the - // desired value already appears to be programmed. - const uint8_t kTargetGap = ASFW::Driver::kPhyGapCountMask; - const uint8_t newReg1 = (reg1Value & 0xC0U) | kTargetGap; - - ASFW_LOG_PHY("Forcing PHY Gap Count write (Reg 1): 0x%02x -> 0x%02x", reg1Value, - newReg1); - - constexpr int kMaxPhyWriteAttempts = 3; - bool wroteOk = false; - for (int attempt = 0; attempt < kMaxPhyWriteAttempts; ++attempt) { - if (!hw.WritePhyRegister(1, newReg1)) { - ASFW_LOG_PHY("PHY write attempt %d failed (writePhyRegister returned false)", - attempt + 1); - // Short delay before retry - IOSleep(1); - continue; - } - - // Give PHY time to latch the value (some parts need an explicit delay) - IODelay(2000); - - // Read-back verification - auto verify = hw.ReadPhyRegister(1); - if (verify && ((*verify & ASFW::Driver::kPhyGapCountMask) == kTargetGap)) { - ASFW_LOG_PHY("✅ PHY Gap Count confirmed: 0x%02x -> 0x%02x (attempt %d)", - reg1Value, *verify, attempt + 1); - wroteOk = true; - break; - } - - // Toggle LPS to try to force PHY latch, then small pause and retry - ASFW_LOG_PHY("PHY gap write verify failed on attempt %d (readback=0x%02x)", - attempt + 1, verify.value_or(0)); - hw.ClearHCControlBits(HCControlBits::kLPS); - IODelay(5); - hw.SetHCControlBits(HCControlBits::kLPS); - IOSleep(5); - } + ASFW_LOG_PHY("PHY probe OK (reg1=0x%02x, gap_count=%u)", + reg1Value, reg1Value & ASFW::Driver::kPhyGapCountMask); - if (!wroteOk) { - ASFW_LOG(Hardware, - "Failed to reliably write PHY Register 1 (gap count) after %d attempts", - kMaxPhyWriteAttempts); - } - // --- FIX END --- + // Note: We do NOT force gap count during init, unlike previous code. + // Linux ohci_enable() uses the PHY's default gap count and only + // optimizes it later after topology discovery (see GapCountOptimizer). + // Forcing gap count = 63 at init could cause communication issues + // with devices that expect the standard default (5). // Step 4: Configure PHY register 4 (Link Active + Contender) // Use constants from IEEE1394.hpp // (kPhyReg4Address, kPhyLinkActive, kPhyContender) // - // CRITICAL FIX: Only set Contender bit if allowCycleMasterEligibility is true - // - This matches Apple's behavior (conditional PHY reg 4 setup) - // - Prevents two-contender bus topology issues with devices like Apogee Duet - // - Default config has allowCycleMasterEligibility=false (delegate mode) + // Per Linux firewire_ohci and Apple IOFireWireController: + // Always set Contender bit. ApplyBringupOverrides ensures + // allowCycleMasterEligibility=true (matching Linux/Apple defaults). const uint8_t phyReg4Bits = config_.allowCycleMasterEligibility @@ -465,14 +430,17 @@ kern_return_t ControllerCore::InitialiseHardware(IOService* provider) { ASFW_LOG(Hardware, "Failed to configure PHY register 4"); } - // Enable PHY accelerated arbitration (IEEE 1394a reg5 bit6) before linkEnable. + // Enable PHY accelerated arbitration + multi-speed packet concatenation + // (IEEE 1394a reg5 bit6 + bit5) before linkEnable. + // Per Linux configure_1394a_enhancements(): both bits are set together. if (phyConfigOk) { const bool accelEnabled = - hw.UpdatePhyRegister(kPhyReg5Address, 0, kPhyEnableAcceleration); + hw.UpdatePhyRegister(kPhyReg5Address, 0, + kPhyEnableAcceleration | kPhyEnableMulti); if (accelEnabled) { - ASFW_LOG_PHY("PHY reg5 configured: Enab_accel=1 (gap writes will stick)"); + ASFW_LOG_PHY("PHY reg5 configured: Enab_accel=1 Enab_multi=1"); } else { - ASFW_LOG(Hardware, "Failed to enable PHY accelerated arbitration (reg5 bit6)"); + ASFW_LOG(Hardware, "Failed to enable PHY 1394a enhancements (reg5)"); phyConfigOk = false; } } @@ -533,14 +501,23 @@ kern_return_t ControllerCore::InitialiseHardware(IOService* provider) { return configRomStatus; } - // Step 7: Set Physical Upper Bound (256MB CSR address range) - // TODO(ASFW-DMA): Confirm whether remote DMA still requires this register programming. - // Per Linux ohci_enable(): Don't pre-write NodeID; bus reset will assign it from Self-ID - // The kProvisionalNodeId value would be immediately overwritten anyway + // Step 7: Set Physical Upper Bound (OHCI §5.5.5) + // Per Linux ohci_enable() (ohci.c:2346): + // reg_write(ohci, OHCI1394_PhyUpperBound, FW_MAX_PHYSICAL_RANGE >> 16); + // FW_MAX_PHYSICAL_RANGE = 1ULL << 32, >> 16 = 0x10000 (4GB physical DMA range) + // This MUST be set before linkEnable to ensure proper physical address routing. + // Without this, CSR high-address space (e.g. ConfigROM at 0xF0000800+) may + // return RCODE_ADDRESS_ERROR on some controllers. + hw.WriteAndFlush(Register32::kPhyUpperBound, 0x10000u); + ASFW_LOG(Hardware, "PhyUpperBound set to 0x10000 (4GB physical DMA range)"); + hw.SetLinkControlBits(ASFW::Driver::kDefaultLinkControl); ASFW_LOG(Hardware, - "LinkControl: rcvSelfID | rcvPhyPkt | cycleTimerEnable (cycleMaster deferred)"); + "LinkControl: rcvSelfID | rcvPhyPkt | cycleTimerEnable | cycleMaster"); hw.WriteAndFlush(Register32::kAsReqFilterHiSet, ASFW::Driver::kAsReqAcceptAllMask); + hw.WriteAndFlush(Register32::kAsReqFilterLoSet, 0xFFFFFFFF); + ASFW_LOG(Hardware, "AsReqFilter: Hi=0x%08x Lo=0xFFFFFFFF (accept all async requests)", + ASFW::Driver::kAsReqAcceptAllMask); // Build full 32-bit value explicitly per OHCI spec: // [31:24]=reserved(0), [23:16]=cycleLimit, [15:8]=maxPhys, [7:4]=maxResp, [3:0]=maxReq @@ -550,10 +527,26 @@ kern_return_t ControllerCore::InitialiseHardware(IOService* provider) { hw.WriteAndFlush(Register32::kATRetries, atRetriesVal); // Force readback to flush write pipeline const uint32_t atRetriesReadback = hw.Read(Register32::kATRetries); - ASFW_LOG(Hardware, "ATRetries configured: maxReq=3 maxResp=3 maxPhys=3 cycleLimit=200"); ASFW_LOG(Hardware, "ATRetries write/readback: 0x%08x / 0x%08x", atRetriesVal, atRetriesReadback); + // FairnessControl (0x0DC): Probe for priority budget support, then clear. + // Per Linux ohci_enable() (firewire/ohci.c): + // reg_write(ohci, OHCI1394_FairnessControl, 0x3f); + // if (reg_read(ohci, OHCI1394_FairnessControl) == 0x3f) + // reg_write(ohci, OHCI1394_FairnessControl, 0); + // Some controllers implement priority arbitration; clearing the register + // ensures standard fairness (equal access for all nodes on the bus). + hw.WriteAndFlush(Register32::kFairnessControl, 0x3F); + const uint32_t fcReadback = hw.Read(Register32::kFairnessControl); + if (fcReadback == 0x3F) { + hw.WriteAndFlush(Register32::kFairnessControl, 0); + ASFW_LOG(Hardware, "FairnessControl: priority-budget capable, cleared to 0"); + } else { + ASFW_LOG(Hardware, "FairnessControl: readback=0x%08x (no priority-budget support)", + fcReadback); + } + // Bus timing state: mark cycle timer as inactive during init // Linux: ohci->bus_time_running = false; // Ensures init path doesn't assume active isochronous timing @@ -634,16 +627,7 @@ kern_return_t ControllerCore::EnableInterruptsAndStartBus() { ASFW_LOG(Hardware, "Setting linkEnable + BIBimageValid atomically - will trigger auto bus reset"); hw.SetHCControlBits(HCControlBits::kLinkEnable | HCControlBits::kBibImageValid); - - if (phyProgramSupported_ && phyConfigOk_) { - ASFW_LOG(Hardware, "Forcing bus reset via PHY to guarantee Config ROM shadow activation"); - const bool forced = hw.InitiateBusReset(false); - if (!forced) { - ASFW_LOG(Hardware, "WARNING: Forced bus reset failed; will rely on auto reset"); - } - } else { - ASFW_LOG(Hardware, "Skipping forced reset; relying on auto reset from linkEnable"); - } + ASFW_LOG(Hardware, "Relying on linkEnable auto reset for Config ROM shadow activation"); if (deps_.asyncController) { const kern_return_t armStatus = deps_.asyncController->ArmARContextsOnly(); diff --git a/ASFWDriver/Debug/BusResetPacketCapture.cpp b/ASFWDriver/Debug/BusResetPacketCapture.cpp index 18d27f91..d1a68f4a 100644 --- a/ASFWDriver/Debug/BusResetPacketCapture.cpp +++ b/ASFWDriver/Debug/BusResetPacketCapture.cpp @@ -51,12 +51,17 @@ void BusResetPacketCapture::CapturePacket(const uint32_t* dmaQuadlets, snapshot.captureTimestamp = GetCurrentTimestamp(); snapshot.generation = generation; - // Copy raw quadlets (little-endian from DMA) - std::memcpy(snapshot.rawQuadlets, dmaQuadlets, sizeof(snapshot.rawQuadlets)); + // The synthetic bus-reset packet can be routed from an unaligned AR buffer. + // DriverKit's memcpy may still use wider loads, so copy byte-by-byte. + const auto* bytes = reinterpret_cast(dmaQuadlets); + auto* rawBytes = reinterpret_cast(snapshot.rawQuadlets); + for (size_t i = 0; i < sizeof(snapshot.rawQuadlets); ++i) { + rawBytes[i] = bytes[i]; + } // Convert to wire format (big-endian) for (int i = 0; i < 4; ++i) { - snapshot.wireQuadlets[i] = LEtoBE(dmaQuadlets[i]); + snapshot.wireQuadlets[i] = LEtoBE(snapshot.rawQuadlets[i]); } // Extract tCode from wire format Q0[31:28] diff --git a/ASFWDriver/Discovery/DeviceRegistry.cpp b/ASFWDriver/Discovery/DeviceRegistry.cpp index b5807957..4cc769b6 100644 --- a/ASFWDriver/Discovery/DeviceRegistry.cpp +++ b/ASFWDriver/Discovery/DeviceRegistry.cpp @@ -249,7 +249,7 @@ DeviceKind DeviceRegistry::ClassifyDevice(const ConfigROM& rom) const { return DeviceKind::Storage; } } - + return DeviceKind::Unknown; } diff --git a/ASFWDriver/Discovery/DiscoveryConvergence.hpp b/ASFWDriver/Discovery/DiscoveryConvergence.hpp new file mode 100644 index 00000000..26fc89e6 --- /dev/null +++ b/ASFWDriver/Discovery/DiscoveryConvergence.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "../Controller/ControllerTypes.hpp" +#include "DiscoveryTypes.hpp" + +namespace ASFW::Discovery { + +[[nodiscard]] inline bool HasRemoteLinkActiveNode(const ASFW::Driver::TopologySnapshot& topology) { + if (!topology.localNodeId.has_value()) { + return false; + } + + const uint8_t localNodeId = *topology.localNodeId; + for (const auto& node : topology.nodes) { + if (node.nodeId != localNodeId && node.linkActive) { + return true; + } + } + return false; +} + +[[nodiscard]] inline bool IsZeroRomScanInconclusive( + Generation scanGeneration, + std::size_t romCount, + const ASFW::Driver::TopologySnapshot& topology) { + return romCount == 0U && topology.generation == scanGeneration.value && + HasRemoteLinkActiveNode(topology); +} + +} // namespace ASFW::Discovery diff --git a/ASFWDriver/Discovery/DiscoveryTypes.hpp b/ASFWDriver/Discovery/DiscoveryTypes.hpp index a05edb71..727211ee 100644 --- a/ASFWDriver/Discovery/DiscoveryTypes.hpp +++ b/ASFWDriver/Discovery/DiscoveryTypes.hpp @@ -88,6 +88,8 @@ enum class CfgKey : uint8_t { Node_Capabilities = 0x0C, Unit_Directory = 0xD1, // IEEE 1212 Unit_Directory (keyId=0x11 when keyType=3) Management_Agent_Offset = 0x54, // SBP-2 (keyType=CSR offset, keyId=0x14) + Unit_Characteristics = 0x39, // SBP-2 (immediate in unit directory) + Fast_Start = 0x3A, // SBP-2 (leaf in unit directory) }; struct RomEntry { @@ -108,6 +110,11 @@ struct UnitDirectory { std::optional logicalUnitNumber; std::optional modelId; std::optional modelName; + + // SBP-2 specific (from Management_Agent_Offset, Unit_Characteristics, Fast_Start keys) + std::optional managementAgentOffset; + std::optional unitCharacteristics; + std::optional fastStart; }; // ROM lifecycle state (matching Apple IOFireWireROMCache patterns) diff --git a/ASFWDriver/Discovery/FWDevice.cpp b/ASFWDriver/Discovery/FWDevice.cpp index ead10d93..541a6fa5 100644 --- a/ASFWDriver/Discovery/FWDevice.cpp +++ b/ASFWDriver/Discovery/FWDevice.cpp @@ -116,10 +116,10 @@ std::vector FWDevice::ExtractUnitDirectory( entries.push_back(RomEntry{CfgKey::Unit_Sw_Version, value, keyType, 0}); } break; - case 0x14: // Logical_Unit_Number or SBP-2 Management_Agent_Offset + case 0x14: // Logical_Unit_Number if (keyType == 0) { // Immediate entries.push_back(RomEntry{CfgKey::Logical_Unit_Number, value, keyType, 0}); - } else if (keyType == 1) { // CSR offset + } else if (keyType == 1) { // CSR offset: SBP-2 Management_Agent_Offset entries.push_back(RomEntry{CfgKey::Management_Agent_Offset, value, keyType, 0}); } break; @@ -128,6 +128,24 @@ std::vector FWDevice::ExtractUnitDirectory( entries.push_back(RomEntry{CfgKey::Management_Agent_Offset, value, keyType, 0}); } break; + case 0x39: // Unit_Characteristics (SBP-2, immediate) + if (keyType == 0) { + entries.push_back(RomEntry{CfgKey::Unit_Characteristics, value, keyType, 0}); + } + break; + case 0x3A: // Fast_Start (SBP-2, leaf) + if (keyType == 2) { + // Compute leaf offset: value is a signed 24-bit offset from this entry + const int32_t signedValue = ((value & 0x800000U) != 0U) + ? static_cast(value | 0xFF000000U) + : static_cast(value); + const int32_t rel = static_cast(i) + signedValue; + if (rel >= 0) { + entries.push_back(RomEntry{CfgKey::Fast_Start, value, keyType, + static_cast(rel)}); + } + } + break; default: break; } diff --git a/ASFWDriver/Discovery/FWUnit.cpp b/ASFWDriver/Discovery/FWUnit.cpp index c2b66f75..f6c8e0bc 100644 --- a/ASFWDriver/Discovery/FWUnit.cpp +++ b/ASFWDriver/Discovery/FWUnit.cpp @@ -62,6 +62,18 @@ void FWUnit::ParseEntries(const std::vector& entries) modelId_ = entry.value; break; + case CfgKey::Management_Agent_Offset: + managementAgentOffset_ = entry.value; + break; + + case CfgKey::Unit_Characteristics: + unitCharacteristics_ = entry.value; + break; + + case CfgKey::Fast_Start: + fastStart_ = entry.value; + break; + // Other keys (CSR offsets, dependent directories) ignored for now default: break; diff --git a/ASFWDriver/Discovery/FWUnit.hpp b/ASFWDriver/Discovery/FWUnit.hpp index 57b67072..ab91cd55 100644 --- a/ASFWDriver/Discovery/FWUnit.hpp +++ b/ASFWDriver/Discovery/FWUnit.hpp @@ -32,6 +32,10 @@ class FWUnit : public std::enable_shared_from_this { std::optional GetLUN() const { return logicalUnitNumber_; } uint32_t GetDirectoryOffset() const { return directoryOffset_; } + std::optional GetManagementAgentOffset() const { return managementAgentOffset_; } + std::optional GetUnitCharacteristics() const { return unitCharacteristics_; } + std::optional GetFastStart() const { return fastStart_; } + std::string_view GetVendorName() const { return vendorName_; } std::string_view GetProductName() const { return productName_; } @@ -65,6 +69,11 @@ class FWUnit : public std::enable_shared_from_this { uint32_t modelId_{0}; std::optional logicalUnitNumber_; + // SBP-2 specific metadata + std::optional managementAgentOffset_; + std::optional unitCharacteristics_; + std::optional fastStart_; + std::string vendorName_; std::string productName_; diff --git a/ASFWDriver/Hardware/OHCIConstants.hpp b/ASFWDriver/Hardware/OHCIConstants.hpp index 0ff70dbe..fc8ff4f3 100644 --- a/ASFWDriver/Hardware/OHCIConstants.hpp +++ b/ASFWDriver/Hardware/OHCIConstants.hpp @@ -14,16 +14,22 @@ namespace ASFW::Driver { constexpr uint32_t kAsReqAcceptAllMask = 0x80000000u; // Default link control configuration used during controller initialization +// Per Linux ohci_enable() (ohci.c:2317-2318): cycleMaster is set at init time, +// not deferred. For simple 2-node topologies the host is root and must immediately +// act as cycle master to generate cycle-start packets. constexpr uint32_t kDefaultLinkControl = LinkControlBits::kRcvSelfID | LinkControlBits::kRcvPhyPkt | - LinkControlBits::kCycleTimerEnable; + LinkControlBits::kCycleTimerEnable | + LinkControlBits::kCycleMaster; // Posted write priming bits (OHCI HCControl - enable posted writes and LPS) constexpr uint32_t kPostedWritePrimingBits = HCControlBits::kPostedWriteEnable | HCControlBits::kLPS; -// Default ATRetries value (cycleLimit=200 maxPhys=3 maxResp=3 maxReq=3) -constexpr uint32_t kDefaultATRetries = (3u << 0) | (3u << 4) | (3u << 8) | (200u << 16); +// Default ATRetries value +// Per Linux firewire_ohci ohci_enable(): maxReq=15, maxResp=2, maxPhys=8, cycleLimit=200 +// Higher maxReq/maxPhys reduce transaction failures on slow or busy devices. +constexpr uint32_t kDefaultATRetries = (15u << 0) | (2u << 4) | (8u << 8) | (200u << 16); // Node capabilities advertised in our local Config ROM. Keep the baseline // conservative and only set cPhyEnhance when the PHY/Link 1394a enhancement diff --git a/ASFWDriver/Info.plist b/ASFWDriver/Info.plist index c02a2368..2e97a059 100644 --- a/ASFWDriver/Info.plist +++ b/ASFWDriver/Info.plist @@ -42,6 +42,8 @@ ASFWLogStatistics + ASFWExperimentalHostCycleMasterBringup + CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) IOClass diff --git a/ASFWDriver/Logging/Logging.cpp b/ASFWDriver/Logging/Logging.cpp index 5932f8c6..1339886d 100644 --- a/ASFWDriver/Logging/Logging.cpp +++ b/ASFWDriver/Logging/Logging.cpp @@ -31,5 +31,6 @@ os_log_t AVC() { static os_log_t log = MakeCategory("avc"); return os_log_t Isoch() { static os_log_t log = MakeCategory("isoch"); return log; } os_log_t Audio() { static os_log_t log = MakeCategory("audio"); return log; } os_log_t DICE() { static os_log_t log = MakeCategory("dice"); return log; } +os_log_t SBP2() { static os_log_t log = MakeCategory("sbp2"); return log; } } // namespace ASFW::Driver::Logging diff --git a/ASFWDriver/Logging/Logging.hpp b/ASFWDriver/Logging/Logging.hpp index 3bd5585e..9a3c998b 100644 --- a/ASFWDriver/Logging/Logging.hpp +++ b/ASFWDriver/Logging/Logging.hpp @@ -98,6 +98,7 @@ os_log_t AVC(); os_log_t Isoch(); os_log_t Audio(); os_log_t DICE(); +os_log_t SBP2(); } // namespace ASFW::Driver::Logging // ----- time helpers (header-only, safe in DriverKit) ----- diff --git a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp index 90801cbd..e1667ebd 100644 --- a/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp +++ b/ASFWDriver/Protocols/SBP2/AddressSpaceManager.hpp @@ -2,10 +2,10 @@ #include #include -#include #include #include #include +#include #include #include #include @@ -272,9 +272,10 @@ class AddressSpaceManager { offset = static_cast(address - range->meta.address); ASFW_ADDRSPACE_LOG( - "AddressSpaceManager[%p] remote write addr=0x%012llx len=%zu src=%p " + "AddressSpaceManager[%p] remote write label=%s addr=0x%012llx len=%zu src=%p " "handle=0x%llx rangeAddr=0x%012llx off=%u buf=%p mapped=%p backing=%u", this, + range->debugLabel, static_cast(address), payload.size(), payload.data(), @@ -328,6 +329,18 @@ class AddressSpaceManager { outSlice->payloadDeviceAddress = payloadAddress; outSlice->payloadLength = length; + ASFW_ADDRSPACE_LOG( + "AddressSpaceManager[%p] remote read-block label=%s addr=0x%012llx len=%u " + "handle=0x%llx rangeAddr=0x%012llx off=%llu dma=0x%08x", + this, + range->debugLabel, + static_cast(address), + length, + static_cast(range->meta.handle), + static_cast(range->meta.address), + static_cast(offset), + static_cast(payloadAddress)); + IOLockUnlock(lock_); return Async::ResponseCode::Complete; } @@ -349,6 +362,17 @@ class AddressSpaceManager { range->buffer.data() + static_cast(offset), sizeof(uint32_t)); + ASFW_ADDRSPACE_LOG( + "AddressSpaceManager[%p] remote read-quadlet label=%s addr=0x%012llx " + "handle=0x%llx rangeAddr=0x%012llx off=%u value=0x%08x", + this, + range->debugLabel, + static_cast(address), + static_cast(range->meta.handle), + static_cast(range->meta.address), + offset, + *outValue); + IOLockUnlock(lock_); return Async::ResponseCode::Complete; } @@ -392,6 +416,19 @@ class AddressSpaceManager { IOLockUnlock(lock_); } + void SetDebugLabel(uint64_t handle, const char* label) { + if (!lock_ || handle == 0) { + return; + } + + IOLockLock(lock_); + auto it = ranges_.find(handle); + if (it != ranges_.end()) { + it->second.debugLabel = label != nullptr ? label : "unlabeled"; + } + IOLockUnlock(lock_); + } + void ClearAll() { if (!lock_) { return; @@ -416,6 +453,7 @@ class AddressSpaceManager { void* owner{nullptr}; std::vector buffer; RemoteWriteCallback onRemoteWrite; + const char* debugLabel{"unlabeled"}; OSSharedPtr descriptor{}; OSSharedPtr dmaCommand{}; @@ -485,8 +523,9 @@ class AddressSpaceManager { for (const auto& entry : ranges_) { const auto& range = entry.second; ASFW_ADDRSPACE_LOG( - "AddressSpaceManager[%p] range handle=0x%llx owner=%p addr=0x%012llx len=%u backing=%u dma=0x%08x", + "AddressSpaceManager[%p] range label=%s handle=0x%llx owner=%p addr=0x%012llx len=%u backing=%u dma=0x%08x", this, + range.debugLabel, static_cast(range.meta.handle), range.owner, static_cast(range.meta.address), diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp index 6179b62e..56b9a609 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.cpp @@ -1,12 +1,11 @@ // SBP-2 Normal Command ORB implementation. +// Ported from Apple IOFireWireSBP2ORB. // Ref: SBP-2 §5.1.1 (Normal Command ORB format) #include "SBP2CommandORB.hpp" #include "SBP2DelayedDispatch.hpp" #include "../../Common/FWCommon.hpp" -#include - namespace ASFW::Protocols::SBP2 { // --------------------------------------------------------------------------- @@ -19,12 +18,11 @@ SBP2CommandORB::SBP2CommandORB(AddressSpaceManager& addrMgr, void* owner, , owner_(owner) , maxCommandBlockSize_(maxCommandBlockSize) { - AllocateResources(); + isValid_ = AllocateResources(); } SBP2CommandORB::~SBP2CommandORB() { CancelTimer(); - lifetimeToken_.reset(); DeallocateResources(); } @@ -33,6 +31,10 @@ SBP2CommandORB::~SBP2CommandORB() { // --------------------------------------------------------------------------- bool SBP2CommandORB::AllocateResources() noexcept { + if (orbHandle_ != 0) { + return true; + } + const uint32_t orbSize = Wire::NormalORB::kHeaderSize + maxCommandBlockSize_; orbStorage_.resize(orbSize, 0); @@ -41,11 +43,11 @@ bool SBP2CommandORB::AllocateResources() noexcept { owner_, 0xFFFF, orbSize, &orbHandle_, &orbMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2CommandORB: failed to allocate ORB address space: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2CommandORB: failed to allocate ORB address space: 0x%08x", kr); return false; } - ASFW_LOG(Async, "SBP2CommandORB: allocated %u-byte ORB at %04x:%08x", + ASFW_LOG(SBP2, "SBP2CommandORB: allocated %u-byte ORB at %04x:%08x", orbSize, orbMeta_.addressHi, orbMeta_.addressLo); return true; } @@ -64,6 +66,10 @@ void SBP2CommandORB::DeallocateResources() noexcept { // --------------------------------------------------------------------------- void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { + if (!IsValid() || orbStorage_.size() < Wire::NormalORB::kHeaderSize) { + return; + } + const uint32_t copyLen = static_cast( std::min(cdb.size(), static_cast(maxCommandBlockSize_))); @@ -83,9 +89,13 @@ void SBP2CommandORB::SetCommandBlock(std::span cdb) noexcept { // Prepare for execution (fills in dynamic fields) // --------------------------------------------------------------------------- -void SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, - FW::FwSpeed speed, - uint16_t maxPayloadLog) noexcept { +kern_return_t SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, + FW::FwSpeed speed, + uint16_t maxPayloadLog) noexcept { + if (!IsValid() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + return kIOReturnNotReady; + } + auto* orb = reinterpret_cast(orbStorage_.data()); const uint16_t busNodeID = Wire::NormalizeBusNodeID(localNodeID); @@ -151,20 +161,25 @@ void SBP2CommandORB::PrepareForExecution(uint16_t localNodeID, orb->dataSize = dataDescriptor_.dataSize; // Flush ORB to address space - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } // --------------------------------------------------------------------------- // Write ORB buffer to DMA-backed address space // --------------------------------------------------------------------------- -void SBP2CommandORB::WriteORBToAddressSpace() noexcept { +kern_return_t SBP2CommandORB::WriteORBToAddressSpace() noexcept { + if (!IsValid() || orbHandle_ == 0) { + return kIOReturnNotReady; + } + const auto span = std::span(orbStorage_.data(), orbStorage_.size()); const kern_return_t kr = addrMgr_.WriteLocalData( owner_, orbHandle_, 0, span); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2CommandORB: failed to write ORB to address space: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2CommandORB: failed to write ORB to address space: 0x%08x", kr); } + return kr; } // --------------------------------------------------------------------------- @@ -179,62 +194,77 @@ Async::FWAddress SBP2CommandORB::GetORBAddress() const noexcept { return Async::FWAddress(parts); } -void SBP2CommandORB::SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept { +kern_return_t SBP2CommandORB::SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept { + if (!IsValid() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + return kIOReturnNotReady; + } + auto* orb = reinterpret_cast(orbStorage_.data()); orb->nextORBAddressHi = hi; orb->nextORBAddressLo = lo; - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } -void SBP2CommandORB::SetToDummy() noexcept { +kern_return_t SBP2CommandORB::SetToDummy() noexcept { + if (!IsValid() || orbStorage_.size() < sizeof(Wire::NormalORB)) { + return kIOReturnNotReady; + } + // Set rq_fmt=3 (bits [13:12] = 11) to make device skip this ORB auto* orb = reinterpret_cast(orbStorage_.data()); uint16_t hostOptions = Wire::FromBE16(orb->options); hostOptions = (hostOptions & ~0x3000u) | 0x6000u; orb->options = Wire::ToBE16(hostOptions); - WriteORBToAddressSpace(); + return WriteORBToAddressSpace(); } // --------------------------------------------------------------------------- // Timer management // --------------------------------------------------------------------------- -void SBP2CommandORB::StartTimer(IODispatchQueue* queue) noexcept { +void SBP2CommandORB::StartTimer(IODispatchQueue* completionQueue, + IODispatchQueue* timeoutQueue) noexcept { CancelTimer(); - if (queue == nullptr || timeoutDuration_ == 0) { + if (completionQueue == nullptr || timeoutQueue == nullptr || timeoutDuration_ == 0) { return; } - timerQueue_ = queue; - inProgress_.store(true, std::memory_order_relaxed); + completionQueue_ = completionQueue; + timerQueue_ = timeoutQueue; + auto timerState = timerState_; + timerState->inProgress.store(true, std::memory_order_relaxed); const uint32_t timeout = timeoutDuration_; const uint64_t expectedGeneration = - timerGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; - const std::weak_ptr weakLifetime = lifetimeToken_; + timerState->generation.fetch_add(1, std::memory_order_acq_rel) + 1ULL; const uint64_t delayNs = static_cast(timeout) * 1'000'000ULL; - DispatchAfterCompat(queue, delayNs, [this, weakLifetime, expectedGeneration, timeout]() { - if (weakLifetime.expired()) { - return; - } - if (timerGeneration_.load(std::memory_order_acquire) != expectedGeneration || - !inProgress_.load(std::memory_order_relaxed) || - !completionCallback_) { - return; - } - - ASFW_LOG(Async, "SBP2CommandORB: ORB timeout after %u ms", timeout); - inProgress_.store(false, std::memory_order_relaxed); - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - completionCallback_(-1, Wire::SBPStatus::kUnspecifiedError); + DispatchAfterCompat(timeoutQueue, delayNs, [timerState, + expectedGeneration, + timeout, + completionQueue]() { + DispatchAsyncCompat(completionQueue, [timerState, + expectedGeneration, + timeout]() { + if (timerState->generation.load(std::memory_order_acquire) != expectedGeneration || + !timerState->inProgress.load(std::memory_order_relaxed) || + !timerState->completionCallback) { + return; + } + + ASFW_LOG(SBP2, "SBP2CommandORB: ORB timeout after %u ms", timeout); + timerState->inProgress.store(false, std::memory_order_relaxed); + timerState->generation.fetch_add(1, std::memory_order_acq_rel); + timerState->completionCallback(-1, Wire::SBPStatus::kUnspecifiedError); + }); }); } void SBP2CommandORB::CancelTimer() noexcept { - inProgress_.store(false, std::memory_order_relaxed); + timerState_->inProgress.store(false, std::memory_order_relaxed); + completionQueue_ = nullptr; timerQueue_ = nullptr; - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); + timerState_->generation.fetch_add(1, std::memory_order_acq_rel); } } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp index cd88d810..98e6658a 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp @@ -3,6 +3,7 @@ // SBP-2 Normal Command ORB. // Represents a single SCSI command submitted to the device after login. // +// Ported from Apple IOFireWireSBP2ORB. // Ref: SBP-2 §5.1.1 (Normal Command ORB format) #include "AddressSpaceManager.hpp" @@ -53,31 +54,36 @@ class SBP2CommandORB { void SetFlags(uint32_t flags) noexcept { flags_ = flags; } void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } void SetTimeout(uint32_t ms) noexcept { timeoutDuration_ = ms; } - void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); } + void SetCompletionCallback(CompletionCallback cb) noexcept { + completionCallback_ = std::move(cb); + timerState_->completionCallback = completionCallback_; + } // Bind page table result from SBP2PageTable::Build. void SetDataDescriptor(const SBP2PageTable::Result& ptResult) noexcept { dataDescriptor_ = ptResult; } - // Internal: called by the session layer before submission. - void PrepareForExecution(uint16_t localNodeID, FW::FwSpeed speed, - uint16_t maxPayloadLog) noexcept; + // Internal: called by SBP2LoginSession before submission. + [[nodiscard]] kern_return_t PrepareForExecution(uint16_t localNodeID, + FW::FwSpeed speed, + uint16_t maxPayloadLog) noexcept; // Internal: ORB address for fetch agent / chaining. [[nodiscard]] Async::FWAddress GetORBAddress() const noexcept; // Internal: set the next ORB pointer (big-endian values). - void SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept; + [[nodiscard]] kern_return_t SetNextORBAddress(uint32_t hi, uint32_t lo) noexcept; // Set rq_fmt=3 (NOP dummy) so device skips this ORB if already fetched. - void SetToDummy() noexcept; + [[nodiscard]] kern_return_t SetToDummy() noexcept; // Internal: timer management. - void StartTimer(IODispatchQueue* queue) noexcept; + void StartTimer(IODispatchQueue* completionQueue, IODispatchQueue* timeoutQueue) noexcept; void CancelTimer() noexcept; // State tracking. + [[nodiscard]] bool IsValid() const noexcept { return isValid_; } [[nodiscard]] bool IsAppended() const noexcept { return isAppended_; } void SetAppended(bool state) noexcept { isAppended_ = state; } @@ -88,9 +94,15 @@ class SBP2CommandORB { [[nodiscard]] CompletionCallback& GetCompletionCallback() noexcept { return completionCallback_; } private: + struct TimerState { + std::atomic inProgress{false}; + std::atomic generation{0}; + CompletionCallback completionCallback{}; + }; + bool AllocateResources() noexcept; void DeallocateResources() noexcept; - void WriteORBToAddressSpace() noexcept; + [[nodiscard]] kern_return_t WriteORBToAddressSpace() noexcept; AddressSpaceManager& addrMgr_; void* owner_; @@ -110,14 +122,14 @@ class SBP2CommandORB { SBP2PageTable::Result dataDescriptor_{}; // State. + bool isValid_{false}; bool isAppended_{false}; - std::atomic inProgress_{false}; uint32_t fetchAgentWriteRetries_{20}; // Timer. + IODispatchQueue* completionQueue_{nullptr}; IODispatchQueue* timerQueue_{nullptr}; - std::atomic timerGeneration_{0}; - std::shared_ptr lifetimeToken_{std::make_shared(0)}; + std::shared_ptr timerState_{std::make_shared()}; }; } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp b/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp index 43ebcf44..6ece74e8 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2DelayedDispatch.hpp @@ -12,6 +12,22 @@ namespace ASFW::Protocols::SBP2 { +inline void DispatchAsyncCompat(IODispatchQueue* queue, + std::function callback) noexcept { + if (queue == nullptr || !callback) { + return; + } + +#ifdef ASFW_HOST_TEST + queue->DispatchAsync(std::move(callback)); +#else + auto work = std::move(callback); + queue->DispatchAsync(^{ + work(); + }); +#endif +} + inline void DispatchAfterCompat(IODispatchQueue* queue, uint64_t delayNs, std::function callback) noexcept { diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp new file mode 100644 index 00000000..a7c694ac --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.cpp @@ -0,0 +1,1551 @@ +#include "SBP2LoginSession.hpp" +#include "SBP2DelayedDispatch.hpp" +#include "AddressSpaceManager.hpp" + +#include "../../Async/Interfaces/IFireWireBus.hpp" +#include "../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../Common/FWCommon.hpp" + +#include + +namespace ASFW::Protocols::SBP2 { + +using namespace ASFW::Protocols::SBP2::Wire; + +// --------------------------------------------------------------------------- +// Construction / Destruction +// --------------------------------------------------------------------------- + +SBP2LoginSession::SBP2LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) {} + +SBP2LoginSession::~SBP2LoginSession() { + CancelPendingTimer(); + ClearORBTracking(true); + lifetimeToken_.reset(); + ReleaseOwnedTimeoutQueue(); + DeallocateResources(); +} + +void SBP2LoginSession::SetWorkQueue(IODispatchQueue* queue) noexcept { + workQueue_ = queue; + if (timeoutQueue_ == nullptr) { + EnsureTimeoutQueue(); + } +} + +void SBP2LoginSession::SetTimeoutQueue(IODispatchQueue* queue) noexcept { + timeoutQueue_ = queue; + if (queue != nullptr) { + ReleaseOwnedTimeoutQueue(); + } else { + EnsureTimeoutQueue(); + } +} + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +void SBP2LoginSession::Configure(const SBP2TargetInfo& info) noexcept { + targetInfo_ = info; + configured_ = true; + + ASFW_LOG(SBP2, + "SBP2LoginSession: configured target node=0x%04x mgmt_offset=%u LUN=%u " + "mgmt_timeout=%ums max_orb=%u max_cmd_block=%u", + info.targetNodeId, + info.managementAgentOffset, + info.lun, + info.managementTimeoutMs, + info.maxORBSize, + info.maxCommandBlockSize); +} + +// --------------------------------------------------------------------------- +// Login +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Login() noexcept { + if (!configured_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: not configured"); + return false; + } + + if (state_ == LoginState::LoggingIn || state_ == LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: state=%s, ignoring", ToString(state_)); + return false; + } + + // Allocate address spaces for ORB/response/status on first login. + if (!AllocateResources()) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: resource allocation failed"); + SetState(LoginState::Failed); + return false; + } + + SetState(LoginState::LoggingIn); + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + + BuildLoginORB(); + + ASFW_LOG(SBP2, + "SBP2LoginSession::Login: sending login ORB to node 0x%04x gen=%u LUN=%u", + loginNodeID_, loginGeneration_, targetInfo_.lun); + + // Write the Login ORB address to the management agent. + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const Async::FWAddress mgmtAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_ + } + }; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + loginWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{loginORBAddressBE_.data(), loginORBAddressBE_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnLoginWriteComplete(requestGeneration, status, response); + }); + + if (!loginWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Login: WriteBlock failed immediately"); + SetState(LoginState::Failed); + return false; + } + + // Start management timeout + StartLoginTimer(); + return true; +} + +// --------------------------------------------------------------------------- +// Logout +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Logout() noexcept { + if (state_ != LoginState::LoggedIn && state_ != LoginState::Suspended) { + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: state=%s, ignoring", ToString(state_)); + return false; + } + + SetState(LoginState::LoggingOut); + BuildLogoutORB(); + + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: sending logout ORB loginID=%u", loginID_); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const Async::FWAddress mgmtAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_ + } + }; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + logoutWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{logoutORBAddressBE_.data(), logoutORBAddressBE_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnLogoutWriteComplete(requestGeneration, status, response); + }); + + if (!logoutWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Logout: WriteBlock failed"); + SetState(LoginState::Failed); + return false; + } + + return true; +} + +// --------------------------------------------------------------------------- +// Reconnect +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::Reconnect() noexcept { + if (state_ != LoginState::Suspended && state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::Reconnect: state=%s, ignoring", ToString(state_)); + return false; + } + + SetState(LoginState::Reconnecting); + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + + BuildReconnectORB(); + + ASFW_LOG(SBP2, + "SBP2LoginSession::Reconnect: sending reconnect ORB loginID=%u gen=%u", + loginID_, loginGeneration_); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const Async::FWAddress mgmtAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = 0xFFFF, + .addressLo = ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + .nodeID = loginNodeID_ + } + }; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + reconnectWriteHandle_ = bus_.WriteBlock( + gen, node, mgmtAddr, + std::span{reconnectORBAddressBE_.data(), reconnectORBAddressBE_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnReconnectWriteComplete(requestGeneration, status, response); + }); + + if (!reconnectWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::Reconnect: WriteBlock failed, will retry"); + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { OnReconnectTimeout(); }); + reconnectTimerActive_ = true; + return true; // Will retry + } + + return true; +} + +// --------------------------------------------------------------------------- +// Bus Reset Handling +// --------------------------------------------------------------------------- + +void SBP2LoginSession::HandleBusReset(uint16_t newGeneration) noexcept { + ASFW_LOG(SBP2, + "SBP2LoginSession::HandleBusReset: state=%s newGen=%u loginGen=%u", + ToString(state_), newGeneration, loginGeneration_); + + switch (state_) { + case LoginState::LoggingIn: + // Login was in progress — cancel and retry after reset settles. + CancelLoginTimer(); + loginRetryCount_ = 0; + loginGeneration_ = newGeneration; + ClearORBTracking(true); + SetState(LoginState::Idle); + SubmitDelayedCallback(100, [this]() { (void)Login(); }); + break; + + case LoginState::LoggedIn: + // Transition to Suspended — wait for topology then reconnect. + CancelPendingTimer(); + ClearORBTracking(true); + SetState(LoginState::Suspended); + loginGeneration_ = newGeneration; + break; + + case LoginState::Reconnecting: + // Reconnect was in flight — retry. + reconnectTimerActive_ = false; + CancelPendingTimer(); + ClearORBTracking(true); + loginGeneration_ = newGeneration; + SetState(LoginState::Suspended); + SubmitDelayedCallback(100, [this]() { (void)Reconnect(); }); + break; + + case LoginState::LoggingOut: + // Logout in flight during bus reset — consider logged out. + CancelPendingTimer(); + ClearORBTracking(true); + SetState(LoginState::Idle); + break; + + default: + break; + } +} + +// --------------------------------------------------------------------------- +// Accessors +// --------------------------------------------------------------------------- + +Async::FWAddress SBP2LoginSession::CommandBlockAgent() const noexcept { + return commandBlockAgent_; +} + +uint32_t SBP2LoginSession::ReconnectHoldSeconds() const noexcept { + return reconnectHold_ > 0 ? (1u << reconnectHold_) : 0; +} + +// --------------------------------------------------------------------------- +// Resource Allocation +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::AllocateResources() noexcept { + if (loginORBHandle_ != 0) { + return true; // Already allocated + } + + if (!AllocateLoginORBAddressSpace()) return false; + if (!AllocateLoginResponseAddressSpace()) return false; + if (!AllocateStatusBlockAddressSpace()) return false; + if (!AllocateReconnectORBAddressSpace()) return false; + if (!AllocateLogoutORBAddressSpace()) return false; + + // Register a callback for status block writes — the device writes status + // here to signal login/reconnect/logout completion (and ORB completion + // in Step 2). + addrSpaceMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { + OnStatusBlockRemoteWrite(offset, payload); + }); + + ASFW_LOG(SBP2, "SBP2LoginSession: all address spaces allocated"); + return true; +} + +void SBP2LoginSession::DeallocateResources() noexcept { + ClearORBTracking(true); + + if (loginORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, loginORBHandle_); + loginORBHandle_ = 0; + } + if (loginResponseHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, loginResponseHandle_); + loginResponseHandle_ = 0; + } + if (statusBlockHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, statusBlockHandle_); + statusBlockHandle_ = 0; + } + if (reconnectORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, reconnectORBHandle_); + reconnectORBHandle_ = 0; + } + if (logoutORBHandle_) { + addrSpaceMgr_.DeallocateAddressRange(this, logoutORBHandle_); + logoutORBHandle_ = 0; + } +} + +bool SBP2LoginSession::AllocateLoginORBAddressSpace() noexcept { + // Login ORB is 32 bytes, readable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginORB::kSize, + &loginORBHandle_, &loginORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate login ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginORBHandle_, "sbp2-login-orb"); + return true; +} + +bool SBP2LoginSession::AllocateLoginResponseAddressSpace() noexcept { + // Login response is 16 bytes, writable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LoginResponse::kSize, + &loginResponseHandle_, &loginResponseMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate login response address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(loginResponseHandle_, "sbp2-login-response"); + return true; +} + +bool SBP2LoginSession::AllocateStatusBlockAddressSpace() noexcept { + // Status block is up to 32 bytes, writable by target device. + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::StatusBlock::kMaxSize, + &statusBlockHandle_, &statusBlockMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate status block address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(statusBlockHandle_, "sbp2-status-fifo"); + return true; +} + +bool SBP2LoginSession::AllocateReconnectORBAddressSpace() noexcept { + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::ReconnectORB::kSize, + &reconnectORBHandle_, &reconnectORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate reconnect ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(reconnectORBHandle_, "sbp2-reconnect-orb"); + return true; +} + +bool SBP2LoginSession::AllocateLogoutORBAddressSpace() noexcept { + auto kr = addrSpaceMgr_.AllocateAddressRangeAuto( + this, 0xFFFF, Wire::LogoutORB::kSize, + &logoutORBHandle_, &logoutORBMeta_); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession: failed to allocate logout ORB address space: 0x%08x", kr); + return false; + } + addrSpaceMgr_.SetDebugLabel(logoutORBHandle_, "sbp2-logout-orb"); + return true; +} + +// --------------------------------------------------------------------------- +// ORB Construction +// --------------------------------------------------------------------------- + +void SBP2LoginSession::BuildLoginORB() noexcept { + std::memset(&loginORBBuffer_, 0, sizeof(loginORBBuffer_)); + + // Get local node ID for filling address fields. + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + // Login response address: nodeID in upper 16 bits of addressHi. + const uint32_t responseAddrHi = ToBE32( + ComposeBusAddressHi(localNode, loginResponseMeta_.addressHi)); + const uint32_t responseAddrLo = ToBE32(loginResponseMeta_.addressLo); + + // Status FIFO address. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + + // Fill login ORB fields. + loginORBBuffer_.loginResponseAddressHi = responseAddrHi; + loginORBBuffer_.loginResponseAddressLo = responseAddrLo; + loginORBBuffer_.options = static_cast( + Options::kLoginNotify | Options::kExclusiveLogin); + loginORBBuffer_.lun = ToBE16(targetInfo_.lun); + loginORBBuffer_.passwordLength = 0; + loginORBBuffer_.loginResponseLength = ToBE16(sizeof(Wire::LoginResponse)); + loginORBBuffer_.statusFIFOAddressHi = statusAddrHi; + loginORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + // Write login ORB data to address space so device can read it. + addrSpaceMgr_.WriteLocalData( + this, loginORBHandle_, 0, + std::span{reinterpret_cast(&loginORBBuffer_), + sizeof(loginORBBuffer_)}); + + // Build the 8-byte management agent write payload: ORB address in big-endian. + // Format: [nodeID(2)][addressHi(2)][addressLo(4)] + loginORBAddressBE_[0] = static_cast(localNode >> 8); + loginORBAddressBE_[1] = static_cast(localNode & 0xFF); + loginORBAddressBE_[2] = static_cast(loginORBMeta_.addressHi >> 8); + loginORBAddressBE_[3] = static_cast(loginORBMeta_.addressHi & 0xFF); + const uint32_t orbAddrLoBE = ToBE32(loginORBMeta_.addressLo); + std::memcpy(&loginORBAddressBE_[4], &orbAddrLoBE, sizeof(uint32_t)); + + ASFW_LOG(SBP2, + "SBP2LoginSession::BuildLoginORB: mgmt=0x%08x payload=%02x%02x:%02x%02x:%02x%02x%02x%02x " + "ORB at %04x:%08x, response at %04x:%08x, status at %04x:%08x, LUN=%u", + ManagementAgentAddressLo(targetInfo_.managementAgentOffset), + loginORBAddressBE_[0], loginORBAddressBE_[1], + loginORBAddressBE_[2], loginORBAddressBE_[3], + loginORBAddressBE_[4], loginORBAddressBE_[5], + loginORBAddressBE_[6], loginORBAddressBE_[7], + localNode, loginORBMeta_.addressLo, + localNode, loginResponseMeta_.addressLo, + localNode, statusBlockMeta_.addressLo, + targetInfo_.lun); +} + +void SBP2LoginSession::BuildReconnectORB() noexcept { + std::memset(&reconnectORBBuffer_, 0, sizeof(reconnectORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + // Reconnect ORB: options = reconnect (3) | notify + reconnectORBBuffer_.options = Options::kReconnectNotify; + reconnectORBBuffer_.loginID = ToBE16(loginID_); + + // Status FIFO address — reuse the dedicated status block address space. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + reconnectORBBuffer_.statusFIFOAddressHi = statusAddrHi; + reconnectORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + // Write reconnect ORB data. + addrSpaceMgr_.WriteLocalData( + this, reconnectORBHandle_, 0, + std::span{reinterpret_cast(&reconnectORBBuffer_), + sizeof(reconnectORBBuffer_)}); + + // Build management agent write payload. + reconnectORBAddressBE_[0] = static_cast(localNode >> 8); + reconnectORBAddressBE_[1] = static_cast(localNode & 0xFF); + reconnectORBAddressBE_[2] = static_cast(reconnectORBMeta_.addressHi >> 8); + reconnectORBAddressBE_[3] = static_cast(reconnectORBMeta_.addressHi & 0xFF); + const uint32_t addrLoBE = ToBE32(reconnectORBMeta_.addressLo); + std::memcpy(&reconnectORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +void SBP2LoginSession::BuildLogoutORB() noexcept { + std::memset(&logoutORBBuffer_, 0, sizeof(logoutORBBuffer_)); + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + + logoutORBBuffer_.options = Options::kLogoutNotify; + logoutORBBuffer_.loginID = ToBE16(loginID_); + + // Status FIFO address — reuse the dedicated status block address space. + const uint32_t statusAddrHi = ToBE32( + ComposeBusAddressHi(localNode, statusBlockMeta_.addressHi)); + const uint32_t statusAddrLo = ToBE32(statusBlockMeta_.addressLo); + logoutORBBuffer_.statusFIFOAddressHi = statusAddrHi; + logoutORBBuffer_.statusFIFOAddressLo = statusAddrLo; + + addrSpaceMgr_.WriteLocalData( + this, logoutORBHandle_, 0, + std::span{reinterpret_cast(&logoutORBBuffer_), + sizeof(logoutORBBuffer_)}); + + logoutORBAddressBE_[0] = static_cast(localNode >> 8); + logoutORBAddressBE_[1] = static_cast(localNode & 0xFF); + logoutORBAddressBE_[2] = static_cast(logoutORBMeta_.addressHi >> 8); + logoutORBAddressBE_[3] = static_cast(logoutORBMeta_.addressHi & 0xFF); + const uint32_t addrLoBE = ToBE32(logoutORBMeta_.addressLo); + std::memcpy(&logoutORBAddressBE_[4], &addrLoBE, sizeof(uint32_t)); +} + +// --------------------------------------------------------------------------- +// Completion Handlers +// --------------------------------------------------------------------------- + +void SBP2LoginSession::OnLoginWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingIn) { + return; + } + + CancelLoginTimer(); + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnLoginWriteComplete: status=%s, retrying (%u/%u)", + Async::ToString(status), loginRetryCount_ + 1, kLoginRetryMax); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: login retries exhausted"); + SetState(LoginState::Failed); + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Management agent write ACK'd. The device will now: + // 1. Fetch the ORB via read from our address space + // 2. Process the login + // 3. Write login response to our address space + // 4. Write status block to our status FIFO + // + // We wait for the status block write callback (OnStatusBlockRemoteWrite) + // before reading the login response. Restart the timer for the device + // processing window. + ASFW_LOG(SBP2, "SBP2LoginSession: management agent write ACK'd, waiting for status block"); + StartLoginTimer(); +} + +void SBP2LoginSession::OnLoginTimeout() noexcept { + loginTimerActive_ = false; + + if (state_ != LoginState::LoggingIn) { + return; // Already handled + } + + ASFW_LOG(SBP2, + "SBP2LoginSession: login timeout (%u/%u) waiting for target node=0x%04x " + "to read label=sbp2-login-orb at %04x:%08x and write label=sbp2-status-fifo at %04x:%08x", + loginRetryCount_ + 1, + kLoginRetryMax, + loginNodeID_, + loginORBMeta_.addressHi, + loginORBMeta_.addressLo, + statusBlockMeta_.addressHi, + statusBlockMeta_.addressLo); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SetState(LoginState::Idle); + (void)Login(); + } else { + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -2; // timeout + params.generation = loginGeneration_; + loginCallback_(params); + } + } +} + +void SBP2LoginSession::OnReconnectWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::Reconnecting) { + return; + } + + reconnectTimerActive_ = false; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnReconnectWriteComplete: status=%s, retrying", + Async::ToString(status)); + + SubmitDelayedCallback(100, [this]() { (void)Reconnect(); }); + return; + } + + // Reconnect ORB write ACK'd. Wait for status block from device. + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect write ACK'd, waiting for status block"); +} + +void SBP2LoginSession::OnReconnectTimeout() noexcept { + reconnectTimerActive_ = false; + + if (state_ != LoginState::Reconnecting) { + return; + } + + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect timeout, falling back to full login"); + SetState(LoginState::Idle); + (void)Login(); +} + +void SBP2LoginSession::OnLogoutWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggingOut) { + return; + } + + logoutTimerActive_ = false; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnLogoutWriteComplete: status=%s", + Async::ToString(status)); + } + + const uint16_t oldLoginID = loginID_; + loginID_ = 0; + SetState(LoginState::Idle); + + ASFW_LOG(SBP2, "SBP2LoginSession: logout complete (was loginID=%u)", oldLoginID); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = (status == Async::AsyncStatus::kSuccess) ? 0 : -1; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +void SBP2LoginSession::OnLogoutTimeout() noexcept { + logoutTimerActive_ = false; + ASFW_LOG(SBP2, "SBP2LoginSession: logout timeout, transitioning to Idle anyway"); + loginID_ = 0; + SetState(LoginState::Idle); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = -2; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +// --------------------------------------------------------------------------- +// Status Block Handling +// --------------------------------------------------------------------------- + +void SBP2LoginSession::OnStatusBlockRemoteWrite(uint32_t offset, + std::span payload) noexcept { + if (payload.empty()) { + return; + } + + Wire::StatusBlock block{}; + uint32_t len = static_cast(payload.size()); + if (len > sizeof(block)) { + len = sizeof(block); + } + std::memcpy(&block, payload.data(), len); + + ASFW_LOG(SBP2, + "SBP2LoginSession::OnStatusBlockRemoteWrite: state=%s offset=%u len=%u " + "src=%u resp=%u dead=%u sbpStatus=%u", + ToString(state_), offset, len, + block.Source(), block.Response(), block.DeadBit(), block.sbpStatus); + + // Dispatch to state-specific handler. + switch (state_) { + case LoginState::LoggingIn: + CancelLoginTimer(); + CompleteLoginFromStatusBlock(block, len); + break; + + case LoginState::Reconnecting: + reconnectTimerActive_ = false; + CompleteReconnectFromStatusBlock(block, len); + break; + + case LoginState::LoggingOut: + logoutTimerActive_ = false; + CompleteLogoutFromStatusBlock(block, len); + break; + + case LoginState::LoggedIn: + // Unsolicited status or ORB completion — forward to callback. + ProcessStatusBlock(block, len); + break; + + default: + ASFW_LOG(SBP2, "SBP2LoginSession: unexpected status block in state %s", ToString(state_)); + break; + } +} + +void SBP2LoginSession::CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2LoginSession: login failed — sbpStatus=%u, retrying (%u/%u)", + block.sbpStatus, loginRetryCount_ + 1, kLoginRetryMax); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Login succeeded — read the login response that the device wrote to + // our address space. + std::vector responseData; + auto kr = addrSpaceMgr_.ReadIncomingData( + this, loginResponseHandle_, 0, sizeof(Wire::LoginResponse), &responseData); + + if (kr != kIOReturnSuccess || responseData.size() < sizeof(Wire::LoginResponse)) { + ASFW_LOG(SBP2, + "SBP2LoginSession: failed to read login response (kr=0x%08x, len=%zu)", + kr, responseData.size()); + + if (loginRetryCount_ < kLoginRetryMax) { + loginRetryCount_++; + SubmitDelayedCallback(kLoginRetryDelayMs, [this]() { + loginGeneration_ = static_cast(busInfo_.GetGeneration().value); + loginNodeID_ = targetInfo_.targetNodeId; + SetState(LoginState::Idle); + (void)Login(); + }); + return; + } + + SetState(LoginState::Failed); + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = -1; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } + return; + } + + // Parse login response. + Wire::LoginResponse resp{}; + std::memcpy(&resp, responseData.data(), sizeof(resp)); + + loginID_ = FromBE16(resp.loginID); + reconnectHold_ = FromBE16(resp.reconnectHold); + loginResponse_ = resp; + + // Extract command block agent address. + const uint32_t cbaHi = FromBE32(resp.commandBlockAgentAddressHi); + const uint32_t cbaLo = FromBE32(resp.commandBlockAgentAddressLo); + commandBlockAgent_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = static_cast(cbaHi & 0xFFFFu), + .addressLo = cbaLo, + .nodeID = loginNodeID_ + } + }; + + loginRetryCount_ = 0; + SetState(LoginState::LoggedIn); + + // If unsolicited status was requested while not logged in, enable it now. + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatus(); + } + + // Compute CBA-derived addresses for fetch agent, doorbell, agent reset, + // and unsolicited status enable. + fetchAgentAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kFetchAgent, + .nodeID = loginNodeID_ + } + }; + doorbellAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kDoorbell, + .nodeID = loginNodeID_ + } + }; + agentResetAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kAgentReset, + .nodeID = loginNodeID_ + } + }; + unsolicitedStatusAddress_ = Async::FWAddress{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = commandBlockAgent_.addressHi, + .addressLo = commandBlockAgent_.addressLo + Wire::CommandBlockAgentOffsets::kUnsolicitedStatusEnable, + .nodeID = loginNodeID_ + } + }; + + ASFW_LOG(SBP2, + "SBP2LoginSession: login successful — loginID=%u, CBA=%04x:%08x, " + "reconnectHold=2^%u=%us", + loginID_, + commandBlockAgent_.addressHi, commandBlockAgent_.addressLo, + reconnectHold_, ReconnectHoldSeconds()); + + // Send Set Busy Timeout to the device. + { + const uint32_t busyTimeout = ToBE32(kBusyTimeoutValue); + const Async::FWAddress busyAddr{ + Async::FWAddress::QualifiedAddressParts{ + .addressHi = kCSRBusAddressHi, + .addressLo = kBusyTimeoutAddressLo, + .nodeID = loginNodeID_ + } + }; + bus_.WriteBlock( + FW::Generation{loginGeneration_}, + FW::NodeId{static_cast(loginNodeID_ & 0x3Fu)}, + busyAddr, + std::span{reinterpret_cast(&busyTimeout), 4}, + busInfo_.GetSpeed(FW::NodeId{static_cast(loginNodeID_ & 0x3Fu)}), + [](Async::AsyncStatus, std::span) {}); + } + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = 0; + params.loginResponse = loginResponse_; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } +} + +void SBP2LoginSession::CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2LoginSession: reconnect failed — sbpStatus=%u, falling back to full login", + block.sbpStatus); + + SetState(LoginState::Idle); + (void)Login(); + return; + } + + SetState(LoginState::LoggedIn); + ASFW_LOG(SBP2, "SBP2LoginSession: reconnect successful — loginID=%u", loginID_); + + // If unsolicited status was requested while not logged in, enable it now. + if (unsolicitedStatusRequested_) { + unsolicitedStatusRequested_ = false; + EnableUnsolicitedStatus(); + } + + if (loginCallback_) { + LoginCompleteParams params{}; + params.status = 0; + params.loginResponse = loginResponse_; + params.statusBlock = block; + params.statusBlockLength = length; + params.generation = loginGeneration_; + loginCallback_(params); + } +} + +void SBP2LoginSession::CompleteLogoutFromStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + const uint16_t oldLoginID = loginID_; + loginID_ = 0; + SetState(LoginState::Idle); + + ASFW_LOG(SBP2, "SBP2LoginSession: logout complete (was loginID=%u)", oldLoginID); + + if (logoutCallback_) { + LogoutCompleteParams params{}; + params.status = 0; + params.generation = loginGeneration_; + logoutCallback_(params); + } +} + +void SBP2LoginSession::ProcessStatusBlock(const Wire::StatusBlock& block, + uint32_t length) noexcept { + // Distinguish unsolicited vs solicited status. + // Unsolicited: (details & 0xC0) == 0x80 (source bit set, resp == 0) + const bool isUnsolicited = (block.details & 0xC0) == 0x80; + + if (statusCallback_) { + statusCallback_(block, length); + } + + if (isUnsolicited) { + // Re-enable unsolicited status so device can send more + EnableUnsolicitedStatus(); + return; + } + + const uint64_t orbKey = MakeORBKey(FromBE16(block.orbOffsetHi), FromBE32(block.orbOffsetLo)); + const auto it = outstandingORBs_.find(orbKey); + if (it == outstandingORBs_.end()) { + ASFW_LOG(SBP2, + "SBP2LoginSession::ProcessStatusBlock: unmatched ORB status hi=%04x lo=%08x", + FromBE16(block.orbOffsetHi), + FromBE32(block.orbOffsetLo)); + return; + } + + SBP2CommandORB* orb = it->second; + outstandingORBs_.erase(it); + if (orb != nullptr) { + orb->CancelTimer(); + auto& cb = orb->GetCompletionCallback(); + if (cb) { + cb(0, block.sbpStatus); + } + } +} + +// --------------------------------------------------------------------------- +// Internal Helpers +// --------------------------------------------------------------------------- + +void SBP2LoginSession::SetState(LoginState newState) noexcept { + if (state_ != newState) { + ASFW_LOG(SBP2, "SBP2LoginSession: state %s -> %s", ToString(state_), ToString(newState)); + state_ = newState; + } +} + +void SBP2LoginSession::StartLoginTimer() noexcept { + loginTimerActive_ = true; + SubmitDelayedCallback(targetInfo_.managementTimeoutMs, [this]() { + OnLoginTimeout(); + }); +} + +void SBP2LoginSession::CancelLoginTimer() noexcept { + loginTimerActive_ = false; + CancelPendingTimer(); +} + +void SBP2LoginSession::CancelPendingTimer() noexcept { + delayedCallbackGeneration_.fetch_add(1, std::memory_order_acq_rel); +} + +void SBP2LoginSession::EnsureTimeoutQueue() noexcept { + if (timeoutQueue_ != nullptr || workQueue_ == nullptr) { + return; + } + +#ifdef ASFW_HOST_TEST + ownedTimeoutQueue_ = std::make_unique(); + if (ownedTimeoutQueue_ != nullptr && + workQueue_->UsesManualDispatchForTesting()) { + ownedTimeoutQueue_->SetManualDispatchForTesting(true); + } + timeoutQueue_ = ownedTimeoutQueue_.get(); +#else + IODispatchQueue* queue = nullptr; + const kern_return_t kr = IODispatchQueue::Create("com.asfw.sbp2.timeout", 0, 0, &queue); + if (kr != kIOReturnSuccess || queue == nullptr) { + ASFW_LOG(SBP2, + "SBP2LoginSession: failed to create timeout queue (kr=0x%08x), falling back to workQueue", + kr); + timeoutQueue_ = workQueue_; + return; + } + ownedTimeoutQueue_ = queue; + timeoutQueue_ = ownedTimeoutQueue_; +#endif +} + +void SBP2LoginSession::ReleaseOwnedTimeoutQueue() noexcept { +#ifdef ASFW_HOST_TEST + IODispatchQueue* ownedQueue = ownedTimeoutQueue_.get(); + ownedTimeoutQueue_.reset(); + if (timeoutQueue_ == ownedQueue) { + timeoutQueue_ = nullptr; + } +#else + IODispatchQueue* ownedQueue = ownedTimeoutQueue_; + if (ownedTimeoutQueue_ != nullptr) { + ownedTimeoutQueue_->release(); + ownedTimeoutQueue_ = nullptr; + } + if (timeoutQueue_ == ownedQueue) { + timeoutQueue_ = nullptr; + } +#endif +} + +IODispatchQueue* SBP2LoginSession::EffectiveTimeoutQueue() const noexcept { + if (timeoutQueue_ != nullptr) { + return timeoutQueue_; + } + return workQueue_; +} + +void SBP2LoginSession::SubmitDelayedCallback(uint64_t delayMs, + std::function callback) noexcept { + IODispatchQueue* delayQueue = timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_; + if (workQueue_ == nullptr || delayQueue == nullptr || !callback) { + return; + } + + const uint64_t expectedGeneration = + delayedCallbackGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; + const std::weak_ptr weakLifetime = lifetimeToken_; + const uint64_t delayNs = delayMs * 1'000'000ULL; + IODispatchQueue* bounceQueue = workQueue_; + + DispatchAfterCompat(delayQueue, + delayNs, + [this, + weakLifetime, + expectedGeneration, + bounceQueue, + cb = std::move(callback)]() mutable { + if (weakLifetime.expired()) { + return; + } + DispatchAsyncCompat(bounceQueue, + [this, + weakLifetime, + expectedGeneration, + cb = std::move(cb)]() mutable { + if (weakLifetime.expired()) { + return; + } + if (delayedCallbackGeneration_.load(std::memory_order_acquire) != expectedGeneration) { + return; + } + cb(); + }); + }); +} + +uint64_t SBP2LoginSession::MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept { + return (static_cast(addressHi) << 32) | static_cast(addressLo); +} + +uint64_t SBP2LoginSession::MakeORBKey(const Async::FWAddress& address) noexcept { + return MakeORBKey(address.addressHi, address.addressLo); +} + +void SBP2LoginSession::ClearORBTracking(bool cancelTimers) noexcept { + if (cancelTimers) { + for (auto& [key, orb] : outstandingORBs_) { + if (orb != nullptr) { + orb->CancelTimer(); + orb->SetAppended(false); + } + } + if (activeFetchAgentORB_ != nullptr) { + activeFetchAgentORB_->CancelTimer(); + } + for (auto* orb : pendingImmediateORBs_) { + if (orb != nullptr) { + orb->CancelTimer(); + } + } + } + + outstandingORBs_.clear(); + pendingImmediateORBs_.clear(); + chainTailORB_ = nullptr; + activeFetchAgentORB_ = nullptr; + fetchAgentWriteHandle_ = {}; + fetchAgentWriteInUse_ = false; + doorbellWriteHandle_ = {}; + doorbellInProgress_ = false; + doorbellRingAgain_ = false; +} + +// --------------------------------------------------------------------------- +// ORB Submission +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::SubmitORB(SBP2CommandORB* orb) noexcept { + if (state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: state=%s, rejecting", ToString(state_)); + return false; + } + + if (orb == nullptr || !orb->IsValid() || orb->IsAppended()) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: invalid ORB (null=%d, valid=%d, appended=%d)", + orb == nullptr, + orb != nullptr && orb->IsValid(), + orb != nullptr && orb->IsAppended()); + return false; + } + + // Fetch agent and doorbell addresses are computed at login time. + + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const FW::FwSpeed speed = busInfo_.GetSpeed( + FW::NodeId{static_cast(loginNodeID_ & 0x3Fu)}); + + // Max payload log: derive from maxPayloadSize_ (bytes → log2(quadlets)). + // Capped at 15 (max 2^15 = 32768 quadlets = 128KB). + uint16_t maxPayloadLog = 0; + { + uint16_t payloadBytes = maxPayloadSize_; + if (payloadBytes > 4096) payloadBytes = 4096; + uint16_t quadlets = payloadBytes / 4; + if (quadlets > 0) { + maxPayloadLog = 0; + while ((1u << maxPayloadLog) < quadlets && maxPayloadLog < 15) { + maxPayloadLog++; + } + } + } + + const kern_return_t prepareKr = orb->PrepareForExecution(localNode, speed, maxPayloadLog); + if (prepareKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: PrepareForExecution failed: 0x%08x", + prepareKr); + return false; + } + + orb->SetFetchAgentWriteRetries(20); + orb->SetAppended(true); + outstandingORBs_[MakeORBKey(orb->GetORBAddress())] = orb; + + const bool isImmediate = (orb->GetFlags() & SBP2CommandORB::kImmediate) != 0; + + if (isImmediate) { + chainTailORB_ = orb; + + if (fetchAgentWriteInUse_) { + pendingImmediateORBs_.push_back(orb); + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitORB: fetch agent busy, deferring ORB"); + return true; + } + + return AppendORBImmediate(orb); + } else { + // Chained: link to last ORB, ring doorbell + if (!AppendORB(orb)) { + return false; + } + RingDoorbell(); + } + + return true; +} + +bool SBP2LoginSession::AppendORBImmediate(SBP2CommandORB* orb) noexcept { + if (orb == nullptr || fetchAgentWriteInUse_) { + return false; + } + + // Build 8-byte ORB address in big-endian + // Format: [nodeID(2)][addressHi(2)][addressLo(4)] + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const Async::FWAddress orbAddr = orb->GetORBAddress(); + + fetchAgentWriteData_[0] = static_cast(localNode >> 8); + fetchAgentWriteData_[1] = static_cast(localNode & 0xFF); + fetchAgentWriteData_[2] = static_cast(orbAddr.addressHi >> 8); + fetchAgentWriteData_[3] = static_cast(orbAddr.addressHi & 0xFF); + const uint32_t addrLoBE = ToBE32(orbAddr.addressLo); + std::memcpy(&fetchAgentWriteData_[4], &addrLoBE, sizeof(uint32_t)); + + activeFetchAgentORB_ = orb; + fetchAgentWriteInUse_ = true; + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + fetchAgentWriteHandle_ = bus_.WriteBlock( + gen, node, fetchAgentAddress_, + std::span{fetchAgentWriteData_.data(), fetchAgentWriteData_.size()}, + speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnFetchAgentWriteComplete(requestGeneration, status, response); + }); + + if (!fetchAgentWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::AppendORBImmediate: WriteBlock failed"); + fetchAgentWriteInUse_ = false; + activeFetchAgentORB_ = nullptr; + FailSubmittedORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + + ASFW_LOG(SBP2, + "SBP2LoginSession::AppendORBImmediate: wrote ORB addr %04x:%08x to fetch agent", + localNode, orbAddr.addressLo); + return true; +} + +void SBP2LoginSession::FailSubmittedORB(SBP2CommandORB* orb, + int transportStatus, + uint8_t sbpStatus) noexcept { + if (orb == nullptr) { + return; + } + + const auto key = MakeORBKey(orb->GetORBAddress()); + outstandingORBs_.erase(key); + pendingImmediateORBs_.erase( + std::remove(pendingImmediateORBs_.begin(), pendingImmediateORBs_.end(), orb), + pendingImmediateORBs_.end()); + if (activeFetchAgentORB_ == orb) { + activeFetchAgentORB_ = nullptr; + } + if (chainTailORB_ == orb) { + chainTailORB_ = nullptr; + } + orb->CancelTimer(); + orb->SetAppended(false); + + auto& cb = orb->GetCompletionCallback(); + if (cb) { + cb(transportStatus, sbpStatus); + } +} + +bool SBP2LoginSession::AppendORB(SBP2CommandORB* orb) noexcept { + if (chainTailORB_ == nullptr) { + // First ORB — write directly to fetch agent instead of chaining + chainTailORB_ = orb; + return AppendORBImmediate(orb); + } + + if (chainTailORB_ != orb) { + const Async::FWAddress orbAddr = orb->GetORBAddress(); + + // Set the new ORB's address in big-endian into the last ORB's next pointer + const uint16_t localNode = + NormalizeBusNodeID(static_cast(busInfo_.GetLocalNodeID().value)); + const uint32_t nextHi = ToBE32(ComposeBusAddressHi(localNode, orbAddr.addressHi)); + const uint32_t nextLo = ToBE32(orbAddr.addressLo); + const kern_return_t linkKr = chainTailORB_->SetNextORBAddress(nextHi, nextLo); + if (linkKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::AppendORB: SetNextORBAddress failed: 0x%08x", + linkKr); + FailSubmittedORB(orb, -1, Wire::SBPStatus::kUnspecifiedError); + return false; + } + + chainTailORB_ = orb; + } + + return true; +} + +void SBP2LoginSession::RingDoorbell() noexcept { + if (doorbellInProgress_) { + doorbellRingAgain_ = true; + return; + } + + doorbellInProgress_ = true; + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + doorbellWriteHandle_ = bus_.WriteQuad( + gen, node, doorbellAddress_, 0, speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnDoorbellComplete(requestGeneration, status, response); + }); + + if (!doorbellWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::RingDoorbell: WriteQuad failed"); + doorbellInProgress_ = false; + } +} + +void SBP2LoginSession::OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + fetchAgentWriteInUse_ = false; + fetchAgentWriteHandle_ = {}; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, + "SBP2LoginSession::OnFetchAgentWriteComplete: status=%s, retries=%u", + Async::ToString(status), + activeFetchAgentORB_ ? activeFetchAgentORB_->GetFetchAgentWriteRetries() : 0); + + if (activeFetchAgentORB_ != nullptr) { + uint32_t retries = activeFetchAgentORB_->GetFetchAgentWriteRetries(); + if (retries > 0) { + retries--; + activeFetchAgentORB_->SetFetchAgentWriteRetries(retries); + // Retry after a delay + SBP2CommandORB* retryORB = activeFetchAgentORB_; + SubmitDelayedCallback(1000, [this, retryORB]() { + if (activeFetchAgentORB_ == retryORB) { + AppendORBImmediate(retryORB); + } + }); + return; + } + + // Retries exhausted — report failure + outstandingORBs_.erase(MakeORBKey(activeFetchAgentORB_->GetORBAddress())); + activeFetchAgentORB_->SetAppended(false); + auto& cb = activeFetchAgentORB_->GetCompletionCallback(); + if (cb) { + cb(-1, Wire::SBPStatus::kUnspecifiedError); + } + } + activeFetchAgentORB_ = nullptr; + if (!pendingImmediateORBs_.empty()) { + SBP2CommandORB* next = pendingImmediateORBs_.front(); + pendingImmediateORBs_.pop_front(); + AppendORBImmediate(next); + } + return; + } + + // Fetch agent write succeeded. Submit deferred ORB if any. + activeFetchAgentORB_ = nullptr; + + if (!pendingImmediateORBs_.empty()) { + SBP2CommandORB* next = pendingImmediateORBs_.front(); + pendingImmediateORBs_.pop_front(); + ASFW_LOG(SBP2, "SBP2LoginSession: submitting deferred ORB"); + AppendORBImmediate(next); + } +} + +void SBP2LoginSession::OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + doorbellInProgress_ = false; + doorbellWriteHandle_ = {}; + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnDoorbellComplete: status=%s", + Async::ToString(status)); + } + + if (doorbellRingAgain_) { + doorbellRingAgain_ = false; + RingDoorbell(); + } +} + +// --------------------------------------------------------------------------- +// Management ORB Submission +// --------------------------------------------------------------------------- + +bool SBP2LoginSession::SubmitManagementORB(SBP2ManagementORB* orb) noexcept { + if (state_ != LoginState::LoggedIn) { + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitManagementORB: state=%s, rejecting", + ToString(state_)); + return false; + } + + if (orb == nullptr) { + return false; + } + + EnsureTimeoutQueue(); + + // Configure the management ORB with current session parameters + orb->SetLoginID(loginID_); + orb->SetManagementAgentOffset(targetInfo_.managementAgentOffset); + orb->SetTargetNode(loginGeneration_, loginNodeID_); + orb->SetTimeout(targetInfo_.managementTimeoutMs); + orb->SetWorkQueue(workQueue_); + orb->SetTimeoutQueue(EffectiveTimeoutQueue()); + + ASFW_LOG(SBP2, "SBP2LoginSession::SubmitManagementORB: function=%u", + static_cast(orb->GetFunction())); + + return orb->Execute(); +} + +// --------------------------------------------------------------------------- +// Fetch Agent Reset +// --------------------------------------------------------------------------- + +void SBP2LoginSession::ResetFetchAgent(std::function callback) noexcept { + if (state_ != LoginState::LoggedIn) { + if (callback) callback(-1); + return; + } + + if (agentResetInProgress_) { + if (callback) callback(-1); + return; + } + + agentResetInProgress_ = true; + agentResetCallback_ = std::move(callback); + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + agentResetWriteHandle_ = bus_.WriteQuad( + gen, node, agentResetAddress_, 0, speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnAgentResetComplete(requestGeneration, status, response); + }); + + if (!agentResetWriteHandle_) { + ASFW_LOG(SBP2, "SBP2LoginSession::ResetFetchAgent: WriteQuad failed"); + agentResetInProgress_ = false; + if (agentResetCallback_) { + agentResetCallback_(-1); + agentResetCallback_ = nullptr; + } + } +} + +void SBP2LoginSession::OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_) { + return; + } + + agentResetInProgress_ = false; + + // Clear ORB chain after reset + ClearORBTracking(true); + + ASFW_LOG(SBP2, "SBP2LoginSession::OnAgentResetComplete: status=%s, ORB chain cleared", + Async::ToString(status)); + + if (agentResetCallback_) { + int result = (status == Async::AsyncStatus::kSuccess) ? 0 : -1; + auto cb = std::move(agentResetCallback_); + agentResetCallback_ = nullptr; + cb(result); + } +} + +// --------------------------------------------------------------------------- +// Unsolicited Status Enable +// --------------------------------------------------------------------------- + +void SBP2LoginSession::EnableUnsolicitedStatus() noexcept { + if (state_ != LoginState::LoggedIn) { + unsolicitedStatusRequested_ = true; + return; + } + + const FW::Generation gen{loginGeneration_}; + const FW::NodeId node{static_cast(loginNodeID_ & 0x3Fu)}; + const FW::FwSpeed speed = busInfo_.GetSpeed(node); + + unsolicitedStatusWriteHandle_ = bus_.WriteQuad( + gen, node, unsolicitedStatusAddress_, 0, speed, + [this, requestGeneration = loginGeneration_](Async::AsyncStatus status, + std::span response) { + OnUnsolicitedStatusEnableComplete(requestGeneration, status, response); + }); +} + +void SBP2LoginSession::OnUnsolicitedStatusEnableComplete( + uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept { + if (expectedGeneration != loginGeneration_ || state_ != LoginState::LoggedIn) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2LoginSession::OnUnsolicitedStatusEnableComplete: status=%s", + Async::ToString(status)); + } +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp new file mode 100644 index 00000000..a98e9284 --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp @@ -0,0 +1,448 @@ +#pragma once + +// SBP-2 Login/Reconnect/Logout state machine for ASFW. +// Ported from Apple IOFireWireSBP2Login.cpp — simplified for DriverKit. +// +// Lifecycle: +// 1. Create SBP2LoginSession with bus + address-space deps +// 2. Call Configure() with ROM-derived parameters (management offset, LUN, etc.) +// 3. Call Login() — sends login ORB to device's management agent +// 4. On success, session is kLoggedIn — can submit ORBs via fetch agent +// 5. On bus reset, auto Reconnect() with stored loginID +// 6. Call Logout() to terminate session + +#include "SBP2WireFormats.hpp" +#include "SBP2CommandORB.hpp" +#include "SBP2ManagementORB.hpp" +#include "AddressSpaceManager.hpp" +#include "../../Async/AsyncTypes.hpp" +#include "../../Logging/Logging.hpp" + +#include +#ifdef ASFW_HOST_TEST +#include "../../Testing/HostDriverKitStubs.hpp" +#else +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +namespace ASFW::Async { +class IFireWireBus; +class IFireWireBusInfo; +} + +namespace ASFW::Protocols::SBP2 { + +// --------------------------------------------------------------------------- +// Configuration parameters (from Config ROM Unit_Directory parsing) +// --------------------------------------------------------------------------- + +struct SBP2TargetInfo { + uint32_t managementAgentOffset{0}; // From Management_Agent_Offset key + uint16_t lun{0}; // Logical unit number + + // From Unit_Characteristics key (if present) + uint32_t managementTimeoutMs{2000}; // (byte[1] of unitCharacteristics) * 500 ms + uint16_t maxORBSize{32}; // (byte[0] * 4), min 32 + uint16_t maxCommandBlockSize{0}; // maxORBSize - sizeof(NormalORB header) + + // From Fast_Start key (optional) + bool fastStartSupported{false}; + uint8_t fastStartOffset{0}; + uint8_t fastStartMaxPayload{0}; + + // Target node (from discovery) + uint16_t targetNodeId{0xFFFF}; +}; + +// --------------------------------------------------------------------------- +// Login completion callback parameters +// --------------------------------------------------------------------------- + +struct LoginCompleteParams { + int status{0}; // 0 = success, negative = errno-style error + Wire::LoginResponse loginResponse{}; + Wire::StatusBlock statusBlock{}; + uint32_t statusBlockLength{0}; + uint16_t generation{0}; +}; + +struct LogoutCompleteParams { + int status{0}; + uint16_t generation{0}; +}; + +// --------------------------------------------------------------------------- +// Login session states +// --------------------------------------------------------------------------- + +enum class LoginState : uint8_t { + Idle, + LoggingIn, + LoggedIn, + Reconnecting, + LoggingOut, + Suspended, // Lost after bus reset, waiting for reconnect + Failed +}; + +[[nodiscard]] inline constexpr const char* ToString(LoginState s) noexcept { + switch (s) { + case LoginState::Idle: return "Idle"; + case LoginState::LoggingIn: return "LoggingIn"; + case LoginState::LoggedIn: return "LoggedIn"; + case LoginState::Reconnecting: return "Reconnecting"; + case LoginState::LoggingOut: return "LoggingOut"; + case LoginState::Suspended: return "Suspended"; + case LoginState::Failed: return "Failed"; + } + return "Unknown"; +} + +// --------------------------------------------------------------------------- +// SBP2LoginSession +// --------------------------------------------------------------------------- + +class SBP2LoginSession { + friend class SBP2SessionRegistry; + +public: + using LoginCallback = std::function; + using LogoutCallback = std::function; + using StatusCallback = std::function; + + SBP2LoginSession(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr); + ~SBP2LoginSession(); + + SBP2LoginSession(const SBP2LoginSession&) = delete; + SBP2LoginSession& operator=(const SBP2LoginSession&) = delete; + + // ----------------------------------------------------------------------- + // Configuration (call once before Login) + // ----------------------------------------------------------------------- + + /// Configure target parameters from Config ROM. Must be called before Login(). + void Configure(const SBP2TargetInfo& info) noexcept; + + /// Set login completion callback. + void SetLoginCallback(LoginCallback cb) noexcept { loginCallback_ = std::move(cb); } + + /// Set logout completion callback. + void SetLogoutCallback(LogoutCallback cb) noexcept { logoutCallback_ = std::move(cb); } + + /// Set status block notification callback (receives solicited + unsolicited status). + void SetStatusCallback(StatusCallback cb) noexcept { statusCallback_ = std::move(cb); } + + /// Bind the IODispatchQueue used for delayed callbacks (timers). + /// Must be called before Login() for timeout/retry support. + void SetWorkQueue(IODispatchQueue* queue) noexcept; + void SetTimeoutQueue(IODispatchQueue* queue) noexcept; + + // ----------------------------------------------------------------------- + // Session operations + // ----------------------------------------------------------------------- + + /// Initiate login to device. Completion via loginCallback_. + /// Returns false if already logged in or configuration missing. + [[nodiscard]] bool Login() noexcept; + + /// Initiate logout. Completion via logoutCallback_. + [[nodiscard]] bool Logout() noexcept; + + /// Reconnect after bus reset. Called automatically or manually. + [[nodiscard]] bool Reconnect() noexcept; + + /// Handle bus reset notification — transitions to Suspended if logged in. + void HandleBusReset(uint16_t newGeneration) noexcept; + + // ----------------------------------------------------------------------- + // Accessors + // ----------------------------------------------------------------------- + + [[nodiscard]] LoginState State() const noexcept { return state_; } + [[nodiscard]] uint16_t LoginID() const noexcept { return loginID_; } + [[nodiscard]] uint16_t Generation() const noexcept { return loginGeneration_; } + [[nodiscard]] const SBP2TargetInfo& TargetInfo() const noexcept { return targetInfo_; } + + /// Command Block Agent address (valid after successful login). + [[nodiscard]] Async::FWAddress CommandBlockAgent() const noexcept; + + /// Get negotiated reconnect hold time (seconds). + [[nodiscard]] uint32_t ReconnectHoldSeconds() const noexcept; + + /// Get max payload size for ORBs (bytes). + [[nodiscard]] uint16_t MaxPayloadSize() const noexcept { return maxPayloadSize_; } + + /// Set max payload size override (clipped by login response). + void SetMaxPayloadSize(uint16_t bytes) noexcept { maxPayloadSize_ = bytes; } + + // ----------------------------------------------------------------------- + // ORB submission + // ----------------------------------------------------------------------- + + /// Submit a Normal Command ORB to the device's fetch agent. + /// Requires LoggedIn state. ORB must be fully configured before calling. + [[nodiscard]] bool SubmitORB(SBP2CommandORB* orb) noexcept; + + /// Submit a management ORB (abort task, reset, etc). + /// Requires LoggedIn state. ORB must be fully configured before calling. + [[nodiscard]] bool SubmitManagementORB(SBP2ManagementORB* orb) noexcept; + + /// Reset the fetch agent. Clears ORB chain. Completion via callback. + void ResetFetchAgent(std::function callback) noexcept; + + /// Re-enable unsolicited status after device sends one. + void EnableUnsolicitedStatus() noexcept; + +private: + // ----------------------------------------------------------------------- + // Internal: resource allocation + // ----------------------------------------------------------------------- + + bool AllocateResources() noexcept; + void DeallocateResources() noexcept; + + bool AllocateLoginORBAddressSpace() noexcept; + bool AllocateLoginResponseAddressSpace() noexcept; + bool AllocateStatusBlockAddressSpace() noexcept; + bool AllocateReconnectORBAddressSpace() noexcept; + bool AllocateLogoutORBAddressSpace() noexcept; + + // ----------------------------------------------------------------------- + // Internal: ORB construction and submission + // ----------------------------------------------------------------------- + + void BuildLoginORB() noexcept; + void BuildReconnectORB() noexcept; + void BuildLogoutORB() noexcept; + + // ----------------------------------------------------------------------- + // Internal: completion handlers + // ----------------------------------------------------------------------- + + void OnLoginWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnLoginTimeout() noexcept; + void OnReconnectWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnReconnectTimeout() noexcept; + void OnLogoutWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + void OnLogoutTimeout() noexcept; + + // ----------------------------------------------------------------------- + // Internal: status block handling + // ----------------------------------------------------------------------- + + /// Called by AddressSpaceManager remote-write callback when the device + /// writes a status block. Dispatches to the appropriate state handler. + void OnStatusBlockRemoteWrite(uint32_t offset, std::span payload) noexcept; + + /// Parse and dispatch a received status block. + void ProcessStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + + // Internal: login/reconnect completion via status block + void CompleteLoginFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteReconnectFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + void CompleteLogoutFromStatusBlock(const Wire::StatusBlock& block, uint32_t length) noexcept; + + // ----------------------------------------------------------------------- + // Internal: helpers + // ----------------------------------------------------------------------- + + void SetState(LoginState newState) noexcept; + void StartLoginTimer() noexcept; + void CancelLoginTimer() noexcept; + + /// Submit a delayed callback via IOTimerDispatchSource. + void SubmitDelayedCallback(uint64_t delayMs, + std::function callback) noexcept; + + /// Cancel any pending timer callback. + void CancelPendingTimer() noexcept; + void EnsureTimeoutQueue() noexcept; + void ReleaseOwnedTimeoutQueue() noexcept; + [[nodiscard]] IODispatchQueue* EffectiveTimeoutQueue() const noexcept; + void ClearORBTracking(bool cancelTimers) noexcept; + [[nodiscard]] static uint64_t MakeORBKey(uint16_t addressHi, uint32_t addressLo) noexcept; + [[nodiscard]] static uint64_t MakeORBKey(const Async::FWAddress& address) noexcept; + + // ----------------------------------------------------------------------- + // Members + // ----------------------------------------------------------------------- + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + + // Configuration + SBP2TargetInfo targetInfo_{}; + bool configured_{false}; + uint16_t maxPayloadSize_{4096}; // default, clipped by login + + // Session state + LoginState state_{LoginState::Idle}; + uint16_t loginID_{0}; + uint16_t loginGeneration_{0}; + uint16_t loginNodeID_{0xFFFF}; + + // Login response data + Wire::LoginResponse loginResponse_{}; + Async::FWAddress commandBlockAgent_{}; + uint16_t reconnectHold_{0}; + + // Login retry state + uint32_t loginRetryCount_{0}; + static constexpr uint32_t kLoginRetryMax = 32; + static constexpr uint64_t kLoginRetryDelayMs = 1000; + + // ----------------------------------------------------------------------- + // Address space handles (from AddressSpaceManager) + // ----------------------------------------------------------------------- + + uint64_t loginORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta loginORBMeta_{}; + Wire::LoginORB loginORBBuffer_{}; + + uint64_t loginResponseHandle_{0}; + AddressSpaceManager::AddressRangeMeta loginResponseMeta_{}; + + uint64_t statusBlockHandle_{0}; + AddressSpaceManager::AddressRangeMeta statusBlockMeta_{}; + + uint64_t reconnectORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta reconnectORBMeta_{}; + Wire::ReconnectORB reconnectORBBuffer_{}; + + uint64_t logoutORBHandle_{0}; + AddressSpaceManager::AddressRangeMeta logoutORBMeta_{}; + Wire::LogoutORB logoutORBBuffer_{}; + + // ORB addresses as big-endian for management agent write (8 bytes each) + std::array loginORBAddressBE_{}; + std::array reconnectORBAddressBE_{}; + std::array logoutORBAddressBE_{}; + + // ----------------------------------------------------------------------- + // Async handles for in-flight operations + // ----------------------------------------------------------------------- + + Async::AsyncHandle loginWriteHandle_{}; + Async::AsyncHandle reconnectWriteHandle_{}; + Async::AsyncHandle logoutWriteHandle_{}; + + bool loginTimerActive_{false}; + bool reconnectTimerActive_{false}; + bool logoutTimerActive_{false}; + + // ----------------------------------------------------------------------- + // Callbacks + // ----------------------------------------------------------------------- + + LoginCallback loginCallback_; + LogoutCallback logoutCallback_; + StatusCallback statusCallback_; + + // ----------------------------------------------------------------------- + // Timer infrastructure + // ----------------------------------------------------------------------- + + IODispatchQueue* workQueue_{nullptr}; + IODispatchQueue* timeoutQueue_{nullptr}; +#ifdef ASFW_HOST_TEST + std::unique_ptr ownedTimeoutQueue_{}; +#else + IODispatchQueue* ownedTimeoutQueue_{nullptr}; +#endif + std::atomic delayedCallbackGeneration_{0}; + std::shared_ptr lifetimeToken_{std::make_shared(0)}; + + // ----------------------------------------------------------------------- + // Constants + // ----------------------------------------------------------------------- + + static constexpr uint16_t kCSRBusAddressHi = 0x0000FFFFu; + static constexpr uint32_t kBusyTimeoutAddressLo = 0xF0000210u; + static constexpr uint32_t kBusyTimeoutValue = 0x0000000Fu; + + // ----------------------------------------------------------------------- + // Fetch Agent / Doorbell internals + // ----------------------------------------------------------------------- + + /// Write ORB address to fetch agent (CBA + kORBPointer). + bool AppendORBImmediate(SBP2CommandORB* orb) noexcept; + void FailSubmittedORB(SBP2CommandORB* orb, + int transportStatus, + uint8_t sbpStatus) noexcept; + + /// Chain ORB to last ORB's next pointer. + [[nodiscard]] bool AppendORB(SBP2CommandORB* orb) noexcept; + + /// Ring doorbell (write quadlet to CBA + kDoorbell). + void RingDoorbell() noexcept; + + /// Fetch agent write completion handler. + void OnFetchAgentWriteComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Doorbell write completion handler. + void OnDoorbellComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Fetch agent reset completion handler. + void OnAgentResetComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + /// Unsolicited status enable completion handler. + void OnUnsolicitedStatusEnableComplete(uint16_t expectedGeneration, + Async::AsyncStatus status, + std::span response) noexcept; + + // Fetch agent state + Async::FWAddress fetchAgentAddress_{}; + Async::FWAddress doorbellAddress_{}; + Async::AsyncHandle fetchAgentWriteHandle_{}; + bool fetchAgentWriteInUse_{false}; + + // ORB chain state + SBP2CommandORB* chainTailORB_{nullptr}; + SBP2CommandORB* activeFetchAgentORB_{nullptr}; + std::deque pendingImmediateORBs_; + std::unordered_map outstandingORBs_; + + // Doorbell state + Async::AsyncHandle doorbellWriteHandle_{}; + bool doorbellInProgress_{false}; + bool doorbellRingAgain_{false}; + + // Fetch agent write data (8-byte BE ORB address) + std::array fetchAgentWriteData_{}; + + // Agent reset state + Async::FWAddress agentResetAddress_{}; + Async::AsyncHandle agentResetWriteHandle_{}; + bool agentResetInProgress_{false}; + std::function agentResetCallback_; + + // Unsolicited status enable state + Async::FWAddress unsolicitedStatusAddress_{}; + Async::AsyncHandle unsolicitedStatusWriteHandle_{}; + bool unsolicitedStatusRequested_{false}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp index dd75eee3..767454eb 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp +++ b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.cpp @@ -1,4 +1,5 @@ // SBP-2 Management ORB implementation. +// Ported from Apple IOFireWireSBP2ManagementORB. // Ref: SBP-2 §6 (Task Management) #include "SBP2ManagementORB.hpp" @@ -12,6 +13,155 @@ namespace ASFW::Protocols::SBP2 { using namespace ASFW::Protocols::SBP2::Wire; +namespace { + +constexpr int kManagementTransportFailure = -1; +constexpr int kManagementTimeout = -2; +constexpr int kManagementMalformedStatus = -3; +constexpr int kManagementDeviceFailure = -4; + +using ManagementAsyncState = SBP2ManagementORB::AsyncState; + +Async::AsyncHandle TakeWriteHandle(const std::shared_ptr& state) noexcept { + return Async::AsyncHandle{state->writeHandleValue.exchange(0, std::memory_order_acq_rel)}; +} + +void ClearStatusBlockCallback(const std::shared_ptr& state) noexcept { + const uint64_t handle = state->statusBlockHandle.load(std::memory_order_acquire); + if (handle != 0 && state->addrMgr != nullptr) { + state->addrMgr->SetRemoteWriteCallback(handle, {}); + } +} + +bool CompleteAsyncOperation(const std::shared_ptr& state, + int status) noexcept { + if (!state->inProgress.exchange(false, std::memory_order_acq_rel)) { + return false; + } + + state->timerActive.store(false, std::memory_order_relaxed); + state->timerGeneration.fetch_add(1, std::memory_order_acq_rel); + ClearStatusBlockCallback(state); + + const Async::AsyncHandle pendingWrite = TakeWriteHandle(state); + if (pendingWrite && state->bus != nullptr) { + (void)state->bus->Cancel(pendingWrite); + } + + if (!state->destroyed.load(std::memory_order_acquire) && + state->completionCallback) { + state->completionCallback(status); + } + return true; +} + +void HandleStatusBlockWrite(const std::shared_ptr& state, + uint32_t offset, + std::span payload) noexcept { + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + ASFW_LOG(SBP2, "SBP2ManagementORB: received status block (offset=%u len=%zu)", + offset, payload.size()); + + if (offset != 0 || payload.size() < 8 || payload.size() > Wire::StatusBlock::kMaxSize) { + (void)CompleteAsyncOperation(state, kManagementMalformedStatus); + return; + } + + Wire::StatusBlock block{}; + std::memcpy(&block, payload.data(), payload.size()); + + const uint16_t orbOffsetHi = FromBE16(block.orbOffsetHi); + const uint32_t orbOffsetLo = FromBE32(block.orbOffsetLo); + if (orbOffsetHi != state->expectedORBAddressHi.load(std::memory_order_acquire) || + orbOffsetLo != state->expectedORBAddressLo.load(std::memory_order_acquire)) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: status block ORB mismatch expected=%04x:%08x got=%04x:%08x", + state->expectedORBAddressHi.load(std::memory_order_relaxed), + state->expectedORBAddressLo.load(std::memory_order_relaxed), + orbOffsetHi, + orbOffsetLo); + (void)CompleteAsyncOperation(state, kManagementMalformedStatus); + return; + } + + if (block.Response() != 0 || block.DeadBit() != 0) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: device rejected management ORB resp=%u dead=%u status=%u", + block.Response(), + block.DeadBit(), + block.sbpStatus); + (void)CompleteAsyncOperation(state, kManagementDeviceFailure); + return; + } + + if (block.sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + ASFW_LOG(SBP2, + "SBP2ManagementORB: management ORB completed with sbpStatus=%u", + block.sbpStatus); + (void)CompleteAsyncOperation(state, kManagementDeviceFailure); + return; + } + + (void)CompleteAsyncOperation(state, 0); +} + +void HandleTimeout(const std::shared_ptr& state) noexcept { + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + ASFW_LOG(SBP2, "SBP2ManagementORB: timeout"); + (void)CompleteAsyncOperation(state, kManagementTimeout); +} + +void HandleWriteComplete(const std::shared_ptr& state, + Async::AsyncStatus status, + std::span /*response*/, + uint32_t timeoutMs, + IODispatchQueue* workQueue, + IODispatchQueue* timeoutQueue) noexcept { + state->writeHandleValue.store(0, std::memory_order_release); + + if (!state->inProgress.load(std::memory_order_relaxed)) { + return; + } + + if (status != Async::AsyncStatus::kSuccess) { + ASFW_LOG(SBP2, "SBP2ManagementORB::OnWriteComplete: status=%s", + Async::ToString(status)); + (void)CompleteAsyncOperation(state, kManagementTransportFailure); + return; + } + + state->timerActive.store(true, std::memory_order_relaxed); + ASFW_LOG(SBP2, "SBP2ManagementORB: mgmt agent ACK'd, waiting for status block (timeout=%ums)", + timeoutMs); + + if (workQueue == nullptr || timeoutQueue == nullptr || timeoutMs == 0) { + return; + } + + const uint64_t expectedGeneration = + state->timerGeneration.fetch_add(1, std::memory_order_acq_rel) + 1ULL; + const uint64_t delayNs = static_cast(timeoutMs) * 1'000'000ULL; + + DispatchAfterCompat(timeoutQueue, delayNs, [state, expectedGeneration, workQueue]() { + DispatchAsyncCompat(workQueue, [state, expectedGeneration]() { + if (state->timerGeneration.load(std::memory_order_acquire) != expectedGeneration || + !state->timerActive.load(std::memory_order_relaxed) || + !state->inProgress.load(std::memory_order_relaxed)) { + return; + } + HandleTimeout(state); + }); + }); +} + +} // namespace + // --------------------------------------------------------------------------- // Construction / Destruction // --------------------------------------------------------------------------- @@ -22,11 +172,19 @@ SBP2ManagementORB::SBP2ManagementORB(Async::IFireWireBus& bus, : bus_(bus) , busInfo_(busInfo) , addrMgr_(addrMgr) - , owner_(owner) {} + , owner_(owner) + , asyncState_(std::make_shared(&bus_, &addrMgr_)) {} SBP2ManagementORB::~SBP2ManagementORB() { - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - lifetimeToken_.reset(); + asyncState_->destroyed.store(true, std::memory_order_release); + asyncState_->inProgress.store(false, std::memory_order_relaxed); + asyncState_->timerActive.store(false, std::memory_order_relaxed); + asyncState_->timerGeneration.fetch_add(1, std::memory_order_acq_rel); + ClearStatusBlockCallback(asyncState_); + const Async::AsyncHandle pendingHandle = TakeWriteHandle(asyncState_); + if (pendingHandle) { + (void)bus_.Cancel(pendingHandle); + } DeallocateResources(); } @@ -35,8 +193,19 @@ SBP2ManagementORB::~SBP2ManagementORB() { // --------------------------------------------------------------------------- bool SBP2ManagementORB::AllocateResources() noexcept { - if (orbHandle_ != 0) { - return true; // Already allocated + const auto state = asyncState_; + const auto registerStatusWriteCallback = [this, state]() { + addrMgr_.SetRemoteWriteCallback( + statusBlockHandle_, + [state](uint64_t /*handle*/, uint32_t offset, std::span payload) { + HandleStatusBlockWrite(state, offset, payload); + }); + state->statusBlockHandle.store(statusBlockHandle_, std::memory_order_release); + }; + + if (orbHandle_ != 0 && statusBlockHandle_ != 0) { + registerStatusWriteCallback(); + return true; } // Allocate ORB address space (32 bytes) @@ -44,7 +213,7 @@ bool SBP2ManagementORB::AllocateResources() noexcept { owner_, 0xFFFF, Wire::TaskManagementORB::kSize, &orbHandle_, &orbMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB: failed to allocate ORB: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to allocate ORB: 0x%08x", kr); return false; } @@ -53,18 +222,14 @@ bool SBP2ManagementORB::AllocateResources() noexcept { owner_, 0xFFFF, Wire::StatusBlock::kMaxSize, &statusBlockHandle_, &statusBlockMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB: failed to allocate status block: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to allocate status block: 0x%08x", kr); addrMgr_.DeallocateAddressRange(owner_, orbHandle_); orbHandle_ = 0; return false; } // Register remote-write callback for the per-ORB status block - addrMgr_.SetRemoteWriteCallback( - statusBlockHandle_, - [this](uint64_t /*handle*/, uint32_t offset, std::span payload) { - OnStatusBlockWrite(offset, payload); - }); + registerStatusWriteCallback(); return true; } @@ -86,7 +251,11 @@ void SBP2ManagementORB::DeallocateResources() noexcept { // ORB construction // --------------------------------------------------------------------------- -void SBP2ManagementORB::BuildManagementORB() noexcept { +kern_return_t SBP2ManagementORB::BuildManagementORB() noexcept { + if (orbHandle_ == 0 || statusBlockHandle_ == 0) { + return kIOReturnNotReady; + } + std::memset(&orbBuffer_, 0, sizeof(orbBuffer_)); const uint16_t localNode = @@ -109,10 +278,14 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { orbBuffer_.statusFIFOAddressLo = ToBE32(statusBlockMeta_.addressLo); // Write ORB to address space - addrMgr_.WriteLocalData( + const kern_return_t writeKr = addrMgr_.WriteLocalData( owner_, orbHandle_, 0, std::span{reinterpret_cast(&orbBuffer_), sizeof(orbBuffer_)}); + if (writeKr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2ManagementORB: failed to write management ORB: 0x%08x", writeKr); + return writeKr; + } // Build 8-byte management agent write payload: ORB address in BE orbAddressBE_[0] = static_cast(localNode >> 8); @@ -122,11 +295,12 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { const uint32_t addrLoBE = ToBE32(orbMeta_.addressLo); std::memcpy(&orbAddressBE_[4], &addrLoBE, sizeof(uint32_t)); - ASFW_LOG(Async, + ASFW_LOG(SBP2, "SBP2ManagementORB: built function=%u loginID=%u ORB at %04x:%08x status at %04x:%08x", fn, loginID_, localNode, orbMeta_.addressLo, localNode, statusBlockMeta_.addressLo); + return kIOReturnSuccess; } // --------------------------------------------------------------------------- @@ -134,8 +308,8 @@ void SBP2ManagementORB::BuildManagementORB() noexcept { // --------------------------------------------------------------------------- bool SBP2ManagementORB::Execute() noexcept { - if (inProgress_.load(std::memory_order_relaxed)) { - ASFW_LOG(Async, "SBP2ManagementORB::Execute: already in progress"); + if (asyncState_->inProgress.load(std::memory_order_relaxed)) { + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: already in progress"); return false; } @@ -143,9 +317,17 @@ bool SBP2ManagementORB::Execute() noexcept { return false; } - BuildManagementORB(); + const kern_return_t buildKr = BuildManagementORB(); + if (buildKr != kIOReturnSuccess) { + return false; + } - inProgress_.store(true, std::memory_order_relaxed); + asyncState_->destroyed.store(false, std::memory_order_release); + asyncState_->completionCallback = completionCallback_; + asyncState_->expectedORBAddressHi.store(orbMeta_.addressHi, std::memory_order_release); + asyncState_->expectedORBAddressLo.store(orbMeta_.addressLo, std::memory_order_release); + asyncState_->timerActive.store(false, std::memory_order_relaxed); + asyncState_->inProgress.store(true, std::memory_order_relaxed); // Write ORB address to management agent const FW::Generation gen{generation_}; @@ -159,91 +341,28 @@ bool SBP2ManagementORB::Execute() noexcept { }; const FW::FwSpeed speed = busInfo_.GetSpeed(node); - writeHandle_ = bus_.WriteBlock( + const Async::AsyncHandle writeHandle = bus_.WriteBlock( gen, node, mgmtAddr, std::span{orbAddressBE_.data(), orbAddressBE_.size()}, speed, - [this](Async::AsyncStatus status, std::span response) { - OnWriteComplete(status, response); + [state = asyncState_, + timeoutMs = timeoutMs_, + workQueue = workQueue_, + effectiveTimeoutQueue = (timeoutQueue_ != nullptr ? timeoutQueue_ : workQueue_)]( + Async::AsyncStatus status, + std::span response) { + HandleWriteComplete(state, status, response, timeoutMs, workQueue, effectiveTimeoutQueue); }); - if (!writeHandle_) { - ASFW_LOG(Async, "SBP2ManagementORB::Execute: WriteBlock failed"); - inProgress_.store(false, std::memory_order_relaxed); + if (!writeHandle) { + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: WriteBlock failed"); + asyncState_->inProgress.store(false, std::memory_order_relaxed); return false; } + asyncState_->writeHandleValue.store(writeHandle.value, std::memory_order_release); - ASFW_LOG(Async, "SBP2ManagementORB::Execute: wrote management ORB to agent"); + ASFW_LOG(SBP2, "SBP2ManagementORB::Execute: wrote management ORB to agent"); return true; } -// --------------------------------------------------------------------------- -// Completion handlers -// --------------------------------------------------------------------------- - -void SBP2ManagementORB::OnWriteComplete(Async::AsyncStatus status, - std::span response) noexcept { - if (status != Async::AsyncStatus::kSuccess) { - ASFW_LOG(Async, "SBP2ManagementORB::OnWriteComplete: status=%s", - Async::ToString(status)); - Complete(-1); - return; - } - - // Management agent write ACK'd. Start timeout, wait for status block. - timerActive_.store(true, std::memory_order_relaxed); - ASFW_LOG(Async, "SBP2ManagementORB: mgmt agent ACK'd, waiting for status block (timeout=%ums)", - timeoutMs_); - - if (workQueue_ && timeoutMs_ > 0) { - const uint32_t timeout = timeoutMs_; - const uint64_t expectedGeneration = - timerGeneration_.fetch_add(1, std::memory_order_acq_rel) + 1ULL; - const std::weak_ptr weakLifetime = lifetimeToken_; - const uint64_t delayNs = static_cast(timeout) * 1'000'000ULL; - - DispatchAfterCompat(workQueue_, delayNs, [this, weakLifetime, expectedGeneration]() { - if (weakLifetime.expired()) { - return; - } - if (timerGeneration_.load(std::memory_order_acquire) != expectedGeneration || - !timerActive_.load(std::memory_order_relaxed) || - !inProgress_.load(std::memory_order_relaxed)) { - return; - } - OnTimeout(); - }); - } -} - -void SBP2ManagementORB::OnStatusBlockWrite(uint32_t offset, - std::span payload) noexcept { - if (!inProgress_.load(std::memory_order_relaxed)) { - return; - } - - ASFW_LOG(Async, "SBP2ManagementORB: received status block (offset=%u len=%zu)", - offset, payload.size()); - - Complete(0); -} - -void SBP2ManagementORB::OnTimeout() noexcept { - if (!inProgress_.load(std::memory_order_relaxed)) { - return; - } - ASFW_LOG(Async, "SBP2ManagementORB: timeout"); - Complete(-2); -} - -void SBP2ManagementORB::Complete(int status) noexcept { - inProgress_.store(false, std::memory_order_relaxed); - timerActive_.store(false, std::memory_order_relaxed); - timerGeneration_.fetch_add(1, std::memory_order_acq_rel); - - if (completionCallback_) { - completionCallback_(status); - } -} - } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp index b7c01ee0..2d0dabe1 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp @@ -4,6 +4,7 @@ // Written to the management agent address (same as login/reconnect/logout). // Has its own per-ORB status FIFO address space. // +// Ported from Apple IOFireWireSBP2ManagementORB. // Ref: SBP-2 §6 (Task Management) #include "AddressSpaceManager.hpp" @@ -18,7 +19,6 @@ #include #endif -#include #include #include #include @@ -60,31 +60,51 @@ class SBP2ManagementORB { } void SetManagementAgentOffset(uint32_t offset) noexcept { managementAgentOffset_ = offset; } void SetTimeout(uint32_t ms) noexcept { timeoutMs_ = ms; } - void SetCompletionCallback(CompletionCallback cb) noexcept { completionCallback_ = std::move(cb); } + void SetCompletionCallback(CompletionCallback cb) noexcept { + completionCallback_ = std::move(cb); + asyncState_->completionCallback = completionCallback_; + } - // Set node targeting before Execute. + // Set node targeting (called by SBP2LoginSession before Execute) void SetTargetNode(uint16_t generation, uint16_t nodeID) noexcept { generation_ = generation; nodeID_ = nodeID; } void SetWorkQueue(IODispatchQueue* queue) noexcept { workQueue_ = queue; } + void SetTimeoutQueue(IODispatchQueue* queue) noexcept { timeoutQueue_ = queue; } // Lifecycle [[nodiscard]] bool Execute() noexcept; [[nodiscard]] Function GetFunction() const noexcept { return function_; } - [[nodiscard]] bool InProgress() const noexcept { return inProgress_.load(std::memory_order_relaxed); } + [[nodiscard]] bool InProgress() const noexcept { + return asyncState_->inProgress.load(std::memory_order_relaxed); + } + + // Shared by delayed callbacks after the ORB object itself may have been destroyed. + struct AsyncState { + AsyncState(Async::IFireWireBus* busIn, AddressSpaceManager* addrMgrIn) + : bus(busIn) + , addrMgr(addrMgrIn) {} + + Async::IFireWireBus* bus{nullptr}; + AddressSpaceManager* addrMgr{nullptr}; + std::atomic destroyed{false}; + std::atomic inProgress{false}; + std::atomic timerActive{false}; + std::atomic timerGeneration{0}; + std::atomic statusBlockHandle{0}; + std::atomic writeHandleValue{0}; + std::atomic expectedORBAddressHi{0}; + std::atomic expectedORBAddressLo{0}; + CompletionCallback completionCallback{}; + }; private: bool AllocateResources() noexcept; void DeallocateResources() noexcept; - void BuildManagementORB() noexcept; - - void OnWriteComplete(Async::AsyncStatus status, std::span response) noexcept; - void OnStatusBlockWrite(uint32_t offset, std::span payload) noexcept; - void OnTimeout() noexcept; - void Complete(int status) noexcept; + [[nodiscard]] kern_return_t BuildManagementORB() noexcept; // Dependencies Async::IFireWireBus& bus_; @@ -114,11 +134,8 @@ class SBP2ManagementORB { // Management agent write payload (8-byte BE ORB address) std::array orbAddressBE_{}; - Async::AsyncHandle writeHandle_{}; - // State - std::atomic inProgress_{false}; - std::atomic timerActive_{false}; + std::shared_ptr asyncState_; // Node targeting uint16_t generation_{0}; @@ -126,8 +143,7 @@ class SBP2ManagementORB { // Timer infrastructure IODispatchQueue* workQueue_{nullptr}; - std::atomic timerGeneration_{0}; - std::shared_ptr lifetimeToken_{std::make_shared(0)}; + IODispatchQueue* timeoutQueue_{nullptr}; }; } // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp index 9f16f490..4967a3e2 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp @@ -4,6 +4,7 @@ // Converts scatter-gather DMA segments into SBP-2 Page Table Entries (PTEs) // or a single direct-address descriptor when possible. // +// Ported from Apple IOFireWireSBP2ORB::setCommandBuffers. // Ref: SBP-2 §5.1.2 (Page Table Entry format) #include "AddressSpaceManager.hpp" @@ -111,7 +112,7 @@ class SBP2PageTable { owner_, 0xFFFF, ptSize, &pageTableHandle_, &pageTableMeta_); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2PageTable: failed to allocate page table: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2PageTable: failed to allocate page table: 0x%08x", kr); return false; } @@ -120,7 +121,7 @@ class SBP2PageTable { reinterpret_cast(ptes.data()), ptSize); kr = addrMgr_.WriteLocalData(owner_, pageTableHandle_, 0, pteSpan); if (kr != kIOReturnSuccess) { - ASFW_LOG(Async, "SBP2PageTable: failed to write page table: 0x%08x", kr); + ASFW_LOG(SBP2, "SBP2PageTable: failed to write page table: 0x%08x", kr); Clear(); return false; } @@ -132,7 +133,7 @@ class SBP2PageTable { result_.options = Wire::Options::kPageTableUnrestricted; result_.isDirect = false; - ASFW_LOG(Async, "SBP2PageTable: built %u PTEs (%u bytes) at %04x:%08x", + ASFW_LOG(SBP2, "SBP2PageTable: built %u PTEs (%u bytes) at %04x:%08x", pteCount_, ptSize, pageTableMeta_.addressHi, pageTableMeta_.addressLo); return true; } diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp new file mode 100644 index 00000000..c2b32caf --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.cpp @@ -0,0 +1,506 @@ +#include "SBP2SessionRegistry.hpp" + +#include "../../Async/Interfaces/IFireWireBusInfo.hpp" +#include "../../Discovery/FWDevice.hpp" +#include "../../Discovery/FWUnit.hpp" + +#include + +namespace ASFW::Protocols::SBP2 { + +namespace { + +class IOLockGuard { +public: + explicit IOLockGuard(IOLock* lock) : lock_(lock) { + if (lock_ != nullptr) { + IOLockLock(lock_); + } + } + + ~IOLockGuard() { + if (lock_ != nullptr) { + IOLockUnlock(lock_); + } + } + + IOLockGuard(const IOLockGuard&) = delete; + IOLockGuard& operator=(const IOLockGuard&) = delete; + +private: + IOLock* lock_{nullptr}; +}; + +constexpr uint8_t kInquiryOpcode = 0x12; + +uint32_t BuildCommandFlags(SCSI::DataDirection direction) { + uint32_t flags = SBP2CommandORB::kNotify | + SBP2CommandORB::kImmediate | + SBP2CommandORB::kNormalORB; + if (direction == SCSI::DataDirection::FromTarget) { + flags |= SBP2CommandORB::kDataFromTarget; + } + return flags; +} + +SBP2TargetInfo BuildTargetInfoFromUnit(const Discovery::FWUnit& unit) { + SBP2TargetInfo info{}; + + info.managementAgentOffset = unit.GetManagementAgentOffset().value_or(0); + info.lun = static_cast(unit.GetLUN().value_or(0) & 0xFFFF); + + if (auto uc = unit.GetUnitCharacteristics(); uc.has_value()) { + const uint32_t value = *uc; + const uint8_t orbSizeUnits = static_cast((value >> 24) & 0xFF); + const uint8_t timeoutUnits = static_cast((value >> 16) & 0xFF); + info.managementTimeoutMs = static_cast(timeoutUnits) * 500; + info.maxORBSize = std::max(static_cast(orbSizeUnits) * 4, 32); + } + info.maxCommandBlockSize = info.maxORBSize > Wire::NormalORB::kHeaderSize + ? static_cast(info.maxORBSize - Wire::NormalORB::kHeaderSize) + : 0; + + if (auto fastStart = unit.GetFastStart(); fastStart.has_value()) { + const uint32_t value = *fastStart; + info.fastStartSupported = true; + info.fastStartOffset = static_cast((value >> 8) & 0xFF); + info.fastStartMaxPayload = static_cast(value & 0xFF); + } + + if (auto device = unit.GetDevice(); device) { + info.targetNodeId = device->GetNodeID(); + } + + return info; +} + +} // namespace + +SBP2SessionRegistry::SBP2SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + IODispatchQueue* workQueue) + : bus_(bus) + , busInfo_(busInfo) + , addrSpaceMgr_(addrSpaceMgr) + , deviceManager_(deviceManager) + , workQueue_(workQueue) { + lock_ = IOLockAlloc(); +} + +SBP2SessionRegistry::~SBP2SessionRegistry() { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + CleanupCommandResources(record); + } + sessions_.clear(); + + if (lock_ != nullptr) { + IOLockFree(lock_); + lock_ = nullptr; + } +} + +std::expected SBP2SessionRegistry::CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset) { + auto unit = ResolveUnit(guid, romOffset); + if (!unit) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: no unit found for guid=0x%016llx romOffset=%u", + guid, romOffset); + return std::unexpected(kIOReturnNotFound); + } + + if (!unit->Matches(kSBP2UnitSpecId, kSBP2UnitSwVersion)) { + ASFW_LOG(SBP2, + "SBP2SessionRegistry: unit identity spec=0x%06x sw=0x%06x is not SBP-2", + unit->GetUnitSpecID(), unit->GetUnitSwVersion()); + return std::unexpected(kIOReturnUnsupported); + } + + const auto mgmtOffset = unit->GetManagementAgentOffset(); + if (!mgmtOffset.has_value() || *mgmtOffset == 0) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: unit has no Management_Agent_Offset"); + return std::unexpected(kIOReturnUnsupported); + } + + auto targetInfo = BuildTargetInfoFromUnit(*unit); + if (targetInfo.managementAgentOffset == 0) { + return std::unexpected(kIOReturnUnsupported); + } + + auto session = std::make_unique(bus_, busInfo_, addrSpaceMgr_); + session->Configure(targetInfo); + session->SetWorkQueue(workQueue_); + + IOLockGuard lock(lock_); + const uint64_t handle = nextHandle_++; + + SBP2SessionRecord record{}; + record.handle = handle; + record.owner = owner; + record.guid = guid; + record.romOffset = romOffset; + record.session = std::move(session); + + auto [it, inserted] = sessions_.emplace(handle, std::move(record)); + if (!inserted) { + return std::unexpected(kIOReturnNoMemory); + } + + ASFW_LOG(SBP2, "SBP2SessionRegistry: created session handle=%llu guid=0x%016llx romOffset=%u", + handle, guid, romOffset); + return handle; +} + +bool SBP2SessionRegistry::StartLogin(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->session) { + return false; + } + + if (record->session->State() != LoginState::Idle) { + return false; + } + + record->session->SetLoginCallback([this, handle](const LoginCompleteParams& params) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(handle); + if (rec == nullptr) { + return; + } + + rec->state.lastError = params.status; + if (params.status == 0) { + rec->state.loginID = params.loginResponse.loginID; + rec->state.loginState = LoginState::LoggedIn; + rec->state.generation = params.generation; + } else { + rec->state.loginState = LoginState::Failed; + } + }); + + return record->session->Login(); +} + +std::optional SBP2SessionRegistry::GetSessionState(uint64_t handle) const { + IOLockGuard lock(lock_); + const auto* record = FindByHandle(handle); + if (!record || !record->session) { + return std::nullopt; + } + + SBP2SessionState state{}; + state.loginState = record->session->State(); + state.loginID = record->session->LoginID(); + state.generation = record->session->Generation(); + state.lastError = record->state.lastError; + state.reconnectPending = (state.loginState == LoginState::Suspended); + return state; +} + +bool SBP2SessionRegistry::SubmitInquiry(uint64_t handle, uint8_t allocationLength) { + return SubmitCommand(handle, SCSI::BuildInquiryRequest(allocationLength)); +} + +std::optional> SBP2SessionRegistry::GetInquiryResult(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->commandReady || !record->pendingCommandResult.has_value() || + record->lastCompletedCommandOpcode != kInquiryOpcode) { + return std::nullopt; + } + + if (record->pendingCommandResult->transportStatus != 0 || + record->pendingCommandResult->sbpStatus != Wire::SBPStatus::kNoAdditionalInfo) { + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return std::nullopt; + } + + auto payload = std::move(record->pendingCommandResult->payload); + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return payload; +} + +bool SBP2SessionRegistry::SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->session || request.cdb.empty()) { + return false; + } + + if (record->session->State() != LoginState::LoggedIn || record->commandInFlight) { + return false; + } + + if (request.transferLength > 0 && request.direction == SCSI::DataDirection::None) { + return false; + } + if (request.direction == SCSI::DataDirection::FromTarget && !request.outgoingPayload.empty()) { + return false; + } + if (request.direction == SCSI::DataDirection::ToTarget && + request.outgoingPayload.size() != request.transferLength) { + return false; + } + + const uint16_t maxCDB = record->session->TargetInfo().maxCommandBlockSize; + if (maxCDB < request.cdb.size()) { + return false; + } + + uint64_t bufferHandle = 0; + AddressSpaceManager::AddressRangeMeta bufferMeta{}; + if (request.transferLength > 0) { + const kern_return_t kr = addrSpaceMgr_.AllocateAddressRangeAuto( + record->owner, 0xFFFF, request.transferLength, &bufferHandle, &bufferMeta); + if (kr != kIOReturnSuccess) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: failed to allocate command buffer: 0x%08x", kr); + return false; + } + } + + std::unique_ptr pageTable; + if (request.transferLength > 0) { + if (request.direction == SCSI::DataDirection::ToTarget) { + const kern_return_t writeKr = addrSpaceMgr_.WriteLocalData( + record->owner, + bufferHandle, + 0, + std::span{request.outgoingPayload.data(), request.outgoingPayload.size()}); + if (writeKr != kIOReturnSuccess) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + return false; + } + } + + pageTable = std::make_unique(addrSpaceMgr_, record->owner); + SBP2PageTable::Segment segment{bufferMeta.address, request.transferLength}; + if (!pageTable->Build(std::span(&segment, 1), + busInfo_.GetLocalNodeID().value)) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + return false; + } + } + + auto orb = std::make_unique(addrSpaceMgr_, record->owner, maxCDB); + orb->SetCommandBlock(std::span{request.cdb.data(), request.cdb.size()}); + orb->SetFlags(BuildCommandFlags(request.direction)); + orb->SetMaxPayloadSize(record->session->MaxPayloadSize()); + orb->SetTimeout(request.timeoutMs > 0 + ? request.timeoutMs + : record->session->TargetInfo().managementTimeoutMs); + if (pageTable) { + orb->SetDataDescriptor(pageTable->GetResult()); + } + + const uint64_t captureHandle = handle; + orb->SetCompletionCallback([this, captureHandle](int transportStatus, uint8_t sbpStatus) { + IOLockGuard cbLock(lock_); + auto* rec = FindByHandle(captureHandle); + if (rec == nullptr) { + return; + } + + rec->commandInFlight = false; + rec->commandReady = true; + rec->lastCompletedCommandOpcode = rec->activeCommandOpcode; + rec->activeCommandOpcode.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = transportStatus; + result.sbpStatus = sbpStatus; + + if (transportStatus == 0 && + sbpStatus == Wire::SBPStatus::kNoAdditionalInfo && + rec->activeCommandRequest.has_value() && + rec->activeCommandRequest->direction == SCSI::DataDirection::FromTarget && + rec->activeCommandRequest->transferLength > 0 && + rec->commandBufferHandle != 0) { + std::vector payload; + const kern_return_t readKr = addrSpaceMgr_.ReadIncomingData( + rec->owner, + rec->commandBufferHandle, + 0, + rec->activeCommandRequest->transferLength, + &payload); + if (readKr == kIOReturnSuccess) { + result.payload = std::move(payload); + } else { + result.transportStatus = static_cast(readKr); + } + } + + if (rec->activeCommandRequest.has_value() && rec->activeCommandRequest->captureSenseData) { + result.senseData = result.payload; + } + + rec->state.lastError = static_cast(result.transportStatus); + if (result.transportStatus == 0 && result.sbpStatus == Wire::SBPStatus::kNoAdditionalInfo) { + rec->state.lastError = 0; + } + rec->pendingCommandResult = std::move(result); + rec->activeCommandRequest.reset(); + CleanupCommandResources(*rec); + }); + + if (!record->session->SubmitORB(orb.get())) { + if (bufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record->owner, bufferHandle); + } + return false; + } + + record->commandInFlight = true; + record->commandReady = false; + record->pendingCommandResult.reset(); + record->activeCommandRequest = request; + record->activeCommandOpcode = request.cdb.front(); + record->commandBufferHandle = bufferHandle; + record->commandORB = std::move(orb); + record->commandPageTable = std::move(pageTable); + return true; +} + +std::optional SBP2SessionRegistry::GetCommandResult(uint64_t handle) { + IOLockGuard lock(lock_); + auto* record = FindByHandle(handle); + if (!record || !record->commandReady || !record->pendingCommandResult.has_value()) { + return std::nullopt; + } + + SCSI::CommandResult result = std::move(*record->pendingCommandResult); + record->pendingCommandResult.reset(); + record->lastCompletedCommandOpcode.reset(); + record->commandReady = false; + return result; +} + +bool SBP2SessionRegistry::ReleaseSession(uint64_t handle) { + IOLockGuard lock(lock_); + auto it = sessions_.find(handle); + if (it == sessions_.end()) { + return false; + } + + auto& record = it->second; + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + + CleanupCommandResources(record); + sessions_.erase(it); + return true; +} + +void SBP2SessionRegistry::ReleaseOwner(void* owner) { + IOLockGuard lock(lock_); + for (auto it = sessions_.begin(); it != sessions_.end();) { + if (it->second.owner == owner) { + auto& record = it->second; + if (record.session && record.session->State() == LoginState::LoggedIn) { + (void)record.session->Logout(); + } + CleanupCommandResources(record); + it = sessions_.erase(it); + } else { + ++it; + } + } +} + +void SBP2SessionRegistry::OnBusReset(uint16_t newGeneration) { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (record.session) { + record.session->HandleBusReset(newGeneration); + } + if (record.commandInFlight || record.commandORB) { + record.commandInFlight = false; + record.commandReady = true; + record.lastCompletedCommandOpcode = record.activeCommandOpcode; + record.activeCommandOpcode.reset(); + + SCSI::CommandResult result{}; + result.transportStatus = static_cast(kIOReturnAborted); + result.sbpStatus = Wire::SBPStatus::kRequestAborted; + record.pendingCommandResult = std::move(result); + record.state.lastError = static_cast(kIOReturnAborted); + record.activeCommandRequest.reset(); + CleanupCommandResources(record); + } + } +} + +void SBP2SessionRegistry::RefreshTargets(Discovery::Generation gen) { + IOLockGuard lock(lock_); + for (auto& [handle, record] : sessions_) { + if (!record.session || record.session->State() != LoginState::Suspended) { + continue; + } + + auto unit = ResolveUnit(record.guid, record.romOffset); + if (!unit) { + ASFW_LOG(SBP2, "SBP2SessionRegistry: RefreshTargets: unit not found for handle=%llu", + handle); + continue; + } + + auto targetInfo = BuildTargetInfoFromUnit(*unit); + record.session->Configure(targetInfo); + + ASFW_LOG(SBP2, "SBP2SessionRegistry: reconnecting session handle=%llu gen=%u", + handle, gen.value); + (void)record.session->Reconnect(); + } +} + +SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +const SBP2SessionRecord* SBP2SessionRegistry::FindByHandle(uint64_t handle) const { + auto it = sessions_.find(handle); + return it != sessions_.end() ? &it->second : nullptr; +} + +std::shared_ptr SBP2SessionRegistry::ResolveUnit(uint64_t guid, + uint32_t romOffset) const { + const auto devices = deviceManager_.GetAllDevices(); + for (const auto& device : devices) { + if (!device || device->GetGUID() != guid) { + continue; + } + for (const auto& unit : device->GetUnits()) { + if (unit && unit->GetDirectoryOffset() == romOffset) { + return unit; + } + } + } + return nullptr; +} + +void SBP2SessionRegistry::CleanupCommandResources(SBP2SessionRecord& record) { + if (record.session) { + record.session->ClearORBTracking(true); + } + if (record.commandBufferHandle != 0) { + addrSpaceMgr_.DeallocateAddressRange(record.owner, record.commandBufferHandle); + record.commandBufferHandle = 0; + } + record.commandORB.reset(); + record.commandPageTable.reset(); + record.commandInFlight = false; +} + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp new file mode 100644 index 00000000..edaa912a --- /dev/null +++ b/ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp @@ -0,0 +1,125 @@ +#pragma once + +// SBP-2 Session Registry — bridges discovery metadata to SBP2LoginSession instances. +// Owns sessions keyed by (guid, romOffset), handles bus-reset suspend/reconnect, +// and provides the INQUIRY command job for the v1 vertical slice. + +#include "SBP2LoginSession.hpp" +#include "SBP2CommandORB.hpp" +#include "SBP2PageTable.hpp" +#include "SCSICommandSet.hpp" +#include "AddressSpaceManager.hpp" +#include "../../Discovery/IDeviceManager.hpp" +#include "../../Discovery/DiscoveryTypes.hpp" +#include "../../Logging/Logging.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ASFW::Protocols::SBP2 { + +// SBP-2 Unit_Directory identity used by Nikon/modernscan and visible in raw ROM. +inline constexpr uint32_t kSBP2UnitSpecId = 0x00609E; +inline constexpr uint32_t kSBP2UnitSwVersion = 0x010483; + +// Per-session state exposed to UserClient +struct SBP2SessionState { + LoginState loginState{LoginState::Idle}; + uint16_t loginID{0}; + uint16_t generation{0}; + int32_t lastError{0}; + bool reconnectPending{false}; +}; + +struct SBP2SessionRecord { + uint64_t handle{0}; + void* owner{nullptr}; + uint64_t guid{0}; + uint32_t romOffset{0}; + std::unique_ptr session; + SBP2SessionState state{}; + + std::optional activeCommandRequest; + std::optional pendingCommandResult; + std::optional activeCommandOpcode; + std::optional lastCompletedCommandOpcode; + bool commandReady{false}; + bool commandInFlight{false}; + std::unique_ptr commandORB; + std::unique_ptr commandPageTable; + uint64_t commandBufferHandle{0}; +}; + +class SBP2SessionRegistry { +public: + SBP2SessionRegistry(Async::IFireWireBus& bus, + Async::IFireWireBusInfo& busInfo, + AddressSpaceManager& addrSpaceMgr, + Discovery::IDeviceManager& deviceManager, + IODispatchQueue* workQueue = nullptr); + + ~SBP2SessionRegistry(); + + SBP2SessionRegistry(const SBP2SessionRegistry&) = delete; + SBP2SessionRegistry& operator=(const SBP2SessionRegistry&) = delete; + + // Create a session for (owner, guid, romOffset). + // Validates the unit is SBP-2 and has Management_Agent_Offset. + [[nodiscard]] std::expected CreateSession(void* owner, + uint64_t guid, + uint32_t romOffset); + + // Start login for a session. Returns false if not in Idle state. + [[nodiscard]] bool StartLogin(uint64_t handle); + + // Get session state. Returns nullopt if handle not found. + [[nodiscard]] std::optional GetSessionState(uint64_t handle) const; + + // Submit SCSI INQUIRY. Returns false if not logged in or inquiry already in-flight. + [[nodiscard]] bool SubmitInquiry(uint64_t handle, uint8_t allocationLength = 96); + + // Get inquiry result (destructive read). Returns nullopt if not ready. + [[nodiscard]] std::optional> GetInquiryResult(uint64_t handle); + + // Submit a generic SCSI command. Returns false if not logged in or another command is active. + [[nodiscard]] bool SubmitCommand(uint64_t handle, const SCSI::CommandRequest& request); + + // Get generic command result (destructive read). Returns nullopt if not ready. + [[nodiscard]] std::optional GetCommandResult(uint64_t handle); + + // Release a specific session. + [[nodiscard]] bool ReleaseSession(uint64_t handle); + + // Release all sessions for an owner (best-effort logout + cleanup). + void ReleaseOwner(void* owner); + + // Bus reset: suspend all active sessions. + void OnBusReset(uint16_t newGeneration); + + // After discovery completes: refresh target info and reconnect suspended sessions. + void RefreshTargets(Discovery::Generation gen); + +private: + SBP2SessionRecord* FindByHandle(uint64_t handle); + const SBP2SessionRecord* FindByHandle(uint64_t handle) const; + + std::shared_ptr ResolveUnit(uint64_t guid, uint32_t romOffset) const; + + void CleanupCommandResources(SBP2SessionRecord& record); + + Async::IFireWireBus& bus_; + Async::IFireWireBusInfo& busInfo_; + AddressSpaceManager& addrSpaceMgr_; + Discovery::IDeviceManager& deviceManager_; + IODispatchQueue* workQueue_{nullptr}; + + IOLock* lock_{nullptr}; + std::map sessions_; + uint64_t nextHandle_{1}; +}; + +} // namespace ASFW::Protocols::SBP2 diff --git a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp index 12fa5f16..806df769 100644 --- a/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp +++ b/ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp @@ -1,13 +1,15 @@ #pragma once // SBP-2 (Serial Bus Protocol 2) wire-format definitions. -// Based on ANSI INCITS 335-1999 (SBP-2). +// Based on ANSI INCITS 335-1999 (SBP-2) and Apple IOFireWireSBP2 structures. // All multi-byte fields are stored in **big-endian** (bus/wire) order. // Use ToBusOrder / FromBusOrder from Core/PhyPackets.hpp or std::byteswap for conversion. #include +#include #include #include +#include namespace ASFW::Protocols::SBP2::Wire { @@ -154,18 +156,25 @@ struct NormalORB { // Access via CommandBlock() helper. [[nodiscard]] uint32_t* CommandBlock() noexcept { - return reinterpret_cast(reinterpret_cast(this) + 16); + return reinterpret_cast(reinterpret_cast(this) + kHeaderSize); } [[nodiscard]] const uint32_t* CommandBlock() const noexcept { - return reinterpret_cast(reinterpret_cast(this) + 16); + return reinterpret_cast(reinterpret_cast(this) + kHeaderSize); } // Minimum ORB size (no command block) - static constexpr uint32_t kHeaderSize = 16; + static constexpr uint32_t kHeaderSize = 20; // Null next-ORB indicator (bit 31 set in hi address) static constexpr uint32_t kNextORBNull = 0x80000000u; }; +static_assert(sizeof(NormalORB) == NormalORB::kHeaderSize, "NormalORB header must be 20 bytes"); +static_assert(std::is_trivially_copyable_v, "NormalORB must stay trivially copyable"); +static_assert(offsetof(NormalORB, nextORBAddressHi) == 0, "NormalORB next hi offset changed"); +static_assert(offsetof(NormalORB, dataDescriptorHi) == 8, "NormalORB data descriptor hi offset changed"); +static_assert(offsetof(NormalORB, options) == 16, "NormalORB options offset changed"); +static_assert(offsetof(NormalORB, dataSize) == 18, "NormalORB data size offset changed"); + // Page Table Entry — maps data buffer for DMA. // Ref: SBP-2 §5.1.2 struct PageTableEntry { @@ -177,6 +186,10 @@ struct PageTableEntry { }; static_assert(sizeof(PageTableEntry) == PageTableEntry::kSize, "PTE must be 8 bytes"); +static_assert(std::is_trivially_copyable_v, + "PageTableEntry must stay trivially copyable"); +static_assert(offsetof(PageTableEntry, segmentLength) == 0, "PTE length offset changed"); +static_assert(offsetof(PageTableEntry, segmentBaseAddressLo) == 4, "PTE addressLo offset changed"); // --------------------------------------------------------------------------- // SBP-2 Status Block @@ -199,6 +212,11 @@ struct StatusBlock { }; static_assert(sizeof(StatusBlock) == 32, "StatusBlock must be 32 bytes"); +static_assert(std::is_trivially_copyable_v, + "StatusBlock must stay trivially copyable"); +static_assert(offsetof(StatusBlock, details) == 0, "StatusBlock details offset changed"); +static_assert(offsetof(StatusBlock, orbOffsetHi) == 2, "StatusBlock orbOffsetHi offset changed"); +static_assert(offsetof(StatusBlock, orbOffsetLo) == 4, "StatusBlock orbOffsetLo offset changed"); // --------------------------------------------------------------------------- // SBP-2 Management ORB (Task Management) @@ -219,6 +237,14 @@ struct TaskManagementORB { }; static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); +static_assert(std::is_trivially_copyable_v, + "TaskManagementORB must stay trivially copyable"); +static_assert(offsetof(TaskManagementORB, orbOffsetHi) == 0, + "TaskManagementORB orbOffsetHi offset changed"); +static_assert(offsetof(TaskManagementORB, options) == 16, + "TaskManagementORB options offset changed"); +static_assert(offsetof(TaskManagementORB, statusFIFOAddressHi) == 24, + "TaskManagementORB status FIFO hi offset changed"); // --------------------------------------------------------------------------- // Management Agent address calculation @@ -231,8 +257,8 @@ static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); } // SBP-2 ORBs embed a full 16-bit node ID in bus addresses. ASFW's generic -// bus-info path may expose only the local 6-bit physical node number, so expand -// it to the local-bus form (0xffc0 | phyId) before writing ORBs. +// bus-info path exposes only the local 6-bit physical node number, so expand it +// to the local-bus form Apple uses (0xffc0 | phyId) before writing ORBs. [[nodiscard]] inline constexpr uint16_t NormalizeBusNodeID(uint16_t nodeID) noexcept { if ((nodeID & 0xFFC0u) == 0xFFC0u) { return nodeID; @@ -247,6 +273,7 @@ static_assert(sizeof(TaskManagementORB) == TaskManagementORB::kSize); } // Command Block Agent register offsets (relative to agent base from login response). +// Verified against Apple IOFireWireSBP2Login::clearAllTasksInSet / login response processing. struct CommandBlockAgentOffsets { static constexpr uint32_t kAgentReset = 0x04; // Fetch agent reset (quadlet write) static constexpr uint32_t kFetchAgent = 0x08; // ORB pointer write (fetch agent, non-fast-start) diff --git a/ASFWDriver/Service/DriverContext.cpp b/ASFWDriver/Service/DriverContext.cpp index cef59fa5..5532f0f8 100644 --- a/ASFWDriver/Service/DriverContext.cpp +++ b/ASFWDriver/Service/DriverContext.cpp @@ -25,8 +25,10 @@ #include "../Hardware/HardwareInterface.hpp" #include "../Hardware/InterruptManager.hpp" #include "../Logging/Logging.hpp" +#include "../Protocols/AVC/AVCDiscovery.hpp" #include "../Protocols/AVC/FCPResponseRouter.hpp" #include "../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../Protocols/SBP2/SBP2SessionRegistry.hpp" #include "../Scheduling/Scheduler.hpp" void ServiceContext::Reset() { @@ -44,12 +46,13 @@ void ServiceContext::Reset() { deps.configRomStager.reset(); deps.interrupts.reset(); deps.topology.reset(); - deps.fcpResponseRouter.reset(); // Clean up FCP router + deps.fcpResponseRouter.reset(); // Clean up FCP router deps.sbp2AddressSpaceManager.reset(); - deps.avcDiscovery.reset(); // Clean up AV/C discovery - deps.irmClient.reset(); // Clean up IRM client + deps.sbp2SessionRegistry.reset(); + deps.avcDiscovery.reset(); // Clean up AV/C discovery + deps.irmClient.reset(); // Clean up IRM client deps.asyncController.reset(); - deps.asyncSubsystem.reset(); // Stop and cleanup asyncSubsystem + deps.asyncSubsystem.reset(); // Stop and cleanup asyncSubsystem statusPublisher.Reset(); watchdog.Reset(); #ifndef ASFW_HOST_TEST @@ -137,116 +140,185 @@ void DriverWiring::EnsureDeps(ASFWDriver* driver, ::ServiceContext& ctx) { void DriverWiring::EnsureSbp2Deps(::ServiceContext& ctx) { auto& d = ctx.deps; + if (!d.fcpResponseRouter && d.avcDiscovery && ctx.controller) { + auto& bus = ctx.controller->Bus(); + d.fcpResponseRouter = std::make_shared( + *d.avcDiscovery, + bus + ); + ASFW_LOG(Controller, "[Controller] FCPResponseRouter initialized"); + } + if (!d.sbp2AddressSpaceManager && d.hardware) { - d.sbp2AddressSpaceManager = - std::make_shared(d.hardware.get()); + d.sbp2AddressSpaceManager = std::make_shared( + d.hardware.get()); ASFW_LOG(Controller, "[Controller] SBP2 AddressSpaceManager initialized"); } + if (!d.sbp2SessionRegistry && ctx.controller && d.deviceManager && d.sbp2AddressSpaceManager) { + auto& bus = ctx.controller->Bus(); + d.sbp2SessionRegistry = + std::make_shared( + bus, + bus, + *d.sbp2AddressSpaceManager, + *d.deviceManager, + ctx.workQueue.get()); + ASFW_LOG(Controller, "[Controller] SBP2 SessionRegistry initialized"); + } + if (ctx.controller) { ctx.controller->SetSbp2AddressSpaceManager(d.sbp2AddressSpaceManager); - } + ctx.controller->SetSBP2SessionRegistry(d.sbp2SessionRegistry); + } + + if (d.asyncSubsystem) { + if (auto* router = d.asyncSubsystem->GetPacketRouter()) { + auto* sbp2Manager = d.sbp2AddressSpaceManager.get(); + auto* fcpRouter = d.fcpResponseRouter.get(); + auto* responder = router->GetResponseSender(); + + // Quadlet write requests (tCode 0x0): direct address-space writes. + router->RegisterRequestHandler( + 0x0, + [sbp2Manager](const ASFW::Async::ARPacketView& packet) { + uint64_t destOffset = 0; + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + if (!sbp2Manager || packet.header.size() < 16) { + return ASFW::Async::ResponseCode::AddressError; + } + + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + const auto quadletData = + std::span(packet.header.data() + 12, 4); + result = sbp2Manager->ApplyRemoteWrite(destOffset, quadletData); + ASFW_LOG_V2( + Async, + "SBP2 AR write-quadlet req: src=0x%04x offset=0x%012llx len=4 -> rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + static_cast(result)); + return result; + }); + + // Block write requests (tCode 0x1): SBP-2 first, then FCP fallback. + router->RegisterRequestHandler( + 0x1, + [sbp2Manager, fcpRouter](const ASFW::Async::ARPacketView& packet) { + uint64_t destOffset = 0; + if (sbp2Manager && packet.header.size() >= 16 && !packet.payload.empty()) { + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + const auto sbp2Result = + sbp2Manager->ApplyRemoteWrite(destOffset, packet.payload); + ASFW_LOG_V2( + Async, + "SBP2 AR write-block req: src=0x%04x offset=0x%012llx len=%zu -> rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + packet.payload.size(), + static_cast(sbp2Result)); + if (sbp2Result != ASFW::Async::ResponseCode::AddressError) { + return sbp2Result; + } + } + + if (fcpRouter) { + const Protocols::Ports::BlockWriteRequestView request{ + .sourceID = packet.sourceID, + .destOffset = ASFW::Async::ExtractDestOffset(packet.header), + .payload = packet.payload, + }; + const auto disposition = fcpRouter->RouteBlockWrite(request); + if (disposition == + Protocols::Ports::BlockWriteDisposition::kAddressError) { + return ASFW::Async::ResponseCode::AddressError; + } + return ASFW::Async::ResponseCode::Complete; + } - if (!d.asyncSubsystem) { - return; - } - - auto* router = d.asyncSubsystem->GetPacketRouter(); - if (!router) { - return; - } - - auto* sbp2Manager = d.sbp2AddressSpaceManager.get(); - auto* fcpRouter = d.fcpResponseRouter.get(); - auto* responder = router->GetResponseSender(); - - router->RegisterRequestHandler( - 0x0, - [sbp2Manager](const ASFW::Async::ARPacketView& packet) { - if (!sbp2Manager || packet.header.size() < 16) { - return ASFW::Async::ResponseCode::AddressError; - } - - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto quadletData = std::span(packet.header.data() + 12, 4); - return sbp2Manager->ApplyRemoteWrite(destOffset, quadletData); - }); - - router->RegisterRequestHandler( - 0x1, - [sbp2Manager, fcpRouter](const ASFW::Async::ARPacketView& packet) { - if (sbp2Manager && packet.header.size() >= 16 && !packet.payload.empty()) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto sbp2Result = - sbp2Manager->ApplyRemoteWrite(destOffset, packet.payload); - if (sbp2Result != ASFW::Async::ResponseCode::AddressError) { - return sbp2Result; - } - } - - if (fcpRouter) { - const ASFW::Protocols::Ports::BlockWriteRequestView request{ - .sourceID = packet.sourceID, - .destOffset = ASFW::Async::ExtractDestOffset(packet.header), - .payload = packet.payload, - }; - const auto disposition = fcpRouter->RouteBlockWrite(request); - if (disposition == ASFW::Protocols::Ports::BlockWriteDisposition::kAddressError) { return ASFW::Async::ResponseCode::AddressError; - } - return ASFW::Async::ResponseCode::Complete; - } - - return ASFW::Async::ResponseCode::AddressError; - }); - - router->RegisterRequestHandler( - 0x4, - [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { - uint32_t quadlet = 0; - auto result = ASFW::Async::ResponseCode::AddressError; - - if (sbp2Manager && packet.header.size() >= 12) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - result = sbp2Manager->ReadQuadlet(destOffset, &quadlet); - } - - if (responder) { - responder->SendReadQuadletResponse(packet, result, quadlet); - } - return ASFW::Async::ResponseCode::NoResponse; - }); - - router->RegisterRequestHandler( - 0x5, - [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { - auto result = ASFW::Async::ResponseCode::AddressError; - ASFW::Protocols::SBP2::AddressSpaceManager::ReadSlice slice{}; - - if (sbp2Manager && packet.header.size() >= 16) { - const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); - const auto readLength = - static_cast(ASFW::Async::ExtractDataLength(packet.header)); - if (readLength > 0) { - result = sbp2Manager->ResolveReadSlice(destOffset, readLength, &slice); - } else { - result = ASFW::Async::ResponseCode::DataError; - } - } - - if (responder) { - if (result == ASFW::Async::ResponseCode::Complete) { - responder->SendReadBlockResponse( - packet, result, slice.payloadDeviceAddress, slice.payloadLength); - } else { - responder->SendReadBlockResponse(packet, result, 0, 0); - } - } - return ASFW::Async::ResponseCode::NoResponse; - }); - - ASFW_LOG(Controller, - "[Controller] PacketRouter wired for SBP2 address spaces (tCode 0x0/0x1/0x4/0x5)"); + }); + + // Read quadlet requests (tCode 0x4): active read response from address-space manager. + router->RegisterRequestHandler( + 0x4, + [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { + uint32_t quadlet = 0; + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + + if (sbp2Manager && packet.header.size() >= 12) { + const uint64_t destOffset = ASFW::Async::ExtractDestOffset(packet.header); + result = sbp2Manager->ReadQuadlet(destOffset, &quadlet); + } + + if (responder) { + responder->SendReadQuadletResponse(packet, result, quadlet); + } + return ASFW::Async::ResponseCode::NoResponse; + }); + + // Read block requests (tCode 0x5): active read response from address-space manager. + router->RegisterRequestHandler( + 0x5, + [sbp2Manager, responder](const ASFW::Async::ARPacketView& packet) { + ASFW::Async::ResponseCode result = ASFW::Async::ResponseCode::AddressError; + ASFW::Protocols::SBP2::AddressSpaceManager::ReadSlice slice{}; + uint64_t destOffset = 0; + uint32_t readLength = 0; + + if (sbp2Manager && packet.header.size() >= 16) { + destOffset = ASFW::Async::ExtractDestOffset(packet.header); + readLength = + static_cast(ASFW::Async::ExtractDataLength(packet.header)); + if (readLength > 0) { + result = sbp2Manager->ResolveReadSlice(destOffset, readLength, &slice); + } else { + result = ASFW::Async::ResponseCode::DataError; + } + } + + if (result == ASFW::Async::ResponseCode::Complete) { + ASFW_LOG_V2( + Async, + "SBP2 AR read-block req: src=0x%04x offset=0x%012llx len=%u -> " + "rcode=0x%x payload=0x%08x/%u", + packet.sourceID, + static_cast(destOffset), + readLength, + static_cast(result), + static_cast(slice.payloadDeviceAddress), + slice.payloadLength); + } else { + ASFW_LOG_V2( + Async, + "SBP2 AR read-block req: src=0x%04x offset=0x%012llx len=%u -> " + "rcode=0x%x", + packet.sourceID, + static_cast(destOffset), + readLength, + static_cast(result)); + } + + if (responder) { + if (result == ASFW::Async::ResponseCode::Complete) { + responder->SendReadBlockResponse( + packet, + result, + slice.payloadDeviceAddress, + slice.payloadLength); + } else { + responder->SendReadBlockResponse(packet, result, 0, 0); + } + } + + return ASFW::Async::ResponseCode::NoResponse; + }); + + ASFW_LOG( + Controller, + "[Controller] PacketRouter wired for SBP2/FCP handlers (tCode 0x0/0x1/0x4/0x5)"); + } + } } kern_return_t DriverWiring::PrepareQueue(ASFWDriver& service, ::ServiceContext& ctx) { diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp index ec7f5b69..67cbba29 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.cpp @@ -58,6 +58,19 @@ enum { kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + // Diagnostics + kMethodGetBusStateDiagnostics = 50, + kMethodReadPhyRegister = 51, + kMethodInitiateBusReset = 52, + // SBP-2 session management + kMethodCreateSBP2Session = 53, + kMethodStartSBP2Login = 54, + kMethodGetSBP2SessionState = 55, + kMethodSubmitSBP2Inquiry = 56, + kMethodGetSBP2InquiryResult = 57, + kMethodReleaseSBP2Session = 58, + kMethodSubmitSBP2Command = 59, + kMethodGetSBP2CommandResult = 60, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, @@ -205,6 +218,8 @@ kern_return_t IMPL(ASFWDriverUserClient, Stop) { ivars->driver = nullptr; } if (auto* runtimeState = ASFW::UserClient::GetRuntimeState(this); runtimeState != nullptr) { + // Release owner-bound SBP-2 resources before handler teardown so + // abrupt client exit cannot strand address ranges inside the driver. runtimeState->ReleaseOwner(this); runtimeState->ResetHandlers(); } @@ -494,6 +509,41 @@ kern_return_t ASFWDriverUserClient::ExternalMethod(uint64_t selector, case kMethodStopIsochTransmit: return runtimeState->Isoch().StopIsochTransmit(arguments); + // DiagnosticsHandler methods (50, 51, 52) + case kMethodGetBusStateDiagnostics: + return runtimeState->Diagnostics().GetBusStateDiagnostics(arguments); + + case kMethodReadPhyRegister: + return runtimeState->Diagnostics().ReadPhyRegister(arguments); + + case kMethodInitiateBusReset: + return runtimeState->Diagnostics().InitiateBusReset(arguments); + + // SBP-2 session management (53-60) + case kMethodCreateSBP2Session: + return runtimeState->SBP2().CreateSBP2Session(arguments, this); + + case kMethodStartSBP2Login: + return runtimeState->SBP2().StartSBP2Login(arguments); + + case kMethodGetSBP2SessionState: + return runtimeState->SBP2().GetSBP2SessionState(arguments); + + case kMethodSubmitSBP2Inquiry: + return runtimeState->SBP2().SubmitSBP2Inquiry(arguments); + + case kMethodGetSBP2InquiryResult: + return runtimeState->SBP2().GetSBP2InquiryResult(arguments); + + case kMethodReleaseSBP2Session: + return runtimeState->SBP2().ReleaseSBP2Session(arguments, this); + + case kMethodSubmitSBP2Command: + return runtimeState->SBP2().SubmitSBP2Command(arguments); + + case kMethodGetSBP2CommandResult: + return runtimeState->SBP2().GetSBP2CommandResult(arguments); + default: return kIOReturnBadArgument; } diff --git a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig index 599ca8e7..1fa20251 100644 --- a/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig +++ b/ASFWDriver/UserClient/Core/ASFWDriverUserClient.iig @@ -59,6 +59,19 @@ public: kMethodDeallocateAddressRange = 47, kMethodReadIncomingData = 48, kMethodWriteLocalData = 49, + // Diagnostics + kMethodGetBusStateDiagnostics = 50, + kMethodReadPhyRegister = 51, + kMethodInitiateBusReset = 52, + // SBP-2 session management + kMethodCreateSBP2Session = 53, + kMethodStartSBP2Login = 54, + kMethodGetSBP2SessionState = 55, + kMethodSubmitSBP2Inquiry = 56, + kMethodGetSBP2InquiryResult = 57, + kMethodReleaseSBP2Session = 58, + kMethodSubmitSBP2Command = 59, + kMethodGetSBP2CommandResult = 60, // TODO(ASFW-IRM): Remove temporary IRM test method after dedicated validation tooling exists. kMethodTestIRMAllocation = 26, kMethodTestIRMRelease = 27, diff --git a/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp b/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp index 666ed035..ed0fec01 100644 --- a/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp +++ b/ASFWDriver/UserClient/Core/UserClientRuntimeState.hpp @@ -7,6 +7,7 @@ #include "../Handlers/BusResetHandler.hpp" #include "../Handlers/ConfigROMHandler.hpp" #include "../Handlers/ControllerCoreAccess.hpp" +#include "../Handlers/DiagnosticsHandler.hpp" #include "../Handlers/DeviceDiscoveryHandler.hpp" #include "../Handlers/IsochHandler.hpp" #include "../Handlers/SBP2Handler.hpp" @@ -51,15 +52,18 @@ class UserClientRuntimeState final { auto* controllerCore = GetControllerCorePtr(driver); auto* avcDiscovery = controllerCore ? controllerCore->GetAVCDiscovery() : nullptr; - auto* sbp2Manager = controllerCore ? controllerCore->GetSbp2AddressSpaceManager() : nullptr; + auto* sbp2Mgr = controllerCore ? controllerCore->GetSbp2AddressSpaceManager() : nullptr; + auto* sbp2Registry = controllerCore ? controllerCore->GetSBP2SessionRegistry() : nullptr; avcHandler_ = std::make_unique(avcDiscovery); isochHandler_ = std::make_unique(driver); - sbp2Handler_ = std::make_unique(sbp2Manager); + sbp2Handler_ = std::make_unique(sbp2Mgr, sbp2Registry); + diagnosticsHandler_ = std::make_unique(driver); return HandlersReady(); } void ResetHandlers() noexcept { + diagnosticsHandler_.reset(); sbp2Handler_.reset(); isochHandler_.reset(); avcHandler_.reset(); @@ -72,7 +76,10 @@ class UserClientRuntimeState final { } void ReleaseOwner(void* owner) noexcept { - if (owner != nullptr && sbp2Handler_ != nullptr) { + if (owner == nullptr) { + return; + } + if (sbp2Handler_ != nullptr) { sbp2Handler_->ReleaseOwner(owner); } } @@ -82,7 +89,8 @@ class UserClientRuntimeState final { statusHandler_ != nullptr && transactionHandler_ != nullptr && configRomHandler_ != nullptr && deviceDiscoveryHandler_ != nullptr && avcHandler_ != nullptr && isochHandler_ != nullptr && - sbp2Handler_ != nullptr; + sbp2Handler_ != nullptr && + diagnosticsHandler_ != nullptr; } [[nodiscard]] TransactionStorage& TransactionResults() noexcept { return transactionStorage_; } @@ -98,6 +106,7 @@ class UserClientRuntimeState final { [[nodiscard]] AVCHandler& AVC() noexcept { return *avcHandler_; } [[nodiscard]] IsochHandler& Isoch() noexcept { return *isochHandler_; } [[nodiscard]] SBP2Handler& SBP2() noexcept { return *sbp2Handler_; } + [[nodiscard]] DiagnosticsHandler& Diagnostics() noexcept { return *diagnosticsHandler_; } private: TransactionStorage transactionStorage_{}; @@ -110,6 +119,7 @@ class UserClientRuntimeState final { std::unique_ptr avcHandler_{}; std::unique_ptr isochHandler_{}; std::unique_ptr sbp2Handler_{}; + std::unique_ptr diagnosticsHandler_{}; }; template diff --git a/ASFWDriver/UserClient/Handlers/ConfigROMHandler.cpp b/ASFWDriver/UserClient/Handlers/ConfigROMHandler.cpp index 449c5532..8fd50261 100644 --- a/ASFWDriver/UserClient/Handlers/ConfigROMHandler.cpp +++ b/ASFWDriver/UserClient/Handlers/ConfigROMHandler.cpp @@ -27,6 +27,22 @@ class ASFWDriver { namespace ASFW::UserClient { +namespace { + +constexpr uint32_t kUnitSpecIdSBP2 = 0x00609E; +constexpr uint32_t kUnitSwVersionSBP2 = 0x010483; + +[[nodiscard]] bool HasSBP2Unit(const ASFW::Discovery::ConfigROM& rom) noexcept { + for (const auto& unit : rom.unitDirectories) { + if (unit.unitSpecId == kUnitSpecIdSBP2 && unit.unitSwVersion == kUnitSwVersionSBP2) { + return true; + } + } + return false; +} + +} // namespace + ConfigROMHandler::ConfigROMHandler(ASFWDriver* driver) : driver_(driver) {} // NOLINTNEXTLINE(readability-function-cognitive-complexity) - UserClient argument plumbing. @@ -106,8 +122,12 @@ kern_return_t ConfigROMHandler::ExportConfigROM(IOUserClientMethodArguments* arg return kIOReturnNoMemory; } - ASFW_LOG(UserClient, "ExportConfigROM: returning %zu quadlets (%zu bytes) for node=%u gen=%u", - rom->rawQuadlets.size(), dataSize, nodeId, resolvedGen.value); + ASFW_LOG(UserClient, + "ExportConfigROM: returning %zu quadlets (%zu bytes) for node=%u gen=%u " + "state=%u rootEntries=%zu unitDirs=%zu hasSBP2=%d requestedGen=%u", + rom->rawQuadlets.size(), dataSize, nodeId, resolvedGen.value, + static_cast(rom->state), rom->rootDirMinimal.size(), + rom->unitDirectories.size(), HasSBP2Unit(*rom) ? 1 : 0, requestedGen.value); if (args->scalarOutput != nullptr && args->scalarOutputCount >= 1) { args->scalarOutput[0] = static_cast(resolvedGen.value & 0xFFFFU); diff --git a/ASFWDriver/UserClient/Handlers/DeviceDiscoveryHandler.cpp b/ASFWDriver/UserClient/Handlers/DeviceDiscoveryHandler.cpp index 6c4f0122..bf347996 100644 --- a/ASFWDriver/UserClient/Handlers/DeviceDiscoveryHandler.cpp +++ b/ASFWDriver/UserClient/Handlers/DeviceDiscoveryHandler.cpp @@ -139,7 +139,7 @@ kern_return_t DeviceDiscoveryHandler::GetDiscoveredDevices(IOUserClientMethodArg deviceWire.nodeId = device->GetNodeID(); deviceWire.state = StateToWire(device->GetState()); deviceWire.unitCount = static_cast(device->GetUnits().size()); - deviceWire._padding = 0; + deviceWire.deviceKind = static_cast(device->GetKind()); // Copy vendor and model names CopyStringToBuffer(deviceWire.vendorName, sizeof(deviceWire.vendorName), @@ -160,6 +160,10 @@ kern_return_t DeviceDiscoveryHandler::GetDiscoveredDevices(IOUserClientMethodArg unitWire.romOffset = unit->GetDirectoryOffset(); unitWire.state = UnitStateToWire(unit->GetState()); memset(unitWire._padding, 0, sizeof(unitWire._padding)); + unitWire.managementAgentOffset = unit->GetManagementAgentOffset().value_or(0); + unitWire.lun = unit->GetLUN().value_or(0); + unitWire.unitCharacteristics = unit->GetUnitCharacteristics().value_or(0); + unitWire.fastStart = unit->GetFastStart().value_or(0); // Copy vendor and product names CopyStringToBuffer(unitWire.vendorName, sizeof(unitWire.vendorName), diff --git a/ASFWDriver/UserClient/Handlers/DiagnosticsHandler.cpp b/ASFWDriver/UserClient/Handlers/DiagnosticsHandler.cpp new file mode 100644 index 00000000..b7aced8a --- /dev/null +++ b/ASFWDriver/UserClient/Handlers/DiagnosticsHandler.cpp @@ -0,0 +1,163 @@ +// +// DiagnosticsHandler.cpp +// ASFWDriver +// +// Handler for bus state diagnostics UserClient methods +// + +#include "DiagnosticsHandler.hpp" +#include "../../Bus/BusManager.hpp" +#include "../../Bus/BusResetCoordinator.hpp" +#include "../../Controller/ControllerCore.hpp" +#include "../../Hardware/HardwareInterface.hpp" +#include "../../Logging/Logging.hpp" +#include "../WireFormats/DiagnosticsWireFormats.hpp" +#include "ASFWDriver.h" +#include "ControllerCoreAccess.hpp" + +#include +#include + +using ASFW::UserClient::Wire::BusStateWire; + +namespace ASFW::UserClient { + +DiagnosticsHandler::DiagnosticsHandler(ASFWDriver* driver) : driver_(driver) {} + +kern_return_t DiagnosticsHandler::GetBusStateDiagnostics(IOUserClientMethodArguments* args) { + if (!args) { + return kIOReturnBadArgument; + } + + auto* controller = GetControllerCorePtr(driver_); + if (!controller) { + return kIOReturnNotReady; + } + + BusStateWire wire{}; + std::memset(&wire, 0, sizeof(wire)); + + // Hardware registers + auto* hw = controller->GetHardware(); + if (hw) { + wire.hcControl = hw->ReadHCControl(); + wire.linkControl = hw->ReadLinkControl(); + wire.nodeId = hw->ReadNodeID(); + wire.cycleTime = hw->ReadCycleTime(); + + auto phy1 = hw->ReadPhyRegister(1); + wire.phyReg1 = phy1.value_or(0xFF); + + auto phy4 = hw->ReadPhyRegister(4); + wire.phyReg4 = phy4.value_or(0xFF); + } + + // BusResetCoordinator FSM state and metrics + auto* busReset = controller->GetBusResetCoordinator(); + if (busReset) { + wire.busResetFsmState = static_cast(busReset->GetState()); + wire.busResetCount = busReset->Metrics().resetCount; + const auto resetDiag = busReset->Diagnostics(); + wire.diagnosticsVersion = 2; + wire.readyForDiscoveryFailureBits = resetDiag.readyForDiscoveryFailureBits; + wire.lastRecoveryReasonCode = static_cast(resetDiag.lastRecoveryReasonCode); + wire.lastResetKind = resetDiag.lastResetKind; + wire.driverStartId = resetDiag.driverStartId; + wire.resetEpoch = resetDiag.resetEpoch; + wire.manualResetEpoch = resetDiag.manualResetEpoch; + wire.softwareResetIssuedCount = resetDiag.softwareResetIssuedCount; + wire.busResetIrqCount = resetDiag.busResetIrqCount; + wire.lastAcceptedGeneration = resetDiag.lastAcceptedGeneration; + wire.lastTopologyNodeCount = resetDiag.lastTopologyNodeCount; + wire.recoveryResetAttempts = resetDiag.recoveryResetAttempts; + wire.discoveryCallbackCount = resetDiag.discoveryCallbackCount; + } + + // Topology + if (auto topo = controller->LatestTopology()) { + wire.generation = topo->generation; + wire.localNodeId = topo->localNodeId.has_value() + ? static_cast(*topo->localNodeId & 0x3F) + : 0xFF; + wire.rootNodeId = topo->rootNodeId.has_value() + ? static_cast(*topo->rootNodeId & 0x3F) + : 0xFF; + wire.irmNodeId = topo->irmNodeId.has_value() ? static_cast(*topo->irmNodeId & 0x3F) + : 0xFF; + wire.gapCount = topo->gapCount; + } + + // BusManager config + auto* busMgr = controller->GetBusManager(); + if (busMgr) { + const auto& cfg = busMgr->GetConfig(); + wire.rootPolicy = static_cast(cfg.rootPolicy); + wire.delegateCm = cfg.delegateCycleMaster ? 1 : 0; + } + + // Return as OSData + OSData* data = OSData::withBytes(&wire, sizeof(wire)); + if (!data) { + return kIOReturnNoMemory; + } + args->structureOutput = data; + args->structureOutputDescriptor = nullptr; + + return kIOReturnSuccess; +} + +kern_return_t DiagnosticsHandler::ReadPhyRegister(IOUserClientMethodArguments* args) { + if (!args || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint8_t address = static_cast(args->scalarInput[0] & 0xFF); + if (address > 7) { + return kIOReturnBadArgument; + } + + auto* controller = GetControllerCorePtr(driver_); + if (!controller) { + return kIOReturnNotReady; + } + + auto* hw = controller->GetHardware(); + if (!hw) { + return kIOReturnNotReady; + } + + auto value = hw->ReadPhyRegister(address); + if (!value.has_value()) { + return kIOReturnIOError; + } + + if (args->scalarOutput != nullptr && args->scalarOutputCount >= 1) { + args->scalarOutput[0] = static_cast(*value); + args->scalarOutputCount = 1; + } + + return kIOReturnSuccess; +} + +kern_return_t DiagnosticsHandler::InitiateBusReset(IOUserClientMethodArguments* args) { + bool shortReset = true; + if (args && args->scalarInputCount >= 1) { + shortReset = (args->scalarInput[0] == 0); + } + + auto* controller = GetControllerCorePtr(driver_); + if (!controller) { + return kIOReturnNotReady; + } + + auto* busReset = controller->GetBusResetCoordinator(); + if (!busReset) { + return kIOReturnNotReady; + } + + ASFW_LOG(Hardware, "UserClient: InitiateBusReset (%s)", shortReset ? "short" : "long"); + busReset->RequestUserReset(shortReset); + return kIOReturnSuccess; +} + +} // namespace ASFW::UserClient diff --git a/ASFWDriver/UserClient/Handlers/DiagnosticsHandler.hpp b/ASFWDriver/UserClient/Handlers/DiagnosticsHandler.hpp new file mode 100644 index 00000000..7bf6d59a --- /dev/null +++ b/ASFWDriver/UserClient/Handlers/DiagnosticsHandler.hpp @@ -0,0 +1,42 @@ +// +// DiagnosticsHandler.hpp +// ASFWDriver +// +// Handler for bus state diagnostics UserClient methods +// + +#ifndef ASFW_USERCLIENT_DIAGNOSTICS_HANDLER_HPP +#define ASFW_USERCLIENT_DIAGNOSTICS_HANDLER_HPP + +#include + +// Forward declarations +class ASFWDriver; + +namespace ASFW::UserClient { + +class DiagnosticsHandler { +public: + explicit DiagnosticsHandler(ASFWDriver* driver); + ~DiagnosticsHandler() = default; + + // Disable copy/move + DiagnosticsHandler(const DiagnosticsHandler&) = delete; + DiagnosticsHandler& operator=(const DiagnosticsHandler&) = delete; + + // Method 50: Get bus state diagnostics (OHCI registers, topology, PHY, FSM) + kern_return_t GetBusStateDiagnostics(IOUserClientMethodArguments* args); + + // Method 51: Read PHY register + kern_return_t ReadPhyRegister(IOUserClientMethodArguments* args); + + // Method 52: Initiate software bus reset + kern_return_t InitiateBusReset(IOUserClientMethodArguments* args); + +private: + ASFWDriver* driver_; +}; + +} // namespace ASFW::UserClient + +#endif // ASFW_USERCLIENT_DIAGNOSTICS_HANDLER_HPP diff --git a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp index 78e424a5..4b053499 100644 --- a/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp +++ b/ASFWDriver/UserClient/Handlers/SBP2Handler.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -8,19 +9,24 @@ #include "../../Logging/Logging.hpp" #include "../../Protocols/SBP2/AddressSpaceManager.hpp" +#include "../../Protocols/SBP2/SCSICommandSet.hpp" +#include "../../Protocols/SBP2/SBP2SessionRegistry.hpp" +#include "../WireFormats/SBP2CommandWireFormats.hpp" namespace ASFW::UserClient { class SBP2Handler { public: - explicit SBP2Handler(ASFW::Protocols::SBP2::AddressSpaceManager* manager) - : manager_(manager) {} + explicit SBP2Handler(ASFW::Protocols::SBP2::AddressSpaceManager* manager, + ASFW::Protocols::SBP2::SBP2SessionRegistry* registry) + : manager_(manager), registry_(registry) {} ~SBP2Handler() = default; SBP2Handler(const SBP2Handler&) = delete; SBP2Handler& operator=(const SBP2Handler&) = delete; + // Address space management (selectors 46-49) kern_return_t AllocateAddressRange(IOUserClientMethodArguments* args, void* owner) { if (!manager_) { return kIOReturnNotReady; @@ -78,14 +84,34 @@ class SBP2Handler { std::vector data; const kern_return_t kr = manager_->ReadIncomingData(owner, handle, offset, length, &data); if (kr != kIOReturnSuccess) { + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> kr=0x%x", + owner, + static_cast(handle), + offset, + length, + static_cast(kr)); return kr; } OSData* output = OSData::withBytes(data.data(), static_cast(data.size())); if (!output) { + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> no memory", + owner, + static_cast(handle), + offset, + length); return kIOReturnNoMemory; } + ASFW_LOG(UserClient, + "ReadIncomingData: owner=%p handle=0x%llx offset=%u len=%u -> %u bytes", + owner, + static_cast(handle), + offset, + length, + static_cast(data.size())); args->structureOutput = output; args->structureOutputDescriptor = nullptr; return kIOReturnSuccess; @@ -128,10 +154,230 @@ class SBP2Handler { if (manager_) { manager_->ReleaseOwner(owner); } + if (registry_) { + registry_->ReleaseOwner(owner); + } + } + + // Session management (selectors 53-58) + + kern_return_t CreateSBP2Session(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 3 || + !args->scalarOutput || args->scalarOutputCount < 1) { + return kIOReturnBadArgument; + } + + const uint32_t guidHi = static_cast(args->scalarInput[0] & 0xFFFF'FFFFu); + const uint32_t guidLo = static_cast(args->scalarInput[1] & 0xFFFF'FFFFu); + const uint32_t romOffset = static_cast(args->scalarInput[2] & 0xFFFF'FFFFu); + const uint64_t guid = (static_cast(guidHi) << 32) | guidLo; + + auto result = registry_->CreateSession(owner, guid, romOffset); + if (!result.has_value()) { + return result.error(); + } + + args->scalarOutput[0] = *result; + args->scalarOutputCount = 1; + return kIOReturnSuccess; + } + + kern_return_t StartSBP2Login(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->StartLogin(handle) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t GetSBP2SessionState(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto state = registry_->GetSessionState(handle); + if (!state.has_value()) { + return kIOReturnNotFound; + } + + // Return as scalars: loginState, loginID, generation, lastError, reconnectPending + if (args->scalarOutput && args->scalarOutputCount >= 5) { + args->scalarOutput[0] = static_cast(state->loginState); + args->scalarOutput[1] = static_cast(state->loginID); + args->scalarOutput[2] = static_cast(state->generation); + args->scalarOutput[3] = static_cast(static_cast(state->lastError)); + args->scalarOutput[4] = state->reconnectPending ? 1 : 0; + args->scalarOutputCount = 5; + } + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2Inquiry(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 2) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + const uint8_t allocationLength = static_cast(args->scalarInput[1] & 0xFFu); + return registry_->SubmitInquiry(handle, allocationLength) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t GetSBP2InquiryResult(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto result = registry_->GetInquiryResult(handle); + if (!result.has_value()) { + return kIOReturnNotFound; + } + + OSData* output = OSData::withBytes(result->data(), static_cast(result->size())); + if (!output) { + return kIOReturnNoMemory; + } + + args->structureOutput = output; + args->structureOutputDescriptor = nullptr; + return kIOReturnSuccess; + } + + kern_return_t SubmitSBP2Command(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1 || !args->structureInput) { + return kIOReturnBadArgument; + } + + OSData* input = OSDynamicCast(OSData, args->structureInput); + if (!input) { + return kIOReturnBadArgument; + } + + const auto* bytes = static_cast(input->getBytesNoCopy()); + const size_t inputLength = input->getLength(); + if (!bytes || inputLength < sizeof(Wire::SBP2CommandRequestWire)) { + return kIOReturnBadArgument; + } + + const auto* header = reinterpret_cast(bytes); + const size_t expectedLength = + sizeof(Wire::SBP2CommandRequestWire) + + static_cast(header->cdbLength) + + static_cast(header->outgoingLength); + if (inputLength != expectedLength || header->cdbLength == 0) { + return kIOReturnBadArgument; + } + + Protocols::SBP2::SCSI::DataDirection direction{}; + switch (header->direction) { + case 0: + direction = Protocols::SBP2::SCSI::DataDirection::None; + break; + case 1: + direction = Protocols::SBP2::SCSI::DataDirection::FromTarget; + break; + case 2: + direction = Protocols::SBP2::SCSI::DataDirection::ToTarget; + break; + default: + return kIOReturnBadArgument; + } + + Protocols::SBP2::SCSI::CommandRequest request{}; + request.direction = direction; + request.transferLength = header->transferLength; + request.timeoutMs = header->timeoutMs; + request.captureSenseData = header->captureSenseData != 0; + + const uint8_t* cursor = bytes + sizeof(Wire::SBP2CommandRequestWire); + request.cdb.assign(cursor, cursor + header->cdbLength); + cursor += header->cdbLength; + request.outgoingPayload.assign(cursor, cursor + header->outgoingLength); + + const uint64_t handle = args->scalarInput[0]; + return registry_->SubmitCommand(handle, request) ? kIOReturnSuccess : kIOReturnError; + } + + kern_return_t GetSBP2CommandResult(IOUserClientMethodArguments* args) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + auto result = registry_->GetCommandResult(handle); + if (!result.has_value()) { + return kIOReturnNotFound; + } + + Wire::SBP2CommandResultWire header{}; + header.transportStatus = result->transportStatus; + header.sbpStatus = result->sbpStatus; + header.payloadLength = static_cast(result->payload.size()); + header.senseLength = static_cast(result->senseData.size()); + + std::vector serialized( + sizeof(Wire::SBP2CommandResultWire) + + result->payload.size() + + result->senseData.size()); + memcpy(serialized.data(), &header, sizeof(header)); + + size_t offset = sizeof(Wire::SBP2CommandResultWire); + if (!result->payload.empty()) { + memcpy(serialized.data() + offset, result->payload.data(), result->payload.size()); + offset += result->payload.size(); + } + if (!result->senseData.empty()) { + memcpy(serialized.data() + offset, result->senseData.data(), result->senseData.size()); + } + + OSData* output = OSData::withBytes(serialized.data(), static_cast(serialized.size())); + if (!output) { + return kIOReturnNoMemory; + } + + args->structureOutput = output; + args->structureOutputDescriptor = nullptr; + return kIOReturnSuccess; + } + + kern_return_t ReleaseSBP2Session(IOUserClientMethodArguments* args, void* owner) { + if (!registry_) { + return kIOReturnNotReady; + } + if (!args || !args->scalarInput || args->scalarInputCount < 1) { + return kIOReturnBadArgument; + } + + const uint64_t handle = args->scalarInput[0]; + return registry_->ReleaseSession(handle) ? kIOReturnSuccess : kIOReturnNotFound; } private: ASFW::Protocols::SBP2::AddressSpaceManager* manager_{nullptr}; + ASFW::Protocols::SBP2::SBP2SessionRegistry* registry_{nullptr}; }; } // namespace ASFW::UserClient diff --git a/ASFWDriver/UserClient/Handlers/TransactionHandler.cpp b/ASFWDriver/UserClient/Handlers/TransactionHandler.cpp index b0540bf7..7d70f971 100644 --- a/ASFWDriver/UserClient/Handlers/TransactionHandler.cpp +++ b/ASFWDriver/UserClient/Handlers/TransactionHandler.cpp @@ -358,15 +358,14 @@ kern_return_t TransactionHandler::GetTransactionResult(IOUserClientMethodArgumen args->scalarOutputCount = 3; } - if (args->structureOutput && foundResult->dataLength > 0) { - OSData* resultData = OSData::withBytes(foundResult->data, foundResult->dataLength); - if (resultData) { - args->structureOutput = resultData; - args->structureOutputDescriptor = nullptr; - } else { - storage_->Unlock(); - return kIOReturnNoMemory; - } + const void* resultBytes = foundResult->Data(); + OSData* resultData = OSData::withBytes(resultBytes, foundResult->dataLength); + if (resultData) { + args->structureOutput = resultData; + args->structureOutputDescriptor = nullptr; + } else { + storage_->Unlock(); + return kIOReturnNoMemory; } ASFW_LOG(UserClient, "GetTransactionResult: handle=0x%04x status=%u rCode=0x%02x len=%u", diff --git a/ASFWDriver/UserClient/Storage/TransactionStorage.cpp b/ASFWDriver/UserClient/Storage/TransactionStorage.cpp index 5e9161fd..5383fc0c 100644 --- a/ASFWDriver/UserClient/Storage/TransactionStorage.cpp +++ b/ASFWDriver/UserClient/Storage/TransactionStorage.cpp @@ -10,7 +10,6 @@ #include #include -#include namespace ASFW::UserClient { @@ -51,11 +50,13 @@ bool TransactionStorage::StoreResult(uint16_t handle, uint32_t status, uint8_t r result.handle = handle; result.status = status; result.responseCode = responseCode; - result.dataLength = (responseLength > 512) ? 512 : responseLength; + result.data.clear(); - if (responsePayload && responseLength > 0 && result.dataLength > 0) { - std::memcpy(result.data, responsePayload, result.dataLength); + if (responsePayload && responseLength > 0) { + const auto* bytes = static_cast(responsePayload); + result.data.assign(bytes, bytes + responseLength); } + result.dataLength = static_cast(result.data.size()); completedHead_ = nextHead; diff --git a/ASFWDriver/UserClient/Storage/TransactionStorage.hpp b/ASFWDriver/UserClient/Storage/TransactionStorage.hpp index 9a7754a6..58e2cafc 100644 --- a/ASFWDriver/UserClient/Storage/TransactionStorage.hpp +++ b/ASFWDriver/UserClient/Storage/TransactionStorage.hpp @@ -10,6 +10,7 @@ #include #include +#include // Forward declarations struct IOLock; @@ -22,7 +23,11 @@ struct TransactionResult { uint32_t status{0}; // AsyncStatus value uint8_t responseCode{0xFF}; uint32_t dataLength{0}; - uint8_t data[512]{}; // Max response data size + std::vector data{}; + + [[nodiscard]] const uint8_t* Data() const { + return data.empty() ? nullptr : data.data(); + } }; // Ring buffer storage for completed transaction results diff --git a/ASFWDriver/UserClient/WireFormats/DeviceDiscoveryWireFormats.hpp b/ASFWDriver/UserClient/WireFormats/DeviceDiscoveryWireFormats.hpp index f92c50e5..53256d8b 100644 --- a/ASFWDriver/UserClient/WireFormats/DeviceDiscoveryWireFormats.hpp +++ b/ASFWDriver/UserClient/WireFormats/DeviceDiscoveryWireFormats.hpp @@ -19,6 +19,10 @@ struct __attribute__((packed)) FWUnitWire { uint32_t romOffset; uint8_t state; // 0=Created, 1=Ready, 2=Suspended, 3=Terminated uint8_t _padding[3]; + uint32_t managementAgentOffset; + uint32_t lun; + uint32_t unitCharacteristics; + uint32_t fastStart; char vendorName[64]; // null-terminated char productName[64]; // null-terminated }; @@ -32,7 +36,7 @@ struct __attribute__((packed)) FWDeviceWire { uint8_t nodeId; uint8_t state; // 0=Created, 1=Ready, 2=Suspended, 3=Terminated uint8_t unitCount; // Number of units following this device - uint8_t _padding; + uint8_t deviceKind; // DeviceKind enum value char vendorName[64]; // null-terminated char modelName[64]; // null-terminated // Followed by: FWUnitWire array (unitCount elements) diff --git a/ASFWDriver/UserClient/WireFormats/DiagnosticsWireFormats.hpp b/ASFWDriver/UserClient/WireFormats/DiagnosticsWireFormats.hpp new file mode 100644 index 00000000..b1f929e8 --- /dev/null +++ b/ASFWDriver/UserClient/WireFormats/DiagnosticsWireFormats.hpp @@ -0,0 +1,54 @@ +// +// DiagnosticsWireFormats.hpp +// ASFWDriver +// +// Wire format structures for bus state diagnostics +// + +#ifndef ASFW_USERCLIENT_DIAGNOSTICS_WIRE_FORMATS_HPP +#define ASFW_USERCLIENT_DIAGNOSTICS_WIRE_FORMATS_HPP + +#include "WireFormatsCommon.hpp" + +namespace ASFW::UserClient::Wire { + +// Bus state diagnostics snapshot (64 bytes) +struct __attribute__((packed)) BusStateWire { + uint32_t hcControl; // OHCI HCControl register + uint32_t linkControl; // OHCI LinkControl register + uint32_t nodeId; // OHCI NodeID register + uint32_t cycleTime; // OHCI CycleTimer register + uint32_t generation; // Bus generation + uint32_t busResetCount; // Bus reset count + // 24 bytes above + uint8_t busResetFsmState; // BusResetCoordinator::State enum value + uint8_t localNodeId; // Local node ID from topology (0xFF if none) + uint8_t rootNodeId; // Root node ID from topology (0xFF if none) + uint8_t irmNodeId; // IRM node ID from topology (0xFF if none) + uint8_t gapCount; // Gap count from topology + uint8_t rootPolicy; // BusManager::Config::RootPolicy enum + uint8_t delegateCm; // delegateCycleMaster bool (0/1) + uint8_t phyReg1; // PHY register 1 value + uint8_t phyReg4; // PHY register 4 value + // 9 bytes above (24 + 9 = 33). Versioned reset diagnostics occupy the + // previous padding area; old clients that only know the first 33 bytes + // continue to parse the stable prefix. + uint8_t diagnosticsVersion; + uint8_t readyForDiscoveryFailureBits; + uint8_t lastRecoveryReasonCode; + uint8_t lastResetKind; + uint32_t driverStartId; + uint32_t resetEpoch; + uint32_t manualResetEpoch; + uint32_t softwareResetIssuedCount; + uint32_t busResetIrqCount; + uint32_t lastAcceptedGeneration; + uint8_t lastTopologyNodeCount; + uint8_t recoveryResetAttempts; + uint8_t discoveryCallbackCount; +}; +static_assert(sizeof(BusStateWire) == 64, "BusStateWire must be 64 bytes"); + +} // namespace ASFW::UserClient::Wire + +#endif // ASFW_USERCLIENT_DIAGNOSTICS_WIRE_FORMATS_HPP diff --git a/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp new file mode 100644 index 00000000..3564e4d3 --- /dev/null +++ b/ASFWDriver/UserClient/WireFormats/SBP2CommandWireFormats.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace ASFW::UserClient::Wire { + +struct __attribute__((packed)) SBP2CommandRequestWire { + uint32_t cdbLength; + uint32_t transferLength; + uint32_t outgoingLength; + uint32_t timeoutMs; + uint8_t direction; + uint8_t captureSenseData; + uint8_t _reserved[2]; + // Followed by: CDB bytes, then outgoing payload bytes. +}; + +struct __attribute__((packed)) SBP2CommandResultWire { + int32_t transportStatus; + uint8_t sbpStatus; + uint8_t _reserved[3]; + uint32_t payloadLength; + uint32_t senseLength; + // Followed by: payload bytes, then sense bytes. +}; + +} // namespace ASFW::UserClient::Wire diff --git a/ASFWDriver/fixlog.sh b/ASFWDriver/fixlog.sh deleted file mode 100755 index 8adfa578..00000000 --- a/ASFWDriver/fixlog.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/zsh -# wraps %s to %{public}s in all files in ASFWDriver directory -# to avoid privacy issues in logs -setopt extendedglob - -total=0 -for f in **/*(.); do - # Skip this script itself and other shell scripts - [[ "$f" == *.sh ]] && continue - - count=$(grep -o '%s' "$f" | wc -l) - if (( count > 0 )); then - echo "[$count] $f" - total=$((total + count)) - sed -i '' 's/%s/%{public}s/g' "$f" - fi -done -echo "Total replacements: ${total}" \ No newline at end of file diff --git a/ASFWTests/DeviceDiscoveryWireParsingTests.swift b/ASFWTests/DeviceDiscoveryWireParsingTests.swift new file mode 100644 index 00000000..a84eab92 --- /dev/null +++ b/ASFWTests/DeviceDiscoveryWireParsingTests.swift @@ -0,0 +1,111 @@ +import Foundation +import Testing +@testable import ASFW + +struct DeviceDiscoveryWireParsingTests { + private func appendLE(_ value: T, to data: inout Data) { + var raw = value.littleEndian + withUnsafeBytes(of: &raw) { bytes in + data.append(contentsOf: bytes) + } + } + + private func appendCString(_ value: String, byteCount: Int, to data: inout Data) { + precondition(byteCount > 0) + + var bytes = Array(value.utf8.prefix(byteCount - 1)) + bytes.append(0) + if bytes.count < byteCount { + bytes.append(contentsOf: repeatElement(0, count: byteCount - bytes.count)) + } + data.append(contentsOf: bytes) + } + + @Test func parsesStorageDeviceKindAndUnitROMOffset() { + var wire = Data() + + appendLE(UInt32(1), to: &wire) + appendLE(UInt32(0), to: &wire) + + let guid: UInt64 = 0x0003_DB00_01DD_DD11 + appendLE(guid, to: &wire) + appendLE(UInt32(0x0003DB), to: &wire) + appendLE(UInt32(0x01DDDD), to: &wire) + appendLE(UInt32(7), to: &wire) + wire.append(0x1C) // nodeId + wire.append(1) // state = Ready + wire.append(1) // unitCount + wire.append(4) // deviceKind = Storage + appendCString("Oxford", byteCount: 64, to: &wire) + appendCString("911 Bridge", byteCount: 64, to: &wire) + + appendLE(UInt32(0x00609E), to: &wire) + appendLE(UInt32(0x010483), to: &wire) + appendLE(UInt32(0x44), to: &wire) + wire.append(1) // unitState = Ready + wire.append(contentsOf: [UInt8](repeating: 0, count: 3)) + appendLE(UInt32(0), to: &wire) // management agent offset + appendLE(UInt32(0), to: &wire) // lun + appendLE(UInt32(0), to: &wire) // unit characteristics + appendLE(UInt32(0), to: &wire) // fast start + appendCString("Oxford", byteCount: 64, to: &wire) + appendCString("SBP-2 Unit", byteCount: 64, to: &wire) + + let devices = ASFWDriverConnector.parseDeviceDiscoveryWire(wire) + #expect(devices?.count == 1) + + guard let device = devices?.first else { return } + #expect(device.guid == guid) + #expect(device.deviceKind == 4) + #expect(device.isStorage) + #expect(device.vendorName == "Oxford") + #expect(device.modelName == "911 Bridge") + #expect(device.units.count == 1) + #expect(device.units[0].romOffset == 0x44) + #expect(device.units[0].specId == 0x00609E) + #expect(device.units[0].isSBP2Storage) + } + + @Test func parsesSBP2UnitMetadataEvenWhenDeviceKindIsNotStorage() { + var wire = Data() + + appendLE(UInt32(1), to: &wire) + appendLE(UInt32(0), to: &wire) + + let guid: UInt64 = 0x0003_DB00_01AA_AA22 + appendLE(guid, to: &wire) + appendLE(UInt32(0x0003DB), to: &wire) + appendLE(UInt32(0x01AAAA), to: &wire) + appendLE(UInt32(9), to: &wire) + wire.append(0x21) // nodeId + wire.append(1) // state = Ready + wire.append(1) // unitCount + wire.append(0) // deviceKind = Unknown + appendCString("ScannerCo", byteCount: 64, to: &wire) + appendCString("FilmScanner", byteCount: 64, to: &wire) + + appendLE(UInt32(0x00609E), to: &wire) + appendLE(UInt32(0x010483), to: &wire) + appendLE(UInt32(0x88), to: &wire) + wire.append(1) // unitState = Ready + wire.append(contentsOf: [UInt8](repeating: 0, count: 3)) + appendLE(UInt32(0x00000080), to: &wire) // management agent offset + appendLE(UInt32(0x00000002), to: &wire) // lun + appendLE(UInt32(0x00080400), to: &wire) // unit characteristics + appendLE(UInt32(0x00000011), to: &wire) // fast start + appendCString("ScannerCo", byteCount: 64, to: &wire) + appendCString("Scanner Unit", byteCount: 64, to: &wire) + + let devices = ASFWDriverConnector.parseDeviceDiscoveryWire(wire) + #expect(devices?.count == 1) + + guard let device = devices?.first else { return } + #expect(!device.isStorage) + #expect(device.hasSBP2Unit) + #expect(device.sbp2Units.count == 1) + #expect(device.sbp2Units[0].managementAgentOffset == 0x80) + #expect(device.sbp2Units[0].lun == 0x02) + #expect(device.sbp2Units[0].unitCharacteristics == 0x00080400) + #expect(device.sbp2Units[0].fastStart == 0x11) + } +} diff --git a/tests/AddressSpaceManagerTests.cpp b/tests/AddressSpaceManagerTests.cpp index 681a8944..2ba13fe0 100644 --- a/tests/AddressSpaceManagerTests.cpp +++ b/tests/AddressSpaceManagerTests.cpp @@ -261,6 +261,41 @@ TEST(AddressSpaceManagerTests, AutoAllocationReusesFreedGap) { EXPECT_EQ(firstMeta.addressLo, thirdMeta.addressLo); } +TEST(AddressSpaceManagerTests, ClearingRemoteWriteCallbackStopsFurtherNotifications) { + ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); + + uint64_t handle = 0; + ASFW::Protocols::SBP2::AddressSpaceManager::AddressRangeMeta meta{}; + ASSERT_EQ(kIOReturnSuccess, + manager.AllocateAddressRange(reinterpret_cast(0xC), + 0xFFFF, + 0x0050'0000, + 8, + &handle, + &meta)); + + int callbackCount = 0; + manager.SetRemoteWriteCallback( + handle, + [&callbackCount](uint64_t, uint32_t, std::span) { + ++callbackCount; + }); + + const std::array payload{0xDE, 0xAD, 0xBE, 0xEF}; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + manager.ApplyRemoteWrite( + ComposeAddress(meta.addressHi, meta.addressLo), + std::span(payload.data(), payload.size()))); + EXPECT_EQ(1, callbackCount); + + manager.SetRemoteWriteCallback(handle, {}); + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, + manager.ApplyRemoteWrite( + ComposeAddress(meta.addressHi, meta.addressLo), + std::span(payload.data(), payload.size()))); + EXPECT_EQ(1, callbackCount); +} + TEST(AddressSpaceManagerTests, AutoAllocationRejectsRequestLargerThanWindow) { ASFW::Protocols::SBP2::AddressSpaceManager manager(nullptr); diff --git a/tests/AsyncPacketSerDesLinuxCompatTests.cpp b/tests/AsyncPacketSerDesLinuxCompatTests.cpp index bce63bb6..6fb5e1a7 100644 --- a/tests/AsyncPacketSerDesLinuxCompatTests.cpp +++ b/tests/AsyncPacketSerDesLinuxCompatTests.cpp @@ -34,16 +34,17 @@ constexpr std::array LoadHostQuadlets(const uint8_t* base) { return words; } -std::vector MakeARBufferFromWireWords(std::initializer_list quadlets, +std::vector MakeARBufferFromOHCIWords(std::initializer_list hostOrderQuadlets, uint32_t trailerLE = 0) { std::vector bytes; - bytes.reserve(quadlets.size() * sizeof(uint32_t) + sizeof(uint32_t)); + bytes.reserve(hostOrderQuadlets.size() * sizeof(uint32_t) + sizeof(uint32_t)); - for (uint32_t word : quadlets) { - bytes.push_back(static_cast((word >> 24) & 0xFF)); - bytes.push_back(static_cast((word >> 16) & 0xFF)); - bytes.push_back(static_cast((word >> 8) & 0xFF)); + for (uint32_t word : hostOrderQuadlets) { + // OHCI AR DMA stores each received quadlet in little-endian memory order. bytes.push_back(static_cast(word & 0xFF)); + bytes.push_back(static_cast((word >> 8) & 0xFF)); + bytes.push_back(static_cast((word >> 16) & 0xFF)); + bytes.push_back(static_cast((word >> 24) & 0xFF)); } // OHCI appends a little-endian trailer. Zero is portable regardless of byte order. @@ -208,7 +209,7 @@ TEST(AsyncPacketSerDesLinuxCompat, LockRequestMatchesLinuxVector) { // ----------------------- TEST(AsyncPacketSerDesLinuxCompat, ParseReadQuadletResponseMatchesLinuxVector) { - const auto packet = MakeARBufferFromWireWords({ + const auto packet = MakeARBufferFromOHCIWords({ 0xFFC1F160u, 0xFFC00000u, 0x00000000u, @@ -241,7 +242,7 @@ TEST(AsyncPacketSerDesLinuxCompat, ParseReadQuadletResponseMatchesLinuxVector) { TEST(AsyncPacketSerDesLinuxCompat, ParseReadBlockResponseComputesPayloadLength) { // Q3 specifies data_length = 0x20 (32 bytes), so we need to include 32 bytes of payload - const auto packet = MakeARBufferFromWireWords({ + const auto packet = MakeARBufferFromOHCIWords({ 0xFFC1E170u, // Q0: header 0xFFC00000u, // Q1: source ID 0x00000000u, // Q2: reserved @@ -275,7 +276,7 @@ TEST(AsyncPacketSerDesLinuxCompat, ParseReadBlockResponseComputesPayloadLength) } TEST(AsyncPacketSerDesLinuxCompat, ParseLockResponsePreservesExtendedTCodeLength) { - const auto packet = MakeARBufferFromWireWords({ + const auto packet = MakeARBufferFromOHCIWords({ 0xFFC12DB0u, 0xFFC00000u, 0x00000000u, @@ -305,11 +306,50 @@ TEST(AsyncPacketSerDesLinuxCompat, ParseLockResponsePreservesExtendedTCodeLength EXPECT_TRUE(handled); } +TEST(AsyncPacketSerDesLinuxCompat, RequestPayloadIsCopiedIntoAlignedScratchBeforeHandler) { + const auto packet = MakeARBufferFromOHCIWords({ + 0xFFC16510u, // Q0: tCode=0x1 (write block), tLabel arbitrary + 0xFFC0ECC0u, // Q1: src=0xFFC0, addrHi=0xECC0 + 0x00000000u, // Q2: addrLo + 0x00080000u, // Q3: data_length=8 + 0x11223344u, // payload q0 + 0x55667788u, // payload q1 + }); + + std::vector misaligned; + misaligned.reserve(packet.size() + 4); + misaligned.insert(misaligned.end(), {0xDE, 0xAD, 0xBE, 0xEF}); + misaligned.insert(misaligned.end(), packet.begin(), packet.end()); + + const auto buffer = std::span(misaligned.data() + 4, packet.size()); + const auto rawPayloadPtr = reinterpret_cast(buffer.data() + 16); + + PacketRouter router; + bool handled = false; + router.RegisterRequestHandler(0x1, [&](const ARPacketView& view) { + handled = true; + EXPECT_EQ(view.payload.size(), 8u); + EXPECT_EQ(0u, reinterpret_cast(view.payload.data()) & 0x7u); + EXPECT_NE(rawPayloadPtr, reinterpret_cast(view.payload.data())); + if (view.payload.size() == 8u) { + EXPECT_EQ((std::array{0x44, 0x33, 0x22, 0x11, 0x88, 0x77, 0x66, 0x55}), + (std::array{ + view.payload[0], view.payload[1], view.payload[2], view.payload[3], + view.payload[4], view.payload[5], view.payload[6], view.payload[7], + })); + } + return ResponseCode::Complete; + }); + + router.RoutePacket(ARContextType::Request, buffer); + EXPECT_TRUE(handled); +} + TEST(AsyncPacketSerDesLinuxCompat, ExtractTLabelUsesWireByteTwo) { - // Read quadlet response packet: tLabel=48, tCode=6, rCode=0 - // IEEE 1394 wire: Byte2=[tLabel:6][rt:2], Byte3=[tCode:4][rCode:4] + // Read quadlet response packet as OHCI AR DMA memory: tLabel=48, tCode=6, rCode=0. + // After the little-endian quadlet write, memory byte1 holds [tLabel:6][rt:2]. const std::array responseBytes{ - 0x60, 0x01, 0xC2, 0x60, // Fixed byte3: was 0xFF (invalid tCode=0xF) → 0x60 (tCode=6) + 0x60, 0xC2, 0x01, 0x60, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x04, 0x20, 0x8F, 0xE2, diff --git a/tests/BusManagerGapOptimizationTests.cpp b/tests/BusManagerGapOptimizationTests.cpp index 7c992a38..bae81784 100644 --- a/tests/BusManagerGapOptimizationTests.cpp +++ b/tests/BusManagerGapOptimizationTests.cpp @@ -4,6 +4,8 @@ #include #include "ASFWDriver/Bus/BusManager.hpp" +#include "ASFWDriver/Controller/ControllerConfig.hpp" +#include "ASFWDriver/Controller/BringupOverrides.hpp" using namespace ASFW::Driver; @@ -31,8 +33,69 @@ TopologySnapshot MakeTopology(const std::optional localNodeId, return topology; } +TopologyNode MakeNode(const uint8_t nodeId, const bool contender, const bool linkActive = true) { + TopologyNode node{}; + node.nodeId = nodeId; + node.isIRMCandidate = contender; + node.linkActive = linkActive; + return node; +} + } // namespace +TEST(BusManagerGapOptimizationTests, ControllerConfigDefaultsPreserveDelegatedMode) { + ControllerConfig config{}; + EXPECT_FALSE(config.allowCycleMasterEligibility); + EXPECT_FALSE(config.experimentalHostCycleMasterBringup); + + const ControllerConfig defaultConfig = ControllerConfig::MakeDefault(); + EXPECT_FALSE(defaultConfig.allowCycleMasterEligibility); + EXPECT_FALSE(defaultConfig.experimentalHostCycleMasterBringup); +} + +TEST(BusManagerGapOptimizationTests, DefaultBringupDelegatesRootToPeerContender) { + BusManager busManager; + + TopologySnapshot topology{}; + topology.localNodeId = 1U; + topology.rootNodeId = 1U; + topology.irmNodeId = 1U; + topology.nodes = { + MakeNode(0U, true), + MakeNode(1U, true), + }; + + const auto command = busManager.AssignCycleMaster(topology, {}); + ASSERT_TRUE(command.has_value()); + ASSERT_TRUE(command->forceRootNodeID.has_value()); + ASSERT_TRUE(command->setContender.has_value()); + EXPECT_EQ(*command->forceRootNodeID, 0U); + EXPECT_FALSE(*command->setContender); +} + +TEST(BusManagerGapOptimizationTests, ExperimentalHostCycleMasterBringupDisablesDelegation) { + ControllerConfig config{}; + config.experimentalHostCycleMasterBringup = true; + + BusManager busManager; + ApplyBringupOverrides(config, &busManager); + + EXPECT_TRUE(config.allowCycleMasterEligibility); + EXPECT_FALSE(busManager.GetConfig().delegateCycleMaster); + + TopologySnapshot topology{}; + topology.localNodeId = 1U; + topology.rootNodeId = 1U; + topology.irmNodeId = 1U; + topology.nodes = { + MakeNode(0U, true), + MakeNode(1U, true), + }; + + const auto command = busManager.AssignCycleMaster(topology, {}); + EXPECT_FALSE(command.has_value()); +} + TEST(BusManagerGapOptimizationTests, InconsistentObservedBaseGapsForceConservative63) { BusManager busManager; busManager.SetGapOptimizationEnabled(true); @@ -60,9 +123,42 @@ TEST(BusManagerGapOptimizationTests, ObservedZeroGapRetoolsToCurrentTargetGap) { EXPECT_EQ(decision->gapCount, 10U); } -TEST(BusManagerGapOptimizationTests, ObservedGapsMatchingPreviousGapNeedNoAction) { +TEST(BusManagerGapOptimizationTests, ObservedDefault63GapWithUnknownHistoryRetoolsToCurrentTargetGap) { + BusManager busManager; + busManager.SetGapOptimizationEnabled(true); + + const auto topology = MakeTopology(0U, 0U, 4U); + const auto decision = + busManager.EvaluateGapPolicy(topology, + {MakeBaseSelfID(0U, 63U), MakeBaseSelfID(1U, 63U)}); + + ASSERT_TRUE(decision.has_value()); + EXPECT_EQ(decision->reason, BusManager::GapDecisionReason::TargetGap); + EXPECT_EQ(decision->gapCount, 10U); +} + +TEST(BusManagerGapOptimizationTests, TwoNodeLocalRootSkipsTargetGapOptimization) { + BusManager busManager; + busManager.SetGapOptimizationEnabled(true); + + auto topology = MakeTopology(1U, 1U, 1U); + topology.rootNodeId = 1U; + topology.nodes = { + MakeNode(0U, true), + MakeNode(1U, true), + }; + + const auto decision = + busManager.EvaluateGapPolicy(topology, + {MakeBaseSelfID(0U, 63U), MakeBaseSelfID(1U, 63U)}); + + EXPECT_FALSE(decision.has_value()); +} + +TEST(BusManagerGapOptimizationTests, ObservedGapsMatchingConfirmedGapNeedNoAction) { BusManager busManager; busManager.SetGapOptimizationEnabled(true); + busManager.NoteStableGapObserved(63U); const auto topology = MakeTopology(0U, 0U, 4U); const auto decision = diff --git a/tests/BusResetCoordinatorTests.cpp b/tests/BusResetCoordinatorTests.cpp index f027066e..a1b6e43b 100644 --- a/tests/BusResetCoordinatorTests.cpp +++ b/tests/BusResetCoordinatorTests.cpp @@ -355,7 +355,7 @@ TEST(BusResetCoordinatorTests, StableResetPublishesTopologyExactlyOnce) { 7U, {MakeBaseSelfID(0U, 63U, true, true), MakeBaseSelfID(1U, 63U)}); rig.PrimeCapture(rawCapture, 7U); rig.TriggerStickyCompletion(); - rig.AdvanceMs(1U); + rig.AdvanceMs(100U); ASSERT_EQ(rig.publishedTopologies.size(), 1U); EXPECT_EQ(rig.publishedTopologies.front().generation, 7U); @@ -370,6 +370,25 @@ TEST(BusResetCoordinatorTests, StableResetPublishesTopologyExactlyOnce) { EXPECT_FALSE(rig.hardware.TestBusResetIssued()); } +TEST(BusResetCoordinatorTests, StableResetDelaysDiscoveryByAppleScanDelay) { + BusResetTestRig rig; + rig.Initialize(); + + rig.StartResetCycle(); + + const auto rawCapture = MakeRawSelfIDCapture( + 7U, {MakeBaseSelfID(0U, 63U, true, true), MakeBaseSelfID(1U, 63U)}); + rig.PrimeCapture(rawCapture, 7U); + rig.TriggerStickyCompletion(); + + rig.AdvanceMs(99U); + EXPECT_TRUE(rig.publishedTopologies.empty()); + + rig.AdvanceMs(1U); + ASSERT_EQ(rig.publishedTopologies.size(), 1U); + EXPECT_EQ(rig.publishedTopologies.front().generation, 7U); +} + TEST(BusResetCoordinatorTests, StickyCompletionOnlyStillCompletesDecodePath) { BusResetTestRig rig; rig.Initialize(); @@ -380,7 +399,7 @@ TEST(BusResetCoordinatorTests, StickyCompletionOnlyStillCompletesDecodePath) { 3U, {MakeBaseSelfID(0U, 63U, true, false), MakeBaseSelfID(1U, 63U)}); rig.PrimeCapture(rawCapture, 3U); rig.TriggerStickyCompletion(); - rig.AdvanceMs(1U); + rig.AdvanceMs(100U); ASSERT_EQ(rig.publishedTopologies.size(), 1U); EXPECT_EQ(rig.publishedTopologies.front().generation, 3U); @@ -435,7 +454,7 @@ TEST(BusResetCoordinatorTests, InvalidTopologyDoesNotReusePreviouslyPublishedSna MakeRawSelfIDCapture(12U, {MakeBaseSelfID(0U, 63U, true, true), MakeBaseSelfID(1U, 63U)}); rig.PrimeCapture(stableCapture, 12U); rig.TriggerStickyCompletion(); - rig.AdvanceMs(1U); + rig.AdvanceMs(100U); ASSERT_EQ(rig.publishedTopologies.size(), 1U); rig.ResetHardwareState(); @@ -473,6 +492,40 @@ TEST(BusResetCoordinatorTests, SelfIDTimeoutRequestsShortRecoveryResetAfterDeadl std::string::npos); } +TEST(BusResetCoordinatorTests, ManualResetWithoutIrqTriggersOneBoundedRecoveryReset) { + BusResetTestRig rig; + rig.Initialize(); + + rig.coordinator.RequestUserReset(true); + rig.DrainReady(); + + auto countBusResets = [&rig]() { + const auto operations = rig.hardware.CopyTestOperations(); + return static_cast(std::count(operations.begin(), operations.end(), + HardwareInterface::TestOperation::InitiateBusReset)); + }; + + EXPECT_EQ(countBusResets(), 1U); + auto diag = rig.coordinator.Diagnostics(); + EXPECT_EQ(diag.manualResetEpoch, 1U); + EXPECT_EQ(diag.resetEpoch, 0U); + EXPECT_EQ(diag.softwareResetIssuedCount, 1U); + + rig.AdvanceMs(499U); + EXPECT_EQ(countBusResets(), 1U); + + rig.AdvanceMs(1U); + EXPECT_EQ(countBusResets(), 2U); + diag = rig.coordinator.Diagnostics(); + EXPECT_EQ(diag.recoveryResetAttempts, 1U); + EXPECT_EQ(diag.softwareResetIssuedCount, 2U); + EXPECT_EQ(diag.lastRecoveryReasonCode, + BusResetCoordinator::RecoveryReasonCode::ManualResetWatchdog); + + rig.AdvanceMs(1000U); + EXPECT_EQ(countBusResets(), 2U); +} + TEST(BusResetCoordinatorTests, GapMismatchResetIsDeferredThenSentWithPhyConfig) { BusResetTestRig rig; rig.Initialize(true); @@ -676,7 +729,7 @@ TEST(BusResetCoordinatorTests, StableAcceptedGenerationCommitsGapAfterSuccessful MakeBaseSelfID(1U, 21U, true, false)}), 21U); rig.TriggerStickyCompletion(); - rig.AdvanceMs(1U); + rig.AdvanceMs(100U); EXPECT_FALSE(rig.hardware.TestPhyConfigIssued()); EXPECT_FALSE(rig.hardware.TestBusResetIssued()); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 46556694..c57b6361 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -253,6 +253,7 @@ gtest_discover_tests(SelfIDCaptureTests) add_executable(BusManagerGapOptimizationTests "${ASFW_TESTS_DIR}/BusManagerGapOptimizationTests.cpp" "${ASFW_DRIVER_DIR}/Bus/BusManager.cpp" + "${ASFW_DRIVER_DIR}/Controller/ControllerConfig.cpp" "${ASFW_TESTS_DIR}/LoggingStubs.cpp" ) @@ -339,6 +340,29 @@ target_include_directories(FireWireIOReturnTests PRIVATE ${ASFW_COMMON_INCLUDES} gtest_discover_tests(FireWireIOReturnTests) +# ============================================================================= +# Transaction Storage Tests - async user-client result retention +# ============================================================================= +add_executable(TransactionStorageTests + "${ASFW_TESTS_DIR}/TransactionStorageTests.cpp" + "${ASFW_DRIVER_DIR}/UserClient/Storage/TransactionStorage.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(TransactionStorageTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(TransactionStorageTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(TransactionStorageTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(TransactionStorageTests) + # ============================================================================= # DMA Memory Tests - FakeDMAMemory unit tests # ============================================================================= @@ -1228,6 +1252,101 @@ target_include_directories(SimITEngineTests PRIVATE ${ASFW_COMMON_INCLUDES}) gtest_discover_tests(SimITEngineTests) +# SBP-2 Address Space Manager Tests +add_executable(AddressSpaceManagerTests + "${ASFW_TESTS_DIR}/AddressSpaceManagerTests.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" +) + +target_link_libraries(AddressSpaceManagerTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(AddressSpaceManagerTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(AddressSpaceManagerTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(AddressSpaceManagerTests) + +# SBP-2 Login Session Tests +add_executable(SBP2LoginSessionTests + "${ASFW_TESTS_DIR}/SBP2LoginSessionTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2LoginSessionTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2LoginSessionTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2LoginSessionTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2LoginSessionTests) + +# SBP-2 ORB timer tests +add_executable(SBP2ORBTests + "${ASFW_TESTS_DIR}/SBP2ORBTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2ORBTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2ORBTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2ORBTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2ORBTests) + +# SBP-2 Session Registry / command tests +add_executable(SBP2SessionRegistryTests + "${ASFW_TESTS_DIR}/SBP2SessionRegistryTests.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2SessionRegistry.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2LoginSession.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" + "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" + "${ASFW_DRIVER_DIR}/Discovery/DeviceManager.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWDevice.cpp" + "${ASFW_DRIVER_DIR}/Discovery/FWUnit.cpp" + "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" + "${ASFW_TESTS_DIR}/LoggingStubs.cpp" +) + +target_link_libraries(SBP2SessionRegistryTests + PRIVATE + GTest::gtest_main +) + +target_compile_definitions(SBP2SessionRegistryTests + PRIVATE + ASFW_HOST_TEST +) + +target_include_directories(SBP2SessionRegistryTests PRIVATE ${ASFW_COMMON_INCLUDES}) + +gtest_discover_tests(SBP2SessionRegistryTests) + # Device protocol factory routing tests add_executable(DeviceProtocolFactoryTests "${ASFW_TESTS_DIR}/DeviceProtocolFactoryTests.cpp" @@ -1422,61 +1541,39 @@ target_include_directories(ROMScannerDetailsDiscoveryTests PRIVATE ${ASFW_COMMON gtest_discover_tests(ROMScannerDetailsDiscoveryTests) -add_executable(ROMScanNodeStateMachineTests - "${ASFW_TESTS_DIR}/ROMScanNodeStateMachineTests.cpp" +add_executable(DiscoveryConvergenceTests + "${ASFW_TESTS_DIR}/DiscoveryConvergenceTests.cpp" ) -target_link_libraries(ROMScanNodeStateMachineTests +target_link_libraries(DiscoveryConvergenceTests PRIVATE GTest::gtest_main ) -target_compile_definitions(ROMScanNodeStateMachineTests +target_compile_definitions(DiscoveryConvergenceTests PRIVATE ASFW_HOST_TEST ) -target_include_directories(ROMScanNodeStateMachineTests PRIVATE ${ASFW_COMMON_INCLUDES}) +target_include_directories(DiscoveryConvergenceTests PRIVATE ${ASFW_COMMON_INCLUDES}) -gtest_discover_tests(ROMScanNodeStateMachineTests) +gtest_discover_tests(DiscoveryConvergenceTests) -add_executable(AddressSpaceManagerTests - "${ASFW_TESTS_DIR}/AddressSpaceManagerTests.cpp" - "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" +add_executable(ROMScanNodeStateMachineTests + "${ASFW_TESTS_DIR}/ROMScanNodeStateMachineTests.cpp" ) -target_link_libraries(AddressSpaceManagerTests +target_link_libraries(ROMScanNodeStateMachineTests PRIVATE GTest::gtest_main ) -target_compile_definitions(AddressSpaceManagerTests +target_compile_definitions(ROMScanNodeStateMachineTests PRIVATE ASFW_HOST_TEST ) -target_include_directories(AddressSpaceManagerTests PRIVATE ${ASFW_COMMON_INCLUDES}) - -gtest_discover_tests(AddressSpaceManagerTests) - -add_executable(SBP2ORBTests - "${ASFW_TESTS_DIR}/SBP2ORBTests.cpp" - "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2CommandORB.cpp" - "${ASFW_DRIVER_DIR}/Protocols/SBP2/SBP2ManagementORB.cpp" - "${ASFW_TESTS_DIR}/HardwareInterfaceStub.cpp" - "${ASFW_TESTS_DIR}/LoggingStubs.cpp" -) - -target_link_libraries(SBP2ORBTests - PRIVATE - GTest::gtest_main -) - -target_compile_definitions(SBP2ORBTests - PRIVATE - ASFW_HOST_TEST -) +target_include_directories(ROMScanNodeStateMachineTests PRIVATE ${ASFW_COMMON_INCLUDES}) -target_include_directories(SBP2ORBTests PRIVATE ${ASFW_COMMON_INCLUDES}) +gtest_discover_tests(ROMScanNodeStateMachineTests) -gtest_discover_tests(SBP2ORBTests) diff --git a/tests/ConfigROMStoreConcurrencyTests.cpp b/tests/ConfigROMStoreConcurrencyTests.cpp index 24380130..d95c170d 100644 --- a/tests/ConfigROMStoreConcurrencyTests.cpp +++ b/tests/ConfigROMStoreConcurrencyTests.cpp @@ -20,6 +20,18 @@ ASFW::Discovery::ConfigROM MakeROM(ASFW::Discovery::Generation gen, return rom; } +ASFW::Discovery::ConfigROM MakeSBP2ROM(ASFW::Discovery::Generation gen, + uint8_t nodeId, + ASFW::Discovery::Guid64 guid) { + auto rom = MakeROM(gen, nodeId, guid); + ASFW::Discovery::UnitDirectory unit{}; + unit.unitSpecId = 0x00609E; + unit.unitSwVersion = 0x010483; + rom.unitDirectories.push_back(unit); + rom.rawQuadlets.resize(34, 0); + return rom; +} + } // namespace TEST(ConfigROMStoreConcurrencyTests, ConcurrentInsertAndLookupDoesNotCrash) { @@ -56,3 +68,38 @@ TEST(ConfigROMStoreConcurrencyTests, ConcurrentInsertAndLookupDoesNotCrash) { thread.join(); } } + +TEST(ConfigROMStoreConcurrencyTests, LatestLookupPrefersPreviousProfileOverNewerPartialROM) { + ASFW::Discovery::ConfigROMStore store; + constexpr ASFW::Discovery::Guid64 kGuid = 0x0090b54001ffffffULL; + + store.Insert(MakeSBP2ROM(ASFW::Discovery::Generation{2}, 0, kGuid)); + store.Insert(MakeROM(ASFW::Discovery::Generation{3}, 0, kGuid)); + + const auto* latest = store.FindLatestForNode(0); + ASSERT_NE(latest, nullptr); + EXPECT_EQ(latest->gen.value, 2u); + EXPECT_EQ(latest->unitDirectories.size(), 1u); + + const auto* byGuid = store.FindByGuid(kGuid); + ASSERT_NE(byGuid, nullptr); + EXPECT_EQ(byGuid->gen.value, 2u); + EXPECT_EQ(byGuid->unitDirectories.size(), 1u); +} + +TEST(ConfigROMStoreConcurrencyTests, LatestLookupUsesNewerProfileWhenItCompletes) { + ASFW::Discovery::ConfigROMStore store; + constexpr ASFW::Discovery::Guid64 kGuid = 0x0090b54001ffffffULL; + + store.Insert(MakeSBP2ROM(ASFW::Discovery::Generation{2}, 0, kGuid)); + store.Insert(MakeROM(ASFW::Discovery::Generation{3}, 0, kGuid)); + store.Insert(MakeSBP2ROM(ASFW::Discovery::Generation{4}, 0, kGuid)); + + const auto* latest = store.FindLatestForNode(0); + ASSERT_NE(latest, nullptr); + EXPECT_EQ(latest->gen.value, 4u); + + const auto* byGuid = store.FindByGuid(kGuid); + ASSERT_NE(byGuid, nullptr); + EXPECT_EQ(byGuid->gen.value, 4u); +} diff --git a/tests/DiscoveryConvergenceTests.cpp b/tests/DiscoveryConvergenceTests.cpp new file mode 100644 index 00000000..431bfa36 --- /dev/null +++ b/tests/DiscoveryConvergenceTests.cpp @@ -0,0 +1,54 @@ +#include "Discovery/DiscoveryConvergence.hpp" + +#include + +namespace { + +ASFW::Driver::TopologySnapshot MakeTopologyWithRemoteLinkActiveNode() { + ASFW::Driver::TopologySnapshot snapshot{}; + snapshot.generation = 42; + snapshot.localNodeId = 1; + snapshot.nodeCount = 2; + snapshot.nodes = { + ASFW::Driver::TopologyNode{.nodeId = 0, .linkActive = true}, + ASFW::Driver::TopologyNode{.nodeId = 1, .linkActive = true}, + }; + return snapshot; +} + +TEST(DiscoveryConvergenceTests, ZeroRomScanIsInconclusiveWhenTopologyStillHasRemoteNode) { + const auto snapshot = MakeTopologyWithRemoteLinkActiveNode(); + + EXPECT_TRUE(ASFW::Discovery::IsZeroRomScanInconclusive( + ASFW::Discovery::Generation{42}, /*romCount=*/0, snapshot)); +} + +TEST(DiscoveryConvergenceTests, NonEmptyRomScanIsConclusive) { + const auto snapshot = MakeTopologyWithRemoteLinkActiveNode(); + + EXPECT_FALSE(ASFW::Discovery::IsZeroRomScanInconclusive( + ASFW::Discovery::Generation{42}, /*romCount=*/1, snapshot)); +} + +TEST(DiscoveryConvergenceTests, ZeroRomScanIsConclusiveWhenTopologyHasNoRemoteNode) { + ASFW::Driver::TopologySnapshot snapshot{}; + snapshot.generation = 42; + snapshot.localNodeId = 1; + snapshot.nodeCount = 1; + snapshot.nodes = { + ASFW::Driver::TopologyNode{.nodeId = 1, .linkActive = true}, + }; + + EXPECT_FALSE(ASFW::Discovery::IsZeroRomScanInconclusive( + ASFW::Discovery::Generation{42}, /*romCount=*/0, snapshot)); +} + +TEST(DiscoveryConvergenceTests, StaleTopologyGenerationIsConclusive) { + auto snapshot = MakeTopologyWithRemoteLinkActiveNode(); + snapshot.generation = 41; + + EXPECT_FALSE(ASFW::Discovery::IsZeroRomScanInconclusive( + ASFW::Discovery::Generation{42}, /*romCount=*/0, snapshot)); +} + +} // namespace diff --git a/tests/FCPPacketParsingTests.cpp b/tests/FCPPacketParsingTests.cpp index c99e34cc..5deb7580 100644 --- a/tests/FCPPacketParsingTests.cpp +++ b/tests/FCPPacketParsingTests.cpp @@ -95,6 +95,23 @@ TEST_F(FCPPacketParsingTest, RealPacket_FCPResponse_SubunitInfo) { << "ASFW and Linux implementations should produce identical results"; } +TEST_F(FCPPacketParsingTest, RealPacket_FCPResponse_DataLengthUsesOHCILittleEndianOrder) { + // Same real FCP packet as above. Q3 bytes in AR DMA memory are: + // 00 00 08 00 + // which represents data_length=8, extended_tcode=0. + const uint8_t realPacket[] = { + 0x10, 0x7D, 0xC0, 0xFF, + 0xFF, 0xFF, 0xC2, 0xFF, + 0x00, 0x0D, 0x00, 0xF0, + 0x00, 0x00, 0x08, 0x00, + }; + + std::span header(realPacket, 16); + EXPECT_EQ(8u, ExtractDataLength(header)) + << "OHCI AR DMA stores Q3 little-endian in memory, so block data_length " + "must decode to 8 bytes for the real FCP packet"; +} + TEST_F(FCPPacketParsingTest, RealPacket_FCPResponse_Retry1) { // Second FCP response from logs (timestamp 13:34:48.266683+0100) // Same SUBUNIT_INFO response, different tLabel diff --git a/tests/ROMScanNodeStateMachineTests.cpp b/tests/ROMScanNodeStateMachineTests.cpp index a4aab551..cfc8d23b 100644 --- a/tests/ROMScanNodeStateMachineTests.cpp +++ b/tests/ROMScanNodeStateMachineTests.cpp @@ -29,10 +29,22 @@ TEST(ROMScanNodeStateMachineTests, RejectsInvalidTransition) { EXPECT_EQ(node.CurrentState(), ROMScanNodeStateMachine::State::Idle); } +TEST(ROMScanNodeStateMachineTests, AcceptsSlowPublishRetryTransitions) { + ROMScanNodeStateMachine node(6, Generation{12}, FwSpeed::S100, 2); + + EXPECT_TRUE(node.TransitionTo(ROMScanNodeStateMachine::State::ReadingBIB)); + EXPECT_TRUE(node.TransitionTo(ROMScanNodeStateMachine::State::WaitingRepublish)); + EXPECT_TRUE(node.TransitionTo(ROMScanNodeStateMachine::State::ReadingRootDir)); + EXPECT_TRUE(node.TransitionTo(ROMScanNodeStateMachine::State::WaitingRepublish)); + EXPECT_TRUE(node.TransitionTo(ROMScanNodeStateMachine::State::Idle)); + EXPECT_EQ(node.CurrentState(), ROMScanNodeStateMachine::State::Idle); +} + TEST(ROMScanNodeStateMachineTests, ResetForGenerationReinitializesNodeData) { ROMScanNodeStateMachine node(6, Generation{12}, FwSpeed::S100, 2); node.MutableROM().vendorName = "X"; node.SetBIBInProgress(true); + node.SetSlowPublishRetriesLeft(3); node.ForceState(ROMScanNodeStateMachine::State::Failed); node.ResetForGeneration(Generation{20}, 7, FwSpeed::S200, 4); @@ -45,4 +57,5 @@ TEST(ROMScanNodeStateMachineTests, ResetForGenerationReinitializesNodeData) { EXPECT_EQ(node.ROM().nodeId, 7); EXPECT_TRUE(node.ROM().vendorName.empty()); EXPECT_FALSE(node.BIBInProgress()); + EXPECT_EQ(node.SlowPublishRetriesLeft(), 0); } diff --git a/tests/ROMScannerCompletionTests.cpp b/tests/ROMScannerCompletionTests.cpp index b1e748ca..b476f43e 100644 --- a/tests/ROMScannerCompletionTests.cpp +++ b/tests/ROMScannerCompletionTests.cpp @@ -178,10 +178,11 @@ class MockAsyncSubsystem : public ASFW::Async::IFireWireBus { // Helper to create minimal BIB (Bus Info Block) for testing // Q0: info_length=1 (minimal ROM), crc_length=1, crc=valid // Format: [31:24]=info_length, [23:16]=crc_length, [15:0]=crc -std::vector CreateMinimalBIB() { +std::vector CreateMinimalBIB(uint64_t guid = 0) { // Minimal BIB: header quadlet + 4 quadlets of zeros // info_length=4 (standard BIB), crc_length=4 (minimal total ROM), crc=0x0000 - return {0x04040000, 0, 0, 0, 0}; + return {0x04040000, 0, 0, static_cast(guid >> 32), + static_cast(guid & 0xFFFFFFFF)}; } // Helper to create full BIB with GUID @@ -263,6 +264,135 @@ TEST(ROMScannerCompletion, ManualRead_MinimalROM_InvokesCallbackImmediately) { EXPECT_EQ(completedROMs[0].gen.value, 42u); } +TEST(ROMScannerCompletion, ManualRead_NikonMinimalROM_ProbesRootDirBeforeCompletion) { + MockAsyncSubsystem mockAsync; + SpeedPolicy speedPolicy; + + bool callbackInvoked = false; + bool hadBusyNodes = false; + std::vector completedROMs; + std::mutex mtx; + std::condition_variable cv; + + constexpr uint64_t kNikonGuid = 0x0090B54001FFFFFFULL; + + ROMScannerParams params{}; + params.doIRMCheck = false; + ROMScanner scanner(mockAsync, speedPolicy, params); + + TopologySnapshot topology; + topology.generation = 43; + topology.busBase16 = 0xFFC0; + topology.nodes.push_back({.nodeId = 1, .linkActive = true}); + + ROMScanRequest request{}; + request.gen = Generation{topology.generation}; + request.topology = topology; + request.localNodeId = 0; + request.targetNodes = {1}; + + ScanCompletionCallback onComplete = [&](Generation /*gen*/, std::vector roms, + bool busy) { + std::lock_guard lock(mtx); + callbackInvoked = true; + hadBusyNodes = busy; + completedROMs = std::move(roms); + cv.notify_one(); + }; + + ASSERT_TRUE(scanner.Start(request, onComplete)); + mockAsync.WaitForPendingReads(1); + + mockAsync.SimulateFullBIBSuccess(CreateMinimalBIB(kNikonGuid)); + + EXPECT_FALSE(callbackInvoked) << "Nikon minimal ROM should not complete immediately"; + + mockAsync.WaitForPendingReads(5); + EXPECT_EQ(mockAsync.GetPendingReadCount(), 5u) + << "Compatibility path should schedule a root directory probe"; + EXPECT_EQ(mockAsync.pendingReads_[4].address.addressLo, + ASFW::FW::ConfigROMAddr::kAddressLo + 20u); + + const std::vector rootDir = { + 0x00020000, + 0x03000001, + 0x17000002, + }; + mockAsync.SimulateSequentialReads(4, rootDir); + + { + std::unique_lock lock(mtx); + cv.wait_for(lock, std::chrono::seconds(1), [&callbackInvoked] { return callbackInvoked; }); + } + + EXPECT_TRUE(callbackInvoked); + EXPECT_TRUE(hadBusyNodes); + ASSERT_EQ(completedROMs.size(), 1u); + EXPECT_EQ(completedROMs[0].bib.guid, kNikonGuid); + EXPECT_FALSE(completedROMs[0].rootDirMinimal.empty()); +} + +TEST(ROMScannerCompletion, ManualRead_NikonMinimalROM_RootDirProbeTimeoutsThenCompletesMinimal) { + MockAsyncSubsystem mockAsync; + SpeedPolicy speedPolicy; + + bool callbackInvoked = false; + bool hadBusyNodes = false; + std::vector completedROMs; + std::mutex mtx; + std::condition_variable cv; + + constexpr uint64_t kNikonGuid = 0x0090B54001FFFFFFULL; + + ROMScannerParams params{}; + params.doIRMCheck = false; + ROMScanner scanner(mockAsync, speedPolicy, params); + + TopologySnapshot topology; + topology.generation = 44; + topology.busBase16 = 0xFFC0; + topology.nodes.push_back({.nodeId = 1, .linkActive = true}); + + ROMScanRequest request{}; + request.gen = Generation{topology.generation}; + request.topology = topology; + request.localNodeId = 0; + request.targetNodes = {1}; + + ScanCompletionCallback onComplete = [&](Generation /*gen*/, std::vector roms, + bool busy) { + std::lock_guard lock(mtx); + callbackInvoked = true; + hadBusyNodes = busy; + completedROMs = std::move(roms); + cv.notify_one(); + }; + + ASSERT_TRUE(scanner.Start(request, onComplete)); + mockAsync.WaitForPendingReads(1); + + mockAsync.SimulateFullBIBSuccess(CreateMinimalBIB(kNikonGuid)); + EXPECT_FALSE(callbackInvoked); + + for (size_t attempt = 0; attempt < 4; ++attempt) { + mockAsync.WaitForPendingReads(5 + attempt); + EXPECT_EQ(mockAsync.pendingReads_[4 + attempt].address.addressLo, + ASFW::FW::ConfigROMAddr::kAddressLo + 20u); + mockAsync.SimulateReadTimeout(4 + attempt); + } + + { + std::unique_lock lock(mtx); + cv.wait_for(lock, std::chrono::seconds(1), [&callbackInvoked] { return callbackInvoked; }); + } + + EXPECT_TRUE(callbackInvoked); + EXPECT_TRUE(hadBusyNodes); + ASSERT_EQ(completedROMs.size(), 1u); + EXPECT_EQ(completedROMs[0].bib.guid, kNikonGuid); + EXPECT_TRUE(completedROMs[0].rootDirMinimal.empty()); +} + TEST(ROMScannerCompletion, ManualRead_FullROM_InvokesCallbackAfterBothReads) { // Test full ROM read (BIB + root directory) MockAsyncSubsystem mockAsync; diff --git a/tests/ResponseSenderHeaderFormatTests.cpp b/tests/ResponseSenderHeaderFormatTests.cpp index a0f21708..37fee627 100644 --- a/tests/ResponseSenderHeaderFormatTests.cpp +++ b/tests/ResponseSenderHeaderFormatTests.cpp @@ -37,6 +37,8 @@ constexpr uint32_t OHCI_AT_Q1_RCODE_SHIFT = 12; // Transaction codes constexpr uint8_t TCODE_WRITE_RESPONSE = 0x2; +constexpr uint8_t TCODE_READ_QUADLET_RESPONSE = 0x6; +constexpr uint8_t TCODE_READ_BLOCK_RESPONSE = 0x7; // Speed codes constexpr uint8_t SPEED_S400 = 0x02; @@ -84,6 +86,54 @@ void BuildWriteResponseHeader_OHCIFormat( header[2] = 0; } +void BuildReadQuadletResponseHeader_OHCIFormat( + uint16_t destID, + uint8_t tLabel, + uint8_t rcode, + uint32_t quadletData, + uint32_t header[4]) +{ + constexpr uint8_t kSrcBusID = 0; + constexpr uint8_t kSpeed = SPEED_S400; + constexpr uint8_t kRetry = RETRY_X; + constexpr uint8_t kPriority = 0; + + header[0] = (static_cast(kSrcBusID & 0x01) << OHCI_AT_Q0_SRCBUSID_SHIFT) | + (static_cast(kSpeed & 0x07) << OHCI_AT_Q0_SPEED_SHIFT) | + (static_cast(tLabel & 0x3F) << OHCI_AT_Q0_TLABEL_SHIFT) | + (static_cast(kRetry & 0x03) << OHCI_AT_Q0_RETRY_SHIFT) | + (static_cast(TCODE_READ_QUADLET_RESPONSE & 0x0F) << OHCI_AT_Q0_TCODE_SHIFT) | + (static_cast(kPriority) & OHCI_AT_Q0_PRIORITY_MASK); + header[1] = (static_cast(destID) << OHCI_AT_Q1_DESTID_SHIFT) | + (static_cast(rcode & 0x0F) << OHCI_AT_Q1_RCODE_SHIFT); + header[2] = 0; + header[3] = quadletData; +} + +void BuildReadBlockResponseHeader_OHCIFormat( + uint16_t destID, + uint8_t tLabel, + uint8_t rcode, + uint16_t dataLength, + uint32_t header[4]) +{ + constexpr uint8_t kSrcBusID = 0; + constexpr uint8_t kSpeed = SPEED_S400; + constexpr uint8_t kRetry = RETRY_X; + constexpr uint8_t kPriority = 0; + + header[0] = (static_cast(kSrcBusID & 0x01) << OHCI_AT_Q0_SRCBUSID_SHIFT) | + (static_cast(kSpeed & 0x07) << OHCI_AT_Q0_SPEED_SHIFT) | + (static_cast(tLabel & 0x3F) << OHCI_AT_Q0_TLABEL_SHIFT) | + (static_cast(kRetry & 0x03) << OHCI_AT_Q0_RETRY_SHIFT) | + (static_cast(TCODE_READ_BLOCK_RESPONSE & 0x0F) << OHCI_AT_Q0_TCODE_SHIFT) | + (static_cast(kPriority) & OHCI_AT_Q0_PRIORITY_MASK); + header[1] = (static_cast(destID) << OHCI_AT_Q1_DESTID_SHIFT) | + (static_cast(rcode & 0x0F) << OHCI_AT_Q1_RCODE_SHIFT); + header[2] = 0; + header[3] = static_cast(dataLength) << 16; +} + /** * @brief Build a Write Response header in WRONG IEEE 1394 wire format. * @@ -265,3 +315,38 @@ TEST_F(ResponseSenderHeaderFormatTest, Regression_TCode_AtCorrectPosition) { EXPECT_EQ(TCODE_WRITE_RESPONSE, tCode) << "tCode should be WRITE_RESPONSE (0x2) at Q0 bits[7:4]"; } + +TEST_F(ResponseSenderHeaderFormatTest, ReadQuadletResponse_HasExpectedTCodeAndData) { + uint32_t header[4]{}; + constexpr uint32_t kQuadletData = 0xA1B2C3D4; + BuildReadQuadletResponseHeader_OHCIFormat( + kRemoteNodeID, + kTLabel, + kRCodeComplete, + kQuadletData, + header); + + const uint8_t tCode = (header[0] >> 4) & 0x0F; + const uint8_t rCode = (header[1] >> 12) & 0x0F; + EXPECT_EQ(TCODE_READ_QUADLET_RESPONSE, tCode); + EXPECT_EQ(kRCodeComplete, rCode); + EXPECT_EQ(kQuadletData, header[3]); +} + +TEST_F(ResponseSenderHeaderFormatTest, ReadBlockResponse_HasExpectedTCodeAndLength) { + uint32_t header[4]{}; + constexpr uint16_t kDataLength = 32; + BuildReadBlockResponseHeader_OHCIFormat( + kRemoteNodeID, + kTLabel, + kRCodeComplete, + kDataLength, + header); + + const uint8_t tCode = (header[0] >> 4) & 0x0F; + const uint8_t rCode = (header[1] >> 12) & 0x0F; + const uint16_t dataLength = static_cast((header[3] >> 16) & 0xFFFF); + EXPECT_EQ(TCODE_READ_BLOCK_RESPONSE, tCode); + EXPECT_EQ(kRCodeComplete, rCode); + EXPECT_EQ(kDataLength, dataLength); +} diff --git a/tests/SBP2LoginSessionTests.cpp b/tests/SBP2LoginSessionTests.cpp new file mode 100644 index 00000000..903bd755 --- /dev/null +++ b/tests/SBP2LoginSessionTests.cpp @@ -0,0 +1,376 @@ +#include + +#include "ASFWDriver/Protocols/SBP2/SBP2LoginSession.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" + +#include +#include +#include +#include + +namespace { + +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::LoginState; +using ASFW::Protocols::SBP2::SBP2CommandORB; +using ASFW::Protocols::SBP2::SBP2LoginSession; +using ASFW::Protocols::SBP2::SBP2TargetInfo; +using ASFW::Protocols::SBP2::Wire::FromBE16; +using ASFW::Protocols::SBP2::Wire::FromBE32; +using ASFW::Protocols::SBP2::Wire::LoginORB; +using ASFW::Protocols::SBP2::Wire::LoginResponse; +using ASFW::Protocols::SBP2::Wire::NormalizeBusNodeID; +using ASFW::Protocols::SBP2::Wire::StatusBlock; +using ASFW::Protocols::SBP2::Wire::ToBE16; +using ASFW::Protocols::SBP2::Wire::ToBE32; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; + +uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | lo; +} + +uint64_t DecodeOrbAddressFromPayload(std::span payload) { + const uint16_t addressHi = + static_cast((static_cast(payload[2]) << 8) | payload[3]); + const uint32_t addressLo = + (static_cast(payload[4]) << 24) | + (static_cast(payload[5]) << 16) | + (static_cast(payload[6]) << 8) | + static_cast(payload[7]); + return ComposeAddress(addressHi, addressLo); +} + +uint32_t ReadQuadlet(AddressSpaceManager& manager, uint64_t address) { + uint32_t value = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, manager.ReadQuadlet(address, &value)); + return value; +} + +uint64_t ReadORBAddress(AddressSpaceManager& manager, + uint64_t orbAddress, + size_t hiOffset, + size_t loOffset) { + const uint32_t hi = FromBE32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = FromBE32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +class SessionRig { +public: + SessionRig() + : session(bus, bus, addressManager) { + workQueue.SetManualDispatchForTesting(true); + timeoutQueue.SetManualDispatchForTesting(true); + ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); + + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + + SBP2TargetInfo info{}; + info.managementAgentOffset = 0x100; + info.lun = 3; + info.managementTimeoutMs = 10; + info.maxORBSize = 32; + info.maxCommandBlockSize = 12; + + session.SetWorkQueue(&workQueue); + session.SetTimeoutQueue(&timeoutQueue); + session.Configure(info); + } + + ~SessionRig() { + ASFW::Testing::ResetHostMonotonicClockForTesting(); + } + + void DrainReady() { + while (workQueue.DrainReadyForTesting() > 0U || + timeoutQueue.DrainReadyForTesting() > 0U) { + } + } + + void AdvanceMs(uint64_t milliseconds) { + nowNs += milliseconds * 1'000'000ULL; + DrainReady(); + } + + void LoginSuccessfully(uint16_t loginId = 0x0042, uint32_t commandBlockAgentLo = 0x0020'0000) { + ASSERT_TRUE(session.Login()); + ASSERT_EQ(1u, bus.PendingWriteCount()); + + const auto& loginWrite = bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + const uint64_t loginResponseAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, loginResponseAddressHi), + offsetof(LoginORB, loginResponseAddressLo)); + const uint64_t statusAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, statusFIFOAddressHi), + offsetof(LoginORB, statusFIFOAddressLo)); + sessionStatusAddress = statusAddress; + + LoginResponse response{}; + response.length = ToBE16(LoginResponse::kSize); + response.loginID = ToBE16(loginId); + response.commandBlockAgentAddressHi = ToBE32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = ToBE32(commandBlockAgentLo); + response.reconnectHold = ToBE16(1); + + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + addressManager.ApplyRemoteWrite( + loginResponseAddress, + std::span{reinterpret_cast(&response), sizeof(response)}); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + addressManager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + while (bus.PendingWriteCount() > 0U) { + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + } + DrainReady(); + ASSERT_EQ(LoginState::LoggedIn, session.State()); + } + + ASFW::Async::Testing::DeferredFireWireBus bus; + AddressSpaceManager addressManager{nullptr}; + SBP2LoginSession session; + IODispatchQueue workQueue; + IODispatchQueue timeoutQueue; + uint64_t nowNs{0}; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SBP2LoginSessionTests, LoginAckCancelsStaleTimeoutBeforeStatusArrives) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(LoginState::LoggingIn, rig.session.State()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + rig.AdvanceMs(5); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + rig.AdvanceMs(5); + + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(1u, rig.bus.WriteCount()); +} + +TEST(SBP2LoginSessionTests, LoginORBMatchesAppleWireLayout) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + const auto& loginWrite = rig.bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + + const uint32_t quadlet4 = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, options))); + const uint32_t quadlet5 = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, passwordLength))); + + EXPECT_EQ(0x9000u, static_cast(quadlet4 >> 16)); + EXPECT_EQ(3u, static_cast(quadlet4 & 0xFFFFu)); + EXPECT_EQ(0u, static_cast(quadlet5 >> 16)); + EXPECT_EQ(LoginResponse::kSize, static_cast(quadlet5 & 0xFFFFu)); +} + +TEST(SBP2LoginSessionTests, LoginORBUsesFullBusNodeIdInEmbeddedAddresses) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + + const auto& loginWrite = rig.bus.WriteAt(0); + ASSERT_EQ(8u, loginWrite.data.size()); + + const uint16_t payloadNode = + static_cast((static_cast(loginWrite.data[0]) << 8) | + loginWrite.data[1]); + const uint64_t loginOrbAddress = DecodeOrbAddressFromPayload(loginWrite.data); + const uint32_t responseHi = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, loginResponseAddressHi))); + const uint32_t statusHi = FromBE32( + ReadQuadlet(rig.addressManager, loginOrbAddress + offsetof(LoginORB, statusFIFOAddressHi))); + + const uint16_t expectedNode = NormalizeBusNodeID(0x2A); + EXPECT_EQ(expectedNode, payloadNode); + EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, responseHi); + EXPECT_EQ((static_cast(expectedNode) << 16) | 0xFFFFu, statusHi); +} + +TEST(SBP2LoginSessionTests, BusResetWhileLoggingInRetriesLoginAfterDelay) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + ASSERT_EQ(LoginState::LoggingIn, rig.session.State()); + ASSERT_EQ(1u, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + EXPECT_EQ(LoginState::Idle, rig.session.State()); + + rig.AdvanceMs(99); + EXPECT_EQ(1u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::LoggingIn, rig.session.State()); + EXPECT_EQ(2u, rig.bus.WriteCount()); + EXPECT_EQ(2u, rig.session.Generation()); +} + +TEST(SBP2LoginSessionTests, LoginRetryDelayUsesTimeoutQueueInsteadOfWorkQueue) { + SessionRig rig; + + ASSERT_TRUE(rig.session.Login()); + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + + EXPECT_EQ(0u, rig.workQueue.PendingTaskCountForTesting()); + EXPECT_GT(rig.timeoutQueue.PendingTaskCountForTesting(), 0u); + + int workExecuted = 0; + rig.workQueue.DispatchAsync([&workExecuted]() { ++workExecuted; }); + rig.DrainReady(); + + EXPECT_EQ(1, workExecuted); +} + +TEST(SBP2LoginSessionTests, BusResetWhileReconnectingRetriesReconnectAfterDelay) { + SessionRig rig; + rig.LoginSuccessfully(); + + rig.bus.SetGeneration(ASFW::FW::Generation{2}); + rig.session.HandleBusReset(2); + ASSERT_EQ(LoginState::Suspended, rig.session.State()); + ASSERT_TRUE(rig.session.Reconnect()); + ASSERT_EQ(LoginState::Reconnecting, rig.session.State()); + ASSERT_EQ(2u, rig.session.Generation()); + ASSERT_EQ(3u, rig.bus.WriteCount()); + + rig.bus.SetGeneration(ASFW::FW::Generation{3}); + rig.session.HandleBusReset(3); + EXPECT_EQ(LoginState::Suspended, rig.session.State()); + + rig.AdvanceMs(99); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + EXPECT_EQ(LoginState::Reconnecting, rig.session.State()); + EXPECT_EQ(4u, rig.bus.WriteCount()); + EXPECT_EQ(3u, rig.session.Generation()); +} + +TEST(SBP2LoginSessionTests, ImmediateORBRetryStaysBoundToOriginalORBAndQueuesNextImmediate) { + SessionRig rig; + rig.LoginSuccessfully(); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(SBP2CommandORB::kImmediate); + first.SetTimeout(50); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(SBP2CommandORB::kImmediate); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_EQ(1u, rig.bus.PendingWriteCount()); + const size_t firstFetchWriteIndex = rig.bus.WriteCount() - 1; + const uint32_t firstAddressLo = static_cast( + DecodeOrbAddressFromPayload(rig.bus.WriteAt(firstFetchWriteIndex).data)); + + ASSERT_TRUE(rig.session.SubmitORB(&second)); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kTimeout)); + rig.DrainReady(); + + rig.AdvanceMs(999); + EXPECT_EQ(3u, rig.bus.WriteCount()); + + rig.AdvanceMs(1); + ASSERT_EQ(4u, rig.bus.WriteCount()); + const uint32_t retryAddressLo = static_cast( + DecodeOrbAddressFromPayload(rig.bus.WriteAt(3).data)); + EXPECT_EQ(firstAddressLo, retryAddressLo); +} + +TEST(SBP2LoginSessionTests, SolicitedStatusCompletesORBMatchingByORBAddress) { + SessionRig rig; + rig.LoginSuccessfully(); + + ASSERT_NE(0u, rig.session.CommandBlockAgent().addressLo); + + SBP2CommandORB first(rig.addressManager, &rig.session, 16); + first.SetFlags(0); + int firstStatus = 99; + first.SetCompletionCallback([&firstStatus](int status, uint8_t) { firstStatus = status; }); + + SBP2CommandORB second(rig.addressManager, &rig.session, 16); + second.SetFlags(0); + int secondStatus = 99; + second.SetCompletionCallback([&secondStatus](int status, uint8_t) { secondStatus = status; }); + + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + ASSERT_TRUE(rig.session.SubmitORB(&second)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + const auto firstAddress = first.GetORBAddress(); + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(firstAddress.addressHi); + status.orbOffsetLo = ToBE32(firstAddress.addressLo); + + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + EXPECT_EQ(0, firstStatus); + EXPECT_EQ(99, secondStatus); +} + +TEST(SBP2LoginSessionTests, ChainedORBLinkFailureReturnsFalseWithoutDoorbellWrite) { + SessionRig rig; + rig.LoginSuccessfully(); + + auto* firstOwner = reinterpret_cast(0x101); + auto* secondOwner = reinterpret_cast(0x102); + + SBP2CommandORB first(rig.addressManager, firstOwner, 16); + first.SetFlags(0); + ASSERT_TRUE(rig.session.SubmitORB(&first)); + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + rig.addressManager.ReleaseOwner(firstOwner); + + SBP2CommandORB second(rig.addressManager, secondOwner, 16); + second.SetFlags(0); + + int secondTransportStatus = 99; + second.SetCompletionCallback([&secondTransportStatus](int status, uint8_t) { + secondTransportStatus = status; + }); + + const size_t writeCountBeforeSubmit = rig.bus.WriteCount(); + EXPECT_FALSE(rig.session.SubmitORB(&second)); + EXPECT_EQ(writeCountBeforeSubmit, rig.bus.WriteCount()); + EXPECT_EQ(-1, secondTransportStatus); + EXPECT_FALSE(second.IsAppended()); +} + +} // namespace diff --git a/tests/SBP2ORBTests.cpp b/tests/SBP2ORBTests.cpp index 689494bd..524aabae 100644 --- a/tests/SBP2ORBTests.cpp +++ b/tests/SBP2ORBTests.cpp @@ -2,7 +2,6 @@ #include "ASFWDriver/Protocols/SBP2/SBP2CommandORB.hpp" #include "ASFWDriver/Protocols/SBP2/SBP2ManagementORB.hpp" -#include "ASFWDriver/Protocols/SBP2/SBP2PageTable.hpp" #include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" #include "ASFWDriver/Testing/HostDriverKitStubs.hpp" #include "tests/mocks/DeferredFireWireBus.hpp" @@ -17,8 +16,6 @@ namespace { using ASFW::Protocols::SBP2::AddressSpaceManager; using ASFW::Protocols::SBP2::SBP2CommandORB; using ASFW::Protocols::SBP2::SBP2ManagementORB; -using ASFW::Protocols::SBP2::SBP2PageTable; -using ASFW::Protocols::SBP2::Wire::FromBE16; using ASFW::Protocols::SBP2::Wire::FromBE32; using ASFW::Protocols::SBP2::Wire::ManagementAgentAddressLo; using ASFW::Protocols::SBP2::Wire::NormalizeBusNodeID; @@ -61,7 +58,8 @@ uint64_t ReadStatusAddressFromManagementORB(AddressSpaceManager& manager, uint64 class ORBTimerRig { public: ORBTimerRig() { - queue.SetManualDispatchForTesting(true); + workQueue.SetManualDispatchForTesting(true); + timeoutQueue.SetManualDispatchForTesting(true); ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); bus.SetGeneration(ASFW::FW::Generation{1}); @@ -74,7 +72,8 @@ class ORBTimerRig { } void DrainReady() { - while (queue.DrainReadyForTesting() > 0U) { + while (workQueue.DrainReadyForTesting() > 0U || + timeoutQueue.DrainReadyForTesting() > 0U) { } } @@ -85,7 +84,8 @@ class ORBTimerRig { ASFW::Async::Testing::DeferredFireWireBus bus; AddressSpaceManager addressManager{nullptr}; - IODispatchQueue queue; + IODispatchQueue workQueue; + IODispatchQueue timeoutQueue; uint64_t nowNs{0}; }; @@ -97,7 +97,7 @@ TEST(SBP2ORBTests, CommandORBTimerFiresOnHostQueue) { orb.SetTimeout(5); orb.SetCompletionCallback([&completionStatus](int status, uint8_t) { completionStatus = status; }); - orb.StartTimer(&rig.queue); + orb.StartTimer(&rig.workQueue, &rig.timeoutQueue); rig.AdvanceMs(5); EXPECT_EQ(-1, completionStatus); @@ -111,7 +111,7 @@ TEST(SBP2ORBTests, CommandORBCancelSuppressesPendingTimeout) { orb.SetTimeout(5); orb.SetCompletionCallback([&completionCount](int, uint8_t) { ++completionCount; }); - orb.StartTimer(&rig.queue); + orb.StartTimer(&rig.workQueue, &rig.timeoutQueue); orb.CancelTimer(); rig.AdvanceMs(5); @@ -127,65 +127,13 @@ TEST(SBP2ORBTests, CommandORBDestructionInvalidatesPendingTimeout) { rig.addressManager, reinterpret_cast(0x3), 16); orb->SetTimeout(5); orb->SetCompletionCallback([&completionCount](int, uint8_t) { ++completionCount; }); - orb->StartTimer(&rig.queue); + orb->StartTimer(&rig.workQueue, &rig.timeoutQueue); } rig.AdvanceMs(5); EXPECT_EQ(0, completionCount); } -TEST(SBP2ORBTests, PageTableUsesDirectDescriptorForSingleAlignedSegment) { - ORBTimerRig rig; - - SBP2PageTable pageTable(rig.addressManager, reinterpret_cast(0x40)); - const std::array segments{{ - {.address = 0x0001'2345'6000ULL, .length = 512}, - }}; - - ASSERT_TRUE(pageTable.Build(segments, 0x21)); - - const auto& result = pageTable.GetResult(); - EXPECT_TRUE(result.isDirect); - EXPECT_EQ(1u, pageTable.EntryCount()); - EXPECT_EQ(0x0001u, FromBE32(result.dataDescriptorHi) & 0xFFFFu); - EXPECT_EQ(0x2345'6000u, FromBE32(result.dataDescriptorLo)); - EXPECT_EQ(512u, FromBE16(result.dataSize)); - EXPECT_EQ(0u, result.options); -} - -TEST(SBP2ORBTests, PageTableSplitsSegmentsIntoPublishedEntries) { - ORBTimerRig rig; - - SBP2PageTable pageTable(rig.addressManager, reinterpret_cast(0x41)); - const std::array segments{{ - {.address = 0x0001'0000'1000ULL, .length = 0x30}, - }}; - - ASSERT_TRUE(pageTable.Build(segments, 0x21, 0x10)); - - const auto& result = pageTable.GetResult(); - ASSERT_FALSE(result.isDirect); - ASSERT_EQ(3u, pageTable.EntryCount()); - EXPECT_EQ(3u, FromBE16(result.dataSize)); - EXPECT_EQ(ASFW::Protocols::SBP2::Wire::Options::kPageTableUnrestricted, - result.options); - - const uint32_t descriptorHi = FromBE32(result.dataDescriptorHi); - const uint16_t expectedNode = NormalizeBusNodeID(0x21); - EXPECT_EQ(expectedNode, static_cast(descriptorHi >> 16)); - EXPECT_EQ(0xFFFFu, descriptorHi & 0xFFFFu); - - const uint64_t tableAddress = - ComposeAddress(static_cast(descriptorHi & 0xFFFFu), - FromBE32(result.dataDescriptorLo)); - const uint32_t firstEntryHeader = FromBE32(ReadQuadlet(rig.addressManager, tableAddress)); - const uint32_t firstEntryLo = FromBE32(ReadQuadlet(rig.addressManager, tableAddress + 4)); - - EXPECT_EQ(0x0010u, firstEntryHeader >> 16); - EXPECT_EQ(0x0001u, firstEntryHeader & 0xFFFFu); - EXPECT_EQ(0x0000'1000u, firstEntryLo); -} - TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { ORBTimerRig rig; @@ -195,7 +143,8 @@ TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { orb.SetManagementAgentOffset(0x80); orb.SetTargetNode(1, 0x3F); orb.SetTimeout(5); - orb.SetWorkQueue(&rig.queue); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); int completionStatus = 99; orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); @@ -213,6 +162,8 @@ TEST(SBP2ORBTests, ManagementORBStatusWriteCancelsTimeout) { StatusBlock status{}; status.details = 0; status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); rig.addressManager.ApplyRemoteWrite( ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), std::span{reinterpret_cast(&status), sizeof(status)}); @@ -257,7 +208,7 @@ TEST(SBP2ORBTests, CommandORBDirectDescriptorUsesFullBusNodeId) { descriptor.isDirect = true; orb.SetDataDescriptor(descriptor); - orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6); + ASSERT_EQ(kIOReturnSuccess, orb.PrepareForExecution(0x21, ASFW::FW::FwSpeed::S400, 6)); const auto orbAddress = orb.GetORBAddress(); const uint64_t packedAddress = ComposeAddress(orbAddress.addressHi, orbAddress.addressLo); @@ -281,7 +232,8 @@ TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { orb->SetManagementAgentOffset(0x81); orb->SetTargetNode(1, 0x3F); orb->SetTimeout(5); - orb->SetWorkQueue(&rig.queue); + orb->SetWorkQueue(&rig.workQueue); + orb->SetTimeoutQueue(&rig.timeoutQueue); orb->SetCompletionCallback([&completionCount](int) { ++completionCount; }); ASSERT_TRUE(orb->Execute()); @@ -293,4 +245,140 @@ TEST(SBP2ORBTests, ManagementORBDestructionInvalidatesPendingTimeout) { EXPECT_EQ(0, completionCount); } +TEST(SBP2ORBTests, ManagementORBPropagatesDeviceStatusFailure) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0x8)); + orb.SetFunction(SBP2ManagementORB::Function::LogicalUnitReset); + orb.SetLoginID(0x44); + orb.SetManagementAgentOffset(0x90); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kFunctionRejected; + status.orbOffsetHi = ToBE16(static_cast((orbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(orbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{reinterpret_cast(&status), sizeof(status)}); + + rig.DrainReady(); + EXPECT_EQ(-4, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBRejectsMalformedStatusPayload) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0x9)); + orb.SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb.SetLoginID(0x45); + orb.SetManagementAgentOffset(0x91); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + const std::array shortPayload{0, 0, 0, 0}; + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{shortPayload.data(), shortPayload.size()}); + + rig.DrainReady(); + EXPECT_EQ(-3, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBRejectsMismatchedStatusORBAddress) { + ORBTimerRig rig; + + SBP2ManagementORB orb(rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0xA)); + orb.SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb.SetLoginID(0x46); + orb.SetManagementAgentOffset(0x92); + orb.SetTargetNode(1, 0x3F); + orb.SetTimeout(5); + orb.SetWorkQueue(&rig.workQueue); + orb.SetTimeoutQueue(&rig.timeoutQueue); + + int completionStatus = 99; + orb.SetCompletionCallback([&completionStatus](int status) { completionStatus = status; }); + + ASSERT_TRUE(orb.Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + + ASSERT_TRUE(rig.bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + rig.DrainReady(); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast(((orbAddress + 8) >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast((orbAddress + 8) & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress), + std::span{reinterpret_cast(&status), sizeof(status)}); + + rig.DrainReady(); + EXPECT_EQ(-3, completionStatus); +} + +TEST(SBP2ORBTests, ManagementORBDestroyedAfterExecuteIgnoresPendingWriteAndStatus) { + ORBTimerRig rig; + + int completionCount = 0; + uint64_t statusAddress = 0; + { + auto orb = std::make_unique( + rig.bus, rig.bus, rig.addressManager, reinterpret_cast(0xB)); + orb->SetFunction(SBP2ManagementORB::Function::AbortTaskSet); + orb->SetLoginID(0x47); + orb->SetManagementAgentOffset(0x93); + orb->SetTargetNode(1, 0x3F); + orb->SetTimeout(5); + orb->SetWorkQueue(&rig.workQueue); + orb->SetTimeoutQueue(&rig.timeoutQueue); + orb->SetCompletionCallback([&completionCount](int) { ++completionCount; }); + + ASSERT_TRUE(orb->Execute()); + const auto& write = rig.bus.WriteAt(0); + const uint64_t orbAddress = DecodeOrbAddressFromPayload(write.data); + statusAddress = ReadStatusAddressFromManagementORB(rig.addressManager, orbAddress); + } + + EXPECT_EQ(0u, rig.bus.PendingWriteCount()); + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + rig.addressManager.ApplyRemoteWrite( + statusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + rig.AdvanceMs(5); + + EXPECT_EQ(0, completionCount); +} + } // namespace diff --git a/tests/SBP2SessionRegistryTests.cpp b/tests/SBP2SessionRegistryTests.cpp new file mode 100644 index 00000000..90c89e4c --- /dev/null +++ b/tests/SBP2SessionRegistryTests.cpp @@ -0,0 +1,317 @@ +#include + +#include "ASFWDriver/Discovery/DeviceManager.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2SessionRegistry.hpp" +#include "ASFWDriver/Protocols/SBP2/SCSICommandSet.hpp" +#include "ASFWDriver/Protocols/SBP2/SBP2WireFormats.hpp" +#include "ASFWDriver/Testing/HostDriverKitStubs.hpp" +#include "tests/mocks/DeferredFireWireBus.hpp" + +#include +#include +#include + +namespace { + +constexpr uint32_t kSBP2UnitSpecId = 0x00609E; +constexpr uint32_t kSBP2UnitSwVersion = 0x010483; + +using ASFW::Discovery::CfgKey; +using ASFW::Discovery::ConfigROM; +using ASFW::Discovery::DeviceKind; +using ASFW::Discovery::DeviceManager; +using ASFW::Discovery::DeviceRecord; +using ASFW::Discovery::Generation; +using ASFW::Discovery::LifeState; +using ASFW::Discovery::LinkPolicy; +using ASFW::Discovery::RomEntry; +using ASFW::Protocols::SBP2::AddressSpaceManager; +using ASFW::Protocols::SBP2::SBP2SessionRegistry; +namespace SCSI = ASFW::Protocols::SBP2::SCSI; +using ASFW::Protocols::SBP2::Wire::FromBE16; +using ASFW::Protocols::SBP2::Wire::FromBE32; +using ASFW::Protocols::SBP2::Wire::LoginORB; +using ASFW::Protocols::SBP2::Wire::LoginResponse; +using ASFW::Protocols::SBP2::Wire::NormalORB; +using ASFW::Protocols::SBP2::Wire::StatusBlock; +using ASFW::Protocols::SBP2::Wire::ToBE16; +using ASFW::Protocols::SBP2::Wire::ToBE32; +namespace SBPStatus = ASFW::Protocols::SBP2::Wire::SBPStatus; + +uint64_t ComposeAddress(uint16_t hi, uint32_t lo) { + return (static_cast(hi) << 32) | lo; +} + +uint64_t DecodeAddressFromWritePayload(std::span payload) { + const uint16_t addressHi = + static_cast((static_cast(payload[2]) << 8) | payload[3]); + const uint32_t addressLo = + (static_cast(payload[4]) << 24) | + (static_cast(payload[5]) << 16) | + (static_cast(payload[6]) << 8) | + static_cast(payload[7]); + return ComposeAddress(addressHi, addressLo); +} + +uint32_t ReadQuadlet(AddressSpaceManager& manager, uint64_t address) { + uint32_t value = 0; + EXPECT_EQ(ASFW::Async::ResponseCode::Complete, manager.ReadQuadlet(address, &value)); + return value; +} + +uint64_t ReadORBAddress(AddressSpaceManager& manager, + uint64_t orbAddress, + size_t hiOffset, + size_t loOffset) { + const uint32_t hi = FromBE32(ReadQuadlet(manager, orbAddress + hiOffset)); + const uint32_t lo = FromBE32(ReadQuadlet(manager, orbAddress + loOffset)); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +uint64_t ReadDataBufferAddress(AddressSpaceManager& manager, uint64_t orbAddress) { + const uint32_t hi = FromBE32( + ReadQuadlet(manager, orbAddress + offsetof(NormalORB, dataDescriptorHi))); + const uint32_t lo = FromBE32( + ReadQuadlet(manager, orbAddress + offsetof(NormalORB, dataDescriptorLo))); + return ComposeAddress(static_cast(hi & 0xFFFFu), lo); +} + +class SessionRegistryRig { +public: + SessionRegistryRig() + : registry(bus, bus, addressManager, deviceManager, &queue) { + queue.SetManualDispatchForTesting(true); + ASFW::Testing::SetHostMonotonicClockForTesting([this]() { return nowNs; }); + + bus.SetGeneration(ASFW::FW::Generation{1}); + bus.SetLocalNodeID(ASFW::FW::NodeId{0x2A}); + bus.SetDefaultSpeed(ASFW::FW::FwSpeed::S400); + + DeviceRecord record{}; + record.guid = kGuid; + record.vendorId = 0x001122; + record.modelId = 0x334455; + record.kind = DeviceKind::Unknown; + record.vendorName = "Scanner Vendor"; + record.modelName = "Scanner Model"; + record.gen = Generation{1}; + record.nodeId = 0x32; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = Generation{1}; + rom.nodeId = record.nodeId; + rom.vendorName = record.vendorName; + rom.modelName = record.modelName; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Spec_Id, kSBP2UnitSpecId, 0, 0}, + RomEntry{CfgKey::Unit_Sw_Version, kSBP2UnitSwVersion, 0, 0}, + RomEntry{CfgKey::Logical_Unit_Number, 0x000002, 0, 0}, + RomEntry{CfgKey::Management_Agent_Offset, 0x000080, 1, 0}, + RomEntry{CfgKey::Unit_Characteristics, 0x080400, 0, 0}, + }; + + auto device = deviceManager.UpsertDevice(record, rom); + EXPECT_NE(nullptr, device); + if (!device) { + return; + } + EXPECT_FALSE(device->GetUnits().empty()); + } + + ~SessionRegistryRig() { + ASFW::Testing::ResetHostMonotonicClockForTesting(); + } + + void AdvanceMs(uint64_t milliseconds) { + nowNs += milliseconds * 1'000'000ULL; + while (queue.DrainReadyForTesting() > 0U) { + } + } + + uint64_t CreateSession() { + auto result = registry.CreateSession(reinterpret_cast(0xCAFE), kGuid, 0); + EXPECT_TRUE(result.has_value()); + return result.value_or(0); + } + + void LoginSuccessfully(uint64_t handle, + uint16_t loginId = 0x0042, + uint32_t commandBlockAgentLo = 0x0020'0000) { + ASSERT_TRUE(registry.StartLogin(handle)); + ASSERT_EQ(1u, bus.PendingWriteCount()); + + const auto& loginWrite = bus.WriteAt(0); + const uint64_t loginOrbAddress = DecodeAddressFromWritePayload(loginWrite.data); + const uint64_t loginResponseAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, loginResponseAddressHi), + offsetof(LoginORB, loginResponseAddressLo)); + sessionStatusAddress = + ReadORBAddress(addressManager, + loginOrbAddress, + offsetof(LoginORB, statusFIFOAddressHi), + offsetof(LoginORB, statusFIFOAddressLo)); + + LoginResponse response{}; + response.length = ToBE16(LoginResponse::kSize); + response.loginID = ToBE16(loginId); + response.commandBlockAgentAddressHi = ToBE32(0x0000'FFFFu); + response.commandBlockAgentAddressLo = ToBE32(commandBlockAgentLo); + response.reconnectHold = ToBE16(1); + + ASSERT_TRUE(bus.CompleteNextWrite(ASFW::Async::AsyncStatus::kSuccess)); + addressManager.ApplyRemoteWrite( + loginResponseAddress, + std::span{reinterpret_cast(&response), + sizeof(response)}); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + addressManager.ApplyRemoteWrite( + sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + } + + static constexpr uint64_t kGuid = 0x0003DB0001DDDDA1ULL; + + ASFW::Async::Testing::DeferredFireWireBus bus; + AddressSpaceManager addressManager{nullptr}; + DeviceManager deviceManager; + IODispatchQueue queue; + SBP2SessionRegistry registry; + uint64_t nowNs{0}; + uint64_t sessionStatusAddress{0}; +}; + +TEST(SBP2SessionRegistryTests, BuildStandardCommandHelpersUseExpectedOpCodes) { + const auto inquiry = SCSI::BuildInquiryRequest(64); + ASSERT_EQ(6u, inquiry.cdb.size()); + EXPECT_EQ(0x12, inquiry.cdb[0]); + EXPECT_EQ(64u, inquiry.transferLength); + + const auto tur = SCSI::BuildTestUnitReadyRequest(); + ASSERT_EQ(6u, tur.cdb.size()); + EXPECT_EQ(0x00, tur.cdb[0]); + EXPECT_EQ(SCSI::DataDirection::None, tur.direction); + + const auto sense = SCSI::BuildRequestSenseRequest(18); + ASSERT_EQ(6u, sense.cdb.size()); + EXPECT_EQ(0x03, sense.cdb[0]); + EXPECT_EQ(SCSI::DataDirection::FromTarget, sense.direction); +} + +TEST(SBP2SessionRegistryTests, SubmitRequestSenseCapturesPayloadAndSenseData) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + const auto request = SCSI::BuildRequestSenseRequest(18); + const size_t pendingBeforeSubmit = rig.bus.PendingWriteCount(); + ASSERT_TRUE(rig.registry.SubmitCommand(handle, request)); + ASSERT_EQ(pendingBeforeSubmit + 1U, rig.bus.PendingWriteCount()); + + const auto& write = rig.bus.WriteAt(rig.bus.WriteCount() - 1); + const uint64_t commandOrbAddress = DecodeAddressFromWritePayload(write.data); + const uint64_t dataBufferAddress = + ReadDataBufferAddress(rig.addressManager, commandOrbAddress); + + const std::vector sensePayload{ + 0x70, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x3A, 0x01 + }; + rig.addressManager.ApplyRemoteWrite(dataBufferAddress, sensePayload); + + StatusBlock status{}; + status.details = 0; + status.sbpStatus = SBPStatus::kNoAdditionalInfo; + status.orbOffsetHi = ToBE16(static_cast((commandOrbAddress >> 32) & 0xFFFFu)); + status.orbOffsetLo = ToBE32(static_cast(commandOrbAddress & 0xFFFF'FFFFu)); + rig.addressManager.ApplyRemoteWrite( + rig.sessionStatusAddress, + std::span{reinterpret_cast(&status), sizeof(status)}); + + auto result = rig.registry.GetCommandResult(handle); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(0, result->transportStatus); + EXPECT_EQ(SBPStatus::kNoAdditionalInfo, result->sbpStatus); + EXPECT_EQ(sensePayload, result->payload); + EXPECT_EQ(sensePayload, result->senseData); +} + +TEST(SBP2SessionRegistryTests, SubmitCommandRejectsCDBLargerThanORBPayloadBudget) { + SessionRegistryRig rig; + const uint64_t handle = rig.CreateSession(); + rig.LoginSuccessfully(handle); + + SCSI::CommandRequest request{}; + request.cdb = std::vector(16, 0x12); + request.direction = SCSI::DataDirection::None; + request.transferLength = 0; + request.timeoutMs = 100; + + EXPECT_FALSE(rig.registry.SubmitCommand(handle, request)); +} + +TEST(SBP2SessionRegistryTests, CreateSessionAcceptsRealSBP2SpecAndVersion) { + SessionRegistryRig rig; + auto result = rig.registry.CreateSession(reinterpret_cast(0xCAFE), + SessionRegistryRig::kGuid, + 0); + ASSERT_TRUE(result.has_value()); +} + +TEST(SBP2SessionRegistryTests, DeviceDiscoveryParsesNikonStyleManagementAgentCSRKey) { + DeviceManager deviceManager; + + DeviceRecord record{}; + record.guid = SessionRegistryRig::kGuid + 1U; + record.vendorId = 0x0090B5; + record.modelId = 0x004001; + record.kind = DeviceKind::Unknown; + record.vendorName = "Nikon"; + record.modelName = "LS-4000 ED"; + record.gen = Generation{1}; + record.nodeId = 0x00; + record.link = LinkPolicy{}; + record.state = LifeState::Ready; + + ConfigROM rom{}; + rom.gen = Generation{1}; + rom.nodeId = record.nodeId; + rom.bib.busInfoLength = 4; + rom.rootDirMinimal = { + RomEntry{CfgKey::Unit_Directory, 0x000001, 3, 1}, + }; + rom.rawQuadlets = { + ToBE32(0x04045343u), + ToBE32(0x31333934u), + ToBE32(0x00FF5012u), + ToBE32(0x0090B540u), + ToBE32(0x01FFFFFFu), + ToBE32(0x0001B344u), + ToBE32(0x0004CAEEu), + ToBE32(0x1200609Eu), + ToBE32(0x13010483u), + ToBE32(0x5400C000u), + ToBE32(0x14060000u), + }; + + auto device = deviceManager.UpsertDevice(record, rom); + ASSERT_NE(device, nullptr); + ASSERT_EQ(device->GetUnits().size(), 1u); + + const auto& unit = device->GetUnits().front(); + ASSERT_NE(unit, nullptr); + EXPECT_TRUE(unit->Matches(kSBP2UnitSpecId, kSBP2UnitSwVersion)); + ASSERT_TRUE(unit->GetManagementAgentOffset().has_value()); + EXPECT_EQ(*unit->GetManagementAgentOffset(), 0x00C000u); + ASSERT_TRUE(unit->GetLUN().has_value()); + EXPECT_EQ(*unit->GetLUN(), 0x060000u); +} + +} // namespace diff --git a/tests/TransactionStorageTests.cpp b/tests/TransactionStorageTests.cpp new file mode 100644 index 00000000..541f5367 --- /dev/null +++ b/tests/TransactionStorageTests.cpp @@ -0,0 +1,51 @@ +#include "UserClient/Storage/TransactionStorage.hpp" + +#include + +#include +#include + +namespace { + +TEST(TransactionStorageTests, StoresCompletePayloadBeyondLegacy512ByteLimit) { + ASFW::UserClient::TransactionStorage storage; + ASSERT_TRUE(storage.IsValid()); + + std::array payload{}; + for (std::size_t i = 0; i < payload.size(); ++i) { + payload[i] = static_cast(i & 0xffU); + } + + EXPECT_TRUE(storage.StoreResult(0x1234, 7, 0x11, payload.data(), + static_cast(payload.size()))); + + storage.Lock(); + ASFW::UserClient::TransactionResult* result = storage.FindResult(0x1234); + ASSERT_NE(result, nullptr); + EXPECT_EQ(result->status, 7U); + EXPECT_EQ(result->responseCode, 0x11U); + ASSERT_EQ(result->dataLength, payload.size()); + ASSERT_NE(result->Data(), nullptr); + for (std::size_t i = 0; i < payload.size(); ++i) { + EXPECT_EQ(result->Data()[i], payload[i]) << "byte " << i; + } + storage.Unlock(); +} + +TEST(TransactionStorageTests, PreservesStatusAndEmptyPayload) { + ASFW::UserClient::TransactionStorage storage; + ASSERT_TRUE(storage.IsValid()); + + EXPECT_TRUE(storage.StoreResult(0x4321, 5, 0x04, nullptr, 0)); + + storage.Lock(); + ASFW::UserClient::TransactionResult* result = storage.FindResult(0x4321); + ASSERT_NE(result, nullptr); + EXPECT_EQ(result->status, 5U); + EXPECT_EQ(result->responseCode, 0x04U); + EXPECT_EQ(result->dataLength, 0U); + EXPECT_EQ(result->Data(), nullptr); + storage.Unlock(); +} + +} // namespace