private void ZwaveMessageReceived(object sender, ZWaveMessageReceivedEventArgs args) { // discard repeated messages within last 2 seconds time range bool repeated = false; if (lastMessage != null) { var elapsed = (DateTime.UtcNow - lastMessageTimestamp); if (elapsed.TotalSeconds <= 2 && lastMessage.SequenceEqual(args.Message)) { //Utility.DebugLog(DebugMessageType.Information, " lastMessage: " + Utility.ByteArrayToString(lastMessage)); //Utility.DebugLog(DebugMessageType.Information, "args.Message: " + Utility.ByteArrayToString(args.Message)); repeated = true; } } lastMessageTimestamp = DateTime.UtcNow; lastMessage = new byte[args.Message.Length]; //Utility.DebugLog(DebugMessageType.Information, " lastMessage2: " + Utility.ByteArrayToString(lastMessage)); //Utility.DebugLog(DebugMessageType.Information, "args.Message2: " + Utility.ByteArrayToString(args.Message)); Buffer.BlockCopy(args.Message, 0, lastMessage, 0, args.Message.Length * sizeof(byte)); if (repeated) { Utility.DebugLog(DebugMessageType.Warning, "Repeated message discarded."); return; } // int length = args.Message.Length; try { MessageHeader zwaveHeader = (MessageHeader)args.Message[0]; switch (zwaveHeader) { case MessageHeader.CAN: // RESEND //Utility.DebugLog(DebugMessageType.Warning, "Received CAN, resending last message"); //zp.ResendLastMessage(); break; case MessageHeader.ACK: break; case MessageHeader.SOF: // start of zwave frame // // parse frame headers // //int msgLength = (int)args.Message[1]; var msgType = (MessageType)args.Message[2]; var function = (args.Message.Length > 3 ? (Function)args.Message[3] : 0); byte sourceNodeId = 0; byte nodeOperation = 0; // switch (msgType) { case MessageType.Request: if (devices.Count == 0) { break; } switch (function) { case Function.None: break; case Function.NodeAdd: nodeOperation = args.Message[5]; if (nodeOperation == (byte)NodeFunctionStatus.AddNodeAddingSlave) { nodeOperationIdCheck = args.Message[6]; var newNode = CreateDevice(nodeOperationIdCheck, 0x00); // Extract node information frame int nodeInfoLength = (int)args.Message[7]; // we don't need to exclude the last 2 CommandClasses byte[] nodeInfo = new byte[nodeInfoLength]; Array.Copy(args.Message, 8, nodeInfo, 0, nodeInfoLength); newNode.NodeInformationFrame = nodeInfo; newNode.BasicClass = args.Message[8]; newNode.GenericClass = args.Message[9]; newNode.SpecificClass = args.Message[10]; devices.Add(newNode); if (newNode.SupportCommandClass(CommandClass.Security)) { var nodeSecurityData = Security.GetSecurityData(newNode); nodeSecurityData.IsAddingNode = true; Security.GetScheme(newNode); } else { gotNodeUpdateInformation(newNode); } } else if (nodeOperation == (byte)NodeFunctionStatus.AddNodeProtocolDone /* || nodeOperation == (byte)NodeFunctionStatus.AddNodeDone */) { if (nodeOperationIdCheck == args.Message[6]) { Thread.Sleep(500); GetNodeCapabilities(args.Message[6]); var newNode = devices.Find(n => n.Id == args.Message[6]); if (newNode != null) { ManufacturerSpecific.Get(newNode); } } OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.DiscoveryEnd)); } else if (nodeOperation == (byte)NodeFunctionStatus.AddNodeFailed) { Utility.DebugLog(DebugMessageType.Warning, "ADDING NODE FAILED (" + args.Message[6] + ")"); } break; case Function.NodeRemove: nodeOperation = args.Message[5]; if (nodeOperation == (byte)NodeFunctionStatus.RemoveNodeRemovingSlave) { nodeOperationIdCheck = args.Message[6]; } else if (nodeOperation == (byte)NodeFunctionStatus.RemoveNodeDone) { if (nodeOperationIdCheck == args.Message[6]) { RemoveDevice(args.Message[6]); } OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.DiscoveryEnd)); } else if (nodeOperation == (byte)NodeFunctionStatus.RemoveNodeFailed) { Utility.DebugLog(DebugMessageType.Warning, "REMOVING NODE FAILED (" + args.Message[6] + ")"); } break; case Function.ApplicationCommand: sourceNodeId = args.Message[5]; var node = devices.Find(n => n.Id == sourceNodeId); if (node != null) { try { node.MessageRequestHandler(args.Message); } catch (Exception ex) { Utility.DebugLog(DebugMessageType.Error, "Exception occurred in node.MessageRequestHandler: " + ex.Message + "\n" + ex.StackTrace); } } else { Utility.DebugLog(DebugMessageType.Error, "Unknown node id " + sourceNodeId); } break; case Function.SendData: byte commandId = args.Message[4]; if (commandId == 0x01) // SEND DATA OK { // TODO: ... what does that mean? } else if (args.Message[5] == 0x00) { // Messaging complete, remove callbackid zwavePort.PendingMessages.RemoveAll(zm => zm.CallbackId == commandId); } else if (args.Message[5] == 0x01) { var unsentMessage = zwavePort.PendingMessages.Find(zm => zm.CallbackId == commandId); byte nodeID = zwavePort.ResendLastMessage(commandId); if (nodeID != 0) { // Resend timed out OnControllerEvent(new ControllerEventArgs(nodeID, ControllerStatus.NodeError)); // Check if node supports WakeUp class, and add message to wake up message queue if (unsentMessage != null) { var sleepingNode = devices.Find(n => n.Id == nodeID); if (sleepingNode != null && sleepingNode.SupportCommandClass(CommandClass.WakeUp)) { WakeUp.ResendOnWakeUp(sleepingNode, unsentMessage.Message); } } } } break; case Function.NodeUpdateInfo: sourceNodeId = args.Message[5]; int nifLength = (int)args.Message[6]; var znode = devices.Find(n => n.Id == sourceNodeId); if (znode != null) { // we don't need to exclude the last 2 CommandClasses byte[] nodeInfo = new byte[nifLength]; Array.Copy(args.Message, 7, nodeInfo, 0, nifLength); znode.NodeInformationFrame = nodeInfo; if (znode.SupportCommandClass(CommandClass.Security)) { // ask the node what security command classes are supported Security.GetSupported(znode); } else { gotNodeUpdateInformation(znode); } } break; default: Utility.DebugLog(DebugMessageType.Warning, "Unhandled REQUEST " + Utility.ByteArrayToString(args.Message)); break; } break; case MessageType.Response: switch (function) { case Function.DiscoveryNodes: MessageResponseNodeBitMaskHandler(args.Message); break; case Function.GetNodeProtocolInfo: MessageResponseNodeCapabilityHandler(args.Message); break; case Function.RequestNodeInfo: // TODO: shall we do something here? break; case Function.SendData: // TODO: shall we do something here? break; default: Utility.DebugLog(DebugMessageType.Warning, "Unhandled RESPONSE " + Utility.ByteArrayToString(args.Message)); break; } break; default: Utility.DebugLog(DebugMessageType.Warning, "Unhandled MESSAGE TYPE " + Utility.ByteArrayToString(args.Message)); break; } break; } } catch (Exception ex) { Utility.DebugLog(DebugMessageType.Error, "Exception occurred :" + ex.Message + "\n" + ex.StackTrace); } }
private void ZwaveMessageReceived(object sender, ZWaveMessageReceivedEventArgs args) { // discard repeated messages within last 2 seconds time range bool repeated = false; if (lastMessage != null) { var elapsed = (DateTime.UtcNow - lastMessageTimestamp); if (elapsed.TotalSeconds <= 2 && lastMessage.SequenceEqual(args.Message)) { repeated = true; } } lastMessageTimestamp = DateTime.UtcNow; lastMessage = new byte[args.Message.Length]; Buffer.BlockCopy(args.Message, 0, lastMessage, 0, args.Message.Length * sizeof(byte)); if (repeated) { zwavePort.SendAck(); Console.WriteLine("ZWaveLib: repeated message discarded."); return; } // int length = args.Message.Length; try { MessageHeader zwaveHeader = (MessageHeader)args.Message[0]; switch (zwaveHeader) { case MessageHeader.CAN: zwavePort.SendAck(); // RESEND //Console.WriteLine("ZWaveLib: received CAN, resending last message"); //zp.ResendLastMessage(); break; case MessageHeader.ACK: zwavePort.SendAck(); break; case MessageHeader.SOF: // start of zwave frame // // parse frame headers // //int msgLength = (int)args.Message[1]; var msgType = (MessageType)args.Message[2]; var function = (args.Message.Length > 3 ? (Function)args.Message[3] : 0); byte sourceNodeId = 0; byte nodeOperation = 0; // switch (msgType) { case MessageType.Request: zwavePort.SendAck(); if (devices.Count == 0) { break; } switch (function) { case Function.None: break; case Function.NodeAdd: nodeOperation = args.Message[5]; if (nodeOperation == (byte)NodeFunctionStatus.AddNodeAddingSlave) { //Console.WriteLine("\n\nADDING NODE SLAVE {0}\n -> ", zp.ByteArrayToString(args.Message)); nodeOperationIdCheck = args.Message[6]; var newNode = CreateDevice(nodeOperationIdCheck, 0x00); // Extract node information frame int nodeInfoLength = (int)args.Message[7]; byte[] nodeInfo = new byte[nodeInfoLength - 2]; Array.Copy(args.Message, 8, nodeInfo, 0, nodeInfoLength - 2); newNode.NodeInformationFrame = nodeInfo; RaiseUpdateParameterEvent(new ZWaveEvent(newNode, EventParameter.NodeInfo, Utility.ByteArrayToString(nodeInfo), 0)); SaveNodesConfig(); } else if (nodeOperation == (byte)NodeFunctionStatus.AddNodeProtocolDone /* || nodeOperation == (byte)NodeFunctionStatus.AddNodeDone */) { if (nodeOperationIdCheck == args.Message[6]) { //Console.WriteLine("\n\nADDING NODE DONE {0} {1}\n\n", args.Message[6], callbackid); Thread.Sleep(500); GetNodeCapabilities(args.Message[6]); var newNode = devices.Find(n => n.Id == args.Message[6]); if (newNode != null) { ManufacturerSpecific.Get(newNode); } } OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.DiscoveryEnd)); } else if (nodeOperation == (byte)NodeFunctionStatus.AddNodeFailed) { //Console.WriteLine("\n\nADDING NODE FAIL {0}\n\n", args.Message[6]); } break; case Function.NodeRemove: nodeOperation = args.Message[5]; if (nodeOperation == (byte)NodeFunctionStatus.RemoveNodeRemovingSlave) { //Console.WriteLine("\n\nREMOVING NODE SLAVE {0}\n\n", args.Message[6]); nodeOperationIdCheck = args.Message[6]; } else if (nodeOperation == (byte)NodeFunctionStatus.RemoveNodeDone) { if (nodeOperationIdCheck == args.Message[6]) { //Console.WriteLine("\n\nREMOVING NODE DONE {0} {1}\n\n", args.Message[6], callbackid); RemoveDevice(args.Message[6]); } OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.DiscoveryEnd)); } else if (nodeOperation == (byte)NodeFunctionStatus.RemoveNodeFailed) { //Console.WriteLine("\n\nREMOVING NODE FAIL {0}\n\n", args.Message[6]); } break; case Function.ApplicationCommand: sourceNodeId = args.Message[5]; var node = devices.Find(n => n.Id == sourceNodeId); if (node == null) { CreateDevice(sourceNodeId, 0x00); GetNodeCapabilities(sourceNodeId); } try { node.MessageRequestHandler(args.Message); } catch (Exception ex) { Console.WriteLine("# " + ex.Message + "\n" + ex.StackTrace); } break; case Function.SendData: byte commandId = args.Message[4]; if (commandId == 0x01) // SEND DATA OK { // TODO: ... what does that mean? } else if (args.Message[5] == 0x00) { // Messaging complete, remove callbackid zwavePort.PendingMessages.RemoveAll(zm => zm.CallbackId == commandId); } else if (args.Message[5] == 0x01) { var unsentMessage = zwavePort.PendingMessages.Find(zm => zm.CallbackId == commandId); byte nodeID = zwavePort.ResendLastMessage(commandId); if (nodeID != 0) { // Resend timed out OnControllerEvent(new ControllerEventArgs(nodeID, ControllerStatus.NodeError)); // Check if node supports WakeUp class, and add message to wake up message queue if (unsentMessage != null) { var sleepingNode = devices.Find(n => n.Id == nodeID); if (sleepingNode != null && sleepingNode.SupportCommandClass(CommandClass.WakeUp)) { WakeUp.ResendOnWakeUp(sleepingNode, unsentMessage.Message); } } } } break; case Function.NodeUpdateInfo: sourceNodeId = args.Message[5]; int nifLength = (int)args.Message[6]; var znode = devices.Find(n => n.Id == sourceNodeId); if (znode != null) { byte[] nodeInfo = new byte[nifLength - 2]; //Console.WriteLine(ByteArrayToString(args.Message)); Array.Copy(args.Message, 7, nodeInfo, 0, nifLength - 2); znode.NodeInformationFrame = nodeInfo; // RaiseUpdateParameterEvent(new ZWaveEvent(znode, EventParameter.NodeInfo, Utility.ByteArrayToString(nodeInfo), 0)); RaiseUpdateParameterEvent(new ZWaveEvent(znode, EventParameter.WakeUpNotify, "1", 0)); SaveNodesConfig(); } break; default: Console.WriteLine("\nUNHANDLED Z-Wave REQUEST\n " + Utility.ByteArrayToString(args.Message) + "\n"); break; } break; case MessageType.Response: switch (function) { case Function.DiscoveryNodes: MessageResponseNodeBitMaskHandler(args.Message); break; case Function.GetNodeProtocolInfo: MessageResponseNodeCapabilityHandler(args.Message); break; case Function.RequestNodeInfo: // TODO: shall we do something here? break; case Function.SendData: // TODO: shall we do something here? break; default: Console.WriteLine("\nUNHANDLED Z-Wave RESPONSE\n " + Utility.ByteArrayToString(args.Message) + "\n"); break; } break; default: Console.WriteLine("\nUNHANDLED Z-Wave message TYPE\n " + Utility.ByteArrayToString(args.Message) + "\n"); break; } break; } } catch (Exception ex) { Console.WriteLine(ex.Message + "\n" + ex.StackTrace); } }
private void ZwaveMessageReceived(object sender, ZWaveMessageReceivedEventArgs args) { // discard repeated messages within last 2 seconds time range bool repeated = false; if (lastMessage != null) { var elapsed = (DateTime.UtcNow - lastMessageTimestamp); if (elapsed.TotalSeconds <= 2 && lastMessage.SequenceEqual(args.Message)) { //Utility.DebugLog(DebugMessageType.Information, " lastMessage: " + Utility.ByteArrayToString(lastMessage)); //Utility.DebugLog(DebugMessageType.Information, "args.Message: " + Utility.ByteArrayToString(args.Message)); repeated = true; } } lastMessageTimestamp = DateTime.UtcNow; lastMessage = new byte[args.Message.Length]; //Utility.DebugLog(DebugMessageType.Information, " lastMessage2: " + Utility.ByteArrayToString(lastMessage)); //Utility.DebugLog(DebugMessageType.Information, "args.Message2: " + Utility.ByteArrayToString(args.Message)); Buffer.BlockCopy(args.Message, 0, lastMessage, 0, args.Message.Length * sizeof(byte)); if (repeated) { Utility.DebugLog(DebugMessageType.Warning, "Repeated message discarded."); return; } // int length = args.Message.Length; try { MessageHeader zwaveHeader = (MessageHeader)args.Message[0]; switch (zwaveHeader) { case MessageHeader.CAN: // RESEND ?!?! break; case MessageHeader.ACK: break; case MessageHeader.SOF: // start of zwave frame var messageType = MessageType.None; Enum.TryParse(args.Message[2].ToString(), out messageType); var functionType = Function.None; Enum.TryParse(args.Message[3].ToString(), out functionType); switch (messageType) { case MessageType.Request: if (devices.Count == 0) { break; } switch (functionType) { case Function.None: break; case Function.NodeAdd: var nodeAddStatus = NodeFunctionStatus.None; Enum.TryParse(args.Message[5].ToString(), out nodeAddStatus); switch (nodeAddStatus) { case NodeFunctionStatus.AddNodeAddingSlave: nodeOperationIdCheck = args.Message[6]; var newNode = CreateDevice(nodeOperationIdCheck, 0x00); // Extract node information frame int nodeInfoLength = (int)args.Message[7]; // we don't need to exclude the last 2 CommandClasses byte[] nodeInfo = new byte[nodeInfoLength]; Array.Copy(args.Message, 8, nodeInfo, 0, nodeInfoLength); newNode.NodeInformationFrame = nodeInfo; newNode.BasicClass = args.Message[8]; newNode.GenericClass = args.Message[9]; newNode.SpecificClass = args.Message[10]; devices.Add(newNode); if (newNode.SupportCommandClass(CommandClass.Security)) { var nodeSecurityData = Security.GetSecurityData(newNode); nodeSecurityData.IsAddingNode = true; Security.GetScheme(newNode); } else { NodeInformationFrameComplete(newNode); } break; //case NodeFunctionStatus.AddNodeDone: case NodeFunctionStatus.AddNodeProtocolDone: if (nodeOperationIdCheck == args.Message[6]) { Thread.Sleep(500); GetNodeCapabilities(args.Message[6]); var addedNode = devices.Find(n => n.Id == args.Message[6]); if (addedNode != null) { ManufacturerSpecific.Get(addedNode); } } OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.NodeAdded)); // force refresh of nodelist sending DiscoverEnd event // TODO: deprecate this and update the Web UI to refresh modules list on NodeAdded event OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.DiscoveryEnd)); break; case NodeFunctionStatus.AddNodeFailed: Utility.DebugLog(DebugMessageType.Warning, "ADDING NODE FAILED (" + args.Message[6] + ")"); break; } break; case Function.NodeRemove: var nodeRemoveStatus = NodeFunctionStatus.None; Enum.TryParse(args.Message[5].ToString(), out nodeRemoveStatus); switch (nodeRemoveStatus) { case NodeFunctionStatus.RemoveNodeRemovingSlave: nodeOperationIdCheck = args.Message[6]; break; case NodeFunctionStatus.RemoveNodeDone: if (nodeOperationIdCheck == args.Message[6]) { RemoveDevice(nodeOperationIdCheck); } OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.NodeRemoved)); // force refresh of nodelist sending DiscoverEnd event // TODO: deprecate this and update the Web UI to refresh modules list on NodeRemoved event OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.DiscoveryEnd)); break; case NodeFunctionStatus.RemoveNodeFailed: OnControllerEvent(new ControllerEventArgs(0x00, ControllerStatus.NodeError)); Utility.DebugLog(DebugMessageType.Warning, "REMOVING NODE FAILED (" + args.Message[6] + ")"); break; } break; case Function.ApplicationCommandHandler: var node = devices.Find(n => n.Id == args.Message[5]); if (node != null) { try { node.ApplicationCommandHandler(args.Message); } catch (Exception ex) { Utility.DebugLog(DebugMessageType.Error, "Exception occurred in node.ApplicationCommandHandler: " + ex.Message + "\n" + ex.StackTrace); } } else { Utility.DebugLog(DebugMessageType.Error, "Unknown node id " + args.Message[5]); } break; case Function.SendData: byte callbackId = args.Message[4]; if (callbackId == 0x01) // 0x01 is "SEND DATA OK" { // TODO: ... is there anything to be done here? } else { var status = CallbackStatus.Nack; Enum.TryParse(args.Message[5].ToString(), out status); switch (status) { case CallbackStatus.Ack: // Messaging complete, remove callbackid zwavePort.NodeRequestAck(callbackId); break; case CallbackStatus.Nack: var pendingMessage = zwavePort.NodeRequestNack(callbackId); if (pendingMessage != null && pendingMessage.ResendCount >= ZWaveMessage.ResendMaxAttempts) { // Resend timed out OnControllerEvent(new ControllerEventArgs(pendingMessage.Node.Id, ControllerStatus.NodeError)); // Check if node supports WakeUp class, and add message to wake up message queue if (pendingMessage != null) { var sleepingNode = pendingMessage.Node; if (sleepingNode != null && sleepingNode.SupportCommandClass(CommandClass.WakeUp)) { WakeUp.ResendOnWakeUp(sleepingNode, pendingMessage.Message); } } } break; case CallbackStatus.NeighborUpdateStarted: // Neighbour Update Options STARTED //var message = zwavePort.GetPendingMessage(commandId); // TODO: don't know what to do here... break; case CallbackStatus.NeighborUpdateDone: var message = zwavePort.GetPendingMessage(callbackId); if (message != null) { RequestNeighborsUpdate(message.Node); // send ack so the message is removed from the pending message list zwavePort.NodeRequestAck(callbackId); } break; } } break; case Function.ApplicationUpdate: int nifLength = (int)args.Message[6]; var znode = devices.Find(n => n.Id == args.Message[5]); if (znode != null) { // we don't need to exclude the last 2 CommandClasses byte[] nodeInfo = new byte[nifLength]; Array.Copy(args.Message, 7, nodeInfo, 0, nifLength); znode.NodeInformationFrame = nodeInfo; if (znode.SupportCommandClass(CommandClass.Security)) { // ask the node what security command classes are supported Security.GetSupported(znode); } else { NodeInformationFrameComplete(znode); } } break; case Function.RequestNodeNeighborsUpdateOptions: case Function.RequestNodeNeighborsUpdate: var neighborUpdateStatus = NeighborsUpdateStatus.None; Enum.TryParse(args.Message[5].ToString(), out neighborUpdateStatus); var pm = zwavePort.GetPendingMessage(args.Message[4]); switch (neighborUpdateStatus) { case NeighborsUpdateStatus.NeighborsUpdateStared: OnControllerEvent(new ControllerEventArgs(pm.Node.Id, ControllerStatus.NeighborUpdateStarted)); break; case NeighborsUpdateStatus.NeighborsUpdateDone: nodeOperationIdCheck = pm.Node.Id; OnControllerEvent(new ControllerEventArgs(nodeOperationIdCheck, ControllerStatus.NeighborUpdateDone)); GetNeighborsRoutingInfo(pm.Node); if (pm != null) { zwavePort.NodeRequestAck(pm.CallbackId); } break; case NeighborsUpdateStatus.NeighborsUpdateFailed: OnControllerEvent(new ControllerEventArgs(pm.Node.Id, ControllerStatus.NeighborUpdateFailed)); if (pm != null) { zwavePort.NodeRequestNack(pm.CallbackId); } break; default: Utility.DebugLog(DebugMessageType.Warning, "Unhandled Node Neighbor Update REQUEST " + Utility.ByteArrayToString(args.Message)); break; } break; default: Utility.DebugLog(DebugMessageType.Warning, "Unhandled REQUEST " + Utility.ByteArrayToString(args.Message)); break; } break; case MessageType.Response: switch (functionType) { case Function.DiscoveryNodes: NodeBitMaskResponseHandler(args.Message); break; case Function.GetNodeProtocolInfo: NodeCapabilitiesResponseHandler(args.Message); break; case Function.RequestNodeInfo: // TODO: shall we do something here? break; case Function.SendData: // TODO: shall we do something here? break; case Function.GetRoutingInfo: var routingInfo = ExtractRoutingFromBitMask(args.Message); if (routingInfo.Count > 0) { var routedNode = devices.Find(n => n.Id == nodeOperationIdCheck); if (routedNode != null) { // routedNode.RaiseUpdateParameterEvent(new ZWaveEvent(routedNode, EventParameter.RoutingInfo, String.Join(" ", routingInfo), 0)); } } else { Utility.DebugLog(DebugMessageType.Warning, "No routing nodes reported."); } break; default: Utility.DebugLog(DebugMessageType.Warning, "Unhandled RESPONSE " + Utility.ByteArrayToString(args.Message)); break; } break; default: Utility.DebugLog(DebugMessageType.Warning, "Unhandled MESSAGE TYPE " + Utility.ByteArrayToString(args.Message)); break; } break; } } catch (Exception ex) { Utility.DebugLog(DebugMessageType.Error, "Exception occurred :" + ex.Message + "\n" + ex.StackTrace); } }