bynkii (bynkii) wrote,
bynkii
bynkii

The Write Way...

(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 "<encoding method> 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.
Subscribe
  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 0 comments