X.10 serial port control from Rebol
[1/2] from: parki::whatevernot::com at: 6-Sep-2003 18:11
My first 'real' Rebol script - this is a certainly unfinished script
which controls my X.10 lights in my house - with simple commands I can
turn on and off lights on the X.10 circuit in my house. I have looked
at this in C (working with a developer to port to Mac OS X) as well as
Perl and Java, and can certainly say that the Rebol approach, which
built in serial:// protocol, makes life a snap.
This also contains some playing around with the notions of Singleton
idiom and data hiding. It's likely overkill.
I am posting it here as people might be interested, and I welcome any
feedback on style and whatevernot.
Cheers,
parki...
--- x8 snip
REBOL [
Title: "CM11A X.10 serial controller"
Author: "Brian Parkinson"
Email: [parki--whatevernot--com]
Date: 6-Sep-2003
File: %cm11a.r
Version: 0.1.0
Purpose: "Support simple access to an X.10 controller to turn
lights on and off."
History: [
0.1.0 [6-Sep-2003 "First cut."]
]
Comment: {
Example usage:
x: cm11a ; Initialize the interface to the CM11A
controller
x/ex 3 'on ; Turn on light 3 in default house code #"A"
x/houseCode #"B" ; Set the house code to #"B"
x/ex 4 'off ; Turn off light 4 in house code #"B"
x/deinit ; Deinitialize the controller when all are done
with it
Should be mentioned that at of this version (0.1.0) I have only
really tested that
lights on and off work - more features can be added. This is based
around a singleton
idiom that is somewhat nifty - data hiding is implemented.
To make this work on a different machine, you will almost
certainly have to change the
set to system/ports/serial in the cm11aInit function.
}
]
; The use construct allows us to hide a number of variable and
functions and in so
; doing provide some nice data hiding
;
use [ privateCtor instance_ cm11aInit cm11aSend serialPort_ HOUSE_CODES
DEVICE_CODES FUNCTION_CODES ]
[
; The singleton instance which ensures that we only create one object
;
instance_: false
; Define our constants based on the CM11A protocol
;
HOUSE_CODES: [ 96 224 32 160 16 144 80 208 112 240 48 176 0 128 64 192
]
DEVICE_CODES: [ 6 14 2 10 1 9 5 13 7 15 3 11 0 8 4 12 ]
FUNCTION_CODES: [ all-units-off all-lights-on on off dim
bright all-lights-off extended hail-request hail-acknowledge
preset-dim1 preset-dim2 extended-data
status-on status-off status-request
]
; The function which returns the global singleton instance of the
CM11A controller
;
cm11a: func [] [
either instance_ [
instance_
] [
instance_: privateCtor
]
]
; Private func inits serial port connection to the CM11A controller -
called from
; the private constructor so we only init the connection when the
cm11a object
; is constructed, and not at script load time
;
cm11aInit: func [] [
; /dev/cu.USA19QI14P1.1 is the serial device for X.10 on my machine
; See /dev on the machine for the name of this port
;
system/ports/serial: [ cu.USA19QI14P1.1 ]
; The serial port contained by the singleton object
; Open the connection to the serial port
serialPort_: open/binary/direct/no-wait serial://port1/4800/8/none/1
; Turn off handshaking (required by CM11A controller)
serialPort_/rts-cts: false
update serialPort_
]
; Private function to send a value to the CM11A controller
; Need to add in exception handling if fall through loop
;
cm11aSend: func [ value /local reply ] [
insert serialPort_ value
repeat count 100 [
wait 0.1
reply: copy serialPort_
if not reply = #{} [ return 0 ]
]
-1
]
; Private constructor which only gets called once upon first access
;
privateCtor: func [] [
make object! [
; The house code that this object will control - defaults to A but
; can be set with the houseCode function
;
houseCode_: #"A"
; Initialize the CM11A controller
;
cm11aInit
; When you are done with the serial port, you should call deinit to
close
; the connection
;
deinit: func [] [
close serialPort_
]
; Set the current house code used by the controller
;
houseCode: func [
houseCode [ char! ] "The house code character: A,...,P"
] [
houseCode_: houseCode
]
; The main entry point - ensure that the controller has been init'ed
; Example use: "ex 3 'on"
;
ex: function [
deviceCode [ number! ] "Device code number: 1,..,16"
funcWord [word!] "The function to run: 'on, 'off, etc"
] [
houseVal deviceVal functionVal addrHex funcHex
] [
houseVal: pick HOUSE_CODES (to-integer houseCode_) - (to-integer
#"@")
deviceVal: pick DEVICE_CODES deviceCode
functionVal: (index? find FUNCTION_CODES funcWord) - 1
addrHex: debase/base (to-string copy/part at to-hex (1024 +
houseVal + deviceVal) 5 4) 16
funcHex: debase/base (to-string copy/part at to-hex (1536 +
houseVal + functionVal) 5 4) 16
; Make the calls to the serial port
;
cm11aSend addrHex
cm11aSend #{00}
cm11aSend funcHex
cm11aSend #{00}
]
]
]
]
[2/2] from: greggirwin:mindspring at: 7-Sep-2003 11:42
Hi Parki,
Thanks for posting that. Good stuff! Makes me want to go out and find
an X10 controller so I can play with things again. :)
BP> I have looked
BP> at this in C (working with a developer to port to Mac OS X) as well as
BP> Perl and Java, and can certainly say that the Rebol approach, which
BP> built in serial:// protocol, makes life a snap.
Good to know. We all have different experience, so I always like
hearing where REBOL works well and where it doesn't.
BP> This also contains some playing around with the notions of Singleton
BP> idiom and data hiding. It's likely overkill.
BP> I am posting it here as people might be interested, and I welcome any
BP> feedback on style and whatevernot.
It could be overkill. One of the things about REBOL, or the approach
*I* tend to use with REBOL, as compared to some other languages is
that I don't try to exert too much control over how modules may be
used. That is, REBOL is very flexible, so if I build a module that is
locked down really tight in how it can be used, that makes more work
for me, and more work for the person who wants to use it differently.
It's sort of an accountability thing, and it's a fine line. On one
hand, I don't want to write things so knowledge of internals is
required for people to use it (some VID styles suffer from this IMO),
but on the other hand, in REBOL people can find a way around almost
any blockades you try to put up to protect them.
In this case, I'd weigh the pros and cons of the singleton stuff
versus just using an object. What happens if they create more than
one? Would that ever be useful? Stuff like that.
Now, I can make some comments on your code (already very good) which
are related more to REBOL style and idioms than correct code.
First, your posting line-wrapped heavily here. REBOL is free form, so
you can often adjust the formatting to suit shorter lines if you want.
Second, great comments! Don't forget the COMMENT function for long
comments that may wrap. Also, glad to see you used doc attributes on
your ex function parameters!
Where you use:
repeat count 100 [
You're not using 'count, so you could use LOOP instead.
loop 100 [
Where you use:
if not reply = #{} [ return 0 ]
Consider:
if not empty? reply [ return 0 ]
EMPTY? is often clearer, as to the intent of what you're checking,
than comparing equality against specific values.
Just as FUNC is a shortcut for FUNCTION, DOES and HAS are shortcuts
for functions with no params or those with only local var decls,
respectively. Don't be afraid of one-liner functions.
deinit: func [] [
close serialPort_
]
Could be:
deinit: does [close serialPort_]
Standard REBOL style doesn't use CamelCase for words. It uses all
lowercase, with hyphens, and hyphens are used in place of underscores
in most cases. Also, all-caps aren't used for constants, maybe
because, well, there really *aren't* any constants in REBOL. :)
ex: function [
deviceCode [ number! ] "Device code number: 1,..,16"
funcWord [word!] "The function to run: 'on, 'off, etc"
] [
houseVal deviceVal functionVal addrHex funcHex
...
Vs:
ex: function [
device-code [ number! ] "Device code number: 1,..,16"
func-word [word!] "The function to run: 'on, 'off, etc"
] [
house-val device-val function-val addr-hex func-hex
...
You've made good use of data, but consider how else you might express
your data, and how that would affect the code. e.g.
house-codes: [
#"A" 96 #"B" 224 #"C" 32 #"D" 160 #"E" 16 #"F" 144 #"G" 80 #"H" 208
#"I" 112 #"J" 240 #"K" 48 #"L" 176 #"M" 0 #"N" 128 #"O" 64 #"P" 192
]
...
house-code_: #"A"
...
house-val: house-codes/:house-code_
You could even validate house codes
valid-house-codes: charset [#"A" - #"P"]
; or, with the above house-codes definition
;valid-house-codes: charset extract house-codes 2
valid-house-code?: func [house-code [char!]] [
found? find valid-house-codes house-code
]
You could also use a function for your data conversion lines on the
address and function values. You could even define it locally in EX.
I'm sure you would have done this in your next cut anyway as it's not
really a REBOL specific thing. I do this, sometimes, so I can take
the function and play with it out of context--often in the console.
X10ify: func [val] [
debase/base (to-string copy/part at to-hex val 5 4) 16
;?? will this work here instead?
;at debase/base to-hex val 16 3
]
...
addr-hex: X10ify 1024 + house-val + device-val
func-hex: X10ify 1536 + house-val + function-val
Another handy idiom is to write your function, or a wrapper for it, to
allow multiple values to be iterated over, rather than calling the
function manually with one argument, or arg list, multiple times.
cm11aSend addrHex
cm11aSend #{00}
cm11aSend funcHex
cm11aSend #{00}
Versus:
send-cmds: func [cmds [block!] [
foreach cmd cmds [cm11aSend cmd]
]
send-cmds reduce [addrHex #{00} funcHex #{00}]
If the #{00} command has a special meaning, or is used after every
command, you could give it a special name or just send it silently in
'cm11aSend, maybe based on a refinement. SEND-CMDS could even treat
the block as a simple dialect so you could include WAIT commands and
such.
It's good stuff in any case! I hope my comments give you some thoughts
and ideas. There's certainly nothing wrong with your code as it stands
now.
-- Gregg