(Just for those who care: no, i'm not an experienced Cocoa or ObjC programmer. It should have been obvious if you knew enough to ask, but just to clear it up.)
This post, we're going to take the initial save file setup we had from
last week, and expand that to actually writing data to a new file. Doing this is going to require splitting up the overall writing code across three handlers in my application. In this case, we have made some minor changes to the code that executes after the user clicks "Okay" in the save panel dialog:
if theSavePanelResult is 1 then
--if they clicked the 'save' button, then we want to get the path and set some flags
set theDataFileURL to theSavePanel's |URL|()
--get the encoded URL, which is not the file path, but we'll need it
set theDataFilePath to theSavePanel's filename()
--get the posix path to the file
set createDataFile to true
--we're creating a new data file, so this has to be true
set appendToExisting to false
--we are not appending data, so set to false
set theFileManager to my NSFileManager's defaultManager()
--create a file manager object so that we can create a blank file
set theCreateFileResults to theFileManager's createFileAtPath_contents_attributes_(theDataFilePath, missing value, missing value)
--creates a blank file at the path specified. Do NOT use "my theFileManager's..." because errors will happen
set theFileHandle to my NSFileHandle's fileHandleForWritingToURL_error_(theDataFileURL, missing value)
--use this to write to the file URL
end if
So as you look at it, you can see that all we did was make the file handle setup the last line. Note that I've set up theFileHandle as a property, because it's the lazy way to avoid scope issues. So we clicked our button to create a new file, we gave it a name, and a location, and we have a file path to get data into it. How do we build the data? Well, first, we have to define what that data is. In this application's case, it's series of lines of tab-delimited text. Each item is a bit of info about the current WiFi network, along with a timestamp for when the data was read. This can be entered as part of the track functionality, writing one line of data per second, or when you hit the refresh button, to write the data manually. The file handle is closed when you either turn off the track function, or deselect the save to file checkbox in the application UI. So let's look at building the data string.
Since I have a nice handler to grab the wifi data and populate the UI with the manual refresh or with the track button, we'll add some code to that. Here's the entire loadData handler:
on loadData(theCurrentInterface)
currentSSID's setStringValue_(theCurrentInterface's ssid())
--set the contents of SSID field to the current SSID
set theCurrentAuthMode to (theCurrentInterface's securityMode() as text)
--it's an NSNumber, will deal with it later. Forcing to text works for now
if theCurrentAuthMode is "0" then
--mode number to mode name
authMode's setStringValue_("Open")
else if theCurrentAuthMode is "1" then
authMode's setStringValue_("WEP")
else if theCurrentAuthMode is "2" then
authMode's setStringValue_("WPA1 Personal")
else if theCurrentAuthMode is "3" then
authMode's setStringValue_("WPA2 Personal")
else if theCurrentAuthMode is "4" then
authMode's setStringValue_("WPA1 Enterprise")
else if theCurrentAuthMode is "5" then
authMode's setStringValue_("WPA2 Enterprise")
else if theCurrentAuthMode is "6" then
authMode's setStringValue_("WiFi Protected Setup")
else if theCurrentAuthMode is "7" then
authMode's setStringValue_("Dynamic Wep 802.1X")
else
authMode's setStringValue_("Unknown/Invalid")
end if
currentChannel's setStringValue_(theCurrentInterface's channel())
--set the channel
currentDataRate's setStringValue_(theCurrentInterface's txRate())
--set the data rate in Mbps
signalStrength's setStringValue_(theCurrentInterface's rssi())
--set the signal strength in dbm
signalNoise's setStringValue_(theCurrentInterface's noise())
--set the signal noise in dbm
currentWAPMAC's setStringValue_(theCurrentInterface's bssid())
--set the MAC of the base station
theTime's setStringValue_((time string of (current date)))
--let's add the code to save to a new file
if (createDataFile is true) and (theSaveFileFlag is true) then
set theTempString to (theTime's stringValue() as text) & " "
& (currentSSID's stringValue() as text) & " "
& (authMode's stringValue() as text) & " "
& (currentChannel's stringValue() as text) & " "
& (currentDataRate's stringValue() as text) & " "
& (signalStrength's stringValue() as text) & " "
& (signalNoise's stringValue() as text) & " "
& (currentWAPMAC's stringValue() as text) & "
"
set theFileString to my NSString's stringWithString_(theTempString)
set theFileData to theFileString's dataUsingEncoding_(NSUTF8StringEncoding of current application)
theFileHandle's writeData_(theFileData)
--you could probably also use fileHandleForWritingToPath here, but since URLs are the way of the future
--we should use them where we can
--we stash the close function where it's going to be actually used
end if
end loadData
There's really not much going on here. This handler, (and you can tell it's a 'normal' AppleScript handler because it doesn't have an underscore in the name), is passed a WiFi interface object, theCurrentInterface. It then pulls data from that to set various text fields in the Application UI. For example, we grab the SSID of the current WiFi network via theCurrentInterface's ssid(), and use that to set the contents of that text field in the UI via currentSSID's setStringValue_(theCurrentInterface's ssid())
Since the enumeration for the authentication mode can be one of 9 values, including "other", we have a series of if then else statements to handle that. To get the time string we use for theTime, instead of using the Cocoa technique, and NSDate/NSDateComponents, we use a more traditional AppleScript way: time String of (current date). It works just as well as any other method for our needs, and is simpler to code. adding that to the UI is then a single line: theTime's setStringValue_((time string of (current date)))
So now, we have all the information we need to build our string that we want to write to file. To do that, we again, combine Cocoa and traditional AppleScript to build a tab-delimited line with a trailing return:
set theTempString to (theTime's stringValue() as text) & " " & (currentSSID's stringValue() as text) & " " & (authMode's stringValue() as text) & " " & (currentChannel's stringValue() as text) & " " & (currentDataRate's stringValue() as text) & " " & (signalStrength's stringValue() as text) & " " & (signalNoise's stringValue() as text) & " " & (currentWAPMAC's stringValue() as text) & "
"
The blank spaces in between the quotes are the presentation of the tab formatter, \t. The odd quotation mark by itself is the presentation of the return formatter, \r.
So now we spend three statements on converting theTempString to an NSData object and writing that to a file. First, we create the NSString object: set theFileString to my NSString's stringWithString_(theTempString). Pretty clear, we use the stringWithString function, passing it the temp string. Next, we encode theFileString and convert it to an NSData Object:
set theFileData to theFileString's dataUsingEncoding_(NSUTF8StringEncoding of current application)
We're using UTF8 encoding, because that's the better way to do things, but there are a variety of encodings available, including MacRoman, etc. One thing to watch here is that you have to set the encoding as " of current application", or it fails miserably. Finally, we write that NSData object to the file we created via the file handler:
theFileHandle's writeData_(theFileData)
Every time this handler is called, as long as createFileData and theSaveFileFlag are both true, we get a line of data written.
So what happens when we're done writing? Well, it wouldn't make a lot of sense to put that code here, so we put it in the functions that we use when we're done tracking data, or when we deselect the save file checkbox. First, when we stop tracking:
on timerFired_(thetimer) --this handler runs the actual code for the timer
if trackButtonSTate is 1 then --'on'
loadData(theCurrentInterface) of me --grab stats once per second
else if trackButtonSTate is 0 then --"off"
thetimer's invalidate() --kill the timer
theFileHandle's closeFile() --close the file handle we've been writing to
set theSaveFileFlag to false --kill the save file flag
set createDataFile to false --kill the new file flag
set appendToExisting to false --kill the append file flag
saveToFileCheckBox's setIntValue_(0) --disable the checkbox
my createNewDataButton's setEnabled_(false) --disable the button to create a new data file
my appendToExistingDataButton's setEnabled_(false) --disable the button to append to an existing data file
end if
end timerFired_
We put the file handle cleanup code in the same block as the timer cleanup code. When you disable tracking, it's going to run the code to kill the timer that's controlling how fast the tracking is happening anyway, so it makes sense to put it here. It's only one line to shut down the file handle:
theFileHandle's closeFile()
That's it, the file handle is closed, file writing is done. The rest is just cleaning up properties and resetting controls to give a better indication to the user that they're no longer recording data to a file.
The same basic thing happens if they disable the save to file checkbox:
on saveToFile_(sender)
--when you click the "Save to file" checkbox this ONLY controls the button states, not what the buttons do
if sender's intValue() is 1 then --if you're checking the checkbox
set theSaveFileFlag to true
my createNewDataButton's setEnabled_(true) --enable the button to create a new data file
--the "my" is critical to having the now enabled button be able to send events
--without "my", the button knows it's enabled, nothing else does
my appendToExistingDataButton's setEnabled_(true) --enable the button to append to an existing data file
else if sender's intValue() is 0 then --if you're de-checking the checkbox
theFileHandle's closeFile() --close the file handle we've been writing to
set theSaveFileFlag to false --kill this file flag
set createDataFile to false --kill the new file flag
set appendToExisting to false --kill the append file flag
my createNewDataButton's setEnabled_(false) --disable the button to create a new data file
my appendToExistingDataButton's setEnabled_(false) --disable the button to append to an existing data file
end if
end saveToFile_
Nothing new here, close the file handle, reset things, and bob's your uncle.
Once you wrap your head around things, especially the encoding method thing, writing data to a file is pretty easy. Really, that was the biggest frustration for me, because even reading the Cocoa docs on this, there was nothing to really indicate that had to happen. Again, many thanks to Shane, Craig, and everyone else on the AppleScriptObjC list for all their help.